Skip to content

perf(preload): bound the extracted video frame cache - #240

Closed
LeadcodeDev wants to merge 1 commit into
perf/gif-decode-stampedefrom
perf/video-frame-cache-budget
Closed

LeadcodeDev wants to merge 1 commit into
perf/gif-decode-stampedefrom
perf/video-frame-cache-budget

Conversation

@LeadcodeDev

Copy link
Copy Markdown
Owner

Severity Medium, category quality. Location: crates/rustmotion/src/engine/preload.rs:238

Impact

Triggering scenario: a scenario JSON declaring a video component with style.width/height of 1920x1080 inside a 60 s scene at 30 fps. .output() (line 234) collects ffmpeg's entire rawvideo stdout into one Vec<u8>: 1800 frames x 8.3 MB = 15 GB. data then stays borrowed for the whole copy loop, so the per-frame .to_vec() copies build a second 15 GB structure alongside it — 30 GB peak before output.stdout is dropped. The result is inserted into the process-global VIDEO_FRAME_CACHE (assets.rs:168), which has no eviction and no clear function at all. There is no size, duration, or resolution cap anywhere on this path, and the inputs are entirely scenario-controlled. Under --watch this compounds: changing the component's style.width mints a new "src:WxH" key while the old multi-GB entry is never freed. Secondary hazard on the same line: width * height * 4 is plain u32 arithmetic, so a declared 65536x16384 wraps to 0 and data.len() / frame_size panics with a divide-by-zero.

Fix

Stream ffmpeg's stdout frame-by-frame into the frames vector instead of .output() (spawn + read exact frame_size chunks from the pipe), which removes the 2x peak. Add a byte budget for the cache and refuse (with a clear error) sources that exceed it, and make frame_size a checked u64 computation.

Evidence the audit read

let frame_size = (width * height * 4) as usize;
let data = &output.stdout;
let num_frames = data.len() / frame_size;
let mut frames: Vec<(f64, Vec<u8>, u32, u32)> = Vec::with_capacity(num_frames);
for idx in 0..num_frames {
    let start = idx * frame_size;
    let frame_data = data[start..start + frame_size].to_vec();
    ...
}
cache.insert(cache_key, Arc::new(frames));

Stacked on perf/gif-decode-stampede, which carries the previous finding of this workstream. GitHub shows only this finding's diff; merge in order.

Part of the September 2026 audit remediation chantier. Refs #220 (RM-21).

@LeadcodeDev LeadcodeDev added the bug Something isn't working label Sep 21, 2026
@LeadcodeDev LeadcodeDev self-assigned this Sep 21, 2026
@LeadcodeDev
LeadcodeDev force-pushed the perf/gif-decode-stampede branch from 9ab7bc9 to 4d15a91 Compare September 22, 2026 06:10
@LeadcodeDev
LeadcodeDev force-pushed the perf/video-frame-cache-budget branch from 2eeb8b8 to e10ffb1 Compare September 22, 2026 06:10
@LeadcodeDev
LeadcodeDev force-pushed the perf/gif-decode-stampede branch from 4d15a91 to c89ddad Compare September 22, 2026 08:34
@LeadcodeDev
LeadcodeDev force-pushed the perf/video-frame-cache-budget branch from e10ffb1 to f6a17df Compare September 22, 2026 08:34
@LeadcodeDev
LeadcodeDev force-pushed the perf/gif-decode-stampede branch from c89ddad to 1c12b20 Compare September 22, 2026 08:44
Triggering scenario: a scenario JSON declaring a video component with
style.width/height of 1920x1080 inside a 60 s scene at 30 fps. .output()
(line 234) collects ffmpeg's entire rawvideo stdout into one Vec<u8>: 1800
frames x 8.3 MB = 15 GB. data then stays borrowed for the whole copy loop,
so the per-frame .to_vec() copies build a second 15 GB structure alongside
it — 30 GB peak before output.stdout is dropped. The result is inserted into
the process-global VIDEO_FRAME_CACHE (assets.rs:168), which has no eviction
and no clear function at all. There is no size, duration, or resolution cap
anywhere on this path, and the inputs are entirely scenario-controlled.
Under --watch this compounds: changing the component's style.width mints a
new "src:WxH" key while the old multi-GB entry is never freed. Secondary
hazard on the same line: width * height * 4 is plain u32 arithmetic, so a
declared 65536x16384 wraps to 0 and data.len() / frame_size panics with a
divide-by-zero.

Fix: Stream ffmpeg's stdout frame-by-frame into the frames vector instead of
.output() (spawn + read exact frame_size chunks from the pipe), which
removes the 2x peak. Add a byte budget for the cache and refuse (with a
clear error) sources that exceed it, and make frame_size a checked u64
computation.

Refs #220
@LeadcodeDev
LeadcodeDev force-pushed the perf/video-frame-cache-budget branch from f6a17df to 5e0f4c1 Compare September 22, 2026 08:44
@LeadcodeDev
LeadcodeDev deleted the branch perf/gif-decode-stampede September 22, 2026 08:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant