CN

Android Perfetto Series 18: Response Latency in Practice, from Input Events t...

Word count: 8kReading time: 49 min
2026/05/04
loading

This article addresses a topic easily overshadowed by startup speed and smoothness: first-frame interaction response. It asks how long the screen takes to provide the first frame of feedback after a user action. Complete page loading and stability later in an animation require other metrics.

After tapping a button, how long until its pressed state appears? After tapping a WeChat conversation, how long until the first frame of the conversation page starts moving? After swiping a list, how long until its content follows the finger? These questions relate to slow startup and dropped frames, but need their own measurement definitions.

We use Perfetto to break down this interval: from InputReader read_time, through the App receiving the event, to an associated frame’s present. This gives a candidate internal delay, estimated_input_to_present_ms. Only after confirming the target layer and that the business state visibly changed on that frame should a report call it input_to_present_ms. Neither value covers touch IC latency, delays before driver reporting, display scanning, or panel response. By the end, you should be able to define a start, endpoint, capture configuration, SQL metrics, and supplementary App markers for a tap or scroll scenario.

The article examines response latency through four independent measurements—read/dispatch, handling, ACK→first frame, and scroll responsiveness. Each comes with capture configuration, the first tracks to inspect in the UI, and SQL. It then covers five common attribution paths and the respective boundaries of high-speed cameras and Perfetto.

Version note (reviewed October 2026): Configuration and SQL are checked against Perfetto v58.3. Record Android, device-producer, and analysis-tool versions separately. Input round trips, frame_id, and candidate present do not form a complete visual causal chain. Version-specific association limits are documented below. Schemas, marker names, thresholds, and report numbers are examples requiring scenario validation, not device measurements.

Perfetto Series Catalog

  1. Android Perfetto Series Catalog
  2. Android Perfetto Series 1: Introduction to Perfetto
  3. Android Perfetto Series 2: Capturing Perfetto Traces
  4. Android Perfetto Series 3: Familiarizing with the Perfetto View
  5. Android Perfetto Series 4: Opening Large Traces via Command Line
  6. Android Perfetto Series 5: Choreographer-based Rendering Flow
  7. Android Perfetto Series 6: Why 120Hz? Advantages and Challenges
  8. Android Perfetto Series 7: MainThread and RenderThread Deep Dive
  9. Android Perfetto Series 8: Understanding Vsync and Performance Analysis
  10. Android Perfetto Series 9: Interpreting CPU Information
  11. Android Perfetto Series 10: Binder Scheduling and Lock Contention
  12. Android Perfetto Series 11: PerfettoSQL, Trace Processor and Regression Detection
  13. Android Perfetto Series 12: Trace Dataflow and Data Loss
  14. Android Perfetto Series 13: Perfetto SDK, Track Event and App Field Traces
  15. Android Perfetto Series 14: heapprofd and Memory Profiling
  16. Android Perfetto Series 15: Boot Traces and Long-running Field Tracing
  17. Android Perfetto Series 16: GPU, Power Counters and Hardware Bottlenecks
  18. Android Perfetto Series 17: Scenario Automation and Platform Tracing
  19. Android Perfetto Series 18: Input Response Latency
  20. Video (Bilibili) - Android Perfetto Basics and Case Studies
  21. Video (Bilibili) - Android Perfetto: Trace Graph Types - AOSP, WebView, Flutter + OEM System Optimization

This Article Covers Only the First Frame After an Interaction

The earlier Systrace response-speed series covered response latency more broadly: startup, navigation, screen on/off, unlocking, a busy system, Binder, and other scenarios. The smoothness series covered the stability of consecutive frames. Parts 07 and 08 of the Perfetto series also broke down the frame path through MainThread, RenderThread, Vsync, and SurfaceFlinger.

This article adds just one segment:

1
2
3
4
5
6
7
8
User input
-> InputReader reads the event
-> InputDispatcher dispatches it to the target window
-> The App main thread receives input
-> The App handles input and requests the next frame
-> RenderThread submits a buffer
-> SurfaceFlinger composites and presents
-> Confirm the first feedback frame against the target visual state

android_input_events.end_to_end_latency_dur runs from read_time to the associated frame’s present. The standard library’s association is still a candidate: is_speculative_frame=false means only that an input-handling slice overlaps a doFrame on the same thread. It does not prove a visual change on the target layer. Queries below therefore output estimated_input_to_present_ms. Only after checking the frame token, target layer, actual present, and business state should a report call it confirmed input_to_present_ms.

If you start from event_time or a business marker, report a separate field; do not mix it with input_to_present_ms for comparisons.

v58.3 has two further implementation limits. Its present subquery filters by upid and app_ts at or after the candidate frame, then uses LIMIT 1 without ORDER BY, so it does not guarantee the earliest present. The final frame join uses event_seq without also constraining the channel. Recheck original slices for multiple channels, duplicate candidates, or sequence reuse. frame_id is the Vsync ID associated with doFrame; it is not guaranteed to be the surface/display token producing the candidate present. A single row still needs target-layer verification.

Timeline from input to candidate present

Illustration: ACK and frame display are different stages. A candidate present still needs confirmation against the layer and business state; it cannot directly stand for the first user-visible frame.

Break Response Latency into Four Metrics

Discussions of slow response often mix different concepts. Before analyzing an interaction, separate it into four metrics.

Metric Start End Question it answers
Dispatch latency InputDispatcher sends the event App receives the event Are system dispatch and InputChannel slow?
Handling latency App receives the event App sends ACK Is App input handling slow?
estimated_input_to_present_ms Input read (read_time) Associated frame present When did the candidate frame present? Confirm the target layer and business state before calling it visible feedback.
Completion latency Business trigger Page or animation becomes stable How long does the entire operation take?

Report first-feedback delay separately from completion time. Early pressed-state feedback followed by later content differs from a long interval without visible feedback; assess its effect within the scenario and with external measurements.

