Skip to content

fix(mcp): stop idle window tracking from burning CPU on app churn - #1013

Merged
steipete merged 1 commit into
mainfrom
fix/mcp-idle-app-churn-cpu-pr
Oct 10, 2026
Merged

steipete merged 1 commit into
mainfrom
fix/mcp-idle-app-churn-cpu-pr

Conversation

@steipete

Copy link
Copy Markdown
Collaborator

Fixes #1005.

Summary

An idle peekaboo mcp server burned CPU whenever any unrelated application launched or quit: about 0.5–0.7 s per launch+quit on a Mac with ~600 running applications. Each server pays this separately, and so does any other host running WindowTrackerService's eight global AX watchers.

The cost was in AXorcist's AXWorkspaceApplicationMonitor, which is fixed upstream in openclaw/AXorcist#96 (merged as 700e5ac). This PR bumps the submodule to that commit and adds the changelog entry. Two independent causes:

  1. NSRunningApplication.hash is constant. Every running app reports the same hash (verified: 92 apps → 1 distinct value). Sets and dictionaries keyed by NSRunningApplication therefore did a linear isEqual: / _LSCompareASNs scan per operation, making each snapshot O(N²). The reporter's release-binary samples point straight at this. AXorcist now indexes entries by the identity of the retained wrapper (NSWorkspace returns the same objects across snapshots) and falls back to isEqual: only for unseen wrappers.
  2. A LaunchServices lookup per application per event. Every launch or quit bumps LaunchServices' application-information seed, which invalidates AppKit's cached dynamic properties on every wrapper. Re-reading each known application's processIdentifier on every snapshot then cost a cold LaunchServices lookup per app. A Time Profiler trace attributed 75% of the remaining CPU to this. AXorcist now skips the read for a wrapper it already resolved, while new or equal-but-distinct wrappers are still read, so PID-transition, readiness and replacement contracts are unchanged.

Window tracking for newly launched applications is unchanged: new wrappers are always read and delivered as launches, and AXorcist's tests assert this, including for processes that never report finished launching. No observers are dropped.

Measurements

Release builds of Peekaboo, idle peekaboo mcp --no-remote after the MCP handshake, ~594 running applications, and 60 short-lived titled processes launched and quit (the issue's methodology). Two interleaved rounds:

Build CPU per unrelated launch+quit
main 0.668 s / 0.684 s
Identity index only 0.108 s / 0.180 s
This PR (AXorcist 700e5ac) 0.025 s / 0.021 s

Idle CPU without churn is unchanged, at about 0.2–0.3 s per 30 s. That meets the issue's restoration check of at most about 3 s for 120 launches.

Notes

  • The root Package.swift still pins AXorcist exact: "0.2.1" for external SwiftPM consumers, and scripts/test-swiftpm-consumer.sh asserts that pin. External consumers such as OpenClaw get the fix once AXorcist 0.2.2 is tagged and the pin is aligned at the next release, following the precedent of build(deps): align consumers on AXorcist 0.2.1 #925. Peekaboo's own CLI, MCP server and app build from the submodule, so they get it now.
  • AXorcist validation: swift test (294 tests), make check, and CI on Swift 6.2.4 and 6.4.0 all pass. The constant-hash and no-reread regressions fail on the old code. Codex autoreview was scoped-clean on each commit.

@clawsweeper

clawsweeper Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

🦞👀
ClawSweeper picked this up.

Pull request received. I will update this pull request when review starts.

ClawSweeper review blocked

Automated review did not run, so no review verdict was produced.

Reason: The input-safety check could not safely complete for this revision.

Automatic review is on hold, including after source changes. Request a fresh re-review after resolving the failure; maintainers can also release the hold through an intentional scanner-policy update.

Next step: No contributor action is requested. Maintainers should inspect the linked workflow run.

View the workflow run.

@cursor

cursor Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

PR Summary

Low Risk
Performance-only AXorcist monitor indexing with preserved launch/PID contracts; Peekaboo’s own builds use the submodule, while root SwiftPM exact: "0.2.1" consumers wait for a tagged AXorcist release.

Overview
Fixes idle CPU spikes when unrelated apps launch or quit by updating the AXorcist submodule (upstream 700e5ac) and documenting the change in Unreleased.

AXWorkspaceApplicationMonitor no longer treats every NSRunningApplication as colliding in hash-based sets (avoiding O(N²) work on large app lists) and stops re-reading processIdentifier on wrappers already resolved, removing per-event LaunchServices churn. Peekaboo builds that use the submodule—peekaboo mcp, CLI, and app hosts running WindowTrackerService—pick this up immediately; behavior for new launches and PID transitions is intended to stay the same.

The visible diff here is the changelog entry (~0.68s → ~0.02s CPU per unrelated launch+quit at ~600 apps); the functional change is the dependency pointer.

Reviewed by Cursor Bugbot for commit 2f5330f. Bugbot is set up for automated code reviews on this repo. Configure here.

@steipete
steipete merged commit d1f7a79 into main Oct 10, 2026
12 checks passed
@steipete
steipete deleted the fix/mcp-idle-app-churn-cpu-pr branch October 10, 2026 08:10
steipete added a commit that referenced this pull request Oct 10, 2026
Move the AXorcist submodule to the signed v0.2.2 tag and the public root Package.swift pin from exact 0.2.1 to exact 0.2.2 so SwiftPM consumers of Peekaboo's library products get the idle-observer CPU fix (#1005, #1013); align docs/building.md and the SwiftPM consumer contract. Verification: scripts/test-swiftpm-consumer.sh resolved 0.2.2 from the published tag and built all four public products; macOS CI green (CLI, Core, Tachikoma, app builds, SwiftFormat, SwiftLint).
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.

fix(mcp): idle peekaboo mcp spends about half a second of CPU for each app that starts and quits elsewhere on the Mac

1 participant