@vercel/devlow-bench 0.3.5 → 16.4.0-canary.24

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.
Files changed (46) hide show
  1. package/README.md +50 -33
  2. package/dist/browser.d.ts +1 -1
  3. package/dist/browser.js +52 -44
  4. package/dist/cli.js +114 -69
  5. package/dist/compare.d.ts +14 -0
  6. package/dist/compare.js +209 -0
  7. package/dist/describe.d.ts +1 -1
  8. package/dist/describe.js +18 -18
  9. package/dist/file.js +5 -5
  10. package/dist/index.d.ts +16 -3
  11. package/dist/index.js +3 -2
  12. package/dist/interfaces/compare.d.ts +4 -0
  13. package/dist/interfaces/compare.js +27 -0
  14. package/dist/interfaces/compose.d.ts +1 -1
  15. package/dist/interfaces/compose.js +1 -1
  16. package/dist/interfaces/console.d.ts +4 -2
  17. package/dist/interfaces/console.js +30 -8
  18. package/dist/interfaces/constants.js +7 -7
  19. package/dist/interfaces/datadog.d.ts +1 -1
  20. package/dist/interfaces/datadog.js +13 -13
  21. package/dist/interfaces/interactive.d.ts +1 -1
  22. package/dist/interfaces/interactive.js +8 -8
  23. package/dist/interfaces/json.d.ts +4 -2
  24. package/dist/interfaces/json.js +36 -17
  25. package/dist/interfaces/shell-test.d.ts +1 -0
  26. package/dist/interfaces/shell-test.js +56 -0
  27. package/dist/interfaces/snapshot.d.ts +6 -0
  28. package/dist/interfaces/snapshot.js +60 -0
  29. package/dist/interfaces/snowflake-test.js +3 -3
  30. package/dist/interfaces/snowflake.d.ts +1 -1
  31. package/dist/interfaces/snowflake.js +15 -13
  32. package/dist/runner.d.ts +5 -2
  33. package/dist/runner.js +131 -24
  34. package/dist/shell.d.ts +5 -3
  35. package/dist/shell.js +56 -28
  36. package/dist/snapshot.d.ts +18 -0
  37. package/dist/snapshot.js +162 -0
  38. package/dist/statistics-test.d.ts +1 -0
  39. package/dist/statistics-test.js +102 -0
  40. package/dist/statistics.d.ts +19 -0
  41. package/dist/statistics.js +123 -0
  42. package/dist/table.js +51 -51
  43. package/dist/units.js +6 -6
  44. package/dist/utils.d.ts +1 -0
  45. package/dist/utils.js +8 -6
  46. package/package.json +6 -5
