Ladybird 构建性能剖析:从 .ninja_log 火焰图到编译器内部 Pass 耗时的三级分析方法
本文围绕 Ladybird 浏览器仓库中的构建剖析文档 BuildProfilingInstructions.md 展开,系统讲解测量该大型 C++ 项目编译耗时的三种递进手段:用 time ninja 做整体计时、用 Ninja 日志配合 ninjatracing 生成逐文件编译火焰图、以及用 GCC/Clang 的 -ftime-report 查看单个文件内部各编译阶段的耗时。读完本文,你可以定位出拖慢 Ladybird 整体构建时间的最慢翻译单元(translation unit),并进一步判断瓶颈究竟出在词法分析、模板实例化还是代码生成阶段。
三种测量编译时间的方法概览
原文档将编译时间剖析归纳为三个层次,深度依次递进、粒度依次细化:
- 整体计时:用
time ninja install代替ninja install,得到一次完整安装构建的总耗时; - 逐文件计时:读取 Ninja 的日志文件(
.ninja_log),获得每个.cpp文件的编译耗时; - 文件内部阶段计时:为 GCC 或 Clang 开启
-ftime-report等标志,获得单个文件内各编译器 Pass 的耗时分解。
这三种方法互为补充:第一层告诉你“构建整体是否变慢了”,第二层告诉你“哪些文件变慢了”,第三层告诉你“文件内部是哪个阶段变慢了”。在优化 Ladybird 这类包含数千个源文件、且大量使用模板与 GC 相关宏的项目时,逐层下钻是定位瓶颈的标准路径。
方法一:用 time 命令做整体计时
最简单的方式是在构建命令前加 time:
time ninja install
它给出用户态时间(user)、系统态时间(sys)和墙钟时间(wall)三项指标。由于 Ladybird 的构建默认按核心数并行,wall 与 user 的比值可以粗略反映并行效率:如果 user 远大于 wall,说明编译任务在多个 worker 上摊开了;如果二者接近,则构建基本是串行的,此时单文件耗时会直接累加到总时长中。
整体计时的价值在于回归对比——例如提交了一处头文件改动后,用它快速确认总构建时间是否恶化。但它无法回答“是哪个文件慢”,因此只作为基线手段。
方法二:Ninja 日志 + ninjatracing 生成逐文件火焰图
原理
Ninja 在每次构建时都会向日志文件写入每个构建步骤的开始时间、结束时间与耗时,该文件默认写入调用 ninja 的目录下的 .ninja_log。这份日志天然包含“每个 .cpp 文件花了多少秒”的信息,正是发现“哪些文件是构建加速首选目标”的最佳入口。
开源的 Python 脚本 ninjatracing(一个独立的 python3 单文件脚本)可以把 .ninja_log 转换成 Chrome Trace 兼容的 JSON 格式,供 Speedscope 等性能可视化工具以火焰图形式展示。
前置条件
- 获取
ninjatracing脚本并保存到本地,确保其标记为可执行,且位于PATH中(或某个方便手动调用的位置)。 - 该脚本的 shebang 指向
python,而现代发行版通常只提供python3。原文档给出两种解决办法:- 自行创建
python到python3的符号链接; - 或直接修改
ninjatracing文件,把 shebang 中的python改为python3。
- 自行创建
操作步骤
第 1 步:清理构建缓存。 这是保证数据有意义的关键——只要有任何文件命中缓存,其“编译时间”会接近于 0,火焰图就会严重失真。原文档给出的命令为:
ninja clean
ccache --clear
第 2 步:正常执行构建:
ninja
第 3 步:转换日志。 日志默认写入调用 ninja 目录下的 .ninja_log。务必从当初调用 ninja 的同一目录执行转换命令,这样相对路径才能找到日志文件:
ninjatracing .ninja_log > trace.json
第 4 步:可视化。 将生成的 trace.json 拖入 Speedscope 或任何兼容 Chrome Trace 格式的火焰图查看器,即可按耗时排序查看每个编译目标。
为什么必须清 ccache:结合仓库源码看缓存机制
ccache --clear 这一步在 Ladybird 中不是可选的,而是默认构建链的组成部分,这一点可以从仓库源码得到印证:
- cmake_options.cmake 中定义了构建选项
ENABLE_LAGOM_CCACHE,描述为“Enable ccache for Lagom builds”,默认值为 ON; - 根目录 CMakeLists.txt 在该开关打开时
include(setup_ccache); - setup_ccache.cmake 会通过
find_program(CCACHE_PROGRAM ccache)查找 ccache,并为 C/C++ 编译器设置<compiler>_LAUNCHER,即让 CMake 通过CMAKE_C_COMPILER_LAUNCHER/CMAKE_CXX_COMPILER_LAUNCHER让每个编译步骤都经过 ccache 代理; - 值得注意的是,Ladybird 已经是一个 C++ 与 Rust 混合的项目。rust_crate.cmake 中还额外处理了 Rust 侧的缓存:它会查找
sccache程序并设置RUSTC_WRAPPER,使 Rust crate 的编译同样走缓存。
由此可以推断:在标准配置下,几乎全部 C/C++ 编译都经由 ccache(Rust 经由 sccache)。因此剖析前不清空缓存,火焰图看到的将是“缓存命中时间”而非真实编译时间,结论完全不可用。此外,AdvancedBuildInstructions.md 还提到 CI 场景下可通过 ENABLE_CI_BASELINE_CPU 选项统一目标架构(见 compile_options.cmake),其目的正是让不同 CI runner 共享 ccache 缓存——这也从侧面说明缓存对 Ladybird 构建时间的支配性影响。
补充:链接耗时的观察
.ninja_log 记录的是“每个构建步骤”,其中也包括链接步骤。Ladybird 最终链接体积可观的浏览器进程,链接本身可能占去可观时间。如果火焰图中某个链接节点异常突出,可以结合 cmake_options.cmake 中的 LAGOM_USE_LINKER(可选 lld、mold 等更快链接器)与 LAGOM_LINK_POOL_SIZE(限制并行链接数)两个选项做针对性调优。
方法三:-ftime-report 查看编译器内部 Pass 耗时
方法说明
给 GCC 加上 -ftime-report 标志后,编译器会为每个编译的文件在构建输出中打印一份各阶段耗时分解表。原文档收录的一份真实输出如下(表头为四列:用户态时间、系统态时间、墙钟时间、GGC 内存分配量):
GCC -ftime-report 输出示例(来自原文档)
Time variable usr sys wall GGC
phase setup : 0.00 ( 0%) 0.00 ( 0%) 0.01 ( 0%) 1326k ( 2%)
phase parsing : 0.57 ( 61%) 0.19 ( 83%) 1.63 ( 65%) 59M ( 74%)
phase lang. deferred : 0.10 ( 11%) 0.03 ( 13%) 0.30 ( 12%) 8761k ( 11%)
phase opt and generate : 0.23 ( 25%) 0.01 ( 4%) 0.48 ( 19%) 10M ( 13%)
phase last asm : 0.03 ( 3%) 0.00 ( 0%) 0.08 ( 3%) 539k ( 1%)
|name lookup : 0.11 ( 12%) 0.01 ( 4%) 0.25 ( 10%) 2004k ( 2%)
|overload resolution : 0.08 ( 9%) 0.00 ( 0%) 0.26 ( 10%) 7900k ( 10%)
dump files : 0.02 ( 2%) 0.00 ( 0%) 0.00 ( 0%) 0 ( 0%)
callgraph construction : 0.04 ( 4%) 0.00 ( 0%) 0.06 ( 2%) 2677k ( 3%)
callgraph optimization : 0.00 ( 0%) 0.00 ( 0%) 0.01 ( 0%) 4752 ( 0%)
callgraph functions expansion : 0.15 ( 16%) 0.00 ( 0%) 0.32 ( 13%) 6267k ( 8%)
callgraph ipa passes : 0.03 ( 3%) 0.01 ( 4%) 0.08 ( 3%) 1413k ( 2%)
ipa inheritance graph : 0.00 ( 0%) 0.00 ( 0%) 0.02 ( 1%) 3760 ( 0%)
ipa profile : 0.00 ( 0%) 0.00 ( 0%) 0.01 ( 0%) 0 ( 0%)
trivially dead code : 0.01 ( 1%) 0.00 ( 0%) 0.00 ( 0%) 224 ( 0%)
df reg dead/unused notes : 0.01 ( 1%) 0.00 ( 0%) 0.01 ( 0%) 104k ( 0%)
alias analysis : 0.01 ( 1%) 0.00 ( 0%) 0.00 ( 0%) 70k ( 0%)
preprocessing : 0.06 ( 6%) 0.03 ( 13%) 0.16 ( 6%) 1365k ( 2%)
parser (global) : 0.04 ( 4%) 0.04 ( 17%) 0.13 ( 5%) 6894k ( 8%)
parser struct body : 0.09 ( 10%) 0.03 ( 13%) 0.20 ( 8%) 9020k ( 11%)
parser function body : 0.00 ( 0%) 0.00 ( 0%) 0.01 ( 0%) 83k ( 0%)
parser inl. func. body : 0.03 ( 3%) 0.01 ( 4%) 0.05 ( 2%) 1567k ( 2%)
parser inl. meth. body : 0.11 ( 12%) 0.03 ( 13%) 0.30 ( 12%) 10M ( 13%)
template instantiation : 0.27 ( 29%) 0.08 ( 35%) 0.89 ( 36%) 25M ( 32%)
constant expression evaluation : 0.01 ( 1%) 0.00 ( 0%) 0.01 ( 0%) 356k ( 0%)
constraint satisfaction : 0.01 ( 1%) 0.00 ( 0%) 0.02 ( 1%) 130k ( 0%)
early inlining heuristics : 0.00 ( 0%) 0.00 ( 0%) 0.02 ( 1%) 21k ( 0%)
inline parameters : 0.01 ( 1%) 0.00 ( 0%) 0.03 ( 1%) 146k ( 0%)
integration : 0.00 ( 0%) 0.00 ( 0%) 0.01 ( 0%) 564k ( 1%)
tree CFG cleanup : 0.00 ( 0%) 0.00 ( 0%) 0.01 ( 0%) 5768 ( 0%)
tree SSA other : 0.00 ( 0%) 0.00 ( 0%) 0.01 ( 0%) 13k ( 0%)
tree operand scan : 0.01 ( 1%) 0.00 ( 0%) 0.00 ( 0%) 429k ( 1%)
tree CCP : 0.01 ( 1%) 0.00 ( 0%) 0.00 ( 0%) 18k ( 0%)
expand : 0.01 ( 1%) 0.00 ( 0%) 0.03 ( 1%) 1303k ( 2%)
varconst : 0.00 ( 0%) 0.00 ( 0%) 0.04 ( 2%) 6744 ( 0%)
forward prop : 0.01 ( 1%) 0.00 ( 0%) 0.02 ( 1%) 3520 ( 0%)
CSE : 0.01 ( 1%) 0.00 ( 0%) 0.00 ( 0%) 1144 ( 0%)
loop fini : 0.00 ( 0%) 0.00 ( 0%) 0.01 ( 0%) 0 ( 0%)
branch prediction : 0.00 ( 0%) 0.01 ( 4%) 0.00 ( 0%) 20k ( 0%)
combiner : 0.01 ( 1%) 0.00 ( 0%) 0.02 ( 1%) 138k ( 0%)
integrated RA : 0.01 ( 1%) 0.00 ( 0%) 0.01 ( 0%) 1112k ( 1%)
LRA reload inheritance : 0.00 ( 0%) 0.00 ( 0%) 0.02 ( 1%) 13k ( 0%)
LRA create live ranges : 0.00 ( 0%) 0.00 ( 0%) 0.01 ( 0%) 8568 ( 0%)
reload CSE regs : 0.01 ( 1%) 0.00 ( 0%) 0.02 ( 1%) 100k ( 0%)
final : 0.00 ( 0%) 0.00 ( 0%) 0.02 ( 1%) 323k ( 0%)
variable output : 0.01 ( 1%) 0.00 ( 0%) 0.00 ( 0%) 15k ( 0%)
symout : 0.09 ( 10%) 0.00 ( 0%) 0.20 ( 8%) 13M ( 17%)
variable tracking : 0.00 ( 0%) 0.00 ( 0%) 0.04 ( 2%) 471k ( 1%)
var-tracking dataflow : 0.02 ( 2%) 0.00 ( 0%) 0.03 ( 1%) 18k ( 0%)
var-tracking emit : 0.00 ( 0%) 0.00 ( 0%) 0.04 ( 2%) 117k ( 1%)
rest of compilation : 0.01 ( 1%) 0.00 ( 0%) 0.03 ( 1%) 1258k ( 2%)
TOTAL : 0.93 0.23 2.50 79M
[24/3139] Building CXX object Kernel/CMakeFiles/Kernel.dir/CommandLine.cpp.o
如何读这张表:
- 前四个
phase行是编译器的四大宏观阶段:setup(初始化)、parsing(词法/语法分析)、lang. deferred(语言层延迟处理)、opt and generate(优化与代码生成),示例文件中 parsing 占用户态时间 61%、GGC 内存分配 74%,是绝对主导; |前缀的行(name lookup、overload resolution 等)属于 parsing 阶段的细分;- 具体行中
template instantiation(模板实例化,墙钟 36%、内存 32%)与symout(符号输出,内存 17%)是 C++ 项目里典型的耗时大户——Ladybird 的 AK 库与 LibJS 中大量模板化容器和 GC 宏,正是这类开销的典型来源; GGC列是 GCC 全局内存分配器的分配量,某一阶段内存分配异常庞大往往意味着该阶段的中间表示(IR)规模失控,可结合代码结构进一步排查。
原文档对此方法的坦率评价是:这是否对你有帮助,取决于你是否了解编译器内部实现;一般而言并不推荐——它信息量极大,但对不熟悉前端/中端结构的人几乎不可读。Clang 同样支持 -ftime-report,但原文档作者声明未测试过其输出格式;此外还可以追加 -ftime-report-details 获得更细粒度的 Pass 数据(作者同样未测试)。
在哪里加这个标志:以当前仓库结构为准
原文档给出的操作是“编辑 ladybird 根目录的 CMakeLists.txt,在约第 220 行开始的编译选项列表中加上 add_compile_options(-ftime-report)”。需要注意,这一行号描述相对当前仓库已经过时:现在的根目录 CMakeLists.txt 只有 149 行,编译选项整体迁移到了 compile_options.cmake,由根 CMakeLists 通过 include(compile_options) 引入。
因此按当前仓库结构,等价且更规范的做法是在 compile_options.cmake 附近添加:
add_cxx_compile_options(-ftime-report)
这里推荐使用仓库自带的 add_cxx_compile_options 宏而非裸 add_compile_options,因为从宏定义(该文件第 10–16 行)可以看到,它会把选项包进 $<$<COMPILE_LANGUAGE:C,CXX,ASM>:...> 生成器表达式,仅作用于 C/C++/汇编编译,避免把 -ftime-report 误传给其他构建步骤。
一个实用技巧是:不必全局开启。-ftime-report 的输出会淹没构建日志,更好的工作流是先用方法二锁定最慢的几个 .cpp 文件,然后只对这些文件手动追加该标志编译一次(例如直接调用 compile_commands.json 中对应的编译命令并附加 -ftime-report;仓库已在 CMakeLists.txt 中通过 CMAKE_EXPORT_COMPILE_COMMANDS ON 默认导出该文件),即可获得干净的单文件分解数据。
结合构建类型选择剖析配置
编译耗时与优化级别强相关,剖析时应明确所测的是哪种配置。从 compile_options.cmake 可以看到当前仓库各构建类型对应的非 MSVC 标志:
| 构建类型 | C++ 优化/调试标志 | 说明 |
|---|---|---|
| Debug | -Og -ggdb3 |
弱优化 + 完整调试信息,编译最快,但与发布性能差异最大 |
| RelWithDebInfo | -O2 -g1 |
文档推荐的常规构建配置 |
| Release | -O3,且可选 LTO |
ENABLE_LTO_FOR_RELEASE 默认开启(静态 GCC 构建除外,见 cmake_options.cmake 的注释:静态构建下 LTO 内存消耗过大,故默认关闭) |
由此可以推断两个实践结论:
- 若优化目标是日常开发体验(编译快、改动能快速重编),应以 Debug 或 RelWithDebInfo 配置做剖析,避免把精力花在与日常构建无关的
-O3路径上; - 若优化目标是Release/LTO 场景(LTO 的 interprocedural 优化会显著增加单文件耗时),则必须在 Release 配置下重新测量,否则结论会失真。
三种方法的适用场景对比
| 方法 | 粒度 | 工具 | 前置清理 | 典型用途 |
|---|---|---|---|---|
time ninja |
整个构建 | shell 内建 time |
建议同配置对比 | 快速回归验证总时长 |
.ninja_log + ninjatracing |
每个编译/链接步骤 | ninja 日志、python3、火焰图查看器 | 必须 ninja clean + ccache --clear |
找出最慢的源文件,作为加速目标 |
-ftime-report |
单文件内各 Pass | GCC/Clang 编译选项 | 对锁定文件单独编译 | 判断瓶颈在解析、模板实例化还是代码生成 |
推荐的标准工作流是:先以 time ninja install 建立基线;再用方法二(清缓存后全量编译)得到逐文件火焰图,锁定 Top N 慢文件;最后仅对这几个文件用 -ftime-report 做单文件解剖,决定是拆分头文件依赖、减少模板实例化,还是调整包含结构。三种手段层层下钻,就能在不猜测的情况下把 Ladybird 这类大型项目的构建时间瓶颈定位到具体文件、甚至具体编译器阶段。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00