Skip to content

perf(rendering): skip re-sorting mask keyframes that are already in order - #2767

Open
celeroncoder wants to merge 1 commit into
CapSoftware:mainfrom
celeroncoder:perf/mask-keyframes-skip-sort
Open

celeroncoder wants to merge 1 commit into
CapSoftware:mainfrom
celeroncoder:perf/mask-keyframes-skip-sort

Conversation

@celeroncoder

Copy link
Copy Markdown
Contributor

interpolate_vector and interpolate_scalar in crates/rendering/src/mask.rs copied and sorted the keyframe list on every call. They run once per frame for each active mask's position, size and intensity tracks (from interpolate_masks, twice per frame during a transition), and the keyframes are static for the life of the project, so the copy and the sort were repeated for nothing.

Change

One private helper, sorted_by_time, returns the keyframes as a borrowed slice when every adjacent pair is already in non-decreasing time order, and only clones and sorts otherwise. Both interpolation functions use it; their bodies are otherwise unchanged.

fn sorted_by_time<T: Clone>(keys: &[T], time: impl Fn(&T) -> f64) -> Cow<'_, [T]> {
    if keys.is_sorted_by(|a, b| time(a) <= time(b)) {
        return Cow::Borrowed(keys);
    }
    let mut sorted = keys.to_vec();
    sorted.sort_by(|a, b| time(a).partial_cmp(&time(b)).unwrap_or(std::cmp::Ordering::Equal));
    Cow::Owned(sorted)
}
  • Same idiom as sample_track in camera3d.rs from perf(rendering): avoid cloning sorted 3D camera tracks #2503.
  • Behaviour is identical: equal times keep insertion order, which is what the stable sort produced; any NaN time makes <= false, so that input still takes the old clone-and-sort path.
  • Every in-app writer keeps mask keyframes in order (the GPUI editor only clears or retimes them, and its retime path inserts at partition_point), so the fast path is the normal case; cap project config set can write arbitrary JSON, which is why the check stays at runtime rather than sorting at load. Sorting at load was rejected because ProjectConfiguration::write round-trips the arrays as-is.

Tests compare both functions against a verbatim copy of the old code for sorted, unsorted, tied, empty and single-key input at boundary, NaN and infinite query times, and pin that ordered input is borrowed and unsorted or NaN input is sorted.

Results

Standalone copies of the old and new interpolate_vector, release build, 100k calls, 2 vCPU aarch64 Linux box: 8 already-sorted keys 35.0 ns → 14.7 ns per call; 8 unsorted keys 68.6 ns → 70.3 ns (the cost of the check). This is an allocation cleanup on a per-frame path, not a visible frame-time change.

Overlap

How this was made

This PR is agent-generated.

  • The finding comes from an audit of main by OpenCode scouts and Gemini 3.8 Flash; the maintainer asked for this part to be filed on its own.
  • DeepSeek V4.1 Flash and Gemini 3.8 Flash reviewed the design first (keyframe writers, call graph, perf(rendering): filter inactive masks before sorting #2578 interplay, sort-at-load alternative).
  • Claude Opus 5.5 wrote the change and tests on current main.
  • Gemini 3.8 Flash and DeepSeek V4.1 Flash reviewed the final diff independently (Gemini re-ran clippy and the tests); both approved, and DeepSeek's two nits (a redundant #[inline], a NaN-only differential case) were folded in.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

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

A member of the Team first needs to authorize it.

@celeroncoder

Copy link
Copy Markdown
Contributor Author

cc @richiemcilroy, this is the mask-keyframe half you suggested filing on its own; the timeline hold-window part is dropped.

@vercel

vercel Bot commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

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

A member of the Team first needs to authorize it.

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