fix(cua-driver-rs)(windows): stop idle overlay CPU + orphan mcp child (#1808) - #1933
Conversation
…#1808) The Windows agent-cursor overlay render timer ran at ~125 Hz unconditionally: every WM_TIMER tick allocated a full virtual-screen tiny-skia pixmap, swizzled it RGBA->BGRA pixel-by-pixel, and blitted it via UpdateLayeredWindow — even when no cursor was animating and the pointer was static. An idle `cua-driver mcp` therefore pinned 60-85% of a CPU core (issue #1808), and long-lived instances accumulated CPU-hours. Part A (idle CPU): mirror the macOS fix (#1865). Add a `needs_frame_tick` predicate (in-flight path / spring / click pulse / unfinished idle-fade) and gate the composite+blit+z-order behind it. The render timer is now re-armed between an ACTIVE cadence (~125 Hz, smooth animation) and a slow IDLE heartbeat (250 ms) once every cursor goes quiescent. `send_command` / `remove_cursor` call `wake_overlay()` to flip back to ACTIVE within ~8 ms via a cross-thread SetTimer, so the first move after idle is not delayed. A final settle frame is still emitted as animations finish, so the layered window is left in its resting/cleared state before the loop parks. No full-screen pixmap allocation, no RGBA->BGRA copy, no UpdateLayeredWindow while idle. Part B (orphan on disconnect): the overlay runs on a detached STA thread with its own Win32 message loop, so returning from `async_main` after the stdio MCP server loop ended (stdin EOF) was not guaranteed to tear it down promptly. The in-process Windows/Linux `mcp` path now `std::process::exit`es once `server::run` returns, mirroring the macOS arm, so the overlay thread dies with the process the moment the client disconnects. Adds headless unit tests for the quiescent-sentinel state, the active-animation state, and the click-pulse-then-quiescent transition. Verified `platform-windows` cross-compiles cleanly for x86_64-pc-windows-msvc and all platform-windows lib tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThe Win32 cursor overlay render loop is changed from a fixed-cadence timer to an event-driven idle gate: ChangesWindows Idle Render Gating and MCP Cleanup
Sequence Diagram(s)sequenceDiagram
participant Client as MCP Client
participant main as async_main
participant send_command as send_command / remove_cursor
participant WM_TIMER as Win32 WM_TIMER
participant UpdateLayeredWindow
Client->>main: stdin EOF / pipe closed
main->>main: capture server::run() result, log error if any
main->>main: process::exit(0 or 1)
Note over send_command, WM_TIMER: Normal operation (overlay active)
send_command->>send_command: enqueue OverlayMsg
send_command->>WM_TIMER: wake_overlay() → SetTimer(ACTIVE)
WM_TIMER->>WM_TIMER: drain commands → had_msg=true
WM_TIMER->>WM_TIMER: tick cursors → needs_tick
WM_TIMER->>UpdateLayeredWindow: composite pixmap
Note over WM_TIMER: After animation completes
WM_TIMER->>WM_TIMER: had_msg=false, needs_tick=false
WM_TIMER->>WM_TIMER: SetTimer(IDLE) — skip UpdateLayeredWindow
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Linux visual regression artifactsMatrix jobs now run independently. Download visual artifacts from this workflow run.
|
…ash churn) The overlay fix needs no new dependencies; an incidental cargo build had re-synced the workspace member versions (0.5.3 -> 0.5.6) in Cargo.lock, which fetchCargoVendor hashes, breaking the Nix cargoHash and turning every Linux nix job red. Restore Cargo.lock to main's committed state so the hash stays valid. (The Cargo.toml/Cargo.lock version drift on main is a separate pre-existing issue, not this PR's concern.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n bump (kill version drift) (#1934) The release bump edits only Cargo.toml's `[workspace.package] version`, leaving Cargo.lock's 9 workspace-member versions stale. Nix pinned a manual `cargoHash` that hashes the vendored lockfile, so the committed lock + hash stayed mutually consistent (and green) only by never touching the lock — a frozen-inconsistent state that detonated the instant anyone ran `cargo build`, which re-locked the members and invalidated the hash, turning every nix job red (hit on PR #1933). The drift had silently accumulated across 0.5.3 -> 0.5.6. Three changes so this can't recur: 1. package.nix uses `cargoLock.lockFile` instead of `cargoHash`. importCargoLock derives each dep's hash from the lockfile itself, so there is NO hash to hand-maintain — Cargo.lock can change freely and the build keeps working. Verified the old "apple crates unreachable from crates.io" rationale is false: apple-cf/apple-metal/objc2 are all registry crates and there are zero git deps, so no `outputHashes` are needed. 2. package.nix reads `version` from Cargo.toml's `[workspace.package]` instead of a hardcoded literal (which had drifted to 0.5.3). 3. Re-locked Cargo.lock to 0.5.6, and the bump workflow now runs `cargo update --workspace` after bump2version and folds the synced lockfile into the bump commit (moving the tag), so the manifest and lockfile ship in sync every release. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Fixes #1808.
Root cause
The Windows agent-cursor overlay (
crates/platform-windows/src/overlay.rs) drives its render loop with a Win32SetTimerfiring every 8 ms (~125 Hz). TheWM_TIMERhandler ran the full render pipeline on every tick, unconditionally:tiny_skia::Pixmap,UpdateLayeredWindowblit,SetWindowPos.This happened even with the cursor static and no client activity, so an idle
cua-driver mcppinned 60–85% of one core (reporter measured--no-overlay→ 0%). This is the same class of bug as the macOS render loop fixed in #1865 — actually worse, because Windows ran at 125 Hz with a full-screen pixel swizzle.Second failure mode: the overlay runs on a detached STA thread with its own
GetMessageWloop. The in-process Windowsmcp(stdio) path returned fromasync_mainon stdin EOF but never force-exited, so on some disconnects the process lingered with the overlay loop still spinning → an orphan accumulating CPU over a day.Fix
A — idle CPU (event-driven render):
RenderState::needs_frame_tick()/render_map_needs_frame_tick()mirroring macOS fix(cua-driver)(macos): stop idle overlay frame ticks #1865: true only while a cursor has an in-flight glide path, a spring-settle, a click pulse, or an unfinished idle-fade.UpdateLayeredWindow+ z-order behindhad_msg || needs_tick || was_active. A fully-quiescent tick does no compositing at all and a final settle frame is still emitted as animations finish (so the layered window is left in its resting/cleared state).send_command/remove_cursorcallwake_overlay()to flip back to ACTIVE within ~8 ms via a cross-threadSetTimer, so the first move after idle isn't delayed.B — orphan on disconnect:
mcppath nowstd::process::exites onceserver::runreturns (stdin EOF / fatal I/O), mirroring the macOS arm. The detached overlay thread dies with the process the moment the transport closes.Tests
cargo test -p platform-windows --lib→ 13 passed (incl. the 3 new).cargo check -p platform-windows --target x86_64-pc-windows-msvc→ clean (the real Windows compile gate for the cfg-gated code). The fullcua-driverbinary cross-compile only fails on theringC dep needing Windows headers on this macOS host — CI's Windows job is the authoritative build gate.Notes
--no-overlayremains a full workaround for headless runs.🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
Bug Fixes
New Features
--no-overlayoption to disable the agent cursor for headless runs.