Skip to content

perf(android): reuse live helper for input and capture - #3811

Merged
thymikee merged 9 commits into
mainfrom
t3/investigate-android-speed
Oct 19, 2026
Merged

thymikee merged 9 commits into
mainfrom
t3/investigate-android-speed

Conversation

@thymikee

@thymikee thymikee commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

Summary

Reuse the live Android helper for input, verified fill, focus, and PNG capture. Uncertain mutations fail without a resend. Validated no-injection refusals and failed reads stop the helper before using the host path. Dead sessions retire before piggyback commands. Duplicate resource IDs retain live point reads within the requested package/class.

agent-device screenshot screen.png --normalize-status-bar

Historical original-app measurements: tap p50 120→67.7 ms, fill 972→760 ms, screenshot 1448→311 ms; 42/42 cases on both variants. Cases and method. These precede the review fixes.

55 files: production +620/−181; tests +670/−5; docs +994/−4; 2,474 gross lines. Scope includes the evidenced live-text and Android TV fixture fixes. Reference: e2e#1234. Follow-up: #3815; adapter #1253 needs an SDK release.

Validation

Final 1977e685b2, rebased onto main a661e43d83: Affected gate passed: 16,388 tests passed, one skipped; receipt run-gWuy8f, exactCommit=true, checkoutChanged=false. Cancellable tap/back calls retain the upstream host path; ordinary input retains helper reuse. Independent rebase review passed.

New regressions: original 0d4b58c341 8 failed/5 passed; fixed runtime all 13 passed. Two live-helper signal-forwarding cases and 56 focused input/alert tests passed. Live API 36: real fill refusal→ADB success and controlled screenshot protocol failure→valid screencap PNG, with both fallback diagnostics. Independent reviews passed after corrections.

Physical devices, older APIs, Appium, and actual-head CI coverage/native/device/provider lanes remain separate obligations.

Fresh combined-stack benchmark: original 335.069→current 273.989 s (18.2% reduction); pre-daemon 282.000 s. All 42 passed, one authored skip, zero retries/recoveries. One local run per arm.

@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-10-19 17:53 UTC

@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.44 MB 5.45 MB +10.5 kB
Package (unpacked) 5.44 MB 5.45 MB +10.5 kB
Package (download) 1.64 MB 1.65 MB +7.1 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 28.1 ms 28.3 ms +0.2 ms
CLI --help 52.4 ms 52.2 ms -0.1 ms

@thymikee

Copy link
Copy Markdown
Member Author

Three problems remain at ddba75f, so this PR needs changes before merge. Each one turns a call that adb completes today into a failure when the live helper session is active.

