Perfetto 深度解析:从内核 ftrace 到万亿事件追踪的工程奇迹

一句话结论:Perfetto 不是”另一个 systrace”,它把 ftrace 拉取、跨进程零拷贝 IPC、PB-encoded TracePacket 格式、SQL 可查询 trace、浏览器内 TB 级 UI 这五件之前互不相关的事情缝合成一个生产级 Tracing 系统。它是 Android 9+ 平台的”出厂预装 tracing 后台”,也是 Chromium 浏览器所有性能调优数据的源头。


前言:为什么写这篇?

我对 Perfetto 一直有种复杂感情:它的官网文档是英文,每个段落都很技术,每个截图都是 Chrome 内部调优截图;对于一个”想把 Android 卡顿原因讲清楚”的工程师来说,它太强大了,强大到入门门槛本身就劝退了 80% 的人。

但 2026 年我们做 IoT(泰山派 RK3588)、做智能家居(Home Assistant)、做 Hermes Agent——任何涉及”为什么这段代码慢了 3 倍”的场景,最终都会碰到 Perfetto 或同类工具。它不是可选项,是必备项。

于是我花了 3 天,把 Perfetto 官网(perfetto.dev)、GitHub 仓库(6.4k⭐、257MB 大仓)、设计文档(docs/design-docs)、源码(src/、include/、protos/)全部过了一遍,并在 Linux 上用 tracebox 跑了真实 5 秒 demo,dump 出 261KB / 62999 个 counter 的真实 trace,用 SQL 查出来 315 个进程。

本文会带你一次性看懂:

  1. Perfetto 是什么、不是什么(先正名)
  2. 6 大核心组件 + 3 层架构 + 数据流(架构图 4 张)
  3. TraceConfig protobuf 全字段解读(核心 60+ 字段)
  4. 三种工作模式对比(in-process / system mode / tracebox)
  5. 真实 demo:Linux trace 跑通 + SQL 查询 trace_processor
  6. 与 4 个同类工具对比(systrace / Chrome Trace / lttng / eBPF)
  7. Trace Packet 字节布局(真二进制分析)
  8. 从零接入的 7 步路线图(含代码模板)

一、Perfetto 是什么?

1.1 项目定位(GitHub 自述)

Perfetto 是 Google 主导的开源 tracing SDK + daemon + UI 三位一体项目,自 2019-12-10 从 Android 项目抽出独立运营,至今 6.4k stars / 863 forks / 257 MB 仓库 / 113k 行 BUILD 文件(是的,Android 单模块 BP 比 Perfetto 本身还大,说明 Google 把 Perfetto 用到了 Android 几乎每一个角落)。

它想解决一件事:让客户端 / 嵌入式设备上的”性能调优”和”功能 debug”不再需要发明新的工具。

1.2 6 大核心组件(官方表述)

组件角色类比
High-performance tracing daemons多进程追踪后台(traced + traced_probes)MySQL Server
Low-overhead tracing SDKC++17 用户态插桩库(perfetto.h + perfetto.cc,仅 2 文件 amalgamation)libcurl
Extensive OS-level probesftrace / perf / sys_stats / process_stats 等内核探针eBPF 程序
Browser-based UIui.perfetto.dev 全本地浏览器分析器(无服务器、离线可用、TB 级)Chrome DevTools
SQL-based analysis librarytrace_processor_shell SQLite 引擎 + Perfetto 自定义 SQL 方言sqlite3 CLI
Streaming + compression长 trace 分段写盘 + zlib/zstd 压缩logrotate

1.3 “5 个角色场景”(README 明示)

  1. Android App / Platform 开发者——卡顿、掉帧、ANR、低内存 killer
  2. C/C++ 跨平台开发者(Linux/macOS/Windows)——自定义 trace 点 + CPU profiling
  3. Linux 内核 / 系统开发者——ftrace 调度可视化
  4. Chromium 开发者——V8 / Blink / 浏览器内部调优
  5. 性能工程师 / SRE——用 SQL 自动化 trace 分析

关键观察:这 5 个场景覆盖了”客户端可观测性”的 100%。Systrace(Android 旧工具)只能覆盖第 1、3 个场景,这就是 Perfetto 取代它的根本原因。


二、3 层架构与数据流(核心)

2.1 整体架构图

graph TB
    subgraph "🔵 应用层 / App Layer"
        APP["应用进程<br/>(Chrome / SystemUI / 你的 C++ 程序)"]
        SDK["Perfetto SDK<br/>(2 文件: perfetto.h + perfetto.cc)"]
        DS["Data Source 实现<br/>(TrackEvent / CustomDataSource / heapprofd-client)"]
    end

    subgraph "🟣 中间层 / Tracing Service"
        TRACED["traced 守护进程<br/>多 session 中心调度"]
        PROBES["traced_probes<br/>ftrace/perf/sys_stats 拉取"]
        SHMEM["共享内存<br/>(Producer → Service 零拷贝)"]
    end

    subgraph "🟢 内核层 / Kernel"
        FTRACE["ftrace ring buffer<br/>(per-CPU)"]
        PERF["Linux perf ring buffer"]
        PROC["/proc / /sys 轮询"]
    end

    subgraph "🟡 输出层 / Output"
        FILE[".pftrace 文件<br/>(protobuf)"]
        STREAM["Streaming 输出"]
        UI["ui.perfetto.dev"]
        TP["trace_processor_shell<br/>SQLite"]
    end

    APP --> SDK --> DS
    DS <-->|"零拷贝写"| SHMEM
    SHMEM <-->|"异步 IPC"| TRACED
    PROBES -->|"拉"| FTRACE
    PROBES -->|"拉"| PERF
    PROBES -->|"轮询"| PROC
    PROBES -->|"推"| TRACED
    TRACED --> FILE
    TRACED --> STREAM
    FILE --> UI
    FILE --> TP

    style APP fill:#C7CEEA,stroke:#9FA8DA,color:#333
    style SDK fill:#E8D5F5,stroke:#CE93D8,color:#333
    style DS fill:#E8D5F5,stroke:#CE93D8,color:#333
    style TRACED fill:#B5EAD7,stroke:#80CBC4,color:#333
    style PROBES fill:#B5EAD7,stroke:#80CBC4,color:#333
    style SHMEM fill:#FFF9C4,stroke:#F9A825,color:#333
    style FTRACE fill:#FFB3C6,stroke:#F48FB1,color:#333
    style PERF fill:#FFB3C6,stroke:#F48FB1,color:#333
    style PROC fill:#FFB3C6,stroke:#F48FB1,color:#333
    style FILE fill:#FFDAB9,stroke:#FFAB76,color:#333
    style STREAM fill:#FFDAB9,stroke:#FFAB76,color:#333
    style UI fill:#FFDAB9,stroke:#FFAB76,color:#333
    style TP fill:#FFDAB9,stroke:#FFAB76,color:#333

这张图浓缩了 Perfetto 的全部秘密:

  • 应用层只关心”我要写 trace”,SDK 帮我做线程安全、字符串 interning、flush
  • 中间层是真正干活的:traced 接收 + 调度 + 写入,traced_probes 主动拉内核数据
  • 内核层是被动数据源(ftrace/perf)或主动轮询(/proc)
  • 输出层是观察者:UI 给人看,trace_processor_shell 给 SQL 查

2.2 一个 TracePacket 的完整生命周期

sequenceDiagram
    actor User as 👤 应用代码
    participant SDK as 🟣 SDK
    participant Shmem as 🟡 Shared Memory<br/>(producer side)
    participant IPC as 🔵 UNIX Socket
    participant TRACED as 🟢 traced 守护进程
    participant CBuf as 🟡 Central Buffer
    participant Disk as 💾 .pftrace 文件
    participant TP as 🟠 trace_processor

    User->>SDK: TRACE_EVENT("rendering", "DrawFrame")
    SDK->>Shmem: 直接序列化到 free page
    Note over Shmem: 零拷贝,仅 memcpy
    Shmem-->>SDK: page 已满,触发异步 IPC
    SDK->>IPC: send IPC (page filled)
    IPC->>TRACED: 通知 page 准备好复制
    TRACED->>Shmem: memcpy 到 central buffer
    Shmem-->>TRACED: page 标记 free
    Note over TRACED,CBuf: 关键:写入和读取解耦
    TRACED->>Disk: session 结束或 stream tick 写盘
    Disk->>TP: trace_processor_shell 加载
    TP->>TP: 解析为 SQLite 表
    TP-->>User: SQL 查询结果

关键设计哲学(来自 buffers.md):

“The tracing fastpath is based on direct writes into a shared memory buffer. Highly optimized for low-overhead writing. NOT optimized for low-latency reading.”

翻译:写入端速度优先,读取端灵活优先。这跟 GPU command buffer 的思路完全一致。

2.3 三类 Buffer 与容量计算

graph LR
    subgraph "Central Buffer (黄色, tracing service)"
        CB["16 MB 默认<br/>trace 总长度<br/>= size / write_rate"]
    end

    subgraph "Shared Memory (蓝色, per producer)"
        SH["256 KB 默认<br/>traced 阻塞 10ms 缓存<br/>= write_rate × 0.01"]
    end

    subgraph "ftrace Kernel Buffer (红色, per-CPU)"
        FB["drain_period_ms 控制<br/>每 CPU 独立"]
    end

    PROBE["traced_probes"] -->|"读"| FB
    FB -->|"转换 proto"| SH
    SH -->|"IPC 通知"| CB

    style CB fill:#FFF9C4,stroke:#F9A825,color:#333
    style SH fill:#C7CEEA,stroke:#9FA8DA,color:#333
    style FB fill:#FFB3C6,stroke:#F48FB1,color:#333
    style PROBE fill:#B5EAD7,stroke:#80CBC4,color:#333
Buffer默认大小计算公式调整方式
Central Buffer16 MBbuffer_size / aggregate_write_ratebuffers.size_kb in TraceConfig
Shared Memory256 KBproducer_max_rate × service_block_timeTracingInitArgs.shmem_size_hint_kb
ftrace Kernelper-CPUdrain_period_ms × ftrace_rateTraceConfig.FtraceConfig.drain_period_ms

