Skip to content

perf(cursor): stop hashing an unchanged cursor image every tick - #2466

Open
AatmanAJ wants to merge 2 commits into
CapSoftware:mainfrom
AatmanAJ:perf/cursor-recorder-skip-unchanged-image
Open

AatmanAJ wants to merge 2 commits into
CapSoftware:mainfrom
AatmanAJ:perf/cursor-recorder-skip-unchanged-image

Conversation

@AatmanAJ

@AatmanAJ AatmanAJ commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

The cursor recorder runs for the whole length of every Studio recording. On each tick (a 16 ms wait plus the work), it fetches the current cursor image and hashes it with SHA-256 twice:

  1. On macOS, macos_cursor_data hashes the TIFF to look up the system cursor shape (CursorShapeMacOS::from_hash).
  2. The loop hashes the same bytes again to get the cursor id.

Both hashes run on every tick, even though the image almost never changes between ticks and both results are only used when a new cursor gets registered. macOS cursor TIFFs are often hundreds of KB, which makes this several milliseconds of work per tick.

This PR moves the id lookup into cursor_id_for_image, with two changes:

  • One hash per new image: the id and the macOS shape both come from the same digest.
  • Unchanged images skip hashing: while the image bytes match the previous tick, the last id is reused. An image that fails to decode or save isn't cached, so it's still retried on the next tick as before.

Recorded ids, shapes and cursor files are unchanged.

Results

Each tick's work was timed against the live system cursor on an M5 Max running macOS 27. The old path is the fetch plus both hashes; the new path is the fetch plus cursor_id_for_image. Each run interleaves 20 rounds of 50 ticks per path. Cursor TIFF size depends on which cursor is showing, so the three runs differ:

Cursor TIFF (median) Before, per tick After, per tick Fetch alone
21 KB 399 µs 136 µs 130 µs
264 KB 3,282 µs 453 µs 405 µs
583 KB 5,204 µs 538 µs 438 µs

With the larger cursors, that's 3.3–5.2 ms out of every 19–21 ms tick, or roughly 17–25% of a core, for the whole recording. Because the work runs after the 16 ms wait, it also stretched each tick, so cursor positions were sampled at about 48 Hz instead of 60. (Both figures are arithmetic from the timings above.) What's left is mostly NSCursor.currentSystemCursor plus TIFFRepresentation itself.

Tests

  • an_unchanged_cursor_image_reuses_its_id_without_hashing
  • a_changed_cursor_image_gets_hashed_and_a_new_id
  • a_cursor_image_that_fails_to_decode_is_not_cached
  • a_cursor_image_that_fails_to_save_is_retried: a directory at the cursor's path blocks the PNG write. After it's removed, the next call saves and registers the cursor.

These were checked by mutation. The first test fails without the fast path, and the last two fail without the "only cache what was saved" check.

  • cargo test -p cap-recording --lib: 566 passed.
  • cargo clippy -p cap-recording --all-targets is clean.

Hand-tested

I tested this on a debug GPUI build with this change, on macOS 27.0.1 with an M5 Max. I made a 12.7 s Studio area recording while moving the pointer over plain areas (arrow), text (I-beam), a link (pointing hand) and window edges (resize). The results:

  • The recorder saved five cursor images, cursor_0.png to cursor_4.png, one per distinct cursor: the arrow, the I-beam, the pointing hand and two horizontal-resize variants. Each cursor was saved once.
  • cursor.json has 318 moves and 14 clicks, and they reference all five ids.
  • The editor played the recording back at 59.9 fps with no dropped frames, and the cursor changed shape at those points.

RetriggerConfidence Score: 4/5

The PR appears safe to merge, with a non-blocking gap in the write-retry test.

Findings

  1. P2 Write retries lack a test ▶
Fix with agent prompt
### Issue 1
crates/recording/src/cursor.rs:1223-1228
`a_cursor_image_that_fails_to_save_is_retried` passes invalid image bytes, so it never reaches `save`. It also calls `cursor_id_for_image` only once. A broken retry after a write failure could therefore pass this test.

Keep the decode-failure check, and add a valid PNG with a temporarily blocked output path. Then unblock the path and call again to verify that the image is saved and registered.

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!

---

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

Summary

Reuses the previous cursor ID when image bytes are unchanged.

  • Repeated cursor images reuse their id without another hash.

Reviews (1) · Last reviewed commit: "perf(cursor): stop hashing an unchanged ..." · Reviewed by Greptile

The cursor recorder fetched the cursor image and SHA-256'd it twice on
every ~16ms tick: once on macOS to look up the system cursor shape, and
again for the cursor id. For a large macOS cursor TIFF that is several
milliseconds per tick. Hash each new image once, deriving both the id and
the shape from the same digest, and reuse the last id while the image
bytes are unchanged.
@AatmanAJ
AatmanAJ marked this pull request as ready for review October 11, 2026 11:39
Comment on lines +1223 to +1228
let id = cursor_id_for_image(
cursor_data(b"not an image".to_vec()),
&mut response,
dir.path(),
&mut last,
);

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 Write retries lack a test

a_cursor_image_that_fails_to_save_is_retried passes invalid image bytes, so it never reaches save. It also calls cursor_id_for_image only once. A broken retry after a write failure could therefore pass this test.

Keep the decode-failure check, and add a valid PNG with a temporarily blocked output path. Then unblock the path and call again to verify that the image is saved and registered.

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

Comment:
**Write retries lack a test**

`a_cursor_image_that_fails_to_save_is_retried` passes invalid image bytes, so it never reaches `save`. It also calls `cursor_id_for_image` only once. A broken retry after a write failure could therefore pass this test.

Keep the decode-failure check, and add a valid PNG with a temporarily blocked output path. Then unblock the path and call again to verify that the image is saved and registered.

---

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!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in c243b59. a_cursor_image_that_fails_to_save_is_retried now uses a valid PNG and blocks the write with a directory at the cursor's path. It checks that nothing gets cached, then removes the directory and checks that the next call saves and registers the cursor. The decode-failure case moved to its own test, a_cursor_image_that_fails_to_decode_is_not_cached.

The existing retry test fed undecodable bytes, so it never reached the
save. Block the write with a directory at the cursor's path, check
nothing is cached, then unblock it and check the next tick saves and
registers the cursor.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant