EN

Android Perfetto 系列 18:响应速度实战,从输入事件到首帧反馈

Word count: 8.4kReading time: 35 min
2026/05/04
loading

这篇补一个很容易被启动速度和流畅度盖住的话题:交互首帧响应。它关心用户刚操作完之后,屏幕多久给出第一帧反馈;页面完整加载和动画后半段稳定性要用其他指标衡量。

点一个按钮,多久出现按压态;点微信会话,多久看到会话页第一帧开始动;手指滑动列表,多久看到内容跟着手指移动。这些问题和启动慢有关,也和掉帧有关,但分析口径要单独拎出来。

本文用 Perfetto 拆这段时间:从 InputReader read_time 起算,到 App 收到事件,再到第一帧 present。主指标叫 input_to_present_ms,只度量 input read 到关联帧 present;它不覆盖触控 IC、驱动上报前延迟、显示扫描和面板响应。读完之后,你应该能给一个点击或滑动场景定义起点、终点、采集配置、SQL 指标和 App 侧补充 marker。

文章把响应慢拆成 read/dispatch、handling、ACK→first frame、滑动跟手四个独立指标,每个指标都配上采集配置、UI 上先看哪几条轨道和 SQL 写法,最后归纳五条常见归因路径,以及高速相机和 Perfetto 各自的边界。

Perfetto 系列文章

  1. Android Perfetto 系列目录
  2. Android Perfetto 系列 1:Perfetto 工具简介
  3. Android Perfetto 系列 2:Perfetto Trace 抓取
  4. Android Perfetto 系列 3:熟悉 Perfetto View
  5. Android Perfetto 系列 4:使用命令行在本地打开超大 Trace
  6. Android Perfetto 系列 5:Android App 基于 Choreographer 的渲染流程
  7. Android Perfetto 系列 6:为什么是 120Hz?高刷新率的优势与挑战
  8. Android Perfetto 系列 7:MainThread 和 RenderThread 解读
  9. Android Perfetto 系列 8:深入理解 Vsync 机制与性能分析
  10. Android Perfetto 系列 9:CPU 信息解读
  11. Android Perfetto 系列 10:Binder 调度与锁竞争
  12. Android Perfetto 系列 11:PerfettoSQL、Trace Processor 与回归检测
  13. Android Perfetto 系列 12:Trace 数据流与丢失排查
  14. Android Perfetto 系列 13:Perfetto SDK、Track Event 与 App 现场 Trace
  15. Android Perfetto 系列 14:heapprofd 与内存 Profiling
  16. Android Perfetto 系列 15:Boot Trace、长时间现场 Trace 与偶发问题抓取
  17. Android Perfetto 系列 16:GPU、Power Counters 与硬件瓶颈分析
  18. Android Perfetto 系列 17:专项场景自动化、问题清单与平台体系
  19. Android Perfetto 系列 18:响应速度实战,从输入事件到首帧反馈
  20. 视频(B站) - Android Perfetto 基础和案例分享
  21. 视频(B站) - Android Perfetto 分享 - 出图类型分享:AOSP、WebView、Flutter + OEM 系统优化分享

本文只看交互后的第一帧

旧的 Systrace 响应速度系列已经讲过广义响应慢:启动、页面跳转、亮灭屏、解锁、系统繁忙、Binder 等场景。流畅度系列讲连续帧是否稳定。Perfetto 系列第 07、08 篇又把 MainThread、RenderThread、Vsync、SurfaceFlinger 的帧路径拆过一轮。

这一篇只补一段:

1
2
3
4
5
6
7
8
用户输入
-> InputReader 读到事件
-> InputDispatcher 派发给目标窗口
-> App 主线程收到 input
-> App 处理 input 并请求下一帧
-> RenderThread 提交 buffer
-> SurfaceFlinger 合成并 present
-> 用户看到第一帧反馈

本文把主指标统一叫 input_to_present_ms:从 android_input_events.read_time 到关联帧 present,SQL 字段对应 end_to_end_latency_dur。它回答的是“点了以后多久有第一帧反馈”,不是“整个页面多久加载完”,也不是“动画播放过程中有没有掉帧”。标准库给出的 present 是候选关联:is_speculative_frame=false 只表示输入处理切片与同线程 doFrame 相交,不能证明业务因果或目标 layer 已发生视觉变化。下文查询统一输出 estimated_input_to_present_ms;只有另行核对目标 layer、frame token、实际 present 与业务状态后,才能把它提升为已确认的 input_to_present_ms

如果用 event_time 或业务 marker 作为起点,报告要另列字段,不能和 input_to_present_ms 混在一起比较。

输入到候选 present 的时间链

示意图:ACK 与帧显示是不同阶段;候选 present 还需结合 layer 和业务状态确认,不能直接当用户可见首帧。

响应慢要拆成四个指标

响应慢的讨论经常混在一起。开始分析前,先把同一次交互拆成四个指标。

指标 起点 终点 适合回答的问题
Dispatch latency InputDispatcher 发出事件 App 收到事件 系统分发和 InputChannel 是否慢
Handling latency App 收到事件 App 发出 ACK App input 处理是否慢
input_to_present_ms input read (read_time) 关联帧 present 用户多久看到第一帧反馈
Completion latency 业务触发点 页面或动画稳定 整个操作多久完成

用户最敏感的是 input_to_present_ms。第一次反馈快,后续内容分批出来,用户会觉得系统有响应;第一次反馈慢,后续动画再稳定,也容易被感知成“点了没反应”。

点击和滑动的终点也不一样:

  • 点击按钮:终点可以是按压态 present、弹窗第一帧 present、页面转场第一帧 present。
  • 点击会话:终点可以是会话页进入动画第一帧 present,也可以是第一条消息内容绘制完成。
  • 手指滑动:终点应该是内容第一次发生可见位移的 present。

