overtake 2.0.0 → 2.0.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.
package/bin/overtake.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env -S node --experimental-vm-modules --no-warnings --expose-gc
2
- import '../src/cli.ts';
2
+ import '../build/cli.js';
package/build/cli.js ADDED
@@ -0,0 +1,157 @@
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';
5
+ import { Benchmark, printTableReports, printJSONReports, printSimpleReports, printMarkdownReports, printHistogramReports, printComparisonReports, reportsToBaseline, DEFAULT_REPORT_TYPES, DEFAULT_WORKERS, } from './index.js';
6
+ import { REPORT_TYPES } from './types.js';
7
+ import { resolveHookUrl } from './utils.js';
8
+ register(resolveHookUrl);
9
+ const require = createRequire(import.meta.url);
10
+ const { name, version, description } = require('../package.json');
11
+ const BENCHMARK_URL = Symbol.for('overtake.benchmarkUrl');
12
+ const FORMATS = ['simple', 'json', 'pjson', 'table', 'markdown', 'histogram'];
13
+ const { values: opts, positionals: patterns } = parseArgs({
14
+ args: process.argv.slice(2),
15
+ allowPositionals: true,
16
+ options: {
17
+ 'report-types': { type: 'string', short: 'r', multiple: true },
18
+ workers: { type: 'string', short: 'w' },
19
+ format: { type: 'string', short: 'f' },
20
+ 'abs-threshold': { type: 'string' },
21
+ 'rel-threshold': { type: 'string' },
22
+ 'warmup-cycles': { type: 'string' },
23
+ 'max-cycles': { type: 'string' },
24
+ 'min-cycles': { type: 'string' },
25
+ 'no-gc-observer': { type: 'boolean' },
26
+ progress: { type: 'boolean' },
27
+ 'save-baseline': { type: 'string' },
28
+ 'compare-baseline': { type: 'string' },
29
+ help: { type: 'boolean', short: 'h' },
30
+ version: { type: 'boolean', short: 'v' },
31
+ },
32
+ });
33
+ if (opts.version) {
34
+ console.log(version);
35
+ process.exit(0);
36
+ }
37
+ if (opts.help || patterns.length === 0) {
38
+ console.log(`${name} v${version} - ${description}
39
+
40
+ Usage: overtake [options] <paths...>
41
+
42
+ Options:
43
+ -r, --report-types <type> statistic type, repeat for multiple (-r ops -r p99)
44
+ -w, --workers <n> number of concurrent workers (default: ${DEFAULT_WORKERS})
45
+ -f, --format <format> output format: ${FORMATS.join(', ')} (default: simple)
46
+ --abs-threshold <ns> absolute error threshold in nanoseconds
47
+ --rel-threshold <frac> relative error threshold (0-1)
48
+ --warmup-cycles <n> warmup cycles before measuring
49
+ --max-cycles <n> maximum measurement cycles per feed
50
+ --min-cycles <n> minimum measurement cycles per feed
51
+ --no-gc-observer disable GC overlap detection
52
+ --progress show progress bar
53
+ --save-baseline <file> save results to baseline file
54
+ --compare-baseline <file> compare results against baseline file
55
+ -v, --version show version
56
+ -h, --help show this help`);
57
+ process.exit(0);
58
+ }
59
+ const reportTypes = opts['report-types']?.length
60
+ ? opts['report-types'].filter((t) => REPORT_TYPES.includes(t))
61
+ : DEFAULT_REPORT_TYPES;
62
+ const format = opts.format && FORMATS.includes(opts.format) ? opts.format : 'simple';
63
+ const executeOptions = {
64
+ reportTypes,
65
+ workers: opts.workers ? parseInt(opts.workers) : DEFAULT_WORKERS,
66
+ absThreshold: opts['abs-threshold'] ? parseFloat(opts['abs-threshold']) : undefined,
67
+ relThreshold: opts['rel-threshold'] ? parseFloat(opts['rel-threshold']) : undefined,
68
+ warmupCycles: opts['warmup-cycles'] ? parseInt(opts['warmup-cycles']) : undefined,
69
+ maxCycles: opts['max-cycles'] ? parseInt(opts['max-cycles']) : undefined,
70
+ minCycles: opts['min-cycles'] ? parseInt(opts['min-cycles']) : undefined,
71
+ gcObserver: !opts['no-gc-observer'],
72
+ progress: opts.progress ?? false,
73
+ format,
74
+ };
75
+ let baseline = null;
76
+ if (opts['compare-baseline']) {
77
+ try {
78
+ const content = await readFile(opts['compare-baseline'], 'utf8');
79
+ baseline = JSON.parse(content);
80
+ }
81
+ catch {
82
+ console.error(`Warning: Could not load baseline file: ${opts['compare-baseline']}`);
83
+ }
84
+ }
85
+ const files = new Set((await Promise.all(patterns.map((pattern) => Array.fromAsync(glob(pattern, { cwd: process.cwd() })).catch(() => [])))).flat());
86
+ const allBaselineResults = {};
87
+ for (const file of files) {
88
+ const stats = await stat(file).catch(() => false);
89
+ if (stats && stats.isFile()) {
90
+ const identifier = pathToFileURL(file).href;
91
+ let instance;
92
+ globalThis.benchmark = (...args) => {
93
+ if (instance) {
94
+ throw new Error('Only one benchmark per file is supported');
95
+ }
96
+ instance = Benchmark.create(...args);
97
+ return instance;
98
+ };
99
+ try {
100
+ await import(identifier);
101
+ }
102
+ catch (e) {
103
+ console.error(`Error loading ${file}: ${e instanceof Error ? e.message : String(e)}`);
104
+ continue;
105
+ }
106
+ if (instance) {
107
+ let reports;
108
+ try {
109
+ reports = await instance.execute({
110
+ ...executeOptions,
111
+ [BENCHMARK_URL]: identifier,
112
+ });
113
+ }
114
+ catch (e) {
115
+ console.error(`Error executing ${file}: ${e instanceof Error ? e.message : String(e)}`);
116
+ continue;
117
+ }
118
+ if (opts['save-baseline']) {
119
+ const bd = reportsToBaseline(reports);
120
+ Object.assign(allBaselineResults, bd.results);
121
+ }
122
+ if (baseline) {
123
+ printComparisonReports(reports, baseline);
124
+ }
125
+ else {
126
+ switch (format) {
127
+ case 'json':
128
+ printJSONReports(reports);
129
+ break;
130
+ case 'pjson':
131
+ printJSONReports(reports, 2);
132
+ break;
133
+ case 'table':
134
+ printTableReports(reports);
135
+ break;
136
+ case 'markdown':
137
+ printMarkdownReports(reports);
138
+ break;
139
+ case 'histogram':
140
+ printHistogramReports(reports);
141
+ break;
142
+ default:
143
+ printSimpleReports(reports);
144
+ }
145
+ }
146
+ }
147
+ }
148
+ }
149
+ if (opts['save-baseline'] && Object.keys(allBaselineResults).length > 0) {
150
+ const baselineData = {
151
+ version: 1,
152
+ timestamp: new Date().toISOString(),
153
+ results: allBaselineResults,
154
+ };
155
+ await writeFile(opts['save-baseline'], JSON.stringify(baselineData, null, 2));
156
+ console.log(`Baseline saved to: ${opts['save-baseline']}`);
157
+ }
@@ -1,5 +1,5 @@
1
- import { Report } from './reporter.ts';
2
- import { type ExecutorRunOptions, type ReportOptions, type BenchmarkOptions, type ReportTypeList, type ProgressCallback } from './types.ts';
1
+ import { Report } from './reporter.js';
2
+ import { type ExecutorRunOptions, type ReportOptions, type BenchmarkOptions, type ReportTypeList, type ProgressCallback } from './types.js';
3
3
  export type ExecutorReport<R extends ReportTypeList> = Record<R[number], Report> & {
4
4
  count: number;
5
5
  heapUsedKB: number;
@@ -0,0 +1,139 @@
1
+ import { Worker } from 'node:worker_threads';
2
+ import { once } from 'node:events';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { createReport, computeStats, Report } from './reporter.js';
5
+ import { cmp, assertNoClosure, normalizeFunction } from './utils.js';
6
+ import { Control, CONTROL_SLOTS, COMPLETE_VALUE, } from './types.js';
7
+ const BENCHMARK_URL = Symbol.for('overtake.benchmarkUrl');
8
+ export const createExecutor = (options) => {
9
+ const { workers, warmupCycles, maxCycles, minCycles, absThreshold, relThreshold, gcObserver = true, reportTypes, onProgress, progressInterval = 100 } = options;
10
+ const benchmarkUrl = options[BENCHMARK_URL];
11
+ const resolvedBenchmarkUrl = typeof benchmarkUrl === 'string' ? benchmarkUrl : pathToFileURL(process.cwd()).href;
12
+ const pending = [];
13
+ const activeWorkers = new Set();
14
+ let running = 0;
15
+ const schedule = async (task) => {
16
+ running++;
17
+ try {
18
+ return await runTask(task);
19
+ }
20
+ finally {
21
+ running--;
22
+ if (pending.length > 0) {
23
+ const next = pending.shift();
24
+ schedule(next.task).then(next.resolve, next.reject);
25
+ }
26
+ }
27
+ };
28
+ const pushAsync = (task) => {
29
+ if (running < workers) {
30
+ return schedule(task);
31
+ }
32
+ return new Promise((resolve, reject) => {
33
+ pending.push({ task, resolve: resolve, reject });
34
+ });
35
+ };
36
+ const runTask = async ({ id, setup, teardown, pre, run, post, data }) => {
37
+ const setupCode = setup ? normalizeFunction(setup.toString()) : undefined;
38
+ const teardownCode = teardown ? normalizeFunction(teardown.toString()) : undefined;
39
+ const preCode = pre ? normalizeFunction(pre.toString()) : undefined;
40
+ const runCode = normalizeFunction(run.toString());
41
+ const postCode = post ? normalizeFunction(post.toString()) : undefined;
42
+ if (setupCode)
43
+ assertNoClosure(setupCode, 'setup');
44
+ if (teardownCode)
45
+ assertNoClosure(teardownCode, 'teardown');
46
+ if (preCode)
47
+ assertNoClosure(preCode, 'pre');
48
+ assertNoClosure(runCode, 'run');
49
+ if (postCode)
50
+ assertNoClosure(postCode, 'post');
51
+ const controlSAB = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * CONTROL_SLOTS);
52
+ const durationsSAB = new SharedArrayBuffer(BigUint64Array.BYTES_PER_ELEMENT * maxCycles);
53
+ const workerFile = new URL('./worker.js', import.meta.url);
54
+ const workerData = {
55
+ benchmarkUrl: resolvedBenchmarkUrl,
56
+ setupCode,
57
+ teardownCode,
58
+ preCode,
59
+ runCode,
60
+ postCode,
61
+ data,
62
+ warmupCycles,
63
+ minCycles,
64
+ absThreshold,
65
+ relThreshold,
66
+ gcObserver,
67
+ controlSAB,
68
+ durationsSAB,
69
+ };
70
+ const worker = new Worker(workerFile, {
71
+ workerData,
72
+ });
73
+ activeWorkers.add(worker);
74
+ const control = new Int32Array(controlSAB);
75
+ let progressIntervalId;
76
+ if (onProgress && id) {
77
+ progressIntervalId = setInterval(() => {
78
+ const progress = control[Control.PROGRESS] / COMPLETE_VALUE;
79
+ onProgress({ id, progress });
80
+ }, progressInterval);
81
+ }
82
+ const WORKER_TIMEOUT_MS = 300_000;
83
+ const exitPromise = once(worker, 'exit');
84
+ const timeoutId = setTimeout(() => worker.terminate(), WORKER_TIMEOUT_MS);
85
+ let workerError;
86
+ try {
87
+ const [exitCode] = await exitPromise;
88
+ clearTimeout(timeoutId);
89
+ if (progressIntervalId)
90
+ clearInterval(progressIntervalId);
91
+ if (exitCode !== 0) {
92
+ workerError = `worker exited with code ${exitCode}`;
93
+ }
94
+ }
95
+ catch (err) {
96
+ clearTimeout(timeoutId);
97
+ if (progressIntervalId)
98
+ clearInterval(progressIntervalId);
99
+ workerError = err instanceof Error ? err.message : String(err);
100
+ }
101
+ activeWorkers.delete(worker);
102
+ const count = control[Control.INDEX];
103
+ const heapUsedKB = control[Control.HEAP_USED];
104
+ const durations = new BigUint64Array(durationsSAB).slice(0, count).sort(cmp);
105
+ const DCE_THRESHOLD_OPS = 5_000_000_000;
106
+ let dceWarning = false;
107
+ if (count > 0) {
108
+ let sum = 0n;
109
+ for (const d of durations)
110
+ sum += d;
111
+ const avgNs = Number(sum / BigInt(count)) / 1000;
112
+ const opsPerSec = avgNs > 0 ? 1_000_000_000 / avgNs : Infinity;
113
+ if (opsPerSec > DCE_THRESHOLD_OPS) {
114
+ dceWarning = true;
115
+ }
116
+ }
117
+ const stats = count > 0 ? computeStats(durations) : undefined;
118
+ const report = reportTypes
119
+ .map((type) => [type, createReport(durations, type, stats)])
120
+ .concat([
121
+ ['count', count],
122
+ ['heapUsedKB', heapUsedKB],
123
+ ['dceWarning', dceWarning],
124
+ ['error', workerError],
125
+ ]);
126
+ return Object.fromEntries(report);
127
+ };
128
+ return {
129
+ pushAsync,
130
+ kill() {
131
+ for (const w of activeWorkers)
132
+ w.terminate();
133
+ activeWorkers.clear();
134
+ for (const p of pending)
135
+ p.reject(new Error('Executor killed'));
136
+ pending.length = 0;
137
+ },
138
+ };
139
+ };
@@ -0,0 +1,16 @@
1
+ export class GCWatcher {
2
+ #registry = new FinalizationRegistry(() => { });
3
+ start() {
4
+ const target = {};
5
+ const ref = new WeakRef(target);
6
+ this.#registry.register(target, null, ref);
7
+ return { ref };
8
+ }
9
+ seen(marker) {
10
+ const collected = marker.ref.deref() === undefined;
11
+ if (!collected) {
12
+ this.#registry.unregister(marker.ref);
13
+ }
14
+ return collected;
15
+ }
16
+ }
package/build/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { type ExecutorOptions, type ExecutorReport } from './executor.ts';
2
- import { type MaybePromise, type StepFn, type SetupFn, type TeardownFn, type FeedFn, type ReportType, type ReportTypeList } from './types.ts';
1
+ import { type ExecutorOptions, type ExecutorReport } from './executor.js';
2
+ import { type MaybePromise, type StepFn, type SetupFn, type TeardownFn, type FeedFn, type ReportType, type ReportTypeList } from './types.js';
3
3
  declare global {
4
4
  const benchmark: typeof Benchmark.create;
5
5
  }