Skip to content

feat(perf): time every iOS command against the fixture app - #3885

Merged
thymikee merged 7 commits into
mainfrom
perf/ios-command-suite
Oct 22, 2026
Merged

thymikee merged 7 commits into
mainfrom
perf/ios-command-suite

Conversation

@thymikee

@thymikee thymikee commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Summary

pnpm perf --platform ios --udid <udid> times every catalog command that the iOS simulator serves, against the fixture app. The suite has 105 steps in 7 scenarios: lifecycle, home reads, catalog, form, automation, heavy feed, and session. The report lists 16 excluded commands with a reason, and ios-suite.test.ts keeps measured + excluded equal to the catalog.

Each sample records wall time, the client daemon_request time, and a profile from its --debug request log. The output is summary.json, report.md, and the raw logs. The nightly workflow and perf:compare come from #3886, which merged into this branch.

  • home-reads/snapshot-forced-tree pins the XCTest tree through the SDK. Its expectation requires tree with the runner's requested-backend code, so a degraded capture fails instead of becoming a baseline.
  • Heavy-feed longpress, swipe, and back fail today (XCTest kAXErrorIllegalArgument). They stay as measured steps so that a fix shows as fixed.
  • Run metadata uses the benchmark's host readers, moved to scripts/lib/bench-host.ts. host-kit and the daemon client export the phase names that the profiler reads.

46 files. This PR is rebased on main after #3884, and main is merged in for #3892.

Validation

  • At 8958314, I ran home-reads,heavy-feed locally (3 rounds + 1 warmup, iPhone 17 Pro, iOS 27.0). snapshot-forced-tree was served by tree/healthy/requested-backend ×3. The only failures were the 3 heavy-feed gaps. The evidence is in the PR comments.
  • At aed5aa7, --scenarios automation ran clean (1 warmup + 3 rounds). logs-start → logs-stop passed 3/3 with no daemon_stuck, now that the fix(logs): keep an early iOS simulator logs stop from hanging the session #3892 workaround is removed.
  • vitest scripts/perf scripts/lib scripts/ios-snapshot-benchmark: passed.
  • pnpm check:affected --run --head 8958314a16: passed at aed5aa7 (exactCommit: true, FAIL-OPEN full set: 36 passed, 25 GitHub-authoritative skips).
  • GitHub owns the native, device, and coverage lanes.

🤖 Generated with Claude Code

@thymikee

Copy link
Copy Markdown
Member Author

Local proof run (raw report.md)

Source 7a801ca5a1 (suite code of this layer before the lint, AGENTS.md, and stuck-daemon-handling amendments), dedicated simulator Perf Suite Worker (iPhone 17 Pro, iOS 27.0), Xcode 27.1, M4 Pro. Command: pnpm perf --platform ios --udid <udid> --rounds 3 --warmup 1. 102/105 steps clean; the 3 failing steps are the heavy-feed XCTest gaps described in the PR body. Device-specific numbers; do not compare them with the CI host.

report.md

agent-device perf — ios

  • Commit: 7a801ca (perf/nightly-comparison)
  • Device: Perf Suite Worker · iOS-27-0
  • Toolchain: Xcode 27.1 Build version 27A9275 · Node v26.11.1 · agent-device 0.21.31-dev
  • Host: Darwin 27.0.0 · Apple M4 Pro · 12 cores · 48 GB
  • Rounds: 3 measured after 1 warmup · 2026-10-21T20:20:45.469Z → 2026-10-21T20:34:38.871Z

Times are milliseconds. See scripts/perf/README.md for each metric. The second table of each
scenario holds per-request medians from the daemon --debug request logs.

lifecycle

step command n wall mean ± sd median p95 min max cv daemon fail
open-relaunch open {app} --relaunch 3 2098 ± 28 2092 2129 2074 2129 1% 1926 0
appstate appstate 3 239 ± 86 191 338 188 338 36% 12 0
home home 3 731 ± 80 711 819 662 819 11% 492 0
open-resume open {app} 3 780 ± 235 684 1048 608 1048 30% 447 0
app-switcher app-switcher 3 2058 ± 290 1985 2377 1811 2377 14% 1658 0
orientation-landscape orientation landscape-left 3 225 ± 59 193 293 189 293 26% 22 0
orientation-portrait orientation portrait 3 334 ± 271 179 647 176 647 81% 23 0
settings-appearance settings appearance light 3 347 ± 45 363 382 297 382 13% 161 0
clipboard-write clipboard write agent-device perf 3 184 ± 21 176 207 167 207 11% 21 0
clipboard-read clipboard read 3 353 ± 26 345 383 333 383 7% 180 0
action-button action-button 3 5903 ± 6 5904 5908 5896 5908 0% 5743 0
prepare-runner prepare ios-runner 3 356 ± 19 354 376 338 376 5% 190 0
devices devices --platform ios 3 1922 ± 117 1892 2051 1823 2051 6% 1740 0
apps apps 3 450 ± 152 371 625 354 625 34% 210 0
capabilities capabilities 3 196 ± 68 163 274 152 274 34% 4 0
doctor doctor 3 2278 ± 386 2068 2723 2043 2723 17% 1914 0
trigger-app-event trigger-app-event fixture.ready {"source":"perf"} 3 317 ± 22 316 339 295 339 7% 159 0
step runner spawns snapshot source settle wait stabilization retries producer top phases
open-relaunch 6 17 / 638 67 – 0 0 – open_timing 1888, exec_command 638, ios.snapshot-source.acquire 67, ios_runner_command_send 6
appstate 4 0 / 0 0 – 0 0 – ios_runner_command_send 4, ios_runner_readiness_preflight 2
home 482 0 / 0 0 – 0 0 – ios_runner_command_send 482, ios_runner_readiness_preflight 3
open-resume 4 4 / 277 149 – 0 0 simulator-ax-bridge×3 open_timing 409, exec_command 277, ios.snapshot-source.acquire 149, ios_runner_command_send 4
app-switcher 1651 0 / 0 0 – 0 0 – ios_runner_command_send 1651, ios_runner_readiness_preflight 2
orientation-landscape 15 0 / 0 0 – 0 0 – ios_runner_command_send 15, ios_runner_readiness_preflight 2
orientation-portrait 15 0 / 0 0 – 0 0 – ios_runner_command_send 15, ios_runner_readiness_preflight 2
settings-appearance 0 1 / 157 0 – 0 0 – exec_command 157
clipboard-write 13 0 / 0 0 – 0 0 – ios_runner_command_send 13, ios_runner_readiness_preflight 2
clipboard-read 0 1 / 177 0 – 0 0 – exec_command 177
action-button 5733 0 / 0 0 – 0 0 – ios_runner_command_send 5733, ios_runner_readiness_preflight 3
prepare-runner 3 1 / 155 0 – 0 0 – prepare_apple_runner 328, simulator_state 156, exec_command 155, prepare_admit_runtime 12, ios_runner_command_send 3
devices 0 3 / 2059 0 – 0 0 – exec_command 2059
apps 0 2 / 200 0 – 0 0 – exec_command 200
capabilities 0 0 / 0 0 – 0 0 – –
doctor 0 7 / 2189 0 – 0 0 – exec_command 2189
trigger-app-event 0 1 / 156 0 – 0 0 – exec_command 156

home-reads

step command n wall mean ± sd median p95 min max cv daemon fail
snapshot-i snapshot -i 3 298 ± 9 303 304 287 304 3% 145 0
snapshot snapshot 3 320 ± 24 313 347 300 347 8% 153 0
snapshot-raw snapshot --raw 3 314 ± 17 312 332 299 332 5% 148 0
snapshot-diff snapshot --diff 3 312 ± 25 307 339 289 339 8% 149 0
diff-snapshot diff snapshot 3 313 ± 33 295 350 292 350 10% 143 0
wait-text wait text Agent Device Tester 3 228 ± 45 202 280 202 280 20% 50 0
wait-selector wait id="gesture-canary-ready" 3 308 ± 17 299 327 297 327 6% 144 0
wait-absent wait absent id="heavy-feed-title" 3 359 ± 106 300 481 296 481 30% 145 0
wait-stable wait stable 300 5000 3 771 ± 19 780 785 750 785 2% 613 0
is-visible is visible id="gesture-canary-ready" 3 305 ± 11 311 311 292 311 4% 159 0
is-exists is exists text="Gesture lab" 3 156 ± 5 154 162 153 162 3% 2 0
get-text get text id="gesture-fling-status" 3 155 ± 5 154 161 152 161 3% 3 0
get-attrs get attrs id="drag-source" 3 156 ± 6 154 164 152 164 4% 3 0
find-text find text Gesture lab exists 3 313 ± 7 315 320 306 320 2% 155 0
batch-reads batch --steps [{"command":"get","input":{"format":"text","target":{"kind":"selector","selector":"id=\"gesture-fling-status\""}}},{"command":"is","input":{"predicate":"visible","selector":"id=\"gesture-canary-ready\""}}] 3 311 ± 5 314 315 306 315 2% 149 0
gesture-pan gesture pan {point:gesture-target} 40 0 300 3 939 ± 6 941 943 932 943 1% 777 0
gesture-pinch gesture pinch 1.4 {point:gesture-target} 3 1035 ± 18 1045 1047 1014 1047 2% 888 0
screenshot screenshot {out:home.png} 3 835 ± 81 808 926 770 926 10% 652 0
screenshot-overlay screenshot {out:home-refs.png} --overlay-refs 3 902 ± 9 906 908 891 908 1% 740 0
events events 3 155 ± 2 154 158 154 158 1% 3 0
logs-mark logs mark agent-device perf 3 156 ± 8 153 165 151 165 5% 2 0
perf-memory perf memory sample 3 598 ± 7 602 603 590 603 1% 451 0
react-native react-native dismiss-overlay 3 375 ± 14 372 390 362 390 4% 213 0
artifacts artifacts 3 158 ± 3 160 160 155 160 2% 2 0
step runner spawns snapshot source settle wait stabilization retries producer top phases
snapshot-i 0 6 / 151 66 – 0 0 simulator-ax-bridge×3 exec_command 151, ios.snapshot-source.acquire 66
snapshot 0 6 / 150 74 – 0 0 simulator-ax-bridge×3 exec_command 150, ios.snapshot-source.acquire 74
snapshot-raw 0 6 / 144 70 – 0 0 simulator-ax-bridge×3 exec_command 144, ios.snapshot-source.acquire 70
snapshot-diff 0 6 / 151 65 – 0 0 simulator-ax-bridge×3 exec_command 151, ios.snapshot-source.acquire 65
diff-snapshot 0 6 / 141 68 – 0 0 simulator-ax-bridge×3 exec_command 141, ios.snapshot-source.acquire 68, ios.snapshot-source.present 1
wait-text 42 0 / 0 0 – 0 0 – ios_runner_command_send 42, ios_runner_readiness_preflight 3
wait-selector 0 6 / 147 65 – 0 0 simulator-ax-bridge×3 exec_command 147, ios.snapshot-source.acquire 65, ios.snapshot-source.present 1
wait-absent 0 6 / 147 68 – 0 0 simulator-ax-bridge×3 exec_command 147, ios.snapshot-source.acquire 68
wait-stable 0 12 / 321 135 – 0 0 simulator-ax-bridge×3 exec_command 321, ios.snapshot-source.acquire 135, ios.snapshot-source.present 1
is-visible 0 6 / 141 84 – 0 0 simulator-ax-bridge×3 exec_command 141, ios.snapshot-source.acquire 84, ios.snapshot-source.present 1
is-exists 0 0 / 0 0 – 0 0 – –
get-text 0 0 / 0 0 – 0 0 – –
get-attrs 0 0 / 0 0 – 0 0 – –
find-text 0 6 / 147 80 – 0 0 simulator-ax-bridge×3 exec_command 147, ios.snapshot-source.acquire 80, ios.snapshot-source.present 1
batch-reads 0 6 / 151 67 – 0 0 simulator-ax-bridge×3 exec_command 151, ios.snapshot-source.acquire 67
gesture-pan 762 0 / 0 0 – 0 0 – ios_runner_command_send 762, ios_runner_readiness_preflight 5
gesture-pinch 875 0 / 0 0 – 0 0 – ios_runner_command_send 875, ios_runner_readiness_preflight 2
screenshot 0 2 / 572 0 – 0 0 – exec_command 572
screenshot-overlay 0 8 / 664 69 – 0 0 simulator-ax-bridge×3 exec_command 664, ios.snapshot-source.acquire 69, ios.snapshot-source.present 1
events 0 0 / 0 0 – 0 0 – –
logs-mark 0 0 / 0 0 – 0 0 – –
perf-memory 0 4 / 447 0 – 0 0 – exec_command 447
react-native 203 0 / 0 0 – 0 0 apple-runner×3 snapshot_capture 210, ios_runner_command_send 203, ios_runner_readiness_preflight 3
artifacts 0 0 / 0 0 – 0 0 – –

