@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.
- 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 +415 -0
- package/dist/benchmark_format.d.ts +92 -0
- package/dist/benchmark_format.d.ts.map +1 -0
- package/dist/benchmark_format.js +327 -0
- package/dist/benchmark_stats.d.ts +112 -0
- package/dist/benchmark_stats.d.ts.map +1 -0
- package/dist/benchmark_stats.js +336 -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/library_json.d.ts +3 -3
- package/dist/library_json.d.ts.map +1 -1
- package/dist/library_json.js +1 -1
- package/dist/object.js +1 -1
- package/dist/stats.d.ts +126 -0
- package/dist/stats.d.ts.map +1 -0
- package/dist/stats.js +262 -0
- package/dist/time.d.ts +161 -0
- package/dist/time.d.ts.map +1 -0
- package/dist/time.js +260 -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 +573 -0
- package/src/lib/benchmark_format.ts +379 -0
- package/src/lib/benchmark_stats.ts +448 -0
- package/src/lib/benchmark_types.ts +197 -0
- package/src/lib/library_json.ts +3 -3
- package/src/lib/object.ts +1 -1
- package/src/lib/stats.ts +353 -0
- package/src/lib/time.ts +314 -0
- package/src/lib/timings.ts +17 -17
- package/src/lib/types.ts +2 -2
package/src/lib/stats.ts
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Statistical analysis utilities.
|
|
3
|
+
* Pure functions with zero dependencies - can be used standalone for any data analysis.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Statistical constants (defaults)
|
|
7
|
+
const DEFAULT_IQR_MULTIPLIER = 1.5;
|
|
8
|
+
const DEFAULT_MAD_Z_SCORE_THRESHOLD = 3.5;
|
|
9
|
+
const DEFAULT_MAD_Z_SCORE_EXTREME = 5.0;
|
|
10
|
+
const DEFAULT_MAD_CONSTANT = 0.6745; // For normal distribution approximation
|
|
11
|
+
const DEFAULT_OUTLIER_RATIO_HIGH = 0.3;
|
|
12
|
+
const DEFAULT_OUTLIER_RATIO_EXTREME = 0.4;
|
|
13
|
+
const DEFAULT_OUTLIER_KEEP_RATIO = 0.8;
|
|
14
|
+
const DEFAULT_CONFIDENCE_Z = 1.96; // 95% confidence
|
|
15
|
+
const DEFAULT_MIN_SAMPLE_SIZE = 3;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Calculate the mean (average) of an array of numbers.
|
|
19
|
+
*/
|
|
20
|
+
export const stats_mean = (values: Array<number>): number => {
|
|
21
|
+
if (values.length === 0) return NaN;
|
|
22
|
+
return values.reduce((sum, val) => sum + val, 0) / values.length;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Calculate the median of an array of numbers.
|
|
27
|
+
*/
|
|
28
|
+
export const stats_median = (values: Array<number>): number => {
|
|
29
|
+
if (values.length === 0) return NaN;
|
|
30
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
31
|
+
const mid = Math.floor(sorted.length / 2);
|
|
32
|
+
return sorted.length % 2 === 0 ? (sorted[mid - 1]! + sorted[mid]!) / 2 : sorted[mid]!;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Calculate the standard deviation of an array of numbers.
|
|
37
|
+
* Uses population standard deviation (divides by n, not n-1).
|
|
38
|
+
* For benchmarks with many samples, this is typically appropriate.
|
|
39
|
+
*/
|
|
40
|
+
export const stats_std_dev = (values: Array<number>, mean?: number): number => {
|
|
41
|
+
if (values.length === 0) return NaN;
|
|
42
|
+
const m = mean ?? stats_mean(values);
|
|
43
|
+
const variance = values.reduce((sum, val) => sum + (val - m) ** 2, 0) / values.length;
|
|
44
|
+
return Math.sqrt(variance);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Calculate the variance of an array of numbers.
|
|
49
|
+
*/
|
|
50
|
+
export const stats_variance = (values: Array<number>, mean?: number): number => {
|
|
51
|
+
if (values.length === 0) return NaN;
|
|
52
|
+
const m = mean ?? stats_mean(values);
|
|
53
|
+
return values.reduce((sum, val) => sum + (val - m) ** 2, 0) / values.length;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Calculate a percentile of an array of numbers using linear interpolation.
|
|
58
|
+
* Uses the "R-7" method (default in R, NumPy, Excel) which interpolates between
|
|
59
|
+
* data points for more accurate percentile estimates, especially with smaller samples.
|
|
60
|
+
* @param values - Array of numbers
|
|
61
|
+
* @param p - Percentile (0-1, e.g., 0.95 for 95th percentile)
|
|
62
|
+
*/
|
|
63
|
+
export const stats_percentile = (values: Array<number>, p: number): number => {
|
|
64
|
+
if (values.length === 0) return NaN;
|
|
65
|
+
if (values.length === 1) return values[0]!;
|
|
66
|
+
|
|
67
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
68
|
+
const n = sorted.length;
|
|
69
|
+
|
|
70
|
+
// R-7 method: index = (n - 1) * p
|
|
71
|
+
const index = (n - 1) * p;
|
|
72
|
+
const lower = Math.floor(index);
|
|
73
|
+
const upper = Math.ceil(index);
|
|
74
|
+
|
|
75
|
+
if (lower === upper) {
|
|
76
|
+
return sorted[lower]!;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Linear interpolation between the two nearest values
|
|
80
|
+
const fraction = index - lower;
|
|
81
|
+
return sorted[lower]! + fraction * (sorted[upper]! - sorted[lower]!);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Calculate the coefficient of variation (CV).
|
|
86
|
+
* CV = standard deviation / mean, expressed as a ratio.
|
|
87
|
+
* Useful for comparing relative variability between datasets.
|
|
88
|
+
*/
|
|
89
|
+
export const stats_cv = (mean: number, std_dev: number): number => {
|
|
90
|
+
if (mean === 0) return NaN;
|
|
91
|
+
return std_dev / mean;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Calculate min and max values.
|
|
96
|
+
*/
|
|
97
|
+
export const stats_min_max = (values: Array<number>): {min: number; max: number} => {
|
|
98
|
+
if (values.length === 0) return {min: NaN, max: NaN};
|
|
99
|
+
let min = values[0]!;
|
|
100
|
+
let max = values[0]!;
|
|
101
|
+
for (let i = 1; i < values.length; i++) {
|
|
102
|
+
const val = values[i]!;
|
|
103
|
+
if (val < min) min = val;
|
|
104
|
+
if (val > max) max = val;
|
|
105
|
+
}
|
|
106
|
+
return {min, max};
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Result from outlier detection.
|
|
111
|
+
*/
|
|
112
|
+
export interface StatsOutlierResult {
|
|
113
|
+
/** Values after removing outliers */
|
|
114
|
+
cleaned: Array<number>;
|
|
115
|
+
/** Detected outlier values */
|
|
116
|
+
outliers: Array<number>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Configuration options for IQR outlier detection.
|
|
121
|
+
*/
|
|
122
|
+
export interface StatsOutliersIqrOptions {
|
|
123
|
+
/** Multiplier for IQR bounds (default: 1.5) */
|
|
124
|
+
iqr_multiplier?: number;
|
|
125
|
+
/** Minimum sample size to perform outlier detection (default: 3) */
|
|
126
|
+
min_sample_size?: number;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Detect outliers using the IQR (Interquartile Range) method.
|
|
131
|
+
* Values outside [Q1 - multiplier*IQR, Q3 + multiplier*IQR] are considered outliers.
|
|
132
|
+
*/
|
|
133
|
+
export const stats_outliers_iqr = (
|
|
134
|
+
values: Array<number>,
|
|
135
|
+
options?: StatsOutliersIqrOptions,
|
|
136
|
+
): StatsOutlierResult => {
|
|
137
|
+
const iqr_multiplier = options?.iqr_multiplier ?? DEFAULT_IQR_MULTIPLIER;
|
|
138
|
+
const min_sample_size = options?.min_sample_size ?? DEFAULT_MIN_SAMPLE_SIZE;
|
|
139
|
+
|
|
140
|
+
if (values.length < min_sample_size) {
|
|
141
|
+
return {cleaned: values, outliers: []};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
145
|
+
const q1 = sorted[Math.floor(sorted.length * 0.25)]!;
|
|
146
|
+
const q3 = sorted[Math.floor(sorted.length * 0.75)]!;
|
|
147
|
+
const iqr = q3 - q1;
|
|
148
|
+
|
|
149
|
+
if (iqr === 0) {
|
|
150
|
+
return {cleaned: values, outliers: []};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const lower_bound = q1 - iqr_multiplier * iqr;
|
|
154
|
+
const upper_bound = q3 + iqr_multiplier * iqr;
|
|
155
|
+
|
|
156
|
+
const cleaned: Array<number> = [];
|
|
157
|
+
const outliers: Array<number> = [];
|
|
158
|
+
|
|
159
|
+
for (const value of values) {
|
|
160
|
+
if (value < lower_bound || value > upper_bound) {
|
|
161
|
+
outliers.push(value);
|
|
162
|
+
} else {
|
|
163
|
+
cleaned.push(value);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {cleaned, outliers};
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Configuration options for MAD outlier detection.
|
|
172
|
+
*/
|
|
173
|
+
export interface StatsOutliersMadOptions {
|
|
174
|
+
/** Modified Z-score threshold for outlier detection (default: 3.5) */
|
|
175
|
+
z_score_threshold?: number;
|
|
176
|
+
/** Extreme Z-score threshold when too many outliers detected (default: 5.0) */
|
|
177
|
+
z_score_extreme?: number;
|
|
178
|
+
/** MAD constant for normal distribution (default: 0.6745) */
|
|
179
|
+
mad_constant?: number;
|
|
180
|
+
/** Ratio threshold to switch to extreme mode (default: 0.3) */
|
|
181
|
+
outlier_ratio_high?: number;
|
|
182
|
+
/** Ratio threshold to switch to keep-closest mode (default: 0.4) */
|
|
183
|
+
outlier_ratio_extreme?: number;
|
|
184
|
+
/** Ratio of values to keep in keep-closest mode (default: 0.8) */
|
|
185
|
+
outlier_keep_ratio?: number;
|
|
186
|
+
/** Minimum sample size to perform outlier detection (default: 3) */
|
|
187
|
+
min_sample_size?: number;
|
|
188
|
+
/** Options to pass to IQR fallback when MAD is zero */
|
|
189
|
+
iqr_options?: StatsOutliersIqrOptions;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Detect outliers using the MAD (Median Absolute Deviation) method.
|
|
194
|
+
* More robust than IQR for skewed distributions.
|
|
195
|
+
* Uses modified Z-score: |0.6745 * (x - median) / MAD|
|
|
196
|
+
* Values with modified Z-score > threshold are considered outliers.
|
|
197
|
+
*/
|
|
198
|
+
export const stats_outliers_mad = (
|
|
199
|
+
values: Array<number>,
|
|
200
|
+
options?: StatsOutliersMadOptions,
|
|
201
|
+
): StatsOutlierResult => {
|
|
202
|
+
const z_score_threshold = options?.z_score_threshold ?? DEFAULT_MAD_Z_SCORE_THRESHOLD;
|
|
203
|
+
const z_score_extreme = options?.z_score_extreme ?? DEFAULT_MAD_Z_SCORE_EXTREME;
|
|
204
|
+
const mad_constant = options?.mad_constant ?? DEFAULT_MAD_CONSTANT;
|
|
205
|
+
const outlier_ratio_high = options?.outlier_ratio_high ?? DEFAULT_OUTLIER_RATIO_HIGH;
|
|
206
|
+
const outlier_ratio_extreme = options?.outlier_ratio_extreme ?? DEFAULT_OUTLIER_RATIO_EXTREME;
|
|
207
|
+
const outlier_keep_ratio = options?.outlier_keep_ratio ?? DEFAULT_OUTLIER_KEEP_RATIO;
|
|
208
|
+
const min_sample_size = options?.min_sample_size ?? DEFAULT_MIN_SAMPLE_SIZE;
|
|
209
|
+
const iqr_options = options?.iqr_options;
|
|
210
|
+
|
|
211
|
+
if (values.length < min_sample_size) {
|
|
212
|
+
return {cleaned: values, outliers: []};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
216
|
+
const median = stats_median(sorted);
|
|
217
|
+
|
|
218
|
+
// Calculate MAD (Median Absolute Deviation)
|
|
219
|
+
const deviations = values.map((v) => Math.abs(v - median));
|
|
220
|
+
const sorted_deviations = [...deviations].sort((a, b) => a - b);
|
|
221
|
+
const mad = stats_median(sorted_deviations);
|
|
222
|
+
|
|
223
|
+
// If MAD is zero, fall back to IQR method
|
|
224
|
+
if (mad === 0) {
|
|
225
|
+
return stats_outliers_iqr(values, iqr_options);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Use modified Z-score with MAD
|
|
229
|
+
let cleaned: Array<number> = [];
|
|
230
|
+
let outliers: Array<number> = [];
|
|
231
|
+
|
|
232
|
+
for (const value of values) {
|
|
233
|
+
const modified_z_score = (mad_constant * (value - median)) / mad;
|
|
234
|
+
if (Math.abs(modified_z_score) > z_score_threshold) {
|
|
235
|
+
outliers.push(value);
|
|
236
|
+
} else {
|
|
237
|
+
cleaned.push(value);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// If too many outliers, increase threshold and try again
|
|
242
|
+
if (outliers.length > values.length * outlier_ratio_high) {
|
|
243
|
+
cleaned = [];
|
|
244
|
+
outliers = [];
|
|
245
|
+
|
|
246
|
+
for (const value of values) {
|
|
247
|
+
const modified_z_score = (mad_constant * (value - median)) / mad;
|
|
248
|
+
if (Math.abs(modified_z_score) > z_score_extreme) {
|
|
249
|
+
outliers.push(value);
|
|
250
|
+
} else {
|
|
251
|
+
cleaned.push(value);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// If still too many outliers, keep closest values to median
|
|
256
|
+
if (outliers.length > values.length * outlier_ratio_extreme) {
|
|
257
|
+
const with_distances = values.map((v) => ({
|
|
258
|
+
value: v,
|
|
259
|
+
distance: Math.abs(v - median),
|
|
260
|
+
}));
|
|
261
|
+
with_distances.sort((a, b) => a.distance - b.distance);
|
|
262
|
+
|
|
263
|
+
const keep_count = Math.floor(values.length * outlier_keep_ratio);
|
|
264
|
+
cleaned = with_distances.slice(0, keep_count).map((d) => d.value);
|
|
265
|
+
outliers = with_distances.slice(keep_count).map((d) => d.value);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return {cleaned, outliers};
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Common z-scores for confidence intervals.
|
|
274
|
+
*/
|
|
275
|
+
export const CONFIDENCE_Z_SCORES: Record<number, number> = {
|
|
276
|
+
0.8: 1.282,
|
|
277
|
+
0.9: 1.645,
|
|
278
|
+
0.95: 1.96,
|
|
279
|
+
0.99: 2.576,
|
|
280
|
+
0.999: 3.291,
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Convert a confidence level (0-1) to a z-score.
|
|
285
|
+
* Uses a lookup table for common values, approximates others.
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* ```ts
|
|
289
|
+
* confidence_level_to_z_score(0.95); // 1.96
|
|
290
|
+
* confidence_level_to_z_score(0.99); // 2.576
|
|
291
|
+
* ```
|
|
292
|
+
*/
|
|
293
|
+
export const confidence_level_to_z_score = (level: number): number => {
|
|
294
|
+
if (level <= 0 || level >= 1) {
|
|
295
|
+
throw new Error('Confidence level must be between 0 and 1 (exclusive)');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Check lookup table first
|
|
299
|
+
if (level in CONFIDENCE_Z_SCORES) {
|
|
300
|
+
return CONFIDENCE_Z_SCORES[level]!;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// For confidence level c, we want z such that P(-z < Z < z) = c
|
|
304
|
+
// This means Φ(z) = (1 + c) / 2, so z = Φ⁻¹((1 + c) / 2)
|
|
305
|
+
// Using Φ⁻¹(p) = √2 * erfinv(2p - 1)
|
|
306
|
+
const p = (1 + level) / 2; // e.g., 0.95 -> 0.975
|
|
307
|
+
const x = 2 * p - 1; // Argument for erfinv, e.g., 0.975 -> 0.95
|
|
308
|
+
|
|
309
|
+
// Winitzki approximation for erfinv
|
|
310
|
+
const a = 0.147;
|
|
311
|
+
const ln_term = Math.log(1 - x * x);
|
|
312
|
+
const term1 = 2 / (Math.PI * a) + ln_term / 2;
|
|
313
|
+
const erfinv = Math.sign(x) * Math.sqrt(Math.sqrt(term1 * term1 - ln_term / a) - term1);
|
|
314
|
+
|
|
315
|
+
return Math.SQRT2 * erfinv;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Configuration options for confidence interval calculation.
|
|
320
|
+
*/
|
|
321
|
+
export interface StatsConfidenceIntervalOptions {
|
|
322
|
+
/** Z-score for confidence level (default: 1.96 for 95% CI) */
|
|
323
|
+
z_score?: number;
|
|
324
|
+
/** Confidence level (0-1), alternative to z_score. If both provided, z_score takes precedence. */
|
|
325
|
+
confidence_level?: number;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Calculate confidence interval for the mean.
|
|
330
|
+
* @param values - Array of numbers
|
|
331
|
+
* @param options - Configuration options
|
|
332
|
+
* @returns [lower_bound, upper_bound]
|
|
333
|
+
*/
|
|
334
|
+
export const stats_confidence_interval = (
|
|
335
|
+
values: Array<number>,
|
|
336
|
+
options?: StatsConfidenceIntervalOptions,
|
|
337
|
+
): [number, number] => {
|
|
338
|
+
// z_score takes precedence, then confidence_level, then default
|
|
339
|
+
const z_score =
|
|
340
|
+
options?.z_score ??
|
|
341
|
+
(options?.confidence_level ? confidence_level_to_z_score(options.confidence_level) : null) ??
|
|
342
|
+
DEFAULT_CONFIDENCE_Z;
|
|
343
|
+
|
|
344
|
+
if (values.length === 0) return [NaN, NaN];
|
|
345
|
+
|
|
346
|
+
const mean = stats_mean(values);
|
|
347
|
+
const std_dev = stats_std_dev(values, mean);
|
|
348
|
+
|
|
349
|
+
const se = std_dev / Math.sqrt(values.length);
|
|
350
|
+
const margin = z_score * se;
|
|
351
|
+
|
|
352
|
+
return [mean - margin, mean + margin];
|
|
353
|
+
};
|
package/src/lib/time.ts
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time utilities.
|
|
3
|
+
* Provides cross-platform high-resolution timing and measurement helpers.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Timer interface for measuring elapsed time.
|
|
8
|
+
* Returns time in nanoseconds for maximum precision.
|
|
9
|
+
*/
|
|
10
|
+
export interface Timer {
|
|
11
|
+
/** Get current time in nanoseconds */
|
|
12
|
+
now: () => number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Node.js high-resolution timer using process.hrtime.bigint().
|
|
17
|
+
* Provides true nanosecond precision.
|
|
18
|
+
*/
|
|
19
|
+
export const timer_node: Timer = {
|
|
20
|
+
now: (): number => {
|
|
21
|
+
const ns = process.hrtime.bigint();
|
|
22
|
+
return Number(ns); // Native nanoseconds
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Browser high-resolution timer using performance.now().
|
|
28
|
+
* Converts milliseconds to nanoseconds for consistent API.
|
|
29
|
+
*
|
|
30
|
+
* **Precision varies by browser due to Spectre/Meltdown mitigations:**
|
|
31
|
+
* - Chrome: ~100μs (coarsened)
|
|
32
|
+
* - Firefox: ~1ms (rounded)
|
|
33
|
+
* - Safari: ~100μs
|
|
34
|
+
* - Node.js: ~1μs
|
|
35
|
+
*
|
|
36
|
+
* For nanosecond-precision benchmarks, use Node.js with `timer_node`.
|
|
37
|
+
*/
|
|
38
|
+
export const timer_browser: Timer = {
|
|
39
|
+
now: (): number => {
|
|
40
|
+
return performance.now() * 1_000_000; // Convert ms to ns
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Detect the best available timer function for the current environment.
|
|
46
|
+
* Called once and cached for performance.
|
|
47
|
+
*/
|
|
48
|
+
const detect_timer_fn = (): (() => number) => {
|
|
49
|
+
// Check if we're in Node.js with hrtime.bigint support
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
51
|
+
if (typeof process !== 'undefined' && process.hrtime) {
|
|
52
|
+
try {
|
|
53
|
+
if (typeof process.hrtime.bigint !== 'undefined') {
|
|
54
|
+
return timer_node.now;
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// Ignore and fall through
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Fallback to performance.now() (works in browsers and modern Node.js)
|
|
61
|
+
if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
|
|
62
|
+
return timer_browser.now;
|
|
63
|
+
}
|
|
64
|
+
// Last resort: Date.now() (millisecond precision only)
|
|
65
|
+
return () => Date.now() * 1_000_000;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Cache the detected timer function
|
|
69
|
+
let _cached_timer_fn: (() => number) | null = null;
|
|
70
|
+
const get_timer_fn = (): (() => number) => {
|
|
71
|
+
if (_cached_timer_fn === null) {
|
|
72
|
+
_cached_timer_fn = detect_timer_fn();
|
|
73
|
+
}
|
|
74
|
+
return _cached_timer_fn;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Auto-detected timer based on environment.
|
|
79
|
+
* Uses process.hrtime in Node.js, performance.now() in browsers.
|
|
80
|
+
* The timer function is detected once and cached for performance.
|
|
81
|
+
*/
|
|
82
|
+
export const timer_default: Timer = {
|
|
83
|
+
now: (): number => get_timer_fn()(),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Time units and conversions.
|
|
88
|
+
*/
|
|
89
|
+
export const TIME_NS_PER_US = 1_000;
|
|
90
|
+
export const TIME_NS_PER_MS = 1_000_000;
|
|
91
|
+
export const TIME_NS_PER_SEC = 1_000_000_000;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Convert nanoseconds to microseconds.
|
|
95
|
+
*/
|
|
96
|
+
export const time_ns_to_us = (ns: number): number => ns / TIME_NS_PER_US;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Convert nanoseconds to milliseconds.
|
|
100
|
+
*/
|
|
101
|
+
export const time_ns_to_ms = (ns: number): number => ns / TIME_NS_PER_MS;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Convert nanoseconds to seconds.
|
|
105
|
+
*/
|
|
106
|
+
export const time_ns_to_sec = (ns: number): number => ns / TIME_NS_PER_SEC;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Time unit for formatting.
|
|
110
|
+
*/
|
|
111
|
+
export type TimeUnit = 'ns' | 'us' | 'ms' | 's';
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Detect the best time unit for a set of nanosecond values.
|
|
115
|
+
* Chooses the unit where most values fall in the range 1-9999.
|
|
116
|
+
* @param values_ns - Array of times in nanoseconds
|
|
117
|
+
* @returns Best unit to use for all values
|
|
118
|
+
*/
|
|
119
|
+
export const time_unit_detect_best = (values_ns: Array<number>): TimeUnit => {
|
|
120
|
+
if (values_ns.length === 0) return 'ms';
|
|
121
|
+
|
|
122
|
+
// Filter out invalid values
|
|
123
|
+
const valid = values_ns.filter((v) => isFinite(v) && v > 0);
|
|
124
|
+
if (valid.length === 0) return 'ms';
|
|
125
|
+
|
|
126
|
+
// Find the median value (more stable than mean for outliers)
|
|
127
|
+
const sorted = [...valid].sort((a, b) => a - b);
|
|
128
|
+
const median = sorted[Math.floor(sorted.length / 2)]!;
|
|
129
|
+
|
|
130
|
+
// Choose unit based on median magnitude
|
|
131
|
+
if (median < 1_000) {
|
|
132
|
+
return 'ns'; // < 1μs
|
|
133
|
+
} else if (median < 1_000_000) {
|
|
134
|
+
return 'us'; // < 1ms
|
|
135
|
+
} else if (median < 1_000_000_000) {
|
|
136
|
+
return 'ms'; // < 1s
|
|
137
|
+
} else {
|
|
138
|
+
return 's'; // >= 1s
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Format time with a specific unit.
|
|
144
|
+
* @param ns - Time in nanoseconds
|
|
145
|
+
* @param unit - Unit to use ('ns', 'us', 'ms', 's')
|
|
146
|
+
* @param decimals - Number of decimal places (default: 2)
|
|
147
|
+
* @returns Formatted string like "3.87μs"
|
|
148
|
+
*/
|
|
149
|
+
export const time_format = (ns: number, unit: TimeUnit, decimals: number = 2): string => {
|
|
150
|
+
if (!isFinite(ns)) return String(ns);
|
|
151
|
+
|
|
152
|
+
switch (unit) {
|
|
153
|
+
case 'ns':
|
|
154
|
+
return `${ns.toFixed(decimals)}ns`;
|
|
155
|
+
case 'us':
|
|
156
|
+
return `${time_ns_to_us(ns).toFixed(decimals)}μs`;
|
|
157
|
+
case 'ms':
|
|
158
|
+
return `${time_ns_to_ms(ns).toFixed(decimals)}ms`;
|
|
159
|
+
case 's':
|
|
160
|
+
return `${time_ns_to_sec(ns).toFixed(decimals)}s`;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Format time with adaptive units (ns/μs/ms/s) based on magnitude.
|
|
166
|
+
* @param ns - Time in nanoseconds
|
|
167
|
+
* @param decimals - Number of decimal places (default: 2)
|
|
168
|
+
* @returns Formatted string like "3.87μs" or "1.23ms"
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```ts
|
|
172
|
+
* time_format_adaptive(1500) // "1.50μs"
|
|
173
|
+
* time_format_adaptive(3870) // "3.87μs"
|
|
174
|
+
* time_format_adaptive(1500000) // "1.50ms"
|
|
175
|
+
* time_format_adaptive(1500000000) // "1.50s"
|
|
176
|
+
* ```
|
|
177
|
+
*/
|
|
178
|
+
export const time_format_adaptive = (ns: number, decimals: number = 2): string => {
|
|
179
|
+
if (!isFinite(ns)) return String(ns);
|
|
180
|
+
|
|
181
|
+
// Choose unit based on magnitude
|
|
182
|
+
if (ns < 1_000) {
|
|
183
|
+
return time_format(ns, 'ns', decimals);
|
|
184
|
+
} else if (ns < 1_000_000) {
|
|
185
|
+
return time_format(ns, 'us', decimals);
|
|
186
|
+
} else if (ns < 1_000_000_000) {
|
|
187
|
+
return time_format(ns, 'ms', decimals);
|
|
188
|
+
} else {
|
|
189
|
+
return time_format(ns, 's', decimals);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Result from timing a function execution.
|
|
195
|
+
* All times in nanoseconds for maximum precision.
|
|
196
|
+
*/
|
|
197
|
+
export interface TimeResult {
|
|
198
|
+
/** Elapsed time in nanoseconds */
|
|
199
|
+
elapsed_ns: number;
|
|
200
|
+
/** Elapsed time in microseconds (convenience) */
|
|
201
|
+
elapsed_us: number;
|
|
202
|
+
/** Elapsed time in milliseconds (convenience) */
|
|
203
|
+
elapsed_ms: number;
|
|
204
|
+
/** Start time in nanoseconds (from timer.now()) */
|
|
205
|
+
started_at_ns: number;
|
|
206
|
+
/** End time in nanoseconds (from timer.now()) */
|
|
207
|
+
ended_at_ns: number;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Time an asynchronous function execution.
|
|
212
|
+
* @param fn - Async function to time
|
|
213
|
+
* @param timer - Timer to use (defaults to timer_default)
|
|
214
|
+
* @returns Object containing the function result and timing information
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* ```ts
|
|
218
|
+
* const {result, timing} = await time_async(async () => {
|
|
219
|
+
* await fetch('https://api.example.com/data');
|
|
220
|
+
* return 42;
|
|
221
|
+
* });
|
|
222
|
+
* console.log(`Result: ${result}, took ${time_format_adaptive(timing.elapsed_ns)}`);
|
|
223
|
+
* ```
|
|
224
|
+
*/
|
|
225
|
+
export const time_async = async <T>(
|
|
226
|
+
fn: () => Promise<T>,
|
|
227
|
+
timer: Timer = timer_default,
|
|
228
|
+
): Promise<{result: T; timing: TimeResult}> => {
|
|
229
|
+
const started_at_ns = timer.now();
|
|
230
|
+
const result = await fn();
|
|
231
|
+
const ended_at_ns = timer.now();
|
|
232
|
+
const elapsed_ns = ended_at_ns - started_at_ns;
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
result,
|
|
236
|
+
timing: {
|
|
237
|
+
elapsed_ns,
|
|
238
|
+
elapsed_us: time_ns_to_us(elapsed_ns),
|
|
239
|
+
elapsed_ms: time_ns_to_ms(elapsed_ns),
|
|
240
|
+
started_at_ns,
|
|
241
|
+
ended_at_ns,
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Time a synchronous function execution.
|
|
248
|
+
* @param fn - Sync function to time
|
|
249
|
+
* @param timer - Timer to use (defaults to timer_default)
|
|
250
|
+
* @returns Object containing the function result and timing information
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* ```ts
|
|
254
|
+
* const {result, timing} = time_sync(() => {
|
|
255
|
+
* return expensive_computation();
|
|
256
|
+
* });
|
|
257
|
+
* console.log(`Result: ${result}, took ${time_format_adaptive(timing.elapsed_ns)}`);
|
|
258
|
+
* ```
|
|
259
|
+
*/
|
|
260
|
+
export const time_sync = <T>(
|
|
261
|
+
fn: () => T,
|
|
262
|
+
timer: Timer = timer_default,
|
|
263
|
+
): {result: T; timing: TimeResult} => {
|
|
264
|
+
const started_at_ns = timer.now();
|
|
265
|
+
const result = fn();
|
|
266
|
+
const ended_at_ns = timer.now();
|
|
267
|
+
const elapsed_ns = ended_at_ns - started_at_ns;
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
result,
|
|
271
|
+
timing: {
|
|
272
|
+
elapsed_ns,
|
|
273
|
+
elapsed_us: time_ns_to_us(elapsed_ns),
|
|
274
|
+
elapsed_ms: time_ns_to_ms(elapsed_ns),
|
|
275
|
+
started_at_ns,
|
|
276
|
+
ended_at_ns,
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Measure multiple executions of a function and return all timings.
|
|
283
|
+
* @param fn - Function to measure (sync or async)
|
|
284
|
+
* @param iterations - Number of times to execute
|
|
285
|
+
* @param timer - Timer to use (defaults to timer_default)
|
|
286
|
+
* @returns Array of elapsed times in nanoseconds
|
|
287
|
+
*
|
|
288
|
+
* @example
|
|
289
|
+
* ```ts
|
|
290
|
+
* const timings_ns = await time_measure(async () => {
|
|
291
|
+
* await process_data();
|
|
292
|
+
* }, 100);
|
|
293
|
+
*
|
|
294
|
+
* import {BenchmarkStats} from './benchmark_stats.js';
|
|
295
|
+
* const stats = new BenchmarkStats(timings_ns);
|
|
296
|
+
* console.log(`Mean: ${time_format_adaptive(stats.mean_ns)}`);
|
|
297
|
+
* ```
|
|
298
|
+
*/
|
|
299
|
+
export const time_measure = async (
|
|
300
|
+
fn: () => unknown,
|
|
301
|
+
iterations: number,
|
|
302
|
+
timer: Timer = timer_default,
|
|
303
|
+
): Promise<Array<number>> => {
|
|
304
|
+
const timings: Array<number> = [];
|
|
305
|
+
|
|
306
|
+
for (let i = 0; i < iterations; i++) {
|
|
307
|
+
const started_at_ns = timer.now();
|
|
308
|
+
await fn(); // eslint-disable-line no-await-in-loop
|
|
309
|
+
const ended_at_ns = timer.now();
|
|
310
|
+
timings.push(ended_at_ns - started_at_ns);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return timings;
|
|
314
|
+
};
|