游戏引擎学习第254天:重新启用性能分析

打印 上一主题 下一主题

主题 1611|帖子 1611|积分 4833

运行游戏并尝试让性能分析系统规复部分功能

我们现在的调试系统这几天基本整理得差不多了,因此我们打算开始清理一些功能,比如目前固然已经在收集性能分析数据,但我们没有办法检察或有效使用这些信息。本日的计划可能会围绕这方面展开:要么开始真正构建性能分析器,要么尝试一些更现实的整合工作,看看它将怎样在现实使用中运行。
我们也可能会处理一下绘制界面方面的问题,由于当前绘制逻辑还比力凌乱,结构也比力杂乱,显示效果还不理想,像边框和控件都没画好。
现在我们运行游戏,能看到根本的窗口系统已经可以工作,也可以调解窗口巨细。但是我们目前并没有展示任何性能分析数据,而这是我们接下来的重点。
我们希望展示两类性能分析信息:一是内存占用情况,二是耗时分析。此中耗时是最重要的,我们希望能够在调试系统中显示性能分析信息。
我们想要的性能信息主要分为两种:

  • 高层次的性能概览:这是一个宏观视角,用来展示每帧大抵耗时,包括渲染、模仿、输入处理等各个部分所占用的时间比例。这样可以资助我们确保每部分运行时间大抵正常,不会出现某一部分突然耗时激增的情况。
  • 详细的代码性能分析:这是用于代码优化的详细信息。比如当我们发现某段代码运行缓慢时,需要能插入某种标志或调用,以便详细检察那段代码的性能表现。这种分析是为了资助我们理解并优化特定代码段。
通常这两种信息不会同时需要。我们希望能长期在屏幕上保存一份高层次的性能概览,而当需要优化详细代码时再调用更详细的分析工具。
所以现在我们要做的,大概就是继承构建一个“概览型性能分析器”,雷同于传统性能分析器那种列出函数列表、显示函数自身耗时及其子函数耗时、调用次数等信息的方式。这种结构化的展示方式可以让我们快速看出哪部分代码占用时间最多,是一个合理的出发点。
此外,传统分析器通常只支持单线程分析,而我们现在的步伐是多线程的,因此可能也要考虑用条形图的方式展示不同线程中的事件重叠情况,以便更直观地看到多线程任务的时间关系。
同时我们也留意到当前的 UI 交互还有很多粗糙的地方,比如窗口可以被拖动,但交互手感不好,内容很难看清,缺乏背景遮挡或分隔,使得一些文字在浅色背景下根本看不清。因此也需要加些界面上的遮罩或暗化处理,让调试界面更容易使用。
总的来说,我们现在要解决的是两个问题:一是性能分析系统的构建与可视化,二是界面交互与可读性的提拔。从当前情况来看,性能分析是最急迫的问题,所以我们会优先处理这一块,接着再清理其他部分。我们需要回顾之前已经写好的部分,决定接下来要怎样把这套分析系统做好。


game_debug.cpp:重新认识 CollateDebugRecords 函数

当前的调试系统中,我们在 CollateDebugRecords 的代码部分可以看到性能分析的残留结构。在那里,我们记录了每一帧的标志(Frame Marker),并且记录了与这些标志关联的 wall-clock 时间(现实经过的时间),这为后续的性能分析提供了根本数据。
我们还保存了 BEGIN_BLOCK 和 END_BLOCK 这样的事件,它们之间可以配对,以确定代码块的执行时间。系统会查找这些配对的开始与结束事件,并将其构造成时间段(Span)用于分析。不外现在的处理方式还有些问题:


  • 目前并没有很好地处理那些跨帧的 block(即在一帧开始,在另一帧结束的代码块),这些情况被忽略了,将来需要加以修复。
  • 对于匹配的 block,我们现在只记录了部分特定名称的 block,这是暂时做法,不够通用。
在整理这些 debug 记录时,每帧结束都会遍历一次所有事件,然后尝试将它们整理成我们需要的格式。这个整理过程让我们思考是否有更好的方式来构建性能分析结构。
目前对事件的处理方式比力繁琐,尤其是对 BEGIN_BLOCK 和 END_BLOCK 的特殊处理。我们考虑是否可以换个思绪,把这些 block 也当作一种“事件”来处理,就像对待平凡的数据块一样,而不是单独去匹配成一个 span 区间。也就是说,可以把 begin 和 end block 处理成同一结构,每次记录一个事件,而不是在整理阶段再去做匹配和转换。
这种做法看起来更简朴,更同一。每个 block 的记录都会变成一个事件,并关联到某个调试元素(debug element),不再需要在事后分析中单独处理。只要在记录的时间,能明白知道这个事件属于哪个元素,就可以建立起完整的事件流。
这种结构的好处在于:


  • 所有事件都可以走同一套系统,低落逻辑复杂度;
  • 不需要在事后再花大量逻辑去匹配 BEGIN_BLOCK 和 END_BLOCK;
  • 可以更机动地记录事件,不再依赖于是否恰好有匹配的 begin 和 end。
固然,这种方式会加快调试数据的占用,我们会更快地耗尽调试数据的存储空间。但调试阶段并不在意内存斲丧,我们完全可以为调试系统分配更大的存储空间,比如 16GB 的内存,足够记录几千帧的详细信息。如果只需要回看几帧,也不会有任何问题。
因此,更倾向于采用这种事件式的同一处理方式,让所有事件都走一样的路径进行存储和回放,把调试系统变得更通用和高效。我们将对 BEGIN_BLOCK 和 END_BLOCK 进行重构,同一到一个记录事件的系统中。

我们目前认为,把 BEGIN_BLOCK 和 END_BLOCK 同一为通用事件来处理是一个不错的想法。固然还不知道现实实施时是否会暴袒露问题,但我们决定先按这个思绪尝试一下,后续如果发现问题再做调解。
接下来准备开始清理 StoreEvent 部分的代码,着手进行调解。不外在这个过程中突然遇到一个奇怪的情况——无法切换到编辑模式,表现为按键操作不正常。
详细来说,发现 Shift 键像是被锁定了一样,导致输入状态异常。这种情况以前从未遇到过,无论是在这台机器上还是其他机器上。开端猜疑可能是键盘的问题。
当前使用的是较老的 Das Keyboard 3 型号,以前也曾在另一块 Das Keyboard 上遇到过雷同“按键卡住”的问题。而其他地方都已经更换成 Razer 键盘,只是还没给这台开发机配一把新的。考虑到以往的经历,这次的问题可能也是这块旧键盘自己存在物理故障大概是接触问题。
固然这是个小插曲,但还是提示了我们旧设备可能带来的问题,后续有必要更换这块键盘以避免干扰正常开发流程。
game_debug.cpp:简化 CollateDebugRecords 并修改转达给 StoreEvent 的内容

我们决定将 BEGIN_BLOCK 和 END_BLOCK 事件直接存储到对应的元素中,不再进行之前复杂的匹配和处理。每当遇到一个 BEGIN_BLOCK 或 END_BLOCK,就把这个事件存储到其对应的 debug 元素中。由于这些事件在创建元素时就关联在一起,因此可以简化处理逻辑,不再关心 frame index 是否匹配,只要配对关系成立,就进行存储。
这样,每个元素中就会有一个以“进入-退出-进入-退出”的模式组织的事件链表,代表了该代码块在运行过程中的每一次进入与退出。这个结构能够完整记录每次调用的机遇和持续时间。
不外,这种方式固然能记录“某段代码被调用了多少次,每次持续了多久”,但仍存在一个问题:我们并不知道这些调用是被谁触发的。换句话说,缺少了调用关系的信息。要知道调用关系,我们只能依赖事件的时间顺序进行推断,比如通过分析时间戳判断哪个事件嵌套在另一个事件内部,但这种方式不够直接,也容易堕落。
为了解决这个问题,我们考虑在存储事件的时间,加入一个额外字段,用来记录“当前事件的父级元素”,即这个代码块是被哪个上层代码块调用的。这样就能在分析调用关系时更加明白,能够清晰地追踪每个调用链上的父子层级。
简而言之,我们的思绪是:

  • 简化事件处理流程,遇到 BEGIN_BLOCK 和 END_BLOCK 就立刻记录,不再依赖复杂匹配。
  • 每个元素维护一组“进入-退出”的事件序列,完整还原每次执行时长。
  • 为每个事件记录其“父级调用者”,明白调用关系,方便之后构建调用树或进行性能分析。
  • 删除原有多余的逻辑和暂时代码,进一步简化结构。
这样处理之后,性能分析数据将更加结构化、可视化效果也更容易做出明白的父子层级展示,同时提拔了系统的机动性和可维护性。

黑板:调用归因(Call Attribution)

我们设计了一个用于调试的结构体(debug element),这个结构体记录了每一次函数调用的信息。我们能够把握的信息包括:


  • 每个函数被调用的次数;
  • 每次调用持续的时间;
  • 每次调用发生在哪一帧(frame)上。
这些数据都存储在一个雷同链表的结构中,我们已经具备了相应的采样记录本领。
不外,目前仍有一个重要的信息缺失:调用者是谁。例如,函数 foo 可能既被函数 bar 调用,也可能被函数 boz 调用。如果我们在调试过程中发现 foo 被调用了多次,并且我们已经记录了调用的时间和帧位置,但我们无法区分哪一次调用是来自 bar,哪一次是来自 boz。这对于后续分析调用关系、定位性能瓶颈会造成未便。
为了能够得到更精细的调用上下文分析结果,比如“函数 bar 导致了 foo 耗费了多少时间”、“函数 boz 又占用了多少”,我们希望在调试信息中增加一个额外的指针或引用,用来指向调用该函数的上层函数对应的 debug element。这样我们就能从 foo 的调用记录中追溯到详细的调用泉源。
因此,建议在事件存储结构中,加入一个额外的指针字段,用来标志“谁”调用了这个函数调用。这种方式在现实使用中会提供更高的机动性和更丰富的分析本领,比如可以选择性地启用或禁用这部分引用信息,视详细分析需求而定。
这类优化将在调试和性能分析工具(如 game profiling 系统)中提供更高质量的追踪数据。我们可以进一步检察事件存储结构的界说(如 game 的 StoredEvent)来确认该指针的可添加位置。
game_debug.h:记下将调用归因数据存储到 debug_stored_event 的计划

在当前的事件记录机制中,我们生存了事件的帧索引和事件的类型。但如果能额外存储一个信息,例如事件的“父级调用者”,会对后续分析非常有资助。
固然我们暂时不需要立刻添加这个信息,但我们意识到这是一个重要的点,由于我们只在当前的相关性分析过程中才气知道每个事件的调用者是谁,也就是说,只有在执行事件匹配遍历(correlation pass)的时间,才气知道一个事件是被谁调用的。
这种信息之所以只能在特定阶段得到,是由于我们是通过遍历事件数组并按照顺序配对“打开”和“关闭”事件,推断出哪个事件是谁的父级。在这个阶段我们把握了所有调用关系。但这个数组在分析完成之后就会被开释,也就是说,绘制图形或回放调试信息的时间,我们再也无法得到这些调用链信息。由于原始事件流不再保存,我们也没有记录每个事件的父级信息。
因此,如果我们希望后期还原调用关系,就需要在事件存储结构中预先记录好父级指针,否则信息就会丢失。
也考虑过一种替代方案:直接写入一个持续增长的事件流,不做任何中间结构生存,而是通过向后查找的方式追踪调用者。但这种方式可能服从较低,尤其是在处理大要量事件时会产生很高的性能开销。因此,引入中间结构并定期进行处理,是为了在性能和可用性之间取得平衡。
总体来说,当前的设计思绪是:仅处理当前帧的新事件,避免每次绘制或分析都去遍历整个事件数组。这样既节省计算资源,又可以保存需要的重要数据。最终目标是淘汰处理开销,同时保存必要的调试信息。我们所做的权衡,是在保存调试所需信息的条件下,只管淘汰无效数据扫描和处理的成本。
game_debug.cpp:实现调用归因

