学习记录:Perfetto工具_2 TraceConfig 了解

15 阅读12分钟

原文

Trace configuration - Perfetto Tracing Docs

下半部分是该文档翻译,主要描述Trace Config中的关键字段等等,

Trace configuration(痕迹、轨迹追踪 配置)

与许多“一直开着”的日志系统(例如 Linux 的 rsyslog、Android 的 logcat)不同,Perfetto 默认让所有数据源处于空闲状态,只有在收到明确指令时才开始记录数据。

一个简单的 trace 配置长这样:

duration_ms: 10000          # 总共采 10 秒
buffers {
  size_kb: 65536            # 给 64 MB 环形缓冲区
  fill_policy: RING_BUFFER
}
data_sources {
  config {
    name: "linux.ftrace"    # 启用 ftrace 数据源
    target_buffer: 0
    ftrace_config {
      ftrace_events: "sched_switch"
      ftrace_events: "sched_wakeup"
    }
  }
}

使用方法:

perfetto --txt -c config.pbtx -o trace_file.perfetto-trace
  • --txt 告诉 perfetto 这个配置是文本格式(pbtx)。
  • -c 后面跟配置文件,-o 后面跟输出 trace 文件。

★ 提示:仓库里 /test/configs/ 目录下有更完整的配置示例。

注意:如果在 Android 上用 adb 抓 trace 遇到问题,请看下文“Android 注意事项”部分。

TraceConfig (轨迹配置)

TraceConfig 是一个 protobuf 消息,它定义了下面三件事:

  1. 整个 tracing 系统的全局行为,例如:

    • 最大 trace 时长
    • 内存缓冲区个数及每块大小
    • 输出文件的最大体积
  2. 要启用哪些数据源,以及每个数据源自己的参数,例如:

    • 对内核数据源(ftrace)要打开哪些事件
    • 对堆分析器要指定目标进程名、采样率

具体怎么配各个内置数据源,见 “data sources”章节。

  1. “数据源 → 缓冲区”映射:指定每个数据源把数据写进哪块缓冲区(见下文 buffers 节)。

tracing 服务(traced)充当“配置分发器”:

它从 perfetto 命令行客户端(或其他 Consumer)收到一份 TraceConfig,然后把配置里的不同部分转发给各个已连接的 Producer。

当 Consumer 启动一次 trace 会话时,traced 会:

  • 读取 TraceConfig 的外层字段(如 duration_ms、buffers 等),决定自己的运行时行为;
  • 读取 data_sources 列表。对每个列出的数据源,如果已有同名的 Producer 注册(例如示例里的 "linux.ftrace"),traced 就让该 Producer 启动对应数据源,并把 data_sources 里对应的子配置整块原样传给它(详见后向/前向兼容章节)。

image.png

Buffers(缓冲区)

buffers 段定义 tracing 服务拥有的内存缓冲区的数量、大小以及填满策略。示例:

# 缓冲区 buffer_0
buffers {
  size_kb: 4096
  fill_policy: RING_BUFFER  环形缓冲区
}

# 缓冲区 buffer_1
buffers {
  size_kb: 8192
  fill_policy: DISCARD  写满即丢弃
}

每个缓冲区都有一个 fill_policy(填满策略):

  • RING_BUFFER(默认) :环形缓冲区。写满后从头覆盖最旧的数据。
  • DISCARD :写满即丢弃。后续写入直接丢掉,不再接受新数据。

⚠️ 注意:

DISCARD 对某些“只在 trace 结束时才提交数据”的数据源会产生意外副作用——如果缓冲区在它们提交前就满了,这些数据会永远丢失。

一份有效的 TraceConfig 至少要定义一个缓冲区。

最简单的情况:所有数据源都把数据写进同一块缓冲区。

对大多数基本场景够用,但如果各数据源写入速率差异很大,就会出问题。

举例:

假设一份配置同时打开:

  1. 内核调度器跟踪:典型 Android 手机上约 1 万事件/秒,持续写 1 MB/s。
  2. 内存定时采样:每 5 秒读一次 /proc/meminfo,每次约 100 KB。

若两者共用一块 4 MB 的缓冲区:

在 5 秒的采样间隔里,调度事件已把缓冲区写满并覆盖了好几轮,导致内存快照被挤掉。

结果:大多数 trace 里根本看不到内存数据,即使第二个数据源工作正常。

→ 解决办法:给低速数据源单独分配一块缓冲区,避免被高速数据源冲掉。

Dynamic buffer mapping(动态缓冲区映射)

Perfetto 里 “数据源 → 缓冲区” 的映射是动态可配的。

最简单的情况:只定义一块缓冲区,所有数据源默认都往里面写。

但在前面的例子里,把不同速率的数据源分开写会更安全

做法:用 TraceConfig 里每个数据源的 target_buffer 字段指定它要写入第几块缓冲区。

示例对应关系:

image.png

数据源

  1. 调度器跟踪(高速)
  2. /proc/meminfo 轮询(低速)
  3. 堆分析器(中低速)

缓冲区

  • Buffer #0:4 MB
  • Buffer #1:8 MB

配置片段:

data_sources {
  config {
    name: "linux.ftrace"
    target_buffer: 0          // ← 写入 4 MB 的 Buffer #0
    ftrace_config { ... }
  }
}

data_sources {
  config {
    name: "linux.sys_stats"
    target_buffer: 1          // ← 写入 8 MB 的 Buffer #1
    sys_stats_config { ... }
  }
}

data_sources {
  config {
    name: "android.heapprofd"
    target_buffer: 1          // ← 也写进 8 MB 的 Buffer #1
    heapprofd_config { ... }
  }
}

这样高速的调度事件只在 Buffer #0 里环形覆盖,不会把低速的内存快照和堆数据冲掉。

PBTX vs Binary Format(二进制格式)

给 perfetto 命令行传配置时有两种办法:

1. 文本格式(.pbtx)

-   适合人手写、临时调试。
-   文件是 ProtoBuf Textual Representation(后缀常叫 .pbtx 或 .config)。
-   命令里加 --txt 告诉 perfetto 这是文本:
perfetto -c /path/to/config.pbtx --txt -o trace_file.perfetto-trace

注意:

--txt 只有 Android 10(Q)及以后 才支持;老版本只能用二进制。

– 不要让脚本、CI、benchmark 工具生成 PBTX,否则字段改名、枚举值变化就可能炸。

2. 二进制格式

-   机器对机器(M2M)的正式方案。
-   先把 PBTX 用官方 protoc 编译成二进制:
cd ~/code/perfetto          # 或 Android 源码 external/perfetto
protoc --encode=perfetto.protos.TraceConfig \
       -I. protos/perfetto/config/perfetto_config.proto \
       < config.pbtx \
       > config.bin
-   然后 去掉 --txt 直接喂给 perfetto:
perfetto -c config.bin -o trace_file.perfetto-trace

总结:

人眼看/手写 → PBTX + --txt

脚本/工具/长期维护 → 二进制 .bin,用 protoc 提前转好。

Streaming long traces(长 trace 的流式写入)

Perfetto 默认把整个 trace 缓冲区一直放在内存里,等 tracing 会话结束时才一次性写进 -o 指定的文件。 这样做能减少运行时对性能的影响,但 trace 的最大体积就被物理内存大小卡住了,常常不够用。 流式写入场景,跑 benchmark 或难复现的 bug 时,往往需要抓比内存大得多的 trace,宁可牺牲一点 I/O 性能也要把数据持续刷到磁盘。

Perfetto 支持周期性地把缓冲区内容“倒”进目标文件(或 stdout),关键字段:

  • write_into_file: true

开启周期刷盘。此时用户空间缓冲区只需容纳两次刷盘间隔之间产生的数据即可。 典型 trace 数据率约 1–4 MB/s,16 MB 缓冲区大约能扛 4 秒,再久就可能丢数据。

  • file_write_period_ms: 3000

(可选)把默认 5 s 的刷盘间隔改短。间隔越短→所需缓冲区越小,但对性能干扰越大。如果设成 < 100 ms,系统会强制按 100 ms 执行。

  • max_file_size_bytes: 1_000_000_000

(可选)写到指定字节数就自动停 trace,用来给文件大小设上限。

完整示例配置见仓库:

/test/configs/long_trace.cfg 一句话:打开 write_into_file,按需调 file_write_period_ms 和 max_file_size_bytes,就能边抓边落盘,不再受内存大小限制。

Data-source specific config(数据源专属配置)

除了 整个 trace 共用的参数,TraceConfig 里还能给每个数据源单独下指令。

在 proto 模式(data_source_config.proto)里,这部分放在 TraceConfig 的 DataSourceConfig 消息内:

message TraceConfig {
  repeated DataSource data_sources = 2;   // 下面会用到
}

message DataSource {
  optional protos.DataSourceConfig config = 1;  // 真正的数据源配置
}

message DataSourceConfig {
  optional string name = 1;
  optional FtraceConfig        ftrace_config        = 180 [lazy = true];
  optional AndroidPowerConfig  android_power_config = 106 [lazy = true];
  …
}
  • ftrace_configandroid_power_config 等等都是数据源专属的子配置。
  • tracing 服务完全不管这些字段里写了什么,整块 DataSourceConfig 原封不动地转发给注册同名的数据源。

关于 [lazy = true]: protozero 代码生成器会把它当成“原始字段”处理,生成类似

const std::string& ftrace_config_raw()

而不是常规的

const protos::FtraceConfig& ftrace_config()

这样做是为了减少头文件依赖,避免把数据源实现层的二进制体积撑大。

向后 / 向前兼容性的说明

tracing 服务在把 DataSourceConfig 转发给同名数据源时,不做任何解码再编码,而是直接把整块原始二进制 blob 传过去。

因此,即使 TraceConfig 里出现了服务编译时还不认识的新字段,服务也会原封不动地透传给数据源。

这样就能在不升级服务的前提下,随时添加新的数据源或新字段。

⚠️ 已知问题:

目前要想给 DataSourceConfig 增加自定义 proto 字段,还得去改 Perfetto 仓库里的 data_source_config.proto,对外部项目来说很不方便。

长期计划是:

  • 预留一段字段号给“非官方扩展”
  • 提供通用模板接口,让客户端自己定义结构

在那之前,如果你有自定义数据源的配置需求,只能把补丁提上游,让官方先把你的字段合进 data_source_config.proto。

Multi-process data sources(多进程数据源)

有些数据源是“系统单例”。

例如 Android 自带的调度器跟踪,整个系统只有一个实例,由 traced_probes 服务统一提供。

但在更一般的情况下,多个进程都可以声明自己支持同一个数据源——最典型的场景是用 Perfetto SDK 做应用层插桩(track_event)。

默认行为:

当 TraceConfig 里启用了某个数据源,Perfetto 会让所有声明了该数据源的进程一起开始记录。

如果想只让**特定进程(或进程集合)**开启,可以用两个过滤字段:

  • producer_name_filter // 精确匹配进程名
  • producer_name_regex_filter // 正则匹配进程名

(注意:Perfetto 的运行模型通常是“一个进程 == 一个 Producer;一个 Producer 可托管多个数据源”。)

示例:只给 Chrome 和 Chrome Canary 开启 track_event

buffers { size_kb: 4096 }

data_sources {
  config {
    name: "track_event"
    # 仅在这两个应用里启用
    producer_name_filter: "com.android.chrome"
    producer_name_filter: "com.google.chrome.canary"
  }
}

加了过滤后,tracing 服务只会让匹配到的 Producer 启动该数据源,其余进程即使声明了也不会被激活。

Triggers(触发器)

在常规流程里,一次 tracing 会话的生命周期就是 perfetto 命令行的运行时间:把 TraceConfig 传进去就开始,到 duration_ms 超时或进程被 kill 时结束。

Perfetto 还提供另一种“触发器”模式,可以在配置里预先声明:

  • 一组自由命名的触发器字符串;
  • 每个触发器是“启动”还是“停止” trace,以及延迟多久生效。

为什么不用直接 起/停 perfetto

核心原因是安全模型:

在 Android 这类环境里,只有特权实体(如 adb shell)才能配置/启停 tracing;普通 App 没权限。

触发器给无权限进程提供了一种**受限的、只能“喊口号”**的方式来影响 tracing 生命周期。

概念模型:

  1. 特权 Consumer(如 adb shell)提前在配置里写好:

“允许哪些触发器名字、各自会做什么”。

  1. 无权限进程(任意 App)只能在运行时

发出触发信号,但不能决定触发器具体行为。 发信号的方法:

  • 命令行:
/system/bin/trigger_perfetto trigger_name
  • 或者再起一个独立 perfetto 会话,在配置里只写 activate_triggers: "trigger_name" 也能触发。

触发器分两种类型(下文继续展开)。

Start triggers(启动触发器)

作用: 先把 tracing 会话保持在“空闲待命”状态(不记录任何数据),直到

  1. 指定的 start 触发器被触发,或
  2. trigger_timeout_ms 超时。

注意:

  • 一旦使用 START_TRACING 模式,就不能再同时用普通的 duration_ms(两者互斥)。
  • 触发后,trace 开始记录,并可用 stop_delay_ms 设定再过多久自动结束。

示例配置(PBTX 格式):

trigger_config {
  trigger_mode: START_TRACING

  triggers {
    name: "myapp_is_slow"   # 触发器名字
    stop_delay_ms: 5000     # 触发后再采 5 秒自动停
  }

  # 如果 30 秒内都没触发,直接结束,什么也不记录
  trigger_timeout_ms: 30000
}

# 其余部分照常:缓冲区、数据源等
buffers { ... }
data_sources { ... }

使用流程:

  1. 特权端(adb)提前用上述配置启动 perfetto,进程进入“等待触发”状态。
  2. 普通 App 在关键事件处执行:
/system/bin/trigger_perfetto myapp_is_slow

3. 触发成功 → 立即开始记录,5 秒后自动停;30 秒仍无触发 → 直接退出,trace 文件为空。

Stop triggers(停止触发器)

STOP_TRACING 触发器的作用是:trace 立即开始(和普通模式一样),但当触发器被命中时提前结束,相当于“手动提前停”。

典型用法——飞行记录仪(flight-recorder)模式:

  • 缓冲区设成 RING_BUFFER(循环覆盖)
  • 启动后一直录,触发事件发生时再停,这样就能把事发前最近一段数据保留下来。

示例配置:

# 如果 30 秒内一直没人触发,就按超时正常结束
trigger_timeout_ms: 30000

trigger_config {
  trigger_mode: STOP_TRACING

  triggers {
    name: "missed_frame"   # 触发器名字
    stop_delay_ms: 1000    # 触发后再录 1 秒然后停
  }
}

# 其余照常:缓冲区、数据源等
buffers { ... }
data_sources { ... }

使用流程:

  1. 特权端(adb)用上述配置启动 perfetto,trace 立即开始并循环写缓冲区。
  2. 当 App 检测到掉帧时执行
/system/bin/trigger_perfetto missed_frame

3. 触发后继续录 1 秒 → 自动停,最终 trace 文件里保留了“掉帧前 + 1 秒”的关键数据。

Android 平台

在 Android 平台上,使用 adb shell 时需要注意以下几点:

  • Ctrl+C 信号处理:通常 Ctrl+C 会优雅地终止跟踪,但通过 adb shell 运行 perfetto 时,ADB 不会传播此信号。只有在通过 adb shell 使用交互式 PTY 会话时才会传播。
  • 非 Root 设备的配置传递限制:
    • 在 Android 12 之前的非 Root 设备上,由于过于严格的 SELinux 规则,配置只能通过 cat config | adb shell perfetto -c - 的方式传递(- 表示标准输入)。
    • 从 Android 12 开始,可以使用 /data/misc/perfetto-configs 目录来存储配置文件。
  • Android 10 之前设备的跟踪文件获取:
    • 在 Android 10 之前的设备上,adb 无法直接拉取 /data/misc/perfetto-traces 目录中的文件。
    • 解决方法是使用 adb shell cat /data/misc/perfetto-traces/trace > trace 命令来获取跟踪文件。
  • 长时跟踪的控制:
    • 在基准测试或持续集成(CI)等场景中捕获长时跟踪时,可以使用 PID=(perfettobackground)在后台启动跟踪,然后通过kill(perfetto --background) 在后台启动跟踪,然后通过 kill PID 来停止跟踪。

其他资源

  • TraceConfig 参考文档
  • 缓冲区与数据流