Skip to content

Commit 2fd740e

Browse files
committed
Merge branch 'cursor/playback-performance-and-sync-dec3' of https://github.com/CapSoftware/Cap into cursor/playback-performance-and-sync-dec3
2 parents c62eec6 + f0e7760 commit 2fd740e

2 files changed

Lines changed: 59 additions & 79 deletions

File tree

‎crates/editor/PLAYBACK-FINDINGS.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,14 @@ cargo run -p cap-recording --example playback-test-runner -- full
446446
- Prefetch and direct-decode paths now use cached clip-index-to-offset maps instead of repeated linear clip scans.
447447
- Reduces per-frame scheduling overhead in playback and prefetch loops when projects contain many clips.
448448

449+
62. **Deduplicated frame-request watch updates (2026-02-13)**
450+
- Playback loop now uses change-aware frame-request signaling instead of unconditional watch broadcasts.
451+
- Reduces redundant prefetch wakeups and channel churn when requested frame value does not change.
452+
453+
63. **Removed duplicate keyed-buffer lookups during prefetch insert (2026-02-13)**
454+
- Prefetch buffer insertion now uses a single `BTreeMap::entry` match to detect insertion and store new frames.
455+
- Eliminates the prior contains-check plus entry-insert double lookup in prefetch hot path.
456+
449457
---
450458

451459
## Root Cause Analysis Archive
@@ -603,6 +611,8 @@ Decoder Pipeline:
603611
65. Scaled prefetch idle polling with frame budget to reduce scheduler wakeup churn during empty in-flight periods.
604612
66. Bounded behind-prefetch dedupe tracking window to avoid unbounded growth and preserve lookup efficiency over long sessions.
605613
67. Cached clip-offset lookups for prefetch and direct-decode scheduling to replace repeated linear clip scans on decode hot paths.
614+
68. Deduplicated frame-request watch updates so unchanged frame requests no longer trigger redundant watch notifications.
615+
69. Replaced contains+entry prefetch insertion with single-entry map insertion to remove duplicate keyed-buffer lookups in frame-insert hot path.
606616

607617
**Changes Made**:
608618
- `crates/editor/src/playback.rs`: default low-latency audio mode, playback seek channel, seek-aware scheduling.
@@ -639,6 +649,8 @@ Decoder Pipeline:
639649
- `crates/editor/src/playback.rs`: prefetch scheduler idle polling now scales with frame budget (bounded) instead of fixed 1ms delay, reducing idle wakeup overhead.
640650
- `crates/editor/src/playback.rs`: behind-prefetch dedupe tracking now uses a bounded eviction-ordered window to prevent unbounded set growth during long playback.
641651
- `crates/editor/src/playback.rs`: prefetch and playback direct-decode paths now use cached clip-offset maps rebuilt on project updates, avoiding repeated clip list linear searches.
652+
- `crates/editor/src/playback.rs`: frame-request updates now use `watch::Sender::send_if_modified` across playback/warmup/skip paths to avoid redundant unchanged-frame notifications.
653+
- `crates/editor/src/playback.rs`: prefetch insertion now uses single `BTreeMap::entry` insertion path instead of separate contains-check + insert lookup.
642654
- `crates/editor/src/playback.rs`: split prefetch/direct decode in-flight tracking and combined both sets in wait-path in-flight checks.
643655
- `scripts/compare-playback-benchmark-runs.js`: comparison now reports baseline rows missing from candidate and fails by default on coverage gaps.
644656
- `scripts/finalize-playback-matrix.js`: compare stage now runs before publish stage in combined workflows and forwards allow-missing-candidate flag.

‎crates/editor/src/playback.rs‎

Lines changed: 47 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,13 @@ fn insert_prefetched_frame(
139139
}
140140

