EN

Android Perfetto 系列 18:从输入事件到首帧反馈

Word count: 7kReading time: 30 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、滑动跟手四个独立指标,分别对应 InputReader/InputDispatcher、App 主线程、RenderThread/SurfaceFlinger 和滑动场景的位移延迟;给出对应的 Trace 采集配置、Perfetto UI 上要先看哪几条轨道、点击和滑动两类场景的起点终点定义、用 SQL 量化 input_to_present_ms 的写法;最后归纳 Read/dispatch 慢、Handling 慢、ACK 快但首帧慢、ANR 与亚 ANR、滑动不跟手这五条常见归因路径,以及高速相机和 Perfetto 各自的边界。

本文只看交互后的第一帧

旧的 Systrace 响应速度系列已经讲过广义响应慢:启动、页面跳转、亮灭屏、解锁、系统繁忙、Binder 等场景。流畅度系列讲连续帧是否稳定。Perfetto 7 和 8 又把 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。它回答的是“点了以后多久有第一帧反馈”,不是“整个页面多久加载完”,也不是“动画播放过程中有没有掉帧”。只有 associated frame 可用且 is_speculative_frame = false 时,它才是强证据;否则报告应降级为 estimated_input_to_present_ms 或不输出。

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

响应慢要拆成四个指标

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

指标 起点 终点 适合回答的问题
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。

交互场景也要版本化成 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: non_speculative_frame_and_owner_confirmed_layer
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: 8
endpoint_policy:
visual_endpoint: first_scroll_offset_change_present
required_counters:
- Feed#ScrollOffsetY
sample_policy:
scroll_offset_counter: first_change_and_at_most_once_per_frame
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 debuggable system、实验室短窗口 android.input.inputevent、FrameTimeline、sched、App marker、少量系统 category input round trip + input_to_present_ms
field_input_response_low_overhead user/field,平台受控采集 App marker、FrameTimeline/sched 可得信号、少量 ATrace、trigger metadata app_marker_to_present_ms,不输出 input_to_present_ms

本地或实验室分析可以打开更完整的 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.inputevent,后面的 android_input_events round-trip SQL 基本不可用,只能用 atrace input、App 主线程、FrameTimeline、RenderThread、SurfaceFlinger、App marker 和外部视频拼证据。报告要记录用了哪个 fallback、缺哪些字段、结论等级如何降级。

fallback 要提前固定:

缺失信号 还能输出什么 不能输出什么
android.input.inputevent App marker 到 present、FrameTimeline/调度上下文 input_event_id、read/dispatch/handling/ack、input_to_present_ms
缺 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。规则是:只在 offset 值变化时记录,每帧最多一次;报告同时写 first_effective_move_tsfirst_offset_change_tsoffset_delta_pxtouch_slop_pxsample_policy

1
2
3
4
5
6
7
8
9
10
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(rv: RecyclerView, dx: Int, dy: Int) {
val offset = rv.computeVerticalScrollOffset()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Trace.setCounter("Feed#ScrollOffsetY", offset.toLong())
} else {
androidx.tracing.Trace.setCounter("Feed#ScrollOffsetY", offset)
}
}
})

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

有了 Feed#ScrollOffsetY,第一帧开始动就能从主观描述变成可查时间点:先算 move_to_offset_change_ms,再找对应或下一帧 present,算 offset_change_to_present_ms

用 SQL 量化输入延迟

Trace 里包含 android.input.inputevent 时,可以用 PerfettoSQL 标准库里的 android.input 模块。dispatch_latency_durhandling_latency_durack_latency_dur 依赖 inputevent 数据源;input_to_present_ms 还需要可关联的 FrameTimeline frame。缺 FrameTimeline 时仍可输出 input round trip,但 input_to_present_ms 要留空并降级。

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

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

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

WITH marker AS (
SELECT ts AS marker_ts
FROM slice
WHERE name = 'Conversation#Click'
ORDER BY 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 event_action = '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 input_to_present_ms,
(read_time + end_to_end_latency_dur) / 1e9 as associated_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 read_time >= start_ts
AND read_time < 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
  • is_speculative_frame: 帧关联是否为推测结果;为 true 时结论要降级。

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 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 associated_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 read_time >= start_ts
AND read_time < end_ts
ORDER BY COALESCE(end_to_end_latency_dur, total_latency_dur) DESC
LIMIT 50;

只有要排序 input_to_present_ms 时,才额外过滤 end_to_end_latency_dur IS NOT NULL,并把 is_speculative_frame 写进降级状态。

如果 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
43
44
45
46
47
48
49
50
51
52
WITH target_window AS (
SELECT 120e9 AS start_ts, 123e9 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 = 'surfaceflinger'
)
),
running AS (
SELECT
ss.utid,
SUM(MIN(ss.ts + ss.dur, end_ts) - MAX(ss.ts, start_ts)) AS running_ns
FROM sched ss
JOIN target_threads t USING (utid)
CROSS JOIN target_window
WHERE ss.ts < end_ts
AND ss.ts + ss.dur > start_ts
GROUP BY ss.utid
),
runnable AS (
SELECT
ts.utid,
SUM(MIN(ts.ts + ts.dur, end_ts) - MAX(ts.ts, start_ts)) AS runnable_ns
FROM thread_state ts
JOIN target_threads t USING (utid)
CROSS JOIN target_window
WHERE ts.state IN ('R', 'R+')
AND ts.ts < end_ts
AND ts.ts + ts.dur > start_ts
GROUP BY ts.utid
)
SELECT
t.process_name,
t.thread_name,
COALESCE(running.running_ns, 0) / 1e6 AS running_ms,
COALESCE(runnable.runnable_ns, 0) / 1e6 AS runnable_ms
FROM target_threads t
LEFT JOIN running USING (utid)
LEFT JOIN runnable USING (utid)
ORDER BY runnable_ms DESC, running_ms DESC;

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 后第一帧晚了 53ms,其中 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
action: 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: exact_vsync_id
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: 53ms
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
owner_reason: main thread Runnable before doFrame

这个格式要求报告同时写起点、终点、拆分、证据和边界。证据等级固定为:确认 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

关于我 && 博客

欢迎关注 Android Performance

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