overtake 1.3.2 → 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 (56) hide show
  1. package/README.md +12 -15
  2. package/bin/overtake.js +1 -1
  3. package/build/executor.d.ts +8 -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 +7 -7
  8. package/build/utils.d.ts +3 -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 +59 -24
  14. package/src/index.ts +135 -68
  15. package/src/reporter.ts +77 -125
  16. package/src/runner.ts +28 -25
  17. package/src/types.ts +9 -9
  18. package/src/utils.ts +62 -46
  19. package/src/worker.ts +13 -12
  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 -116
  26. package/build/executor.cjs.map +0 -1
  27. package/build/executor.js +0 -106
  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 -400
  34. package/build/index.cjs.map +0 -1
  35. package/build/index.js +0 -335
  36. package/build/index.js.map +0 -1
  37. package/build/reporter.cjs +0 -364
  38. package/build/reporter.cjs.map +0 -1
  39. package/build/reporter.js +0 -346
  40. package/build/reporter.js.map +0 -1
  41. package/build/runner.cjs +0 -528
  42. package/build/runner.cjs.map +0 -1
  43. package/build/runner.js +0 -518
  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 -121
  50. package/build/utils.cjs.map +0 -1
  51. package/build/utils.js +0 -85
  52. package/build/utils.js.map +0 -1
  53. package/build/worker.cjs +0 -158
  54. package/build/worker.cjs.map +0 -1
  55. package/build/worker.js +0 -113
  56. package/build/worker.js.map +0 -1
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,26 +1,26 @@
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 } 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;
22
21
  heapUsedKB: number;
23
22
  dceWarning: boolean;
23
+ error?: string;
24
24
  };
25
25
 
26
26
  export interface ExecutorOptions<R extends ReportTypeList> extends BenchmarkOptions, ReportOptions<R> {
@@ -32,22 +32,58 @@ export interface ExecutorOptions<R extends ReportTypeList> extends BenchmarkOpti
32
32
 
33
33
  const BENCHMARK_URL = Symbol.for('overtake.benchmarkUrl');
34
34
 
35
- 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> => {
36
41
  const { workers, warmupCycles, maxCycles, minCycles, absThreshold, relThreshold, gcObserver = true, reportTypes, onProgress, progressInterval = 100 } = options;
37
42
  const benchmarkUrl = (options as Record<symbol, unknown>)[BENCHMARK_URL];
38
43
  const resolvedBenchmarkUrl = typeof benchmarkUrl === 'string' ? benchmarkUrl : pathToFileURL(process.cwd()).href;
39
44
 
40
- 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>) => {
41
71
  const setupCode = setup?.toString();
42
72
  const teardownCode = teardown?.toString();
43
73
  const preCode = pre?.toString();
44
- const runCode = run.toString()!;
74
+ const runCode = run.toString();
45
75
  const postCode = post?.toString();
46
76
 
77
+ if (setupCode) assertNoClosure(setupCode, 'setup');
78
+ if (teardownCode) assertNoClosure(teardownCode, 'teardown');
79
+ if (preCode) assertNoClosure(preCode, 'pre');
80
+ assertNoClosure(runCode, 'run');
81
+ if (postCode) assertNoClosure(postCode, 'post');
82
+
47
83
  const controlSAB = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * CONTROL_SLOTS);
48
84
  const durationsSAB = new SharedArrayBuffer(BigUint64Array.BYTES_PER_ELEMENT * maxCycles);
49
85
 
50
- const workerFile = new URL('./worker.js', import.meta.url);
86
+ const workerFile = new URL('./worker.ts', import.meta.url);
51
87
  const workerData: WorkerOptions = {
52
88
  benchmarkUrl: resolvedBenchmarkUrl,
53
89
  setupCode,
@@ -83,17 +119,18 @@ export const createExecutor = <TContext, TInput, R extends ReportTypeList>(optio
83
119
  const WORKER_TIMEOUT_MS = 300_000;
84
120
  const exitPromise = once(worker, 'exit');
85
121
  const timeoutId = setTimeout(() => worker.terminate(), WORKER_TIMEOUT_MS);
122
+ let workerError: string | undefined;
86
123
  try {
87
124
  const [exitCode] = await exitPromise;
88
125
  clearTimeout(timeoutId);
89
126
  if (progressIntervalId) clearInterval(progressIntervalId);
90
127
  if (exitCode !== 0) {
91
- throw new Error(`worker exited with code ${exitCode}`);
128
+ workerError = `worker exited with code ${exitCode}`;
92
129
  }
93
130
  } catch (err) {
94
131
  clearTimeout(timeoutId);
95
132
  if (progressIntervalId) clearInterval(progressIntervalId);
96
- throw err;
133
+ workerError = err instanceof Error ? err.message : String(err);
97
134
  }
98
135
 
99
136
  const count = control[Control.INDEX];
@@ -112,19 +149,17 @@ export const createExecutor = <TContext, TInput, R extends ReportTypeList>(optio
112
149
  }
113
150
  }
114
151
 
152
+ const stats = count > 0 ? computeStats(durations) : undefined;
115
153
  const report = reportTypes
116
- .map<[string, unknown]>((type) => [type, createReport(durations, type)] as [ReportType, Report])
154
+ .map<[string, unknown]>((type) => [type, createReport(durations, type, stats)] as [ReportType, Report])
117
155
  .concat([
118
156
  ['count', count],
119
157
  ['heapUsedKB', heapUsedKB],
120
158
  ['dceWarning', dceWarning],
159
+ ['error', workerError],
121
160
  ]);
122
161
  return Object.fromEntries(report);
123
- }, workers);
124
-
125
- executor.error((err) => {
126
- console.error(err);
127
- });
162
+ };
128
163
 
129
- return executor;
164
+ return { pushAsync, kill() {} };
130
165
  };