Taps and scrolls also have different endpoints:

  • Button tap: the endpoint may be the pressed-state present, the first popup frame’s present, or the first page-transition frame’s present.
  • Conversation tap: it may be the first frame of the conversation page’s entry animation, or completion of drawing the first message’s content.
  • Finger scroll: it should be the present at which content first visibly moves.

Perfetto helps locate internal system timing, but whether the first frame actually contains a visual change often needs calibration with App markers or external visual tools.

Also distinguish three “input times”:

  • event_time: event time extracted from an ATrace name. It normally precedes read_time in the input path, but v58.3 only converts the embedded string to an integer here; it does not convert that embedded clock. Do not subtract timestamps across clocks or treat this as an external touch onset.
  • read_time: when InputReader reads the event. The end-to-end field in android_input_events measures from here to present.
  • dispatch_ts / receive_ts: when InputDispatcher sends the event and the App receives it, used to separate system dispatch from App handling.

Do not label all of these “from the tap” in reports. If measurement starts at read_time, say input read; if it starts at a business click handler, say App marker. Numbers with different starting points are not directly comparable.

The library derives read_time from the ts of UnwantedInteractionBlocker::notifyMotion, an instrumentation point in the Android input pipeline, not touch sampling. This implementation matches motion only; a key event can have a complete round trip but no read_time/event_time. MotionEvent.getEventTime() publicly uses milliseconds and the uptime timebase. External records need units, boot identity, and clock mapping; do not treat them directly as Trace Processor nanoseconds.

Before writing SQL, establish the scenario’s object dictionary. At minimum, include the target package, InputChannel, target window, target layer, business markers, and expected endpoint:

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

Automation can first generate candidate objects from package and window names, but reports should use only manually confirmed objects. Otherwise, when a trace contains multiple windows, popups, SurfaceViews, or overlays, SQL can easily associate an input event with the wrong layer.

Also archive the trace hash, upid, receive-track ID, full raw channel, and window/layer lifetimes. Normalized channels can lose instance prefixes and are display labels, not unique keys. ChatActivity and layer names here are hypothetical. When navigation starts in another Activity, record the input-receiving window separately from the endpoint window.

Handle multiple displays and foldables explicitly in the dictionary. Input events are dispatched by display: external monitors, multi-display automotive systems, and a foldable’s inner and outer displays can contain windows and channels with identical names at the same time. android_motion_events / android_key_events have a display_id column, allowing event IDs to identify the destination display. The round-trip table, android_input_events, has no display column. When raw inputevent data is available, normalize the event ID representation before cross-checking those two tables. ATrace string IDs may be hexadecimal, whereas raw event_id values are integers; do not directly join the columns as equal strings. Folding and unfolding can also trigger display switches and window recreation, replacing layer and channel names. Do not combine different postures into one baseline. Record posture (folded/unfolded) as a separate dimension in the scenario dictionary, and compare runs only within the same posture.

Version interaction scenarios as schemas too. A minimal contract for a tap scenario can look like this:

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: candidate_association_requires_endpoint_review
degraded: missing_inputevent_or_frametimeline
privacy_notes:
- do_not_log_conversation_id
- channel_and_layer_name_allowed

For scrolling, refine the endpoint from “frame present” to “the present where content first visibly moves”:

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 # Read from ViewConfiguration; do not hardcode 8px across devices
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 Capture Configuration

Capture has two tiers:

Preset Build/scenario Data sources Reporting scope
lab_input_debug userdebug/eng, short laboratory window input/view/gfx ATrace, FrameTimeline, sched, App markers, plus raw android.input.inputevent Round trip + candidate input-to-present; visual endpoint confirmed separately
field_input_response_low_overhead user/field, platform-controlled capture App markers, FrameTimeline/sched, input/view ATrace where available by version, trigger metadata Round trip when ATrace is complete; otherwise fall back to App-marker measurement

Local or laboratory analysis can enable the more complete input data source. This short-trace configuration reproduces tap/scroll response, with particular attention to android.input.inputevent and atrace_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 targets debuggable system builds—controlled environments such as userdebug/eng. TRACE_MODE_TRACE_ALL records input events handled by the system and is suitable only for local devices and testing.

Do not depend on android.input.inputevent on field/user builds. To reduce privacy exposure even in a debuggable, controlled environment, use TRACE_MODE_USE_RULES, strict matching rules, review of secure/IME/spy windows, and access controls. Rules mode does not turn it into an ordinary production-capture capability. Input events may be delivered to the foreground window, SystemUI, IME, or spy windows simultaneously; poorly designed match_any_packages / match_all_packages rules can broaden collection scope.

Field presets must include secure windows, IME, spy windows, multi-target dispatch, redaction, retention periods, and access controls in their review criteria.

In v58.3, the first matching rule wins; conditions within a rule are ANDed. A rule with no matchers matches every event, and unmatched events are omitted. Allowlists generally use match_all_packages and denylists match_any_packages, with multi-target behavior verified. Redacted events omit coordinates or key/scan codes, so those fields cannot also support exact displacement reconstruction. This configuration does not enable trace_evdev_events and therefore does not directly capture kernel evdev input.

Ordinary apps cannot directly submit arbitrary system TraceConfigs or access the consumer socket. Release builds can expose supported shell-profiling access with <profileable android:shell="true" />. Check app ATrace eligibility against the OS, app settings, and atrace_apps rather than promising capture for every app on Android 12–17. Android 15/API 35 ProfilingManager can request system-managed profiling, including system traces; it does not expose raw inputevent or arbitrary system configuration.

It does not, however, grant an ordinary App on a user build permission to start system inputevent, ftrace, or SurfaceFlinger sessions itself.

android.input includes two different sources. android_key_events/android_motion_events come from raw android.input.inputevent. In contrast, android_input_events reconstructs round trips and frame associations from ATrace slices such as sendMessage/receiveMessage, deliverInputEvent, and UnwantedInteractionBlocker::notifyMotion. Do not conflate them. Round trips may exist without raw inputevent enabled; enabling only raw inputevent without ATrace does not automatically produce round trips. Actual availability depends on the system version, the presence of these instrumentation points, and whether categories such as input/view/gfx were captured.

