01 一份 Perfetto Trace 是怎么形成的

一份 Perfetto Trace 里混合了多种数据。内核产生 sched_switchsched_wakeup、CPU 频率等事件;Android Framework 和 App 通过 Trace API 记录代码区间;SurfaceFlinger 提供 FrameTimeline;process_statssys_stats 等数据源还会采集进程、CPU 和内存统计。这些数据来自不同的 Producer,采集前先由 TraceConfig 决定启用哪些 Data Source。

Data Source 启动后,通过 TraceWriter 把记录编码成 protobuf 格式的 TracePacket,再写入 Producer 和 Tracing Service 共享的内存。Tracing Service 收取已提交的数据,复制到 central buffer,最后输出 .pftrace 文件。因此,Trace 文件的原始结构是一系列 TracePacket;packet 中常见时间戳、sequence ID 和具体 payload,文件内部没有 Perfetto UI 中的轨道,也没有 slice 这样的 SQL 表。

打开 Trace 时,Trace Processor 才开始解析 packet,对齐不同时钟,建立进程、线程和轨道的身份关系,再将数据整理为 processthreadsliceschedcounter 等可查询对象。Perfetto UI 和 PerfettoSQL 使用的都是这层解析结果。

Trace Processor 是 Perfetto 的 C++ 解析和分析引擎,不是 Android 设备上负责采集的后台服务。在 ui.perfetto.dev 中打开 Trace 时,它通常以 WebAssembly 形式运行在本地浏览器中;使用 trace_processor 命令行或 Python API 时,则由电脑上的原生进程完成解析。slice 等表存在于当前 Trace Processor 实例的内存中,不会回写到 .pftrace 文件。

上图已经是 Trace Processor 整理后的界面。CPU Scheduling、Expected Timeline、Actual Timeline 和主线程 Slice 共用一条时间轴;采集时,它们分别来自不同的 Data Source,使用不同的 payload,加载后也会进入不同的表。

这一篇只处理数据本身:数据由谁产生,以什么形式写入,怎样经过 Buffer 形成 Trace 文件,文件内部的 packet 是什么结构,加载后又如何变成轨道和表。

TraceConfig 决定采什么

一次采集从 TraceConfig 开始。它负责设置采集时长、central buffer,以及需要启用的数据源。data_sources.config.name 选择数据源,target_buffer 把该数据源的输出写入指定 central buffer,数据源自己的配置则放在对应的私有字段中。

示意图:TraceConfig 选择 Data Source,并将输出映射到目标 Central Buffer。

下面是一份面向 Android 12 及以上设备的 10 秒短时采集示例,不是对所有设备的兼容性保证。实际可用的数据源、ftrace event、ATrace category 和 FrameTimeline 取决于设备、内核、系统构建配置及 tracing 权限。示例把高频的 ftrace、ATrace 和 FrameTimeline 写入 32 MB buffer;进程与系统统计写入另一个 8 MB buffer。两个 buffer 都采用 RING_BUFFER

textproto 复制代码
buffers {
  size_kb: 32768
  fill_policy: RING_BUFFER
}

buffers {
  size_kb: 8192
  fill_policy: RING_BUFFER
}

duration_ms: 10000

data_sources {
  config {
    name: "linux.ftrace"
    target_buffer: 0
    ftrace_config {
      ftrace_events: "sched/sched_switch"
      ftrace_events: "sched/sched_waking"
      ftrace_events: "sched/sched_wakeup"
      ftrace_events: "power/cpu_frequency"
      ftrace_events: "power/cpu_idle"
      ftrace_events: "binder/binder_transaction"
      ftrace_events: "binder/binder_transaction_received"

      atrace_categories: "gfx"
      atrace_categories: "view"
      atrace_categories: "input"
      atrace_categories: "wm"
      atrace_categories: "am"
      atrace_categories: "aidl"
      atrace_apps: "com.example.wechatfriendforperformance"

      compact_sched {
        enabled: true
      }
    }
  }
}

data_sources {
  config {
    name: "android.surfaceflinger.frametimeline"
    target_buffer: 0
  }
}

data_sources {
  config {
    name: "linux.process_stats"
    target_buffer: 1
    process_stats_config {
      scan_all_processes_on_start: true
      proc_stats_poll_ms: 1000
    }
  }
}

data_sources {
  config {
    name: "linux.sys_stats"
    target_buffer: 1
    sys_stats_config {
      stat_period_ms: 1000
      stat_counters: STAT_CPU_TIMES
      meminfo_period_ms: 1000
      meminfo_counters: MEMINFO_MEM_TOTAL
      meminfo_counters: MEMINFO_MEM_FREE
      meminfo_counters: MEMINFO_MEM_AVAILABLE
      cpufreq_period_ms: 500
    }
  }
}

