Skip to content

Add a jank scenario and three jank plants, captured - #392

Merged
ramesh130 merged 6 commits into
mainfrom
issue-6-jank-scenario
Sep 24, 2026
Merged

ramesh130 merged 6 commits into
mainfrom
issue-6-jank-scenario

Conversation

@ramesh130

Copy link
Copy Markdown
Owner

Closes ramesh130/perfettoagent#6

A jank scenario for devicelab, three plants for it, and fifteen captures. devicelab/lab run jank [--plant <name>] opens the feed, runs its plan once without tracing as a warm-up, and checks it scrolled back to the top. It then traces the same plan: 3 rounds of 8 swipes, each round ending in two taps (pause, then play the watched row). Each trace is about 55 s and 29–32 MB.

Decisions

  • The click needed a handler. Feed rows had no click handler, and the screen picker can't be hit at a device-independent point. So the demo gains tap-to-pause on the feed: tap anywhere on the list to pause the watched row, tap again to play it. That makes the middle of the screen the tap target on any phone (ui_tap_centre, a screen fraction like the swipes). It is a tap gesture plus a semantics action, not clickable, which would have merged every row into one accessibility node.
  • The click is in the same trace. A case is one trace pair, so every regression has to fit in one run. The rests keep the tap frames apart from the scroll frames. There are 6 taps, so a single slow tap stands out as one of six.
  • scenario_setup hook: it gets the demo to the feed and warms it up before a playing trace starts. open_feed and wait_session_state move to lib/playback.sh, shared with the leak hunt.
  • No gfx (perfetto/sources/jank.pbtxt: view, dalvik, binder_driver, sched). android.frames.timeline still finds every frame.
  • Plant sizes come from pilots (devicelab/plants/README.md has the tables):
    • feed-tap-sleep: 120 ms, as the roadmap says.
    • feed-grain-allocations: 1,000,000 boxed floats in each scrolled frame's draw, not 50k. At 50k, GC moved but no frame metric did. Work done between frames showed up only as fewer frames.
    • feed-row-remeasure: the padding breathes by 4 dp, so every visible row is re-measured every frame (ADR-0007). A re-measure of these small rows moved p50 but not p95, so each row's re-measure also fits its title in 0.05 sp steps.

Captures

All captures are on superplayer_verify_36 (API 36), from base 57e3144, clean tree. Runs are in devicelab/out/<run>-jank/ of the main checkout; pilots are in devicelab/out/jank-pilots/. Plant patch sha256s: sleep 27700ab54513…, grain ba6902ce089a…, remeasure 1a29bb777a35….

run build frames app jank any jank p50 p95 p99 doFrame p95 asleep in onTouch GCs GC time
20260924T033342Z (B1) clean 676 54.7% 94.4% 40.4 81.5 154.5 38.0 0 7 259
20260924T033648Z (S1) feed-tap-sleep 733 45.6% 97.4% 34.4 72.6 127.3 36.3 724.7 (6) 8 436
20260924T033945Z (G1) feed-grain-allocations 427 66.5% 96.7% 40.7 87.7 138.0 60.3 0 149 10356
20260924T034233Z (R1) feed-row-remeasure 1643 80.3% 100.0% 62.9 92.7 124.1 69.3 0 203 13632
20260924T034924Z (C1) clean 730 42.2% 95.9% 35.5 72.3 123.5 36.6 0 6 172
20260924T035208Z (B2) clean 746 45.3% 96.9% 37.1 76.5 148.1 39.4 0 8 302
20260924T035447Z (S2) feed-tap-sleep 710 37.5% 97.0% 33.7 65.2 122.4 31.9 726.3 (6) 6 186
20260924T035727Z (G2) feed-grain-allocations 444 65.3% 96.4% 40.7 89.1 129.9 67.0 0 142 11044
20260924T040009Z (R2) feed-row-remeasure 1729 73.5% 99.9% 62.5 88.5 124.2 65.5 0 196 12153
20260924T040632Z (C2) clean 834 44.5% 96.8% 39.3 73.3 136.0 37.0 0 8 256
20260924T040915Z (B3) clean 824 37.1% 97.2% 33.5 67.8 115.8 32.5 2.6 (0) 8 428
20260924T041151Z (S3) feed-tap-sleep 789 42.0% 97.5% 33.0 71.8 130.3 34.3 724.4 (6) 8 406
20260924T041431Z (G3) feed-grain-allocations 406 64.3% 95.8% 40.1 86.0 120.4 61.2 0 134 9484
20260924T041712Z (R3) feed-row-remeasure 1796 73.0% 100.0% 62.1 84.2 115.5 62.6 0 196 11802
20260924T042341Z (C3) clean 760 43.0% 97.8% 38.7 73.3 139.9 35.2 0 8 377