catalog

step command n wall mean ± sd median p95 min max cv daemon fail
press-selector press id="category-produce" 3 1269 ± 14 1269 1283 1255 1283 1% 1107 0
press-selector-settle press id="category-bakery" --settle 3 1550 ± 11 1548 1562 1540 1562 1% 1386 0
press-ref press {ref:category-pantry} 3 769 ± 18 779 780 748 780 2% 615 0
press-ref-settle press {ref:category-produce} --settle 3 1414 ± 18 1404 1435 1403 1435 1% 1250 0
press-point press {point:category-bakery} 3 749 ± 3 749 752 746 752 0% 590 0
press-point-settle press {point:category-pantry} --settle 3 1526 ± 42 1516 1572 1489 1572 3% 1358 0
click-selector click id="category-all" 3 933 ± 4 934 937 929 937 0% 774 0
click-ref click {ref:category-bakery} 3 1043 ± 3 1045 1045 1040 1045 0% 886 0
click-point click {point:category-pantry} 3 791 ± 77 747 879 746 879 10% 595 0
click-settle click id="category-all" --settle 3 1695 ± 33 1687 1731 1667 1731 2% 1526 0
press-count press {point:category-produce} --count 2 --interval-ms 50 3 1398 ± 141 1317 1560 1316 1560 10% 1161 0
fill fill id="catalog-search" tart 3 2704 ± 67 2721 2760 2630 2760 2% 2548 0
fill-settle fill id="catalog-search" kit --settle 3 5291 ± 15 5299 5301 5273 5301 0% 5140 0
focus focus {point:catalog-search} 3 714 ± 3 713 718 711 718 0% 558 0
type type box 3 885 ± 26 876 914 864 914 3% 712 0
keyboard-return keyboard return 3 1596 ± 453 1337 2118 1331 2118 28% 1173 0
fill-clear fill id="catalog-search" 3 2289 ± 113 2225 2419 2223 2419 5% 2058 0
scroll-down scroll down 3 822 ± 15 814 840 812 840 2% 645 0
scroll-down-settle scroll down --settle 3 2997 ± 7 2999 3003 2990 3003 0% 2838 0
scroll-top scroll top 3 2093 ± 46 2071 2145 2062 2145 2% 1924 0
scroll-until scroll down --until id="catalog-footer" 3 1922 ± 110 1871 2048 1847 2048 6% 1719 0
swipe swipe 200 300 200 650 3 851 ± 28 844 882 826 882 3% 689 0
swipe-count swipe 200 650 200 450 --count 2 --pause-ms 50 --pattern ping-pong 3 1656 ± 20 1667 1669 1633 1669 1% 1482 0
step runner spawns snapshot source settle wait stabilization retries producer top phases
press-selector 887 6 / 146 134 – 0 0 simulator-ax-bridge×3 ios_runner_command_send 887, exec_command 146, ios.snapshot-source.acquire 134, ios.snapshot-source.present 1
press-selector-settle 578 18 / 510 227 654 0 0 simulator-ax-bridge×3 ios_runner_command_send 578, exec_command 510, ios.snapshot-source.acquire 227, ios.snapshot-source.present 3
press-ref 607 0 / 0 0 – 0 0 – ios_runner_command_send 607
press-ref-settle 627 12 / 305 142 606 0 0 simulator-ax-bridge×3 ios_runner_command_send 627, exec_command 305, ios.snapshot-source.acquire 142, ios.snapshot-source.present 2
press-point 583 0 / 0 0 – 0 0 – ios_runner_command_send 583
press-point-settle 584 18 / 455 224 625 0 0 simulator-ax-bridge×3 ios_runner_command_send 584, exec_command 455, ios.snapshot-source.acquire 224, ios.snapshot-source.present 3
click-selector 605 6 / 140 90 – 0 0 simulator-ax-bridge×3 ios_runner_command_send 605, exec_command 140, ios.snapshot-source.acquire 90, ios.snapshot-source.present 1
click-ref 880 0 / 0 0 – 0 0 – ios_runner_command_send 880
click-point 590 0 / 0 0 – 0 0 – ios_runner_command_send 590
click-settle 623 18 / 479 346 728 0 0 simulator-ax-bridge×3 ios_runner_command_send 623, exec_command 479, ios.snapshot-source.acquire 346, ios.snapshot-source.present 3
press-count 1152 0 / 0 0 – 0 0 – ios_runner_command_send 1152
fill 2394 6 / 140 71 – 0 0 simulator-ax-bridge×3 ios_runner_command_send 2394, exec_command 140, ios.snapshot-source.acquire 71, ios.snapshot-source.present 1, ios_runner_readiness_preflight 1
fill-settle 3500 30 / 791 299 1481 0 0 simulator-ax-bridge×3 ios_runner_command_send 3500, exec_command 791, ios.snapshot-source.acquire 299, ios.snapshot-source.present 5, ios_runner_readiness_preflight 2
focus 548 0 / 0 0 – 0 0 – ios_runner_command_send 548, ios_runner_readiness_preflight 4
type 701 0 / 0 0 – 0 0 – ios_runner_command_send 701, ios_runner_readiness_preflight 4
keyboard-return 1017 1 / 158 0 – 0 0 – ios_runner_command_send 1017, exec_command 158, ios_runner_readiness_preflight 3
fill-clear 1929 6 / 148 57 – 0 0 simulator-ax-bridge×3 ios_runner_command_send 1929, exec_command 148, ios.snapshot-source.acquire 57, ios_runner_readiness_preflight 2
scroll-down 637 0 / 0 0 – 0 0 – ios_runner_command_send 637, ios_runner_readiness_preflight 3
scroll-down-settle 643 36 / 982 445 2187 534 0 simulator-ax-bridge×3 exec_command 982, ios_runner_command_send 643, post_gesture_snapshot_stabilized 534, ios.snapshot-source.acquire 445, ios.snapshot-source.present 6
scroll-top 877 30 / 786 396 – 0 0 simulator-ax-bridge×3 ios_runner_command_send 877, exec_command 786, ios.snapshot-source.acquire 396, ios.snapshot-source.present 3
scroll-until 1243 18 / 462 219 – 0 0 simulator-ax-bridge×3 ios_runner_command_send 1243, exec_command 462, ios.snapshot-source.acquire 219, ios.snapshot-source.present 2
swipe 675 0 / 0 0 – 0 0 – ios_runner_command_send 675, ios_runner_readiness_preflight 3
swipe-count 1400 0 / 0 0 – 0 0 – ios_runner_command_send 1400, ios_runner_readiness_preflight 7

form

