@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.
Files changed (59) 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 +388 -0
  12. package/dist/benchmark_format.d.ts +87 -0
  13. package/dist/benchmark_format.d.ts.map +1 -0
  14. package/dist/benchmark_format.js +266 -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 +219 -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/git.d.ts +12 -0
  22. package/dist/git.d.ts.map +1 -1
  23. package/dist/git.js +14 -0
  24. package/dist/library_json.d.ts +3 -3
  25. package/dist/library_json.d.ts.map +1 -1
  26. package/dist/library_json.js +1 -1
  27. package/dist/maths.d.ts +4 -0
  28. package/dist/maths.d.ts.map +1 -1
  29. package/dist/maths.js +8 -0
  30. package/dist/object.js +1 -1
  31. package/dist/source_json.d.ts +4 -4
  32. package/dist/stats.d.ts +180 -0
  33. package/dist/stats.d.ts.map +1 -0
  34. package/dist/stats.js +402 -0
  35. package/dist/string.d.ts +13 -0
  36. package/dist/string.d.ts.map +1 -1
  37. package/dist/string.js +58 -0
  38. package/dist/time.d.ts +165 -0
  39. package/dist/time.d.ts.map +1 -0
  40. package/dist/time.js +264 -0
  41. package/dist/timings.d.ts +1 -7
  42. package/dist/timings.d.ts.map +1 -1
  43. package/dist/timings.js +16 -16
  44. package/package.json +21 -19
  45. package/src/lib/async.ts +3 -3
  46. package/src/lib/benchmark.ts +498 -0
  47. package/src/lib/benchmark_baseline.ts +538 -0
  48. package/src/lib/benchmark_format.ts +314 -0
  49. package/src/lib/benchmark_stats.ts +311 -0
  50. package/src/lib/benchmark_types.ts +197 -0
  51. package/src/lib/git.ts +24 -0
  52. package/src/lib/library_json.ts +3 -3
  53. package/src/lib/maths.ts +8 -0
  54. package/src/lib/object.ts +1 -1
  55. package/src/lib/stats.ts +534 -0
  56. package/src/lib/string.ts +66 -0
  57. package/src/lib/time.ts +319 -0
  58. package/src/lib/timings.ts +17 -17
  59. package/src/lib/types.ts +2 -2
