





















@@ -428,6 +428,39 @@ function buildQaRuntimeEnvPatch(params: {
428428return patch;
429429}
430430431+function appendNodeOption(raw: string | undefined, option: string) {
432+const parts = (raw ?? "").split(/\s+/u).filter(Boolean);
433+return parts.includes(option) ? parts.join(" ") : [...parts, option].join(" ");
434+}
435+436+function shouldCaptureGatewayHeapCheckpoints(env: NodeJS.ProcessEnv = process.env) {
437+return parseQaSuiteBooleanEnv(env.OPENCLAW_QA_GATEWAY_HEAP_CHECKPOINTS) === true;
438+}
439+440+function buildQaGatewayHeapCheckpointRuntimeEnvPatch(
441+env: NodeJS.ProcessEnv = process.env,
442+): NodeJS.ProcessEnv | undefined {
443+if (!shouldCaptureGatewayHeapCheckpoints(env)) {
444+return undefined;
445+}
446+return {
447+NODE_OPTIONS: appendNodeOption(env.NODE_OPTIONS, "--heapsnapshot-signal=SIGUSR2"),
448+};
449+}
450+451+function mergeQaRuntimeEnvPatches(
452+ ...patches: Array<NodeJS.ProcessEnv | undefined>
453+): NodeJS.ProcessEnv | undefined {
454+const merged: NodeJS.ProcessEnv = {};
455+for (const patch of patches) {
456+if (!patch) {
457+continue;
458+}
459+Object.assign(merged, patch);
460+}
461+return Object.keys(merged).length > 0 ? merged : undefined;
462+}
463+431464export type QaSuiteSummaryJsonParams = {
432465scenarios: QaSuiteScenarioResult[];
433466startedAt: Date;
@@ -454,6 +487,11 @@ type QaSuiteGatewayRssSample = NonNullable<
454487NonNullable<QaSuiteSummaryJson["metrics"]>["gatewayProcessRssSamples"]
455488>[number];
456489490+type QaGatewayHandle = Awaited<ReturnType<typeof startQaGatewayChild>>;
491+type QaSuiteGatewayHeapSnapshot = NonNullable<
492+NonNullable<QaSuiteSummaryJson["metrics"]>["gatewayHeapSnapshots"]
493+>[number];
494+457495/**
458496 * Pure-ish JSON builder for qa-suite-summary.json. Exported so the GPT-5.5
459497 * parity gate (agentic-parity-report.ts, #64441) and any future parity
@@ -767,18 +805,22 @@ function buildQaSuiteRuntimeMetrics(params: {
767805gatewayProcessRssStartBytes: number | null;
768806gatewayProcessRssEndBytes: number | null;
769807gatewayProcessRssSamples?: QaSuiteGatewayRssSample[];
808+gatewayHeapSnapshots?: QaSuiteGatewayHeapSnapshot[];
770809}): QaSuiteSummaryJson["metrics"] {
771810const wallMs = Math.max(1, params.finishedAt.getTime() - params.startedAt.getTime());
772811const gatewayProcessRssSamples = params.gatewayProcessRssSamples ?? [];
812+const gatewayHeapSnapshots = params.gatewayHeapSnapshots ?? [];
773813const gatewayProcessRssPeakBytes =
774814gatewayProcessRssSamples.length > 0
775815 ? Math.max(...gatewayProcessRssSamples.map((sample) => sample.gatewayProcessRssBytes))
776816 : params.gatewayProcessRssStartBytes === null || params.gatewayProcessRssEndBytes === null
777817 ? null
778818 : Math.max(params.gatewayProcessRssStartBytes, params.gatewayProcessRssEndBytes);
819+const gatewayHeapSnapshotMetrics =
820+gatewayHeapSnapshots.length === 0 ? {} : { gatewayHeapSnapshots };
779821const rssMetrics =
780822params.gatewayProcessRssStartBytes === null || params.gatewayProcessRssEndBytes === null
781- ? {}
823+ ? gatewayHeapSnapshotMetrics
782824 : {
783825gatewayProcessRssStartBytes: params.gatewayProcessRssStartBytes,
784826gatewayProcessRssEndBytes: params.gatewayProcessRssEndBytes,
@@ -791,9 +833,8 @@ function buildQaSuiteRuntimeMetrics(params: {
791833gatewayProcessRssPeakDeltaBytes:
792834gatewayProcessRssPeakBytes - params.gatewayProcessRssStartBytes,
793835}),
794- ...(gatewayProcessRssSamples.length === 0
795- ? {}
796- : { gatewayProcessRssSamples }),
836+ ...(gatewayProcessRssSamples.length === 0 ? {} : { gatewayProcessRssSamples }),
837+ ...gatewayHeapSnapshotMetrics,
797838};
798839if (params.gatewayProcessCpuStartMs === null || params.gatewayProcessCpuEndMs === null) {
799840return { wallMs, ...rssMetrics };
@@ -810,6 +851,81 @@ function buildQaSuiteRuntimeMetrics(params: {
810851};
811852}
812853854+function sanitizeQaHeapCheckpointLabel(label: string) {
855+return label.replace(/[^a-zA-Z0-9._-]+/gu, "-").replace(/^-+|-+$/gu, "") || "checkpoint";
856+}
857+858+async function listGatewayHeapSnapshotFiles(tempRoot: string) {
859+const entries = await fs.readdir(tempRoot, { withFileTypes: true }).catch(() => []);
860+const files = [];
861+for (const entry of entries) {
862+if (!entry.isFile() || !entry.name.endsWith(".heapsnapshot")) {
863+continue;
864+}
865+const pathName = path.join(tempRoot, entry.name);
866+const stats = await fs.stat(pathName).catch(() => null);
867+if (stats) {
868+files.push({ pathName, mtimeMs: stats.mtimeMs, size: stats.size });
869+}
870+}
871+return files.toSorted((left, right) => left.mtimeMs - right.mtimeMs);
872+}
873+874+async function waitForStableFileSize(pathName: string) {
875+let lastSize = -1;
876+for (let attempt = 0; attempt < 30; attempt += 1) {
877+const stats = await fs.stat(pathName).catch(() => null);
878+if (stats && stats.size > 0 && stats.size === lastSize) {
879+return stats.size;
880+}
881+lastSize = stats?.size ?? -1;
882+await sleep(250);
883+}
884+const stats = await fs.stat(pathName);
885+return stats.size;
886+}
887+888+async function captureGatewayHeapSnapshotCheckpoint(params: {
889+gateway: QaGatewayHandle;
890+outputDir: string;
891+label: string;
892+}): Promise<QaSuiteGatewayHeapSnapshot | undefined> {
893+const before = new Set(
894+(await listGatewayHeapSnapshotFiles(params.gateway.tempRoot)).map((file) => file.pathName),
895+);
896+params.gateway.signalProcess("SIGUSR2");
897+let snapshotPath: string | undefined;
898+for (let attempt = 0; attempt < 80; attempt += 1) {
899+const next = (await listGatewayHeapSnapshotFiles(params.gateway.tempRoot)).filter(
900+(file) => !before.has(file.pathName),
901+);
902+snapshotPath = next.at(-1)?.pathName;
903+if (snapshotPath) {
904+break;
905+}
906+await sleep(250);
907+}
908+if (!snapshotPath) {
909+return undefined;
910+}
911+912+const bytes = await waitForStableFileSize(snapshotPath);
913+const snapshotsDir = path.join(params.outputDir, "artifacts", "gateway-heap-snapshots");
914+await fs.mkdir(snapshotsDir, { recursive: true });
915+const relativePath = path.join(
916+"artifacts",
917+"gateway-heap-snapshots",
918+`${sanitizeQaHeapCheckpointLabel(params.label)}.heapsnapshot`,
919+);
920+await fs.copyFile(snapshotPath, path.join(params.outputDir, relativePath));
921+return {
922+label: params.label,
923+at: new Date().toISOString(),
924+path: relativePath,
925+ bytes,
926+};
927+}
928+813929export async function runQaSuite(params?: QaSuiteRunParams): Promise<QaSuiteResult> {
814930const startedAt = new Date();
815931const repoRoot = path.resolve(params?.repoRoot ?? process.cwd());
@@ -853,6 +969,7 @@ export async function runQaSuite(params?: QaSuiteRunParams): Promise<QaSuiteResu
853969defaultQaSuiteConcurrencyForTransport(transportId),
854970);
855971const progressEnabled = shouldLogQaSuiteProgress();
972+const gatewayHeapCheckpointsEnabled = shouldCaptureGatewayHeapCheckpoints();
856973writeQaSuiteProgress(
857974progressEnabled,
858975`run start: scenarios=${selectedCatalogScenarios.length} concurrency=${concurrency} transport=${transportId}`,
@@ -1160,11 +1277,14 @@ export async function runQaSuite(params?: QaSuiteRunParams): Promise<QaSuiteResu
11601277mutateConfig: gatewayConfigPatch
11611278 ? (cfg) => applyQaMergePatch(cfg, gatewayConfigPatch) as OpenClawConfig
11621279 : undefined,
1163-runtimeEnvPatch: buildQaRuntimeEnvPatch({
1164- providerMode,
1165-forcedRuntime: params?.forcedRuntime,
1166-mockBaseUrl: mock?.baseUrl,
1167-}),
1280+runtimeEnvPatch: mergeQaRuntimeEnvPatches(
1281+buildQaRuntimeEnvPatch({
1282+ providerMode,
1283+forcedRuntime: params?.forcedRuntime,
1284+mockBaseUrl: mock?.baseUrl,
1285+}),
1286+buildQaGatewayHeapCheckpointRuntimeEnvPatch(),
1287+),
11681288});
11691289writeQaSuiteProgress(
11701290progressEnabled,
@@ -1232,6 +1352,21 @@ export async function runQaSuite(params?: QaSuiteRunParams): Promise<QaSuiteResu
12321352};
12331353const gatewayProcessCpuStartMs = gateway.getProcessCpuMs?.() ?? null;
12341354const gatewayProcessRssStartBytes = sampleGatewayProcessRss("suite-start");
1355+const gatewayHeapSnapshots: QaSuiteGatewayHeapSnapshot[] = [];
1356+const captureGatewayHeapCheckpoint = async (label: string) => {
1357+if (!gatewayHeapCheckpointsEnabled) {
1358+return;
1359+}
1360+const snapshot = await captureGatewayHeapSnapshotCheckpoint({
1361+ gateway,
1362+ outputDir,
1363+ label,
1364+});
1365+if (snapshot) {
1366+gatewayHeapSnapshots.push(snapshot);
1367+}
1368+};
1369+await captureGatewayHeapCheckpoint("suite-start");
12351370for (const [index, scenario] of selectedCatalogScenarios.entries()) {
12361371const scenarioIdForLog = sanitizeQaSuiteProgressValue(scenario.id);
12371372writeQaSuiteProgress(
@@ -1290,6 +1425,7 @@ export async function runQaSuite(params?: QaSuiteRunParams): Promise<QaSuiteResu
12901425})
12911426 : undefined;
12921427const finishedAt = new Date();
1428+await captureGatewayHeapCheckpoint("suite-finish");
12931429const metrics = buildQaSuiteRuntimeMetrics({
12941430 startedAt,
12951431 finishedAt,
@@ -1298,6 +1434,7 @@ export async function runQaSuite(params?: QaSuiteRunParams): Promise<QaSuiteResu
12981434 gatewayProcessRssStartBytes,
12991435gatewayProcessRssEndBytes: sampleGatewayProcessRss("suite-finish"),
13001436 gatewayProcessRssSamples,
1437+ gatewayHeapSnapshots,
13011438});
13021439const failedCount = scenarios.filter((scenario) => scenario.status === "fail").length;
13031440if (scenarios.some((scenario) => scenario.status === "fail")) {
@@ -1373,8 +1510,11 @@ export async function runQaSuite(params?: QaSuiteRunParams): Promise<QaSuiteResu
13731510}
1374151113751512export const qaSuiteProgressTesting = {
1513+ appendNodeOption,
1514+ buildQaGatewayHeapCheckpointRuntimeEnvPatch,
13761515 buildQaSuiteRuntimeMetrics,
13771516 buildQaRuntimeEnvPatch,
1517+ mergeQaRuntimeEnvPatches,
13781518 parseQaSuiteBooleanEnv,
13791519 remapModelRefForForcedRuntime,
13801520 resolveQaSuiteTransportReadyTimeoutMs,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。