overtake 1.4.0 → 2.0.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 (57) hide show
  1. package/README.md +4 -15
  2. package/bin/overtake.js +1 -1
  3. package/build/executor.d.ts +7 -3
  4. package/build/index.d.ts +10 -11
  5. package/build/reporter.d.ts +10 -2
  6. package/build/runner.d.ts +1 -1
  7. package/build/types.d.ts +6 -6
  8. package/build/utils.d.ts +1 -17
  9. package/package.json +8 -26
  10. package/src/__tests__/assert-no-closure.ts +135 -0
  11. package/src/__tests__/benchmark-execute.ts +48 -0
  12. package/src/cli.ts +139 -144
  13. package/src/executor.ts +48 -18
  14. package/src/index.ts +85 -57
  15. package/src/reporter.ts +27 -19
  16. package/src/runner.ts +2 -5
  17. package/src/types.ts +8 -8
  18. package/src/utils.ts +15 -54
  19. package/src/worker.ts +6 -3
  20. package/tsconfig.json +3 -1
  21. package/build/cli.cjs +0 -179
  22. package/build/cli.cjs.map +0 -1
  23. package/build/cli.js +0 -134
  24. package/build/cli.js.map +0 -1
  25. package/build/executor.cjs +0 -123
  26. package/build/executor.cjs.map +0 -1
  27. package/build/executor.js +0 -113
  28. package/build/executor.js.map +0 -1
  29. package/build/gc-watcher.cjs +0 -30
  30. package/build/gc-watcher.cjs.map +0 -1
  31. package/build/gc-watcher.js +0 -20
  32. package/build/gc-watcher.js.map +0 -1
  33. package/build/index.cjs +0 -442
  34. package/build/index.cjs.map +0 -1
  35. package/build/index.js +0 -377
  36. package/build/index.js.map +0 -1
  37. package/build/reporter.cjs +0 -311
  38. package/build/reporter.cjs.map +0 -1
  39. package/build/reporter.js +0 -293
  40. package/build/reporter.js.map +0 -1
  41. package/build/runner.cjs +0 -532
  42. package/build/runner.cjs.map +0 -1
  43. package/build/runner.js +0 -522
  44. package/build/runner.js.map +0 -1
  45. package/build/types.cjs +0 -66
  46. package/build/types.cjs.map +0 -1
  47. package/build/types.js +0 -33
  48. package/build/types.js.map +0 -1
  49. package/build/utils.cjs +0 -174
  50. package/build/utils.cjs.map +0 -1
  51. package/build/utils.js +0 -132
  52. package/build/utils.js.map +0 -1
  53. package/build/worker.cjs +0 -155
  54. package/build/worker.cjs.map +0 -1
  55. package/build/worker.js +0 -110
  56. package/build/worker.js.map +0 -1
  57. package/src/__tests__/assert-no-closure.js +0 -134
package/src/cli.ts CHANGED
@@ -1,9 +1,7 @@
1
- import { createRequire, Module } from 'node:module';
2
- import { fileURLToPath, pathToFileURL } from 'node:url';
3
- import { SyntheticModule, createContext, SourceTextModule } from 'node:vm';
4
- import { stat, readFile, writeFile } from 'node:fs/promises';
5
- import { Command, Option } from 'commander';
6
- import { glob } from 'glob';
1
+ import { createRequire, register } from 'node:module';
2
+ import { parseArgs } from 'node:util';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { stat, readFile, writeFile, glob } from 'node:fs/promises';
7
5
  import {
8
6
  Benchmark,
9
7
  printTableReports,
@@ -13,152 +11,149 @@ import {
13
11
  printHistogramReports,
14
12
  printComparisonReports,
15
13
  reportsToBaseline,
16
- BaselineData,
14
+ type BaselineData,
17
15
  DEFAULT_REPORT_TYPES,
18
16
  DEFAULT_WORKERS,
19
- } from './index.js';
20
- import { transpile } from './utils.js';
21
- import { REPORT_TYPES } from './types.js';
17
+ } from './index.ts';
18
+ import { REPORT_TYPES } from './types.ts';
19
+ import { resolveHookUrl } from './utils.ts';
20
+
21
+ register(resolveHookUrl);
22
22
 
23
23
  const require = createRequire(import.meta.url);
24
- const { name, description, version } = require('../package.json');
24
+ const { name, version, description } = require('../package.json');
25
25
  const BENCHMARK_URL = Symbol.for('overtake.benchmarkUrl');
