mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 00:03:47 +00:00
test(terminal): make the queue retention tests fail without their fix
Round 3 review found the residual-retention test had gone vacuous. Once the
enqueue boundary started copying each chunk, a baseline taken after enqueue
already contained the parents the test was meant to prove get released, so
disabling residual flattening left it green. Baseline before enqueue instead;
it now fails at 8,720,768 bytes with residual flattening off.
Consumed-slot clearing had no coverage at all - the existing case queues one
chunk per terminal, and the slots only accumulate below the 64-chunk
compaction threshold. Add a case that queues 40 medium chunks per terminal:
363,880 bytes retained with the clearing, 2,928,568 without.
(cherry picked from commit 2707db2b0e)
This commit is contained in:
@@ -1737,6 +1737,66 @@ describe('pane terminal output scheduler', () => {
|
||||
return process.memoryUsage().heapUsed
|
||||
}
|
||||
|
||||
it('releases chunks it has already drained past', async () => {
|
||||
vi.useFakeTimers()
|
||||
const { writeTerminalOutput } = await loadScheduler()
|
||||
|
||||
// Why many medium chunks: compactConsumedChunks only splices once chunkIndex reaches 64,
|
||||
// so below that the drained slots stay in the array and must be cleared individually.
|
||||
const CHUNKS_PER_TERMINAL = 40
|
||||
const CHUNK = 48 * 1024
|
||||
const SINKS = 4
|
||||
|
||||
let writtenChars = 0
|
||||
const terminals = Array.from({ length: SINKS }, () => ({
|
||||
write: (data: string, callback?: () => void) => {
|
||||
writtenChars += data.length
|
||||
callback?.()
|
||||
}
|
||||
}))
|
||||
|
||||
const baseline = collect()
|
||||
|
||||
for (const [index, terminal] of terminals.entries()) {
|
||||
for (let chunk = 0; chunk < CHUNKS_PER_TERMINAL; chunk += 1) {
|
||||
writeTerminalOutput(
|
||||
terminal,
|
||||
String.fromCharCode(65 + index) +
|
||||
String.fromCharCode(48 + (chunk % 10)) +
|
||||
'q'.repeat(CHUNK - 2),
|
||||
{ foreground: false, latencySensitive: false }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const debug = (
|
||||
globalThis as {
|
||||
__terminalOutputSchedulerDebug?: { snapshot: () => { queuedChars: number } }
|
||||
}
|
||||
).__terminalOutputSchedulerDebug
|
||||
if (!debug) {
|
||||
throw new Error('scheduler debug API unavailable')
|
||||
}
|
||||
|
||||
const TAIL_CHARS = 64 * 1024
|
||||
let ticks = 0
|
||||
while (debug.snapshot().queuedChars > TAIL_CHARS && ticks < 40000) {
|
||||
vi.advanceTimersByTime(4)
|
||||
ticks += 1
|
||||
}
|
||||
|
||||
const queuedChars = debug.snapshot().queuedChars
|
||||
const retainedBytes = collect() - baseline
|
||||
|
||||
// Sanity: the queues really drained down, and none hit the backlog cap.
|
||||
expect(writtenChars).toBeGreaterThan(SINKS * CHUNKS_PER_TERMINAL * CHUNK * 0.9)
|
||||
expect(queuedChars).toBeGreaterThan(0)
|
||||
expect(queuedChars).toBeLessThanOrEqual(TAIL_CHARS)
|
||||
|
||||
// The defect: ~39 drained-past slots per terminal kept their strings alive uncharged.
|
||||
expect(retainedBytes).toBeLessThan(2 * 1024 * 1024)
|
||||
})
|
||||
|
||||
it('does not pin the parent when a producer enqueues a slice', async () => {
|
||||
vi.useFakeTimers()
|
||||
const { writeTerminalOutput } = await loadScheduler()
|
||||
@@ -1778,17 +1838,21 @@ describe('pane terminal output scheduler', () => {
|
||||
callback?.()
|
||||
}
|
||||
})
|
||||
const terminals = Array.from({ length: TERMINALS }, makeSink)
|
||||
|
||||
// Distinct leading chars keep V8 from sharing any backing store between terminals.
|
||||
const terminals = Array.from({ length: TERMINALS }, (_unused, index) => {
|
||||
const terminal = makeSink()
|
||||
// Why baseline BEFORE enqueueing: the queue owns a copy of every chunk, so a baseline
|
||||
// taken after enqueue already contains the parents this test must prove get released,
|
||||
// and the assertion would hold even with residual flattening disabled.
|
||||
const baseline = collect()
|
||||
|
||||
for (const [index, terminal] of terminals.entries()) {
|
||||
// Built and dropped inline so only the queue's own copy stays reachable.
|
||||
writeTerminalOutput(
|
||||
terminal,
|
||||
String.fromCharCode(65 + index) + 'q'.repeat(CHUNK_CHARS - 1),
|
||||
{ foreground: false, latencySensitive: false }
|
||||
)
|
||||
return terminal
|
||||
})
|
||||
}
|
||||
|
||||
const debug = (
|
||||
globalThis as {
|
||||
@@ -1799,8 +1863,6 @@ describe('pane terminal output scheduler', () => {
|
||||
throw new Error('scheduler debug API unavailable')
|
||||
}
|
||||
|
||||
const baseline = collect()
|
||||
|
||||
// Why stop early: the leak is what a PARTIALLY drained queue pins. Draining to empty
|
||||
// frees the chunks either way, so a full drain cannot observe the defect.
|
||||
const TAIL_CHARS = 64 * 1024
|
||||
@@ -1819,9 +1881,8 @@ describe('pane terminal output scheduler', () => {
|
||||
expect(queuedChars).toBeGreaterThan(0)
|
||||
expect(queuedChars).toBeLessThanOrEqual(TAIL_CHARS)
|
||||
|
||||
// The defect: those few queued KB pinned 8 x 2 MB of parent chunks (~16 MB).
|
||||
// The defect: those few queued KB pinned 8 x 2 MB of chunks (~16 MB).
|
||||
expect(retainedBytes).toBeLessThan(4 * 1024 * 1024)
|
||||
expect(terminals).toHaveLength(TERMINALS)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user