| 1 | // Synthetic native A/B: identical renderer work in fresh Electron processes. |
| 2 | // This measures diagnostic overhead, not the original Windows user workload. |
| 3 | import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; |
| 4 | import { createHash } from "node:crypto"; |
| 5 | import { join, resolve } from "node:path"; |
| 6 | import { execFileSync } from "node:child_process"; |
| 7 | import { performanceFixture } from "./performance-fixture.mjs"; |
| 8 | |
| 9 | const rows = []; |
| 10 | const modes = ["off", "monitor", "capture"]; |
| 11 | const artifactDir = resolve(import.meta.dirname, "../artifacts/performance"); |
| 12 | mkdirSync(artifactDir, { recursive: true }); |
| 13 | writeFileSync(join(artifactDir, "overhead.json"), JSON.stringify({ status: "running", rows })); |
| 14 | const metricSnapshot = (app) => app.evaluate(({ app }) => app.getAppMetrics().map((m) => ({ |
| 15 | pid: m.pid, creationTime: m.creationTime, type: m.type, |
| 16 | cpuSeconds: m.cpu.cumulativeCPUUsage ?? null, |
| 17 | cpuPercent: m.cpu.percentCPUUsage, |
| 18 | workingSetMb: m.memory?.workingSetSize === undefined ? null : m.memory.workingSetSize / 1024, |
| 19 | }))); |
| 20 | const sumMemory = (metrics) => metrics.every((m) => m.workingSetMb !== null) ? metrics.reduce((sum, m) => sum + m.workingSetMb, 0) : null; |
| 21 | try { |
| 22 | for (let trial = 0; trial < 3; trial++) { |
| 23 | for (let index = 0; index < modes.length; index++) { |
| 24 | const mode = modes[(index + trial) % modes.length]; |
| 25 | const fixture = await performanceFixture(mode !== "off", { benchmark: true }); |
| 26 | try { |
| 27 | const { app, page, temp } = fixture; |
| 28 | const rendererBuildSha256 = createHash("sha256").update(readFileSync(join(temp, "assets/workload.js"))).digest("hex"); |
| 29 | await page.evaluate(() => window.diagnosticFixture.run(1000)); |
| 30 | // Measure after the real monitor's 15s startup grace, and across the |
| 31 | // native sampler's 30s tick. Short startup-only trials miss both costs. |
| 32 | await page.waitForFunction(() => performance.now() >= 16_000); |
| 33 | const before = await metricSnapshot(app); |
| 34 | const started = performance.now(); |
| 35 | const [work, profile] = await Promise.all([ |
| 36 | page.evaluate(() => window.diagnosticFixture.run(16_000)), |
| 37 | mode === "capture" ? page.evaluate(() => window.reasonixDesktop.native.captureRendererProfile()) : Promise.resolve(null), |
| 38 | ]); |
| 39 | const elapsedMs = performance.now() - started; |
| 40 | const after = await metricSnapshot(app); |
| 41 | if (mode === "capture" && profile.status !== "captured") throw new Error(`capture was ${profile.status}; invalid benchmark trial`); |
| 42 | const cpuSeconds = after.every((m) => m.cpuSeconds !== null) && before.every((m) => m.cpuSeconds !== null) |
| 43 | ? after.reduce((sum, m) => sum + Math.max(0, m.cpuSeconds - (before.find((b) => b.pid === m.pid && b.creationTime === m.creationTime)?.cpuSeconds ?? 0)), 0) : null; |
| 44 | const metricCost = await app.evaluate(({ app }) => { |
| 45 | const samples = []; |
| 46 | for (let i = 0; i < 30; i++) { const start = performance.now(); app.getAppMetrics(); samples.push(performance.now() - start); } |
| 47 | samples.sort((a, b) => a - b); |
| 48 | return { p95Ms: samples[Math.floor(samples.length * .95)], maxMs: samples[samples.length - 1] }; |
| 49 | }); |
| 50 | const row = { trial, mode, rendererBuildSha256, ...work, elapsedMs, cpuSeconds, workingSetBeforeMb: sumMemory(before), workingSetAfterMb: sumMemory(after), metricCost, profileStatus: profile?.status ?? "off" }; |
| 51 | rows.push(row); |
| 52 | console.log(JSON.stringify(row)); |
| 53 | } finally { await fixture.close(); } |
| 54 | } |
| 55 | } |
| 56 | if (new Set(rows.map((row) => row.rendererBuildSha256)).size !== 1) throw new Error("renderer bundles differ between benchmark modes"); |
| 57 | } catch (error) { |
| 58 | writeFileSync(join(artifactDir, "overhead.json"), JSON.stringify({ status: "failed", rows, failure: String(error) }, null, 2)); |
| 59 | throw error; |
| 60 | } |
| 61 | const median = (values) => values.filter((v) => v !== null).sort((a, b) => a - b)[Math.floor(values.length / 2)] ?? null; |
| 62 | const summary = modes.map((mode) => { |
| 63 | const subset = rows.filter((r) => r.mode === mode); |
| 64 | return { mode, frames: median(subset.map((r) => r.frames)), frameP95Ms: median(subset.map((r) => r.frameP95Ms)), cpuSeconds: median(subset.map((r) => r.cpuSeconds)), workingSetAfterMb: median(subset.map((r) => r.workingSetAfterMb)), metricP95Ms: median(subset.map((r) => r.metricCost.p95Ms)) }; |
| 65 | }); |
| 66 | writeFileSync(join(artifactDir, "overhead.json"), JSON.stringify({ |
| 67 | status: "completed", |
| 68 | platform: process.platform, arch: process.arch, |
| 69 | sourceHead: execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8", cwd: resolve(import.meta.dirname, "..") }).trim(), |
| 70 | sourceStatus: execFileSync("git", ["status", "--short", "--", "src", "scripts", "../frontend/src"], { encoding: "utf8", cwd: resolve(import.meta.dirname, "..") }).trim(), |
| 71 | scope: "Synthetic Electron workload with controlled foreground signals and background throttling disabled; three fresh-process trials per mode; heap snapshots excluded; production activity lifecycle separately tested by native smoke", rows, summary, |
| 72 | }, null, 2)); |
| 73 | console.log(JSON.stringify({ summary })); |
| 74 |