mirror of
https://github.com/stablyai/orca.git
synced 2026-10-26 00:03:55 +00:00
perf(terminal): detach retained PTY slice strings from their source chunks
`String.prototype.slice` on a long string yields a V8 SlicedString that keeps
a pointer to its parent. Extraction boundaries that park a short result in
long-lived state — an OSC title in pane/tab state, a scan tail in a per-PTY
map, a carry in a per-pane scanner closure — therefore pinned the entire
source PTY chunk for as long as that short string stayed reachable.
Add `detachString` (src/shared/detached-string.ts), which forces V8 to flatten
via `` ` ${value}`.slice(1) ``, and apply it at 22 extraction boundaries across
shared, main, and renderer.
This is memory hygiene, not a crash fix. Production splits renderer-bound PTY
writes at PTY_BATCH_FLUSH_CHUNK_CHARS (16 KiB) and `setRuntimePaneTitle` writes
`[paneId]: title`, an overwrite that releases the previous parent, so live
retention is O(panes) and measures ~0.01 MiB today. It is bounded by that
overwrite, unbounded in principle, and cheap to make robust.
Retention guards resolve a real `gc` binding via `v8.setFlagsFromString`
instead of requiring `--expose-gc`, so they actually run under the plain
`vitest run` CI uses rather than silently skipping. Neutering `detachString`
to `return value` fails all 23 guards.
This commit is contained in:
@@ -3,6 +3,7 @@ import {
|
||||
createCommandCodeOutputStatusDetector,
|
||||
stripTerminalControl
|
||||
} from './command-code-output-status'
|
||||
import { createForceGc, resolveForcedGc } from './forced-gc-for-retention-tests'
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
@@ -462,4 +463,28 @@ describe('terminal control stripping', () => {
|
||||
expectLegacyEquivalent(data)
|
||||
}
|
||||
})
|
||||
|
||||
// Every pane runs this detector (the ring is written before the Command Code
|
||||
// early-out), so an attached ring pins one whole PTY chunk per open pane.
|
||||
const forcedGc = resolveForcedGc()
|
||||
const itWithGc = forcedGc ? it : it.skip
|
||||
itWithGc('does not pin the source chunk behind the recent raw-text ring', () => {
|
||||
const forceGc = createForceGc(forcedGc!)
|
||||
const panes = 512
|
||||
const chunkChars = 16 * 1024
|
||||
|
||||
forceGc()
|
||||
const before = process.memoryUsage().heapUsed
|
||||
const detectors = Array.from({ length: panes }, (_unused, index) => {
|
||||
const detector = createCommandCodeOutputStatusDetector({ onWorking: () => undefined })
|
||||
detector.observe(`${'x'.repeat(chunkChars)}pane-${index}`)
|
||||
return detector
|
||||
})
|
||||
forceGc()
|
||||
const retainedMiB = (process.memoryUsage().heapUsed - before) / (1024 * 1024)
|
||||
|
||||
expect(detectors).toHaveLength(panes)
|
||||
// ~8 MiB of source chunks stay alive if the rings are still attached.
|
||||
expect(retainedMiB).toBeLessThan(2)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user