diff --git a/scripts/openclaw-performance-source-summary.mjs b/scripts/openclaw-performance-source-summary.mjs index 3f8465d6c109..31f092def46f 100644 --- a/scripts/openclaw-performance-source-summary.mjs +++ b/scripts/openclaw-performance-source-summary.mjs @@ -255,6 +255,12 @@ function validateExtensionMemoryArtifact(extensionMemory, filePath) { if (!Array.isArray(extensionMemory?.topByDeltaMb) || extensionMemory.topByDeltaMb.length === 0) { throw new Error(`[source-performance] missing extension memory rows: ${filePath}`); } + if ( + !finiteNumber(extensionMemory?.baseline?.maxRssMb) || + !finiteNumber(extensionMemory?.combined?.maxRssMb) + ) { + throw new Error(`[source-performance] incomplete extension memory context: ${filePath}`); + } for (const entry of extensionMemory.topByDeltaMb) { if (!finiteNumber(entry?.maxRssMb) || !finiteNumber(entry?.deltaFromBaselineMb)) { throw new Error( @@ -510,6 +516,30 @@ function buildExtensionMemoryRows(extensionMemory) { ]); } +function buildExtensionMemoryContextRows(extensionMemory) { + const baselineMb = extensionMemory?.baseline?.maxRssMb; + const combinedMb = extensionMemory?.combined?.maxRssMb; + const totalEntries = extensionMemory?.counts?.totalEntries; + return [ + [ + "empty Node process", + formatMb(baselineMb), + formatMb(0), + extensionMemory?.baseline?.status ?? "unknown", + ], + [ + finiteNumber(totalEntries) + ? `all ${totalEntries} bundled plugins` + : "all selected bundled plugins", + formatMb(combinedMb), + finiteNumber(baselineMb) && finiteNumber(combinedMb) + ? formatMb(combinedMb - baselineMb) + : "n/a", + extensionMemory?.combined?.status ?? "unknown", + ], + ]; +} + function buildSqlitePerfRows(sqlitePerf) { if (!sqlitePerf) { return []; @@ -606,8 +636,14 @@ export function buildMarkdown(sourceDir, baselineSourceDir) { ), "## Bundled Plugin Import Memory", "", + "Per-plugin rows are isolated cold imports and are not additive. The combined row measures all selected bundled-plugin entrypoints in one process.", + "", ...table( - ["plugin", "max RSS", "delta from empty process", "status"], + ["measurement", "max RSS", "delta from empty process", "status"], + buildExtensionMemoryContextRows(current.extensionMemory), + ), + ...table( + ["plugin", "isolated max RSS", "isolated delta from empty process", "status"], buildExtensionMemoryRows(current.extensionMemory), ), "## Startup Hotspots", diff --git a/test/scripts/openclaw-performance-source-summary.test.ts b/test/scripts/openclaw-performance-source-summary.test.ts index 667bc96608a2..0f012bda0910 100644 --- a/test/scripts/openclaw-performance-source-summary.test.ts +++ b/test/scripts/openclaw-performance-source-summary.test.ts @@ -73,6 +73,9 @@ function writeSourceFixture(sourceDir: string) { }, }); writeJson(path.join(sourceDir, "extension-memory.json"), { + baseline: { maxRssMb: 50, status: "ok" }, + combined: { maxRssMb: 180, status: "ok" }, + counts: { totalEntries: 12 }, topByDeltaMb: [ { dir: "extensions/browser", maxRssMb: 80, deltaFromBaselineMb: 12, status: "ok" }, ], @@ -171,6 +174,13 @@ describe("buildMarkdown", () => { expect(buildMarkdown(sourceDir, null)).not.toContain("phase.load.total"); expect(buildMarkdown(sourceDir, null)).not.toContain("phase.load.itemCount"); expect(buildMarkdown(sourceDir, null)).not.toContain("memory.ready.heapUsedMb"); + expect(buildMarkdown(sourceDir, null)).toContain( + "Per-plugin rows are isolated cold imports and are not additive.", + ); + expect(buildMarkdown(sourceDir, null)).toContain( + "| all 12 bundled plugins | 180.0MB | 130.0MB | ok |", + ); + expect(buildMarkdown(sourceDir, null)).toContain("isolated delta from empty process"); }); it("rejects a missing source directory", () => { @@ -230,6 +240,20 @@ describe("buildMarkdown", () => { ); }); + it("rejects extension memory artifacts without combined-process context", () => { + const sourceDir = mkTmpRoot(); + writeSourceFixture(sourceDir); + writeJson(path.join(sourceDir, "extension-memory.json"), { + topByDeltaMb: [ + { dir: "extensions/browser", maxRssMb: 80, deltaFromBaselineMb: 12, status: "ok" }, + ], + }); + + expect(() => buildMarkdown(sourceDir, null)).toThrow( + "[source-performance] incomplete extension memory context:", + ); + }); + it("allows source performance fixtures without older-ref SQLite smoke artifacts", () => { const sourceDir = mkTmpRoot(); writeSourceFixture(sourceDir);