perf(coding-agent): coalesce subagent output reconstruction

Defer recentOutput line reconstruction from every text_delta to the
progress emit boundary. appendRecentOutputTail only extends the capped
raw tail and marks dirty; refreshRecentOutput runs the exact old
split/filter/slice(-8)/reverse algorithm as the first step of every
emitProgressNow snapshot (onProgress + event bus), including coalesced
and finalize/error/cancel flushes. Reset publishes [] immediately;
replace marks dirty; past snapshot arrays stay immutable via spread.

Before (base pool median-of-5):
  w8_d3  61.55 cpu_ms/1k_events
  w32_d3 44.16 cpu_ms/1k_events
After (stable final run on E+G, 7 episodes, trimmed CV gate pass):
  w8_d3  55.78 cpu_ms/1k_events  (1.103×)  trimmed CV 15.1%
  w32_d3 40.72 cpu_ms/1k_events  (1.084×)  trimmed CV 11.1%
Checksums match prior exactness baseline; retained_after_release_kb
1284 / 2864 (no regression vs prior concur).

Op: GConcurEmitBoundary emit-boundary dirty flag
Restores: none
This commit is contained in:
metaphorics
2026-07-18 11:07:51 +09:00
parent 48241afcc4
commit 8d8ad0faed
2 changed files with 488 additions and 28 deletions
+23 -28
View File
@@ -914,7 +914,7 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
const finalOutputChunks: string[] = [];
const RECENT_OUTPUT_TAIL_BYTES = 8 * 1024;
let recentOutputTail = "";
let tailLastLineRepresentable = false;
let recentOutputDirty = false;
let resolved = false;
let abortSent = false;
let abortReason: AbortReason | undefined;
@@ -1054,7 +1054,22 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
let lastProgressEmitMs = 0;
let progressTimeoutId: NodeJS.Timeout | null = null;
// Recompute progress.recentOutput from the capped tail. Deferred: text_delta
// appends only extend the tail and mark it dirty; the (up to 8KB) split/filter
// runs synchronously here, immediately before the ONLY places the progress
// object is snapshotted ({...progress} for onProgress and the eventBus
// progress channel, both inside emitProgressNow — including the
// scheduleProgress(flush) finalize/error/cancel paths). Observers therefore
// always see exact state; no staleness beyond the existing 150ms coalescing.
const refreshRecentOutput = () => {
if (!recentOutputDirty) return;
recentOutputDirty = false;
const filtered = recentOutputTail.split("\n").filter(line => line.trim());
progress.recentOutput = filtered.slice(-8).reverse();
};
const emitProgressNow = () => {
refreshRecentOutput();
progress.durationMs = Date.now() - startTime;
onProgress?.({ ...progress });
const activityGist =
@@ -1137,36 +1152,16 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
return message.usage;
};
const updateRecentOutputLines = () => {
const lines = recentOutputTail.split("\n");
const filtered = lines.filter(line => line.trim());
progress.recentOutput = filtered.slice(-8).reverse();
// The tail's last raw segment (after its final newline) is "represented"
// in recentOutput only when it trims non-empty — an empty/whitespace-only
// trailing segment is filtered out, so recentOutput[0] is then the line
// before it, not the tail's true last line.
tailLastLineRepresentable = lines[lines.length - 1].trim().length > 0;
};
const appendRecentOutputTail = (text: string) => {
if (!text) return;
recentOutputTail += text;
const truncated = recentOutputTail.length > RECENT_OUTPUT_TAIL_BYTES;
if (truncated) {
if (recentOutputTail.length > RECENT_OUTPUT_TAIL_BYTES) {
recentOutputTail = recentOutputTail.slice(-RECENT_OUTPUT_TAIL_BYTES);
}
// Fast path: a token without a newline only extends the current last line.
// This runs on every text_delta token (hundreds/thousands per second while
// streaming), so skip re-splitting the whole (up to 8KB) tail unless the line
// structure actually changed. Requires no truncation AND the tail's last line
// already represented (trims non-empty) — otherwise boundaries shift and a
// full recompute is required. Appending to a non-empty line keeps it non-empty,
// so the flag stays valid across consecutive fast-path tokens.
if (truncated || text.includes("\n") || !tailLastLineRepresentable || progress.recentOutput.length === 0) {
updateRecentOutputLines();
} else {
progress.recentOutput = [progress.recentOutput[0] + text, ...progress.recentOutput.slice(1)];
}
// O(chunk) hot path: this runs on every text_delta token (hundreds/
// thousands per second while streaming). Line reconstruction is deferred
// to refreshRecentOutput() at the emit boundary.
recentOutputDirty = true;
};
const replaceRecentOutputFromContent = (content: unknown[]) => {
@@ -1181,12 +1176,12 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
recentOutputTail = recentOutputTail.slice(-RECENT_OUTPUT_TAIL_BYTES);
}
}
updateRecentOutputLines();
recentOutputDirty = true;
};
const resetRecentOutput = () => {
recentOutputTail = "";
tailLastLineRepresentable = false;
recentOutputDirty = false;
progress.recentOutput = [];
};