Skip to content

Commit 46fbd0c

Browse files
feat: compute and display median and median absolute deviation (#89)
Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
1 parent ab467e2 commit 46fbd0c

File tree

6 files changed

+74
-16
lines changed

6 files changed

+74
-16
lines changed

README.md

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,12 @@ await bench.run();
5353
console.table(bench.table());
5454

5555
// Output:
56-
// ┌─────────┬───────────────┬──────────┬───────────────────────────────┬─────────┐
57-
// │ (index) │ Task Name │ ops/sec │ Average Time (ns) │ Margin │ Samples │
58-
// ├─────────┼───────────────┼──────────┼───────────────────────────────┼─────────┤
59-
//0 │ 'faster task' │ '41,621' │ 24025.791819761525 │ '±20.50%' │ 4257
60-
//1 │ 'slower task' │ '828' │ 1207382.7838323202 │ '±7.07%' │ 83
61-
// └─────────┴───────────────┴──────────┴───────────────────────────────┴─────────┘
56+
// ┌─────────┬───────────────┬──────────┬──────────────────────┬──────────┬──────────────────────────────────┬─────────┐
57+
// │ (index) │ Task name │ ops/sec │ Average time/op (ns) │ Margin │ Median time/op (ns) │ Samples │
58+
// ├─────────┼───────────────┼──────────┼──────────────────────┼──────────┼──────────────────────────────────┼─────────┤
59+
//0 │ 'faster task' │ '38,832' │ 25751.297631307978 │ '±3.48%' │ '22016.49999997812±5.5000000145' │ 3884
60+
//1 │ 'slower task' │ '669' │ 1493338.567164177 │ '±5.98%' │ '1445076.0000000286' │ 67
61+
// └─────────┴───────────────┴──────────┴──────────────────────┴──────────┴──────────────────────────────────┴─────────┘
6262

6363
console.table(bench.table((task) => ({ 'Task name': task.name })));
6464

@@ -282,6 +282,16 @@ export type TaskResult = {
282282
*/
283283
rme: number;
284284

285+
/**
286+
* median absolute deviation
287+
*/
288+
mad: number;
289+
290+
/**
291+
* p50/median percentile
292+
*/
293+
p50: number;
294+
285295
/**
286296
* p75 percentile
287297
*/

examples/src/simple.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ await bench.run();
1818
console.table(bench.table());
1919

2020
// Output:
21-
// ┌─────────┬───────────────┬──────────┬───────────────────────────────┬─────────┐
22-
// │ (index) │ Task Name │ ops/sec │ Average Time (ns) │ Margin │ Samples │
23-
// ├─────────┼───────────────┼──────────┼───────────────────────────────┼─────────┤
24-
// │ 0 │ 'faster task' │ '41,621' │ 24025.791819761525 │ '±20.50%' │ 4257
25-
// │ 1 │ 'slower task' │ '828' │ 1207382.7838323202 │ '±7.07%' │ 83
26-
// └─────────┴───────────────┴──────────┴───────────────────────────────┴─────────┘
21+
// ┌─────────┬───────────────┬──────────┬──────────────────────┬──────────┬──────────────────────────────────┬─────────┐
22+
// │ (index) │ Task name │ ops/sec │ Average time/op (ns) │ Margin │ Median time/op (ns) │ Samples │
23+
// ├─────────┼───────────────┼──────────┼──────────────────────┼──────────┼──────────────────────────────────┼─────────┤
24+
// │ 0 │ 'faster task' │ '38,832' │ 25751.297631307978 │ '±3.48%' │ '22016.49999997812±5.5000000145' │ 3884
25+
// │ 1 │ 'slower task' │ '669' │ 1493338.567164177 │ '±5.98%' │ '1445076.0000000286' │ 67
26+
// └─────────┴───────────────┴──────────┴──────────────────────┴──────────┴──────────────────────────────────┴─────────┘
2727

2828
console.table(
2929
bench.todos.map(({ name }) => ({

src/bench.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,10 +242,11 @@ export default class Bench extends EventTarget {
242242
throw task.result.error;
243243
}
244244
return convert?.(task) || {
245-
'Task Name': task.name,
245+
'Task name': task.name,
246246
'ops/sec': task.result.error ? 'NaN' : parseInt(task.result.hz.toString(), 10).toLocaleString(),
247-
'Average Time (ns)': task.result.error ? 'NaN' : task.result.mean * 1000 * 1000,
247+
'Average time/op (ns)': task.result.error ? 'NaN' : task.result.mean * 1e6,
248248
Margin: task.result.error ? 'NaN' : `\xb1${task.result.rme.toFixed(2)}%`,
249+
'Median time/op (ns)': task.result.error ? 'NaN' : `${task.result.p50 * 1e6}${task.result.mad * 1e6 > 0 ? `\xb1${(task.result.mad * 1e6).toFixed(10)}` : ''}`,
249250
Samples: task.result.error ? 'NaN' : task.result.samples.length,
250251
};
251252
}

src/task.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@ import Bench from './bench';
1010
import tTable from './constants';
1111
import { createBenchEvent } from './event';
1212
import { AddEventListenerOptionsArgument, RemoveEventListenerOptionsArgument } from './types';
13-
import { getVariance, isAsyncTask, quantileSorted } from './utils';
13+
import {
14+
absoluteDeviation,
15+
getVariance,
16+
isAsyncTask,
17+
medianSorted,
18+
quantileSorted,
19+
} from './utils';
1420

1521
/**
1622
* A class that represents each benchmark task in Tinybench. It keeps track of the
@@ -152,6 +158,9 @@ export default class Task extends EventTarget {
152158
const moe = sem * critical;
153159
const rme = (moe / mean) * 100;
154160

161+
const mad = absoluteDeviation(samples, medianSorted);
162+
163+
const p50 = medianSorted(samples);
155164
const p75 = quantileSorted(samples, 0.75);
156165
const p99 = quantileSorted(samples, 0.99);
157166
const p995 = quantileSorted(samples, 0.995);
@@ -176,6 +185,8 @@ export default class Task extends EventTarget {
176185
critical,
177186
moe,
178187
rme,
188+
mad,
189+
p50,
179190
p75,
180191
p99,
181192
p995,

src/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,16 @@ export type TaskResult = {
105105
*/
106106
rme: number;
107107

108+
/**
109+
* median absolute deviation
110+
*/
111+
mad: number;
112+
113+
/**
114+
* p50/median percentile
115+
*/
116+
p50: number;
117+
108118
/**
109119
* p75 percentile
110120
*/

src/utils.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ function isPromiseLike<T>(maybePromiseLike: any): maybePromiseLike is PromiseLik
1919
const AsyncFunctionConstructor = (async () => {}).constructor;
2020

2121
/**
22-
* an async function check method only consider runtime support async syntax
22+
* An async function check method only consider runtime support async syntax
2323
*/
2424
export const isAsyncFunction = (fn: Fn) => fn.constructor === AsyncFunctionConstructor;
2525

@@ -113,3 +113,29 @@ export const quantileSorted = (samples: number[], q: number) => {
113113
}
114114
return samples[baseIndex];
115115
};
116+
117+
/**
118+
* Computes the median of a sorted sample.
119+
*
120+
* @param samples the sorted sample
121+
* @returns the median of the sample
122+
*/
123+
export const medianSorted = (samples: number[]) => quantileSorted(samples, 0.5);
124+
125+
/**
126+
* Computes the absolute deviation of a sample given an aggregation.
127+
*
128+
* @param samples the sample
129+
* @param aggFn the aggregation function to use
130+
* @returns the absolute deviation of the sample given the aggregation
131+
*/
132+
export const absoluteDeviation = (samples: number[], aggFn: (arr: number[]) => number | undefined) => {
133+
const value = aggFn(samples);
134+
const absoluteDeviations: number[] = [];
135+
136+
for (const sample of samples) {
137+
absoluteDeviations.push(Math.abs(sample - value!));
138+
}
139+
140+
return aggFn(absoluteDeviations);
141+
};

0 commit comments

Comments
 (0)