overtake 0.1.2 → 1.0.0-rc.2

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 (70) hide show
  1. package/README.md +76 -82
  2. package/bin/overtake.js +2 -0
  3. package/build/__tests__/runner.d.ts +1 -0
  4. package/build/benchmark.cjs +237 -0
  5. package/build/benchmark.cjs.map +1 -0
  6. package/build/benchmark.d.ts +64 -0
  7. package/build/benchmark.js +189 -0
  8. package/build/benchmark.js.map +1 -0
  9. package/build/cli.cjs +149 -0
  10. package/build/cli.cjs.map +1 -0
  11. package/build/cli.d.ts +1 -0
  12. package/build/cli.js +104 -0
  13. package/build/cli.js.map +1 -0
  14. package/build/executor.cjs +68 -0
  15. package/build/executor.cjs.map +1 -0
  16. package/build/executor.d.ts +10 -0
  17. package/build/executor.js +58 -0
  18. package/build/executor.js.map +1 -0
  19. package/build/index.cjs +20 -0
  20. package/build/index.cjs.map +1 -0
  21. package/build/index.d.ts +5 -0
  22. package/build/index.js +3 -0
  23. package/build/index.js.map +1 -0
  24. package/build/queue.cjs +48 -0
  25. package/build/queue.cjs.map +1 -0
  26. package/build/queue.d.ts +3 -0
  27. package/build/queue.js +38 -0
  28. package/build/queue.js.map +1 -0
  29. package/build/reporter.cjs +175 -0
  30. package/build/reporter.cjs.map +1 -0
  31. package/build/reporter.d.ts +11 -0
  32. package/build/reporter.js +157 -0
  33. package/build/reporter.js.map +1 -0
  34. package/build/runner.cjs +92 -0
  35. package/build/runner.cjs.map +1 -0
  36. package/build/runner.d.ts +2 -0
  37. package/build/runner.js +82 -0
  38. package/build/runner.js.map +1 -0
  39. package/build/types.cjs +48 -0
  40. package/build/types.cjs.map +1 -0
  41. package/build/types.d.ts +59 -0
  42. package/build/types.js +21 -0
  43. package/build/types.js.map +1 -0
  44. package/build/utils.cjs +100 -0
  45. package/build/utils.cjs.map +1 -0
  46. package/build/utils.d.ts +20 -0
  47. package/build/utils.js +67 -0
  48. package/build/utils.js.map +1 -0
  49. package/build/worker.cjs +29 -0
  50. package/build/worker.cjs.map +1 -0
  51. package/build/worker.d.ts +1 -0
  52. package/build/worker.js +25 -0
  53. package/build/worker.js.map +1 -0
  54. package/package.json +15 -13
  55. package/src/__tests__/runner.ts +34 -0
  56. package/src/benchmark.ts +231 -0
  57. package/src/cli.ts +114 -0
  58. package/src/executor.ts +73 -0
  59. package/src/index.ts +6 -0
  60. package/src/queue.ts +42 -0
  61. package/src/reporter.ts +139 -0
  62. package/src/runner.ts +111 -0
  63. package/src/types.ts +72 -0
  64. package/src/utils.ts +65 -0
  65. package/src/worker.ts +46 -0
  66. package/tsconfig.json +17 -0
  67. package/cli.js +0 -70
  68. package/index.d.ts +0 -56
  69. package/index.js +0 -303
  70. package/runner.js +0 -3
package/package.json CHANGED
@@ -1,18 +1,22 @@
1
1
  {
2
2
  "name": "overtake",
3
- "version": "0.1.2",
3
+ "version": "1.0.0-rc.2",
4
4
  "description": "NodeJS performance benchmark",
5
- "main": "index.js",
6
- "types": "index.d.ts",
5
+ "main": "build/index.js",
6
+ "types": "build/index.d.ts",
7
7
  "type": "module",
8
8
  "bin": {
9
- "overtake": "cli.js"
9
+ "overtake": "bin/overtake.js"
10
10
  },
11
11
  "scripts": {
12
- "start": "./cli.js",
13
- "test": "jest --detectOpenHandles",
12
+ "build": "rm -rf build && inop src build -i __tests__ -i *.tmp.ts && tsc --declaration --emitDeclarationOnly",
13
+ "start": "./bin/overtake.js",
14
+ "test": "jest --detectOpenHandles --passWithNoTests",
14
15
  "prepare": "husky"
15
16
  },
17
+ "engines": {
18
+ "node": ">=22"
19
+ },
16
20
  "repository": {
17
21
  "type": "git",
18
22
  "url": "git+https://github.com/3axap4eHko/overtake.git"
@@ -31,23 +35,21 @@
31
35
  "homepage": "https://github.com/3axap4eHko/overtake#readme",
32
36
  "devDependencies": {
33
37
  "@jest/globals": "^29.7.0",
34
- "@swc/core": "^1.11.24",
35
38
  "@swc/jest": "^0.2.38",
39
+ "@types/async": "^3.2.24",
36
40
  "@types/jest": "^29.5.14",
37
41
  "@types/node": "^22.15.18",
38
- "ajv": "^8.17.1",
39
- "ascertain": "1.2.104",
40
42
  "husky": "^9.1.7",
43
+ "inop": "^0.7.8",
41
44
  "jest": "^29.7.0",
42
- "mongodb": "^6.16.0",
43
- "pg": "^8.16.0",
44
45
  "prettier": "^3.5.3",
45
46
  "pretty-quick": "^4.1.1",
46
- "zod": "^3.24.4"
47
+ "typescript": "^5.8.3"
47
48
  },
48
49
  "dependencies": {
50
+ "@swc/core": "^1.11.24",
51
+ "async": "^3.2.6",
49
52
  "commander": "^13.1.0",
50
- "conode": "^0.2.0",
51
53
  "glob": "^11.0.2"
52
54
  },
53
55
  "packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
@@ -0,0 +1,34 @@
1
+ import { Benchmark } from '../benchmark.js';
2
+
3
+ const benchmark = Benchmark.create('void')
4
+ .feed('strings', () => ['a', 'b', 'c'])
5
+ .feed('numbers', () => [0, 1, 2]);
6
+
7
+ const httpServer = benchmark.target('node http', async () => {
8
+ const { createServer, Server } = await import('node:http');
9
+ const server = createServer();
10
+ return new Promise<InstanceType<typeof Server>>((resolve) => {
11
+ server.on('listen', () => resolve(server));
12
+ });
13
+ });
14
+
15
+ httpServer.teardown((ctx) => {
16
+ ctx.close();
17
+ });
18
+
19
+ httpServer
20
+ .measure('something to bench', async (ctx, input) => {
21
+ ctx.emit('whatever', input);
22
+ })
23
+ .pre(async (ctx, input) => {})
24
+ .post(async (ctx, input) => {});
25
+
26
+ const forLoop = benchmark.target('for loop');
27
+
28
+ forLoop
29
+ .measure('1k', (_, input) => {
30
+ const n = input?.length ?? 0;
31
+ for (let i = 0; i < n; i++) {}
32
+ })
33
+ .pre(async (ctx, input) => {})
34
+ .post(async (ctx, input) => {});
@@ -0,0 +1,231 @@
1
+ import { cpus } from 'node:os';
2
+ import { createExecutor, ExecutorOptions, ExecutorReport } from './executor.js';
3
+ import { MaybePromise, StepFn, SetupFn, TeardownFn, FeedFn, ReportType, ReportTypeList, DEFAULT_CYCLES } from './types.js';
4
+
5
+ export const DEFAULT_WORKERS = cpus().length;
6
+
7
+ export const AsyncFunction = (async () => {}).constructor;
8
+
9
+ export interface TargetReport<R extends ReportTypeList> {
10
+ target: string;
11
+ measures: MeasureReport<R>[];
12
+ }
13
+
14
+ export interface MeasureReport<R extends ReportTypeList> {
15
+ measure: string;
16
+ feeds: FeedReport<R>[];
17
+ }
18
+
19
+ export interface FeedReport<R extends ReportTypeList> {
20
+ feed: string;
21
+ data: ExecutorReport<R>;
22
+ }
23
+
24
+ export const DEFAULT_REPORT_TYPES = ['ops'] as const;
25
+ export type DefaultReportTypes = (typeof DEFAULT_REPORT_TYPES)[number];
26
+
27
+ export class MeasureContext<TContext, TInput> {
28
+ public pre?: StepFn<TContext, TInput>;
29
+ public post?: StepFn<TContext, TInput>;
30
+
31
+ constructor(
32
+ public title: string,
33
+ public run: StepFn<TContext, TInput>,
34
+ ) {}
35
+ }
36
+
37
+ export class Measure<TContext, TInput> {
38
+ #ctx: MeasureContext<TContext, TInput>;
39
+
40
+ constructor(ctx: MeasureContext<TContext, TInput>) {
41
+ this.#ctx = ctx;
42
+ }
43
+
44
+ pre(fn: StepFn<TContext, TInput>): Measure<TContext, TInput> {
45
+ this.#ctx.pre = fn;
46
+ return this;
47
+ }
48
+ post(fn: StepFn<TContext, TInput>): Measure<TContext, TInput> {
49
+ this.#ctx.post = fn;
50
+ return this;
51
+ }
52
+ }
53
+
54
+ export class TargetContext<TContext, TInput> {
55
+ public teardown?: TeardownFn<TContext>;
56
+ public measures: MeasureContext<TContext, TInput>[] = [];
57
+
58
+ constructor(
59
+ readonly title: string,
60
+ readonly setup?: SetupFn<MaybePromise<TContext>>,
61
+ ) {}
62
+ }
63
+
64
+ export class Target<TContext, TInput> {
65
+ #ctx: TargetContext<TContext, TInput>;
66
+
67
+ constructor(ctx: TargetContext<TContext, TInput>) {
68
+ this.#ctx = ctx;
69
+ }
70
+ teardown(fn: TeardownFn<TContext>): Target<TContext, TInput> {
71
+ this.#ctx.teardown = fn;
72
+
73
+ return this;
74
+ }
75
+ measure(title: string, run: StepFn<TContext, TInput>): Measure<TContext, TInput> {
76
+ const measure = new MeasureContext(title, run);
77
+ this.#ctx.measures.push(measure);
78
+
79
+ return new Measure(measure);
80
+ }
81
+ }
82
+
83
+ export class FeedContext<TInput> {
84
+ constructor(
85
+ readonly title: string,
86
+ readonly fn?: FeedFn<TInput>,
87
+ ) {}
88
+ }
89
+
90
+ export class Benchmark<TInput> {
91
+ #targets: TargetContext<unknown, TInput>[] = [];
92
+ #feeds: FeedContext<TInput>[] = [];
93
+ #executed = false;
94
+
95
+ static create(title: string): Benchmark<void>;
96
+ static create<I>(title: string, fn: FeedFn<I>): Benchmark<I>;
97
+ static create<I>(title: string, fn?: FeedFn<I> | undefined): Benchmark<I> {
98
+ if (fn) {
99
+ return new Benchmark(title, fn);
100
+ } else {
101
+ return new Benchmark(title);
102
+ }
103
+ }
104
+
105
+ constructor(title: string);
106
+ constructor(title: string, fn: FeedFn<TInput>);
107
+ constructor(title: string, fn?: FeedFn<TInput> | undefined) {
108
+ if (fn) {
109
+ this.feed(title, fn);
110
+ } else {
111
+ this.feed(title);
112
+ }
113
+ }
114
+
115
+ feed(title: string): Benchmark<TInput | void>;
116
+ feed<I>(title: string, fn: FeedFn<I>): Benchmark<TInput | I>;
117
+ feed<I>(title: string, fn?: FeedFn<I> | undefined): Benchmark<TInput | I> {
118
+ const self = this as Benchmark<TInput | I>;
119
+ self.#feeds.push(fn ? new FeedContext(title, fn) : new FeedContext(title));
120
+
121
+ return self;
122
+ }
123
+
124
+ target<TContext>(title: string): Target<void, TInput>;
125
+ target<TContext>(title: string, setup: SetupFn<Awaited<TContext>>): Target<TContext, TInput>;
126
+ target<TContext>(title: string, setup?: SetupFn<Awaited<TContext>> | undefined): Target<TContext, TInput> {
127
+ const target = new TargetContext<TContext, TInput>(title, setup);
128
+ this.#targets.push(target as TargetContext<unknown, TInput>);
129
+
130
+ return new Target<TContext, TInput>(target);
131
+ }
132
+
133
+ async execute<R extends readonly ReportType[] = typeof DEFAULT_REPORT_TYPES>({
134
+ workers = DEFAULT_WORKERS,
135
+ warmupCycles = 20,
136
+ maxCycles = DEFAULT_CYCLES,
137
+ minCycles = 50,
138
+ absThreshold = 1_000,
139
+ relThreshold = 0.02,
140
+ reportTypes = DEFAULT_REPORT_TYPES as unknown as R,
141
+ }: ExecutorOptions<R>): Promise<TargetReport<R>[]> {
142
+ if (this.#executed) {
143
+ throw new Error("Benchmark is executed and can't be reused");
144
+ }
145
+ this.#executed = true;
146
+
147
+ const executor = createExecutor<unknown, TInput, R>({
148
+ workers,
149
+ warmupCycles,
150
+ maxCycles,
151
+ minCycles,
152
+ absThreshold,
153
+ relThreshold,
154
+ reportTypes,
155
+ });
156
+
157
+ const reports: TargetReport<R>[] = [];
158
+ for (const target of this.#targets) {
159
+ const targetReport: TargetReport<R> = { target: target.title, measures: [] };
160
+ for (const measure of target.measures) {
161
+ const measureReport: MeasureReport<R> = { measure: measure.title, feeds: [] };
162
+ for (const feed of this.#feeds) {
163
+ const data = await feed.fn?.();
164
+ executor
165
+ .push<ExecutorReport<R>>({
166
+ setup: target.setup,
167
+ teardown: target.teardown,
168
+ pre: measure.pre,
169
+ run: measure.run,
170
+ post: measure.post,
171
+ data,
172
+ })
173
+ .then((data) => {
174
+ measureReport.feeds.push({
175
+ feed: feed.title,
176
+ data,
177
+ });
178
+ });
179
+ }
180
+ targetReport.measures.push(measureReport);
181
+ }
182
+ reports.push(targetReport);
183
+ }
184
+ await executor.drain();
185
+ executor.kill();
186
+
187
+ return reports;
188
+ }
189
+ }
190
+
191
+ export const printSimpleReports = <R extends ReportTypeList>(reports: TargetReport<R>[]) => {
192
+ for (const report of reports) {
193
+ for (const { measure, feeds } of report.measures) {
194
+ console.group('\n', report.target, measure);
195
+ for (const { feed, data } of feeds) {
196
+ const output = Object.entries(data)
197
+ .map(([key, report]) => `${key}: ${report.toString()}`)
198
+ .join('; ');
199
+ console.log(feed, output);
200
+ }
201
+ console.groupEnd();
202
+ }
203
+ }
204
+ };
205
+
206
+ export const printTableReports = <R extends ReportTypeList>(reports: TargetReport<R>[]) => {
207
+ for (const report of reports) {
208
+ for (const { measure, feeds } of report.measures) {
209
+ console.log('\n', report.target, measure);
210
+ const table: Record<string, unknown> = {};
211
+ for (const { feed, data } of feeds) {
212
+ table[feed] = Object.fromEntries(Object.entries(data).map(([key, report]) => [key, report.toString()]));
213
+ }
214
+ console.table(table);
215
+ }
216
+ }
217
+ };
218
+
219
+ export const printJSONReports = <R extends ReportTypeList>(reports: TargetReport<R>[], padding?: number) => {
220
+ const output = {} as Record<string, Record<string, Record<string, string>>>;
221
+ for (const report of reports) {
222
+ for (const { measure, feeds } of report.measures) {
223
+ const row = {} as Record<string, Record<string, string>>;
224
+ for (const { feed, data } of feeds) {
225
+ row[feed] = Object.fromEntries(Object.entries(data).map(([key, report]) => [key, report.toString()]));
226
+ }
227
+ output[`${report.target} ${measure}`] = row;
228
+ }
229
+ }
230
+ console.log(JSON.stringify(output, null, padding));
231
+ };
package/src/cli.ts ADDED
@@ -0,0 +1,114 @@
1
+ import { createRequire, Module } from 'node:module';
2
+ import { SyntheticModule, createContext, SourceTextModule, Module as VMModule } from 'node:vm';
3
+ import { stat, readFile } from 'node:fs/promises';
4
+ import { parse, print } from '@swc/core';
5
+ import { Command, Option } from 'commander';
6
+ import { glob } from 'glob';
7
+ import { Benchmark, printTableReports, printJSONReports, printSimpleReports, DEFAULT_REPORT_TYPES, DEFAULT_WORKERS } from './benchmark.js';
8
+ import { REPORT_TYPES } from './types.js';
9
+
10
+ const require = createRequire(import.meta.url);
11
+ const { name, description, version } = require('../package.json');
12
+
13
+ const commander = new Command();
14
+
15
+ const transpile = async (code: string): Promise<string> => {
16
+ const ast = await parse(code, {
17
+ syntax: 'typescript',
18
+ dynamicImport: true,
19
+ target: 'esnext',
20
+ });
21
+
22
+ const output = await print(ast, {
23
+ module: {
24
+ type: 'es6',
25
+ },
26
+ jsc: {
27
+ target: 'esnext',
28
+ parser: {
29
+ syntax: 'typescript',
30
+ },
31
+ experimental: {},
32
+ },
33
+ });
34
+ return output.code;
35
+ };
36
+
37
+ commander
38
+ .name(name)
39
+ .description(description)
40
+ .version(version)
41
+ .argument('<path>', 'glob pattern to find benchmarks')
42
+ .addOption(new Option('-r, --report-types [reportTypes...]', 'statistic types to include in the report').choices(REPORT_TYPES).default(DEFAULT_REPORT_TYPES))
43
+ .addOption(new Option('-w, --workers [workers]', 'number of concurent workers').default(DEFAULT_WORKERS).argParser(parseInt))
44
+ .addOption(new Option('-f, --format [format]', 'output format').default('simple').choices(['simple', 'json', 'pjson', 'table']))
45
+ .addOption(new Option('--abs-threshold [absThreshold]', 'absolute error threshold in nanoseconds').argParser(parseInt))
46
+ .addOption(new Option('--rel-threshold [relThreshold]', 'relative error threshold (fraction between 0 and 1)').argParser(parseInt))
47
+ .addOption(new Option('--warmup-cycles [warmupCycles]', 'number of warmup cycles before measuring').argParser(parseInt))
48
+ .addOption(new Option('--max-cycles [maxCycles]', 'maximum measurement cycles per feed').argParser(parseInt))
49
+ .addOption(new Option('--min-cycles [minCycles]', 'minimum measurement cycles per feed').argParser(parseInt))
50
+ .action(async (path, executeOptions) => {
51
+ const files = await glob(path, { absolute: true, cwd: process.cwd() }).catch(() => []);
52
+ for (const file of files) {
53
+ const stats = await stat(file).catch(() => false as const);
54
+ if (stats && stats.isFile()) {
55
+ const content = await readFile(file, 'utf8');
56
+ const code = await transpile(content);
57
+ let instance: Benchmark<unknown> | undefined;
58
+ const benchmark = (...args: Parameters<(typeof Benchmark)['create']>) => {
59
+ if (instance) {
60
+ throw new Error('Only one benchmark per file is supported');
61
+ }
62
+ instance = Benchmark.create(...args);
63
+ return instance;
64
+ };
65
+ const script = new SourceTextModule(code, {
66
+ context: createContext({ benchmark }),
67
+ });
68
+ const imports = new Map();
69
+ await script.link(async (specifier: string, referencingModule) => {
70
+ if (imports.has(specifier)) {
71
+ return imports.get(specifier);
72
+ }
73
+ const mod = await import(Module.isBuiltin(specifier) ? specifier : require.resolve(specifier));
74
+ const exportNames = Object.keys(mod);
75
+ const imported = new SyntheticModule(
76
+ exportNames,
77
+ () => {
78
+ exportNames.forEach((key) => imported.setExport(key, mod[key]));
79
+ },
80
+ { identifier: specifier, context: referencingModule.context },
81
+ );
82
+
83
+ imports.set(specifier, imported);
84
+ return imported;
85
+ });
86
+ await script.evaluate();
87
+
88
+ if (instance) {
89
+ const reports = await instance.execute(executeOptions);
90
+ switch (executeOptions.format) {
91
+ case 'json':
92
+ {
93
+ printJSONReports(reports);
94
+ }
95
+ break;
96
+ case 'pjson':
97
+ {
98
+ printJSONReports(reports, 2);
99
+ }
100
+ break;
101
+ case 'table':
102
+ {
103
+ printTableReports(reports);
104
+ }
105
+ break;
106
+ default:
107
+ printSimpleReports(reports);
108
+ }
109
+ }
110
+ }
111
+ }
112
+ });
113
+
114
+ commander.parse(process.argv);
@@ -0,0 +1,73 @@
1
+ import { Worker } from 'node:worker_threads';
2
+ import { once } from 'node:events';
3
+ import { queue } from 'async';
4
+ import { RunOptions, ReportOptions, WorkerOptions, BenchmarkOptions, Control, ReportType, ReportTypeList, CONTROL_SLOTS } from './types.js';
5
+ import { createReport, Report } from './reporter.js';
6
+ import { cmp } from './utils.js';
7
+
8
+ export type ExecutorReport<R extends ReportTypeList> = Record<R[number], Report> & { count: number };
9
+
10
+ export interface ExecutorOptions<R extends ReportTypeList> extends BenchmarkOptions, ReportOptions<R> {
11
+ workers?: number;
12
+ maxCycles?: number;
13
+ }
14
+
15
+ export const createExecutor = <TContext, TInput, R extends ReportTypeList>({
16
+ workers,
17
+ warmupCycles,
18
+ maxCycles,
19
+ minCycles,
20
+ absThreshold,
21
+ relThreshold,
22
+ reportTypes,
23
+ }: Required<ExecutorOptions<R>>) => {
24
+ const executor = queue<RunOptions<TContext, TInput>>(async ({ setup, teardown, pre, run, post, data }) => {
25
+ const setupCode = setup?.toString();
26
+ const teardownCode = teardown?.toString();
27
+ const preCode = pre?.toString();
28
+ const runCode = run.toString()!;
29
+ const postCode = post?.toString();
30
+
31
+ const controlSAB = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * CONTROL_SLOTS);
32
+ const durationsSAB = new SharedArrayBuffer(BigUint64Array.BYTES_PER_ELEMENT * maxCycles);
33
+
34
+ const workerFile = new URL('./worker.js', import.meta.url);
35
+ const workerData: WorkerOptions = {
36
+ setupCode,
37
+ teardownCode,
38
+ preCode,
39
+ runCode,
40
+ postCode,
41
+ data,
42
+
43
+ warmupCycles,
44
+ minCycles,
45
+ absThreshold,
46
+ relThreshold,
47
+
48
+ controlSAB,
49
+ durationsSAB,
50
+ };
51
+
52
+ const worker = new Worker(workerFile, {
53
+ workerData,
54
+ });
55
+ const [exitCode] = await once(worker, 'exit');
56
+ if (exitCode !== 0) {
57
+ throw new Error(`worker exited with code ${exitCode}`);
58
+ }
59
+
60
+ const control = new Int32Array(controlSAB);
61
+ const count = control[Control.INDEX];
62
+ const durations = new BigUint64Array(durationsSAB).slice(0, count).sort(cmp);
63
+
64
+ const report = reportTypes.map<[string, unknown]>((type) => [type, createReport(durations, type)] as [ReportType, Report]).concat([['count', count]]);
65
+ return Object.fromEntries(report);
66
+ }, workers);
67
+
68
+ executor.error((err) => {
69
+ console.error(err);
70
+ });
71
+
72
+ return executor;
73
+ };
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from './benchmark.js';
2
+ import { Benchmark as _Benchmark } from './benchmark.js';
3
+
4
+ declare global {
5
+ const benchmark: (typeof _Benchmark)['create'];
6
+ }
package/src/queue.ts ADDED
@@ -0,0 +1,42 @@
1
+ export const createQueue = <T>(worker: (task: T) => Promise<void>, concurency: number = 1) => {
2
+ const queue = new Set<T>();
3
+ const processing = new Map<number, Promise<void>>();
4
+ const iterator = queue[Symbol.iterator]();
5
+
6
+ let next: () => void;
7
+ let counter = 0;
8
+
9
+ queueMicrotask(async () => {
10
+ while (true) {
11
+ if (concurency > 0 && processing.size === concurency) {
12
+ await Promise.race(processing.values());
13
+ }
14
+ if (queue.size === 0) {
15
+ const { promise, resolve } = Promise.withResolvers<void>();
16
+ next = resolve;
17
+ await promise;
18
+ }
19
+ const result = iterator.next();
20
+ if (result.done) {
21
+ break;
22
+ }
23
+ const id = counter++;
24
+ const task = Promise.resolve(worker(result.value))
25
+ .catch(() => {})
26
+ .finally(() => {
27
+ processing.delete(id);
28
+ });
29
+ processing.set(id, task);
30
+ }
31
+ });
32
+
33
+ return {
34
+ push: async (input: T) => {
35
+ queue.add(input);
36
+
37
+ if (queue.size === 0) {
38
+ next?.();
39
+ }
40
+ },
41
+ };
42
+ };
@@ -0,0 +1,139 @@
1
+ import { div, max, divs } from './utils.js';
2
+ import { ReportType } from './types.js';
3
+
4
+ const units = [
5
+ { unit: 'ns', factor: 1 },
6
+ { unit: 'µs', factor: 1e3 },
7
+ { unit: 'ms', factor: 1e6 },
8
+ { unit: 's', factor: 1e9 },
9
+ { unit: 'm', factor: 60 * 1e9 },
10
+ { unit: 'h', factor: 3600 * 1e9 },
11
+ ] as const;
12
+
13
+ function smartFixed(n: number): string {
14
+ return n.toLocaleString(undefined, {
15
+ minimumFractionDigits: 0,
16
+ maximumFractionDigits: 2,
17
+ useGrouping: false,
18
+ });
19
+ }
20
+ export class Report {
21
+ constructor(
22
+ public readonly type: ReportType,
23
+ public readonly value: bigint,
24
+ public readonly uncertainty: number = 0,
25
+ public readonly scale: bigint = 1n,
26
+ ) {}
27
+ valueOf() {
28
+ return Number(div(this.value, this.scale));
29
+ }
30
+ toString() {
31
+ const uncertainty = this.uncertainty ? ` ± ${smartFixed(this.uncertainty)}%` : '';
32
+
33
+ const value = this.valueOf();
34
+ if (this.type === 'ops') {
35
+ return `${smartFixed(value)} ops/s${uncertainty}`;
36
+ }
37
+ let display = value;
38
+ let unit = 'ns';
39
+
40
+ for (const { unit: u, factor } of units) {
41
+ const candidate = value / factor;
42
+ if (candidate < 1000) {
43
+ display = candidate;
44
+ unit = u;
45
+ break;
46
+ }
47
+ }
48
+ return `${smartFixed(display)} ${unit}${uncertainty}`;
49
+ }
50
+ }
51
+
52
+ export const createReport = (durations: BigUint64Array, type: ReportType): Report => {
53
+ const n = durations.length;
54
+ if (n === 0) {
55
+ return new Report(type, 0n);
56
+ }
57
+ switch (type) {
58
+ case 'min': {
59
+ return new Report(type, durations[0]);
60
+ }
61
+ case 'max': {
62
+ return new Report(type, durations[n - 1]);
63
+ }
64
+ case 'median': {
65
+ const mid = Math.floor(n / 2);
66
+ const med = n % 2 === 0 ? (durations[mid - 1] + durations[mid]) / 2n : durations[mid];
67
+ return new Report(type, med);
68
+ }
69
+
70
+ case 'mode': {
71
+ const freq = new Map<bigint, bigint>();
72
+ let maxCount = 0n;
73
+ let modeVal = durations[0];
74
+ for (const d of durations) {
75
+ const count = (freq.get(d) || 0n) + 1n;
76
+ freq.set(d, count);
77
+ if (count > maxCount) {
78
+ maxCount = count;
79
+ modeVal = d;
80
+ }
81
+ }
82
+ let lower = modeVal;
83
+ let upper = modeVal;
84
+ const firstIdx = durations.indexOf(modeVal);
85
+ const lastIdx = durations.lastIndexOf(modeVal);
86
+ if (firstIdx > 0) lower = durations[firstIdx - 1];
87
+ if (lastIdx < n - 1) upper = durations[lastIdx + 1];
88
+ const gap = max(modeVal - lower, upper - modeVal);
89
+ const uncertainty = modeVal > 0 ? Number(((gap / 2n) * 100n) / modeVal) : 0;
90
+ return new Report(type, modeVal, uncertainty);
91
+ }
92
+
93
+ case 'ops': {
94
+ let sum = 0n;
95
+ for (const duration of durations) {
96
+ sum += duration;
97
+ }
98
+ const avgNs = sum / BigInt(n);
99
+ const nsPerSec = 1_000_000_000n;
100
+ const raw = Number(nsPerSec) / Number(avgNs);
101
+ const extra = raw < 1 ? Math.ceil(-Math.log10(raw)) : 0;
102
+
103
+ const exp = raw > 100 ? 0 : 2 + extra;
104
+
105
+ const scale = 10n ** BigInt(exp);
106
+
107
+ const value = avgNs > 0n ? (nsPerSec * scale) / avgNs : 0n;
108
+ const deviation = durations[n - 1] - durations[0];
109
+ const uncertainty = avgNs > 0 ? Number(div(deviation * scale, 2n * avgNs)) : 0;
110
+ return new Report(type, value, uncertainty, scale);
111
+ }
112
+ case 'mean': {
113
+ let sum = 0n;
114
+ for (const duration of durations) {
115
+ sum += duration;
116
+ }
117
+ const value = divs(sum, BigInt(n), 1n);
118
+ return new Report(type, value);
119
+ }
120
+
121
+ default: {
122
+ const p = Number(type.slice(1));
123
+ if (p === 0) {
124
+ return new Report(type, durations[0]);
125
+ }
126
+ if (p === 100) {
127
+ return new Report(type, durations[n - 1]);
128
+ }
129
+ const idx = Math.ceil((p / 100) * n) - 1;
130
+ const value = durations[Math.min(Math.max(idx, 0), n - 1)];
131
+ const prev = idx > 0 ? durations[idx - 1] : value;
132
+ const next = idx < n - 1 ? durations[idx + 1] : value;
133
+ const gap = max(value - prev, next - value);
134
+ const uncertainty = value > 0 ? Number(div(divs(gap, 2n, 100_00n), value)) / 100 : 0;
135
+
136
+ return new Report(type, value, uncertainty);
137
+ }
138
+ }
139
+ };