Skip to content

Commit bdb0b60

Browse files
improve: make scrub supersession thresholds runtime configurable
Co-authored-by: Richie McIlroy <richiemcilroy@users.noreply.github.com>
1 parent 41fa6a7 commit bdb0b60

3 files changed

Lines changed: 149 additions & 7 deletions

File tree

crates/editor/PLAYBACK-BENCHMARKS.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ cargo run -p cap-editor --example playback-benchmark -- --video /path/to/video.m
8888
```bash
8989
# Simulate rapid scrub bursts and track latest-request latency
9090
cargo run -p cap-editor --example scrub-benchmark -- --video /path/to/video.mp4 --fps 60 --bursts 20 --burst-size 12 --sweep-seconds 2.0
91+
92+
# Runtime tuning for FFmpeg scrub supersession heuristic
93+
CAP_FFMPEG_SCRUB_SUPERSEDE_MIN_PIXELS=3686400 \
94+
CAP_FFMPEG_SCRUB_SUPERSEDE_MIN_REQUESTS=8 \
95+
CAP_FFMPEG_SCRUB_SUPERSEDE_MIN_SPAN_FRAMES=45 \
96+
cargo run -p cap-editor --example scrub-benchmark -- --video /path/to/video.mp4
9197
```
9298

9399
#### Playback Startup Latency Report (log analysis)
@@ -526,6 +532,56 @@ cargo run -p cap-recording --example playback-test-runner -- full
526532
- Effective FPS: **60.19**
527533
- Decode: avg **4.99ms**, p95 **7.17ms**, p99 **9.64ms**, max **13.37ms**
528534

535+
### Benchmark Run: 2026-02-14 00:00:00 UTC (Scrub supersession runtime controls)
536+
537+
**Environment:** Linux runner with synthetic 1080p60 and 4k60 MP4 assets
538+
**Commands:** `scrub-benchmark`, `decode-benchmark`, `playback-benchmark`
539+
**Change under test:** FFmpeg scrub supersession thresholds moved to env-configurable runtime controls
540+
541+
#### Scrub Burst Benchmark — 1080p60 (`/tmp/cap-bench-1080p60.mp4`)
542+
- Requests: **240 success / 0 failures**
543+
- All-request latency: avg **211.38ms**, p95 **417.65ms**, p99 **435.23ms**, max **454.51ms**
544+
- Last-request-in-burst latency: avg **303.76ms**, p95 **435.23ms**, p99 **454.51ms**, max **454.51ms**
545+
546+
#### Scrub Burst Benchmark — 4k60 (`/tmp/cap-bench-4k60.mp4`)
547+
- Requests: **240 success / 0 failures**
548+
- All-request latency: avg **812.11ms**, p95 **1767.50ms**, p99 **1822.52ms**, max **1822.52ms**
549+
- Last-request-in-burst latency: avg **820.99ms**, p95 **1767.50ms**, p99 **1822.52ms**, max **1822.52ms**
550+
551+
#### Decode Benchmark — 1080p60 (`/tmp/cap-bench-1080p60.mp4`)
552+
- Decoder init: **6.64ms**
553+
- Sequential decode: **335.5 fps**, avg **2.98ms**
554+
- Seek latency (avg / p95 / max):
555+
- 0.5s: **48.41 / 96.68 / 96.68ms**
556+
- 1.0s: **71.81 / 151.73 / 151.73ms**
557+
- 2.0s: **152.21 / 372.41 / 372.41ms**
558+
- 5.0s: **233.93 / 388.51 / 388.51ms**
559+
- Random access: avg **115.07ms**, p95 **354.67ms**, p99 **399.31ms**
560+
561+
#### Decode Benchmark — 4k60 (`/tmp/cap-bench-4k60.mp4`)
562+
- Decoder init: **32.18ms**
563+
- Sequential decode: **98.7 fps**, avg **10.13ms**
564+
- Seek latency (avg / p95 / max):
565+
- 0.5s: **201.24 / 387.51 / 387.51ms**
566+
- 1.0s: **347.03 / 774.83 / 774.83ms**
567+
- 2.0s: **623.25 / 1499.39 / 1499.39ms**
568+
- 5.0s: **961.84 / 1629.35 / 1629.35ms**
569+
- Random access: avg **524.19ms**, p95 **1485.61ms**, p99 **1619.96ms**
570+
571+
#### Playback Throughput Benchmark — 1080p60 (`/tmp/cap-bench-1080p60.mp4`)
572+
- Target: **60 fps**, budget **16.67ms**
573+
- Decoded: **240/240**, failures **0**
574+
- Missed deadlines: **0**
575+
- Effective FPS: **60.22**
576+
- Decode: avg **1.33ms**, p95 **2.49ms**, p99 **2.80ms**, max **3.90ms**
577+
578+
#### Playback Throughput Benchmark — 4k60 (`/tmp/cap-bench-4k60.mp4`)
579+
- Target: **60 fps**, budget **16.67ms**
580+
- Decoded: **240/240**, failures **0**
581+
- Missed deadlines: **2**
582+
- Effective FPS: **60.17**
583+
- Decode: avg **6.43ms**, p95 **8.82ms**, p99 **14.14ms**, max **17.52ms**
584+
529585
<!-- PLAYBACK_BENCHMARK_RESULTS_END -->
530586