Perfetto 能帮你定位系统内部时间,但“第一帧是否真的有视觉变化”经常需要 App marker 或外部视觉工具校准。

还要区分三个“输入时间”:

  • event_time: 输入事件发生时间,比 read_time 更靠前,但仍不是触控固件、电信号或外部可见起点。
  • read_time: InputReader 读到事件的时间,android_input_events 的 end-to-end 字段从这里算到 present。
  • dispatch_ts / receive_ts: InputDispatcher 发出事件、App 接收事件的时间,用来拆系统分发和 App 处理。

报告里不要混着写“从点击开始”。如果你用 read_time 起算,就写 input read;如果你用业务 click handler 起算,就写 App marker。起点不同,数字不能直接比较。

在写 SQL 前,还要把本次场景的对象字典固定下来。至少包括目标包名、InputChannel、目标窗口、目标 layer、业务 marker 和预期终点:

1
2
3
4
5
6
7
8
9
10
11
scenario_id: conversation_click
process_name: com.example.app
input_channel: com.example.app/com.example.ChatActivity
target_window: ChatActivity
target_layer: TX - com.example.app/com.example.ChatActivity#0
business_markers:
click: Conversation#Click
transition_start: Conversation#TransitionStart
first_content_drawn: Conversation#FirstContentDrawn
expected_endpoint: transition_first_presented_frame
owner_confirmed: true

自动化脚本可以先按包名和窗口名生成候选对象,但报告只使用人工确认过的对象。否则一次 Trace 里有多个窗口、弹窗、SurfaceView 或 overlay 时,SQL 很容易把输入事件关联到错误的 layer。

多显示器和折叠屏要在字典层面单独处理。输入事件是按 display 派发的:外接屏、多屏车机、折叠屏内外屏上可能同时存在同名窗口和同名 channel。android_motion_events / android_key_events 里有 display_id 列,可以按 event id 确认事件落在哪个 display;round-trip 表 android_input_events 没有 display 列;原始 inputevent 数据可用时,先统一 event ID 表示,再与这两张表核对。ATrace 字符串 ID 可能是十六进制,原始 event_id 是整数,不能直接把两列当字符串等值连接。折叠屏开合还会触发 display 切换和窗口重建,layer 名和 channel 名会换代,跨形态的数据不要合进同一条基线;scenario 字典里把 posture(折叠/展开)当成独立维度记录,横向比较只在同形态内做。

交互场景也要版本化成 schema。点击场景的最小契约可以这样写:

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
schema_version: input_response_v1
scenario_id: conversation_click
interaction_type: click
start_event_policy:
preferred: ACTION_UP
fallback: Conversation#Click
endpoint_policy:
visual_endpoint: transition_first_presented_frame
business_state_marker: Conversation#FirstContentDrawn
process_name: com.example.app
input_channel: com.example.app/com.example.ChatActivity
target_layer: TX - com.example.app/com.example.ChatActivity#0
required_markers:
- Conversation#Click
- Conversation#TransitionStart
required_counters: []
association_method:
primary: android_input_events.frame_id
fallback: target_layer_present_after_marker
fallback_policy:
missing_inputevent: app_marker_plus_frametimeline
missing_frametimeline: app_marker_plus_external_video
evidence_grade_rules:
confirmed: verified_target_layer_present_and_business_state
likely: frame_associated_but_no_external_video
degraded: missing_inputevent_or_frametimeline
privacy_notes:
- do_not_log_conversation_id
- channel_and_layer_name_allowed

滑动场景要把终点从“帧 present”细化成“内容第一次可见位移的 present”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
schema_version: input_response_v1
scenario_id: feed_first_scroll
interaction_type: scroll
start_event_policy:
preferred: first_effective_ACTION_MOVE
touch_slop_px: device_scaled_touch_slop # 从 ViewConfiguration 读取,不能跨设备写死 8px
endpoint_policy:
visual_endpoint: first_scroll_offset_change_present
required_counters:
- Feed#ScrollOffsetY
sample_policy:
scroll_offset_counter: each_nonzero_consumed_scroll_delta
report_fields:
- first_effective_move_ts
- first_offset_change_ts
- offset_delta_px
- move_to_offset_change_ms
- offset_change_to_present_ms

Trace 采集配置

采集分两层:

preset build/场景 数据源 输出口径
lab_input_debug userdebug/eng、实验室短窗口 input/view/gfx ATrace、FrameTimeline、sched、App marker,另加原始 android.input.inputevent round trip + 候选 input-to-present,另行确认视觉终点
field_input_response_low_overhead user/field,平台受控采集 App marker、FrameTimeline/sched、按版本可得的 input/view ATrace、trigger metadata 有完整 ATrace 时可查 round trip;否则退到 App marker 口径

本地或实验室分析可以打开更完整的 input 数据源。这份短 Trace 配置用于复现点击/滑动响应,重点看 android.input.inputeventatrace_categories: "input"

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
buffers {
size_kb: 98304
fill_policy: RING_BUFFER
}

duration_ms: 10000
flush_period_ms: 5000

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

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

data_sources {
config {
name: "linux.process_stats"
process_stats_config {
scan_all_processes_on_start: true
proc_stats_poll_ms: 1000
}
}
}

data_sources {
config {
name: "linux.sys_stats"
sys_stats_config {
cpufreq_period_ms: 1000
}
}
}

data_sources {
config {
name: "linux.system_info"
}
}

data_sources {
config {
name: "android.input.inputevent"
android_input_event_config {
mode: TRACE_MODE_TRACE_ALL
trace_dispatcher_input_events: true
trace_dispatcher_window_dispatch: true
}
}
}

android.input.inputevent 面向 debuggable system build,也就是 userdebug/eng 这类受控环境。TRACE_MODE_TRACE_ALL 会记录系统处理的输入事件,只适合本地设备和测试。

field/user build 上不要依赖 android.input.inputevent。如果 debuggable/受控环境里要降低隐私风险,也只能用 TRACE_MODE_USE_RULES、严格匹配规则、secure/IME/spy window 审查和访问控制;规则模式不能把它变成普通线上采集能力。Input 事件可能同时发给前台窗口、SystemUI、IME 或 spy window,match_any_packages / match_all_packages 设计不当会扩大采集范围。

field preset 要把 secure window、IME、spy window、多目标分发、脱敏、留存周期和访问控制写进评审项。

普通 App 不能自己开启系统 ftrace、FrameTimeline 或 android.input.inputevent session。<profileable android:shell="true" /> 适合让 release 包开放 CPU/内存 profiler 等本地 profiling;Android 12+ App tracing 默认可用于所有 App,Android 11 及以下的 android.os.Trace 仍要注意 profileable/debuggable 边界。

但它不会让普通 user build App 获得系统 inputevent、ftrace 或 SurfaceFlinger session 的自启动权限。

android.input 同时包含两类来源:android_key_events/android_motion_events 来自原始 android.input.inputeventandroid_input_events 则从 ATrace 的 sendMessage/receiveMessagedeliverInputEventUnwantedInteractionBlocker::notifyMotion 等切片重建往返和帧关联。两者不能混为一谈。没开原始 inputevent 仍可能有 round trip;只开原始 inputevent 而缺 ATrace,也不会自动得到 round trip。实际可用性取决于系统版本、这些埋点是否存在及 input/view/gfx 等 category 是否采到。

fallback 要提前固定:

缺失信号 还能输出什么 不能输出什么
缺原始 android.input.inputevent ATrace 完整时仍可重建 round trip 与候选帧关联 原始 motion/key 详情与 display 信息
缺输入 ATrace 的 send/receive 或 ACK 切片 App marker、FrameTimeline/调度上下文 完整 round trip;原始 inputevent 表不能直接替代
缺 FrameTimeline input round trip、App marker、线程和 SurfaceFlinger 线索 associated frame、input_to_present_ms
SurfaceView/游戏/视频 App marker、layer、外部视频、厂商工具 仅凭 FrameTimeline 认定首个可见变化
data loss / clock sync 异常 残留证据里的局部观察 强结论和跨数据源精确耗时

FrameTimeline 也要写边界:它需要 Android 12 及之后版本。官方文档当前说明 SurfaceView 不受支持,所以视频、游戏、地图这类场景不能只靠 FrameTimeline 判断首帧可见变化,要结合 layer、App marker、外部视频或厂商图形工具校准。

input_to_present_ms 只能说明某个输入事件关联到某帧 present;是否为目标 UI 的首个可见变化,还要由 target layer、业务 marker、scroll counter、截图/视频或领域工具确认。frame_id 有值也不是视觉变化的充分证明。

本地深入排查时,还可以配合 Winscope 的 SurfaceFlinger layers/transactions 数据源看窗口和 layer 状态。但这类数据源可能很重,尤其 MODE_ACTIVE、buffer、HWC 等 trace flag 会明显增加数据量,只适合短时间实验室 Trace。

线上现场仍然要依靠第 13、15、17 篇里的受控 preset、trigger、业务 marker 和隐私规则。

采完 trace 后先跑数据质量检查。input 分析高度依赖时间顺序和 FrameTimeline 关联,ftrace 或中央 buffer 丢数据时,报告要降级:

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
SELECT name, idx, severity, source, value
FROM stats
WHERE value != 0
AND (
severity IN ('error', 'data_loss')
OR name GLOB 'ftrace_cpu_*overrun*'
OR name GLOB 'ftrace_cpu_*dropped*'
OR name GLOB 'traced_buf_*packet_loss'
OR name IN (
'ftrace_setup_errors',
'ftrace_cpu_has_data_loss',
'traced_buf_trace_writer_packet_loss',
'traced_buf_chunks_overwritten',
'traced_buf_chunks_discarded',
'traced_buf_patches_failed',
'traced_flushes_failed',
'traced_final_flush_failed',
'android_input_event_parse_errors',
'frame_timeline_event_parser_errors',
'frame_timeline_unpaired_end_event',
'graphics_frame_event_parser_errors',
'clock_sync_failure',
'invalid_clock_snapshots'
)
)
ORDER BY name, idx;

这些异常要按 inputeventFrameTimelineftrace/schedcentral_bufferclock_sync 分组输出;任一关键组异常,都要降低 input_to_present_ms 的证据等级。

linux.sys_stats 能补 CPU 频率采样,但频率判断仍要看 idle、cluster/cpuset、uclamp、thermal、窗口前一条 freq 状态,以及目标线程是否真的在对应 CPU/cluster 上运行。这个 preset 默认不打开 IRQ、softirq、workqueue;怀疑 hardirq、kworker 或 softirq 抢占时,要另开短窗口专项 preset。

IRQ/softirq/workqueue 只进短窗口专项:irq/irq_handler_entryirq/irq_handler_exitirq/softirq_entryirq/softirq_exitirq/softirq_raiseworkqueue/workqueue_execute_startworkqueue/workqueue_execute_end。采完必须检查 ftrace overrun/dropped,不能把这些事件放进默认 field preset。

Perfetto UI 里先看哪几条轨道

一次点击或滑动响应,建议按这个顺序看:

  1. InputReader / InputDispatcher:确认事件是否读到,是否派发给目标窗口。
  2. App 主线程:确认事件是否到达,收到后是否马上 Running。
  3. Choreographer#doFrame:确认状态变化是通过 InputInsets AnimationAnimationTraversal 还是 Commit 推进到下一帧。input receive/ACK 不一定发生在 doFrame 里,batched input 也可能通过 CALLBACK_INPUT 消费。
  4. RenderThread:确认 UI 线程提交后,渲染线程有没有卡在 syncFrameStateDrawFramedequeueBufferqueueBuffer 等阶段。
  5. FrameTimeline:确认关联帧的 expected/actual、token、layer_name 和帧关联方式。
  6. SurfaceFlinger:确认 buffer 是否及时 latch,是否因为 HWC/GPU composition、acquire/release/present fence 或其他 layer 影响最终上屏。
  7. CPU scheduling/frequency:确认主线程、RenderThread、InputDispatcher 有没有 Runnable 等待、频率不足、迁核异常。

分析时不要从最长的 slice 开始猜。先把输入事件和第一帧反馈连起来,再判断时间花在哪一段。

点击场景:从 DOWN/UP 到第一帧

点击分析要先选起点。不同产品对“点击响应”的定义不同。

场景 推荐起点 推荐终点
按钮按压态 ACTION_DOWN 或 input read 按压态第一帧 present
点击确认 ACTION_UP 或 click handler 目标 UI 第一帧 present
页面跳转 click handler / 业务 marker 转场第一帧 present
内容可用 click handler / 业务 marker 首屏内容 marker

以“点会话进入聊天页”为例,Perfetto 里按这几步看:

  1. android_input_events 或 InputDispatcher 轨道上找到对应 ACTION_UP。
  2. 看 dispatch 到 App receive 的耗时,判断系统分发是否慢。
  3. 看 App 主线程收到事件后是否被及时调度。
  4. 看 click handler 里是否有 Binder、IO、锁等待、同步布局、图片解码。
  5. startActivity Binder、Activity launch、窗口创建、focus 切换、starting window、transition 和目标 layer 可见性是否插入等待。
  6. 看下一次 Choreographer#doFrame 是否把状态推进到 AnimationTraversalCommit
  7. 看 RenderThread、FrameTimeline 和 SurfaceFlinger,确认关联帧是否真的 present。
  8. 回到 App marker 和对象字典,确认这一帧是否就是用户可见的会话页第一帧。

这里最容易误判的一点是:Perfetto 可能显示 App 画了一帧,但这一帧没有视觉变化。App actual timeline 的结束点是 App 完成并提交帧,不等于用户已经看到。

确认首帧反馈时,要回到 android_input_events.end_to_end_latency_dur 的关联帧、SurfaceFlinger actual/display frame、layer_name、token/flow 和外部证据。没有业务 marker 时,分析者只能靠 UI 截图、layer 名称或外部视频判断。

事件契约先写清,再落到代码里:

事件 类型 线程 语义 报告字段 隐私
Conversation#Click slice UI thread click handler 执行窗口 business_marker_tsclick_handler_dur 不带会话 id
Conversation#TransitionStart short slice milestone UI thread 业务认为转场开始 transition_marker_ts 不带页面参数
Conversation#FirstContentDrawn short slice milestone UI thread / render callback 首屏内容业务完成候选 content_marker_tscontent_marker_seen 不带内容文本
Feed#ScrollOffsetY counter UI thread 列表可见位移 first_offset_change_tsoffset_delta_px 只记录数值

Conversation#FirstContentDrawn 属于 business_state_marker,不是 visual_endpoint。最终报告必须写 present_tsassociated_frame_idlayer_nameassociation_method;没有 present 关联时,只能写 content_marker_seen=true,不能输出 input_to_present_ms

下面的代码用于给会话点击场景补三个业务点。读者重点看命名:事件名固定,动态 ID 不进入 trace section。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fun onConversationClick(conversationId: String) {
Trace.beginSection("Conversation#Click")
try {
openConversation(conversationId)
} finally {
Trace.endSection()
}
}

fun onConversationTransitionStart() {
Trace.beginSection("Conversation#TransitionStart")
Trace.endSection()
}

fun onConversationFirstContentDrawn() {
Trace.beginSection("Conversation#FirstContentDrawn")
Trace.endSection()
}

这三个 marker 分别对应点击处理、转场开始、首屏内容完成。后两个是 milestone 风格的短 slice,报告只取 ts,不把 dur 当业务耗时;需要严格 instant、参数或 flow 时,用 Perfetto SDK Track Event。Perfetto 负责告诉你系统时间,marker 负责告诉你业务语义;终点仍要取关联帧的 present。

android.os.Trace / ATrace 适合放稳定 slice 名和 counter。动态的会话 id、页面 id、实验分组不要拼进 section 名;需要参数、flow 或跨线程 track 时,用第 13 篇的 Perfetto SDK Track Event,或者把业务元数据写进同一个 case 包。

marker 只是业务状态,present 时间来自关联帧。报告里要把 business_marker_tsassociated_frame_idpresent_tsassociation_method 分开写。

滑动场景:从第一个有效 MOVE 到内容位移

滑动响应比点击更容易混淆。用户手指刚动时,系统会收到很多 MOVE,但 App 可能还没有越过 touch slop,也可能因为事件合并、采样和 Vsync 节奏,只有某一帧才真的产生内容位移。

推荐口径是:

1
第一个业务有效 ACTION_MOVE -> 内容第一次可见位移的 present

分析步骤:

  1. 找到 ACTION_DOWN 后的 MOVE 序列。
  2. 排除 touch slop 内的小幅 MOVE,选出业务认为开始滑动的 MOVE。
  3. 看 App 主线程是否收到这次 MOVE,是否及时 ACK。
  4. Choreographer#doFrameInput 阶段是否更新 scroll offset。
  5. 看下一帧是否提交给 RenderThread。
  6. 看 FrameTimeline present 时间。
  7. 用 App counter 或外部视频确认内容是否发生位移。

Perfetto 自己不总能知道“内容移动了多少”。自研 App 可以加一个轻量 counter。下面记录每次非零消费位移,保留第一次变化的时间;如需每帧合并,须另加帧级调度,并记录合并后的采样延迟,不能假设 onScrolled 一帧只调用一次;报告同时写 first_effective_move_tsfirst_offset_change_tsoffset_delta_pxtouch_slop_pxsample_policy

1
2
3
4
5
6
7
8
9
10
11
12
13
var gestureScrollY = 0L // 新手势开始时归零,并记录一次基线 counter。
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(rv: RecyclerView, dx: Int, dy: Int) {
if (dy == 0) return
gestureScrollY += dy.toLong()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Trace.setCounter("Feed#ScrollOffsetY", gestureScrollY)
} else {
// AndroidX 接口接收 int;长距离滚动需分段或换 SDK counter。
androidx.tracing.Trace.setCounter("Feed#ScrollOffsetY", gestureScrollY.toInt())
}
}
})

android.os.Trace.setCounter(String, long) 从 API 29 起可用;低 minSdk 项目要用 AndroidX tracing 或自己加版本保护。大范围 scroll offset 可用 SDK Track Event counter 或分桶值。这个 counter 不要放在每次布局细节里打爆,只记录用户可见状态,例如 scroll offset、播放器 position、当前 scene id。

computeVerticalScrollOffset() 是滚动条估算值,单位由 LayoutManager 决定,不宜直接当成准确位移 px。这里累加 dy 得到本次手势消费的内容位移;它仍不是屏幕已经移动的证明,item animation、嵌套滚动和数据刷新还要单独校准。先算 move_to_offset_change_ms,再核对确实承载该状态的帧及其 present,不能随便取“下一帧”当视觉终点。

从有效 MOVE 到位移反馈

示意图:先量有效 MOVE 到滚动状态变化,再核对承载该状态的帧及 present;counter 更新不是屏幕已移动的证明。

用 SQL 量化输入延迟

INCLUDE PERFETTO MODULE android.input,再检查输入 ATrace 能否组成 send → receive → finish → ACK。当前实现对这四段做内连接:任一缺失,该事件可能整行消失,尤其不能据此排除未 ACK 的 ANR。read_time 和帧关联是额外信息,可能为空;缺 FrameTimeline 时仍可能有 round trip。

本文不使用 Chrome EventLatency。Android App 侧以 android.input / android_input_events 和 FrameTimeline 为准;WebView/Chrome 场景另按 Chrome 模块分析。

先选择目标输入事件,避免同一进程里的 DOWN/MOVE/UP、IME、overlay 或多个窗口事件混在一起。

动手前先确认这份 Trace 里 action 到底长什么样,不要照搬 Android 常量名。event_action 是从 trace 里的 slice 名切出来的,Perfetto 自己的测试数据里是 HOVER_MOVESCROLL 这种不带 ACTION_ 前缀的写法,event_type 则可能是 MOTION,也可能是 0x1 这类原始值,取决于系统版本中 InputTransport 的 ATrace 命名,不能用它判断是否启用了原始 inputevent 数据源:

1
2
3
4
5
6
7
INCLUDE PERFETTO MODULE android.input;

SELECT event_type, event_action, COUNT(*) AS n
FROM android_input_events
WHERE process_name = 'com.example.app'
GROUP BY event_type, event_action
ORDER BY n DESC;

确认取值之后再精确匹配。*UP 还会匹配 POINTER_UP,不能用来选择单指点击。下面按当前切片命名选择 UP,并固定目标进程的 click marker;marker 和输入 channel 要对应接收点击的来源窗口,不一定是跳转后的目标 Activity:

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 android.input;

WITH marker AS (
SELECT s.ts AS marker_ts, th.upid
FROM slice s
JOIN thread_track tt ON s.track_id = tt.id
JOIN thread th USING (utid)
JOIN process p USING (upid)
WHERE s.name = 'Conversation#Click' AND p.name = 'com.example.app'
AND s.ts >= 120000000000 AND s.ts < 123000000000
ORDER BY s.ts
LIMIT 1
),
candidate_events AS (
SELECT
input_event_id,
event_seq,
event_action,
event_channel,
normalized_event_channel,
read_time,
ABS(read_time - marker_ts) AS marker_delta_ns
FROM android_input_events
CROSS JOIN marker
WHERE process_name = 'com.example.app'
AND android_input_events.upid = marker.upid
AND event_action = 'UP'
AND normalized_event_channel LIKE '%ChatActivity%'
AND read_time BETWEEN marker_ts - 500000000 AND marker_ts + 500000000
)
SELECT
input_event_id,
event_seq,
event_action,
event_channel,
normalized_event_channel,
read_time / 1e9 AS read_ts_s,
marker_delta_ns / 1e6 AS matched_marker_delta_ms,
COUNT(*) OVER () AS candidate_count,
'nearest Conversation#Click marker on confirmed channel' AS selection_reason
FROM candidate_events
ORDER BY marker_delta_ns
LIMIT 1;

目标事件选定后,再看 input round trip。重点看 dispatch、handling、ack 和 input-to-present 四列:

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
INCLUDE PERFETTO MODULE android.input;

WITH target_window AS (
SELECT 120e9 AS start_ts, 123e9 AS end_ts
)
SELECT
input_event_id,
process_name,
thread_name,
event_channel,
normalized_event_channel,
event_type,
event_action,
read_time / 1e9 as read_ts_s,
dispatch_ts / 1e9 as dispatch_ts_s,
receive_ts / 1e9 as receive_ts_s,
(dispatch_ts - read_time) / 1e6 as read_to_dispatch_ms,
dispatch_latency_dur / 1e6 as dispatch_ms,
handling_latency_dur / 1e6 as handling_ms,
ack_latency_dur / 1e6 as ack_ms,
total_latency_dur / 1e6 as total_ms,
end_to_end_latency_dur / 1e6 as estimated_input_to_present_ms,
(read_time + end_to_end_latency_dur) / 1e9 as candidate_present_ts_s,
frame_id,
is_speculative_frame
FROM android_input_events
CROSS JOIN target_window
WHERE process_name = 'com.example.app'
AND normalized_event_channel LIKE '%ChatActivity%'
AND COALESCE(read_time, dispatch_ts) >= start_ts
AND COALESCE(read_time, dispatch_ts) < end_ts
ORDER BY read_time;

字段解释如下:

  • dispatch_latency_dur: InputDispatcher 发出事件到 App 收到事件。
  • read_to_dispatch_ms: InputReader 读到事件到 InputDispatcher 发出事件,能暴露分发前排队、policy/interception 或 Dispatcher 前置等待。
  • handling_latency_dur: App 输入通道收到事件到 finish/ACK 的框架口径。它不是 click handler 专属耗时;要定位业务处理,必须再用 ViewRootImpl/InputStage slice、App marker 或 click handler section 切分。
  • ack_latency_dur: App 发出 ACK 到系统收到 ACK。
  • total_latency_dur: dispatch 到 ACK 完成。
  • end_to_end_latency_dur: input read 到标准库候选 present 的差;关联失败时为 NULL。它需要额外校验目标 layer、帧 token、帧顺序和非负耗时。
  • is_speculative_frame: true 表示选择同线程未来 doFrame,false 表示输入处理与 doFrame 时间相交。它不校验目标 layer 的视觉变化。当前实现随后按同进程后续非 dropped 帧寻找 present,不能把它当成严格的 input → 指定 surface token 因果链。

end_to_end_latency_durNULL 不一定代表没有视觉反馈。它可能是事件没有关联到 FrameTimeline 帧,也可能是反馈发生在 SurfaceView、游戏引擎或另一个 layer 上。遇到这种情况,要回到 App marker、SurfaceFlinger、外部视频或领域工具,不要把 NULL 写成“没有绘制”。

排查慢输入时,先查 round-trip 慢事件,不过滤 end_to_end_latency_dur

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
INCLUDE PERFETTO MODULE android.input;

WITH target_window AS (
SELECT 120e9 AS start_ts, 123e9 AS end_ts
)
SELECT
input_event_id,
process_name,
event_action,
event_channel,
read_time / 1e9 as read_ts_s,
dispatch_ts / 1e9 as dispatch_ts_s,
receive_ts / 1e9 as receive_ts_s,
(dispatch_ts - read_time) / 1e6 as read_to_dispatch_ms,
end_to_end_latency_dur / 1e6 as estimated_input_to_present_ms,
CASE
WHEN end_to_end_latency_dur IS NULL THEN 'frame_unassociated'
WHEN is_speculative_frame THEN 'speculative_frame'
ELSE 'associated'
END AS association_status,
(read_time + end_to_end_latency_dur) / 1e9 as candidate_present_ts_s,
dispatch_latency_dur / 1e6 as dispatch_ms,
handling_latency_dur / 1e6 as handling_ms,
ack_latency_dur / 1e6 as ack_ms,
frame_id,
is_speculative_frame,
read_time
FROM android_input_events
CROSS JOIN target_window
WHERE process_name = 'com.example.app'
AND normalized_event_channel LIKE '%ChatActivity%'
AND COALESCE(read_time, dispatch_ts) >= start_ts
AND COALESCE(read_time, dispatch_ts) < end_ts
ORDER BY COALESCE(end_to_end_latency_dur, total_latency_dur) DESC
LIMIT 50;

round-trip 查询按 COALESCE(read_time, dispatch_ts) 选窗,避免 read_time 缺失时把仍有 dispatch/ACK 的事件过滤掉。只有要排序候选 input-to-present 时,才额外要求 end_to_end_latency_dur >= 0;关联标志、目标 layer 和实际 present 仍须复查。

如果 read_to_dispatch_ms 高,先看 InputDispatcher 状态、窗口目标、policy/interception、焦点窗口等待、IME/spy/secure window,再看 system_server 调度;不要直接从 read_to_dispatch 高跳到 CPU/freq 结论。如果 handling_ms 很高,优先回到 App 主线程看 ViewRootImpl/InputStage、click handler、Binder、锁和 IO。

如果 handling_ms 不高但 input_to_present_ms 高,优先看 post-ACK 的 App/Framework 异步路径、Activity/Fragment transaction、数据回调、下一帧生产、RenderThread、FrameTimeline、SurfaceFlinger 和调度。

调度部分也要分 Running 和 Runnable。前置条件是 sched_switch、wakeup/waking 完整,且没有 ftrace_cpu_has_data_loss、overrun/dropped 或 ftrace_setup_errors;否则只能写“残留证据中看到 Runnable/Running”,不能写“没有调度等待”。

这条查询示例覆盖 App main、RenderThread、system_server 的 InputReader/InputDispatcher、WindowManager 相关线程和 SurfaceFlinger。实际工程应由 scenario_objects 驱动 role in ('input_reader','input_dispatcher','window_manager','app_main','render_thread','surfaceflinger'),线程名只作为候选:

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
WITH target_window AS (
SELECT 120000000000 AS start_ts, 123000000000 AS end_ts
),
target_threads AS (
SELECT th.utid, p.name AS process_name, th.name AS thread_name
FROM thread th
JOIN process p USING (upid)
WHERE (
p.name = 'com.example.app'
AND (th.is_main_thread OR th.name = 'RenderThread')
) OR (
p.name = 'system_server'
AND (
th.name IN ('InputReader', 'InputDispatcher')
OR LOWER(th.name) LIKE '%window%'
)
) OR (
p.name IN ('surfaceflinger', '/system/bin/surfaceflinger')
)
),
state_totals AS (
SELECT s.utid,
SUM(MIN(s.ts + s.dur, w.end_ts) - MAX(s.ts, w.start_ts)) AS covered_ns,
SUM(CASE WHEN s.state = 'Running'
THEN MIN(s.ts + s.dur, w.end_ts) - MAX(s.ts, w.start_ts) ELSE 0 END) AS running_ns,
SUM(CASE WHEN s.state IN ('R', 'R+')
THEN MIN(s.ts + s.dur, w.end_ts) - MAX(s.ts, w.start_ts) ELSE 0 END) AS runnable_ns
FROM thread_state s
CROSS JOIN target_window w
WHERE s.dur > 0 AND s.ts < w.end_ts AND s.ts + s.dur > w.start_ts
GROUP BY s.utid
)
SELECT
t.utid, t.process_name, t.thread_name,
CASE WHEN s.covered_ns = w.end_ts - w.start_ts THEN 'full'
WHEN s.covered_ns IS NULL THEN 'missing' ELSE 'partial' END AS sched_coverage,
CASE WHEN s.covered_ns = w.end_ts - w.start_ts THEN s.running_ns / 1e6 END AS running_ms,
CASE WHEN s.covered_ns = w.end_ts - w.start_ts THEN s.runnable_ns / 1e6 END AS runnable_ms
FROM target_threads t
CROSS JOIN target_window w
LEFT JOIN state_totals s USING (utid)
ORDER BY runnable_ms DESC, running_ms DESC;

这里用 thread_state 的 Running/Runnable 状态统一统计,缺记录或只覆盖部分窗口时耗时留空。full 也只表示区间覆盖,不能替代 stats 检查;线程只在窗口的一部分存活时,应缩小窗口后再算。

Runnable 高说明线程想运行但没拿到 CPU,优先看其他 runnable task、RT 任务、cgroup/cpuset、uclamp/capacity、thermal、迁核和频点;Running 高说明线程已经在 CPU 上消耗时间,再去看函数级热点、Binder、锁或布局绘制。唤醒方向要再看 thread_state 的唤醒信息、IRQ context 或相邻 ftrace 事件,不能只靠 CPU 忙做结论。

常见归因路径

Read/dispatch 慢

表现通常是 read_to_dispatch_msdispatch_latency_dur 高。优先检查:

  • InputReader 到 InputDispatcher 之间是否排队。
  • InputDispatcher 是否忙。
  • system_server 是否 Runnable 等待。
  • 窗口焦点、目标窗口、InputChannel 状态是否异常。
  • 是否存在 policy/interception、IME、overlay、焦点窗口等待或 WindowManager 状态切换。
  • CPU 是否被其他 runnable task、RT 任务、cgroup/cpuset/uclamp、thermal 或迁核策略影响。

这类问题偏平台侧,App 只能提供页面和操作上下文。

Handling 慢

表现通常是 handling_latency_dur 高。优先检查:

  • App 主线程收到 input 后是否长时间 Running。
  • Input 阶段是否做了重计算。
  • click handler 是否同步发 Binder、读文件、等锁、等网络回调。
  • 是否有同步布局、图片解码、复杂 Compose recomposition 或 RecyclerView 大量 bind。

这类问题大多能靠 App marker 和主线程 slice 定位。

ACK 快,但 first frame 慢

这种情况很常见:App 很快 ACK 了 input,用户仍然觉得反馈慢。优先检查:

  • App 是否没有请求下一帧,或者请求发生得太晚。
  • ACK 之后是否在等 Binder 回调、Activity/Fragment transaction、数据回调或窗口切换。
  • Animation / Traversal 是否排在后一个 Vsync。
  • RenderThread CPU 是否卡在 syncFrameStateDrawFrame
  • dequeueBuffer / queueBuffer 是否说明 BufferQueue backpressure。
  • App GPU 完成、acquire/release fence、present fence 是否拖住这一帧。
  • SurfaceFlinger/HWC/GPU composition 是否让 display frame 晚于预期。
  • 主线程或 RenderThread 是否 Runnable 等待 CPU。

这里要把 input round trip 和帧生产分开看。ACK 快只说明 App 处理输入完成,不等于第一帧已经显示。

ANR 和亚 ANR

交互响应慢多数是亚 ANR 问题:用户已经感到慢,但还没到系统弹 ANR 的程度。Input ANR 主要分两类看:

  • 事件已经发给连接,App 长时间没有 ACK,通常会体现在 handling_latency_dur 或 ACK 相关时间异常。
  • focused event 一直等不到 focused window,通常要看 WindowManager / ActivityTaskManager、窗口创建、焦点切换和 InputDispatcher 状态。

ACK 快,但 first frame 慢 一般不会触发 input ANR,因为输入事件已经处理完;它更像首帧生产或窗口可见性问题。

滑动不跟手

滑动不跟手经常表现为第一帧位移晚、后续帧又恢复稳定。优先检查:

  • 第一个有效 MOVE 到 scroll offset counter 变化的时间。
  • scroll offset 变化到 present 的时间。
  • MOVE 是否被合并,App 是否等到下一次 Vsync 才处理。
  • 主线程是否 Runnable 等待,导致第一帧错过当前 Vsync。
  • 后续 FrameTimeline 是否稳定;如果稳定,问题主要在起步 latency。

这个场景要避免只看 jank count。用户说“不跟手”,常常说的是输入到第一帧的延迟。

高速相机和 Perfetto 的边界

Perfetto 记录的是系统内部时间,用户眼睛看到的时间还包含触控采样、触控固件处理、显示扫描、面板响应和外部拍摄误差。做竞品、验收或厂商横向对比时,外部视觉测量仍然必要。

推荐组合是:

1
2
3
4
5
6
7
8
高速相机/机械手:
得到用户可见延迟,定义外部起点和终点

Perfetto:
拆内部耗时,定位 Input、App、RenderThread、SurfaceFlinger、CPU/GPU 哪一段慢

App marker:
标记业务点击、转场开始、首屏内容完成、scroll offset 变化

外部测量给结果,Perfetto 给原因,App marker 给业务语义。三者合在一起,响应速度问题才不会停留在“体感慢”的层面。

一个最小 walkthrough 应该长这样:先定位 ACTION_UPinput_event_id,看到 handling_ms = 18read_to_dispatch_ms = 3dispatch_ms = 4;再看 系统收到 ACK 后候选帧晚了 50ms,其中 App 主线程在 doFrame 前 Runnable 21ms,FrameTimeline 显示关联帧 missed 1 vsync。

这个证据串可以写成“倾向 App 主线程调度等待导致首帧晚一拍”,但如果 is_speculative_frame = true 或没有外部视频,就不能写成“确认用户可见延迟 76ms”。

一份响应速度报告可以固定成下面的格式:

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
scenario: conversation click
trace_id: camera-free-chat-click-run03
config_version: input_response_lab_v2
query_id: input_to_present_v3
collection:
mode: lab
build_type: userdebug
input_trace_mode: TRACE_MODE_TRACE_ALL
raw_trace_upload: local_only
consent: lab_device
data_quality:
stats: clean
required_signals:
android.input.inputevent: present
FrameTimeline: present
sched: present
scenario_objects:
process_name: com.example.app
input_channel: com.example.app/com.example.ChatActivity
target_layer: TX - com.example.app/com.example.ChatActivity#0
endpoint_definition: transition_first_presented_frame
measurement_window: read_time..present_ts
start_clock: read_time
end_clock: present
input_event:
input_event_id: "21847"
event_seq: 77
event_action: UP
event_time: 12.342s
read_time: 12.345s
dispatch_ts: 12.348s
receive_ts: 12.352s
event_channel: com.example.app/com.example.ChatActivity
normalized_event_channel: com.example.app/com.example.ChatActivity
selection_reason: nearest Conversation#Click marker on confirmed channel
candidate_count: 1
business_marker:
name: Conversation#TransitionStart
ts: 12.371s
associated_frame:
frame_id: 912344
association_method: doframe_interval_overlap_then_present_candidate
speculative: false
present_ts: 12.421s
internal_input_read_to_present_ms: 76
user_visible_latency_ms: null
breakdown:
read_to_dispatch: 3ms
dispatch: 4ms
handling: 18ms
ack: 1ms
frame_after_ack: 50ms
evidence:
- App main thread Runnable 21ms before doFrame
- RenderThread DrawFrame 7ms
- FrameTimeline actual present missed expected by 1 vsync
review_points:
- ts=12.345s track=android_input_events input_event_id=21847
- ts=12.389s process=com.example.app thread=main state=Runnable dur=21ms
- ts=12.421s frame_id=912344 layer=ChatActivity
evidence_grade: likely
boundary:
fallback_used: none
missing_signals: []
external_measurement:
video: not_captured
touch_actuator: none
video_fps: none
display_refresh: 120hz
sync_method: none
error_bound_ms: unknown
owner_hint: App UI + platform scheduling review
owner_reason: Runnable identifies the waiting thread, not the party responsible for CPU contention

这个格式要求报告同时写起点、终点、拆分、证据和边界。证据等级固定为:确认 confirmed、倾向 likely、排除 excluded、降级 degraded、未知 unknown,报告字段里统一使用英文等级。is_speculative_frame = true、缺 FrameTimeline、缺外部视频、data loss、fallback 路径都会降低等级。响应速度问题不能只给一个总耗时;总耗时说明用户慢在哪里感知到,拆分才说明工程上该找谁。没有外部测量时,报告只能写系统内部延迟,不能写用户可见绝对延迟。

收束

交互首帧响应的分析顺序可以固定下来:

  1. 定义起点:DOWN、UP、有效 MOVE、业务 click。
  2. 定义终点:按压态第一帧、转场第一帧、内容第一次位移、首屏内容完成。
  3. android_input_eventsread_to_dispatch、dispatch、handling、ack、input_to_present_ms
  4. 用 App marker 说明业务状态,用关联帧 present 作为屏幕终点。
  5. 用 FrameTimeline、RenderThread、SurfaceFlinger、CPU 调度解释第一帧为什么早或晚。
  6. 用高速相机校准用户可见延迟。

启动速度看的是一个长场景,流畅度看的是连续帧稳定性,交互首帧响应看的是用户动作后的第一次反馈。三者分开,Perfetto 里的证据才会清楚。

参考文档

  1. PerfettoSQL standard library - android.input
  2. TraceConfig reference - AndroidInputEventConfig
  3. Android Jank detection with FrameTimeline
  4. ATrace: Android system and app trace events
  5. Track events
  6. Trace configuration
  7. CPU Scheduling events
  8. Buffers and dataflow
  9. Capture traces with adb commands - Input
  10. Android <profileable> manifest element
  11. ANR detection in InputDispatcher
  12. ViewRootImpl input pipeline

源码核对版本:b23c67be1159e3f0b0657bd97be9e139e0d8ee2c(2026-09-19)。

关于我 && 博客

欢迎关注 Android Performance

CATALOG
  1. 1. Perfetto 系列文章
  2. 2. 本文只看交互后的第一帧
  3. 3. 响应慢要拆成四个指标
  4. 4. Trace 采集配置
  5. 5. Perfetto UI 里先看哪几条轨道
  6. 6. 点击场景:从 DOWN/UP 到第一帧
  7. 7. 滑动场景:从第一个有效 MOVE 到内容位移
  8. 8. 用 SQL 量化输入延迟
  9. 9. 常见归因路径
    1. 9.1. Read/dispatch 慢
    2. 9.2. Handling 慢
    3. 9.3. ACK 快,但 first frame 慢
    4. 9.4. ANR 和亚 ANR
    5. 9.5. 滑动不跟手
  10. 10. 高速相机和 Perfetto 的边界
  11. 11. 收束
  12. 12. 参考文档
  13. 13. 关于我 && 博客