这几个配置项各管一件事:

  • buffers 定义 Tracing Service 持有的 central buffer。size_kb 是容量,fill_policy 是写满后的处理方式。
  • duration_ms 控制采集时长,这里是 10000 ms
  • data_sources.config.name 决定由哪个 Producer 响应。例如 linux.ftrace 采集内核 ftrace 与 ATrace,android.surfaceflinger.frametimeline 记录帧时间线。
  • target_bufferbuffers 的零基索引。值 0 指向 32 MB buffer,值 1 指向 8 MB buffer。
  • ftrace_configprocess_stats_configsys_stats_config 是数据源私有配置。它们决定具体事件、类别和轮询周期。

配置没有启用的数据不会写入 Trace。采集结束后再打开更多 UI 轨道或加载 SQL 模块,也无法补回当时没有记录的 sched_switch、CPU 频率或 FrameTimeline。

ATrace 和 TrackEvent 是两条写入路径

ATrace 和 TrackEvent 都能记录 Slice 和 Counter,它们进入 Trace 的方式不同。平时在 Android App 中调用的 android.os.Trace.beginSection() 走 ATrace 路径;使用 Perfetto SDK 的 TRACE_EVENT() 则属于 TrackEvent 路径。

常见调用 归属 进入 Trace 的路径 常见解析结果
android.os.Trace.beginSection()/endSection() ATrace trace_marker -> ftrace ring buffer -> linux.ftrace -> ftrace_events slice
androidx.tracing.trace("name") {} ATrace Jetpack 封装最终调用 Trace.beginSection()/endSection() slice
NDK ATrace_beginSection()/ATrace_endSection() ATrace Native ATrace 事件进入 ftrace ring buffer slice
Perfetto SDK TRACE_EVENT() TrackEvent TraceWriter -> shared memory -> track_event slicecounterflow

ATrace 是 Android 4.3 开始提供的系统 Trace 插桩机制,时间早于 Perfetto。Java/Kotlin 使用 android.os.Trace,NDK 使用 ATrace_*,Android 平台内部则有 ATRACE_BEGIN() 等宏。这些入口最终把事件送入内核 ftrace ring buffer,Perfetto 通过 linux.ftrace Data Source 采集。要记录某个 App 的 ATrace,ftrace_config 中还要配置对应包名:

textproto 复制代码
ftrace_config {
  atrace_apps: "com.example.wechatfriendforperformance"
}

TrackEvent 属于 Perfetto Tracing SDK。SDK 中的 TRACE_EVENT()TRACE_EVENT_BEGIN()TRACE_COUNTER() 可以定义 Category、Track、参数和 Flow。它们不经过 ftrace trace_marker,由 Perfetto SDK 通过 TraceWriter 写入 track_event payload;采集配置需要启用 track_event Data Source。

AndroidX Tracing 2.0 又提供了 Tracer.globalTraceEventtracing-wire 等新 API。这套 API 使用 Perfetto packet 格式和可替换的 driver/sink,不再是传统 androidx.tracing.trace("name") {} 的 ATrace 封装。因此,判断一段 Trace API 属于哪条路径时,需要看具体调用的类和方法,不能只看依赖名是否叫 androidx.tracing

八种常见写入形态

"Data Source""数据形态""TracePacket payload""SQL 对象"是四个不同维度。一个 Data Source 可以产生多种 payload;一种 payload 可以贡献给多个表;一个逻辑事件也可能由多个 packet 共同表达。下表只给出常见主路径,不表示严格的一对一映射。

写入形态 常见 Producer 表达的内容 常见 TracePacket payload 主要解析表族
kernel event Linux ftrace sched_switch、唤醒、CPU 频率等内核事件 ftrace_events schedthread_state、CPU/counter 相关对象
interval / Slice Android framework/应用 ATrace;Perfetto SDK TrackEvent instrumentation 一段有开始和结束时间的工作 ATrace 经 linux.ftrace 写入 ftrace_events;SDK TrackEvent instrumentation 写入 track_event slice 与各类 track
instant event TrackEvent instrumentation 某一时刻发生的点事件 track_event 常见归入 slice,以零时长事件呈现
Counter ftrace、sys stats、TrackEvent 某个时间点上的数值 ftrace_eventstrack_event 或数据源专用 payload counter 与 counter track
process/thread metadata linux.process_stats、Perfetto SDK instrumentation 进程、线程及其身份信息 process_treetrack_descriptor processthreadprocess_trackthread_track
FrameTimeline Android FrameTimeline Producer Expected/Actual frame 时间线及帧属性 本文不对具体 payload 与字段作逐项映射 expected_frame_timeline_sliceactual_frame_timeline_slice
Profiling heapprofd、perf sampling 等 Producer 栈采样、堆分析及调用关系 profile_packet 等 profiling payload stack_profile_callsitestack_profile_frame
metadata Producer、Tracing Service 时钟、trace 状态及其他描述信息 clock_snapshottrace_stats 等 metadata payload 时钟、统计与参数相关对象,取决于 payload