我们开始重新编译代码,并准备进入分析和实现性能分析(profiling)相关功能的阶段。接下来的目标是让绘制调试菜单中的性能分析部分(profile UI)正常工作。
在完成事件归并(collation)之后,绘制完成各个调试元素(debug elements)之后,我们会进入主调试菜单的绘制过程。在这一阶段,我们会调用 DebugDrawMainMenu 相关的函数,并在内部进一步处理性能分析的绘制工作。关键点在于:我们开始进入现实将性能分析信息可视化的流程。
调试系统中包含多种类型的元素,此中之一是 CounterThreadList,这是我们想要首先处理的部分。我们开始分析它的作用和使用方式。
接下来检察了与 CounterThreadList 相关的结构界说和调用位置。我们留意到在某些位置它被用于打印信息,但目前我们还不完全理解这些结构的详细意义。因此,暂时将相关部分注释掉,以便聚焦于理解其现实作用和分析在哪些位置被使用。
当前已知的是:


  • CounterThreadList 和 CounterFunctionList 都是性能分析中用于统计信息的数据结构;
  • 它们可能被用于绘制与线程或函数调用相关的性能图表或统计视图;
  • 在现实绘制逻辑中,它们作为调试元素的一部分存在,在调试菜单中被绘制出来;
  • 我们将逐步理清它们的结构、用途以及怎样通过它们来实现可视化功能。
目前的计谋是:从分析调用路径和绘制流程开始,逐步实现或修复 CounterThreadList 等数据的可视化输出,为后续的性能分析提供可靠的展示工具。通过理解其用途和代码流转方式,为调试系统构建更完善的分析界面奠定根本。

game_debug.cpp:将 DEBUGDrawElement 和 DEBUGDrawEvent 合并为一个函数

目前代码中存在两个看起来雷同的函数:DebugDrawElement 和 DebugDrawEvent。回顾后发现,它们现实上没有明显区别,其存在只是由于早期对怎样分别状态存储方式还存在疑问而做出的结构安排。
随着代码演进,这种分离已不再必要,因此决定将两个函数合并成一个同一的函数,即只保存 DebugDrawElement。原来的 DebugDrawEvent 中的逻辑和代码已经与 DebugDrawElement 几乎完全一致,没有保存的意义。
接下来,移除了多余的函数界说,并将可能遗留的逻辑暂时整合到当前保存的函数中,以防后续还需要参考。
在新保存的 DebugDrawElement 函数中,现实上只依赖一个内容:当前正在绘制的 StoredEvent(也就是存储的调试事件)。原来还转达了一个 view 参数用于视图计算,但从逻辑看出,这个参数并未真正使用,因此也一并省略,简化了函数签名和使用方式。
最后,调解函数中处理 element 的调用方式,使逻辑更加直观一致。将原来分散的事件绘制逻辑提取、合并,保存焦点操作,并确认调用的是近来一个事件的数据作为绘制目标。
总体调解结果如下:


  • 合并 DebugDrawElement 和 DebugDrawEvent,只保存前者;
  • 移除冗余代码和无效参数(如视图);
  • 明白绘制依赖的数据结构为近来的 StoredEvent;
  • 同一命名和调用方式,淘汰重复,简化团体结构。
这次整理让绘制调试元素的流程更加清晰,也为后续功能拓展或维护低落了复杂度。
黑板:草拟概览性能分析器

当前的任务是规划我们希望构建的性能分析(profile)视图类型。我们决定从最基本的一种开始——概览视图(overview),这也是之前已经实现过的一种情势。
这类概览视图的目标是以线程为单位,展示在一段时间内各线程的运行活动。可视化方式是以“线程”作为纵向的分类(垂直方向每一行代表一个线程),而以“时间”作为横向的刻度(从左到右表现时间流逝)。在每个线程的时间线上,绘制不同函数的执行区间,通过方块或条形的方式展示。
详细情势如下:


  • 纵向表现不同线程(lanes)
  • 横向表现时间流逝(clock ticks)
  • 在每个线程时间线上,绘制函数调用的执行区间,显示函数名
  • 基于事件中的 open/close 块数据来确定函数的执行时间段,并以此绘制可视化块
目前我们在数据结构上还没有一个完全的“层次结构”(hierarchy)来呈现调用关系,这使得要构建一个明白的调用图(flow graph)仍有一定难度。因此,在开始绘制前,我们意识到也允许以在可视化过程中暂时构建某种情势的调用结构或数据索引,以便更有效地结构和展示各个函数调用。
此外,随着现实实现推进,我们也会逐步明白更多细节,包括:


  • 怎样更清晰地区分线程和函数的显示地区;
  • 是否需要对函数调用深度做视觉上的嵌套处理;
  • 怎样提拔渲染性能,避免处理巨大的事件数组时出现服从问题。
这套概览视图的实现将作为性能分析系统的根本,为之后更复杂的视图(比如调用图、按函数分类统计等)提供出发点。我们计划从这个最直接、最易理解的展示方式入手,一步步建立起完整的性能可视化工详细系。
game_debug_interface.h:开始实现 ThreadIntervalGraph

