Skip to content

Add a startup scenario and a plant mechanism, with the main-thread I/O plant captured - #391

Merged
ramesh130 merged 5 commits into
mainfrom
issue-5-startup-scenario
Sep 24, 2026
Merged

ramesh130 merged 5 commits into
mainfrom
issue-5-startup-scenario

Conversation

@ramesh130

Copy link
Copy Markdown
Owner

Closes ramesh130/perfettoagent#5

Adds a startup devicelab scenario, a --plant option that builds a run with a known regression and takes it out before measuring, and the first plant: synchronous main-thread I/O in Application.onCreate (perfettoagent ADR-0007: the demo has no Application subclass, so the patch adds one). It also adds reportFullyDrawn at the demo's first video frame, so traces carry TTFD, and the baseline, regressed and clean-pair captures.

Decisions

  • --plant <name> on lab run, not a separate driver. A plant changes the build, and the build belongs to the harness: the apply has to come before the publish and the revert straight after the install, in the middle of lab run. A separate driver would have to repeat that sequence. So any scenario takes any plant, and Enforce the public API surface in CI #6's three plants will each be one patch file plus one row in devicelab/plants/README.md.

    • It refuses a dirty tree and runs git apply --check before the boot.
    • It reverts after install, and the exit trap reverts on failure; the tree is proved clean afterwards.
    • run.json gains plant (name, patch sha256, base commit). This is an added field, so schema stays 1. The patch is also copied into the run as plant.patch.
    • The self-test checks that every shipped plant still applies to HEAD.
  • 20 cold starts in one trace. p95 by nearest rank is then the 19th launch rather than the slowest. One trace means the distribution is one android_startups query.

  • SCENARIO_TRACE_FROM=playing. The first launch after an install carries one-off work, so the harness's own launch is a warm-up and not a sample. With --trace-from launch it lands in the trace, and the report expects one more startup than the scenario counted.

  • What counts as a cold launch:

    • am force-stop first;
    • the page cache dropped where the device has root, as Macrobenchmark does, so the plant's read is real I/O;
    • am start -W, with the launch refused unless it reports LaunchState: COLD;
    • then a wait for playback, so TTFD lands inside the trace.

    Whether the cache was dropped is recorded in startup/conditions.json.

  • Data sources:

    • startup: sched with sched_blocked_reason; atrace am, wm, view, dalvik, res and binder_driver; the demo's own sections; compact_sched.
    • frametimeline: android_frames is empty without it, and TTID with it.
    • SurfaceFlinger's gfx is left out. It was 1.2M of 2M slices and nothing in the startup modules reads it. That took 20 launches from 192 MB to about 70 MB.
  • TTFD. The platform cannot know when a player is "fully drawn", so the demo reports it at onRenderedFirstFrame. TTFD includes the network fetch, which the README says.

  • Plant size: 192 MiB, read and checksummed. Cold reads on the emulator ran at 190–720 MB/s. 64 MiB moved p50 by about 90 ms and overlapped the clean run. 192 MiB separated them. The argument is made on p50, as the captures below show.

Captures

All runs are on superplayer_verify_36, headless, from 7b43b5f with a clean tree. The planted ones add startup-main-thread-io (patch sha256 1ccfc7b78f9cc1079b54a3dc513a5f619ff73ad4b916a149ecc9a0834624c828). Each run is 20 cold launches with the page cache dropped, and android_startups found 20/20 in every run.

Times are in ms, from the trace. The last two columns are p50 over the run's startups: I/O wait is the main thread's thread_state D with io_wait=1 inside bindApplication.

