mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-10 20:41:25 +00:00
Preview: frame lanes are four threads wide, and cache_bench measures the cadence
This commit is contained in:
1 parent
893d9a0b04
commit
3709cccedf
4 files changed
+91
-20
No files matched your search
@@ -10,8 +10,8 @@
|
||||
//!
|
||||
//! 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),
|
||||
//! `--slots N` (frames in flight, at least 1, default one per lane as the app does when idle),
|
||||
//! `--lane-width W` (threads per lane; the app's default when absent; `--play` takes it too),
|
||||
//! `--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
|
||||
@@ -37,6 +37,11 @@ fn arg(name: &str) -> Option<String> {
|
||||
a.iter().position(|x| x == name).and_then(|i| a.get(i + 1).cloned())
|
||||
}
|
||||
|
||||
/// `--lane-width W`: threads per frame lane (the app's default when absent).
|
||||
fn lane_width() -> Option<usize> {
|
||||
arg("--lane-width").and_then(|v| v.parse().ok())
|
||||
}
|
||||
|
||||
fn run(s: &mut Session, cmd: &str, p: Value) -> Value {
|
||||
s.execute(cmd, p).unwrap_or_else(|e| panic!("{cmd}: {e}"))
|
||||
}
|
||||
@@ -130,9 +135,9 @@ fn main() {
|
||||
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));
|
||||
let frames = lane_width().map_or_else(Frames::default, Frames::with_lane_width);
|
||||
// (as Cache Frames When Idle: one frame per lane, see `app/prefetch.rs`)
|
||||
let slots: usize = arg("--slots").and_then(|v| v.parse().ok()).map(|n: usize| n.max(1)).unwrap_or(frames.lanes());
|
||||
// Room for every frame: this measures speed, not eviction.
|
||||
frames.set_budget(64 << 30);
|
||||
use effectcraft_engine::render::Backend;
|
||||
@@ -168,9 +173,10 @@ fn main() {
|
||||
};
|
||||
|
||||
eprintln!(
|
||||
"caching {n} frames of {}×{} at scale {scale} with {slots} in flight{}…",
|
||||
"caching {n} frames of {}×{} at scale {scale} with {slots} in flight on {} lanes{}…",
|
||||
comp.width,
|
||||
comp.height,
|
||||
frames.lanes(),
|
||||
if src.gpu.is_some() { " on the GPU" } else { "" }
|
||||
);
|
||||
let (gop0, t0) = (effectcraft_media::gop_stats(), Instant::now());
|
||||
@@ -224,6 +230,9 @@ fn play(session: effectcraft_engine::Session, cid: ItemId, secs: f64, gpu: bool)
|
||||
let mut h = builder.build_eframe(move |_| EffectcraftApp::new(session).with_control(rx));
|
||||
{
|
||||
let app = h.state_mut();
|
||||
if let Some(w) = lane_width() {
|
||||
app.frames = effectcraft_ui_egui::frames::Frames::with_lane_width(w);
|
||||
}
|
||||
let _ = app.session.execute("comp.open", json!({"comp": cid.0}));
|
||||
app.ui.viewer.zoom = Some(1.0);
|
||||
app.ui.cache_when_idle = false;
|
||||
@@ -242,6 +251,10 @@ fn play(session: effectcraft_engine::Session, cid: ItemId, secs: f64, gpu: bool)
|
||||
let _ = tx.send(ControlRequest { method: "ui.playback".into(), params: json!({"action": "play"}), reply });
|
||||
let t0 = Instant::now();
|
||||
let (mut started, mut stalled, mut last, mut scales) = (None, 0.0, t0, std::collections::BTreeMap::<u32, u64>::new());
|
||||
// When each new frame went on screen (seconds since play was pressed): the cadence. The
|
||||
// current time, not `Playback::shown`: frames shown while the clock holds don't count there.
|
||||
let fr = h.state().session.project.comp(cid).map(|c| c.frame_rate);
|
||||
let (mut shown_at, mut last_frame) = (Vec::<f64>::new(), None);
|
||||
while t0.elapsed().as_secs_f64() < secs {
|
||||
h.step();
|
||||
let now = Instant::now();
|
||||
@@ -252,20 +265,51 @@ fn play(session: effectcraft_engine::Session, cid: ItemId, secs: f64, gpu: bool)
|
||||
stalled += (now - last).as_secs_f64();
|
||||
}
|
||||
}
|
||||
let frame = fr.map(|fr| fr.frame_at(app.session.time()));
|
||||
if app.playback.playing && frame != last_frame {
|
||||
last_frame = frame;
|
||||
shown_at.push((now - t0).as_secs_f64());
|
||||
}
|
||||
*scales.entry(app.shown_series(cid).scale).or_default() += 1;
|
||||
last = now;
|
||||
std::thread::sleep(Duration::from_millis(2));
|
||||
}
|
||||
let app = h.state();
|
||||
let played = started.map_or(0.0, |t| t.elapsed().as_secs_f64());
|
||||
let shown = app.playback.shown;
|
||||
let fr = app.session.project.comp(cid).map(|c| c.frame_rate);
|
||||
let shown = shown_at.len();
|
||||
let frame = fr.map_or(0, |fr| fr.frame_at(app.session.time()));
|
||||
println!(
|
||||
"played {shown} frames shown in {played:.2} s: {:.1} frames/s, stalled {stalled:.2} s, reached frame {frame}{}",
|
||||
shown as f64 / played.max(1e-9),
|
||||
if gpu { " (GPU)" } else { "" }
|
||||
);
|
||||
// Cadence: how evenly the frames went on screen (the intervals between consecutive shown
|
||||
// frames after the first), and how long the first two took.
|
||||
let mut gaps: Vec<f64> = shown_at.windows(2).map(|w| w[1] - w[0]).collect();
|
||||
gaps.sort_by(f64::total_cmp);
|
||||
let at = |q: f64| gaps.get(((gaps.len() as f64 - 1.0) * q).round() as usize).copied().unwrap_or(0.0) * 1000.0;
|
||||
let holds = gaps.iter().filter(|g| **g > 0.2).count();
|
||||
println!(
|
||||
"cadence first frame {:.2} s, second {:.2} s; gaps median {:.0} ms, p95 {:.0} ms, max {:.0} ms, {holds} holds > 200 ms",
|
||||
shown_at.first().copied().unwrap_or(0.0),
|
||||
shown_at.get(1).copied().unwrap_or(0.0),
|
||||
at(0.5),
|
||||
at(0.95),
|
||||
at(1.0)
|
||||
);
|
||||
// The opening (the first 3 s, before an adaptive step down can have taken effect): how
|
||||
// evenly the uncached frames at the starting resolution went on screen.
|
||||
let mut early: Vec<f64> = shown_at.windows(2).filter(|w| w[1] <= 3.0).map(|w| w[1] - w[0]).collect();
|
||||
early.sort_by(f64::total_cmp);
|
||||
let at = |q: f64| early.get(((early.len() as f64 - 1.0) * q).round() as usize).copied().unwrap_or(0.0) * 1000.0;
|
||||
println!(
|
||||
"opening (3 s) {} frames; gaps median {:.0} ms, p95 {:.0} ms, max {:.0} ms, {} holds > 200 ms",
|
||||
early.len() + usize::from(!early.is_empty()),
|
||||
at(0.5),
|
||||
at(0.95),
|
||||
at(1.0),
|
||||
early.iter().filter(|g| **g > 0.2).count()
|
||||
);
|
||||
let passes: u64 = scales.values().sum();
|
||||
let s: Vec<String> = scales.iter().map(|(k, v)| format!("{:.3}: {:.0} %", *k as f64 / 1000.0, 100.0 * *v as f64 / passes.max(1) as f64)).collect();
|
||||
println!("viewer scale {}", s.join(", "));
|
||||
|
||||
@@ -549,13 +549,10 @@ impl EffectcraftApp {
|
||||
side(comp.width) * side(comp.height) * frames::frame_pixel_bytes(&self.session.project.settings) as f64
|
||||
}
|
||||
|
||||
/// Frames rendered at once by a preview or idle caching: the lanes (in the browser the frame
|
||||
/// workers; without them frames render one at a time on the UI thread, `Frames::pump`).
|
||||
fn frames_parallelism(&self) -> usize {
|
||||
if cfg!(target_arch = "wasm32") {
|
||||
// frame workers render one frame each; without them frames render one at a time
|
||||
// on the UI thread (`Frames::pump`)
|
||||
return self.frames.remote_slots().max(1);
|
||||
}
|
||||
self.frames.parallelism()
|
||||
self.frames.lanes()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -707,15 +707,36 @@ struct DiskCarry {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
const MAX_DISK_SERIES: usize = 8;
|
||||
|
||||
/// Threads per lane (see [`Queue::lanes`]). A frame's passes are memory-bound and stop scaling
|
||||
/// at about 4 threads, so a core per lane (12 frames at once, each on one thread) and 4 cores
|
||||
/// per lane cache frames at the same rate (within 3 %, widths 1–4 measured on 12 cores); but the
|
||||
/// 12 single-threaded frames start and finish together, so a preview that can't keep up shows
|
||||
/// them in bursts, and its second frame waits for the whole batch (0.3–1.3 s). Three lanes of
|
||||
/// four threads finish frames 3× as often and the second frame shows in 0.13–0.45 s.
|
||||
/// Never fewer than two lanes: one would finish frames in a single file.
|
||||
const LANE_WIDTH: usize = 4;
|
||||
|
||||
impl Default for Frames {
|
||||
fn default() -> Self {
|
||||
Self::with_lane_width(LANE_WIDTH)
|
||||
}
|
||||
}
|
||||
|
||||
impl Frames {
|
||||
/// A frame cache whose lanes are `width` threads each (see [`LANE_WIDTH`]); `Default` uses
|
||||
/// [`LANE_WIDTH`], benchmarks compare widths.
|
||||
pub fn with_lane_width(width: usize) -> Self {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let _ = width;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let threads = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4).clamp(2, 16);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let lanes: Vec<rayon::ThreadPool> = (0..threads)
|
||||
let width = width.clamp(1, (threads / 2).max(1));
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let lanes: Vec<rayon::ThreadPool> = (0..(threads / width).max(1))
|
||||
.map_while(|i| {
|
||||
rayon::ThreadPoolBuilder::new()
|
||||
.num_threads(1)
|
||||
.num_threads(width)
|
||||
.thread_name(move |_| format!("ec-preview-{i}"))
|
||||
.panic_handler(|_| log::error!("a frame render panicked; that frame was skipped"))
|
||||
.build()
|
||||
@@ -765,6 +786,15 @@ impl Default for Frames {
|
||||
disk_series: Arc::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// How many frames render at once on the lanes (a playing preview's prefetch, Cache Frames
|
||||
/// When Idle); in the browser, the frame workers.
|
||||
pub fn lanes(&self) -> usize {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
return self.remote_slots().max(1);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
self.queue.lock().map_or(1, |q| q.lanes.max(1))
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether viewer frames of a project with `settings` are half floats ([`F16Frame`], rendered
|
||||
|
||||
@@ -215,7 +215,7 @@ fn preview_with_audio_holds_the_first_frame_and_sounds_from_it() {
|
||||
let gate = GatedFrames::default();
|
||||
let renderer: Arc<dyn RemoteFrames> = Arc::new(gate.clone());
|
||||
app.frames.set_remote(Some(renderer));
|
||||
let ahead = (app.frames.parallelism() * 2).max(4) as i64;
|
||||
let ahead = (app.frames.lanes() * 2).max(4) as i64;
|
||||
app.session.execute("comp.settings", json!({"comp": cid.0, "duration": (4 * ahead) as f64 / 30.0})).unwrap();
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).with_step_dt(1.0 / 30.0).build_eframe(|_| app);
|
||||
h.state_mut().use_manual_clock();
|
||||
@@ -258,7 +258,7 @@ fn preview_with_audio_sounds_without_caching_the_work_area() {
|
||||
h.state_mut().use_manual_clock();
|
||||
h.run_steps(2);
|
||||
gate.discard(&mut h);
|
||||
let ahead = (h.state().frames.parallelism() * 2).max(4) as i64;
|
||||
let ahead = (h.state().frames.lanes() * 2).max(4) as i64;
|
||||
let now = h.state().now();
|
||||
h.state_mut().play(now);
|
||||
let frames: Vec<i64> = (0..ahead).collect();
|
||||
@@ -439,7 +439,7 @@ fn until_started(h: &mut Harness<'_, EffectcraftApp>) -> bool {
|
||||
fn startup_finishes_at_any_ram_preview_budget() {
|
||||
let probe = Fake::default();
|
||||
let (app, _) = setup(true, &probe);
|
||||
let ahead = (app.frames.parallelism() * 2).max(4) as i64;
|
||||
let ahead = (app.frames.lanes() * 2).max(4) as i64;
|
||||
// A 64×36 frame is 9216 bytes; `fit` frames fit in (fit + ½) frames' worth / 0.9.
|
||||
let mut fits = vec![2, 3, 4, ahead - 1, ahead, ahead + 1];
|
||||
fits.sort();
|
||||
@@ -505,7 +505,7 @@ fn extended_viewer(fake: &Fake) -> (Harness<'static, EffectcraftApp>, ItemId, [u
|
||||
fn startup_finishes_when_extended_viewer_frames_are_larger() {
|
||||
let fake = Fake::default();
|
||||
let (mut h, cid, size) = extended_viewer(&fake);
|
||||
let ahead = (h.state().frames.parallelism() * 2).max(4);
|
||||
let ahead = (h.state().frames.lanes() * 2).max(4);
|
||||
let (comp_bytes, real_bytes) = (160 * 120 * 4, size[0] * size[1] * 4);
|
||||
// Room for `ahead` comp-sized frames (and a half), which is fewer real ones.
|
||||
let budget = ((ahead as f64 + 0.5) * comp_bytes as f64 / 0.9) as usize;
|
||||
|
||||
Reference in new issue
Block a user