step command n wall mean ± sd median p95 min max cv daemon fail
fill-name fill id="field-name" Ada Lovelace 3 2839 ± 45 2845 2880 2790 2880 2% 2679 0
fill-email fill id="field-email" ada@example 3 2948 ± 66 2982 2990 2871 2990 2% 2776 0
type-suffix type .test 3 944 ± 18 934 964 933 964 2% 772 0
keyboard-dismiss keyboard dismiss 3 3039 ± 34 3050 3067 3001 3067 1% 2889 0
press-checkbox press id="checkbox-agree" 3 1762 ± 24 1772 1779 1735 1779 1% 1616 0
press-submit-settle press id="submit-order" --settle 3 2019 ± 38 2031 2050 1977 2050 2% 1877 0
step runner spawns snapshot source settle wait stabilization retries producer top phases
fill-name 2501 6 / 154 60 – 0 0 simulator-ax-bridge×3 ios_runner_command_send 2501, exec_command 154, ios.snapshot-source.acquire 60, ios_runner_readiness_preflight 2
fill-email 2617 6 / 141 83 – 0 0 simulator-ax-bridge×3 ios_runner_command_send 2617, exec_command 141, ios.snapshot-source.acquire 83, ios_runner_readiness_preflight 2, ios.snapshot-source.present 1
type-suffix 764 0 / 0 0 – 0 0 – ios_runner_command_send 764, ios_runner_readiness_preflight 2
keyboard-dismiss 2727 1 / 156 0 – 0 0 – ios_runner_command_send 2727, exec_command 156, ios_runner_readiness_preflight 3
press-checkbox 761 18 / 447 188 – 834 0 simulator-ax-bridge×3 post_gesture_snapshot_stabilized 834, ios_runner_command_send 761, exec_command 447, ios.snapshot-source.acquire 188, ios.snapshot-source.present 2
press-submit-settle 761 24 / 635 265 626 478 0 simulator-ax-bridge×3 ios_runner_command_send 761, exec_command 635, post_gesture_snapshot_stabilized 478, ios.snapshot-source.acquire 265, ios.snapshot-source.present 4

automation

step command n wall mean ± sd median p95 min max cv daemon fail
record-start record start {out:automation.mp4} --hide-touches 3 954 ± 8 950 964 949 964 1% 790 0
record-stop record stop 3 862 ± 126 795 1007 783 1007 15% 624 0
trace-start trace start {out:automation.trace} 3 180 ± 36 160 222 158 222 20% 3 0
trace-stop trace stop {out:automation.trace} 3 163 ± 5 165 167 158 167 3% 2 0
longpress longpress id="automation-longpress" 800 3 1912 ± 15 1913 1926 1896 1926 1% 1755 0
longpress-settle longpress id="automation-longpress" 800 --settle 3 2217 ± 53 2198 2277 2176 2277 2% 2032 0
alert-open press id="automation-open-alert" 3 1163 ± 8 1165 1170 1154 1170 1% 1006 0
alert-wait alert wait 10000 3 1131 ± 8 1134 1137 1121 1137 1% 969 0
alert-get alert get 3 1101 ± 5 1099 1107 1097 1107 0% 946 0
alert-accept alert accept 3 2045 ± 25 2035 2072 2026 2072 1% 1871 0
alert-dismiss alert dismiss 3 2127 ± 128 2056 2275 2050 2275 6% 1884 0
logs-start logs start 3 371 ± 26 365 400 349 400 7% 208 0
logs-stop logs stop 3 191 ± 5 192 195 186 195 2% 32 0
network-dump network dump 10 3 1527 ± 24 1524 1552 1504 1552 2% 1371 0
back back 3 911 ± 24 897 938 897 938 3% 732 0
back-settle back --settle 3 1497 ± 44 1496 1541 1453 1541 3% 1343 0
step runner spawns snapshot source settle wait stabilization retries producer top phases
record-start 187 2 / 247 0 – 0 0 apple-runner×3 exec_command 247, ios_runner_command_send 187, ios_runner_readiness_preflight 2
record-stop 0 6 / 236 0 – 0 0 – exec_command 236
trace-start 0 0 / 0 0 – 0 0 – –
trace-stop 0 0 / 0 0 – 0 0 – –
longpress 1260 12 / 287 135 – 483 0 simulator-ax-bridge×3 ios_runner_command_send 1260, post_gesture_snapshot_stabilized 483, exec_command 287, ios.snapshot-source.acquire 135, ios.snapshot-source.present 1
longpress-settle 1263 18 / 490 203 618 0 0 simulator-ax-bridge×3 ios_runner_command_send 1263, exec_command 490, ios.snapshot-source.acquire 203, ios.snapshot-source.present 2
alert-open 489 12 / 304 148 – 509 0 simulator-ax-bridge×3 post_gesture_snapshot_stabilized 509, ios_runner_command_send 489, exec_command 304, ios.snapshot-source.acquire 148, ios.snapshot-source.present 1
alert-wait 960 0 / 0 0 – 0 0 – ios_runner_command_send 960, ios_runner_readiness_preflight 3
alert-get 937 0 / 0 0 – 0 0 – ios_runner_command_send 937, ios_runner_readiness_preflight 2
alert-accept 1862 0 / 0 0 – 0 0 – ios_runner_command_send 1862, ios_runner_readiness_preflight 2
alert-dismiss 1876 0 / 0 0 – 0 0 – ios_runner_command_send 1876, ios_runner_readiness_preflight 2
logs-start 0 2 / 171 0 – 0 0 – exec_command 171
logs-stop 0 0 / 0 0 – 0 0 – –
network-dump 0 1 / 1366 0 – 0 0 – exec_command 1366
back 724 0 / 0 0 – 0 0 – ios_runner_command_send 724, ios_runner_readiness_preflight 3
back-settle 714 12 / 345 138 620 0 0 simulator-ax-bridge×3 ios_runner_command_send 714, exec_command 345, ios.snapshot-source.acquire 138, ios_runner_readiness_preflight 2, ios.snapshot-source.present 2

heavy-feed

step command n wall mean ± sd median p95 min max cv daemon fail
snapshot-i snapshot -i 3 402 ± 15 402 417 387 417 4% 234 0
snapshot snapshot 3 385 ± 7 382 392 380 392 2% 221 0
snapshot-raw snapshot --raw 3 390 ± 4 388 394 386 394 1% 234 0
press-like-selector press id="heavy-post-0-like" 3 796 ± 12 793 808 785 808 1% 634 0
press-like-settle press id="heavy-post-1-like" --settle 3 1576 ± 42 1560 1623 1545 1623 3% 1390 0
press-like-ref press {ref:heavy-post-0-like} 3 575 ± 8 575 582 567 582 1% 417 0
press-like-point press {point:heavy-post-0-like} 3 582 ± 20 575 605 566 605 4% 412 0
wait-text wait text Liked posts: 2 3 201 ± 11 203 211 190 211 5% 45 0
is-visible is visible id="heavy-post-1-like" 3 387 ± 5 389 392 382 392 1% 230 0
get-text get text id="heavy-feed-last-action" 3 165 ± 3 165 168 162 168 2% 2 0
find-text find text Post 1: exists 3 373 ± 20 365 397 359 397 5% 214 0
longpress-post longpress id="heavy-post-1" 800 3 – – – – – – – 3
scroll-down scroll down 3 5659 ± 103 5638 5771 5569 5771 2% 5468 0
snapshot-i-scrolled snapshot -i 3 1511 ± 547 1824 1828 879 1828 36% 1652 0
scroll-down-settle scroll down --settle 3 2122 ± 109 2184 2185 1997 2185 5% 2028 0
swipe swipe 200 650 200 300 3 – – – – – – – 3
snapshot-raw-forced-tree snapshot --raw 3 4852 ± 28 4845 4882 4828 4882 1% 4843 0
back back 3 – – – – – – – 3
step runner spawns snapshot source settle wait stabilization retries producer top phases
snapshot-i 0 6 / 149 154 – 0 0 simulator-ax-bridge×3 ios.snapshot-source.acquire 154, exec_command 149, ios.snapshot-source.present 2
snapshot 0 6 / 141 143 – 0 0 simulator-ax-bridge×3 ios.snapshot-source.acquire 143, exec_command 141, ios.snapshot-source.present 1
snapshot-raw 0 6 / 139 155 – 0 0 simulator-ax-bridge×3 ios.snapshot-source.acquire 155, exec_command 139, ios.snapshot-source.present 1
press-like-selector 400 6 / 143 147 – 0 0 simulator-ax-bridge×3 ios_runner_command_send 400, ios.snapshot-source.acquire 147, exec_command 143, ios.snapshot-source.present 2
press-like-settle 403 18 / 456 453 768 0 0 simulator-ax-bridge×3 exec_command 456, ios.snapshot-source.acquire 453, ios_runner_command_send 403, ios.snapshot-source.present 5
press-like-ref 408 0 / 0 0 – 0 0 – ios_runner_command_send 408
press-like-point 407 0 / 0 0 – 0 0 – ios_runner_command_send 407
wait-text 37 0 / 0 0 – 0 0 – ios_runner_command_send 37, ios_runner_readiness_preflight 3
is-visible 0 6 / 146 155 – 0 0 simulator-ax-bridge×3 ios.snapshot-source.acquire 155, exec_command 146, ios.snapshot-source.present 1
get-text 0 0 / 0 0 – 0 0 – –
find-text 0 6 / 139 140 – 0 0 simulator-ax-bridge×3 ios.snapshot-source.acquire 140, exec_command 139, ios.snapshot-source.present 1
longpress-post – – – – – – – –
scroll-down 791 7 / 825 0 – 0 22 – ios_runner_readiness_preflight 4166, exec_command 825, ios_runner_command_send 791, ios_runner_session_startup_timings 279, ios_runner_startup_cleanup_stale_bundles 241
snapshot-i-scrolled 0 24 / 592 730 – 1648 0 simulator-ax-bridge×3 post_gesture_snapshot_stabilization_timeout 1648, ios.snapshot-source.acquire 730, exec_command 592, ios.snapshot-source.present 5
scroll-down-settle 519 18 / 471 742 1505 919 0 simulator-ax-bridge×3 post_gesture_snapshot_stabilized 919, ios.snapshot-source.acquire 742, ios_runner_command_send 519, exec_command 471, ios.snapshot-source.present 5
swipe – – – – – – – –
snapshot-raw-forced-tree 4831 0 / 0 0 – 0 0 apple-runner×3 snapshot_capture 4837, ios_runner_command_send 4831, ios_runner_readiness_preflight 2
back – – – – – – – –

session

step command n wall mean ± sd median p95 min max cv daemon fail
close close 3 177 ± 9 181 183 167 183 5% 12 0
step runner spawns snapshot source settle wait stabilization retries producer top phases
close 0 0 / 0 0 – 0 0 – –

