fix: refresh SCShareableContent on topology change, not per call#12
Open
divanshu-go wants to merge 1 commit into
Open
fix: refresh SCShareableContent on topology change, not per call#12divanshu-go wants to merge 1 commit into
divanshu-go wants to merge 1 commit into
Conversation
4fccb0f to
001aab1
Compare
001aab1 to
69c64d0
Compare
Fixes screenpipe#1. SCShareableContent.current forces replayd to enumerate every window, app, and display system-wide, building an NSSet of NSDictionary metadata with O(n²) equality checks. Apple designed it for one-time initialization; this crate called it on every Window::all() / Monitor::all() / one-shot capture / stream filter build — for an always-on consumer that's an enumeration every few seconds, which pins replayd at 100% CPU (issue screenpipe#1's profiling, Hyperion #1921) and feeds its slow memory bloat (alt-tab-macos #4194). A fixed TTL would still enumerate ~17k times/day; the prior art in Hyperion #1921 is explicit — fetch once, refresh only when topology actually changes. The snapshot is now cached and revalidated per call against a cheap window-topology fingerprint from CGWindowListCopyWindowInfo — the pre-ScreenCaptureKit WindowServer query (sub-millisecond, no replayd; the pre-SCK backend used it wholesale with no replayd cost). The fingerprint hashes every onscreen window's id, owner pid, and title (title changes can flip exclusion-pattern matches downstream) plus the online display count, so: - a static desktop performs ZERO replayd enumerations; - a new / closed / retitled window is picked up on the very next call — fresher than any TTL; - a 300s safety-net TTL bounds staleness the fingerprint can't see, and a 5s blind TTL applies if CGWindowList is ever unavailable. Correctness notes: - 0-display snapshots (SCK's stale post-wake state) are never cached, so the existing wake-retry loop can't get pinned behind the cache. - stop_all_streams() also drops the cache (streams are torn down wholesale on wake / display changes); hosts can call the exported invalidate_shareable_content_cache() directly. - Ids and exclusion filters derive from the same snapshot, staying mutually consistent. Measured (M-series, 151 onscreen windows): uncached call = ~75ms of replayd enumeration; cached call = ~0.35ms signature check. tests/ cache_timing.rs exercises the path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
69c64d0 to
eaff777
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1.
Problem
SCShareableContent.currentforces replayd to enumerate every window, app, and display system-wide, building an NSSet of NSDictionary metadata with O(n²) equality checks. Apple designed it for one-time initialization — but this crate called it on everyWindow::all()/Monitor::all()/ one-shot capture / stream-filter build. For an always-on consumer that's an enumeration every few seconds: replayd pinned at 100% in-[NSSetM addObject:]and jetsam-killed in a loop (#1's profiling), plus the slow replayd memory bloat documented in alt-tab-macos #4194.A fixed TTL is not the fix — it still enumerates ~17k times/day forever. The prior art in Hyperion #1921 is explicit (m13v's comment): fetch once, refresh only when the window/display topology actually changes.
Fix — topology-validated cache
The snapshot is cached and revalidated per call against a cheap fingerprint from
CGWindowListCopyWindowInfo— the pre-ScreenCaptureKit WindowServer query (sub-millisecond, no replayd involvement; the pre-SCK capture backend used it wholesale with zero replayd cost, per #1's own context). The fingerprint hashes every onscreen window's id, owner pid, and title (title changes can flip exclusion-pattern matches downstream), plus the online display count.The fingerprint is deliberately order-insensitive (per-window hashes, sorted, then combined): CGWindowList returns windows in z-order, and a mere focus switch reorders the list without changing the window set — hashing in list order would turn every cmd-tab into a replayd enumeration, exactly the load this cache exists to remove. Nothing downstream consumes z-order from the snapshot; window focus is derived live from NSWorkspace at
Window::all()time.Properties:
osascriptapp activation vs 20–75ms for a real fetch).stop_all_streams()drops the cache (wake/display teardown), and hosts can call the exportedinvalidate_shareable_content_cache()directly.Measurements (M-series, ~150 onscreen windows)
tests/cache_timing.rs— 20 back-to-backWindow::all()calls:Live screenpipe debug engine, 60s run with an ignore pattern configured (
RUST_LOG=sck_rs=debug):Each fetch corresponds to an actual topology change; idle stretches and focus switches perform none. Streams, exclusion filters, captures, and DB writes fully functional;
cargo testsuite green.Honest caveat: on this machine (fast Apple Silicon) replayd never showed the catastrophic spin even uncached — that needs the bigger NSSet from many Electron apps on macOS 14.x. The fix removes the anti-pattern at the source either way, following the issue's prior art.
🤖 Generated with Claude Code