|
| 1 | +# #1403 experiment: usbmux as the primary physical-iOS runner transport |
| 2 | + |
| 3 | +Live results from 2026-07-30, thymikee-iphone (iPhone 17 Pro, iOS 26.5.2, UDID |
| 4 | +00008150-001849640CF8401C), macOS host with Xcode 26.2. Method: forced-route |
| 5 | +env override `AGENT_DEVICE_IOS_RUNNER_ROUTE=usbmux` in `runner-command-route.ts` |
| 6 | +(uncommitted experiment patch), isolated `--state-dir` daemons per leg, timings |
| 7 | +from per-request `--debug` ndjson plus wall-clock. Every timing sample was |
| 8 | +validated against real command output after an early instrument artifact (see |
| 9 | +Pitfalls). |
| 10 | + |
| 11 | +## Numbers (same build, same cable state, verified output) |
| 12 | + |
| 13 | +| Scenario | network (tunnel-IP) route | usbmux route | |
| 14 | +|---|---|---| |
| 15 | +| Steady-state snapshot, cable in | 350–450 ms | 440 ms | |
| 16 | +| Snapshot after 40 s idle | **4.5–4.6 s** (tunnel re-probe + session re-establish) | **440 ms** (unchanged) | |
| 17 | +| Runner killed, next snapshot | ~4.0 s self-heal | 4.7 s self-heal | |
| 18 | +| Steady-state snapshot, Wi-Fi only (no cable) | 425–495 ms | fails (see below) | |
| 19 | +| Full lifecycle open/snapshot/tap/screenshot/close | works | works | |
| 20 | + |
| 21 | +Older-build measurements (first pass) agreed in shape: first open pays one-time |
| 22 | +xctestrun prep (~13 s cold) and runner startup; fresh device resolution costs |
| 23 | +~6.7 s of devicectl listing either way. |
| 24 | + |
| 25 | +## Findings |
| 26 | + |
| 27 | +1. **The idle tax is the payoff, and it is bigger than expected.** Past the |
| 28 | + 30 s tunnel-IP cache TTL, the network route re-probes the tunnel and |
| 29 | + re-establishes runner readiness: ~4.5 s per idle gap, every time. Over |
| 30 | + usbmux the session stays hot indefinitely (40 s gaps measured at steady |
| 31 | + 440 ms). Agent workflows are exactly the idle-gappy workload that hits this |
| 32 | + tax on almost every step. |
| 33 | +2. **Steady state is a wash.** The per-command usbmuxd handshake (fresh unix |
| 34 | + socket, ListDevices, Connect) roughly cancels the network route's pooled- |
| 35 | + connection advantage; ~±90 ms either way. |
| 36 | +3. **Wi-Fi deletion is off the table.** A CoreDevice-paired, Wi-Fi-reachable |
| 37 | + iPhone (devicectl `available (paired)`, live tunnel) does NOT appear in |
| 38 | + usbmuxd at all — modern CoreDevice Wi-Fi runs over `remoted`, invisible to |
| 39 | + usbmuxd. usbmuxd listed the device only while cabled, `ConnectionType: USB` |
| 40 | + only, and the entry disappears on unplug. The CoreDevice network route must |
| 41 | + stay as the Wi-Fi fallback → this is a "usbmux-primary + network-fallback" |
| 42 | + reshuffle, not a full tunnel-code deletion. |
| 43 | +4. **Failure semantics gap (also affects today's xctest backend).** With the |
| 44 | + cable out, forced-usbmux commands hang for 2×45 s of retries and surface a |
| 45 | + generic "Runner did not accept connection" — the usbmux client's precise |
| 46 | + `DEVICE_NOT_FOUND` ("Connect the device by cable, trust this Mac…") is |
| 47 | + swallowed by `shouldRetryRunnerConnectError`. An implementation should make |
| 48 | + usbmux DEVICE_NOT_FOUND non-retryable (or fall back to the network route |
| 49 | + immediately) and preserve the hint. |
| 50 | +5. **Lock behavior is transport-independent.** A locked phone wedges AX |
| 51 | + capture/runner relaunch identically on both routes (SBMainWorkspace |
| 52 | + "Locked" denial; xcodebuild "Unlock thymikee-iphone to Continue"). |
| 53 | +6. **usbmux UDID matching is exact.** usbmuxd `SerialNumber` equals the |
| 54 | + dashed hardware UDID that `DeviceInfo.id` already uses for devicectl-listed |
| 55 | + devices; DeviceID (10) is the mux handle. Multi-device disambiguation is |
| 56 | + structural. (Multi-attached-device matrix not exercised: one device only.) |
| 57 | + |
| 58 | +## Implied design (matches the issue's desired outcome, minus full deletion) |
| 59 | + |
| 60 | +- One private route resolver: physical devices try usbmux first; on usbmux |
| 61 | + DEVICE_NOT_FOUND (not attached via USB) resolve the CoreDevice tunnel and |
| 62 | + use the network route. Tunnel cache/invalidations stay but only run on the |
| 63 | + fallback path, so cabled devices never pay the probe/TTL tax. |
| 64 | +- Failure semantics: usbmux DEVICE_NOT_FOUND → immediate fallback attempt; |
| 65 | + if the fallback also fails, surface the usbmux hint (cable/trust/unlock). |
| 66 | +- The xctest backend keeps usbmux-only (CoreDevice unavailable by definition) |
| 67 | + but needs the same non-retryable classification for DEVICE_NOT_FOUND. |
| 68 | + |
| 69 | +## Pitfalls for whoever implements/re-measures |
| 70 | + |
| 71 | +- `bin/agent-device.mjs` runs `dist/`, not `src/`. A stale dist silently ran |
| 72 | + the first "usbmux leg" on stock network code while the env override sat |
| 73 | + inert in the daemon (numbers looked plausible; only the cable-pull test |
| 74 | + exposed it). Rebuild and verify the daemon process (`ps eww <pid>`, dist |
| 75 | + mtime, and a behavioral probe) before trusting route attribution. |
| 76 | +- `/usr/bin/time` on a failing command still prints a small `real` — a fast |
| 77 | + error latency is indistinguishable from a fast success unless output is |
| 78 | + asserted. Runner-lease contention ("already owned by another daemon") |
| 79 | + produced exactly this artifact. |
| 80 | +- Sessions are cwd-scoped; run every command of a leg from the same cwd. |
| 81 | +- Auto-Lock must be Never during measurement or the runner dies mid-series. |
0 commit comments