Skip to content

perf(rendering): speed up zoom timeline rebuilds after live edits - #2557

Merged
richiemcilroy merged 3 commits into
CapSoftware:mainfrom
JeffreyLin1:perf/zoom-timeline-rebuild
Oct 14, 2026
Merged

richiemcilroy merged 3 commits into
CapSoftware:mainfrom
JeffreyLin1:perf/zoom-timeline-rebuild

Conversation

@JeffreyLin1

@JeffreyLin1 JeffreyLin1 commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Editing a long recording that has zoom segments gets slower the further the playhead is into it. On every slider tick, GPUI's publish_project sends the edited project and asks for the paused frame again, and the preview renderer throws away its zoom timelines and simulates them from 0:00 up to the playhead. Playback does the same when you press play or edit while playing. #2476 fixed this for scrubbing, but edits still pay for a full rebuild, and any edit to the zooms themselves has to rebuild anyway. #2507 skips the cursor rebuild on unrelated edits but still rebuilds zoom timelines, so it doesn't overlap with this.

Most of the rebuild was repeated work, so this PR makes it cheaper without changing what it computes:

  • targets_at scanned every zoom segment twice on each 8 ms step, and a 30 minute recording has 225k steps. A small index now finds the active segment with binary search, using the same first-match (start, end] rule.
  • Every auto segment filtered every cursor move in the recording to build its clusters. When the moves are in time order, binary search narrows each slice first and the same filters run on it. Unordered input falls back to the full scan.
  • The spring solver recomputed its exp/sin/cos terms on every step, even though they only change with the step length and the spring constants. Each simulation now caches them.

Output is bit-identical. The benchmark hashes every sample and all scenarios match main, and the new tests compare each fast path against the old scan. I also ran 20,000 random projects through both versions (the old copy isn't committed) and every sample matched.

Results

The new live-edit-benchmark example drives EditorInstance with the GPUI app's preview settings and drags the padding slider while paused, 60 edits at each position. The project is a synthetic 30 minute 1080p recording with 60 Hz cursor data and 60 auto zooms. Apple M3 Pro, release build, before and after runs interleaved, median of 3.

Edit while paused at Before After
0:36 8.6 ms 2.9 ms
15:00 18.9 ms 5.6 ms
29:24 28.7 ms 8.4 ms
CPU time over 180 edits 2.97 s 0.63 s

Near the end of that recording a slider drag now fits in a 60 Hz frame. On a 10 minute recording with 20 zooms the median edit goes from 6.6 to 4.3 ms, and recordings of a few minutes only save a millisecond or two.

Timing just the rebuild (benchmark_zoom_timeline_rebuild) with the playhead at the end gives 30.4 -> 6.4 ms for 30 minutes with 60 zooms and 104 -> 13.7 ms for 60 minutes with 120. The existing benchmark_long_recording_zoom_startup also runs about twice as fast.

Reproduce

cargo test -p cap-rendering --release --lib benchmark_zoom_timeline_rebuild -- --ignored --nocapture
cd apps/desktop-gpui && cargo run --release --example live-edit-benchmark -- /path/to/copy-of-project.cap

Point the example at a copy, since the editor can write project-config.json back into the project. cargo test -p cap-rendering, clippy and fmt all pass.

RetriggerConfidence Score: 5/5

The PR appears safe to merge, with the previous test concern addressed and no new actionable issues found.

Summary

Speeds up zoom timeline rebuilds with a segment index, narrower cursor searches, and cached spring calculations. Adds a live-edit benchmark.

  • Zoom timelines find active segments and cursor moves with shorter searches.
  • Spring steps reuse calculations when their settings stay the same.
  • Benchmarks show rebuild and live-edit times at different playhead positions.

Reviews (2) · Last reviewed commit: "test(rendering): check cached spring ste..." · Reviewed by Greptile

@vercel

vercel Bot commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

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

A member of the Team first needs to authorize it.

@JeffreyLin1 JeffreyLin1 changed the title perf(rendering): rebuild zoom timelines up to 8x faster (−70% live-edit latency on long recordings) perf(rendering): speed up zoom timeline rebuilds after live edits Oct 14, 2026
@JeffreyLin1
JeffreyLin1 force-pushed the perf/zoom-timeline-rebuild branch from 9ad2297 to 39e6f94 Compare October 14, 2026 06:20
The editor drops its zoom timelines on every project change and simulates
them again from 0:00 to the playhead. A paused live edit pays for the whole
recording up to the playhead, and so does pressing play or editing during
playback.

Most of that time was repeated work. targets_at scanned every zoom segment
twice per 8 ms step, and now uses a small index with binary searches. Each
auto segment filtered every cursor move to build its clusters; time-ordered
moves are now narrowed by binary search first and the same filters run on
what is left. The spring solver also recomputed exp, sin and cos terms on
every step even though they only depend on the step length and the spring
constants, so each simulation now caches them.

Samples are bit-identical to the previous implementation. On a 30 minute
recording with 60 auto zooms, a paused edit at 29:24 goes from 28.7 ms to
8.4 ms.
@JeffreyLin1
JeffreyLin1 force-pushed the perf/zoom-timeline-rebuild branch from 39e6f94 to 3156dd2 Compare October 14, 2026 06:22
@JeffreyLin1
JeffreyLin1 marked this pull request as ready for review October 14, 2026 06:22
Comment on lines +281 to +289
let mut fresh = SpringMassDamperSimulation::new(config);
fresh.set_position(position);
fresh.set_velocity(velocity);
fresh.set_target_position(target);
let expected = fresh.run(dt_ms);

cached.set_config(config);
cached.set_target_position(target);
let actual = cached.run(dt_ms);

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 Spring test shares its solver

cached_spring_steps_match_fresh_simulations compares two instances that both use the new solver. It checks cache reuse, but a mistake in the moved spring formulas would affect both sides and still pass. Keep the original solver as a test-only reference, or assert saved baseline results, so the promised bit-identical output has a lasting regression check.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/rendering/src/spring_mass_damper.rs
Line: 281-289

Comment:
**Spring test shares its solver**

`cached_spring_steps_match_fresh_simulations` compares two instances that both use the new solver. It checks cache reuse, but a mistake in the moved spring formulas would affect both sides and still pass. Keep the original solver as a test-only reference, or assert saved baseline results, so the promised bit-identical output has a lasting regression check.

---

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!

The spring test compared the cached simulation with fresh instances of the
same new solver, so a mistake in the moved formulas would pass on both
sides. Compare against a test-only copy of the original solver instead, and
add a config that reaches the degenerate overdamped branch.
@JeffreyLin1

Copy link
Copy Markdown
Contributor Author

@greptile

@richiemcilroy
richiemcilroy merged commit db7ecfc into CapSoftware:main Oct 14, 2026
22 of 24 checks passed
@richiemcilroy

Copy link
Copy Markdown
Member

Thanks so much!

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.

2 participants