Skip to content

perf(ios-runner): don't restart the settle window when re-binding an already-foreground target - #3532

Merged
thymikee merged 4 commits into
mainfrom
perf/ios-runner-skipped-activation-settle
Oct 13, 2026
Merged

thymikee merged 4 commits into
mainfrom
perf/ios-runner-skipped-activation-settle

Conversation

@thymikee

@thymikee thymikee commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

Summary

After open --relaunch, the first tap waited an extra 250 ms. The daemon's targetReset starts the post-activation settle window, then the tap re-binds the target through activateTarget(reason: bundle_changed), which restarted the window even when activation was skipped (ACTIVATE_SKIPPED reason=already_foreground).

The runner now records where the window came from instead of inferring it:

  • FirstInteractionWindow { readyUptime, origin }, with origin .activation or .announcedRelaunch(bundleId:).
  • targetReset stamps .announcedRelaunch with the binding it dropped (nil if none, which matches any bundle; the wire carries no bundle). Every other stamp is .activation.
  • activateTarget keeps the existing window only when no activation ran and the origin is an announced relaunch of this bundle; otherwise it stamps a fresh window.
  • Removed: reacquiresBoundTarget, the window-mode enum, and the nil writes in invalidateCachedTarget and ensureRunnerHostAppActive. A stale .activation window is harmless because it restamps.

The invariant stays "at least 250 ms between activating a target and its first interaction" (#2395).

Validation

  • Tested commit 3aabddd92c.

  • 15 LifecycleCache runner unit tests: Executed 15, 0 failures. Mutations caught: keep the window regardless of origin (3 failures); ignore whether an activation ran (2 failures).

  • pnpm check:affected --run, pnpm lint, and check:xctest-selection pass.

  • Live runner.log (see PR comment): ACTIVATE_SKIPPED is followed by the text-input probe with no 250 ms gap.

  • A/B measured at fb1e51accd; the redesign keeps the same relaunch-to-skipped-activation behavior. 5 paired rounds on one iPhone 17 Pro / iOS 26.2 sim, open com.apple.Preferences --relaunch then click 'label="General"':

    metric (median Δ vs main) agent-paced back-to-back
    runner accept→dispatch −224 ms −219 ms
    click −256 ms −195 ms

    Every paired round improved (−156 to −252 ms).

…he same foreground target

A skipped activation (target already foreground) re-bound the target and
restarted the 250 ms first-interaction window, charging the first tap after
every relaunch for time the targetReset round trip had already spent. Only an
actual activation, a different bundle, or a different process restarts the
window; a same-target reacquisition keeps an existing deadline and stamps only
when none exists.
…pped for any reason but an announced relaunch

A dropped binding with a leftover deadline must mean the announced external
relaunch, so a skipped activation can keep that deadline safely. Error
invalidations and the host-app rebind now clear it and the next activation
stamps a fresh window as before.
@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.17 MB 5.17 MB +1.2 kB
Package (unpacked) 5.17 MB 5.17 MB +1.2 kB
Package (download) 1.55 MB 1.55 MB +248 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 21.6 ms 21.1 ms -0.6 ms
CLI --help 55.8 ms 56.5 ms +0.7 ms

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 2 files

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

@thymikee

Copy link
Copy Markdown
Member Author

Code looks good at fb1e51a, but the PR is not ready to merge yet: the live A/B numbers appear only in the PR body, and no runner.log excerpt shows ACTIVATE_SKIPPED reason=already_foreground followed by no restamp. Please attach that excerpt from the tested commit. Both Smoke Tests jobs are still queued, not failed. They drive iOS sim open and interaction flows through prepareActivatedTarget and activateTarget, which this PR changes, so their result counts once they finish. There are no conflicts.

Could the runner record where the settle window came from instead of inferring it? For example, resetTargetAfterExternalRelaunch could stamp the window with an origin, like firstInteractionWindow = (readyUptime, origin: .announcedRelaunch | .boundTarget(bundleId, pid)). activateTarget would then keep a pending window only when no activation ran and the origin is the announced relaunch or the same binding. The nil writes in invalidateCachedTarget and ensureRunnerHostAppActive, and reacquiresBoundTarget, would become unnecessary, because no surviving deadline would need to mean anything. A smaller option is to keep the window on a skipped activation only when previousBundleId == nil, and drop the pid comparison. All of this is runner-local, and the targetReset wire contract can stay as it is.

Not blocking, and you can take or leave these: reacquiresBoundTarget is a second identity rule beside shouldRefreshCachedTarget (Lifecycle.swift:267-272), and they disagree when one pid is missing (cached nil, observed 42). Its same-bundle, non-nil branch is reachable only through a foreground-state race, since every activateTarget caller enters with a dropped or different bundle or a non-foreground state. The new firstInteractionReadyUptime = nil in ensureRunnerHostAppActive never has an effect, because both callers (CommandDispatch:611, 634) run after invalidateCachedTarget("missing_app_bundle") at :603 already clears the deadline. Either express the rule through shouldRefreshCachedTarget's semantics or key it on the dropped binding only, and drop the nil write or make it the only owner.

I did not run the runner unit tests or the claimed mutations. I read the pre-change code to see that the keep-elapsed test is red on base. I also did not check that the daemon sends targetReset only after the relaunched process is foreground, and the reset deadline is the right anchor only if it does (notifyAppleRunnerRelaunch in packages/platform-apple/src/lifecycle.ts).

The test-name thread still stands: target_terminated comes from a successful terminate (CommandDispatch.swift:476-478), so "ForError" in LifecycleCacheTests.swift:122 is misleading. Two other cubic-dev-ai threads do not apply, so please resolve them. The nil==nil pid thread matches the existing shouldRefreshCachedTarget rule, and the same-bundle branch is only reachable through a foreground race. The host-app activation thread does not apply because host-app activation never stamped a window, and invalidateCachedTarget at :603 already clears the deadline before both callers run.

Before merge, Smoke Tests must finish green on fb1e51a, and the design question above needs an answer.

@thymikee

Copy link
Copy Markdown
Member Author

@thymikee implemented the origin-tagged window in 3aabddd92c.

  • firstInteractionReadyUptime is now FirstInteractionWindow { readyUptime, origin }. targetReset stamps .announcedRelaunch(bundleId:); everything else stamps .activation. activateTarget keeps a window only when no activation ran and the origin is the announced relaunch of this bundle.
  • Removed reacquiresBoundTarget, the window-mode enum, and both nil writes (invalidateCachedTarget, ensureRunnerHostAppActive). The wire targetReset carries no bundle, so the stamp uses the binding being dropped, or matches any bundle when none was bound.
  • Ordering: the daemon sends targetReset only after simctl launch returns (lifecycle.ts dispatchAppleOpen then notifyAppleRunnerRelaunch), so the anchor is never earlier than the real activation.
  • Tests: 15 LifecycleCache tests, 0 failures. Mutations (keep window regardless of origin; ignore whether an activation ran) fail 3 and 2 tests. A stale .activation window after a terminate invalidation restamps fresh.
  • Cubic threads replied to and resolved. check:affected --run and lint pass.

Live runner.log, second tap after open --relaunch. ACTIVATE_SKIPPED to the first probe to the TextField probe (t=9.74 s) is tens of ms, with no 250 ms gap:

2026-10-12 21:07:59.894243+0200 COMMAND_ACCEPTED command=targetReset commandId=runner-dfb9a7b5-9ef6-4f83-b25c-09fbbc6d54db
2026-10-12 21:07:59.895109+0200 TARGET_CACHE_INVALIDATE reason=external_app_relaunch
2026-10-12 21:07:59.895233+0200 COMMAND_COMPLETED command=targetReset commandId=runner-dfb9a7b5-9ef6-4f83-b25c-09fbbc6d54db ok=1
2026-10-12 21:08:01.768723+0200 COMMAND_ACCEPTED command=tap commandId=runner-36545062-2895-431e-b26f-704f8e18c6c2
    t =     9.71s Get all elements bound by index for: Descendants matching type Alert
    t =     9.72s Get all elements bound by index for: Descendants matching type Sheet
2026-10-12 21:08:01.798766+0200 ACTIVATE bundle=com.apple.Preferences state=4 reason=bundle_changed
2026-10-12 21:08:01.798851+0200 ACTIVATE_SKIPPED bundle=com.apple.Preferences reason=already_foreground
    t =     9.74s Get all elements bound by index for: Descendants matching type TextField
2026-10-12 21:08:02.083896+0200 SYNTHESIZED_DISPATCH kind=tap point=(201.0,406.0) reference=(0.0,0.0,402.0,874.0) orientation=1
2026-10-12 21:08:02.108219+0200 SYNTHESIZED_RECORD name=agent-device-tap displayID=1 interfaceOrientation=1
2026-10-12 21:08:02.391706+0200 COMMAND_COMPLETED command=tap commandId=runner-36545062-2895-431e-b26f-704f8e18c6c2 ok=1

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 3 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

…s dropped again

A snapshot or XCTest failure that drops the binding after a targetReset left the
relaunch-origin window in place, so a later skipped activation could reuse it.
Invalidation now re-attributes a surviving window to an activation, so only the
binding the reset dropped can inherit it.
@thymikee

Copy link
Copy Markdown
Member Author

The code in 518f832 looks good, and it fixes the open P2 from the earlier review at fb1e51a (#3532 (comment)). invalidateCachedTarget now re-attributes to .activation at Lifecycle.swift#L221, and LifecycleCacheTests.swift#L179 covers it. CI is green: 19 checks pass, and the Smoke Tests, which drive the prepareActivatedTarget/activateTarget route this PR changes, passed. There are no conflicts.

On evidence: I read the new test against the pre-delta code and it would fail there, but I did not run the runner LifecycleCache tests or the claimed mutation of removing :221. The tests call activateTarget directly with swizzled XCUIApplication state, so the dispatch route is covered only by the live log and the Smoke Tests. The live runner.log is from 3aabddd, not 518f832. The last commit does not change the targetReset to skipped-activation route, but no live run exists at the head.

Of the cubic-dev-ai threads, these no longer apply, so please resolve them. #3532 (comment) is fixed at head, with invalidateCachedTarget re-attributing to .activation. #3532 (comment) is benign now, because reacquiresBoundTarget and its pid comparison are deleted and the keep rule at Lifecycle.swift:406 uses only the origin. #3532 (comment) is fixed, since ensureRunnerHostAppActive (Lifecycle.swift:184-197) no longer discards the pending window. #3532 (comment) is fixed, with the test renamed to testStaleActivationWindowSurvivingTerminateInvalidationStampsFreshOnSkippedActivation. No review thread still applies.

Nothing else is needed before merge, so this is ready for a maintainer to merge.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Oct 12, 2026
@thymikee
thymikee merged commit 64c1c88 into main Oct 13, 2026
19 checks passed
@thymikee
thymikee deleted the perf/ios-runner-skipped-activation-settle branch October 13, 2026 05:49
@github-actions

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-10-13 05:49 UTC

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