当前的目标是为性能分析系统实现一种新的可视化类型,我们将其命名为 线程区间图(Thread Interval Graph),它专注于展示各线程在一段时间内执行函数的区间分布。
我们界说这种视图类型为一种“更深层”的视图,用于更细致地检察线程中的函数执行情况。设计的焦点思绪如下:

线程区间图的基本理念



  • 每个线程有一条独立的时间线;
  • 在线上绘制函数调用区间(使用开始时间和结束时间);
  • 显示函数名称;
  • 横轴为时间刻度;
  • 纵轴为线程分布;
  • 支持筛选,仅显示特定函数的调用区间(如果传入特定函数名);
  • 如果传入空字符串,则显示所有函数。

实现规划

我们在现有的 debug_profile 模块中加入这个新的视图类型:


  • 界说新的视图类型名为 DebugView_ThreadIntervalGraph;
  • 注册该类型,使其能够在调试界面中被调用;
  • 提供函数名参数:作为过滤器,决定是否只显示某一个函数;
  • 在调试菜单中,默认只显示 game_update 和 render 等游戏逻辑代码,而非平台层逻辑;
  • 将这个视图作为一个根本结构添加进调试系统,使得用户可以选择它并检察每帧中不同线程的函数执行情况;
  • 可以考虑在视图中显示每个执行块的句柄信息或 open block 数据,便于追踪泉源。

关于部分旧代码的处理

之前存在一些与函数列表、计数器等有关的调试逻辑,但这些代码目前不符合新的需求,也不再适用,因此可以忽略或清理。同时,对于这部分线程区间图的焦点绘制逻辑,目前还未开始正式实现,但已经完成了结构的开端搭建,为后续图形绘制和逻辑遍历做好了准备。

当前阶段目标总结



  • 明白了我们要实现一种新的可视化图;
  • 将其纳入已有的调试系统框架中;
  • 提供了参数支持(函数名过滤);
  • 定位其适用范围(主要关注游戏逻辑线程);
  • 清理了无关代码,为后续实现留出干净入口。
接下来的工作将聚焦于怎样使用记录的函数调用事件,绘制出清晰的、可交互的线程区间图形界面。



(void) 是消除一些clangd 的警告


运行游戏并看到 END_BLOCK 被打印出来β


段错误吗?

当前我们已经移除了冗余的函数,使代码结构变得更清晰,这是一个积极的希望。接下来我们观察到了调试系统中一些异常举动,并对其进行了开端分析。

当前调试举动及异常现象



  • 我们留意到调试信息中正在打印 endblock 数据块,这引起了我们的留意;
  • 开端判断这可能是由于我们在调用 store_event 的时间自动触发的举动;
  • 然而目前这块数据的打印看起来十分奇怪,有些抖动或跳变现象;
  • 在调试停息状态下,系统本不应该更新,但依然发生了革新举动,这非常异常,推测可能是某处逻辑未精确检测停息状态;
  • 这一举动非常可疑,需要后续进一步观察其根本缘故原由。

内存地区耗尽测试



  • 此外,我们故意识地让内存分配地区(arena)耗尽以观察系统的反应;
  • 系统提示大约还有 30 秒内存空间,随后逐步淘汰至 5MB、4MB……最终耗尽;
  • 在耗尽时我们特殊留意系统是否能正常处理该情况;
  • 最终在内存耗尽时出现了某些“奇怪的数据”或举动(可能是图像/调试数据显示异常),这同样是值得关注的问题;
  • 除此之外,系统大要上在其他方面保持了正常运行。

总结与下一步方向



  • 成功移除了不必要的函数,提高了团体代码可读性;
  • 发现 store_event 可能引发了不希望发生的数据输出举动;
  • 留意到调试系统在停息状态下依然有“跳变”或“闪动”,违反预期;
  • 内存地区耗尽时,系统虽未瓦解,但出现了可疑的数据图像,应进一步观察内存管理与调试渲染逻辑;
  • 下一步需要会合精神定位调试系统在停息状态下仍更新的问题,并进一步分析 arena 耗尽后的容错机制是否结实。
这些观察结果为调试系统的结实性提供了宝贵信息,有助于后续优化和稳固性提拔。

game_debug.cpp:注释掉 StoreEvent 的调用,以确认 32MiB 的内存不足

我们对当前的调试系统进行了一些内存相关的测试,并得出了几个关键结论和后续改进方向:

内存占用测试及现象分析



  • 使用 begin_block 和 end_block 进行测试,初始情况下不存储事件时系统运行良好;
  • 只存储此中一个事件时,内存占用减半,依然表现正常;
  • 同时存储两个事件后内存开始迅速耗尽;
  • 初始设置的 32MB 调试存储空间完全不足以容纳当前记录的事件数目,内存被快速“吃掉”;
  • 当只存储一个事件时运行正常,说明系统逻辑自己没有问题,只是事件数目太多;
  • 暂时将调试存储空间设置为一个更大的值后,系统能够稳固记录完整的一帧数据;
  • 说明只要内存富足,系统的事件记录机制是有效的;

对事件量的开端判断与优化方向



  • 当前调试系统在每帧记录的事件数目过多,意味着某些地方设置了过多的地区(zone)或嵌套;
  • 只管增大内存可以缓解问题,但并不能从根本上解决事件过多的问题;
  • 可能需要对记录计谋进行优化,例如合并重复的事件、限制最大嵌套层级、仅记录关键事件等;
  • 系统固然能记录下完整帧,但每次革新内存的速度极快,依旧不够理想,后续需做进一步内存优化设计;

调试层级显示的修正需求



  • 当前的调试层级(hierarchy)显示中出现了一些不应当出现在此中的元素;
  • 推测是某些调试元素(debug elements)被错误地加入到了层级结构中;
  • 为了更好地组织视图,需要将这类不应出现在主层级中的元素清除出去;
  • 后续需要调解处理逻辑,区分哪些是用于显示层级的焦点事件,哪些是辅助性事件或不应显示的内部记录项;
  • 这将改善调试视图的清晰度和结构逻辑,使调试工具更易于阅读和分析。

