Files
effectcraft/crates/media/tests/open_in_place.rs
vangel76andClaude Opus 5.5 d648026ffd #794: open movies in place instead of reading them whole
Problem: probe() and MediaPool's movie sources called std::fs::read and then
filmcraft_codecs::open_bytes, so a clip cost its full size in memory, twice
while probing (a 245 MiB MP4 peaked at 494 MiB just to import).

Fix: container footage on disk is opened through a positional-read
ByteReader (stream::FileReader) and FilmCraft's reader openers
(filmcraft_media::reader::open_reader; MP4/MOV, Matroska/WebM, MXF, Ogg,
MPEG-PS/TS), which read the index and then single samples. Probing takes this
path only when nothing probed before containers (still/model extensions,
PSD/SVG/PDF, image magic) claims the file; everything else, whole-file audio
formats, in-memory files (add_bytes) and wasm32 builds keep the byte path.
Regression test: probing and decoding a frame and audio of a movie with a
256 MiB tail grows peak RSS by <64 MiB (512 MiB before), same results.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 13:42:24 +02:00

102 lines
4.2 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Opening a long movie reads its index, not the whole file: probing it and decoding a frame and
//! some audio must not grow peak memory by anything near the file's size.
//!
//! The large movie is a small ffmpeg-made MP4 followed by a 256 MiB `free` box (a sparse file, so
//! it costs no disk writes). Reading it whole, as before, grew peak RSS by its size; read in place,
//! the box is skipped by its header. The only test in this binary, so peak RSS is its own.
//! Skips (passes with a note) without ffmpeg or off Linux (peak RSS from `/proc/self/status`).
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
use effectcraft_media::{MediaPool, probe};
use effectcraft_time::Tick;
const PADDING: u64 = 256 << 20;
fn fixtures() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target/fixtures/media")
}
/// A 2 s 320×240 H.264 + AAC movie (written to a temp name and renamed, so concurrent test
/// processes never see a partial file). `None` without ffmpeg.
fn small_mp4() -> Option<PathBuf> {
let out = fixtures().join("in_place_small.mp4");
if out.exists() {
return Some(out);
}
if !Command::new("ffmpeg").arg("-version").output().is_ok_and(|o| o.status.success()) {
return None;
}
std::fs::create_dir_all(fixtures()).unwrap();
let tmp = fixtures().join(format!("in_place_tmp_{}.mp4", std::process::id()));
let st = Command::new("ffmpeg")
.args(["-hide_banner", "-loglevel", "error", "-y", "-f", "lavfi", "-i", "testsrc2=size=320x240:rate=30:duration=2"])
.args(["-f", "lavfi", "-i", "sine=frequency=440:sample_rate=48000:duration=2"])
.args(["-c:v", "libx264", "-pix_fmt", "yuv420p", "-c:a", "aac", "-shortest"])
.arg(&tmp)
.status()
.unwrap();
assert!(st.success(), "ffmpeg failed");
std::fs::rename(&tmp, &out).unwrap();
Some(out)
}
/// `/proc/self/status` field `key` in KiB.
fn status_kib(key: &str) -> Option<u64> {
let s = std::fs::read_to_string("/proc/self/status").ok()?;
let line = s.lines().find(|l| l.starts_with(key))?;
line.split_whitespace().nth(1)?.parse().ok()
}
/// Peak RSS so far in bytes, after resetting it to the current RSS where the kernel allows that.
fn reset_peak() -> Option<u64> {
let _ = std::fs::write("/proc/self/clear_refs", "5");
status_kib("VmHWM:").map(|k| k * 1024)
}
#[test]
fn a_long_movie_is_read_in_place_not_whole() {
if !cfg!(target_os = "linux") {
eprintln!("peak RSS is read from /proc: skipping");
return;
}
let Some(small) = small_mp4() else {
eprintln!("ffmpeg not found: skipping");
return;
};
// The same movie with 256 MiB of padding after its boxes.
let large = fixtures().join(format!("in_place_large_{}.mp4", std::process::id()));
std::fs::copy(&small, &large).unwrap();
let mut f = std::fs::OpenOptions::new().append(true).open(&large).unwrap();
f.write_all(&(8 + PADDING as u32).to_be_bytes()).unwrap();
f.write_all(b"free").unwrap();
f.set_len(std::fs::metadata(&small).unwrap().len() + 8 + PADDING).unwrap();
drop(f);
// Warm up on the small file (code paths, allocator, decoder tables).
let small_footage = probe(&small).unwrap();
let pool = MediaPool::new();
pool.set_read_ahead(false);
let small_frame = pool.frame_at(&small_footage, Tick::ZERO).unwrap();
let small_audio = pool.audio_samples(&small_footage, Tick::ZERO, 4800, 48_000);
let before = reset_peak().unwrap();
let footage = probe(&large).unwrap();
let frame = pool.frame_at(&footage, Tick::ZERO).unwrap();
let audio = pool.audio_samples(&footage, Tick::ZERO, 4800, 48_000);
let after = status_kib("VmHWM:").unwrap() * 1024;
drop(pool);
let _ = std::fs::remove_file(&large);
let growth = after.saturating_sub(before);
eprintln!("peak RSS growth: {} KiB", growth >> 10);
assert!(growth < 64 << 20, "probing and decoding a {} MiB movie grew peak RSS by {} MiB", PADDING >> 20, growth >> 20);
// Same movie, same results.
assert_eq!(footage, effectcraft_project::Footage { path: footage.path.clone(), ..small_footage });
assert_eq!(frame, small_frame);
assert_eq!(audio, small_audio);
assert!(audio.iter().any(|s| s.abs() > 0.1), "the tone decodes");
}