benchforge 0.1.3 → 0.1.6

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.
@@ -1,4 +1,5 @@
1
1
  import { a as createAdaptiveWrapper, c as BasicRunner, i as createRunner, l as computeStats, n as getElapsed, o as average, r as getPerfNow, s as bootstrapDifferenceCI, t as debugWorkerTiming, u as discoverVariants } from "./TimingUtils-ClclVQ7E.mjs";
2
+ import { n as parseGcLine, t as aggregateGcStats } from "./GcStats-ByEovUi1.mjs";
2
3
  import { mkdir, readFile, writeFile } from "node:fs/promises";
3
4
  import { fileURLToPath } from "node:url";
4
5
  import { execSync, fork, spawn } from "node:child_process";
@@ -8,7 +9,6 @@ import pico from "picocolors";
8
9
  import { table } from "table";
9
10
  import yargs from "yargs";
10
11
  import { hideBin } from "yargs/helpers";
11
- import { chromium } from "playwright";
12
12
  import { createServer } from "node:http";
13
13
  import open from "open";
14
14
 
@@ -30,81 +30,6 @@ async function loadCaseData(casesModule, caseId) {
30
30
  return { data: caseId };
31
31
  }
32
32
 
33
- //#endregion
34
- //#region src/runners/GcStats.ts
35
- /** Parse a single --trace-gc-nvp stderr line */
36
- function parseGcLine(line) {
37
- if (!line.includes("pause=")) return void 0;
38
- const fields = parseNvpFields(line);
39
- if (!fields.gc) return void 0;
40
- const int = (k) => Number.parseInt(fields[k] || "0", 10);
41
- const type = parseGcType(fields.gc);
42
- const pauseMs = Number.parseFloat(fields.pause || "0");
43
- const allocated = int("allocated");
44
- const promoted = int("promoted");
45
- const survived = int("new_space_survived") || int("survived");
46
- const startSize = int("start_object_size");
47
- const endSize = int("end_object_size");
48
- const collected = startSize > endSize ? startSize - endSize : 0;
49
- if (Number.isNaN(pauseMs)) return void 0;
50
- return {
51
- type,
52
- pauseMs,
53
- allocated,
54
- collected,
55
- promoted,
56
- survived
57
- };
58
- }
59
- /** Parse name=value pairs from trace-gc-nvp line */
60
- function parseNvpFields(line) {
61
- const fields = {};
62
- const matches = line.matchAll(/(\w+)=([^\s,]+)/g);
63
- for (const [, key, value] of matches) fields[key] = value;
64
- return fields;
65
- }
66
- /** Map V8 gc type codes to our types */
67
- function parseGcType(gcField) {
68
- if (gcField === "s" || gcField === "scavenge") return "scavenge";
69
- if (gcField === "mc" || gcField === "ms" || gcField === "mark-compact") return "mark-compact";
70
- if (gcField === "mmc" || gcField === "minor-mc" || gcField === "minor-ms") return "minor-ms";
71
- return "unknown";
72
- }
73
- /** Aggregate GC events into summary stats */
74
- function aggregateGcStats(events) {
75
- let scavenges = 0;
76
- let markCompacts = 0;
77
- let gcPauseTime = 0;
78
- let totalCollected = 0;
79
- let hasNodeFields = false;
80
- let totalAllocated = 0;
81
- let totalPromoted = 0;
82
- let totalSurvived = 0;
83
- for (const e of events) {
84
- if (e.type === "scavenge" || e.type === "minor-ms") scavenges++;
85
- else if (e.type === "mark-compact") markCompacts++;
86
- gcPauseTime += e.pauseMs;
87
- totalCollected += e.collected;
88
- if (e.allocated != null) {
89
- hasNodeFields = true;
90
- totalAllocated += e.allocated;
91
- totalPromoted += e.promoted ?? 0;
92
- totalSurvived += e.survived ?? 0;
93
- }
94
- }
95
- return {
96
- scavenges,
97
- markCompacts,
98
- totalCollected,
99
- gcPauseTime,
100
- ...hasNodeFields && {
101
- totalAllocated,
102
- totalPromoted,
103
- totalSurvived
104
- }
105
- };
106
- }
107
-
108
33
  //#endregion
109
34
  //#region src/runners/RunnerOrchestrator.ts
110
35
  const logTiming = debugWorkerTiming ? (message) => console.log(`[RunnerOrchestrator] ${message}`) : () => {};
@@ -1012,8 +937,9 @@ const cliOptions = {
1012
937
  },
1013
938
  "chrome-args": {
1014
939
  type: "string",
940
+ array: true,
1015
941
  requiresArg: true,
1016
- describe: "extra Chromium flags (space-separated)"
942
+ describe: "extra Chromium flags"
1017
943
  }
1018
944
  };