By hand:

  • Sleep: six sleeps of 120–123 ms inside AndroidOwner:onTouch in every planted run, 724–726 ms in total. No clean run has one over 16 ms.
  • Grain: 134–149 GCs taking 9.5–11.0 s, against 6–8 GCs taking 0.17–0.43 s. The main thread waits for GC (GC: Wait For Completion Alloc) only in planted runs. doFrame p95 is 60–67 ms against 32–39. App jank is 64–67% against 37–55%; that gap is 10 points, less than the widest clean pair.
  • Remeasure: p50 is 62–63 ms against 33–40, and app jank 73–80%. p95 is 84–93 ms against 68–82: every planted run is above every clean run, but the smallest gap is 2.7 ms, inside the clean spread of 13.7 ms. p99 does not separate.
  • Clean pairs: B1/C1, B2/C2, B3/C3.

Frames stdlib evidence (for perfettoagent#8)

Checked with perfettoagent's pinned trace processor on API 36 without gfx:

  • Frame count and duration agree. android_frames finds the window's frames plus the few that never reached the timeline: 701 = 676 + 25 in B1. Its dur p95 matches the window layer's (81.2 vs 81.5 ms).
  • The frame-to-timeline join is unreliable here. actual_frame_timeline_id usually points at a video SurfaceView layer's row (563 of 701). App jank read through that join is 12.7%, against 54.7% from the window layer. Add MediaRequest with content identity, ordered sources, and start position #8 must filter by layer.
  • jank_type != 'None' is saturated at 92–100% on the emulator (Buffer Stuffing, Prediction Error). The usable signal is App Deadline Missed.

Review (two axes)

  • Standards: everything fixed in 775b0a0 and the docs commit:
    • Fixes: the clickable accessibility merge, the pause race (the service player is now confirmed paused), duplicated feed-opening code, unexplained constants, the SQL headers and the GC scope (collections on HeapTaskDaemon, waits by thread), the tap-coordinates paragraph in the harness README, the docs index, the CLAUDE.md commands, and knob validation plus tests.
    • Left as judgement calls: the plausibility of the grain and title plants, and duplicated screen-size parsing in ui.sh.
  • Spec:
    • Done: the plants README rows and sizing, the capture table mapped to the roadmap metrics, and the clean pairs.
    • Open, outside this repo: perfettoagent should record an ADR for the 1M allocation, the title fit, and "jank = App Deadline Missed on the window layer" (roadmap item 5).
    • Known limitation: rebase-merge rewrites shas, so 57e3144 is recorded in run.json, and its main equivalent will be noted on the issue.

Checks

  • devicelab/test/selftest: devicelab 104, jank 20, leak-hunt 34, startup 16 passed.
  • ./gradlew assemble check --max-workers=1: BUILD SUCCESSFUL, 1187 JUnit tests, 0 failures.
  • Demo assembleDebug lintDebug spotlessCheck: green.
  • Tree clean after the captures.

Skipped

  • No new issue for the main-thread block when the pool recycles a player (Compose:onForgotten, 2–3 s per run); it is noted against Measure UI jank during playback in the feed demo #51 in devicelab/jank/README.md.
  • No knobs in run.json (every capture used the defaults).
  • p95 separation for the remeasure plant is thin, and is reported as such.

🤖 Generated with Claude Code

https://claude.ai/code/session_017etd1nNb2E2XPL6WTvkAbo

ramesh130 and others added 6 commits September 24, 2026 12:14
A tap anywhere on the feed pauses the row being watched and a second tap plays
it again, which is what a short-form feed's viewer expects of the picture in
front of them. The whole list is the target, not the row's box: the watched row
is the one at the top and a thumb lands in the middle. The pause belongs to the
row, so a row scrolled back to is still paused on whichever player it gets.

It is also the feed's first click handler. devicelab's jank scenario taps the
middle of the feed, a point that is on the list on any phone, and
perfettoagent#6's click-handler regression needs a real handler to land in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017etd1nNb2E2XPL6WTvkAbo
`lab run jank` opens the feed, runs its plan once untraced as a warm-up and
scrolls back to the top, then traces the same plan: three rounds of eight
swipes down the feed, each ending in two taps that pause and play the watched
row. The trace carries the frame timeline, the demo's `view` and `dalvik`
slices and the scheduler, and not SurfaceFlinger's `gfx`, which nothing here
reads (perfetto/sources/jank.pbtxt).

The report reads the trace with five queries in jank/sql: the window's frames
(the video `SurfaceView` layers left out), the app's work per frame, the main
thread's states, where it slept in the middle of work by the slice it slept
in, and GC.

The harness gains `scenario_setup`, run once playback is confirmed and before
a `playing` trace starts, so that getting to the feed is not in the trace, and
`ui_tap_centre`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017etd1nNb2E2XPL6WTvkAbo
- `feed-tap-sleep`: the feed's tap handler holds the main thread for 120 ms.
- `feed-grain-allocations`: a grain over the feed, drawn from a million
  boxed floats allocated in each scrolled frame's draw.
- `feed-row-remeasure`: every row's padding breathes, so each visible row is
  re-measured every frame, and a row's re-measure fits its title by stepping
  its font size.

Each is sized from pilot captures on the API 36 emulator; plants/README.md
has the sizes and the reasons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017etd1nNb2E2XPL6WTvkAbo
…ries that say what they count

- The feed takes its tap through a tap gesture and a semantics action rather
  than `clickable`, which merged every row into the list's one accessibility
  node.
- `open_feed` and `wait_session_state` move to lib/playback.sh; the leak hunt
  and the jank scenario open the feed the same way, with the service's player
  confirmed paused first.
- The jank scenario refuses a knob that is not a count, checks the warm-up
  left the feed at its top, and says what `--trace-from launch` adds.
- gc.sql reads collections on `HeapTaskDaemon` only and lists every wait for
  one by thread; frames.sql leaves out frames with no present; blocked.sql
  says how it differs from `sleeping_in_slice_ms`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017etd1nNb2E2XPL6WTvkAbo
…pilots

Fifteen runs on the API 36 emulator, all from 57e3144 with a clean tree, in
three cycles of clean, the three plants, and clean again. By hand:

- feed-tap-sleep: six sleeps of 120–123 ms inside `AndroidOwner:onTouch` in
  every planted run, none over 16 ms in any clean one.
- feed-grain-allocations: 134–149 collections taking 9.5–11.0 s against 6–8
  taking under half a second, a main thread that waits for GC only when
  planted, and app jank 64–67% against 37–55%.
- feed-row-remeasure: frame p50 62–63 ms against 33–40 and p95 84–93 against
  68–82, above every clean run but by less than the clean spread.

The clean pairs are B1/C1, B2/C2 and B3/C3. plants/README.md sizes each
plant from pilot runs: 120 ms stands; the grain is a million objects in each
scrolled frame's draw, not 50k, because smaller or elsewhere it moved GC and no
frame; and the re-measured row fits a title, because re-measuring the demo's
rows alone moved the middle of the frames and not the tail.

The harness README, the command list and the docs index name the scenario.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017etd1nNb2E2XPL6WTvkAbo
@ramesh130
ramesh130 merged commit 9fdd93c into main Sep 24, 2026
1 check passed
@ramesh130
ramesh130 deleted the issue-6-jank-scenario branch September 24, 2026 04:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

devicelab: jank scenario + allocation-storm, sleep and layout-thrash plants captured

1 participant