@@ -0,0 +1,314 @@
1
+ import type {BenchmarkResult, BenchmarkGroup} from './benchmark_types.js';
2
+ import {time_unit_detect_best, time_format, TIME_UNIT_DISPLAY} from './time.js';
3
+ import {string_display_width, pad_width} from './string.js';
4
+ import {format_number} from './maths.js';
5
+
6
+ /**
7
+ * Format results as an ASCII table with percentiles, min/max, and relative performance.
8
+ * All times use the same unit for easy comparison.
9
+ * @param results - Array of benchmark results
10
+ * @returns Formatted table string with enhanced metrics
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * console.log(benchmark_format_table(results));
15
+ * // ┌─────────────┬────────────┬────────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┐
16
+ * // │ Task Name │ ops/sec │ median(μs) │ p75 (μs) │ p90 (μs) │ p95 (μs) │ p99 (μs) │ min (μs) │ max (μs) │ vs Best │
17
+ * // ├─────────────┼────────────┼────────────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┤
18
+ * // │ slugify v2 │ 1,237,144 │ 0.81 │ 0.85 │ 0.89 │ 0.95 │ 1.20 │ 0.72 │ 2.45 │ baseline │
19
+ * // │ slugify │ 261,619 │ 3.82 │ 3.95 │ 4.12 │ 4.35 │ 5.10 │ 3.21 │ 12.45 │ 4.73x │
20
+ * // └─────────────┴────────────┴────────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┘
21
+ * ```
22
+ */
23
+ export const benchmark_format_table = (results: Array<BenchmarkResult>): string => {
24
+ if (results.length === 0) return '(no results)';
25
+
26
+ // Detect best unit for all results
27
+ const mean_times = results.map((r) => r.stats.mean_ns);
28
+ const unit = time_unit_detect_best(mean_times);
29
+ const unit_str = TIME_UNIT_DISPLAY[unit];
30
+
31
+ // Find fastest for relative comparison
32
+ const fastest_ops = Math.max(...results.map((r) => r.stats.ops_per_second));
33
+
34
+ const rows: Array<Array<string>> = [];
35
+
36
+ // Header with unit
37
+ rows.push([
38
+ 'Task Name',
39
+ 'ops/sec',
40
+ `median (${unit_str})`,
41
+ `p75 (${unit_str})`,
42
+ `p90 (${unit_str})`,
43
+ `p95 (${unit_str})`,
44
+ `p99 (${unit_str})`,
45
+ `min (${unit_str})`,
46
+ `max (${unit_str})`,
47
+ 'vs Best',
48
+ ]);
49
+
50
+ // Data rows - all use same unit
51
+ results.forEach((r) => {
52
+ const ops_sec = benchmark_format_number(r.stats.ops_per_second, 2);
53
+ const median = time_format(r.stats.median_ns, unit, 2).replace(unit_str, '').trim();
54
+ const p75 = time_format(r.stats.p75_ns, unit, 2).replace(unit_str, '').trim();
55
+ const p90 = time_format(r.stats.p90_ns, unit, 2).replace(unit_str, '').trim();
56
+ const p95 = time_format(r.stats.p95_ns, unit, 2).replace(unit_str, '').trim();
57
+ const p99 = time_format(r.stats.p99_ns, unit, 2).replace(unit_str, '').trim();
58
+ const min = time_format(r.stats.min_ns, unit, 2).replace(unit_str, '').trim();
59
+ const max = time_format(r.stats.max_ns, unit, 2).replace(unit_str, '').trim();
60
+
61
+ // Calculate relative performance
62
+ const ratio = fastest_ops / r.stats.ops_per_second;
63
+ const vs_best = ratio === 1.0 ? 'baseline' : `${ratio.toFixed(2)}x`;
64
+
65
+ rows.push([r.name, ops_sec, median, p75, p90, p95, p99, min, max, vs_best]);
66
+ });
67
+
68
+ // Calculate column widths (using display width for proper emoji handling)
69
+ const widths = rows[0]!.map((_, col_i) => {
70
+ return Math.max(...rows.map((row) => string_display_width(row[col_i]!)));
71
+ });
72
+
73
+ // Build table
74
+ const lines: Array<string> = [];
75
+
76
+ // Top border
77
+ lines.push('┌' + widths.map((w) => '─'.repeat(w + 2)).join('┬') + '┐');
78
+
79
+ // Header
80
+ const header = rows[0]!.map((cell, i) => ' ' + pad_width(cell, widths[i]!) + ' ').join('│');
81
+ lines.push('│' + header + '│');
82
+
83
+ // Header separator
84
+ lines.push('├' + widths.map((w) => '─'.repeat(w + 2)).join('┼') + '┤');
85
+
86
+ // Data rows
87
+ for (let i = 1; i < rows.length; i++) {
88
+ const row = rows[i]!.map((cell, col_i) => {
89
+ const width = widths[col_i]!;
90
+ // Left-align task name, right-align numbers
91
+ if (col_i === 0) {
92
+ return ' ' + pad_width(cell, width, 'left') + ' ';
93
+ } else {
94
+ return ' ' + pad_width(cell, width, 'right') + ' ';
95
+ }
96
+ }).join('│');
97
+ lines.push('│' + row + '│');
98
+ }
99
+
100
+ // Bottom border
101
+ lines.push('└' + widths.map((w) => '─'.repeat(w + 2)).join('┴') + '┘');
102
+
103
+ return lines.join('\n');
104
+ };
105
+
106
+ /**
107
+ * Format results as a Markdown table with key metrics.
108
+ * All times use the same unit for easy comparison.
109
+ * @param results - Array of benchmark results
110
+ * @returns Formatted markdown table string
111
+ *
112
+ * @example
113
+ * ```ts
114
+ * console.log(benchmark_format_markdown(results));
115
+ * // | Task Name | ops/sec | median (μs) | p75 (μs) | p90 (μs) | p95 (μs) | p99 (μs) | min (μs) | max (μs) | vs Best |
116
+ * // |------------|------------|-------------|----------|----------|----------|----------|----------|----------|----------|
117
+ * // | slugify v2 | 1,237,144 | 0.81 | 0.85 | 0.89 | 0.95 | 1.20 | 0.72 | 2.45 | baseline |
118
+ * // | slugify | 261,619 | 3.82 | 3.95 | 4.12 | 4.35 | 5.10 | 3.21 | 12.45 | 4.73x |
119
+ * ```
120
+ */
121
+ export const benchmark_format_markdown = (results: Array<BenchmarkResult>): string => {
122
+ if (results.length === 0) return '(no results)';
123
+
124
+ // Detect best unit for all results
125
+ const mean_times = results.map((r) => r.stats.mean_ns);
126
+ const unit = time_unit_detect_best(mean_times);
127
+ const unit_str = TIME_UNIT_DISPLAY[unit];
128
+
129
+ // Find fastest for relative comparison
130
+ const fastest_ops = Math.max(...results.map((r) => r.stats.ops_per_second));
131
+
132
+ const rows: Array<Array<string>> = [];
133
+
134
+ // Header with unit
135
+ rows.push([
136
+ 'Task Name',
137
+ 'ops/sec',
138
+ `median (${unit_str})`,
139
+ `p75 (${unit_str})`,
140
+ `p90 (${unit_str})`,
141
+ `p95 (${unit_str})`,
142
+ `p99 (${unit_str})`,
143
+ `min (${unit_str})`,
144
+ `max (${unit_str})`,
145
+ 'vs Best',
146
+ ]);
147
+
148
+ // Data rows - all use same unit
149
+ results.forEach((r) => {
150
+ const ops_sec = benchmark_format_number(r.stats.ops_per_second, 2);
151
+ const median = time_format(r.stats.median_ns, unit, 2).replace(unit_str, '').trim();
152
+ const p75 = time_format(r.stats.p75_ns, unit, 2).replace(unit_str, '').trim();
153
+ const p90 = time_format(r.stats.p90_ns, unit, 2).replace(unit_str, '').trim();
154
+ const p95 = time_format(r.stats.p95_ns, unit, 2).replace(unit_str, '').trim();
155
+ const p99 = time_format(r.stats.p99_ns, unit, 2).replace(unit_str, '').trim();
156
+ const min = time_format(r.stats.min_ns, unit, 2).replace(unit_str, '').trim();
157
+ const max = time_format(r.stats.max_ns, unit, 2).replace(unit_str, '').trim();
158
+
159
+ // Calculate relative performance
160
+ const ratio = fastest_ops / r.stats.ops_per_second;
161
+ const vs_best = ratio === 1.0 ? 'baseline' : `${ratio.toFixed(2)}x`;
162
+
163
+ rows.push([r.name, ops_sec, median, p75, p90, p95, p99, min, max, vs_best]);
164
+ });
165
+
166
+ // Calculate column widths
167
+ const widths = rows[0]!.map((_, col_i) => {
168
+ return Math.max(...rows.map((row) => row[col_i]!.length));
169
+ });
170
+
171
+ // Build table
172
+ const lines: Array<string> = [];
173
+
174
+ // Header
175
+ const header = rows[0]!.map((cell, i) => cell.padEnd(widths[i]!)).join(' | ');
176
+ lines.push('| ' + header + ' |');
177
+
178
+ // Separator
179
+ const separator = widths.map((w) => '-'.repeat(w)).join(' | ');
180
+ lines.push('| ' + separator + ' |');
181
+
182
+ // Data rows
183
+ for (let i = 1; i < rows.length; i++) {
184
+ const row = rows[i]!.map((cell, col_i) => {
185
+ const width = widths[col_i]!;
186
+ // Right-align numbers, left-align names
187
+ if (col_i === 0) {
188
+ return cell.padEnd(width);
189
+ } else {
190
+ return cell.padStart(width);
191
+ }
192
+ }).join(' | ');
193
+ lines.push('| ' + row + ' |');
194
+ }
195
+
196
+ return lines.join('\n');
197
+ };
198
+
199
+ export interface BenchmarkFormatJsonOptions {
200
+ /** Whether to pretty-print (default: true) */
201
+ pretty?: boolean;
202
+ /** Whether to include raw timings array (default: false, can be large) */
203
+ include_timings?: boolean;
204
+ }
205
+
206
+ /**
207
+ * Format results as JSON.
208
+ * @param results - Array of benchmark results
209
+ * @param options - Formatting options
210
+ * @returns JSON string
211
+ *
212
+ * @example
213
+ * ```ts
214
+ * console.log(format_json(results));
215
+ * console.log(format_json(results, {pretty: false}));
216
+ * console.log(format_json(results, {include_timings: true}));
217
+ * ```
218
+ */
219
+ export const benchmark_format_json = (
220
+ results: Array<BenchmarkResult>,
221
+ options?: BenchmarkFormatJsonOptions,
222
+ ): string => {
223
+ const pretty = options?.pretty ?? true;
224
+ const include_timings = options?.include_timings ?? false;
225
+ // Flatten stats into result object for easier consumption
226
+ const flattened = results.map((r) => ({
227
+ name: r.name,
228
+ iterations: r.iterations,
229
+ total_time_ms: r.total_time_ms,
230
+ ops_per_second: r.stats.ops_per_second,
231
+ mean_ns: r.stats.mean_ns,
232
+ median_ns: r.stats.median_ns,
233
+ std_dev_ns: r.stats.std_dev_ns,
234
+ min_ns: r.stats.min_ns,
235
+ max_ns: r.stats.max_ns,
236
+ p75_ns: r.stats.p75_ns,
237
+ p90_ns: r.stats.p90_ns,
238
+ p95_ns: r.stats.p95_ns,
239
+ p99_ns: r.stats.p99_ns,
240
+ cv: r.stats.cv,
241
+ confidence_interval_ns: r.stats.confidence_interval_ns,
242
+ outliers: r.stats.outliers_ns.length,
243
+ outlier_ratio: r.stats.outlier_ratio,
244
+ sample_size: r.stats.sample_size,
245
+ raw_sample_size: r.stats.raw_sample_size,
246
+ failed_iterations: r.stats.failed_iterations,
247
+ ...(include_timings ? {timings_ns: r.timings_ns} : {}),
248
+ }));
249
+
250
+ return pretty ? JSON.stringify(flattened, null, 2) : JSON.stringify(flattened);
251
+ };
252
+
253
+ /**
254
+ * Format results as a grouped table with visual separators between groups.
255
+ * @param results - Array of benchmark results
256
+ * @param groups - Array of group definitions
257
+ * @returns Formatted table string with group separators
258
+ *
259
+ * @example
260
+ * ```ts
261
+ * const groups = [
262
+ * { name: 'FAST PATHS', filter: (r) => r.name.includes('fast') },
263
+ * { name: 'SLOW PATHS', filter: (r) => r.name.includes('slow') },
264
+ * ];
265
+ * console.log(benchmark_format_table_grouped(results, groups));
266
+ * // 📦 FAST PATHS
267
+ * // ┌────┬─────────────┬────────────┬...┐
268
+ * // │ 🐆 │ fast test 1 │ 1,237,144 │...│
269
+ * // │ 🐇 │ fast test 2 │ 261,619 │...│
270
+ * // └────┴─────────────┴────────────┴...┘
271
+ * //
272
+ * // 📦 SLOW PATHS
273
+ * // ┌────┬─────────────┬────────────┬...┐
274
+ * // │ 🐢 │ slow test 1 │ 10,123 │...│
275
+ * // └────┴─────────────┴────────────┴...┘
276
+ * ```
277
+ */
278
+ export const benchmark_format_table_grouped = (
279
+ results: Array<BenchmarkResult>,
280
+ groups: Array<BenchmarkGroup>,
281
+ ): string => {
282
+ if (results.length === 0) return '(no results)';
283
+
284
+ const sections: Array<string> = [];
285
+
286
+ for (const group of groups) {
287
+ const group_results = results.filter(group.filter);
288
+ if (group_results.length === 0) continue;
289
+
290
+ // Add group header and table
291
+ const header = group.description
292
+ ? `\n📦 ${group.name}\n ${group.description}`
293
+ : `\n📦 ${group.name}`;
294
+ sections.push(header);
295
+ sections.push(benchmark_format_table(group_results));
296
+ }
297
+
298
+ // Handle ungrouped results (those that don't match any group)
299
+ const grouped_names = new Set(groups.flatMap((g) => results.filter(g.filter).map((r) => r.name)));
300
+ const ungrouped = results.filter((r) => !grouped_names.has(r.name));
301
+
302
+ if (ungrouped.length > 0) {
303
+ sections.push('\n📦 Other');
304
+ sections.push(benchmark_format_table(ungrouped));
305
+ }
306
+
307
+ return sections.join('\n');
308
+ };
309
+
310
+ /**
311
+ * Format a number with fixed decimal places and thousands separators.
312
+ * @see {@link format_number} in maths.ts for the underlying implementation.
313
+ */
314
+ export const benchmark_format_number = format_number;
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Benchmark-specific statistical analysis.
3
+ * Uses the general stats utilities from stats.ts for timing/performance analysis.
4
+ * All timing values are in nanoseconds.
5
+ */
6
+
7
+ import {TIME_NS_PER_SEC, time_format_adaptive} from './time.js';
8
+ import {
9
+ stats_mean,
10
+ stats_median,
11
+ stats_std_dev,
12
+ stats_percentile,
13
+ stats_cv,
14
+ stats_min_max,
15
+ stats_confidence_interval,
16
+ stats_outliers_mad,
17
+ stats_welch_t_test,
18
+ stats_t_distribution_p_value,
19
+ } from './stats.js';
20
+
21
+ /**
22
+ * Minimal stats interface for comparison.
23
+ * This allows comparing stats from different sources (e.g., loaded baselines).
24
+ */
25
+ export interface BenchmarkStatsComparable {
26
+ mean_ns: number;
27
+ std_dev_ns: number;
28
+ sample_size: number;
29
+ confidence_interval_ns: [number, number];
30
+ }
31
+
32
+ /**
33
+ * Effect size magnitude interpretation (Cohen's d).
34
+ */
35
+ export type EffectMagnitude = 'negligible' | 'small' | 'medium' | 'large';
36
+
37
+ /**
38
+ * Result from comparing two benchmark stats.
39
+ */
40
+ export interface BenchmarkComparison {
41
+ /** Which benchmark is faster ('a', 'b', or 'equal' if difference is negligible) */
42
+ faster: 'a' | 'b' | 'equal';
43
+ /** How much faster the winner is (e.g., 1.5 means 1.5x faster) */
44
+ speedup_ratio: number;
45
+ /** Whether the difference is statistically significant at the given alpha */
46
+ significant: boolean;
47
+ /** P-value from Welch's t-test (lower = more confident the difference is real) */
48
+ p_value: number;
49
+ /** Cohen's d effect size (magnitude of difference independent of sample size) */
50
+ effect_size: number;
51
+ /** Interpretation of effect size */
52
+ effect_magnitude: EffectMagnitude;
53
+ /** Whether the 95% confidence intervals overlap */
54
+ ci_overlap: boolean;
55
+ /** Human-readable interpretation of the comparison */
56
+ recommendation: string;
57
+ }
58
+
59
+ /**
60
+ * Options for benchmark comparison.
61
+ */
62
+ export interface BenchmarkCompareOptions {
63
+ /** Significance level for hypothesis testing (default: 0.05) */
64
+ alpha?: number;
65
+ }
66
+
67
+ /**
68
+ * Complete statistical analysis of timing measurements.
69
+ * Includes outlier detection, descriptive statistics, and performance metrics.
70
+ * All timing values are in nanoseconds.
71
+ */
72
+ export class BenchmarkStats {
73
+ /** Mean (average) time in nanoseconds */
74
+ readonly mean_ns: number;
75
+ /** Median time in nanoseconds */
76
+ readonly median_ns: number;
77
+ /** Standard deviation in nanoseconds */
78
+ readonly std_dev_ns: number;
79
+ /** Minimum time in nanoseconds */
80
+ readonly min_ns: number;
81
+ /** Maximum time in nanoseconds */
82
+ readonly max_ns: number;
83
+ /** 75th percentile in nanoseconds */
84
+ readonly p75_ns: number;
85
+ /** 90th percentile in nanoseconds */
86
+ readonly p90_ns: number;
87
+ /** 95th percentile in nanoseconds */
88
+ readonly p95_ns: number;
89
+ /** 99th percentile in nanoseconds */
90
+ readonly p99_ns: number;
91
+ /** Coefficient of variation (std_dev / mean) */
92
+ readonly cv: number;
93
+ /** 95% confidence interval for the mean in nanoseconds */
94
+ readonly confidence_interval_ns: [number, number];
95
+ /** Array of detected outlier values in nanoseconds */
96
+ readonly outliers_ns: Array<number>;
97
+ /** Ratio of outliers to total samples */
98
+ readonly outlier_ratio: number;
99
+ /** Number of samples after outlier removal */
100
+ readonly sample_size: number;
101
+ /** Original number of samples (before outlier removal) */
102
+ readonly raw_sample_size: number;
103
+ /** Operations per second (NS_PER_SEC / mean_ns) */
104
+ readonly ops_per_second: number;
105
+ /** Number of failed iterations (NaN, Infinity, or negative values) */
106
+ readonly failed_iterations: number;
107
+
108
+ constructor(timings_ns: Array<number>) {
109
+ // Filter out invalid values (NaN, Infinity, negative)
110
+ const valid_timings: Array<number> = [];
111
+ let failed_count = 0;
112
+
113
+ for (const t of timings_ns) {
114
+ if (!isNaN(t) && isFinite(t) && t > 0) {
115
+ valid_timings.push(t);
116
+ } else {
117
+ failed_count++;
118
+ }
119
+ }
120
+
121
+ this.failed_iterations = failed_count;
122
+ this.raw_sample_size = timings_ns.length;
123
+
124
+ // If no valid timings, return empty stats
125
+ if (valid_timings.length === 0) {
126
+ this.mean_ns = NaN;
127
+ this.median_ns = NaN;
128
+ this.std_dev_ns = NaN;
129
+ this.min_ns = NaN;
130
+ this.max_ns = NaN;
131
+ this.p75_ns = NaN;
132
+ this.p90_ns = NaN;
133
+ this.p95_ns = NaN;
134
+ this.p99_ns = NaN;
135
+ this.cv = NaN;
136
+ this.confidence_interval_ns = [NaN, NaN];
137
+ this.outliers_ns = [];
138
+ this.outlier_ratio = 0;
139
+ this.sample_size = 0;
140
+ this.ops_per_second = 0;
141
+ return;
142
+ }
143
+
144
+ // Detect and remove outliers
145
+ const {cleaned, outliers} = stats_outliers_mad(valid_timings);
146
+ const sorted_cleaned = [...cleaned].sort((a, b) => a - b);
147
+
148
+ this.outliers_ns = outliers;
149
+ this.outlier_ratio = outliers.length / valid_timings.length;
150
+ this.sample_size = cleaned.length;
151
+
152
+ // Calculate statistics on cleaned data
153
+ this.mean_ns = stats_mean(cleaned);
154
+ this.median_ns = stats_median(sorted_cleaned);
155
+ this.std_dev_ns = stats_std_dev(cleaned, this.mean_ns);
156
+
157
+ const {min, max} = stats_min_max(sorted_cleaned);
158
+ this.min_ns = min;
159
+ this.max_ns = max;
160
+
161
+ this.p75_ns = stats_percentile(sorted_cleaned, 0.75);
162
+ this.p90_ns = stats_percentile(sorted_cleaned, 0.9);
163
+ this.p95_ns = stats_percentile(sorted_cleaned, 0.95);
164
+ this.p99_ns = stats_percentile(sorted_cleaned, 0.99);
165
+
166
+ this.cv = stats_cv(this.mean_ns, this.std_dev_ns);
167
+ this.confidence_interval_ns = stats_confidence_interval(cleaned);
168
+
169
+ // Calculate throughput (operations per second)
170
+ this.ops_per_second = this.mean_ns > 0 ? TIME_NS_PER_SEC / this.mean_ns : 0;
171
+ }
172
+
173
+ /**
174
+ * Format stats as a human-readable string.
175
+ */
176
+ toString(): string {
177
+ return `BenchmarkStats(mean=${time_format_adaptive(this.mean_ns)}, ops/sec=${this.ops_per_second.toFixed(2)}, cv=${(this.cv * 100).toFixed(1)}%, samples=${this.sample_size})`;
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Compare two benchmark results for statistical significance.
183
+ * Uses Welch's t-test (handles unequal variances) and Cohen's d effect size.
184
+ *
185
+ * @param a - First benchmark stats (or any object with required properties)
186
+ * @param b - Second benchmark stats (or any object with required properties)
187
+ * @param options - Comparison options
188
+ * @returns Comparison result with significance, effect size, and recommendation
189
+ *
190
+ * @example
191
+ * ```ts
192
+ * const comparison = benchmark_stats_compare(result_a.stats, result_b.stats);
193
+ * if (comparison.significant) {
194
+ * console.log(`${comparison.faster} is ${comparison.speedup_ratio.toFixed(2)}x faster`);
195
+ * }
196
+ * ```
197
+ */
198
+ export const benchmark_stats_compare = (
199
+ a: BenchmarkStatsComparable,
200
+ b: BenchmarkStatsComparable,
201
+ options?: BenchmarkCompareOptions,
202
+ ): BenchmarkComparison => {
203
+ const alpha = options?.alpha ?? 0.05;
204
+
205
+ // Handle edge cases
206
+ if (a.sample_size === 0 || b.sample_size === 0) {
207
+ return {
208
+ faster: 'equal',
209
+ speedup_ratio: 1,
210
+ significant: false,
211
+ p_value: 1,
212
+ effect_size: 0,
213
+ effect_magnitude: 'negligible',
214
+ ci_overlap: true,
215
+ recommendation: 'Insufficient data for comparison',
216
+ };
217
+ }
218
+
219
+ // Calculate speedup ratio (lower time = faster, so compare by time not ops/sec)
220
+ const speedup_ratio = a.mean_ns < b.mean_ns ? b.mean_ns / a.mean_ns : a.mean_ns / b.mean_ns;
221
+ const faster: 'a' | 'b' | 'equal' =
222
+ a.mean_ns < b.mean_ns ? 'a' : a.mean_ns > b.mean_ns ? 'b' : 'equal';
223
+
224
+ // Welch's t-test (handles unequal variances)
225
+ // Special case: if both have zero variance, t-test is undefined
226
+ let p_value: number;
227
+ if (a.std_dev_ns === 0 && b.std_dev_ns === 0) {
228
+ // When there's no variance, any difference is 100% reliable (p=0) or identical (p=1)
229
+ p_value = a.mean_ns === b.mean_ns ? 1 : 0;
230
+ } else {
231
+ const {t_statistic, degrees_of_freedom} = stats_welch_t_test(
232
+ a.mean_ns,
233
+ a.std_dev_ns,
234
+ a.sample_size,
235
+ b.mean_ns,
236
+ b.std_dev_ns,
237
+ b.sample_size,
238
+ );
239
+ // Calculate two-tailed p-value using t-distribution approximation
240
+ p_value = stats_t_distribution_p_value(Math.abs(t_statistic), degrees_of_freedom);
241
+ }
242
+
243
+ // Cohen's d effect size
244
+ const pooled_std_dev = Math.sqrt(
245
+ ((a.sample_size - 1) * a.std_dev_ns ** 2 + (b.sample_size - 1) * b.std_dev_ns ** 2) /
246
+ (a.sample_size + b.sample_size - 2),
247
+ );
248
+
249
+ // When pooled_std_dev is 0 but means differ, effect is maximal (infinite)
250
+ // When means are equal, effect is 0
251
+ let effect_size: number;
252
+ let effect_magnitude: EffectMagnitude;
253
+
254
+ if (pooled_std_dev === 0) {
255
+ // Zero variance case - if means differ, it's a definitive difference
256
+ if (a.mean_ns === b.mean_ns) {
257
+ effect_size = 0;
258
+ effect_magnitude = 'negligible';
259
+ } else {
260
+ // Any difference is 100% reliable when there's no variance
261
+ effect_size = Infinity;
262
+ effect_magnitude = 'large';
263
+ }
264
+ } else {
265
+ effect_size = Math.abs(a.mean_ns - b.mean_ns) / pooled_std_dev;
266
+ // Interpret effect size (Cohen's conventions)
267
+ effect_magnitude =
268
+ effect_size < 0.2
269
+ ? 'negligible'
270
+ : effect_size < 0.5
271
+ ? 'small'
272
+ : effect_size < 0.8
273
+ ? 'medium'
274
+ : 'large';
275
+ }
276
+
277
+ // Check confidence interval overlap
278
+ const ci_overlap =
279
+ a.confidence_interval_ns[0] <= b.confidence_interval_ns[1] &&
280
+ b.confidence_interval_ns[0] <= a.confidence_interval_ns[1];
281
+
282
+ // Determine significance
283
+ const significant = p_value < alpha;
284
+
285
+ // Generate recommendation
286
+ let recommendation: string;
287
+ if (!significant) {
288
+ recommendation =
289
+ effect_magnitude === 'negligible'
290
+ ? 'No meaningful difference detected'
291
+ : `Difference not statistically significant (p=${p_value.toFixed(3)}), but effect size suggests ${effect_magnitude} practical difference`;
292
+ } else if (effect_magnitude === 'negligible') {
293
+ recommendation = `Statistically significant but negligible practical difference (${speedup_ratio.toFixed(2)}x)`;
294
+ } else {
295
+ recommendation = `${faster === 'a' ? 'First' : 'Second'} is ${speedup_ratio.toFixed(2)}x faster with ${effect_magnitude} effect size (p=${p_value.toFixed(3)})`;
296
+ }
297
+
298
+ // Adjust 'faster' to 'equal' if effect is negligible
299
+ const adjusted_faster = effect_magnitude === 'negligible' ? 'equal' : faster;
300
+
301
+ return {
302
+ faster: adjusted_faster,
303
+ speedup_ratio,
304
+ significant,
305
+ p_value,
306
+ effect_size,
307
+ effect_magnitude,
308
+ ci_overlap,
309
+ recommendation,
310
+ };
311
+ };