EN

Android Perfetto 系列 13:用 Track Event 给系统 Trace 补业务语义

Word count: 7.4kReading time: 31 min
2026/05/04
loading

系统 Trace 能告诉你线程什么时候运行、CPU 跑在哪个核、FrameTimeline 哪一帧超时;在启用 Binder/ftrace/sched 证据时,也能还原 Binder 调用和等待线索。但它不知道播放器正在解哪一个 frame id,也不知道游戏引擎正在加载哪个 scene,更不知道一个业务任务在队列里排了多久。

第 13 篇只回答一个问题:业务语义怎么进入 Trace。系统能告诉你主线程慢了,Track Event 负责告诉你当时在 decode 哪一帧、哪个队列堵住、哪个请求跨线程流转。两边合在一起,才能把“主线程慢了 18ms”缩小到 frame 1082 附近的纹理上传候选区间,再结合 RenderThread/HWUI、GPU 和 FrameTimeline 继续验证。

文章按一条接入路线展开:先判断什么时候真的需要业务语义、SDK 接入边界在哪里;再约定一份业务阶段字典,让事件命名先于代码确定;选定 in-process / system backend 解决不同问题;给出最小接入骨架;用 category 当线上开关、和系统 Trace 一起抓;跨线程任务用 flow 或独立 track 串起来;Counter 只放状态、事件名要当 SQL 接口;最后是控制写入量、App 侧现场 Trace 协议、以及 Trace package 怎么带上下文回到分析侧。

什么时候需要业务语义

没有业务语义缺口时,不需要接 SDK。先按场景选工具:

场景 建议
Java/Kotlin 层临时标记函数耗时 android.os.Trace 或 AndroidX tracing,抓取时打开应用 atrace
Android native / C++ 模块要长期埋点 用 Perfetto SDK 的 Track Event
跨平台引擎、游戏、播放器、Camera pipeline 用 Track Event,加稳定参数和 counter
要输出复杂结构化状态,普通 slice/counter 不够 再考虑自定义 DataSource
只想知道系统线程状态、Binder、频率 先用系统数据源,没业务语义缺口时不用接 SDK

Android-only 场景里,如果 android.os.Trace 和 NDK ATrace_* 已经够用,官方也建议继续使用它们。SDK 仍然适合 Android 的 native、跨平台和结构化业务语义场景;简单 Android-only section 不必为了形式替换 ATrace。它们会进入 Perfetto 的 atrace 路径,适合普通 App 函数标记;抓取时要在 linux.ftrace 里配置 atrace_apps: "com.example.app" 或通过命令行 -a com.example.app 打开对应 App。

Perfetto SDK 更适合 native 模块和跨平台模块。它直接产生 Track Event,支持 category、slice、counter、flow、参数、独立 track,也方便和系统 Trace 放在同一个时间轴上。

App 侧 tracing 能力先按边界选:

能力 典型用途 版本和现场边界
android.os.Trace / AndroidX Trace Java/Kotlin 粗粒度 section 走 atrace;Android 12/API 31+ App tracing 默认对所有 App 可用,API 29/30 非 debuggable App 依赖 profileable,更低版本或特殊场景可用 AndroidX forceEnableAppTracing();采集侧仍要打开 atrace_apps
NDK ATrace_* native 粗粒度 section begin/end/isEnabled 从 API 23 起可用,async/counter 类能力有更高 API 边界;同样走 atrace
AndroidX tracing-perfetto App 自己的启动期/本地 Perfetto 采集 解决 App 内采集和初始化,不等同于系统冷启动全程分析
Perfetto SDK kSystemBackend native / 跨平台模块写 Track Event 到系统 Trace Android 9+ 平台包含 tracing 服务,但 P/Q 尤其非 Pixel 设备要验证 traced 是否启用和 producer socket 是否可达;Android 11+ 默认条件更稳定

ATrace 也有开销。热循环、每次锁尝试、每个对象都打 android.os.Trace / ATrace_*,会让 JNI、trace_marker 和内核路径本身扰动结果。Java/Kotlin 和 NDK ATrace 更适合粗粒度 section;高频业务状态优先用 SDK category gating、sampling,或者默认关闭的 debug category。

SDK 接入边界

Perfetto SDK 里有两层能力:

能力 适合表达 代价
Track Event 函数耗时、业务阶段、队列长度、frame id、请求 id、跨线程关系 接入成本低,Trace Processor 和 UI 原生支持
Custom DataSource 自定义 protobuf 状态、高频结构化数据、专用二进制格式 需要维护 schema,通常还要给 Trace Processor 补解析

只要 slice、counter、flow 和参数能表达问题,优先用 Track Event。自定义 DataSource 的门槛更高,不适合作为第一版埋点方案。

自定义 DataSource 适合两类情况:一类是普通 slice、counter 和 debug annotation 表达不了结构,比如要周期性转储一个子系统状态;另一类是数据量太大,需要强类型 schema 减少每条事件的体积。

它的代价也更直接:Trace Processor 默认不知道你的 protobuf,后续要补解析、查询表或 Trace Summary 输出。第一版先用 Track Event,能少维护一整套数据格式。

业务阶段字典先于代码

Track Event 不是把宏塞进每个函数。先定义业务阶段字典,再写代码。字典要让后续 SQL、报告和看板知道:事件叫什么、属于哪个阶段、字段单位是什么、缺字段时怎么降级。

event_name category phase 必填参数 可选参数 单位/基数 采样 报告指标
DecodeFrame player decode frame_idstream_idcodec widthheight frame_idstream_id 内唯一 每帧一次,debug 字段按需开 decode_dur_msdecode_max_ms
UploadTexture rendering upload frame_idtexture_bytes surface_id texture_bytes 用 bytes 每帧最多一次 upload_dur_msupload_bytes
RenderSubmit rendering submit frame_idsurface_id queue_depth queue_depth 用 items 每帧一次 submit_dur_msqueue_depth_items
WaitForBuffer camera wait request_idbuffer_queue producer request_id 在 session 内唯一 只在等待超过阈值时记录 wait_for_buffer_ms

字段改名、删字段、改单位,都要提升 arg_schema_version。长期报告字段不要叫 debug.frame_id,而应叫业务 schema 里的 frame_id / request_id,并记录 frame_id_domainnullable_policy 和兼容策略。缺 RenderSubmit 时,报告只能输出 decode/upload 阶段,不能写端到端结论;字段里应写 missing_marker=RenderSubmitevidence_grade=partial

两种 backend 解决不同问题

Perfetto SDK 可以用 in-process backend,也可以用 system backend:

Backend 谁控制 Trace 适合场景
kInProcessBackend App 自己创建 tracing session 单进程验证、离线测试、只看业务事件
kSystemBackend 外部 perfetto 命令或系统服务控制 和 sched、freq、Binder、FrameTimeline 合并分析

Android 性能分析里更常用 system backend。App 在这里只是 producer,系统 tracing session 决定什么时候开始、什么时候停止。这个边界要写进设计:收集系统 Trace 时,App 不能自己读取整份系统 Trace,避免拿到其他进程的数据。

Android 9(P) 虽然已有 Perfetto/traced,但命令行文本配置 --txt 从 Android 10(Q) 起才更适合直接使用;P 设备通常要走二进制 TraceConfig。实验室和本地问题复现适合用 system backend,线上产品默认接入时还要单独评估兼容、包体和开关策略。

最小接入骨架

最小接入只解决三件事:稳定 category、system backend、可查询事件名。native 模块接入骨架如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
// tracing_categories.h
#pragma once

#include <perfetto.h>

PERFETTO_DEFINE_CATEGORIES(
perfetto::Category("rendering")
.SetDescription("Rendering pipeline"),
perfetto::Category("player")
.SetDescription("Media pipeline"),
perfetto::Category("pipeline.debug")
.SetDescription("Verbose pipeline events")
.SetTags("debug"));

头文件只声明 category。category 名会进入 Trace 和后续 SQL,名字要稳定,不要把动态 id、页面实例号或请求号塞进 category 名里。

1
2
3
4
// tracing_categories.cc
#include "tracing_categories.h"

PERFETTO_TRACK_EVENT_STATIC_STORAGE();

PERFETTO_TRACK_EVENT_STATIC_STORAGE() 给 Track Event 注册需要的静态存储留位置。它只应该出现在一个 .cc 文件里,避免多重定义。

1
2
3
4
5
6
7
8
9
// tracing_init.cc
#include "tracing_categories.h"

void InitPerfetto() {
perfetto::TracingInitArgs args;
args.backends |= perfetto::kSystemBackend;
perfetto::Tracing::Initialize(args);
perfetto::TrackEvent::Register();
}

初始化阶段选择 backend,并把前面定义的 category 注册给 Track Event。Android 系统 Trace 场景里通常用 kSystemBackend,这样 App 事件才能和系统 sched、Binder、FrameTimeline 放在同一份 Trace 里。

之后就能在业务代码里打点。这段用 slice 表示耗时,用参数记录 frame id:

1
2
3
4
void DecodeFrame(int frame_id) {
TRACE_EVENT("player", "DecodeFrame", "frame_id", frame_id);
DecodeFrameImpl(frame_id);
}

TRACE_EVENT 是作用域事件。进入当前作用域时开始,离开当前作用域时结束。它适合包住一段同步工作,例如 decode、layout、upload、submit。

不跟随函数作用域的事件,用 TRACE_EVENT_BEGINTRACE_EVENT_END。这对跨多个函数的阶段有用,但不要把 begin/end 分散到完全不相关的调用路径里,容易造成同线程 slice 嵌套混乱、闭合错配或 SQL 解释困难。

接 SDK 时还要留一个构建层面的边界。官方 C++ SDK 是 C++17 库,发布包里常见形态是 perfetto.hperfetto.cc 两个合并版源文件,适合塞进已有 native 构建系统。

只放进 perfetto.h 还不够:要确认编译标准、线程库、符号裁剪、包体增量,以及 App 进程启动早期是否已经完成 Tracing::Initialize()

早期启动阶段也要谨慎。C++ SDK 只有在 tracing session 已经启用后才会记录普通 Track Event;AndroidX tracing-perfetto 提供了启动阶段初始化能力,但它解决的是 App 自己的启动期采集,不等于系统冷启动分析。冷启动仍要回到 ActivityTaskManager、zygote、Binder、主线程和首帧显示这些系统信号。

category 是线上开关

category 决定哪些事件会被打开。建议把它当成配置接口设计,不要随手起名。C++ API 里没有命中任何规则时,非 debug、非 slow category 默认会被打开;C API 的默认行为不同。

线上 preset 的主规则很简单:显式 disabled_categories: "*",再白名单打开需要的 category。这样可以避开 C/C++ 默认差异,也能避免 debug category 在现场意外打开。debugslow tag 默认关闭,专项分析时再显式打开。

常见划分方式:

  • player:播放器主阶段,例如 prepare、decode、render。
  • rendering:渲染提交、纹理上传、场景切换。
  • camera:request、HAL callback、buffer queue。
  • pipeline.debug:调试用高频事件,定义 category 时加 debug tag。
  • pipeline.slow:开销较高的事件,定义 category 时加 slow tag。

category 应按子系统和开关粒度设计,不按页面、实验组、请求类型命名;页面、实验组、请求类型放参数或 metadata,避免 category 数量膨胀。

以下配置明确选择 category:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
buffers {
size_kb: 65536
fill_policy: RING_BUFFER
}

data_sources {
config {
name: "track_event"
target_buffer: 0
track_event_config {
disabled_categories: "*"
enabled_categories: "player"
enabled_categories: "rendering"
}
}
producer_name_filter: "com.example.app"
}

这样做的好处是,现场抓 Trace 时不必换包,只改配置就能决定打开哪些业务事件。system backend 下 track_event 可能来自多个 producer,现场或线上 preset 要用 producer_name_filter / regex 把范围收住。示例里的 com.example.app 只是占位;先从 trace、data source 或 producer 列表确认实际 producer name,多进程 App 用多个 exact filter 或 regex filter。

和系统 Trace 一起抓

业务事件只解释“业务正在做什么”。系统事件解释“这段业务为什么慢”。合抓时,一般把 track_event 和 ftrace 放在同一份配置里:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
buffers { size_kb: 131072 fill_policy: RING_BUFFER } # 0: sched / ftrace
buffers { size_kb: 32768 fill_policy: RING_BUFFER } # 1: app track event

data_sources {
config {
name: "linux.ftrace"
target_buffer: 0
ftrace_config {
ftrace_events: "sched/sched_switch"
ftrace_events: "sched/sched_waking"
ftrace_events: "power/cpu_frequency"
ftrace_events: "power/cpu_idle"
atrace_categories: "gfx"
atrace_categories: "view"
atrace_categories: "binder_driver"
atrace_apps: "com.example.app"
}
}
}

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

data_sources {
config {
name: "track_event"
target_buffer: 1
track_event_config {
disabled_categories: "*"
enabled_categories: "player"
enabled_categories: "rendering"
}
}
producer_name_filter: "com.example.app"
}

duration_ms: 10000

抓完后,用 SQL 确认事件进来了。这条 SQL 用第 11 篇讲过的 thread_slice 视图,直接把进程名、线程名和业务参数查出来。thread_sliceslices.with_context 提供的视图,已经把 slice、track、thread、process 关联好;单篇阅读时只要保留前面的 INCLUDE 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
INCLUDE PERFETTO MODULE slices.with_context;
INCLUDE PERFETTO MODULE time.conversion;

SELECT
time_to_ms(ts) AS ts_ms,
time_to_ms(dur) AS dur_ms,
name,
thread_name,
process_name,
EXTRACT_ARG(arg_set_id, 'debug.frame_id') AS frame_id
FROM thread_slice
WHERE process_name = 'com.example.app'
AND name IN ('DecodeFrame', 'RenderScene', 'UploadTexture')
ORDER BY ts;

这条查询适合默认线程 track 上的 Track Event 或 atrace slice。独立 track、process track 事件不一定有 utid,可能不会出现在 thread_slice 里;这类事件要改用 process_slicethread_or_process_slice,或直接从 slice JOIN track 查,再按 track.name/parent_id 还原上下文。

这条 SQL 的价值不止是“查到了事件”,它还能继续和 thread_state、CPU 频率、Binder 事件、FrameTimeline 做时间范围关联。TrackEvent slice 是业务时间轴证据,不等于 CPU 一直在执行这段业务;CPU 执行要再关联 sched / thread_state,并区分默认 ThreadTrack、自定义 Track 和跨线程 flow。普通 key/value 参数会作为 debug annotation 进入 args,SQL 用 debug.<key> 读取;typed TrackEvent 字段不走这个 debug.* args 口径,需要对应解析、表或专用字段。

时间范围关联只是第一步。App 的 frame_id 是业务帧,FrameTimeline 的 frame/vsync 是系统帧;只靠时间重叠容易把排队、预渲染、跨线程工作归到错误帧。最终要结合 Choreographer frame/vsync、JankStats frame start、RenderThread/HWUI slice、actual_frame_timeline_slice / expected_frame_timeline_slice 再确认。

这里的 frame_id 是 debug annotation,适合排查和临时 SQL。debug annotation 默认是调试字段;如果进入长期指标,要把字段名、类型、单位、缺省值和迁移策略当成接口冻结。只有能控制 Perfetto proto、解析和发布流程时,再考虑 typed TrackEvent 字段或自定义 DataSource。现场 preset 还可以用 filter_debug_annotations / filter_dynamic_event_names 降低隐私和体积风险。

业务阶段报告至少要把 marker 完整性写出来:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
INCLUDE PERFETTO MODULE slices.with_context;
INCLUDE PERFETTO MODULE time.conversion;

WITH app_events AS (
SELECT
process_name,
thread_name,
name AS phase,
ts,
dur,
EXTRACT_ARG(arg_set_id, 'debug.frame_id') AS frame_id
FROM thread_slice
WHERE process_name = 'com.example.app'
AND name IN ('DecodeFrame', 'UploadTexture', 'RenderSubmit')
),
frame_quality AS (
SELECT
frame_id,
CASE
WHEN SUM(CASE WHEN phase = 'RenderSubmit' THEN 1 ELSE 0 END) = 0
THEN 'partial'
ELSE 'complete'
END AS evidence_grade,
CASE
WHEN SUM(CASE WHEN phase = 'RenderSubmit' THEN 1 ELSE 0 END) = 0
THEN 'RenderSubmit'
ELSE ''
END AS missing_marker
FROM app_events
GROUP BY frame_id
)
SELECT
e.frame_id,
e.phase,
MIN(time_to_ms(e.ts)) AS start_ms,
time_to_ms(SUM(e.dur)) AS phase_total_ms,
MAX(e.thread_name) AS sample_thread,
q.evidence_grade,
q.missing_marker
FROM app_events e
JOIN frame_quality q USING (frame_id)
GROUP BY e.frame_id, e.phase, q.evidence_grade, q.missing_marker
ORDER BY frame_id, start_ms;

CSV 输出可以固定成这类字段:

1
2
3
4
trace_name,scenario,frame_or_request_id,phase,start_ms,dur_ms,thread_name,frame_jank_type,evidence_grade,missing_marker
run01.perfetto-trace,feed_scroll,1082,DecodeFrame,1240.2,5.8,decoder-1,jank,complete,
run01.perfetto-trace,feed_scroll,1082,UploadTexture,1248.7,9.1,RenderThread,jank,complete,
run01.perfetto-trace,feed_scroll,1082,RenderSubmit,,,,jank,partial,RenderSubmit

跨线程任务用 flow 或独立 track

很多业务慢不在单个函数里,而在队列之间。比如 UI 线程提交一个请求,decoder 线程处理,render 线程消费。只用同线程 slice,读者很难看出它们属于同一个 frame。

可以用 flow 把相关事件连起来。不要直接把容易复用的业务 frame_id 当 flow id,先生成一次 Trace 内唯一的 token:

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
uint64_t MakeFlowId(uint64_t session_id, uint64_t stream_id, uint64_t frame_id) {
return Hash64(session_id, stream_id, frame_id);
}

void EnqueueFrame(uint64_t session_id, uint64_t stream_id, uint64_t frame_id) {
uint64_t flow_id = MakeFlowId(session_id, stream_id, frame_id);
TRACE_EVENT("player", "EnqueueFrame",
perfetto::Flow::ProcessScoped(flow_id),
"stream_id", stream_id,
"frame_id", frame_id);
}

void DecodeFrame(uint64_t session_id, uint64_t stream_id, uint64_t frame_id) {
uint64_t flow_id = MakeFlowId(session_id, stream_id, frame_id);
TRACE_EVENT("player", "DecodeFrame",
perfetto::Flow::ProcessScoped(flow_id),
"stream_id", stream_id,
"frame_id", frame_id);
}

void RenderFrame(uint64_t session_id, uint64_t stream_id, uint64_t frame_id) {
uint64_t flow_id = MakeFlowId(session_id, stream_id, frame_id);
TRACE_EVENT("player", "RenderFrame",
perfetto::TerminatingFlow::ProcessScoped(flow_id),
"stream_id", stream_id,
"frame_id", frame_id);
}

选中事件时,Perfetto UI 会用箭头展示关系。中间阶段继续使用 Flow,最终消费阶段再用 TerminatingFlow

这里用的是 ProcessScoped(flow_id),所以 id 至少要在进程内、一次 trace 生命周期内稳定,不要直接拿容易复用的临时指针当 flow id;跨进程 flow 不要继续用 ProcessScoped 语义,至少要使用全局唯一 id,并明确 namespace 和来源进程。多阶段 pipeline 也可以给每条边单独分配 flow id,例如 enqueue_to_decode_flow_iddecode_to_render_flow_id,避免一条 flow 被多条路径复用。

如果一个任务跨函数、跨线程持续很久,也可以放到独立 track 上:

1
2
3
4
5
6
7
8
9
10
11
12
13
void StartRequest(uint64_t request_id) {
perfetto::Track track(request_id);
auto desc = track.Serialize();
desc.set_name("player.request");
perfetto::TrackEvent::SetTrackDescriptor(track, desc);

TRACE_EVENT_BEGIN("player", "Request", track,
"request_id", request_id);
}

void FinishRequest(uint64_t request_id) {
TRACE_EVENT_END("player", perfetto::Track(request_id));
}

这类事件适合任务队列、播放器 pipeline、异步加载、跨线程 GPU 提交。它比“每个函数都打点”更能解释业务路径。

perfetto::Track(request_id) 里的 id 要在一次 Trace 内对这个并发任务唯一。线上 request id、frame id 很容易复用,复用后不同任务会合到同一条 track,看起来像一个长任务或错误串行。更稳的做法是加 session/pipeline namespace,或者生成专用 64-bit track id;需要长期阅读时,补 track descriptor / name,让 UI 和 SQL 都能识别生命周期。descriptor / interning 丢失时,要回到第 12 篇的 stats 检查。

Counter 只放状态,不放日志

counter 适合随时间变化的数值,例如队列长度、in-flight 请求数、buffer 占用。frame id 是标识符,不是可聚合指标;它更适合放在 slice 参数或 flow id 里。这个例子记录 decode 队列深度:

1
2
3
4
5
void ReportDecodeQueueDepth(int depth) {
TRACE_COUNTER("player",
perfetto::CounterTrack("DecodeQueueDepth", "items"),
depth);
}

counter 不适合承载日志文本,也不适合拿标识符做 avg/max。日志放 logcat,Trace 里只保留能和时间轴关联、可以被解释为状态的数值。

counter 要固定单位和聚合口径。DecodeQueueDepth_items 看峰值和窗口内均值,InFlightRequests_count 看峰值和持续时间,BufferBytes_bytes 看峰值、增长斜率和是否回落;不要对 frame_idrequest_id 这类标识符求 avg/max。

事件名是 SQL 接口

事件名一旦进了 SQL、看板和回归报告,就变成了接口。不要把动态 id 拼进事件名。

1
2
3
4
5
// 不建议:每个 frame 都会生成不同事件名,SQL 很难稳定统计。
TRACE_EVENT("player", perfetto::DynamicString{"DecodeFrame_123"});

// 建议:事件名稳定,动态信息放参数。
TRACE_EVENT("player", "DecodeFrame", "frame_id", frame_id);

更好的命名方式是“阶段 + 动作”:

  • DecodeFrame:解码一个业务帧,参数带 frame_idstream_idcodec
  • UploadTexture:上传纹理或 buffer,参数带 frame_idtexture_bytes
  • RenderScene:提交一轮场景渲染,参数带 scene_idsurface_id
  • SubmitRequest:提交一个跨线程请求,参数带 request_idqueue_depth
  • WaitForBuffer:等待上游 buffer,可和 Binder、线程状态、buffer queue 证据关联。

事件名稳定以后,后面的 SQL 才能稳定。这里顺手引入 time.conversion,让最大耗时的单位直接写在 SQL 里;平均值保留一位小数时仍然可以用 / 1e6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
INCLUDE PERFETTO MODULE slices.with_context;
INCLUDE PERFETTO MODULE time.conversion;

