Files
orca/src/shared/own-retained-string.ts
T
Neil 1e2ed20b35 perf(terminal): release oversized backing strings behind pending controls (#19396)
* perf(terminal): release oversized backing strings behind pending controls

* test(terminal): record reproducible pending-storage gate evidence

* perf(terminal): own retained control fragments with a fast copy primitive

The pending-control ownership landed with a charCodeAt block copier (10 us
at 4 Ki, 170 us at 64 Ki), so it needed a "copy only when discarded output
dominates the tail" gate to stay affordable. That gate was the whole cost
problem: on adversarial streams it fires every chunk and pays the slow copy
(+45% on 16 Ki ANSI chunks, +81..111% on 194 Ki status chunks), and it also
skipped ownership on fragments too small to be sliced strings anyway.

ownRetainedString replaces it with a Buffer utf16le round trip (0.57 us at
4 Ki, 21.9 us at 64 Ki) and returns anything below V8's SlicedString
kMinLength unchanged. Buffer is absent in the renderer and on mobile, so the
copier is resolved once behind a lone-surrogate round-trip self-check and
falls back to the block copier. With a ~1 us copy the gate is unnecessary:
ownership is now unconditional at all three retention sites and the
adversarial cases land within noise of the un-owned parsers.

The three forced-GC threshold fixtures are replaced by one forced-GC test
for the primitive plus deterministic spy assertions that each site routes
its retained value through ownRetainedString. All fidelity and differential
coverage is kept.

* fix(terminal): escape the NUL in the round-trip probe

A raw NUL byte in the source made git treat the file as binary, so its
diffs and blame were unreadable. Escapes are equivalent at runtime.
2026-09-09 03:40:03 -07:00

51 lines
1.7 KiB
TypeScript

import { copyUtf16SuffixToOwnedString } from './owned-utf16-suffix'
// V8 SlicedString::kMinLength. Shorter slices are already flat, so copying them is pure waste.
const MIN_SLICED_STRING_LENGTH = 13
/** Lone surrogates and an unpaired trail must survive the round trip byte-for-byte. */
const ROUND_TRIP_PROBE = '\ud800a\udfff\u0000\u6f22'
let copyString: ((value: string) => string) | null = null
function detectBufferCopier(): ((value: string) => string) | null {
const buffer = (globalThis as { Buffer?: typeof Buffer }).Buffer
if (typeof buffer?.from !== 'function') {
return null
}
try {
if (buffer.from(ROUND_TRIP_PROBE, 'utf16le').toString('utf16le') !== ROUND_TRIP_PROBE) {
return null
}
} catch {
return null
}
return (value) => buffer.from(value, 'utf16le').toString('utf16le')
}
function resolveCopyString(): (value: string) => string {
if (!copyString) {
// Buffer is absent in the renderer and on mobile; fall back to the code-unit block copier.
copyString =
detectBufferCopier() ?? ((value: string) => copyUtf16SuffixToOwnedString(value, value.length))
}
return copyString
}
/**
* Return a standalone copy of a string that outlives the chunk it came from.
* Why: `bigChunk.slice(a, b)` is a V8 SlicedString that pins the whole parent,
* so retaining a few KiB of tail can pin megabytes of already-consumed output.
*/
export function ownRetainedString(value: string): string {
if (value.length < MIN_SLICED_STRING_LENGTH) {
return value
}
return resolveCopyString()(value)
}
/** Test-only: drop the memoized copier so a fallback path can be exercised. */
export function resetOwnRetainedStringCopier(): void {
copyString = null
}