mirror of
https://github.com/storytold/filmcraft.git
synced 2026-11-02 22:40:04 -06:00
A step now renders batch k+1 on the rayon pool while the calling thread encodes, muxes and mixes the audio of batch k. The file is byte-identical with or without overlap. A prefetch that hits pending media is dropped and rendered again, cancel frees it, a render panic becomes an error, and wasm never overlaps. perf.stats export.stages gains waitMs. Memory: up to 2 x batch x frame bytes of finished frames are in flight, so the batch is capped to a 512 MiB budget (1080p keeps 16, 4K SDR 8, 4K HDR 2 frames). Measured peak RSS vs before: 1080p +67..+158 MB; 4K SDR -535..-927 MB; 4K HDR about -2.6 GB (5.2 -> 2.6 GB). Wall time 1.06-1.16x faster. Known pre-existing web issue, untouched: AudioOut::pull advances before the pending check, so a retried audio group is lost. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
122 lines
5.4 KiB
Rust
122 lines
5.4 KiB
Rust
//! `perf.stats`: performance counters an agent can read (headless engine part). The desktop UI
|
|
//! extends the same query with playback, frame-worker and UI timings (`ui-egui` `perf.rs`).
|
|
|
|
use serde_json::{Value, json};
|
|
|
|
use crate::Session;
|
|
|
|
/// Decoder / GOP-cache counters since the process started (they only grow: diff two readings to
|
|
/// measure an interval).
|
|
pub fn decode_json() -> Value {
|
|
let g = filmcraft_codecs::gop_stats();
|
|
let hw = filmcraft_codecs::hw::hw_stats();
|
|
json!({
|
|
"requests": g.hits + g.misses,
|
|
"cacheHits": g.hits,
|
|
"cacheMisses": g.misses,
|
|
"cacheHitRate": g.hit_rate(),
|
|
"seeks": g.seeks,
|
|
"samplesDecoded": g.decoded,
|
|
"samplesSkipped": g.skipped,
|
|
"draftFrames": g.draft,
|
|
"h264Threads": filmcraft_codecs::video::h264_threads(),
|
|
"evicted": g.evicted,
|
|
// sources that hold a decoder right now (idle ones give theirs up beyond a cap)
|
|
"liveDecoders": filmcraft_codecs::live_decoders(),
|
|
// decoded frames all sources hold, and the budget idle sources are trimmed to
|
|
"cacheMB": filmcraft_codecs::cached_bytes() as f64 / 1e6,
|
|
"cacheBudgetMB": filmcraft_codecs::FRAME_BUDGET as f64 / 1e6,
|
|
// plane buffers of evicted frames waiting for the next decoded pictures, and how many
|
|
// planes were decoded into a recycled buffer
|
|
"planePoolMB": filmcraft_frame::pool::stats().idle_bytes as f64 / 1e6,
|
|
"planesReused": filmcraft_frame::pool::stats().reused,
|
|
"decodeMs": g.decode_ns as f64 / 1e6,
|
|
"decodeMsPerSample": g.decode_ms_per_sample(),
|
|
"framesDecoded": g.frames,
|
|
// Settings ▸ Playback ▸ Hardware decoding: pictures from OS hardware decoders vs ours,
|
|
// hardware decoders created, streams handed to software up front, mid-stream fallbacks.
|
|
"hardware": {
|
|
"enabled": filmcraft_codecs::hw::hardware_decoding(),
|
|
// the OS decoder backend registered at startup (null where there is none)
|
|
"backend": filmcraft_codecs::hw::hw_backend(),
|
|
"frames": hw.frames,
|
|
"softwareFrames": g.frames.saturating_sub(hw.frames),
|
|
"sessions": hw.sessions,
|
|
"declined": hw.declined,
|
|
"fallbacks": hw.fallbacks,
|
|
},
|
|
})
|
|
}
|
|
|
|
/// Export counters: pictures encoded by hardware encoders, sessions, declined requests; and the wall
|
|
/// time of each export stage in milliseconds (`stages`: setup, loudness, render, encode with its
|
|
/// `convert` part, audio, mux, finish, wait), summed over the exports of this process (diff two readings).
|
|
/// Render and encode overlap (the next batch renders while this one is encoded), so their sum can exceed
|
|
/// the export's wall time; `waitMs` is the encoding side's idle wait for the render.
|
|
pub fn export_json() -> Value {
|
|
let hw = filmcraft_export::hw_encode_stats();
|
|
let stages: serde_json::Map<String, Value> =
|
|
filmcraft_export::stage_times().into_iter().map(|(s, ns)| (format!("{}Ms", s.name()), json!(ns as f64 / 1e6))).collect();
|
|
json!({"hardware": {"frames": hw.frames, "sessions": hw.sessions, "declined": hw.declined}, "stages": stages})
|
|
}
|
|
|
|
/// The engine's `perf.stats`.
|
|
pub fn stats(s: &Session) -> Value {
|
|
let running = s.jobs.iter().filter(|j| j.result.lock().map(|r| r.is_none()).unwrap_or(false)).count();
|
|
json!({
|
|
"decode": decode_json(),
|
|
"export": export_json(),
|
|
"media": {"openSources": s.media.open_sources()},
|
|
"jobs": {"total": s.jobs.len(), "running": running},
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use serde_json::json;
|
|
|
|
use crate::Session;
|
|
|
|
#[test]
|
|
fn perf_stats_query_reports_decode_counters() {
|
|
let mut s = Session::default();
|
|
assert!(s.execute("perf.stats", json!({})).is_ok(), "available without a project");
|
|
s.execute("file.openDemoProject", json!({})).unwrap();
|
|
let undo = s.history.undo.len();
|
|
let v = s.execute("perf.stats", json!({})).unwrap();
|
|
for k in [
|
|
"requests",
|
|
"cacheHitRate",
|
|
"seeks",
|
|
"samplesDecoded",
|
|
"samplesSkipped",
|
|
"draftFrames",
|
|
"h264Threads",
|
|
"decodeMs",
|
|
"decodeMsPerSample",
|
|
"framesDecoded",
|
|
"liveDecoders",
|
|
"cacheMB",
|
|
"cacheBudgetMB",
|
|
"planePoolMB",
|
|
"planesReused",
|
|
] {
|
|
assert!(v["decode"][k].is_number(), "decode.{k} in {v}");
|
|
}
|
|
for k in ["frames", "softwareFrames", "sessions", "declined", "fallbacks"] {
|
|
assert!(v["decode"]["hardware"][k].is_number(), "decode.hardware.{k} in {v}");
|
|
}
|
|
assert!(v["decode"]["hardware"]["enabled"].is_boolean());
|
|
assert!(v["decode"]["hardware"]["backend"].is_null() || v["decode"]["hardware"]["backend"].is_string());
|
|
for k in ["frames", "sessions", "declined"] {
|
|
assert!(v["export"]["hardware"][k].is_number(), "export.hardware.{k} in {v}");
|
|
}
|
|
for k in ["setupMs", "loudnessMs", "renderMs", "encodeMs", "convertMs", "audioMs", "muxMs", "finishMs", "waitMs"] {
|
|
assert!(v["export"]["stages"][k].is_number(), "export.stages.{k} in {v}");
|
|
}
|
|
assert!(v["media"]["openSources"].is_number());
|
|
assert_eq!(v["jobs"]["running"], json!(0));
|
|
assert_eq!(s.history.undo.len(), undo, "a query adds no undo step");
|
|
}
|
|
}
|