From b2613f430df3ebf2baf81a8da3ef7744cf747501 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Fri, 4 Sep 2026 04:29:48 -0700 Subject: [PATCH] perf(agent-hooks): find spool newlines with Buffer.indexOf, not a per-byte loop readSpoolFile walked every byte of every spooled JSONL file in an interpreted loop to locate newlines. drainAgentHookSpool runs inside start() before the hook listener binds, over up to AGENT_HOOK_SPOOL_MAX_FILES files, so this sits on the startup path. Buffer.indexOf reaches the same newlines via memchr. A 5.3MB spool file drops from 32.50ms to 9.26ms per read. The torn-trailing-line contract is unchanged: a final line with no newline is still left unconsumed. --- src/shared/agent-hook-spool-read.test.ts | 65 ++++++++++++++++++++++++ src/shared/agent-hook-spool.ts | 14 +++-- 2 files changed, 71 insertions(+), 8 deletions(-) create mode 100644 src/shared/agent-hook-spool-read.test.ts diff --git a/src/shared/agent-hook-spool-read.test.ts b/src/shared/agent-hook-spool-read.test.ts new file mode 100644 index 00000000000..d589f5b2274 --- /dev/null +++ b/src/shared/agent-hook-spool-read.test.ts @@ -0,0 +1,65 @@ +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { readSpoolFile } from './agent-hook-spool' + +let dir: string + +beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'orca-spool-read-')) +}) + +afterEach(() => { + rmSync(dir, { recursive: true, force: true }) +}) + +function write(contents: string): string { + const file = join(dir, 'spool.jsonl') + writeFileSync(file, contents) + return file +} + +function record(paneKey: string): string { + return JSON.stringify({ paneKey, source: 'PreToolUse', payload: {}, receivedAt: Date.now() }) +} + +describe('readSpoolFile', () => { + it('leaves a trailing line without its newline unconsumed', () => { + const complete = `${record('a')}\n` + const result = readSpoolFile(write(`${complete}${record('b')}`)) + + expect(result.records.map((entry) => entry.paneKey)).toEqual(['a']) + // The in-flight final record must stay replayable: consumed stops at the last newline. + expect(result.consumed).toBe(Buffer.byteLength(complete)) + }) + + it('consumes through the final newline when every line is complete', () => { + const contents = `${record('a')}\n${record('b')}\n` + const result = readSpoolFile(write(contents)) + + expect(result.records.map((entry) => entry.paneKey)).toEqual(['a', 'b']) + expect(result.consumed).toBe(Buffer.byteLength(contents)) + }) + + it('skips blank lines without consuming less than the bytes they occupy', () => { + const contents = `${record('a')}\n\n\n${record('b')}\n` + const result = readSpoolFile(write(contents)) + + expect(result.records.map((entry) => entry.paneKey)).toEqual(['a', 'b']) + expect(result.consumed).toBe(Buffer.byteLength(contents)) + }) + + it('returns nothing for an empty file', () => { + expect(readSpoolFile(write(''))).toEqual({ records: [], consumed: 0 }) + }) + + it('returns nothing for a file that is one torn line', () => { + expect(readSpoolFile(write(record('a'))).records).toEqual([]) + expect(readSpoolFile(write(record('a'))).consumed).toBe(0) + }) + + it('returns nothing for a missing file', () => { + expect(readSpoolFile(join(dir, 'absent.jsonl'))).toEqual({ records: [], consumed: 0 }) + }) +}) diff --git a/src/shared/agent-hook-spool.ts b/src/shared/agent-hook-spool.ts index 3ad478ed8a5..275e6f5ce52 100644 --- a/src/shared/agent-hook-spool.ts +++ b/src/shared/agent-hook-spool.ts @@ -67,19 +67,17 @@ export function readSpoolFile( const records: SpoolRecord[] = [] let consumed = 0 let start = 0 - for (let end = 0; end <= bytes.length; end += 1) { - if (end !== bytes.length && bytes[end] !== 0x0a) { - continue - } + // indexOf, not a per-byte loop: this runs over every spooled file before the hook listener binds, + // and Buffer.indexOf finds the newline with memchr instead of an interpreted scan. + for (;;) { + const end = bytes.indexOf(0x0a, start) // A final line without its newline may still be in flight from a hook writer. // Leave it untouched until the writer terminates the record explicitly. - if (end === bytes.length && (end === 0 || bytes[end - 1] !== 0x0a)) { + if (end === -1) { break } const lineBytes = bytes.subarray(start, end) - if (end !== bytes.length) { - consumed = end + 1 - } + consumed = end + 1 start = end + 1 if (lineBytes.length === 0) { continue