Define fallbacks in advance:

Missing signal What can still be reported What cannot be reported
Raw android.input.inputevent missing Complete ATrace can still reconstruct round trips and candidate frame associations Raw motion/key details and display information
Input ATrace send/receive or ACK slices missing App markers, FrameTimeline/scheduling context Complete round trips; raw inputevent tables cannot directly replace them
FrameTimeline missing Input round trips, App markers, thread and SurfaceFlinger clues Associated frame, input_to_present_ms
SurfaceView/games/video App markers, layers, external video, vendor tools First visible change established solely through FrameTimeline
Data loss / clock sync anomalies Local observations from surviving evidence Strong conclusions and precise cross-source durations

State FrameTimeline’s limits too: it requires Android 12 or later. The official documentation currently says SurfaceView is unsupported, so video, games, and maps cannot rely on FrameTimeline alone to identify the first visible frame change. Calibrate with layers, App markers, external video, or vendor graphics tools.

estimated_input_to_present_ms only records the library’s candidate difference from input to a present. Whether this was the target UI’s first visible change still needs confirmation through the target layer, business markers, scroll counters, screenshots/video, or domain tools. A populated frame_id is not sufficient proof of visual change either.

For deeper local investigation, Winscope’s SurfaceFlinger layers/transactions sources can help inspect window and layer state. These sources can be heavy, particularly MODE_ACTIVE and buffer/HWC trace flags, which substantially increase data volume. Reserve them for short laboratory traces.

Field investigations still rely on the controlled presets, triggers, business markers, and privacy rules from Parts 13, 15, and 17.

Run data-quality checks immediately after capture. Input analysis depends heavily on timing order and FrameTimeline associations; ftrace or central-buffer data loss requires downgrading the report:

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;

Group these anomalies under inputevent, FrameTimeline, ftrace/sched, central_buffer, and clock_sync. An anomaly in any critical group must lower the evidence grade for input_to_present_ms.

An anomaly list is not a completeness gate. Check source registration and actual events, startup-error details, target-window coverage, clocks, and parser versions. Empty stats do not establish clean data. Ring-buffer overwrites may precede the target window; determine the affected scope. The 96 MiB buffer and polling periods are starting settings requiring overhead/coverage measurements. Flushes here do not periodically save a file.

linux.sys_stats adds CPU frequency samples, but frequency assessment still needs idle state, cluster/cpuset, uclamp, thermal conditions, the frequency state immediately before the window, and whether the target thread actually ran on the relevant CPU/cluster. This preset does not enable IRQ, softirq, or workqueue events by default. If you suspect hardirq, kworker, or softirq interference, use a separate short-window specialist preset.

Keep IRQ/softirq/workqueue events in that short-window preset only: irq/irq_handler_entry, irq/irq_handler_exit, irq/softirq_entry, irq/softirq_exit, irq/softirq_raise, workqueue/workqueue_execute_start, and workqueue/workqueue_execute_end. Check ftrace overrun/dropped after capture; do not put these events in the default field preset.

Which Tracks to Inspect First in Perfetto UI

For a tap or scroll response, inspect in this order:

  1. InputReader / InputDispatcher: verify that the event was read and dispatched to the target window.
  2. App main thread: verify arrival and whether the thread became Running promptly.
  3. Choreographer#doFrame: determine whether the state change advances into the next frame through Input, Insets Animation, Animation, Traversal, or Commit. Input receive/ACK need not occur inside doFrame; batched input may also be consumed through CALLBACK_INPUT.
  4. RenderThread: after the UI thread submits work, check for stalls in stages such as syncFrameState, DrawFrame, dequeueBuffer, and queueBuffer.
  5. FrameTimeline: check the associated frame’s expected/actual timelines, token, layer_name, and association method.
  6. SurfaceFlinger: check timely buffer latching and whether HWC/GPU composition, acquire/release/present fences, or other layers affect final presentation.
  7. CPU scheduling/frequency: check the main thread, RenderThread, and InputDispatcher for Runnable waits, insufficient frequency, or abnormal CPU migration.

Do not start by guessing from the longest slice. Connect the input event to the first frame of feedback, then determine where the time went.

I would rather leave the visual endpoint unconfirmed than take the next present timestamp as proof that the tap changed the screen. That shortcut produces a precise number for the wrong layer surprisingly easily.

Tap Scenarios: From DOWN/UP to the First Frame

Choose the starting point first. Products define “tap response” differently.

Scenario Recommended start Recommended end
Button pressed state ACTION_DOWN or input read First pressed-state frame present
Tap confirmation ACTION_UP or click handler Target UI’s first frame present
Page navigation Click handler / business marker First transition frame present
Content available Click handler / business marker First-screen content marker

For “tap a conversation to enter the chat page,” inspect Perfetto in these steps:

  1. Find the corresponding ACTION_UP in android_input_events or on the InputDispatcher track.
  2. Measure dispatch to App receive to determine whether system dispatch is slow.
  3. Check whether the App main thread is scheduled promptly after receiving the event.
  4. Check the click handler for Binder, IO, lock waits, synchronous layout, and image decoding.
  5. Look for waits introduced by the startActivity Binder call, Activity launch, window creation, focus changes, starting window, transition, and target-layer visibility.
  6. Check whether the next Choreographer#doFrame advances state through Animation, Traversal, or Commit.
  7. Inspect RenderThread, FrameTimeline, and SurfaceFlinger to confirm the associated frame actually presents.
  8. Return to App markers and the object dictionary to confirm that this is the conversation page’s first user-visible frame.

The easiest mistake here is that Perfetto can show an App drawing a frame that contains no visual change. The end of the App actual timeline means the App completed and submitted the frame; it does not mean the user has seen it.

To confirm first-frame feedback, return to the frame associated with android_input_events.end_to_end_latency_dur, the SurfaceFlinger actual/display frame, layer_name, token/flow, and external evidence. Without business markers, the analyst must judge from UI screenshots, layer names, or external video.

Specify the event contract before implementing it:

Event Type Thread Meaning Report fields Privacy
Conversation#Click slice UI thread Click-handler execution window business_marker_ts, click_handler_dur No conversation ID
Conversation#TransitionStart short slice milestone UI thread Business-defined transition start transition_marker_ts No page parameters
Conversation#FirstContentDrawn short slice milestone UI thread / render callback Candidate business completion of first-screen content content_marker_ts, content_marker_seen No content text
Feed#ScrollOffsetY counter UI thread Accumulated consumed list displacement first_offset_change_ts, offset_delta_px Numeric values only

Conversation#FirstContentDrawn is a business_state_marker, not a visual_endpoint. The final report must include present_ts, associated_frame_id, layer_name, and association_method. Without a present association, report only content_marker_seen=true; do not output input_to_present_ms.

The following code adds three business markers to the conversation-tap scenario. Pay attention to naming: event names are fixed, and dynamic IDs do not enter trace sections.

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()
}

These markers represent click handling, transition start, and first-screen content completion. The latter two are milestone-style short slices: reports use only their ts, not dur as business duration. Use Perfetto SDK Track Event when you need strict instants, arguments, or flows. Perfetto supplies system timing; markers supply business meaning. The endpoint remains the associated frame’s present.

Import android.os.Trace and integrate these fragments with existing app callbacks. openConversation is an app function, not a standalone runnable example. Pair begin/end on the same thread. try/finally measures the synchronous click call, not asynchronous page loading. Repeated or concurrent interactions need case-local operation IDs or flows instead of selecting the first same-name marker; IDs must not contain conversation content or user identifiers.

android.os.Trace / ATrace work well for stable slice names and counters. Do not concatenate dynamic conversation IDs, page IDs, or experiment groups into section names. For arguments, flows, or cross-thread tracks, use Perfetto SDK Track Event from Part 13, or put business metadata in the same case package.

A marker represents business state only; present time comes from the associated frame. Report business_marker_ts, associated_frame_id, present_ts, and association_method separately.

Scroll Scenarios: From the First Effective MOVE to Content Displacement

Scroll response is easier to misinterpret than tap response. When a finger first moves, the system receives many MOVE events, but the App may not yet have crossed touch slop. Event coalescing, sampling, and Vsync cadence may also mean that only a later frame produces actual content displacement.

The recommended definition is:

1
First ACTION_MOVE considered effective by business logic -> Present where content first visibly moves

Analysis steps:

  1. Find the MOVE sequence after ACTION_DOWN.
  2. Exclude small MOVE events within touch slop and select the MOVE that the business logic treats as the start of scrolling.
  3. Check whether the App main thread receives this MOVE and ACKs promptly.
  4. Check whether the Input stage of Choreographer#doFrame updates the scroll offset.
  5. Check whether the next frame is submitted to RenderThread.
  6. Check the FrameTimeline present time.
  7. Confirm content displacement with an App counter or external video.

Perfetto cannot always tell “how far the content moved.” Apps you control can add a lightweight counter. The following records each nonzero consumed displacement, preserving the first change’s timestamp. If updates must be coalesced per frame, add frame-level scheduling separately and record the resulting sampling delay; do not assume onScrolled is called only once per frame. Report first_effective_move_ts, first_offset_change_ts, offset_delta_px, touch_slop_px, and sample_policy together.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var gestureScrollY = 0L
var recordingFeedGesture = false

fun beginFeedGesture() {
gestureScrollY = 0L
recordingFeedGesture = true
android.os.Trace.setCounter("Feed#ScrollOffsetY", 0L)
}

fun endFeedGesture() {
recordingFeedGesture = false
}

val feedScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(rv: RecyclerView, dx: Int, dy: Int) {
if (!recordingFeedGesture || rv.scrollState != RecyclerView.SCROLL_STATE_DRAGGING || dy == 0) return
gestureScrollY += dy.toLong()
android.os.Trace.setCounter("Feed#ScrollOffsetY", gestureScrollY)
}
}
recyclerView.addOnScrollListener(feedScrollListener)

android.os.Trace.setCounter(String, long) starts at API 29; this example assumes Android 12+. It records one RecyclerView while its state is DRAGGING, excluding flings and non-dragging states. Programmatic scrolling during a drag still requires application-side source attribution. Reset and record a baseline at a new DOWN, then end at UP/CANCEL. Parent interception or custom gesture handling requires matching lifecycle integration. Layout changes can also call onScrolled with zero dx/dy; callback counts do not equal MOVE counts.

Integrate this recorder with existing touch dispatch: call beginFeedGesture before delivering the target DOWN to RecyclerView, and endFeedGesture after delivering UP/CANCEL, also ending on parent interruption. Run it on the UI thread, register the listener once, and remove it with removeOnScrollListener when the view is destroyed. Copying the listener alone does not connect gesture boundaries.

computeVerticalScrollOffset() is a scrollbar estimate whose units depend on the LayoutManager; it should not be treated directly as exact displacement in pixels. Here, accumulating dy gives the content displacement consumed by this gesture. It still does not prove that the screen has moved. Item animation, nested scrolling, and data refresh need separate calibration. First calculate move_to_offset_change_ms, then verify the frame that actually carries that state and its present. Do not arbitrarily use “the next frame” as the visual endpoint.

Record the slop axis/tool type, pointer ID, history/batching, and resampling policy rather than selecting the first MOVE beyond a fixed pixel distance. The counter reflects cumulative state when the callback runs. Identical counter names across list instances can mix; use distinct tracks or instrument only the target instance, preserving gesture/operation boundaries.

From effective MOVE to displacement feedback

Illustration: first measure effective MOVE to scroll-state change, then verify the frame carrying that state and its present. A counter update does not prove that the screen has moved.

Quantifying Input Latency with SQL

First INCLUDE PERFETTO MODULE android.input, then check whether input ATrace can form send → receive → finish → ACK. The current implementation inner-joins these four stages. If any stage is missing, the entire event row may disappear; in particular, this cannot rule out an ANR involving a missing ACK. read_time and frame association are additional information and may be null. Round trips can still exist without FrameTimeline.

This article does not use Chrome EventLatency. Android App analysis uses android.input / android_input_events and FrameTimeline; analyze WebView/Chrome scenarios separately with Chrome modules.

Choose the target input event first, so DOWN/MOVE/UP events, IME, overlays, or multiple windows within the same process are not mixed together.

Before proceeding, inspect how actions actually appear in this trace rather than copying Android constant names. event_action is extracted from slice names in the trace. Do not assume the extracted action has an ACTION_ prefix. event_type may be MOTION or a raw value such as 0x1, depending on the system version’s InputTransport ATrace naming. It cannot tell you whether the raw inputevent data source was enabled:

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

SELECT upid, pid, process_name, event_channel, normalized_event_channel,
receive_track_id, event_type, event_action, COUNT(*) AS candidate_rows
FROM android_input_events
WHERE process_name = 'com.example.app'
GROUP BY upid, pid, process_name, event_channel, normalized_event_channel,
receive_track_id, event_type, event_action
ORDER BY upid, event_channel, candidate_rows DESC;

Inspect values before filtering candidates. *UP also matches POINTER_UP, while v58.3 derives event_action through the frame-association branch, so a missing association can leave it NULL. Filtering only UP cannot prove absence of input. The query below uses a confirmed click slice_id, upid, full raw channel, and receive track, listing candidates before the marker within an explicitly chosen window. It does not automatically select the nearest row. Replace NULL with actual trace-local values; choose the window for the interaction.

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

WITH target(upid, marker_slice_id, event_channel, receive_track_id, start_ts, end_ts) AS (
VALUES (NULL, NULL, NULL, NULL, NULL, NULL)
),
marker AS (
SELECT s.id AS marker_slice_id, s.ts AS marker_ts, tt.utid, th.upid
FROM slice s
JOIN thread_track tt ON s.track_id = tt.id
JOIN thread th USING (utid)
CROSS JOIN target t
WHERE s.id = t.marker_slice_id AND th.upid = t.upid
AND s.name = 'Conversation#Click' AND s.dur >= 0
AND typeof(t.start_ts) = 'integer' AND typeof(t.end_ts) = 'integer'
AND t.end_ts > t.start_ts AND s.ts >= t.start_ts AND s.ts < t.end_ts
),
candidates AS (
SELECT i.*, m.marker_slice_id, m.marker_ts,
m.marker_ts - i.receive_ts AS receive_to_marker_ns,
COUNT(*) OVER (PARTITION BY i.upid, i.event_channel, i.event_seq,
i.dispatch_track_id, i.dispatch_ts, i.receive_track_id) AS rows_for_dispatch
FROM android_input_events i
JOIN marker m ON i.upid = m.upid
CROSS JOIN target t
WHERE i.event_channel = t.event_channel AND i.receive_track_id = t.receive_track_id
AND i.receive_ts >= t.start_ts AND i.receive_ts <= m.marker_ts
)
SELECT upid, input_event_id, event_seq, event_action, event_channel,
dispatch_track_id, receive_track_id, dispatch_ts, receive_ts, read_time,
marker_slice_id, marker_ts, receive_to_marker_ns, rows_for_dispatch,
COUNT(*) OVER () AS candidate_count,
CASE WHEN event_action IS NULL THEN 'action_unavailable'
WHEN event_action = 'UP' THEN 'up_candidate'
ELSE 'other_action' END AS action_status,
'requires_operation_and_channel_review' AS selection_status
FROM candidates
ORDER BY receive_to_marker_ns, dispatch_ts, event_seq;

After confirming an event, identify its round trip with the full channel, event_seq, dispatch_ts, and upid, not input_event_id or normalized channel alone. The following lists all candidates in the target window and counts rows sharing one dispatch identity, exposing association multiplication. Filter by the reviewed composite key for a single-event report; retain and downgrade duplicate or negative-duration evidence.

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

WITH target(upid, event_channel, receive_track_id, start_ts, end_ts) AS (
VALUES (NULL, NULL, NULL, NULL, NULL)
),
candidates AS (
SELECT i.*, COUNT(*) OVER (PARTITION BY i.upid, i.event_channel, i.event_seq,
i.dispatch_track_id, i.dispatch_ts, i.receive_track_id) AS rows_for_dispatch
FROM android_input_events i CROSS JOIN target t
WHERE i.upid = t.upid AND i.event_channel = t.event_channel
AND i.receive_track_id = t.receive_track_id
AND typeof(t.start_ts) = 'integer' AND typeof(t.end_ts) = 'integer'
AND t.end_ts > t.start_ts
AND COALESCE(i.read_time, i.dispatch_ts) >= t.start_ts
AND COALESCE(i.read_time, i.dispatch_ts) < t.end_ts
)
SELECT i.upid, p.machine_id, i.input_event_id, i.event_seq,
i.event_channel, i.normalized_event_channel, i.event_type, i.event_action,
i.dispatch_track_id, i.receive_track_id, tt.utid AS receive_utid,
i.thread_name AS finish_thread_name,
i.event_time AS embedded_event_time_raw, i.read_time, i.dispatch_ts, i.receive_ts,
CASE WHEN i.read_time IS NULL THEN 'dispatch_ts' ELSE 'read_time' END AS window_basis,
i.receive_ts + i.handling_latency_dur AS finish_ts,
i.dispatch_ts + i.total_latency_dur AS system_ack_ts,
(i.dispatch_ts - i.read_time) / 1e6 AS read_to_dispatch_ms,
i.dispatch_latency_dur / 1e6 AS dispatch_ms,
i.handling_latency_dur / 1e6 AS handling_ms,
i.ack_latency_dur / 1e6 AS ack_ms,
i.total_latency_dur / 1e6 AS total_ms,
i.rows_for_dispatch,
CASE WHEN i.rows_for_dispatch != 1 THEN 'duplicate_candidates'
WHEN i.dispatch_latency_dur < 0 OR i.handling_latency_dur < 0
OR i.ack_latency_dur < 0 OR i.dispatch_ts < i.read_time THEN 'invalid_order'
ELSE 'round_trip_candidate' END AS round_trip_status,
CASE WHEN i.end_to_end_latency_dur IS NULL THEN 'frame_unassociated'
WHEN i.end_to_end_latency_dur < 0 THEN 'invalid_frame_order'
WHEN i.is_speculative_frame THEN 'speculative_candidate'
ELSE 'candidate_requires_layer_review' END AS association_status,
i.end_to_end_latency_dur AS raw_candidate_read_to_present_ns,
CASE WHEN i.rows_for_dispatch = 1 AND i.end_to_end_latency_dur >= 0
AND i.dispatch_latency_dur >= 0 AND i.handling_latency_dur >= 0
AND i.ack_latency_dur >= 0 AND i.dispatch_ts >= i.read_time
THEN i.end_to_end_latency_dur / 1e6 END AS estimated_input_to_present_ms,
CASE WHEN i.rows_for_dispatch = 1 AND i.end_to_end_latency_dur >= 0
AND i.dispatch_latency_dur >= 0 AND i.handling_latency_dur >= 0
AND i.ack_latency_dur >= 0 AND i.dispatch_ts >= i.read_time
THEN i.read_time + i.end_to_end_latency_dur END AS candidate_present_ts,
i.frame_id, i.is_speculative_frame
FROM candidates i
LEFT JOIN process p ON p.upid = i.upid
LEFT JOIN thread_track tt ON tt.id = i.receive_track_id
ORDER BY COALESCE(i.read_time, i.dispatch_ts), i.event_seq;

The fields mean:

  • dispatch_latency_dur: InputDispatcher sending the event to the App receiving it.
  • read_to_dispatch_ms: InputReader reading the event to InputDispatcher sending it. This can expose pre-dispatch queuing, policy/interception, or waits before dispatch.
  • handling_latency_dur: the framework interval from the App input channel receiving the event to finish/ACK. This is not exclusively click-handler time. To locate business handling, split it further using ViewRootImpl/InputStage slices, App markers, or click-handler sections.
  • ack_latency_dur: the App sending ACK to the system receiving ACK.
  • total_latency_dur: dispatch to ACK completion.
  • end_to_end_latency_dur: the difference between input read and the standard library’s candidate present; NULL when association fails. It needs additional checks of the target layer, frame token, frame ordering, and nonnegative duration.
  • is_speculative_frame: true means a future doFrame on the same thread was selected; false means input handling overlaps doFrame in time. It does not verify visual changes on the target layer. The current implementation then looks for present through subsequent non-dropped frames in the same process. Do not treat this as a strict causal chain from input to a specific surface token.

A NULL end_to_end_latency_dur does not necessarily mean no visual feedback occurred. The event may not have been associated with a FrameTimeline frame, or feedback may have occurred on a SurfaceView, game engine, or another layer. Return to App markers, SurfaceFlinger, external video, or domain tools in these cases; do not translate NULL into “nothing was drawn.”

When investigating slow input, rank round trips by total_latency_dur without filtering 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
INCLUDE PERFETTO MODULE android.input;

WITH target(upid, event_channel, receive_track_id, start_ts, end_ts) AS (
VALUES (NULL, NULL, NULL, NULL, NULL)
),
candidates AS (
SELECT i.*, COUNT(*) OVER (PARTITION BY i.upid, i.event_channel, i.event_seq,
i.dispatch_track_id, i.dispatch_ts, i.receive_track_id) AS rows_for_dispatch
FROM android_input_events i CROSS JOIN target t
WHERE i.upid = t.upid AND i.event_channel = t.event_channel
AND i.receive_track_id = t.receive_track_id
AND typeof(t.start_ts) = 'integer' AND typeof(t.end_ts) = 'integer'
AND t.end_ts > t.start_ts
AND COALESCE(i.read_time, i.dispatch_ts) >= t.start_ts
AND COALESCE(i.read_time, i.dispatch_ts) < t.end_ts
)
SELECT upid, input_event_id, event_seq, event_action, event_channel,
dispatch_track_id, receive_track_id, read_time, dispatch_ts, receive_ts,
dispatch_latency_dur, handling_latency_dur, ack_latency_dur, total_latency_dur,
rows_for_dispatch, frame_id, is_speculative_frame,
CASE WHEN rows_for_dispatch != 1 THEN 'duplicate_candidates'
WHEN total_latency_dur < 0 OR dispatch_latency_dur < 0
OR handling_latency_dur < 0 OR ack_latency_dur < 0 THEN 'invalid_order'
ELSE 'round_trip_candidate' END AS status
FROM candidates
ORDER BY total_latency_dur DESC, dispatch_ts, event_seq
LIMIT 50;

The round-trip query selects the window with COALESCE(read_time, dispatch_ts) so events that retain dispatch/ACK are not filtered out when read_time is missing. Require end_to_end_latency_dur >= 0 additionally only when ranking candidate input-to-present durations. Association flags, the target layer, and actual present still need review.

This window selects event starts in a half-open interval, not every overlapping dispatch/handling span; expand it for boundary investigations. rows_for_dispatch counts only surviving selected rows, not globally deduplicated inputs. In v58.3, tid/thread_name actually come from the finish side; resolve the receiving thread through receive_track_id and thread_track. The slowest 50 completed round trips still omit unacknowledged events; inspect raw InputDispatcher evidence and logs separately.