Backends and fallbacks

  • lifecycle/open-relaunch — backends –; fallbacks simulator-ax-bridge: iOS Simulator snapshot source unsupported: foreground-owner-unverified×3
  • heavy-feed/snapshot-i — backends –; fallbacks simulator-ax-bridge: truncated at 32 levels×3
  • heavy-feed/snapshot — backends –; fallbacks simulator-ax-bridge: truncated at 32 levels×3
  • heavy-feed/snapshot-raw — backends –; fallbacks simulator-ax-bridge: truncated at 32 levels×3
  • heavy-feed/press-like-selector — backends –; fallbacks simulator-ax-bridge: truncated at 32 levels×3
  • heavy-feed/press-like-settle — backends –; fallbacks simulator-ax-bridge: truncated at 32 levels×3
  • heavy-feed/is-visible — backends –; fallbacks simulator-ax-bridge: truncated at 32 levels×3
  • heavy-feed/find-text — backends –; fallbacks simulator-ax-bridge: truncated at 32 levels×3
  • heavy-feed/scroll-down — backends –; fallbacks ios_runner_readiness_preflight: healthy_mutation_stale×3, ios_runner_session_invalidated: runner_readiness_preflight_failed_before_command_send×3, retry×3
  • heavy-feed/snapshot-i-scrolled — backends –; fallbacks post_gesture_snapshot_stabilization_timeout×2, simulator-ax-bridge: truncated at 32 levels×3
  • heavy-feed/scroll-down-settle — backends –; fallbacks simulator-ax-bridge: truncated at 32 levels×3
  • heavy-feed/snapshot-raw-forced-tree — backends private-ax/recovered/ax-rejected×3; fallbacks –

Failures

  • heavy-feed/longpress-post — 3/3 failed: COMMAND_FAILED — NSInternalInconsistencyException: Invalid parameter not satisfying: point.x != INFINITY && point.y != INFINITY
  • heavy-feed/swipe — 3/3 failed: COMMAND_FAILED — Active app interaction viewport is unavailable
  • heavy-feed/back — 3/3 failed: COMMAND_FAILED — in-app back control is not available

Coverage

Measured commands (43): action-button, alert, app-switcher, apps, appstate, artifacts, back, batch, capabilities, click, clipboard, close, devices, diff, doctor, events, fill, find, focus, gesture, get, home, is, keyboard, logs, longpress, network, open, orientation, perf, prepare, press, react-native, record, screenshot, scroll, settings, snapshot, swipe, trace, trigger-app-event, type, wait.

  • audio not measured: Host audio capture needs a macOS audio permission that hosted runners do not grant.
  • boot not measured: Simulator power cycle: CoreSimulator boot time dominates and it ends the session under test.
  • fold not measured: iPhone Duo only; the suite device has one panel.
  • hardware-button not measured: The iOS simulator runtime refuses hardware-button presses.
  • hover not measured: The iOS runtime refuses hover; it is a web pointer contract.
  • install not measured: Bundle provisioning: simctl copy time dominates; the job installs the fixture once.
  • install-from-source not measured: Bundle provisioning from a source build; the job installs a cached Release fixture.
  • push not measured: The fixture holds no notification authorization, so simctl refuses the payload; iOS push coverage is contract-only.
  • reinstall not measured: Bundle provisioning: simctl copy time dominates and it resets the app under test.
  • replay not measured: Composite runner: a replay is the sum of the measured commands it runs.
  • shutdown not measured: Simulator power cycle: it ends the session and the runner under test.
  • telephony not measured: The iOS simulator runtime refuses simulated calls and SMS.
  • test not measured: Composite runner: a test suite is the sum of the measured commands it runs.
  • tv-remote not measured: tvOS only.
  • uninstall not measured: Bundle provisioning: it removes the app under test.
  • viewport not measured: The iOS runtime refuses viewport; it resizes web viewports only.

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.54 MB 5.54 MB +233 B
Package (unpacked) 5.54 MB 5.54 MB +233 B
Package (download) 1.68 MB 1.68 MB +90 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 45.4 ms 45.0 ms -0.4 ms
CLI --help 91.9 ms 87.7 ms -4.2 ms

@thymikee
thymikee force-pushed the perf/ios-command-suite branch from 6454e27 to b0b469b Compare October 21, 2026 21:29
@thymikee

Copy link
Copy Markdown
Member Author

I found no blocking defect in b0b469b, and the 14 checks on this PR pass. The diff touches only scripts/perf, the parked perf-nightly.yml and vitest.config.ts, so no production package code changes. I did not open the local proof report or the artifacts of CI run 39679264668, so the 102/105 and 101/105 counts rest on your description. I also did not run vitest scripts/perf or the forced-backend capture, so I have not confirmed that the daemon honors the snapshotPreferredBackend pin through the SDK transport. Please attach that capture, showing the sdk-forced-tree step served by the tree backend, before merge.

Not blocking, and you can take or leave these: (1) metadata.ts re-implements readTarget, readGitRevision and readToolchain from scripts/ios-snapshot-benchmark/host.ts, with no dirty flag, no 'available' filter and a silent undefined for an unknown UDID. One module should record iOS bench host and target identity, so move those readers to scripts/lib and build RunMetadata from them. (2) profile.ts and profile.test.ts copy phase names and the diag prefix as string literals from their emitters, and the fallback labels use data.error message text. Each phase the profiler reads should be declared once by its emitter and imported in both files, with fallbacks keyed on failure codes. (3) In ios-suite.ts, step() takes command separately from args[0]. Derive it from args[0] and validate it against PUBLIC_COMMANDS, or assert they match in the partition test. (4) In ios-harness.ts, the sdk-forced-tree row uses a request-log timestamp span for daemonMs and an in-process SDK call for wall time, unlike the CLI rows. Mark that row as span/in-process in report.md, and make treeBackendDegraded require backend === 'tree' so a daemon that ignores the pin cannot pass. (5) The README says "Comparisons use this metric", and the PR body says a fix "shows as fixed", but this layer has no comparison code. I assume a later layer adds it and did not read that layer. Move that wording there. The PR body also says 15 exclusions where the code lists 16, and 21 files where the diff has 20.

Could you keep the step table and profile.ts, but delete metadata.ts and build RunMetadata from the host.ts readers moved to scripts/lib? Could the Android tour run on the same SuiteScenario runner, so harness.ts, scenario.ts and platform-profiles.ts collapse into one table with a single round, session and daemon-stuck loop? Could forced-backend.ts wrap the SDK's own default transport to add the pin, instead of rebuilding daemon lookup from src internals, or could that one step wait until the pin has a supported surface? That would remove roughly 250 to 350 production lines and the second harness. It needs exported diagnostic phase-name constants from platform-apple and host-kit first, plus the shared scripts/lib home for bench host and target identity.

Before merge, the forced-backend capture above must be attached. Please also reuse the host.ts readers, and either share the round loop with the Android harness or explain why there are two harnesses.

@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-10-22 12:08 UTC

@thymikee
thymikee added this pull request to stack #3891 October 22, 2026 04:41
@thymikee

Copy link
Copy Markdown
Member Author

Addressed the four findings in 558da5c (follow-up commit, no force-push). Each finding below lists the fix, the test, and the repro on 00d3d0c.

1. Dispatch runs can replace independent baseline nights (P2)

Fix: selectNightlyBaseline in nightly-artifacts.ts takes scheduled runs only, one per UTC night. Each night counts its newest scheduled run that has a usable summary covering the judged scenarios. If that run has no artifact, an older scheduled run of the same night takes its place. listRuns now pages through gh api …/workflows/<file>/runs (100 per page, newest first), and the selection stops as soon as it has --nights nights. There is no longer a cap at 100 completed runs. Explicit --to/--from <ref> and <ref>..<ref> still accept runs of any trigger and local paths, so you can still compare dispatches on request.
Tests: nightly-artifacts.test.ts:

  • a rolling baseline takes one scheduled run per UTC night and skips dispatches
  • a night without a usable summary gives way to an older scheduled run of that night
  • a rolling baseline reads past 100 runs and stops once it has enough nights: 150 dispatches come first, and the generator throws if it is read past the last needed night.

On 00d3d0c: baselineCandidates returns 20:workflow_dispatch, 19:workflow_dispatch, 18:workflow_dispatch for three afternoon dispatches.

2. Timing verdicts accept fewer than three successful measurements (P2)

Fix:

  • --rounds/--n must be ≥ MIN_MEASURED_ROUNDS (3) in config.ts. This applies to every run, with no ad-hoc exception.
  • compare.ts has a new minSamples threshold (3). Tonight needs ≥3 successful samples of the metric, or the step gets insufficient-samples.
  • A baseline night counts toward the median and the spread only if it has ≥3 successful samples. Rows report timedNights. If fewer than three nights qualify, the step gets insufficient-baseline.

Tests:

  • compare.test.ts → withholds a timing verdict when tonight or a baseline night has fewer than three successes. This is your repro (three one-sample nights at 1,000 ms, one sample at 1,500 ms), plus a case with one thin night.
  • config.test.ts → a run measures at least three rounds…

On 00d3d0c: the repro gives regression.

3. Worsening failures can be reported as an improvement (P2)

Fix: Each row now has two separate verdicts: timing and failures (new-failures | more-failures | fixed | unchanged).

  • Rule: failures have materially worsened when tonight has more failed samples than every baseline night, both as a count and as a share of samples. new-failures is the case where no night failed.
  • No improvement while failures worsen: a faster median then gets insufficient-samples instead of improvement, because the samples that still pass are a biased subset.
  • Reporting: the table shows both columns, the job summary counts each kind, and both regression and new-failures/more-failures emit ::warning. The workflow stays warn-only and green.

Tests: compare.test.ts:

  • reports worse failures over a flaky baseline and never as an improvement (next to the flaky-baseline case). It covers your repro (failures [1,0,0], tonight 2 failures + one 500 ms success → more-failures / insufficient-samples) and a variant with 3 successes at 500 ms + 2 failures, where the speedup is withheld.
  • The existing flaky case (equal failures) stays unchanged/unchanged as the closest negative.

On 00d3d0c: the repro gives improvement.

4. Interrupted runs lose diagnostic logs (P2)

Fix: iosRunFinalizer in ios-harness.ts replaces the separate stop and finally paths. In order, it:

  1. Saves the allowlisted logs (daemon.log, sessions/; the token file stays behind).
  2. Runs close and daemon stop --clean, each bounded by timeoutMs: 60_000 (new invokeCli option).
  3. Saves the logs again so they include the teardown.
  4. Removes the state dir.

