Skip to content

fix(recording): keep cursor sampling running through output storage stalls - #2579

Merged
richiemcilroy merged 2 commits into
mainfrom
fix/cursor-recorder-storage-stall
Oct 14, 2026
Merged

richiemcilroy merged 2 commits into
mainfrom
fix/cursor-recorder-storage-stall

Conversation

@richiemcilroy

@richiemcilroy richiemcilroy commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

When the recording volume stops accepting writes for a while (slow SD card, external or network drive), Studio recordings lost all cursor movement, clicks and key presses for the length of the stall. The cursor recorder samples input and writes cursor.json, the keyboard file and new cursor images on the same thread, so one blocked write pauses sampling.

flowchart LR
  subgraph Before
    S1["cursor thread: sample every 16 ms"] -- "every 5 s: serialize + write" --> V1[("output volume")]
    S1 -- "new cursor shape: PNG save" --> V1
  end
  subgraph After
    S2["cursor thread: sample every 16 ms"] -- "snapshot (at most one waiting)" --> W2["cursor-writer thread"]
    S2 -- "cursor image" --> W2
    W2 --> V2[("output volume")]
  end
Loading

Root cause

  • flush_cursor_data / flush_keyboard_data run inline in the sampling loop every 5 s (crates/recording/src/cursor.rs), and rgba_image.save runs inline when a new cursor shape appears.
  • Sampling is polling, so while a write blocks, positions are never read and button or key transitions that start and end inside the stall are never seen. The in-memory event list loses them too, not only the file.
  • This applies to every macOS Studio recording with crash-recoverable recording on (the default), with or without a camera. fix(recording): keep macOS recordings intact through output storage stalls #2566 does not touch this path.

Change

  • New CursorFileWriter thread (cursor-writer) owns all cursor file writes: periodic snapshots, the final snapshot and cursor PNGs, in FIFO order, so a PNG is on disk before any snapshot that follows it.
  • A periodic flush is skipped while a snapshot is still waiting for the writer; the next one carries every event. The final snapshot is always written, and the thread is joined before the recorder replies, so stop behaves as before.
  • File contents are byte-identical to the inline writes.
  • Cursor images that fail to save keep their ID and are retried every second on the writer thread, so a transient write error no longer loses the image. Any image still unsaved when recording stops is dropped from the cursor list, which is what inline saving did. If the writer thread cannot be started, everything is written inline as before.
  • Memory: at most one waiting snapshot plus the one being written, each a copy of the cursor and keyboard event lists (about 100 bytes per event). Cursor images are queued as decoded RGBA, a few KB each, once per distinct shape.

Results

Synthetic setup: a DYLD_INSERT_LIBRARIES interposer blocks write, pwrite, pwritev, aio_write, fsync, write-mode open and rename for 20 s under the recording directory. The workload is the macOS Studio-with-camera configuration: 3024x1964 and 1280x720 NV12 at 30 fps through the AVFoundation screen and camera muxers, mic and system audio through the Ogg muxer, and the real cursor recorder with incremental outputs. Runs are interleaved pairs against main. "Longest sampling pause" is the largest gap between two iterations of the cursor sampling loop (normally 16 ms plus the work in one iteration).

main this PR
20 s stall during a 60 s recording (4 pairs): longest sampling pause 19.9 s (19.86 to 19.88) 36 ms (36 to 40)
20 s stall: cursor samples missed about 1,240 0
Stop 12 s into a 20 s stall (4 pairs): longest sampling pause 19.8 s 37 ms
Stop 12 s into a 20 s stall: pipeline stop time 7.92 s 7.92 s
15 s, no stall (4 pairs): longest sampling pause 35 ms 38 ms
15 s, no stall: video frames, audio drops, peak memory 439 / 0 / 112 MB 439 / 0 / 112 MB
60 s under 8 busy CPU threads (8 pairs): sampling loop iterations 2,340 to 2,736 2,361 to 2,702
60 s under CPU load: cursor samples missed 0 to 6 0

Screen, camera and audio results are the same in both columns in every class; this change only moves cursor file writes. One CPU-load run on this PR had a 5.4 s pause in which every thread in the process (including the tokio timer heartbeat) stopped at once; it is excluded above as a machine-wide stall, not a cursor writer effect.

RetriggerConfidence Score: 5/5

The PR appears safe to merge; both earlier findings are addressed.

Summary

Moves cursor file writes off the sampling thread so storage stalls do not stop input capture.

  • Cursor sampling keeps running while files are written on another thread.

Reviews (2) · Last reviewed commit: "fix(recording): retry cursor images that..." · Reviewed by Greptile

@vercel

vercel Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
cap-status Ready Ready Preview Oct 14, 2026 2:28pm UTC

Comment thread crates/recording/src/cursor.rs Outdated
Comment thread crates/recording/src/cursor.rs Outdated
@richiemcilroy

Copy link
Copy Markdown
Member Author

hey @greptileai, please re-review the PR

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capy found no issues.

View 2 other findings in Capy.

Open in Capy

@richiemcilroy
richiemcilroy merged commit 86ee46f into main Oct 14, 2026
33 checks passed

This branch was successfully deployed

2 active deployments
Preview – cap-web — 9dbb6bcf Deployed Oct 14, 2026 by vercel[bot]
Preview – cap-status — 9dbb6bcf Deployed Oct 14, 2026 by vercel[bot]
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.

1 participant