perf(orchestration): bound the worker terminal archive in linear time (#18622)

boundArchiveLines built its kept-lines array with kept.unshift() per line,
which is O(n) per call. The 256KB char budget admits ~262k lines when they are
short, so a blank-line-heavy terminal tail turned the truncation into a
quadratic main-process stall: 4.3s for a 300k-line input here, versus 6ms
after collecting newest-first and reversing once.

Order and truncation boundary are unchanged.
This commit is contained in:
Neil
2026-09-04 14:31:01 -07:00
committed by GitHub
parent ef428d879e
commit cae616384f
2 changed files with 50 additions and 6 deletions
@@ -0,0 +1,41 @@
import { describe, expect, it } from 'vitest'
import { boundArchiveLines } from './worker-output-archive'
const TERMINAL_ARCHIVE_MAX_CHARS = 262_144
function totalCost(lines: string[]): number {
return lines.reduce((sum, line) => sum + line.length + 1, 0)
}
describe('boundArchiveLines', () => {
it('returns the original array untouched when the tail already fits', () => {
const lines = ['one', 'two', 'three']
const bounded = boundArchiveLines(lines)
expect(bounded.truncated).toBe(false)
expect(bounded.lines).toBe(lines)
})
it('keeps the newest lines in order and reports truncation', () => {
const lines = Array.from({ length: 40_000 }, (_, index) => `line ${index}`)
const bounded = boundArchiveLines(lines)
expect(bounded.truncated).toBe(true)
expect(totalCost(bounded.lines)).toBeLessThanOrEqual(TERMINAL_ARCHIVE_MAX_CHARS)
expect(bounded.lines.at(-1)).toBe(lines.at(-1))
expect(bounded.lines).toEqual(lines.slice(lines.length - bounded.lines.length))
})
it('truncates a single oversized line from its tail', () => {
const bounded = boundArchiveLines(['x'.repeat(TERMINAL_ARCHIVE_MAX_CHARS * 2)])
expect(bounded.truncated).toBe(true)
expect(bounded.lines).toHaveLength(1)
expect(bounded.lines[0]).toHaveLength(TERMINAL_ARCHIVE_MAX_CHARS - 1)
})
it('bounds a blank-line flood in linear time', () => {
// The char budget admits ~262k blank lines; an unshift-per-line build was ~4.3s here.
const startedAt = performance.now()
const bounded = boundArchiveLines(Array.from({ length: 300_000 }, () => ''))
expect(bounded.lines).toHaveLength(TERMINAL_ARCHIVE_MAX_CHARS)
expect(performance.now() - startedAt).toBeLessThan(500)
})
})
@@ -114,7 +114,7 @@ export async function captureWorkerOutputArchive(args: {
}
}
function boundArchiveLines(lines: string[]): { lines: string[]; truncated: boolean } {
export function boundArchiveLines(lines: string[]): { lines: string[]; truncated: boolean } {
let total = 0
for (const line of lines) {
total += line.length + 1
@@ -122,18 +122,21 @@ function boundArchiveLines(lines: string[]): { lines: string[]; truncated: boole
if (total <= TERMINAL_ARCHIVE_MAX_CHARS) {
return { lines, truncated: false }
}
const kept: string[] = []
// Collected newest-first and reversed once: unshift per line is O(n^2) and the
// char budget admits ~260k blank lines.
const keptReversed: string[] = []
let budget = TERMINAL_ARCHIVE_MAX_CHARS
for (let index = lines.length - 1; index >= 0; index -= 1) {
const cost = lines[index].length + 1
if (cost > budget) {
if (kept.length === 0 && budget > 1) {
kept.unshift(lines[index].slice(-(budget - 1)))
if (keptReversed.length === 0 && budget > 1) {
keptReversed.push(lines[index].slice(-(budget - 1)))
}
break
}
kept.unshift(lines[index])
keptReversed.push(lines[index])
budget -= cost
}
return { lines: kept, truncated: true }
keptReversed.reverse()
return { lines: keptReversed, truncated: true }
}