It runs once. run.ts registers the same function for SIGINT/SIGTERM and calls it in finally. A teardown that stalls therefore cannot lose the logs that existed before it, and cannot hold the run open.
Test: ios-harness.test.ts → finalizing a run saves the logs before and after a failing teardown, once. It drives the real invokeCli through AGENT_DEVICE_PERF_CLI with a fake CLI that records whether the logs were already saved, appends to daemon.log, and exits 1. It asserts:

  • both teardown calls saw saved logs;
  • the saved daemon.log includes the teardown lines;
  • the session log is copied and daemon.json is not;
  • the state dir is removed;
  • a second call does nothing.

On 00d3d0c: the interrupt handler ran only stop and never copied anything, and the normal path copied only after an unbounded stop.

Mutation checks (each fails the named test)

Each of these mutations fails its test:

  • nightly-artifacts: drop the schedule filter, the one-per-night rule, or the early stop.
  • compare: drop the sample minimum, allow improvement with worse failures, or replace more-failures with unchanged.
  • config: allow --rounds 1.
  • ios-harness: drop the save before teardown, the save after teardown, or the run-once guard.

Docs

  • scripts/perf/README.md: baseline nights, the rounds minimum, timing and failure verdicts, the warn-only note, and finalization on signals.
  • compare-cli help.
  • Descriptions of the rounds and baseline-nights workflow inputs.

New head: 558da5c
Gates (local, on a host with load average 600–970 from other worktrees' fuzz, coverage, and simulator runs):

  • Perf tests: pnpm exec vitest run --project unit-core scripts/perf/ passed (33 tests).
  • pnpm check:affected --run --head 558da5cdbb: failed. exactCommit: true. The plan failed open to the full set because the branch diff touches package.json/vitest.config.ts.
    • 11 checks passed: format, lint, typecheck, layering, di-seams, fallow, mcp-metadata, build, package, integration-node, macos-coverage.
    • vitest-related failed: 27 tests in 19 files, almost all Test timed out in 5000ms, plus one DEVICE_IN_USE on the shared agent-browser-chrome key.
    • The remaining 26 checks did not run. I ran these on their own afterwards, and all passed: production-exports, tmpdir-leaks, docs-prose, docs-links, gate-manifest, depgraph, agent-guidance.
  • pnpm gate unit-ci: failed. 7 tests in 6 files, all timeouts. Only 4 of these files also failed in the first run.
  • Attribution:
    • No failing test imports scripts/perf, and this commit changes nothing else that those tests run.
    • Rerun alone, 17 of the 20 failing files passed.
    • The remaining 3 (apple-platform-output-guard, ios-lifecycle, macos-helper-package-contents) fail the same way on an unchanged 00d3d0c worktree under the same load: alternating base/head runs gave 3/2/2/3 failures.
    • I'm treating these as host contention reproduced on base, not as a result of this change. Please read the Coverage and Integration Tests jobs on CI for the authoritative result.

No device run: the suite measures the same steps. Only teardown ordering and deadlines changed.

Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com

@thymikee

Copy link
Copy Markdown
Member Author

The Mutation Harness Canary failures at 558da5c don't come from this PR. The lane is advisory and never gates (mutation-affected.yml). It ran here because the PR touches package.json and vitest.config.ts. All five kernel-errors shards stop in Stryker's initial dry run, at one test from main: packages/host-kit/src/internal/exec-trace.test.ts › "environment-only late close appends to the finalized record (fixedLogPath=false)" (ENOENT … /undefined, meaning the diagnostics flush returned no record). That test was added in #3846 on 2026-10-20. The last green kernel-errors canary ran on 2026-10-19, so this has failed on main since #3846. Lint fails only because main's snapshot-route.test.ts is over the line cap; #3890 fixes that.

@thymikee

Copy link
Copy Markdown
Member Author

The earlier review of b0b469b asked for one capture of the sdk-forced-tree step served by the tree backend. At 558da5c that capture is still missing, so the code needs no change yet, but the PR needs this evidence before merge. scripts/perf/forced-backend.ts is unchanged in the delta, and the author comments do not supply a run. The nightly compare treats sdk-forced-tree like any other step. If the daemon ignores the snapshotPreferredBackend pin, a degraded-backend row gets judged as a timing baseline, and nobody can tell it ran on the wrong backend. Please attach one run, local or CI, at this head. It should include the sdk-forced-tree sample's request log line showing the pinned backend, and a summary.json and report.md where that sample's profile reports snapshot backend 'tree', not degraded.

Not blocking, and you can take or leave these: summary-fixture.ts is imported only by the three test files but counts as production code (and may show up as an unused export), so a test-helper name or a tests/fixtures path would fit better; and the header in perf-nightly.yml cites n=5 medians while the nightly now runs 3 rounds, so it could say that one failed sample withholds that step's timing verdict.

CI shows two red areas that look unrelated to this PR. Lint fails on the line cap in packages/platform-apple/src/snapshot-route.test.ts:1003, which this PR does not touch. The Mutants kernel-errors jobs fail in the Stryker dry run on the host-kit exec-trace test, and this PR does not touch host-kit. I did not run vitest scripts/perf or your mutation checks, and I did not open the CI artifacts, so the 102/105 and 101/105 counts rest on the PR body. The compare step has not run against real scheduled baselines yet, so its paging and selection against the live GitHub API are unverified.

Before merge, please attach the sdk-forced-tree capture and answer the open simplicity question: does the perf lane need both host readers and two harnesses?

Base automatically changed from perf/heavy-feed-fixture to main October 22, 2026 08:07
thymikee and others added 3 commits October 22, 2026 10:07
pnpm perf --platform ios now drives the test app through seven scenarios that
time each catalog command the iOS simulator serves, including the heavy feed
and a raw capture with the XCTest tree backend pinned. The suite records a
reason for every command it does not time, and a unit test keeps that
partition complete.

Each sample keeps the client daemon round trip and a phase profile from its
--debug request log: runner commands, host process spawns, snapshot sources,
retries, stabilization, and fallbacks. A run writes summary.json, report.md,
and the raw daemon and session logs. The iOS Settings tour is removed; the
Android tour stays as it was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…3886)

pnpm perf:compare judges one run against earlier runs: by default the seven
nights before it, or a chosen run, date, range, or local output. A step's
baseline is the median of its nightly medians. A change is flagged only when
it is larger than 20%, three times the night-to-night scaled MAD, twice the
run's own cv/sqrt(n), and 50 ms; new failures and fixed steps are flagged
too.

