Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
bd2fa7a
fix: report heap and render failures instead of faking success
IronsDu Sep 13, 2026
2280741
feat(web): add the missing state-based heap view to the panel
IronsDu Sep 13, 2026
e2cb610
feat(heap): make the collection window configurable and use it everyw…
IronsDu Sep 13, 2026
d6a4328
feat(heap): let the graph endpoints render the state-based snapshot too
IronsDu Sep 13, 2026
dd01bf9
fix(web): make the chart actions work and stop hiding empty renders
IronsDu Sep 13, 2026
20e6825
fix(heap): name the empty-snapshot case instead of reporting a render…
IronsDu Sep 13, 2026
381e610
fix(heap): make flame-graph rendering use the same view as the svg path
IronsDu Sep 14, 2026
9653f1d
fix(web): actually add fetchHeapChart, which I had reported as done
IronsDu Sep 14, 2026
7f9e737
fix: free the profile returned by GetHeapProfile in stopHeapProfiler
IronsDu Sep 14, 2026
a1f24b7
refactor(api)!: consolidate nine endpoints into four
IronsDu Sep 14, 2026
4f42a76
fix(web): serve SVGs as image/svg+xml and expose the inline mode in t…
IronsDu Sep 14, 2026
c363c46
docs: add the delivery-mode section the previous commit claimed to add
IronsDu Sep 14, 2026
373e577
feat(web): make the heap snapshot output a single dropdown
IronsDu Sep 14, 2026
684adbd
docs: resync with the API refactor, and clamp duration where the docs…
IronsDu Sep 15, 2026
8c9ac86
fix(tests): make the suite parallel-safe, which is how CI breaks it
IronsDu Sep 15, 2026
0e3deee
docs: fix the feature-list claim and two contradictions it hid
IronsDu Sep 15, 2026
660ac77
refactor: drop the unused getThreadStacks(), and name threads in the …
IronsDu Sep 15, 2026
f662092
feat(example): name the sample threads, and fix a filename collision …
IronsDu Sep 15, 2026
8f6efdd
fix: stop freeing the GetHeapProfile buffer, which was aborting ASan
IronsDu Sep 15, 2026
40e9a74
refactor: dump heap profiles to disk instead of taking GetHeapProfile…
IronsDu Sep 15, 2026
801b45d
docs: drop two claims the code no longer supports, and stop citing li…
IronsDu Sep 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 133 additions & 8 deletions CHANGELOG.md

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,27 @@ if(REMOTE_PROFILER_BUILD_TESTS)
)
add_test(NAME AsyncExecutorTest COMMAND test_async_executor)

# Return-value parsing shared by the HTTP layer (internal, no Drogon needed)
add_executable(test_result_parsing tests/test_result_parsing.cpp)
target_link_libraries(test_result_parsing
profiler_core
GTest::gtest
GTest::gtest_main
pthread
)
add_test(NAME ResultParsingTest COMMAND test_result_parsing)

# Thread stack capture (core only -- works without Drogon)
add_executable(test_thread_stacks tests/test_thread_stacks.cpp)
target_link_libraries(test_thread_stacks
PRIVATE
profiler_core
GTest::gtest
GTest::gtest_main
pthread
)
add_test(NAME ThreadStacksTest COMMAND test_thread_stacks)

message(STATUS "Tests will be built")
else()
message(STATUS "Tests disabled")
Expand Down
26 changes: 26 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,32 @@ cd build
ctest -R <test_name> --output-on-failure
```

### 提交前的附加检查

`ctest` 通过**不足以**覆盖两类问题,仓库里有两个脚本专门补这个缺口:

```bash
# 1) Sanitizer:本地复刻 CI 的 ASan/UBSan/TSan 配置
scripts/check-sanitizers.sh asan # 默认只跑 ASan
scripts/check-sanitizers.sh all # ASan + UBSan + TSan

