From c01d5e167a0b68ecc0d4adf6cc9446f59fcab705 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sun, 26 Jul 2026 19:39:20 -0700 Subject: [PATCH] perf(memory-safety): scan JSON structure by char code The structure scan added to the daemon's receive path cost 4x more than the JSON.parse it guards: it indexed the string per character, allocating a one-character string for every byte, and walked string bodies one byte at a time even though a string's contents carry no structure. Read char codes instead, and skip each string body to its closing quote in one indexOf. Two cursors keep that skip linear: a monotonic escape cursor so backslash lookups never rescan, and a hoisted closing-quote index that is re-derived only when the escape cursor passes it. Deriving the closing quote inside the escape loop instead would rescan the tail once per escape, which is quadratic on a payload carrying escaped text -- 2.3s on a 4 MB body. Same input, same verdict: 505,720 differential cases against the previous char-by-char implementation -- 240,000 randomized over a structural / escape / unicode alphabet under four limit sets, plus every string up to length 5 over { } [ ] , : " \ a -- produce identical results, including the thrown resource and limit. Dropping the closing-quote refresh reintroduces mismatches, so the coverage is not vacuous. 20k scans of a 2 KiB message: 286ms -> 5.3ms. The guard now costs less than the parse it protects, which takes the daemon's fragmented-feed overhead from 2.95x down to 1.44x. On a 4 MB escape-dense body: 14.0ms -> 0.9ms. Co-authored-by: Orca --- .../json-text-structure-limit.test.ts | 70 +++++++++++++++++ .../json-text-structure-limit.ts | 75 ++++++++++++------- 2 files changed, 117 insertions(+), 28 deletions(-) diff --git a/src/shared/memory-safety/json-text-structure-limit.test.ts b/src/shared/memory-safety/json-text-structure-limit.test.ts index 2a1112ba772..6576e47ceac 100644 --- a/src/shared/memory-safety/json-text-structure-limit.test.ts +++ b/src/shared/memory-safety/json-text-structure-limit.test.ts @@ -37,4 +37,74 @@ describe('JSON text structure admission', () => { }) ).not.toThrow() }) + + // The scan skips string bodies wholesale, so a backslash run that ends a string on an even + // count — and one that escapes the quote on an odd count — must resolve the same way. + it.each([ + ['{"value":"\\\\"}', 'even backslash run closes the string'], + ['{"value":"\\\\\\\\"}', 'longer even backslash run closes the string'], + ['{"value":"\\""}', 'escaped quote does not close the string'], + ['{"a":"}","b":"]"}', 'structural characters inside adjacent strings'], + ['{"emoji":"💥{[,:"}', 'astral-plane characters inside a string'] + ])('tracks string boundaries through %s', (content) => { + expect(() => + assertJsonTextStructureWithinLimits(content, { + structuralTokens: 1_000, + nestingDepth: 8 + }) + ).not.toThrow() + }) + + it('stops scanning at an unterminated string instead of counting its contents', () => { + expect(() => + assertJsonTextStructureWithinLimits('{"value":"[[[[[[[[[[', { + structuralTokens: 2, + nestingDepth: 1 + }) + ).not.toThrow() + }) + + it('scans an escape-dense string body in linear time', () => { + // Why: a frame carrying a text file is one huge escaped string. Rescanning for the closing + // quote once per escape is quadratic and stalls the receive loop for seconds at 4 MB. + const body = `${'a'.repeat(63)}\\n`.repeat(66_000) + const frame = `{"jsonrpc":"2.0","params":{"data":"${body}"}}` + expect(frame.length).toBeGreaterThan(4_000_000) + + const startedAt = performance.now() + assertJsonTextStructureWithinLimits(frame, { + structuralTokens: 1_000_000, + nestingDepth: 128 + }) + + // Generous versus the ~1ms linear scan, but far under the ~2.5s the quadratic path takes. + expect(performance.now() - startedAt).toBeLessThan(500) + }) + + it.each([ + ['{"a":"x\\\\","b":1}', 'a body ending in an escaped backslash'], + ['{"a":"\\\\\\"}","b":2}', 'an escaped backslash before an escaped quote'], + ['{"a":"\\"\\"\\"","b":3}', 'consecutive escaped quotes'], + ['{"a":"\\n\\n\\n","b":4}', 'repeated short escapes'] + ])('finds the true string end past %s', (content) => { + // Why: the scan advances a cursor past each escape and only rescans for the closing quote + // once the cursor passes it — a stale quote index would end the string early and count + // the remaining body as structure. + expect(() => + assertJsonTextStructureWithinLimits(content, { + structuralTokens: 6, + nestingDepth: 4 + }) + ).not.toThrow() + }) + + it('rejects a deeply nested line before it becomes an object graph', () => { + const depth = 1_024 + expect(() => + assertJsonTextStructureWithinLimits(`${'['.repeat(depth)}${']'.repeat(depth)}`, { + structuralTokens: 1_000_000, + nestingDepth: 128 + }) + ).toThrowError(new JsonTextStructureCapacityError('nestingDepth', 128)) + }) }) diff --git a/src/shared/memory-safety/json-text-structure-limit.ts b/src/shared/memory-safety/json-text-structure-limit.ts index e0ede23db54..1d9f82bd032 100644 --- a/src/shared/memory-safety/json-text-structure-limit.ts +++ b/src/shared/memory-safety/json-text-structure-limit.ts @@ -17,6 +17,14 @@ export class JsonTextStructureCapacityError extends Error { } } +const QUOTE = 0x22 +const COMMA = 0x2c +const COLON = 0x3a +const OPEN_BRACKET = 0x5b +const CLOSE_BRACKET = 0x5d +const OPEN_BRACE = 0x7b +const CLOSE_BRACE = 0x7d + export function assertJsonTextStructureWithinLimits( content: string, limits: JsonTextStructureLimits @@ -25,38 +33,60 @@ export function assertJsonTextStructureWithinLimits( assertLimit(limits.nestingDepth) let structuralTokens = 0 let depth = 0 - let inString = false - let escaped = false + // Why: this runs on the daemon's hot receive path, so the scan reads char codes and skips + // string bodies wholesale. A monotonic escape cursor keeps the lookups O(n) overall. + let nextEscape = content.indexOf('\\') for (let index = 0; index < content.length; index += 1) { - const character = content[index] - if (inString) { - if (escaped) { - escaped = false - } else if (character === '\\') { - escaped = true - } else if (character === '"') { - inString = false + const code = content.charCodeAt(index) + + if (code === QUOTE) { + // String contents carry no structure; jump to the closing quote rather than + // stepping through every byte of a large payload. + let cursor = index + 1 + // Why: hoisted so an escape-dense body rescans the tail once per *skipped* quote rather + // than once per escape — the latter is quadratic on a payload carrying escaped text. + let closing = content.indexOf('"', cursor) + for (;;) { + if (closing < 0) { + return + } + if (nextEscape >= 0 && nextEscape < cursor) { + nextEscape = content.indexOf('\\', cursor) + } + if (nextEscape < 0 || nextEscape > closing) { + index = closing + break + } + cursor = nextEscape + 2 + if (cursor > closing) { + closing = content.indexOf('"', cursor) + } } continue } - if (character === '"') { - inString = true - continue - } - if (!isStructuralToken(character)) { + + if ( + code !== OPEN_BRACE && + code !== CLOSE_BRACE && + code !== OPEN_BRACKET && + code !== CLOSE_BRACKET && + code !== COMMA && + code !== COLON + ) { continue } + structuralTokens += 1 if (structuralTokens > limits.structuralTokens) { throw new JsonTextStructureCapacityError('structuralTokens', limits.structuralTokens) } - if (character === '{' || character === '[') { + if (code === OPEN_BRACE || code === OPEN_BRACKET) { depth += 1 if (depth > limits.nestingDepth) { throw new JsonTextStructureCapacityError('nestingDepth', limits.nestingDepth) } - } else if (character === '}' || character === ']') { + } else if (code === CLOSE_BRACE || code === CLOSE_BRACKET) { depth = Math.max(0, depth - 1) } } @@ -67,14 +97,3 @@ function assertLimit(value: number): void { throw new RangeError('JSON structure limits must be non-negative safe integers') } } - -function isStructuralToken(character: string | undefined): boolean { - return ( - character === '{' || - character === '}' || - character === '[' || - character === ']' || - character === ',' || - character === ':' - ) -}