总结



  • 当前调试系统基本运行稳固,但内存使用率极高;
  • 暂时扩展内存缓解问题,但需从源头优化事件记录计谋;
  • 调试显示结构存在错误归类问题,需修正层级渲染逻辑;
  • 团体机制可用,后续工作重心在于压缩事件数据、提拔可读性、控制内存斲丧。



奇怪



为什么只显示这一行








game_debug.cpp:修改 GetElementFromEvent,加入参数 b32 CreateHierarchy,以便根据条件调用 GetGroupForHierarchicalName

我们当前遇到的问题是在处理调试事件时,有一类特殊的元素(Hierarchy类型的 debug element)不应当到场正常的层级结构分组处理。它们应被同一归入一个固定的分组中,而不是通过一般的层级规则进行归类。为了解决这个问题,我们需要对事件解析和分组逻辑进行调解。

当前问题形貌



  • 有一批特定的调试事件不应到场层级结构的归类;
  • 这类事件应始终被归入一个固定的父组,而不是根据名称层级进行分组;
  • 当前 get_element_from_event 函数会默认通过 get_group_for_hierarchical_name 来查找或创建层级分组;
  • 这就导致了我们不希望出现的自动归类举动;

解决思绪与设计调解

我们可以在 get_element_from_event 函数中引入一个额外的参数或机制来控制是否跳过层级结构的查找过程:
新增标志控制举动



  • 引入一个 add_directly_to_parent 的布尔标志,用于控制是否直接将该事件归入指定父组;
  • 当该标志为 true 时,跳过 get_group_for_hierarchical_name 的调用,直接使用提供的 parent_group;
  • 否则,执行原有的层级结构归类逻辑;
函数内部逻辑变动示例

  1. if (add_directly_to_parent) {
  2.     // 直接使用传入的 parent,不进行层级归类
  3.     assign_to_group(parent_group);
  4. } else {
  5.     // 按照名称层级查找或创建分组
  6.     group = get_group_for_hierarchical_name(name);
  7.     assign_to_group(group);
  8. }
复制代码

来由与好处



  • 避免了非层级元素被错误地纳入分组结构;
  • 允许更机动地控制调试元素的归类举动;
  • 保存现有层级分组逻辑的完整性;
  • 简化了这些特殊元素的调试显示逻辑;

将来可优化方向



  • 可以进一步将事件分为平凡层级事件与特殊事件两个通路处理;
  • 根据事件类型自动决定是否走“扁平化分组”逻辑;
  • 让调试系统自己具有更清晰的分类本领,以淘汰每次手动判断与传参的负担;

小结

我们现在通过引入一个控制标志 add_directly_to_parent,成功区分了哪些事件需要到场层级结构,哪些事件应同一归为某一组。这样一来,调试显示逻辑就更加清晰且可控,避免不必要的杂乱结构,同时为后续调试系统的维护和优化打下了良好的根本。

game_debug.cpp:引入 ProfileGroup 的概念

我们现在的目标是让某些调试事件(例如 Hierarchy类型的调试元素)不被加入层级结构中,而是直接存入一个专门的“分析块容器”中,以便更好地管理和展示这些不需要到场常规层级结构的调试数据。

当前处理逻辑概述


  • 在调用 get_element_from_event 函数时,我们决定通过传入参数控制是否允许该元素进入层级结构;
  • 对于某些不希望层级化的事件(如 Hierarchy类型),我们在调用时将 create_hierarchy 参数设置为 false;
  • 这类元素会被同一放入一个专门的容器,例如 profile_group,这个容器位于顶层节点 profile_root 之下;
  • profile_group 是一个我们新增的变量,需要将其加入调试状态结构中(debug_state);
  • 目前由于函数 get_element_from_event 被多重声明(重载),而 create_hierarchy 参数只出现在此中一个界说中,导致调用时不明白,编译器报出调用歧义错误;
  • 进一步发现问题是由于我们在头文件中对 get_element_from_event 的声明不完整,缺少了新的参数,需要更新声明以匹配实现;

操作细节与调解



  • 将 get_element_from_event 函数增加的新参数 create_hierarchy 精确声明在头文件中,消除重载歧义;
  • 确保只有一处函数声明,并与现实实现一致;
  • 在 debug_state 中添加新的 profile_group 字段,用于生存这类特殊事件的容器;
  • 在创建变量组时(create_variable_group),指定对应的巨细为 7 或 8,并绑定到新的容器中;
  • 编译后检查 get_element_from_event 调用是否精确解析,是否成功地将事件放入非层级化结构中;

团体架构演变意义



  • 实现了调试系统中“层级事件”和“非层级事件”的并存机制;
  • 提高事件展示与管理的机动性;
  • 通过简朴标志参数控制是否到场层级化归类,逻辑清晰、易于维护;
  • 建立了专属的 profile_group 容器,将来可以扩展更多仅限于该组的展示和处理方式;

小结

通过对 get_element_from_event 函数添加控制参数并修正声明,同时引入新的分析容器 profile_group,我们成功实现了将特定调试事件清除在默认层级结构之外的逻辑。这一改进提拔了调试工具对不同类型事件的表达本领,为后续的可视化和调试体验打下良好根本。


运行游戏并检察调试可视化效果

我们现在的处理逻辑是:固然仍然在记录所有调试信息,但这些信息已经被存储到了一个不可见的“后台地区”中——这是我们想要的效果。

当前目标与思绪

我们想要从这个专门的调试组中,将数据提取出来并以图形方式绘制出来。为此,我们可以:

  • 使用已有的 profile 数据结构,此中包含线程相关的事件信息;
  • 通过访问这个结构内的构成员,遍历出所有我们感兴趣的事件;
  • 将这些事件绘制出来,构建可视化的线程时间图(thread interval graph);
  • 固然当前系统已经具备这些数据,但还没有现实绘图,我们现在准备实现这一步;

技能细节分析



  • thread interval graph 是我们用来可视化线程运行区段的模块;
  • 每个线程拥有一组事件,我们通过遍历 element group 中的成员来获取这些事件;
  • 需要知道当前的帧索引,以便绘制“最新的一帧”;
  • 推测中,我们应该在调试存储结构中已经存储了最新帧的索引,例如通过 most_recent_frame 这样的字段;
  • 可以通过 most_recent_frame.index 之类的方式获取精确的时间轴基准;
  • 遇到了一个变量 frame_bar_scale,似乎是早期移植代码时残留的,可能原意是计算显示缩放比例或线程总数,但现在看起来已经无效,因此可以忽略或清除;

小结



  • 所有调试信息现在已经按照预期进入专门的组中;
  • 下一步可以在 thread interval graph 中遍历这些事件并开始绘图;
  • 需要从调试状态中提取“当前帧”的索引,用于绘制近来一次的线程活动;
  • 遇到的 frame_bar_scale 被识别为废弃变量,不再具有现实意义;
  • 接下来我们将基于已组织好的调试数据,实现更直观的线程区段可视化图形。
这意味着团体框架已经买通,后续重点放在怎样渲染可视化图形上。
game_debug.cpp:让 DrawProfileIn 绘制一帧

我们当前的任务是将性能分析系统中记录的数据以图形方式绘制出来,重点是以帧为单位进行绘制。目前我们采取的是从最新的帧中提取时间块数据进行可视化的开端尝试。以下是详细总结:

目标与思绪梳理



  • 当前的渲染逻辑中,已经实现了性能分析数据的记录和存储,但绘制部分仍未完成;
  • 我们决定只绘制最新的一帧,而不是遍历所有帧,由于绘制全部帧过于耗资源,屏幕空间也不足;
  • 将通过“存储事件”中的“打开块(begin block)”和“关闭块(end block)”来计算时间区间,进行绘制;
  • 时间区间对应屏幕上的 X 轴坐标,线程对应 Y 轴坐标(每个线程对应一条“lane”)。

数据结构与绘图逻辑

帧选择与时间跨度计算



  • 使用“最新帧”进行绘制,详细是 most_recent_frame;
  • 确认“帧标志”是在调试数据绘制后执行,因此 most_recent_frame 是已完成记录的一帧;
  • 获取该帧的时间跨度为:
    1. frame_span = frame_end_clock - frame_begin_clock
    复制代码
  • 计算屏幕绘图的缩放比例(frame_scale):
    1. frame_scale = pixel_span / frame_span
    复制代码
    即将时间范围尺度化后映射到绘图地区的宽度上。

线程信息处理与图形绘制

线程区间绘制



  • 从 profile_group 中遍历所有事件(stored_event);
  • 判断每个事件是否属于当前帧(根据其帧索引判断);
  • 对于成对出现的 begin_block 和 end_block:

    • 记录下 begin_block,当遇到对应的 end_block 时使用两者之间的时间跨度绘制矩形;
    • 位置计算如下:
      1. min_x = profile_rect.min_x + scale * (open_event.clock - frame.begin_clock)
      2. max_x = profile_rect.min_x + scale * (close_event.clock - frame.begin_clock)
      复制代码

  • Y 轴位置根据线程在 lane 中的位置进行分配,使用:
    1. lane_height = profile_rect.height / lane_count
    复制代码
    然后:
    1. min_y = profile_rect.max_y - (lane_index + 1) * lane_height
    2. max_y = profile_rect.max_y - lane_index * lane_height
    复制代码
问题点与后续计划



  • 当前存储事件是单向链表(从 oldest 到 most_recent),无法从尾部逆向遍历;
  • 可能需要改成双向链表,大概以后考虑用树形结构来优化事件组织;
  • 目前线程是用系统 ThreadID 表现的,不方便进行稳固排序和索引,需要将其更换为内部维护的“序号化 ID”;
  • 图形绘制采用 push_rect 的方式将矩形压入渲染组,使用的是 no_transform;
  • 目前省略了文本绘制(如函数名标签),后续再实现;
  • 无用的变量(如 frame_bar_scale)已清理,保持绘图逻辑简洁清晰;

总体结构现状与后续工作



  • 当前绘制逻辑已经可运行,并且可以从最新帧中精确提取和渲染函数块时间;
  • 线程图将以条形地区方式直观显示每个线程的执行片段;
  • 后续工作包括:

    • 优化线程 ID 管理;
    • 改进事件链表结构;
    • 添加文本信息(函数名称等);
    • 支持多帧回顾或缩放;
    • 提高绘制性能和美观性。


整个系统已经完成从数据记录到图形开端绘制的闭环,将来将聚焦在可用性提拔与功能细化上。

奇怪怎么没有呢


屏幕打开profile 没进来


DebugType_EndBlock 中StoreEvent被屏蔽掉了


再次运行



world 里面显示会有bug,


为什么上面树折叠对下面有影响



运行游戏并检察精确的调试可视化ε

现在的效果非常理想,甚至可以说出乎意料地精确。
固然目前还没有任何机制确保绘制的顺序是精确的,但开端结果已经达到了可接受的状态。接下来仍然有很多工作要做,尤其是排序顺序的问题还没有解决。当前的绘制过程缺乏对调用层级或先后关系的把握,导致无法精确堆叠调用块,显示出精确的嵌套或包含关系。
这也表现了性能分析器中处理调用关系的一大难点:没有现成的信息说明哪个函数是谁的调用者或被调用者。因此,要精确堆叠这些时间块并在图表中呈现函数嵌套结构,是一个比力复杂的问题。
只管如此,整个绘制系统的根本框架已经逐步搭建完成,希望令人满意。我们正在一步步接近理想状态。后续将重点解决排序与堆叠的逻辑,确保最终展示的数据既精确又具有层次感。团体来看,性能分析的可视化已经迈出了关键的一步。
Q&A

game_debug.cpp:将 PointerToUint32 转换为 CloseEvent->GUID

我们考虑到,如果拿到一个指针后,希望将其转换成可以直接使用的情势,比如某种更简化、更适合处理的类型(例如一个索引或偏移量),这一步操作必须在某个明白的位置进行,也就是说,它必须在某个阶段完成这个转换过程。
这是在处理数据结构或事件记录时非常关键的一步,尤其是在可视化大概调试系统中,当我们遍历和渲染各种数据时,如果依赖于指针自己,那么在渲染或比力时将非常不方便。因此,必须将指针或引用的信息提取出可以计算和比力的值(比如将其转换为编号、序号、数组索引等)。
这类转换通常要保证:

  • 同一性:所有指针都应该以雷同规则转换成可处理情势;
  • 安全性:转换之后不应丢失必要的语义信息,例如不能转换得太粗糙以至于无法区分不同的元素;
  • 服从:转换过程应只管靠近数据收罗或数据初次记录的位置完成,避免每次使用时重复处理。
因此,这种“指针向可用情势转换”的操作必须出现在一个明白而适当的阶段,以保证后续渲染、比力、排序等处理逻辑可以顺遂进行。


  1. #define PointerToUint32(Pointer) ((uint32)(uintptr_t)(Pointer)
  2. )
复制代码
uintptr_t 能生存一个指针
#define PointerToUint32(Pointer) ((uint32)(uintptr_t)(Pointer)
)

这段宏界说的作用是将一个指针类型安全地转换为一个 uint32 类型的整数。

为什么要用 uintptr_t?

uintptr_t 是 C/C++ 尺度库 <stdint.h> 或 <cstdint> 中界说的一个 无符号整数类型,它的关键特性是:
   它保证足够大,能够安全地存储任意指针的值。
  换句话说,它是专门用来存放指针数值(即地点)的整数类型。

分析这段宏的转换流程:

  1. (uint32)(uintptr_t)(Pointer)
复制代码

  • (uintptr_t)(Pointer)
    这一步把 Pointer(原本是一个指针类型,比如 void*)转换成 uintptr_t,也就是把指针地点转换成一个整数值。这样可以安全得到指针的“地点数值”。
  • (uint32)
    然后再将这个整数地点强制转换为 uint32,也就是取这个地点的低 32 位。

为什么不能直接 (uint32)(Pointer)?

直接这么写是不安全的,缘故原由有:


  • 在某些平台(尤其是 64 位系统)上,指针是 64 位的,而 uint32 只有 32 位。直接强制转换会丢掉高 32 位,编译器可能会发出警告或报错
  • 不同架构对指针到整数的直接转换有不同的要求。使用 uintptr_t 是尺度界说的方式,可以确保转换的安全性和跨平台兼容性。

总结

使用 uintptr_t 的缘故原由是为了:


  • 安全性:明白表现我们是要把指针的地点转成整数。
  • 可移植性:保证在不同平台上(如 32 位/64 位系统)都能精确处理指针到整数的转换。
  • 避免编译警告或错误:防止类型系统由于不安全的强转报错。

如果你在做雷同 ID 映射、调试可视化、缓存索引等,需要把指针“压缩”成一个整型值的情形,这种做法是尺度且推荐的方式。需要留意的是:最终的 uint32 可能会导致截断,如果系统是 64 位的,多个不同的指针可能映射成雷同的 uint32 值,这在设计上需要考虑。

越界了


问:你觉得用“调用树”来追踪多线程之间的依赖关系怎么样?

我们在考虑使用调用树来追踪多线程中的依赖关系时,其实面临一些困难。如果我们以性能分析器为背景来讨论,会发现传统的调用树并不能真正帮到我们。
我们真正需要了解的是——每一组调用是怎样与其“父级调用”关联的。这一点非常复杂,由于平凡调用树或调用图只能提供“函数A调用函数B”这样的信息,但它不能告诉我们:在特定的一次调用过程中,这一组调用链是怎样形成的,尤其在多线程环境下更难追踪。
我们可能需要引入更精细的记录方式,比如为每一次函数调用都记录它的“父调用”的返回信息,甚至为每个调用都维护一条详细的路径。这种方式固然看起来像调用树,但其实远远凌驾了传统调用树的本领。
传统的调用图仅仅表现“函数A偶然会调用函数B”,但这种结构并不能区分不同线程或不同时间片中,哪一次调用详细属于哪个调用上下文。我们要做的更像是一种及时的调用链追踪,记录每一个函数调用的完整泉源和上下文,而不是静态的、抽象的函数依赖关系图。
我们考虑引入某种机制,以精确地记录每一个函数调用的发生过程和其所属的父调用,这样才气在多线程场景下真正还原调用链路、分析依赖,并用于性能分析或调试。
问:你用什么命令从过场切换到游戏?

我们在游戏中切换操作模式时,使用的是空格键(Space Bar)。当需要从“切割”模式切换出来或进入其他状态时,就按下空格键来实现这个转换。这个按键被用作模式切换的快捷方式,方便我们在不同操作之间快速过渡。整个过程依赖键盘输入,通过监听空格键来触发对应的状态切换逻辑,从而改变当前的交互方式或举动。
问:你怎么处理在多线程中运行但计算时间凌驾一帧的任务?比如我听说新《极限竞速》的后视镜更新频率只有主画面的一半