531587
---

crates/editor/PLAYBACK-FINDINGS.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,40 @@ The CPU RGBA→NV12 conversion was taking 15-25ms per frame for 3024x1964 resolu
743743

744744
---
745745

746+
### Session 2026-02-14 (Supersession runtime configurability)
747+
748+
**Goal**: Enable faster cross-platform tuning of scrub supersession without code edits
749+
750+
**What was done**:
751+
1. Added environment-driven controls for FFmpeg scrub supersession behavior:
752+
- `CAP_FFMPEG_SCRUB_SUPERSEDE_DISABLED`
753+
- `CAP_FFMPEG_SCRUB_SUPERSEDE_MIN_PIXELS`
754+
- `CAP_FFMPEG_SCRUB_SUPERSEDE_MIN_REQUESTS`
755+
- `CAP_FFMPEG_SCRUB_SUPERSEDE_MIN_SPAN_FRAMES`
756+
2. Kept default behavior equivalent to current tuned path.
757+
3. Re-ran scrub, decode, and playback benchmarks with defaults to verify no functional regressions.
758+
759+
**Changes Made**:
760+
- `crates/rendering/src/decoder/ffmpeg.rs`
761+
- added `ScrubSupersessionConfig` with `OnceLock` initialization
762+
- replaced hard-coded supersession thresholds with config values
763+
- `crates/editor/PLAYBACK-BENCHMARKS.md`
764+
- added command examples for runtime supersession tuning
765+
- added validation benchmark run for the configurable defaults
766+
767+
**Results**:
768+
- ✅ Scrub supersession behavior preserved with defaults:
769+
- 4k last-request avg **~821ms**, p95 **~1768ms**
770+
- 1080p last-request avg **~304ms**, p95 **~435ms**
771+
- ✅ Playback throughput remains at 60fps-class:
772+
- 1080p: **60.22 fps**
773+
- 4k: **60.17 fps**
774+
- ✅ Decode benchmark metrics remain in expected variance envelope after config refactor.
775+
776+
**Stopping point**: supersession tuning is now runtime-configurable, enabling platform-specific calibration runs (especially macOS/Windows) without recompiling.
777+
778+
---
779+
746780
## References
747781

748782
- `PLAYBACK-BENCHMARKS.md` - Raw performance test data (auto-updated by test runner)

crates/rendering/src/decoder/ffmpeg.rs

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ use ffmpeg::{format, frame};
66
use std::{
77
cell::RefCell,
88
collections::BTreeMap,
9+
env,
910
path::PathBuf,
1011
rc::Rc,
12+
sync::OnceLock,
1113
sync::{Arc, mpsc},
1214
};
1315
use tokio::sync::oneshot;
@@ -71,6 +73,53 @@ struct PendingRequest {
7173
order: u64,
7274
}
7375

