Skip to content

fix(ios): prepare shared synthesized input without contacts - #2362

Closed
thiagobrez wants to merge 6 commits into
mainfrom
fix/ios-synthesized-input-cold-start-warmup
Closed

fix(ios): prepare shared synthesized input without contacts#2362
thiagobrez wants to merge 6 commits into
mainfrom
fix/ios-synthesized-input-cold-start-warmup

Conversation

@thiagobrez

@thiagobrez thiagobrez commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Synthesize one empty XCTest event record, with no pointer path, per runner process before the first real record. It runs at the shared record boundary (RunnerCreateEventRecord), so gesture/sequence, scroll, synthesized drag, swipe and coordinate tap all pass through it. No contact reaches the target, and the flag is set only after a successful preparation, so a failure is retried on the next record.

What this does and does not establish

  • It pays XCTest's one-time synthesis setup (about 0.4 s on a warm CI simulator, about 5 s on a cold hosted one) ahead of the first timed pointer path instead of inside it. It adds no cost per command.
  • It does not fix the missed first reorder on cold hosted iOS 26.5 runs that motivated it. The matched cold-boot comparisons in this thread reproduced the failure after a successful preparation. The kernel timeline and file trace localize that delay to the app's main thread blocking in UIKit's feature-flag read of UIKit.plist on a disk-image I/O wait, before application event entry. That is outside this runner.

Regression

testSynthesizedInputPreparationDoesNotDeliverContactsAndOrdersMixedRoutes swizzles XCSynthesizedEventRecord to record the submitted path count per record across all six entry points, with the first preparation rejected: [0, 1, 0, 1, 1, 1, 1, 1]. A test-only +resetSynthesizedInputPreparation on RunnerSynthesizedGesture makes it self-contained, so it runs in the shared test process like every other runner unit test. The earlier isolated xcodebuild invocation, xcresult merge and multi-invocation selection parsing are gone; the workflows and selection script match main apart from the PR-lane entry.

Validation (head d3a651c, rebased on main fda41b4)

  • pnpm check:xctest-selection: 230 declared, PR list 83, nightly 228, 0 dark.
  • iOS and macOS runner builds with -D AGENT_DEVICE_RUNNER_UNIT_TESTS: clean, no warnings.
  • The regression run three times in one process together with testSinglePointerFlingFallsBackToXCTestCoordinateDragWhenPrivateSynthesisFails on an iOS 26.2 simulator: 6/6 passed, with the failed-then-performed preparation sequence logged each iteration.

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
Installed (including dependencies) 4.51 MB 4.51 MB +2.3 kB
Package (unpacked) 4.51 MB 4.51 MB +2.3 kB
Package (download) 1.34 MB 1.34 MB +534 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 26.6 ms 27.6 ms +0.9 ms
CLI --help 77.1 ms 75.9 ms -1.1 ms

@thiagobrez

Copy link
Copy Markdown
Contributor Author

CI validation (draft run)

All code-quality gates pass: Lint & Format, Typecheck & Package, Repo Guards, Coverage, Compatibility & Provenance, CodeQL, Bundle Size, and the iOS/macOS/Linux smoke lanes.

The iOS lane exercised the change end to end on a simulator, all green:

  • Restore and build iOS XCTest runner — the new RunnerTests+SynthesizedInputWarmup.swift compiles.
  • Run targeted iOS runner XCTest regressions — pass.
  • The warm-up ran once, as designed: AGENT_DEVICE_RUNNER_SYNTHESIZED_INPUT_WARMUP outcome=performed elapsedMs=433.
  • gesture-pan-duration.ad synthesized-gesture replay — pass (26.3 s), confirming the warm-up does not perturb normal synthesized-gesture operation.
  • Run fixture-backed iOS simulator E2E smoke — pass.

On this warm CI simulator the one-time attach is cheap (433 ms), so the warm-up is a near-no-op — which is the intended behavior on a warm host. The cold-boot case it targets is not reproducible on an already-booted smoke simulator; that is what the external reproduction branch demonstrates the defect on, and what a maintainer cold-boot device run (this lane on ready_for_review) would confirm the fix against.

The two red checks are pre-existing flakes unrelated to this iOS-only change and both re-run: Android Smoke (agent-device press on a live emulator, smoke:automation-system) — Android uses the separate helper, untouched by #if os(iOS) Swift; CI Integration (provider-backed suite) — passes 190/190 on a clean local rebuild of this branch.

@thiagobrez

Copy link
Copy Markdown
Contributor Author

Update: CI Integration passed on re-run (it was a flake). Android Smoke is a pre-existing repo-wide failure, not this PR: the same smoke:automation-systempress semantic canary assertion fails on main (e.g. bd08e6e, dcd8b65) and on every current open PR I checked (#2356, #2359, #2360, #2361). It is Android-only and cannot be affected by this #if os(iOS) Swift change. Leaving it rather than re-running into the same base failure.

@thymikee

thymikee commented Sep 6, 2026

Copy link
Copy Markdown
Member

The warm-up at 5d4fd44 sends a real tap to the top of the app window. That point is not guaranteed to be inert in a status-bar-hidden or edge-to-edge app, so it can activate content before the requested gesture. Use a non-delivering warm-up and prove that it cannot change app state.

It also runs only for gesture, while scroll and synthesized drag reach the same timed-input pipeline without it. Put the once-only preparation at the shared input boundary and test ordering across those routes. The reported warm CI run does not establish that the cold-start failure is fixed; a cold-start red/green run is still needed.

thymikee commented Sep 6, 2026

Copy link
Copy Markdown
Member

Reviewed 5d4fd44. Agreeing with @thymikee on the two structural points, plus four things in the code itself.

1. The warm-up point is computed in one frame and delivered in another. synthesizedInputWarmupPoint is fed resolvedTouchReferenceFrame (RunnerTests+SynthesizedInputWarmup.swift:43), i.e. the window frame, which can have a non-zero origin. synthesizedTapAt then discards that frame entirely: it re-resolves its own context from XCUIScreen.main.screenshot() (RunnerTests+Interaction.swift:667-679, :895), whose origin is always (0,0), and nativeSynthesizedPoint subtracts that origin. So an absolute point is consumed as a relative one. On a full-screen iPhone the two coincide and it works by accident; under iPad Slide Over / Stage Manager the contact is shifted by the window origin. testSynthesizedInputWarmupPointSitsInTheStatusBarBandOfTheFrame uses a frame at origin (10,20) and asserts frame.contains(point) — precisely the case where the consumer does not honour that contract, so the test passes while the delivered point is wrong.

2. The warm-up adds AX and a screenshot ahead of the gesture it exists to speed up. resolvedTouchReferenceFrame does app.windows.firstMatch.exists/.frame plus visibleKeyboardFrame — AX round-trips — and synthesizedTapAt then takes a full-screen screenshot. Both are serialized in front of the first gesture on a cold or loaded simulator, the exact state this targets. The file's own comment says the digitizer attaches "regardless of where the contact lands", which makes all of this frame resolution unnecessary.

3. Non-delivering warm-up, concretely. The status-bar band is not inert even when the tap does reach the status bar: a status-bar tap triggers scrollsToTop on the app's top scroll view — a real state change, and worst for the list/drag scenarios this PR is about. RunnerSynthesizedGesture.m already has the pieces for a warm-up that delivers nothing: RunnerCreateEventRecord + RunnerSynthesizeEventRecord with no pointer paths. That forces the synthesis call (and so the attach) without a contact, needs no frame at all, and is unit-testable as "the record carries zero paths" — which is the proof that it cannot change app state.

4. Policy mismatch, and the flag is set before the attempt. The warm-up runs under .coordinateTap policy while the gesture that follows runs under .synthesizedDrag. didWarmSynthesizedInput = true is set before synthesizedTapAt, so a context failure specific to .coordinateTap permanently disables the warm-up for the process even though the real gesture path would have synthesized fine. The comment's justification ("a real gesture would hit the same condition") doesn't hold across two different policies. Set the flag on .performed, or resolve under the policy the gesture will actually use.

On the shared boundary (@thymikee's second point) — the three synthesis entry points are synthesizedTapAt, synthesizedDragAt and the sampled synthesizeGesture path. Only the gesture handler warms (RunnerTests+CommandExecution.swift:2272); executeSynthesizedDragGesturesynthesizedDragAt (:2427, :2522, including .controlledScroll) and RunnerTests+SequenceExecution.swift:166 reach the same pipeline cold.

On evidence: the three unit tests are CGRect arithmetic — nothing in the suite fails if ensureSynthesizedInputWarmed is deleted from the gesture handler. Once the preparation sits at the shared boundary, an ordering test (no synthesis on any route before the warm-up, exactly one warm-up across mixed gesture/scroll/drag/sequence traffic) would be cheap and would cover the actual contract. That still leaves the causal claim — a cold-boot red/green run is the only thing that establishes the lazy-attach diagnosis.


Generated by Claude Code

@thiagobrez thiagobrez changed the title fix(ios): warm the synthesized-input digitizer before the first gesture fix(ios): prepare shared synthesized input without contacts Sep 6, 2026
@thiagobrez

Copy link
Copy Markdown
Contributor Author

Addressed the structural feedback in 1cf7e5f:

  • Replaced the real tap with a record containing zero pointer paths. Removed the frame calculation, AX/screenshot work, and .coordinateTap preparation policy.
  • Moved preparation into the shared event-record factory, before any pointer paths are constructed. Gesture/sequence, scroll, synthesized drag, swipe, and synthesized tap all pass through it.
  • The process-wide flag is set only after successful preparation. Failed preparation leaves the requested input available and permits preparation on the next route.
  • Replaced the arithmetic tests with an XCTest spy on the actual bridge entry points. It fails against 5d4fd44 with [1,1,1,1,1,1]; the revised code passes with [0,1,0,1,1,1,1,1], including an intentionally failed first preparation. The test runs separately because the production state is process-scoped.

I also restored Agent Device's iOS 26 pointer route in thiagobrez/react-native-reorderable#101, preserving the simplified candidate pipeline. Its workflows now accept an immutable upstream commit for source-build validation.

Cold-start evidence is still in progress. The first local cold boot with this build passed (empty preparation took 5.2 s; all three drag results were prompt), but the local released baseline did not reproduce a silent drop. I have corrected the PR body to avoid claiming the causal diagnosis is established and am running the hosted baseline/source comparison before claiming resolution.

@thymikee

thymikee commented Sep 6, 2026

Copy link
Copy Markdown
Member

The shared contact-free preparation at 444cfeb addresses the earlier structural findings, and the reported red/green bridge test covers ordering and retry. Coverage now fails because nightly skips this regression without running it separately. Add the isolated test invocation to nightly too, preserving the selection guarantees. Comparable cold-start baseline/candidate evidence is still pending, so this is not merge-ready yet.

@thiagobrez

Copy link
Copy Markdown
Contributor Author

One correction to the original diagnosis from the local cold-run trace: the Recap digitizer is attached/detached per real gesture, rather than remaining attached for the whole runner process. In the revised run:

20:54:59.360  empty preparation completed (5213 ms)
20:54:59.365  digitizer attached for the real drag
20:55:09.348  digitizer detached
20:55:09.812  next drag's digitizer attached

There is no attachment during that empty preparation interval. This does not rule out preparation of other shared XCTest state, but it does rule out treating these logs as proof of the original persistent-digitizer explanation. Two valid local cold boots passed, but the source-baseline/fixed hosted comparison is still needed to establish whether this prevents the failure instead of just adding delay. Keeping the causal claim explicitly unproven.

@thiagobrez

Copy link
Copy Markdown
Contributor Author

Addressed the nightly coverage gap in 211f232.

Nightly now runs the preparation regression in its own test invocation, then the remaining suite. It merges both xcresult bundles before the existing source-derived executed-count assertion. The selection guard now unions separate invocations while respecting skips within each invocation; regressions cover removing the isolated run and conflicting flags in one command. No coverage invariant was waived.

The 40 selection/summary tests and pnpm check:affected --run pass. The local remaining native suite passed all 225 tests; the isolated preparation test also passes. I am verifying the merged bundle and waiting on the comparable hosted source-baseline/candidate cold runs. Downstream iOS 27 fallback has passed with the candidate; iOS 26 and the other Apple lane are still running.

@thiagobrez

Copy link
Copy Markdown
Contributor Author

The nightly merge was verified locally: the isolated regression plus remaining suite merge to totalTestCount: 226, passedTests: 226, and the unchanged xctest-run-summary.ts liveness assertion passes. The first full-suite run passed its assertions but stalled exporting diagnostics; a local rerun with -collect-test-diagnostics never completed. CI settings were not changed. CI/coverage is now green at 211f232.

The first hosted candidate cold sample is not green: job 101549746436 has no usable first-gesture measurement. Boot 1 hit a 5 s toolchain-query timeout during prepare; boot 2 rejected both gestures before synthesis because viewport was 134 × 291.33 while selector points were in the 402 × 874 coordinate space; boot 3 failed selector resolution. Boot 2's post-relaunch gesture did eventually commit, with a 13.6 s post-command observation wait and a 10.45 s empty preparation.

This does not establish the empty preparation solves cold input. The exact-source baseline and second candidate sample are still running. Downstream iOS 27 fallback passed, while the iOS 26/native contract lanes are still pending. I am examining the pre-synthesis blockers without folding them into the warm-up claim.

@thiagobrez

Copy link
Copy Markdown
Contributor Author

Two further corrections from the recordings/logs:

  • The supposed public-XCTest tap control (press id="engine-fallback") actually uses private synthesis in both 0.20.10 and the candidate: kind=coordinateTap ... fallbackAttempted=false. It cannot discriminate private from public input delivery. I have withdrawn that claim in the downstream report and renamed the current probe fields to selector press.
  • Candidate sample 1 / boot 3 had the URL confirmation sheet covering the app. Setup ignored an alert-handler timeout and matched background text. The repro now treats that timeout as setup failure and stops that attempt immediately; only an explicit absent-alert result is tolerated.

Neither correction turns the hosted candidate red into green. The iOS 27 fallback and native downstream contracts passed; iOS 26 and the remaining cold runs are pending.

@thymikee

thymikee commented Sep 6, 2026

Copy link
Copy Markdown
Member

The nightly selection fix looks good at 211f232. It runs the isolated regression and retains the full-suite count check; the reported local merged result contains all 226 passing tests. The remaining blocker is comparable cold-start evidence: the first hosted candidate sample does not establish that preparation fixes the original issue. Keep this draft until that comparison is usable. Android smoke is still red and iOS smoke is pending; the changed nightly workflow also needs a hosted run.

@thiagobrez

Copy link
Copy Markdown
Contributor Author

The requested hosted nightly is running at 211f232: 34060558728. The iOS smoke retry has passed fixture compilation and the targeted XCTest regressions and is progressing through replays. Android main is also red (at a different smoke assertion); I started one unchanged Android rerun and am retaining the failures.

All four downstream device lanes, consumer/runtime checks, and parity passed in 34056536337. iOS 26 used its existing one whole-job retry after the first attempt failed the pre-drag scenario wait. The optional final npm dry-run then failed on the already-published 1.0.0 version. A fresh complete matrix is running at the latest source SHA through the existing validation-only channel: 34060592647.

Cold comparison is still not usable as a green claim. Exact base bd08e6 produced two unobserved first drops and one delayed observation. Candidate sample 2 had one prompt first observation, one delayed observation, and one falsely classified “lost”: its recording clearly shows the committed drop, while the observer failed with an invalid viewport. The harness now separates observer errors from established target absence and rejects failed alert setup. Fresh same-harness runs are underway: base, candidate. Keeping the PR draft as requested.

@thiagobrez

Copy link
Copy Markdown
Contributor Author

The changed hosted nightly workflow passed at 211f232ee: isolated regression, remaining suite, merged results, and the unchanged source-derived count check (Executed 226 test(s); the source reaches 226 on this lane.). iOS smoke attempt 2 also passed.

Android attempt 2 repeats a pre-input selector failure. Its failed-step-34.png shows the Automation page scrolled beyond automation-press (the top visible input section starts at “Long presses: 0”). The immediately preceding step blindly scrolls down 0.7 after landscape/portrait rotation, then press id="automation-press" fails selector resolution. This points to smoke-test positioning, independently of the iOS bridge change. I am checking a bounded visibility-based setup correction while the fresh cold comparison finishes; no Android runtime changes or extra retries have been added.

@thiagobrez

Copy link
Copy Markdown
Contributor Author

The Android smoke positioning correction is isolated in #2369, with required local checks passing at 6211bfc3f. It preserves the input/alert/diff outcome assertions and has no runtime changes. Its hosted Android run is pending. #2362 remains unchanged at 211f232ee for the current cold-start comparison.

@thiagobrez

Copy link
Copy Markdown
Contributor Author

The separate Android smoke correction #2369 is now green at 6211bfc3f: Android live smoke, CI/coverage, integration and the remaining checks all pass. The previously failing automation-system scenario retains its input outcome assertions; only test positioning changed. It is ready for review. #2362 remains draft and unchanged at 211f232ee while the corrected hosted cold comparison finishes.

@thiagobrez

Copy link
Copy Markdown
Contributor Author

The corrected same-harness comparison finished; neither run passes validation. Downstream harness SHA is afdde2e0867beae448ec9784a641bbdd6cbdbde4.

Source / cold boot First gesture Second After relaunch
base bd08e6 / 1 unobserved, wait_target_absent prompt prompt
base / 2 command error command error unobserved
base / 3 prompt prompt prompt
candidate 211f232 / 1 invalid viewport runner busy observation deadline
candidate / 2 invalid viewport runner busy observed after 5.077 s wait
candidate / 3 prompt prompt prompt

Both candidate first-command rejections use viewport 134 × 291.33 with a selector point at 201,389. They occur before the preparation/real synthesis. Their recordings start with a black app surface even though setup's AX text checks succeeded. The later relaunches render the app. This is another readiness/coordinate-evidence problem, not evidence that the warm-up ran and lost those first two gestures.

Candidate boot 1's post-relaunch gesture is also concerning: empty preparation completes at 21:28:55.269 (elapsedMs=5232), then the real gesture returns ok at 21:29:06.037. The following wait makes 25 readable captures without the expected counter before a final truncated capture (wait_deadline_exceeded), so the conservative harness labels it observation-error. The recording's later visible app frames retain callback count 0 through the selector press; I have not found a committed drop. This must not be presented as a green warm-up result.

All three candidate preparation markers report success (~5.2–5.3 s). One clean candidate boot passes, but these data do not establish that empty preparation fixes the original symptom. Keeping this draft. I am tracing the viewport mismatch and the first actual synthesis after relaunch before another comparison. Is there an existing contact-free XCTest bootstrap that initializes the event-delivery machinery beyond an empty record? The current preparation does not attach a virtual digitizer in the retained traces.

@thiagobrez

Copy link
Copy Markdown
Contributor Author

The retained simulator logs narrow this further: the app does receive UIKit touch events in the unobserved candidate boot-1 relaunch gesture. That contradicts a complete “no touch stream reached the app” explanation for this sample.

Using timestamps from simulator-log.txt (not video PTS):

Gesture Virtual digitizer attached → first app Sending UIEvent type: 0 Logged app window dispatches before detach App outcome
candidate boot 1, relaunch 2,296 ms 4 no recorded drop
candidate boot 2, relaunch 33 ms 72 committed
candidate boot 3, first 32 ms 74 committed
base boot 1, first 1,293 ms 36 unobserved
base boot 2, relaunch 2,438 ms 2 unobserved

These are logged UIKit dispatches, not a count of every hardware sample or an exact touch-down delivery measurement. The delayed first app dispatch and reduced dispatch count are consistent with queued/coalesced delivery disrupting the scripted 650 ms source hold and 1,200 ms movement; that causal link still needs touch-phase/timestamp evidence. In the candidate failure, BackBoard logs attachment at 21:28:55.544, first contact at 21:28:57.028, and the app's first dispatch at 21:28:57.840. Empty preparation had already completed at 21:28:55.269.

This strengthens the reason to keep the PR draft rather than interpreting its successful empty record as demonstrated readiness.

@thymikee

thymikee commented Sep 6, 2026

Copy link
Copy Markdown
Member

The completed comparison does not establish the fix at 211f232. As reported, neither baseline nor candidate passed validation, and one candidate gesture still produced no observed drop after successful empty preparation. Keep this draft until a controlled reproduction demonstrates the intended improvement; successful preparation alone is not evidence of input readiness.

@thiagobrez

Copy link
Copy Markdown
Contributor Author

The iOS 26 versus iOS 27 comparison deserves to be explicit, because it is the starting observation for this report.

The closest control is iOS 27 fallback, not just iOS 27 native: iOS 26 auto-selects the fallback engine, while the iOS 27 fallback lane explicitly selects that same engine. They run the same RN 0.85 Scenario Lab, public-outcome scenarios, semantic drag targets/timings, and Agent Device revision. In completed matrix 34060592647, Agent Device is 211f232ee85df5742e3be093a008b46f80a6e1a7.

Lane Final result
iOS 27 fallback Passed
iOS 27 native Passed
iOS 26 auto-fallback Failed both preparation attempts: 5-second xcrun --show-sdk-version / --show-sdk-build-version timeouts

That final iOS 26 failure corrects my earlier interim progress report. It is a toolchain-query/preflight failure, not a missing-touch measurement. Separate retained iOS 26 cold runs demonstrate unobserved app outcomes, delayed UIKit dispatch and viewport errors, as detailed above. iOS 26 also has passing runs; this is intermittent.

This is not an OS-only controlled experiment: iOS 26 uses the macos-26 runner with Xcode 26 and iOS 26.5; iOS 27 uses xcode-27 with Xcode 27 and iOS 27.0. The completed jobs also report different host macOS versions (26.6.2 versus 26.5.2). The result localizes the observed reliability difference to those environment combinations; it does not yet prove which layer causes it.

The subsequent touch-phase diagnostic run collected no measured gestures: two preparation errors and a setup timeout. It was observation-only, so its green workflow status is not a successful fix validation. The proposal remains draft, consistent with the latest maintainer feedback.

@thiagobrez
thiagobrez force-pushed the fix/ios-synthesized-input-cold-start-warmup branch from 211f232 to 84af5b1 Compare September 8, 2026 08:37
@thiagobrez

Copy link
Copy Markdown
Contributor Author

Rebased onto main 527a56a6e703297783a9c14b0de0abbf639aeb0f; the PR head is now 84af5b152af8f2f07169e505d88842dd2938f584. Main now includes #2325's cold-boot budget correction, #2329's app observation/runner-demand changes, and the merged Android smoke correction #2369. The preparation implementation is unchanged.

pnpm install --frozen-lockfile, pnpm build, pnpm check:affected --run, and clean-install package validation passed locally at this head before publication. The changed hosted nightly and downstream full matrix are running.

A fresh comparison uses the same diagnostic app/harness revision for main baseline and candidate, three cold boots each. A temporary app probe records first observed touch phase, first movement, termination, event/arrival timing, target ancestry and recognizer state to distinguish delayed/coalesced delivery from wrong targeting or recognizer readiness. It is isolated from the downstream production PR. The prior diagnostic attempt never reached gestures, so it provides no touch-phase evidence.

Keeping this draft as requested. The previously failed comparison remains failed evidence at its original commits; this rebase is not a fix claim.

@thymikee

thymikee commented Sep 8, 2026

Copy link
Copy Markdown
Member

The rebase at 84af5b1 preserves all five commits without changes; no new code findings. The controlled cold baseline and candidate runs, downstream matrix, and nightly validation are still running. Keep this draft until the comparison demonstrates the intended improvement in observed app behavior; successful empty preparation alone does not establish input readiness.

@thiagobrez

Copy link
Copy Markdown
Contributor Author

All active PR checks now pass at 84af5b152af8f2f07169e505d88842dd2938f584, including CI/coverage/integration and iOS smoke. The separately dispatched nightly also passed: isolated regression plus the remaining suite, with the source-derived assertion confirming 226 executed tests.

The rebuilt package passed a local cold boot with all three drag outcomes and the selector press observed (post-command drag observation waits 132–186 ms). The same-harness hosted baseline/candidate cold comparison is still running, as is the downstream matrix. Keeping this draft: these completed checks do not replace that controlled behavior evidence.

@thiagobrez

Copy link
Copy Markdown
Contributor Author

The fresh main baseline 34205356736 completed at 527a56a6e: all three cold boots failed preparation before any gesture. Every attempt reports apple_toolchain_probe_unavailable for xcrun --sdk iphonesimulator --show-sdk-version timing out after 5,000 ms; the cache-prewarm warning and subsequent startup both encounter it. The simulator boot itself completed each time. No touch-phase samples were measured, so this is not a usable behavioral baseline.

The candidate comparison is still running. I have started a small independent host probe to time the exact toolchain commands before/after cold boots and on immediate repeats, retaining selected Xcode and actual outputs. It omits our app build and Agent Device so it can distinguish slow valid toolchain queries from environment selection or wrapper behavior. No production retry, cache fallback or timeout change has been made.

On the local Xcode 26.6 host the queries succeed in milliseconds. The recurring hosted preparation failure needs resolution before this baseline/candidate comparison can establish anything about the proposed gesture preparation.

@thiagobrez

Copy link
Copy Markdown
Contributor Author

The candidate run 34205555155 completed and fails validation. The touch-phase probe finally captured a discriminating failed/successful pair in cold boot 2:

Measurement from the app probe First gesture, no recorded drop Second gesture, committed
Touch-down event timestamp → app arrival 3,656.4 ms 18.4 ms
App arrival gap: down → first observed move 46.8 ms 661.4 ms
Event timestamp gap: down → first observed move 3,683.0 ms 666.7 ms
First observed movement (201,389) straight to destination (201,587) (201,719) to (201,716.3)

The failed touch targets ReorderableContentView#card-card-0, as intended. At down, RNBetterPanGestureRecognizer is present in possible state; it is absent from the next observed move's recognizer list. The successful follow-up reports that recognizer in changed state on its first move. The app uses a 350 ms activation hold; the script provides 650 ms before movement. These data support delayed/coalesced delivery collapsing the hold as experienced by the recognizer in this sample, rather than wrong targeting or complete non-delivery. They do not yet identify which upstream queue causes the delay.

Empty preparation had succeeded at 09:03:03.628 (elapsedMs=9095) before this failure. The first gesture returned success; its wait made 21 readable captures without the expected counter before a final truncated capture, so the classifier remains observation-error. The recording keeps callback count 0 until the second gesture commits. This is direct evidence that the current preparation does not prevent the observed timing failure.

Other results must remain separate:

  • Boot 1: first/second commands fail viewport observation; post-relaunch drop commits, with a late observation.
  • Boot 3: first gesture's touch timing is normal and the recording shows its commit, but observation returns after ~61 seconds. The second gesture has down then cancellation at the source, no logged move, and wait_target_absent; it is a different dispatch pattern. Relaunch commits.
  • All selector presses pass.

The baseline failed all three preparations, as reported above, so there is still no usable red/green comparison. The proposed empty record is insufficient on its own. I am preserving these artifacts while resolving the separate SDK-query preparation blocker; the PR remains draft.

@thiagobrez

Copy link
Copy Markdown
Contributor Author

The independent host probe reproduced severe toolchain slowness without Agent Device or our app. It runs the exact commands directly through Python subprocesses on macos-26, before and after two fresh iOS 26.5 boots.

xcodebuild -version takes 257 ms before boot; after cold boot it times out once with a 30-second subprocess timeout, then succeeds on an immediate repeat in 29.6 seconds. After the second boot, the complete subprocess call returns successfully after 52.6 seconds. That last elapsed time also shows why process-creation time must be separated from the subprocess communication timeout; it is not a reliable 30-second wall budget. Every successful result names Xcode 26.6 / 17F113. SDK queries return 26.5 / 23F81a. On the local host, the same two-boot probe's Xcode queries stay below 340 ms and SDK queries below 18 ms.

This establishes a slow valid toolchain operation outside the Agent Device wrapper, rather than proving every earlier SDK failure has the same cause. I am comparing process-creation timing and host resource snapshots on the iOS 26/27 runner images next. No production timeout/cache change has been made. The probe workflow's success means evidence collection completed, not that every query met its timeout.

@thymikee

thymikee commented Sep 8, 2026

Copy link
Copy Markdown
Member

The rebase at 03fec1c preserves the reviewed input proposal; no new code findings. Keep this draft until a controlled cold-boot comparison shows that empty-event preparation improves the missing first reorder. The new nightly failed on the Xcode SDK-version probe before XCTest ran, so the 228 selected tests are not a passing execution result; that setup failure appears unrelated to this change.

@thiagobrez

thiagobrez commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Continuing at 03fec1c0a and keeping this draft as requested.

The completed downstream matrix on merged main 78cfc4505 passed iOS 27 fallback, Android and all consumers. iOS 26 reached no gestures: initial capture failed in attempt 1; in attempt 2 the alert resolver's own main-thread query timed out, without the duplicate routing probe removed by #2398. The iOS 27 native failure was the retained Detox ios-inactive outcome assertion, not an Agent Device pointer result.

The main-run-loop diagnostic on 64de917 did not capture the delayed-down failure: all five synthesized drags had prompt down delivery (8–48 ms to application entry); four callback assertions passed and one observer errored. Correction: video inspection shows the second gesture inherited the first gesture’s callback after that observer error, so its apparent pass is invalid. The earlier timing measurements remain valid; follow-up outcome counts require this caveat. Other boots/steps failed setup or relaunch. Its wake/sleep timestamps therefore cannot distinguish the cause of the earlier delayed event. The preceding single-link run had only one usable boot (three gestures and selector passed); the other two failed Xcode preparation.

For this exact PR head, the isolated regression plus 82 remaining native PR tests passed. Live iOS smoke failed locating automation-longpress after scrolling, and nightly stopped before tests on an SDK-query timeout. Both failures remain recorded; one rerun of each is pending.

New matched cold comparisons use the same app, single-link setup and run-loop probes: base 78cfc4505 and candidate 03fec1c0a, three erased boots each. Neither run is evidence of improvement until the app outcomes and timing are available.

@thiagobrez

Copy link
Copy Markdown
Contributor Author

All PR checks at 03fec1c0a now pass, including the iOS smoke rerun; nightly rerun executed and passed all 228 selected tests. The first failed attempts remain recorded.

I also found and corrected a diagnostic oracle error. In run 34225370081, boot 1's first gesture physically committed, but its observer returned invalid-viewport. The harness kept its observed callback count at zero; the next gesture then incorrectly passed against the first gesture's existing Callback count: 1. The recording shows no second reorder. This does not invalidate the first-gesture delayed-down traces, but follow-up success counts after an observation error cannot be trusted without independent evidence.

Diagnostic correction: observe Last committed event for the specific source (card-0 or card-5) instead of deriving expected counts from previous observation success. No input timing or additional polling changed. Local live negative/positive verification: after card 0 committed, the old callback query passed while the card-5 query returned typed wait_target_absent; after an actual card-5 gesture, the new query passed. The currently running matched pair still uses the old harness, so I will retain that attribution caveat for follow-ups.

Separately, our downstream PR now pins published agent-device@0.21.0: inspection of the actual npm tarball confirms #2398 and typed alert absence, although the older Git tag lacks #2398. Both local setup paths and three cold gestures plus selector press passed with that package. Full downstream CI on the default published pin is running.

@thiagobrez

thiagobrez commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

The matched comparison has completed; both jobs failed overall, so this remains draft.

  • Base 78cfc4505: all three boots reached a first gesture. Boot 1 reproduces the compressed hold on the correct card: touch-down timestamp 1190.984783, application entry 1191.451471 (466.7 ms late), window entry another 0.4 ms later; first movement arrived only 215.6 ms after down, with the pan recognizer still state 0. The recording shows no reorder. Its wait had 21 readable misses, then a final capture deadline error, so the harness correctly retains an observation-error verdict rather than a clean absence claim. Later boots show normal first-touch timing, with capture, busy-runner and relaunch errors also present.
  • The new main-run-loop probe adds a useful boundary: boot 1's last sleep was 1190.989161, last wake 1191.000595, and event entry followed 450.9 ms after that wake. The recorded wake boundary precedes most of the delay. This common-mode observer cannot rule out unobserved mode changes or distinguish main-thread work, host descheduling, and input processing before UIApplication.sendEvent.
  • Candidate 03fec1c0a: only boot 2 reached gestures; all three plus selector press passed. Down-to-application was 9.5/15.3/16.6 ms, holds 680/666/665 ms. Empty preparation took 5.217 s. Boot 1 failed the SDK-version query; boot 3 failed alert handling and then initial-outcome observation. This is one usable boot, insufficient to establish improvement over the baseline's intermittent failure or overturn the earlier candidate failure.

No runtime change has been added on the strength of these results. The published-package downstream matrix continues; the PR's own checks and 228-test nightly are green.

@thiagobrez

Copy link
Copy Markdown
Contributor Author

Downstream restoration now has a complete passing matrix with published agent-device@0.21.0, without this warmup proposal: run 34288702857, downstream head 6d1b8a53da64f423929be6f15f658a7b96502550.

The iOS 26 lane passed on attempt 1 (all five Agent Device pointer scenarios plus retained contracts); both iOS 27 lanes, Android, consumer/minimum-runtime checks, 16 shared outcomes across four configurations, and the exact-candidate gate also passed. The npm package contains the merged alert-routing fix from #2398.

This is evidence that our restored workflow can pass on iOS 26. It does not supersede the failed matched cold-boot comparison or demonstrate an improvement from empty-event preparation. Keeping this draft as requested.

While investigating the remaining dispatch delay, I found and separately fixed a profiler process-selection bug in #2406: a resolved executable path could still fall back to the basename and include another simulator’s app. The live two-simulator check selects only the requested process after that fix; trace stop still timed out, so there is no successful CPU trace to claim yet.

@thiagobrez

Copy link
Copy Markdown
Contributor Author

The focused run with the corrected per-drag assertion executed zero gestures across three cold boots. Boots 1–2 failed the 5-second Xcode/SDK version probes. Boot 3 recovered through alert timeouts, then failed waiting for the initial order.

The new failure screenshot made boot 3 more informative: the scenario and all six cards were visibly present, but the app itself displayed “Order: Seeded initial order” (iteration-3/failure.png in the run artifact). This case was not merely an accessibility capture missing an accurate outcome.

I reproduced a downstream app bug: resetting a pristine scenario or opening the same deep link clears the parent’s outcome, but unchanged child state does not publish it again. Both regression tests failed with that exact placeholder; resetting after an actual reorder already passed. Downstream fix a6951c2 gives resets a fresh scenario instance. All 17 example tests, lint, typecheck, and the Release build pass. Live iOS 26.5 with published 0.21.0 passes repeated links, pristine Reset, a real drag, and post-drag Reset.

The next candidate run uses this app fix and the corrected assertion. The warmup proposal is unchanged; no new delivery result or improvement is established yet.

@thiagobrez

thiagobrez commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

The latest candidate still reproduces a missed first reorder after fixing the downstream reset bug: run 34295525695, candidate 03fec1c0a5cfe621bf146f0f8c5b7b3ee045324e, app/harness 61c55b6172c88751199f806ebcbd2c0d033278c6.

All three cold boots reached gestures. Nine gesture commands returned success; seven committed-source outcomes were observed, and two waits ended with observation errors. All three selector presses passed.

Boot 3 provides the counterexample:

  • Empty preparation completed successfully in 5.237 seconds.
  • The first down targeted the correct card-0 but reached UIApplication.sendEvent 809.6 ms late; window dispatch added only 0.6 ms.
  • The first observed move reached the window 47.9 ms after down (its event timestamp was 850 ms after down's timestamp). The pan recognizer did not appear in that move's recognizer list.
  • The recording visibly retains the initial order, Last committed event: None, and callback count 0 after the gesture. The automated wait had ten readable captures and one capture deadline, so its verdict remains observation-error rather than a clean absence assertion.
  • Relaunch restored normal timing: down 8.5 ms late, approximately 663 ms before movement, and a confirmed reorder.

The system log also records that same touch timestamp (2152.91908) in BackBoard at uptime 2152.92750, about 8.4 ms later. Most of the delay therefore follows system input arrival and precedes application event entry. This does not yet distinguish system routing from app-thread work or descheduling.

Boot 2's “late” classification refers to a 3.573-second observation wait; its physical input timing was normal. This is separate from boot 3's delayed down.

A matching baseline run is running with the same updated app/harness. Keeping this draft: successful empty preparation does not prevent the original failure.

@thiagobrez

thiagobrez commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

The controlled comparison now has three usable cold boots on both revisions, using the same app/harness 61c55b6172c88751199f806ebcbd2c0d033278c6 and hosted iOS 26.5 configuration:

  • Baseline 78cfc4505: PASS. All nine drag outcomes and three selector presses were observed. First-down application delays were 250.2 / 20.2 / 24.0 ms; the corresponding arrival holds were 425.7 / 657.8 / 661.8 ms, with the pan recognizer active on the first observed move in each boot. The first boot therefore still had noticeable delay, but enough hold remained for activation.
  • Candidate 03fec1c0a: FAIL. Seven of nine drag outcomes observed; boot 3 missed the first reorder despite successful preparation, with an 810 ms down delay and 48 ms arrival hold. See the recording and boundary evidence.

These small samples on separate hosted machines do not establish that preparation causes a regression. They do provide a usable comparison with no demonstrated benefit, plus a direct counterexample. This remains draft.

Separately, the complete downstream matrix passes at the current app-fix commit with published 0.21.0, without this proposal. It required one failed-job rerun; iOS 26 then used its retained infrastructure retry after an alert timeout.

Apple's sample successfully captured readable stacks from our owned local simulator, including during three successful cold-run gestures. An optional diagnostic run now captures app stacks around input to investigate the delay between system input arrival and application dispatch. Sampling adds diagnostic overhead; this run is not an uninstrumented performance comparison.

The missed activation is consistent with the recognizer implementation: our fallback config requires 350 ms. RNGH 3.0.0 starts its timer in interactionsBegan and cancels pending activation after movement exceeds 10 points. Candidate boot 3's first observed move was 29 points only 47.9 ms after down arrived. This explains the missed drag; the cause of the preceding delivery delay remains unresolved.

@thiagobrez

Copy link
Copy Markdown
Contributor Author

The first stack-capture run did not yield usable stacks.

Boot 1 stopped during scenario setup on a main-thread timeout. Boots 2–3 reached all six gestures; five committed outcomes and both selector presses were observed. Boot 2's first reorder is visibly missing in the recording: preparation completed in 5.223 seconds, down reached application entry 1,891 ms late, and the first observed move followed about 329 ms later. Its outcome observation failed on typed invalid-viewport, so that failure is kept separate from the recording/touch evidence. Boot 3's 3.602-second “late” result was observation latency; its down delay was only 13 ms.

Both sampler processes finished their 20-second collection but exceeded the independent 45-second limit while processing symbols, producing no report. In boot 2, collection also ended before the actual first down because the gesture command took about 71 seconds overall. This instrumented run therefore cannot locate the blocking stack or establish uninstrumented timing.

The next diagnostic run uses a 60-second collection window and a bounded 180-second completion allowance, keeping the first app instance alive until its report is written. It also records whether the report contains actual stack samples. A local cold run passed all three gestures plus selector press and produced a report with real main-thread stacks. This changes diagnostic overhead and follow-up spacing only; the passing production workflow and input implementation are unchanged.

@thiagobrez

thiagobrez commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

The corrected capture run produced usable app stacks in two failing cold boots at candidate 03fec1c0a5cfe621bf146f0f8c5b7b3ee045324e (diagnostic app/harness 361f565692bb43bc9f75a3d35391d4fad0a51351).

  • Boot 1: successful 5.218-second preparation; down 592.4 ms late at application entry, then first observed movement only 315.7 ms later. The recording retains the initial order and zero callbacks. Observation had 35 readable captures plus one deadline error.
  • Boot 2: successful 5.218-second preparation; down 861.0 ms late, then movement 11.3 ms later. The recording again shows no first reorder. This time the oracle returned wait_target_absent with 30/30 readable captures, a clean absence result.
  • Both second gestures and both relaunch gestures committed. Boot 3 stopped during setup; it measured no gestures.

Both main-thread call graphs contain this pre-application-dispatch stack:

__dispatchPreprocessedEventFromEventQueue
  _AddTouchToEventAndDetermineIfNeedsCancel
    -[UITouch _effectivelyAuthentic]
      _os_feature_enabled_SLOWPATH
        _os_feature_enabled_load
          pread

The pread leaf has 45 samples in boot 1 and 98 in boot 2. Each collection covers the first gesture and its observation; the second gesture starts only after that report finishes. Evidence is in iteration-{1,2}/app-stacks.txt, app-stacks.json, simulator-log.txt, and screen.mp4 in the run artifact.

This points to synchronous feature-flag file I/O in UIKit's touch preprocessing as a concrete lead. These are aggregated stacks from an instrumented run, so the counts are not exact delay measurements and do not identify the file or establish sole causation. The next useful probe is the read's path and duration. No feature settings or input-validation behavior have been changed; the empty-event proposal remains unproven and draft.

An app-scoped file-trace run is now running on diagnostic revision 25b06ecd853b8e9942bbb1125ff6c386cdbb2622, with the same candidate and three cold boots. It runs a bounded 60-second fs_usage capture against the exact owned app PID. Local validation confirmed PID scoping and the stack mode; file tracing reported the local sudo privilege restriction, so usable file output still needs verification on the isolated CI runner.

@thiagobrez

thiagobrez commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

The file-trace run identifies the read behind the earlier stack lead. Candidate 03fec1c0a5cfe621bf146f0f8c5b7b3ee045324e, diagnostic app/harness 25b06ecd853b8e9942bbb1125ff6c386cdbb2622, three cold boots on the same hosted iOS 26.5 runner.

All three exact-app-PID captures succeeded with actual file events. All nine gesture commands returned success; eight committed outcomes and all three selector presses were observed. Boot 1's first reorder was lost with 30/30 readable captures (wait_target_absent); its recording retains the initial order, Last committed event: None, and callback count 0. Both followups and all gestures in boots 2–3 passed.

In boot 1, the app's main thread (dc61 in the app log = decimal thread 56417 in fs_usage) performs:

03:39:26.851174 open  F=4 …/System/Library/FeatureFlags/Domain/UIKit.plist
03:39:26.851175 lseek F=4 <SEEK_END>
03:39:27.990809 pread F=4 B=0x2b2b  elapsed=1.139634 seconds
03:39:27.991263 close F=4

The trace therefore connects the same descriptor and main thread to the runtime's UIKit.plist, rather than inferring a file from a symbol name. The 11,051-byte read consumes about 1.14 seconds immediately before application touch entry. Down arrives 1,168.5 ms late, and movement follows only 22.0 ms later, with no pan recognizer in that move. The app's down log at 03:39:28.022 is printed after its measured 20.5 ms handler. Empty preparation had already succeeded in 5.210 seconds.

The corresponding main-thread reads in successful boots 2 and 3 take 21 µs and 8 µs. Their first-down delays are 17.2/21.2 ms, with 661.7/666.1 ms arrival holds and active pan recognition.

Together with the previous UIKit preprocessing stacks, this localizes nearly all of this failure's delay to the runtime feature-file read. It still does not explain why that read is slow, or prove a portable agent-device fix. These are instrumented observations; no feature values, validation behavior, or input timings were modified. Evidence: iteration-{1,2,3}/app-files.{json,txt}, simulator-log.txt, iteration.json, and screen.mp4 in the artifact.

The next run tests a diagnostic-only, read-only prefetch of that exact runtime file. If it removes both the slow app read and missing first reorder, that supports a host file-cache/readiness cause; it would not validate the current empty-event proposal. Keeping this draft.

@thiagobrez

thiagobrez commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

The first read-only prefetch control is inconclusive, not a passing comparison.

The host read the exact 11,051-byte runtime UIKit.plist in 0.813 ms before the cold boots. Its SHA-256 matches the local file (dcd3b318b1963094f404a5a2947f0dbe297157e093180961622c6c7dfb7d9e42).

  • Boot 1 stopped on the five-second Xcode SDK-version queries before any gesture.
  • Boot 2 passed all three drags and selector press. The first down reached app entry 30.2 ms late, with a 645.2 ms arrival hold and active pan recognition. The main-thread UIKit file read took 12.109 ms.
  • Boot 3's recording visibly shows the first drag committing card-0 before card-3, with callback count 1. The automated observation failed on invalid-viewport, followed by runner/capture failures. Its file trace contains a 13 µs UIKit read, but I cannot correlate that thread to an application-entry probe: follow-up failures lasted long enough that the final fixed eight-minute log query excluded the first input.

The diagnostic now collects logs from five seconds before the first gesture, using the native log --start argument, rather than an expiring lookback. Local native CLI acceptance and script checks pass.

The next control, harness 1f5a771b0a75165da1348def6a9060a714a7ed1d, tests one first cold boot on each of two fresh hosted runners, with prefetch and file tracing. This avoids counting later boots on an already-used host as independent file-cache evidence. Input implementation and production workflows remain unchanged; the proposal stays draft.

Update: both fresh-runner controls passed: six drag outcomes and two selector presses. First-down delays were 170.8 / 81.1 ms; arrival holds were 517.8 / 601.7 ms, with active pan recognition in both. Host prefetch took 1.102 / 1.826 ms. Sample 1's app-process discovery failed, so it has no file trace. Sample 2 captured the same main-thread UIKit read at 51.476 ms (PID 38660; app thread 18b40 = trace thread 101184). Successful prefetch therefore does not eliminate all delay in that read.

The matching no-prefetch controls now run on the same harness and input commit, again one cold boot on each of two fresh hosts. These small, separate-host samples do not yet establish causality or validate an upstream implementation change.

The no-prefetch run’s first attempt measured zero gestures on both hosts: sample 1 failed the five-second SDK build-version query; sample 2 rebuilt the runner but failed to connect with dyld: Library not loaded: /usr/lib/libcurl.4.dylib. I have requested one failed-job rerun. These setup failures provide no evidence for or against prefetching.

Final matching-control result: the one allowed rerun still had an SDK-query setup failure on sample 1, but sample 2 passed all three drags and selector press without prefetch. Its first-down delay was 39.7 ms, arrival hold 647.9 ms, and pan state active. The captured main-thread UIKit.plist read took 9 µs (PID 34324; thread 16245 = 90693). Thus only one of four no-prefetch host attempts reached gestures; the others are setup failures, not missing inputs.

This comparison does not demonstrate a prefetch benefit. The usable no-prefetch control was faster than the two prefetched controls. I am not promoting the file-prefetch experiment into production or treating it as an upstream fix. The identified 1.14-second read remains concrete failure evidence; why it sometimes stalls remains unresolved. No further rerun of these same controls is queued.

@thiagobrez

thiagobrez commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

The app-only kernel timeline captured the missing first reorder and localizes the wait inside AppleDiskImages2's request pool.

Candidate 03fec1c0a5cfe621bf146f0f8c5b7b3ee045324e, diagnostic harness be60635cc03edff0a81d606ced385d8860c27285, no file prefetch. Sample 2 stopped on the SDK-version query before gestures. Sample 1 produced a valid report containing only our app, PID 31628.

In sample 1, empty preparation succeeded in 5.215 seconds. The first down reached app entry 2,063.9 ms late, and the first observed move followed only 16.5 ms later, without the pan recognizer. The recording retains the initial order, no committed event, and callback count 0 after this gesture. Its automated observation failed on invalid-viewport, so the recording and timing evidence remain separate from that observation error. The second and relaunch gestures, plus selector press, passed.

Main thread 0x148f8 has 201 consecutive samples, indices 627–827, along this path:

UIKit touch preprocessing → -[UITouch _effectivelyAuthentic]
  → _os_feature_enabled_SLOWPATH → pread
  → apfs_vnop_read → cluster_read_ext
  → AppleDiskImageDevice::doAsyncReadWrite
  → DIDeviceRequestPool::GetRequest
  → IOLWPoolBase::waitForEntriesLocked
  → lck_mtx_sleep

The first 118 samples reach that wait through APFS block mapping/data-hash metadata lookup; the next 83 reach it through the data-read strategy.

Re-reporting the saved binary with spindump -i app-timeline.txt -indexRange 627-827 -onlyBlocked -heavy -noBinary -noFile retains all 201 main-thread read samples. The corresponding -onlyRunnable report has no main-thread stacks. The sampled interval spans 2.04 seconds, with less than 0.001 seconds of main-thread CPU time. Its priority is 47, user-interactive QoS, I/O tier 0. This is a blocked disk-image I/O wait, not evidence of React Native CPU work.

The report starts at 05:59:34.268 UTC; the selected interval starts about 6.34 seconds later and ends about 8.38 seconds later. App down is logged at 05:59:42.669 after its 15.8 ms handler, aligning the wait with the delayed input.

This timeline does not record the filename. The separate file trace identified UIKit.plist during a similar failure. Why the disk-image pool is unavailable remains unresolved, and instrumentation adds overhead. No portable agent-device fix is demonstrated; keeping this draft.

@thymikee, does this disk-image request-pool wait suggest a supported preparation hook worth testing here? The current empty-record proposal still has no demonstrated benefit. I would like to avoid turning an OS-specific file-cache experiment into agent-device behavior without evidence that it reliably addresses this boundary.

@thymikee

thymikee commented Sep 9, 2026

Copy link
Copy Markdown
Member

The new timeline localizes the delayed first input to a blocked disk-image I/O wait, but it also reproduces the missed reorder after empty preparation succeeded. Green checks do not establish a benefit for this change. Keep this draft until a matched comparison demonstrates that the proposed preparation improves first-input delivery; the separate prefetch controls do not show that benefit.

thiagobrez and others added 6 commits September 9, 2026 14:06
XCTest attaches its HID digitizer lazily, on the first synthesized event a runner
process posts. On a cold or loaded simulator that attach can lag several seconds
behind the synthesizeWithError call that triggers it. When the first synthesized
gesture of a process is timed (a drag with an activation hold, a paced pan), the
touch-down then lands seconds into a window whose later samples were scheduled
relative to the intended touch-down, so the app reconstructs a malformed gesture
even though synthesizeWithError reported success. Once attached, the digitizer
stays attached for the runner process, so every later gesture — including after a
target relaunch — lands on schedule.

Force the one-time attach with a throwaway synthesized contact before the first
real gesture. The gesture's own timings are unchanged; the warm-up only moves the
unavoidable one-time attach cost off the first user gesture, and is a no-op on a
warm host. Runs once per runner process, before the first `gesture` command.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…t process

The process-scoped preparation flag forced its regression into its own
xcodebuild invocation on the PR and nightly lanes, with an xcresult merge
and a multi-invocation selection parser to keep the executed-count
guarantee. A test-only reset on RunnerSynthesizedGesture makes the test
self-contained instead, so it runs like every other runner unit test and
the workflows, selection script and its tests return to main.

Also drops the iOS-only guard (every synthesized route is already
`#if os(iOS)` in Swift) and the lock (commands run on a serial queue),
and names the preparation consistently in the log marker and test file.
@thymikee
thymikee force-pushed the fix/ios-synthesized-input-cold-start-warmup branch from 03fec1c to d3a651c Compare September 9, 2026 12:12
@thymikee

thymikee commented Sep 9, 2026

Copy link
Copy Markdown
Member

Took this over and force-pushed d3a651c, rebased on main fda41b4 (the five reviewed commits are preserved, one commit on top).

Kept: the contact-free preparation at the shared record boundary, the flag set only on success, and the swizzle-spy regression.

Changed: a test-only +resetSynthesizedInputPreparation on RunnerSynthesizedGesture makes the regression self-contained, so it runs in the shared test process like every other runner unit test. That removes the isolated xcodebuild invocation from ios.yml and nightly, the xcresult merge, and the multi-invocation parsing in check-xctest-selection.ts; all of those are back to main. Also dropped the TARGET_OS_IOS guard (every synthesized route is already #if os(iOS) in Swift) and the @synchronized (commands run on a serial queue). The log marker is now AGENT_DEVICE_RUNNER_SYNTHESIZED_INPUT_PREPARATION.

The PR body now states plainly that this does not fix the cold-boot missed reorder; the diagnosis in this thread places that delay in the app process. Diff vs main is 4 files, +141/−9.

@thymikee
thymikee marked this pull request as ready for review September 9, 2026 12:12
@thymikee

thymikee commented Sep 9, 2026

Copy link
Copy Markdown
Member

The simplified implementation and self-contained ordering test look sound at d3a651c. The remaining question is benefit: the recorded cold-boot miss still occurs after successful empty preparation. Please provide a matched comparison showing that moving setup ahead of the first timed path improves delivery or timing before making this preparation permanent. The iOS alert-test failure appears unrelated; the new preparation test passed.

@thymikee

thymikee commented Sep 9, 2026

Copy link
Copy Markdown
Member

On benefit: the matched data already in this thread answers the question, and the answer is that moving setup ahead of the first timed path does not change delivery or timing.

  • Setup latency cannot collapse a hold. The record's offsets are applied at playback, after synthesizeWithError: completes its own setup. The no-preparation baseline shows this directly: the first real record delivered 657.8 / 661.8 ms holds in boots 2–3 under the same ~5 s first-synthesis cost (baseline 78cfc45).
  • Preparation does not change first-touch delivery. Candidate boots with prompt delivery measured 9.5 / 15.3 / 16.6 ms down-to-application and 665–680 ms holds, indistinguishable from the baseline's 20.2 / 24.0 ms. Candidate boots that missed the reorder did so after a successful 5.2 s preparation, with 592–2,064 ms delays (timeline, file trace).
  • The delay is in the target process: UIKit's first-touch feature-flag pread of UIKit.plist blocked on disk-image I/O before application event entry. Nothing the runner synthesizes before that touch reaches that code.

So no matched comparison can show an improvement from this change. What the current head does is pay XCTest's one-time synthesis setup before the first timed record instead of inside it, at no added cost per command. Merging it rests on that alone, not on the cold-boot symptom.

On CI: the testAlertAcceptDoesNotActivateAReplacementWithASharedButton failure is unrelated. In that run the first preparation ran at 12:22:33 inside testPenalizedCoordinateTapOnNonTextControlDoesNotAuthorizeBareType, two minutes after the alert test failed at 12:20:35 with ALERT_DEADLINE_EXCEEDED and no button activated, and the alert handler never reaches the synthesized-record path. No other failed iOS run in the last two days shows that test failing. The failed job is re-running.

@thymikee

thymikee commented Sep 9, 2026

Copy link
Copy Markdown
Member

The rerun is green, and the new comparison settles the benefit question: moving synthesis setup does not improve delivery or timing. I would close this change rather than add permanent preparation solely to move that cost. The cold first-touch delay needs work in the target app path identified above.

@thymikee thymikee closed this Sep 9, 2026
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-09-09 15:46 UTC

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.

2 participants