Reconcile eviction comment wording; add handleSourceEnded cascade test (Phase 21.1)
The inclusive <= bound is correct; comments now say 'at or behind'. New test drives eviction through the real onended trigger with a live mid-array source pinning the frontier.
This commit is contained in:
@@ -253,6 +253,78 @@ test('eviction does not drop buffers under live sources or past the schedule cur
|
|||||||
assertEqual(dropped, 0, 'no eviction while front sources are live');
|
assertEqual(dropped, 0, 'no eviction while front sources are live');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// handleSourceEnded cascade: eviction fires from the real production trigger (onended), not
|
||||||
|
// via a direct evictPlayedBuffers() call. Confirms the anchor/index invariants hold end-to-end
|
||||||
|
// through the scheduler's own event handling while playback is still active with a live source.
|
||||||
|
//
|
||||||
|
// Setup: 0.3s buffers so the 500ms lookahead window fits exactly two sources after
|
||||||
|
// playFromPosition(0). Buffer 0 ends at ~0.31s, buffer 1 ends at ~0.61s — both are scheduled.
|
||||||
|
// Clock is then advanced to t=0.6 so buffer 0's end (0.31) < evictBefore (0.6) while the live
|
||||||
|
// source on buffer 1 pins firstLiveIndex=1, blocking further eviction. This is the mid-array
|
||||||
|
// pinning scenario that later waves (21.2/21.3) build on.
|
||||||
|
test('eviction via handleSourceEnded: position exact, live bufferIndex decremented, frontier respected', () => {
|
||||||
|
const cm = new FakeContextManager();
|
||||||
|
const s = makeScheduler(cm);
|
||||||
|
// Retain nothing behind the playhead — evict aggressively so the cascade fires.
|
||||||
|
s.setBackRetainSeconds(0);
|
||||||
|
|
||||||
|
// Eight 0.3s buffers. scheduleBuffersFrom with lookaheadTarget=0.5s at t=0:
|
||||||
|
// after buf 0: nextScheduleTime≈0.31, lookahead=0.31 < 0.5 → continues
|
||||||
|
// after buf 1: nextScheduleTime≈0.61, lookahead=0.61 > 0.5 → breaks
|
||||||
|
// → exactly two sources are live after playFromPosition.
|
||||||
|
for (let i = 0; i < 8; i++) s.addBuffer(buf(0.3));
|
||||||
|
|
||||||
|
cm.now = 0;
|
||||||
|
s.playFromPosition(0);
|
||||||
|
|
||||||
|
// Reach inside to see which sources were scheduled and what bufferIndex they hold.
|
||||||
|
const priv = s as unknown as {
|
||||||
|
scheduledSources: Array<{ source: FakeSource; bufferIndex: number; startTime: number; endTime: number }>;
|
||||||
|
nextBufferIndex: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Confirm two sources are live — the setup guarantee.
|
||||||
|
if (priv.scheduledSources.length < 2) {
|
||||||
|
throw new Error(`Expected ≥2 scheduled sources after playFromPosition, got ${priv.scheduledSources.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identify the first and second scheduled sources by bufferIndex order.
|
||||||
|
const sorted = [...priv.scheduledSources].sort((a, b) => a.bufferIndex - b.bufferIndex);
|
||||||
|
const firstScheduled = sorted[0]; // bufferIndex 0
|
||||||
|
const secondScheduled = sorted[1]; // bufferIndex 1
|
||||||
|
const secondBufferIndexBefore = secondScheduled.bufferIndex; // must be 1
|
||||||
|
|
||||||
|
// Record the second FakeSource so we can assert it was not stopped by eviction.
|
||||||
|
const secondFakeSource = secondScheduled.source as unknown as FakeSource;
|
||||||
|
|
||||||
|
// Advance clock to 0.6s. Buffer 0 ends at ~0.31s → evictBefore=0.6, end=0.31 ≤ 0.6 →
|
||||||
|
// droppable. Buffer 1 ends at ~0.61s → its live source pins firstLiveIndex=1 → NOT dropped.
|
||||||
|
cm.now = 0.6;
|
||||||
|
|
||||||
|
// Confirm playback is still active before firing the cascade.
|
||||||
|
assertEqual(s.isActive(), true, 'isActive must be true before cascade');
|
||||||
|
|
||||||
|
// Fire the cascade via the production trigger: stop the first source, which calls onended,
|
||||||
|
// which calls handleSourceEnded, which calls evictPlayedBuffers internally.
|
||||||
|
(firstScheduled.source as unknown as FakeSource).stop();
|
||||||
|
|
||||||
|
// (a) Absolute position must remain exactly 0.6.
|
||||||
|
assertClose(s.getCurrentPosition(), 0.6, 'position after handleSourceEnded cascade');
|
||||||
|
|
||||||
|
// (b) The second live source's bufferIndex must have been decremented by 1 (the one evicted
|
||||||
|
// front buffer), shifting it from absolute index 1 to absolute index 0.
|
||||||
|
const expectedSecondIndex = secondBufferIndexBefore - 1;
|
||||||
|
assertEqual(secondScheduled.bufferIndex, expectedSecondIndex, 'live source bufferIndex decremented');
|
||||||
|
|
||||||
|
// (c) Eviction stopped at firstLiveIndex=1, not nextBufferIndex — the second buffer was
|
||||||
|
// NOT dropped. Verify the second source was not stopped (it remained live throughout).
|
||||||
|
assertEqual(secondFakeSource.stopped, false, 'live second source not stopped by eviction');
|
||||||
|
// And the scheduler still has buffers (the array was not wiped past the frontier).
|
||||||
|
if (s.getBufferCount() === 0) {
|
||||||
|
throw new Error('eviction wiped all buffers — should have stopped at firstLiveIndex');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// --- run -------------------------------------------------------------------------------------
|
// --- run -------------------------------------------------------------------------------------
|
||||||
if (failures.length > 0) {
|
if (failures.length > 0) {
|
||||||
console.error(failures.join('\n'));
|
console.error(failures.join('\n'));
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ export class PlaybackScheduler {
|
|||||||
* Drop already-played buffers from the front of the array, reclaiming their decoded float
|
* Drop already-played buffers from the front of the array, reclaiming their decoded float
|
||||||
* memory, and advance the time anchor so all position/index bookkeeping stays exact.
|
* memory, and advance the time anchor so all position/index bookkeeping stays exact.
|
||||||
*
|
*
|
||||||
* Eviction frontier: any buffer whose absolute END time is older than
|
* Eviction frontier: any buffer whose absolute END time is at or older than
|
||||||
* (currentPosition - backRetainSeconds) is droppable. We evict a contiguous run from the
|
* (currentPosition - backRetainSeconds) is droppable. We evict a contiguous run from the
|
||||||
* front only — buffers are appended in playback order, so the front is always the oldest.
|
* front only — buffers are appended in playback order, so the front is always the oldest.
|
||||||
*
|
*
|
||||||
@@ -179,7 +179,7 @@ export class PlaybackScheduler {
|
|||||||
let accumulatedEnd = this.playbackOffset;
|
let accumulatedEnd = this.playbackOffset;
|
||||||
for (let i = 0; i < maxEvictable; i++) {
|
for (let i = 0; i < maxEvictable; i++) {
|
||||||
accumulatedEnd += this.buffers[i].duration;
|
accumulatedEnd += this.buffers[i].duration;
|
||||||
// Drop only buffers whose END is strictly behind the retain frontier.
|
// Drop buffers whose END is at or behind the retain frontier (inclusive bound).
|
||||||
if (accumulatedEnd <= evictBefore) {
|
if (accumulatedEnd <= evictBefore) {
|
||||||
evictCount = i + 1;
|
evictCount = i + 1;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user