@@ -0,0 +1,209 @@
1
+ import picocolors from 'picocolors';
2
+ import { mannWhitneyU, quantile } from './statistics.js';
3
+ import { formatUnit } from './units.js';
4
+ const { bold, dim, green, red, underline } = picocolors;
5
+ const SIGNIFICANCE_THRESHOLD = 0.05;
6
+ // Groups SnapshotRows into per-(scenario, variant, metric) sample arrays.
7
+ // Skips rows whose value is non-finite (e.g. failed CSV parse).
8
+ export function groupRows(rows) {
9
+ const out = new Map();
10
+ for (const r of rows) {
11
+ if (!Number.isFinite(r.value))
12
+ continue;
13
+ const key = makeKey(r.scenario, r.variant, r.metric);
14
+ let group = out.get(key);
15
+ if (!group) {
16
+ group = {
17
+ scenario: r.scenario,
18
+ variant: r.variant,
19
+ metric: r.metric,
20
+ unit: r.unit,
21
+ samples: [],
22
+ };
23
+ out.set(key, group);
24
+ }
25
+ group.samples.push(r.value);
26
+ }
27
+ return out;
28
+ }
29
+ export function makeKey(scenario, variant, metric) {
30
+ return `${scenario}\x00${variant}\x00${metric}`;
31
+ }
32
+ // Prints the comparison table comparing baseline (A) to current (B).
33
+ // Reports series only in one side and unit mismatches separately.
34
+ export function printComparison(baseline, current, options = {
35
+ baselineLabel: 'baseline',
36
+ }) {
37
+ const { baselineLabel, currentLabel } = options;
38
+ console.log();
39
+ if (currentLabel) {
40
+ console.log(bold(underline(`Comparison: ${baselineLabel} → ${currentLabel}`)));
41
+ }
42
+ else {
43
+ console.log(bold(underline(`Comparison vs baseline: ${baselineLabel}`)));
44
+ }
45
+ const pairs = [];
46
+ const onlyInCurrent = [];
47
+ const onlyInBaseline = [];
48
+ const skippedMismatch = [];
49
+ for (const [key, cur] of current) {
50
+ const base = baseline.get(key);
51
+ if (!base) {
52
+ onlyInCurrent.push(`${cur.scenario} · ${cur.variant} · ${cur.metric}`);
53
+ continue;
54
+ }
55
+ if (base.unit !== cur.unit) {
56
+ skippedMismatch.push(`${cur.scenario} · ${cur.variant} · ${cur.metric} (baseline=${base.unit}, current=${cur.unit})`);
57
+ continue;
58
+ }
59
+ pairs.push({ base, cur });
60
+ }
61
+ for (const [key, base] of baseline) {
62
+ if (!current.has(key)) {
63
+ onlyInBaseline.push(`${base.scenario} · ${base.variant} · ${base.metric}`);
64
+ }
65
+ }
66
+ // Hoist the most common sample count for each side into a banner above the
67
+ // table; per-row `(n)` is then shown only for rows that deviate from it,
68
+ // so the eye-catching exceptions aren't drowned in repeated `(7)`s.
69
+ const baseMode = modeN(pairs.map((p) => p.base.samples.length));
70
+ const curMode = modeN(pairs.map((p) => p.cur.samples.length));
71
+ if (pairs.length > 0) {
72
+ if (baseMode != null && curMode != null) {
73
+ const banner = baseMode === curMode
74
+ ? `n = ${baseMode} per metric`
75
+ : `baseline n = ${baseMode}, current n = ${curMode}`;
76
+ console.log(bold(banner));
77
+ }
78
+ const header = [
79
+ 'Scenario · Variant',
80
+ 'Metric',
81
+ 'Baseline p50 / p90 / p99',
82
+ 'Current p50 / p90 / p99',
83
+ 'Δ p50',
84
+ 'Δ%',
85
+ 'p',
86
+ ];
87
+ const rows = pairs.map(({ base, cur }) => formatRow(base, cur, baseMode, curMode));
88
+ printTable(header, rows);
89
+ }
90
+ else {
91
+ console.log(dim('No matching (scenario, variant, metric) tuples between current and baseline.'));
92
+ }
93
+ logList('Only in current', onlyInCurrent);
94
+ logList('Only in baseline', onlyInBaseline);
95
+ logList('Skipped due to unit mismatch', skippedMismatch, Infinity);
96
+ }
97
+ function logList(label, items, max = 5) {
98
+ if (items.length === 0)
99
+ return;
100
+ const shown = items.slice(0, max).join(', ');
101
+ const suffix = items.length > max ? '…' : '';
102
+ console.log(dim(`${label} (${items.length}): ${shown}${suffix}`));
103
+ }
104
+ // Picks the most common n in the list. Ties are broken in favor of the
105
+ // larger n (treat "the typical fully-collected sample count" as the default).
106
+ // Returns null for an empty list.
107
+ function modeN(ns) {
108
+ if (ns.length === 0)
109
+ return null;
110
+ const counts = new Map();
111
+ for (const n of ns)
112
+ counts.set(n, (counts.get(n) ?? 0) + 1);
113
+ let bestN = ns[0];
114
+ let bestCount = 0;
115
+ for (const [n, c] of counts) {
116
+ if (c > bestCount || (c === bestCount && n > bestN)) {
117
+ bestN = n;
118
+ bestCount = c;
119
+ }
120
+ }
121
+ return bestN;
122
+ }
123
+ function formatRow(base, cur, baseMode, curMode) {
124
+ const bs = percentiles(base.samples);
125
+ const cs = percentiles(cur.samples);
126
+ const delta = cs.p50 - bs.p50;
127
+ const deltaPct = bs.p50 === 0 ? NaN : (delta / bs.p50) * 100;
128
+ const mwu = mannWhitneyU(base.samples, cur.samples);
129
+ // Color verdict uses Mann–Whitney U's p as the significance signal, with
130
+ // the sign of the p50 shift determining improvement vs regression.
131
+ let verdict;
132
+ if (Number.isFinite(mwu.p) && mwu.p < SIGNIFICANCE_THRESHOLD && delta !== 0) {
133
+ verdict = delta < 0 ? 'improved' : 'regressed';
134
+ }
135
+ return {
136
+ cells: [
137
+ `${base.scenario} · ${base.variant}`,
138
+ base.metric,
139
+ formatGroupCell(base, bs, base.samples.length !== baseMode),
140
+ formatGroupCell(cur, cs, cur.samples.length !== curMode),
141
+ formatDelta(delta, base.unit),
142
+ Number.isFinite(deltaPct) ? `${deltaPct.toFixed(1)}%` : 'n/a',
143
+ formatP(mwu.p),
144
+ ],
145
+ verdict,
146
+ };
147
+ }
148
+ function percentiles(samples) {
149
+ return {
150
+ p50: quantile(samples, 0.5),
151
+ p90: quantile(samples, 0.9),
152
+ p99: quantile(samples, 0.99),
153
+ };
154
+ }
155
+ function formatGroupCell(group, s, showN) {
156
+ const parts = [s.p50, s.p90, s.p99].map((v) => splitFormattedUnit(v, group.unit));
157
+ const nSuffix = showN ? ` (${group.samples.length})` : '';
158
+ // If all three values render with the same unit suffix (e.g. all "requests"),
159
+ // only print the suffix on the last value to avoid "7 req / 7 req / 7 req".
160
+ if (parts[0].suffix === parts[1].suffix &&
161
+ parts[1].suffix === parts[2].suffix) {
162
+ return `${parts[0].num} / ${parts[1].num} / ${parts[2].num}${parts[2].suffix}${nSuffix}`;
163
+ }
164
+ return `${parts[0].num}${parts[0].suffix} / ${parts[1].num}${parts[1].suffix} / ${parts[2].num}${parts[2].suffix}${nSuffix}`;
165
+ }
166
+ function splitFormattedUnit(value, unit) {
167
+ const formatted = formatUnit(value, unit);
168
+ const m = /^(-?\d+(?:\.\d+)?)(.*)$/.exec(formatted);
169
+ if (!m)
170
+ return { num: formatted, suffix: '' };
171
+ return { num: m[1], suffix: m[2] };
172
+ }
173
+ function formatDelta(d, unit) {
174
+ if (!Number.isFinite(d))
175
+ return 'n/a';
176
+ const sign = d > 0 ? '+' : '';
177
+ return `${sign}${formatUnit(d, unit)}`;
178
+ }
179
+ function formatP(p) {
180
+ if (!Number.isFinite(p))
181
+ return String(p);
182
+ if (p === 0)
183
+ return '0';
184
+ if (p < 1e-3)
185
+ return p.toExponential(2);
186
+ return p.toFixed(3);
187
+ }
188
+ function printTable(header, rows) {
189
+ const widths = header.map((h) => h.length);
190
+ for (const r of rows) {
191
+ for (let i = 0; i < r.cells.length; i++) {
192
+ if (r.cells[i].length > widths[i])
193
+ widths[i] = r.cells[i].length;
194
+ }
195
+ }
196
+ const pad = (s, w) => s + ' '.repeat(w - s.length);
197
+ const line = (cells, paint = (s) => s) => '| ' + cells.map((c, i) => paint(pad(c, widths[i]))).join(' | ') + ' |';
198
+ const sep = '|' + widths.map((w) => '-'.repeat(w + 2)).join('|') + '|';
199
+ console.log(line(header));
200
+ console.log(sep);
201
+ for (const r of rows) {
202
+ const paint = r.verdict === 'improved'
203
+ ? green
204
+ : r.verdict === 'regressed'
205
+ ? red
206
+ : undefined;
207
+ console.log(line(r.cells, paint));
208
+ }
209
+ }
@@ -1,4 +1,4 @@
1
- import type { ConfigFor, CurrentScenario, Scenario } from "./index.js";
1
+ import type { ConfigFor, CurrentScenario, Scenario } from './index.js';
2
2
  export declare function setCurrentScenarios(scenarios: Scenario[] | null): void;
