最近,笔者需要统计 rustc 在编译特定程序时的代码覆盖率。由于这一过程涉及构建带有插桩(instrumentation)的自定义 rustc,步骤较为繁琐,容易出错。为方便日后回顾,也为了给有类似需求的读者提供参考,特将完整流程整理成文。

构建插桩 rustc

首先安装系统依赖:

sudo apt install ninja-build pkg-config libssl-dev libzstd-dev llvm lld clang cmake

然后安装覆盖率插件:

cargo install grcov

接着,下载最新版本的 rustc 源代码并解压,笔者这里下载的是 1.87.0 版本:

wget https://static.rust-lang.org/dist/rustc-1.87.0-src.tar.xz
tar -xf rustc-1.87.0-src.tar.xz

进入源代码目录,运行 ./configure 指令生成默认构建配置 bootstrap.toml

cd rustc-1.87.0-src
./configure

打开 bootstrap.toml 文件,开启 profiler 选项,其他内容保持默认:

[build]
profiler = true

[target.x86_64-unknown-linux-gnu]
profiler = true

然后需要修改 rustc 的 builder 源码,添加插桩命令。具体地,在 src/bootstrap/src/core/builder/cargo.rs 文件的 580 行左右(可以全局搜索 rust_new_symbol_mangling 关键字,就在它附近)添加以下插桩的命令:

if mode != Mode::Std {
    rustflags.arg("-Cinstrument-coverage");
}

需要注意的是,对于标准库的编译不能插桩,否则会报错提示缺少 profiler 库。

最后,运行以下命令开始编译:

./x build && ./x install

根据机器配置的不同,编译过程大约耗时 1 小时,最终构建出的 rustc 安装在 /usr/local/bin/rustc 目录下。

计算代码覆盖率

编译程序

使用刚才构建的插桩 rustc 编译给定程序:

LLVM_PROFILE_FILE="coverage/%p-%m.profraw" /usr/local/bin/rustc t1.rs
LLVM_PROFILE_FILE="coverage/%p-%m.profraw" /usr/local/bin/rustc t2.rs

对于多个 rust 文件,使用以上命令依次编译即可,所有的结果都会存在 coverage 文件夹中。

如果是 Cargo 项目,使用我们构建的 cargo 二进制文件即可,它会自动调用插桩的 rustc:

/usr/local/bin/cargo build

查看覆盖率

编译完成后可以通过以下命令查看 rustc 的代码覆盖率:

grcov coverage/*.profraw \
  -b /root/Desktop/rustc-1.87.0-src/build/x86_64-unknown-linux-gnu/stage1 \
  -s /root/Desktop/rustc-1.87.0-src/compiler \
  -t html \
  --branch \
  --ignore-not-existing \
  --llvm-path /root/rustc-1.87.0-src/build/x86_64-unknown-linux-gnu/llvm/bin \
  -o coverage/

对于多个 .profraw 文件,也可以使用以下命令合并后再导出覆盖率报告:

/root/rustc-1.87.0-src/build/x86_64-unknown-linux-gnu/llvm/bin/llvm-profdata merge -sparse *.profraw -o merged.profdata

[!NOTE]

  • 上述指令中 /root/Desktop/rustc-1.87.0-src 是构建插桩 rustc 的源码路径,需要根据实际情况修改
  • 开启 profiler 之后 rustc 的运行效率将受到较大影响,因此不建议在计算覆盖率的同时观测和性能有关的数据
  • .profraw 文件体积较大,大规模的任务极易耗尽存储空间,建议另起一个脚本监控 coverage 目录,滚动合并 rustc 生成的 .profraw 文件

参考资料