Repository navigation
Conversation
On iOS every attribute read from a live XCUIElement re-runs its query, and each query snapshots the whole app (~90-110 ms on Bluesky's React Native Search screen). A fill read identity, frame, and value one attribute at a time and resolved the field again right before each post, so it made ~37 queries. - Resolution returns the snapshot it read; value, frame, identity, and the replacement baseline come from that snapshot. - The focus step hands its post-focus read to the entry, and a post reuses the last read when nothing was dispatched since it was taken. After a clear, the field resolves again under its bound identity. - The point lookup runs one query for all four text input types, reads each candidate through one snapshot, and still hands out per-type handles. Bluesky Search fill, runner type: 5.6-6.2 s -> 3.3-3.7 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Size Report
Startup median (7 runs, lower is better):
|
Two reuses from the one-snapshot change could pair a read with the wrong input: - The point lookup counted per-type positions across candidates read from a shared query that each re-ran it. An input that left the app between two candidate reads shifted the later positions, so a handle could name another input than the snapshot it carried. Each handed-out handle now reads as the candidate's identity and frame; on a mismatch the lookup runs the per-type queries, whose handles read through their own snapshots. - The warmup kept its read across the pause it takes for an unreadable value or an expired deadline, so the next post skipped the identity check and could type into a successor. Only a read that no pause follows is reused now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Both review findings are fixed in 6711463 (pushed on top of 8894c98, no force-push). P2-1 — snapshot attached to the wrong per-type handle. Fixed. The point lookup still reads candidates through the shared
P2-2 — warmup kept identity evidence across a sleep. Fixed. The warmup keeps its read for the next post only when it returns without pausing (a readable, non-pending value). Both the unreadable-value pause and the deadline-expiry path leave
Fail on 8894c98. I ran both tests against 8894c98's production code with only the two hooks inserted. Both failed for the intended reasons:
Both pass on 6711463. Validation at 6711463
Bluesky 1.136.0 Search fill,
Cost of the fixes: about +70 ms per fill, all in the focus phase (1048–1059 → 1115–1132 ms). That is the extra snapshot that validates the point-lookup handle. The by-type fallback never triggered on Bluesky. Two notes:
|
|
At 6711463, the point lookup in Each candidate in the shared Could the lookup use Not blocking, and you can take or leave it: for an unbound target, All 16 checks pass, including the Swift runner lanes that exercise the changed path, and there are no conflicts. The claims "fails on base 35 > 24" and "fails on 8894c98" are the author's, and I did not reproduce them. The Bluesky timings are also the author's, from an iOS 27.0 simulator, and there is no physical-device run. Before merge, the point lookup's shift check must cover every shared-query position, including vanished and non-matching ones, or the shared query must give way to the per-type lookup. |
The point lookup validated a handle only for a candidate at the point, after the point filter. An input of another type that left the app after an earlier position's read moved a later input into a position already read, so the input at the point was never read: the next position read an input elsewhere, which the filter dropped, and the last position no longer existed and was skipped. The lookup returned no input without falling back, so a resolved-coordinate fill failed with NO_TEXT_INPUT_AT_POINT and a coordinate tap could lose its typing witness. Every position now must still exist and every handle must read as its position's input, at the point or not; otherwise the lookup runs the per-type queries. A position's existence is read only when its snapshot fails, so a lookup reads each candidate twice, as before for the input at the point. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Round 2, P2 ( Finding. The lookup checked a handle only for a candidate at the point, after the point filter. Take Fix. The checks now run before the point filter, for every position:
Each candidate still takes two reads: the position snapshot and the handle snapshot. The Tests (
The helper also asserts that no XCTest failure was recorded. The test hook now runs after a position's handle check. On 6711463's
At bef8706, all three pass. Validation at bef8706
Timing. The hot path changed, so I re-measured. Bluesky 1.136.0, Search
There is no measurable difference. Absolute numbers aren't comparable with the PR table, because the host was under heavy load and the metric is different. 🤖 Generated with Claude Code |
The shared elementType IN query saved three queries per point lookup but needed index rebinding, a validation snapshot per candidate, and a fallback for every shift race. Per-type handles never shift across types, so the lookup now reads each type's query and snapshots each candidate once, reading existence only when the snapshot fails. Bluesky and test-app fills measured no slower. An unbound target that resolves through its refresh point now keeps the snapshot the point lookup read instead of reading the input again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Round 3: both points are answered in 2d34505 (fast-forward on top of bef8706). Can the lookup use the per-type queries alone? Yes, and it now does. I measured first. On one iPhone 17 Pro iOS 27.0 simulator, I compared two runners:
Runs alternated between the runners: 3 rounds per runner, each with 1 warm-up fill and 3 measured fills. Before each block, I stopped the daemon with
Logged-out Bluesky has no Explore or Search, and I did not sign a real account into a fresh simulator. The Sign in form is the Bluesky screen I measured. The per-type lookup now snapshots each candidate first. It reads This deletes:
The snapshot-per-resolution and undispatched-read parts stay. A same-type shift is still possible, as it is on main. Unbound snapshot pass-through: taken.
The new test is Validation
🤖 Generated with Claude Code |
|
The PR is ready. At 2d34505 the findings from the earlier review are fixed, and I found no new problems. Not blocking: the per-candidate read in RunnerTests+TextInputProbe.swift (line 182) takes the snapshot before checking exists, while ResolvedTextEntryInput(reading:) and textEntrySnapshot in RunnerTests+TextEntry.swift check exists first, so you could add a second named initializer (for example snapshot-first) to make the type own that choice, or leave it as is since both sites document the trade-off. I ran no XCTest or runner suites. The before-and-after numbers for the new regression test (5 vs 3) and the 19-value query budget are yours. The Bluesky and test-app timings come from your iPhone 17 Pro iOS 27.0 simulator, and there is no physical-device run. The same-type index shift on removal still exists, as it does on main, so it is not a regression. No conflicts. Lint, Typecheck & Package fails only on the line cap for packages/platform-apple/src/snapshot-route.test.ts (1003 lines). That failure comes from main and #3890 fixes it, and this PR's diff does not touch that file. Rebase onto main once #3890 lands so that check goes green, and then the PR can merge. |
* origin/main: feat(test-app): add a Bluesky-depth heavy feed screen (#3884) fix(android): read a child that vanished mid-read as gone (#3901) feat(ios): route iPad Simulator snapshots through the host AX bridge (#3895) Prepare shared resource ownership and selection for #3325 (#3887) refactor(kernel): track held execution locks in keyed-lock (#3879) perf(ios): ask a ready runner about surface hosts before spawning probes (#3877) fix(ios): keep the bridge while a surface host only lingers (#3873) perf(ios): synthesize taps on resolved in-app back controls (#3880) fix(macos): open a running app by its own path instead of its bundle id (#3889) fix(macos): accept mixed-case bundle ids in open (#3867) fix(macos-helper): scroll SwiftUI forms whose rows are scroll-area children (#3888)
Every resolver now returns ResolvedTextEntryInput, which owns the exists-first and snapshot-first reads. TextEntryReader hands a read out again until the next dispatch or pause, replacing the post-focus and undispatched reads tracked at each call site. The point lookup's snapshots answer identity, so its consumers stop probing candidates again. A fill reads the counted field 17 times instead of 19. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Append verification, the repair decision, and the repair itself read through TextEntryReader, so a verification starts from the read its check took and the repair dispatches through the reader. The clear's caret tap takes its frame from the read it clears. A read that raises logs its exception, and every input comes from a named read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Redesign: one read type, one owner for reuse. Two commits on top of 2d34505 (merged with main first, no force-push): 90fa513 and 737fa35. Design note
Replaces Deleted elsewhere. The point lookup's snapshots now answer identity, so Kept. Two pieces of the fixture stay:
Cost. Readiness's slow path, where focus was not confirmed, pays one extra snapshot. That is the price of a single resolver. Numbers
One test-app block for the new runner was discarded. Its Validation
🤖 Generated with Claude Code |
|
This PR is ready at 737fa35. The redesign since my earlier review at 2d34505 holds up: one snapshot per read, reused only until the next dispatch, and I found no code problem. All 16 checks pass, including the Apple Smoke Tests job, which only covers the iOS runner build with the changed text-entry files. Its XCTest lane ran 4 unrelated tests, so CI does not exercise the changed path. The 121-test pass, the 17-query count, the warmup-pause mutation and the test-app and Bluesky fills are your simulator results (iPhone 17 Pro, iOS 27.0), and I ran no runner or device myself. Not blocking, and fine to take or leave: |
Summary
Each attribute read from a live
XCUIElementre-runs its query, and each query snapshots the whole app. A fill made 35 queries. Now:ResolvedTextEntryInput(element + snapshot). It owns the exists-first and snapshot-first reads. Value, frame, identity, and the baseline come from that snapshot. The point lookup's snapshots also answer identity, so its consumers stop probing again.TextEntryReaderowns reuse:input()hands out the last read untildispatch { }runs, which wraps every post, tap, and pause. The focus step's post-focus read seeds it. A bound target refuses a replacement input after any dispatch.Read-back, unconfirmed evidence, identity refusal, the tap witness,
textEntryRoute, and the penalized route are unchanged. 12 runner files. The fixture gains a value-read counter and an on-demand input replacement.Validation
pnpm check:affected --run --head 737fa35cbc: passed,exactCommit: true. GitHub owns the Swift and smoke lanes.testFillFailsWhenAppReplacesMaskedInputWhileWarmupPauses(types23456into the successor).typemedian ms, interleaved, 2d34505 → 737fa35: test-appfield-email3483 → 3280; Bluesky Sign in 2910 → 2767.Risk: a fill can reuse a read only between dispatches; readiness's slow path pays one extra snapshot.
🤖 Generated with Claude Code