我们在处理运行于独立线程的任务时,即使它们的执行时间凌驾一个帧的计算周期,也可以很好地应对。这种情况完全没有问题。当前我们暂时不会对这些任务进行绘制,但一旦团体流程整理完毕,就能轻松地添加这部分功能。
详细处理方式也不复杂。我们可以通过事件的开启与关闭来判断跨帧任务的存在。例如,如果当前帧中没有检测到“开启事件”,但却发现了“关闭事件”,这说明该任务其实是从前一帧就已经开始运行了。在这种情况下,我们只需从当前帧的出发点开始绘制即可。
为此,我们可以将其绘图的时间偏移量直接设置为零(即表现从该帧的起始位置开始),这样就能精确反映出任务的持续时间,即便其开始时间早于当前帧。这个处理计谋可以确保跨帧任务也能被精确地可视化,保证数据的完整性与时间线的一连性。
问:为什么没人告诉我游戏里有克兰普斯?

我们一开始居然没有被告知这个游戏里居然有克拉帕斯(Krampus),实在令人惊讶。其实很早以前我们就已经收到过克拉帕斯的图片,甚至可能在他首次出现在直播前,我们的邮箱里就已经有相关内容了。大概有人没留意到,但我们记得自己确实收到了关于克拉帕斯的信息,他早就已经在游戏中了。
更重要的是,克拉帕斯不但仅是游戏中的一个角色,他几乎是整个游戏存在的焦点动因。由于圣诞老人本质上只是个自尊又冷漠的家伙,完全不能理解一个没有手的小男孩怎么可能满意于每年只收到几顶帽子。他需要的是能在丛林中冒险、行动的本领,而不是装饰品。
而克拉帕斯出现之后,情况完全改变了。他理解这个孩子的真正需求,并且作为那个众所周知的“会肢解小孩”的存在,克拉帕斯天然在他的袋子里带了很多手。于是他将一只手送给了小男孩,让他重新拥有行动本领。从这个角度看,克拉帕斯拯救了一切。
这也构成了整个故事的主旨:克拉帕斯并不是恐怖的怪物,而是真正资助孩子的好汉。我们从这个设定中得出结论,他在游戏中所扮演的角色不但有趣,甚至是富有象征意义的焦点存在。
问:一边写代码一边和观众聊天对你有资助吗?

一边编码一边说话其实并不会带来什么资助,反而会让编程变得非常困难。我们经常发现,在尝试处理复杂内容的时间,同时进行语言表达会干扰思绪,导致服从下降。这种情况下,我们很难专注于逻辑推理大概细致的实现细节。
只管如此,我们还是尽力而为,在保证思维不被完全打断的情况下去表达正在进行的操作大概表明思绪。但总体而言,同时说话和编码确实是种挑战,需要在精神分配上不绝调解。
问:有没有考虑用火焰图来可视化性能数据?

我们确实没有考虑使用 flame graph(火焰图)来进行时间可视化,缘故原由有很多。首先,目前我们刚刚开始进行性能分析的可视化,像 flame graph 这样相对复杂的工具还需要编写大量的额外代码,特殊是对于一个内部的分析系统来说,这种投入并不划算。
固然以后理论上可以实现 flame graph,但我们并没有太大意愿去做,由于我们并不真正需要它。我们已经在调试系统上投入了不少时间,更希望把精神放在系统架构自己,让整个设计能清晰地展现出权衡和决策的过程,而不是把时间花在制作一些“花哨”的图表上。
flame graph 自己在信息转达上其实并没有太多独特优势,很多时间从更简朴的图形中就能获取我们需要的信息。除非是在某种特殊杂乱、结构极其复杂的代码库中,我们才可能真的需要 flame graph 来帮忙理解代码执行路径。但在我们的情况下,它并不属于一个高优先级的工具。
因此,我们倾向于使用更直接、更轻量的可视化方式来辅助性能分析,避免过分投入在看起来好看但实用性不高的工具上。总体而言,flame graph 并非必须。
问:这是不是软件渲染?帧时间太高了

当前帧时间变卡顿的缘故原由并不是运算逻辑自己在运行,而是我们正在处理数目非常巨大的调试元素。由于我们记录了任意数目的帧,每一帧中包含大量的事件数据,现在每次都必须遍历所有帧、每一个事件进行处理,这种做法代价极高。
目前的系统中,已经积累了成百上千甚至上百万条调试记录,而我们仍在使用一个简朴的线性遍历方式去查询和处理它们。每一帧都需要重新完整地遍历所有数据,这种方式显然在数据量较大时无法承受,会导致性能急剧下降,尤其是在持续记录的场景下尤为严峻。
因此,现有方式已经不适合继承使用,我们必须寻找一种更高效的查询机制来获取所需信息。必须优化这部分逻辑,例如通过构建更高效的数据结构,缓存机制或索引来快速定位目标数据,淘汰不必要的重复计算。换句话说,我们需要一种性能更高、响应更快的方式来支撑分析和绘制工作。
game_debug.cpp:停止每次都循环遍历所有事件

目前系统的性能瓶颈基本上出现在一个特定的 if 判断中。如果我们将这个判断逻辑去掉,性能问题就会得到缓解,看起来一切就能正常运行。然而,代码并没有精确地重新加载,缘故原由在于有一段代码被标志为“无法到达”,这导致编译器并未真正构建新的版本。
在我们手动绕过这部分判断之后,步伐确实规复了正常运行,但这也揭示了一个潜在问题 —— 我们当前对字符串的处理存在严峻缺陷。固然逻辑上是将字符串压入某个容器或栈中,但现实上某些字符串并没有成功地被推入进去。
这一现象在代码热重载时表现得尤为明显。本应被加载的字符串在热重载后丢失了,说明它们在数据结构中的插入操作未能精确执行。推测问题发生在某个阶段字符串没有被加入,导致热重载后的状态不完整,这是一个明显的 bug。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

梦见你的名字

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表