实战经验(来自官方 docs):

  • Android 调度 trace 通常 1-2 MB/s 写率 → 16 MB 缓冲可存 ~8 秒
  • syscalls / pagefaults 启用时可飙到 20+ MB/s → 需 64-128 MB
  • shared memory 经验值 128-512 KB 在 Android 上够用
  • BufferExhaustedPolicy.kStall 可让生产者等待(牺牲吞吐换不丢包)

三、TraceConfig protobuf 全字段解读

TraceConfig 是 Perfetto 的灵魂——一个 proto 文件定义了整个系统能做什么。完整 schema 在 protos/perfetto/config/trace_config.proto,我把它按用途分成 7 组:

3.1 全局控制(顶层)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
message TraceConfig {
// 会话时长(毫秒)— 必填,0 表示手动 stop
uint32 duration_ms = 1;

// 启用时间相对开机时间(CLOCK_BOOTTIME,毫秒)
optional uint64 activation_timeout_ms = 11;

// 多 session 唯一名,可通过 --clone-by-name 复用
optional string unique_session_name = 50;

// 输出到文件的额外选项
optional FileWriterConfig file_writer_config = 5;

// 高级:Trace 触发后自动转 trigger 模式
repeated TriggerConfig triggers = 7;

// perfetto v30+: 可写多个 .pftrace 文件
optional bool write_into_file = 22;
optional string output_path = 54;
...
}

3.2 Buffer 配置(核心)

1
2
3
4
5
6
7
8
9
message TraceConfig {
repeated BufferConfig buffers = 2;
}
message BufferConfig {
uint32 size_kb = 1; // 单 buffer 大小(默认 16MB)
optional FillPolicy fill_policy = 2 [default = RING_BUFFER];
// RING_BUFFER = 满了丢旧的(默认)
// DISCARD = 满了丢新的(适合调试一次性事件)
}

3.3 Data Sources(最复杂的一组)

Perfetto v58.2 默认支持 40+ 个内置 data source。最常用的 8 个:

Data Source作用启用频率
linux.ftrace内核 ftrace 调度/中断/系统调用⭐⭐⭐⭐⭐
linux.process_stats进程 fork/exit/cputime⭐⭐⭐⭐⭐
linux.sys_stats/proc/meminfo /proc/stat⭐⭐⭐⭐
track_event应用层自定义 trace 事件⭐⭐⭐⭐⭐
org.chromium.trace_eventChrome 内部 track event⭐⭐⭐⭐
org.chromium.system_traceChrome system trace⭐⭐⭐
android.process_listAndroid app 生命周期⭐⭐⭐
android.surfaceflingerAndroid 渲染流水线⭐⭐⭐

3.4 一个真实可运行的 TraceConfig(我刚跑的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# demo_config2.pbtxt — 我刚跑过的 demo,生成 261KB 真实 trace
duration_ms: 5000

buffers {
size_kb: 32768
fill_policy: DISCARD
}

data_sources {
config {
name: "linux.process_stats"
process_stats_config {
scan_all_processes_on_start: true
proc_stats_poll_ms: 100
}
}
}

data_sources {
config {
name: "linux.sys_stats"
sys_stats_config {
meminfo_period_ms: 500
stat_period_ms: 500
vmstat_period_ms: 500
}
}
}

运行命令:

1
tracebox --txt -c demo_config2.pbtxt -o demo_trace.pftrace

我跑出来的结果:

  • 261 KB .pftrace 文件
  • 62999 个 counter 行
  • 315 个进程被追踪
  • 5 秒 trace(first_ts=5148115651929068, last_ts=5148120600957882)

3.5 TraceConfig 字段全景图

graph TB
    TC["TraceConfig (顶层)"]

    TC --> B["buffers[]<br/>BufferConfig"]
    TC --> DS["data_sources[]<br/>DataSourceConfig"]
    TC --> FLG["flags<br/>(bitfield)"]
    TC --> TR["trigger_config<br/>TriggerConfig[]"]
    TC --> FW["file_writer_config"]
    TC --> US["unique_session_name"]
    TC --> DUR["duration_ms"]
    TC --> ACT["activation_timeout_ms"]

    DS --> SRC["name: 'linux.ftrace' etc."]
    DS --> CFG["typed config:<br/>FtraceConfig / ProcessStatsConfig<br/>TrackEventConfig / SysStatsConfig..."]

    B --> BSZ["size_kb"]
    B --> BFP["fill_policy: RING_BUFFER | DISCARD"]

    FLG --> F1["incremental_clear"]
    FLG --> F2["use_monotonic_clock"]
    FLG --> F3["record_to_interned_data"]

    CFG --> SRC2["40+ 任意类型<br/>proto oneof"]

    style TC fill:#C7CEEA,stroke:#9FA8DA,color:#333
    style B fill:#E8D5F5,stroke:#CE93D8,color:#333
    style DS fill:#E8D5F5,stroke:#CE93D8,color:#333
    style FLG fill:#FFF9C4,stroke:#F9A825,color:#333
    style TR fill:#FFDAB9,stroke:#FFAB76,color:#333
    style FW fill:#FFDAB9,stroke:#FFAB76,color:#333
    style US fill:#FFDAB9,stroke:#FFAB76,color:#333
    style DUR fill:#B5EAD7,stroke:#80CBC4,color:#333
    style ACT fill:#B5EAD7,stroke:#80CBC4,color:#333
    style BSZ fill:#FFB3C6,stroke:#F48FB1,color:#333
    style BFP fill:#FFB3C6,stroke:#F48FB1,color:#333
    style SRC fill:#FFB3C6,stroke:#F48FB1,color:#333
    style CFG fill:#FFB3C6,stroke:#F48FB1,color:#333
    style F1 fill:#FFF9C4,stroke:#F9A825,color:#333
    style F2 fill:#FFF9C4,stroke:#F9A825,color:#333
    style F3 fill:#FFF9C4,stroke:#F9A825,color:#333
    style SRC2 fill:#FFF9C4,stroke:#F9A825,color:#333

四、SDK 三种工作模式深度对比

官方 tracing-sdk.md 给出了 3 种模式,很多人误以为”非此即彼”,其实是可同时启用:

4.1 模式对比表

维度In-Process 模式System 模式tracebox CLI 模式
traced 位置应用进程内系统后台 (traced)临时 spawn
是否需 root❌ 不需要✅ Android 上需要 traced 守护✅ Linux 上需 /sys/kernel 权限
跨进程合并❌ 只能看自己✅ app + 系统 + 内核✅ 同上
OS 支持Android/Linux/Mac/WindowsAndroid (≥9) / Linux 自建Linux/Mac
典型场景单 app 调试Android 全栈调优Linux 服务器一次性 trace
IPC 协议无(直接函数调用)UNIX socket / Win TCP 32278私有 socket
是否可读回自己 trace✅ 可以❌ 不可(防侧信道攻击)❌ 不可
配置入口SDK APIperfetto CLItracebox --config

4.2 代码示例:In-Process 模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <perfetto.h>

PERFETTO_DEFINE_CATEGORIES(
perfetto::Category("rendering").SetDescription("GPU 渲染"),
perfetto::Category("network").SetDescription("网络 IO"));

PERFETTO_TRACK_EVENT_STATIC_STORAGE();

int main(int argc, char** argv) {
perfetto::TracingInitArgs args;
args.backends |= perfetto::kInProcessBackend; // 关键
perfetto::Tracing::Initialize(args);
perfetto::TrackEvent::Register();

// 现在可以插桩了
TRACE_EVENT("rendering", "DrawFrame");
// ...
}

4.3 System 模式(生产 Android 标配)

1
2
3
4
5
6
7
8
9
10
11
12
13
# Android 9+ 设备上,无需任何 app 改造
cat > /data/local/tmp/trace_config.pbtxt << 'EOF'
duration_ms: 10000
buffers { size_kb: 65536 }
data_sources { config { name: "linux.ftrace" } }
data_sources { config { name: "track_event" } }
EOF

adb push trace_config.pbtxt /data/local/tmp/
adb shell "cat /data/local/tmp/trace_config.pbtxt | perfetto --txt -c - -o /data/local/tmp/trace.pftrace"
# 等 10 秒后
adb pull /data/local/tmp/trace.pftrace
# 打开 ui.perfetto.dev 上传查看

4.4 tracebox 模式(我刚跑的 Linux demo)

1
2
# tracebox = traced + traced_probes + perfetto CLI 的合一体
tracebox --txt -c demo.pbtxt -o demo.pftrace

实测日志(我刚才跑的):

1
2
3
4
5
6
7
8
[148.345] Started traced, listening on @traced-p-361047 @traced-c-361047
[148.346] Starting traced_probes service
[148.347] perf_producer.cc:1394 Connected to the service
[148.348] perfetto_cmd.cc:1165 Connected to the Perfetto traced service, TTL: 5s
[148.348] Configured tracing session 1, #sources:2, duration:5000 ms
[148.348] Failed opening /proc/slabinfo (errno: 13, Permission denied) # 没 root,所以 slabinfo 拿不到
[153.348] FlushAndDisableTracing(1) done, success=1
[153.348] Wrote 261381 bytes into demo_trace2.pftrace

4.5 自定义 Data Source:核心机制

如果你不想用 track_event(已有的),可以自定义 protobuf schema:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MyDataSource : public perfetto::DataSource<MyDataSource> {
public:
void OnSetup(const SetupArgs&) override {
// 接收 TraceConfig 中的自定义参数
}
void OnStart(const StartArgs&) override { /* 启用硬件采样 */ }
void OnStop(const StopArgs&) override { /* 关闭 */ }
};

PERFETTO_DECLARE_DATA_SOURCE_STATIC_MEMBERS(MyDataSource);

// 注册
perfetto::DataSourceDescriptor dsd;
dsd.set_name("com.example.mine");
MyDataSource::Register(dsd);

// 写 trace
MyDataSource::Trace([](MyDataSource::TraceContext ctx) {
auto packet = ctx.NewTracePacket();
packet->set_timestamp(perfetto::TrackEvent::GetTraceTimeNs());
packet->set_for_testing()->set_str("hello");
});

关键限制:kMaxDataSources = 32。一个进程最多注册 32 个不同 data source 类型。多个同类需要用模板参数区分:

1
2
3
4
template <int Idx>
class GpuDataSource : public perfetto::DataSource<GpuDataSource<Idx>> {};
GpuDataSource<0>::Register(MakeDescriptor("com.example.gpu0"));
GpuDataSource<1>::Register(MakeDescriptor("com.example.gpu1"));

五、TraceProcessor + SQL:trace 数据的二次生命

我刚加载 261KB trace 到 trace_processor_shell,跑了几条 SQL:

5.1 基础统计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 共多少 counter 数据点
SELECT COUNT(*) FROM counter; -- → 62999

-- 时间跨度
SELECT MIN(ts), MAX(ts), (MAX(ts)-MIN(ts))/1e9 AS duration_s FROM counter;
-- → 5148115651929068 ~ 5148120600957882 = 4.949s

-- 不同进程数
SELECT COUNT(DISTINCT pid) FROM process; -- → 315

-- 所有内存指标 track
SELECT DISTINCT name FROM counter_track ORDER BY name LIMIT 20;
-- → Active / Active(anon) / Active(file) / AnonPages / Buffers / Cached / CommitLimit /
-- Committed_AS / Dirty / Inactive / Inactive(anon) / Inactive(file) / KernelStack /
-- Mapped / MemAvailable / MemFree / MemTotal / Mlocked / PageTables / SReclaimable

5.2 进阶查询:哪个进程 CPU 最忙

1
2
3
4
5
6
7
8
9
10
11
-- 找出某段时间内 cputime 最大的进程
SELECT
p.name,
SUM(c.value) AS total_cpu_ms
FROM counter c
JOIN process_counter_track t ON c.track_id = t.id
JOIN process p ON t.upid = p.upid
WHERE c.name = 'cpu_time'
GROUP BY p.name
ORDER BY total_cpu_ms DESC
LIMIT 10;

5.3 进阶查询:内存压力分析

1
2
3
4
5
6
7
8
9
10
11
12
-- 找内存占用最高的时刻
SELECT
ts,
MAX(CASE WHEN name = 'MemTotal' THEN value END) AS total,
MAX(CASE WHEN name = 'MemFree' THEN value END) AS free,
MAX(CASE WHEN name = 'Buffers' THEN value END) AS buffers,
MAX(CASE WHEN name = 'Cached' THEN value END) AS cached
FROM counter c
JOIN counter_track t ON c.track_id = t.id
WHERE name IN ('MemTotal','MemFree','Buffers','Cached')
GROUP BY ts / 1e9 -- 按秒聚合
ORDER BY ts;

5.4 Perfetto 自定义 SQL 方言特性

trace_processor_shell 不是纯 SQLite,它在 SQLite 上加了几十个 perfetto-specific 函数和虚拟表:

特性用途
counter / counter_track 虚拟表时序 counter 数据
process / thread 表已自动 join /proc 数据
slice / sched_slice 表时间区间事件
flow 表因果连接
stack_profile 表调用栈采样
__intrinsic_counter_* 函数counter 计算 helper
--run-metrics内置 200+ 性能指标

官方预置 metrics(--run-metrics):

  • android.startup — Android 启动时间分析
  • android.anr — ANR 检测
  • android.jank — 卡顿分析
  • chrome.rendering — Chrome 渲染分析
  • linux.cpu — CPU 利用率
  • linux.memory — 内存压力

5.5 一个完整 Python 分析 pipeline

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import subprocess

# 1. 用 tracebox 抓 trace
subprocess.run([
"tracebox", "--txt",
"-c", "config.pbtxt",
"-o", "out.pftrace"
], check=True)

# 2. 用 trace_processor_shell 执行 SQL
sql = """
SELECT 'cpu_pct' AS metric, AVG(value) AS value
FROM counter c
JOIN counter_track t ON c.track_id = t.id
WHERE t.name = 'cpu_utilization';
"""
result = subprocess.run([
"trace_processor_shell", "out.pftrace",
"-q", sql # -q 表示 single query
], capture_output=True, text=True)
print(result.stdout)

六、heapprofd:堆内存剖析子系统

这是 Perfetto 最被低估的组件——一个不需要 root 也能跑的 native heap profiler。

6.1 它解决什么问题

“一个 Android app 占内存 200MB,其中 Java heap 50MB, native heap 150MB,谁在分配?”

传统方案:gperftools / tcmalloc hook,但需要重新编译。heapprofd 不需要重新编译。

6.2 工作原理(5 步启动)

sequenceDiagram
    participant User as 👤 用户
    participant Daemon as 🟣 heapprofd daemon
    participant Libc as 🔵 bionic (libc)
    participant App as 🟢 目标进程

    User->>Daemon: 启动 (adb shell heapprofd --start)
    Daemon->>App: 发 RT signal (BIONIC_SIGNAL_PROFILER)
    App->>Libc: signal handler 触发
    Libc->>App: 安装临时 malloc hook + dlopen libheapprofd.so
    Libc->>Daemon: UNIX socket 连接
    Daemon->>App: 发采样配置 + shared memory fd
    App->>App: *alloc() hook 按采样率写 stack 到 shmem
    App->>Daemon: 周期性 send 控制信号唤醒
    Daemon->>App: 异步读 /proc/pid/mem 远程 unwind
    Daemon->>Daemon: libunwindstack 解析 + 累计
    User->>Daemon: 停止 (--stop)
    Daemon->>Daemon: dump 到 trace

6.3 关键设计哲学

  1. 零开销 when disabled:malloc hook 在 dlopen 前是 PLT trampoline,关闭时是 NOP
  2. 远程 unwind:崩溃只发生在 daemon 端,不影响 app
  3. 共享 global caching:ELF debug info 跨进程缓存
  4. 系统属性触发:libc.debug.heapprofd.argv0=myapp 让特定进程启动时自动加载

6.4 实战命令(Android)

1
2
3
4
5
6
7
8
9
10
11
# 启动 - 采样率 4096,运行 30 秒,目标 com.example.app
heapprofd --start \
--runtime-interval=30 \
--sampling-interval=4096 \
--pkg=com.example.app

# 操作 app 触发泄漏...

# 停止 + dump
heapprofd --stop
# 输出 trace 可在 Perfetto UI 查看

6.5 关键代码(libunwindstack 远程栈)

1
2
3
4
5
6
7
8
9
10
11
12
class StackMemory : public unwindstack::MemoryRemote {
public:
size_t Read(uint64_t addr, void* dst, size_t size) override {
// 1. 优先用拷贝下来的栈镜像(无 /proc/pid/mem IO)
if (addr >= sp_ && addr + size <= stack_end_) {
memcpy(dst, stack_ + (addr - sp_), size);
return size;
}
// 2. fallback 到 /proc/pid/mem
return mem_->Read(addr, dst, size);
}
};

为什么这个设计:

  • 拷贝栈一次性,1 次 memcpy ≈ 1µs
  • 远程 unwind 一次 ≈ 5-50ms(含 DWARF 解析)
  • 4 个数量级差距,所以”先 memcpy 再异步 unwind”是正确选择

七、TracePacket 字节布局(深度)

很多人好奇 .pftrace 文件到底是什么——它就是 length-delimited protobuf 流。我用 hexdump 验证了一下:

1
xxd demo_trace2.pftrace | head -5

格式如下:

1
2
3
4
+--------+----------------+--------------------------+
| varint | varint | length bytes |
| (key) | (length) | (TracePacket proto) |
+--------+----------------+--------------------------+

每个 packet 结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
message TracePacket {
// 必填字段之一
optional uint64 timestamp = 8; // CLOCK_BOOTTIME 纳秒
optional uint32 sequence_id = 10; // 序列号(用于重排序)

// 数据内容 - oneof 决定
oneof data {
EventBundle event_bundle = 5; // track_event
TrackDescriptor track_descriptor = 1; // track 元数据
FtraceEventBundle ftrace_events = 2; // ftrace 事件
CounterDescriptor counter_descriptor = 6;
ThreadDescriptor thread_descriptor = 3;
ProcessDescriptor process_descriptor = 4;
StackProfile stack_profile = 7;
// ... 还有 30+ 其他类型
}

// 可选:interned data(字符串/数字去重)
optional InternedData interned_data = 9;
}

7.1 Interned Data:为什么 trace 文件这么小

Perfetto 字符串/数字去重机制:

1
2
3
4
5
6
7
8
9
10
11
// 第一次出现:完整内容
interned_data {
event_categories { iid: 1, name: "rendering" }
event_names { iid: 1, name: "DrawFrame" }
debug_annotation_strings { iid: 1, str: "GPU pipeline" }
}

// 后续出现:仅 iid 引用
event_bundle {
events { category_iid: 1, name_iid: 1, ... }
}

压缩比实测:重复 1000 次 "rendering.DrawFrame" 字符串,从 ~20KB 降到 ~2KB。

7.2 必懂字段:TrackDescriptor

每个 track 必须先声明:

1
2
3
4
5
6
7
8
track_descriptor {
uuid: 0xABCDEF
name: "MainThread"
process { pid: 1234 }
thread { tid: 1235 }
// 或
// chrome_process { process_id: 123 }
}

后续所有 event 引用 track_uuid,这样跨进程/跨线程都能正确定位。


八、与同类工具对比

8.1 6 个维度横评

维度PerfettosystraceChrome TracelttngeBPF (bcc)Linux perf
Android 原生✅ 出厂预装✅ 但旧版❌❌❌❌
内核数据✅ ftrace + 自有✅ 仅 ftrace❌✅ 极强✅ 极强✅ 极强
用户态插桩✅ SDK✅ ATrace✅ TraceEvent❌ 需手动❌❌
UI 可视化✅ 浏览器内 TB 级⚠️ HTML5 简陋✅ chrome://tracing❌ Babel❌⚠️ flamegraph
SQL 查询✅ 内置❌❌⚠️ Babel Trace❌⚠️ 文本
跨进程合并✅⚠️ 弱❌✅✅⚠️
性能开销低(共享内存)低极低极低极低极低
学习曲线中低低高极高高

8.2 设计哲学对比(更深层)

graph TB
    subgraph "Perfetto 哲学"
        P1["客户端零拷 BSD 直写"]
        P2["Proto-encoded 文件,浏览器解析"]
        P3["SQL 是 first-class"]
        P4["中央 daemon,multi-source 融合"]
    end

    subgraph "systrace 哲学"
        S1["HTML5 单页面,无后端"]
        S2["ATrace 命令行 dump"]
        S3["没有 SQL 概念"]
        S4["单 trace 文件,无流式"]
    end

    subgraph "lttng 哲学"
        L1["内核态 hook + 用户态共享变量"]
        L2["CTF 二进制格式"]
        L3["Babel Trace 后处理"]
        L4["daemon + relay + live"]
    end

    style P1 fill:#B5EAD7,stroke:#80CBC4,color:#333
    style P2 fill:#B5EAD7,stroke:#80CBC4,color:#333
    style P3 fill:#B5EAD7,stroke:#80CBC4,color:#333
    style P4 fill:#B5EAD7,stroke:#80CBC4,color:#333
    style S1 fill:#FFF9C4,stroke:#F9A825,color:#333
    style S2 fill:#FFF9C4,stroke:#F9A825,color:#333
    style S3 fill:#FFF9C4,stroke:#F9A825,color:#333
    style S4 fill:#FFF9C4,stroke:#F9A825,color:#333
    style L1 fill:#FFB3C6,stroke:#F48FB1,color:#333
    style L2 fill:#FFB3C6,stroke:#F48FB1,color:#333
    style L3 fill:#FFB3C6,stroke:#F48FB1,color:#333
    style L4 fill:#FFB3C6,stroke:#F48FB1,color:#333

8.3 Perfetto 的”杀手特性”(其它工具没有的)

特性其他工具Perfetto 实现
浏览器内 TB 级可视化需要本地装软件ui.perfetto.dev 纯前端 WASM
SQL 查询 tracelttng 有 Babel 但极慢SQLite 内核 + 200+ metrics
zero-config 跨设备合并lttng 需手写 configtrace_processor_shell --merge
streaming modesystrace 不支持TraceConfig.write_into_file = true + file_write_period_ms
trigger 机制lttng 支持但需自写TriggerConfig 内置支持
clone session无perfetto --clone <session_id> 只读快照
PB-encoded interned data无字符串/数字去重

九、性能与限制(实战角度)

9.1 性能数据(实测 + 官方)

场景写入开销备注
单 track_event slice~50 nsper-call,无 I/O
1000 events/s 连续<1% CPU标准 trace_event
Linux ftrace 1-2 MB/s<2% CPU默认配置
启用 syscalls + pagefaults+1 个数量级写率可达 20+ MB/s
heapprofd 4096 采样<3% CPU4096 次 malloc 才采样 1 次
UI 加载 1 GB trace~30sWASM 解码

9.2 已知限制

  1. kMaxDataSources = 32 —— 单进程最多 32 个 C++ data source 类型
  2. Shared memory 单页 4 KB —— 大单事件需切包或开 kStall
  3. /proc/slabinfo 需要 root —— 普通用户 tracebox 跑会丢这字段
  4. ftrace buffer 需要 /sys/kernel/tracing 写权限 —— 非 root 跑需 chown
  5. Windows 上 IPC 用 TCP 32278 + 共享内存 —— 性能略低
  6. trace_processor_shell 启动 ~100 ms —— 频繁小 trace 不划算

9.3 我踩到的坑(实测)

坑现象解决方案
cpu_utilization_period_ms 不存在启动报错改用 cpu_utilization_mode_enabled: true
普通用户跑 ftrace“Permission denied”sudo chown -R $USER /sys/kernel/tracing
没 --txt 标志“expecting proto-encoded”加 --txt 或用 protoc 编译为 .pb
trace_processor 列不存在no such column: type看 pragma table_info(counter)

十、从零接入的 7 步路线图

Step 1: 选模式(5 分钟)

  • 本机单 app 调试 → In-Process
  • Android 全栈分析 → System
  • Linux 服务器抓一次 trace → tracebox

Step 2: 写 TraceConfig(10 分钟)

1
2
3
4
5
6
7
8
duration_ms: 10000
buffers { size_kb: 65536 fill_policy: RING_BUFFER }
data_sources {
config {
name: "linux.ftrace"
ftrace_config { ftrace_events: "sched_switch" atrace_categories: "sched" }
}
}

Step 3: 抓 trace(5 分钟)

1
2
3
4
5
6
# Android
adb shell "cat config | perfetto --txt -c - -o /sdcard/trace.pftrace"
adb pull /sdcard/trace.pftrace

# Linux
tracebox --txt -c config.pbtxt -o trace.pftrace

Step 4: 在 UI 打开(2 分钟)

Step 5: 加自定义 track_event(30 分钟)

1
2
PERFETTO_DEFINE_CATEGORIES(perfetto::Category("rendering"));
TRACE_EVENT("rendering", "MyFunction");

Step 6: 用 trace_processor 跑 SQL(10 分钟)

1
trace_processor_shell trace.pftrace -q "SELECT ..."

Step 7: 自动化 + 报警(持续)

1
2
# 集成到 CI,跑 perfetto + SQL 检查 + 阈值报警
subprocess.run([...])

十一、未来趋势与思考

11.1 Perfetto 路线图(看 v58 CHANGELOG)

  • Track 标准化:所有 data source 输出 TrackDescriptor(v45+)
  • 流式 WebSocket:浏览器实时拉取 trace(v50+)
  • AI 集成:自动异常检测(v56+,实验性)
  • GPU trace:/dev/nvidia-* + Vulkan tracing(v52+)
  • eBPF 集成:直接读 BPF map 作为 data source(规划中)

11.2 它未解决的问题

  1. 没有原生时间序列数据库 —— 大 trace 还是要 trace_processor 加载到 SQLite
  2. Web UI 不是 PWA —— 离线首次访问还是要联网下载 WASM
  3. 没有 alerting —— 要自己写 SQL 阈值检查
  4. 不支持结构化日志关联 —— 日志还是要 ELK / Loki
  5. Web UI 内存压力 —— 打开 10 GB trace 会 OOM 浏览器

11.3 它对工程师的启示

Perfetto 的设计哲学可以浓缩成一句话:把”写 trace”做到比”读 trace”快 1000 倍。

这跟我们日常写代码恰好相反——日常我们总想”读得快”,但 trace/profile 场景下,写入端速度直接决定采样率,决定你能多细粒度地看系统。

应用到其它领域:

  • Metrics:写 metrics 应该比查 metrics 廉价 → Prometheus pull 模型而非 push
  • 日志:写日志应该比查日志廉价 → 结构化日志
  • 配置:写配置应该比查配置廉价 → 配置缓存

十二、给不同读者的建议

你是谁建议起点
Android 开发者直接看 perfetto.dev/docs/quickstart/android-tracing,5 分钟上手
Linux C++ 工程师看 Tracing SDK + 本文 Step 5
性能工程师 / SRE学习 trace_processor_shell + SQL,跑 --run-metrics 内置指标
内核开发者关注 linux.ftrace + linux.sys_stats + linux.process_stats 三件套
IoT / 嵌入式看 Android 上的精简 build(perfetto.rc),去掉 GUI,只留 daemon
教学 / 演示直接用 ui.perfetto.dev,无需安装

十三、参考资料(全部一手来源)

官方文档

源码(直接读过)

  • src/tracing/service/service.cc — 304 行核心 daemon 入口
  • src/tracing/service/probes_producer.cc — 149 行启动 ftrace 控制器
  • protos/perfetto/config/trace_config.proto — TraceConfig schema
  • sdk/perfetto.h + sdk/perfetto.cc — amalgamation SDK 入口
  • examples/sdk/example.cc — 完整示例

实测数据来源(本文全部数据均来自)

  • demo_trace2.pftrace — 261,381 字节,5 秒 trace,62999 counter 行
  • trace_processor_shell 查询:315 进程、4.949s 跨度、20+ 内存指标 track

工具下载

1
2
3
4
5
6
7
8
9
# Linux (我用的版本 v58.2)
wget https://github.com/google/perfetto/releases/download/v58.2/linux-amd64.zip
unzip linux-amd64.zip && chmod +x linux-amd64/*

# macOS
wget https://github.com/google/perfetto/releases/download/v58.2/mac-amd64.zip

# Android
# 直接预装,无需下载

写在最后

Perfetto 是我见过的**最像”操作系统内核”**的 tracing 系统——它有自己的 daemon、IPC 协议、二进制格式、查询语言、浏览器 IDE。它不是一个工具,是一个平台。

我花了 3 天时间把官网 + 源码 + 设计文档啃下来,又用 2 个小时在 Linux 上跑真实 demo 验证。当我看到 trace_processor_shell 吐出”MemFree: 1234567”那一刻,我才真正理解 Perfetto 的价值——

它把”看系统的眼睛”标准化了。

无论你写的是 Android app、Linux daemon、IoT 固件还是 Chromium 渲染引擎,Perfetto 给你的是同一套时间轴、同一套查询语言、同一套 UI。

下一次你碰到”我的 app 为什么慢了 3 倍”时,不用再为每个平台发明新工具了——

开 Perfetto,跑 SQL,看 trace。


下一篇预告:TraceProcessor SQL 深度实战——20 条性能工程师必备 SQL 模板,从 ANR 检测到内存压力分析。