从 Perfetto Android Trace 定位最耗 CPU 的线程:PerfettoSQL 查询方法与 Agent 评测案例实战
2026/9/17 13:48:05 网站建设 项目流程

从 Perfetto Android Trace 定位最耗 CPU 的线程:PerfettoSQL 查询方法与 Agent 评测案例实战

【免费下载链接】perfettoProduction-grade client-side tracing, profiling, and analysis for complex software systems.项目地址: https://gitcode.com/GitHub_Trending/pe/perfetto

拿到一份 Android 手机上的 Perfetto Trace,如何用trace_processor准确回答"整段 trace 里哪个线程消耗的 CPU 时间最多、大约是多少、它属于哪个进程"?这是 Perfetto 官方为 AI 编码 Agent 设计的一组评测(eval)案例中最具代表性的一道题。本文以仓库中ai/evals/cases/cpu-top-thread/prompt.md这一评测用例为核心,完整还原问题定义、Ground Truth 的计算方式(sched_sliceutid求和)、可直接复现的 PerfettoSQL 查询语句,并结合sched.with_context标准库模块的源码,讲清楚底层sched表的数据结构,最后说明这套评测框架如何自动化判定 Agent 回答的对错。读完你既能独立完成这类 CPU 热点分析,也能理解 Perfetto 官方如何用"评测驱动"的方式验证 Agent 技能的有效性。

一、案例是什么:一个标准的 Android Trace CPU 分析问题

ai/evals/cases/cpu-top-thread/prompt.md是一个评测案例的完整定义文件,采用"YAML frontmatter + 提示词正文"的结构。其核心是一道面向 AI Agent 的开放式问题,但它的解题思路对人工分析同样成立。

1.1 frontmatter:案例的元信息

--- name: Top CPU thread in an Android system trace tags: [android, cpu, adhoc] runs: 3 files: - src: "{repo}/test/data/example_android_trace_30s.pb" dst: trace.pb ---

各字段的含义如下:

字段含义
nameTop CPU thread in an Android system trace案例名称:Android 系统 trace 中的 CPU 占用最高线程
tags[android, cpu, adhoc]按 ai/evals/README.md 的约定:android/cpu是领域标签(问题关于什么);adhoc是"路线"标签(该问题主要靠针对核心表的裸 SQL 解答,而非某个标准库模块或现成 runbook 全权接管)
runs3每个条件(condition)下重复运行 3 次。原因在 README 中有明确说明:非确定性的 Agent 单次运行只是噪声,必须多次取聚合
files{repo}变量展开为仓库路径,把测试 trace 复制到工作区并命名为trace.pb每次评测都会在系统临时目录下创建全新工作区,只复制案例声明的输入文件

注意{repo}是一个会被 run_evals.py 中的expand()函数替换的模板变量。所用的输入 trace 是test/data/example_android_trace_30s.pb(对应的校验文件为 test/data/example_android_trace_30s.pb.sha256,另有.gz压缩版),即一段 30 秒的 Android 系统级 trace,通过 Linux ftrace 采集了调度信息。

1.2 提示词正文:一句话的开放式问题

I have a Perfetto trace from an Android phone at ./trace.pb. Which thread used the most CPU time over the whole trace, and roughly how much? Give me the thread name and the process it belongs to.

问题拆解成三个必须回答的子问题:

  1. 整段 trace(约 30 秒)内,哪个线程消耗的 CPU 时间最多;
  2. 大约多少(对数量级有要求,但允许"roughly");
  3. 该线程的名称以及它所属的进程名称。

注意提示词刻意没有出现 "Perfetto" 字样以外的任何提示(与no-mention类陷阱案例不同,本题明确给出了 trace 类型),但它属于adhoc路线——标准库的sched.with_context模块可以大幅简化查询,却并非唯一解法,Agent 完全可以只对核心表sched写裸 SQL 得到正确答案。

二、Ground Truth:评测如何定义"正确回答"

评测的价值在于有一个可自动验证的标准答案。ai/evals/cases/cpu-top-thread/graders/目录下的四个 grader 文件共同定义了本题的判分规则,其中也记录了标准答案的计算方式。

2.1 标准答案(来自 grader 文件注释)

thread-name.md的说明直接给出了 Ground Truth 的完整描述:

Ground truth (sched_slice summed per utid):CrRendererMainincom.android.chrome:sandboxed_process0with~2273 msof CPU time, ahead of traced_probes (~1496 ms) and .android.chrome (~1442 ms).

即在example_android_trace_30s.pb中,CPU 时间排名前三的线程是:

排名线程名所属进程CPU 时间
1CrRendererMaincom.android.chrome:sandboxed_process0~2273 ms (2.27 s)
2traced_probestraced_probes~1496 ms
3.android.chromecom.android.chrome~1442 ms

值得注意的细节:这是一个"渲染进程"(Chrome 的sandboxed_process0)而非主进程,说明真实世界的 CPU 热点常常不在名字最显眼的进程里——这正是该案例想考验 Agent 是否真正逐线程统计,而不是凭直觉猜一个进程。

2.2 判分方式:四个 grader

grader 文件type判定内容
cpu-time.mdregex答案中报告的 CPU 时间须落在 ~2273 ms 的 1% 容差内(正则同时接受2.2x s22xx ms2.27等不同书写形式,兼容逗号/点号小数点)
process-name.mdregex答案必须包含进程名sandboxed_process0
thread-name.mdregex答案必须包含线程名CrRendererMain
used-trace-processor.mdbashscored: false过程指标:Agent 是否真的驱动了trace_processor(而不是用 Python API、UI 或凭空猜测),不计入分数

这套设计体现了 ai/evals/README.md 中反复强调的原则:"Outcomes first, process second"——regex 判分器只检查答案中的事实(线程名、进程名、数值),Agent 无论走什么路径(裸 SQL、标准库模块、多条查询组合)只要答对就算过;是否使用了trace_processor只作为过程指标单独报告,不掺入正确性分数。而 CPU 时间正则专门设计了 ~1% 的容差并兼容多种单位书写,是因为"roughly how much"本身就允许近似,不同 Agent 的舍入习惯也不一致。

三、手把手复现:用 trace_processor 和 PerfettoSQL 求解

无论你是人类分析师还是被评测的 Agent,标准解法都一样:把 trace 加载进trace_processor,对每个线程的调度切片(sched slice)的dur求和,取最大值。下面是完整的可复现流程。

3.1 加载 trace 并启动查询会话

按 ai/skills/perfetto/infra-references/querying.md 的推荐,用"一次加载、多次查询"的模式:

trace_processor server unix --name topcpu --daemonize ./trace.pb trace_processor query --remote topcpu "SELECT 1" # 验证会话可用
  • 解析(loading/parsing)是 trace_processor 最耗时的阶段,大型 trace 可能耗时数秒到数分钟,因此复用会话是效率关键;
  • 会话状态跨调用保持:一次INCLUDE PERFETTO MODULECREATE PERFETTO TABLE之后,后续查询都能看到;
  • 空闲会话 30 分钟后会被回收,用完记得trace_processor server kill topcpu
  • 若只做一次查询,也可以trace_processor query ./trace.pb "...",但每次都会重新解析整个 trace。

3.2 方案 A:使用标准库模块(推荐)

INCLUDE PERFETTO MODULE sched.with_context; SELECT thread_name, process_name, SUM(dur) / 1e6 AS cpu_ms FROM sched_with_thread_process GROUP BY utid ORDER BY cpu_ms DESC LIMIT 10;

sched_with_thread_process视图已经帮我们把线程名、进程名 join 到每一条调度切片上了,查询一行都不用写 join。预期输出第一名即CrRendererMain / com.android.chrome:sandboxed_process0 / 2273左右。

3.3 方案 B:直接对核心表写裸 SQL(adhoc 路线)

SELECT utid, SUM(dur) / 1e6 AS cpu_ms FROM sched GROUP BY utid ORDER BY cpu_ms DESC LIMIT 5;

得到utid后再查threadprocess表还原名称:

SELECT t.name AS thread_name, p.name AS process_name FROM thread t JOIN process p USING (upid) WHERE t.utid = <top_utid>;

utid是 trace 内的唯一线程标识,跨进程时不会像 OS 的tid那样被回收复用,因此是可靠的 join 键。这也是 querying.md 中 PerfettoSQL 规则之一:join 用utid/upid,报告给用户时用名称。

