@fuzdev/fuz_util 0.42.0 → 0.43.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.
Files changed (46) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +19 -12
  3. package/dist/async.d.ts +2 -2
  4. package/dist/async.d.ts.map +1 -1
  5. package/dist/async.js +2 -2
  6. package/dist/benchmark.d.ts +179 -0
  7. package/dist/benchmark.d.ts.map +1 -0
  8. package/dist/benchmark.js +400 -0
  9. package/dist/benchmark_baseline.d.ts +195 -0
  10. package/dist/benchmark_baseline.d.ts.map +1 -0
  11. package/dist/benchmark_baseline.js +415 -0
  12. package/dist/benchmark_format.d.ts +92 -0
  13. package/dist/benchmark_format.d.ts.map +1 -0
  14. package/dist/benchmark_format.js +327 -0
  15. package/dist/benchmark_stats.d.ts +112 -0
  16. package/dist/benchmark_stats.d.ts.map +1 -0
  17. package/dist/benchmark_stats.js +336 -0
  18. package/dist/benchmark_types.d.ts +174 -0
  19. package/dist/benchmark_types.d.ts.map +1 -0
  20. package/dist/benchmark_types.js +1 -0
  21. package/dist/library_json.d.ts +3 -3
  22. package/dist/library_json.d.ts.map +1 -1
  23. package/dist/library_json.js +1 -1
  24. package/dist/object.js +1 -1
  25. package/dist/stats.d.ts +126 -0
  26. package/dist/stats.d.ts.map +1 -0
  27. package/dist/stats.js +262 -0
  28. package/dist/time.d.ts +161 -0
  29. package/dist/time.d.ts.map +1 -0
  30. package/dist/time.js +260 -0
  31. package/dist/timings.d.ts +1 -7
  32. package/dist/timings.d.ts.map +1 -1
  33. package/dist/timings.js +16 -16
  34. package/package.json +21 -19
  35. package/src/lib/async.ts +3 -3
  36. package/src/lib/benchmark.ts +498 -0
  37. package/src/lib/benchmark_baseline.ts +573 -0
  38. package/src/lib/benchmark_format.ts +379 -0
  39. package/src/lib/benchmark_stats.ts +448 -0
  40. package/src/lib/benchmark_types.ts +197 -0
  41. package/src/lib/library_json.ts +3 -3
  42. package/src/lib/object.ts +1 -1
  43. package/src/lib/stats.ts +353 -0
  44. package/src/lib/time.ts +314 -0
  45. package/src/lib/timings.ts +17 -17
  46. package/src/lib/types.ts +2 -2
