# MRamTrace **Repository Path**: RT-Thread-Mirror/MRamTrace ## Basic Information - **Project Name**: MRamTrace - **Description**: No description available - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2026-08-29 - **Last Updated**: 2026-08-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # MRamTrace 轻量级 MCU 函数追踪与离线可视化工具。它使用 GCC/Clang `-finstrument-functions` 将函数进入/退出事件写入用户提供的 RAM 环形缓冲区,冻结后通过 UART、日志、文件、蓝牙或自定义后端导出,再结合 对应 ELF 生成 Perfetto、Speedscope、KCachegrind 和火焰图数据。 > Lightweight RAM-backed function tracing and offline visualization for > bare-metal and RTOS firmware. ## 为什么需要它 MRamTrace 面向无法长期连接调试器、没有 ETM/SWO,或者只能通过普通通信 链路取回现场数据的设备。采集阶段不依赖串口、文件系统、RTOS 或 Python; 目标侧只保存固定16字节事件,复杂分析全部在电脑端完成。 它可以回答: - 哪条函数调用路径消耗最多时间? - 某个函数由谁调用,又把时间花在了哪些子函数? - 两个 RTOS 任务的调用行为是否不同? - 某一时间点正在运行哪层调用栈? - 环形缓冲是否覆盖、传输是否丢块、ELF 是否匹配? ## 输出视图 | 输出 | 工具 | 主要用途 | |---|---|---| | `trace.chrome.json` | Perfetto | 精确时间线、任务切换视角、调用顺序 | | `trace.speedscope.json` | Speedscope | Time Order、Left Heavy、Sandwich | | `trace.callgrind` | KCachegrind | 调用关系图、Self/Inclusive cycles、调用次数 | | `trace.-.callgrind` | KCachegrind | 单独分析某个任务/上下文 | | `trace.folded` | FlameGraph | 标准折叠栈和差分火焰图输入 | | `trace.flamegraph.svg` | 浏览器 | 无依赖、可分享的静态火焰图 | | `trace.report.*` | 文本/JSON | 覆盖率、事件速率、异常与符号解析健康度 | ## 截图 ### KCachegrind 聚合视图同时展示函数的 Inclusive/Self cycles、调用次数、Callee Map 和 caller→callee 调用图,适合分析“成本如何沿调用链传递”。 ![KCachegrind aggregate call graph](docs/images/kcachegrind-call-graph.png)
查看 KCachegrind 成本、调用图与 Callee Map 动态演示 ![KCachegrind interactive call graph demo](docs/images/kcachegrind-demo.gif)
`workload_maintenance_check` 仅出现6次,这个视图展示了 KCachegrind 对低频分支的调用者、被调用者和边计数分析。
查看低频 maintenance 调用路径 ![KCachegrind rare maintenance path](docs/images/kcachegrind-rare-path.png)
### Speedscope Left Heavy 将相同调用路径合并,可快速看到主路径、分支权重, 并在 `worker-a` 和 `worker-b` 之间切换。 ![Speedscope Left Heavy](docs/images/speedscope-left-heavy.png)
查看 Sandwich 与 Time Order 视图 Sandwich 视图展示函数 Total/Self 排行,选中函数后同时展示它的 callers 和 callees: ![Speedscope Sandwich](docs/images/speedscope-sandwich.png) Time Order 保留真实进入/退出顺序,可放大到某一次具体调用: ![Speedscope Time Order](docs/images/speedscope-time-order.png)
### Perfetto Perfetto 按任务展示函数时间线,适合查看不同 RTOS 上下文的调用层级、 开始时间和持续时间。 ![Perfetto task timeline](docs/images/perfetto-timeline.png)
查看 Perfetto 缩放与时间线导航动态演示 ![Perfetto interactive timeline demo](docs/images/perfetto-demo.gif)
## 三种可视化工具怎么选 三个工具读取的是同一次采集产生的不同输出格式,底层事件和时间戳相同,区别 主要在观察视角,而不是数据精度。实际分析时通常先用 Perfetto 找到异常时段, 再用 Speedscope 判断主要调用栈,最后用 KCachegrind 深挖函数之间的成本传递。 ### Perfetto:看“什么时候发生了什么” Perfetto 保留事件的先后顺序,将每个任务或上下文展示为独立时间线。它最适合 观察某次具体调用的开始时间、持续时间、嵌套层级以及不同任务在同一时段的行为。 可以连续缩放到单次函数调用,但不擅长直接回答整个采集窗口内哪个调用路径累计 最热。使用网页版即可打开,无需安装桌面软件。 ### Speedscope:看“时间主要花在哪条栈上” Speedscope 兼顾时间顺序和聚合分析。Time Order 还原调用过程,Left Heavy 将 相同调用栈合并以突出主路径,Sandwich 则从选中函数同时查看 callers、callees、 Total 和 Self。它界面轻量、容易分享,适合快速定位热点和比较不同任务;但调用 边、调用次数和大型调用关系图不如 KCachegrind 完整。 ### KCachegrind:看“成本如何沿调用关系传播” KCachegrind 将整段采集聚合为调用图,重点展示 Inclusive/Self cycles、调用次数、 caller/callee 边和 Callee Map。它适合量化某个函数为何昂贵、成本来自哪个子函数, 也容易发现低频但单次代价很高的路径。代价是丢失单次事件的时间顺序,而且通常 需要在 Linux/WSL 图形环境中安装桌面程序。 | 工具 | 核心视角 | 时间顺序 | 聚合热点 | 调用关系与次数 | 多任务/上下文 | 最适合 | |---|---|:---:|:---:|:---:|:---:|---| | Perfetto | 时间线 | 强 | 弱 | 基础嵌套 | 强,同屏分轨 | 时序、延迟、某次具体调用 | | Speedscope | 调用栈/火焰图 | 强 | 强 | 中等,Sandwich 视图 | 强,可切换 profile | 快速找热点、比较主调用路径 | | KCachegrind | 聚合调用图 | 无 | 强 | 最强,含边和调用次数 | 可打开汇总或单任务文件 | 成本归因、调用关系、量化优化 | 如果只选一个工具:排查时序问题优先 Perfetto,做日常热点分析优先 Speedscope, 准备深入优化函数和调用关系时优先 KCachegrind。三者结合使用能覆盖“发生时间、 热点路径、成本来源”三个互补维度。 ## 架构 ```text __cyg_profile_func_enter/exit | v 16-byte trace event | v caller-owned RAM ring buffer | stop/freeze | v registered save sink callback UART / log / file / BLE / custom | v serial log + matching ELF | v Perfetto / Speedscope / KCachegrind / SVG ``` 核心没有堆分配、没有后台任务,保存过程同步执行。应用决定缓冲区来自静态数组、 链接段、FreeRTOS heap、RT-Thread heap 或其他分配器,也决定在哪个普通任务执行 慢速 I/O。 ## RAM 占用与性能影响 ### 缓冲区需要多大 Version 1 的每个事件固定占用 **16 字节**。一次被插桩函数的完整调用通常产生 一个 enter 和一个 exit,因此需要 **2 个事件,即 32 字节**。用户事件也各占 16 字节。传给 `ram_trace_init()` 的字节数会向下取整为完整事件数: ```text 事件容量 = floor(buffer_size / 16) 完整函数调用数 ≈ 事件容量 / 2 可保留时长(秒)≈ 事件容量 / event_rate ``` 下表中的保留时长按约 5,000 events/s 估算,接近本项目 STM32F407 示例的 实测 4,932.8 events/s。实际值取决于插桩范围和业务调用频率。 | 缓冲区 | 事件数(采样点) | 约等于完整函数调用数 | 约可保留时长 @ 5k events/s | |---:|---:|---:|---:| | 4 KiB | 256 | 128 | 51 ms | | 8 KiB | 512 | 256 | 102 ms | | 16 KiB | 1,024 | 512 | 205 ms | | 24 KiB | 1,536 | 768 | 307 ms | | 32 KiB | 2,048 | 1,024 | 410 ms | | 64 KiB | 4,096 | 2,048 | 819 ms | | 128 KiB | 8,192 | 4,096 | 1.64 s | STM32F407 示例配置为 24 KiB,200 ms 抓取实际生成 984 个事件,没有覆盖。 除用户缓冲区外,当前 ARM GCC 示例中核心状态、service 和 6 个 sink 注册槽约占 96 字节常驻 RAM(具体值随 ABI、编译器和配置变化)。导出阶段还会使用任务栈: 默认 12 events/block 时,编码数组共 448 字节;当前示例构建中 `ram_trace_export()` 自身栈帧约 552 字节,尚不包含 sink 驱动及其调用链。因此 应通过链接器 map/stack-usage 文件和 RTOS 栈高水位检查最终栈余量;示例的导出 任务使用 1,536 字节栈。 ### 如何选择和配置大小 第一次接入建议先分配 16–32 KiB,进行一次短时抓取。分析器会根据实测 `event rate` 和默认 1.25 安全系数,在 `trace.report.txt/json` 中自动给出 100、500、1000 ms 三档建议,也可以指定其他窗口: ```bash python3 tools/ram_trace/ram_trace_analyze.py serial.log \ --elf firmware.elf \ --recommend-window-ms 200 \ --buffer-safety-factor 1.5 ``` 计算方法为: ```text buffer_bytes >= event_rate × target_seconds × 16 × safety_factor ``` 建议 `safety_factor` 取 1.2–1.5,并将结果向上取整到 16 字节。RAM 紧张时,优先 排除高频叶子函数,而不是盲目扩大缓冲区。`RAM_TRACE_MODE_WRAP` 保留停止前最新的 一段数据,适合复现故障前现场;`RAM_TRACE_MODE_STOP_WHEN_FULL` 保留从启动开始的 完整前缀,适合启动过程分析。 使用事件数组配置最直观,并天然满足对齐要求: ```c #define TRACE_EVENT_CAPACITY 1536U /* 24 KiB */ static RamTraceEvent_t trace_events[TRACE_EVENT_CAPACITY]; config.buffer = trace_events; config.buffer_size = sizeof(trace_events); ``` 也可以从 RTOS heap 分配字节缓冲区: ```c #define TRACE_BUFFER_SIZE (24U * 1024U) void *trace_buffer = pvPortMalloc(TRACE_BUFFER_SIZE); config.buffer = trace_buffer; config.buffer_size = TRACE_BUFFER_SIZE; ``` 需要确保分配结果非空且至少按 `RamTraceEvent_t` 对齐;采集期间不得释放或复用。 ### 对运行性能的影响 性能影响不能用一个与平台无关的固定百分比描述。每个已启用事件会执行一次短 临界区、读取时间戳和上下文、顺序写入 16 字节并更新计数器;一次完整函数调用 会执行两次。采集过程中没有格式化、CRC、Base64 或 UART/BLE I/O,这些操作都在 停止采集后执行,不会污染已冻结的时间线。 可用下面的公式估算目标板 CPU 开销: ```text CPU 开销比例 ≈ event_rate × cycles_per_event / CPU_frequency 单次函数调用附加周期 ≈ 2 × cycles_per_event ``` ### STM32F407 实测结果 以下数据来自 LXB407ZG(STM32F407ZG,168 MHz)、ARM GCC 9.2.1、Release 构建。 Benchmark 每组执行 512 次调用、重复7轮并取中位数,测量区间关闭中断且没有 串口 I/O: | 测量项 | 实测周期 | 约合时间 @ 168 MHz | |---|---:|---:| | 无插桩 `plain_cycles_per_call` | 17.04 cycles/call | 101 ns/call | | 已插桩、未采集 `stopped_cycles_per_call` | 121.03 cycles/call | 720 ns/call | | 已插桩、正在采集 `active_cycles_per_call` | 400.98 cycles/call | 2.39 μs/call | | 事件写入增量 `capture_cycles_per_event` | **139.97 cycles/event** | **833 ns/event** | 同一次 FULL workload 抓取在 199.513 ms 内生成 984 个事件,即约 4,932 events/s,没有发生环形覆盖。按这个实际事件率估算: | 开销来源 | 当前 workload 的 CPU 占用 | |---|---:| | Hook 存在但采集关闭(stopped − plain) | 约 0.153% | | 采集期间写入事件(active − stopped) | 约 0.411% | | 综合软件追踪开销 | **约 0.564%** | Benchmark 同时报告 `slowdown_percent=2252.52%`,这是一个函数体仅约17周期的 极短测试函数从 17.04 增至 400.98 cycles/call 后的相对结果,**不代表整个系统 变慢22倍**。函数本身越耗时,固定插桩成本的相对占比越低;实际系统影响应使用 业务事件率按上述公式计算。该结果只代表此芯片、编译器、优化配置和插桩范围, 更换平台或构建参数后应重新测量。 STM32F407 示例默认启用 DWT 基准,在正式采集前分别测量无插桩、已插桩但未采集、 正在采集三种状态,并输出多轮测试的中位数: ```text RTRACE:BENCH iterations=512 trials=7 events=1024 plain_cycles_per_call=... stopped_cycles_per_call=... active_cycles_per_call=... capture_cycles_per_event=... slowdown_percent=... ``` 其中 `capture_cycles_per_event` 是 active 相对 stopped 的增量,避免把禁用状态下 仍然存在的函数 hook 成本混入事件写入成本。可以通过 CMake 参数 `-DRAM_TRACE_ENABLE_BENCHMARK=OFF` 关闭示例基准。其他目标应使用 DWT cycle counter 或硬件定时器做同类测量。分析器会识别 `RTRACE:BENCH`,并将这些实测值 写入 `trace.report.txt/json`。还应分别关注: - 插桩本身的函数进入/退出调用,即使 trace 未启动也存在少量开销; - 高频小函数的相对扰动会明显高于耗时较长的业务函数; - 临界区会短暂增加中断延迟,硬实时 ISR 应排除插桩; - `-fno-inline` 会额外改变性能和代码尺寸,仅建议演示调用层级时使用; - 缩小插桩范围通常比增加 RAM 更能同时降低性能扰动和事件速率。 ## 快速接入 ### 1. 加入构建 纯裸机或自定义 RTOS: ```cmake add_subdirectory(path/to/MRamTrace/ram_trace) target_link_libraries(your_firmware PRIVATE ram_trace) ``` FreeRTOS: ```cmake set(RAM_TRACE_WITH_FREERTOS ON CACHE BOOL "" FORCE) add_subdirectory(path/to/MRamTrace/ram_trace) target_link_libraries(your_firmware PRIVATE ram_trace) ``` 只给需要观察的应用源文件开启插桩: ```cmake set_source_files_properties(src/application.c PROPERTIES COMPILE_OPTIONS "-finstrument-functions;-fno-optimize-sibling-calls") ``` Trace核心、平台回调、导出代码、串口/文件/BLE驱动必须使用 `-fno-instrument-functions`。 ### 2. 初始化与采集 ```c RamTraceConfig_t config = { .buffer = trace_buffer, .buffer_size = sizeof(trace_buffer), .timestamp_frequency = CPU_CLOCK_HZ, .mode = RAM_TRACE_MODE_WRAP, .platform = platform_ops, }; ram_trace_service_init(&config); ram_trace_service_register_sink(&uart_sink); ram_trace_service_start(1U); /* reset then start */ /* application runs */ ram_trace_service_stop(); ram_trace_service_save("uart"); ``` 底层 `ram_trace_init/start/stop/export` API 仍可直接使用;service 只是一个轻量 统一入口。 ### 3. 注册保存方式 ```c static RamTraceResult_t log_write(void *context, const uint8_t *data, size_t length) { app_log_write(context, data, length); return RAM_TRACE_OK; } static const RamTraceSink_t log_sink = { .name = "log", .write = log_write, .context = NULL, }; ram_trace_service_register_sink(&log_sink); ram_trace_service_save("log"); ``` 文件后端可在 `begin` 中打开文件,在 `write` 中写入,在 `end` 中关闭。蓝牙后端 使用相同接口完成 MTU 分片或发送队列适配,模块本身不绑定具体文件系统和协议栈。 ### 4. Shell/命令调用 将 Shell、FreeRTOS CLI 或 AT 命令解析得到的 `argc/argv` 转发给 `ram_trace_command_execute()`: ```text trace status trace start trace stop trace reset trace sinks trace save uart trace save file trace.rtrace trace save ble ``` ## 生成分析结果 ```bash python3 tools/ram_trace/ram_trace_analyze.py serial.log \ --elf firmware.elf \ --output trace-output ``` 分析器仅使用 Python 标准库。符号解析需要 `arm-none-eabi-nm`,也可以在主机 测试 ELF 上使用普通 `nm`。日志既可以是纯文本,也可以是 WSL Serial Monitor 生成的 NDJSON。 打开结果: ```bash kcachegrind trace-output/trace.callgrind ``` - Speedscope:访问 ,拖入 `trace.speedscope.json`。 - Perfetto:访问 ,打开 `trace.chrome.json`。 - SVG:直接用浏览器打开 `trace.flamegraph.svg`。 必须使用与烧录固件严格匹配的 ELF。分析器会验证每个数据块和完整数据流的 CRC32,并报告事件序号缺口、环形覆盖、调用栈错配和未解析地址。 ## 示例 `examples/stm32f407-freertos/` 提供 STM32F407 + FreeRTOS 参考接入,包括: - DWT cycle counter 时间戳; - FreeRTOS task 上下文识别; - UART保存后端; - 两个工作任务; - 25个测试函数和最长6层调用路径。 该示例不复制 FreeRTOS、CMSIS、启动文件和链接脚本,请通过 CMake 参数指向 本机已有依赖,详见示例目录 README。 ## 本机测试 ```bash cmake -S . -B build -G Ninja cmake --build build ctest --test-dir build --output-on-failure ``` ## 当前限制 - Version 1 面向单核、32位、小端目标。 - 函数耗时是墙钟时间;任务被抢占的时间尚未从函数耗时扣除。 - 高频叶子函数可能快速填满缓冲区,应通过编译期排除列表控制范围。 - 环形覆盖后可能从半条调用栈开始,分析器会报告并丢弃不完整前缀。 - 保存是同步操作,必须在停止采集后由普通任务执行,不能在 ISR 中调用。 - 它是低成本软件追踪方案,不替代 ETM 等无侵入硬件追踪。 ## License [MIT](LICENSE)