If read_to_dispatch_ms is high, first inspect InputDispatcher state, window targets, policy/interception, focused-window waits, and IME/spy/secure windows, then system_server scheduling. Do not jump directly from high read-to-dispatch to a CPU/frequency conclusion. If handling_ms is high, return first to the App main thread and inspect ViewRootImpl/InputStage, the click handler, Binder, locks, and IO.

If handling_ms is low but candidate estimated_input_to_present_ms is high, prioritize post-ACK App/Framework asynchronous paths, Activity/Fragment transactions, data callbacks, next-frame production, RenderThread, FrameTimeline, SurfaceFlinger, and scheduling.

Separate Running from Runnable in scheduling analysis. The prerequisites are complete sched_switch and wakeup/waking events, with no ftrace_cpu_has_data_loss, overrun/dropped, or ftrace_setup_errors. Otherwise, say only “Runnable/Running appears in the surviving evidence,” not “there was no scheduling wait.”

Scheduling targets should come from the reviewed scenario dictionary: app main, RenderThread, InputReader/InputDispatcher, relevant WindowManager, and SurfaceFlinger threads. Fill explicit utid/role pairs below; NULL matches nothing. Add or remove confirmed rows rather than treating every SurfaceFlinger thread as relevant. Use the target input/dependency interval; accumulated Running or Runnable across processes cannot be summed into end-to-end latency.

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
WITH target_window(start_ts, end_ts) AS (
VALUES (NULL, NULL)
),
confirmed(utid, role) AS (
VALUES (NULL, 'app_main'), (NULL, 'render_thread'),
(NULL, 'input_reader'), (NULL, 'input_dispatcher'),
(NULL, 'window_manager'), (NULL, 'surfaceflinger')
),
target_threads AS (
SELECT th.utid, p.upid, p.machine_id, p.name AS process_name, th.name AS thread_name
FROM thread th JOIN process p USING (upid)
WHERE th.utid IN (SELECT utid FROM confirmed WHERE utid IS NOT NULL)
),
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 typeof(w.start_ts) = 'integer' AND typeof(w.end_ts) = 'integer'
AND w.end_ts > w.start_ts AND 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.upid, t.machine_id, t.process_name, t.thread_name,
w.start_ts, w.end_ts, s.covered_ns,
CASE WHEN typeof(w.start_ts) != 'integer' OR typeof(w.end_ts) != 'integer'
OR w.end_ts <= w.start_ts THEN 'invalid_window'
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;

This uses the Running/Runnable states in thread_state for consistent aggregation, leaving durations blank when records are missing or cover only part of the window. full means interval coverage only and does not replace stats checks. If a thread lives for only part of the window, narrow the window before calculating.

Coverage sums assume non-overlapping intervals per utid; downgrade invalid overlaps first. Running means the scheduler placed the thread on a CPU, not that every nanosecond executed app instructions; IRQs need separate evidence. High Running does not prove sufficient or insufficient frequency, and blocked Binder/lock time is not CPU execution time.

High Runnable time means the thread wants to run but has not obtained CPU time. First inspect other runnable tasks, RT tasks, cgroup/cpuset, uclamp/capacity, thermal conditions, CPU migration, and frequency. High Running time means the thread is already consuming CPU time; then inspect function-level hotspots, Binder, locks, or layout/drawing. To understand wakeup direction, inspect wakeup information in thread_state, IRQ context, or adjacent ftrace events. A busy CPU alone is not enough for a conclusion.

Common Attribution Paths

Slow Read/Dispatch

This usually appears as high read_to_dispatch_ms or dispatch_latency_dur. Check:

  • Queuing between InputReader and InputDispatcher.
  • Whether InputDispatcher is busy.
  • Whether system_server is waiting in Runnable state.
  • Abnormal window focus, target-window, or InputChannel state.
  • Policy/interception, IME, overlays, focused-window waits, or WindowManager state transitions.
  • CPU impact from other runnable tasks, RT tasks, cgroup/cpuset/uclamp, thermal conditions, or migration policy.

These issues tend to be platform-side; the App can supply page and interaction context.

Slow Handling

This usually appears as high handling_latency_dur. Check:

  • Whether the App main thread remains Running for a long time after receiving input.
  • Heavy computation during the Input stage.
  • Synchronous Binder calls, file reads, lock waits, or waits for network callbacks in the click handler.
  • Synchronous layout, image decoding, complex Compose recomposition, or extensive RecyclerView binding.

App markers and main-thread slices can locate most of these issues.

Fast ACK, Slow First Frame

This is common: the App ACKs input quickly, but feedback still feels slow. Check:

  • Whether the App failed to request the next frame or requested it too late.
  • Whether it waits after ACK for a Binder callback, Activity/Fragment transaction, data callback, or window switch.
  • Whether Animation / Traversal is scheduled for a later Vsync.
  • Whether RenderThread CPU work stalls in syncFrameState or DrawFrame.
  • Whether dequeueBuffer / queueBuffer indicates BufferQueue backpressure.
  • Whether App GPU completion, acquire/release fences, or present fences hold up the frame.
  • Whether SurfaceFlinger/HWC/GPU composition makes the display frame later than expected.
  • Whether the main thread or RenderThread waits for CPU in Runnable state.

Analyze the input round trip and frame production separately. A fast ACK means only that the input-channel finish protocol has advanced; it does not mean the first frame has appeared.

ANRs and Sub-ANR Delays

Most slow interaction responses are sub-ANR problems: the user already notices the delay, but it has not reached the system’s ANR threshold. Examine input ANRs in two main categories:

  • The event was sent to a connection, but the App does not ACK for a long time. An event with no ACK may be absent from the fully joined android_input_events round-trip table; inspect InputDispatcher, relevant logs where available, and the bugreport instead of treating missing rows as a fast response.
  • A focused event keeps waiting for a focused window. Inspect WindowManager / ActivityTaskManager, window creation, focus changes, and InputDispatcher state.