以同步 Slice 为例,开始与结束可能分开写入,Trace Processor 解析后才形成带 tsdur 的区间。Counter 没有区间语义,它记录某个时间点上的值。调度数据虽然也能形成持续区间,但 schedthread_state 属于专用调度对象,不经普通 track event 的轨道访问。

Perfetto UI 中的 Choreographer#doFrame 会显示为主线程上的长条。只看这条 UI Slice 或最终的 slice 记录,无法反推出唯一的 packet 类型:它可能来自经 linux.ftrace 写入 ftrace_events 的 ATrace,也可能来自写入 track_event 的 SDK instrumentation。Trace Processor 会把两条路径规范化为 slice,再根据 descriptor 与身份数据关联线程和进程。

从 Producer 到文件

Data Source 不直接写最终文件。Producer 侧的 Data Source 获得 TraceWriter,先把 packet 写入 Producer 与 Tracing Service 之间的 shared memory。Shared memory 按 page 管理,TraceWriter 在 page 内申请 chunk 并顺序写入 packet;提交协议用 chunk 标识已完成的写入范围。正常写入时,已填满或完成写入的 chunk 会被提交给 Tracing Service;执行 flush 或结束采集时,仍未填满但已经写入数据的 chunk 也会提交。Tracing Service 随后把已提交的数据复制到 central buffer,采集停止或执行流式输出时再进入 trace 文件。

示意图:Producer Shared Memory 是暂存区,Central Buffer 才是输出文件前的主要保存位置。

完整路径可以写成:

Data Source -> TraceWriter -> producer shared memory -> chunk commit -> Tracing Service -> central buffer -> trace file

Linux ftrace 还多一层内核缓冲。内核事件先进入各 CPU 的 per-CPU ring buffer,再由 ftrace Data Source 读取并写入 Perfetto 的 Producer shared memory。内核 ring buffer、Producer shared memory 和 central buffer 处于不同阶段,容量与丢失原因也不同。

Central Buffer 写满后,两种策略留下的证据不同:

  • RING_BUFFER 覆盖最旧数据,保留采集结束前的最近时间窗口。长时间采集若容量不足,起始阶段可能已经消失。
  • DISCARD 保留已有数据并丢弃新数据。它能保住采集开头,buffer 满后的事件不会进入文件。

因此,"Trace 文件成功生成"不等于所需时间段完整存在。采集前要按数据速率和目标时间窗口分配 buffer;采集后还要检查丢包、覆盖与 trace stats。

文件里是 TracePacket

.pftrace.perfetto-trace 文件使用 protobuf 格式。根消息 Trace 包含重复的 TracePacket,文件可以理解为一组线性排列的 packet,不是 SQLite 数据库。

示意图:每个 TracePacket 带公共时序字段,并从 payload 的 oneof 成员中选择一个。

常见公共字段包括:

  • timestamp:packet 的时间戳。具体时钟域还需结合 trace 中的时钟信息解释。
  • trusted_packet_sequence_id:标识 packet 所属的受信 packet sequence,供解析器维护该 sequence 的增量状态。
  • payload:ftrace_eventstrack_eventprocess_treeprofile_packet 等数据字段。图中用 oneof 关系表示单个 packet 选择一个对应 payload 成员,不表示一个 packet 同时写入所有类型。

顺序保证有明确边界。同一 TraceWriter sequence 的 packet 按写入顺序读取;不同 writer 的 packet 可以在文件中交错。时钟完成同步后,带有可比较 timestamp 的 packet 可以按时间先后比较或排序,但这不会产生唯一的全局顺序,也不能证明因果关系。时间戳相同或缺失时,歧义仍然存在。

这项边界会直接影响分析方法。不能根据两个不同数据源的 packet 在文件中的前后位置判断事件先后,也不能仅凭时间接近就断言一个事件导致了另一个事件。Trace Processor 会统一可比较的时间,因果关系仍需 flow、waker、Binder、父子 Slice 等关系数据支持。

