41 个版本

0.13.0 2023 年 9 月 27 日
0.12.1 2023 年 7 月 26 日
0.11.1 2023 年 2 月 22 日
0.11.0 2022 年 11 月 3 日
0.3.12 2019 年 11 月 27 日

#2 in 性能分析

Download history 83341/week @ 2024-05-01 88761/week @ 2024-05-08 102415/week @ 2024-05-15 106853/week @ 2024-05-22 103882/week @ 2024-05-29 99273/week @ 2024-06-05 102545/week @ 2024-06-12 96947/week @ 2024-06-19 103380/week @ 2024-06-26 95444/week @ 2024-07-03 99228/week @ 2024-07-10 108345/week @ 2024-07-17 106162/week @ 2024-07-24 93559/week @ 2024-07-31 115021/week @ 2024-08-07 109914/week @ 2024-08-14

447,672 每月下载量
用于 182 个 crates(直接使用 163 个)

Apache-2.0

96KB
2K SLoC

pprof

pprof 是一个易于集成到 Rust 程序中的 CPU 分析器。

Actions Status Crates.io Dependency Status FOSSA Status

使用方法

首先,获取一个守护者以开始分析。分析将在守护者被丢弃后继续。

let guard = pprof::ProfilerGuardBuilder::default().frequency(1000).blocklist(&["libc", "libgcc", "pthread", "vdso"]).build().unwrap();

在分析期间,您可以使用守护者获取报告。

if let Ok(report) = guard.report().build() {
    println!("report: {:?}", &report);
};

DebugReport 实现了。它将打印一个可读的堆栈计数器报告。以下是一个示例

FRAME: backtrace::backtrace::trace::h3e91a3123a3049a5 -> FRAME: pprof::profiler::perf_signal_handler::h7b995c4ab2e66493 -> FRAME: Unknown -> FRAME: prime_number::is_prime_number::h70653a2633b88023 -> FRAME: prime_number::main::h47f1058543990c8b -> FRAME: std::rt::lang_start::{{closure}}::h4262e250f8024b06 -> FRAME: std::rt::lang_start_internal::{{closure}}::h812f70926ebbddd0 -> std::panicking::try::do_call::h3210e2ce6a68897b -> FRAME: __rust_maybe_catch_panic -> FRAME: std::panicking::try::h28c2e2ec1c3871ce -> std::panic::catch_unwind::h05e542185e35aabf -> std::rt::lang_start_internal::hd7efcfd33686f472 -> FRAME: main -> FRAME: __libc_start_main -> FRAME: _start -> FRAME: Unknown -> THREAD: prime_number 1217
FRAME: backtrace::backtrace::trace::h3e91a3123a3049a5 -> FRAME: pprof::profiler::perf_signal_handler::h7b995c4ab2e66493 -> FRAME: Unknown -> FRAME: alloc::alloc::box_free::h82cea48ed688e081 -> FRAME: prime_number::main::h47f1058543990c8b -> FRAME: std::rt::lang_start::{{closure}}::h4262e250f8024b06 -> FRAME: std::rt::lang_start_internal::{{closure}}::h812f70926ebbddd0 -> std::panicking::try::do_call::h3210e2ce6a68897b -> FRAME: __rust_maybe_catch_panic -> FRAME: std::panicking::try::h28c2e2ec1c3871ce -> std::panic::catch_unwind::h05e542185e35aabf -> std::rt::lang_start_internal::hd7efcfd33686f472 -> FRAME: main -> FRAME: __libc_start_main -> FRAME: _start -> FRAME: Unknown -> THREAD: prime_number 1
FRAME: backtrace::backtrace::trace::h3e91a3123a3049a5 -> FRAME: pprof::profiler::perf_signal_handler::h7b995c4ab2e66493 -> FRAME: Unknown -> FRAME: prime_number::main::h47f1058543990c8b -> FRAME: std::rt::lang_start::{{closure}}::h4262e250f8024b06 -> FRAME: std::rt::lang_start_internal::{{closure}}::h812f70926ebbddd0 -> std::panicking::try::do_call::h3210e2ce6a68897b -> FRAME: __rust_maybe_catch_panic -> FRAME: std::panicking::try::h28c2e2ec1c3871ce -> std::panic::catch_unwind::h05e542185e35aabf -> std::rt::lang_start_internal::hd7efcfd33686f472 -> FRAME: main -> FRAME: __libc_start_main -> FRAME: _start -> FRAME: Unknown -> THREAD: prime_number 1

