Skip to content

perf(mac): reduce native polling and async IO stalls#534

Open
JustYannicc wants to merge 4 commits into
SuperCmdLabs:mainfrom
JustYannicc:codex/consolidate-native-async-io
Open

perf(mac): reduce native polling and async IO stalls#534
JustYannicc wants to merge 4 commits into
SuperCmdLabs:mainfrom
JustYannicc:codex/consolidate-native-async-io

Conversation

@JustYannicc

@JustYannicc JustYannicc commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

What changed

  • Consolidates macOS/native/audio/camera and async IO performance fixes into one upstream PR.
  • Gates auto-quit AppleScript probes until a tracked app is actually due, and only runs recording/music probes when they can affect a quit decision.
  • Replaces AX BFS removeFirst() traversal with index-cursor queues in native selected-text/caret paths.
  • Moves AeroSpace workspace reconciliation to an async queued mover so launcher show paths do not block on aerospace commands.
  • Replaces speech settings polling with initial settings load plus onSettingsUpdated listener sync.
  • Avoids audio buffer conversion while idle unless a meter update is due.
  • Coalesces notes autosave persistence and clipboard history writes into async atomic saves with explicit flushes on shutdown/restart paths.
  • Uses a single PNG Blob encode for camera capture and object URLs for previews.
  • Adds focused regression and measurement scripts for the consolidated paths.

Why

Several macOS-facing paths were doing synchronous or repeated work during idle polling, launcher visibility changes, autosave bursts, clipboard history changes, and camera capture. Consolidating the related fixes lets upstream review the native polling and async IO stall reduction together while preserving existing user-facing behavior.

Compatibility impact

  • Auto-quit behavior is preserved for due apps; protected, frontmost, recording, and music-playback safeguards still apply, but idle ticks no longer run unnecessary AppleScript probes.
  • AeroSpace integration remains best-effort and fire-and-forget; missing binaries are cached as unavailable, while transient command failures still retry later.
  • Speech voice/model settings still sync on startup and live settings changes without a polling interval.
  • Notes and clipboard data continue to persist atomically, with pending writes flushed before normal quit and update restart paths.
  • Camera captures still save and copy PNG files, while previews use revocable object URLs instead of a second base64 encode.

How tested

  • LSP diagnostics: no errors for touched TypeScript/TSX files and touched Swift files.
  • LSP reference checks for async clipboard handlers/flushes, notes flushes, and the AeroSpace mover.
  • node scripts/test-auto-quit-manager.mjs
  • node scripts/test-auto-quit-manager.mjs --metrics
  • node scripts/test-aerospace-workspace.mjs
  • node scripts/test-speak-settings-listener.mjs
  • node scripts/test-notes-async-saves.mjs
  • node scripts/test-clipboard-history-persistence.mjs
  • node scripts/test-camera-capture-single-encode.mjs
  • swift scripts/bench-ax-bfs-queue.swift
  • swiftc -O -o /tmp/supercmd-native-validation/get-selected-text src/native/get-selected-text.swift -framework Foundation -framework ApplicationServices -framework AppKit
  • swiftc -O -o /tmp/supercmd-native-validation/emoji-trigger-monitor src/native/emoji-trigger-monitor.swift src/native/ax-caret-query.swift -framework AppKit -framework ApplicationServices
  • swiftc -O -o /tmp/supercmd-native-validation/audio-capturer src/native/audio-capturer.swift -framework AVFoundation -framework Foundation
  • pnpm exec tsc -p tsconfig.main.json --noEmit --pretty false
  • pnpm test
  • pnpm exec tsc -p tsconfig.renderer.json --noEmit --pretty false fails on the standalone branch because of existing repo-wide renderer typing debt unrelated to this consolidation.

Performance evidence

  • Auto-quit no-due-app scenario over 3 ticks: osascript=0, frontmost=0, recording=0, music=0, quit=0.
  • Auto-quit due non-music app scenario: only frontmost=1, recording=1, quit=1, music=0.
  • AX BFS benchmark: removeFirst=2547.65ms, cursor queue 1313.94ms, speedup 1.94x.
  • AeroSpace test confirms slow command in flight does not block a zero-delay timer and concurrent requests coalesce into one follow-up run.
  • Notes autosave burst: 20 repeated updates persisted with writes=1, syncWrites=0, asyncWrites=1, latest state flushed.
  • Clipboard rapid additions: 100 additions coalesced to coalescedWrites=1, blockingMs=0.42, and quit/clear/delete flush paths are covered.
  • Camera test confirms production capture calls toBlob once, never calls toDataURL, and revokes preview object URLs on clear/unmount.
  • Speech test confirms settings sync uses initial load plus onSettingsUpdated listener and no polling interval.

Stack validation

Standalone renderer typecheck still hits existing repo-wide renderer typing debt. Applied this consolidation over #532 (fix(validation): restore project checks, JustYannicc:codex/consolidate-validation-checks) and validated the combined stack:

  • pnpm exec tsc -p tsconfig.renderer.json --noEmit --pretty false
  • pnpm test

Replaces

@shobhit99

Copy link
Copy Markdown
Contributor

Reviewed the full diff plus the surrounding code (settings broadcast wiring, notes-store callers, clipboard call sites, Swift BFS bounds). This is a clean, well-tested batch of behavior-preserving perf refactors — moving synchronous/native work off idle-poll and launcher-show hot paths.

Strengths

  • auto-quit-manager.ts: AppleScript probes (frontmost/recording/music) are now correctly gated behind actual due-ness. The lazy timestamp refresh still protects the active app — a frontmost app can enter dueCandidates but is filtered out of nonFrontmostDueCandidates and its lastFrontmostAt is refreshed on the same tick, so nothing is quit prematurely. Recording still hard-pauses quits; music is only probed when a due music app would otherwise be killed. Excellent test matrix.
  • aerospace-workspace.ts: Replacing three blocking execFileSync (up to 500ms each) on the launcher-show path with an async coalescing mover is the biggest UX win here. The coalesce loop (do { queued=false; await reconcileOnce() } while (queued && canAttemptMove()) + synchronous finally) has no interleaving gap, so no requests are lost. ENOENT-caching preserved.
  • clipboard/notes: atomic temp+rename async writes, debounced and coalesced, with a solid before-quit preventDefault()→flush→app.quit() guard and idempotent flush* helpers. Flush is also wired into the update-restart path.
  • Swift: index-cursor queues replace O(n) removeFirst() with identical BFS order (~2x per the bench). Audio idle path skips convertBuffer, and the new multi-format meter (float32/64/int16/int32 + interleaved stride) is strictly more robust than the old float32-mono assumption.
  • useSpeakManager: verified onSettingsUpdated exists (preload.ts:361) and broadcastSettingsToAllWindows fires settings-updated to mainWindow on the generic saveSettings(patch) handler — so dropping the 2s poll for the listener does not regress live TTS voice sync. speakOptionsVoiceRef correctly avoids stale-closure re-subscription.

Concerns / potential bugs (all minor — none blocking)

  1. main.ts before-quit (~19397): shouldFlushClipboard is gated by updateRestartInProgress but shouldFlushNotes is not. If a debounced notes save is queued in the small window between the restart path's own flushNotesToDisk() and quitAndInstall()'s before-quit, this handler will preventDefault() + app.quit(). On darwin autoInstallOnAppQuit = true is set so the update still installs, so it's low-risk — but the asymmetric gating is worth a comment or aligning both flags.
  2. ax-caret-query.swift:163 — the index-cursor queue has maxDepth=6 but (unlike get-selected-text.swift's maxElements=240) no element cap. Since nodes are no longer shifted off, transient peak memory is now "total visited" rather than "max frontier width" for a deep+wide AX tree with no selection. In practice the focused-child fast path + early return keep it tiny and the array is freed on return (not a leak), but adding a small element cap here would make it symmetric and pathological-case-proof.
  3. Behavior change to flag for reviewers: notes/clipboard persistence is now debounced (~250ms) rather than synchronous. Graceful quit/restart paths flush, but a hard kill (SIGKILL/power loss) can now lose up to ~250ms of the most recent edits — a real semantic shift from the previous synchronous writes. Reasonable tradeoff, just noting it.

Suggestions

  • Consider gating notes and clipboard flush identically in before-quit, or a one-line comment explaining why notes isn't gated on restart.
  • Optionally add a maxElements guard to the ax-caret-query BFS to match get-selected-text.

🟡 Approve with minor comments

🤖 Automated review by Claude Code (Opus 4.8).

@JustYannicc

JustYannicc commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the review notes from the async/native perf pass:

  • Aligned the before-quit updater restart behavior for notes + clipboard: both now bypass the generic async-storage quit interceptor once the updater restart handoff is in progress, with an inline comment documenting that the restart path already flushes before quitAndInstall().
  • Added a maxElements = 240 cap to src/native/ax-caret-query.swift BFS traversal and bounded both traversal and enqueue growth, matching the selected-text helper's pathological-case guard.
  • Documented the debounced persistence semantics at the notes/clipboard debounce constants, and added source assertions so the 250ms debounce + graceful flush contract stays visible in tests. Graceful quit/restart paths flush; hard kill/power loss can still lose up to the debounce window, which is the intended async persistence tradeoff.

Verification:

  • node --test scripts/test-aerospace-workspace.mjs scripts/test-auto-quit-manager.mjs scripts/test-camera-capture-single-encode.mjs scripts/test-clipboard-history-persistence.mjs scripts/test-notes-async-saves.mjs scripts/test-speak-settings-listener.mjs ✅ 20 passed
  • swiftc -parse src/native/ax-caret-query.swift
  • swift scripts/bench-ax-bfs-queue.swift ✅ cursor traversal remains faster than removeFirst
  • git diff --check

Also rechecked GraphQL review state: reviewThreads(first:100) is empty; the only maintainer feedback found was the top-level comment addressed above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants