mirror of
https://github.com/stablyai/orca.git
synced 2026-10-19 16:03:26 +00:00
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 <help@stably.ai>
This commit is contained in:
@@ -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))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 === ':'
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user