run dir plant base sha TTID p50 TTID p95 TTID range bindApplication p50 its I/O wait p50
20260924T011856Z-startup (B1) none 7b43b5f 350 456 271–1079 98 21
20260924T012233Z-startup (R1) startup-main-thread-io 7b43b5f 665 1061 497–1136 465 144
20260924T012633Z-startup (C1) none 7b43b5f 346 460 272–516 92 33
20260924T013010Z-startup (B2) none 7b43b5f 426 717 311–784 106 28
20260924T013400Z-startup (R2) startup-main-thread-io 7b43b5f 703 880 603–1045 497 162
20260924T013758Z-startup (C2) none 7b43b5f 387 583 293–630 101 30
20260924T014149Z-startup (B3) none 7b43b5f 451 813 291–900 98 35
20260924T014554Z-startup (R3) startup-main-thread-io 7b43b5f 738 998 631–1148 517 177
  • Clean pairs: B1 with C1 (p50 4 ms apart) and B2 with C2 (39 ms apart). B3 is a third baseline.
  • The regression by hand:
    • Planted, bindApplication is about 5× longer at p50, and the main thread's I/O wait inside it is also about 5×.
    • The rest of the growth is Running time, from the copy and the CRC.
    • TTID's p50 moves by 214–392 ms, depending on which clean run it is set against.
  • Finding: the clean runs drift over the hour, mostly in the tail. p95 went from 456 to 813 ms, and one clean launch took 1079 ms. The extremes overlap across runs, so the separation is argued on p50: the smallest regressed-to-clean p50 gap is 214 ms, twice the whole spread of clean p50s.
  • Where they live: /Users/ramesh/projects/myprojects/superPlayer/devicelab/out/<run dir>/, about 70 MB each. The three sizing runs (20260924T005812Z, …010253Z, …010729Z) are there too, built from pre-review work-in-progress trees.
  • The base sha after merge: this PR is merged by rebase, so 7b43b5f gets a new sha on main. The tree is identical, and the new sha is posted on the perfettoagent issue.

Q4 evidence: is android_startups reliable on API 36 emulator traces?

I ran SELECT * FROM android_startups with modules=['android.startup.startups'] through perfettoagent's pinned trace processor on B1 and R1:

  • It returns 20 rows each, all package=com.superplayer.demo and startup_type=cold, which is the right count.
  • dur matches am start -W TotalTime launch for launch, within 1 ms (mean absolute difference 0.5 ms).
  • Across all 8 runs, TTID from android_startup_time_to_display ends 3–6 ms before dur on average (at most 26 ms), and TTFD was found for all 160 startups.
  • Caveat for Play DASH through the same public API as HLS #7: TTID is NULL unless the trace also has frametimeline. The first pilot, without it, found every startup and no TTID.

Review

I reviewed by hand on two separate axes (sub-agents); the two are kept apart below.

Standards: no hard violations of superPlayer's binding rules, CONTRIBUTING or the devicelab conventions. Fixed:

  • the leak hunt's CSV/Markdown helpers are now shared from lib/trace_processor.sh, replacing a weaker copy;
  • device_has_root is one probe instead of two copies;
  • the "plant or dirty" note is decided once;
  • PLANT_REQUESTED and PLANT_NAME are split;
  • the README's run steps include the plant;
  • tree_dirty's meaning for planted runs is documented;
  • perfettoagent is cited by full link, not a bare #5.

Spec: meets the issue and ADR-0007. Fixed:

  • a planted run's run.json, report.md and patch name the plant, so the README now says they are the operator's record and not evidence to hand to the model;
  • a no-plant control test was added;
  • the page-cache condition is recorded;
  • the README's claim about the cache file's lifetime was corrected.

Carried to #10, below.

Tests

  • devicelab/test/selftest: devicelab 100 passed, leak-hunt 34 passed, startup 16 passed.
  • ./gradlew assemble check --max-workers=1: 1187 JUnit tests, 0 failures, 0 errors.
  • (cd demo && ./gradlew assembleDebug lintDebug spotlessCheck): green.
  • git status --short after all captures: empty.

Skipped, and why

  • The plant name is visible in superPlayer. perfettoagent's eval rule is that nothing the model sees names the plant. superPlayer is the target repo, and after this merges devicelab/plants/startup-main-thread-io.patch, its README and this PR's commit subjects are readable by grep_repo and git log. The issue requires the patch to live in devicelab, so it stays. Handle audio focus, becoming-noisy, wake locks, and configuration change #10 has to build case ranges from a copy or history that excludes devicelab/plants/, or a base that predates it.
  • Handle audio focus, becoming-noisy, wake locks, and configuration change #10 must also build the regressed side itself. Each planted capture is base plus patch, not a commit inside a range of at least 8 unrelated commits.
  • No TV run. Out of scope; one phone emulator only.
  • The base config's DISCARD buffer. It pairs with write_into_file, and trace processor flags that combination (config_write_into_file_discard). No data-loss stats fired. It is shared by every scenario, so it is left alone here.

🤖 Generated with Claude Code

https://claude.ai/code/session_017etd1nNb2E2XPL6WTvkAbo

ramesh130 and others added 5 commits September 24, 2026 11:14
A measurement nobody has seen fail cannot be told apart from one that always
passes, and until now the only regression devicelab had been shown was typed by
hand: the leak hunt's negative test commented four lines out, built, and put
them back. Nobody can repeat that exactly.