26
26
 
27
- const commander = new Command();
28
-
29
- commander
30
- .name(name)
31
- .description(description)
32
- .version(version)
33
- .argument('<paths...>', 'glob pattern to find benchmarks')
34
- .addOption(new Option('-r, --report-types [reportTypes...]', 'statistic types to include in the report').choices(REPORT_TYPES).default(DEFAULT_REPORT_TYPES))
35
- .addOption(new Option('-w, --workers [workers]', 'number of concurent workers').default(DEFAULT_WORKERS).argParser(parseInt))
36
- .addOption(new Option('-f, --format [format]', 'output format').default('simple').choices(['simple', 'json', 'pjson', 'table', 'markdown', 'histogram']))
37
- .addOption(new Option('--abs-threshold [absThreshold]', 'absolute error threshold in nanoseconds').argParser(parseFloat))
38
- .addOption(new Option('--rel-threshold [relThreshold]', 'relative error threshold (fraction between 0 and 1)').argParser(parseFloat))
39
- .addOption(new Option('--warmup-cycles [warmupCycles]', 'number of warmup cycles before measuring').argParser(parseInt))
40
- .addOption(new Option('--max-cycles [maxCycles]', 'maximum measurement cycles per feed').argParser(parseInt))
41
- .addOption(new Option('--min-cycles [minCycles]', 'minimum measurement cycles per feed').argParser(parseInt))
42
- .addOption(new Option('--no-gc-observer', 'disable GC overlap detection'))
43
- .addOption(new Option('--progress', 'show progress bar during benchmark execution'))
44
- .addOption(new Option('--save-baseline <file>', 'save benchmark results to baseline file'))
45
- .addOption(new Option('--compare-baseline <file>', 'compare results against baseline file'))
46
- .action(async (patterns: string[], executeOptions) => {
47
- let baseline: BaselineData | null = null;
48
- if (executeOptions.compareBaseline) {
49
- try {
50
- const content = await readFile(executeOptions.compareBaseline, 'utf8');
51
- baseline = JSON.parse(content) as BaselineData;
52
- } catch {
53
- console.error(`Warning: Could not load baseline file: ${executeOptions.compareBaseline}`);
27
+ const FORMATS = ['simple', 'json', 'pjson', 'table', 'markdown', 'histogram'] as const;
28
+
29
+ const { values: opts, positionals: patterns } = parseArgs({
30
+ args: process.argv.slice(2),
31
+ allowPositionals: true,
32
+ options: {
33
+ 'report-types': { type: 'string', short: 'r', multiple: true },
34
+ workers: { type: 'string', short: 'w' },
35
+ format: { type: 'string', short: 'f' },
36
+ 'abs-threshold': { type: 'string' },
37
+ 'rel-threshold': { type: 'string' },
38
+ 'warmup-cycles': { type: 'string' },
39
+ 'max-cycles': { type: 'string' },
40
+ 'min-cycles': { type: 'string' },
41
+ 'no-gc-observer': { type: 'boolean' },
42
+ progress: { type: 'boolean' },
43
+ 'save-baseline': { type: 'string' },
44
+ 'compare-baseline': { type: 'string' },
45
+ help: { type: 'boolean', short: 'h' },
46
+ version: { type: 'boolean', short: 'v' },
47
+ },
48
+ });
49
+
50
+ if (opts.version) {
51
+ console.log(version);
52
+ process.exit(0);
53
+ }
54
+
55
+ if (opts.help || patterns.length === 0) {
56
+ console.log(`${name} v${version} - ${description}
57
+
58
+ Usage: overtake [options] <paths...>
59
+
60
+ Options:
61
+ -r, --report-types <type> statistic type, repeat for multiple (-r ops -r p99)
62
+ -w, --workers <n> number of concurrent workers (default: ${DEFAULT_WORKERS})
63
+ -f, --format <format> output format: ${FORMATS.join(', ')} (default: simple)
64
+ --abs-threshold <ns> absolute error threshold in nanoseconds
65
+ --rel-threshold <frac> relative error threshold (0-1)
66
+ --warmup-cycles <n> warmup cycles before measuring
67
+ --max-cycles <n> maximum measurement cycles per feed
68
+ --min-cycles <n> minimum measurement cycles per feed
69
+ --no-gc-observer disable GC overlap detection
70
+ --progress show progress bar
71
+ --save-baseline <file> save results to baseline file
72
+ --compare-baseline <file> compare results against baseline file
73
+ -v, --version show version
74
+ -h, --help show this help`);
75
+ process.exit(0);
76
+ }
77
+
78
+ const reportTypes = opts['report-types']?.length
79
+ ? opts['report-types'].filter((t): t is (typeof REPORT_TYPES)[number] => REPORT_TYPES.includes(t as (typeof REPORT_TYPES)[number]))
80
+ : DEFAULT_REPORT_TYPES;
81
+ const format = opts.format && FORMATS.includes(opts.format as (typeof FORMATS)[number]) ? opts.format : 'simple';
82
+
83
+ const executeOptions = {
84
+ reportTypes,
85
+ workers: opts.workers ? parseInt(opts.workers) : DEFAULT_WORKERS,
86
+ absThreshold: opts['abs-threshold'] ? parseFloat(opts['abs-threshold']) : undefined,
87
+ relThreshold: opts['rel-threshold'] ? parseFloat(opts['rel-threshold']) : undefined,
88
+ warmupCycles: opts['warmup-cycles'] ? parseInt(opts['warmup-cycles']) : undefined,
89
+ maxCycles: opts['max-cycles'] ? parseInt(opts['max-cycles']) : undefined,
90
+ minCycles: opts['min-cycles'] ? parseInt(opts['min-cycles']) : undefined,
91
+ gcObserver: !opts['no-gc-observer'],
92
+ progress: opts.progress ?? false,
93
+ format,
94
+ };
95
+
96
+ let baseline: BaselineData | null = null;
97
+ if (opts['compare-baseline']) {
98
+ try {
99
+ const content = await readFile(opts['compare-baseline'], 'utf8');
100
+ baseline = JSON.parse(content) as BaselineData;
101
+ } catch {
102
+ console.error(`Warning: Could not load baseline file: ${opts['compare-baseline']}`);
103
+ }
104
+ }
105
+
106
+ const files = new Set((await Promise.all(patterns.map((pattern) => Array.fromAsync(glob(pattern, { cwd: process.cwd() })).catch(() => [] as string[])))).flat());
107
+
108
+ for (const file of files) {
109
+ const stats = await stat(file).catch(() => false as const);
110
+ if (stats && stats.isFile()) {
111
+ const identifier = pathToFileURL(file).href;
112
+ let instance: Benchmark<unknown> | undefined;
113
+ (globalThis as any).benchmark = (...args: Parameters<(typeof Benchmark)['create']>) => {
114
+ if (instance) {
115
+ throw new Error('Only one benchmark per file is supported');
54
116
  }
55
- }
117
+ instance = Benchmark.create(...args);
118
+ return instance;
119
+ };
120
+ await import(identifier);
56
121
 
57
- const files = new Set<string>();
58
- await Promise.all(
59
- patterns.map(async (pattern) => {
60
- const matches = await glob(pattern, { absolute: true, cwd: process.cwd() }).catch(() => []);
61
- matches.forEach((file) => files.add(file));
62
- }),
63
- );
64
-
65
- for (const file of files) {
66
- const stats = await stat(file).catch(() => false as const);
67
- if (stats && stats.isFile()) {
68
- const content = await readFile(file, 'utf8');
69
- const identifier = pathToFileURL(file).href;
70
- const code = await transpile(content);
71
- let instance: Benchmark<unknown> | undefined;
72
- const benchmark = (...args: Parameters<(typeof Benchmark)['create']>) => {
73
- if (instance) {
74
- throw new Error('Only one benchmark per file is supported');
75
- }
76
- instance = Benchmark.create(...args);
77
- return instance;
78
- };
79
- const globals = Object.create(null);
80
- for (const k of Object.getOwnPropertyNames(globalThis)) {
81
- globals[k] = (globalThis as any)[k];
82
- }
83
- globals.benchmark = benchmark;
84
- const script = new SourceTextModule(code, {
85
- identifier,
86
- context: createContext(globals),
87
- initializeImportMeta(meta) {
88
- meta.url = identifier;
89
- },
90
- async importModuleDynamically(specifier, referencingModule) {
91
- if (Module.isBuiltin(specifier)) {
92
- return import(specifier);
93
- }
94
- const baseIdentifier = referencingModule.identifier ?? identifier;
95
- const resolveFrom = createRequire(fileURLToPath(baseIdentifier));
96
- const resolved = resolveFrom.resolve(specifier);
97
- return import(resolved);
98
- },
99
- });
100
- const imports = new Map<string, SyntheticModule>();
101
- await script.link(async (specifier: string, referencingModule) => {
102
- const baseIdentifier = referencingModule.identifier ?? identifier;
103
- const resolveFrom = createRequire(fileURLToPath(baseIdentifier));
104
- const target = Module.isBuiltin(specifier) ? specifier : resolveFrom.resolve(specifier);
105
- const cached = imports.get(target);
106
- if (cached) {
107
- return cached;
108
- }
109
- const mod = await import(target);
110
- const exportNames = Object.keys(mod);
111
- const imported = new SyntheticModule(
112
- exportNames,
113
- () => {
114
- exportNames.forEach((key) => imported.setExport(key, mod[key]));
115
- },
116
- { identifier: target, context: referencingModule.context },
117
- );
118
-
119
- imports.set(target, imported);
120
- return imported;
121
- });
122
- await script.evaluate();
123
-
124
- if (instance) {
125
- const reports = await instance.execute({
126
- ...executeOptions,
127
- [BENCHMARK_URL]: identifier,
128
- } as typeof executeOptions);
129
-
130
- if (executeOptions.saveBaseline) {
131
- const baselineData = reportsToBaseline(reports);
132
- await writeFile(executeOptions.saveBaseline, JSON.stringify(baselineData, null, 2));
133
- console.log(`Baseline saved to: ${executeOptions.saveBaseline}`);
134
- }
135
-
136
- if (baseline) {
137
- printComparisonReports(reports, baseline);
138
- } else {
139
- switch (executeOptions.format) {
140
- case 'json':
141
- printJSONReports(reports);
142
- break;
143
- case 'pjson':
144
- printJSONReports(reports, 2);
145
- break;
146
- case 'table':
147
- printTableReports(reports);
148
- break;
149
- case 'markdown':
150
- printMarkdownReports(reports);
151
- break;
152
- case 'histogram':
153
- printHistogramReports(reports);
154
- break;
155
- default:
156
- printSimpleReports(reports);
157
- }
158
- }
122
+ if (instance) {
123
+ const reports = await instance.execute({
124
+ ...executeOptions,
125
+ [BENCHMARK_URL]: identifier,
126
+ } as typeof executeOptions);
127
+
128
+ if (opts['save-baseline']) {
129
+ const baselineData = reportsToBaseline(reports);
130
+ await writeFile(opts['save-baseline'], JSON.stringify(baselineData, null, 2));
131
+ console.log(`Baseline saved to: ${opts['save-baseline']}`);
132
+ }
133
+
134
+ if (baseline) {
135
+ printComparisonReports(reports, baseline);
136
+ } else {
137
+ switch (format) {
138
+ case 'json':
139
+ printJSONReports(reports);
140
+ break;
141
+ case 'pjson':
142
+ printJSONReports(reports, 2);
143
+ break;
144
+ case 'table':
145
+ printTableReports(reports);
146
+ break;
147
+ case 'markdown':
148
+ printMarkdownReports(reports);
149
+ break;
150
+ case 'histogram':
151
+ printHistogramReports(reports);
152
+ break;
153
+ default:
154
+ printSimpleReports(reports);
159
155
  }
160
156
  }
161
157
  }
162
- });
163
-
164
- commander.parse(process.argv);
158
+ }
159
+ }
package/src/executor.ts CHANGED
@@ -1,21 +1,20 @@
1
1
  import { Worker } from 'node:worker_threads';
2
2
  import { once } from 'node:events';
3
- import { queue } from 'async';
4
3
  import { pathToFileURL } from 'node:url';
5
- import { createReport, Report } from './reporter.js';
6
- import { cmp, assertNoClosure } from './utils.js';
4
+ import { createReport, computeStats, Report } from './reporter.ts';
5
+ import { cmp, assertNoClosure } from './utils.ts';
7
6
  import {
8
- ExecutorRunOptions,
9
- ReportOptions,
10
- WorkerOptions,
11
- BenchmarkOptions,
7
+ type ExecutorRunOptions,
8
+ type ReportOptions,
9
+ type WorkerOptions,
10
+ type BenchmarkOptions,
12
11
  Control,
13
- ReportType,
14
- ReportTypeList,
12
+ type ReportType,
13
+ type ReportTypeList,
15
14
  CONTROL_SLOTS,
16
15
  COMPLETE_VALUE,
17
- ProgressCallback,
18
- } from './types.js';
16
+ type ProgressCallback,
17
+ } from './types.ts';
19
18
 
20
19
  export type ExecutorReport<R extends ReportTypeList> = Record<R[number], Report> & {
21
20
  count: number;
@@ -33,16 +32,46 @@ export interface ExecutorOptions<R extends ReportTypeList> extends BenchmarkOpti
33
32
 
34
33
  const BENCHMARK_URL = Symbol.for('overtake.benchmarkUrl');
35
34
 
36
- export const createExecutor = <TContext, TInput, R extends ReportTypeList>(options: Required<ExecutorOptions<R>>) => {
35
+ export interface Executor<TContext, TInput> {
36
+ pushAsync<T>(task: ExecutorRunOptions<TContext, TInput>): Promise<T>;
37
+ kill(): void;
38
+ }
39
+
40
+ export const createExecutor = <TContext, TInput, R extends ReportTypeList>(options: Required<ExecutorOptions<R>>): Executor<TContext, TInput> => {
37
41
  const { workers, warmupCycles, maxCycles, minCycles, absThreshold, relThreshold, gcObserver = true, reportTypes, onProgress, progressInterval = 100 } = options;
38
42
  const benchmarkUrl = (options as Record<symbol, unknown>)[BENCHMARK_URL];
39
43
  const resolvedBenchmarkUrl = typeof benchmarkUrl === 'string' ? benchmarkUrl : pathToFileURL(process.cwd()).href;
40
44
 
41
- const executor = queue<ExecutorRunOptions<TContext, TInput>>(async ({ id, setup, teardown, pre, run, post, data }) => {
45
+ const pending: { task: ExecutorRunOptions<TContext, TInput>; resolve: (v: unknown) => void; reject: (e: unknown) => void }[] = [];
46
+ let running = 0;
47
+
48
+ const schedule = async (task: ExecutorRunOptions<TContext, TInput>) => {
49
+ running++;
50
+ try {
51
+ return await runTask(task);
52
+ } finally {
53
+ running--;
54
+ if (pending.length > 0) {
55
+ const next = pending.shift()!;
56
+ schedule(next.task).then(next.resolve, next.reject);
57
+ }
58
+ }
59
+ };
60
+
61
+ const pushAsync = <T>(task: ExecutorRunOptions<TContext, TInput>): Promise<T> => {
62
+ if (running < workers) {
63
+ return schedule(task) as Promise<T>;
64
+ }
65
+ return new Promise<T>((resolve, reject) => {
66
+ pending.push({ task, resolve: resolve as (v: unknown) => void, reject });
67
+ });
68
+ };
69
+
70
+ const runTask = async ({ id, setup, teardown, pre, run, post, data }: ExecutorRunOptions<TContext, TInput>) => {
42
71
  const setupCode = setup?.toString();
43
72
  const teardownCode = teardown?.toString();
44
73
  const preCode = pre?.toString();
45
- const runCode = run.toString()!;
74
+ const runCode = run.toString();
46
75
  const postCode = post?.toString();
47
76
 
48
77
  if (setupCode) assertNoClosure(setupCode, 'setup');
@@ -54,7 +83,7 @@ export const createExecutor = <TContext, TInput, R extends ReportTypeList>(optio
54
83
  const controlSAB = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * CONTROL_SLOTS);
55
84
  const durationsSAB = new SharedArrayBuffer(BigUint64Array.BYTES_PER_ELEMENT * maxCycles);
56
85
 
57
- const workerFile = new URL('./worker.js', import.meta.url);
86
+ const workerFile = new URL('./worker.ts', import.meta.url);
58
87
  const workerData: WorkerOptions = {
59
88
  benchmarkUrl: resolvedBenchmarkUrl,
60
89
  setupCode,
@@ -120,8 +149,9 @@ export const createExecutor = <TContext, TInput, R extends ReportTypeList>(optio
120
149
  }
121
150
  }
122
151
 
152
+ const stats = count > 0 ? computeStats(durations) : undefined;
123
153
  const report = reportTypes
124
- .map<[string, unknown]>((type) => [type, createReport(durations, type)] as [ReportType, Report])
154
+ .map<[string, unknown]>((type) => [type, createReport(durations, type, stats)] as [ReportType, Report])
125
155
  .concat([
126
156
  ['count', count],
127
157
  ['heapUsedKB', heapUsedKB],
@@ -129,7 +159,7 @@ export const createExecutor = <TContext, TInput, R extends ReportTypeList>(optio
129
159
  ['error', workerError],
130
160
  ]);
131
161
  return Object.fromEntries(report);
132
- }, workers);
162
+ };
133
163
 
134
- return executor;
164
+ return { pushAsync, kill() {} };
135
165
  };
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { cpus } from 'node:os';
2
2
  import Progress from 'progress';
3
- import { createExecutor, ExecutorOptions, ExecutorReport } from './executor.js';
4
- import { MaybePromise, StepFn, SetupFn, TeardownFn, FeedFn, ReportType, ReportTypeList, DEFAULT_CYCLES, ProgressInfo } from './types.js';
3
+ import { createExecutor, type ExecutorOptions, type ExecutorReport } from './executor.ts';
4
+ import { type MaybePromise, type StepFn, type SetupFn, type TeardownFn, type FeedFn, type ReportType, type ReportTypeList, DEFAULT_CYCLES, type ProgressInfo } from './types.ts';
5
5
 
6
6
  declare global {
7
7
  const benchmark: typeof Benchmark.create;
@@ -9,7 +9,6 @@ declare global {
9
9
 
10
10
  export const DEFAULT_WORKERS = Math.max(1, Math.ceil(cpus().length / 4));
11
11
 
12
- export const AsyncFunction = (async () => {}).constructor;
13
12
  const BENCHMARK_URL = Symbol.for('overtake.benchmarkUrl');
14
13
 
15
14
  export interface TargetReport<R extends ReportTypeList> {
@@ -30,14 +29,24 @@ export interface FeedReport<R extends ReportTypeList> {
30
29
  export const DEFAULT_REPORT_TYPES = ['ops'] as const;
31
30
  export type DefaultReportTypes = (typeof DEFAULT_REPORT_TYPES)[number];
32
31
 
33
- export class MeasureContext<TContext, TInput> {
34
- public pre?: StepFn<TContext, TInput>;
35
- public post?: StepFn<TContext, TInput>;
32
+ const createExecutorErrorReport = <R extends ReportTypeList>(error: unknown): ExecutorReport<R> =>
33
+ ({
34
+ count: 0,
35
+ heapUsedKB: 0,
36
+ dceWarning: false,
37
+ error: error instanceof Error ? error.message : String(error),
38
+ }) as ExecutorReport<R>;
36
39
 
37
- constructor(
38
- public title: string,
39
- public run: StepFn<TContext, TInput>,
40
- ) {}
40
+ export class MeasureContext<TContext, TInput> {
41
+ pre?: StepFn<TContext, TInput>;
42
+ post?: StepFn<TContext, TInput>;
43
+ title: string;
44
+ run: StepFn<TContext, TInput>;
45
+
46
+ constructor(title: string, run: StepFn<TContext, TInput>) {
47
+ this.title = title;
48
+ this.run = run;
49
+ }
41
50
  }
42
51
 
43
52
  export class Measure<TContext, TInput> {
@@ -58,13 +67,15 @@ export class Measure<TContext, TInput> {
58
67
  }
59
68
 
60
69
  export class TargetContext<TContext, TInput> {
61
- public teardown?: TeardownFn<TContext>;
62
- public measures: MeasureContext<TContext, TInput>[] = [];
63
-
64
- constructor(
65
- readonly title: string,
66
- readonly setup?: SetupFn<MaybePromise<TContext>>,
67
- ) {}
70
+ teardown?: TeardownFn<TContext>;
71
+ measures: MeasureContext<TContext, TInput>[] = [];
72
+ readonly title: string;
73
+ readonly setup?: SetupFn<MaybePromise<TContext>>;
74
+
75
+ constructor(title: string, setup?: SetupFn<MaybePromise<TContext>>) {
76
+ this.title = title;
77
+ this.setup = setup;
78
+ }
68
79
  }
69
80
 
70
81
  export class Target<TContext, TInput> {
@@ -87,10 +98,13 @@ export class Target<TContext, TInput> {
87
98
  }
88
99
 
89
100
  export class FeedContext<TInput> {
90
- constructor(
91
- readonly title: string,
92
- readonly fn?: FeedFn<TInput>,
93
- ) {}
101
+ readonly title: string;
102
+ readonly fn?: FeedFn<TInput>;
103
+
104
+ constructor(title: string, fn?: FeedFn<TInput>) {
105
+ this.title = title;
106
+ this.fn = fn;
107
+ }
94
108
  }
95
109
 
96
110
  export class Benchmark<TInput> {
@@ -136,7 +150,7 @@ export class Benchmark<TInput> {
136
150
  return new Target<TContext, TInput>(target);
137
151
  }
138
152
 
139
- async execute<R extends readonly ReportType[] = typeof DEFAULT_REPORT_TYPES>(options: ExecutorOptions<R> & { progress?: boolean }): Promise<TargetReport<R>[]> {
153
+ async execute<R extends readonly ReportType[] = typeof DEFAULT_REPORT_TYPES>(options: Partial<ExecutorOptions<R>> & { progress?: boolean } = {}): Promise<TargetReport<R>[]> {
140
154
  const {
141
155
  workers = DEFAULT_WORKERS,
142
156
  warmupCycles = 20,
@@ -193,45 +207,59 @@ export class Benchmark<TInput> {
193
207
  } as Required<ExecutorOptions<R>>);
194
208
 
195
209
  const reports: TargetReport<R>[] = [];
196
- for (const target of this.#targets) {
197
- const targetReport: TargetReport<R> = { target: target.title, measures: [] };
198
- for (const measure of target.measures) {
199
- const measureReport: MeasureReport<R> = { measure: measure.title, feeds: [] };
200
- for (const feed of this.#feeds) {
201
- const id = `${target.title}/${measure.title}/${feed.title}`;
202
- const data = await feed.fn?.();
203
- executor
204
- .push<ExecutorReport<R>>({
205
- id,
206
- setup: target.setup,
207
- teardown: target.teardown,
208
- pre: measure.pre,
209
- run: measure.run,
210
- post: measure.post,
211
- data,
212
- })
213
- .then((data) => {
214
- progressMap.delete(id);
215
- completedBenchmarks++;
216
- measureReport.feeds.push({
217
- feed: feed.title,
218
- data,
219
- });
220
- });
210
+ const pendingReports: Promise<void>[] = [];
211
+
212
+ try {
213
+ const feedData = await Promise.all(this.#feeds.map(async (feed) => ({ title: feed.title, data: await feed.fn?.() })));
214
+ for (const target of this.#targets) {
215
+ const targetReport: TargetReport<R> = { target: target.title, measures: [] };
216
+ for (const measure of target.measures) {
217
+ const measureReport: MeasureReport<R> = { measure: measure.title, feeds: [] };
218
+ for (const feed of feedData) {
219
+ const id = `${target.title}/${measure.title}/${feed.title}`;
220
+ const feedReport: FeedReport<R> = {
221
+ feed: feed.title,
222
+ data: createExecutorErrorReport<R>('Benchmark did not produce a report'),
223
+ };
224
+
225
+ measureReport.feeds.push(feedReport);
226
+ pendingReports.push(
227
+ (async () => {
228
+ try {
229
+ feedReport.data = await executor.pushAsync<ExecutorReport<R>>({
230
+ id,
231
+ setup: target.setup,
232
+ teardown: target.teardown,
233
+ pre: measure.pre,
234
+ run: measure.run,
235
+ post: measure.post,
236
+ data: feed.data,
237
+ });
238
+ } catch (error) {
239
+ feedReport.data = createExecutorErrorReport<R>(error);
240
+ } finally {
241
+ progressMap.delete(id);
242
+ completedBenchmarks++;
243
+ }
244
+ })(),
245
+ );
246
+ }
247
+ targetReport.measures.push(measureReport);
221
248
  }
222
- targetReport.measures.push(measureReport);
249
+ reports.push(targetReport);
223
250
  }
224
- reports.push(targetReport);
225
- }
226
- await executor.drain();
227
- executor.kill();
228
251
 
229
- if (bar) {
230
- bar.update(1, { label: 'done' });
231
- bar.terminate();
232
- }
252
+ await Promise.all(pendingReports);
233
253
 
234
- return reports;
254
+ if (bar) {
255
+ bar.update(1, { label: 'done' });
256
+ bar.terminate();
257
+ }
258
+
259
+ return reports;
260
+ } finally {
261
+ executor.kill();
262
+ }
235
263
  }
236
264
  }
237
265