@@ -0,0 +1,379 @@
1
+ import type {BenchmarkResult, BenchmarkGroup} from './benchmark_types.js';
2
+ import {time_unit_detect_best, time_format, type TimeUnit} from './time.js';
3
+
4
+ /**
5
+ * Calculate the display width of a string in terminal columns.
6
+ * Emojis and other wide characters take 2 columns.
7
+ */
8
+ const string_display_width = (str: string): number => {
9
+ let width = 0;
10
+ for (const char of str) {
11
+ const code = char.codePointAt(0)!;
12
+ // Emoji and other wide characters (rough heuristic)
13
+ // - Most emoji are in range 0x1F300-0x1FAFF
14
+ // - Some are in 0x2600-0x27BF (misc symbols)
15
+ // - CJK characters 0x4E00-0x9FFF also double-width but not handling here
16
+ if (
17
+ (code >= 0x1f300 && code <= 0x1faff) ||
18
+ (code >= 0x2600 && code <= 0x27bf) ||
19
+ (code >= 0x1f600 && code <= 0x1f64f) ||
20
+ (code >= 0x1f680 && code <= 0x1f6ff)
21
+ ) {
22
+ width += 2;
23
+ } else {
24
+ width += 1;
25
+ }
26
+ }
27
+ return width;
28
+ };
29
+
30
+ /**
31
+ * Pad a string to a target display width (accounting for wide characters).
32
+ */
33
+ const pad_to_width = (
34
+ str: string,
35
+ target_width: number,
36
+ align: 'left' | 'right' = 'left',
37
+ ): string => {
38
+ const current_width = string_display_width(str);
39
+ const padding = Math.max(0, target_width - current_width);
40
+ if (align === 'left') {
41
+ return str + ' '.repeat(padding);
42
+ } else {
43
+ return ' '.repeat(padding) + str;
44
+ }
45
+ };
46
+
47
+ /**
48
+ * Format results as an ASCII table with percentiles, min/max, and relative performance.
49
+ * All times use the same unit for easy comparison.
50
+ * @param results - Array of benchmark results
51
+ * @returns Formatted table string with enhanced metrics
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * console.log(benchmark_format_table(results));
56
+ * // ┌────┬─────────────┬────────────┬────────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┐
57
+ * // │ │ Task Name │ ops/sec │ median(μs) │ p75 (μs) │ p90 (μs) │ p95 (μs) │ p99 (μs) │ min (μs) │ max (μs) │ vs Best │
58
+ * // ├────┼─────────────┼────────────┼────────────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┤
59
+ * // │ 🐇 │ slugify v2 │ 1,237,144 │ 0.81 │ 0.85 │ 0.89 │ 0.95 │ 1.20 │ 0.72 │ 2.45 │ baseline │
60
+ * // │ 🐢 │ slugify │ 261,619 │ 3.82 │ 3.95 │ 4.12 │ 4.35 │ 5.10 │ 3.21 │ 12.45 │ 4.73x │
61
+ * // └────┴─────────────┴────────────┴────────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┘
62
+ * ```
63
+ *
64
+ * **Performance tier animals:**
65
+ * - 🐆 Cheetah: >1M ops/sec (extremely fast)
66
+ * - 🐇 Rabbit: >100K ops/sec (fast)
67
+ * - 🐢 Turtle: >10K ops/sec (moderate)
68
+ * - 🐌 Snail: <10K ops/sec (slow)
69
+ */
70
+ export const benchmark_format_table = (results: Array<BenchmarkResult>): string => {
71
+ if (results.length === 0) return '(no results)';
72
+
73
+ // Detect best unit for all results
74
+ const mean_times = results.map((r) => r.stats.mean_ns);
75
+ const unit = time_unit_detect_best(mean_times);
76
+ const unit_str = UNIT_LABELS[unit];
77
+
78
+ // Find fastest for relative comparison
79
+ const fastest_ops = Math.max(...results.map((r) => r.stats.ops_per_second));
80
+
81
+ const rows: Array<Array<string>> = [];
82
+
83
+ // Header with unit
84
+ rows.push([
85
+ '',
86
+ 'Task Name',
87
+ 'ops/sec',
88
+ `median (${unit_str})`,
89
+ `p75 (${unit_str})`,
90
+ `p90 (${unit_str})`,
91
+ `p95 (${unit_str})`,
92
+ `p99 (${unit_str})`,
93
+ `min (${unit_str})`,
94
+ `max (${unit_str})`,
95
+ 'vs Best',
96
+ ]);
97
+
98
+ // Data rows - all use same unit
99
+ results.forEach((r) => {
100
+ const tier = get_perf_tier(r.stats.ops_per_second);
101
+ const ops_sec = benchmark_format_number(r.stats.ops_per_second, 2);
102
+ const median = time_format(r.stats.median_ns, unit, 2).replace(unit_str, '').trim();
103
+ const p75 = time_format(r.stats.p75_ns, unit, 2).replace(unit_str, '').trim();
104
+ const p90 = time_format(r.stats.p90_ns, unit, 2).replace(unit_str, '').trim();
105
+ const p95 = time_format(r.stats.p95_ns, unit, 2).replace(unit_str, '').trim();
106
+ const p99 = time_format(r.stats.p99_ns, unit, 2).replace(unit_str, '').trim();
107
+ const min = time_format(r.stats.min_ns, unit, 2).replace(unit_str, '').trim();
108
+ const max = time_format(r.stats.max_ns, unit, 2).replace(unit_str, '').trim();
109
+
110
+ // Calculate relative performance
111
+ const ratio = fastest_ops / r.stats.ops_per_second;
112
+ const vs_best = ratio === 1.0 ? 'baseline' : `${ratio.toFixed(2)}x`;
113
+
114
+ rows.push([tier, r.name, ops_sec, median, p75, p90, p95, p99, min, max, vs_best]);
115
+ });
116
+
117
+ // Calculate column widths (using display width for proper emoji handling)
118
+ const widths = rows[0]!.map((_, col_i) => {
119
+ return Math.max(...rows.map((row) => string_display_width(row[col_i]!)));
120
+ });
121
+
122
+ // Build table
123
+ const lines: Array<string> = [];
124
+
125
+ // Top border
126
+ lines.push('┌' + widths.map((w) => '─'.repeat(w + 2)).join('┬') + '┐');
127
+
128
+ // Header
129
+ const header = rows[0]!.map((cell, i) => ' ' + pad_to_width(cell, widths[i]!) + ' ').join('│');
130
+ lines.push('│' + header + '│');
131
+
132
+ // Header separator
133
+ lines.push('├' + widths.map((w) => '─'.repeat(w + 2)).join('┼') + '┤');
134
+
135
+ // Data rows
136
+ for (let i = 1; i < rows.length; i++) {
137
+ const row = rows[i]!.map((cell, col_i) => {
138
+ const width = widths[col_i]!;
139
+ // Left-align tier emoji and task name, right-align numbers
140
+ if (col_i === 0 || col_i === 1) {
141
+ return ' ' + pad_to_width(cell, width, 'left') + ' ';
142
+ } else {
143
+ return ' ' + pad_to_width(cell, width, 'right') + ' ';
144
+ }
145
+ }).join('│');
146
+ lines.push('│' + row + '│');
147
+ }
148
+
149
+ // Bottom border
150
+ lines.push('└' + widths.map((w) => '─'.repeat(w + 2)).join('┴') + '┘');
151
+
152
+ return lines.join('\n');
153
+ };
154
+
155
+ /**
156
+ * Format results as a Markdown table with key metrics.
157
+ * All times use the same unit for easy comparison.
158
+ * @param results - Array of benchmark results
159
+ * @returns Formatted markdown table string
160
+ *
161
+ * @example
162
+ * ```ts
163
+ * console.log(benchmark_format_markdown(results));
164
+ * // | Task Name | ops/sec | median (μs) | p75 (μs) | p90 (μs) | p95 (μs) | p99 (μs) | min (μs) | max (μs) | vs Best |
165
+ * // |------------|------------|-------------|----------|----------|----------|----------|----------|----------|----------|
166
+ * // | slugify v2 | 1,237,144 | 0.81 | 0.85 | 0.89 | 0.95 | 1.20 | 0.72 | 2.45 | baseline |
167
+ * // | slugify | 261,619 | 3.82 | 3.95 | 4.12 | 4.35 | 5.10 | 3.21 | 12.45 | 4.73x |
168
+ * ```
169
+ */
170
+ export const benchmark_format_markdown = (results: Array<BenchmarkResult>): string => {
171
+ if (results.length === 0) return '(no results)';
172
+
173
+ // Detect best unit for all results
174
+ const mean_times = results.map((r) => r.stats.mean_ns);
175
+ const unit = time_unit_detect_best(mean_times);
176
+ const unit_str = UNIT_LABELS[unit];
177
+
178
+ // Find fastest for relative comparison
179
+ const fastest_ops = Math.max(...results.map((r) => r.stats.ops_per_second));
180
+
181
+ const rows: Array<Array<string>> = [];
182
+
183
+ // Header with unit
184
+ rows.push([
185
+ 'Task Name',
186
+ 'ops/sec',
187
+ `median (${unit_str})`,
188
+ `p75 (${unit_str})`,
189
+ `p90 (${unit_str})`,
190
+ `p95 (${unit_str})`,
191
+ `p99 (${unit_str})`,
192
+ `min (${unit_str})`,
193
+ `max (${unit_str})`,
194
+ 'vs Best',
195
+ ]);
196
+
197
+ // Data rows - all use same unit
198
+ results.forEach((r) => {
199
+ const ops_sec = benchmark_format_number(r.stats.ops_per_second, 2);
200
+ const median = time_format(r.stats.median_ns, unit, 2).replace(unit_str, '').trim();
201
+ const p75 = time_format(r.stats.p75_ns, unit, 2).replace(unit_str, '').trim();
202
+ const p90 = time_format(r.stats.p90_ns, unit, 2).replace(unit_str, '').trim();
203
+ const p95 = time_format(r.stats.p95_ns, unit, 2).replace(unit_str, '').trim();
204
+ const p99 = time_format(r.stats.p99_ns, unit, 2).replace(unit_str, '').trim();
205
+ const min = time_format(r.stats.min_ns, unit, 2).replace(unit_str, '').trim();
206
+ const max = time_format(r.stats.max_ns, unit, 2).replace(unit_str, '').trim();
207
+
208
+ // Calculate relative performance
209
+ const ratio = fastest_ops / r.stats.ops_per_second;
210
+ const vs_best = ratio === 1.0 ? 'baseline' : `${ratio.toFixed(2)}x`;
211
+
212
+ rows.push([r.name, ops_sec, median, p75, p90, p95, p99, min, max, vs_best]);
213
+ });
214
+
215
+ // Calculate column widths
216
+ const widths = rows[0]!.map((_, col_i) => {
217
+ return Math.max(...rows.map((row) => row[col_i]!.length));
218
+ });
219
+
220
+ // Build table
221
+ const lines: Array<string> = [];
222
+
223
+ // Header
224
+ const header = rows[0]!.map((cell, i) => cell.padEnd(widths[i]!)).join(' | ');
225
+ lines.push('| ' + header + ' |');
226
+
227
+ // Separator
228
+ const separator = widths.map((w) => '-'.repeat(w)).join(' | ');
229
+ lines.push('| ' + separator + ' |');
230
+
231
+ // Data rows
232
+ for (let i = 1; i < rows.length; i++) {
233
+ const row = rows[i]!.map((cell, col_i) => {
234
+ const width = widths[col_i]!;
235
+ // Right-align numbers, left-align names
236
+ if (col_i === 0) {
237
+ return cell.padEnd(width);
238
+ } else {
239
+ return cell.padStart(width);
240
+ }
241
+ }).join(' | ');
242
+ lines.push('| ' + row + ' |');
243
+ }
244
+
245
+ return lines.join('\n');
246
+ };
247
+
248
+ export interface BenchmarkFormatJsonOptions {
249
+ /** Whether to pretty-print (default: true) */
250
+ pretty?: boolean;
251
+ /** Whether to include raw timings array (default: false, can be large) */
252
+ include_timings?: boolean;
253
+ }
254
+
255
+ /**
256
+ * Format results as JSON.
257
+ * @param results - Array of benchmark results
258
+ * @param options - Formatting options
259
+ * @returns JSON string
260
+ *
261
+ * @example
262
+ * ```ts
263
+ * console.log(format_json(results));
264
+ * console.log(format_json(results, {pretty: false}));
265
+ * console.log(format_json(results, {include_timings: true}));
266
+ * ```
267
+ */
268
+ export const benchmark_format_json = (
269
+ results: Array<BenchmarkResult>,
270
+ options?: BenchmarkFormatJsonOptions,
271
+ ): string => {
272
+ const pretty = options?.pretty ?? true;
273
+ const include_timings = options?.include_timings ?? false;
274
+ // Flatten stats into result object for easier consumption
275
+ const flattened = results.map((r) => ({
276
+ name: r.name,
277
+ iterations: r.iterations,
278
+ total_time_ms: r.total_time_ms,
279
+ ops_per_second: r.stats.ops_per_second,
280
+ mean_ns: r.stats.mean_ns,
281
+ median_ns: r.stats.median_ns,
282
+ std_dev_ns: r.stats.std_dev_ns,
283
+ min_ns: r.stats.min_ns,
284
+ max_ns: r.stats.max_ns,
285
+ p75_ns: r.stats.p75_ns,
286
+ p90_ns: r.stats.p90_ns,
287
+ p95_ns: r.stats.p95_ns,
288
+ p99_ns: r.stats.p99_ns,
289
+ cv: r.stats.cv,
290
+ confidence_interval_ns: r.stats.confidence_interval_ns,
291
+ outliers: r.stats.outliers_ns.length,
292
+ outlier_ratio: r.stats.outlier_ratio,
293
+ sample_size: r.stats.sample_size,
294
+ raw_sample_size: r.stats.raw_sample_size,
295
+ failed_iterations: r.stats.failed_iterations,
296
+ ...(include_timings ? {timings_ns: r.timings_ns} : {}),
297
+ }));
298
+
299
+ return pretty ? JSON.stringify(flattened, null, 2) : JSON.stringify(flattened);
300
+ };
301
+
302
+ /**
303
+ * Format results as a grouped table with visual separators between groups.
304
+ * @param results - Array of benchmark results
305
+ * @param groups - Array of group definitions
306
+ * @returns Formatted table string with group separators
307
+ *
308
+ * @example
309
+ * ```ts
310
+ * const groups = [
311
+ * { name: 'FAST PATHS', filter: (r) => r.name.includes('fast') },
312
+ * { name: 'SLOW PATHS', filter: (r) => r.name.includes('slow') },
313
+ * ];
314
+ * console.log(benchmark_format_table_grouped(results, groups));
315
+ * // 📦 FAST PATHS
316
+ * // ┌────┬─────────────┬────────────┬...┐
317
+ * // │ 🐆 │ fast test 1 │ 1,237,144 │...│
318
+ * // │ 🐇 │ fast test 2 │ 261,619 │...│
319
+ * // └────┴─────────────┴────────────┴...┘
320
+ * //
321
+ * // 📦 SLOW PATHS
322
+ * // ┌────┬─────────────┬────────────┬...┐
323
+ * // │ 🐢 │ slow test 1 │ 10,123 │...│
324
+ * // └────┴─────────────┴────────────┴...┘
325
+ * ```
326
+ */
327
+ export const benchmark_format_table_grouped = (
328
+ results: Array<BenchmarkResult>,
329
+ groups: Array<BenchmarkGroup>,
330
+ ): string => {
331
+ if (results.length === 0) return '(no results)';
332
+
333
+ const sections: Array<string> = [];
334
+
335
+ for (const group of groups) {
336
+ const group_results = results.filter(group.filter);
337
+ if (group_results.length === 0) continue;
338
+
339
+ // Add group header and table
340
+ const header = group.description
341
+ ? `\n📦 ${group.name}\n ${group.description}`
342
+ : `\n📦 ${group.name}`;
343
+ sections.push(header);
344
+ sections.push(benchmark_format_table(group_results));
345
+ }
346
+
347
+ // Handle ungrouped results (those that don't match any group)
348
+ const grouped_names = new Set(groups.flatMap((g) => results.filter(g.filter).map((r) => r.name)));
349
+ const ungrouped = results.filter((r) => !grouped_names.has(r.name));
350
+
351
+ if (ungrouped.length > 0) {
352
+ sections.push('\n📦 Other');
353
+ sections.push(benchmark_format_table(ungrouped));
354
+ }
355
+
356
+ return sections.join('\n');
357
+ };
358
+
359
+ // TODO consider extracting to a general format utility module when more formatters are needed
360
+ /**
361
+ * Format a number with fixed decimal places and thousands separators.
362
+ */
363
+ export const benchmark_format_number = (n: number, decimals: number = 2): string => {
364
+ if (!isFinite(n)) return String(n);
365
+ return n.toFixed(decimals).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
366
+ };
367
+
368
+ /**
369
+ * Get performance tier symbol based on ops/sec.
370
+ */
371
+ const get_perf_tier = (ops_per_sec: number): string => {
372
+ if (ops_per_sec >= 1_000_000) return '🐆'; // > 1M ops/sec (cheetah - extremely fast)
373
+ if (ops_per_sec >= 100_000) return '🐇'; // > 100K ops/sec (rabbit - fast)
374
+ if (ops_per_sec >= 10_000) return '🐢'; // > 10K ops/sec (turtle - moderate)
375
+ return '🐌'; // < 10K ops/sec (snail - slow)
376
+ };
377
+
378
+ /** Unit labels for display (μs instead of us). */
379
+ const UNIT_LABELS: Record<TimeUnit, string> = {ns: 'ns', us: 'μs', ms: 'ms', s: 's'};