perf-nightly runs the iOS suite at 02:40 UTC again (#1781 A3). Each run uploads
perf-ios-summary (90 days) and perf-ios-raw (30 days), and writes the
comparison to the job summary with warnings for regressions and new
failures. Android stays report-only behind a dispatch input.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…interrupt

- Rolling baselines take one scheduled run per UTC night (the newest
  usable one) and page through workflow runs until enough nights are
  found. Explicit --to/--from references still accept any trigger.
- A run measures at least three rounds. A timing verdict needs three
  successful samples tonight and on each counted baseline night.
- Failures get their own verdict. more-failures flags tonight failing
  more than every baseline night, in count and share; a faster median
  with worse failures is not an improvement.
- The interrupt handler and the normal finish share one finalizer:
  save logs, stop session and daemon within a deadline, save again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thymikee
thymikee force-pushed the perf/ios-command-suite branch from 558da5c to fed2a4c Compare October 22, 2026 08:07
…t readers

- The forced-tree step ran a raw capture. The runner honors a backend pin only on the
  regular plan, so the raw plan recovered through private AX and the expectation passed
  a degraded row. The step is now a regular pinned capture on the home screen, and its
  expectation requires the pinned backend with the runner's `requested-backend` code.
- Perf metadata reads the revision, toolchain, and simulator through the readers of
  the iOS snapshot benchmark, moved to scripts/lib/bench-host.ts.
- host-kit and the daemon client export the phase names that the profiler reads.
- summary-fixture.ts becomes summary.fixtures.ts; the nightly header matches 3 rounds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thymikee

Copy link
Copy Markdown
Member Author

I found no code problem at fed2a4c that must change before merge, and the 23 checks are green. The change touches only scripts/perf, the nightly workflow, package.json, vitest.config.ts and one repo-guard test, with no package code changes. I did not run vitest or any gate myself, I did not open the CI artifacts (the 101/105 count rests on the PR body), and the pin evidence is the local report at 7a801ca, not a run at fed2a4c. Before merge, please update the PR body to cover the folded-in compare layer, the nightly schedule and the 16 exclusions, and answer the question below.

Not blocking, and you can take or leave each: (1) scripts/perf/ios-suite.ts:50 lets the pinned row pass when backend is undefined, which looks like what a host AX bridge capture would produce if it carries no snapshotQuality. The row should pass only when the XCTest runner served it, so require sample.profile.runnerCommands.snapshot and a backend in {tree, private-ax}, and add a test case with backend undefined. (2) scripts/perf/metadata.ts:26 re-implements readTarget, readGitRevision and readToolchain from scripts/ios-snapshot-benchmark/host.ts without the available filter or the dirty flag, so moving those readers to scripts/lib and building RunMetadata from them would remove the copies. (3) scripts/perf/profile.ts:19 copies phase names and the diag prefix as string literals from emitters that do not export them, and the fallback labels embed error message text. Export each name from its emitter, import it here and in the test, and key fallbacks on failure codes. (4) scripts/perf/ios-suite.ts:79 takes command apart from args[0], so a step labeled C.hover that runs press passes the catalog-partition test. The test should assert each step's command matches args[0] or its catalog alias, exempting only the sdk-forced-tree row. (5) The PR body describes layer 1 only, while the head adds a nightly schedule (one ~45 min macos-26 job a night) and 16 exclusions, and the header at perf-nightly.yml:7 still says n=5 while the nightly runs 3 rounds. (6) The sdk-forced-tree branch at ios-harness.ts:167 sets the error fields but never calls recordFailure, unlike the CLI branch at lines 191-198. Call it with result.json there too.

One question on design: runIosSuite and iosRunFinalizer in ios-harness.ts:232 run a second round, session and teardown loop beside runScenario, setupIsolation and teardownIsolation in harness.ts, and hard-code ios and the udid. Could one platform-neutral runSuite(ctx, cfg, suite) own the rounds, warmup drop, session establish, daemon-stuck restart and finalizer, with suite = {platform, baseFlags, establishArgs, scenarios, exclusions}? The Android Settings tour would then become a table on the same loop, or be deleted if it has no consumer (it is dispatch-only and report-only), which would also remove harness.ts, scenario.ts and platform-profiles.ts. I see no smaller shape for ios-suite.ts itself, since the 105-step table is data. Would the ~700-line compare and nightly layer also fit a separate PR, as the stack first had it? Folding it in doubles the review surface.

thymikee added a commit that referenced this pull request Oct 22, 2026
#3885 was rebased onto main after #3884 merged. This merge takes its new head
with the Android layer's two commits replayed on it. Suite conflicts resolve to
the renamed suite.ts and suite-harness.ts: the pinned-tree capture moves to
home-reads (iOS only) and the Android one-shot step keeps its transport check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thymikee

Copy link
Copy Markdown
Member Author

Update: this PR is now rebased on main after #3884 merged. The maintainer did the rebase, and its content matches 558da5c. The new head is 8958314, one commit on top of fed2a4c. This comment covers each open point.

1. sdk-forced-tree capture (blocking): the row was degraded, and the step is now fixed

What the old step measured. I ran the heavy-feed scenario at fed2a4c, before any change, on my own iPhone 17 Pro simulator (iOS 27.0, Xcode 27.1). All three heavy-feed/snapshot-raw-forced-tree samples came back as private-ax/recovered/ax-rejected (236–245 nodes, depth 65, truncated). The old treeBackendDegraded expectation accepted them. You were right: that row was a degraded-backend timing.

Why. The daemon does honor the pin. It skips the host AX bridge, and the request log shows snapshot_capture {"backend":"xctest"}. The runner applies the pin only to the regular plan, though (effectiveSnapshotCapturePlan in RunnerTests+SnapshotCapturePlan.swift). A --raw capture runs rawDiagnosticPlan, which is tree first and then private AX. On the heavy feed, XCTest rejects the tree (kAXErrorIllegalArgument), so private AX recovered the capture. I probed both modes on the same daemon:

capture heavy feed home screen
raw + pin private-ax/recovered/ax-rejected (runner.log: SNAPSHOT_BACKEND_FAILED backend=tree then SNAPSHOT_RECOVERED backend=private-ax) tree/healthy, no reason code (tree first, pin unused)
regular + pin fails: regular iOS snapshot presentation requires a valid viewport (runner.log: SNAPSHOT_BACKEND_FAILED backend=tree, no recovery) tree/healthy/requested-backend

The tree backend cannot serve the heavy feed at all. A tree-pinned row on that screen can only be degraded or failed.

Fix (8958314).

  • The step is now home-reads/snapshot-forced-tree. It is a regular pinned capture: forced-backend.ts no longer sends raw: true.
  • treeBackendDegraded is replaced by expect: { pinnedBackend: 'tree' }. A successful sample now fails with expectation_failed unless snapshotQuality.backend === 'tree' and reasonCode === 'requested-backend'. The runner stamps that code only on a capture that honored the pin (xcTestChannelStateFirstFailure). So two things cannot pass as a baseline: a daemon that drops the pin (which gives an unstamped tree-first or bridge capture), and a fallback backend.
  • ios-suite.test.ts covers the exact degraded shape that passed before (private-ax/recovered/ax-rejected), an unstamped tree/healthy, and a missing verdict. Dropping the reason-code check fails that test.
  • The heavy-feed XCTest gap stays visible through longpress-post, swipe, and back.

Evidence at 8958314. Command: pnpm perf --platform ios --udid <Perf Stack Worker> --rounds 3 --warmup 1 --scenarios home-reads,heavy-feed. Simulator: iPhone 17 Pro, iOS 27.0, Xcode 27.1, M4 Pro. summary.json metadata reads commit 8958314a16…, dirty: false.

summary.json, home-reads/snapshot-forced-tree (execMode: "sdk", 0 failures):

round 1 ok wall 390 daemon 385 req cb081607979f1fcf {"backend":"tree","state":"healthy","reasonCode":"requested-backend","nodeCount":50,"maxDepth":9,"truncated":false}
round 2 ok wall 301 daemon 297 req 5a3d456b331b838f {"backend":"tree","state":"healthy","reasonCode":"requested-backend","nodeCount":50,"maxDepth":9,"truncated":false}
round 3 ok wall 310 daemon 301 req d2188e4ec0bd533a {"backend":"tree","state":"healthy","reasonCode":"requested-backend","nodeCount":50,"maxDepth":9,"truncated":false}
profile.backends {"tree/healthy/requested-backend":3}, producers {"apple-runner":3}

report.md:

| snapshot-forced-tree | `snapshot` (SDK, in process; daemon = log span) | 3 | 334 ± 49 | 310 | 390 | 301 | 390 | 15% | 301 | 0 |
| snapshot-forced-tree | 284 | 0 / 0 | 0 | – | 0 | 0 | apple-runner×3 | snapshot_capture 294, ios_runner_command_send 284, ios_runner_readiness_preflight 6 |
- **home-reads/snapshot-forced-tree** — backends tree/healthy/requested-backend×3; fallbacks –

Request log 5a3d456b331b838f.ndjson (log paths trimmed):

{"ts":"2026-10-22T08:47:40.660Z","level":"info","phase":"request_start","session":"perf","requestId":"5a3d456b331b838f","command":"snapshot",…}
{"ts":"2026-10-22T08:47:40.955Z","level":"info","phase":"ios_runner_command_send",…,"durationMs":284,"data":{"command":"snapshot",…,"readOnly":true}}
{"ts":"2026-10-22T08:47:40.955Z","level":"info","phase":"snapshot_capture",…,"durationMs":294,"data":{"backend":"xctest"}}
{"ts":"2026-10-22T08:47:40.957Z","level":"info","phase":"request_success",…}

The daemon request log does not record request flags, so no line there spells out snapshotPreferredBackend. Two things show the pin took effect. First, no ios.snapshot-source.acquire phase ran: the bridge is ineligible only when a preferred backend is set (isEligible in snapshot-route.ts). Second, the runner returned requested-backend.

Every other failed sample in that run is one of the three known heavy-feed gaps (longpress-post, swipe, back, 3/3 each). The full report.md is below.

2. Two host readers, two harnesses (blocking question)

  • Host readers: now one. scripts/ios-snapshot-benchmark/host.ts moved to scripts/lib/bench-host.ts as a pure move, with its Toolchain/HostIdentity/GitRevision/Target types and its test. scripts/perf/metadata.ts now builds RunMetadata from readGitRevision, readToolchain, and readTarget, so it gets the dirty flag, the available filter, and a hard error for an unknown UDID. Its own simctl, git, and xcodebuild readers are deleted. report.md shows (dirty) when the checkout had changes. The CPU fields still come from os.cpus(), because readHostIdentity uses system_profiler and the Android nightly runs on Linux.
  • Harnesses: feat(perf): time every Android command against the fixture app #3902 merges ios-harness.ts and the Android harness into one suite-harness.ts, with one round loop, one session loop, and one daemon-stuck loop. This layer still has the old Android tour, and feat(perf): time every Android command against the fixture app #3902 deletes it.
  • forced-backend.ts: I kept its own transport. The SDK does not export its default transport. That transport (sendToDaemonOnHost → connectToDaemon) can start or replace a daemon, and a replaced daemon is no longer the daemon under test.

3. Phase-name constants

host-kit now exports EXEC_COMMAND_PHASE (/command), RETRY_PHASE (/retry), and DIAGNOSTIC_STDERR_PREFIX (/diagnostics). The daemon client exports DAEMON_REQUEST_PHASE. Each constant is declared in the emitting module and used there. profile.ts and profile.test.ts import them. #3902 does the same for the Android helper phases through @agent-device/platform-android/mechanics.

The three platform-apple phases (ios_runner_command_send, ios.snapshot-source.acquire, ios_snapshot_source_recovery) stay literal in profile.ts, with a note. Their façades (/runner, /snapshot-source) are host-free and lazy, so they cannot re-export from the emitting modules. A separate constants module re-exported there fails check:production-exports, because only scripts would consume it. I tried this and got 3 unused exports. The daemon does not statically import platform-apple, so I did not add a production consumer only to satisfy the gate. Fallback labels still use the error text. Keying them on codes is a larger change, and I left it out of this PR.

4. Non-blocking notes

  • summary-fixture.ts is now summary.fixtures.ts, the repo's test-fixture naming. Fallow's *.fixtures.ts rule covers it.
  • The perf-nightly.yml header no longer says n=5. A night runs 3 rounds, the minimum for a timing verdict, so one failed sample withholds that step's timing verdict.
  • The SDK row is marked in report.md: (SDK, in process; daemon = log span). The README names the step.

Validation at 8958314

  • vitest run scripts/perf scripts/lib scripts/ios-snapshot-benchmark: 24 files, 80 tests passed.
  • check:production-exports and lint passed.
  • pnpm check:affected --run --head 8958314a16: queued behind other workers on the shared host lock. I will add the receipt here when it finishes. CI runs at this head.
report.md (home-reads, heavy-feed at 8958314)

agent-device perf — ios

  • Commit: 8958314 (ps/ios-suite)
  • Device: Perf Stack Worker · iOS-27-0
  • Toolchain: Xcode 27.1 Build version 27A9275 · Node v26.11.1 · agent-device 0.21.31-dev
  • Host: darwin 27.0.0 · Apple M4 Pro · 12 cores · 48 GB
  • Rounds: 3 measured after 1 warmup · 2026-10-22T08:43:27.009Z → 2026-10-22T08:52:03.469Z

Times are milliseconds. See scripts/perf/README.md for each metric. The second table of each
scenario holds per-request medians from the daemon --debug request logs.

home-reads

step command n wall mean ± sd median p95 min max cv daemon fail
snapshot-i snapshot -i 3 1237 ± 180 1298 1379 1035 1379 15% 259 0
snapshot snapshot 3 1076 ± 293 984 1405 840 1405 27% 241 0
snapshot-raw snapshot --raw 3 1008 ± 352 879 1407 739 1407 35% 322 0
snapshot-forced-tree snapshot (SDK, in process; daemon = log span) 3 334 ± 49 310 390 301 390 15% 301 0
snapshot-diff snapshot --diff 3 885 ± 334 705 1270 680 1270 38% 237 0
diff-snapshot diff snapshot 3 877 ± 184 797 1087 745 1087 21% 277 0
wait-text wait text Agent Device Tester 3 647 ± 208 733 798 410 798 32% 91 0
wait-selector wait id="gesture-canary-ready" 3 709 ± 239 817 875 435 875 34% 224 0
wait-absent wait absent id="heavy-feed-title" 3 749 ± 258 692 1030 524 1030 34% 238 0
wait-stable wait stable 300 5000 3 1361 ± 404 1144 1827 1112 1827 30% 762 0
is-visible is visible id="gesture-canary-ready" 3 1212 ± 404 1251 1596 790 1596 33% 283 0
is-exists is exists text="Gesture lab" 3 889 ± 742 461 1746 461 1746 83% 8 0
get-text get text id="gesture-fling-status" 3 1168 ± 374 1343 1422 738 1422 32% 352 0
get-attrs get attrs id="drag-source" 3 1130 ± 615 1313 1633 444 1633 54% 220 0
find-text find text Gesture lab exists 3 1438 ± 781 1394 2240 680 2240 54% 294 0
batch-reads batch --steps [{"command":"get","input":{"format":"text","target":{"kind":"selector","selector":"id=\"gesture-fling-status\""}}},{"command":"is","input":{"predicate":"visible","selector":"id=\"gesture-canary-ready\""}}] 3 931 ± 525 759 1520 514 1520 56% 174 0
gesture-pan gesture pan {point:gesture-target} 40 0 300 3 1679 ± 595 1368 2364 1303 2364 35% 1031 0
gesture-pinch gesture pinch 1.4 {point:gesture-target} 3 1443 ± 216 1489 1633 1208 1633 15% 988 0
screenshot screenshot {out:home.png} 3 2322 ± 774 2701 2834 1431 2834 33% 2345 0
screenshot-overlay screenshot {out:home-refs.png} --overlay-refs 3 2610 ± 813 2298 3533 1998 3533 31% 1699 0
events events 3 327 ± 18 330 343 308 343 5% 5 0
logs-mark logs mark agent-device perf 3 285 ± 39 275 328 252 328 14% 5 0
perf-memory perf memory sample 3 2308 ± 711 2366 2987 1570 2987 31% 2044 0
react-native react-native dismiss-overlay 3 579 ± 20 588 594 556 594 3% 257 0
artifacts artifacts 3 377 ± 134 302 532 297 532 36% 4 0
step runner spawns snapshot source settle wait stabilization retries producer top phases
snapshot-i 16 3 / 72 129 – 0 0 simulator-ax-bridge×3 ios.snapshot-source.acquire 129, exec_command 72, ios_runner_command_send 16, ios_runner_readiness_preflight 12, ios.snapshot-source.present 2
snapshot 12 3 / 112 159 – 0 0 simulator-ax-bridge×3 ios.snapshot-source.acquire 159, exec_command 112, ios_runner_command_send 12, ios_runner_readiness_preflight 7, ios.snapshot-source.present 2
snapshot-raw 15 3 / 146 163 – 0 0 simulator-ax-bridge×3 ios.snapshot-source.acquire 163, exec_command 146, ios_runner_command_send 15, ios_runner_readiness_preflight 14, ios.snapshot-source.present 1
snapshot-forced-tree 284 0 / 0 0 – 0 0 apple-runner×3 snapshot_capture 294, ios_runner_command_send 284, ios_runner_readiness_preflight 6
snapshot-diff 14 3 / 84 144 – 0 0 simulator-ax-bridge×3 ios.snapshot-source.acquire 144, exec_command 84, ios_runner_command_send 14, ios_runner_readiness_preflight 13, ios.snapshot-source.present 1
diff-snapshot 30 3 / 43 115 – 0 0 simulator-ax-bridge×3 ios.snapshot-source.acquire 115, exec_command 43, ios_runner_command_send 30, ios_runner_readiness_preflight 11, ios.snapshot-source.present 1
wait-text 56 0 / 0 0 – 0 0 – ios_runner_command_send 56, ios_runner_readiness_preflight 5
wait-selector 25 3 / 85 113 – 0 0 simulator-ax-bridge×3 ios.snapshot-source.acquire 113, exec_command 85, ios_runner_command_send 25, ios_runner_readiness_preflight 10, ios.snapshot-source.present 1
wait-absent 17 3 / 94 137 – 0 0 simulator-ax-bridge×3 ios.snapshot-source.acquire 137, exec_command 94, ios_runner_command_send 17, ios_runner_readiness_preflight 10, ios.snapshot-source.present 1
wait-stable 30 6 / 225 318 – 0 0 simulator-ax-bridge×3 ios.snapshot-source.acquire 318, exec_command 225, ios_runner_command_send 30, ios_runner_readiness_preflight 15, ios.snapshot-source.present 3
is-visible 16 3 / 146 165 – 0 0 simulator-ax-bridge×3 ios.snapshot-source.acquire 165, exec_command 146, ios_runner_command_send 16, ios_runner_readiness_preflight 11, ios.snapshot-source.present 1
is-exists 0 0 / 0 0 – 0 0 simulator-ax-bridge×1 –
get-text 16 3 / 203 179 – 0 0 simulator-ax-bridge×3 exec_command 203, ios.snapshot-source.acquire 179, ios_runner_command_send 16, ios_runner_readiness_preflight 11, ios.snapshot-source.present 1
get-attrs 14 3 / 107 119 – 0 0 simulator-ax-bridge×2 ios.snapshot-source.acquire 119, exec_command 107, ios_runner_command_send 14, ios_runner_readiness_preflight 8, ios.snapshot-source.present 1
find-text 18 3 / 123 172 – 0 0 simulator-ax-bridge×3 ios.snapshot-source.acquire 172, exec_command 123, ios_runner_command_send 18, ios_runner_readiness_preflight 9, ios.snapshot-source.present 1
batch-reads 12 3 / 69 120 – 0 0 simulator-ax-bridge×3 ios.snapshot-source.acquire 120, exec_command 69, ios_runner_command_send 12, ios_runner_readiness_preflight 6, ios.snapshot-source.present 1
gesture-pan 1007 0 / 0 0 – 0 0 – ios_runner_command_send 1007, ios_runner_readiness_preflight 11
gesture-pinch 927 0 / 0 0 – 0 0 – ios_runner_command_send 927, ios_runner_readiness_preflight 10
screenshot 0 2 / 1953 0 – 0 0 – exec_command 1953
screenshot-overlay 12 5 / 1082 126 – 0 0 simulator-ax-bridge×3 exec_command 1082, ios.snapshot-source.acquire 126, ios_runner_command_send 12, ios_runner_readiness_preflight 10, ios.snapshot-source.present 2
events 0 0 / 0 0 – 0 0 – –
logs-mark 0 0 / 0 0 – 0 0 – –
perf-memory 0 4 / 2033 0 – 0 0 – exec_command 2033
react-native 245 0 / 0 0 – 0 0 apple-runner×3 snapshot_capture 252, ios_runner_command_send 245, ios_runner_readiness_preflight 4
artifacts 0 0 / 0 0 – 0 0 – –

heavy-feed

step command n wall mean ± sd median p95 min max cv daemon fail
snapshot-i snapshot -i 3 1658 ± 1056 1786 2644 543 2644 64% 278 0
snapshot snapshot 3 1456 ± 1181 997 2797 574 2797 81% 279 0
snapshot-raw snapshot --raw 3 1175 ± 1059 568 2398 559 2398 90% 265 0
press-like-selector press id="heavy-post-0-like" 3 1409 ± 414 1292 1868 1066 1868 29% 703 0
press-like-settle press id="heavy-post-1-like" --settle 3 3117 ± 707 3100 3833 2419 3833 23% 1899 0
press-like-ref press {ref:heavy-post-0-like} 3 1268 ± 409 1461 1545 798 1545 32% 443 0
press-like-point press {point:heavy-post-0-like} 3 1179 ± 498 947 1751 839 1751 42% 444 0
wait-text wait text Liked posts: 2 3 866 ± 539 671 1475 452 1475 62% 66 0
is-visible is visible id="heavy-post-1-like" 3 843 ± 96 854 933 742 933 11% 300 0
get-text get text id="heavy-feed-last-action" 3 916 ± 822 453 1865 430 1865 90% 11 0
find-text find text Post 1: exists 3 905 ± 409 796 1357 561 1357 45% 270 0
longpress-post longpress id="heavy-post-1" 800 3 – – – – – – – 3
scroll-down scroll down 3 14992 ± 5053 13164 20704 11107 20704 34% 12597 0
snapshot-i-scrolled snapshot -i 3 1482 ± 121 1433 1620 1393 1620 8% 906 0
scroll-down-settle scroll down --settle 3 3275 ± 1081 3289 4349 2187 4349 33% 2944 0
swipe swipe 200 650 200 300 3 – – – – – – – 3
back back 3 – – – – – – – 3
step runner spawns snapshot source settle wait stabilization retries producer top phases
snapshot-i 13 3 / 72 210 – 0 0 simulator-ax-bridge×3 ios.snapshot-source.acquire 210, exec_command 72, ios_runner_command_send 13, ios_runner_readiness_preflight 8, ios.snapshot-source.present 3
snapshot 13 3 / 63 198 – 0 0 simulator-ax-bridge×3 ios.snapshot-source.acquire 198, exec_command 63, ios_runner_command_send 13, ios_runner_readiness_preflight 6, ios.snapshot-source.present 2
snapshot-raw 14 3 / 48 210 – 0 0 simulator-ax-bridge×3 ios.snapshot-source.acquire 210, exec_command 48, ios_runner_command_send 14, ios_runner_readiness_preflight 7, ios.snapshot-source.present 1
press-like-selector 446 3 / 58 204 – 0 0 simulator-ax-bridge×3 ios_runner_command_send 446, ios.snapshot-source.acquire 204, exec_command 58, ios_runner_readiness_preflight 9, ios.snapshot-source.present 3
press-like-settle 478 9 / 344 783 1001 0 0 simulator-ax-bridge×3 ios.snapshot-source.acquire 783, ios_runner_command_send 478, exec_command 344, ios_runner_readiness_preflight 35, ios.snapshot-source.present 12
press-like-ref 427 0 / 0 0 – 0 0 – ios_runner_command_send 427
press-like-point 429 0 / 0 0 – 0 0 – ios_runner_command_send 429
wait-text 39 0 / 0 0 – 0 0 – ios_runner_command_send 39, ios_runner_readiness_preflight 4
is-visible 11 3 / 83 233 – 0 0 simulator-ax-bridge×3 ios.snapshot-source.acquire 233, exec_command 83, ios_runner_command_send 11, ios_runner_readiness_preflight 8, ios.snapshot-source.present 2
get-text 0 0 / 0 0 – 0 0 simulator-ax-bridge×1 –
find-text 12 3 / 95 229 – 0 0 simulator-ax-bridge×3 ios.snapshot-source.acquire 229, exec_command 95, ios_runner_command_send 12, ios_runner_readiness_preflight 6, ios.snapshot-source.present 1
longpress-post – – – – – – – –
scroll-down 979 5 / 2393 0 – 0 27 – ios_runner_readiness_preflight 9985, exec_command 2393, ios_runner_command_send 979, ios_runner_startup_stop_stale_session 682, ios_runner_session_startup_timings 471
snapshot-i-scrolled 38 6 / 177 484 – 889 0 simulator-ax-bridge×3 post_gesture_snapshot_stabilized 889, ios.snapshot-source.acquire 484, exec_command 177, ios_runner_readiness_preflight 61, ios_runner_command_send 38
scroll-down-settle 600 9 / 407 1024 2395 1396 0 simulator-ax-bridge×3 ios.snapshot-source.acquire 1024, post_gesture_snapshot_stabilized 781, ios_runner_command_send 600, exec_command 407, ios_runner_readiness_preflight 50
swipe – – – – – – – –
back – – – – – – – –

Backends and fallbacks

  • home-reads/snapshot-forced-tree — backends tree/healthy/requested-backend×3; fallbacks –
  • heavy-feed/snapshot-i — backends –; fallbacks simulator-ax-bridge: truncated at 32 levels×3
  • heavy-feed/snapshot — backends –; fallbacks simulator-ax-bridge: truncated at 32 levels×3
  • heavy-feed/snapshot-raw — backends –; fallbacks simulator-ax-bridge: truncated at 32 levels×3
  • heavy-feed/press-like-selector — backends –; fallbacks simulator-ax-bridge: truncated at 32 levels×3
  • heavy-feed/press-like-settle — backends –; fallbacks simulator-ax-bridge: truncated at 32 levels×3
  • heavy-feed/is-visible — backends –; fallbacks simulator-ax-bridge: truncated at 32 levels×3
  • heavy-feed/get-text — backends –; fallbacks simulator-ax-bridge: truncated at 32 levels×1
  • heavy-feed/find-text — backends –; fallbacks simulator-ax-bridge: truncated at 32 levels×3
  • heavy-feed/scroll-down — backends –; fallbacks ios_runner_readiness_preflight: healthy_mutation_stale×1, ios_runner_session_invalidated: runner_readiness_preflight_failed_before_command_send×1, retry×1, ios_runner_simulator_app_terminate_failed×1
  • heavy-feed/snapshot-i-scrolled — backends –; fallbacks simulator-ax-bridge: truncated at 32 levels×3
  • heavy-feed/scroll-down-settle — backends –; fallbacks simulator-ax-bridge: truncated at 32 levels×3, post_gesture_snapshot_stabilization_timeout×1

Failures

  • heavy-feed/longpress-post — 3/3 failed: COMMAND_FAILED — NSInternalInconsistencyException: Invalid parameter not satisfying: point.x != INFINITY && point.y != INFINITY
  • heavy-feed/swipe — 3/3 failed: COMMAND_FAILED — Active app interaction viewport is unavailable
  • heavy-feed/back — 3/3 failed: COMMAND_FAILED — in-app back control is not available

Coverage

Measured commands (19): artifacts, back, batch, diff, events, find, gesture, get, is, logs, longpress, perf, press, react-native, screenshot, scroll, snapshot, swipe, wait.

  • audio not measured: Host audio capture needs a macOS audio permission that hosted runners do not grant.
  • boot not measured: Simulator power cycle: CoreSimulator boot time dominates and it ends the session under test.
  • fold not measured: iPhone Duo only; the suite device has one panel.
  • hardware-button not measured: The iOS simulator runtime refuses hardware-button presses.
  • hover not measured: The iOS runtime refuses hover; it is a web pointer contract.
  • install not measured: Bundle provisioning: simctl copy time dominates; the job installs the fixture once.
  • install-from-source not measured: Bundle provisioning from a source build; the job installs a cached Release fixture.
  • push not measured: The fixture holds no notification authorization, so simctl refuses the payload; iOS push coverage is contract-only.
  • reinstall not measured: Bundle provisioning: simctl copy time dominates and it resets the app under test.
  • replay not measured: Composite runner: a replay is the sum of the measured commands it runs.
  • shutdown not measured: Simulator power cycle: it ends the session and the runner under test.
  • telephony not measured: The iOS simulator runtime refuses simulated calls and SMS.
  • test not measured: Composite runner: a test suite is the sum of the measured commands it runs.
  • tv-remote not measured: tvOS only.
  • uninstall not measured: Bundle provisioning: it removes the app under test.
  • viewport not measured: The iOS runtime refuses viewport; it resizes web viewports only.

Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com

Only scripts read it, so a root export has no production consumer and fails the
fallow audit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thymikee

Copy link
Copy Markdown
Member Author

Gate update: the first check:affected at 8958314 failed the fallow audit on one finding. DAEMON_REQUEST_PHASE was exported from src/daemon-client/daemon-client.ts, but only scripts read it, so the export has no production consumer. That is the same constraint that keeps the platform-apple phase names literal. 9e9ccc3 reverts it to a literal in profile.ts and leaves src/ untouched versus main.

pnpm check:affected --run --head 9e9ccc3add then passed: exactCommit: true, FAIL-OPEN full set (the diff touches package.json and vitest.config.ts), 36 checks passed, and 25 GitHub-authoritative checks were skipped. The device evidence above is from 8958314, and this commit does not change the suite.

Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com

thymikee and others added 2 commits October 22, 2026 12:33
Brings in #3892, which fixes the iOS simulator logs stop hang.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* origin/main:
  perf(ios): read each resolved text input through one snapshot (#3882)
  fix(gate): take the host validation lock for heavy gates (#3900)
  fix(logs): keep an early iOS simulator logs stop from hanging the session (#3892)
  fix(apple): clean the runner derived path the runner uses (#3897)
  test(provider-scenarios): open deadline scenarios without the test IME (#3903)
  test(host-kit): stub os.homedir for the exec-trace late-close test (#3898)
#3892 fixed the iOS simulator logs stop hang, so the untimed wait before
logs-stop is no longer needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thymikee

Copy link
Copy Markdown
Member Author

Head is now aed5aa7:

Device check on my own iPhone 17 Pro simulator (iOS 27.0) at aed5aa7, with summary.json reporting dirty: false. Command: pnpm perf --platform ios --scenarios automation --rounds 3 --warmup 1. All 16 automation steps passed in every round, including the warmup, with no daemon_stuck.

automation/logs-start  3/3 ok  daemon 577 / 499 / 503 ms
automation/logs-stop   3/3 ok  daemon  31 /  33 /  28 ms

pnpm check:affected --run --head aed5aa7e94 passed: exactCommit: true, 36 checks passed, and 25 GitHub-authoritative checks were skipped.

Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com

@thymikee

Copy link
Copy Markdown
Member Author

The earlier notes from fed2a4c are now addressed, and I found no new problems in the code at aed5aa7. The checks are green: 23 checks, none failing. There are no conflicts. I did not run vitest, check:affected, check:production-exports or any device run. The 3/3 run at aed5aa7 and the tree/healthy/requested-backend x3 run at 8958314 rest on your comments. I did not open the CI job logs. I did not review #3902, so I could not confirm that it merges ios-harness.ts and the Android harness into one suite-harness.ts. One question: if the nightly checkout already holds untracked files when collectMetadata runs, would dirty read true every night? Only the report label reads it, so this is minor. The compare step has not run against real scheduled baselines yet, so its paging and selection against the live GitHub API are still unverified. Before merge, the maintainer needs to accept moving the single suite loop to the stacked #3902 and folding in the compare layer (#3886). Earlier notes 4 and 6 stay optional.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Oct 22, 2026
@thymikee
thymikee merged commit d217300 into main Oct 22, 2026
23 checks passed
@thymikee
thymikee deleted the perf/ios-command-suite branch October 22, 2026 12:07
thymikee added a commit that referenced this pull request Oct 22, 2026
#3885 squash-merged as d217300 with the same scripts/perf and
perf-nightly.yml content as its final head aed5aa7, so every
conflict keeps this branch's side.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
thymikee added a commit that referenced this pull request Oct 22, 2026
* origin/main:
  perf(ios): confirm fill focus from the tapped input before the keyboard (#3906)
  feat(perf): time every iOS command against the fixture app (#3885)
  fix(ios): stop the XCTest-channel penalty from re-arming itself (#3896)
  test(android): pin that a child vanishing mid-read reads as gone (#3905)
  ci: skip device smoke lanes when a PR changes no lane input (#3904)
  fix(ios): verify deep-tree back on the sole screen (#3894)
  fix(ios): synthesize long presses and the gesture viewport on deep trees (#3893)
  perf(ios): read each resolved text input through one snapshot (#3882)
  fix(gate): take the host validation lock for heavy gates (#3900)
  fix(logs): keep an early iOS simulator logs stop from hanging the session (#3892)
  fix(apple): clean the runner derived path the runner uses (#3897)
  test(provider-scenarios): open deadline scenarios without the test IME (#3903)
  test(host-kit): stub os.homedir for the exec-trace late-close test (#3898)
  feat(test-app): add a Bluesky-depth heavy feed screen (#3884)
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