Repository navigation
perf(rendering): reuse styled project snapshots across frames (352–2,135× faster warm CPU preparation) - #3112
Merged
richiemcilroy merged 2 commits intoOct 24, 2026
Conversation
Contributor
|
@kartikkabadi 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 was referenced Oct 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Styled frames currently clone and later drop the full
ProjectConfiguration, including unrelated caption and keyboard data, on every call toProjectUniforms::new. Reuse one resolved snapshot while the source Arc and winning background/camera/cursor style segments remain the same.A Weak source reference prevents allocation identity reuse and makes
Arc::make_mutedits invalidate the entry. Frame snapshots keep their own references. Full cloning and destruction stay outside the mutex; concurrent misses reconcile before publishing. Time-dependent uniforms still update every frame. The cache retains one resolved snapshot, including across later unstyled calls.The diff contains only configuration/cache logic, renderer integration, and focused regression tests in three existing files. Benchmark programs and investigation logs are provided below as review evidence rather than permanent repository files.
This completes the active-style case left by #2483, which shares the base project between frames, and #2957, which selects style groups in one pass. Both still clone the project when an override applies. #2620 keeps the editor's source Arc stable; this change reuses the resolved styled result. A fresh audit of all 145 open PRs, 500 recently updated merged PRs, and the patches for 55 related candidates found no equivalent published change.
Evidence
Before, every styled request cloned the project. After, matching warmed requests reuse the same snapshot, with zero allocations in the measured cache-hit path.
Independent local baseline/cached comparison against
e258c87007d9eb6b2ff2b4f73cd645ec2bdb9fa7:ProjectUniformsconstruction/releaseProjectUniformsconstruction/releaseFive fresh process pairs alternated baseline/cached order. Times are medians of per-process medians; speedups are medians of paired ratios. All five native-render pairs improved. A separate macOS sampling profile identifies project/String/Vec copying and allocation under
ProjectUniforms -> style_atin the baseline.In a separate allocation fixture, uncached styled resolution made 6,031 allocations/query for 10-minute data and 36,061 for 60-minute data. Warm hits made zero. Cold cached resolution made 6,032/36,062 allocations. This fixture has a different caption density and keyboard segments; it is not the uniform fixture.
Output checks:
Scope and tradeoffs
The large multiplier measures warmed
ProjectUniformsconstruction/release, not application FPS or overall export speed. That fixture loads captions but leaves caption rendering disabled and supplies no decoded video/camera frames. The separate native probe enables captions and supplies screen/camera buffers, rendering and reading back actual Metal output at 640×360. It excludes decode/encode.The complete export probes use seven-second and one-minute source clips carrying 10/60 minutes worth of caption records. They are not actual 10/60-minute videos. Export timings were inconclusive under variable background load. Median paired throughput ratios were 0.82×/0.99× for seven-second clips and 0.94×/1.05× for one-minute clips. No full-export speedup or regression is established.
Cold misses retain the full clone cost. Frequent random style changes can lose the benefit. In one seven-sample process, the 60-minute random-seek fixture was about 18% slower with caching and still made about 35,357 allocations/query. This needs a quiet-machine measurement before claiming a general seek regression. Unstyled frames do not lock or allocate. Callers that create a fresh source Arc every frame cannot expect warmed hits.
Host: Apple M1 Pro, 16 GB, macOS 27.0.1, Metal. Root benchmark builds used pinned Rust 1.88.0,
opt-level=s, thin LTO, one codegen unit, and release debug info disabled. The standalone shipping GPUI workspace defaults to optimization level 3, so these are not timings of the complete desktop binary. Desktop GUI interaction and fresh Windows/Linux execution were not tested.Checks
The full local regression run passed 951 tests on upstream
12ee63a24d1de2eb63d6cd68c75011a993f6726e. The final branch is based onf510acfa1846552a490bfcc3267910218c606364, which adds an unrelated screenshot optimization and web test; the project/renderer/editor/export code and this patch are unchanged between those bases. Formatting, the CI-equivalent workspace lint command and the desktop compile check were rerun on the final base. Rust commands use pinned 1.88.0 unless noted.cargo +1.88.0 fmt --all -- --check.cargo +1.88.0 check --locked -p cap-project -p cap-rendering --all-targets --all-features.cargo +1.88.0 test --locked -p cap-project --lib -- --test-threads=1.cargo +1.88.0 test --locked -p cap-rendering -- --test-threads=1.cargo +1.88.0 test --locked -p cap-rendering --lib -- --ignored --skip benchmark --skip dump_chrome_pngs --skip camera_setup_keeps_frame_resources_out_of_first_uploads --nocapture --test-threads=1.cargo +1.88.0 test --locked -p cap-editor --lib -- --test-threads=1.cargo +1.88.0 test --locked -p cap-export --lib -- --test-threads=1.cargo +1.88.0 test --locked -p cap-utils -p cap-cli-install --lib.cargo +1.88.0 clippy --workspace --exclude cap-desktop --all-features --locked -- -D warnings.cargo +1.88.0 test --locked --release -p cap-project --test independent_style_cache -- --test-threads=1.cargo +1.96.1 check --locked --manifest-path apps/desktop-gpui/Cargo.toml. This is a compile check; GPUI CI uses Rust 1.95.0.The independent oracle is a temporary integration test provided below and is not part of the repository diff. The normal project/renderer/editor/export suites and native GPU cases were run; unrelated ignored benchmarks and fixture-dependent audio/decoder tests were not enabled.
The native run excludes
layers::camera::tests::camera_setup_keeps_frame_resources_out_of_first_uploads, an already ignored test proven to fail identically on the untouched baseline atlayers/camera.rs:846. On macOS,RendererLayers::prepare_for_video_dimensionsdeliberately defers allocation for IOSurface conversion, while the test expects eager camera texture allocation. Style resolution is not reached. This pre-existing code and assertion are unchanged. The normal renderer suite passes.Merge Danger
Door: two-way. Reverting restores per-frame configuration copying; no schema, stored project data or dependency changes.
Blast radius: rendering. Source identity or style selection errors could retain stale settings across preview/export frames. Regression tests exercise edits, style boundaries, snapshot ownership and concurrent sources. Memory retention is one cached resolved snapshot plus references held by in-flight frames. Miss-heavy work still clones the project and can pay cache overhead.
Measurement procedure and per-process results
Both variants used identical probe source, package selection, native dependencies, target settings and inputs. The uniform example performs warmup and seven samples per case; the native example performs warmup and three samples per case. Each was launched in five alternating fresh-process pairs, with no validation builds running during timing. The host still had unrelated background load.
The source programs below are temporary local examples. Copy the uniform/native/export programs to their named example paths on both the base and patched checkout. The allocation program references the new cache and runs on the patched checkout, comparing its uncached resolver and cached resolver in one binary. The independent oracle is a temporary project integration test. They need not be checked in.
Use the repository's normal native dependency setup first. Build the same package/feature selection on each side:
Preserve the binaries from each checkout separately before rebuilding the other. Place relocated binaries under a directory whose
../Frameworks/Spacedrive.frameworkpoints to the installed native framework, or retain the original target-directory layout. SetORT_DYLIB_PATHto the repository's installed ONNX runtime library when needed. Run with identical environment and alternate the launch order five times. For each native probe pair, pass different output directories and compare every generated.rgbafile directly.The allocation example uses
512 7arguments. Itsns_per_queryexcludes cache setup and final destruction; add(cache_setup_ns + cache_drop_ns) / queriesfor an inclusive comparison. Allocation counts are separate untimed passes.warm-sequenceprimes its first query only; subsequent style changes remain measured misses.Per-process values used for the local summaries follow. Units are ns/frame for uniform/native render and ms/export for export cases.
[ { "scope": "uniforms", "case": "0m active=False", "before_runs": [ 557.8121583333333, 404.778475, 398.202775, 393.37604166666665, 394.0864583333333 ], "after_runs": [ 393.9048583333333, 355.41979166666664, 355.50173333333333, 357.72569166666665, 356.00695 ], "paired_speedup_median": 1.1201148620747465 }, { "scope": "uniforms", "case": "0m active=True", "before_runs": [ 1804.2514475743349, 1505.485606060606, 1705.1972811671087, 1465.732946635731, 1483.8227688787185 ], "after_runs": [ 396.89548333333335, 405.5809, 389.977775, 389.24965, 393.7767333333333 ], "paired_speedup_median": 3.7681829404142517 }, { "scope": "uniforms", "case": "10m active=False", "before_runs": [ 440.490625, 394.8788166666667, 426.94549166666667, 391.9993083333333, 409.14549166666666 ], "after_runs": [ 356.12708333333336, 360.82013333333333, 355.059375, 353.009375, 355.18645833333335 ], "paired_speedup_median": 1.1519174846544802 }, { "scope": "uniforms", "case": "10m active=True", "before_runs": [ 140838.19583333333, 139437.84583333333, 157010.59166666667, 136625.175, 138390.27708333332 ], "after_runs": [ 391.90625, 523.7854166666667, 387.58576666666664, 388.15555, 393.38298333333336 ], "paired_speedup_median": 351.9856279267422 }, { "scope": "uniforms", "case": "60m active=False", "before_runs": [ 394.246525, 409.5493, 401.89479166666666, 392.9684, 392.371525 ], "after_runs": [ 356.1413166666667, 408.72743333333335, 352.5864583333333, 355.509725, 414.39861666666667 ], "paired_speedup_median": 1.1053661049637953 }, { "scope": "uniforms", "case": "60m active=True", "before_runs": [ 872759.725, 874996.1833333333, 1162243.4083333334, 812397.575, 820807.9833333333 ], "after_runs": [ 390.89166666666665, 409.8170166666667, 387.81875, 389.71110833333336, 445.1295166666667 ], "paired_speedup_median": 2135.089924889649 }, { "scope": "native-render", "case": "10m", "before_runs": [ 1777479.1666666667, 1616854.1666666667, 1689203.125, 1611585.0833333333, 1665555.5833333333 ], "after_runs": [ 1499479.1666666667, 1487437.5, 1500428.8333333333, 1437321.1666666667, 1548704.875 ], "paired_speedup_median": 1.121242155690789 }, { "scope": "native-render", "case": "60m", "before_runs": [ 2426748.2916666665, 2447534.75, 2305314.2083333335, 2472010.4166666665, 2380328.125 ], "after_runs": [ 1453064.25, 1902649.2916666667, 1547784.7083333333, 1465533.0, 1702954.875 ], "paired_speedup_median": 1.4894282104749013 }, { "scope": "export", "case": "10m export_ms", "before_runs": [ 949.26225, 626.035084, 700.183042, 985.064125, 603.90175 ], "after_runs": [ 567.4071670000001, 761.502167, 1237.8901250000001, 608.519083, 1704.8805 ], "paired_speedup_median": 0.8221054530498795 }, { "scope": "export", "case": "60m export_ms", "before_runs": [ 631.917041, 912.5153750000001, 577.249459, 1028.923541, 950.662833 ], "after_runs": [ 636.2462089999999, 1113.382166, 601.345125, 941.0375829999999, 767.800917 ], "paired_speedup_median": 0.9931957661377596 }, { "scope": "export", "case": "60s clip 10m export_ms", "before_runs": [ 5325.474375, 3455.3021670000003, 15635.257875000001, 2487.368917, 2690.102583 ], "after_runs": [ 3542.4995, 3960.4449160000004, 3370.2108329999996, 3340.155792, 2867.348625 ], "paired_speedup_median": 0.9381846907437005 }, { "scope": "export", "case": "60s clip 60m export_ms", "before_runs": [ 5510.017666, 9736.093291, 15778.977333, 2478.722416, 2633.77925 ], "after_runs": [ 3061.388167, 10440.471208, 5550.754041, 2467.545333, 2514.443458 ], "paired_speedup_median": 1.0474601214914254 } ]CPU uniform benchmark
Temporary file:
crates/rendering/examples/styled-uniforms-benchmark.rsAllocation, cold, boundary and random-seek benchmark
Temporary file:
crates/project/examples/style-cache-benchmark.rsIndependent original-implementation differential oracle
Temporary file:
crates/project/tests/independent_style_cache.rsNative Metal rendering and pixel comparison probe
Temporary file:
crates/rendering/examples/independent-native-probe.rsComplete MP4 export probe
Temporary file:
crates/export/examples/independent-style-export.rsExport fixtures and decoded-frame checks
Generate 640×360, 30 fps test-source MP4 input for each duration. The seven-second configuration uses styles from 1 to 5 seconds; the one-minute configuration uses styles from 1 to 59 seconds. Enable captions, include both source and projected caption records, and use the same input and configuration on both variants. Captions have 200/1,200 segments with eight words each, spaced three seconds apart. This matches the uniform fixture's caption density, with caption rendering explicitly enabled for export.
The following temporary Python script generates either measured configuration structure and a synthetic source clip. Its generated 10/60-minute caption configurations were compared structurally with all four measured configuration files. Run
python3 export-fixture.py input.cap 7or use duration60. Useinput.cap/config-10.jsonandinput.cap/config-60.jsonwith the export probe. FFmpeg source encoding can vary by local version; within each comparison both renderer variants must use the exact same generated input.Fixture recording metadata:
{"pretty_name":"Independent style-cache validation","display":{"path":"content/display.mp4","fps":30},"camera":null,"audio":null,"cursor":null}Use the export probe as:
Require exactly 210 or 1,800 decoded frames, depending on duration, and compare every non-comment framemd5 record, including timestamps. All twenty local before/after pairs matched exactly. The MP4 containers themselves were not used for equality.


Confidence Score: 5/5
The PR appears safe to merge; no actionable issues were found.Summary
Reuses a resolved styled project while the source
Arcand winning background, camera, and cursor segments stay unchanged.Reviews (1) · Last reviewed commit: "perf(rendering): cache resolved styles b..." · Reviewed by Greptile