ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于 Perfetto 与 AI 的 Android 性能自动化诊断方案

基于 Perfetto 与 AI 的 Android 性能自动化诊断方案 基于 Perfetto 与 AI 的 Android 性能自动化诊断方案1. 为什么要给 Perfetto 诊断套上一层 AI 自动化1.1 从人工拖时间轴到批量诊断的转变做 Android 性能优化的人基本都经历过这个场景线上压测或用户反馈卡之后我们拿回一份 pftrace 文件打开 Perfetto UI把时间轴放大缩小反复拖找掉帧区间看主线程是不是长时间 Running 或 Runnable查 CPU frequency再看 Binder 调用有没有阻塞。问题简单时几分钟能定位但一旦涉及多线程竞争、调度延迟、CPU 变频叠加人工分析就变得又慢又靠经验。Perfetto 本身足够强大它解决的问题是把系统运行过程完整录下来而不是告诉你问题出在哪。一份 10 秒的 trace 可能包含几十个进程、上千个线程、几十万条事件。人眼根本扫不完只能挑重点看。而机器学习和大模型恰恰擅长从大量事件里找异常模式再结合规则引擎做归因最后输出自然语言诊断结论。这套方案的本质是把 Perfetto 的采集能力 TraceProcessor 的结构化查询能力 AI 的归纳推理能力串成一条流水线采集端自动抓 trace解析端把 trace 转成特征表诊断端把特征表映射成业务上可理解的结论。1.2 适合自动化的 trace 分析对象不是所有 trace 分析都适合交给 AI但下面这四类很适合第一类是卡顿定位。说白了就是哪一帧掉了当时主线程在干什么。这类分析特征非常清晰可以从 frame timeline、主线程切片、sched 表、Binder wait 里提取数据和结论之间的因果关系相对固定。第二类是异常功耗分析。比如一个应用在后台频繁唤醒 CPU这种模式通常会体现在 wakelock、alarm、cpu_idle 状态上用统计模型就能发现偏离正常基线的行为。第三类是启动耗时分段。应用冷启动经过 attach、layout、第一帧绘制等阶段每个阶段的耗时是否超标是明确的时间范围问题规则引擎可以初步判断AI 负责解释超标原因。第四类是跨线程资源竞争。比如两个线程抢同一个锁导致关键线程长时间 Waiting。这类问题光靠肉眼很难在 trace 里快速串联但 AI 如果把线程状态和锁事件放在一起看很容易给出疑似持锁线程是 xxx这样的提示。1.3 规则引擎与大模型的合理分工我见过不少团队一上来就想让大模型直接读原始 trace这几乎必失败。原始 trace 太大Token 数根本不够用而且 Event 之间的关联是结构化的光靠 LLM 的上下文窗口理解不靠谱。合理的做法是规则引擎做粗筛大模型做精判。规则引擎负责三件事第一把 trace 中的事件转成统计特征比如某线程 on-CPU 时间占比、某切片的累计时长、掉帧数量第二按照已知的优化经验做一级判断比如主线程 Runnable 但 16ms 没有调度这可以直接跳过 AI第三压缩上下文把 trace 部分转成长度可控的摘要让大模型在有限 Token 内读到关键信息。AI 大模型负责四件事跨特征关联归因、生成自然语言的诊断结论、把分析结果组织成报告、以及针对新出现的异常类型提供解释建议。规则引擎能覆盖的场景不用 AI 重复劳动AI 不能覆盖的再去调用这样既稳定又省成本。2. 采集侧工程化准备让 Trace 数据足够规范2.1 TraceConfig 的最小可靠配置Perfetto 的能力很强但默认配置往往不是为自动化诊断准备的。你在命令行里随手敲perfetto -t 10s -o /data/misc/perfetto-traces/test.pftrace拿到的文件很可能缺这缺那分析时才发现关键事件没录上。我建议所有自动化采集都用显式 TraceConfig。下面这份配置是我的常用底稿它能覆盖大多数性能诊断场景buffers { size_kb: 131072 fill_policy: RING_BUFFER } data_sources { config { name: linux.ftrace ftrace_config { ftrace_events: sched/sched_switch ftrace_events: sched/sched_waking ftrace_events: power/cpu_frequency ftrace_events: power/cpu_idle ftrace_events: kmem/mm_event ftrace_events: binder/binder_transaction ftrace_events: binder/binder_transaction_received atrace_categories: gfx atrace_categories: view atrace_categories: am atrace_categories: input atrace_categories: wm } } } duration_ms: 10000这里面有几处关键细节。size_kb: 131072是 128MB 的环形缓冲区对于大多数中端机 10 秒的抓取是够用的如果只抓 sched 和 cpu_frequency建议 64MB 起步加了 binder 和 mm_event 之后最好 128MB。fill_policy选RING_BUFFER这样抓取过程中若缓冲区写满新事件会覆盖旧事件保证抓到停止时刻附近的信息对事后诊断价值更大。ftrace_events里的每一项必须和内核配置对上。比如binder/binder_transaction_received在一些 4.19 内核上不存在配置下发时 Perfetto 会报错如果错误处理不当整个 trace 都抓不到。2.2 数据源选型与开销控制Perfetto 的数据源很多但自动化场景下控制采集开销是第一位。如果采集动作本身拖慢了系统那诊断出的性能问题就是假的。优先选 ftrace 事件因为它们走内核 tracefs整体开销非常低。Atrace 打点的用户空间事件开销略高但 gfx、input、view 这几类对卡顿分析非常关键必须开。sampling profiler 不建议在自动化采集中常开因为周期采样 CPU 会占用额外资源更适合单独做深度的函数级分析。heap profiling 更别轻易开它要 native 内存分配会给应用带来明显性能和内存压力。曾经有团队把 heapprofd 集成进常规性能回归测试结果误报率奇高最后排查发现是 profiler 本身导致应用主线程分配变慢。如果要做功耗分析加上power/cpu_idle和power/cpu_frequency就够了。不需要额外抓thermal事件除非明确在看温控降频。2.3 自动化采集的触发策略采集时机决定了 trace 是素材还是噪声。我见过最简单的做法是在压测脚本里固定抓 15 秒但问题发生于压测中后段前 10 秒采的数据全浪费了或者问题只出现在某一次操作里恰好没抓到。现在自动化平台普遍采用事件驱动 环形缓冲的策略。让 Perfetto 开启一个长时间运行的跟踪会话使用环形缓冲模式正常运行时不断往缓冲区写入当某个指标超过阈值时立刻冻结缓冲区并把内容导出。这样就能保证拿到的是出问题那一刻及之前的数据。在 Android 上面可以通过Config.Flags控制trace_buffer_siz和write_into_file也可以用一个常驻 daemon 实现连续抓取。实际操作中更简单的方法是压测脚本里预置一个持续 120 秒的跟踪会话脚本在每次关键操作前打 tag用 Perfetto 的--attach机制后续操作外导对应时段数据。这样虽然会存很多临时文件但不会漏重要场景。2.4 抓取时常见数据缺失原因采集侧常见的坑我列几个真实遇到过的ftrace buffer 被系统其他组件抢占。低内存设备上系统可能主动关闭 tracefs 以释放内存导致你的数据源无声无息地消失。检查方法是在 trace 里查ftrace_idle事件如果出现大段缺失大概率是这个原因。SELinux 权限问题。普通应用的 shell 进程不一定有权限开启所有 atrace category。比如想抓am的张量数据但权限不够Perfetto 会跳过该 category而你看到的 trace 里没有 AM 事件还在傻傻排查为什么ActivityTaskManager日志那么少。时区与命名问题。多个设备同时回传 trace 文件时如果文件名只用了时间戳区分但设备时间不同步后续分析排序会全乱。归档时必须绑定 device id 轮次 场景名这个习惯能帮你省很多时间。3. 用 TraceProcessor 把 Trace 变成结构化样本3.1 为什么用 SQL 做特征提取而不是直接丢给大模型Perfetto 自带的 TraceProcessor 会加载 trace 文件解析成一张张表process、thread、thread_slice、sched、counter、counter_track等。你可以通过 SQL 对这些表做关联查询这比直接读原始事件文件高效得多。我把直接丢大模型和先做特征提取的区别总结一下维度直接丢原始 Trace 给大模型先用 SQL 提取特征Token 消耗一份 10 秒 trace 转文本可达百 MB根本传不完特征表控制在几百行内推理速度极慢基本不可用秒级或毫秒级结论稳定性大模型容易漏关键事件规则 SQL 保证关键指标不丢可维护性换一个 trace 文件版本就失效SQL 逻辑固定容易回归测试特征提取的思路是把 trace 压缩成一份诊断报告样本。比如你想看主线程 UI 是否被卡住你要的不是sched表里几千条切换记录而是主线程在 [开始, 结束] 内 Running 总时长、Runnable 总时长、连续 Waiting 最长一次、发生了 N 次 Binder 调用这些摘要指标。3.2 一个可运行的 CPU 归因 SQL下面这个 SQL 可以用来快速找出指定时间窗内 CPU 占用前十的线程SELECT thread.name AS thread_name, process.name AS process_name, COUNT(*) * 1e6 AS running_time_ns FROM sched LEFT JOIN thread USING (utid) LEFT JOIN process USING (upid) WHERE sched.ts BETWEEN 1000000000 AND 2000000000 -- 纳秒时间戳按实际窗口调整 AND sched.dur 0 GROUP BY thread.name, process.name ORDER BY running_time_ns DESC LIMIT 10;这里字段含义要解释清楚。sched表里每一行是一条线程在 CPU 上的调度段dur是纳秒持续时间。10 秒的 trace 窗口换成纳秒就是10 * 1e9。COUNT(*) 在这里不是简单计数而是表示总调度次数再乘以 1e6 得到的是纳秒转换为毫秒后的总 CPU 占用但实际上是毫秒因为dur是纳秒且核数可能多个所以我们用SUM(dur)更准确SELECT thread.name AS thread_name, process.name AS process_name, SUM(sched.dur) / 1e6 AS cpu_ms FROM sched LEFT JOIN thread USING (utid) LEFT JOIN process USING (upid) WHERE sched.ts BETWEEN 1000000000 AND 2000000000 AND sched.dur 0 GROUP BY thread_name, process_name ORDER BY cpu_ms DESC LIMIT 10;SUM(sched.dur) / 1e6得到的是毫秒因为原始 dur 是纳秒。这个结果可以直接作为特征值输入 AI比如com.foo.player 的 RenderThread 在该窗口内占用了 4.2 秒 CPU就这么一句话大模型就能判断渲染线程负载异常偏高。3.3 Jank 帧与主线程状态的关联分析卡顿诊断最核心的关联是Jank 帧发生的时间点和主线程当时在做什么。Perfetto 会对 Android 的 Choreographer 打点形成draw等 slice。通过查询 slice 表可以找到掉帧区间SELECT slice.ts, slice.dur, slice.name, thread.name AS thread_name FROM slice JOIN thread_track ON slice.track_id thread_track.id JOIN thread USING (utid) WHERE slice.name Choreographer#doFrame OR slice.name LIKE %DrawFrame% ORDER BY slice.ts;然后把掉帧时间点传给下一条 SQL查该时间段内主线程的所有状态SELECT sched.ts, sched.dur, sched.end_state, sched.priority FROM sched JOIN thread USING (utid) WHERE thread.name main AND sched.ts 1005000000000 AND sched.ts 1008000000000 ORDER BY sched.ts;把这些结果拼起来AI 能得出非常清晰的结论在 Jank 帧时间窗口内主线程有两次超过 100ms 的 Uninterruptible Sleep且这两次都发生在 Binder 调用后可能与磁盘 I/O 或 Remote 端响应慢有关。如果你把这两条 SQL 固化成一个模板每次拿到 trace 后只改时间戳整套诊断流程的可复现性会非常高。3.4 Python 侧组装诊断样本Perfetto 提供了 Python APItrace_processor我们可以在脚本里执行 SQL直接拿到 DataFrame。import pandas as pd from perfetto.trace_processor import TraceProcessor tp TraceProcessor(trace_pathtest.pftrace) query SELECT thread.name AS thread_name, process.name AS process_name, SUM(sched.dur) / 1e6 AS cpu_ms FROM sched LEFT JOIN thread USING (utid) LEFT JOIN process USING (upid) WHERE sched.ts BETWEEN 1000000000 AND 2000000000 GROUP BY thread_name, process_name ORDER BY cpu_ms DESC LIMIT 10 df tp.query(query).as_pandas_dataframe()拿到 DataFrame 后你可以把它转成 JSON 摘要加上掉帧数、主线程 Waiting 次数、最大连续 Runnable 时长等指标拼成一个统一结构{ app_package: com.foo.player, window: [1000000000, 2000000000], jank_frame_count: 12, top_cpu_threads: [ {thread: RenderThread, cpu_ms: 4200} ], main_thread: { running_ms: 2300, runnable_ms: 680, waiting_ms: 4210 } }这样一份样本直接送到大模型它不需要理解 trace只需要基于这些指标做归因推理。这也是后面 AI 诊断引擎能稳定的根本原因——输入数据足够高质量。4. AI 诊断引擎的落地形态4.1 用摘要上下文让大模型做归因让大模型在完整 trace 上做归因既不现实也不稳定。我采用的是规则摘要 大模型归因的混合策略。首先规则引擎已经给出候选问题区间。例如掉帧集中在 1.5 秒到 3.2 秒那我只提取这个窗口的样本。然后把样本 JSON 和一段解释性前缀拼成 prompt你是 Android 性能诊断工程师。以下是某应用在 1.5s-3.2s 内的 trace 摘要。请基于指标分析卡顿原因输出1) 直接原因 2) 涉及线程 3) 复现建议。大模型会对这些结构化的指标生成自然语言结论。需要注意约束 prompt 让它只基于给出的指标分析不要编造不存在的数据。再加上 system prompt 中明确如果指标不足请说明缺少哪些信息能显著降低幻觉风险。4.2 结合知识库做固定模式的专项分析大模型的优点是泛化能力强缺点是不稳定同一个输入可能给出不同结论。所以固定模式的问题不要交给它自由发挥而是用知识库配合规则模板。比如主线程长时间 Uninterruptible Sleep是一个固定的模式知识库里定义了排查步骤先查对应时间戳是否有f2fs_read、block_rq_issue等事件再判断是磁盘 I/O 还是 storage 性能问题。遇到这个模式时我们直接给大模型一个固定模板让它把查到的磁盘事件填进去得出结论。我把知识库和 Prompt 的关系比喻成病历模板和主治医生。模板负责指出所有该检查的项目医生负责做综合判断。模板保证了不遗漏LLM 负责把看起来不相关的指标串成因果链。4.3 报告生成与结果回传诊断结果不能只输出一句性能存在瓶颈要可操作。我的报告模板包含四部分异常摘要、关键时间线、证据链、建议措施。异常摘要用两到三句话说清楚出了什么问题、影响多大关键时间线索引用具体时间戳和线程名证据链把命中的特征指标列出来建议措施按优先级给出可执行项。如果接入 CI/CD报告还可以自动回传到缺陷管理系统。这里有个细节AI 诊断的置信度字段要保留。置信度低于 70% 的结果不要自动创建工单只标记为待人工确认。不然一个误报就能让开发对整套系统失去信任。4.4 诊断质量校验机制AI 加入后我们必须解决怎么知道它说得对不对的问题。我建议做一个 ground truth 标注库。每次人工确认之后把 trace 特征、AI 结论、人工结论、最终修复方案存下来。定期用这批数据回测 AI 模型或 Prompt 模板看准确率和召回率。可以建立一个最简单的混淆矩阵AI 判定人工确认是人工确认否引擎判定是TPFP引擎判定否FNTN当 FP 偏高时说明规则或 Prompt 过于激进需要收紧条件当 FN 偏高时说明知识库覆盖的场景不够需要补充模式模板。这套校验机制比任何花哨的模型评估都实用。5. 这套方案在真实项目中的收益与坑5.1 收益把一次性能问题定位从小时级缩短到分钟级在开发团队日常 trace 分析最多的场景中这套自动化诊断方案最直观的价值体现是冷启动期间首帧耗时增长 100ms这类回归问题。过去流程是开发提交包 - 测试跑场景 - 发现问题 - 抓 trace - 发给负责同学 - 人工分析 trace定位半天。现在流程变成自动化测试环境跑完场景后立即抓 trace 并启动诊断流水线告警工单里直接带 AI 生成的初步归因负责人只需验证是否属实。某一次实际项目里一个视频播放器在滑动列表时掉帧率持续走高。AI 诊断结论是GPU 等待 Buffer 的时间变长主线程和 RenderThread 存在约 30ms 的跨线程串行等待。人工复核后发现是页面在滑动中触发布局缓存失效导致LinearLayout重新测量。如果没有这套自动化系统这类问题在 trace 里其实并不难找难的是每次都要花 30 分钟以上去定位而且容易漏。5.2 踩坑记录trace 文件过大、SQL 扫全表、事件丢失第一个坑是 trace 文件过大。开满数据源抓五分钟一个 pftrace 可能到 1GB 以上。TraceProcessor 加载这种文件会吃好几个 GB 内存CI 机器直接 OOM。解决方案是抓取时间控制在 15 秒到 30 秒并且只保留需要的数据源。另外可以在采集端用--txt输出部分事件减少非关键事件体积。第二个坑是 SQL 扫全表。TraceProcessor 的 SQLite 有时会因为 WHERE 条件写得不精确而扫整个 sched 表查询慢到几十秒。你需要确保sched.ts上建有索引或者用聚合时给ts范围加 index hint。更实用的是先把 trace 导入到 DuckDB 等列式数据库中做二次分析TraceProcessor 只负责初始解析。反正别直接在生产管道上对 1GB trace 跑全表 Java Heap 查询。第三个坑是事件丢失。使用环形缓冲区时如果抓取时间过长而缓冲区太小旧事件会被覆盖。结果就是你可能看到掉帧区间的进程信息不完整怎么查都查不到罪魁祸首。采集前先按经验估算事件速率每线程每秒大约产生几千次调度事件活跃场景下的 trace 速率可能在 10~30 MB/s。按这个估算size_kb值宁多勿少。第四个坑是所有设备厂商都有自己的增强行为。高通平台有qcom内核事件联发科有mtk事件同类问题在不同芯片上的调度 trace 事件名不一样。所以在特征提取层最好做一层归一化把不同平台的事件统一映射到语义层。比如主线程被切换到 non-root 线程这种结论要跨平台一致否则 AI 的规则模板会失效。5.3 一些取舍建议如果你团队刚起步不要一开始就上大模型。先用 TraceProcessor SQL 加一些简单统计规则比如掉帧数、主线程 Running 时间占比、CPU 频率下探次数配合看板展示。等这些跑顺了积累了足够多的真实问题样本再接入大模型做自动化归因。否则没有 ground truth 数据参照你很难判断大模型输出到底准不准。另外要关注成本。Perfetto 分析本身几乎免费但大模型 API 调用是按 Token 收费的。我在流程里做了分级反馈只有规则引擎命中的问题才进入大模型诊断没问题的 trace 直接归档这样每日诊断成本能控制在一个比较低的水平。从个人经验看这套方案最大的价值其实不是替代人而是逼着团队把如何判断卡顿这件事沉淀成可执行的规则。当你把 trace 分析从老师傅拍脑袋变成SQL 规则 AI 推理的流水线性能诊断才真正从手艺活变成工程能力。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进