SELECT
process_name,
thread_name,
name,
COUNT(*) AS count,
ROUND(AVG(dur) / 1e6, 1) AS avg_ms,
time_to_ms(MAX(dur)) AS max_ms
FROM thread_slice
WHERE process_name = 'com.example.app'
AND name IN ('DecodeFrame', 'UploadTexture', 'RenderScene')
GROUP BY process_name, thread_name, name
ORDER BY max_ms DESC;

控制写入量

Track Event 开销低,但不是免费。第 12 篇讲过 data loss,这里要先拆清两条路径:android.os.Trace / ATrace 写太密会压 ftrace per-CPU buffer;Perfetto SDK Track Event 写太密主要压 producer shared memory、central buffer 和 incremental state。SDK Track Event 丢包时不要去调 ftrace buffer,ATrace 丢事件时也不要只盯 track_event stats。

建议给每类埋点设一个预算:

  • 事件密度:按 events/sec、每帧事件数、参数字节数和目标 Trace 时长估算写入量。120Hz、多线程、多阶段和动态字符串叠加后,每帧一次也可能过密。
  • 数据形状:每个小对象、每次锁尝试、每个像素级循环都不适合默认开启;高频 debug category 默认关闭,专项 Trace 可用 1/N 采样或触发后短窗口开启。
  • 参数开销:便宜标量可直接传;昂贵字符串、JSON、容器遍历、锁内状态快照放在 enabled 判断或 lambda 内。参数在宏调用前已经拼好时,category gating 救不了这部分开销。
  • 采集验收:长 Trace 里把业务 Track Event 放到独立 central buffer,抓完检查 statsevent_count_by_categorytrack_event_loss_stats;这只能隔离 central buffer 竞争,不能修复 producer shared memory burst loss。
  • Ring Buffer 场景要关注 Track Event 的 descriptor / string interning / incremental state,可以配合 incremental_state_config.clear_period_ms 或低写入率 buffer,避免后半段事件缺名字和上下文。

统一验收查询可以先从第 12 篇的 health check 开始,再补 Track Event 专用项:

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
SELECT name, idx, severity, source, value
FROM stats
WHERE value != 0
AND (
severity IN ('error', 'data_loss')
OR name IN (
'traced_buf_chunks_overwritten',
'traced_buf_chunks_discarded',
'traced_buf_trace_writer_packet_loss',
'traced_buf_sequence_packet_loss',
'traced_buf_incremental_sequences_dropped',
'traced_buf_patches_failed',
'track_event_parser_errors',
'track_event_tokenizer_errors',
'track_hierarchy_missing_uuid',
'track_event_thread_invalid_end',
'track_event_missing_sequence_id',
'interned_data_tokenizer_errors',
'tokenizer_skipped_packets',
'packet_skipped_seq_needs_incremental_state_invalid'
)
OR name GLOB 'track_descriptor_*'
OR name GLOB 'track_event_skipped_*'
)
ORDER BY name, idx;

报告里单独给 track_event_trust_level。比如 packet loss 为 0,但 track_event_tokenizer_errorstrack_hierarchy_missing_uuid 非 0,业务事件仍要降级。

App 侧现场 Trace 协议

App 侧接 Perfetto,最容易踩的坑是把 App 当成一个小型 adb shell perfetto 客户端。普通 App 没有随意启动系统 tracing 的权限,也不适合在线上动态下发高开销 TraceConfig。现场方案更适合拆成三层:

  • 平台侧:预置受控 TraceConfig,声明 trigger、Ring Buffer、文件路径、数据源白名单和采集上限。
  • App 侧:记录业务埋点和页面状态,检测慢帧、卡死、超时等异常,按平台约定激活 trigger。
  • 服务端:接收 Trace package,跑 stats、摘要 SQL 和聚合索引,给人工分析留出可复查入口。

Perfetto 的 STOP trigger 很适合这类场景:trace 先以 Ring Buffer 方式循环记录,App 检测到问题后触发一个已声明的名字,系统在 stop_delay_ms 之后收尾。这样能保留问题发生前的一段现场,也能把触发后的尾巴收进来。

一个受控现场 UI 卡顿 preset 可以长这样。它适合灰度白名单、采样率受控、写入速率已实测、stats 可验收的实验室或现场排查;线上默认 preset 应减少 atrace category,必要时只保留 sched、FrameTimeline 和最小 Track Event。它不是“全量图形专项 preset”,但足够支持主线程、RenderThread/HWUI、FrameTimeline、CPU 调度、SurfaceFlinger 侧 FrameTimeline/线程调度线索和 App Track Event 的第一轮定位;涉及 Layer、Transaction、HWC、Display 归因时,切换图形专项 preset。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
buffers { size_kb: 131072 fill_policy: RING_BUFFER } # 0: sched / ftrace
buffers { size_kb: 32768 fill_policy: RING_BUFFER } # 1: app track event / frame context

incremental_state_config {
clear_period_ms: 5000
}

trigger_config {
trigger_mode: STOP_TRACING
trigger_timeout_ms: 1800000 # 最长等待 trigger 的窗口
triggers {
name: "app_ui_jank"
stop_delay_ms: 1000
max_per_24_h: 8
skip_probability: 0.8
}
}

data_sources {
config {
name: "linux.ftrace"
target_buffer: 0
ftrace_config {
ftrace_events: "sched/sched_switch"
ftrace_events: "sched/sched_wakeup"
ftrace_events: "sched/sched_waking"
ftrace_events: "power/cpu_frequency"
ftrace_events: "power/cpu_idle"
atrace_categories: "gfx"
atrace_categories: "view"
atrace_categories: "wm"
atrace_apps: "com.example.app"
}
}
}

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

data_sources {
config {
name: "track_event"
target_buffer: 1
track_event_config {
disabled_categories: "*"
enabled_categories: "player"
enabled_categories: "rendering"
}
}
producer_name_filter: "com.example.app"
}

这里的 max_per_24_hskip_probability 是兜底,实际产品仍要在平台侧做用户、设备、版本、trigger 和天维度限频。遇到 GPU、SurfaceFlinger、HWC 或功耗专项时,再切换更完整的图形/功耗 preset。

测试设备上可以用命令验证 trigger:

1
/system/bin/trigger_perfetto app_ui_jank

Perfetto trigger 的模型是:有权限的 consumer 预先声明 trigger 名字,App 这类 producer 只能激活已声明的名字。接了 Perfetto SDK 的 native 模块也可以用 SDK 触发;产品上仍建议由平台接口统一控制权限和配额:

1
perfetto::Tracing::ActivateTriggers({"app_ui_jank"}, 10000);

第二个参数是触发请求的 TTL,避免 producer 还没连上 tracing service 时留下无限期的 pending trigger。

线上 App 不应假设自己能执行 /system/bin/trigger_perfetto。更推荐由平台提供受控接口,例如系统服务、厂商 SDK、企业版设备管理组件或测试框架代理。App 只提交一个受限请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"preset": "ui_jank_field_v3",
"trigger": "app_ui_jank",
"reason": "three_jank_frames_in_2s",
"scenario_id": "feed_scroll_120hz",
"session_id": "s-20260504-001",
"action_id": "scroll-42",
"page": "Feed",
"action": "scroll",
"elapsed_realtime_ns": 123456789000000,
"jank_frames": [
{
"frame_start_elapsed_realtime_ns": 123456700000000,
"duration_ns": 32000000,
"expected_duration_ns": 8333333,
"states": {
"page": "Feed",
"action": "scroll"
}
}
]
}

平台侧根据 preset 白名单、采样率、权限、设备状态和每日配额决定是否触发。这套接口约定比“App 自己拼 TraceConfig”更适合长期维护。服务端匹配时应输出 trigger_event_tsnearest_markermarker_delta_msmatched_by,避免外部 JSON 和 trace 时间轴只靠人工猜。

触发策略要靠近用户感知,而不是靠近内部实现细节:

触发 典型信号 Trace 里要看什么
UI 慢帧 JankStats 连续慢帧、严重超预算 主线程、RenderThread、FrameTimeline、CPU
主线程卡死前兆 2s / 4s / 8s watchdog 主线程状态、Binder、锁、IO、系统服务
业务 timeout Camera open、播放器 prepare、首屏 timeout App marker、Binder、HAL、线程状态

触发名进入平台 preset 后要保持稳定。改名会让服务端聚合、SQL 模板和历史数据断开。

Trace package 要带上下文

一份现场包先分最小必需和平台化扩展。第 13 篇只要求最小必需:Trace 本体、业务 metadata、采集 preset、stats 可信度结果。上传策略、日志白名单和保留周期属于平台化后的规则,可以后续单独设计。

1
2
3
4
5
6
7
8
trace-package/
trace.perfetto-trace
app_metadata.json
app_events.jsonl
trace_preset.txt
trace_stats.json
upload_policy.json # 平台化后再补
log_excerpt.txt # 平台化后再补

app_metadata.json 描述这次问题现场,字段要固定并带 schema 版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"schema_version": 3,
"app_version": "8.12.0",
"package_name": "com.example.app",
"page": "Feed",
"action": "scroll",
"trigger": "app_ui_jank",
"trigger_reason": "three_jank_frames_in_2s",
"trigger_elapsed_realtime_ns": 123456789000000,
"scenario_id": "feed_scroll_120hz",
"session_id": "s-20260504-001",
"action_id": "scroll-42",
"refresh_rate_hz": 120,
"thermal_state": "normal",
"trace_preset": "ui_jank_field_v3"
}

其他几个文件也要有最小字段:

trace_stats.json 至少保留第 12 篇 health check 的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"trace_quality_status": "partial",
"track_event_trust_level": "weak",
"stats": [
{
"name": "traced_buf_chunks_overwritten",
"idx": 1,
"idx_semantics": "buffer",
"severity": "info",
"source": "trace",
"value": 12,
"affected_evidence": "track_event",
"decision": "degrade_app_marker_metrics"
}
]
}

upload_policy.json 说明这份现场包能不能上传、保留多久:

1
2
3
4
5
6
7
{
"policy_version": 2,
"network": "wifi_only",
"daily_device_quota": 3,
"retention_days": 14,
"encryption": "required"
}

app_events.jsonl 用一行一个 JSON 事件保存触发点和少量 App 侧状态,并和 Track Event 共享 scenario_id/session_id/action_id

1
{"elapsed_realtime_ns":123456789000000,"event":"trigger","scenario_id":"feed_scroll_120hz","session_id":"s-20260504-001","action_id":"scroll-42","page":"Feed","action":"scroll"}

log_excerpt.txt 只放白名单 tag 的短窗口摘要,至少记录 tag、level、elapsed realtime 和脱敏后的 message。

时间戳建议统一使用 elapsedRealtimeNanos / CLOCK_BOOTTIME 语义,并在字段名里写清楚。Perfetto 能同步 trace packet 里带 timestamp_clock_id / ClockSnapshot 的时间域,但外部 JSON 不会自动归一。metadata 里最好记录 trace start/end 的 elapsed realtime,或者服务端使用明确 offset 把外部事件放回 Trace 窗口。Track Event 默认让 SDK 取时间;只有能保证 clock domain 和转换逻辑时才传显式 timestamp,并记录 clock source、offset 和单位。

App 侧不要直接截断 trace.perfetto-trace。Perfetto trace 是 protobuf 流,粗暴二进制裁剪可能破坏 packet、时钟同步和增量状态。更稳的做法是在采集侧控制窗口:Ring Buffer 控制触发前窗口,stop_delay_ms 控制触发后窗口,trigger 限频控制重复抓取。

max_file_size_bytes 是文件大小硬上限,达到后会停止 tracing;长时间 flight recorder 如果 cap 太小,可能在 trigger 前就结束。它适合和 write_into_file / file_write_period_ms 一起控制落盘风险,不是窗口裁剪工具。

平台化后,上传策略再把配额、隐私和重试写进产品规则:

  • 设备侧配额:按用户、设备、版本、trigger 和天维度限频。
  • 网络条件:默认只在 Wi-Fi 或用户允许的网络上传,低电量、温度异常、后台受限时延后。
  • 内容白名单:metadata 字段固定,log 只允许白名单 tag,App trace section 禁止拼接用户输入。
  • Trace 本体:最小化数据源,限制进程/producer,避免默认 logcat;ftrace、sched、Binder、线程名、进程名本身也可能是敏感信息。
  • 传输和保留:加密上传,服务端访问审计,Trace、log 摘要、聚合结果设置独立 TTL,到期自动清理。

App 提供业务阶段、异常触发和 metadata;平台提供受控 preset、trigger 和文件管理;服务端提供可信度检查、摘要和聚合。三部分配齐之后,Perfetto 才能从一次次手工抓取,进入可持续的线上问题排查流程。

总结

Perfetto SDK 的价值是补业务语义:系统知道线程在跑,SDK 告诉你这段时间在 decode 哪一帧、队列有多深、请求从哪里流到哪里。

工程上建议按这个顺序做:Java/Kotlin 层先用 android.os.Trace,native 长期埋点用 Track Event,复杂结构再考虑自定义 DataSource。事件名稳定、category 可控、counter 有单位、flow 有稳定 id,后面的 SQL 和报告才可长期复用。进入现场 Trace 后,App 还要把触发、metadata、证据包和上传策略一起设计好。

参考文档

  1. Tracing SDK
  2. Track events
  3. Recording In-App Traces with Perfetto
  4. androidx.tracing.perfetto
  5. Trace configuration - Triggers
  6. android.os.Trace
  7. JankStats Library

关于我 && 博客

欢迎关注 Android Performance

CATALOG
  1. 1. 什么时候需要业务语义
  2. 2. SDK 接入边界
  3. 3. 业务阶段字典先于代码
  4. 4. 两种 backend 解决不同问题
  5. 5. 最小接入骨架
  6. 6. category 是线上开关
  7. 7. 和系统 Trace 一起抓
  8. 8. 跨线程任务用 flow 或独立 track
  9. 9. Counter 只放状态,不放日志
  10. 10. 事件名是 SQL 接口
  11. 11. 控制写入量
  12. 12. App 侧现场 Trace 协议
  13. 13. Trace package 要带上下文
  14. 14. 总结
  15. 15. 参考文档
  16. 16. 关于我 && 博客