`lab run <scenario> --plant <name>` makes it mechanical. It applies
`devicelab/plants/<name>.patch` to a clean tree, publishes, builds and
installs, and reverts the patch as soon as the APK is on the device, before
anything is measured; the exit trap reverts it too when the build fails. A
dirty tree is refused, because the run records the commit and the patch and
uncommitted changes would be a third thing it measured and did not name.
`run.json` gains `plant` (name, patch sha256, base commit; null otherwise),
an added field, so `schema` stays 1, and the patch is copied into the run.

It is an option of the harness rather than of a scenario because what it
changes is the build, which is the harness's: the apply has to come before the
publish and the revert straight after the install. The self-test applies and
reverts a plant in a scratch repository, and checks every shipped plant still
applies to HEAD, since a patch against the demo rots as the demo moves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017etd1nNb2E2XPL6WTvkAbo
The platform measures a launch to its first frame by itself, which is time to
initial display. Time to full display needs the app to say when it shows what
it was opened for, and for a player that is the first frame of video. Without
the call every startup trace had TTID and no TTFD, and Perfetto's
`android_startup_time_to_display` left the column empty.

The listener is removed on dispose like the one beside it, because the player
belongs to the service and outlives the screen. An Activity reports once, so a
stream switch that renders a first frame again adds nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017etd1nNb2E2XPL6WTvkAbo
`lab run startup` cold-starts the demo twenty times in one trace for
perfettoagent#5: each launch is a force-stop, a dropped page cache where the
device has root, `am start -W` refused unless it reports COLD, and a wait for
playback so the startup runs to its first video frame inside the trace. The
trace starts once the harness's own launch is playing, so the first launch
after an install is a warm-up rather than a sample. Twenty, so p95 by nearest
rank is the 19th launch rather than the slowest; one trace, so the
distribution is one query.

The `startup` data source records scheduling with blocked reasons and the
atrace categories Perfetto's startup modules read; `frametimeline` rides with
it because `android_frames`, and so TTID, is empty without it. SurfaceFlinger's
`gfx` is left out: it was 1.2 of 2 million slices, and nothing reads it.

`startup-main-thread-io` adds the `Application` subclass the demo lacks, whose
`onCreate` reads and checksums a 192 MiB file on the main thread (ADR-0007 in
perfettoagent). The size is measured: 64 MiB overlapped the clean launches,
192 MiB put every regressed launch above every clean one.

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

- Trace processor's CSV and the Markdown table it becomes were the leak hunt's
  (`tp_csv_to_tsv`, `tsv_to_markdown`); they move to lib/trace_processor.sh and
  the startup report uses them, rather than a weaker copy that split quoted
  commas.
- `device_has_root` in lib/device.sh is the one root probe; the leak hunt's GC
  and the startup scenario's page-cache drop both read it.
- What the tree carried beyond the commit is decided once (`SP_TREE_NOTE`) for
  the log and the report, and the option's variable is `PLANT_REQUESTED`, apart
  from `PLANT_NAME`, which is set only once a plant is applied.
- devicelab/README.md's run steps now say where a plant goes in and comes out,
  and that `tree_dirty` is true for a planted run by construction.
- A planted run's run.json, report and patch name the plant; plants/README.md
  says they are the operator's record, not evidence to hand to whatever is
  tested on the trace.
- The startup scenario writes `startup/conditions.json`, recording whether the
  page cache was dropped, since runs that differ in it are not comparable.
- The plant's cache file outlives a run (`install -r` keeps data); the README
  said otherwise.
- The self-test also checks that a run with no plant leaves the tree clean.
- perfettoagent is cited by its full issue link rather than by a bare `#5`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017etd1nNb2E2XPL6WTvkAbo
Three planted runs and five clean ones, interleaved on the API 36 emulator,
all from 7b43b5f with a clean tree. Planted, `bindApplication` is about five
times as long at p50 and so is the main thread's I/O wait inside it; TTID's
p50 moves by 214–392 ms. The clean pairs differ by 4 and 39 ms at p50.

The sizing argument is restated on p50. The sizing run put every regressed
launch above every clean one, but across five clean captures a single clean
launch reached 1079 ms, so the extremes overlap and the claim that holds is the
one about the middle: the smallest regressed-to-clean p50 gap is twice the
whole spread of clean p50s. The clean runs also drifted over the hour, most in
the tail, which is recorded as a finding rather than smoothed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017etd1nNb2E2XPL6WTvkAbo
@ramesh130
ramesh130 merged commit 3daf407 into main Sep 24, 2026
1 check passed
@ramesh130
ramesh130 deleted the issue-5-startup-scenario branch September 24, 2026 01:58
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: startup scenario + main-thread-I/O plant captured

1 participant