@fuzdev/fuz_util 0.42.0 → 0.44.0
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.
- package/LICENSE +1 -1
- package/README.md +19 -12
- package/dist/async.d.ts +2 -2
- package/dist/async.d.ts.map +1 -1
- package/dist/async.js +2 -2
- package/dist/benchmark.d.ts +179 -0
- package/dist/benchmark.d.ts.map +1 -0
- package/dist/benchmark.js +400 -0
- package/dist/benchmark_baseline.d.ts +195 -0
- package/dist/benchmark_baseline.d.ts.map +1 -0
- package/dist/benchmark_baseline.js +388 -0
- package/dist/benchmark_format.d.ts +87 -0
- package/dist/benchmark_format.d.ts.map +1 -0
- package/dist/benchmark_format.js +266 -0
- package/dist/benchmark_stats.d.ts +112 -0
- package/dist/benchmark_stats.d.ts.map +1 -0
- package/dist/benchmark_stats.js +219 -0
- package/dist/benchmark_types.d.ts +174 -0
- package/dist/benchmark_types.d.ts.map +1 -0
- package/dist/benchmark_types.js +1 -0
- package/dist/git.d.ts +12 -0
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +14 -0
- package/dist/library_json.d.ts +3 -3
- package/dist/library_json.d.ts.map +1 -1
- package/dist/library_json.js +1 -1
- package/dist/maths.d.ts +4 -0
- package/dist/maths.d.ts.map +1 -1
- package/dist/maths.js +8 -0
- package/dist/object.js +1 -1
- package/dist/source_json.d.ts +4 -4
- package/dist/stats.d.ts +180 -0
- package/dist/stats.d.ts.map +1 -0
- package/dist/stats.js +402 -0
- package/dist/string.d.ts +13 -0
- package/dist/string.d.ts.map +1 -1
- package/dist/string.js +58 -0
- package/dist/time.d.ts +165 -0
- package/dist/time.d.ts.map +1 -0
- package/dist/time.js +264 -0
- package/dist/timings.d.ts +1 -7
- package/dist/timings.d.ts.map +1 -1
- package/dist/timings.js +16 -16
- package/package.json +21 -19
- package/src/lib/async.ts +3 -3
- package/src/lib/benchmark.ts +498 -0
- package/src/lib/benchmark_baseline.ts +538 -0
- package/src/lib/benchmark_format.ts +314 -0
- package/src/lib/benchmark_stats.ts +311 -0
- package/src/lib/benchmark_types.ts +197 -0
- package/src/lib/git.ts +24 -0
- package/src/lib/library_json.ts +3 -3
- package/src/lib/maths.ts +8 -0
- package/src/lib/object.ts +1 -1
- package/src/lib/stats.ts +534 -0
- package/src/lib/string.ts +66 -0
- package/src/lib/time.ts +319 -0
- package/src/lib/timings.ts +17 -17
- package/src/lib/types.ts +2 -2
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Benchmark baseline storage and comparison utilities.
|
|
3
|
+
* Save benchmark results to disk and compare against baselines for regression detection.
|
|
4
|
+
*/
|
|
5
|
+
import { readFile, writeFile, mkdir, rm } from 'node:fs/promises';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { fs_exists } from './fs.js';
|
|
9
|
+
import { git_info_get } from './git.js';
|
|
10
|
+
import { benchmark_stats_compare, } from './benchmark_stats.js';
|
|
11
|
+
import { stats_confidence_interval_from_summary } from './stats.js';
|
|
12
|
+
// Version for forward compatibility - increment when schema changes
|
|
13
|
+
const BASELINE_VERSION = 1;
|
|
14
|
+
/**
|
|
15
|
+
* Schema for a single benchmark entry in the baseline.
|
|
16
|
+
*/
|
|
17
|
+
export const BenchmarkBaselineEntry = z.object({
|
|
18
|
+
name: z.string(),
|
|
19
|
+
mean_ns: z.number(),
|
|
20
|
+
median_ns: z.number(),
|
|
21
|
+
std_dev_ns: z.number(),
|
|
22
|
+
min_ns: z.number(),
|
|
23
|
+
max_ns: z.number(),
|
|
24
|
+
p75_ns: z.number(),
|
|
25
|
+
p90_ns: z.number(),
|
|
26
|
+
p95_ns: z.number(),
|
|
27
|
+
p99_ns: z.number(),
|
|
28
|
+
ops_per_second: z.number(),
|
|
29
|
+
sample_size: z.number(),
|
|
30
|
+
});
|
|
31
|
+
/**
|
|
32
|
+
* Schema for the complete baseline file.
|
|
33
|
+
*/
|
|
34
|
+
export const BenchmarkBaseline = z.object({
|
|
35
|
+
version: z.number(),
|
|
36
|
+
timestamp: z.string(),
|
|
37
|
+
git_commit: z.string().nullable(),
|
|
38
|
+
git_branch: z.string().nullable(),
|
|
39
|
+
node_version: z.string(),
|
|
40
|
+
entries: z.array(BenchmarkBaselineEntry),
|
|
41
|
+
});
|
|
42
|
+
const DEFAULT_BASELINE_PATH = '.gro/benchmarks';
|
|
43
|
+
const BASELINE_FILENAME = 'baseline.json';
|
|
44
|
+
/**
|
|
45
|
+
* Convert benchmark results to baseline entries.
|
|
46
|
+
*/
|
|
47
|
+
const results_to_entries = (results) => {
|
|
48
|
+
return results.map((r) => ({
|
|
49
|
+
name: r.name,
|
|
50
|
+
mean_ns: r.stats.mean_ns,
|
|
51
|
+
median_ns: r.stats.median_ns,
|
|
52
|
+
std_dev_ns: r.stats.std_dev_ns,
|
|
53
|
+
min_ns: r.stats.min_ns,
|
|
54
|
+
max_ns: r.stats.max_ns,
|
|
55
|
+
p75_ns: r.stats.p75_ns,
|
|
56
|
+
p90_ns: r.stats.p90_ns,
|
|
57
|
+
p95_ns: r.stats.p95_ns,
|
|
58
|
+
p99_ns: r.stats.p99_ns,
|
|
59
|
+
ops_per_second: r.stats.ops_per_second,
|
|
60
|
+
sample_size: r.stats.sample_size,
|
|
61
|
+
}));
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Save benchmark results as the current baseline.
|
|
65
|
+
*
|
|
66
|
+
* @param results - Benchmark results to save
|
|
67
|
+
* @param options - Save options
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* const bench = new Benchmark();
|
|
72
|
+
* bench.add('test', () => fn());
|
|
73
|
+
* await bench.run();
|
|
74
|
+
* await benchmark_baseline_save(bench.results());
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export const benchmark_baseline_save = async (results, options = {}) => {
|
|
78
|
+
const base_path = options.path ?? DEFAULT_BASELINE_PATH;
|
|
79
|
+
// Get git info if not provided
|
|
80
|
+
let git_commit = options.git_commit;
|
|
81
|
+
let git_branch = options.git_branch;
|
|
82
|
+
if (git_commit === undefined || git_branch === undefined) {
|
|
83
|
+
const git_info = await git_info_get();
|
|
84
|
+
git_commit ??= git_info.commit;
|
|
85
|
+
git_branch ??= git_info.branch;
|
|
86
|
+
}
|
|
87
|
+
const baseline = {
|
|
88
|
+
version: BASELINE_VERSION,
|
|
89
|
+
timestamp: new Date().toISOString(),
|
|
90
|
+
git_commit,
|
|
91
|
+
git_branch,
|
|
92
|
+
node_version: process.version,
|
|
93
|
+
entries: results_to_entries(results),
|
|
94
|
+
};
|
|
95
|
+
await mkdir(base_path, { recursive: true });
|
|
96
|
+
const filepath = join(base_path, BASELINE_FILENAME);
|
|
97
|
+
await writeFile(filepath, JSON.stringify(baseline, null, '\t'), 'utf-8');
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Load the current baseline from disk.
|
|
101
|
+
*
|
|
102
|
+
* @param options - Load options
|
|
103
|
+
* @returns The baseline, or null if not found or invalid
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```ts
|
|
107
|
+
* const baseline = await benchmark_baseline_load();
|
|
108
|
+
* if (baseline) {
|
|
109
|
+
* console.log(`Baseline from ${baseline.timestamp}`);
|
|
110
|
+
* }
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export const benchmark_baseline_load = async (options = {}) => {
|
|
114
|
+
const base_path = options.path ?? DEFAULT_BASELINE_PATH;
|
|
115
|
+
const filepath = join(base_path, BASELINE_FILENAME);
|
|
116
|
+
if (!(await fs_exists(filepath))) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const contents = await readFile(filepath, 'utf-8');
|
|
121
|
+
const parsed = JSON.parse(contents);
|
|
122
|
+
const baseline = BenchmarkBaseline.parse(parsed);
|
|
123
|
+
// Check version compatibility
|
|
124
|
+
if (baseline.version !== BASELINE_VERSION) {
|
|
125
|
+
// eslint-disable-next-line no-console
|
|
126
|
+
console.warn(`Benchmark baseline version mismatch (got ${baseline.version}, expected ${BASELINE_VERSION}). Removing stale baseline: ${filepath}`);
|
|
127
|
+
await rm(filepath, { force: true });
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
return baseline;
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
// eslint-disable-next-line no-console
|
|
134
|
+
console.warn(`Invalid or corrupted benchmark baseline file. Removing: ${filepath}`, err instanceof Error ? err.message : err);
|
|
135
|
+
await rm(filepath, { force: true });
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* Compare benchmark results against the stored baseline.
|
|
141
|
+
*
|
|
142
|
+
* @param results - Current benchmark results
|
|
143
|
+
* @param options - Comparison options including regression threshold and staleness warning
|
|
144
|
+
* @returns Comparison result with regressions, improvements, and unchanged tasks
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```ts
|
|
148
|
+
* const bench = new Benchmark();
|
|
149
|
+
* bench.add('test', () => fn());
|
|
150
|
+
* await bench.run();
|
|
151
|
+
*
|
|
152
|
+
* const comparison = await benchmark_baseline_compare(bench.results(), {
|
|
153
|
+
* regression_threshold: 1.05, // Only flag regressions 5% or more slower
|
|
154
|
+
* staleness_warning_days: 7, // Warn if baseline is older than 7 days
|
|
155
|
+
* });
|
|
156
|
+
* if (comparison.regressions.length > 0) {
|
|
157
|
+
* console.log('Performance regressions detected!');
|
|
158
|
+
* for (const r of comparison.regressions) {
|
|
159
|
+
* console.log(` ${r.name}: ${r.comparison.speedup_ratio.toFixed(2)}x slower`);
|
|
160
|
+
* }
|
|
161
|
+
* process.exit(1);
|
|
162
|
+
* }
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
export const benchmark_baseline_compare = async (results, options = {}) => {
|
|
166
|
+
const baseline = await benchmark_baseline_load(options);
|
|
167
|
+
const regression_threshold = options.regression_threshold ?? 1.0;
|
|
168
|
+
if (!baseline) {
|
|
169
|
+
return {
|
|
170
|
+
baseline_found: false,
|
|
171
|
+
baseline_timestamp: null,
|
|
172
|
+
baseline_commit: null,
|
|
173
|
+
baseline_age_days: null,
|
|
174
|
+
baseline_stale: false,
|
|
175
|
+
comparisons: [],
|
|
176
|
+
regressions: [],
|
|
177
|
+
improvements: [],
|
|
178
|
+
unchanged: [],
|
|
179
|
+
new_tasks: results.map((r) => r.name),
|
|
180
|
+
removed_tasks: [],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
// Calculate baseline age
|
|
184
|
+
const baseline_date = new Date(baseline.timestamp);
|
|
185
|
+
const now = new Date();
|
|
186
|
+
const baseline_age_days = (now.getTime() - baseline_date.getTime()) / (1000 * 60 * 60 * 24);
|
|
187
|
+
const baseline_stale = options.staleness_warning_days !== undefined &&
|
|
188
|
+
baseline_age_days > options.staleness_warning_days;
|
|
189
|
+
const current_entries = results_to_entries(results);
|
|
190
|
+
const baseline_map = new Map(baseline.entries.map((e) => [e.name, e]));
|
|
191
|
+
const current_map = new Map(current_entries.map((e) => [e.name, e]));
|
|
192
|
+
const comparisons = [];
|
|
193
|
+
const regressions = [];
|
|
194
|
+
const improvements = [];
|
|
195
|
+
const unchanged = [];
|
|
196
|
+
const new_tasks = [];
|
|
197
|
+
const removed_tasks = [];
|
|
198
|
+
// Compare tasks that exist in both
|
|
199
|
+
for (const current of current_entries) {
|
|
200
|
+
const baseline_entry = baseline_map.get(current.name);
|
|
201
|
+
if (!baseline_entry) {
|
|
202
|
+
new_tasks.push(current.name);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
// Create minimal stats objects for comparison
|
|
206
|
+
const baseline_stats = {
|
|
207
|
+
mean_ns: baseline_entry.mean_ns,
|
|
208
|
+
std_dev_ns: baseline_entry.std_dev_ns,
|
|
209
|
+
sample_size: baseline_entry.sample_size,
|
|
210
|
+
confidence_interval_ns: stats_confidence_interval_from_summary(baseline_entry.mean_ns, baseline_entry.std_dev_ns, baseline_entry.sample_size),
|
|
211
|
+
};
|
|
212
|
+
const current_stats = {
|
|
213
|
+
mean_ns: current.mean_ns,
|
|
214
|
+
std_dev_ns: current.std_dev_ns,
|
|
215
|
+
sample_size: current.sample_size,
|
|
216
|
+
confidence_interval_ns: stats_confidence_interval_from_summary(current.mean_ns, current.std_dev_ns, current.sample_size),
|
|
217
|
+
};
|
|
218
|
+
const comparison = benchmark_stats_compare(baseline_stats, current_stats);
|
|
219
|
+
const task_comparison = {
|
|
220
|
+
name: current.name,
|
|
221
|
+
baseline: baseline_entry,
|
|
222
|
+
current,
|
|
223
|
+
comparison,
|
|
224
|
+
};
|
|
225
|
+
comparisons.push(task_comparison);
|
|
226
|
+
// Categorize based on comparison result
|
|
227
|
+
// Note: comparison.faster is 'a' (baseline) or 'b' (current)
|
|
228
|
+
if (comparison.significant && comparison.effect_magnitude !== 'negligible') {
|
|
229
|
+
if (comparison.faster === 'a') {
|
|
230
|
+
// Baseline was faster = potential regression
|
|
231
|
+
// Only count as regression if it exceeds the threshold
|
|
232
|
+
if (comparison.speedup_ratio >= regression_threshold) {
|
|
233
|
+
regressions.push(task_comparison);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
unchanged.push(task_comparison);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
else if (comparison.faster === 'b') {
|
|
240
|
+
// Current is faster = improvement
|
|
241
|
+
improvements.push(task_comparison);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
unchanged.push(task_comparison);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
unchanged.push(task_comparison);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Find removed tasks
|
|
252
|
+
for (const baseline_entry of baseline.entries) {
|
|
253
|
+
if (!current_map.has(baseline_entry.name)) {
|
|
254
|
+
removed_tasks.push(baseline_entry.name);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// Sort regressions and improvements by effect size (largest first)
|
|
258
|
+
const sort_by_effect_size = (a, b) => b.comparison.effect_size - a.comparison.effect_size;
|
|
259
|
+
regressions.sort(sort_by_effect_size);
|
|
260
|
+
improvements.sort(sort_by_effect_size);
|
|
261
|
+
return {
|
|
262
|
+
baseline_found: true,
|
|
263
|
+
baseline_timestamp: baseline.timestamp,
|
|
264
|
+
baseline_commit: baseline.git_commit,
|
|
265
|
+
baseline_age_days,
|
|
266
|
+
baseline_stale,
|
|
267
|
+
comparisons,
|
|
268
|
+
regressions,
|
|
269
|
+
improvements,
|
|
270
|
+
unchanged,
|
|
271
|
+
new_tasks,
|
|
272
|
+
removed_tasks,
|
|
273
|
+
};
|
|
274
|
+
};
|
|
275
|
+
/**
|
|
276
|
+
* Format a baseline comparison result as a human-readable string.
|
|
277
|
+
*
|
|
278
|
+
* @param result - Comparison result from benchmark_baseline_compare
|
|
279
|
+
* @returns Formatted string summary
|
|
280
|
+
*/
|
|
281
|
+
export const benchmark_baseline_format = (result) => {
|
|
282
|
+
if (!result.baseline_found) {
|
|
283
|
+
return 'No baseline found. Call benchmark_baseline_save() to create one.';
|
|
284
|
+
}
|
|
285
|
+
const lines = [];
|
|
286
|
+
lines.push(`Comparing against baseline from ${result.baseline_timestamp}`);
|
|
287
|
+
if (result.baseline_commit) {
|
|
288
|
+
lines.push(`Baseline commit: ${result.baseline_commit.slice(0, 8)}`);
|
|
289
|
+
}
|
|
290
|
+
if (result.baseline_age_days !== null) {
|
|
291
|
+
const age_str = result.baseline_age_days < 1
|
|
292
|
+
? 'less than a day'
|
|
293
|
+
: result.baseline_age_days < 2
|
|
294
|
+
? '1 day'
|
|
295
|
+
: `${Math.floor(result.baseline_age_days)} days`;
|
|
296
|
+
lines.push(`Baseline age: ${age_str}${result.baseline_stale ? ' (STALE)' : ''}`);
|
|
297
|
+
}
|
|
298
|
+
lines.push('');
|
|
299
|
+
if (result.regressions.length > 0) {
|
|
300
|
+
lines.push(`Regressions (${result.regressions.length}):`);
|
|
301
|
+
for (const r of result.regressions) {
|
|
302
|
+
const ratio = r.comparison.speedup_ratio.toFixed(2);
|
|
303
|
+
const p = r.comparison.p_value.toFixed(3);
|
|
304
|
+
lines.push(` ${r.name}: ${ratio}x slower (p=${p}, ${r.comparison.effect_magnitude})`);
|
|
305
|
+
}
|
|
306
|
+
lines.push('');
|
|
307
|
+
}
|
|
308
|
+
if (result.improvements.length > 0) {
|
|
309
|
+
lines.push(`Improvements (${result.improvements.length}):`);
|
|
310
|
+
for (const r of result.improvements) {
|
|
311
|
+
const ratio = r.comparison.speedup_ratio.toFixed(2);
|
|
312
|
+
const p = r.comparison.p_value.toFixed(3);
|
|
313
|
+
lines.push(` ${r.name}: ${ratio}x faster (p=${p}, ${r.comparison.effect_magnitude})`);
|
|
314
|
+
}
|
|
315
|
+
lines.push('');
|
|
316
|
+
}
|
|
317
|
+
if (result.unchanged.length > 0) {
|
|
318
|
+
lines.push(`Unchanged (${result.unchanged.length}):`);
|
|
319
|
+
for (const r of result.unchanged) {
|
|
320
|
+
lines.push(` ${r.name}`);
|
|
321
|
+
}
|
|
322
|
+
lines.push('');
|
|
323
|
+
}
|
|
324
|
+
if (result.new_tasks.length > 0) {
|
|
325
|
+
lines.push(`New tasks (${result.new_tasks.length}): ${result.new_tasks.join(', ')}`);
|
|
326
|
+
}
|
|
327
|
+
if (result.removed_tasks.length > 0) {
|
|
328
|
+
lines.push(`Removed tasks (${result.removed_tasks.length}): ${result.removed_tasks.join(', ')}`);
|
|
329
|
+
}
|
|
330
|
+
// Summary line
|
|
331
|
+
const total = result.comparisons.length;
|
|
332
|
+
const summary_parts = [];
|
|
333
|
+
if (result.regressions.length > 0)
|
|
334
|
+
summary_parts.push(`${result.regressions.length} regressions`);
|
|
335
|
+
if (result.improvements.length > 0)
|
|
336
|
+
summary_parts.push(`${result.improvements.length} improvements`);
|
|
337
|
+
if (result.unchanged.length > 0)
|
|
338
|
+
summary_parts.push(`${result.unchanged.length} unchanged`);
|
|
339
|
+
lines.push('');
|
|
340
|
+
lines.push(`Summary: ${summary_parts.join(', ')} (${total} total)`);
|
|
341
|
+
return lines.join('\n');
|
|
342
|
+
};
|
|
343
|
+
/**
|
|
344
|
+
* Format a baseline comparison result as JSON for programmatic consumption.
|
|
345
|
+
*
|
|
346
|
+
* @param result - Comparison result from benchmark_baseline_compare
|
|
347
|
+
* @param options - Formatting options
|
|
348
|
+
* @returns JSON string
|
|
349
|
+
*/
|
|
350
|
+
export const benchmark_baseline_format_json = (result, options = {}) => {
|
|
351
|
+
const output = {
|
|
352
|
+
baseline_found: result.baseline_found,
|
|
353
|
+
baseline_timestamp: result.baseline_timestamp,
|
|
354
|
+
baseline_commit: result.baseline_commit,
|
|
355
|
+
baseline_age_days: result.baseline_age_days,
|
|
356
|
+
baseline_stale: result.baseline_stale,
|
|
357
|
+
summary: {
|
|
358
|
+
total: result.comparisons.length,
|
|
359
|
+
regressions: result.regressions.length,
|
|
360
|
+
improvements: result.improvements.length,
|
|
361
|
+
unchanged: result.unchanged.length,
|
|
362
|
+
new_tasks: result.new_tasks.length,
|
|
363
|
+
removed_tasks: result.removed_tasks.length,
|
|
364
|
+
},
|
|
365
|
+
regressions: result.regressions.map((r) => ({
|
|
366
|
+
name: r.name,
|
|
367
|
+
speedup_ratio: r.comparison.speedup_ratio,
|
|
368
|
+
effect_size: r.comparison.effect_size,
|
|
369
|
+
effect_magnitude: r.comparison.effect_magnitude,
|
|
370
|
+
p_value: r.comparison.p_value,
|
|
371
|
+
baseline_mean_ns: r.baseline.mean_ns,
|
|
372
|
+
current_mean_ns: r.current.mean_ns,
|
|
373
|
+
})),
|
|
374
|
+
improvements: result.improvements.map((r) => ({
|
|
375
|
+
name: r.name,
|
|
376
|
+
speedup_ratio: r.comparison.speedup_ratio,
|
|
377
|
+
effect_size: r.comparison.effect_size,
|
|
378
|
+
effect_magnitude: r.comparison.effect_magnitude,
|
|
379
|
+
p_value: r.comparison.p_value,
|
|
380
|
+
baseline_mean_ns: r.baseline.mean_ns,
|
|
381
|
+
current_mean_ns: r.current.mean_ns,
|
|
382
|
+
})),
|
|
383
|
+
unchanged: result.unchanged.map((r) => r.name),
|
|
384
|
+
new_tasks: result.new_tasks,
|
|
385
|
+
removed_tasks: result.removed_tasks,
|
|
386
|
+
};
|
|
387
|
+
return options.pretty ? JSON.stringify(output, null, '\t') : JSON.stringify(output);
|
|
388
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { BenchmarkResult, BenchmarkGroup } from './benchmark_types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Format results as an ASCII table with percentiles, min/max, and relative performance.
|
|
4
|
+
* All times use the same unit for easy comparison.
|
|
5
|
+
* @param results - Array of benchmark results
|
|
6
|
+
* @returns Formatted table string with enhanced metrics
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* console.log(benchmark_format_table(results));
|
|
11
|
+
* // ┌─────────────┬────────────┬────────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┐
|
|
12
|
+
* // │ Task Name │ ops/sec │ median(μs) │ p75 (μs) │ p90 (μs) │ p95 (μs) │ p99 (μs) │ min (μs) │ max (μs) │ vs Best │
|
|
13
|
+
* // ├─────────────┼────────────┼────────────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┤
|
|
14
|
+
* // │ slugify v2 │ 1,237,144 │ 0.81 │ 0.85 │ 0.89 │ 0.95 │ 1.20 │ 0.72 │ 2.45 │ baseline │
|
|
15
|
+
* // │ slugify │ 261,619 │ 3.82 │ 3.95 │ 4.12 │ 4.35 │ 5.10 │ 3.21 │ 12.45 │ 4.73x │
|
|
16
|
+
* // └─────────────┴────────────┴────────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┘
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare const benchmark_format_table: (results: Array<BenchmarkResult>) => string;
|
|
20
|
+
/**
|
|
21
|
+
* Format results as a Markdown table with key metrics.
|
|
22
|
+
* All times use the same unit for easy comparison.
|
|
23
|
+
* @param results - Array of benchmark results
|
|
24
|
+
* @returns Formatted markdown table string
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* console.log(benchmark_format_markdown(results));
|
|
29
|
+
* // | Task Name | ops/sec | median (μs) | p75 (μs) | p90 (μs) | p95 (μs) | p99 (μs) | min (μs) | max (μs) | vs Best |
|
|
30
|
+
* // |------------|------------|-------------|----------|----------|----------|----------|----------|----------|----------|
|
|
31
|
+
* // | slugify v2 | 1,237,144 | 0.81 | 0.85 | 0.89 | 0.95 | 1.20 | 0.72 | 2.45 | baseline |
|
|
32
|
+
* // | slugify | 261,619 | 3.82 | 3.95 | 4.12 | 4.35 | 5.10 | 3.21 | 12.45 | 4.73x |
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare const benchmark_format_markdown: (results: Array<BenchmarkResult>) => string;
|
|
36
|
+
export interface BenchmarkFormatJsonOptions {
|
|
37
|
+
/** Whether to pretty-print (default: true) */
|
|
38
|
+
pretty?: boolean;
|
|
39
|
+
/** Whether to include raw timings array (default: false, can be large) */
|
|
40
|
+
include_timings?: boolean;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Format results as JSON.
|
|
44
|
+
* @param results - Array of benchmark results
|
|
45
|
+
* @param options - Formatting options
|
|
46
|
+
* @returns JSON string
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* console.log(format_json(results));
|
|
51
|
+
* console.log(format_json(results, {pretty: false}));
|
|
52
|
+
* console.log(format_json(results, {include_timings: true}));
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export declare const benchmark_format_json: (results: Array<BenchmarkResult>, options?: BenchmarkFormatJsonOptions) => string;
|
|
56
|
+
/**
|
|
57
|
+
* Format results as a grouped table with visual separators between groups.
|
|
58
|
+
* @param results - Array of benchmark results
|
|
59
|
+
* @param groups - Array of group definitions
|
|
60
|
+
* @returns Formatted table string with group separators
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```ts
|
|
64
|
+
* const groups = [
|
|
65
|
+
* { name: 'FAST PATHS', filter: (r) => r.name.includes('fast') },
|
|
66
|
+
* { name: 'SLOW PATHS', filter: (r) => r.name.includes('slow') },
|
|
67
|
+
* ];
|
|
68
|
+
* console.log(benchmark_format_table_grouped(results, groups));
|
|
69
|
+
* // 📦 FAST PATHS
|
|
70
|
+
* // ┌────┬─────────────┬────────────┬...┐
|
|
71
|
+
* // │ 🐆 │ fast test 1 │ 1,237,144 │...│
|
|
72
|
+
* // │ 🐇 │ fast test 2 │ 261,619 │...│
|
|
73
|
+
* // └────┴─────────────┴────────────┴...┘
|
|
74
|
+
* //
|
|
75
|
+
* // 📦 SLOW PATHS
|
|
76
|
+
* // ┌────┬─────────────┬────────────┬...┐
|
|
77
|
+
* // │ 🐢 │ slow test 1 │ 10,123 │...│
|
|
78
|
+
* // └────┴─────────────┴────────────┴...┘
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export declare const benchmark_format_table_grouped: (results: Array<BenchmarkResult>, groups: Array<BenchmarkGroup>) => string;
|
|
82
|
+
/**
|
|
83
|
+
* Format a number with fixed decimal places and thousands separators.
|
|
84
|
+
* @see {@link format_number} in maths.ts for the underlying implementation.
|
|
85
|
+
*/
|
|
86
|
+
export declare const benchmark_format_number: (n: number, decimals?: number) => string;
|
|
87
|
+
//# sourceMappingURL=benchmark_format.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"benchmark_format.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/benchmark_format.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,eAAe,EAAE,cAAc,EAAC,MAAM,sBAAsB,CAAC;AAK1E;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,sBAAsB,GAAI,SAAS,KAAK,CAAC,eAAe,CAAC,KAAG,MAiFxE,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,yBAAyB,GAAI,SAAS,KAAK,CAAC,eAAe,CAAC,KAAG,MA4E3E,CAAC;AAEF,MAAM,WAAW,0BAA0B;IAC1C,8CAA8C;IAC9C,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,0EAA0E;IAC1E,eAAe,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB,GACjC,SAAS,KAAK,CAAC,eAAe,CAAC,EAC/B,UAAU,0BAA0B,KAClC,MA6BF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,8BAA8B,GAC1C,SAAS,KAAK,CAAC,eAAe,CAAC,EAC/B,QAAQ,KAAK,CAAC,cAAc,CAAC,KAC3B,MA2BF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,uBAAuB,0CAAgB,CAAC"}
|