Fill now fails before any adb attempt when the helper refuses. With a live helper session, fillAndroid sends input fill first (https://github.com/callstack/agent-device/blob/ddba75f/packages/platform-android/src/text-input.ts#L88). AutomationCommandHandler.fill throws "No editable field at the fill point" or "Multiple editable fields at the fill point" before it injects any tap. The host maps that to COMMAND_FAILED with dispatched=unknown and rethrows, so the adb path (focus tap, then type) never runs. On base the same input succeeded, because readAndroidFillTargetBeforeMutation tolerates a null target. Examples are a search bar or placeholder whose tap moves focus to an EditText elsewhere, and overlapping editable siblings. After any snapshot, fill on these patterns becomes a hard failure that claims the dispatch state is unknown, though nothing was injected. No test covers a helper refusal before mutation. The rule should be: fall back to adb exactly when the helper proves that no event was injected, the same no/unknown split discloseHelperTouchDispatch already makes for adb refusals. Can the helper report a typed pre-mutation refusal (an injected=false header or a dedicated errorType) that runAndroidAutomationHelper maps to undefined for every input kind? Please add a regression where the fake helper refuses with that header and fill completes through adb.

Screenshot follows the mutation policy by mistake. if (action !== 'focus') throw error (https://github.com/callstack/agent-device/blob/ddba75f/packages/platform-android/src/automation-helper.ts#L37) treats screenshot, an idempotent read, like a mutation. Two cases end the command with COMMAND_FAILED even though screencap -p would succeed and the session has already stopped: a helper ok=false (takeScreenshot() returned null, or PNG encoding failed), and a socket timeout on a large base64 PNG. ADR 0002, which this PR amends, says to retry one-shot after a failed session command only for idempotent reads, and only after the failed session has stopped. The code does not apply that rule to the read it covers. The rule should be one per-action failure trait declared in a single table in automation-helper.ts: reads (screenshot, focus) stop the session and return undefined so the caller uses adb, and mutations (input) rethrow unless the helper reports no injection. Please add a screenshot test where the helper returns ok=false and the PNG comes from screencap.

The live text read can now return stale text for list rows. readTextForNode (https://github.com/callstack/agent-device/blob/ddba75f/packages/daemon/src/execution/selector-text-runtime.ts#L47) now passes target.identifier for every Android node with a resource-id. Then readAndroidTextAtPointInHierarchy (https://github.com/callstack/agent-device/blob/ddba75f/packages/platform-android/src/fill-verification.ts#L230) ignores the point and returns null unless exactly one node in the whole tree has that id. In RecyclerView or LazyColumn rows, several nodes share an id such as @id/title, so the live read returns null, readTextForNode logs no_text_at_point, and get text @eN silently returns the snapshot-time text. That is a regression from base's point read, and it is a ride-along fix outside the stated scope. The rule should be: identity narrows the point read and never makes a readable point unreadable. Use the single identity match when it is unique, choose by containment of the point when several match, and fall back to the point read when identity is ambiguous. Please add a test with two rows that share an id.

Would a smaller shape work? Keep the session channel and put one action-trait table in automation-helper.ts: input is a mutation, screenshot and focus are reads. Read failures stop the session and return undefined for the adb fallback. Mutation failures rethrow unless the helper reports a typed no-injection refusal, which also returns undefined. Replace the five key wrappers in input-actions.ts with one pressAndroidKey(keyCode). Move the live-text identity fix and the TV fixture into a separate PR. That removes the per-site policy and roughly 100 lines, and it enforces ADR 0002's rule instead of restating it. The helper protocol must first carry a typed injected=false or refusal errorType, so the host can tell pre-mutation refusals from partial mutations.

Not blocking, take or leave: the help and website say --normalize-status-bar normalizes "Android" chrome and that Android captures immediately by default, but provider-webdriver (Appium Android) has no stabilize or normalizeStatusBar handling, so name the ADB-backed backends or make that backend refuse the flag (packages/contracts/src/screenshot.ts#L177); without a helper session, readAppFocus falls back to readAppState and detectAndroidEscapeSurface then reads it again, so the escape path pays two dumpsys rounds (android-foreground-surface.ts#L34); selector-text-runtime.ts#L50 branches on device.platform === 'android' to attach a platform-neutral ElementTextTarget, element-text-runtime.ts re-exports that type under a second import spelling, and the moved code still uses android_touch_helper_install phase names; and the PR is large at 1231 gross lines and +5.8 kB npm growth, so splitting out the live-text identity fix and the TV fixture would help review.

I read the code at ddba75f. The emulator benchmark and the full test-app suite run are taken from the docs and evidence in the PR, and I did not reproduce them. Those live runs cover a Pixel 7 API 36 emulator only, so a physical device, API below 33, the helper-unavailable fallback, and a fill on a field that is not editable at the point are not covered. I did not check whether UiAutomation.takeScreenshot orientation matches screencap on rotated displays on every API level. The shared resource-id finding comes from reading the code, and I did not measure how often real RecyclerView or Compose lists hit it.

Smoke Tests was still running when I checked, with no failure to attribute yet. This PR changes default screenshot behavior and CLI help text, which smoke runs may exercise. There are no conflicts. Before merge, helper failures must follow ADR 0002 through one action-trait table, with screenshot and focus falling back to adb and a fill refused before any injection falling back too, and the identity text read must pick by point among duplicate ids, each with a regression test.

@thymikee

Copy link
Copy Markdown
Member Author

The head delta since ddba75f only adds evidence docs, so the three defects from the earlier review are still in the code at 0d4b58c. Nothing from that review is fixed yet. The 42-case parity run on a Pixel 7 API 36 emulator covers the happy path only, and I did not reproduce it.

First, fillAndroid sends input fill to the live helper first. The helper can refuse before it injects anything ("No editable field" or "Multiple editable fields at the fill point"). runAndroidAutomationHelper rethrows that refusal because the action is not focus, so the adb focus-tap-then-type path never runs. A fill that base completes, such as a placeholder or search bar that moves focus, now fails with COMMAND_FAILED and dispatched=unknown, although nothing was typed. The rule: a refusal that happens before any injection must let the caller take the adb path, for every input kind. The helper needs to report that refusal with a typed marker (injected=false or a dedicated errorType), and runAndroidAutomationHelper should map it to undefined. Please add a regression where a fake helper refuses with that marker and the fill completes through adb. That test should fail on head.

Second, if (action !== 'focus') throw error treats screenshot, an idempotent read, as a mutation. If the helper returns ok=false (null screenshot or PNG encode failure) or the 30 s socket timeout hits, the command ends. screenshot.ts only uses screencap when the helper returns undefined. So the user gets COMMAND_FAILED where screencap -p would have worked, and ADR 0002 says idempotent reads retry one-shot after the failed session stops. The rule: failure handling is declared once per action in automation-helper.ts. Reads (screenshot, focus) stop the session, emit a diagnostic and return undefined. Input rethrows unless the no-injection marker from the first finding is present. Please add a screenshot test where the helper returns ok=false and the PNG comes from screencap.

Third, readAndroidTextAtPointInHierarchy ignores (x,y) when target.identifier is set. It returns null unless exactly one node has that resource-id. selector-text-runtime.ts attaches the identifier to every Android node that has one. In RecyclerView or LazyColumn rows that share an id like @id/title, the live read returns null, get text @eN logs no_text_at_point, and it silently returns stale snapshot text. Base's point read returned the live value. The rule: identity may narrow the point read but must never make a readable point unreadable. Use the identity match when it is unique, pick the match that contains the point when several match, and otherwise fall back to the point read. Please add a test with two rows that share an id. You can also move this fix to its own PR, as I proposed before.

My simplicity question from the earlier review is still open, since the delta did not touch it. Could the session channel stay, with one action-trait table in automation-helper.ts (reads fall back to adb, input rethrows unless the helper reports a typed no-injection refusal), the key wrappers in input-actions.ts collapse into one pressAndroidKey(keyCode), and the live-text identity fix and the Android TV fixture move to separate PRs? That would remove the per-site policy and about 100 lines. It needs the helper protocol (AutomationCommandHandler.java and snapshot-helper-session-protocol.ts) to carry the typed refusal first, so the host can tell a pre-mutation refusal from a partial mutation.

After the first two fixes, please run live on Android with a helper session active and show two cases. A fill on a field the helper refuses (no editable node at the point) must complete through adb with the expected text, and the daemon log must show the refusal fallback diagnostic. A forced screenshot helper failure must produce a valid PNG through screencap, with the fallback diagnostic in the session log.

I did not test on a physical device, on API below 33, through the helper-unavailable fallback, or on Appium Android. I also did not compare UiAutomation.takeScreenshot orientation with screencap on rotated displays. I judged how often real lists hit the third finding from the code, not by measurement.

Smoke Tests was still running when I checked, and no check has failed. Before merge, the three fixes above need regression tests that fail on head, and the live run above needs to pass.

@thymikee

thymikee commented Oct 19, 2026 •

Copy link
Copy Markdown
Member Author

Addressed the three blocking findings in runtime cad578adc1, initially published at 2f85c9c01c, now rebased at 1977e685b2:

  • Pre-input refusal: native code tracks whether an injection attempt began and sends injected=false only before that boundary. The host accepts it only after protocol/request correlation. Fill, tap, and key use their existing host paths after stopping the session. The same error text without the marker, true/unknown, and an uncorrelated marker cannot authorize a resend.
  • Read failures: one action-policy table declares screenshot/focus as reads. Session failures stop the helper, emit the action's fallback diagnostic, and return to the host read. Input with an uncertain outcome still throws.
  • Repeated IDs: a unique identity can follow a moved target; repeated IDs select the containing point; unresolved identity falls back to a readable point while retaining explicit package/class constraints.

The new 13-case regression suite on original 0d4b58c341: 8 failed, 5 passed; on the fixed runtime: 13 passed. The full affected gate at cad578adc1 passed 16,115 tests, one skipped, receipt run-K2DWUV, exactCommit=true, checkoutChanged=false. Independent review found an initially unawaited transient-capture admission; that was corrected and the follow-up review has no findings. Final evidence-only 2f85c9c01c preserves every case value, independently reconstructed, with exact incremental receipt run-biMOyO (zero selected checks).

Live Pixel 7/API 36, rebuilt helper 0.21.29-dev:

  • A real non-editable search placeholder refused native fill before injection. ADB completed chips, and its click counter was one. Request diagnostic: android_helper_input_fallback, dispatched: no.
  • A controlled, correlated screenshot error response produced a valid PNG through one screencap, with android_helper_screenshot_fallback. This injects a protocol fault; it does not reproduce a device PNG-encoding defect.
  • Both cases passed again on the rebased upper runtime. Our bundled test-app's normal fill/clear/refill and PNG capture passed, preserving the neighboring field.

For the optional points: help/docs now say ADB-backed Android. The action policy is centralized. I retained the existing named key operations, the base-reproduced TV fixture correction, and the moved-text fix; splitting them further would add stack churn without changing these verified contracts. Duplicate focus dumps and diagnostic-phase renaming remain outside this correctness round.

Physical/older-API devices, Appium, and actual-head CI remain unverified here. No new whole-suite performance result is claimed. All owned sessions/daemons were closed. These reviews were conversation comments; there were no inline threads to resolve.

Current-main conflict follow-up: rebased onto a661e43d83. The input conflict preserves upstream cancellation: explicit-signal tap/back dispatch stays on the cancellable host path; ordinary input retains helper reuse. Two new active-helper signal tests and 56 focused input/alert tests passed. Independent review found no issues. Affected gate passed: 16,388 tests passed, one skipped; receipt run-gWuy8f, exactCommit=true, checkoutChanged=false.

@thymikee
thymikee force-pushed the t3/investigate-android-speed branch from 2f85c9c to 1977e68 Compare October 19, 2026 16:22
@thymikee

Copy link
Copy Markdown
Member Author

Checked latest main a661e43d83, including daemon merges #3797, #3806 and #3818. Both explicit rebase commands were no-ops: parent 1977e685b2, upper c2fddc1ef9 already contain it. Published heads are unchanged and both are conflict-free. Their existing exact-head validation receipts still apply.

Fresh Android comparison: pre-daemon stack 0a43d61ff6 versus current c2fddc1ef9, sequential ABBA, two runs per source, same Pixel 7/API 36 and installed bundled Release test-app, isolated built SDKs/daemon states, zero retries. Native helper source is unchanged. All 236 timed operations, including 24 native clear oracle trials, passed. This compares all intervening main changes, not only daemon work.

Observed p50 in milliseconds:

Operation Before Current
Warm package open 162 170
Relaunch with URL 1054 940
Fill 286 274
Clear 276 275
Screenshot 86 86
Scroll until bottom target + settle 1988 1992
Scroll until top target + settle 1895 1966

No median regression reached 100 ms in these workloads. Every warm package open used zero host am start calls; helper reuse survived the runtime-store migration. No helper starts occurred during measured fill, screenshot or scroll requests. All four runs had zero helper fallbacks.

Cold follow-up: 20 fresh-daemon/app-start cycles, alternating source order, force-stopped app, identical source-built native APK installed before timing. Open median 2,220→2,054 ms; close 277→261 ms; daemon stop 187→187 ms. Both sources used two helper starts during cold foreground open; the extra start observed in one primary cold trial is also present on the older implementation.

Tails remain visible: one primary moving-clear trial took 791 ms, with additional reads in the existing input/verification path, and one cold open took 3.32 s. There was no fallback, resend or wrong edit. The fill verifier and native Java input source are unchanged. A dedicated alternating repeat added 16 moving-clear trials: before median 276.5 ms, range 257–327; current median 279 ms, range 262–290. All had one clear and zero decoy edits/clicks.

This is a small local regression check, not a statistical tail-latency guarantee or a new full 42-case speedup result. Scroll-until needed one or two passes depending on observed content position. The initial harness attempt failed its scroll target after generic Back; it is retained and excluded. Corrected runs use explicit keyboard dismissal and verify target visibility.

All owned sessions/daemons closed. Raw samples, traces, scripts and the excluded setup run are retained locally. No code/push was needed; these measurements do not replace actual-head CI obligations.

@thymikee

Copy link
Copy Markdown
Member Author

The PR is ready. The fixes from the earlier review (#3811 (comment)) are in at 1977e68, and I found no new problems in the code. Not blocking: the cancellable-input check (!options?.signal &&) is repeated in pressAndroid and backAndroid at https://github.com/callstack/agent-device/blob/1977e68/packages/platform-android/src/input-actions.ts#L49, so would it be simpler to let runAndroidAutomationHelper take the signal and decline when one is present? You can take this or leave it.

Smoke Tests was still running when I checked and had no failed log. This diff changes the Android helper APK and the input, screenshot and fill routes, so the Android smoke lane should exercise it. I can't attribute the CI result until it finishes. No conflicts.

On evidence, the live Pixel 7 (API 36) refusal-fallback and screenshot-fallback runs appear only in your comment from 2026-10-19T15:50Z, with no daemon-log excerpt, and I did not reproduce them. I did not run the regression suite, and I judged that the new tests fail on 0d4b58c by reading the old code. I did not check physical devices, API levels below 33, Appium Android, or screenshot orientation of UiAutomation.takeScreenshot versus screencap on rotated displays.

Smoke Tests needs to finish green on 1977e68 before merge.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Oct 19, 2026
@thymikee

Copy link
Copy Markdown
Member Author

Fresh full Android benchmark on the final combined stack, using the original experiment’s unchanged app and test sources.

SDK Commit Passed / failed / authored skip Fixture recoveries Elapsed
Original baseline eef1a3f76d 42 / 0 / 1 0 335.069 s
Stack before daemon rebase 0a43d61ff6 42 / 0 / 1 0 282.000 s
Current combined stack c2fddc1ef9 42 / 0 / 1 0 273.989 s

Current saves 61.080 seconds (18.2% less elapsed time) versus the original baseline, and 8.011 seconds (2.8%) versus the pre-daemon stack. The daemon/main composition did not regress this full run.

All three reports contain exactly the same 43 selected identities / 42 runnable identities, matched against the reference case registry. Every runnable test passed in one attempt. Every attempt reports complete cleanup and no secondary errors. Runner retries were disabled; the fixture’s authored second-tap recovery at tests/fixtures.ts:92 was never called.

This measures #3811 + #3815 + the readiness opt-in from e2e #1253, including intervening main changes. It is not a standalone speedup attribution to either SDK PR or daemon work. The adapter is pinned at 5e1d8952f4b0fd0d6a12b084ed8151e71e7cd889; its default published-SDK dependency is unchanged. The benchmark explicitly links each independently built source SDK.

Method: Pixel_7_CI / emulator-5554, Android 16/API 36; one worker; zero retries; Device panel stream attached throughout. Release APK SHA-256 b163e985ebd8340bf10447cdf6826aea37e429ee8c4382f9179c863c8c0619b2. App/tests match reference 2593e45025edc52b2c381e809b41cb63d5878e2b. Original SDK uses the retained adapter timers because it lacks bounded readiness; both newer SDKs use E2E_MOBILE_DEVICE_READINESS=1. Each arm has isolated dependencies and daemon state. Native helper backend/version was proven before timing; builds and APK installation are excluded.

Largest original→current case reductions:

  • huge virtualized list reaches row 512: 6.476 s saved.
  • control inventory / a field autofocuses on mount and focus() moves the focus: 5.190 s saved.
  • product catalog adds exactly two of the right variant: 5.118 s saved.
  • sticky chrome: accept below the fold, then continue: 4.182 s saved.
  • control inventory / a double tap lands inside the 300 ms window: 3.667 s saved.

Current vs pre-daemon is mixed by case: bottom tabs took 1.023 s longer and error recovery 0.975 s longer; the total still improved by 8.011 s. This is one run per arm with host-load variation, not a statistical guarantee of the 2.8% difference or tail latency. The experimental direct engine, physical devices and older Android APIs were not rerun.

Exact matched runnable case IDs and attempt durations (seconds)
Test ID Original Pre-daemon Current
tests/controls.e2e.ts::control%20inventory::a%20label%20query%20resolves%20a%20labeled%20text%20field 3.576 3.561 3.553
tests/controls.e2e.ts::control%20inventory::a%20labeled%20field%20answers%20name%2C%20display%20value%2C%20value%20reads%2C%20fill%2C%20and%20clear 8.227 7.925 7.872
tests/controls.e2e.ts::control%20inventory::a%20placeholder%20query%20and%20attribute%20reads%20find%20the%20field 3.127 3.109 3.041
tests/controls.e2e.ts::control%20inventory::a%20secure%20field%20takes%20a%20fill%20and%20denies%20value%20reads 4.915 4.494 4.696
tests/controls.e2e.ts::control%20inventory::press%20sends%20a%20character%20and%20Enter%20through%20the%20soft%20keyboard 6.230 5.801 5.801
tests/controls.e2e.ts::control%20inventory::a%20field%20autofocuses%20on%20mount%20and%20focus%28%29%20moves%20the%20focus 9.343 4.290 4.153
tests/controls.e2e.ts::control%20inventory::toBeFocused%20follows%20the%20focus 8.296 5.126 4.763
tests/controls.e2e.ts::control%20inventory::a%20switch%20checks%2C%20unchecks%2C%20and%20reports%20its%20state 9.186 6.390 6.467
tests/controls.e2e.ts::control%20inventory::checkbox%20and%20radio%20roles%20answer%20getByRole%2C%20and%20a%20checkbox%20toggles%20by%20tap 6.265 4.323 4.233
tests/controls.e2e.ts::control%20inventory::checkbox%20and%20radio%20roles%20check%2C%20uncheck%2C%20and%20report%20their%20state 11.286 8.200 8.283
tests/controls.e2e.ts::control%20inventory::a%20tab%20role%20answers%20getByRole 2.981 2.960 3.286
tests/controls.e2e.ts::control%20inventory::a%20radio%20group%20reports%20the%20checked%20option 8.635 6.057 6.374
tests/controls.e2e.ts::control%20inventory::buttons%20report%20disabled%2C%20enabled%2C%20and%20a%20state%20that%20arrives%20later 7.748 5.007 5.164
tests/controls.e2e.ts::control%20inventory::a%20display%3A%20none%20element%20is%20absent%20rather%20than%20hidden%2C%20its%20visible%20twin%20attached 5.623 4.466 4.709
tests/controls.e2e.ts::control%20inventory::a%20header%20answers%20its%20accessible%20name 3.028 2.887 2.906
tests/controls.e2e.ts::control%20inventory::a%20tab%20row%20answers%20count%2C%20all%2C%20texts%2C%20filters%2C%20positions%2C%20and%20switching 4.819 4.727 4.714
tests/controls.e2e.ts::control%20inventory::a%20list%20answers%20counts%2C%20list-form%20text%2C%20reads%2C%20and%20boxes%20in%20order 4.720 3.628 3.524
tests/controls.e2e.ts::control%20inventory::a%20long%20press%20lands%20as%20one 6.708 5.488 5.520
tests/controls.e2e.ts::control%20inventory::a%20double%20tap%20lands%20inside%20the%20300%20ms%20window 7.377 4.101 3.710
tests/controls.e2e.ts::control%20inventory::an%20ambiguous%20locator%20fails%20at%20once 4.286 3.587 3.253
tests/controls.e2e.ts::control%20inventory::screenshot%2C%20restart%2C%20and%20clearState%20reopen%20the%20app%20on%20its%20home%20list 8.018 5.266 5.510
tests/controls.e2e.ts::refusals::verbs%20a%20device%20never%20declares%20are%20refused%20before%20the%20engine 0.002 0.001 0.002
tests/controls.e2e.ts::refusals::keys%20the%20soft%20keyboard%20cannot%20send%20are%20refused%20by%20the%20engine 0.128 1.541 1.480
tests/controls.e2e.ts::refusals::a%20missing%20locator%20fails%20as%20not%20found%20once%20its%20timeout%20passes 1.103 1.103 1.114
tests/flows-lists.e2e.ts::infinite%20scroll%20reaches%20the%20target%20item 14.632 14.200 14.599
tests/flows-lists.e2e.ts::product%20catalog%20adds%20exactly%20two%20of%20the%20right%20variant 9.454 4.464 4.336
tests/flows-lists.e2e.ts::gestures%3A%20long-press%2C%20then%20swipe%20left 7.660 7.234 7.465
tests/flows-lists.e2e.ts::gestures%3A%20double-tap%20completes%20the%20flow 9.002 8.649 8.797
tests/flows-lists.e2e.ts::sticky%20chrome%3A%20accept%20below%20the%20fold%2C%20then%20continue 11.036 6.733 6.854
tests/flows-lists.e2e.ts::async%20states%3A%20load%2C%20pull%20to%20refresh%2C%20claim%20through%20the%20toast 10.116 10.419 10.479
tests/flows-lists.e2e.ts::huge%20virtualized%20list%20reaches%20row%20512 55.260 59.336 48.784
tests/flows.e2e.ts::text%20inputs%20unlock%20the%20submit%20button 5.889 5.594 5.216
tests/flows.e2e.ts::filled%20text%20inputs%20report%20their%20values 5.133 4.747 4.738
tests/flows.e2e.ts::modal%20flow%20confirms%20through%20the%20native%20alert 7.716 4.635 4.395
tests/flows.e2e.ts::bottom%20tabs%20act%20inside%20the%20last%20tab 6.016 4.037 5.060
tests/flows.e2e.ts::error%20recovery%20retries%2C%20then%20confirms%20the%20delete 7.818 6.636 7.611
tests/flows.e2e.ts::choice%20controls%20place%20the%20exact%20order 8.047 4.060 4.471
tests/flows.e2e.ts::debounced%20search%20selects%20the%20target%20once%20results%20arrive 8.088 8.088 7.882
tests/login-form.e2e.ts::login%20form::rejects%20a%20malformed%20email 8.125 6.444 6.480
tests/login-form.e2e.ts::login%20form::rejects%20an%20unknown%20account 8.261 6.452 6.350
tests/login-form.e2e.ts::login%20form::signs%20in%20and%20logs%20out 12.082 11.484 11.570
tests/smoke.e2e.ts::home%20lists%20the%20scenarios%20and%20opens%20one 4.588 4.264 4.142

All owned sessions and daemons closed gracefully. Source SHAs and clean worktree state remained unchanged. Local artifacts retain full reports, source/dependency provenance, runner logs, setup snapshots and analysis scripts. Actual-head CI obligations remain separate.

@thymikee
thymikee merged commit 1c3edff into main Oct 19, 2026
23 checks passed
@thymikee
thymikee deleted the t3/investigate-android-speed branch October 19, 2026 17:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant