perfshield 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,217 @@
1
+ import { createInterface } from "node:readline";
2
+ import { pathToFileURL } from "node:url";
3
+ const toBenchmarkFn = (value, label) => {
4
+ if (typeof value !== "function") {
5
+ throw new Error(`${label} must be a function.`);
6
+ }
7
+ return (...args) => {
8
+ return value(...args);
9
+ };
10
+ };
11
+ const ensureBenchmarkSpec = (spec, label) => {
12
+ if (spec == null || typeof spec !== "object" || Array.isArray(spec)) {
13
+ throw new Error(`${label} must be an object.`);
14
+ }
15
+ const candidate = spec;
16
+ const name = candidate.name;
17
+ if (typeof name !== "string" || name.length === 0) {
18
+ throw new Error(`${label}.name must be a non-empty string.`);
19
+ }
20
+ const fn = toBenchmarkFn(candidate.fn, `${label}.fn`);
21
+ const setup = candidate.setup == null ? undefined : toBenchmarkFn(candidate.setup, `${label}.setup`);
22
+ const teardown = candidate.teardown == null ? undefined : toBenchmarkFn(candidate.teardown, `${label}.teardown`);
23
+ let unit;
24
+ if (candidate.unit != null) {
25
+ if (typeof candidate.unit !== "string") {
26
+ throw new Error(`${label}.unit must be a string.`);
27
+ }
28
+ unit = candidate.unit;
29
+ }
30
+ let iterations;
31
+ if (candidate.iterations != null) {
32
+ if (typeof candidate.iterations !== "number") {
33
+ throw new Error(`${label}.iterations must be a number.`);
34
+ }
35
+ iterations = candidate.iterations;
36
+ }
37
+ let metadata;
38
+ if (candidate.metadata != null) {
39
+ if (typeof candidate.metadata !== "object" || Array.isArray(candidate.metadata)) {
40
+ throw new Error(`${label}.metadata must be an object.`);
41
+ }
42
+ metadata = candidate.metadata;
43
+ }
44
+ return {
45
+ fn,
46
+ iterations,
47
+ metadata,
48
+ name,
49
+ setup,
50
+ teardown,
51
+ unit
52
+ };
53
+ };
54
+ const loadBenchmarkModule = async modulePath => {
55
+ const url = pathToFileURL(modulePath).href;
56
+ const loaded = await import(url);
57
+ const benchmarks = loaded.benchmarks;
58
+ if (!Array.isArray(benchmarks)) {
59
+ throw new Error("Benchmark module must export a benchmarks array.");
60
+ }
61
+ return benchmarks;
62
+ };
63
+ const normalizeBenchmarks = (benchmarks, label) => benchmarks.map((spec, index) => ensureBenchmarkSpec(spec, `${label}.benchmarks[${index}]`));
64
+ const describeBenchmarks = benchmarks => benchmarks.map(benchmark => ({
65
+ iterations: benchmark.iterations,
66
+ metadata: benchmark.metadata,
67
+ name: benchmark.name,
68
+ unit: benchmark.unit
69
+ }));
70
+ const validateBenchmarkPairs = (baseline, current) => {
71
+ if (baseline.length !== current.length) {
72
+ throw new Error(`Benchmark count mismatch: baseline=${baseline.length}, current=${current.length}`);
73
+ }
74
+ baseline.forEach((benchmark, index) => {
75
+ if (benchmark.name !== current[index].name) {
76
+ throw new Error(`Benchmark name mismatch at index ${index}: ` + `${benchmark.name} vs ${current[index].name}`);
77
+ }
78
+ });
79
+ };
80
+ const runBenchmarkSample = async (benchmark, iterationsOverride) => {
81
+ const iterations = Math.max(1, iterationsOverride ?? benchmark.iterations ?? 1);
82
+ let setupCompleted = false;
83
+ if (benchmark.setup) {
84
+ await benchmark.setup();
85
+ setupCompleted = true;
86
+ }
87
+ const start = process.hrtime.bigint();
88
+ try {
89
+ for (let i = 0; i < iterations; i += 1) {
90
+ await benchmark.fn();
91
+ }
92
+ } finally {
93
+ if (benchmark.teardown && setupCompleted) {
94
+ await benchmark.teardown();
95
+ }
96
+ }
97
+ const end = process.hrtime.bigint();
98
+ const durationMs = Number(end - start) / 1e6;
99
+ return durationMs;
100
+ };
101
+ const state = {
102
+ current: null
103
+ };
104
+ const handleInit = async message => {
105
+ const baselinePath = message.baselinePath;
106
+ const currentPath = message.currentPath;
107
+ if (baselinePath == null || currentPath == null) {
108
+ throw new Error("init requires baselinePath and currentPath.");
109
+ }
110
+ const baselineBenchmarksRaw = await loadBenchmarkModule(baselinePath);
111
+ const currentBenchmarksRaw = await loadBenchmarkModule(currentPath);
112
+ const baselineBenchmarks = normalizeBenchmarks(baselineBenchmarksRaw, "baseline");
113
+ const currentBenchmarks = normalizeBenchmarks(currentBenchmarksRaw, "current");
114
+ validateBenchmarkPairs(baselineBenchmarks, currentBenchmarks);
115
+ state.current = {
116
+ baseline: baselineBenchmarks,
117
+ current: currentBenchmarks
118
+ };
119
+ return {
120
+ ok: true
121
+ };
122
+ };
123
+ const handleList = () => {
124
+ if (!state.current) {
125
+ throw new Error("Harness not initialized.");
126
+ }
127
+ return {
128
+ benchmarks: describeBenchmarks(state.current.baseline)
129
+ };
130
+ };
131
+ const handleRun = async message => {
132
+ const currentState = state.current;
133
+ if (!currentState) {
134
+ throw new Error("Harness not initialized.");
135
+ }
136
+ const version = message.version;
137
+ const index = message.index;
138
+ if (version !== "baseline" && version !== "current") {
139
+ throw new Error("run requires version = baseline|current.");
140
+ }
141
+ if (typeof index !== "number" || Number.isNaN(index)) {
142
+ throw new Error("run requires a numeric benchmark index.");
143
+ }
144
+ const list = currentState[version];
145
+ const benchmark = list[index];
146
+ if (!benchmark) {
147
+ throw new Error(`Unknown benchmark index ${index}.`);
148
+ }
149
+ const durationMs = await runBenchmarkSample(benchmark, message.iterations);
150
+ return {
151
+ durationMs
152
+ };
153
+ };
154
+ const handleMessage = async message => {
155
+ switch (message.type) {
156
+ case "init":
157
+ return await handleInit(message);
158
+ case "list":
159
+ return handleList();
160
+ case "run":
161
+ return await handleRun(message);
162
+ case "close":
163
+ return {
164
+ ok: true
165
+ };
166
+ default:
167
+ throw new Error(`Unknown message type: ${String(message.type)}`);
168
+ }
169
+ };
170
+ const sendResponse = payload => {
171
+ const serialized = JSON.stringify(payload);
172
+ if (serialized == null) {
173
+ return;
174
+ }
175
+ process.stdout.write(`${serialized}\n`);
176
+ };
177
+ const rl = createInterface({
178
+ crlfDelay: Infinity,
179
+ input: process.stdin
180
+ });
181
+ rl.on("line", line => {
182
+ if (!line.trim()) {
183
+ return;
184
+ }
185
+ let message;
186
+ try {
187
+ message = JSON.parse(line);
188
+ } catch {
189
+ sendResponse({
190
+ error: {
191
+ message: "Invalid JSON payload."
192
+ },
193
+ id: null,
194
+ ok: false
195
+ });
196
+ return;
197
+ }
198
+ Promise.resolve(handleMessage(message)).then(result => {
199
+ sendResponse({
200
+ id: message.id,
201
+ ok: true,
202
+ ...result
203
+ });
204
+ if (message.type === "close") {
205
+ rl.close();
206
+ process.exit(0);
207
+ }
208
+ }).catch(error => {
209
+ sendResponse({
210
+ error: {
211
+ message: error instanceof Error ? error.message : String(error)
212
+ },
213
+ id: message.id,
214
+ ok: false
215
+ });
216
+ });
217
+ });
@@ -0,0 +1,214 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createInterface } from "node:readline";
3
+ const coerceBenchmarkDescriptor = (value, index) => {
4
+ if (value == null || typeof value !== "object" || Array.isArray(value)) {
5
+ throw new Error(`Benchmark descriptor ${index} must be an object.`);
6
+ }
7
+ const descriptor = value;
8
+ const name = descriptor.name;
9
+ if (typeof name !== "string" || name.length === 0) {
10
+ throw new Error(`Benchmark descriptor ${index}.name is required.`);
11
+ }
12
+ let unit;
13
+ if (descriptor.unit != null) {
14
+ if (typeof descriptor.unit !== "string") {
15
+ throw new Error(`Benchmark descriptor ${index}.unit must be a string.`);
16
+ }
17
+ unit = descriptor.unit;
18
+ }
19
+ let iterations;
20
+ if (descriptor.iterations != null) {
21
+ if (typeof descriptor.iterations !== "number") {
22
+ throw new Error(`Benchmark descriptor ${index}.iterations must be a number.`);
23
+ }
24
+ iterations = descriptor.iterations;
25
+ }
26
+ let metadata;
27
+ if (descriptor.metadata != null) {
28
+ if (typeof descriptor.metadata !== "object" || Array.isArray(descriptor.metadata)) {
29
+ throw new Error(`Benchmark descriptor ${index}.metadata must be an object.`);
30
+ }
31
+ metadata = descriptor.metadata;
32
+ }
33
+ return {
34
+ iterations,
35
+ metadata,
36
+ name,
37
+ unit
38
+ };
39
+ };
40
+ export const runNodeScript = async (engine, scriptPath, args = []) => {
41
+ const command = engine.command || "node";
42
+ const commandArgs = [...(engine.args ?? []), scriptPath, ...args];
43
+ return await new Promise((resolvePromise, reject) => {
44
+ const child = spawn(command, commandArgs, {
45
+ env: {
46
+ ...process.env,
47
+ ...(engine.env ?? {})
48
+ },
49
+ stdio: ["ignore", "pipe", "pipe"]
50
+ });
51
+ let stdout = "";
52
+ let stderr = "";
53
+ child.stdout.on("data", chunk => {
54
+ stdout += String(chunk);
55
+ });
56
+ child.stderr.on("data", chunk => {
57
+ stderr += String(chunk);
58
+ });
59
+ child.on("error", error => {
60
+ reject(error);
61
+ });
62
+ child.on("close", code => {
63
+ resolvePromise({
64
+ exitCode: code ?? 0,
65
+ stderr,
66
+ stdout
67
+ });
68
+ });
69
+ });
70
+ };
71
+ export const createNodeHarness = async (engine, harnessPath, baselinePath, currentPath) => {
72
+ const command = engine.command || "node";
73
+ const args = [...(engine.args ?? []), harnessPath];
74
+ const child = spawn(command, args, {
75
+ env: {
76
+ ...process.env,
77
+ ...(engine.env ?? {})
78
+ },
79
+ stdio: ["pipe", "pipe", "pipe"]
80
+ });
81
+ let closing = false;
82
+ let nextId = 1;
83
+ const pending = new Map();
84
+ let stderrBuffer = "";
85
+ const rejectAll = error => {
86
+ for (const request of pending.values()) {
87
+ request.reject(error);
88
+ }
89
+ pending.clear();
90
+ };
91
+ child.stderr.on("data", chunk => {
92
+ stderrBuffer += String(chunk);
93
+ });
94
+ child.on("exit", code => {
95
+ if (!closing) {
96
+ const message = stderrBuffer.trim();
97
+ const error = new Error(`Harness exited unexpectedly (code ${code ?? "unknown"}).` + (message ? ` ${message}` : ""));
98
+ rejectAll(error);
99
+ }
100
+ });
101
+ const rl = createInterface({
102
+ crlfDelay: Infinity,
103
+ input: child.stdout
104
+ });
105
+ rl.on("line", line => {
106
+ if (!line.trim()) {
107
+ return;
108
+ }
109
+ let payload;
110
+ try {
111
+ payload = JSON.parse(line);
112
+ } catch {
113
+ return;
114
+ }
115
+ if (payload == null || typeof payload.id !== "number") {
116
+ return;
117
+ }
118
+ const request = pending.get(payload.id);
119
+ if (!request) {
120
+ return;
121
+ }
122
+ pending.delete(payload.id);
123
+ if (payload.ok === false) {
124
+ const message = payload.error && typeof payload.error.message === "string" ? payload.error.message : "Harness error.";
125
+ request.reject(new Error(message));
126
+ return;
127
+ }
128
+ request.resolve(payload);
129
+ });
130
+ const send = async (type, body) => new Promise((resolve, reject) => {
131
+ const id = nextId;
132
+ nextId += 1;
133
+ pending.set(id, {
134
+ reject,
135
+ resolve
136
+ });
137
+ const payloadObject = {
138
+ id,
139
+ type
140
+ };
141
+ if (body != null) {
142
+ if (typeof body !== "object" || Array.isArray(body)) {
143
+ pending.delete(id);
144
+ reject(new Error("Harness payload must be an object."));
145
+ return;
146
+ }
147
+ for (const [key, value] of Object.entries(body)) {
148
+ payloadObject[key] = value;
149
+ }
150
+ }
151
+ const payload = JSON.stringify(payloadObject);
152
+ try {
153
+ if (!child.stdin.write(`${payload}\n`)) {
154
+ child.stdin.once("drain", () => {});
155
+ }
156
+ } catch (error) {
157
+ pending.delete(id);
158
+ reject(error instanceof Error ? error : new Error(String(error)));
159
+ }
160
+ });
161
+ try {
162
+ await send("init", {
163
+ baselinePath,
164
+ currentPath
165
+ });
166
+ } catch (error) {
167
+ closing = true;
168
+ child.kill();
169
+ throw error;
170
+ }
171
+ const listBenchmarks = async () => {
172
+ const response = await send("list");
173
+ if (response == null || typeof response !== "object") {
174
+ throw new Error("Invalid harness list response.");
175
+ }
176
+ const responseObject = response;
177
+ const benchmarks = responseObject.benchmarks;
178
+ if (!Array.isArray(benchmarks)) {
179
+ throw new Error("Harness list response missing benchmarks.");
180
+ }
181
+ return benchmarks.map(coerceBenchmarkDescriptor);
182
+ };
183
+ const runSample = async options => {
184
+ const payload = {
185
+ index: options.index,
186
+ iterations: options.iterations,
187
+ version: options.version
188
+ };
189
+ const response = await send("run", payload);
190
+ if (response == null || typeof response !== "object") {
191
+ throw new Error("Invalid harness run response.");
192
+ }
193
+ const durationMs = response.durationMs;
194
+ if (typeof durationMs !== "number") {
195
+ throw new Error("Harness run response missing durationMs.");
196
+ }
197
+ return {
198
+ durationMs
199
+ };
200
+ };
201
+ const close = async () => {
202
+ closing = true;
203
+ try {
204
+ await send("close");
205
+ } finally {
206
+ child.stdin.end();
207
+ }
208
+ };
209
+ return {
210
+ close,
211
+ listBenchmarks,
212
+ runSample
213
+ };
214
+ };
@@ -0,0 +1,17 @@
1
+ const isPositiveInterval = interval => interval.low > 0 && interval.high > 0;
2
+ export const getRegressions = results => {
3
+ const findings = [];
4
+ for (const result of results) {
5
+ for (const entry of result.benchmarks) {
6
+ if (isPositiveInterval(entry.difference.absolute.ci) || isPositiveInterval(entry.difference.relative.ci)) {
7
+ findings.push({
8
+ absolute: entry.difference.absolute.ci,
9
+ benchmark: entry.benchmark.name,
10
+ engine: result.engine.name,
11
+ relative: entry.difference.relative.ci
12
+ });
13
+ }
14
+ }
15
+ }
16
+ return findings;
17
+ };
@@ -0,0 +1,25 @@
1
+ const formatNumber = (value, decimals) => {
2
+ if (!Number.isFinite(value)) {
3
+ return String(value);
4
+ }
5
+ return value.toFixed(decimals);
6
+ };
7
+ const formatInterval = (interval, decimals, suffix = "") => `[${formatNumber(interval.low, decimals)}, ${formatNumber(interval.high, decimals)}]${suffix}`;
8
+ const formatRelativeInterval = (interval, decimals) => formatInterval({
9
+ high: interval.high * 100,
10
+ low: interval.low * 100
11
+ }, decimals, "%");
12
+ const formatRelativeValue = (value, decimals) => `${formatNumber(value * 100, decimals)}%`;
13
+ export const renderConsoleReport = results => {
14
+ const lines = [];
15
+ for (const result of results) {
16
+ lines.push(`Engine: ${result.engine.name}`);
17
+ for (const entry of result.benchmarks) {
18
+ const unit = entry.benchmark.unit != null ? ` ${entry.benchmark.unit}` : "";
19
+ const benchmarkLines = [` Benchmark: ${entry.benchmark.name}`, ` baseline mean=${formatNumber(entry.stats.baseline.mean, 4)}${unit} ci=${formatInterval(entry.stats.baseline.meanCI, 4)} sd=${formatNumber(entry.stats.baseline.standardDeviation, 4)}`, ` current mean=${formatNumber(entry.stats.current.mean, 4)}${unit} ci=${formatInterval(entry.stats.current.meanCI, 4)} sd=${formatNumber(entry.stats.current.standardDeviation, 4)}`, ` diff abs mean=${formatNumber(entry.difference.absolute.mean, 4)}${unit} ci=${formatInterval(entry.difference.absolute.ci, 4)}`, ` diff rel mean=${formatRelativeValue(entry.difference.relative.mean, 2)} ci=${formatRelativeInterval(entry.difference.relative.ci, 2)}`];
20
+ lines.push(...benchmarkLines);
21
+ }
22
+ lines.push("");
23
+ }
24
+ return lines.join("\n").trimEnd();
25
+ };
@@ -0,0 +1,13 @@
1
+ import { renderConsoleReport } from "./console.js";
2
+ import { renderJsonReport } from "./json.js";
3
+ export const renderReports = (results, formats) => {
4
+ const outputs = [];
5
+ for (const format of formats) {
6
+ if (format === "console") {
7
+ outputs.push(renderConsoleReport(results));
8
+ } else if (format === "json") {
9
+ outputs.push(renderJsonReport(results));
10
+ }
11
+ }
12
+ return outputs;
13
+ };
@@ -0,0 +1,12 @@
1
+ export const buildJsonReport = results => ({
2
+ engines: results.map(result => ({
3
+ benchmarks: result.benchmarks.map(entry => ({
4
+ benchmark: entry.benchmark,
5
+ difference: entry.difference,
6
+ samples: entry.samples,
7
+ stats: entry.stats
8
+ })),
9
+ engine: result.engine
10
+ }))
11
+ });
12
+ export const renderJsonReport = results => JSON.stringify(buildJsonReport(results), null, 2);
package/lib/runner.js ADDED
@@ -0,0 +1,177 @@
1
+ import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { transformFileAsync } from "@babel/core";
6
+ import { createNodeHarness } from "./engines/node.js";
7
+ import { computeDifference, summaryStats } from "./stats.js";
8
+ const versions = ["baseline", "current"];
9
+ const autoSampleBatchSize = 10;
10
+ const harnessTempPrefix = "perfshield-harness-";
11
+ const getHarnessPath = () => {
12
+ const override = process.env.WEB_BENCHMARKER_HARNESS_PATH;
13
+ if (override != null) {
14
+ return override;
15
+ }
16
+ return fileURLToPath(new URL("./engines/node-harness.js", import.meta.url).toString());
17
+ };
18
+ const hasBabelConfig = async () => {
19
+ const configPath = resolve(process.cwd(), "babel.config.cjs");
20
+ try {
21
+ await access(configPath);
22
+ return true;
23
+ } catch {
24
+ return false;
25
+ }
26
+ };
27
+ const buildHarnessIfNeeded = async sourcePath => {
28
+ const contents = await readFile(sourcePath, "utf8");
29
+ if (!contents.includes("import type") && !contents.includes("@flow")) {
30
+ return {
31
+ cleanup: null,
32
+ path: sourcePath
33
+ };
34
+ }
35
+ const usesConfig = await hasBabelConfig();
36
+ const result = await transformFileAsync(sourcePath, {
37
+ configFile: usesConfig ? resolve(process.cwd(), "babel.config.cjs") : false,
38
+ presets: usesConfig ? [] : ["@babel/preset-flow"]
39
+ });
40
+ if (!result || !result.code) {
41
+ throw new Error("Failed to compile node harness.");
42
+ }
43
+ const dir = await mkdtemp(join(tmpdir(), harnessTempPrefix));
44
+ const harnessPath = join(dir, "node-harness.js");
45
+ await writeFile(harnessPath, result.code, "utf8");
46
+ return {
47
+ cleanup: async () => {
48
+ await rm(dir, {
49
+ force: true,
50
+ recursive: true
51
+ });
52
+ },
53
+ path: harnessPath
54
+ };
55
+ };
56
+ const warmupBenchmarks = async (harness, benchmarks) => {
57
+ for (let index = 0; index < benchmarks.length; index += 1) {
58
+ const descriptor = benchmarks[index];
59
+ for (const version of versions) {
60
+ await harness.runSample({
61
+ index,
62
+ iterations: descriptor.iterations,
63
+ version
64
+ });
65
+ }
66
+ }
67
+ };
68
+ const collectSamples = async (harness, benchmarks, minSamples) => {
69
+ const samples = benchmarks.map(() => ({
70
+ baseline: [],
71
+ current: []
72
+ }));
73
+ for (let iteration = 0; iteration < minSamples; iteration += 1) {
74
+ for (let index = 0; index < benchmarks.length; index += 1) {
75
+ const descriptor = benchmarks[index];
76
+ for (const version of versions) {
77
+ const result = await harness.runSample({
78
+ index,
79
+ iterations: descriptor.iterations,
80
+ version
81
+ });
82
+ if (version === "baseline") {
83
+ samples[index].baseline.push(result.durationMs);
84
+ } else {
85
+ samples[index].current.push(result.durationMs);
86
+ }
87
+ }
88
+ }
89
+ }
90
+ return samples;
91
+ };
92
+ const intervalContains = (interval, value) => interval.low <= value && value <= interval.high;
93
+ const autoSampleResolved = (samples, conditions) => samples.every(bucket => {
94
+ const baselineStats = summaryStats(bucket.baseline);
95
+ const currentStats = summaryStats(bucket.current);
96
+ const diff = computeDifference(baselineStats, currentStats);
97
+ for (const condition of conditions.absolute) {
98
+ if (intervalContains(diff.absolute.ci, condition)) {
99
+ return false;
100
+ }
101
+ }
102
+ for (const condition of conditions.relative) {
103
+ if (intervalContains(diff.relative.ci, condition)) {
104
+ return false;
105
+ }
106
+ }
107
+ return true;
108
+ });
109
+ const autoSample = async (harness, benchmarks, samples, conditions, timeoutMs) => {
110
+ const startTime = Date.now();
111
+ while (Date.now() - startTime < timeoutMs) {
112
+ if (autoSampleResolved(samples, conditions)) {
113
+ return;
114
+ }
115
+ for (let batch = 0; batch < autoSampleBatchSize; batch += 1) {
116
+ for (let index = 0; index < benchmarks.length; index += 1) {
117
+ const descriptor = benchmarks[index];
118
+ for (const version of versions) {
119
+ const result = await harness.runSample({
120
+ index,
121
+ iterations: descriptor.iterations,
122
+ version
123
+ });
124
+ if (version === "baseline") {
125
+ samples[index].baseline.push(result.durationMs);
126
+ } else {
127
+ samples[index].current.push(result.durationMs);
128
+ }
129
+ }
130
+ }
131
+ }
132
+ }
133
+ };
134
+ export const runEngineComparison = async options => {
135
+ const {
136
+ baselinePath,
137
+ config,
138
+ currentPath,
139
+ engine
140
+ } = options;
141
+ const harnessArtifact = await buildHarnessIfNeeded(getHarnessPath());
142
+ const harness = await createNodeHarness(engine, harnessArtifact.path, resolve(baselinePath), resolve(currentPath));
143
+ try {
144
+ const benchmarks = await harness.listBenchmarks();
145
+ await warmupBenchmarks(harness, benchmarks);
146
+ const samples = await collectSamples(harness, benchmarks, config.sampling.minSamples);
147
+ await autoSample(harness, benchmarks, samples, config.sampling.conditions, config.sampling.timeoutMs);
148
+ const benchmarkResults = benchmarks.map((benchmark, index) => {
149
+ const baselineSamples = samples[index].baseline;
150
+ const currentSamples = samples[index].current;
151
+ const baselineStats = summaryStats(baselineSamples);
152
+ const currentStats = summaryStats(currentSamples);
153
+ const difference = computeDifference(baselineStats, currentStats);
154
+ return {
155
+ benchmark,
156
+ difference,
157
+ samples: {
158
+ baseline: baselineSamples,
159
+ current: currentSamples
160
+ },
161
+ stats: {
162
+ baseline: baselineStats,
163
+ current: currentStats
164
+ }
165
+ };
166
+ });
167
+ return {
168
+ benchmarks: benchmarkResults,
169
+ engine
170
+ };
171
+ } finally {
172
+ await harness.close();
173
+ if (harnessArtifact.cleanup) {
174
+ await harnessArtifact.cleanup();
175
+ }
176
+ }
177
+ };