Perfetto 多 Trace 合并深入解析:全局时钟图、对齐回退策略与机器归属模型
【免费下载链接】perfettoProduction-grade client-side tracing, profiling, and analysis for complex software systems.项目地址: https://gitcode.com/GitHub_Trending/pe/perfetto
Perfetto 的 Trace Processor 可以一次打开多个 trace 文件,将它们合并到一条共享时间线上:来自不同设备、同一设备的不同进程、甚至格式完全不同的 trace 都可以共存于一次分析中。本文以 Trace Merging 概念文档 为主线,结合仓库中的时钟同步引擎(clock_synchronizer)、ClockTracker与 trace manifest 解析器源码,讲清楚独立文件中的事件如何获得可比较的时间戳、数据又如何始终归属于它来源的机器。读完之后,你能够独立配置跨设备合并、诊断合并时的事件丢弃,并理解合并底层的时间转换机制。
问题本质:同时录制的两个文件并不共享时间基
两个"同时"录制的 trace 文件,其时间戳在一般情况下并不可直接比较。每个文件里的时间戳都是某个时钟的读数:一部手机上的BOOTTIME、Chrome 渲染进程内部的MONOTONIC,或者像 Chrome JSON 这类格式根本没有绝对时钟。不同机器上的时钟会各自漂移,同一台机器上不同的时钟域(例如BOOTTIME与REALTIME)也有不同的零点。
如果只是把文件简单拼接,就会把互不相关的刻度放到同一条轴上。因此合并必须为每个事件回答两个问题:
- 这个时间戳是从哪个时钟读出的?
- 这个时钟与合并后 trace 用作时间线的那个时钟(即 "trace time")是什么关系?
这就是整个合并机制要解决的核心问题,下文的所有机制都是围绕它展开的。
时钟的作用域:机器 + 文件
Perfetto 在单个 trace 内部就已经建模了多时钟域,并借助ClockSnapshot数据包在域之间做转换(详见 时钟同步概念文档)。合并把这个模型扩展到跨文件、跨机器:每一个时钟不仅由其域(domain)标识,还由它所属的机器标识,必要时还由它出自哪个文件标识。手机上的BOOTTIME和手表上的BOOTTIME是两个不同的时钟;两个无时钟 JSON 文件各自的私有时间线也是如此。
从源码结构看,这一模型落实在 ClockId 结构体 中,它由四个字段共同构成一个时钟的全局身份:
struct ClockId { uint32_t clock_id = 0; // 时钟域(内置钟、序列钟或文件私有钟) uint32_t seq_id = 0; // 序列作用域时钟的序列号 uint32_t trace_file_id = 0; // 归档中隔离各文件的状态 uint32_t machine_id = 0; // 时钟所属的机器;0 表示宿主机 };machine_id用来区分同一个文件里来自不同机器的数据(一条多机器 proto trace 只对应一个trace_file_id),trace_file_id则用来隔离同一机器上不同文件的时钟状态。每次转换前,ClockTracker::ToTraceTime 都会先通过ClockId::Qualify把时钟打上(machine, file)标签,再交给转换引擎——这正是"时钟按机器和文件限定作用域"在代码层面的直接体现。
全局时钟图及其三条边的来源
所有这些时钟都存在于一张全局时钟图中:节点是时钟,边是两条时钟之间已知的对应关系,每条边表达"当时钟 A 读数为 X 时,时钟 B 读数为 Y"。转换一个时间戳时,Trace Processor 就在这张图上从源时钟寻路到 trace-time 时钟,沿途逐条应用边上的换算。
边的来源有三类:
- trace 文件内部的
ClockSnapshot数据包; - 录制期完成的时钟同步,例如 多机器录制 所用的 ping 协议(
remote_clock_sync); - trace manifest 中的条目,允许用户断言一条 trace 自身并不包含的对应关系(可选带固定偏移)。
转换引擎 ClockSynchronizer 对每次AddSnapshot调用增量构建这张无权重有向图,Convert时用 BFS 查找最短转换路径(允许跨多种快照类型多跳,例如 A→C 经由快照 S1、C→E 经由快照 S2)。一个值得注意的实现细节是:每条进入图的边都会写入clock_snapshot表(见 ClockTracker::AddSnapshot 的注释:"Every edge entering the graph is recorded this way"),因此实际用于转换的整张图都可以用 SQL 完全检查——这是排查合并问题的关键入口。
文件间毫无共同时钟时如何摆放:三级回退策略
如果某个文件的时钟在图上有通往 trace time 的路径,直接走这条路即可。否则 Trace Processor 按以下优先级回退,源码对应 ClockTracker::BridgeToTraceTime 的分支逻辑:
1. REALTIME 会合(rendezvous)。REALTIME(墙钟时间)假定在所有机器上读数相同——实际中机器都通过 NTP 同步它。如果文件所在机器与 trace-time 机器都能关联到REALTIME,文件就经由它定位。这正是两部手机各自独立录制的 trace 能够按真实墙钟位置自动对齐的原理。源码中该分支的实现是把本机的REALTIME节点与 trace-time 所在机器的REALTIME(全局会合节点)以零偏移边相连,再让源时钟经本机REALTIME到达 trace time。
2. 同域假设。不同机器或文件上的同域时钟(例如两个BOOTTIME),在没有更好的证据时以零偏移关联;类似地,文件的私有按文件时钟(BUILTIN_CLOCK_TRACE_FILE)也可以被固定在零偏移上。从源码看,这是"最后手段"分支,且仅当两端之一是文件私有钟、或两端是同域时钟时才注入这条零偏移边——这是一种猜测,只适用于来自同一机器同一开机周期的文件。
3. 丢弃(Drop)。两个不同的真实时钟域(比如这边的BOOTTIME和那边的REALTIME)永远不会被盲目等同。无法与 trace time 关联的时钟,其事件被丢弃并记入 trace 的错误统计(见下文"如何验证结果")。源码注释对此表述很明确:"we do not fabricate a relationship: the conversion fails so the events are dropped and logged rather than silently misplaced"。修复方式是录制时记录时钟快照,或在 manifest 中断言该对应关系。
注意:REALTIME 会合的精度取决于各机器的墙钟。如果 NTP 没有同步它们,trace 之间会相差一个偏移量;已知的偏差可以用 manifest 的
offset_ns修正。
Trace time 与时间边界
合并后的 trace 只有一个时钟充当时间线。规则是:第一个声明 trace-time 时钟的文件获胜。TraceTimeState::TrySetClock 的所有权语义即"首个声明者获胜,其他声明者被忽略"。由于 manifest 总是最先被处理(其 importer 描述符设置了archive_priority = -1和is_manifest = true,见 PerfettoManifestImporter),manifest 里的trace_time字段优先于各 trace 自身声明的任何内容。
合并 trace 的时间边界是所有 (机器, 文件) 对录制区间的并集。因此,相隔几分钟录制的两个 trace 合并后会得到一条很长的时间线,两端各有一簇活动——"合并"是把文件放到它们真实的相对位置上,而不是把它们叠加在一起。
时间戳若被转换到 trace time 起点之前,将无法表示,同样被丢弃并记入错误统计。在合并 trace 中最常见的原因是 manifest 里的offset_ns把文件移得太远。
机器(Machine):合并数据保持来源归属
合并后的数据始终归属于它来源的机器。一台"机器"是一个设备或操作系统实例:手机、服务器、虚拟机。在 trace 模型中,机器是machine表的一行;process、thread、cpu、sched等机器作用域的表都带machine_id列引用它。这与 live 多机器录制 使用同一模型,合并时数据来源有三类:
- trace 内嵌的机器 id。通过 traced_relay 录制、或 SDK producer 通过
TracingInitArgs::machine_id配置了机器 id 的数据包,会携带TracePacket.machine_id,每个不同的 id 成为一台机器。如果一条 trace 的数据完全来自这样的一台机器,它会被"收养"(adopted)到宿主机器行上——因此单机器 trace 恰好只有一台机器,而不是"空宿主 + 一台远程"。 - manifest 声明。manifest 可以把整个文件归属到命名机器,也可以重命名多机器文件内嵌的 id。命名单独说明:manifest 命名机器获得从 2^32 开始的合成
raw_id(源码常量kFirstManifestMachineId = 1ll << 32),落在 32 位内嵌 id 空间之外;多个文件使用相同名字,即表示同一台共享机器。 SystemInfo.machine_name。producer 可以在自己的SystemInfo包中设置人类可读名字,填入machine.name列。没有任何东西会自动设置它;缺省时(且 manifest 未命名),UI 会回退到 "machine 2" 之类的数字标签。
注意:
machine.id(表行 id)在 Perfetto 版本之间不稳定。查询中识别机器应使用machine.raw_id或machine.name。
与 live 多机器录制的关系
合并是获得跨多机器 trace 的三种方式之一,另外两种发生在录制期:将多个 producer 中继到单个traced(traced_relay),或在 SDK producer 中预先打上机器 id。三种方式产生的是同一套模型(上文描述的时钟图 + 机器表),并且事后的合并还可以组合它们的输出——例如合并两条来自不同宿主机、各自用 relay 录制的 trace。三种方案的适用场景与选择依据见 多机器录制文档。
已知限制
- 自身包含多个 trace 的归档不能再嵌套进另一次合并:不支持递归同步。应直接合并叶子文件。
- 把合并后的多机器 trace 导出为 legacy JSON 时,只会导出宿主机器上的第一个 trace。
- UI 构造合并输入时使用内存中的 TAR,因此单个文件大小受限于能用 12 位八进制编码的长度(约 8 GB),归档成员名长度上限为 99 个字符。
实战:构建归档、写 manifest 与验证结果
概念文档偏重机制;任务导向的完整指南见 命令行合并指南 与 UI 合并指南。这里给出可直接复制的最小实操闭环。
归档 + manifest 模型
trace_processor只接受一个 trace 文件参数,合并的入口是一个归档(ZIP 或 TAR),其中装有待合并的文件。util merge子命令是构建这种归档的便捷工具(实现位于 util_subcommand.cc):
trace_processor util merge -o merged.tar trace_a.pftrace trace_b.pftrace trace_processor merged.tar归档是普通的 TAR/ZIP,tar cf merged.tar ...完全等效;util merge额外处理归档布局并把--manifest指定的文件固定命名为perfetto_manifest.json,--strict还能把校验警告变成非零退出码,适合 CI。
用 manifest 保留两台设备的数据分离
默认情况下,两个"看起来像同一台设备"的 trace 会合并到一台机器上。给文件命名机器可以让各自的进程、线程、CPU 保持分组:
{ "perfetto_manifest": { "version": 1, "files": [ {"path": "device_a.pftrace", "machine": {"name": "device-a"}}, {"path": "device_b.pftrace", "machine": {"name": "device-b"}} ] } }trace_processor util merge -o merged.tar --manifest manifest.json \ device_a.pftrace device_b.pftrace trace_processor merged.tar把无时钟 trace 钉到系统 trace 上
Chrome JSON、Gecko、Instruments 等格式没有绝对时钟,自身无法与系统 trace 对齐。manifest 的clocks块可以把文件钉到另一个文件的时钟上,可选固定偏移:
{ "perfetto_manifest": { "version": 1, "trace_time": {"clock": "BOOTTIME"}, "files": [ {"path": "system_trace.pftrace"}, { "path": "app_trace.json", "clocks": { "sync_to": {"file": "system_trace.pftrace", "clock": "BOOTTIME"}, "offset_ns": 100000000 } } ] } }offset_ns的语义(与 PerfettoManifestReader::ParseClocks 的实现一致):在同一时刻,源文件时钟读 T 时参考时钟读 T +offset_ns,因此正值会把文件在时间线上向后移动。注意sync_to.file必须本身是files数组中的一项。另外两个源码可确认的约束:manifest 目前仅支持"version": 1(版本校验);clocks块中可写的时钟名只接受REALTIME、REALTIME_COARSE、MONOTONIC、MONOTONIC_COARSE、MONOTONIC_RAW、BOOTTIME六个(ParseClockName)。
用 SQL 验证合并结果
合并过程是自描述的,合并后直接查询:
-- 合并后的机器,以及各自的数据量 SELECT m.name, m.raw_id, (SELECT COUNT(*) FROM thread t WHERE t.machine_id = m.id) AS threads FROM machine m; -- 输入文件及其处理顺序 SELECT name, trace_type, size FROM trace_file; -- 合并中被丢弃或错位的事件;结果为空说明每个事件都成功落入时间线 SELECT name, value, machine_id, trace_id FROM stats WHERE severity = 'error' AND value > 0;合并时值得盯的统计项:clock_sync_unrelatable_clock_domains与clock_sync_failure_no_path统计无法与时间线关联的时钟上的事件(对策:录制时钟快照,或添加 manifestclocks条目);trace_sorter_negative_timestamp_dropped统计被offset_ns移出时间线起点之前的事件。逐文件的元数据可通过metadata表的trace_id列,或更高层的traceinfo.tracestdlib 模块中的_metadata_by_trace视图获取。
延伸阅读
- Trace manifest 格式参考:手动合并配置的完整字段说明、默认值与错误目录。
- 命令行合并指南:构建合并归档、manifest 自动化的完整流程与 CI 建议。
- UI 中合并 trace:交互式配置对齐与机器归属,并可一键导出 manifest 或自包含 .tar。
- 时钟同步:单 trace 内的
ClockSnapshot模型,即本文合并机制所构建的基础。
【免费下载链接】perfettoProduction-grade client-side tracing, profiling, and analysis for complex software systems.项目地址: https://gitcode.com/GitHub_Trending/pe/perfetto
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考