1019
945
  /** @return yargs with standard benchmark options */
@@ -1025,188 +951,6 @@ function parseCliArgs(args, configure = defaultCliArgs) {
1025
951
  return configure(yargs(args)).parseSync();
1026
952
  }
1027
953
 
1028
- //#endregion
1029
- //#region src/browser/BrowserGcStats.ts
1030
- /** Parse CDP trace events (MinorGC/MajorGC) into GcEvent[] */
1031
- function parseGcTraceEvents(traceEvents) {
1032
- return traceEvents.flatMap((e) => {
1033
- if (e.ph !== "X") return [];
1034
- const type = gcType(e.name);
1035
- if (!type) return [];
1036
- const durUs = e.dur ?? 0;
1037
- const heapBefore = e.args?.usedHeapSizeBefore ?? 0;
1038
- const heapAfter = e.args?.usedHeapSizeAfter ?? 0;
1039
- return [{
1040
- type,
1041
- pauseMs: durUs / 1e3,
1042
- collected: Math.max(0, heapBefore - heapAfter)
1043
- }];
1044
- });
1045
- }
1046
- function gcType(name) {
1047
- if (name === "MinorGC") return "scavenge";
1048
- if (name === "MajorGC") return "mark-compact";
1049
- }
1050
- /** Parse CDP trace events and aggregate into GcStats */
1051
- function browserGcStats(traceEvents) {
1052
- return aggregateGcStats(parseGcTraceEvents(traceEvents));
1053
- }
1054
-
1055
- //#endregion
1056
- //#region src/browser/BrowserHeapSampler.ts
1057
- /** Run browser benchmark, auto-detecting page API mode.
1058
- * Bench function (window.__bench): CLI controls iteration and timing.
1059
- * Lap mode (__start/__lap/__done): page controls the measured region. */
1060
- async function profileBrowser(params) {
1061
- const { url, headless = true, chromeArgs, timeout = 60 } = params;
1062
- const { gcStats: collectGc } = params;
1063
- const { samplingInterval = 32768 } = params.heapOptions ?? {};
1064
- const browser = await chromium.launch({
1065
- headless,
1066
- args: chromeArgs
1067
- });
1068
- try {
1069
- const page = await browser.newPage();
1070
- page.setDefaultTimeout(timeout * 1e3);
1071
- const cdp = await page.context().newCDPSession(page);
1072
- const pageErrors = [];
1073
- page.on("pageerror", (err) => pageErrors.push(err.message));
1074
- const traceEvents = collectGc ? await startGcTracing(cdp) : [];
1075
- const lapMode = await setupLapMode(page, cdp, params, samplingInterval, timeout, pageErrors);
1076
- await page.goto(url, { waitUntil: "load" });
1077
- const hasBench = await page.evaluate(() => typeof globalThis.__bench === "function");
1078
- let result;
1079
- if (hasBench) {
1080
- lapMode.cancel();
1081
- lapMode.promise.catch(() => {});
1082
- result = await runBenchLoop(page, cdp, params, samplingInterval);
1083
- } else {
1084
- result = await lapMode.promise;
1085
- lapMode.cancel();
1086
- }
1087
- if (collectGc) result = {
1088
- ...result,
1089
- gcStats: await collectTracing(cdp, traceEvents)
1090
- };
1091
- return result;
1092
- } finally {
1093
- await browser.close();
1094
- }
1095
- }
1096
- /** Inject __start/__lap as in-page functions, expose __done for results collection.
1097
- * __start/__lap are pure in-page (zero CDP overhead). First __start() triggers
1098
- * instrument start. __done() stops instruments and collects timing data. */
1099
- async function setupLapMode(page, cdp, params, samplingInterval, timeout, pageErrors) {
1100
- const { heapSample } = params;
1101
- const { promise, resolve, reject } = Promise.withResolvers();
1102
- let instrumentsStarted = false;
1103
- await page.exposeFunction("__benchInstrumentStart", async () => {
1104
- if (instrumentsStarted) return;
1105
- instrumentsStarted = true;
1106
- if (heapSample) await cdp.send("HeapProfiler.startSampling", heapSamplingParams(samplingInterval));
1107
- });
1108
- await page.exposeFunction("__benchCollect", async (samples, wallTimeMs) => {
1109
- let heapProfile;
1110
- if (heapSample && instrumentsStarted) heapProfile = (await cdp.send("HeapProfiler.stopSampling")).profile;
1111
- resolve({
1112
- samples,
1113
- heapProfile,
1114
- wallTimeMs
1115
- });
1116
- });
1117
- await page.addInitScript(injectLapFunctions);
1118
- const timer = setTimeout(() => {
1119
- const lines = [`Timed out after ${timeout}s`];
1120
- if (pageErrors.length) lines.push("Page JS errors:", ...pageErrors.map((e) => ` ${e}`));
1121
- else lines.push("Page did not call __done() or define window.__bench");
1122
- reject(new Error(lines.join("\n")));
1123
- }, timeout * 1e3);
1124
- return {
1125
- promise,
1126
- cancel: () => clearTimeout(timer)
1127
- };
1128
- }
1129
- /** In-page timing functions injected via addInitScript (zero CDP overhead).
1130
- * __start/__lap collect timestamps, __done delegates to exposed __benchCollect. */
1131
- function injectLapFunctions() {
1132
- const g = globalThis;
1133
- g.__benchSamples = [];
1134
- g.__benchLastTime = 0;
1135
- g.__benchFirstStart = 0;
1136
- g.__start = () => {
1137
- const now = performance.now();
1138
- g.__benchLastTime = now;
1139
- if (!g.__benchFirstStart) {
1140
- g.__benchFirstStart = now;
1141
- return g.__benchInstrumentStart();
1142
- }
1143
- };
1144
- g.__lap = () => {
1145
- const now = performance.now();
1146
- g.__benchSamples.push(now - g.__benchLastTime);
1147
- g.__benchLastTime = now;
1148
- };
1149
- g.__done = () => {
1150
- const wall = g.__benchFirstStart ? performance.now() - g.__benchFirstStart : 0;
1151
- return g.__benchCollect(g.__benchSamples.slice(), wall);
1152
- };
1153
- }
1154
- function heapSamplingParams(samplingInterval) {
1155
- return {
1156
- samplingInterval,
1157
- includeObjectsCollectedByMajorGC: true,
1158
- includeObjectsCollectedByMinorGC: true
1159
- };
1160
- }
1161
- /** Start CDP GC tracing, returns the event collector array. */
1162
- async function startGcTracing(cdp) {
1163
- const events = [];
1164
- cdp.on("Tracing.dataCollected", ({ value }) => {
1165
- for (const e of value) events.push(e);
1166
- });
1167
- await cdp.send("Tracing.start", { traceConfig: { includedCategories: ["v8", "v8.gc"] } });
1168
- return events;
1169
- }
1170
- /** Bench function mode: run window.__bench in a timed iteration loop. */
1171
- async function runBenchLoop(page, cdp, params, samplingInterval) {
1172
- const { heapSample } = params;
1173
- const maxTime = params.maxTime ?? 642;
1174
- const maxIter = params.maxIterations ?? Number.MAX_SAFE_INTEGER;
1175
- if (heapSample) await cdp.send("HeapProfiler.startSampling", heapSamplingParams(samplingInterval));
1176
- const { samples, totalMs } = await page.evaluate(async ({ maxTime, maxIter }) => {
1177
- const bench = globalThis.__bench;
1178
- const samples = [];
1179
- const startAll = performance.now();
1180
- const deadline = startAll + maxTime;
1181
- for (let i = 0; i < maxIter && performance.now() < deadline; i++) {
1182
- const t0 = performance.now();
1183
- await bench();
1184
- samples.push(performance.now() - t0);
1185
- }
1186
- return {
1187
- samples,
1188
- totalMs: performance.now() - startAll
1189
- };
1190
- }, {
1191
- maxTime,
1192
- maxIter
1193
- });
1194
- let heapProfile;
1195
- if (heapSample) heapProfile = (await cdp.send("HeapProfiler.stopSampling")).profile;
1196
- return {
1197
- samples,
1198
- heapProfile,
1199
- wallTimeMs: totalMs
1200
- };
1201
- }
1202
- /** Stop CDP tracing and parse GC events into GcStats. */
1203
- async function collectTracing(cdp, traceEvents) {
1204
- const complete = new Promise((resolve) => cdp.once("Tracing.tracingComplete", () => resolve()));
1205
- await cdp.send("Tracing.end");
1206
- await complete;
1207
- return browserGcStats(traceEvents);
1208
- }
1209
-
1210
954
  //#endregion
1211
955
  //#region src/export/JsonExport.ts
1212
956
  /** Export benchmark results to JSON file */
@@ -2685,14 +2429,11 @@ function defaultReport(groups, args) {
2685
2429
  }
2686
2430
  /** Build report sections based on CLI options */
2687
2431
  function buildReportSections(adaptive, gcStats, hasCpuData, hasOptData) {
2688
- const sections = adaptive ? [
2689
- adaptiveSection,
2690
- runsSection,
2691
- totalTimeSection
2692
- ] : [timeSection, runsSection];
2432
+ const sections = adaptive ? [adaptiveSection, totalTimeSection] : [timeSection];
2693
2433
  if (gcStats) sections.push(gcStatsSection);
2694
2434
  if (hasCpuData) sections.push(cpuSection);
2695
2435
  if (hasOptData) sections.push(optSection);
2436
+ sections.push(runsSection);
2696
2437
  return sections;
2697
2438
  }
2698
2439
  /** Run benchmarks, display table, and optionally generate HTML report */
@@ -2705,6 +2446,12 @@ async function benchExports(suite, args) {
2705
2446
  /** Run browser profiling via Playwright + CDP, report with standard pipeline */
2706
2447
  async function browserBenchExports(args) {
2707
2448
  warnBrowserFlags(args);
2449
+ let profileBrowser;
2450
+ try {
2451
+ ({profileBrowser} = await import("./BrowserHeapSampler-BzQs0P_z.mjs"));
2452
+ } catch {
2453
+ throw new Error("playwright is required for browser benchmarking (--url).\n\nQuick start: npx benchforge-browser --url <your-url>\n\nOr install manually:\n npm install playwright\n npx playwright install chromium");
2454
+ }
2708
2455
  const url = args.url;
2709
2456
  const { iterations, time } = args;
2710
2457
  const result = await profileBrowser({
@@ -2715,25 +2462,31 @@ async function browserBenchExports(args) {
2715
2462
  stackDepth: args["heap-depth"]
2716
2463
  },
2717
2464
  headless: args.headless,
2718
- chromeArgs: args["chrome-args"]?.split(/\s+/).filter(Boolean),
2465
+ chromeArgs: args["chrome-args"]?.flatMap((a) => a.split(/\s+/)).filter(Boolean),
2719
2466
  timeout: args.timeout,
2720
2467
  gcStats: args["gc-stats"],
2721
2468
  maxTime: iterations ? Number.MAX_SAFE_INTEGER : time * 1e3,
2722
2469
  maxIterations: iterations
2723
2470
  });
2724
- const name = new URL(url).pathname.split("/").pop() || "browser";
2471
+ const results = browserResultGroups(new URL(url).pathname.split("/").pop() || "browser", result);
2472
+ printBrowserReport(result, results, args);
2473
+ await exportReports({
2474
+ results,
2475
+ args
2476
+ });
2477
+ }
2478
+ /** Print browser benchmark tables and heap reports */
2479
+ function printBrowserReport(result, results, args) {
2725
2480
  const hasSamples = result.samples && result.samples.length > 0;
2726
- const results = browserResultGroups(name, result);
2727
- if (hasSamples || result.wallTimeMs != null) console.log(reportResults(results, [timeSection, runsSection]));
2728
- if (result.gcStats) console.log(reportResults(results, [browserGcStatsSection]));
2481
+ const sections = [];
2482
+ if (hasSamples || result.wallTimeMs != null) sections.push(timeSection);
2483
+ if (result.gcStats) sections.push(browserGcStatsSection);
2484
+ if (hasSamples || result.wallTimeMs != null) sections.push(runsSection);
2485
+ if (sections.length > 0) console.log(reportResults(results, sections));
2729
2486
  if (result.heapProfile) printHeapReports(results, {
2730
2487
  ...cliHeapReportOptions(args),
2731
2488
  isUserCode: isBrowserUserCode
2732
2489
  });
2733
- await exportReports({
2734
- results,
2735
- args
2736
- });
2737
2490
  }
2738
2491
  /** Wrap browser profile result as ReportGroup[] for the standard pipeline */
2739
2492
  function browserResultGroups(name, result) {
@@ -3085,4 +2838,4 @@ function getMostRecentModifiedDate(dir) {
3085
2838
 
3086
2839
  //#endregion
3087
2840
  export { timeSection as A, parseCliArgs as B, adaptiveSection as C, gcStatsSection as D, gcSection as E, generateHtmlReport as F, truncate as G, formatBytes as H, formatDateWithTimezone as I, loadCaseData as J, isStatefulVariant as K, prepareHtmlData as L, formatConvergence as M, filterMatrix as N, optSection as O, parseMatrixFilter as P, exportPerfettoTrace as R, reportMatrixResults as S, cpuSection as T, integer as U, reportResults as V, timeMs as W, loadCasesModule as Y, runDefaultMatrixBench as _, cliToMatrixOptions as a, gcStatsColumns as b, exportReports as c, matrixToReportGroups as d, parseBenchArgs as f, runDefaultBench as g, runBenchmarks as h, benchExports as i, totalTimeSection as j, runsSection as k, hasField as l, reportOptStatus as m, getBaselineVersion as n, defaultMatrixReport as o, printHeapReports as p, runMatrix as q, getCurrentGitVersion as r, defaultReport as s, formatGitVersion as t, matrixBenchExports as u, runMatrixSuite as v, buildGenericSections as w, heapTotalColumn as x, gcPauseColumn as y, defaultCliArgs as z };
3088
- //# sourceMappingURL=src-JGOI6_Sc.mjs.map
2841
+ //# sourceMappingURL=src-Dt_T-s_f.mjs.map