游戏引擎学习第249天:清理调试宏
欢迎大家,让我们直接进入调试代码的改进工作
接下来,我们来看一下上次停留的位置。如果我没记错的话,上一场直播的结尾我有提到一些我想做的事情,并且在代码中留下了一个待办事项。所以也许我们今天首先做的就是解决这个问题。但首先回顾一下上次的进展,我们其实已经开始着手完成一些任务了,我发现那时的状态还没有完全完成。
回顾:调试系统相对可用,但所有名称总是被强制通过层级系统,这样会为一些内容创建虚拟节点
实际上,调试系统已经相对可用了,因此可以通过它来创建各种系统。不过,有几个地方需要清理。首先,我们遇到了一个奇怪的问题,就是所有的名称都被强制通过层级系统,这会为一些东西创建虚拟节点,即使我们可能并不希望这样做。当然,在某些情况下,我们其实是希望这样做的。因此,需要决定如何处理这些节点。是希望总是为此目的创建数据块,还是有其他更合适的做法?我们需要进一步思考和决定。
我们还想让分析器(profiler)重新起作用
我们接下来的任务是重新开启性能分析功能,并提升其实用性。目前虽然已经在记录性能分析信息,但实际上并没有真正加以利用。为了实现这一点,我们希望能够将性能分析器系统性地整合进现有的功能集合中。
这个过程应该相对简单,因为我们已经具备相关功能的表达和组织方式。接下来,我们会加载游戏,定位到 game.cpp
文件。在这个文件中,我们曾经添加了一个调试模块。现在的目标是使我们能够更灵活地将性能分析器添加到系统中,并能够借助这个机制去管理和使用性能数据。这样,我们就可以更充分地利用已有的性能信息来辅助优化和开发工作。
game.cpp:在调试数据块中添加 DEBUG_PROFILE()
我们希望实现的功能是:当在调试模块中使用某些代码时,可以直接创建一个性能分析器(profile),并将其嵌入到调试代码块中。我们的目标是能够非常方便地指定某个函数,并对其进行性能分析。
例如,现在我们有一个用于计时的函数,我们希望通过这个机制对 game_update_and_render
这个函数进行计时,并将其纳入性能分析器的记录范围内。这将使我们能够追踪这个函数的执行时间,从而进行进一步优化。
我们打算这么做:调用该计时函数,把需要分析的函数包裹起来,并创建一个新的调试性能分析器。但当前的实现中,如果尝试直接编译,会遇到严重的错误,这是因为我们并没有真正定义 debug_profile
这个功能。因此,下一步我们需要做的就是实现或补全 debug_profile
的定义,使其能被正常调用并用于调试和性能分析。
game_debug_interface.h:定义 DEBUG_PROFILE
我们现在的目标是在接口中加入性能分析器的支持,使其能够被方便地嵌入现有的调试系统中。实现这项功能的思路其实非常清晰,就是仿照之前已有的调试变量(如 debug value)等方式进行处理。具体来说,只需要添加一个新的 #define
宏,例如 DEBUG_PROFILE
,并传入我们想要分析的函数名称。
这个宏的作用就是在调用时记录当前正在进行性能分析的函数名称。由于已经存在函数名,所以可以直接引用该函数。我们希望能够传递一个表示该函数在性能追踪体系中的类型,例如之前定义的 trace
类型。这个系统中已经存在线程计数器列表(counter thread list)的概念,因此我们可以继续使用这个机制。
接下来,计划引入一个新的类型,用来表示正在计时或分析的函数集合(counter function list),将其与函数名称关联,从而在性能数据流中记录该事件。这样在调试系统处理这类事件时,就可以识别出这部分数据并对其进行追踪。
实现逻辑上,当数据被写入调试数据块时,会作为一个元素被存储。一旦这些元素被遍历,我们就能重新拾取到它们并加以利用。不过现在还存在一个小问题:我们可能尚未实现真正的绘制(绘图)逻辑。当前的实现中,似乎只调用了 debug_draw_event
,而不是 debug_draw_element
,因此不能确定是否能正确显示这些新类型的数据。
这里的困惑源于系统本身对数据块与调试元素的区分方式。我们尚不确定这些函数分析器是否应作为普通数据块的一部分来处理,也可能需要独立处理方式。这是目前需要进一步厘清和设计的部分。
总结起来,我们的工作包括:
- 添加
debug_profile
宏以支持性能分析调用; - 为函数分析数据指定类型(如 counter function list);
- 将分析事件写入调试数据流;
- 确保调试系统能够识别和处理这类事件;
- 最终考虑如何绘制和可视化这些数据,以完成从记录到展示的闭环。
我们有调试元素(可以存在于多个层级中)和调试事件(通过流传入的内容,我们试图记录这些事件)
当前的系统中存在两种不同的调试结构:调试元素(debug elements) 和 调试事件(debug events)。调试元素是可以存在于多个层级结构中的实体,可以有多个不同的实例,组成一类树状或分层的可视化数据。而调试事件则是通过数据流记录下来的事件,属于时间线上实际发生的操作记录。
基于这一结构差异,现在考虑在实现函数性能分析器时,或许不需要让其成为依附于某个数据块的数据结构。换句话说,不必再将性能分析数据硬塞进已有的数据块中,而是直接将其作为独立的调试元素来处理。
具体的思路是在事件遍历过程中,当遇到性能分析相关的事件时,直接创建一个新的调试元素,将其作为系统的独立组成部分处理,而不是让它参与已有的数据块结构。例如,在处理事件类型时,系统目前已经有多个事件类型,如 data block open
、data block close
等。针对新的分析器事件,也可以在此类结构中引入对应的类型,并在遍历时识别并创建相应的调试元素。
这种做法的好处是结构更清晰,便于管理和可视化,避免将分析器事件混淆在普通数据块中。这样可以更灵活地展示和操作分析数据,也更符合调试系统中“元素”和“事件”的设计分离原则。总之,这是一种将性能分析系统更好地融入现有调试框架的方式,有助于提升整体的功能完整性和可维护性。
game_debug.cpp:为 DebugType_CounterFunctionList 添加一个空的 case,检查是否使用现有的调试元素足以用于分析
我们当前的任务是处理一个特定类型的调试数据:counter_function_list
,这是用于函数性能分析的一种结构。思路是,当系统在解析调试事件时遇到这种类型的内容时,应该识别出这是一个用于分析的函数,并据此创建对应的性能分析器。
我们已经实现了解析过程,也成功生成了一个 debug_element
实例。接下来希望做的是直接利用这个已经生成的调试元素来创建性能分析器。初步判断这个过程可能可以自动完成,不需要太多额外处理。
然而,在调试的过程中,尝试查看一个被命名为 game_update_and_render
的函数是否已成功作为调试元素加载时,却并没有发现它出现在调试界面中。这引发了一个问题:元素明明被创建了,但似乎没有被真正地插入到调试系统中可视化的结构中。
分析后发现,原因可能在于这些元素没有被加入到某个“组”(group)中。在调试系统的设计中,调试元素需要通过 add_element_to_group
函数被插入到对应的分组结构中,而这一步当前似乎没有正常执行。系统在尝试获取父分组时未能成功,导致元素无法归属到任何组,从而未能被显示或处理。
下一步,我们需要检查这个 add_element_to_group
的具体实现,确认其对父组的处理逻辑。如果没有正确识别或创建根组,那么后续的元素自然也无法加入。这一问题的核心在于组结构未能建立或关联,导致有效的调试数据未能呈现。
因此,接下来的操作是:
- 找到
add_element_to_group
的具体行为逻辑; - 设置断点观察
debug_value
相关元素在加载时是否能正确获得父组; - 确保即便当前元素没有上层组,也能以根元素的形式被添加到调试系统中;
- 最终让
counter_function_list
类型的函数分析数据,能够被识别、存储并可视化展示。
这一过程将确保函数性能分析器能够真正嵌入调试系统,并在运行时动态记录和可视化指定函数的性能信息。
game_debug.cpp:检查 DebugType_MarkDebugValue 是否仍在使用,并删除它
目前正在清理一些代码,发现某些部分似乎已经没有作用,因此尝试删除它们以简化系统结构。在这一过程中,仍然存在一个疑问:即使尝试将性能分析器插入到调试系统中,操作本身看似成功,但结果却没有可见效果。
深入分析发现,虽然插入函数分析器的操作已经执行,但并未显示在可视化界面上的原因,是因为当前调试系统只会为一些特定的预定义模块(如 global_renderer
和 camera
)创建节点,至于最终用户自定义的节点(如 game_update_and_render
),并不会被自动处理。也就是说,虽然数据已生成,但从未主动使用或展示。
为了验证这一判断,进行了一个测试:手动创建一个叫 Foo_GameUpdateAndRender
的节点。结果验证了猜测:系统确实能创建这些节点,只不过默认不会对最终目标函数做任何进一步处理。因此,game_update_and_render
也确实已经存在,只是因为没有显式使用或绑定,它没有被展现出来。
为了解决这个问题,需要将性能分析器插入到合适的位置,并指定其作用范围。也就是说,在调用分析器时,除了传入函数名外,还必须指定其挂载的调试数据结构(比如挂在哪个调试组、属于哪个区域)。目前的分析逻辑只处理了函数名,没有指定目标位置,因此即便创建了分析数据,也无从显示和管理。
这进一步说明,也许将分析器挂在某个数据块中是一个更合理的做法,因为数据块提供了上下文结构,让系统知道该将性能数据展示在哪个层级或模块下。
不过,还有一个不确定的问题:当前系统是否支持在数据块中嵌套或挂载这类函数分析器,还需要进一步确认。如果支持,那么通过数据块管理将是最清晰、结构最合理的路径。
总结关键点如下:
- 删除无效代码以清理系统结构;
- 分析发现性能分析器数据已生成但未展示;
- 原因是默认系统只处理部分预设节点,忽略了最终目标函数;
- 为了让分析器数据可视化,必须指定插入目标位置(如所属数据块);
- 使用数据块来组织分析数据可能是可行的优化方向,但需要确认系统支持程度;
- 后续要扩展性能分析器调用方式,支持传入函数名和目标挂载点,以实现更完整的集成。
game.cpp:检查调试系统是否支持嵌套的数据块
在调试系统中测试了在数据块(data block)内部嵌套其他数据块的行为,目的是了解是否支持这种结构层级。在实验过程中发现,系统目前不支持数据块嵌套。例如创建了一个用于性能分析的 profile
数据块,虽然这个数据块本身被创建了,但并没有嵌套到任何已有的数据块中,呈现的是平级结构。这说明嵌套机制并未被实现。
如果未来确实需要嵌套结构,就必须显式地实现相关逻辑。目前系统的行为还不具备自动处理子块的能力。这也让整个调试结构显得有些混乱,不容易按预期组织好分析数据。
这一发现促使我们重新思考整个系统中调试结构的组织方式。当前实现方式在表现上不够直观,导致即使某些操作“成功”了,实际上也没有完全按照需求被呈现出来。
因此,当前策略是先不强行解决嵌套结构的问题,而是优先完成性能分析器本身的显示工作。先把分析数据在调试界面中正确渲染出来,确保能看到具体的分析内容。之后再回过头来,重新梳理整个调试接口的设计,优化嵌套结构的支持方式。
当前的计划是:
- 确认数据块不能嵌套;
- 先将性能分析器成功写入调试系统,确保可视化正常;
- 后续着手重新设计调试接口,使其清晰、统一、易于扩展;
- 明确最终每个调试功能(如性能分析)的调用方式和数据结构;
最终目标是让调试系统的接口明确规范,能正确支持调试数据的组织与展示,特别是在涉及多个层级或模块时具备良好的可维护性与拓展性。
game_debug.cpp:在 DEBUGDrawElement 中包括 DebugType_CounterFunctionList 的 case
当前的任务是让函数分析器的内容(即函数列表)能在调试系统中被正确绘制出来。我们已经确认,当调用 debug_draw_element
函数时,如果元素的类型是 counter_function_list
,就应该执行对应的绘制逻辑。为了快速验证,临时让它复用现有线程列表(thread_list
)的绘制方式,虽然现在还没有真正绘制函数列表本身,但这只是个过渡方案,后续可以替换为具体的函数数据渲染逻辑。
但在实际运行时发现,并没有看到预期中的函数列表显示出来。进一步排查时注意到,该元素确实是被创建在某个数据块(data block
)内部的,这可能会影响其可见性。理论上,这种结构下它应该在数据块上层进行显示,但实际上并没有呈现。
因此下一步是检查是否函数列表真的被加入了要绘制的元素集中,或者只是被创建但没有注册到绘制流程中。也就是说,有可能这个元素虽然存在于内存中,但由于没有被添加到用于渲染的树状结构或分组列表里,系统并没有真正调用到它的绘制逻辑。
初步猜测的问题点包括:
- 虽然
counter_function_list
类型的元素被创建,但没有被插入到调试元素组(group)中; - 在调用
debug_draw_element
时,系统根本没有迭代到这个元素; - 数据块嵌套受限,导致它被遮蔽或忽略;
- 绘制逻辑没有正确绑定该类型到实际可视化步骤中。
接下来的重点是:
- 检查调试元素是否被正确加入到数据结构中;
- 确保遍历元素时不会跳过
counter_function_list
; - 临时使用
thread_list
的绘制逻辑验证是否能触发显示; - 一旦验证流程通畅,再补充具体的函数列表绘制代码。
整体上,这一阶段的目标是:确认分析器类型的调试元素可以完整地进入绘制流程,并在界面上被看到,哪怕内容暂时不完整。这将为后续功能完善奠定基础。
game_debug.cpp:删除之前为 DebugType_CounterFunctionList 添加的拦截调用(空的 case)
我们现在已经基本理清了调试系统中的分析器功能是如何工作的,函数列表最终能够正确存储到数据块中,这是通过拦截调用机制实现的。验证后也成功确认数据被加入并能够被调试系统识别与处理。
当前的整体架构虽然已能跑通基本功能,但代码结构较为松散、零散,逻辑分布在多个地方,导致维护和扩展不便。因此,下一步目标是对接口部分进行精简与统一,使整个调试和分析功能变得更清晰、更系统化。
当前已有的功能点包括:
- 时间函数记录机制已可用;
- 函数分析器的调试元素能够被识别与存储;
- 能够通过数据流将这些信息传入调试绘制系统。
接下来需要明确和优化的是:
- 界面调用统一化:决定一个清晰、统一的调用接口,使得所有分析相关的功能都能通过一个规范的路径执行,而不是散落在不同模块。
- 调试接口设计:定义一套标准的调试接口,使新增功能只需走这条路径,而不需要修改底层结构或分支逻辑。
- 不再冗余分支判断:现在的代码逻辑中有些地方是为了解决之前未统一处理的特殊情况,优化后希望能减少这种“补丁式”的判断,让逻辑更流畅。
- 重构插入方式:将调试元素的插入流程标准化,比如函数分析器不需要手动判断其位置,而是交由统一的管理器根据类型和上下文自动完成插入和绘制注册。
最终目标是让调试系统具备以下特点:
- 模块结构清晰、逻辑集中;
- 所有数据流动路径可预测、可追踪;
- 添加新调试类型时无需大范围修改原有代码;
- 可视化呈现自动关联,无需手动干预渲染注册流程。
我们将从实际调用入手,回头重新检查每一处分析相关功能的调用路径,确保这些路径清晰、合理并且能够复用。之后再集中整理界面接口,使整个调试与分析系统更简洁、更强大。
game.cpp:优化数据块的语法
我们现在的核心目标是简化并理顺调试系统中的数据块(data block)结构,确保数据能够清晰、合理地呈现,同时具备良好的可维护性和扩展性。
目前构想的做法是,将数据块表示方式简化为一个统一的结构体调用,去除繁琐的 Begin/End
配对调用模式,改为像 debug_data_block("名称")
这样的语义性调用,这样可以通过构造函数/析构函数自动完成开启与关闭,代码逻辑更紧凑、更不易出错。
我们当前的改动和计划如下:
1. 数据块调用方式简化
改为类似如下写法:
{DebugDataBlock block("菜单名/子菜单名/数据块名");DebugValue("变量名", 数值);
}
通过构造函数打开,析构函数自动关闭,不再需要手动管理 Begin/End
。
2. 命名不再强制使用下划线(_)连接
以前用下划线拼接结构名和变量名(例如 Global_Camera_X
),是因为命名限制。现在我们可以直接使用清晰的路径方式,例如 "Global/Camera/X"
,可以更直观地表示调试项的分层结构,也便于菜单系统展示成层级结构。
3. 支持层级结构的路径表达
路径用 /
分隔形成层级,例如:
"Renderer/Camera/FOV"
"Game/GroundChunks/SimulationTime"
这类似于文件系统结构,使调试信息呈现更清晰,并支持自动归类。
4. 唯一标识问题(GUID ID)
为了在多个运行中能识别出相同的数据块,我们使用哈希机制,基于路径字符串进行哈希,避免依赖不稳定的指针地址。
- 使用字符串路径的哈希值作为
GUID ID
; - 避免多个相同名字数据块混淆(例如多个实体的
"SelectedEntity"
); - 后续如果需要更复杂识别机制,可以引入辅助 ID 或作用域识别。
5. 统一处理方式带来的好处
- 不需要关注当前是数据块、线程、计数器还是事件,所有内容都走统一数据结构;
- 数据自动插入层级结构中,渲染时按层级组织;
- 降低开发接入成本,逻辑更加一致。
6. 示例结构(更清晰的菜单展示)
例如:
DebugDataBlock block("AI/Familiar/FollowsHero");
DebugValue("IsFollowing", true);
DebugValue("Distance", 42.7f);
将会呈现为:
AI
└── Familiar└── FollowsHero├── IsFollowing = true└── Distance = 42.7
7. 后续工作
- 将目前已有的 Debug 值迁移为新格式;
- 确保调试绘制逻辑支持路径识别;
- 考虑支持数据块内部嵌套(如 AI 模块中嵌套多个子系统);
- 抽象出更好用的调试接口工具,减少重复代码;
- 统一处理带有名字哈希的调试项注册与管理逻辑。
整体来看,这套调整将会大大提升调试系统的清晰度与可维护性,使其更加模块化、结构化,同时减少不必要的分支判断与手动管理逻辑,为未来功能扩展(如运行时搜索调试项、自动对比等)打下良好基础。
game_debug_interface.h:重写 DEBUG_DATA_BLOCK 以适应 API 的变化
我们现在正在重构调试系统的数据块结构,目标是将原本显式调用 BeginDataBlock
和 EndDataBlock
的方式,改为使用一个自动管理作用域的 RAII(Resource Acquisition Is Initialization)风格结构体,也就是用构造函数开始数据块,用析构函数自动关闭。
当前遇到的技术细节和解决思路如下:
1. 数据块作用域封装
我们希望简化成这样的形式:
{DebugDataBlock block("名称/子项");// 数据项
}
这样只需要一个 debug_data_block
宏或者直接结构体调用,就能完成整个生命周期管理。
2. 不再需要使用宏生成唯一名称
在定时器(例如 timed_block
)中使用宏是因为同一个作用域内可能有多个定时器,需要生成唯一变量名。
而现在我们约定:debug_data_block
只能写在作用域顶部,这样就不可能发生重名问题,也就不需要用宏扩展搞乱七八糟的唯一命名了。
这极大简化了实现:
DebugDataBlock db("System/Renderer");
就足够了,不需要什么 #define
和 __LINE__
拼接的宏技术。
3. 构造函数 & 析构函数自动管理
- 构造函数负责发起“开始数据块”的调试调用;
- 析构函数负责发送“结束数据块”的调试调用。
这样可以防止忘记关闭数据块,减少 Bug。
4. 不再需要 BeginArray
/ EndArray
之前系统中曾定义 BeginArray
和 EndArray
,但现在发现它们实际上没有实现任何实际功能,因此可以直接忽略,无需保留这套 API。
5. 接口最终形态
我们现在构思的接口形式如下:
{DebugDataBlock block("Game/GroundChunks/Renderer");DebugValue("NumVisibleChunks", chunk_count);DebugValue("LastUpdateTimeMS", last_time_ms);
}
这种结构清晰、层次分明、无需额外手动管理闭合、也避免重复变量命名,同时也便于调试 UI 中以树状方式展示。
6. 实现上的下一步
我们只需要:
- 实现
DebugDataBlock
结构体,它在构造时发出BeginDataBlock(name)
,析构时发出EndDataBlock()
; - 调整现有调试代码路径,使用这个结构替换老旧的
Begin/End
风格; - 确保调试渲染器(debug UI)正确识别和绘制这种新结构的分层数据块。
总结
这一重构目标清晰:用更现代、安全、简洁的方式组织调试信息。我们避免了 C++ 宏的复杂性,去掉了无用的接口,统一了调试结构的风格和表现方式,为后续功能扩展(如多层级数据展示、过滤、折叠)提供了更坚实的基础。整个方向正确,当前就是逐步实现构造和数据传递部分的逻辑。
实现数据块开始和结束的构造函数
当前的重点在于重构和简化调试数据块(debug data block
)的管理逻辑,同时让它更整洁、自动化并易于使用。以下是内容的详细总结:
主要目标
- 实现一个自动管理的调试数据块机制,简化之前繁琐的手动调用
begin_data_block()
和end_data_block()
的过程。 - 新方案希望通过构造函数和析构函数的方式,利用对象生命周期自动调用打开和关闭逻辑。
- 使用宏定义(如
DEBUG_DATA_BLOCK(...)
)简化调用,便于调试代码的书写。
技术实现方式与考虑
-
自动封装 Begin/End 调用:
- 利用 RAII(构造/析构)机制,
debug_data_block
类型在构造时调用begin_data_block()
,在析构时调用end_data_block()
。 - 这样可以保证数据块在作用域结束时自动正确关闭。
- 利用 RAII(构造/析构)机制,
-
简化宏定义的形式:
- 使用宏如:
DEBUG_DATA_BLOCK("name")
来自动创建一个本地的debug_data_block
对象。 - 避免了手动书写打开/关闭语句。
- 使用宏如:
-
唯一变量命名问题:
- 宏中通过
##
拼接方式自动生成唯一变量名(如:DataBlockRender_
)。 - 避免变量名冲突并确保每个作用域中只有一个对应的块对象。
- 宏中通过
-
附加信息(如文件名、行号等):
- 思考是否需要额外保存文件名、行号等信息,但目前的系统似乎并没有在普通事件中使用这些信息。
- 因此当前不准备引入这些字段,以保持实现简单。
当前实现状态
- 构造函数中能正确接收块名称,并自动开始一个数据块。
- 宏定义能够顺利展开,生成本地作用域对象,无需手动指定唯一变量名。
- 并未实现文件名、行号等调试信息的存储。
- 数据块的唯一标识目前仅依赖于字符串名称或指针,不涉及多重策略。
后续打算
- 初步实现完成后,下一步是:
- 清理已有的
begin_data_block
和end_data_block
调用方式。 - 将它们替换为新方式统一管理。
- 进一步测试实际运行中调试数据的层级组织是否如预期。
- 清理已有的
- 最终目标是让整个调试 UI 更整洁,并减少人为调用时可能造成的错乱或遗漏。
小结
整体方案旨在利用 C++ 的语言机制(RAII + 宏展开)来构建一个更简洁、自动化的调试数据块系统。当前重点是宏命名展开与生命周期管理,后续将继续完善界面呈现、数据唯一标识等细节。
game_debug.cpp:检查 GUID 的使用位置
当前的工作主要集中在调试系统中数据块(data block)的唯一标识生成与调试事件记录机制的合理性分析。以下是详细的中文总结:
核心关注点
当前正在分析调试数据块中 GUID
参数的用途以及 record_debug_event()
函数调用中 unique_file_counter_string
的实际意义和潜在问题。
具体分析内容
-
GUID
的用途与 ID 生成:- 在执行
open_data_block()
时,系统尝试根据传入的GUID
来生成唯一的标识符(ID)。 GUID
的存在目的,是为了帮助系统区分不同的数据块,使调试信息可以绑定到具体的上下文或结构体实例上。- 虽然目前的逻辑看起来有点复杂,但这是出于对特定数据追踪的需要,暂时不能省略。
- 在执行
-
关于
unique_file_counter_string
的问题:- 此参数通常是在
record_debug_event()
时生成,用于标识事件发生的具体位置(文件 + 行号 + 唯一编号)。 - 但此机制存在一个严重的问题:记录位置是按函数调用位置计算的,而不是按实际数据上下文计算。
- 举个例子,在当前的场景下,如果
record_debug_event()
是在宏展开或统一构造器中被调用,那么所有事件都会获得相同的unique_file_counter_string
。 - 这会导致调试信息错误地指向同一位置,不能反映实际数据结构或层级。
- 此参数通常是在
-
简化的可能性:
- 鉴于现有方案中部分信息(如文件/行号)未必在所有场景中都需要,可以考虑对
record_debug_event()
做一些重构,减少依赖或改进记录机制。 - 比如,如果调试块本身已经包含上下文信息(例如路径式命名或唯一 ID),那么就不需要通过文件/行号反推来源。
- 鉴于现有方案中部分信息(如文件/行号)未必在所有场景中都需要,可以考虑对
初步结论与方向
GUID
是当前系统中确保数据唯一性的关键机制,虽然结构复杂,但仍然必要。unique_file_counter_string
的使用可能带来误导,特别是在抽象封装后统一调用的场景下。- 后续可考虑以下方向优化:
- 精简或替代
unique_file_counter_string
的记录方式; - 让调试事件记录更贴近数据结构本身而非调用代码的位置;
- 保留
GUID
的使用,但简化生成逻辑或提升自动化程度。
- 精简或替代
总结
目前调试系统中存在信息绑定不准确的潜在问题,尤其是在事件记录与源位置关联方面。需要进一步优化,使调试数据块的标识与数据本身而非调用位置绑定,从而提升系统的准确性和可靠性。同时保留关键机制如 GUID
以维持唯一性控制。
game_debug_interface.h:考虑通过 RecordDebugEvent 调用链传递 GUID
一、核心问题与目标
我们的目标是确保调试数据块(debug data block)在事件记录(record_debug_event
)中使用正确的 GUID 作为唯一标识,以便在多次程序运行或多个实例之间能够准确区分和识别调试信息。
二、问题详解
1. GUID 的必要性
- GUID 用于标识每一个调试数据块,使其在调试记录中具有全局唯一性。
- 系统依赖
record_debug_event
中通过 GUID 进行唯一标记,以识别每条记录属于哪个数据块。
2. unique_file_counter_string
的不足
- 虽然
unique_file_counter_string
尝试结合文件、行号、计数器等信息形成某种唯一标识,但:- 它仅在调用宏的位置处生成,无法反映实际的数据语义;
- 多个逻辑上独立的调试数据块可能因宏位置相同而误用相同标识。
3. 当前设计的问题
record_debug_event
在当前设计下无法自己生成可靠的 GUID,因此必须从外部传入。- 例如
timed_block
和debug_data_block
等辅助封装体,都需要传入生成好的 GUID,确保一致性。
三、解决方案与优化建议
1. 显式传入 GUID
- 每个
debug_data_block
或timed_block
的创建应带入显式构造的 GUID,而不是依赖宏展开后的位置生成。 - GUID 可通过一个统一的构造函数来生成,例如:
make_debug_guid("Renderer/Camera", __FILE__, __LINE__);
2. 将 GUID 构建逻辑封装
- 建议构造一个结构体如
DebugGUID
或DebugIdentifier
,包含:- 逻辑名称(如
"Renderer/Camera"
) - 文件名
- 行号
- 编译时间戳等可选信息
- 逻辑名称(如
- 所有事件记录函数、数据块构造器统一使用该结构体,简化参数传递。
3. 精简宏与结构设计
- 当前大量使用的 C++ 宏(如
DEBUG_DATA_BLOCK
)导致代码冗长且不易维护; - 可考虑用类封装带有析构器的方式,如:
struct DebugDataBlock {DebugDataBlock(DebugGUID guid) { OpenDebugBlock(guid); }~DebugDataBlock() { CloseDebugBlock(); } }; #define DEBUG_DATA_BLOCK(name) DebugDataBlock _dbg_##name(make_debug_guid(#name, __FILE__, __LINE__))
四、语言机制限制与哲学思考
- 当前所有的繁琐处理,根源在于 C++ 缺乏对 元编程(metaprogramming)和代码注解(annotations) 的原生支持;
- 如果语言支持编译时 AST 操作或代码插桩(instrumentation),如某些现代语言(Rust、Zig、自定义 DSL),这一切都可由编译器自动完成;
- 所以我们才会不得不手动构造 GUID、手动包装 RAII 析构器、手动展开宏来实现调试功能。
五、最终结论与实践路径
- 抛弃
unique_file_counter_string
,改为统一使用显式构造的GUID
; - 所有调试相关的事件记录必须传入该 GUID;
- 封装一个
DebugGUID
类型并统一传递; - 尽可能简化宏定义,降低耦合,提升可读性;
- 如果后续改用支持元编程的语言或构建工具,调试信息系统可以重构为声明式或注解式写法。
这样一来,调试信息的识别将更可靠,系统的扩展性与可维护性也将显著提升。
将 “Name” 添加到 UniqueFileCounterString(),移除 RecordDebugEvent 和 debug_event 结构中的 BlockName
在这段内容中,主要讨论了如何简化和优化调试信息的存储和记录。以下是具体的细节总结:
-
数据存储格式:
- 如果已经确定了数据存储的格式,可以考虑通过将数据结构调整为包含必要信息的单一字符串(例如,包含块名和数字),从而简化调试记录的内容。这将有助于减少记录时的复杂度。
- 这种方法的好处是减少了传递多个参数,特别是通过仅存储一个字符串指针,可以轻松地管理和访问这些信息。
-
简化信息记录:
- 在调试记录中,除了存储事件类型和(GUID)信息外,其它如线程ID、核心索引等细节可以被简化或省略,减少数据包的负担。
- 记录的结构保持相对简洁,避免了过多的冗余信息。虽然某些信息(如事件的特定细节)可能被省略,但这些不一定影响核心功能。
-
优化事件记录:
- 每个事件只需要记录两个基本信息:事件的类型和相关的网格。通过这样简化的信息,可以减少复杂度,提高代码的可维护性。
GUID
和type
是必须要传入的核心参数,而其他的细节(如文件名、行号等)可以选择性地存储或忽略。
-
减少编译错误和复杂性:
- 在重构过程中,命名的简化可以减少编译时的错误,尤其是在调整变量名称或删除不必要的元素时。通过保持一致的命名规则和结构,可以更容易地维护和更新代码。
-
未来可能的优化方向:
- 如果担心信息量过大,可以考虑进一步简化数据存储结构,甚至可能在未来的某个时刻通过不同的策略进行更精细的优化,比如减少传递的参数数量,或者对记录的数据进行压缩或分层。
-
调整代码简洁性:
- 为了让代码更易于输入和理解,可能会通过给一些变量(如
debug GUID
)起更简洁或更易懂的名字来提高代码的可读性。例如,将debug GUID
改为更直观的名称,避免混淆。
- 为了让代码更易于输入和理解,可能会通过给一些变量(如
总结来说,目的是通过减少不必要的数据记录和传递参数,简化调试信息的存储方式,确保代码更高效、简洁且容易维护。这种方法的核心是通过存储包含所有重要信息的单一字符串,来减少对多个复杂字段的依赖,从而简化代码逻辑并提高性能。
将 UniqueFileCounterString 重命名为 DEBUG_NAME
我们设计了一种统一的方式,用于记录调试事件,并简化调试信息的书写流程。在这个方案中,我们只需要提供一个 调试名称(debug name),而其他额外信息(如文件名、行号、宏计数器等)则由预处理宏自动补全,无需手动传递。
我们可以直接使用一个 RecordDebugEvent("调试名称")
这样的接口来记录事件,宏会自动展开并补全完整调试信息,包括:
- 源文件路径(
__FILE__
) - 代码行号(
__LINE__
) - 宏展开计数器(
__COUNTER__
) - 用户指定的调试标识字符串
宏将这些信息包装起来,形成唯一的事件标识。这样做有几个优点:
- 避免手动拼接字符串,减少错误和重复劳动。
- 事件名称具有唯一性,可准确对应具体位置。
- 在调试输出中自动携带来源信息,提升可读性。
通过这种机制,我们在需要标记事件的位置,只需提供核心语义名称,其他辅助信息完全自动生成。这种方式适用于调试计数器、性能标记、帧标记、块范围标记等多种用途。
目前我们暂且认为该方式合理,即便未来可能需要调整或重构,现在也可以先按这个思路继续推进设计和实现工作。
此外,我们暂时忽略了 int
相关细节或用法,后续根据具体需求再决定是否使用整数或其他参数形式。
检查 Counter 是否使用并移除它,简化 FRAME_MARKER()
我们分析并整理了当前调试事件记录逻辑的结构,逐步清理无用部分并梳理其真正的功能和调用流程,重点聚焦于以下几个方面:
对 counter = counter
的确认
我们发现代码中存在 counter = counter
的语句。经分析:
- 这个语句本身没有任何实际功能,既不会修改数据,也不会带来副作用。
- 它看起来像是“遗留代码”(vestigial),可能是早期开发阶段保留下来的残余内容。
- 所以我们决定将其视为无效代码,可以放心删除或忽略。
调试帧标记(Frame Marker)的实现逻辑
我们进一步确认了帧标记的记录流程:
- 本质上,帧标记就是通过
RecordDebugEvent
函数记录一个事件,并把当前帧的耗时或帧序号等“数值”记录进去。 RecordDebugEvent
是主要的记录入口,它负责将必要的调试信息存储下来。- 用户只需要调用该接口,并传入表示“帧计数”的数值。
宏调用顺序与位置的组织
在实现上,RecordDebugEvent
会被多个调试相关宏所调用,尤其是像 TimeBlock
这类宏,它们用于标记一段代码块的执行时间。
- 因为宏需要展开并调用底层实现,所以我们确保
RecordDebugEvent
函数的定义出现在宏调用语句之后。 - 这样做是为了避免编译器在宏展开前遇到未定义的问题。
TimeBlock
的后续集成
我们计划将 TimeBlock
宏接入 RecordDebugEvent
,使它能记录:
- 当前代码块的标识(如函数名或模块名)
- 执行时间(开始/结束)
- 其他辅助信息(如线程 ID、帧数、事件类型等)
最终,这些信息都会通过统一的记录接口写入调试数据中。
结论与下一步
- 清理无用代码(如
counter = counter
)。 - 统一所有调试事件记录接口,将
RecordDebugEvent
作为核心。 - 完善 TimeBlock 相关的逻辑实现,让它能够无缝接入记录系统。
- 保持代码结构清晰,确保宏定义和函数定义顺序合理,避免编译错误。
这一阶段的工作主要是为了扫清冗余,并构建稳定、统一的调试记录体系。
清理 BEGIN_BLOCK_() 和 END_BLOCK_(),并在调用 RecordDebugEvent 时添加 GUID
我们正在逐步推进调试事件记录系统的规范化,当前聚焦的核心是 调试名称(Debug Name)和事件记录的接口适配问题。以下是具体分析和调整内容:
所有 RecordDebugEvent
统一需要传入 Debug Name(调试名称)
- 现状:原有调用中,有些
RecordDebugEvent
并未显式传入名称参数。 - 调整后:现在所有的事件记录调用都必须传入一个调试名称(debug name),也就是说,必须显式指定当前事件的名称字符串。
- 目的:统一所有事件标识方式,便于后期调试数据检索和可视化。
原始结构(如 BeginBlock 等)不再自动生成名称
- 之前某些宏(如
BeginBlock
)可能会隐式拼接调试名称或通过某些机制间接生成。 - 现在调整为:调用者必须直接传入预先生成好的名称标识(例如:一个已构造的 debug GUID 或 string)。
- 所有这类使用宏的地方也必须修改,确保传入的是完整的调试名称而非之前的参数列表。
GUID
或 Name
的生成与传递方式标准化
- 每次调用
RecordDebugEvent
都必须传入一个有效的调试标识,可能是通过某种宏构造出来的 GUID。 - 所有原来依赖自动生成的地方都必须修改为手动传入,例如:
RecordDebugEvent(Thread, DebugEvent_FrameMarker, name);
这里的 name
必须在调用前就构造好,不再由内部自动拼接。
关于 counter
的进一步确认
- 之前结构中存在
counter
参数,但目前的系统中已不再使用它。 - 原因在于:早期版本中我们可能是手动插槽定位的,现在采用的是 原子自增(atomic add) 方式来分配事件槽。
- 因此,
counter
这个参数已经完全没有用途,可以清理出所有接口和调用中相关的部分。
事件插槽的分配方式已彻底更新
- 现在事件记录结构是通过原子操作自动申请槽位,无需调用者手动指定或维护计数器。
- 这不仅减少了出错概率,也提升了多线程场景下的稳定性。
结论与后续计划
- 全部事件记录接口必须显式传入调试名称,不再自动拼接。
- 清除所有关于
counter
的遗留逻辑,包括宏参数、函数参数和变量定义。 - 梳理所有使用
RecordDebugEvent
和相关宏的地方,确保调用格式符合新的规范。 - 核心目标是:简化、统一、可控,让事件记录系统更加明确、易维护、易扩展。
这一阶段工作核心是参数接口的规范化与遗留结构的清除,是为后续调试系统稳定运行打下基础。
清理 BEGIN_BLOCK(Name) 和 END_BLOCK(Name),只传递 DEBUG_NAME(Name)
我们对代码进行了大幅清理,主要目的是去除过去实验过程中遗留下来的、不再需要的冗余结构,确保系统简洁、明确、稳定。以下是详细总结:
代码中存在大量过时和无用结构
- 之前进行过多种实验性实现,积累了不少临时性或重复性的代码。
- 清理后发现这些结构完全不再需要,属于多余冗余内容,应当彻底移除。
开始和结束时间块的处理逻辑大幅简化
- 原本存在多个用于支持 begin/end block 的辅助结构。
- 现在发现其功能可以通过统一的接口和 debug name 机制简单实现。
- 因此,大部分原有复杂的支持逻辑都可以精简掉,仅保留核心功能。
当前的宏或函数只需简单传递调试名称即可
- 举例来说:
实际上只需要把名称传给BEGIN_BLOCK(debugName)
BeginBlock
宏或函数即可,不需要其他复杂逻辑。 - 同理,结束时间块、记录调试事件等函数也只需要传入一个统一的调试名称。
统一名称传递的好处
- 逻辑清晰,代码阅读和维护更加简单。
- 避免重复造轮子或冗余封装,每个事件都使用统一的名称传递格式。
- 所有调用点职责明确,只负责传入调试名称,不参与事件结构管理。
对结构简化成果的肯定
- 本次清理后,整体事件记录框架变得更纯粹、更易用。
- 再无复杂层层包装的 begin/end block 结构,事件记录功能更集中、更直接。
- 系统变得更加可靠,同时便于后期扩展与重构。
后续关注点
- 检查是否仍有旧接口调用未迁移到简化后的接口。
- 确保所有宏和函数都统一遵循传入调试名称即触发事件的模型。
- 可以考虑移除已经不再调用的辅助结构与多余文件。
这次清理不仅提升了结构的整洁度,更重要的是明确了调试事件系统的使用规范,真正实现了 高内聚、低耦合 的设计目标。
清理 timed_block 结构
我们现在进入了对时间块(time block)系统进一步精简的部分,并取得了一些关键优化成果,以下是详细总结:
不再存储 counter
是一个重要优化
- 之前的结构体中包含
counter
字段,现在已确认完全不需要。 - 移除后,该结构体不会占用栈空间,即使编译器优化不激进,也不会为其分配栈内存。
- 这使得调试系统更加轻量化,有利于在性能关键路径中使用。
初始化逻辑也进行了简化
- 以前时间块初始化时会传入多项参数,现在只保留最核心的
GUID
参数。 - 初始化只需要做一件事:调用
BeginBlock(GUID)
。 - 所有与之无关的辅助数据全部移除,保持逻辑最小化。
精简后的时间块结构如下:
- 开始块只需要传入一个调试标识(GUID),不再依赖其他复杂状态。
- 结束块无需新的标识,只需引用起始块的标识即可。
- 明确了:只有起始块需要唯一的调试名,结束块只作引用,不需额外标识。
“命名闭合块”作为清晰的标识辅助
- 结束块现在以一种明确方式命名,如
x_block
,以区分开始块。 - 这让整体语义更加直观,便于代码结构化阅读和维护。
关于 hit count 的 TODO 留存
- 曾考虑统计命中次数(hit count),但目前尚未实现。
- 暂时将 TODO 保留,未来可在系统完善后加入该功能。
- 命中统计可能在性能分析中有价值,因此需要在结束时阶段性评估其优先级。
当前设计的总体优势
- 逻辑极简:只需标记一次起始,结束只作闭合处理。
- 结构轻量:无冗余数据、无额外存储负担。
- 可维护性强:接口单一清晰,未来如需拓展也易于插入。
这一阶段的重构进一步压缩了调试系统的运行和存储开销,同时保持功能完整性,为后续性能分析和维护打下坚实基础。
清理 TIMED_BLOCK 和 TIMED_FUNCTION 宏
我们对 time_function
和相关宏定义的部分也进行了简化与重构,以下是详细的整理总结:
原有结构存在的复杂性和问题
- 原来用于时间记录的宏如
time_block
、time_function
非常繁琐,需要传入多个参数(如行号、文件名、代码块名、计数器等)。 - 实际使用时这些信息大多数并不必要,反而增加了维护和阅读成本。
- 其中有些字段(如 counter)甚至完全不再使用,属于遗留冗余。
宏的重构与清理逻辑
- 现在已有一个通用的处理函数,利用它可以简化绝大多数操作。
- 对于
time_block
或time_function
,我们只需创建一个调试名称(debug name),并传递给通用函数即可。 - 不再需要传入行号、文件名、块名等信息,全部移除。
仍需保留宏的唯一原因
- 如果希望支持同一位置多次调用的情况(例如同一函数中多次使用
time_block
),仍需在调试名后附加编号来区分。 - 因此宏不能完全消失,但已经极大简化,只保留为处理唯一编号而存在。
简化后操作方式示意
time_block("example")
→ 自动构造 debug name,并调用begin_block(debug_name)
。time_function()
→ 通过当前函数名构造 debug name,调用begin_block(debug_name)
。- 所有复杂拼接、宏参数注入(如
__LINE__
、__FILE__
)都被剔除。
优化后的好处
- 简洁易懂:新结构逻辑清晰,没有多余的信息传递。
- 可维护性强:去除了宏中大量不必要的信息拼装,更容易修改或替换。
- 通用性更高:统一使用
debug_name
接口进行记录,便于集中管理和扩展。 - 无需重复工作:不必再手动维护多个宏定义和命名逻辑。
当前状态总结
- 功能不变,依然支持时间记录与标记。
- 内部结构极大精简,移除了历史遗留代码和无用字段。
- 所有宏或函数的调用现在都转向通用的记录函数,形成单点控制入口,便于后续集中优化。
这一阶段的工作从结构、调用方式到命名策略进行了整体性重构,实现了从复杂混乱到精简统一的转变,为进一步的系统性能分析或工具化处理打下了良好基础。