功能

  • cpp 启用 cpp 解包。
  • flamegraph 启用 flamegraph 报告格式。
  • prost-codec 通过 prost 启用 pprof protobuf 报告格式。
  • protobuf-codec 通过 protobuf crate 启用 pprof protobuf 报告格式。
  • frame-pointer 通过帧指针获取回溯。 仅限 nightly 版本

Flamegraph

pprof = { version = "0.13", features = ["flamegraph"] }

如果启用了 flamegraph 功能,您可以从报告中生成 flamegraph。 Report 结构有一个 flamegraph 方法,可以生成 flamegraph 并将其写入 Write

if let Ok(report) = guard.report().build() {
    let file = File::create("flamegraph.svg").unwrap();
    report.flamegraph(file).unwrap();
};

此外,可以指定自定义 flamegraph 选项。

if let Ok(report) = guard.report().build() {
    let file = File::create("flamegraph.svg").unwrap();
    let mut options = pprof::flamegraph::Options::default();
    options.image_width = Some(2500);
    report.flamegraph_with_options(file, &mut options).unwrap();
};

以下是一个生成的 flamegraph 示例

flamegraph

帧后处理器

在生成报告之前,提供了 frame_post_processor 作为修改原始统计数据的接口。如果您想对多个符号/线程进行分组或对某些符号进行解包,这个功能将有助于您。

例如

fn frames_post_processor() -> impl Fn(&mut pprof::Frames) {
    let thread_rename = [
        (Regex::new(r"^grpc-server-\d*$").unwrap(), "grpc-server"),
        (Regex::new(r"^cop-high\d*$").unwrap(), "cop-high"),
        (Regex::new(r"^cop-normal\d*$").unwrap(), "cop-normal"),
        (Regex::new(r"^cop-low\d*$").unwrap(), "cop-low"),
        (Regex::new(r"^raftstore-\d*$").unwrap(), "raftstore"),
        (Regex::new(r"^raftstore-\d*-\d*$").unwrap(), "raftstore"),
        (Regex::new(r"^sst-importer\d*$").unwrap(), "sst-importer"),
        (
            Regex::new(r"^store-read-low\d*$").unwrap(),
            "store-read-low",
        ),
        (Regex::new(r"^rocksdb:bg\d*$").unwrap(), "rocksdb:bg"),
        (Regex::new(r"^rocksdb:low\d*$").unwrap(), "rocksdb:low"),
        (Regex::new(r"^rocksdb:high\d*$").unwrap(), "rocksdb:high"),
        (Regex::new(r"^snap sender\d*$").unwrap(), "snap-sender"),
        (Regex::new(r"^snap-sender\d*$").unwrap(), "snap-sender"),
        (Regex::new(r"^apply-\d*$").unwrap(), "apply"),
        (Regex::new(r"^future-poller-\d*$").unwrap(), "future-poller"),
    ];

    move |frames| {
        for (regex, name) in thread_rename.iter() {
            if regex.is_match(&frames.thread_name) {
                frames.thread_name = name.to_string();
            }
        }
    }
}
if let Ok(report) = guard.frames_post_processor(frames_post_processor()).report().build() {
    let file = File::create("flamegraph.svg").unwrap();
    report.flamegraph(file).unwrap();
}

pprof 一起使用

启用 protobuf 功能后,pprof-rs 还可以输出 profile.proto 格式。

match guard.report().build() {
    Ok(report) => {
        let mut file = File::create("profile.pb").unwrap();
        let profile = report.pprof().unwrap();

        let mut content = Vec::new();
        profile.encode(&mut content).unwrap();
        file.write_all(&content).unwrap();

        println!("report: {}", &report);
    }
    Err(_) => {}
};

然后您可以使用 pprof 命令与 profile.pb 一起使用。例如

~/go/bin/pprof -svg profile.pb

然后,pprof 将根据配置生成一个 SVG 文件。

tree

criterion 集成

启用 criterion 功能后,在 pprof-rs 中提供了一个 criterion 自定义分析器。

use pprof::criterion::{PProfProfiler, Output};

criterion_group!{
    name = benches;
    config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flamegraph(None)));
    targets = bench
}
criterion_main!(benches);

运行基准测试后,您可以在 target/criterion/<name-of-benchmark>/profile.svg 找到 flamegraph。使用 Output::Protobuf 选项还可以获得 protobuf 输出,这些输出位于 target/criterion/<name-of-benchmark>/profile.pb

有关更多详细信息,您可以查看 examples/criterion.rs,以及 criterion 的分析文档。要快速入门,您可以使用以下命令运行此示例:cargo run --example criterion --release --features="flamegraph criterion" -- --bench --profile-time 5

为什么不...

已经有很多分析器了,为什么我们还要创建一个新的?在这里,我们比较了 pprof-rs 和其他流行的分析器,以帮助您选择最适合的分析器。

gperftools

gperftools 也是一个集成的分析器。Rust 中也有一个名为 cpuprofilergperftools 包装器,这使得它可以在 Rust 程序中编程。

优点

  1. pprof-rs 拥有一个现代化的构建系统,并且可以轻松地集成到 Rust 程序中,而编译 gperftools 静态版本存在错误。
  2. pprof-rs 有一个本地的 Rust 接口,而 gperftools 的包装器只是一个包装器。
  3. 使用 Rust 编程可以保证线程安全。

缺点

  1. gperftools 是一组性能分析工具,包括 CPU 分析器、堆分析器等。而 pprof-rs 目前专注于 CPU 分析器。

perf

perf 是 Linux 中的一个性能分析工具。

优点

  1. 使用 pprof-rs,您不需要启动另一个进程来使用 perf
  2. pprof-rs 可以轻松地集成到 Rust 程序中,这意味着您不需要安装任何其他程序。
  3. pprof-rs 拥有一个现代化的可编程接口,可以对其进行修改。
  4. pprof-rs 理论上支持所有 POSIX 系统,并且可以轻松地支持更多系统。

缺点

  1. perf 比比 pprof-rs 更丰富。
  2. perf 与 Linux 高度集成。

实现

当开始性能分析时,使用了 setitimer 系统调用来设置一个定时器,该定时器会以固定的时间间隔向此程序发送 SIGPROF 信号。

接收到 SIGPROF 信号时,信号处理程序将捕获堆栈跟踪并增加计数。过了一段时间后,分析器可以获取所有可能的堆栈跟踪及其计数。最后,我们可以生成包含分析器数据的报告。

然而,现实世界充满了荆棘。在实现过程中有许多值得注意的部分。

堆栈跟踪

不幸的是,没有100%健壮的堆栈跟踪方法。gperftools已经进行了一些相关研究。[一些相关研究](https://github.com/gperftools/gperftools/wiki/gperftools%27-stacktrace-capturing-methods-and-their-issues)。pprof-rs 使用 backtrace-rs,最终使用由 libgcc 提供的 libunwind

警告:如前述 gperftools 文档所述,libgcc 提供的 libunwind 不是信号安全的。

libgcc 的 unwind 方法在信号处理程序中使用是不安全的。一个特定的死锁原因是在程序抛出异常时,性能分析计时器发生。

可以通过添加一个 blocklist 来解决这个问题

let guard = pprof::ProfilerGuardBuilder::default().frequency(1000).blocklist(&["libc", "libgcc", "pthread", "vdso"]).build().unwrap();

应该也将 vdso 添加到 blocklist 中,因为在某些发行版中(例如 ubuntu 18.04),vdso 中的 dwarf 信息是不正确的。

帧指针

pprof-rs 还支持通过帧指针进行 unwind,而无需使用 libunwind。然而,Rust 编译器附带的标准库中并非每个函数都有正确的帧指针,因此您需要使用 cargo +nightly -Z build-std 来从源代码构建标准库。

由于我们无法在信号处理程序中获取堆栈边界,因此也无法确保安全性。如果帧指针被设置为一个错误的值,程序将崩溃。

信号安全性

保证信号安全性是困难的,但并非那么困难。

首先,我们必须避免死锁。当分析器采样或报告时,它将在分析器上获取全局锁。特别是,当正在运行的程序从分析器获取报告(这将持有锁)时,同时触发 SIGPROF 信号,分析器想要采样(也将持有锁),就会发生死锁。所以我们不在信号处理程序中等待锁,而是在信号处理程序中 try_lock。如果无法获取全局锁,分析器将直接放弃。

然后,信号安全 POSIX 函数相当有限,如 [此处](http://man7.org/linux/man-pages/man7/signal-safety.7.html) 所列。最令人烦恼的问题是,我们无法在信号处理程序中使用 malloc。因此,我们只能在分析器中使用预分配的内存。最简单的方法是将每个样本串行写入文件。我们使用一个固定大小的 hashmap 进行优化,该 hashmap 具有固定数量的桶,每个桶是一个具有固定数量的项的数组。如果 hashmap 已满,则弹出具有最小计数的项并将其写入临时文件。

已添加单元测试以确保样本函数中没有使用 malloc

futex 也不安全在信号处理程序中使用。因此,我们使用自旋锁来避免使用 futex

TODO

  1. 停止分析器后,恢复原始 SIGPROF 处理程序。

最低支持的 Rust 版本

Rust 1.64 或更高版本。

最低支持的Rust版本将来可能会改变,但将通过增加小版本号来实现。

许可证

FOSSA Status

依赖项

~7–21MB
~334K SLoC