# 2) Web UI:用无头 Chrome 驱动真实控制面板,断言每个动作都发出正确的请求
# 并真正落盘(需要先启动 profiler_example,以及 puppeteer-core)
scripts/verify-web-ui.mjs http://localhost:8080
```

为什么需要它们:

- **Sanitizer**:`ctest` 默认构建不会报内存泄漏与未定义行为。曾经有一个
`stopHeapProfiler()` 的 LeakSanitizer 报告只被 CI 发现,因为本地从没跑过
`-fsanitize=address`。
- **Web UI**:`curl` **无法执行页面 JavaScript**,因此看不到"按钮点击报
ReferenceError""按钮卡在 disabled""下载其实没开始"这类问题。前端改动必须用
真实浏览器验证后才算完成。

> 改前端(`src/web_resources.cpp` 里的 HTML/JS 字符串)时尤其注意:那段 JS 是
> C++ 字符串字面量,**编译器不会检查它**——函数名写错、参数漏传都能编译通过。

## 需要帮助?

如果你有任何问题,可以:
Expand Down
222 changes: 149 additions & 73 deletions README.md

Large diffs are not rendered by default.

68 changes: 61 additions & 7 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
### 已修复

<details>
<summary>1. `/api/heap/analyze` 的 duration 语义(✅ 已修复)</summary>
<summary>1. `/api/heap/analyze` 的 duration 语义(✅ 已修复;该路径现已并入 `/api/pprof/heap`)</summary>

原问题:路由只解析 `output_type`,`handleHeapAnalyze()` 也没有 duration 形参,最终硬编码
`analyzeHeapProfile(1, ...)`。
Expand Down Expand Up @@ -68,14 +68,68 @@

</details>

### 已修复(本轮,用 go tool pprof 验证)

- `/pprof/heap` 在采样关闭时返回 200 + `%warn` 文本:`GetHeapSample()` 不返回空串,导致空值检查失效。现按 `@ heap_v2/0` / 前导 `%warn` 识别,返回 500。
- `/api/heap/svg_raw` 与 `flamegraph_raw` 的行为不一致:根因同上,两者现在都返回同样的明确错误(后经 API 重构合并为 `/api/pprof/heap`)。
- 分析类接口的错误响应**双重包裹**成非法 JSON(`{"error":"{"error": "..."}`):现将内部错误消息解出后再包一层。
- `/pprof/heap` 与 `/pprof/growth` 的错误改用 Go `serveError()` 的形状(`text/plain` + `X-Go-Pprof: 1`),`go tool pprof` 能直接显示原因。
- `flamegraph.pl` 对空输入返回"合法 SVG + ERROR 文本",被当作 200 成功:现识别并转为带原因的 500。

### 已完成(本轮 API 重构)

- HTTP API 收敛:9 个端点 → 4 个(`/api/pprof/{cpu,heap,growth}` + `/api/pprof/heap/snapshot`),
渲染器与交付方式改为参数(`renderer` / `output`),消除了 `analyze` 与 `*_raw` 各采样一次的重复。
- `duration` 默认值 1 秒 → 10 秒(实测 3 秒失败率 35–45%、10 秒 0%,见 CHANGELOG)。
- 移除 3 个查看器页及其内联 SVG 分支——其页内缩放从未生效(生成的 SVG 内嵌了从未被初始化的
pan/zoom 库)。面板改为「下载 + 实时日志」。
- 新增 `scripts/verify-web-ui.mjs`(CDP 驱动真实浏览器回归,23 项检查)与
`scripts/check-sanitizers.sh`(本地复刻 CI 的 ASan/UBSan/TSan 配置)。

### 已完成(CI 稳定性)

- 测试可在并行下运行(`ctest -j`)。此前 `pprof_cpu_temp.prof`、`cpu_collapsed.prof` 等
中间产物用**固定文件名**放在共享目录,两个测试进程会互相截断文件——CI 的
`FullFlowTest` 读到 0 字节 profile 即源于此。现按 PID 命名。
- `FullFlowTest.GetRawCPUProfile` 在采样窗口内**真的跑 CPU 负载**,而不是 sleep。
gperftools 只采样正在执行的线程,窗口内空闲时返回空 profile 是**设计如此**,
原断言把这种合法结果当成失败。
- `scripts/check-sanitizers.sh` 支持 `CTEST_JOBS=<n>` 以复现并行场景。

### 待修

- **不要用 `GetHeapProfile()` 取窗口式 profile**(已通过绕开解决,此处记录原因):
它返回的缓冲区头文件说调用方应 `free()`、上游测试也这么做,但没有可移植的释放方式——
静态链接 tcmalloc 时缓冲区来自库自身分配器,进程的 malloc 拦截器(ASan)不认识它,
`free()` 会 abort;`tc_free()` 仅 gperftools ≥ 2.16.90 存在;2.18 的中间 chunk 还走内部
arena,其 `Free` 在私有头文件里。
现在两条窗口式路径(`stopHeapProfiler()`、`getRawHeapProfileSample()`)都改为
**`HeapProfilerDump()` 落盘 + 读回**,内存归我们自己,无泄漏也无需抑制规则。
若将来 gperftools 提供公开释放函数,可再评估直接取缓冲区。
- **`GetHeapSample(string*)` 不能替代窗口式取数**(实测记录,避免误改):它是 **pull 统计抽样**,
与 `HeapProfilerStart/Stop` 的窗口记录是两套机制。实测同一时刻:窗口 dump 报
`202: 13121057`(1312 万字节,即实际分配量),而 `TCMALLOC_SAMPLE_PARAMETER=524288` 下
`GetHeapSample` 只报 `17: 1114112`(111 万),无该变量时干脆返回 `%warn`。若替换会**静默少报约 12 倍**。
状态式快照 `/api/pprof/heap/snapshot` 用它是对的,窗口式不能用。

- **生成的 SVG 无法缩放**:FlameGraph 的 `zoom()` 找不到它要操作的 `#viewport`(其产物只有
`#frames`),pprof 的 SVGPan 库则从未被初始化(产物缺少 `onload` 挂钩)。两条路径的交互脚本
都是死代码。可选方案:改为在服务端生成自己的交互式 SVG,或在文档中明确"下载后用桌面工具看"。

- **火焰图里的函数名带地址后缀**:内置 pprof 的 `--collapsed` 输出把函数名写成
`cpuIntensiveTask()<0000000000409360>`,经 `flamegraph.pl` 渲染后直接显示在图上。除了噪音,还有一个
理论隐患:同一函数出现在不同地址时(模板实例化)会被当成**不同栈帧**,本该合并的被拆开。
可在送入 `flamegraph.pl` 之前剥掉 `<十六进制>` 后缀。
- **部分符号未 demangle**:实测火焰图里残留 `_ZSt12construct_at...` 之类的 mangled 名,且这类帧常显示为
无上下文的 `operator()[inline]`(lambda 的 `operator()`)。可对未解开的符号做二次 `__cxa_demangle`。
- **两个 pprof 实现的差异未写进文档**:内置 Perl pprof(`renderer=callgraph` 的渲染器)符号化更完整——
实测同一份 profile 未解析帧为 0、且能标出 `(inline)`;而 `go tool pprof` 强在对比与交互
(`-diff_base`、`-peek`、`-traces`)。README 目前只说了"两种渲染方式",没说各自适合什么场景。
- **`pprof --svg` 的失败信息不透出**:内置 pprof 脚本用 `dot`(graphviz) 渲染,采样点过少时 `dot` 失败,代码只回一句 `pprof did not generate valid SVG. Output: `(且 `svg_output` 为空),无法定位原因。应把 `dot` 的 stderr 一并返回。**这是异步改造期间实测复现的既有缺陷**,与请求调度无关。
- **`stopHeapProfiler()` 的 `output_path` 语义**:它把 `GetHeapProfile()` 的返回值写进 `output_path` 文件,而 `.heap` 是 gperftools 自己按 prefix 写的,两套产物并存容易混淆。
- **`/pprof/heap` 在采样关闭时返回 200,响应体却不是 profile**:`GetHeapSample()` 在 `TCMALLOC_SAMPLE_PARAMETER` 未设置(默认 0)时**不返回空串**,而是返回 pprof 的 `%warn` 警告文本加一行零样本统计(`heap profile: 0: 0 [0: 0] @ heap_v2/0`)。因此 `if (heap_sample.empty())` 这个错误分支进不去,客户端收到 200 + 看起来像 profile 的东西。应识别 `@ heap_v2/0` 或首行 `%warn` 并返回明确错误。
- **`/api/heap/svg_raw` 与 `/api/heap/flamegraph_raw` 在无 `TCMALLOC_SAMPLE_PARAMETER` 时表现不一致**:两者**都**先调 `getRawHeapSample()`(源码已核对),拿到的都是 `%warn` + 零样本文本,但因为上面那条缺陷没被拦下,后续命令的容错差异决定成败——`pprof --svg`(`svg_raw`)对零样本 profile 失败并超出 3 秒超时,返回 500 `Failed to generate SVG`;`pprof --collapsed` + `flamegraph.pl`(`flamegraph_raw`)却渲染出 13KB 的 SVG,返回 200。实测复现。修掉上面那条(识别零样本)即可同时解决。
- **`/tmp/cpp_profiler` 下的 `.heap` 快照不再清理**:每次 `analyzeHeapProfile` 用唯一前缀(时间戳 + 序号)产生一个新文件,长期运行会累积。
- **`analyzeHeapProfile` 的采样窗口固定 1 秒且不可配置**:分配稀疏的进程在窗口内可能一次分配都没有,只能得到空结果。可考虑加一个可选的窗口参数,或复用 `startHeapProfiler()`/`stopHeapProfiler()` 让调用方掌控。
- ~~**`stopHeapProfiler()` 的 `output_path` 语义**~~(**已解决**):两者不再并存——`HeapProfilerDump()` 写出的 `.heap` 只是**中间产物**,其内容被读出后写入 `output_path`,调用方拿到的仍是它请求的路径。中间文件保留在磁盘上(可作排查用),语义不再含混。
- **窗口式 heap 端点缺少"采样率"调节**:`duration` 控制的是**采集多久**(覆盖度),采样率仍由
`TCMALLOC_SAMPLE_PARAMETER` 在启动时固定。若调用方希望在运行期调整**精度**,目前没有途径
(`HEAP_PROFILE_ALLOCATION_INTERVAL` 只在 `HeapProfilerStart` 前设环境变量才生效)。

### 已完成的调度改造(供参考)

Expand All @@ -84,7 +138,7 @@
- `include/profiler/async_executor.h` — 单工作线程执行器,把阻塞任务搬离事件循环,完成后经 `queueInLoop()` 回送响应
- 阻塞接口全部投递到该工作线程;`/api/status`、`/` 等快速接口保持在事件循环上
- **CPU 采样独占且 fail-fast**:`cpu_profiling_in_progress_` 是所有 CPU 采样入口(`getRawCPUProfile` / `analyzeCPUProfile`)共用的进程级标记。占用期间新请求**立即被拒绝**,不会排队、也不会打断正在进行的采样
- 拒绝语义与 Go 的 `net/http/pprof` 对齐:`/pprof/profile` 返回 500 + `text/plain` + `X-Go-Pprof: 1`;`/api/cpu/*` 返回 409 JSON
- 拒绝语义与 Go 的 `net/http/pprof` 对齐:`/pprof/profile` 返回 500 + `text/plain` + `X-Go-Pprof: 1`;`/api/pprof/cpu` 返回 409 JSON
- 拒绝判定发生在**事件循环线程上、任务入队之前**(`runAsync` 的 precheck)。这是必须的:若放在任务内部判断,只能在前一个任务结束后才执行,永远观察不到它正在运行

实测:20 秒采样期间 `/api/status` 响应 0.0004s;并发请求第二个 0.0005s 内被拒且第一个不受影响。
Expand Down
17 changes: 13 additions & 4 deletions docs/mainpage.dox
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,20 @@
* The canonical, always-current endpoint list lives in the project README under
* "API 端点" (README.md). Summary:
*
* - Standard pprof: @c /pprof/profile, @c /pprof/heap, @c /pprof/growth, @c /pprof/symbol
* - One-click analysis (SVG): @c /api/{cpu,heap,growth}/analyze
* - Raw SVG download: @c /api/{cpu,heap,growth}/svg_raw and @c .../flamegraph_raw
* - Standard pprof (machine-readable): @c /pprof/profile, @c /pprof/heap,
* @c /pprof/growth, @c /pprof/symbol
* - Analysis charts: @c /api/pprof/{cpu,heap,growth} with @c renderer=flamegraph
* (stacked frames) or @c renderer=callgraph (graphviz node/edge graph), plus
* @c duration and @c output=inline|attachment
* - Heap snapshot (state-based, "what is in the heap now"):
* @c /api/pprof/heap/snapshot with @c format=profile|svg
* - Thread stacks: @c /api/thread/stacks
* - Panel and viewers: @c /, @c /show_svg.html, @c /show_heap_svg.html, @c /show_growth_svg.html
* - Control panel: @c /
*
* There are no separate viewer pages; charts are delivered as SVG by the
* analysis endpoints themselves. Note that the generated SVGs cannot be zoomed
* in the browser (the embedded pan/zoom scripts depend on elements that the
* output does not contain), so scaling requires a desktop tool.
*
* @section runtime_dependencies Runtime Dependencies
*
Expand Down
3 changes: 2 additions & 1 deletion docs/user_guide/01_quick_start.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,8 @@ int main() {
profiler::ProfilerHttpHandlers handlers(profiler);

// 调用任意 handler,获得框架无关的响应
profiler::HandlerResponse resp = handlers.handleCpuAnalyze(10, "flamegraph");
profiler::ChartOptions options; // renderer / duration / inline_display
profiler::HandlerResponse resp = handlers.handleCpuChart(options);

// resp.status, resp.content_type, resp.body
// 用你自己的 Web 框架包装这些数据
Expand Down
Loading
Loading