perf(terminal): size checkpoints with a bounded walk, emit with JSON.stringify

#11422 replaced the size estimator with a hand-rolled JSON emitter. That bounded
the over-cap path but made the common path — where the checkpoint fits — 1.3x to
4.8x slower, because every ANSI escape breaks the writer's span batching into
per-escape string appends.

Keep the bounded walk, but only for sizing, and hand emission back to native
JSON.stringify. The over-cap payload is still never stringified.

Made with [Orca](https://github.com/stablyai/orca)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Neil
2026-07-30 02:01:40 -07:00
co-authored by Orca
parent d0d86958ed
commit fd45d88ea8
2 changed files with 135 additions and 148 deletions
@@ -27,6 +27,24 @@ const metadata = {
checkpointedAt: '2026-07-29T00:00:00.000Z'
}
/** Mirrors the on-disk field order the serializer emits, so tests can compare against
* real JSON.stringify output rather than a hand-written string. */
function expectedJson(input: TerminalSnapshot): string {
return JSON.stringify({
snapshotAnsi: input.snapshotAnsi,
scrollbackAnsi: input.scrollbackAnsi,
oscLinks: input.oscLinks,
rehydrateSequences: input.rehydrateSequences,
cwd: metadata.cwd,
cols: input.cols,
rows: input.rows,
modes: input.modes,
scrollbackLines: input.scrollbackLines,
generation: metadata.generation,
checkpointedAt: metadata.checkpointedAt
})
}
describe('terminal checkpoint serializer', () => {
it('matches JSON.stringify exactly at the UTF-8 byte limit', async () => {
const input = snapshot({
@@ -43,19 +61,7 @@ describe('terminal checkpoint serializer', () => {
)}`,
oscLinks: [{ row: 0, startCol: 0, endCol: 1, uri: 'https://example.com/😀\n' }]
})
const expected = JSON.stringify({
snapshotAnsi: input.snapshotAnsi,
scrollbackAnsi: input.scrollbackAnsi,
oscLinks: input.oscLinks,
rehydrateSequences: input.rehydrateSequences,
cwd: metadata.cwd,
cols: input.cols,
rows: input.rows,
modes: input.modes,
scrollbackLines: input.scrollbackLines,
generation: metadata.generation,
checkpointedAt: metadata.checkpointedAt
})
const expected = expectedJson(input)
const exactBytes = Buffer.byteLength(expected, 'utf8')
await expect(serializeTerminalCheckpointWithinLimit(input, metadata, exactBytes)).resolves.toBe(
@@ -63,24 +69,24 @@ describe('terminal checkpoint serializer', () => {
)
})
it('sizes lone surrogates as JSON.stringify escapes them', async () => {
// Why: an unpaired surrogate costs 6 bytes as \\udXXX, not the 3 a BMP char would.
const input = snapshot({
snapshotAnsi: `${String.fromCharCode(0xd800)}lone${String.fromCharCode(0xdfff)}`
})
const expected = expectedJson(input)
await expect(
serializeTerminalCheckpointWithinLimit(input, metadata, Buffer.byteLength(expected, 'utf8'))
).resolves.toBe(expected)
})
it('rejects multibyte input whose code-unit length fits under the byte cap', async () => {
const input = snapshot({
scrollbackAnsi: 'é\r\n'.repeat(100),
scrollbackLines: 100
})
const expected = JSON.stringify({
snapshotAnsi: input.snapshotAnsi,
scrollbackAnsi: input.scrollbackAnsi,
oscLinks: input.oscLinks,
rehydrateSequences: input.rehydrateSequences,
cwd: metadata.cwd,
cols: input.cols,
rows: input.rows,
modes: input.modes,
scrollbackLines: input.scrollbackLines,
generation: metadata.generation,
checkpointedAt: metadata.checkpointedAt
})
const expected = expectedJson(input)
const maxBytes = expected.length + 1
expect(expected.length).toBeLessThan(maxBytes)
@@ -92,7 +98,7 @@ describe('terminal checkpoint serializer', () => {
expect(Buffer.byteLength(serialized, 'utf8')).toBeLessThanOrEqual(maxBytes)
})
it('does not materialize and rescan a passing candidate', async () => {
it('reads each snapshot field once on the passing path', async () => {
let reads = 0
const input = snapshot()
Object.defineProperty(input, 'snapshotAnsi', {
@@ -102,23 +108,28 @@ describe('terminal checkpoint serializer', () => {
return 'visible'
}
})
const stringify = vi.spyOn(JSON, 'stringify')
const byteLength = vi.spyOn(Buffer, 'byteLength')
try {
await serializeTerminalCheckpointWithinLimit(input, metadata, 20 * 1024)
await serializeTerminalCheckpointWithinLimit(input, metadata, 20 * 1024)
expect({
fullCandidateStringifies: stringify.mock.calls.filter(
([value]) => typeof value === 'object' && value !== null
).length,
byteLengthCalls: byteLength.mock.calls.length,
snapshotReads: reads
}).toEqual({ fullCandidateStringifies: 0, byteLengthCalls: 0, snapshotReads: 1 })
} finally {
stringify.mockRestore()
byteLength.mockRestore()
}
expect(reads).toBe(1)
})
it('rejects a candidate whose escapes push it over the cap by a single byte', async () => {
// Why: the sizing walk is the only thing standing between an over-cap payload and disk,
// so pin the boundary in both directions rather than trusting a coarse limit.
const ansi = `${String.fromCharCode(0x1b)}[0m漢😀"\\\n`
const input = snapshot({ snapshotAnsi: ansi.repeat(64) })
const expected = expectedJson(input)
const exactBytes = Buffer.byteLength(expected, 'utf8')
await expect(serializeTerminalCheckpointWithinLimit(input, metadata, exactBytes)).resolves.toBe(
expected
)
const trimmed = await serializeTerminalCheckpointWithinLimit(input, metadata, exactBytes - 1)
expect(trimmed).not.toBe(expected)
expect(Buffer.byteLength(trimmed, 'utf8')).toBeLessThanOrEqual(exactBytes - 1)
})
it('rejects an oversized escaped candidate without materializing it', async () => {
+82 -106
View File
@@ -31,123 +31,90 @@ function checkpointFile(
}
}
class BoundedJsonWriter {
private output = ''
private chunk = ''
/** Tracks how many UTF-8 bytes `JSON.stringify` would emit. `add` returning false unwinds the
* whole walk, so an over-limit checkpoint is abandoned partway instead of measured whole. */
class JsonByteBudget {
private bytes = 0
private exceeded = false
constructor(private readonly maxBytes: number) {}
append(value: string, bytes: number): boolean {
if (this.bytes + bytes > this.maxBytes) {
this.exceeded = true
this.output = ''
this.chunk = ''
return false
}
remaining(): number {
return this.maxBytes - this.bytes
}
add(bytes: number): boolean {
this.bytes += bytes
this.chunk += value
if (this.chunk.length >= 16 * 1024) {
this.output += this.chunk
this.chunk = ''
}
return true
}
result(): string | null {
return this.exceeded ? null : this.output + this.chunk
return this.bytes <= this.maxBytes
}
}
function escapedCodeUnit(codeUnit: number): string | null {
switch (codeUnit) {
case 0x08:
return '\\b'
case 0x09:
return '\\t'
case 0x0a:
return '\\n'
case 0x0c:
return '\\f'
case 0x0d:
return '\\r'
case 0x22:
return '\\"'
case 0x5c:
return '\\\\'
default:
return codeUnit < 0x20 ? `\\u${codeUnit.toString(16).padStart(4, '0')}` : null
}
}
const MEASURE_CHUNK_CODE_UNITS = 8 * 1024
function appendJsonString(writer: BoundedJsonWriter, value: string): boolean {
if (!writer.append('"', 1)) {
return false
}
let spanStart = 0
let spanBytes = 0
for (let index = 0; index < value.length; index += 1) {
const codeUnit = value.charCodeAt(index)
let escaped = escapedCodeUnit(codeUnit)
if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
const next = value.charCodeAt(index + 1)
if (next >= 0xdc00 && next <= 0xdfff) {
spanBytes += 4
index += 1
function measureJsonString(budget: JsonByteBudget, value: string): boolean {
const remaining = budget.remaining()
let bytes = 2
let index = 0
// Why: test the budget per chunk, not per code unit — a multi-megabyte scrollback still
// bails within one chunk of going over, without paying a compare on every character.
while (index < value.length) {
const chunkEnd = Math.min(value.length, index + MEASURE_CHUNK_CODE_UNITS)
for (; index < chunkEnd; index += 1) {
const codeUnit = value.charCodeAt(index)
if (codeUnit === 0x22 || codeUnit === 0x5c) {
bytes += 2
} else if (codeUnit < 0x20) {
bytes +=
codeUnit === 0x08 ||
codeUnit === 0x09 ||
codeUnit === 0x0a ||
codeUnit === 0x0c ||
codeUnit === 0x0d
? 2
: 6
} else if (codeUnit < 0x80) {
bytes += 1
} else if (codeUnit < 0x800) {
bytes += 2
} else if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
const next = value.charCodeAt(index + 1)
if (next >= 0xdc00 && next <= 0xdfff) {
bytes += 4
index += 1
} else {
bytes += 6
}
} else if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) {
bytes += 6
} else {
escaped = `\\u${codeUnit.toString(16)}`
bytes += 3
}
} else if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) {
escaped = `\\u${codeUnit.toString(16)}`
} else if (escaped === null) {
spanBytes += codeUnit < 0x80 ? 1 : codeUnit < 0x800 ? 2 : 3
}
if (escaped !== null) {
if (
(index > spanStart && !writer.append(value.slice(spanStart, index), spanBytes)) ||
!writer.append(escaped, escaped.length)
) {
return false
}
spanStart = index + 1
spanBytes = 0
} else if (index + 1 - spanStart >= 16 * 1024) {
if (!writer.append(value.slice(spanStart, index + 1), spanBytes)) {
return false
}
spanStart = index + 1
spanBytes = 0
if (bytes > remaining) {
return budget.add(bytes)
}
}
if (spanStart < value.length && !writer.append(value.slice(spanStart), spanBytes)) {
return false
}
return writer.append('"', 1)
return budget.add(bytes)
}
function omittedByJson(value: unknown): boolean {
return value === undefined || typeof value === 'function' || typeof value === 'symbol'
}
function appendJsonValue(
writer: BoundedJsonWriter,
function measureJsonValue(
budget: JsonByteBudget,
value: unknown,
activeObjects: Set<object>
): boolean {
if (value === null) {
return writer.append('null', 4)
return budget.add(4)
}
switch (typeof value) {
case 'string':
return appendJsonString(writer, value)
return measureJsonString(budget, value)
case 'boolean':
return writer.append(value ? 'true' : 'false', value ? 4 : 5)
case 'number': {
const json = Number.isFinite(value) ? JSON.stringify(value) : 'null'
return writer.append(json, json.length)
}
return budget.add(value ? 4 : 5)
case 'number':
return budget.add(Number.isFinite(value) ? JSON.stringify(value).length : 4)
case 'bigint':
throw new TypeError('Do not know how to serialize a BigInt')
case 'undefined':
@@ -164,26 +131,28 @@ function appendJsonValue(
activeObjects.add(value)
try {
if (Array.isArray(value)) {
if (!writer.append('[', 1)) {
// Why: both brackets are charged up front — the running total only has to be right at
// the end, and pre-charging the closer keeps the exit paths from having to add it.
if (!budget.add(2)) {
return false
}
for (let index = 0; index < value.length; index += 1) {
if (index > 0 && !writer.append(',', 1)) {
if (index > 0 && !budget.add(1)) {
return false
}
// Why: JSON.stringify renders holes and non-serializable elements as null.
const entry = value[index]
if (omittedByJson(entry)) {
if (!writer.append('null', 4)) {
return false
}
} else if (!appendJsonValue(writer, entry, activeObjects)) {
const fits = omittedByJson(entry)
? budget.add(4)
: measureJsonValue(budget, entry, activeObjects)
if (!fits) {
return false
}
}
return writer.append(']', 1)
return true
}
if (!writer.append('{', 1)) {
if (!budget.add(2)) {
return false
}
let entries = 0
@@ -193,25 +162,32 @@ function appendJsonValue(
continue
}
if (
(entries > 0 && !writer.append(',', 1)) ||
!appendJsonString(writer, key) ||
!writer.append(':', 1) ||
!appendJsonValue(writer, entry, activeObjects)
(entries > 0 && !budget.add(1)) ||
!measureJsonString(budget, key) ||
!budget.add(1) ||
!measureJsonValue(budget, entry, activeObjects)
) {
return false
}
entries += 1
}
return writer.append('}', 1)
return true
} finally {
activeObjects.delete(value)
}
}
function stringifyWithinLimit(checkpoint: TerminalCheckpointFile, maxBytes: number): string | null {
const writer = new BoundedJsonWriter(maxBytes)
appendJsonValue(writer, checkpoint, new Set())
return writer.result()
// Why: size the checkpoint first so an over-cap payload is never handed to JSON.stringify,
// then let the native serializer emit the JSON — it is far faster than emitting it by hand.
if (!measureJsonValue(new JsonByteBudget(maxBytes), checkpoint, new Set())) {
return null
}
const json = JSON.stringify(checkpoint)
if (Buffer.byteLength(json, 'utf8') > maxBytes) {
throw new Error('Terminal checkpoint size estimator mismatch')
}
return json
}
async function replaySnapshot(snapshot: TerminalSnapshot): Promise<HeadlessEmulator> {