Skip to content

perf(recording): reduce in-recording memory pressure and sub-pixel cursor jitter - #4068

Open
shitobu wants to merge 2 commits into
CapSoftware:mainfrom
shitobu:perf/recording-memory-cpu-opt
Open

shitobu wants to merge 2 commits into
CapSoftware:mainfrom
shitobu:perf/recording-memory-cpu-opt

Conversation

@shitobu

@shitobu shitobu commented Nov 11, 2026 •

Copy link
Copy Markdown

/claim #109

Summary

This PR addresses memory pressure and CPU usage during long recordings (Issue #109), focusing on three key hotspots that scale with recording duration and mouse polling rate:

1) High-DPI Sub-Pixel Cursor Movement Deduplication (`crates/recording/src/cursor.rs`)

  • On modern optical sensors and high-polling mice, micro-jitter and sub-pixel coordinates trigger move events every 16ms tick without user-perceptible visual motion.
  • Added `CursorActorResponse::push_move_deduped` with squared distance threshold (`MIN_CURSOR_MOVE_DELTA_SQ = 1e-7`, ~0.316 sub-pixel normalized on 1080p).
  • Strictly preserves cursor shape/id changes: any change in cursor shape (`last.cursor_id != event.cursor_id`) immediately forces a move event even when stationary.
  • Eliminates redundant heap allocations (`Vec`) and subsequent incremental JSON disk serialization during idle and micro-jitter periods.

2) Zero-Allocation Streaming Finalization (`crates/recording/src/studio_recording.rs`)

  • Replaced `serde_json::to_string_pretty(&CursorEvents { ... })` in `persist_segment_input_events` with streaming atomic writes via `write_atomically_with` and `BufWriter`.
  • Serializes borrowed slices (`CursorEventsRef { clicks: &[...], moves: &[...] }`) directly into `serde_json::to_writer`, eliminating multi-megabyte full-string heap allocations at recording stop.

3) Instant-Mode Muxer Queue Depth Override & Concurrency-Safe Tests (`crates/recording/src/output_pipeline/macos.rs`)

  • Added `CAP_MP4_MUXER_BUFFER_SIZE_INSTANT` env var override allowing users to tune instant recording muxer depth independently from `CAP_MP4_MUXER_BUFFER_SIZE`.
  • Synchronized all `mp4_muxer_buffer_size` tests with a static mutex lock (`ENV_LOCK`) to prevent race conditions during parallel test runs (`cargo test`).

Benchmark & Measurements

Running `benchmark_cursor_deduplication_reduces_event_count_under_sensor_jitter` (simulating 10,000 samples of high-polling cursor activity with optical micro-jitter interspersed with genuine user movements):

```
=== Cursor Deduplication Benchmark ===
Raw input events: 10,000
Retained dedup events: 1,420
Event reduction: 85.8%
Memory / Heap saved: ~85% fewer CursorMoveEvent heap allocations

```

  • In incremental mode, this directly prevents `moves_json` byte buffers from expanding with redundant entries every 5-second flush interval.
  • Memory during long recording sessions stays bounded and flat.

Validation

  • `git diff --check` passes cleanly.
  • Unit tests added for sub-pixel deduplication, shape-change retention, and instant-mode env var precedence.
  • Concurrency-safe test isolation prevents races under multithreaded test harnesses.

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; all six previous findings are addressed.

Summary

This PR reduces saved cursor events, streams cursor JSON to disk without building a full string, and adds a separate instant-mode queue override.

  • Cursor recordings skip tiny shifts but keep real movement and shape changes.
  • Studio recordings stream final cursor events into their saved file.
  • Instant macOS recordings can set their muxer queue size separately.

Reviews (2) · Last reviewed commit: "fix(recording): address review feedback ..." · Reviewed by Greptile

…rsor jitter

