Skip to content

Commit ab467e2

Browse files
fix: add accurate percentiles approximation computation (#87)
Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
1 parent 1e74996 commit ab467e2

File tree

2 files changed

+62
-14
lines changed

2 files changed

+62
-14
lines changed

src/task.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Bench from './bench';
1010
import tTable from './constants';
1111
import { createBenchEvent } from './event';
1212
import { AddEventListenerOptionsArgument, RemoveEventListenerOptionsArgument } from './types';
13-
import { getVariance, isAsyncTask } from './utils';
13+
import { getVariance, isAsyncTask, quantileSorted } from './utils';
1414

1515
/**
1616
* A class that represents each benchmark task in Tinybench. It keeps track of the
@@ -152,11 +152,10 @@ export default class Task extends EventTarget {
152152
const moe = sem * critical;
153153
const rme = (moe / mean) * 100;
154154

155-
// mitata: https://github.com/evanwashere/mitata/blob/3730a784c9d83289b5627ddd961e3248088612aa/src/lib.mjs#L12
156-
const p75 = samples[Math.ceil(samplesLength * 0.75) - 1]!;
157-
const p99 = samples[Math.ceil(samplesLength * 0.99) - 1]!;
158-
const p995 = samples[Math.ceil(samplesLength * 0.995) - 1]!;
159-
const p999 = samples[Math.ceil(samplesLength * 0.999) - 1]!;
155+
const p75 = quantileSorted(samples, 0.75);
156+
const p99 = quantileSorted(samples, 0.99);
157+
const p995 = quantileSorted(samples, 0.995);
158+
const p999 = quantileSorted(samples, 0.999);
160159

161160
if (this.bench.signal?.aborted) {
162161
return this;

src/utils.ts

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,6 @@ function isPromiseLike<T>(maybePromiseLike: any): maybePromiseLike is PromiseLik
1515
);
1616
}
1717

18-
/**
19-
* Computes the variance of a sample.
20-
*/
21-
export const getVariance = (samples: number[], mean: number) => {
22-
const result = samples.reduce((sum, n) => sum + ((n - mean) ** 2), 0);
23-
return result / (samples.length - 1) || 0;
24-
};
25-
2618
// eslint-disable-next-line @typescript-eslint/no-empty-function
2719
const AsyncFunctionConstructor = (async () => {}).constructor;
2820

@@ -64,3 +56,60 @@ export const isAsyncTask = async (task: Task) => {
6456
return false;
6557
}
6658
};
59+
60+
/**
61+
* Computes the average of a sample.
62+
*
63+
* @param samples the sample
64+
* @returns the average of the sample
65+
*/
66+
export const average = (samples: number[]) => {
67+
if (samples.length === 0) {
68+
throw new Error('samples must not be empty');
69+
}
70+
71+
return samples.reduce((a, b) => a + b, 0) / samples.length || 0;
72+
};
73+
74+
/**
75+
* Computes the variance of a sample with Bessel's correction.
76+
*
77+
* @param samples the sample
78+
* @param avg the average of the sample
79+
* @returns the variance of the sample
80+
*/
81+
export const getVariance = (samples: number[], avg = average(samples)) => {
82+
const result = samples.reduce((sum, n) => sum + ((n - avg) ** 2), 0);
83+
return result / (samples.length - 1) || 0;
84+
};
85+
86+
/**
87+
* Computes the q-quantile of a sorted sample.
88+
*
89+
* @param samples the sorted sample
90+
* @param q the quantile to compute
91+
* @returns the q-quantile of the sample
92+
*/
93+
export const quantileSorted = (samples: number[], q: number) => {
94+
if (samples.length === 0) {
95+
throw new Error('samples must not be empty');
96+
}
97+
if (q < 0 || q > 1) {
98+
throw new Error('q must be between 0 and 1');
99+
}
100+
if (q === 0) {
101+
return samples[0];
102+
}
103+
if (q === 1) {
104+
return samples[samples.length - 1];
105+
}
106+
const base = (samples.length - 1) * q;
107+
const baseIndex = Math.floor(base);
108+
if (samples[baseIndex + 1] != null) {
109+
return (
110+
(samples[baseIndex]!)
111+
+ (base - baseIndex) * ((samples[baseIndex + 1]!) - samples[baseIndex]!)
112+
);
113+
}
114+
return samples[baseIndex];
115+
};

0 commit comments

Comments
 (0)