Winscope采集链
Winscope 的核心问题不是“在哪个页面点击采集”,而是一次用户选择如何变成设备上的采集会话,最后变成浏览器可以加载的文件。Android 17 的 platform/development project 中,tools/winscope 将这条链拆成请求解析、会话编排、设备命令、文件搬运和前端加载几个 owner;Perfetto 可用时优先走统一 data source,条件不满足时才为部分目标创建 legacy session。
本文所说的 Winscope 控制器代码属于 AOSP 的 platform/development project;它与 frameworks/base、system/core 是不同的源码项目。本文只追踪这个 project 中的控制器和 ADB 适配层,不把设备侧的 traced、WindowManager 或 SurfaceFlinger producer 当作同一份源码来解释。
本文面向已经读过 Perfetto追踪、ADB架构 和 ADB文件传输 的读者。需要理解 TypeScript 类、Promise、ADB shell 和 Perfetto data source;不展开 external/perfetto 服务端、浏览器 Angular 视图渲染、WMS/SF proto 字段含义。读完后,读者应能从 TraceCollectionController.startTrace() 找到一个请求的实际 target,解释它何时走 Perfetto、何时退回 legacy,以及 trace 为什么会被移动到备份目录后才拉回主机。
1. 采集边界
Winscope 内部有四类对象,不能混为“采集器”:
| 对象 | 主要源码 | 持有内容 | 下游消费者 |
|---|---|---|---|
UserRequest | tools/winscope/src/trace_collection/user_request.ts | UI 选择的 target 与配置 | UserRequestParser |
TraceTarget | tools/winscope/src/trace_collection/trace_target.ts | setup/start/stop 命令和文件匹配规则 | TracingSession |
TracingSession | tools/winscope/src/trace_collection/controller/tracing_session.ts | 一个 target 的运行状态 | AdbDeviceConnection |
TraceCollectionController | tools/winscope/src/trace_collection/controller/trace_collection_controller.ts | 活跃会话列表和阶段顺序 | UI 进度、下载文件 |
external/perfetto 不在本 checkout 的证据范围内。因此本文可以证明 Winscope 如何构造 Perfetto 配置、调用设备命令并收集文件,但不能据此声称 traced 如何调度共享内存,也不能把 Winscope 的 parser 实现冒充 Trace Processor 实现。
2. 请求解析
2.1 目标映射
UserRequest 只有 target 和配置树;它不携带 shell 命令。UserRequestParser 先用 targetPerfettoDsMap 将目标映射到 data source,例如 SURFACE_FLINGER_TRACE 对应 android.surfaceflinger.layers,WINDOW_MANAGER_TRACE 对应 android.windowmanager,INPUT 对应 android.input.inputevent。
源码文件:development/tools/winscope/src/trace_collection/controller/user_request_parser.ts
// development/tools/winscope/src/trace_collection/controller/user_request_parser.ts
private readonly targetPerfettoDsMap = new Map([
[UiTraceTarget.SURFACE_FLINGER_TRACE, 'android.surfaceflinger.layers'],
[UiTraceTarget.WINDOW_MANAGER_TRACE, 'android.windowmanager'],
[UiTraceTarget.TRANSACTIONS, 'android.surfaceflinger.transactions'],
[UiTraceTarget.INPUT, 'android.input.inputevent'],
[UiTraceTarget.WINDOW_MANAGER_DUMP, 'android.windowmanager'],
[UiTraceTarget.SURFACE_FLINGER_DUMP, 'android.surfaceflinger.layers'],
]);这张表只说明“请求可能使用哪个 data source”,不说明设备一定支持它。支持性由 PerfettoSessionModerator.isDataSourceAvailable() 查询设备上的 perfetto --query 决定。
2.2 Fallback条件
parse() 对每个请求独立作出选择:data source 存在且当前 Perfetto 会话数未达到 5,才把配置累积到同一个 Perfetto session;否则调用 getNonPerfettoTargets() 创建 legacy target。需要注意,多个可用 Perfetto 请求会合并为一个 session,而多个 legacy 请求仍可能产生多个 TracingSession。
源码文件:development/tools/winscope/src/trace_collection/controller/user_request_parser.ts
相关函数/类型:UserRequestParser.parse
// user_request_parser.ts
const dataSourceAvailable =
ds !== undefined && (await perfettoModerator.isDataSourceAvailable(ds));
const isPerfetto =
!(await perfettoModerator.isTooManySessions()) && dataSourceAvailable;
if (isPerfetto) {
const configFileDs = this.getPerfettoDataSourceConfig(req);
if (configFileDs) perfettoConfigDataSources.push(configFileDs);
} else {
const targets = this.getNonPerfettoTargets(req);
if (targets) traceTargets.push(...targets);
}所以“请求回退到 legacy”可能由两个不同原因触发:设备没有对应 data source,或者设备上的 Perfetto 并发会话达到 5 个。源码只对第二种情况发出“将尝试 legacy traces”的通知;文章不能把所有回退都解释成权限错误。
3. 会话编排
3.1 启动顺序
TraceCollectionController.startTrace() 负责阶段顺序,而不负责具体 target 的命令细节:创建 moderator,解析请求,清理旧配置和备份目录,逐个启动 session,把成功启动的 session 放入 activeTracingSessions,最后额外等待 1 秒以覆盖 screen recording 的启动延迟。
源码文件:development/tools/winscope/src/trace_collection/controller/trace_collection_controller.ts
相关函数/类型:startTrace
// trace_collection_controller.ts
async startTrace(device: AdbDeviceConnection, requestedTraces: UserRequest[]) {
const perfettoModerator = new PerfettoSessionModerator(device, false);
const sessions = await this.getSessions(perfettoModerator, requestedTraces);
this.activeTracingSessions = [];
if (sessions.length === 0) return;
await this.prepareDevice(device, perfettoModerator);
for (const session of sessions) {
await session.start(device);
this.activeTracingSessions.push(session);
}
await new Timer(1000).sleepMs();
}这里有两个重要的生效时机:备份目录在启动前被清空并重新创建;只有 session.start() 成功返回后,session 才加入停止列表。若第二个 session 启动失败,后续 endTrace() 只会看到已经加入列表的会话,调用方需要结合错误状态判断设备上是否留下了部分采集。
3.2 Target状态
TracingSession 用一个布尔值记录 target 是否由本对象启动。start() 先执行 setup,再调用设备的 startTrace(),成功后才将 isTracing 设为 true;stop() 在未启动时直接返回,已启动时调用 endTrace() 后清除状态。
源码文件:development/tools/winscope/src/trace_collection/controller/tracing_session.ts
相关函数/类型:TracingSession
// tracing_session.ts
async start(device: AdbDeviceConnection) {
await this.setup(device);
await device.startTrace(this.target);
this.isTracing = true;
}
async stop(device: AdbDeviceConnection) {
if (!this.isTracing) return;
await device.endTrace(this.target);
this.isTracing = false;
}onDestroy() 当前调用 this.stop(device) 但没有 await。这意味着页面销毁路径发起的是异步停止请求,不能把函数返回当成设备已经停止的证明;这是源码明确暴露的生命周期边界。
4. Perfetto会话
4.1 配置生成
PerfettoSessionModerator.makePerfettoTraceTarget() 把 data source 片段包装成设备端配置文件。除了用户选择的 data source,还会强制加入 linux.process_stats 和 linux.ftrace,配置一个 500000 KB 的 ring buffer,开启写文件,并使用固定的 unique_session_name 便于后续 attach/stop。
源码文件:development/tools/winscope/src/trace_collection/controller/perfetto_session_moderator.ts
相关函数/类型:makePerfettoTraceTarget
// perfetto_session_moderator.ts
data_sources {
config {
name: "linux.process_stats"
target_buffer: 0
process_stats_config {
scan_all_processes_on_start: true
}
}
}
data_sources: {
config {
name: "linux.ftrace"
ftrace_config {
ftrace_events: "ftrace/print"
ftrace_events: "task/task_newtask"
atrace_categories: "ss"
atrace_categories: "wm"
}
}
}
buffers: {
size_kb: 500000
fill_policy: RING_BUFFER
}
write_into_file: true
unique_session_name: "winscope proxy perfetto tracing"duration_ms: 0 表示由控制器显式停止,而不是到固定时长自动结束。endTrace() 最终执行 perfetto --attach=WINSCOPE-PROXY-TRACING-SESSION --stop,所以名称和启动命令必须成对出现。
4.2 并发保护
启动前 tryStopCurrentPerfettoSession() 会查询现有 session;如果发现同名的 Winscope session,先执行 attach stop。isTooManySessions() 将并发数 >= 5 视为过多并触发 legacy fallback。这个阈值是 Winscope 控制器的策略,不是 Perfetto 服务端的通用上限。
5. Legacy路径
5.1 WMS目标
当 android.windowmanager 不可用时,getWmTraceLegacyTarget() 仍然构造一个 target。setup 阶段设置 tracing type、level 和 buffer size;start 阶段执行 cmd window tracing start;stop 阶段执行 cmd window tracing stop;文件匹配器在 /data/misc/wmtrace/ 中寻找 wm_trace.winscope 或旧的 .pb 文件。
源码文件:development/tools/winscope/src/trace_collection/controller/user_request_parser.ts
相关函数/类型:getWmTraceLegacyTarget
// user_request_parser.ts
const setupCmds = [
`su root cmd window tracing ${selectedConfigs['tracingtype']}`,
`su root cmd window tracing level ${selectedConfigs['tracinglevel']}`,
`su root cmd window tracing size ${selectedConfigs['wmbuffersize']}`,
];
return new TraceTarget(
'WmLegacyTrace',
setupCmds,
'su root cmd window tracing start',
'su root cmd window tracing stop',
[new AdbFileIdentifier('/data/misc/wmtrace/', ['wm_trace.winscope', 'wm_trace.pb'], 'wm_trace')],
);legacy target 并不等于“旧 Android 行为”:它是 Android 17 Winscope 在 data source 不可用时仍保留的兼容采集路径。其文件内容和 WMS 服务内部写入逻辑属于另一个源码边界。
5.2 SF目标
SurfaceFlinger legacy target 同样保留 setup/start/stop/file 四元组,但 setup 使用 service call SurfaceFlinger 设置 buffer 与 trace flags。这里的整数码和 flag 位图来自 Winscope 当前代码,不应从旧文章或记忆中推导;如果版本变化,应以对应 tag 的 user_request_parser.ts 为准。
6. 文件搬运
6.1 设备暂存
prepareDevice() 在每次采集前删除 /data/local/tmp/last_winscope_tracing_session/,随后重新创建目录。采集结束后,TracingSession.moveFiles() 按 target 的 fileIdentifiers 搜索文件,并执行“存在则复制、复制成功后删除原文件”的命令。
源码文件:development/tools/winscope/src/trace_collection/controller/tracing_session.ts
相关函数/类型:moveFiles
// tracing_session.ts
const isRoot = await device.checkRoot();
const maybeRootParam = isRoot ? 'su root ' : '';
const output = await device.runShellCommand(
maybeRootParam +
`[ -f ${filepath} ] && ` +
maybeRootParam +
`cp -p ${filepath} ${WINSCOPE_BACKUP_DIR}${file.destName} && ` +
maybeRootParam +
`rm -f ${filepath}`,
);这个顺序解释了一个常见现象:原始 /data/misc/... 文件消失并不表示 trace 丢失,可能只是已经被搬到备份目录。反过来,复制命令失败时 catch 只记录 warning,控制器仍会继续处理其他 session;因此“采集停止成功”不能推出“每个文件都已搬运成功”。
6.2 主机拉取
fetchLastSessionData() 搜索备份目录,逐个调用 device.pullFile(),再用 removeDirFromFileName() 生成浏览器端 File 对象。Winscope proxy 连接中,pullFile() 通过 HTTP FETCH endpoint 取得 JSON,将 base64 内容解码成 Uint8Array;失败时返回空数组并通知 UI。
源码文件:development/tools/winscope/src/trace_collection/winscope_proxy/winscope_proxy_device_connection.ts
相关函数/类型:pullFile
// winscope_proxy_device_connection.ts
override async pullFile(filepath: string): Promise<Uint8Array> {
return await new Promise<Uint8Array>((resolve) => {
getFromProxy(
`${Endpoint.FETCH}${this.encodedId}/${filepath}`,
this.securityHeader,
(response) => resolve(this.onSuccessFetchFile(response, filepath)),
async (newState, errorText) => {
this.setState(newState, errorText);
resolve(Uint8Array.from([]));
},
'arraybuffer',
);
});
}7. 取消与失败
7.1 设备断开
AdbHostConnection.onDestroy() 先销毁 host,再调用每个设备的 onDestroy()。Winscope proxy device 的 onDestroy() 将 isTracing 设为 false 并清除 keep-alive 定时器;正在运行的 trace 是否已经被设备端 stop,取决于 proxy 的后续请求和 TracingSession.onDestroy() 的异步调用,不能把浏览器对象销毁理解为同步清理完成。
7.2 保活超时
代理设备启动 target 后会每秒访问 STATUS endpoint。若响应不是 True,它清除该 target 的定时器并报告 TRACE_TIMEOUT;若仍在采集且没有现存 worker,则重新建立 1 秒间隔。这条状态只证明 Winscope proxy 对目标的保活检查结果,不证明设备端文件完整。
源码文件:development/tools/winscope/src/trace_collection/winscope_proxy/winscope_proxy_device_connection.ts
相关函数/类型:startTrace
// winscope_proxy_device_connection.ts
if (request.text !== 'True') {
this.clearTraceAliveWorker(targetName);
this.logger.warn(targetName + ' timed out');
await this.listener.onConnectionStateChange(ConnectionState.TRACE_TIMEOUT);
} else if (!workerExists && this.isTracing) {
const worker = window.setInterval(
() => this.keepTraceAlive(targetName),
1000,
);
this.keepTraceAliveWorkers.push({name: targetName, worker});
}7.3 部分成功
控制器按 session 顺序启动和停止,文件搬运按 session 顺序执行;任何一个 moveFiles() 内部的复制失败只记录 warning。排查部分成功时,应分别检查:
activeTracingSessions中是否包含该 target;/data/local/tmp/last_winscope_tracing_session/是否有对应文件;pullFile()是否返回空字节数组;- parser 是否在文件已拉回后才报格式错误。
8. 采集测试
8.1 解析测试
user_request_parser_test.ts 是请求分流的第一组对应测试。应重点阅读 data source 可用、不可用、Perfetto session 过多和多个请求合并等测试;这些测试证明的是 target 选择和配置拼接,不证明设备上的真实 WMS/SF 服务已经写出文件。
8.2 会话测试
tracing_session_test.ts 覆盖 setup、start/stop 状态、dump 和文件搬运命令。它能证明 TraceTarget 的命令被按顺序交给 AdbDeviceConnection,不能证明 shell 命令在真实设备上拥有足够权限。
8.3 控制器测试
trace_collection_controller_test.ts 验证控制器的 prepare、session 生命周期、move/fetch 阶段和空 target 警告。阅读断言时要区分两种范围:mock 只证明调用顺序和参数;真实 trace 文件格式仍需用设备采集或官方 proto/parser 测试验证。
8.4 设备连接测试
winscope_proxy_device_connection_test.ts 验证 start/end endpoint、base64 文件解码、状态超时和 keep-alive worker。它证明 HTTP 适配层的状态传播,不证明 proxy 服务端如何执行 adb shell。
9. 可执行验证
不连接设备也能完成源码层验证:
git grep -n "targetPerfettoDsMap\|isTooManySessions\|makePerfettoTraceTarget" \
development/tools/winscope/src/trace_collection
git grep -n "fetchLastSessionData\|moveFiles\|TRACE_TIMEOUT" \
development/tools/winscope/src/trace_collection在具备 Android 17 设备和已启动 Winscope proxy 的环境中,可按以下顺序建立可观察证据:先只请求 WINDOW_MANAGER_TRACE,再同时请求 SURFACE_FLINGER_TRACE;比较 perfetto --query 中是否形成一个合并 session;停止后检查备份目录文件名;最后观察浏览器端 File 是否非空。这个实验能证明请求分流、文件搬运和下载链,不覆盖 WMS/SF 具体 proto 字段正确性。
10. 源码导航
从入口继续阅读时建议按以下顺序跳转:
development/tools/winscope/src/trace_collection/controller/trace_collection_controller.ts:阶段编排和 owner 列表;development/tools/winscope/src/trace_collection/controller/user_request_parser.ts:Perfetto/legacy 分流与 target 构造;development/tools/winscope/src/trace_collection/controller/perfetto_session_moderator.ts:配置、并发和 attach stop;development/tools/winscope/src/trace_collection/controller/tracing_session.ts:setup、状态和文件搬运;development/tools/winscope/src/trace_collection/winscope_proxy/winscope_proxy_device_connection.ts:HTTP endpoint、文件解码和超时;development/tools/winscope/src/trace_collection/controller/*_test.ts:把调用顺序与失败边界反向核对。
沿这条顺序阅读,读者可以把“采集按钮”还原为一组具体的状态变化和设备命令,也能知道哪些结论仍属于 Perfetto、WMS 或 SurfaceFlinger 的外部边界。
