Files
effectcraft/crates/ui-egui/examples/cache_bench.rs
T
nazar-gavrylyk 6bb52b1cb1 cache_bench: --gpu measures the GPU compositor
--gpu renders on a headless GPU of its own and reads the frames back, as the
desktop viewer does; --backend cpu|gpu|auto picks the compositor (default
auto, the viewer's Mercury GPU Acceleration). With auto the bench prints
Auto's per-comp timing history: how many frames each side rendered and how
long they took.
2026-11-10 15:03:10 +01:00

207 lines
9.7 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`,
//! `--gpu` (composite on a GPU of its own, as the desktop app does with Mercury GPU Acceleration:
//! the GPU or the CPU, whichever Auto measures faster per comp, read back to the CPU; the viewer's
//! backend), `--backend cpu|gpu|auto` (with `--gpu`: force one compositor, default `auto`).
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);
use effectcraft_engine::render::Backend;
let backend = match arg("--backend").as_deref() {
Some("cpu") => Backend::Cpu,
Some("gpu") => Backend::Gpu,
_ => Backend::Auto,
};
let opts = RenderOpts { scale, motion_blur: true, backend, ..Default::default() };
let gpu = std::env::args().any(|a| a == "--gpu").then(|| {
effectcraft_gpu::Gpu::headless().unwrap_or_else(|| {
eprintln!("--gpu: no usable GPU adapter");
std::process::exit(2)
})
});
let src = RenderSource {
project: s.project.clone(),
footage: s.footage.clone(),
expr: s.expr.clone(),
layer_cache: s.layer_cache.clone(),
gpu,
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,
if src.gpu.is_some() { " on the GPU" } else { "" }
);
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
);
if let Some(g) = &src.gpu
&& let Some(p) = effectcraft_engine::render::Accelerator::auto_pick(g)
&& let Some(a) = p.stats(effectcraft_engine::render::AutoKey::new(cid, scale, false))
{
let ms = |v: Option<f64>| v.map_or("–".into(), |v| format!("{v:.1} ms"));
println!("auto (Mercury) GPU {} over {} frames, CPU {} over {} frames", ms(a.gpu_ms), a.gpu_frames, ms(a.cpu_ms), a.cpu_frames);
}
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);
}
}