3.4 验证与坑位

  • dur以纳秒为单位,除以1e6得到毫秒;dur = -1表示切片到 trace 结束时仍未关闭,若需要边界内求和应使用IIF(dur = -1, trace_end() - ts, dur)
  • 答案中的"约 2273 ms"来自对sched_slice(即sched表)按utid求和,这是评测认定的 Ground Truth 口径;
  • 该 trace 通过 ftrace 的sched/switchsched/wakeup*事件采集调度信息,因此sched表覆盖完整;若 trace 未开启这些事件,sched表为空,则需改用thread_state(Running 状态区间)统计——但本案例的 trace 是完整的系统级 trace,不存在此问题。

四、源码级原理:sched表与sched_with_thread_process视图

要理解为什么"按sched表求和"就是"CPU 时间",需要看sched表的数据来源。Linux ftrace 的sched_switch事件记录的是每一次 CPU 上的线程切换:谁在哪个 CPU 上从什么时候运行到什么时候。Perfetto 的 trace 处理器把这些事件还原为sched表,每一行是一条"运行区间"(Running period),字段包括ts(开始时间戳)、dur(持续时间)、utid(运行的线程)、cpuend_state(切片结束时的内核调度状态)、priority等。因此对sched.dur按线程求和,得到的就是该线程在整个 trace 期间占用的真实 CPU 时间,这与thread_state表中状态为Running的区间一一对应。

标准库视图定义在 src/trace_processor/perfetto_sql/stdlib/sched/with_context.sql:

INCLUDE PERFETTO MODULE std.thread.with_context; CREATE PERFETTO VIEW sched_with_thread_process( id ID(sched.id), ts TIMESTAMP, dur DURATION, utid JOINID(thread.id), thread_name STRING, upid JOINID(process.id), process_name STRING, cpu LONG, end_state STRING, priority LONG ) AS SELECT sched.id, sched.ts, sched.dur, utid, upid, _thread_with_process.thread_name, _thread_with_process.process_name, sched.cpu, sched.end_state, sched.priority FROM _thread_with_process JOIN sched USING (utid);

从源码可以读出几个关键设计:

  1. 线程/进程上下文被预 join 进_thread_with_process(来自std.thread.with_context模块),使外层所有 join 都变成 INNER JOIN。注释解释了原因:SQLite 不会把虚拟表跨 LEFT JOIN 重排,若直接用thread LEFT JOIN process再 joinsched,查询规划器就无法从sched.id驱动 id-keyed join;
  2. "维度表在前、大事实表在后"的 join 顺序(_thread_with_process在前、sched在后),让规划器可以从任一被过滤的一侧驱动,保证大规模 trace 上的性能;
  3. 视图的end_state字段用单个字符编码线程结束时的调度状态(R 可运行、S 等待唤醒、D 不可中断睡眠、Z 僵尸等),配合priority字段,可以进一步分析"线程为什么没在跑"。

这段源码既是本题 Ground Truth 的实现基础,也印证了评测 README 中"Ground truth from trace_processor itself"的原则:每个 grader 的答案都来自用trace_processor跑真实查询得到的结果,而不是人工编造。

五、评测如何运行:把这个问题放进 Agent 评测框架

本题只是 ai/evals 评测集(共 11 个案例,覆盖 ANR、binder、jank、内存、GPU 等主题)中的一个。它的运行机制决定了这道题的结论是否可信。

5.1 条件(conditions)与对照实验

conditions.json 定义了评测的对照变量,核心是"有技能 vs 无技能":

条件内容
baseline什么都不装、PATH 上什么都没有——Agent 第一次接触时的默认行为
baseline-tp仅把trace_processor加入 PATH,不装技能
skill-published装载用户实际安装的已发布技能
skill-local装载本仓库 ai/skills 的开发中版本
skill-local-tp本地技能 + PATH 上的 trace_processor

只有"同一任务、装与不装技能"的对照才有意义——这正是 ai/skills/README.md 中结论的依据:单有二进制在 PATH 上,Opus 这类强模型几乎能答对所有问题,技能的价值不在于从头教 SQL,而在于提升效率(一次加载多次查询)、诚实性(报告 trace 里的真实数字)和在引导式工作流上的可靠性。

5.2 运行与判定流程

按 ai/evals/README.md 与 run_evals.py 的说明,一次 trial 的流程是:

  1. 在系统临时目录创建全新工作区,仅复制案例声明的输入文件(如trace.pb);
  2. 应用条件(是否装技能、PATH 里有什么、额外环境变量);
  3. Agent 以非交互模式通过自身 CLI 运行,完整事件流被记录;
  4. 转录被归一化为与 harness 无关的形态,然后依次用 regex / bash / tool_used 等确定性判分器评分,只有"是否基于证据、是否编造"这类正则查不了的标准才交给 LLM judge;
  5. 同时计算过程指标:是否调用技能、是否跨查询保持 trace 加载、多少次 trace_processor 调用、多少 SQL 错误、成本与耗时、是否作弊找到本地构建等。

隔离是这套评测的前提:工作区放在仓库之外,repo、主 checkout 和~/.local/share/perfetto对 Agent 隐藏(Linux 用 bubblewrap 以空 tmpfs 覆盖),防止 Agent 直接翻仓库找到构建好的二进制——否则对照实验就没有意义。

5.3 动手运行

# 准备资源(构建 trace_processor_shell、准备测试 trace、装配外部资产) ai/evals/setup_assets.py --out ~/perfetto-eval-assets \ --tp-binary out/mac_release/trace_processor_shell export EVAL_ASSETS=~/perfetto-eval-assets # 带技能 vs 不带技能,每个案例跑 3 次,5 个并行 ai/evals/run_evals.py run --conditions baseline-tp,skill-local --runs 3 --jobs 5 # 只看 CPU 相关案例、换用 Codex 或更便宜的模型 ai/evals/run_evals.py run --agent codex --tag cpu --conditions baseline-tp,skill-local ai/evals/run_evals.py run --model sonnet --budget 2 --conditions baseline-tp,skill-local # 修改判分器后重新判分(保留已有 LLM 判定) ai/evals/run_evals.py grade ai/evals/results/<name> --skip-llm # 多目录结果横向对比 ai/evals/run_evals.py compare ai/evals/results/a ai/evals/results/b \ --conditions baseline-tp,skill-local

六、从案例到方法论:这类 CPU 分析的通用套路

cpu-top-thread案例虽然只有一句话,但它代表的是一整类"CPU 热点定位"问题的分析范式,可直接推广到更复杂的场景:

  1. 先查标准库,再写裸 SQLsched.with_context这类模块把最常用的 join 都封装好了;不确定有哪些可用对象时,可查询__intrinsic_stdlib_objects表搜索(WHERE regexp('cpu|sched', summary, 'i'));
  2. utid聚合而不是按tid。OS 的tid会被回收复用,跨进程 join 会串数据;
  3. 区分"CPU 时间"与"墙钟时间"。本题问的是 CPU 时间(sched.dur求和),如果一个线程长时间挂起但几乎不占 CPU,它不会出现在榜首;
  4. 报告名称而非 IDutid/upid在一次运行内稳定但跨运行不稳定,向用户报告thread.name/process.name
  5. 留意容器/子进程。本题的答案藏在 Chrome 的sandboxed_process0渲染进程里——排名靠前的往往是后台渲染、媒体或 GC 线程,而不是名字最响亮的进程。

如果你想扩展这套评测,ai/evals/README.md 的 "Adding a case" 一节给出了明确准则:选一个真实用户会问的问题 + 一段能回答它的test/datatrace;用trace_processor_shell算出答案并把查询写进 grader 正文;每个 regex grader 只查一个事实;先在 baseline 条件下跑 3 次验证案例的有效性——一个所有条件都能过的案例说明不了任何问题,baseline 过不了、改动后能过的案例才有区分度。


本文事实依据汇总:问题定义来自 ai/evals/cases/cpu-top-thread/prompt.md;标准答案与判分规则来自 ai/evals/cases/cpu-top-thread/graders/ 下的四个 grader 文件;评测框架与运行命令来自 ai/evals/README.md 和 run_evals.py;条件定义来自 conditions.json;查询方法来自 querying.md;视图实现来自 with_context.sql;输入 trace 位于 test/data 目录。

【免费下载链接】perfettoProduction-grade client-side tracing, profiling, and analysis for complex software systems.项目地址: https://gitcode.com/GitHub_Trending/pe/perfetto

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询