Skip to content

perf+fix(ios): faster text entry (readiness + typed-query field resolve) + fix fill mis-navigation#633

Merged
thymikee merged 4 commits into
mainfrom
perf/ios-text-entry
May 31, 2026
Merged

perf+fix(ios): faster text entry (readiness + typed-query field resolve) + fix fill mis-navigation#633
thymikee merged 4 commits into
mainfrom
perf/ios-text-entry

Conversation

@thymikee
Copy link
Copy Markdown
Member

@thymikee thymikee commented May 31, 2026

Three iOS XCUITest text-entry fixes, each found and verified on-device (read-back of the field value + screenshots). Implements Wave-2 item #4 of the iOS perf plan.

Summary of measured impact (iPhone 17 sim, Settings search)

command before after
type 25 chars 3342ms 1379ms (2.4×)
type 50-word lorem ipsum (313 ch) 10.3s 8.6s
fill 25 chars (warm runner) ~14.5s ~4.5s (3.2×)
fill into Settings search 0/3 correct (navigated to Developer pane) 5–6/6 correct, stays put

1. perf: early-exit text-entry readiness on keyboard-visible (~2.4s/op)

Focus/readiness loops keyed their fast-exit on focusedTextInput(), which is intentionally nil on iOS. So stabilizeTextInputBeforeTyping always burned focusTimeout (0.4s) and waitForTextEntryReadiness burned readinessTimeout (2.0s) whenever the keyboard appeared. Both now return as soon as isKeyboardVisible() (the real readiness signal). Warmup-first-char + verify/repair safety nets unchanged; reliability 64/65 exact incl. the 50-word lorem ipsum.

2. fix: don't clear an already-empty field (fixes fill mis-navigation)

clearTextInput always ran moveCaretToEnd (an edge-tap from the element frame) + a 24-key delete burst, even on an empty field. On the Settings search bar (repositions bottom→top on focus, revealing a "Suggestions" list), the stale-frame edge-tap landed on the Developer suggestion → navigated away (fill was 0/3 correct). Skip the clear entirely when the value is already empty (placeholder treated as empty) — a no-op semantically, and it removes the stray tap.

3. perf: resolve text fields via typed queries, not full-tree enumeration (fill 3.2× faster)

