拿到一份卡顿 Trace,大部分人的第一反应(包括早些年的我)是打开 Perfetto UI,框一段时间,看主线程、RenderThread、CPU 状态,再截几张图写结论。这个办法处理眼前问题没什么毛病,但很难回答后面的追问:换一份 Trace 是不是同样结论?换一台设备是不是同样结论?下个版本修完后,怎么确认问题没有回来?
第 11 篇讲 PerfettoSQL 与 Trace Processor。目标很朴素:把 UI 里的判断改成能复查的查询,再用 Python 批量跑多份 Trace。读者不需要先变成 SQL 专家,只需要把 Trace 当成一组带时间戳的表,再把“看图得出的判断”翻译成一段能反复执行的查询就够了。
整篇文章就沿一条路走:认识 Trace Processor,把判断写成窗口化的查询,放进命令行,再用 Python 批量跑多份 Trace,最后沉淀成稳定指标和 Trace Summary。每一步都有能直接抄走的 SQL。
从截图结论到可复查查询
第 03 篇讲过 Perfetto UI 的基本操作,第 04 篇讲过大 Trace 怎么打开,第 09、10 篇讲过 CPU 与 Binder 的一些 SQL 示例。本文不重复这些内容,只补一件事:分析结果怎么从“这次看起来像”变成“下次还能按同一把尺子再量一遍”。
直接读这篇也可以,但最好先准备四样东西:一份 .perfetto-trace,目标 App 包名,UI 里能框出的目标时间窗口,以及本机可运行的 trace_processor。后面的 SQL 都围绕这四个输入展开。
从手工判断到自动化报告,可以拆成这几步:
1 | Perfetto UI 建立判断 |
工程上常见的使用方式有三类:
- 临时排查:UI 里发现某段可疑,再用 SQL 查成时间、线程、进程、持续时间和原始证据。
- 专项分析:同一类问题要看几十份 Trace,例如滑动卡顿、点击响应、Camera 打开慢,输出同一组诊断字段。
- 回归检测:每个版本都跑同一组查询,把 before/after 结果写进报告或看板。
UI 依然很有价值。SQL 的职责是把 UI 里的判断保存下来,方便复查和批量比较。
Trace Processor 是什么
Trace Processor 是 Perfetto 的分析引擎。它负责把 .perfetto-trace、Chrome trace、simpleperf protobuf 等文件解析成统一的 SQL 表。Perfetto UI 里的很多轨道,也是基于这些表和视图查询出来的。
系列第 04 篇用过 trace_processor_shell --httpd 打开大 Trace。官方文档现在把面向用户的命令写成 trace_processor:下载下来的文件是一个 Python 包装脚本,首次运行时会按平台拉取并缓存原生 Trace Processor 可执行文件。
先把命令装好。这段命令下载 trace_processor,并确认它能进入交互式 SQL 环境。
1 | curl -LO https://get.perfetto.dev/trace_processor |
旧文档和旧脚本里经常会看到直接运行 ./trace_processor trace.perfetto-trace 的写法。这种默认交互模式目前仍然可用,但新脚本建议写成子命令,把用途表达清楚。
命令行批处理时,重点看 query。它不会打开 UI,而是加载 Trace、执行 SQL、把结果打印出来:
1 | ./trace_processor query trace.perfetto-trace \ |
大 Trace 仍然可以让 UI 使用本地 Trace Processor,减轻浏览器解析压力。当前子命令写法是:
1 | ./trace_processor server http trace.perfetto-trace |
这个命令只启动本地 HTTP RPC server。浏览器里的 Perfetto UI 还需要按输出的地址和端口连接到这个本地 Trace Processor,才会把解析工作交给本机进程。
trace_processor_shell、--httpd 这类旧入口还有兼容层;新脚本尽量写成子命令。团队里最怕同一个动作有两种写法,半年后没人知道哪个才是当前推荐路径。
把 Trace 当成几张表
PerfettoSQL 是 Trace Processor 提供的 SQL 方言。入门阶段不用背完整 schema,先从几类常用表开始:
| 类型 | 常见表 | 用途 |
|---|---|---|
| 时间片 | slice |
一段有开始时间和持续时间的事件,例如 ATrace、Track Event、部分导入或派生出来的时间片 |
| 计数器 | counter、counter_track |
随时间变化的数值,例如 CPU 频率、内存、功耗、业务计数 |
| 线程与进程 | thread、process |
线程名、进程名、tid、pid、utid、upid |
| 轨道 | track、thread_track、process_track |
slice/counter 等事件的归属关系;UI 可视轨道不一定和这些表一一对应 |
| 调度状态 | thread_state、sched |
thread_state 记录线程状态,sched 记录线程实际占用 CPU 的 Running 区间 |
| CPU 采样 | perf_sample、cpu_profile_stack_sample |
CPU profiling 的采样事实表 |
| 调用栈维表 | stack_profile_* |
callsite、frame、mapping 等栈信息,通常要和采样表一起查 |
| 完整性 | stats |
数据丢失、解析异常、buffer 问题等采集质量信息 |
这里最容易混淆的是 tid/pid 与 utid/upid。系统里的 tid/pid 会复用,同一份 Trace 里也可能出现多个同名线程。Trace Processor 用 utid/upid 表示本次 Trace 内唯一的线程和进程实体。写脚本时优先用 utid/upid,不要只靠线程名。
时间列的单位也要尽早记住:ts 和 dur 通常按纳秒理解。临时查询里用 / 1e6 转成毫秒没问题;写进脚本后,更推荐引入 time.conversion,用 time_to_ms()、time_from_ms() 把单位写在 SQL 里。counter.value 没有统一单位,必须结合 counter_track、具体模块和采集配置解释;CPU 频率、内存、power rail、业务计数不能直接按同一套 AVG 口径比较。
还有一类表要特别分清:thread_slice 这类名字来自 slices.with_context 标准库模块提供的视图,不是原始 schema 表。它提前把 slice、track、thread、process 关联好,所以才能直接查 process_name、thread_name、is_main_thread。复制这类查询时,前面的 INCLUDE PERFETTO MODULE ... 不能漏。
SQL 报错先分三类,再动手改:INCLUDE PERFETTO MODULE 失败或提示模块不存在,通常是本地 Trace Processor 版本太旧、标准库还没有这个模块,先跑 ./trace_processor --version 再决定升级;no such table / no such column 优先怀疑漏了 INCLUDE 或 schema 演进,用 PRAGMA table_info(table_name); 确认列名;查询成功但返回 0 行是第三种情况——数据源没采到或窗口选错,这不是语法问题,要回到 stats 和采集配置。三类的处理路径完全不同,不要一上来就改 SQL 本身。
调度状态也依赖采集配置。要分析 thread_state、Running、Runnable 和 wakeup,Trace 里至少要有 ftrace sched 相关事件,尤其是 sched/sched_switch、sched/sched_wakeup、sched/sched_waking。只看 Running 主要依赖 switch;要解释 wakeup 与 Runnable 的关系,还需要 wakeup/waking。缺这些事件时,只能写“证据不足”,不能写“没有调度问题”。
CPU profiling 也有类似边界。perf_sample、cpu_profile_stack_sample 是采样事实,不是执行全量记录;采样频率、采样窗口、unwind、symbolization 和 lost sample 统计都会影响结论。没有采样表,只能写“未采集 CPU profile”,不能写“没有 CPU 热点”。
一次完整分析怎么写成 SQL
用一个常见场景串起来:UI 里发现某个目标窗口内主线程有明显停顿,现在要把这次判断写成可复查的查询。这个窗口可以来自 UI 选区、Track Event 标记、输入事件,或者某个 frame/CUJ 的起止时间。
采集质量先决定结论语气
写任何结论前,先看采集质量。stats 里既有普通计数,也有错误和数据丢失。做自动化门禁时,不要把所有非零项都当成失败;先把会直接限制结论的项列出来,再按数据源决定是复抓、降级,还是只影响某一类指标。
官方统计项表里的类型会写成 kError、kDataLoss、kInfo,但 stats.severity 的查询值是小写字符串。这条查询保留原始 name/idx/severity/source/value,同时给出影响范围提示;不要在通用入口里把所有 error 或 data_loss 都写成整份 Trace fatal。
1 | SELECT |
如果结果里出现 data_loss、parser error、buffer overrun、packet loss 相关内容,后面的判断都要降低语气。ftrace loss 会直接影响 sched/thread_state/wakeup 判断,但不一定让 App Track Event 全部不可用;FrameTimeline 缺失会影响帧指标,但不一定影响 Binder 或内存查询。报告里要保留 stats 明细,再由每个指标决定复抓、剔除或降级。
需要做采集配置优化时,再放宽到 value != 0 查看完整 stats,因为 buffer 大小、写入量、ftrace 计数这类 info 项也能解释为什么丢。第 12 篇会专门讲完整的数据丢失分类;这一篇先把它放进自动化入口。
如果要做帧指标,还要额外检查 FrameTimeline 数据是否存在。长 slice 不是慢帧口径,FrameTimeline 的 actual/expected frame、jank type、overrun 才能回答帧层面的回归问题:
1 | SELECT 'actual' AS table_name, COUNT(*) AS row_count |
这条查询返回 0 行不一定是 Trace 坏了,可能是 Android 版本、采集配置或场景不支持。报告里要把它写成“帧指标不可用”,不要退回到用 RenderThread 长 slice 代替慢帧结论。
先把目标窗口固定下来
目标窗口不要停留在“UI 里框了一段”。脚本需要明确的 start_ts/end_ts,后面的长切片、调度状态、帧指标都要引用同一个窗口。最稳的来源是 App 自己打的 Trace.beginSection() 或 Track Event 标记:
1 | INCLUDE PERFETTO MODULE slices.with_context; |
如果窗口来自某个 frame、CUJ、输入事件或 UI 选区,也要先把它落成同样的字段。比如先从 FrameTimeline 里挑出目标帧,再把 ts 和 ts + dur 作为窗口。查询返回 0 行时,报告应该写 metric_status=unavailable、degrade_reason=missing_frame_timeline,而不是退回到 RenderThread 长 slice 当帧指标:
1 | INCLUDE PERFETTO MODULE time.conversion; |
后面的示例为了缩短篇幅,仍然用 target_window CTE 写死一个窗口。实际脚本里应从 metadata 或前置查询传入 window_source/window_name/start_ts/end_ts,并把这些字段写进报告。
不要只靠 main 找线程
很多示例喜欢直接写 thread.name = 'main'。这在单个 App 里看起来方便,放到系统 Trace 里就容易误伤,因为每个 App 都可能有一个 main。先把目标进程里的线程列出来,目的是确认 upid/utid:
1 | INCLUDE PERFETTO MODULE time.conversion; |
把 com.example.app 换成自己的包名前缀。多进程 App 可能有 :remote、:push、WebView sandbox、isolated process;UI 渲染问题优先选承载 Activity、FrameTimeline 或 App marker 的进程,跨进程业务路径就保留多个 upid,报告里不要合并成一个“App 总耗时”。
长切片只能列可疑点
PerfettoSQL 标准库已经提供了预先关联好的视图,能少写很多容易错的 JOIN。这段 SQL 引入 slices.with_context 和 time.conversion,查询目标窗口内超过 5ms 的线程切片。
target_window 里的时间是 Trace 时间戳,单位仍然是纳秒。实际使用时,把它替换成 UI 选区、Track Event 或输入事件算出来的起止时间。
1 | INCLUDE PERFETTO MODULE slices.with_context; |
这条查询对应 UI 里的“找主线程长任务”。它不能直接给根因,也不能直接替代慢帧指标;它只是把目标窗口里的可疑点列出来:哪个线程、哪个切片、在窗口内占了多久、原始切片有多长。
Runnable 不是 Running
一个长切片可能是代码执行了很久,也可能是切片跨过了等待时间。要继续看调度状态。thread_state.state = 'Running' 对应实际占用 CPU 的区间;R、R+ 这类状态表示线程可运行但没有在 CPU 上跑,其中 R+ 通常代表被抢占后仍处于 runnable。
做调度分析前先检查同一窗口内有没有 thread_state/sched 数据。目标线程匹配不到,或者 stats 里有 ftrace loss/drop/error 时,报告要输出 insufficient_sched_evidence 或 sched_evidence_grade=weak:
1 | WITH target_window(start_ts, end_ts) AS ( |
这段查询仍然限制在同一个目标窗口内,把主线程与 RenderThread 的 Running、Runnable、S、D 拆开统计:
1 | INCLUDE PERFETTO MODULE time.conversion; |
S 常见于正常 sleep 或 futex 等待,D 更接近不可中断等待;需要定责时再结合 thread_state 的 blocked reason、io_wait、waker 字段和 schema 版本确认。点击响应、滑动首帧、启动首帧这类场景里,出现长 Runnable 说明 CPU 竞争或调度延迟也要一起查,但它还不能单独定责:线程 running time 是线程维度,CPU busy 和 cluster 解释要另查 sched.cpu、CPU idle、CPU frequency、capacity,并限制在同一窗口内。
把查询放到命令行
同一段 SQL 可以放进 queries/slow_slices.sql,再用 Trace Processor 执行:
1 | ./trace_processor query \ |
到这一步,分析已经从截图变成了文本文件。文本文件可以评审、复用、放进 CI,也可以发给同事确认。
Python API 批量分析
一份 Trace 可以用 UI 慢慢看,多份 Trace 就该让脚本跑。脚本要完成四件事:读取 Trace,读取同名 .json 元数据文件,跑质量门禁,按同一个目标窗口输出稳定字段。重点看 TraceProcessor(trace=...)、tp.query(...) 和报告列名。
1 | pip install perfetto |
回归平台里不要依赖“首次运行临时下载”。CI 应该固定 Python perfetto 包版本,预热或缓存 native Trace Processor——TraceProcessorConfig(bin_path=...) 可以直接指定预先下发的二进制,避免 CI 机器首次运行时临时下载——并把 trace_processor --version、TraceConfig id、SQL package version 写入 metadata。否则一次工具升级就可能让 before/after 的解析口径漂移。
1 | import csv |
这个脚本仍然是起点,但它已经有长期脚本需要的三个特征:同一份窗口化 SQL、同一组阈值、同一类输出。QUALITY_DETAIL_QUERY 保留 stats 明细,CSV 用 quality_grade/degrade_reason/data_loss_sources 表示降级原因;SLOW_SLICE_QUERY_TEMPLATE 只看目标窗口内主线程和 RenderThread,不会把整份 Trace 的后台线程长任务误写成 UI 卡顿证据。
Trace 数量上来以后,手写循环可以换成官方 BatchTraceProcessor。它仍然复用同一份 PerfettoSQL,但返回结果时会按 Trace 分开,或者用 query_and_flatten() 合成一张带来源信息的表:
1 | pip install perfetto pandas |
1 | import glob |
这类批处理更适合实验室回归和大批量现场 Trace。代价是内存和并发要受控:一次加载上百份大 Trace 时,先从少量样本试跑,确认机器内存和查询耗时,再扩大规模。
从临时查询到稳定指标
临时 SQL 用来探索,稳定指标用来长期比较。把查询放进工程前,至少补齐四组约束:
- 输出契约:列名、单位、排序规则要固定。
dur > time_from_ms(5)比裸写5e6更适合长期脚本;列名写成overlap_total_ms,读报告的人才知道它已经和目标窗口做过交集。 - 身份归属:多进程场景使用
utid/upid,线程名只适合读,不适合当唯一标识。main、RenderThread、Binder:*都可能重复。 - 目标窗口:点击、启动、滑动首帧、Camera open 这类问题不能只看整份 Trace 的总量。报告里要带
window_source/window_name/window_start_ms/window_dur_ms。 - 数据质量:每份 Trace 都要带 stats 明细和降级字段。
quality_grade/degrade_reason/missing_sources/fallback_used比一个总数更适合做回归门禁。
复杂 JOIN 优先找标准库视图。thread_slice、process_slice、thread_or_process_slice 能减少低级错误。长期项目优先考虑 Trace Summary;官方已经把 v2 summary 作为更适合结构化输出的方向,旧的 v1 metrics 不适合作为新方案起点。
一个实用标准是:查询结果能不能直接放进版本对比。如果还需要人手工解释“这一列上次叫 A,这次叫 B”,它就还不是稳定指标。
还有一条容易被忽略:指标要写出失败时的解释口径。比如“5ms 以上主线程 slice 总耗时”只能回答切片里有多少长任务,它不能独立证明卡顿;“Runnable 总耗时”只能说明线程有等待 CPU 的迹象,它也不能替代 CPU 频率、负载和调度器证据。FrameTimeline 缺失时,报告应写 metric_status=unavailable、fallback_used=false、degrade_reason=missing_frame_timeline;ftrace loss 命中时,调度类指标应写 sched_evidence_grade=weak。
同一目标窗口里的 CPU frequency、CPU idle、thermal、cluster/CPU placement 更适合放在报告的“解释变量”里。它们通常不单独定责,但能解释为什么同样的业务切片在某次 Trace 里变慢。
多 Trace 才能回答回归问题
单份 Trace 适合定位一个现场,多份 Trace 才能回答“优化有没有稳定变好”。很多性能报告的问题不在 Trace 本身,而在证据组织:截图很多,结论下得重,但没有固定配置、没有样本数、没有 data loss 检查、没有 before/after 统计口径。
多 Trace 分析要先固定输入契约。最小集合是 trace_name/scenario/group/device/build/config_id;duration_ms/thermal_state/trace_processor_version/sql_package_version 用来排除环境和工具漂移。每一批 Trace 至少要带这些字段:
| 字段 | 例子 | 用途 |
|---|---|---|
trace_name |
after-run03.perfetto-trace |
追溯原始文件 |
scenario |
feed_scroll_120hz |
区分场景 |
group |
before / after |
做版本对比 |
device |
Pixel_8 |
按机型聚合 |
build |
UP1A.xxx |
定位系统版本 |
config_id |
ui_jank_v4 |
确认 TraceConfig 一致 |
duration_ms |
30000 |
判断采集窗口 |
thermal_state |
nominal |
排除温控干扰 |
trace_processor_version |
v49.0 |
排除解析器或 stdlib 变化 |
sql_package_version |
perf-v3 |
确认查询逻辑一致 |
这些字段可以来自文件名、metadata、测试平台,也可以放进 case 包里的 metadata.json。缺少这些字段时,数字很难解释。
批量脚本的最小输出可以是一张能贴进问题单的 CSV:
1 | trace_name,group,scenario,window_name,quality_grade,degrade_reason,ftrace_loss_count,frame_timeline_available,thread_name,slice_name,slice_count,overlap_total_ms,runnable_ms,conclusion_level,next_action |
质量问题不要只输出一个总数。ftrace loss 会直接影响 sched/thread_state/wakeup 结论,但不一定让 App Track Event 完全不可用;FrameTimeline 缺失会影响帧指标,但不一定影响 Binder 或内存分析。报告里至少要保留 name/source/idx 或按数据源聚合,回归门禁再按指标依赖决定复抓、剔除或降级。
回归判定也不要只比较平均值。建议同时保留中位数、p95、最差值和每次运行的原始值:
| 指标 | 方向 | 判定建议 |
|---|---|---|
| FrameTimeline overrun / jank rate | 越低越好 | 看 p95_frame_overrun_ms、worst_frame_overrun_ms、jank_count、jank_rate、jank_type 分布;valid_frame_count 用来表示样本量 |
| launch duration | 越低越好 | 单独定义起止点,不要和帧时长混在一起 |
| jank count / jank rate | 越低越好 | 同时保留总帧数、有效帧数、窗口时长和比例 |
| slow binder count | 越低越好 | 看总数,也看是否集中在同一阶段 |
| CPU Running time | 不一定 | 要和场景动作、帧窗口一起看 |
| RSS/PSS/未释放 native allocation | 通常越低越好 | 看峰值、增长斜率和是否回落 |
| battery current / power rail | 不一定 | 同设备同条件 A/B 才有意义 |
5 次 before/after 只能帮你发现明显退化,不能证明 1% 或 2% 的变化。报告里要写清样本数和环境条件;样本数不足、温控状态不同、质量门禁不干净时,结论要降级。
Trace Summary 放长期指标
直接 SELECT 很灵活,但输出 schema 会跟着查询变化。接平台、看板、CI 时,工具更需要稳定结构。Trace Summary 的设计目标就是把指标写进统一的 TraceSummary protobuf。
当前命令行入口是 summarize 子命令。如果前面下载的是 trace_processor wrapper,就沿用同一个入口。
下面这个最小 spec.textproto 用内存指标展示 Trace Summary 的结构。这里选内存只是为了让例子短一点;等长切片、帧、Binder 这类指标的 SQL 稳定后,也可以按同样方式迁进去。
1 | metric_spec { |
然后运行这个 summary spec,并请求 memory_per_process 这个 metric:
1 | ./trace_processor summarize \ |
源码树里的 trace_processor_shell summarize 也能表达同一件事。旧版 --summary --summary-spec ... --summary-metrics-v2 ... 形式仍受支持,但新脚本优先使用子命令写法,读者对照官方文档时不容易走岔。
Trace Summary 适合长期指标:内存、启动、帧、Binder、CPU、功耗这类需要跨版本长期跟踪的数据。临时排障仍然可以直接写 SQL。帧指标迁进去时,App FrameTimeline、SurfaceFlinger/display 侧指标要分开输出,不要把 RenderThread 长 slice 当成 frame duration。
一个 summary spec 至少要说明 metric id、维度和值。单个 metric_spec 可以直接声明 unit 和 polarity,例如内存值用 BYTES,方向是 LOWER_IS_BETTER;模板场景再用 value_column_specs 给多个 value column 分别声明单位和方向。消费方拿到 protobuf 后,就知道该怎么排序、标红和比较。
另一个适合平台化的能力是 metadata_query_id。如果 spec 同时定义一个返回 key / value 的 metadata query,命令行可以加 --metadata-query,Python API 也可以传 metadata_query_id。这适合把 device、build、scenario、config_id 这类字段随 summary 一起输出,后续聚合时不用再猜文件名。
项目里可以先用 Python + CSV 起步;当字段稳定、消费方固定、指标需要长期维护时,再把关键指标迁到 Trace Summary。
容易踩的坑
- 时间列如
ts/dur默认按纳秒理解。报告字段写成dur_ms/overlap_ms,脚本里用time_to_ms()和time_from_ms()防住单位误读;counter/value 类字段另看 track 和模块定义。 dur为 -1 的 slice 是 Trace 结束时仍未闭合的事件。做窗口交集和SUM(dur)前先处理它,本文示例统一用dur > 0过滤;如果目标事件正好没闭合,要单独解释,不能当普通长切片统计。main和RenderThread不唯一。先过滤process_name/upid,再过滤thread_name/utid;报告里输出upid/utid,免得踩线程名重复的坑。pid/tid会复用。脚本里优先使用upid/utid;需要回到系统日志时,再把 tid/pid 作为辅助字段。- UI 里的可视轨道不一定对应单张原始表。报告里写清数据来源,例如
thread_slice、thread_state、actual_frame_timeline_slice,防止把展示层理解成 schema。 R、R+是 Runnable,Running才是实际运行。报告里拆成running_ms/runnable_ms/sleeping_ms/uninterruptible_blocked_ms,不要合成“线程忙”。stats有数据丢失时,不要写过强结论。保留name/source/idx和degrade_reason,按指标依赖判断影响范围。- Schema 会演进。批处理脚本里遇到错误时,先用
PRAGMA table_info(table_name);确认列是否存在;报告里保留trace_processor_version/sql_package_version。 - before 和 after 的 TraceConfig 不一致时,不要比较指标。报告里输出
config_id,不一致就先复抓。 - 只保留 report、不保留原始 Trace、config、SQL 和 metadata,后续无法复跑。最低限度也要保留 trace 文件名、配置 id、SQL 版本和窗口来源。
总结
不要把截图当成结论。截图适合发现问题,SQL、metadata、质量门禁和可复跑输出,才让这个判断具备回归证据。
下一次拿到 Trace,可以按这个顺序做:先查 stats,再固定目标窗口和 upid/utid,然后把长 slice、Running/Runnable、帧指标和降级原因写成 CSV 或 Trace Summary。SQL 让判断可复查,但不能替代采集质量和场景定义;窗口错了、Trace 丢了、配置变了,查询写得再漂亮也只能得到不可信的数字。
这条“stats 体检 → 固定窗口 → SQL 证据 → 可复跑输出”的路线,我已经在自己的开源项目 SmartPerfetto 里做成了自动化:本文这类 PerfettoSQL 查询被沉淀成两百多个场景化 SQL skill 和证据规则,接到 AI Agent 运行时上,拿到 Trace 后自动跑体检、选场景策略、输出带证据等级的结论。想直接体验这套方法的完整形态,可以从它入手;当前版本的功能全景见 SmartPerfetto 六周更新复盘。
参考文档
- Trace Processor
- Trace Processor Python API
- Getting Started with PerfettoSQL
- PerfettoSQL standard library
- PerfettoSQL Prelude tables
- Trace Processor Stats
- Trace Summarization
- Batch Trace Processor
关于我 && 博客
欢迎关注 Android Performance。