火焰图是将采样调用栈按路径合并、排序、缩放后渲染的svg,核心在于stackcollapse对带权重样本的路径折叠;pprof原始数据不聚合,需按完整栈帧序列(分隔符为";")分组计数;sample.value含义依profile类型而异:cpu为采样次数,heap为分配字节数。

火焰图不是“画出来的”,而是把一堆采样得到的调用栈,按路径合并、排序、缩放后渲染成 SVG —— 你不需要会 Perl 或 D3,但必须理解 stackcollapse 做了什么,否则改了代码却看不出火焰图变化,纯靠猜。
为什么 go tool pprof 导出的文本里有重复调用栈?
pprof 的原始 profile 数据本质是一组带权重的调用栈样本。比如连续采样 10 次,得到:
main.main
http.(*ServeMux).ServeHTTP
myhandler
json.Marshal
出现 7 次;另一条路径出现 3 次。pprof 不会自动合并它们 —— 合并是后续工具(如 flamegraph.pl)做的事。你看到的“宽条”,就是这些相同路径的样本数累加结果。
这意味着:
-
go tool pprof -raw输出的是未聚合的原始栈列表,每行一个栈,末尾带采样次数 -
go tool pprof -svg内部调用了stackcollapse-perf.pl类脚本,做了路径折叠(fold) - 如果你手动解析 profile,必须自己实现“按栈帧序列分组 + 计数”,不能只按函数名统计
如何用 Go 手动复现 stackcollapse 的核心逻辑?
关键不是写全功能,而是抓住两点:路径标准化 + 栈帧顺序敏感聚合。下面这个小片段能帮你验证理解是否到位:
samples := [][]string{
{"main.main", "http.ServeHTTP", "json.Marshal"},
{"main.main", "http.ServeHTTP", "json.Marshal"},
{"main.main", "http.ServeHTTP", "strings.ReplaceAll"},
}
// 按完整路径字符串聚合,不是单个函数
pathMap := make(map[string]int)
for _, s := range samples {
path := strings.Join(s, ";")
pathMap[path]++
}
// 输出:main.main;http.ServeHTTP;json.Marshal → 2
注意:; 是标准分隔符,go-torch 和 FlameGraph 脚本都依赖它;顺序错一位(比如把 json.Marshal 和 strings.ReplaceAll 位置互换),就算不同路径。
常见坑:
- 忽略空栈帧(如
runtime.goexit)导致路径截断 - 没过滤掉
runtime.开头的系统帧,让火焰图底部堆满无关调用 - 把
main.func1和main.func2当成同一函数 —— 它们是不同匿名函数,必须保留完整名
pprof.Profile 中的 Sample.Value 是什么?
在 Go 的 pprof.Profile 结构里,每个 Sample 的 Value 字段不是时间戳,而是“该样本的权重”。对 CPU profile 来说,它通常是 1(每次采样计 1);但对 memory profile,Value[0] 可能是分配字节数,Value[1] 是分配次数。
所以聚合时不能简单 sum(Value),得看 profile.Type:
-
profile.CPUProfile→ 直接累加Value[0](即采样次数) -
profile.HeapProfile→Value[0]是分配总字节数,要按路径求和而非计数 - 你用
runtime/pprof.WriteHeapProfile得到的数据,Value含义和net/http/pprof返回的不一样
这也是为什么直接拿 pprof.Profile.Samples 去画图,不区分 type 就容易宽度失真。
真正卡住人的,从来不是怎么生成 SVG,而是搞不清哪一行栈帧该被合并、哪一行该被丢弃、Value 到底代表什么。把这三件事在脑子里跑通一遍,再看火焰图,就不是“看图找宽条”,而是“看图反推采样路径”。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











