首页
/ Ladybird 构建性能剖析:从 .ninja_log 火焰图到编译器内部 Pass 耗时的三级分析方法

Ladybird 构建性能剖析:从 .ninja_log 火焰图到编译器内部 Pass 耗时的三级分析方法

2026-09-04 20:59:49作者:袁立春Spencer

本文围绕 Ladybird 浏览器仓库中的构建剖析文档 BuildProfilingInstructions.md 展开,系统讲解测量该大型 C++ 项目编译耗时的三种递进手段:用 time ninja 做整体计时、用 Ninja 日志配合 ninjatracing 生成逐文件编译火焰图、以及用 GCC/Clang 的 -ftime-report 查看单个文件内部各编译阶段的耗时。读完本文,你可以定位出拖慢 Ladybird 整体构建时间的最慢翻译单元(translation unit),并进一步判断瓶颈究竟出在词法分析、模板实例化还是代码生成阶段。

三种测量编译时间的方法概览

原文档将编译时间剖析归纳为三个层次,深度依次递进、粒度依次细化:

  1. 整体计时:用 time ninja install 代替 ninja install,得到一次完整安装构建的总耗时;
  2. 逐文件计时:读取 Ninja 的日志文件(.ninja_log),获得每个 .cpp 文件的编译耗时;
  3. 文件内部阶段计时:为 GCC 或 Clang 开启 -ftime-report 等标志,获得单个文件内各编译器 Pass 的耗时分解。

这三种方法互为补充:第一层告诉你“构建整体是否变慢了”,第二层告诉你“哪些文件变慢了”,第三层告诉你“文件内部是哪个阶段变慢了”。在优化 Ladybird 这类包含数千个源文件、且大量使用模板与 GC 相关宏的项目时,逐层下钻是定位瓶颈的标准路径。

方法一:用 time 命令做整体计时

最简单的方式是在构建命令前加 time

time ninja install

它给出用户态时间(user)、系统态时间(sys)和墙钟时间(wall)三项指标。由于 Ladybird 的构建默认按核心数并行,walluser 的比值可以粗略反映并行效率:如果 user 远大于 wall,说明编译任务在多个 worker 上摊开了;如果二者接近,则构建基本是串行的,此时单文件耗时会直接累加到总时长中。

整体计时的价值在于回归对比——例如提交了一处头文件改动后,用它快速确认总构建时间是否恶化。但它无法回答“是哪个文件慢”,因此只作为基线手段。

方法二:Ninja 日志 + ninjatracing 生成逐文件火焰图

原理

Ninja 在每次构建时都会向日志文件写入每个构建步骤的开始时间、结束时间与耗时,该文件默认写入调用 ninja 的目录下的 .ninja_log。这份日志天然包含“每个 .cpp 文件花了多少秒”的信息,正是发现“哪些文件是构建加速首选目标”的最佳入口。

开源的 Python 脚本 ninjatracing(一个独立的 python3 单文件脚本)可以把 .ninja_log 转换成 Chrome Trace 兼容的 JSON 格式,供 Speedscope 等性能可视化工具以火焰图形式展示。

前置条件

  • 获取 ninjatracing 脚本并保存到本地,确保其标记为可执行,且位于 PATH 中(或某个方便手动调用的位置)。
  • 该脚本的 shebang 指向 python,而现代发行版通常只提供 python3。原文档给出两种解决办法:
    • 自行创建 pythonpython3 的符号链接;
    • 或直接修改 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 这类大型项目的构建时间瓶颈定位到具体文件、甚至具体编译器阶段。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
docsdocs
暂无描述
Markdown
889
5.78 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.17 K
341