76+
#[derive(Clone, Copy)]
77+
struct ScrubSupersessionConfig {
78+
min_requests: usize,
79+
min_span_frames: u32,
80+
min_pixels: u64,
81+
disabled: bool,
82+
}
83+
84+
static SCRUB_SUPERSESSION_CONFIG: OnceLock<ScrubSupersessionConfig> = OnceLock::new();
85+
86+
fn parse_usize_env(key: &str) -> Option<usize> {
87+
env::var(key).ok()?.parse::<usize>().ok()
88+
}
89+
90+
fn parse_u32_env(key: &str) -> Option<u32> {
91+
env::var(key).ok()?.parse::<u32>().ok()
92+
}
93+
94+
fn parse_u64_env(key: &str) -> Option<u64> {
95+
env::var(key).ok()?.parse::<u64>().ok()
96+
}
97+
98+
fn scrub_supersession_config() -> ScrubSupersessionConfig {
99+
*SCRUB_SUPERSESSION_CONFIG.get_or_init(|| {
100+
let min_requests = parse_usize_env("CAP_FFMPEG_SCRUB_SUPERSEDE_MIN_REQUESTS")
101+
.filter(|value| *value > 0)
102+
.unwrap_or(8);
103+
let min_span_frames = parse_u32_env("CAP_FFMPEG_SCRUB_SUPERSEDE_MIN_SPAN_FRAMES")
104+
.filter(|value| *value > 0)
105+
.unwrap_or((FRAME_CACHE_SIZE as u32 / 2).max(1));
106+
let min_pixels = parse_u64_env("CAP_FFMPEG_SCRUB_SUPERSEDE_MIN_PIXELS")
107+
.filter(|value| *value > 0)
108+
.unwrap_or(3_686_400);
109+
let disabled = env::var("CAP_FFMPEG_SCRUB_SUPERSEDE_DISABLED")
110+
.ok()
111+
.map(|value| value == "1" || value.eq_ignore_ascii_case("true"))
112+
.unwrap_or(false);
113+
114+
ScrubSupersessionConfig {
115+
min_requests,
116+
min_span_frames,
117+
min_pixels,
118+
disabled,
119+
}
120+
})
121+
}
122+
74123
fn send_to_replies(
75124
name: &str,
76125
frame_number: u32,
@@ -85,9 +134,9 @@ fn send_to_replies(
85134
}
86135

87136
fn maybe_supersede_scrub_burst(pending_requests: &mut Vec<PendingRequest>, enabled: bool) {
88-
const SCRUB_SUPERSEDE_MIN_REQUESTS: usize = 8;
137+
let config = scrub_supersession_config();
89138

90-
if !enabled || pending_requests.len() < SCRUB_SUPERSEDE_MIN_REQUESTS {
139+
if !enabled || pending_requests.len() < config.min_requests {
91140
return;
92141
}
93142

@@ -102,7 +151,7 @@ fn maybe_supersede_scrub_burst(pending_requests: &mut Vec<PendingRequest>, enabl
102151
.max()
103152
.unwrap_or(0);
104153

105-
if max_frame.saturating_sub(min_frame) <= (FRAME_CACHE_SIZE as u32 / 2) {
154+
if max_frame.saturating_sub(min_frame) <= config.min_span_frames {
106155
return;
107156
}
108157

@@ -354,8 +403,10 @@ impl FfmpegDecoder {
354403
decoder_type: sw_decoder_type,
355404
};
356405
let _ = ready_tx.send(Ok(sw_init_result));
357-
let enable_scrub_supersession =
358-
(video_width as u64) * (video_height as u64) >= 3_686_400;
406+
let supersession_config = scrub_supersession_config();
407+
let enable_scrub_supersession = !supersession_config.disabled
408+
&& (video_width as u64) * (video_height as u64)
409+
>= supersession_config.min_pixels;
359410

360411
while let Ok(r) = rx.recv() {
361412
const MAX_FRAME_TOLERANCE: u32 = 2;
@@ -697,8 +748,9 @@ impl FfmpegDecoder {
697748
decoder_type,
698749
};
699750
let _ = ready_tx.send(Ok(init_result));
700-
let enable_scrub_supersession =
701-
(video_width as u64) * (video_height as u64) >= 3_686_400;
751+
let supersession_config = scrub_supersession_config();
752+
let enable_scrub_supersession = !supersession_config.disabled
753+
&& (video_width as u64) * (video_height as u64) >= supersession_config.min_pixels;
702754

703755
while let Ok(r) = rx.recv() {
704756
const MAX_FRAME_TOLERANCE: u32 = 2;

0 commit comments

Comments
 (0)