3
3
  export declare function describe<P>(name: string, config: ConfigFor<P>, fn: (props: P) => Promise<void>): void;
4
4
  export declare namespace describe {
package/dist/describe.js CHANGED
@@ -1,5 +1,5 @@
1
- import compose from "./interfaces/compose.js";
2
- import { runScenarios } from "./runner.js";
1
+ import compose from './interfaces/compose.js';
2
+ import { runScenarios } from './runner.js';
3
3
  let currentScenarios = null;
4
4
  export function setCurrentScenarios(scenarios) {
5
5
  currentScenarios = scenarios;
@@ -8,9 +8,9 @@ export function describe(name, config, fn) {
8
8
  if (currentScenarios === null) {
9
9
  const scenarios = (currentScenarios = []);
10
10
  Promise.resolve().then(async () => {
11
- const ifaceNames = process.env.INTERFACE || "interactive,console";
11
+ const ifaceNames = process.env.INTERFACE || 'interactive,console';
12
12
  const ifaces = [];
13
- for (const ifaceName of ifaceNames.split(",").map((s) => s.trim())) {
13
+ for (const ifaceName of ifaceNames.split(',').map((s) => s.trim())) {
14
14
  let iface;
15
15
  try {
16
16
  iface = await import(`./interfaces/${ifaceName}.js`);
@@ -19,7 +19,7 @@ export function describe(name, config, fn) {
19
19
  iface = await import(ifaceName);
20
20
  }
21
21
  iface = (iface && iface.default) || iface;
22
- if (typeof iface === "function") {
22
+ if (typeof iface === 'function') {
23
23
  iface = await iface();
24
24
  }
25
25
  if (!iface) {
@@ -32,7 +32,7 @@ export function describe(name, config, fn) {
32
32
  }
33
33
  const normalizedConfig = Object.fromEntries(Object.entries(config).map(([key, value]) => [
34
34
  key,
35
- typeof value === "boolean"
35
+ typeof value === 'boolean'
36
36
  ? [value, !value]
37
37
  : value,
38
38
  ]));
@@ -55,36 +55,36 @@ export function withCurrent(current, fn) {
55
55
  currentScenario = prev;
56
56
  });
57
57
  }
58
- export const PREVIOUS = Symbol("previous measurement with that unit");
58
+ export const PREVIOUS = Symbol('previous measurement with that unit');
59
59
  export async function measureTime(name, options = {}) {
60
60
  const end = Date.now() - (options.offset || 0);
61
- await reportMeasurement(name, end, "ms", {
61
+ await reportMeasurement(name, end, 'ms', {
62
62
  relativeTo: PREVIOUS,
63
63
  ...options,
64
64
  });
65
65
  }
66
66
  export async function reportMeasurement(name, value, unit, options = {}) {
67
67
  if (!currentScenario) {
68
- throw new Error("reportMeasurement() must be called inside of describe()");
68
+ throw new Error('reportMeasurement() must be called inside of describe()');
69
69
  }
70
- if (typeof name !== "string") {
71
- throw new Error("reportMeasurement() must be called with a name that is a string");
70
+ if (typeof name !== 'string') {
71
+ throw new Error('reportMeasurement() must be called with a name that is a string');
72
72
  }
73
- if (typeof value !== "number") {
74
- throw new Error("reportMeasurement() must be called with a value that is a number");
73
+ if (typeof value !== 'number') {
74
+ throw new Error('reportMeasurement() must be called with a value that is a number');
75
75
  }
76
76
  if (isNaN(value)) {
77
- throw new Error("reportMeasurement() must be called with a value that is not NaN");
77
+ throw new Error('reportMeasurement() must be called with a value that is not NaN');
78
78
  }
79
79
  if (!isFinite(value)) {
80
- throw new Error("reportMeasurement() must be called with a value that is finite");
80
+ throw new Error('reportMeasurement() must be called with a value that is finite');
81
81
  }
82
- if (typeof unit !== "string") {
83
- throw new Error("reportMeasurement() must be called with a unit that is a string");
82
+ if (typeof unit !== 'string') {
83
+ throw new Error('reportMeasurement() must be called with a unit that is a string');
84
84
  }
85
85
  let { relativeTo, scenario, props } = options;
86
86
  if (relativeTo === PREVIOUS) {
87
- relativeTo = "previous";
87
+ relativeTo = 'previous';
88
88
  for (const [prevName, prev] of currentScenario.measurements) {
89
89
  if (prev.unit === unit) {
90
90
  relativeTo = prevName;
package/dist/file.js CHANGED
@@ -4,7 +4,7 @@ import { dirname } from 'path';
4
4
  export async function waitForFile(path, timeout) {
5
5
  let currentAction = '';
6
6
  let timeoutRef;
7
- const timeoutPromise = new Promise((resolve, reject) => {
7
+ const timeoutPromise = new Promise((_resolve, reject) => {
8
8
  timeoutRef = setTimeout(() => {
9
9
  reject(new Error(`Timed out waiting for file ${path} (${currentAction}))`));
10
10
  }, timeout || 60000);
@@ -21,16 +21,16 @@ export async function waitForFile(path, timeout) {
21
21
  }
22
22
  elements.reverse();
23
23
  try {
24
- for (const path of elements) {
25
- const checkAccess = () => access(path, constants.F_OK)
24
+ for (const element of elements) {
25
+ const checkAccess = () => access(element, constants.F_OK)
26
26
  .then(() => true)
27
27
  .catch(() => false);
28
28
  if (!(await checkAccess())) {
29
29
  let resolveCheckAgain = () => { };
30
- const watcher = watch(dirname(path), () => {
30
+ const watcher = watch(dirname(element), () => {
31
31
  resolveCheckAgain();
32
32
  });
33
- currentAction = `waiting for ${path}`;
33
+ currentAction = `waiting for ${element}`;
34
34
  let checkAgainPromise = new Promise((resolve) => {
35
35
  resolveCheckAgain = resolve;
36
36
  });
package/dist/index.d.ts CHANGED
@@ -20,15 +20,28 @@ export interface CurrentScenario {
20
20
  }>;
21
21
  }
22
22
  export type Interface = Partial<FullInterface>;
23
+ export interface VariantStatistic {
24
+ samples: number[];
25
+ unit: string;
26
+ relativeTo?: string;
27
+ mean: number;
28
+ p50: number;
29
+ p90: number;
30
+ }
23
31
  export interface FullInterface {
24
32
  filterScenarios(scenarios: Scenario[]): Promise<Scenario[]>;
25
33
  filterScenarioVariants(scenarioVariants: ScenarioVariant[]): Promise<ScenarioVariant[]>;
26
- start(scenario: string, props: Record<string, string | number | boolean | null>): Promise<void>;
34
+ start(scenario: string, props: Record<string, string | number | boolean | null>, runInfo?: {
35
+ run: number;
36
+ total: number;
37
+ warmup: boolean;
38
+ }): Promise<void>;
27
39
  measurement(scenario: string, props: Record<string, string | number | boolean | null>, name: string, value: number, unit: string, relativeTo?: string): Promise<void>;
28
40
  end(scenario: string, props: Record<string, string | number | boolean | null>): Promise<void>;
29
41
  error(scenario: string, props: Record<string, string | number | boolean | null>, error: unknown): Promise<void>;
42
+ variantStatistics(scenario: string, props: Record<string, string | number | boolean | null>, stats: Record<string, VariantStatistic>): Promise<void>;
30
43
  finish(): Promise<void>;
31
44
  }
32
45
  export declare function intoFullInterface(iface: Interface): FullInterface;
33
- export { describe, measureTime, reportMeasurement, PREVIOUS, } from "./describe.js";
34
- export { runScenarios } from "./runner.js";
46
+ export { describe, measureTime, reportMeasurement, PREVIOUS, } from './describe.js';
47
+ export { runScenarios } from './runner.js';
package/dist/index.js CHANGED
@@ -7,8 +7,9 @@ export function intoFullInterface(iface) {
7
7
  measurement: iface.measurement ?? (async () => { }),
8
8
  end: iface.end ?? (async () => { }),
9
9
  error: iface.error ?? (async () => { }),
10
+ variantStatistics: iface.variantStatistics ?? (async () => { }),
10
11
  finish: iface.finish ?? (async () => { }),
11
12
  };
12
13
  }
13
- export { describe, measureTime, reportMeasurement, PREVIOUS, } from "./describe.js";
14
- export { runScenarios } from "./runner.js";
14
+ export { describe, measureTime, reportMeasurement, PREVIOUS, } from './describe.js';
15
+ export { runScenarios } from './runner.js';
@@ -0,0 +1,4 @@
1
+ import type { Interface } from '../index.js';
2
+ export default function createInterface(options: {
3
+ baselinePath: string;
4
+ }): Promise<Interface>;
@@ -0,0 +1,27 @@
1
+ import { groupRows, makeKey, printComparison, } from '../compare.js';
2
+ import { readSnapshot } from '../snapshot.js';
3
+ import { formatVariantProps } from '../utils.js';
4
+ export default async function createInterface(options) {
5
+ const baselinePath = options.baselinePath;
6
+ const baselineRows = await readSnapshot(baselinePath);
7
+ const baseline = groupRows(baselineRows);
8
+ const current = new Map();
9
+ return {
10
+ variantStatistics: async (scenario, props, stats) => {
11
+ const variant = formatVariantProps(props);
12
+ for (const [metric, s] of Object.entries(stats)) {
13
+ const key = makeKey(scenario, variant, metric);
14
+ current.set(key, {
15
+ scenario,
16
+ variant,
17
+ metric,
18
+ unit: s.unit,
19
+ samples: s.samples.slice(),
20
+ });
21
+ }
22
+ },
23
+ finish: async () => {
24
+ printComparison(baseline, current, { baselineLabel: baselinePath });
25
+ },
26
+ };
27
+ }
@@ -1,2 +1,2 @@
1
- import { Interface } from "../index.js";
1
+ import type { Interface } from '../index.js';
2
2
  export default function compose(...ifaces: Interface[]): Interface;
@@ -7,7 +7,7 @@ export default function compose(...ifaces) {
7
7
  }
8
8
  const composed = {};
9
9
  for (const key of allKeys) {
10
- if (key.startsWith("filter")) {
10
+ if (key.startsWith('filter')) {
11
11
  composed[key] = async (items, ...args) => {
12
12
  for (const iface of ifaces) {
13
13
  const anyIface = iface;
@@ -1,2 +1,4 @@
1
- import { Interface } from "../index.js";
2
- export default function createInterface(): Interface;
1
+ import type { Interface } from '../index.js';
2
+ export default function createInterface(options?: {
3
+ n?: number;
4
+ }): Interface;
@@ -1,14 +1,36 @@
1
- import picocolors from "picocolors";
2
- import { formatUnit } from "../units.js";
3
- import { formatVariant } from "../utils.js";
4
- const { bgCyan, bold, magenta, red, underline } = picocolors;
5
- export default function createInterface() {
1
+ import picocolors from 'picocolors';
2
+ import { formatUnit } from '../units.js';
3
+ import { formatVariant } from '../utils.js';
4
+ const { bgCyan, bold, cyan, dim, magenta, red, underline } = picocolors;
5
+ export default function createInterface(options = {}) {
6
+ const n = options.n ?? 1;
7
+ const showSummary = n > 1;
6
8
  const iface = {
7
- start: async (scenario, props) => {
8
- console.log(bold(underline(`Running ${formatVariant(scenario, props)}...`)));
9
+ start: async (scenario, props, runInfo) => {
10
+ const label = formatVariant(scenario, props);
11
+ let progress = '';
12
+ if (runInfo) {
13
+ progress = runInfo.warmup
14
+ ? ` [warmup ${runInfo.run}/${runInfo.total}]`
15
+ : ` [${runInfo.run}/${runInfo.total}]`;
16
+ }
17
+ console.log(bold(underline(`Running ${label}...${progress}`)));
9
18
  },
10
19
  measurement: async (scenario, props, name, value, unit, relativeTo) => {
11
- console.log(bgCyan(bold(magenta(`${formatVariant(scenario, props)}: ${name} = ${formatUnit(value, unit)}${relativeTo ? ` (from ${relativeTo})` : ""}`))));
20
+ console.log(bgCyan(bold(magenta(`${formatVariant(scenario, props)}: ${name} = ${formatUnit(value, unit)}${relativeTo ? ` (from ${relativeTo})` : ''}`))));
21
+ },
22
+ variantStatistics: async (scenario, props, stats) => {
23
+ if (!showSummary)
24
+ return;
25
+ const header = `${formatVariant(scenario, props)} (n=${n})`;
26
+ console.log(bold(cyan(header)));
27
+ for (const [name, s] of Object.entries(stats)) {
28
+ const meanText = formatUnit(s.mean, s.unit);
29
+ const p50Text = formatUnit(s.p50, s.unit);
30
+ const p90Text = formatUnit(s.p90, s.unit);
31
+ const relText = s.relativeTo ? dim(` (from ${s.relativeTo})`) : '';
32
+ console.log(` ${name}: mean=${meanText} p50=${p50Text} p90=${p90Text}${relText}`);
33
+ }
12
34
  },
13
35
  error: async (scenario, props, error) => {
14
36
  console.log(bold(red(`${formatVariant(scenario, props)}: ${(error && error.stack) || error}`)));
@@ -1,19 +1,19 @@
1
- import os from "node:os";
2
- import { command } from "../shell.js";
1
+ import os from 'node:os';
2
+ import { command } from '../shell.js';
3
3
  export const UNIT_MAPPING = {
4
- ms: "millisecond",
5
- requests: "request",
6
- bytes: "byte",
4
+ ms: 'millisecond',
5
+ requests: 'request',
6
+ bytes: 'byte',
7
7
  };
8
8
  export const GIT_SHA = process.env.GITHUB_SHA ??
9
9
  (await (async () => {
10
- const cmd = command("git", ["rev-parse", "HEAD"]);
10
+ const cmd = command('git', ['rev-parse', 'HEAD']);
11
11
  await cmd.ok();
12
12
  return cmd.output.trim();
13
13
  })());
14
14
  export const GIT_BRANCH = process.env.GITHUB_REF_NAME ??
15
15
  (await (async () => {
16
- const cmd = command("git", ["rev-parse", "--abbrev-ref", "HEAD"]);
16
+ const cmd = command('git', ['rev-parse', '--abbrev-ref', 'HEAD']);
17
17
  await cmd.ok();
18
18
  return cmd.output.trim();
19
19
  })());
@@ -1,4 +1,4 @@
1
- import type { Interface } from "../index.js";
1
+ import type { Interface } from '../index.js';
2
2
  export default function createInterface({ apiKey, appKey, host, }?: {
3
3
  apiKey?: string;
4
4
  appKey?: string;
@@ -1,17 +1,17 @@
1
- import datadogApiClient from "@datadog/datadog-api-client";
2
- import os from "os";
3
- import { CPU_ARCH, CPU_MODEL, GIT_BRANCH, GIT_SHA, IS_CI, NODE_VERSION, NUM_CPUS, OS, OS_RELEASE, USERNAME } from "./constants.js";
1
+ import datadogApiClient from '@datadog/datadog-api-client';
2
+ import os from 'os';
3
+ import { CPU_ARCH, CPU_MODEL, GIT_BRANCH, GIT_SHA, IS_CI, NODE_VERSION, NUM_CPUS, OS, OS_RELEASE, USERNAME, } from './constants.js';
4
4
  function toIdentifier(str) {
5
- return str.replace(/\//g, ".").replace(/ /g, "_");
5
+ return str.replace(/\//g, '.').replace(/ /g, '_');
6
6
  }
7
7
  const UNIT_MAPPING = {
8
- ms: "millisecond",
9
- requests: "request",
10
- bytes: "byte",
8
+ ms: 'millisecond',
9
+ requests: 'request',
10
+ bytes: 'byte',
11
11
  };
12
12
  export default function createInterface({ apiKey = process.env.DATADOG_API_KEY, appKey = process.env.DATADOG_APP_KEY, host = process.env.DATADOG_HOST || os.hostname(), } = {}) {
13
13
  if (!apiKey)
14
- throw new Error("Datadog API key is required (set DATADOG_API_KEY)");
14
+ throw new Error('Datadog API key is required (set DATADOG_API_KEY)');
15
15
  const commonTags = [
16
16
  `ci:${IS_CI}`,
17
17
  `os:${OS}`,
@@ -35,7 +35,7 @@ export default function createInterface({ apiKey = process.env.DATADOG_API_KEY,
35
35
  const dataPoints = [];
36
36
  const metricMetadata = {};
37
37
  const iface = {
38
- measurement: async (scenario, props, name, value, unit, relativeTo) => {
38
+ measurement: async (scenario, props, name, value, unit, _relativeTo) => {
39
39
  const ts = Math.round(Date.now() / 1000);
40
40
  const metric = toIdentifier(`devlow_bench/${scenario}/${name}`);
41
41
  if (UNIT_MAPPING[unit]) {
@@ -45,16 +45,16 @@ export default function createInterface({ apiKey = process.env.DATADOG_API_KEY,
45
45
  }
46
46
  dataPoints.push({
47
47
  metric,
48
- type: "distribution",
48
+ type: 'distribution',
49
49
  host,
50
50
  tags: Object.entries(props)
51
- .filter(([, value]) => value !== null)
52
- .map(([key, value]) => `${toIdentifier(key)}:${toIdentifier(value.toString())}`)
51
+ .filter(([, propValue]) => propValue !== null)
52
+ .map(([key, propValue]) => `${toIdentifier(key)}:${toIdentifier(propValue.toString())}`)
53
53
  .concat(commonTags),
54
54
  points: [[ts, [value]]],
55
55
  });
56
56
  },
57
- end: async (scenario, props) => {
57
+ end: async (_scenario, _props) => {
58
58
  await api.submitDistributionPoints({
59
59
  body: {
60
60
  series: dataPoints,
@@ -1,2 +1,2 @@
1
- import { Interface } from "../index.js";
1
+ import type { Interface } from '../index.js';
2
2
  export default function createInterface(): Interface;
@@ -1,5 +1,5 @@
1
- import inquirer from "inquirer";
2
- import { formatVariant } from "../utils.js";
1
+ import inquirer from 'inquirer';
2
+ import { formatVariant } from '../utils.js';
3
3
  export default function createInterface() {
4
4
  const iface = {
5
5
  filterScenarios: async (scenarios) => {
@@ -7,10 +7,10 @@ export default function createInterface() {
7
7
  return scenarios;
8
8
  }
9
9
  let answer = await inquirer.prompt({
10
- type: "checkbox",
11
- name: "scenarios",
10
+ type: 'checkbox',
11
+ name: 'scenarios',
12
12
  default: scenarios.slice(),
13
- message: "Choose scenarios to run",
13
+ message: 'Choose scenarios to run',
14
14
  choices: scenarios.map((scenario) => ({
15
15
  name: scenario.name,
16
16
  value: scenario,
@@ -23,10 +23,10 @@ export default function createInterface() {
23
23
  return variants;
24
24
  }
25
25
  let answer = await inquirer.prompt({
26
- type: "checkbox",
27
- name: "variants",
26
+ type: 'checkbox',
27
+ name: 'variants',
28
28
  default: variants.slice(),
29
- message: "Choose variants to run",
29
+ message: 'Choose variants to run',
30
30
  choices: variants.map((variant) => {
31
31
  return {
32
32
  name: formatVariant(variant.scenario.name, variant.props),
@@ -1,2 +1,4 @@
1
- import { Interface } from "../index.js";
2
- export default function createInterface(file?: string): Interface;
1
+ import type { Interface } from '../index.js';
2
+ export default function createInterface(file?: string, options?: {
3
+ n?: number;
4
+ }): Interface;