SQL 表从哪里来

SQL 能查询 Trace,原因在 Trace Processor。它读取 protobuf payload,完成时钟归一化、进程与线程身份解析、轨道建立、Slice 配对和参数关联,然后向 PerfettoSQL 暴露 tables/views。UI 也使用这些解析后的对象,因此 UI 和 SQL 是同一分析层的两种入口。

采集和分析分属两条数据链:

  • Android 设备上的 traced 负责采集会话、central buffer 和文件输出,数据链是 Producer -> traced -> .pftrace
  • Trace Processor 在文件加载后运行,数据链是 .pftrace -> Trace Processor -> tables/views -> UI/SQL

使用 Perfetto 网页时,浏览器内置的 Trace Processor WebAssembly 会在本地读取文件。如果 Trace 过大,也可以在电脑上启动原生 trace_processor server,让 UI 连接这个本地进程。两种方式使用的解析和 SQL 能力相同,差别主要在运行位置、内存上限和执行性能。

描述这些对象时,需要保留两条分类轴。

对象层与查询可用性

层次 含义 例子
intrinsic storage Trace Processor 的内部存储层;本文不讨论其实现细节 不把内部存储名当成公共查询接口
Prelude public tables/views 自动加载,可直接查询的基础公开对象 sliceschedthread_stateprocessthreadtrackargsflow
ordinary SQL views SQL 引擎登记的逻辑 view 当前 trace 与 shell 中的 sliceschedcounter
explicitly included stdlib modules 使用 INCLUDE PERFETTO MODULE 后出现的高层对象和函数 slices.with_contextsched.with_context 等模块

对象类型描述查询实现,不描述数据语义。本文使用的 trace 与 Trace Processor shell 中,actual_frame_timeline_sliceexpected_frame_timeline_sliceprocess_trackthread_track 登记为 table;argscounterflowprocessschedslicethreadthread_statetrack 登记为 view。这只是当前文件与当前 shell 的实测结果,版本和输入变化后可能不同。

数据语义族

语义族 主要对象 回答的问题
身份 processthread Trace 中有哪些进程和线程
轨道 trackthread_trackprocess_track 事件属于什么上下文分区
Slice / 调度 sliceschedthread_state 某段时间在做什么、线程何时运行或等待
Counter counter 与各类 counter track 数值随时间怎样变化
FrameTimeline expected_frame_timeline_sliceactual_frame_timeline_slice 预期帧窗口和实际帧记录
args / flow argsflow 事件携带哪些参数,Slice 之间如何连接
stack profile stack_profile_callsitestack_profile_frame 样本对应的调用栈结构

同一个对象可以同时出现在两条轴上。slice 在对象层属于自动加载的公开对象,在当前 shell 中登记为 view;在语义层,它始终表达时间区间。actual_frame_timeline_slice 当前登记为 table,在语义层属于 FrameTimeline。不能用 table/view 的字样判断数据来自内核、应用或帧系统。

表之间怎样关联

Trace Processor 会为同一份 Trace 内的实体分配稳定 ID。关联表时优先使用这些 ID,不把操作系统的 pidtid 当成全程唯一身份,因为系统 ID 可能复用。

示意图:箭头从引用字段指向目标键;args.arg_set_id 是关联和分组键,不标作主键。

常用连接字段有六组:

  • upid:Trace 内的进程 ID。thread.upid 可关联 process.upid
  • utid:Trace 内的线程 ID。thread_track.utidsched.utidthread_state.utid 都可回到 thread.utid
  • track_id:事件所属轨道。slice.track_id 关联某类 track 的 idcounter.track_id 关联 counter track。
  • arg_set_id:事件与参数集合之间的关联键。多个参数行可以属于同一个参数集合,因此 args.arg_set_id 不应称为主键。
  • id / parent_idslice.id 标识 Slice,slice.parent_id 指向父 Slice,用于表达同轨道上的嵌套结构。
  • flow:通过输入、输出 Slice ID 连接跨 Slice 的异步关系。它表达事件之间的边,不是单独占据时间轴的区间。

slice 的核心字段子集如下。当前实测 schema 还有其他列,因此这不是完整字段表。

字段 含义
id Trace 内的 Slice ID
ts 开始时间,单位为纳秒
dur 持续时间,单位为纳秒
track_id 所属轨道 ID
name Slice 名称
depth 同一轨道上的嵌套深度
parent_id 父 Slice ID
arg_set_id 参数集合关联键
thread_ts 线程时间戳;仅在采集到线程时间信息时有值
thread_dur 线程时间持续量;仅在采集到线程时间信息时有值