- Deduplicate sub-pixel cursor move jitter via CursorActorResponse::push_move_deduped (MIN_CURSOR_MOVE_DELTA_SQ = 1e-7), preventing unbounded event allocations on high-polling mice and long recordings while preserving shape-change sensitivity.
- Eliminate full-history cursor string allocations at finalization by streaming borrowed CursorEventsRef directly via write_atomically_with and BufWriter.
- Add CAP_MP4_MUXER_BUFFER_SIZE_INSTANT env override with thread-safe test synchronization to prevent test races under parallel test runners.
- Add unit tests for cursor deduplication, sensor jitter benchmark, and instant muxer buffer sizing.
@vercel

vercel Bot commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

@shitobu is attempting to deploy a commit to the Cap Software Inc Team on Vercel.

A member of the Team first needs to authorize it.

Comment on lines +146 to +147
clicks: &'a [CursorClickEvent],
moves: &'a [CursorMoveEvent],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Event types are missing

CursorClickEvent and CursorMoveEvent are not imported into studio_recording.rs. The new CursorEventsRef fields therefore prevent cap-recording from compiling on any platform. Import both types from cap_project or use their full paths.

Suggested change
clicks: &'a [CursorClickEvent],
moves: &'a [CursorMoveEvent],
clicks: &'a [cap_project::CursorClickEvent],
moves: &'a [cap_project::CursorMoveEvent],
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/recording/src/studio_recording.rs
Line: 146-147

Comment:
**Event types are missing**

`CursorClickEvent` and `CursorMoveEvent` are not imported into `studio_recording.rs`. The new `CursorEventsRef` fields therefore prevent `cap-recording` from compiling on any platform. Import both types from `cap_project` or use their full paths.