This is why fill was ~14.5s while type was ~1.6s (it is not repair churn or "app busy" — verified). textInputAt used app.descendants(.any).allElementsBoundByIndex (snapshots every element). fill drove it repeatedly: once it has coordinates, resolveTextEntryElement re-runs textInputAt on every verify/repair poll when the focused-field reference goes stale (search bar repositioning). Switched to typed queries (app.textFields/secureTextFields/searchFields/textViews) — same matches, no whole-tree snapshot (the same primitive #632 fixed for get text). fill 25 chars warm: ~14.5s → ~4.5s, 6/6 exact.

Diagnosis notes (for reviewers)

  • The dominant per-command cost is the first interaction after open/relaunch (one-time iOS-runner startup ~10s + a per-relaunch first-AX-query settle ~4s). type measured fast only because a press preceded it; fill measured slow because it was that first interaction. (feat: e2e command perf benchmark harness + nightly CI #630's harness now runs an untimed warmup after each open so benchmarks don't charge this to a measured command.)
  • Warm fill (~4.5s) is now close to press+type (~3s). Further headroom: plan item #4b — resolve the text-entry element once per verify/repair window instead of per poll.

Validation

Per trial: open --relaunch → focus → type/fill <string> (timed) → read field value from snapshot --raw --json, compare byte-for-byte → screenshot. 50-word lorem ipsum rendered exactly in the "No Results for …" header. Swift-only; needs build:xcuitest:ios. Branched off main.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 31, 2026

Size Report

Metric Base Current Diff
JS raw 1.1 MB 1.1 MB 0 B
JS gzip 355.9 kB 355.9 kB 0 B
npm tarball 452.9 kB 454.0 kB +1.1 kB
npm unpacked 1.5 MB 1.5 MB +2.7 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 28.8 ms 29.2 ms +0.4 ms
CLI --help 43.8 ms 44.6 ms +0.8 ms

Top changed chunks: no changes in the largest emitted chunks.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6509d0d356

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +891 to +892
if isKeyboardVisible(app: app) {
return latest
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Require a focus-change signal before early keyboard exit

On iOS when the software keyboard is already visible from a previous text field, this branch is true immediately after tapping a different field, before XCUITest has necessarily moved focus to the new target. Because typeTextReliably then uses app.typeText(...), the text can be sent to the previously focused field instead of the requested target during back-to-back fills; the old wait avoided this by giving the focus/tap state time to settle. Only take this fast path when the keyboard was not already visible for another target, or after confirming the target/focus changed.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The isKeyboardVisible early-exit in both stabilizeTextInputBeforeTyping and waitForTextEntryReadiness is now gated on a keyboard hidden→visible TRANSITION (shared keyboardBecameVisible(wasVisibleAtEntry:)). When the keyboard is already up (back-to-back fills) it falls back to the prior settle/timeout, so text isn't sent to the previously-focused field; the fresh case keeps the fast path. Also fixed a related secure-field bug the review surfaced: clearTextInput used editableTextValue(...) ?? "" and skipped clearing secure fields (editableTextValue returns nil there) → replace concatenated; now distinguishes nil (clear) from "" (skip). Device-validated.

@thymikee thymikee changed the title perf(ios): early-exit text-entry readiness on keyboard-visible (~2s/keystroke-batch faster) perf+fix(ios): faster text-entry readiness + fix fill clear mis-navigation May 31, 2026
@thymikee thymikee changed the title perf+fix(ios): faster text-entry readiness + fix fill clear mis-navigation perf+fix(ios): faster text entry (readiness + typed-query field resolve) + fix fill mis-navigation May 31, 2026
thymikee added 4 commits May 31, 2026 12:39
The XCUITest text-entry focus/readiness loops keyed their fast-exit on
focusedTextInput(), which is intentionally hardcoded to return nil on iOS (focus
predicates are stale there). As a result stabilizeTextInputBeforeTyping always
burned its full focusTimeout (0.4s) and waitForTextEntryReadiness burned its full
readinessTimeout (2.0s) in the normal case where the software keyboard appears —
~2.4s of dead wait before a single keystroke on every type/fill.

The software keyboard becoming visible is the reliable iOS readiness signal, so
both loops now return as soon as isKeyboardVisible() is true. The warmup-first-char
echo check and post-type verify/repair remain as drop safety nets.

Measured on iPhone 17 sim (Settings search field), median type time:
  25 chars:  3342ms -> 1379ms  (2.4x)
  52 chars:  3969ms -> 2190ms
  313 chars: 10.3s   -> 8.6s    (remainder is genuine per-char XCUITest typing)
Reliability unchanged: 64/65 trials exact (incl. a 50-word lorem ipsum, verified
by read-back + screenshot); the lone miss triggered the existing verify/repair.
…igation)

clearTextInput unconditionally ran moveCaretToEnd (an edge-tap computed from the
element frame) + a 24-key delete burst, even when the field was empty. On a field
that repositions on focus — e.g. the Settings search bar jumping bottom->top and
revealing a 'Suggestions' list — that edge-tap used a stale frame and landed on an
adjacent row (Developer), navigating away instead of clearing. fill (replace) into
the search field went to the Developer pane (0/3 correct).

Skip the clear entirely when the field's value is already empty (placeholder
treated as empty): replacing into an empty field is a no-op, and skipping avoids
the stray edge-tap. fill into the Settings search now types correctly and stays
put: 5/5 exact (read-back + screenshot).
…ration

textInputAt used app.descendants(.any).allElementsBoundByIndex (snapshots EVERY
element) to find the text input at a point. fill drove this repeatedly: once it has
coordinates, resolveTextEntryElement re-runs textInputAt on every verify/repair poll
iteration whenever the focused-field reference goes stale (e.g. the Settings search
bar repositioning bottom->top), so the full-tree enum dominated fill latency.

Query the text-input element types directly (app.textFields/secureTextFields/
searchFields/textViews) instead. Same matches, but XCUITest resolves typed queries
without snapshotting the whole tree. Measured (iPhone 17 sim, warm runner): fill 25
chars ~14.5s -> ~4.5s (3.2x), 6/6 exact. Same primitive #632 killed for get text.
Review P1 (focus race): the isKeyboardVisible early-exit in stabilizeTextInputBeforeTyping
and waitForTextEntryReadiness fired the instant the keyboard was visible — but when it was
ALREADY up from a previous field (back-to-back fills), that is before first-responder moves
to the newly-tapped field, so app.typeText could target the old field. Gate the fast-path on
a keyboard hidden->visible TRANSITION via a shared keyboardBecameVisible(wasVisibleAtEntry:)
helper; when the keyboard was already up, fall back to the settle/timeout (the prior, correct
behavior) instead of the ~2.4s dead wait the fresh case avoids.

Review P1 (F2): clearTextInput used editableTextValue(...) ?? "" and skipped clearing on
empty — but editableTextValue returns nil for secure (and unknown) fields, so secure fields
were NEVER cleared and replace concatenated stale+new. Distinguish nil (clear) from "" (skip).

Device-validated: fresh fill fast-path preserved + exact; a second fill with the keyboard
already up still types into the correct field and replaces (not concatenates).
@thymikee thymikee force-pushed the perf/ios-text-entry branch from 346d2eb to 296364f Compare May 31, 2026 12:39
@thymikee thymikee merged commit 73c057a into main May 31, 2026
18 checks passed
@thymikee thymikee deleted the perf/ios-text-entry branch May 31, 2026 13:08
@github-actions
Copy link
Copy Markdown

PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-05-31 13:09 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.

1 participant