moonflame

    Render folded stack profiles into interactive SVG flame graphs.

    flamegraph
    profiling
    visualization
    svg
    Download zip
    Author
    Version
    0.1.0
    License
    MIT
    Last updated
    6 hours ago
    Downloads
    2

    Dependencies

    #MoonFlame

    用纯 MoonBit 把「折叠栈」性能数据渲染成一张可交互的 SVG 火焰图。

    不依赖 Go、Node 或任何运行时——产物是单个文件,双击即看,可直接贴进 README。

    示例火焰图

    上图由 moon run cmd/main -- testdata/demo.folded --out flame.svg 生成,输入是真实采样数据。 想体验点击缩放、Ctrl-F 搜索等交互,请下载 examples/flame.svg 用浏览器打开 (内嵌脚本在 <img> 引用下不会执行,原因见已知限制)。


    #目录


    #这是什么

    一个渲染器。输入折叠栈(folded stack)文本,输出一张能直接看的 SVG。

    折叠栈文本 ──▶ MoonFlame ──▶ 单个 SVG 文件

    折叠栈是 Brendan Gregg 定义的一种通用中间格式,每行一个调用栈加它的耗时:

    ____moonbit__main;moonflame::demo::bench__sorting;bubble__sort;array::Array::at 73382500

    perf、py-spy、async-profiler、moon-pprof 等工具都能产出它。所以 MoonFlame 不绑定任何语言或平台。

    它不做采样——那是上游工具的职责。它只负责把这些数据变成一张人一眼能看懂的图。

    现有的可视化方案(go tool pprof、speedscope、Firefox Profiler)都需要额外运行时或浏览器会话; MoonFlame 补的是「单文件、零环境、可归档」这一空缺。

    #主要功能

    功能说明
    折叠栈解析容忍超深栈(实测 7254 层)、超长单行(181K 字符)、空帧名等各种真实数据里的脏形状
    调用树聚合同父同名合并累加;计算自身耗时与累计耗时
    限深合并超过 --max-depth 的部分合并为 (deeper) 合成帧,而不是丢弃——否则深层耗时会被错算到最后一个可见帧上
    布局火焰图(根在底部)/ 冰柱图两种方向;按值比例切分,父宽严格等于子宽之和
    可交互 SVG经典火焰图配色 + 内嵌脚本:悬停信息栏、点击缩放、Reset Zoom、Ctrl-F 正则搜索、Ctrl-I 大小写开关、命中占比
    差异火焰图对比两份剖面,红 = 变多、蓝 = 变少、白 = 无变化
    文本报告hotspots / callers / callees / delta 四个子命令,回答「谁最慢」「谁调用它」「它调用谁」「这次改动让什么变慢了」
    确定性输出同一输入必得逐字节相同的 SVG,可做快照测试与版本间对比
    零依赖核心库根包纯计算、无任何外部依赖;文件 IO 只出现在命令行入口

    配色与交互行为对齐经典实现 flamegraph.pl,但对齐的只是功能规格—— 其源码采用 CDDL-1.0(文件级弱 copyleft,不是宽松许可),为避免任何许可证混用问题, 其源码一行未复制。详见 参考与许可。

    #快速验证

    只想看看效果的话,不需要安装任何采样工具——仓库里已带一份真实采样数据:

    moon run cmd/main -- testdata/demo.folded --out flame.svg

    然后用浏览器打开 flame.svg。想看文本热点、不出图:

    moon run cmd/main -- hotspots testdata/demo.folded --top 10

    仓库还带了两个用例,可以直接跑:

    文件用途
    testdata/demo.folded⭐ 主样例:32 栈 / 深度 3–13 / 1893 ms
    testdata/stress-recursive.folded极限用例:79 栈 / 最深 7254 层 / 5.5 MB,用来验证限深与合并

    #完整流程

    想拿它分析自己的程序,走这条四步链路。以 MoonBit 程序为例:

    ① 安装采样器(仅采集端需要,渲染器不需要)

    需要 mizchi/moon-pprof,它依赖 Rust 工具链,安装方式见该仓库说明。

    ② 把程序编译成 wasm-gc

    moon build --target wasm-gc

    必须用 wasm-gc:上游采样器基于 wasmtime,加载不了原生可执行文件。

    ③ 采样并转成折叠栈

    moon-pprof profile --wasm-gc _build/wasm-gc/debug/build/cmd/main/main.wasm --iterations 3 --out app.pb.gz moon-pprof pprof2folded app.pb.gz app.folded

    ④ 出图

    moon run cmd/main -- app.folded --out flame.svg

    demo/ 目录里有一个可直接照抄的完整例子(四点负载 + 一键脚本):

    powershell demo/reproduce.ps1 # 编译 → 采样 → 转折叠栈 → 打印热点

    采样有随机性:重跑会得到不同的栈数与总耗时(实测行数 32–36、总耗时 1887–1917 ms),但热点排序稳定。 折叠栈里一行 = 一个不同的调用栈,重复出现的栈会被合并、权重累加,所以文件 32 行而原始样本有 490 个。

    #命令行

    默认命令只负责出一张图;一切文本报告都是子命令——这样顶层选项不会随功能增长而膨胀。

    moon run cmd/main -- <input> [options] 出图(默认) moon run cmd/main -- hotspots <input> [--top N] 热点榜 moon run cmd/main -- callers <input> <name> 谁调用了它 moon run cmd/main -- callees <input> <name> 它调用了谁 moon run cmd/main -- diff <before> <after> [-o out] 差异火焰图 moon run cmd/main -- delta <before> <after> [--top N] 差异文本排行 moon run cmd/main -- filter <pattern> <input> [-o out] 栈过滤

    顶层选项(默认命令):

    参数默认说明
    <input>必填折叠栈文件
    -o, --out <path>标准输出输出路径;不传就打到标准输出
    --max-depth <n>32每个栈最多保留的帧数,0 表示不限制
    --width <px>1400画布宽度
    --unit <u>ns权重单位:ns / us / ms / s / count
    --inverted关输出冰柱方向(根在顶部);默认是火焰图方向(根在底部)

    为什么需要 --unit? 折叠栈的第 2 列是不透明的权重:moon-pprof 给纳秒, perf / py-spy 给采样计数。数值本身区分不出来(42 既可能是 42 纳秒也可能是 42 次采样), 所以由你声明。时间会按数量级自动缩放(871808200 → 871.81 ms),计数则加千位分隔、不带后缀。

    #文本报告

    moon run cmd/main -- hotspots testdata/demo.folded --top 5 moon run cmd/main -- callers testdata/demo.folded walk_tree moon run cmd/main -- callees testdata/demo.folded build_strings moon run cmd/main -- delta testdata/demo-baseline.folded testdata/demo.folded

    callers / callees 按直接调用者聚合,而不是把每条完整调用链列一行——递归函数会产生大量 几乎相同的长链,全列出来反而看不出「到底是谁在调用它」。查询名会先做归一化再按子串匹配, 所以 walk_tree、walk__tree、demo::walk 都能命中。

    delta 与差异火焰图互补:图看结构(哪条路径变宽了),表看函数(哪个函数的自身耗时变了多少)。 只在其中一侧出现的函数也会列出(另一侧记 0),因为「新增的开销」和「消失的开销」恰恰是差异分析最关心的。

    filter 是经典的 grep funcA input | flamegraph.pl 用法,但省掉了管道—— 输出仍是折叠栈格式,可以直接再喂给出图命令:

    moon run cmd/main -- filter build_strings testdata/demo.folded -o sub.folded moon run cmd/main -- sub.folded --out sub.svg

    权重要原样保留、不做归一化:过滤后的图回答的是「这个子系统内部怎么分配时间」, 而它占全局多少,靠保留原始权重才能和原图对照。

    #名称归一化

    上游的符号还原并不完整,真实数据里几种形态并存。渲染层会自动归一化(解析层始终原样保留,数据不会被改写):

    数据里的样子显示为
    moonflame::demo::build__stringsbuild_strings
    moonflame4demo13run__workloadmoonflame::demo::run_workload
    array5Array3setarray::Array::set
    ____moonbit__main不变(下划线开头不做折叠)

    长度前缀的还原是自校验的:每段声明的长度必须与实际字符数完全吻合,否则放弃。 sha256_finalize、utf16le、base64encode 这类名字不会被误改。 工具提示里会附上原名,信息不丢失。

    #交互与配色

    用浏览器直接打开生成的 SVG:

    操作效果
    悬停底部信息栏显示该帧的完整名字、耗时与占比
    单击某帧以它为根缩放展开;祖先帧变半透明,无关帧隐藏
    Reset Zoom复原到全图
    Ctrl-F / Search正则搜索,命中帧高亮为品红,右下角显示命中占比
    Ctrl-I / ic切换搜索是否区分大小写

    配色沿用经典火焰图的暖色调色板(深红 → 橙 → 黄的单维渐变),同名帧同色,便于跨图追踪同一个函数。

    #差异火焰图

    对比优化前后(或升级前后)两份剖面,把差异直接画进颜色里:

    moon run cmd/main -- diff testdata/demo-baseline.folded testdata/demo.folded -o flame-diff.svg

    差异火焰图

    视觉通道含义
    宽度按当前(第二个)剖面 —— 看清现在的时间花在哪
    🔴 红该帧变多了(劣化),越红变化越大
    🔵 蓝该帧变少了(改善)
    ⚪ 白变化可以忽略(注意不是灰色——灰字在浅色底上读不出来)
    悬停额外显示带符号的变化量

    颜色用全图最大的变化量做归一化——不归一化的话,只要有一处剧变,其它变化在颜色上就会全糊成一片。

    ⚠️ testdata/demo-baseline.folded 是构造的基线,仅用于演示与测试差异模式,不是真实采样。 构造规则只有两条,可逐行核对:含 bubble__sort 的行权重 ×3、含 build__strings 的行权重 ×0.8 (向下取整),其余行不变。真实基线应当来自优化前的那次采样。

    #已知限制

    1. 上游采样仅支持 wasm / wasm-gc 目标,native CPU 采样不可用;
    2. moon-pprof 需要 Rust 工具链(仅采集端);核心库零依赖,只有命令行入口依赖官方包 moonbitlang/x 做文件读写;
    3. 递归程序会产生极深栈(实测最深 7254 层),超过深度上限的部分合并显示为 (deeper);
    4. 上游采样分辨率取决于函数调用频率,而非运行时长(实测:调用密集约 600 样本/秒,循环密集约 42 样本/秒,相差 14 倍)。因此短于约 25 ms 的函数可能采不到,且延长运行时间不会提高统计质量;
    5. 内嵌交互脚本只在直接打开 SVG 或内联嵌入时执行;用 <img> 引用(GitHub 渲染本 README 即是如此)时浏览器不会运行 SVG 内的脚本,只显示静态外观;
    6. 帧的横轴按耗时降序排列(经典实现按名字字母序),这是为了让宽帧聚集在左侧、更易读的刻意选择;
    7. 图中会出现 (self) 与 (deeper) 两个合成帧:前者是该函数的自身耗时,后者是超过深度上限被合并的更深帧。经典实现没有这两个节点,显式画出它们是为了保证「父矩形宽度 = 子矩形宽度之和」——否则图面上会出现空洞。

    #目录结构

    . ├── LICENSE MIT ├── moon.mod / moon.pkg 模块与包配置 ├── pkg.generated.mbti 公开接口(由 moon info 生成,须与源码同步) │ ├── folded.mbt 折叠栈解析 ├── calltree.mbt 调用树聚合 / 限深合并 / 差异标注 ├── unit.mbt 权重单位与格式化 ├── names.mbt 符号名归一化 ├── hotspot.mbt Top-N 热点报告 ├── report.mbt 文本报告(调用关系 / 差异排行 / 栈过滤) ├── layout.mbt 矩形布局(火焰图 / 冰柱) ├── svg.mbt SVG 渲染(经典配色 + 内嵌交互脚本) ├── *_test.mbt 核心逻辑的测试(166 个用例) │ ├── cli/ 命令行参数解析(独立包,可脱离文件系统测试) ├── cmd/main/ 入口:读文件 → 调用核心库 → 写文件 │ ├── .github/workflows/ci.yml 持续集成(三平台 × 四后端 + 端到端出图) ├── demo/ 演示负载(独立模块) │ ├── demo.mbt 四点负载:排序 / 字符串 / 矩阵 / 递归 │ └── reproduce.ps1 一键复现:编译 → 采样 → 转折叠栈 ├── examples/ 示例图(SVG 可交互,PNG 供 README 展示) └── testdata/ 冻结的样例数据(主样例 + 极限用例 + 上游样例)

    分层原则:根包只做纯计算、零外部依赖;参数解析单独成包以便脱离文件系统测试; 文件 IO 只出现在入口包——核心逻辑因此可以在没有文件系统的环境(如 wasm)里复用。

    #开发

    moon check # 类型检查 moon test # 全部测试(122 个) moon info && moon fmt # 提交前更新接口并格式化 moon run cmd/main -- --help

    #参考与许可

    本项目为原创项目,未复制任何第三方源码。

    • 火焰图(Flame Graph)的概念与折叠栈格式由 Brendan Gregg 提出;
    • 渲染外观与交互行为对齐 brendangregg/FlameGraph 的 flamegraph.pl:暖色调色板的数值公式、差异配色规则、以及悬停 / 点击缩放 / Ctrl-F 搜索的交互语义均与之兼容。 ⚠️ 该项目采用 CDDL-1.0——一种文件级弱 copyleft,不是宽松许可(它与 GPL 明确不兼容)。 为避免任何许可证混用问题,本项目其源码一行未复制,只对齐功能规格,MoonBit 与 JavaScript 实现全部原创(与经典实现的偏离项见已知限制);
    • 可选的上游数据来源:mizchi/moon-pprof(Apache-2.0),负责采样与格式归一;
    • 渲染正确性以 google/pprof(Apache-2.0)作为交叉验证基准;
    • testdata/official-sample.wasm 来自 moon-pprof 仓库的样例(Apache-2.0)。

    本项目使用 MIT License。

    CallTree

    pub struct CallTree {
    roots : Array[Node]
    total : Double
    } derive(Eq)

    聚合结果。

    CallTree::equal

    fn CallTree::equal(CallTree, CallTree) -> Bool

    显式声明 Eq 的方法可被当作常规方法调用。

    新版编译器不再自动完成这个提升,不声明会报 implicit_impl_as_method。

    CallTree::max_delta

    fn CallTree::max_delta(self : CallTree) -> Double

    树中最大的变化量绝对值,用于把差异配色归一化到同一量级。

    不做归一化的话,只要有一处剧变,其它所有变化在颜色上就全糊成一片。

    CallTree::not_equal

    fn CallTree::not_equal(x : CallTree, y : CallTree) -> Bool

    显式声明 Eq 的方法可被当作常规方法调用。

    新版编译器不再自动完成这个提升,不声明会报 implicit_impl_as_method。

    DeltaEntry

    pub struct DeltaEntry {
    name : String
    before : Double
    after : Double
    change : Double
    ratio : Double
    } derive(Eq)

    一个函数在两份剖面之间的自身耗时变化。

    DeltaEntry::equal

    fn DeltaEntry::equal(DeltaEntry, DeltaEntry) -> Bool

    显式声明 Eq 的方法可被当作常规方法调用。

    新版编译器不再自动完成这个提升,不声明会报 implicit_impl_as_method。

    DeltaEntry::not_equal

    fn DeltaEntry::not_equal(x : DeltaEntry, y : DeltaEntry) -> Bool

    显式声明 Eq 的方法可被当作常规方法调用。

    新版编译器不再自动完成这个提升,不声明会报 implicit_impl_as_method。

    Hotspot

    pub struct Hotspot {
    name : String
    self_time : Double
    ratio : Double
    occurrences : Int
    } derive(Eq)

    一个热点条目:按函数名聚合后的自身耗时。

    Hotspot::equal

    fn Hotspot::equal(Hotspot, Hotspot) -> Bool

    显式声明 Eq 的方法可被当作常规方法调用。

    新版编译器不再自动完成这个提升,不声明会报 implicit_impl_as_method。

    Hotspot::not_equal

    fn Hotspot::not_equal(x : Hotspot, y : Hotspot) -> Bool

    显式声明 Eq 的方法可被当作常规方法调用。

    新版编译器不再自动完成这个提升,不声明会报 implicit_impl_as_method。

    Layout

    pub struct Layout {
    rects : Array[Rect]
    width : Double
    row_height : Double
    max_depth : Int
    total : Double
    max_delta : Double
    inverted : Bool
    } derive(Eq)

    布局结果。渲染器拿到的布局自带完整几何信息,无需再传画布参数。

    Layout::equal

    fn Layout::equal(Layout, Layout) -> Bool

    显式声明 Eq 的方法可被当作常规方法调用。

    新版编译器不再自动完成这个提升,不声明会报 implicit_impl_as_method。

    Layout::height

    fn Layout::height(self : Layout) -> Double

    画布所需高度。

    Layout::not_equal

    fn Layout::not_equal(x : Layout, y : Layout) -> Bool

    显式声明 Eq 的方法可被当作常规方法调用。

    新版编译器不再自动完成这个提升,不声明会报 implicit_impl_as_method。

    Layout::row_count

    fn Layout::row_count(self : Layout) -> Int

    需要绘制的行数(= 最深深度 + 1)。

    Node

    pub struct Node {
    name : String
    value : Double
    children : Array[Node]
    delta : Double?
    } derive(Eq)

    调用树节点。children 已排序,可直接用于布局。

    Node::child

    fn Node::child(self : Node, name : String) -> Node?

    按名字查找直接子节点。

    Node::equal

    fn Node::equal(Node, Node) -> Bool

    显式声明 Eq 的两个方法可被当作常规方法调用。

    新版编译器(moonc 0.10.14 起)不再自动把 impl Eq 的方法提升为常规方法, 不声明就会报 implicit_impl_as_method 废弃警告。这条声明把该提升写明, 行为与过去一致;配合 CI 的「警告即失败」,也避免未来默认行为变更后突然失效。

    Node::not_equal

    fn Node::not_equal(x : Node, y : Node) -> Bool

    显式声明 Eq 的两个方法可被当作常规方法调用。

    新版编译器(moonc 0.10.14 起)不再自动把 impl Eq 的方法提升为常规方法, 不声明就会报 implicit_impl_as_method 废弃警告。这条声明把该提升写明, 行为与过去一致;配合 CI 的「警告即失败」,也避免未来默认行为变更后突然失效。

    Node::self_ratio

    fn Node::self_ratio(self : Node, total : Double) -> Double

    自身耗时占给定总量的比例,用于文本报告与图上的百分比标注。

    Node::self_time

    fn Node::self_time(self : Node) -> Double

    自身耗时:累计权重减去所有子节点累计权重之和。

    采样栈可能停在中间(例如 main;compute 100 表示这 100 的权重归 compute 自己),此时该节点没有对应子节点,差额即它自身消耗的时间。

    ParseResult

    pub struct ParseResult {
    samples : Array[Sample]
    skipped_lines : Int
    } derive(Eq)

    解析结果。skipped_lines 记录非空但无法解析的行数,便于调用方提示用户。

    ParseResult::equal

    fn ParseResult::equal(ParseResult, ParseResult) -> Bool

    显式声明 Eq 的方法可被当作常规方法调用。

    新版编译器不再自动完成这个提升,不声明会报 implicit_impl_as_method。

    ParseResult::not_equal

    fn ParseResult::not_equal(x : ParseResult, y : ParseResult) -> Bool

    显式声明 Eq 的方法可被当作常规方法调用。

    新版编译器不再自动完成这个提升,不声明会报 implicit_impl_as_method。

    Rect

    pub struct Rect {
    x : Double
    y : Double
    width : Double
    height : Double
    name : String
    value : Double
    depth : Int
    delta : Double?
    } derive(Eq)

    一个待绘制的矩形。

    Rect::equal

    fn Rect::equal(Rect, Rect) -> Bool

    显式声明 Eq 的方法可被当作常规方法调用。

    新版编译器不再自动完成这个提升,不声明会报 implicit_impl_as_method。

    Rect::not_equal

    fn Rect::not_equal(x : Rect, y : Rect) -> Bool

    显式声明 Eq 的方法可被当作常规方法调用。

    新版编译器不再自动完成这个提升,不声明会报 implicit_impl_as_method。

    Relation

    pub struct Relation {
    label : String
    value : Double
    ratio : Double
    } derive(Eq)

    一条调用关系条目。

    Relation::equal

    fn Relation::equal(Relation, Relation) -> Bool

    显式声明 Eq 的方法可被当作常规方法调用。

    新版编译器不再自动完成这个提升,不声明会报 implicit_impl_as_method。

    Relation::not_equal

    fn Relation::not_equal(x : Relation, y : Relation) -> Bool

    显式声明 Eq 的方法可被当作常规方法调用。

    新版编译器不再自动完成这个提升,不声明会报 implicit_impl_as_method。

    Sample

    pub struct Sample {
    frames : Array[String]
    weight : Double
    } derive(Eq)

    一个采样样本:一条调用栈 + 权重。

    frames 自顶向下排列,frames[0] 为根帧。

    Sample::equal

    fn Sample::equal(Sample, Sample) -> Bool

    显式声明 Eq 的方法可被当作常规方法调用。

    新版编译器不再自动完成这个提升,不声明会报 implicit_impl_as_method。

    Sample::not_equal

    fn Sample::not_equal(x : Sample, y : Sample) -> Bool

    显式声明 Eq 的方法可被当作常规方法调用。

    新版编译器不再自动完成这个提升,不声明会报 implicit_impl_as_method。

    WeightUnit

    pub(all) enum WeightUnit {
    Count
    Nanoseconds
    Microseconds
    Milliseconds
    Seconds
    } derive(Eq)

    权重单位。

    注意名字不能叫 Unit:那是 MoonBit 的内置类型,重名会遮蔽它, 导致全仓库所有 -> Unit 的函数都报出「has type Unit, wanted Unit」这类怪错。

    WeightUnit::equal

    fn WeightUnit::equal(WeightUnit, WeightUnit) -> Bool

    显式声明 Eq 的方法可被当作常规方法调用。

    新版编译器不再自动完成这个提升,不声明会报 implicit_impl_as_method。

    WeightUnit::label

    fn WeightUnit::label(self : WeightUnit) -> String

    单位的规范写法,用于帮助信息与报告表头。

    WeightUnit::not_equal

    fn WeightUnit::not_equal(x : WeightUnit, y : WeightUnit) -> Bool

    显式声明 Eq 的方法可被当作常规方法调用。

    新版编译器不再自动完成这个提升,不声明会报 implicit_impl_as_method。

    WeightUnit::parse

    fn WeightUnit::parse(text : String) -> WeightUnit?

    从命令行文本解析单位。无法识别时返回 None。

    BASE_FONT_SIZE

    let BASE_FONT_SIZE : Double

    基础字号。经典实现用 12px。

    CANVAS_XPAD

    let CANVAS_XPAD : Double

    画布左右留白。与经典实现一致,JS 的缩放数学也依赖此值。

    FRAME_HEIGHT

    let FRAME_HEIGHT : Double

    每行高度。经典实现用 16px。

    aggregate

    fn aggregate(samples : Array[Sample], max_depth? : Int) -> CallTree

    把样本聚合成调用树。

    max_depth 限制每个根节点下保留的帧数:超出的部分会被合并进 (deeper) 合成节点(见 insert_path)。max_depth <= 0 或不传表示不限制。

    真实采样数据里递归栈可达数千层(实测最深 7254 层),按每层一行渲染会得到 十几万像素高的图,因此渲染前必须限深——这是 CLI 默认行为。

    compute_delta

    fn compute_delta(before : CallTree, after : CallTree, top : Int) -> Array[DeltaEntry]

    对比两份剖面的自身耗时,按函数名对齐。

    只出现在其中一侧的函数也会被列出(另一侧记 0),因为「新增的开销」 和「消失的开销」恰恰是差异分析最关心的两件事。

    top <= 0 表示不截断。

    deeper_placeholder

    fn deeper_placeholder() -> String

    深度超限时,用来收纳「更深的帧」的合成节点名。

    default_font_size

    fn default_font_size() -> Double

    默认字号。

    default_min_width

    fn default_min_width() -> Double

    默认最小矩形宽度(像素)。

    diff_against

    fn diff_against(tree : CallTree, baseline : CallTree) -> CallTree

    把两份剖面叠加,给 tree 的每个节点标注相对 baseline 的变化量。

    宽度仍按 tree(当前剖面)计算,因此只在基线里出现的帧不会显示—— 这与参考实现 difffolded.pl 的取舍一致(它的建议是交换两份文件再生成一张, 从另一个方向看消失的帧)。

    fill_for_delta

    fn fill_for_delta(delta : Double, max_delta : Double) -> String

    差异模式的配色:红 = 变多(劣化)、蓝 = 变少(改善)、白 = 基本没变。

    用经典实现的 color_scale 公式:底色是白,随变化幅度向红或蓝加深:

    变化 > 0: rgb(255, s, s) s = 210 * (max - delta) / max 变化 < 0: rgb(s, s, 255) s = 210 * (max + delta) / max 变化 = 0: rgb(255, 255, 255)

    用 max_delta 归一化:不归一化的话,只要有一处剧变, 其它所有变化在颜色上就会全糊成一片,等于没画。

    零变化取纯白而不是灰色——灰色在浅色背景上与文字对比度太低, 名字几乎读不出来;纯白配深色文字则清晰可读。

    fill_of

    fn fill_of(name : String) -> String

    帧名对应的填充色。

    用经典实现的 hot 调色板公式,只是把它的随机分量换成确定性的哈希:

    v = 名称哈希([0,1)) r = 205 + int(50 * v) g = 0 + int(230 * v) b = 0 + int(55 * v)

    三个通道共用同一个 v,因此颜色落成一条深红 → 橙 → 黄的单维渐变—— 这正是经典火焰图那种暖色观感。

    filter_folded

    fn filter_folded(text : String, pattern : String) -> String

    按子串过滤折叠栈:只保留调用链里含该子串的行。

    这是经典的 grep funcA input | flamegraph.pl 用法,但省掉了管道—— 过滤后的结果仍是折叠栈格式,可以直接再喂给出图命令。

    匹配前做名字归一化,否则用户按可读名 build_strings 去筛, 会一条都筛不到(数据里存的是 build__strings)。

    权重原样保留、不做归一化:过滤后的图回答的是「这个子系统内部怎么分配时间」, 而它占全局多少,靠保留原始权重才能在两张图之间对照。

    find_callees

    fn find_callees(tree : CallTree, wanted : String) -> Array[Relation]

    查询「这个函数调用了谁」。按被调用者聚合。

    find_callers

    fn find_callers(tree : CallTree, wanted : String) -> Array[Relation]

    查询「谁调用了这个函数」。按直接调用者聚合。

    同一个函数会在多条路径下被调用(例如 array::Array::at 既在冒泡排序里 也在插入排序里被调用),因此返回的是一个列表而不是单条路径。

    format_signed_weights

    fn format_signed_weights(value : Double, unit : WeightUnit) -> String

    带符号的变化量,正数带 + 便于识别。

    format_weight

    fn format_weight(value : Double, unit : WeightUnit) -> String

    把权重格式化成人能读的文本。

    时间单位按数量级自动缩放(871808200 纳秒 → 871.8 ms); 计数则原样显示并加千位分隔(1234567 → 1,234,567),不加后缀—— 加了反而会让人误以为它是时间。

    layout

    fn layout(tree : CallTree, width : Double, row_height : Double, inverted? : Bool) -> Layout

    把调用树布局成矩形列表(前序遍历顺序:父节点先于子节点)。

    多个根节点时按各自权重水平切分画布。

    inverted 选择纵向方向:

    • 默认(false)火焰图:根在底部,逐层向上生长。底部密、顶部疏, 高耗时路径表现为向上伸出的「火苗」——这是 Brendan Gregg 定义的经典形态。
    • inverted = true 冰柱图(icicle):根在顶部,逐层向下。

    两种方向都只影响 y 坐标,水平切分完全一致。

    normalize_name

    fn normalize_name(name : String) -> String

    把上游未还原的符号名归一化成可读形式。

    顺序很重要:先还原长度前缀,再折叠下划线。 反过来的话,13run__workload 被折成 12 个字符后长度前缀就对不上了。

    parse_folded

    fn parse_folded(text : String) -> ParseResult

    解析整个折叠栈文本。

    空白行被直接忽略;非空但无法解析的行计入 skipped_lines。

    parse_line

    fn parse_line(line : StringView) -> Sample?

    解析一行折叠栈。

    返回 None 表示该行不是有效样本,由调用方决定是忽略还是计入跳过数。

    render_delta

    fn render_delta(entries : Array[DeltaEntry], before_total : Double, after_total : Double, unit : WeightUnit) -> String

    把差异排行渲染成文本。

    与差异火焰图的分工:图看结构(哪条路径变宽了), 这张表看函数(哪个函数的自身耗时变了多少)。

    render_hotspots

    fn render_hotspots(hotspots : Array[Hotspot], total : Double, unit : WeightUnit) -> String

    把热点列表渲染成纯文本报告。

    unit 决定绝对值怎么显示(时间按数量级缩放,计数加千位分隔); 占比不受单位影响。

    render_relations

    fn render_relations(relations : Array[Relation], wanted : String, kind : String, unit : WeightUnit) -> String

    把调用关系渲染成文本。

    render_svg

    fn render_svg(layout : Layout, min_width? : Double, font_size? : Double, show_labels? : Bool, unit? : WeightUnit, normalize_names? : Bool) -> String

    把布局渲染成 SVG 文本。

    画布尺寸 = 布局宽度 + 左右留白,高度 = 布局高度 + 上下留白; 留白用于放置标题、Search / Reset Zoom 按钮与底部信息栏。

    min_width 以下的矩形会被跳过(默认 0.1 像素,与经典实现一致): 布局给出的是精确几何,但 sub-pixel 的矩形画出来也看不见,只会白白撑大文件。

    show_labels 控制是否在矩形内写帧名。窄框里写字会溢出,因此按字号粗略 估算宽度,放不下就不画(交互脚本在缩放后也会重新估算并截断)。

    unit 决定悬停提示里绝对值的显示方式;normalize_names 控制是否把上游 未还原的符号名归一化(只影响显示,解析层始终原样保留)。

    self_placeholder

    fn self_placeholder() -> String

    「自身耗时」合成帧的名字。

    top_hotspots

    fn top_hotspots(tree : CallTree, top : Int) -> Array[Hotspot]

    取自身耗时最高的若干函数。

    自身耗时为 0 的函数是纯粹的「过路」节点,不构成热点,会被过滤掉 (与 moon-pprof summary 的口径一致)。

    top <= 0 表示不截断,返回全部条目。

    version

    fn version() -> String

    返回当前版本号,与 moon.mod 中的 version 保持一致。