Skip to content

fix: refresh SCShareableContent on topology change, not per call#12

Open
divanshu-go wants to merge 1 commit into
screenpipe:mainfrom
divanshu-go:fix/cache-shareable-content
Open

fix: refresh SCShareableContent on topology change, not per call#12
divanshu-go wants to merge 1 commit into
screenpipe:mainfrom
divanshu-go:fix/cache-shareable-content

Conversation

@divanshu-go

@divanshu-go divanshu-go commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #1.

Problem

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 — but 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: 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.

get_shareable_content()          ── sync; keeps the post-wake 0-display retry loop
get_shareable_content_cached_async()  ── async twin for one-shot capture paths
        └── shared cache: { fetched_at, topology_signature, content }
            hit ⇔ signature unchanged (and < 300s safety-net TTL)

Properties:

  • Static desktop → zero replayd enumerations. Not 12/min — zero.
  • Focus switches between existing windows → zero enumerations (verified: cached call 2ms after an osascript app activation vs 20–75ms for a real fetch).
  • New / closed / retitled window → picked up on the very next call. Fresher than any TTL; exclusion filters see topology changes immediately.
  • 300s safety-net TTL bounds staleness the fingerprint can't see; a 5s blind TTL applies only if CGWindowList is ever unavailable.
  • 0-display snapshots (SCK's stale post-wake state) are never cached, so the wake-retry logic can't get pinned behind the cache.
  • stop_all_streams() drops the cache (wake/display teardown), and hosts can call the exported invalidate_shareable_content_cache() directly.
  • Ids and exclusion filters derive from the same snapshot, so they stay mutually consistent. Enumeration semantics otherwise unchanged (full window list, as before).

Measurements (M-series, ~150 onscreen windows)

tests/cache_timing.rs — 20 back-to-back Window::all() calls:

first call calls 2–20 (avg)
before (every call = replayd enumeration) ~75ms ~18–75ms each
after (signature-validated cache) ~75ms 0.35ms each (CGWindowList check, no replayd)

Live screenpipe debug engine, 60s run with an ignore pattern configured (RUST_LOG=sck_rs=debug):

fetches=8 frames=7 errors=0
15:33:22.36                                  ← startup
   (43 seconds of static desktop — ZERO enumerations)
15:34:05.85 … 15:34:14.46                    ← real window activity burst

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 test suite 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

@divanshu-go divanshu-go force-pushed the fix/cache-shareable-content branch 3 times, most recently from 4fccb0f to 001aab1 Compare July 5, 2026 10:02
@divanshu-go divanshu-go changed the title fix: cache SCShareableContent to stop replayd 100% CPU spin fix: refresh SCShareableContent on topology change, not per call Jul 5, 2026
@divanshu-go divanshu-go force-pushed the fix/cache-shareable-content branch from 001aab1 to 69c64d0 Compare July 5, 2026 11:11
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>
@divanshu-go divanshu-go force-pushed the fix/cache-shareable-content branch from 69c64d0 to eaff777 Compare July 5, 2026 11:31
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.

[bug] SCShareableContent::current() called per-frame causes replayd to spin at 100% CPU

1 participant