Files
effectcraft/crates/ui-egui/examples/cache_bench.rs
T
nazar-gavrylyk 38dffd4aea Disk cache: frames and layer buffers are compressed on the writer thread
A render thread handed each frame to the disk cache already copied pixel by
pixel and LZ4-compressed (~9 ms per 1080p frame) before it could start the
next one, and layer buffers were encoded even when the queue then dropped
them. The queue now holds raw RGBA (one memcpy via ColorImage::as_raw) and
Arc<Buf>s; the writer thread encodes them. Every frame of a 120-frame bench
run now reaches disk (113–119 before). cache_bench prints the flush time.
2026-11-10 15:03:10 +01:00

180 lines
8.4 KiB
Rust
Raw 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.
//! Background caching benchmark: how fast the RAM preview fills a comp, and where the time goes.
//!
//! `cargo run --release -p effectcraft-ui-egui --example cache_bench -- [options]`
//!
//! Without `--project` it builds a comp like a typical edit: eight 1080p30 H.264 clips one after
//! another (encoded once into `target/cache-bench/media` with the Render Queue, no committed
//! media) under an adjustment layer with Gaussian Blur. It then fills the comp's RAM preview the
//! way idle caching does, through [`Frames`], and prints frames per second with the decoder,
//! footage-cache and disk-cache counters.
//!
//! Options: `--project P --comp C` (a saved project's comp instead), `--scale S` (render scale,
//! default 0.5: a viewer at half size), `--frames N` (frames to cache, default the whole comp),
//! `--slots N` (frames in flight, at least 1, default one per frame thread as the app does when
//! idle),
//! `--disk` (with a fresh disk cache in `target/cache-bench/disk`), `--no-adjustment`.
use std::sync::Arc;
use std::time::{Duration, Instant};
use effectcraft_engine::Session;
use effectcraft_engine::project::ItemId;
use effectcraft_engine::render::RenderOpts;
use effectcraft_ui_egui::frames::{FrameKey, Frames, RenderSource, opts_hash};
use serde_json::{Value, json};
fn arg(name: &str) -> Option<String> {
let a: Vec<String> = std::env::args().collect();
a.iter().position(|x| x == name).and_then(|i| a.get(i + 1).cloned())
}
fn run(s: &mut Session, cmd: &str, p: Value) -> Value {
s.execute(cmd, p).unwrap_or_else(|e| panic!("{cmd}: {e}"))
}
/// Eight copies of one generated clip (separate files: separate decoders, as with real clips).
fn clips(dir: &std::path::Path) -> Vec<String> {
let media = dir.join("media");
std::fs::create_dir_all(&media).expect("media folder");
let first = media.join("clip0.mp4");
if !first.exists() {
eprintln!("encoding the source clip (once)…");
let mut s = effectcraft_host::session();
run(&mut s, "comp.new", json!({"name": "Src", "width": 1920, "height": 1080, "frameRate": 30, "duration": 4.25}));
let l = run(&mut s, "layer.newSolid", json!({"color": "#808080"}))["layer"].clone();
run(&mut s, "effect.apply", json!({"layers": [l], "effect": "Fractal Noise"}));
run(&mut s, "prop.addKey", json!({"layer": l, "path": "effects/#1/evolution", "time": 0.0, "value": 0.0}));
run(&mut s, "prop.addKey", json!({"layer": l, "path": "effects/#1/evolution", "time": 4.25, "value": 720.0}));
run(&mut s, "renderQueue.add", json!({"comp": "Src", "format": "h264", "output": first.to_string_lossy()}));
run(&mut s, "renderQueue.render", json!({"wait": true}));
}
(0..8)
.map(|i| {
let p = media.join(format!("clip{i}.mp4"));
if !p.exists() {
std::fs::copy(&first, &p).expect("copy clip");
}
p.to_string_lossy().to_string()
})
.collect()
}
fn main() {
let dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/cache-bench");
let mut s = effectcraft_host::session();
let pool = Arc::new(effectcraft_media::MediaPool::new());
s.footage = pool.clone();
if std::env::args().any(|a| a == "--disk") {
let disk = dir.join("disk");
if let Err(e) = std::fs::remove_dir_all(&disk)
&& e.kind() != std::io::ErrorKind::NotFound
{
eprintln!("can't clear the disk cache at {}: {e}", disk.display());
std::process::exit(2);
}
s.prefs.disk.disk_cache_enabled = true;
s.prefs.disk.disk_cache_folder = disk.to_string_lossy().to_string();
s.configure_disk_cache();
}
let cid = match arg("--project") {
Some(p) => {
run(&mut s, "file.open", json!({"path": p}));
let comp = match arg("--comp") {
Some(c) => s.project.comps().find(|(id, _)| s.project.item(**id).is_some_and(|it| it.name == c) || id.0.to_string() == c).map(|(id, _)| *id),
None => s.active_comp_id(),
};
comp.unwrap_or_else(|| {
eprintln!("no such composition: {}", arg("--comp").unwrap_or_else(|| "(no active comp; pass --comp)".into()));
std::process::exit(2)
})
}
None => {
let paths = clips(&dir);
let cid = ItemId(
run(&mut s, "comp.new", json!({"name": "Edit", "width": 1920, "height": 1080, "frameRate": 30, "duration": 34}))["comp"]
.as_u64()
.expect("comp"),
);
let items = run(&mut s, "file.import", json!({"paths": paths}))["items"].clone();
for (i, item) in items.as_array().expect("items").iter().enumerate() {
let l = run(&mut s, "layer.addItem", json!({"item": item}))["layer"].as_u64().expect("layer");
let c = Arc::make_mut(&mut s.project).comp_mut(cid).expect("comp");
let fr = c.frame_rate;
let l = c.layer_mut(effectcraft_engine::project::LayerId(l)).expect("layer");
let at = fr.tick_of(i as i64 * 1275 / 10);
(l.start_time, l.in_point, l.out_point) = (at, at, fr.tick_of((i as i64 + 1) * 1275 / 10));
}
if !std::env::args().any(|a| a == "--no-adjustment") {
let adj = run(&mut s, "layer.newAdjustment", json!({}))["layer"].clone();
run(&mut s, "effect.apply", json!({"layers": [adj], "effect": "Gaussian Blur"}));
run(&mut s, "prop.set", json!({"layer": adj, "path": "effects/#1/blurriness", "value": 4.0}));
}
cid
}
};
let comp = s.project.comp(cid).expect("comp").clone();
let total = comp.frame_rate.frame_at(comp.duration);
let n = arg("--frames").and_then(|v| v.parse().ok()).unwrap_or(total).min(total);
let scale: f64 = arg("--scale").and_then(|v| v.parse().ok()).unwrap_or(0.5);
let frames = Frames::default();
// (as Cache Frames When Idle: one frame per frame thread, see `app/prefetch.rs`)
let slots: usize = arg("--slots").and_then(|v| v.parse().ok()).map(|n: usize| n.max(1)).unwrap_or(frames.parallelism().max(1));
// Room for every frame: this measures speed, not eviction.
frames.set_budget(64 << 30);
let opts = RenderOpts { scale, motion_blur: true, ..Default::default() };
let src = RenderSource {
project: s.project.clone(),
footage: s.footage.clone(),
expr: s.expr.clone(),
layer_cache: s.layer_cache.clone(),
gpu: None,
gpu_display: false,
disk: s.disk_cache.clone(),
};
let series = FrameKey {
revision: 1,
content: frames.content_of(&src.project, 1, cid),
comp: cid.0,
frame: 0,
scale: (scale * 1000.0).round() as u32,
view: 0,
opts: opts_hash(&opts),
};
eprintln!("caching {n} frames of {}×{} at scale {scale} with {slots} in flight…", comp.width, comp.height);
let (gop0, t0) = (effectcraft_media::gop_stats(), Instant::now());
let mut next = 0;
loop {
while next < n && frames.inflight() < slots {
frames.request_background(&src, FrameKey { frame: next, ..series }, cid, comp.frame_rate.tick_of(next), opts);
next += 1;
}
if next >= n && frames.inflight() == 0 {
break;
}
std::thread::sleep(Duration::from_millis(1));
}
let secs = t0.elapsed().as_secs_f64();
let gop = effectcraft_media::gop_stats();
let cached = frames.cached_frames(&series).len();
println!("frames cached {cached}/{n} in {secs:.2} s: {:.1} frames/s, {:.1} ms each", cached as f64 / secs, secs * 1000.0 / cached.max(1) as f64);
println!(
"video decoding {:.2} s in decoders, {} samples decoded, {} seeks, {} cache hits",
(gop.decode_ns - gop0.decode_ns) as f64 / 1e9,
gop.decoded - gop0.decoded,
gop.seeks - gop0.seeks,
gop.hits - gop0.hits
);
let p = pool.stats();
println!("footage cache {} hits, {} misses, {} frames ({} MB) kept", p.hits, p.misses, p.frames, p.bytes >> 20);
if let Some(dc) = &s.disk_cache {
// (writes finish on the disk cache's own thread)
let t = Instant::now();
dc.flush();
let flushed = t.elapsed().as_secs_f64();
let d = dc.stats();
println!("disk cache {} written, {} dropped, {} entries ({} MB), flushed in {flushed:.2} s", d.writes, d.dropped, d.entries, d.bytes >> 20);
}
}