“Fast ACK, slow first frame” generally does not trigger an input ANR because the input event has already been handled. It is more likely a first-frame production or window-visibility issue.

Scrolling Does Not Follow the Finger

This often appears as delayed displacement in the first frame, followed by stable subsequent frames. Check:

  • Time from the first effective MOVE to a scroll-offset counter change.
  • Time from the scroll-offset change to present.
  • Whether MOVE events were coalesced, or the App waited until the next Vsync to handle them.
  • Whether a Runnable wait on the main thread made the first frame miss the current Vsync.
  • Whether subsequent FrameTimeline frames are stable. If so, the main problem is initial latency.

Avoid looking only at jank count. When users say scrolling “does not follow my finger,” they often mean input-to-first-frame delay.

Boundaries of High-Speed Cameras and Perfetto

Perfetto records internal system timing. What the user sees also includes touch sampling, touch firmware processing, display scanning, panel response, and error from external recording. External visual measurement remains necessary for competitor comparisons, acceptance testing, or comparisons across vendors.

The recommended combination is:

1
2
3
4
5
6
7
8
High-speed camera / robotic touch actuator:
Measure user-visible latency and define external start and end points

Perfetto:
Break down internal latency to locate delays in Input, App, RenderThread, SurfaceFlinger, or CPU/GPU

App marker:
Mark business clicks, transition start, first-screen content completion, and scroll-offset changes

High-speed video supplies externally observed endpoints; Perfetto separates internal stages, and app markers identify business actions. Calibrate their time domains instead of treating internal present as the photographed pixel transition. Temporal agreement alone does not establish a cause.

Define contact/actuator onset, target pixel region, exposure/rolling shutter, frame timestamps, scanout location, and synchronization residuals. 1000/fps is a sampling interval, not a bound on total experimental error. Check dropped/interpolated frames, slow-motion playback time, and variable refresh rates. Report repeated trials and distributions rather than treating one illustrative 76 ms value as a device conclusion.

The following numbers are hypothetical report examples, not device measurements: handling is 18 ms, read-to-dispatch 3 ms, dispatch 4 ms, and system ACK adds 1 ms. The candidate frame occurs 50 ms after that ACK. A 21 ms Runnable interval and a late frame would be observations requiring dependency and endpoint checks, not proof that scheduling caused the full delay.

Such observations can support an investigation of scheduling delay, but do not establish a one-refresh causal penalty. A speculative frame or missing external measurement cannot support a claim of confirmed 76 ms user-visible latency.

A response-latency report can use this fixed format:

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
74
75
76
77
78
79
example_only: true
scenario: conversation click
trace_id: example-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: not_evaluated
required_signals:
android.input.inputevent: not_evaluated
FrameTimeline: not_evaluated
sched: not_evaluated
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_event: input_read_marker
end_event: candidate_present
clock_domain: trace_processor_normalized_ns
clock_alignment_verified: false
input_event:
input_event_id: "21847"
event_seq: 77
event_action: UP
event_time_raw: null
event_time_clock: unknown
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: illustrative_selected_dispatch_composite_key
candidate_count: 1
business_marker:
name: Conversation#TransitionStart
ts: 12.371s
associated_frame:
frame_id: 912344
association_method: v58_3_doframe_overlap_then_unordered_present_candidate
speculative: false
present_ts: 12.421s
estimated_input_to_present_ms: 76
input_to_present_ms: null
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
internal_evidence_grade: unknown
visual_evidence_grade: unknown
boundary:
fallback_used: none
missing_signals: unknown
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

Grade internal observations separately from external visible measurements. Missing video leaves absolute user-visible latency unmeasured, but does not automatically invalidate an independently supported dispatch interval. Rename a metric when a fallback changes its endpoints. Assign confirmed/likely/excluded/degraded/unknown per claim and coverage scope; missing data cannot justify excluded. This example demonstrates fields and arithmetic only, without device or visual acceptance.

A response-latency report needs more than one total duration: breaking it into stages tells us which team to involve in the next investigation.

Bringing It Together

The analysis sequence for first-frame interaction response can be standardized:

  1. Define the start: DOWN, UP, effective MOVE, or business click.
  2. Define the end: first pressed-state frame, first transition frame, first content displacement, or first-screen content completion.
  3. Use android_input_events to separate read_to_dispatch, dispatch, handling, ACK, and the candidate estimated_input_to_present_ms.
  4. Confirm the target layer and visual-state change with App markers and, where needed, external video before treating the associated frame’s present as the screen endpoint.
  5. Use FrameTimeline, RenderThread, SurfaceFlinger, and CPU scheduling to explain why the first frame is early or late.
  6. Calibrate user-visible latency with a high-speed camera.

Startup speed covers a long scenario, smoothness covers stability across consecutive frames, and first-frame interaction response covers the first feedback after a user action. Keeping them separate makes the Perfetto evidence clear.

References

  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

Source revision checked: v58.3 (2026-10-14).

About Me and the Blog

Follow Android Performance.

CATALOG
  1. 1. Perfetto Series Catalog
  2. 2. This Article Covers Only the First Frame After an Interaction
  3. 3. Break Response Latency into Four Metrics
  4. 4. Trace Capture Configuration
  5. 5. Which Tracks to Inspect First in Perfetto UI
  6. 6. Tap Scenarios: From DOWN/UP to the First Frame
  7. 7. Scroll Scenarios: From the First Effective MOVE to Content Displacement
  8. 8. Quantifying Input Latency with SQL
  9. 9. Common Attribution Paths
    1. 9.1. Slow Read/Dispatch
    2. 9.2. Slow Handling
    3. 9.3. Fast ACK, Slow First Frame
    4. 9.4. ANRs and Sub-ANR Delays
    5. 9.5. Scrolling Does Not Follow the Finger
  10. 10. Boundaries of High-Speed Cameras and Perfetto
  11. 11. Bringing It Together
  12. 12. References
  13. 13. About Me and the Blog