Files
orca/src/shared/ndjson-retained-chunk-memory.test.ts
T
ad5ef0d944 [mem] fix(memory): release consumed buffers, abandoned waits, and deleted diff views (#30650)
* fix(memory): release consumed stream buffers and abandoned request state

* fix(memory): preserve queued cancellation for shared promise callers

* fix(memory): keep workspace cache cleanup free of browser side effects

* fix(memory): follow renamed workspace owners in cached diff views

* fix(memory): a viewer unmounting after its workspace is removed cannot re-create cached diff entries

Combined-diff view state, scroll offset and scroll anchor now live in one owner-tagged
entry per view key. A mounted viewer claims its key; writes only update claimed entries,
so the late unmount write that follows a workspace removal is a no-op instead of
re-adding orphan scroll entries.

* fix(memory): tighten retention fixes and let evicted diff views heal

- Combined diff: writes go through a per-mount lease. Forgetting a workspace revokes its
  leases, so a late unmount write cannot resurrect entries, while an LRU-evicted entry of
  a still-mounted viewer is re-created on its next write. Drops the duplicated view scrollTop.
- NDJSON framer: copy a retained tail only when it is a strict slice of its chunk, copy an
  unfinished line once, and copy every rejected-line prefix.
- Binary frame reader: single exit path for the end-of-feed leftover copy.
- Relay readiness reuses waitForPromiseWithSignal instead of re-implementing it.
- PTY preconnect input: pass the listener-removing settle into createInput instead of
  mutating the input afterwards.

* docs(memory): state the deferSettled ordering guarantee precisely

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
2026-10-19 16:23:32 -07:00

81 lines
2.6 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { createIncrementalNdjsonFramer } from './main-process-ndjson-framer'
const CHUNK_CHARS = 1024 * 1024
const READERS = 32
const TAIL = 'tail'.repeat(1024)
function collectHeap(): number {
if (!globalThis.gc) {
throw new Error('Run this regression with --expose-gc')
}
// V8's last regexp input is a separate owner of the record under test.
void /reset/.test('reset')
globalThis.gc()
globalThis.gc()
return process.memoryUsage().heapUsed
}
describe('NDJSON retained chunk memory', () => {
it.each(['tail', 'paused', 'rejected'] as const)(
'releases consumed megabyte records while retaining %s content exactly',
(mode) => {
const held: { finish: () => void; result: () => string }[] = []
const baseline = collectHeap()
for (let index = 0; index < READERS; index += 1) {
let paused = false
let result = ''
const framer = createIncrementalNdjsonFramer(
(record) => {
if (record && typeof record === 'object' && 'tail' in record) {
result = typeof record.tail === 'string' ? record.tail : ''
}
if (mode === 'paused') {
paused = true
}
},
(rejected) => {
if (rejected.kind === 'line-too-long') {
result = rejected.prefix
}
},
{
maxLineBytes: mode === 'rejected' ? 1024 : CHUNK_CHARS * 2,
shouldPause: () => paused
}
)
const payload = String.fromCharCode(65 + (index % 26)).repeat(CHUNK_CHARS)
const chunk = `{"data":"${payload}"}\n${mode === 'paused' ? '{}\n' : ''}${mode === 'rejected' ? '' : `{"tail":"${TAIL}`}`
framer.feed(chunk)
held.push({
finish: () => {
if (mode === 'paused') {
paused = false
framer.resume()
// Delivering the queued record pauses again before the unfinished tail.
paused = false
framer.resume()
}
if (mode !== 'rejected') {
framer.feed('"}\n')
}
},
result: () => result
})
}
const retainedBytes = collectHeap() - baseline
expect(held).toHaveLength(READERS)
expect(retainedBytes).toBeLessThan(8 * 1024 * 1024)
for (const reader of held) {
reader.finish()
if (mode === 'rejected') {
expect(reader.result()).toHaveLength(64 * 1024)
expect(reader.result().startsWith('{"data":"')).toBe(true)
} else {
expect(reader.result()).toBe(TAIL)
}
}
}
)
})