141141
let frame_number = prefetched.frame_number;
142-
let inserted_new = !buffer.contains_key(&frame_number);
143-
buffer.entry(frame_number).or_insert(prefetched);
142+
let inserted_new = match buffer.entry(frame_number) {
143+
std::collections::btree_map::Entry::Vacant(entry) => {
144+
entry.insert(prefetched);
145+
true
146+
}
147+
std::collections::btree_map::Entry::Occupied(_) => false,
148+
};
144149
let trimmed = trim_prefetch_buffer(buffer, current_frame);
145150
inserted_new || trimmed
146151
}
@@ -255,11 +260,8 @@ impl Playback {
255260
let mut in_flight: FuturesUnordered<PrefetchFuture> = FuturesUnordered::new();
256261
let mut frames_decoded: u32 = 0;
257262
let mut prefetched_behind: HashSet<u32> = HashSet::new();
258-
<<<<<<< HEAD
259-
=======
260263
let mut prefetched_behind_order: VecDeque<u32> = VecDeque::new();
261264
let mut scheduled_in_flight_frames: HashSet<u32> = HashSet::new();
262-
>>>>>>> c801652efd85d0cf6bde578ebe963bc9d7221587
263265
const RAMP_UP_AFTER_FRAMES: u32 = 5;
264266
let dynamic_prefetch_ahead = fps.clamp(30, 90).min(PREFETCH_BUFFER_SIZE as u32);
265267
let dynamic_prefetch_behind = (fps / 4).clamp(8, 24);
@@ -271,28 +273,21 @@ impl Playback {
271273
PARALLEL_DECODE_TASKS
272274
};
273275
let initial_parallel_tasks = dynamic_parallel_tasks.min(4);
274-
<<<<<<< HEAD
275-
=======
276276
let prefetch_idle_poll_interval = Duration::from_secs_f64(1.0 / fps_f64)
277277
.mul_f64(0.25)
278278
.max(Duration::from_millis(2))
279279
.min(Duration::from_millis(8));
280280
let prefetched_behind_capacity = (dynamic_prefetch_behind as usize).saturating_mul(8);
281-
>>>>>>> c801652efd85d0cf6bde578ebe963bc9d7221587
282281
let mut active_generation = *prefetch_seek_generation.borrow();
283282

284283
let mut cached_project = prefetch_project.borrow().clone();
285284
let mut prefetch_clip_offsets = build_clip_offsets_lookup(&cached_project);
286285
info!(
287286
dynamic_prefetch_ahead,
288-
<<<<<<< HEAD
289-
dynamic_prefetch_behind, dynamic_parallel_tasks, "Prefetch window configuration"
290-
=======
291287
dynamic_prefetch_behind,
292288
dynamic_parallel_tasks,
293289
prefetch_idle_poll_interval_ms = prefetch_idle_poll_interval.as_secs_f64() * 1000.0,
294290
"Prefetch window configuration"
295-
>>>>>>> c801652efd85d0cf6bde578ebe963bc9d7221587
296291
);
297292

298293
loop {
@@ -312,18 +307,12 @@ impl Playback {
312307
next_prefetch_frame = *frame_request_rx.borrow();
313308
frames_decoded = 0;
314309
prefetched_behind.clear();
315-
<<<<<<< HEAD
316-
=======
317310
prefetched_behind_order.clear();
318-
>>>>>>> c801652efd85d0cf6bde578ebe963bc9d7221587
319311

320312
if let Ok(mut in_flight_guard) = prefetch_in_flight.write() {
321313
in_flight_guard.clear();
322314
}
323-
<<<<<<< HEAD
324-
=======
325315
scheduled_in_flight_frames.clear();
326-
>>>>>>> c801652efd85d0cf6bde578ebe963bc9d7221587
327316

328317
in_flight = FuturesUnordered::new();
329318
}
@@ -349,10 +338,7 @@ impl Playback {
349338
if let Ok(mut in_flight_guard) = prefetch_in_flight.write() {
350339
in_flight_guard.clear();
351340
}
352-
<<<<<<< HEAD
353-
=======
354341
scheduled_in_flight_frames.clear();
355-
>>>>>>> c801652efd85d0cf6bde578ebe963bc9d7221587
356342
in_flight = FuturesUnordered::new();
357343
}
358344
}
@@ -380,15 +366,7 @@ impl Playback {
380366
break;
381367
}
382368

383-
<<<<<<< HEAD
384-
let already_in_flight = prefetch_in_flight
385-
.read()
386-
.map(|guard| guard.contains(&(active_generation, frame_num)))
387-
.unwrap_or(false);
388-
if already_in_flight {
389-
=======
390369
if scheduled_in_flight_frames.contains(&frame_num) {
391-
>>>>>>> c801652efd85d0cf6bde578ebe963bc9d7221587
392370
next_prefetch_frame += 1;
393371
continue;
394372
}
@@ -450,15 +428,7 @@ impl Playback {
450428
continue;
451429
}
452430

453-
<<<<<<< HEAD
454-
let already_in_flight = prefetch_in_flight
455-
.read()
456-
.map(|guard| guard.contains(&(active_generation, behind_frame)))
457-
.unwrap_or(false);
458-
if already_in_flight {
459-
=======
460431
if scheduled_in_flight_frames.contains(&behind_frame) {
461-
>>>>>>> c801652efd85d0cf6bde578ebe963bc9d7221587
462432
continue;
463433
}
464434

@@ -504,10 +474,7 @@ impl Playback {
504474
biased;
505475

506476
Some((frame_num, segment_index, generation, result)) = in_flight.next() => {
507-
<<<<<<< HEAD
508-
=======
509477
scheduled_in_flight_frames.remove(&frame_num);
510-
>>>>>>> c801652efd85d0cf6bde578ebe963bc9d7221587
511478
if let Ok(mut in_flight_guard) = prefetch_in_flight.write() {
512479
in_flight_guard.remove(&(generation, frame_num));
513480
}
@@ -640,18 +607,6 @@ impl Playback {
640607

641608
tokio::select! {
642609
Some(prefetched) = prefetch_rx.recv() => {
643-
<<<<<<< HEAD
644-
if prefetched.generation == seek_generation {
645-
if insert_prefetched_frame(
646-
&mut prefetch_buffer,
647-
prefetched,
648-
frame_number,
649-
) {
650-
warmup_buffer_changed = true;
651-
}
652-
if first_frame_time.is_none() && !prefetch_buffer.is_empty() {
653-
first_frame_time = Some(Instant::now());
654-
=======
655610
let mut next_prefetched = Some(prefetched);
656611

657612
loop {
@@ -667,7 +622,6 @@ impl Playback {
667622
)
668623
{
669624
warmup_buffer_changed = true;
670-
>>>>>>> c801652efd85d0cf6bde578ebe963bc9d7221587
671625
}
672626

673627
next_prefetched = prefetch_rx.try_recv().ok();
@@ -688,28 +642,14 @@ impl Playback {
688642
first_frame_time = None;
689643
warmup_start = Instant::now();
690644
let _ = seek_generation_tx.send(seek_generation);
691-
let _ = frame_request_tx.send(frame_number);
692-
let _ = playback_position_tx.send(frame_number);
693-
if has_audio
694-
&& audio_playhead_tx
695-
.send(frame_number as f64 / fps_f64)
696-
.is_err()
697-
{
698-
break;
699-
}
700-
}
701-
_ = seek_rx.changed() => {
702-
let seek_frame = *seek_rx.borrow_and_update();
703-
seek_generation = seek_generation.saturating_add(1);
704-
frame_number = seek_frame;
705-
prefetch_buffer.clear();
706-
frame_cache.clear();
707-
warmup_contiguous_prefetched = 0;
708-
warmup_buffer_changed = false;
709-
first_frame_time = None;
710-
warmup_start = Instant::now();
711-
let _ = seek_generation_tx.send(seek_generation);
712-
let _ = frame_request_tx.send(frame_number);
645+
let _ = frame_request_tx.send_if_modified(|requested| {
646+
if *requested == frame_number {
647+
false
648+
} else {
649+
*requested = frame_number;
650+
true
651+
}
652+
});
713653
let _ = playback_position_tx.send(frame_number);
714654
if has_audio
715655
&& audio_playhead_tx
@@ -745,7 +685,14 @@ impl Playback {
745685
prefetch_buffer.clear();
746686
frame_cache.clear();
747687
let _ = seek_generation_tx.send(seek_generation);
748-
let _ = frame_request_tx.send(frame_number);
688+
let _ = frame_request_tx.send_if_modified(|requested| {
689+
if *requested == frame_number {
690+
false
691+
} else {
692+
*requested = frame_number;
693+
true
694+
}
695+
});
749696
let _ = playback_position_tx.send(frame_number);
750697
if has_audio
751698
&& audio_playhead_tx
@@ -783,7 +730,14 @@ impl Playback {
783730
prefetch_buffer.clear();
784731
frame_cache.clear();
785732
let _ = seek_generation_tx.send(seek_generation);
786-
let _ = frame_request_tx.send(frame_number);
733+
let _ = frame_request_tx.send_if_modified(|requested| {
734+
if *requested == frame_number {
735+
false
736+
} else {
737+
*requested = frame_number;
738+
true
739+
}
740+
});
787741
let _ = playback_position_tx.send(frame_number);
788742
if has_audio
789743
&& audio_playhead_tx
@@ -885,7 +839,14 @@ impl Playback {
885839
}
886840
}
887841
} else if prefetch_buffer.is_empty() && total_frames_rendered < 15 {
888-
let _ = frame_request_tx.send(frame_number);
842+
let _ = frame_request_tx.send_if_modified(|requested| {
843+
if *requested == frame_number {
844+
false
845+
} else {
846+
*requested = frame_number;
847+
true
848+
}
849+
});
889850

890851
let wait_result =
891852
tokio::time::timeout(frame_fetch_timeout, prefetch_rx.recv()).await;
@@ -1078,7 +1039,14 @@ impl Playback {
10781039
skip_events = skip_events.saturating_add(1);
10791040

10801041
prune_prefetch_buffer_before_frame(&mut prefetch_buffer, frame_number);
1081-
let _ = frame_request_tx.send(frame_number);
1042+
let _ = frame_request_tx.send_if_modified(|requested| {
1043+
if *requested == frame_number {
1044+
false
1045+
} else {
1046+
*requested = frame_number;
1047+
true
1048+
}
1049+
});
10821050
let _ = playback_position_tx.send(frame_number);
10831051
if has_audio
10841052
&& audio_playhead_tx

0 commit comments

Comments
 (0)