render: the M4 audit follow-ups — autocache priority, cancel-in-flight, decode LRU
- Autocache range jobs now post at Background priority (submit_video_background): they used to go through the Seek path and, after the M4 seek over-admission, jumped ahead of playback and past the render-queue bound. The interactive single-frame preview keeps Seek. - Job.cancelled: the arena installs the slot's cancel atom, and execute_job finishes a cancelled job with Error::State before running the producer — a cancel no longer burns a full render/GPU pass only to discard the result. Exactly-once delivery is unchanged. - DECODE_LRU_CAP 8 -> 2: the decode service's LRU is a hand-off buffer, not the cache of record (the eval-side decoded_frames LRU is); the double-cache footprint at 1080p F32 drops by ~6 frames. A hand-off miss is served from the eval cache without a new decode. - Tests: sequence-aware preview cancel, over-admitted seek ordering, deterministic prefetch LRU reuse, cancelled-job skip, autocache priority. docs §3.4 backfilled with the A/B/C audit outcomes.
This commit is contained in:
@@ -39,9 +39,7 @@ use std::time::{Duration, Instant};
|
||||
|
||||
use oak_core::{PixelFormat, Rational};
|
||||
use oak_render::procpool::{DispatcherConfig, ProcessDispatcher};
|
||||
use oak_render::ticket::{
|
||||
Completion, Producer, TicketPayload, TicketResult, VideoTicketParams,
|
||||
};
|
||||
use oak_render::ticket::{Completion, Producer, TicketPayload, TicketResult, VideoTicketParams};
|
||||
use oak_render::worker::{Job, JobDispatch, JobSchedule};
|
||||
|
||||
/// The comparison frame format: both backends must produce the same
|
||||
@@ -80,10 +78,7 @@ fn cpu_times() -> (f64, f64) {
|
||||
}
|
||||
let self_times = rusage(libc::RUSAGE_SELF);
|
||||
let children = rusage(libc::RUSAGE_CHILDREN);
|
||||
(
|
||||
self_times.0 + children.0,
|
||||
self_times.1 + children.1,
|
||||
)
|
||||
(self_times.0 + children.0, self_times.1 + children.1)
|
||||
}
|
||||
|
||||
/// One footage ticket over the whole timeline.
|
||||
@@ -141,10 +136,7 @@ fn report(entries: &[(i64, Instant, Instant)], start: Instant, elapsed: Duration
|
||||
),
|
||||
);
|
||||
}
|
||||
report(
|
||||
"cpu user + sys",
|
||||
format!("{:.2} + {:.2} s", cpu.0, cpu.1),
|
||||
);
|
||||
report("cpu user + sys", format!("{:.2} + {:.2} s", cpu.0, cpu.1));
|
||||
report(
|
||||
"main-heap frame copies",
|
||||
oak_render::procpool::main_heap_frame_copies().to_string(),
|
||||
@@ -182,7 +174,12 @@ fn run_processes(media: &str, frames: usize, width: i32, height: i32, workers: O
|
||||
let job = Job {
|
||||
node_identity: 1,
|
||||
time: Rational::new(frame, 25),
|
||||
params: Arc::new(footage_params(&media_clone, Rational::new(frame, 25), width, height)),
|
||||
params: Arc::new(footage_params(
|
||||
&media_clone,
|
||||
Rational::new(frame, 25),
|
||||
width,
|
||||
height,
|
||||
)),
|
||||
audio: None,
|
||||
produce: Arc::new(|_, _| {
|
||||
Err(oak_render::error::Error::Failed(
|
||||
@@ -191,10 +188,11 @@ fn run_processes(media: &str, frames: usize, width: i32, height: i32, workers: O
|
||||
}),
|
||||
done: Box::new(move |result: TicketResult| match result {
|
||||
Ok(TicketPayload::ShmFrame(f)) => {
|
||||
results
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.push((frame, start, Instant::now()));
|
||||
results.lock().unwrap_or_else(|e| e.into_inner()).push((
|
||||
frame,
|
||||
start,
|
||||
Instant::now(),
|
||||
));
|
||||
dc.release_frame(&f);
|
||||
}
|
||||
Ok(TicketPayload::ShmAudio(a)) => {
|
||||
@@ -208,6 +206,7 @@ fn run_processes(media: &str, frames: usize, width: i32, height: i32, workers: O
|
||||
}
|
||||
}),
|
||||
schedule: JobSchedule::playback(frame, frame, 0),
|
||||
cancelled: None,
|
||||
};
|
||||
if !dispatcher.post(job) {
|
||||
eprintln!("post refused at frame {frame}");
|
||||
@@ -262,25 +261,30 @@ fn run_pipeline(media: &str, frames: usize, width: i32, height: i32) {
|
||||
let submitted = Instant::now();
|
||||
let done: Completion = Box::new(move |result: TicketResult| {
|
||||
if let Ok(TicketPayload::Video(_)) = result {
|
||||
results
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.push((frame, submitted, Instant::now()));
|
||||
results.lock().unwrap_or_else(|e| e.into_inner()).push((
|
||||
frame,
|
||||
submitted,
|
||||
Instant::now(),
|
||||
));
|
||||
}
|
||||
});
|
||||
let producer: Producer =
|
||||
Arc::new(|time, params| {
|
||||
oak_render::eval::render_produced_frame(time, params)
|
||||
.map(TicketPayload::Video)
|
||||
});
|
||||
let producer: Producer = Arc::new(|time, params| {
|
||||
oak_render::eval::render_produced_frame(time, params).map(TicketPayload::Video)
|
||||
});
|
||||
let job = Job {
|
||||
node_identity: 1,
|
||||
time: Rational::new(frame, 25),
|
||||
params: Arc::new(footage_params(media, Rational::new(frame, 25), width, height)),
|
||||
params: Arc::new(footage_params(
|
||||
media,
|
||||
Rational::new(frame, 25),
|
||||
width,
|
||||
height,
|
||||
)),
|
||||
audio: None,
|
||||
produce: producer,
|
||||
done,
|
||||
schedule: JobSchedule::playback(frame, frame, 0),
|
||||
cancelled: None,
|
||||
};
|
||||
// The blocking post is the pipeline's backpressure: once the render
|
||||
// queue is full the submitter waits (the app's window is capped by
|
||||
@@ -332,7 +336,9 @@ fn main() {
|
||||
.nth(4)
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(480);
|
||||
let mode = std::env::args().nth(5).unwrap_or_else(|| "processes".to_string());
|
||||
let mode = std::env::args()
|
||||
.nth(5)
|
||||
.unwrap_or_else(|| "processes".to_string());
|
||||
|
||||
// The app's preview proxy size: the sequence's aspect scaled to the
|
||||
// long edge (demo.mp4 is 16:9 1080p).
|
||||
|
||||
@@ -57,8 +57,7 @@ fn worker_bin() -> PathBuf {
|
||||
// target/<profile>/examples/bench_process -> target/<profile>/oak-worker
|
||||
if let Some(examples) = exe.parent() {
|
||||
if let Some(profile) = examples.parent() {
|
||||
let candidate =
|
||||
profile.join(format!("oak-worker{}", std::env::consts::EXE_SUFFIX));
|
||||
let candidate = profile.join(format!("oak-worker{}", std::env::consts::EXE_SUFFIX));
|
||||
if candidate.exists() {
|
||||
return candidate;
|
||||
}
|
||||
@@ -91,9 +90,7 @@ fn main() {
|
||||
let dispatcher = ProcessDispatcher::new(config).expect("dispatcher config");
|
||||
dispatcher.start().expect("workers start + handshake");
|
||||
let worker_count = dispatcher.worker_count();
|
||||
println!(
|
||||
"oak-worker pool: {worker_count} worker(s), {frames} x {width}x{height} BGRA8 frames"
|
||||
);
|
||||
println!("oak-worker pool: {worker_count} worker(s), {frames} x {width}x{height} BGRA8 frames");
|
||||
|
||||
// One completion record per frame: (frame number, wall-clock completion).
|
||||
let results = Arc::new(Mutex::new(Vec::<(i64, Instant)>::new()));
|
||||
@@ -152,6 +149,7 @@ fn main() {
|
||||
// Playback priority: the pre-render window schedule (seek/playback
|
||||
// prioritization is what the scheduler benchmark measures).
|
||||
schedule: JobSchedule::playback(frame, 0, 0),
|
||||
cancelled: None,
|
||||
};
|
||||
if !dispatcher.post(job) {
|
||||
eprintln!("post refused at frame {frame}");
|
||||
@@ -175,7 +173,11 @@ fn main() {
|
||||
}
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
let mut entries: Vec<(i64, Instant)> = results.lock().unwrap_or_else(|e| e.into_inner()).drain(..).collect();
|
||||
let mut entries: Vec<(i64, Instant)> = results
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.drain(..)
|
||||
.collect();
|
||||
entries.sort_by_key(|(id, _)| *id);
|
||||
let completed = entries.len();
|
||||
let throughput = completed as f64 / elapsed.as_secs_f64();
|
||||
@@ -195,18 +197,30 @@ fn main() {
|
||||
};
|
||||
report("frames completed", completed.to_string());
|
||||
report("total wall time", format!("{:.2} s", elapsed.as_secs_f64()));
|
||||
report("throughput", format!("{throughput:.1} fps ({:.1} ms/frame)", 1000.0 / throughput));
|
||||
report(
|
||||
"throughput",
|
||||
format!("{throughput:.1} fps ({:.1} ms/frame)", 1000.0 / throughput),
|
||||
);
|
||||
if !deltas.is_empty() {
|
||||
let mean = deltas.iter().sum::<f64>() / deltas.len() as f64;
|
||||
let p95 = deltas[((deltas.len() as f64 * 0.95) as usize).min(deltas.len() - 1)];
|
||||
report("adjacent-frame delta (pairs)", deltas.len().to_string());
|
||||
report(" max", format!("{:.3} ms", deltas.last().unwrap() * 1000.0));
|
||||
report(
|
||||
" max",
|
||||
format!("{:.3} ms", deltas.last().unwrap() * 1000.0),
|
||||
);
|
||||
report(" mean", format!("{:.3} ms", mean * 1000.0));
|
||||
report(" p95", format!("{:.3} ms", p95 * 1000.0));
|
||||
} else {
|
||||
report("adjacent-frame delta", "no adjacent pairs completed".to_string());
|
||||
report(
|
||||
"adjacent-frame delta",
|
||||
"no adjacent pairs completed".to_string(),
|
||||
);
|
||||
}
|
||||
report("main-heap frame copies", oak_render::procpool::main_heap_frame_copies().to_string());
|
||||
report(
|
||||
"main-heap frame copies",
|
||||
oak_render::procpool::main_heap_frame_copies().to_string(),
|
||||
);
|
||||
|
||||
dispatcher.shutdown();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user