SpiderMonkey 的 iongraph:用 GraphViz 可视化 IonMonkey 优化图的完整指南
【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo
本文基于 iongraph 官方文档 撰写,并结合 SpiderMonkey 源码中 JIT Spew 子系统(JitSpewer.cpp、JSONSpewer.cpp)与 iongraph 工具本体 进行深度扩充。
导读
iongraph是 SpiderMonkey(Mozilla 的 JavaScript 引擎,内嵌于本仓库 MongoDB 的src/third_party/mozjs中)自带的 IonMonkey 优化中间表示(IR)可视化工具:它把 JIT 编译过程中产生的 MIR/LIR 图序列导出为 GraphViz 的 Dot 格式,再渲染成 PDF/SVG/PNG,帮助引擎开发者直观"看到"每次优化 pass 前后指令流与控制流的变化。读完本文,你将掌握从 Debug 构建、IONFLAGS=logs采集日志,到iongraph一键出图的完整工作流,并能读懂输出图中每种颜色、线型、标注背后对应的 MIR 标志位含义。
iongraph 是什么
iongraph位于仓库的 devtools/iongraph 目录 下,是一个 Python 3 脚本(原名iongraph,最初由 Sean Stangl 编写,见目录内 LICENSE)。其官方定位一句话即可概括:
Visualizer for IonMonkey graphs using GraphViz.
它的工作本质是一条流水线:
- 采集:以
IONFLAGS=logs环境变量运行js(SpiderMonkey 的 shell),IonMonkey 编译器在每一个优化 pass 前后把 MIR/LIR 图以 JSON 形式写到磁盘; - 转换:
iongraph读取 JSON,为每个 pass 构造两个 Dot 图(MIR 一张、LIR 一张,优先输出 LIR); - 渲染:调用 GraphViz 的
dot程序,把 Dot 图渲染成 PDF(默认)或 SVG/PNG,并按函数/ pass 命名输出文件。
在 iongraph 脚本 中可以看到完整的数据流:json.loads(parenthesize(s))读取 JSON(其中parenthesize()会在日志因崩溃而截断时补全缺失的闭合括号,保证还能解析出已写完的部分),随后buildGraphs(func)/buildOnlyFinalPass(func)构造 Dot 图,最后由genfiles/genmergedpdfs调用dot渲染(iongraph)。
环境要求与前置条件
使用 iongraph 需要以下组件:
| 组件 | 用途 | 备注 |
|---|---|---|
SpiderMonkeyDebug 构建的jsshell | 产出 IONFLAGS 日志 | 官方 README 明确要求 debug build,因为 JIT Spew 代码被#ifdef JS_JITSPEW包裹(见 JitSpewer.cpp),只有 debug 构建才启用 |
GraphViz 的dot可执行文件 | 渲染 Dot 图为最终格式 | 缺失时脚本直接报错退出:ERROR: graphviz (dot) is not installed(iongraph) |
pdftk或qpdf | 仅当输出格式为合并的pdf时需要 | 用于把单个函数的多个 pass 页面合并成一个 PDF;不安装也可以改用--format pdfs生成每图一个 PDF(iongraph) |
此外,日志默认写到/tmp/ion.json(Linux/macOS),Windows 上默认写到当前目录.,Android 上写到/data/local/tmp,这一默认值由 JitSpewer.cpp 中的JIT_SPEW_DIR宏决定。
基本使用步骤
第一步:采集 IonMonkey 日志
用IONFLAGS=logs环境变量运行一个 JS 模块:
IONFLAGS=logs js -m mymodule.mjsSpiderMonkey 会把编译过程中关于图状态的观测信息(每个 pass 的 MIR/LIR、指令属性、ResumePoint、寄存器分配区间等)dump 到/tmp/ion.json。
第二步:生成图
iongraph此时工作目录下会为日志中的每个函数生成一个 PDF(默认格式),每个 pass 对应一页。
两步合一:iongraph js子命令
为了方便,iongraph 提供了js子命令,把运行js与生成图合并为一次调用:
iongraph js -- -m mymodule.mjs以--为界:--之前的参数属于iongraph,--之后的参数原样传给js。例如只针对 1 号函数出图:
iongraph js --funcnum 1 -- -m mymodule.mjs从源码看,js_and_gen会先把logs强制追加进IONFLAGS(即使你只设了别的 flag 也会保证日志开启),再执行js,最后自动以/tmp/ion.json为输入调用生成流程(iongraph)。
命令行参数详解
以下参数定义来自 iongraph 脚本的add_main_arguments,对顶层命令、js子命令与jit-test子命令都适用:
| 参数 | 说明 | 默认值 |
|---|---|---|
-o, --outdir | 输出文件存放目录 | .(当前目录) |
-f, --funcnum | 只处理指定下标的函数,多个用逗号分隔,如1,5,234 | 全部 |
-n, --funcname | 只处理指定名字的函数,多个用逗号分隔,如foo,bar,baz | 全部 |
-p, --passnum | 只处理指定序号的 pass,多个用逗号分隔 | 全部 |
--format | 输出格式,可选gv/pdf/pdfs/png/svg | pdf |
--final | 只生成最终优化后的 MIR/LIR 图 | 关 |
--out-mir | 把选中的 MIR 图写入指定文件 | 无 |
--out-lir | 把选中的 LIR 图写入指定文件 | 无 |
input(位置参数) | IonMonkey 生成的 JSON 日志路径 | /tmp/ion.json |
几个值得注意的行为细节:
pdf与pdfs的区别:pdf会把每个函数的所有 pass 图合并成一个多页 PDF(依赖pdftk/qpdf);pdfs与其它格式一样,每张图单独一个文件(iongraph)。- 默认输出策略:每个 pass 通常只输出 MIR/LIR 中的一种,优先 LIR(因为 LIR 图不含后继信息,绘制边时需要借助 MIR 图,见下文原理)。只有在
--final模式下才同时输出 MIR 与 LIR(iongraph)。 --funcnum与--passnum的下标:函数序号取自 JSON 中functions数组的下标(从 0 开始),pass 序号同理。日志中没有指定函数时会提示abort during SSA construction,正常则打印success; N passes(iongraph)。
jit-test 子命令
除了js子命令,iongraph 还支持jit-test子命令,可配合 SpiderMonkey 的jit_test.py运行单个测试用例并出图:
iongraph jit-test --funcnum 1 -- category/test.js它内部以js/src/jit-test/jit_test.py --one <test>的形式执行测试,同样自动追加logs到IONFLAGS(iongraph)。
输出文件命名与格式
生成的 Dot 文件命名规则在 outputPass 函数 中定义:
func<NN>-pass<NN>-<pass名>-<mir|lir>.gv- 函数序号与 pass 序号都是两位补零(如
func01-pass03); - 图名使用 pass 名(如
LICM、GVN等优化 pass 名称); mir/lir后缀标明是 MIR 图还是 LIR 图。
渲染后,gv原样拷贝,png/svg/pdf(s)调用dot -T<format>转换,pdf模式再用pdftk/qpdf把同一函数的所有页面合并(genmergedpdfs)。生成的 Dot 图中,图的标题标签是「函数名 - pass 名」,并附带图例:蓝色movable、下划线guard、红色in worklist、灰色recovered on bailout(iongraph)。
读懂输出图:图形属性语义
这一节是官方 README 的核心内容,逐条对应到源码实现:
块(Block)的属性
- 绿色边框(limegreen):循环头(loop header)。对应 JSON 中块的
loopheader属性,源码在 JSONSpewer.cpp 中由block->isLoopHeader()输出,iongraph 据此给节点加color: limegreen(iongraph)。 - 红色边框(crimson):包含循环回边(loop backedge)的块,即其某个后继是循环头。对应 JSON 的
backedge属性,由block->isLoopBackedge()输出,iongraph 给节点加color: crimson(JSONSpewer.cpp、iongraph)。 - 虚线边框(dashed):临界边分裂(critical edge splitting)过程中创建的块。对应
splitedge属性,由block->isSplitEdge()输出,iongraph 给节点加style: dashed(JSONSpewer.cpp、iongraph)。
此外,块标签中还会显示块使用计数(Count: N)与上述属性的文字标注(如(loop header)、(backedge)、(split edge)),见 getBlockLabel。
指令(Instruction)的属性
- 蓝色:可移动(Movable)指令。iongraph 在指令属性列表里检测到
Movable时把指令名染成蓝色(iongraph)。 - 下划线:Guard(守卫)指令,例如类型检查、边界检查等。检测到
Guard属性时给指令名加下划线(iongraph)。 - 红色:仍在 worklist 中的指令(
InWorklist,SSA 构建/处理期间的状态),检测到该属性时染红(iongraph)。 - 灰色(gray50):发生 bailout(退出优化)时被恢复(
RecoveredOnBailout)的指令,表示该指令的结果可以在遇到 bailout 时由 interpreter 重新计算,无需保存在栈快照里(iongraph)。
这些属性全部来自 MIR 定义上的标志位,由 JSONSpewer.cpp 的spewMDef通过MIR_FLAG_LIST宏展开后逐个输出到 JSON 的attributes列表中。
MResumePoint
MResumePoint 作为指令行呈现,着色为灰色。每个 MResumePoint 保存了指令执行点处的环境快照(用于 bailout 恢复与 OSR)。JSON 中它以resumePoint字段出现,包含caller(内联调用者所属块)、mode(如At/After等,来自ResumeModeToString)以及按序排列的operands(跨内联调用链时用|分隔),见 JSONSpewer.cpp 的spewMResumePoint。iongraph 中 ResumePoint 行显示为resumepoint <操作数列表>,同样以灰色渲染(getResumePointRow)。块入口处的 ResumePoint 会出现在块顶部,指令自带的 ResumePoint 则以At/After两种 mode 分别出现在指令前/后。
边的标注
以条件分支结尾的块,其出边会用真值 '0' 或 '1' 标注。实现上:当块有 2 个后继时,指向successors[0]的边标1(真分支),指向successors[1]的边标0(假分支)(iongraph)。这对应控制流图中条件的布尔真值,与 MIR 中分支指令的两个出边约定一致。
关于 MIR 与 LIR 两套图
每个 pass 会输出 MIR 与 LIR 两张候选图(buildGraphsForPass)。两者共享相同的块划分:LIR 图不含后继(successor)信息,因此绘制 LIR 图的边时直接复用 MIR 图中对应块的successors列表(见 buildGraphForIR 的注释)。这解释了为什么默认只输出 LIR 而非同时输出两张。
底层原理:JSON 日志是如何产生的
日志的产生完全在 SpiderMonkey 侧,与 iongraph 无关,理解它有助于排查"为什么没有输出"的问题。
IONFLAGS 环境变量
IONFLAGS是逗号分隔的 flag 列表,在 JitSpewer.cpp 的CheckLogging()中被解析。与 iongraph 相关的两个关键 flag:
logs:开启异步JSON 日志(EnableIonDebugAsyncLogging)。所谓异步是指每个 pass 的图先缓存在内存中,到函数编译结束时才一次性写入文件(对应 endFunction 中的 dump 时机)。logs-sync:开启同步日志(EnableIonDebugSyncLogging)。每个 pass 结束后立即 flush 到文件(对应 spewPass),这在编译中途崩溃时能保留更多现场,但官方注释建议此时关闭 off-thread 编译。
在 shell 中可用IONFLAGS=help js打印完整的 flag 列表,其中包括logs JSON visualization logging与logs-sync Same as logs, but flushes between each pass等说明(JitSpewer.cpp)。
输出文件与 JSON 结构
- 文件路径固定为
JIT_SPEW_DIR/ion.json,Linux 上即/tmp/ion.json;若设置了环境变量ION_SPEW_BY_PID,则改为/tmp/ion<pid>.json,便于多进程并行实验(JitSpewer.cpp)。 - 文件顶层结构为
{"functions": [...]}(JitSpewer.cpp 与析构时的\n]}收尾)。 - 每个函数条目由 JSONSpewer.cpp 写出:
beginFunction记录函数名(格式为文件名:行号,如mymodule.mjs:12)并开启passes列表(L21-L25);- 每个 pass 记录
name、mir(块列表)与lir(块列表)(L33-L36、L134-L239); - MIR 块包含
number、attributes、predecessors、successors、instructions、入口resumePoint(L138-L186); - 每个 MIR 指令包含
id、opcode、attributes、inputs(操作数 id)、uses(使用方 id)、memInputs(内存依赖)、type(MIRType,含范围分析结果与截断标记(t))以及可选的resumePoint(L65-L132); - 在带寄存器分配的 pass 中,还会额外输出
ranges,记录每个虚拟寄存器(vreg)的活跃区间(allocation、start、end),这是回溯式寄存器分配器(BacktrackingAllocator)的输出(L241-L282)。
IONFILTER:只记录关心的函数
日志可能极其庞大。环境变量IONFILTER提供了按源文件/行号过滤的机制:值为逗号分隔的文件或文件:行号表达式,只有匹配的函数才会被记录(不匹配时计入filteredOutCompilations,见 JitSpewer.cpp 的FilterContainsLocation)。例如:
IONFILTER=mymodule.mjs IONFLAGS=logs js -m mymodule.mjs典型调试工作流示例
综合以上内容,一个完整的端到端示例:
# 1) 只记录 mymodule.mjs 中的函数,异步采集日志 IONFILTER=mymodule.mjs IONFLAGS=logs js -m mymodule.mjs # 2) 只处理第 1 个函数、只生成最终优化后的 MIR/LIR,输出为 SVG iongraph -f 1 --final --format svg # 3) 或一步到位:先运行 js 再出图(-- 后的参数传给 js) iongraph js --funcnum 1 --format png -- -m mymodule.mjs # 4) 需要单页 PDF(不依赖 pdftk/qpdf)时 iongraph --format pdfs出图后,在生成的func01-passNN-<pass名>-mir.gv(或-lir.gv)中:绿框定位循环结构、红框定位回边、蓝字定位可移动指令、下划线定位守卫指令、灰色定位可恢复指令与 ResumePoint,配合0/1边标注即可逐 pass 追踪优化前后的形态差异。
局限与注意事项
- 必须是 Debug 构建:
JitSpewer与JSONSpewer的实现都包裹在#ifdef JS_JITSPEW内(JitSpewer.cpp、JSONSpewer.cpp),Release 构建不会编译这些代码,也不会有任何日志输出。 - 日志默认路径固定为
/tmp/ion.json(可用ION_SPEW_BY_PID改为带 pid 的文件名),若目录不可写或文件被占用,init()会失败并返回 false(JitSpewer.cpp)。 - 合并 PDF 需要额外工具:不装
pdftk/qpdf时请改用--format pdfs,否则脚本会报错退出。 - JSON 容错:
iongraph的parenthesize()会为崩溃截断的日志补全闭合括号,尽量保住已生成的部分(iongraph),但它明确不处理转义字符串,极端场景下可能解析失败。
参考文件索引
- 官方使用文档:README.md
- iongraph 工具本体(Python 3):iongraph
- IONFLAGS 解析与日志写出:JitSpewer.cpp
- JSON 图序列化(MIR/LIR/ResumePoint/Ranges):JSONSpewer.cpp
【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考