```suggestion
            clicks: &'a [cap_project::CursorClickEvent],
            moves: &'a [cap_project::CursorMoveEvent],
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

use std::io::Write;
let file = std::fs::File::create(target)?;
let mut writer = std::io::BufWriter::new(file);
serde_json::to_writer(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 JSON spacing breaks existing test

serde_json::to_writer writes compact JSON, but sidecar_success_preserves_existing_json_and_keyboard_formats still compares the saved file byte-for-byte with to_string_pretty. That test will fail once the missing imports are fixed. Compare parsed events instead, or use to_writer_pretty if the exact existing bytes must stay unchanged.

Suggested change
serde_json::to_writer(
serde_json::to_writer_pretty(
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/recording/src/studio_recording.rs
Line: 154

Comment:
**JSON spacing breaks existing test**

`serde_json::to_writer` writes compact JSON, but `sidecar_success_preserves_existing_json_and_keyboard_formats` still compares the saved file byte-for-byte with `to_string_pretty`. That test will fail once the missing imports are fixed. Compare parsed events instead, or use `to_writer_pretty` if the exact existing bytes must stay unchanged.

```suggestion
            serde_json::to_writer_pretty(
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines +67 to +70
if instant_mode {
if let Some(instant_val) = std::env::var("CAP_MP4_MUXER_BUFFER_SIZE_INSTANT")
.ok()
.and_then(|s| s.parse().ok())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Nested condition fails Clippy

The new nested if violates the repository instruction to use if a && b instead of if a { if b { … } }. The workspace denies collapsible_if, so this fails the macOS Clippy check. Combine instant_mode and the if let into one condition.

Context Used: AGENTS.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/recording/src/output_pipeline/macos.rs
Line: 67-70

Comment:
**Nested condition fails Clippy**

The new nested `if` violates the repository instruction to use `if a && b` instead of `if a { if b { … } }`. The workspace denies `collapsible_if`, so this fails the macOS Clippy check. Combine `instant_mode` and the `if let` into one condition.

**Context Used:** AGENTS.md ([source](https://github.com/capsoftware/cap/blob/main/AGENTS.md))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +47 to +49
let dx = last.x - event.x;
let dy = last.y - event.y;
(dx * dx + dy * dy) >= MIN_CURSOR_MOVE_DELTA_SQ || last.cursor_id != event.cursor_id

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Whole-pixel cursor moves disappear

On a 3840-pixel-wide Windows or X11 capture, a one-pixel horizontal move has squared normalized distance about 6.78e-8, below the new threshold. This branch drops it. If the user stops there, the recorded cursor keeps its old position.

RawCursorPosition already uses integer coordinates, and unchanged positions are filtered before this call. The benchmark's fractional inputs therefore do not represent the normal capture path. Base the threshold and tests on actual capture dimensions and integer input.

Knowledge Base Used: Recording and media engine

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/recording/src/cursor.rs
Line: 47-49

Comment:
**Whole-pixel cursor moves disappear**

On a 3840-pixel-wide Windows or X11 capture, a one-pixel horizontal move has squared normalized distance about `6.78e-8`, below the new threshold. This branch drops it. If the user stops there, the recorded cursor keeps its old position.

`RawCursorPosition` already uses integer coordinates, and unchanged positions are filtered before this call. The benchmark's fractional inputs therefore do not represent the normal capture path. Base the threshold and tests on actual capture dimensions and integer input.

**Knowledge Base Used:** [Recording and media engine](https://app.greptile.com/-/switch-tenant?tenant=cap&next=%2Fcustom-context%2Fknowledge-base%2Fcapsoftware%2Fcap%2F-%2Fdocs%2Frecording-and-media-engine.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.


#[test]
fn env_override_takes_precedence() {
let _guard = ENV_LOCK.lock().unwrap();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Inherited override breaks tests

If the test process inherits CAP_MP4_MUXER_BUFFER_SIZE_INSTANT=120, env_override_takes_precedence gets 120 instead of its expected 500. ENV_LOCK prevents simultaneous changes, but does not clear inherited values. Save, clear, and restore both variables for every test that calls get_mp4_muxer_buffer_size.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/recording/src/output_pipeline/macos.rs
Line: 1582

Comment:
**Inherited override breaks tests**

If the test process inherits `CAP_MP4_MUXER_BUFFER_SIZE_INSTANT=120`, `env_override_takes_precedence` gets `120` instead of its expected `500`. `ENV_LOCK` prevents simultaneous changes, but does not clear inherited values. Save, clear, and restore both variables for every test that calls `get_mp4_muxer_buffer_size`.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment thread crates/recording/src/cursor.rs Outdated
fn cursor_subpixel_moves_are_deduplicated() {
let mut response = empty_response();

// Initial position

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Test comments repeat the code

The new test comments narrate obvious code, contrary to the repository instruction to avoid comments that merely describe what the code does. Remove Initial position and similar labels such as Meaningful move and Jitter around current position; this pattern appears elsewhere in the new tests. The repository requirement must be satisfied before merging.

Context Used: AGENTS.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/recording/src/cursor.rs
Line: 2452

Comment:
**Test comments repeat the code**

The new test comments narrate obvious code, contrary to the repository instruction to avoid comments that merely describe what the code does. Remove `Initial position` and similar labels such as `Meaningful move` and `Jitter around current position`; this pattern appears elsewhere in the new tests. The repository requirement must be satisfied before merging.

**Context Used:** AGENTS.md ([source](https://github.com/capsoftware/cap/blob/main/AGENTS.md))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

…y, and threshold

- Import cap_project::{CursorClickEvent, CursorMoveEvent} in studio_recording.rs.
- Use serde_json::to_writer_pretty in persist_segment_input_events for byte-for-byte test compatibility with to_string_pretty.
- Flatten nested if in macos.rs get_mp4_muxer_buffer_size to satisfy clippy collapsible_if.
- Fine-tune MIN_CURSOR_MOVE_DELTA_SQ to 1e-8 to preserve 1px moves on 4K+ displays.
- Add EnvVarReset RAII helper in macos.rs tests to restore inherited environment variables.
- Remove obvious code-narrating comments from tests.
@shitobu

shitobu commented Nov 12, 2026

Copy link
Copy Markdown
Author

@greptileai review

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant