mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
#7847 reports a 3.05 MiB request (729 messages / 86 tools) reaching ~12,282 MiB of V8 heap, and asks for "a regression benchmark that records peak heap for representative 500-800-message, tool-rich requests" before any fix lands. There is currently no memory baseline in the repo at all (bench:compression is the only benchmark), so a clone-reduction change could neither be justified nor regression-guarded. npm run bench:heap-body attributes retained heap to each copy the chat path makes: | mechanism | call site | retained | x wire | | cloneLogPayload (unbounded) | chat.ts buildClientRawRequest | 3.18 MiB | 1.04x | | cloneBoundedForLog (bounded) | requestLogger.logClientRawRequest | 0.04 MiB | 0.01x | | structuredClone x3 (combo targets) | combo.ts attemptBody | 9.53 MiB | 3.12x | | JSON.stringify (token estimate) | combo.ts estimateTokens | 3.06 MiB | 1.00x | | per request (sum) | |15.81 MiB | 5.17x | It measures the real production helpers rather than reimplementations, so a change to the log bounds or the clone strategy is reflected directly. Design notes: - Deterministic: fixed-seed LCG, no Math.random(). Verified byte-identical across three consecutive runs — without that, a before/after delta measures noise, not the change. - Corpus lives in its own side-effect-free module so the unit test can import it without booting SQLite (requestLogger transitively opens the DB at import time). - Hermetic: DATA_DIR is redirected to a temp dir before importing, so the benchmark never touches the operator's real ~/.omniroute store. - Node, not bun: --expose-gc and V8 heap accounting are the measurement; another engine's heap number would not describe the production runtime. - --max-retained-mib exits non-zero, so this can become a CI gate once a target is agreed. Reports only; wires nothing into CI and changes no production code.
84 lines
2.4 KiB
TypeScript
84 lines
2.4 KiB
TypeScript
/**
|
|
* Deterministic coding-agent request corpus for the #7847 heap benchmark.
|
|
*
|
|
* Kept separate from `request-body-heap.ts` so it can be imported with zero side effects:
|
|
* the benchmark redirects DATA_DIR and transitively boots SQLite at import time, which a unit
|
|
* test must not do just to assert corpus stability.
|
|
*
|
|
* Determinism is the point. A benchmark corpus built with Math.random() would make a
|
|
* before/after comparison measure noise instead of the change under test, so the generator uses
|
|
* a fixed-seed LCG and is covered by tests/unit/heap-benchmark-corpus.test.ts.
|
|
*/
|
|
|
|
/** Defaults reproduce the #7847 production incident: 3.05 MiB, 729 messages, 86 tools. */
|
|
export const INCIDENT_SHAPE = {
|
|
messages: 729,
|
|
tools: 86,
|
|
/** Calibrated so the defaults land on the incident's wire size. */
|
|
contentWords: 527,
|
|
} as const;
|
|
|
|
function lcg(seed: number): () => number {
|
|
let s = seed >>> 0;
|
|
return () => ((s = (s * 1664525 + 1013904223) >>> 0) / 0x100000000);
|
|
}
|
|
|
|
const WORDS = [
|
|
"refactor",
|
|
"handler",
|
|
"provider",
|
|
"combo",
|
|
"stream",
|
|
"token",
|
|
"context",
|
|
"payload",
|
|
"upstream",
|
|
"fallback",
|
|
"quota",
|
|
"retry",
|
|
"executor",
|
|
"translate",
|
|
"compression",
|
|
"cache",
|
|
];
|
|
|
|
function sentence(rnd: () => number, words: number): string {
|
|
const out: string[] = [];
|
|
for (let i = 0; i < words; i++) out.push(WORDS[Math.floor(rnd() * WORDS.length)]);
|
|
return out.join(" ");
|
|
}
|
|
|
|
/** A coding-agent shaped request: long alternating history plus a large tool catalog. */
|
|
export function buildAgentPayload(
|
|
messages: number = INCIDENT_SHAPE.messages,
|
|
tools: number = INCIDENT_SHAPE.tools,
|
|
contentWords: number = INCIDENT_SHAPE.contentWords
|
|
): Record<string, unknown> {
|
|
const rnd = lcg(0x5eed);
|
|
return {
|
|
model: "claude-opus-5",
|
|
stream: true,
|
|
messages: Array.from({ length: messages }, (_, i) => ({
|
|
role: i % 2 === 0 ? "user" : "assistant",
|
|
content: sentence(rnd, contentWords),
|
|
})),
|
|
tools: Array.from({ length: tools }, (_, i) => ({
|
|
type: "function",
|
|
function: {
|
|
name: `tool_${i}`,
|
|
description: sentence(rnd, 25),
|
|
parameters: {
|
|
type: "object",
|
|
properties: Object.fromEntries(
|
|
Array.from({ length: 8 }, (_, p) => [
|
|
`param_${p}`,
|
|
{ type: "string", description: sentence(rnd, 10) },
|
|
])
|
|
),
|
|
required: ["param_0"],
|
|
},
|
|
},
|
|
})),
|
|
};
|
|
}
|