tsdur 是墙钟时间轴上的字段,thread_tsthread_dur 依赖 TrackEvent 的 thread timestamp collection,不能假定每条 Slice 都有值。

把 UI 轨道映射回数据

UI 中的"轨道"是视觉组织方式,不与 Trace Processor 的一行 track 严格一一对应。一个 UI 轨道可能组合多个查询对象,也可能按进程、线程、CPU 或插件逻辑再次分组。读取截图时,应先辨认事件语义,再查对应表族。

进程、线程与 Slice

进程分组可回到 process,主线程身份来自 thread,承载用户态区间的线程轨道通常关联 thread_track,轨道里的彩色区间来自 slice。选中 Slice 后出现的名称、开始时间、持续时间、线程和进程,分别来自区间字段与身份关系。

CPU Scheduling 与线程状态

顶部 CPU 0 SchedulingCPU 7 Scheduling 对应 sched 的调度区间;选中主线程 Slice 后,详情中的 Running、Runnable 等状态来自同一时间范围内的 thread_stateschedthread_state 是专用调度对象,不能按普通 track_id 访问,需要经 utiducpu 等字段关联线程和 CPU。

Counter 曲线

CPU Frequency 轨道画的是数值曲线。截图中选中的样本显示 787,200 kHz。采样值进入 counter,再由 track_id 关联对应的 counter track。Counter 的 ts 表示采样时间,value 表示该时刻的值;相邻点之间画成阶梯或折线属于 UI 呈现,不会把它变成带 dur 的 Slice。

FrameTimeline

Expected Timeline 和 Actual Timeline 分别映射到 expected_frame_timeline_sliceactual_frame_timeline_slice。选中帧后可以看到 surface frame token、display frame token、进程、持续时间和 layer name。本文只确认这些专用表在当前 shell 中可查询,不把 UI 字段逐项绑定到某个 TracePacket payload。

第一次查询只看三列

完成上述数据链后,SQL 的来源已经明确:查询面对的是 Trace Processor 暴露的 slice,不是直接扫描 protobuf 文件。先取十行,只观察开始时间、持续时间和名称。

截图中的 tsdur 单位都是纳秒。第一行 dur = 141094,换算后约为 0.141094 msname 保存 Slice 名称。返回十行只说明查询命中了十条区间记录,不提供线程归属、父子结构或性能结论。

sql 复制代码
SELECT ts, dur, name FROM slice LIMIT 10;

打开一份 Trace 后检查什么

  • 回看 TraceConfig,确认目标 Data Source、事件类别和应用包名确实启用。
  • 核对 buffer 容量与策略,判断目标时间段是否可能被 RING_BUFFER 覆盖,或因 DISCARD 丢失后续数据。
  • 识别 UI 元素的数据语义,区分 Slice、调度区间、线程状态、Counter 和 FrameTimeline。
  • 找到相关表之间的 upidutidtrack_idarg_set_idparent_id 或 flow 关系,再开始跨对象分析。
相关推荐
布兰妮甜2 小时前
Vue 大型页面性能优化:四个核心策略
vue.js·性能优化·虚拟滚动·组件缓存·按需加载
NutShell Wang3 小时前
每帧重建整条路径、每秒倾倒 48MB 给 GC:实时折线图渲染架构的实测复盘
前端·性能优化·架构·图形渲染·数据可视化·vibe coding
爱喝水的鱼丶6 小时前
SAP-ABAP:调试器高级工具使用——内表分析、SQL追踪、内存检查功能实操
运维·sql·性能优化·sap·abap·经验交流
Ai拆代码的曹操1 天前
MySQL GROUP BY 性能优化:执行计划分析 + 索引重构实战
mysql·性能优化·重构
人间凡尔赛1 天前
React Compiler 正式落地一年:告别手动 useMemo/useCallback 的全栈实践
前端·性能优化·react
小孔龙2 天前
VSync 与同步屏障:doFrame() 的优先调度
android·性能优化
mlidongfeng2 天前
[AI][昇腾950] Scalar 性能优化
人工智能·性能优化
乐启国际旅行社有限公司2 天前
文旅小程序性能优化:分包加载+地图视口懒加载解决景区卡顿与包超限
性能优化·小程序
梦想不只是梦与想3 天前
鸿蒙性能优化:启动速度
性能优化·harmonyos·启动速度