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/README.md CHANGED
@@ -98,8 +98,7 @@ benchmark('data', getData)
98
98
 
99
99
  ### Importing Local Files
100
100
 
101
- - **CLI mode (`npx overtake`)**: `baseUrl` is set to the benchmark file, so `await import('./helper.js')` works.
102
- - **Programmatic mode (`suite.execute`)**: pass `baseUrl: import.meta.url` (the benchmark’s file URL) so relative imports resolve correctly. If you omit it, Overtake falls back to `process.cwd()` and relative imports may fail.
101
+ - **CLI mode (`npx overtake`)**: relative imports resolve from the benchmark file automatically.
103
102
 
104
103
  ```typescript
105
104
  // CLI usage – relative path is fine
@@ -109,16 +108,6 @@ benchmark('local', () => 1)
109
108
  return { helper };
110
109
  })
111
110
  .measure('use helper', ({ helper }) => helper());
112
-
113
- // Programmatic usage – provide baseUrl
114
- const suite = new Benchmark('local');
115
- suite
116
- .target('helper', async () => {
117
- const { helper } = await import('./helpers.js');
118
- return { helper };
119
- })
120
- .measure('use helper', ({ helper }) => helper());
121
- await suite.execute({ baseUrl: import.meta.url });
122
111
  ```
123
112
 
124
113
  ## Usage
@@ -285,7 +274,7 @@ npx overtake <pattern> [options]
285
274
  | Option | Short | Description | Default |
286
275
  | -------------------- | ----- | ----------------------------------------------------- | --------- |
287
276
  | `--format` | `-f` | Output format (see [Output Formats](#output-formats)) | `simple` |
288
- | `--report-types` | `-r` | Stats to show (see [Metrics](#available-metrics)) | `['ops']` |
277
+ | `--report-types` | `-r` | Stats to show, repeat for multiple (`-r ops -r p99`) | `['ops']` |
289
278
  | `--workers` | `-w` | Concurrent workers | CPU count |
290
279
  | `--min-cycles` | | Minimum measurement iterations | 50 |
291
280
  | `--max-cycles` | | Maximum measurement iterations | 1000 |
@@ -304,7 +293,7 @@ npx overtake <pattern> [options]
304
293
  npx overtake "**/*.bench.ts" -f table
305
294
 
306
295
  # Show detailed statistics
307
- npx overtake bench.ts -r ops mean p95 p99
296
+ npx overtake bench.ts -r ops -r mean -r p95 -r p99
308
297
 
309
298
  # Output JSON for CI
310
299
  npx overtake bench.ts -f json > results.json
@@ -400,7 +389,7 @@ Specify with `--report-types` or `reportTypes` option.
400
389
  **Example:**
401
390
 
402
391
  ```bash
403
- npx overtake bench.ts -r ops mean sd rme p50 p95 p99
392
+ npx overtake bench.ts -r ops -r mean -r sd -r rme -r p50 -r p95 -r p99
404
393
  ```
405
394
 
406
395
  ## Baseline Comparison
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 '../build/cli.js';
2
+ import '../src/cli.ts';
@@ -1,5 +1,5 @@
1
- import { Report } from './reporter.js';
2
- import { ExecutorRunOptions, ReportOptions, BenchmarkOptions, ReportTypeList, ProgressCallback } from './types.js';
1
+ import { Report } from './reporter.ts';
2
+ import { type ExecutorRunOptions, type ReportOptions, type BenchmarkOptions, type ReportTypeList, type ProgressCallback } from './types.ts';
3
3
  export type ExecutorReport<R extends ReportTypeList> = Record<R[number], Report> & {
4
4
  count: number;
5
5
  heapUsedKB: number;
@@ -12,4 +12,8 @@ export interface ExecutorOptions<R extends ReportTypeList> extends BenchmarkOpti
12
12
  onProgress?: ProgressCallback;
13
13
  progressInterval?: number;
14
14
  }
15
- export declare const createExecutor: <TContext, TInput, R extends ReportTypeList>(options: Required<ExecutorOptions<R>>) => import("async").QueueObject<ExecutorRunOptions<TContext, TInput>>;
15
+ export interface Executor<TContext, TInput> {
16
+ pushAsync<T>(task: ExecutorRunOptions<TContext, TInput>): Promise<T>;
17
+ kill(): void;
18
+ }
19
+ export declare const createExecutor: <TContext, TInput, R extends ReportTypeList>(options: Required<ExecutorOptions<R>>) => Executor<TContext, TInput>;
package/build/index.d.ts CHANGED
@@ -1,10 +1,9 @@
1
- import { ExecutorOptions, ExecutorReport } from './executor.js';
2
- import { MaybePromise, StepFn, SetupFn, TeardownFn, FeedFn, ReportType, ReportTypeList } from './types.js';
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';
3
3
  declare global {
4
4
  const benchmark: typeof Benchmark.create;
5
5
  }
6
6
  export declare const DEFAULT_WORKERS: number;
7
- export declare const AsyncFunction: Function;
8
7
  export interface TargetReport<R extends ReportTypeList> {
9
8
  target: string;
10
9
  measures: MeasureReport<R>[];
@@ -20,10 +19,10 @@ export interface FeedReport<R extends ReportTypeList> {
20
19
  export declare const DEFAULT_REPORT_TYPES: readonly ["ops"];
21
20
  export type DefaultReportTypes = (typeof DEFAULT_REPORT_TYPES)[number];
22
21
  export declare class MeasureContext<TContext, TInput> {
23
- title: string;
24
- run: StepFn<TContext, TInput>;
25
22
  pre?: StepFn<TContext, TInput>;
26
23
  post?: StepFn<TContext, TInput>;
24
+ title: string;
25
+ run: StepFn<TContext, TInput>;
27
26
  constructor(title: string, run: StepFn<TContext, TInput>);
28
27
  }
29
28
  export declare class Measure<TContext, TInput> {
@@ -33,11 +32,11 @@ export declare class Measure<TContext, TInput> {
33
32
  post(fn: StepFn<TContext, TInput>): Measure<TContext, TInput>;
34
33
  }
35
34
  export declare class TargetContext<TContext, TInput> {
36
- readonly title: string;
37
- readonly setup?: SetupFn<MaybePromise<TContext>> | undefined;
38
35
  teardown?: TeardownFn<TContext>;
39
36
  measures: MeasureContext<TContext, TInput>[];
40
- constructor(title: string, setup?: SetupFn<MaybePromise<TContext>> | undefined);
37
+ readonly title: string;
38
+ readonly setup?: SetupFn<MaybePromise<TContext>>;
39
+ constructor(title: string, setup?: SetupFn<MaybePromise<TContext>>);
41
40
  }
42
41
  export declare class Target<TContext, TInput> {
43
42
  #private;
@@ -47,8 +46,8 @@ export declare class Target<TContext, TInput> {
47
46
  }
48
47
  export declare class FeedContext<TInput> {
49
48
  readonly title: string;
50
- readonly fn?: FeedFn<TInput> | undefined;
51
- constructor(title: string, fn?: FeedFn<TInput> | undefined);
49
+ readonly fn?: FeedFn<TInput>;
50
+ constructor(title: string, fn?: FeedFn<TInput>);
52
51
  }
53
52
  export declare class Benchmark<TInput> {
54
53
  #private;
@@ -60,7 +59,7 @@ export declare class Benchmark<TInput> {
60
59
  feed<I>(title: string, fn: FeedFn<I>): Benchmark<TInput | I>;
61
60
  target<TContext>(title: string): Target<void, TInput>;
62
61
  target<TContext>(title: string, setup: SetupFn<Awaited<TContext>>): Target<TContext, TInput>;
63
- execute<R extends readonly ReportType[] = typeof DEFAULT_REPORT_TYPES>(options: ExecutorOptions<R> & {
62
+ execute<R extends readonly ReportType[] = typeof DEFAULT_REPORT_TYPES>(options?: Partial<ExecutorOptions<R>> & {
64
63
  progress?: boolean;
65
64
  }): Promise<TargetReport<R>[]>;
66
65
  }
@@ -1,4 +1,4 @@
1
- import { ReportType } from './types.js';
1
+ import { type ReportType } from './types.ts';
2
2
  export declare class Report {
3
3
  readonly type: ReportType;
4
4
  readonly value: bigint;
@@ -8,4 +8,12 @@ export declare class Report {
8
8
  valueOf(): number;
9
9
  toString(): string;
10
10
  }
11
- export declare const createReport: (durations: BigUint64Array, type: ReportType) => Report;
11
+ type Stats = {
12
+ sum: bigint;
13
+ mean: bigint;
14
+ ssd: bigint;
15
+ n: bigint;
16
+ };
17
+ export declare const computeStats: (durations: BigUint64Array) => Stats;
18
+ export declare const createReport: (durations: BigUint64Array, type: ReportType, stats?: Stats) => Report;
19
+ export {};
package/build/runner.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { Options } from './types.js';
1
+ import { type Options } from './types.ts';
2
2
  export declare const benchmark: <TContext, TInput>({ setup, teardown, pre, run: runRaw, post, data, warmupCycles, minCycles, absThreshold, relThreshold, gcObserver, durationsSAB, controlSAB, }: Required<Options<TContext, TInput>>) => Promise<number>;
package/build/types.d.ts CHANGED
@@ -53,12 +53,12 @@ export interface Options<TContext, TInput> extends RunOptions<TContext, TInput>,
53
53
  durationsSAB: SharedArrayBuffer;
54
54
  controlSAB: SharedArrayBuffer;
55
55
  }
56
- export declare enum Control {
57
- INDEX = 0,
58
- PROGRESS = 1,
59
- COMPLETE = 2,
60
- HEAP_USED = 3
61
- }
56
+ export declare const Control: {
57
+ readonly INDEX: 0;
58
+ readonly PROGRESS: 1;
59
+ readonly COMPLETE: 2;
60
+ readonly HEAP_USED: 3;
61
+ };
62
62
  export declare const CONTROL_SLOTS: number;
63
63
  export declare const DEFAULT_CYCLES = 10000;
64
64
  export declare const Z95 = 1.96;
package/build/utils.d.ts CHANGED
@@ -1,23 +1,7 @@
1
+ export declare const resolveHookUrl: string;
1
2
  export declare const isqrt: (n: bigint) => bigint;
2
- export declare const abs: (value: bigint) => bigint;
3
3
  export declare const cmp: (a: bigint | number, b: bigint | number) => number;
4
4
  export declare const max: (a: bigint, b: bigint) => bigint;
5
- export declare const divMod: (a: bigint, b: bigint) => {
6
- quotient: bigint;
7
- remainder: bigint;
8
- };
9
5
  export declare function div(a: bigint, b: bigint, decimals?: number): string;
10
6
  export declare function divs(a: bigint, b: bigint, scale: bigint): bigint;
11
- export declare class ScaledBigInt {
12
- value: bigint;
13
- scale: bigint;
14
- constructor(value: bigint, scale: bigint);
15
- add(value: bigint): void;
16
- sub(value: bigint): void;
17
- div(value: bigint): void;
18
- mul(value: bigint): void;
19
- unscale(): bigint;
20
- number(): number;
21
- }
22
7
  export declare function assertNoClosure(code: string, name: string): void;
23
- export declare const transpile: (code: string) => Promise<string>;
package/package.json CHANGED
@@ -1,28 +1,18 @@
1
1
  {
2
2
  "name": "overtake",
3
- "version": "1.4.0",
3
+ "version": "2.0.0",
4
4
  "description": "NodeJS performance benchmark",
5
5
  "type": "module",
6
6
  "types": "build/index.d.ts",
7
- "main": "build/index.cjs",
8
- "module": "build/index.js",
9
7
  "exports": {
10
8
  "types": "./build/index.d.ts",
11
- "require": "./build/index.cjs",
12
- "import": "./build/index.js"
13
- },
14
- "typesVersions": {
15
- "*": {
16
- "*": [
17
- "build/index.d.ts"
18
- ]
19
- }
9
+ "import": "./src/index.ts"
20
10
  },
21
11
  "bin": {
22
12
  "overtake": "bin/overtake.js"
23
13
  },
24
14
  "engines": {
25
- "node": ">=22"
15
+ "node": ">=24"
26
16
  },
27
17
  "repository": {
28
18
  "type": "git",
@@ -41,30 +31,22 @@
41
31
  },
42
32
  "homepage": "https://github.com/3axap4eHko/overtake#readme",
43
33
  "devDependencies": {
44
- "@jest/globals": "^30.2.0",
45
- "@swc/jest": "^0.2.39",
46
- "@types/async": "^3.2.25",
47
- "@types/jest": "^30.0.0",
48
34
  "@types/node": "^25.3.0",
49
35
  "@types/progress": "^2.0.7",
50
36
  "husky": "^9.1.7",
51
- "inop": "^0.9.0",
52
- "jest": "^30.2.0",
53
37
  "overtake": "^1.3.1",
54
38
  "prettier": "^3.8.1",
55
39
  "pretty-quick": "^4.2.2",
56
- "typescript": "^5.9.3"
40
+ "typescript": "5.9.3"
57
41
  },
58
42
  "dependencies": {
59
- "@swc/core": "^1.15.11",
60
- "async": "^3.2.6",
61
- "commander": "^14.0.3",
62
- "glob": "^13.0.6",
43
+ "@swc/core": "^1.15.18",
63
44
  "progress": "^2.0.3"
64
45
  },
65
46
  "scripts": {
66
- "build": "rm -rf build && inop src build -i __tests__ -i *.tmp.ts && tsc --declaration --emitDeclarationOnly",
47
+ "build": "rm -rf build && tsc --declaration --emitDeclarationOnly",
67
48
  "start": "./bin/overtake.js",
68
- "test": "jest --detectOpenHandles --passWithNoTests"
49
+ "test": "node --experimental-test-module-mocks --test src/__tests__/*.ts",
50
+ "test:cov": "node --experimental-test-module-mocks --experimental-test-coverage --test src/__tests__/*.ts"
69
51
  }
70
52
  }
@@ -0,0 +1,135 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { assertNoClosure } from '../utils.ts';
4
+
5
+ describe('assertNoClosure', () => {
6
+ describe('allows functions without closures', () => {
7
+ it('arrow with params only', () => {
8
+ assert.doesNotThrow(() => assertNoClosure('(x) => x * 2', 'run'));
9
+ });
10
+
11
+ it('arrow with local variables', () => {
12
+ assert.doesNotThrow(() => assertNoClosure('(ctx, input) => { const y = ctx.value + input; return y; }', 'run'));
13
+ });
14
+
15
+ it('function expression', () => {
16
+ assert.doesNotThrow(() => assertNoClosure('function(x) { return x + 1; }', 'run'));
17
+ });
18
+
19
+ it('named function expression', () => {
20
+ assert.doesNotThrow(() => assertNoClosure('function run(x) { return x + 1; }', 'run'));
21
+ });
22
+
23
+ it('async arrow', () => {
24
+ assert.doesNotThrow(() => assertNoClosure('async (ctx) => { const r = await fetch("url"); return r; }', 'run'));
25
+ });
26
+
27
+ it('destructured params', () => {
28
+ assert.doesNotThrow(() => assertNoClosure('({a, b: c}, [d, ...e]) => a + c + d + e.length', 'run'));
29
+ });
30
+
31
+ it('nested function declaration', () => {
32
+ assert.doesNotThrow(() => assertNoClosure('(arr) => { function helper(x) { return x * 2; } return arr.map(helper); }', 'run'));
33
+ });
34
+
35
+ it('for-of loop variable', () => {
36
+ assert.doesNotThrow(() => assertNoClosure('(arr) => { let sum = 0; for (const x of arr) sum += x; return sum; }', 'run'));
37
+ });
38
+
39
+ it('member access on params', () => {
40
+ assert.doesNotThrow(() => assertNoClosure('(ctx) => ctx.data.map(x => x.value)', 'run'));
41
+ });
42
+
43
+ it('globals like console, Buffer, Math, Array', () => {
44
+ assert.doesNotThrow(() => assertNoClosure('(ctx) => { console.log(Math.max(...ctx)); return Buffer.from(Array.of(1)); }', 'run'));
45
+ });
46
+
47
+ it('try-catch with error binding', () => {
48
+ assert.doesNotThrow(() => assertNoClosure('(ctx) => { try { return ctx(); } catch (e) { return e; } }', 'run'));
49
+ });
50
+
51
+ it('class expression', () => {
52
+ assert.doesNotThrow(() => assertNoClosure('() => { class Foo { bar() { return 1; } } return new Foo(); }', 'run'));
53
+ });
54
+
55
+ it('label statements', () => {
56
+ assert.doesNotThrow(() => assertNoClosure('() => { outer: for (let i = 0; i < 10; i++) { break outer; } }', 'run'));
57
+ });
58
+ });
59
+
60
+ describe('detects closures', () => {
61
+ it('single closed-over variable', () => {
62
+ assert.throws(() => assertNoClosure('(x) => x + closedOver', 'run'), /closedOver/);
63
+ });
64
+
65
+ it('multiple closed-over variables', () => {
66
+ assert.throws(() => assertNoClosure('(ctx) => sharedData.filter(x => x > threshold)', 'run'), /sharedData/);
67
+ });
68
+
69
+ it('closed-over function call', () => {
70
+ assert.throws(() => assertNoClosure('(ctx) => helper(ctx)', 'run'), /helper/);
71
+ });
72
+
73
+ it('closed-over array', () => {
74
+ assert.throws(() => assertNoClosure('() => myArray.map(x => x * 2)', 'run'), /myArray/);
75
+ });
76
+
77
+ it('computed member access with outer variable', () => {
78
+ assert.throws(() => assertNoClosure('(obj) => obj[key]', 'run'), /key/);
79
+ });
80
+
81
+ it('variable used as argument', () => {
82
+ assert.throws(() => assertNoClosure('() => JSON.stringify(config)', 'run'), /config/);
83
+ });
84
+
85
+ it('variable in template literal', () => {
86
+ assert.throws(() => assertNoClosure('() => `${prefix}-value`', 'run'), /prefix/);
87
+ });
88
+ });
89
+
90
+ describe('error message', () => {
91
+ it('includes the function name', () => {
92
+ assert.throws(() => assertNoClosure('() => x', 'setup'), /"setup"/);
93
+ assert.throws(() => assertNoClosure('() => x', 'run'), /"run"/);
94
+ assert.throws(() => assertNoClosure('() => x', 'teardown'), /"teardown"/);
95
+ });
96
+
97
+ it('lists all closed-over variables', () => {
98
+ try {
99
+ assertNoClosure('() => a + b + c', 'run');
100
+ assert.fail('should have thrown');
101
+ } catch (e) {
102
+ assert.match((e as Error).message, /\ba\b/);
103
+ assert.match((e as Error).message, /\bb\b/);
104
+ assert.match((e as Error).message, /\bc\b/);
105
+ }
106
+ });
107
+
108
+ it('explains the problem and suggests fix', () => {
109
+ try {
110
+ assertNoClosure('() => x', 'run');
111
+ assert.fail('should have thrown');
112
+ } catch (e) {
113
+ const msg = (e as Error).message;
114
+ assert.ok(msg.includes('.toString()'));
115
+ assert.ok(msg.includes('worker'));
116
+ assert.ok(msg.includes('setup'));
117
+ assert.ok(msg.includes('data'));
118
+ }
119
+ });
120
+ });
121
+
122
+ describe('edge cases', () => {
123
+ it('silently passes on unparseable code', () => {
124
+ assert.doesNotThrow(() => assertNoClosure('not valid js {{{', 'run'));
125
+ });
126
+
127
+ it('empty arrow function', () => {
128
+ assert.doesNotThrow(() => assertNoClosure('() => {}', 'run'));
129
+ });
130
+
131
+ it('undefined return', () => {
132
+ assert.doesNotThrow(() => assertNoClosure('() => undefined', 'run'));
133
+ });
134
+ });
135
+ });
@@ -0,0 +1,48 @@
1
+ import { describe, it, mock } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ describe('Benchmark.execute', () => {
5
+ it('returns an error report when executor task setup fails', async () => {
6
+ const pushAsync = mock.fn(() => Promise.reject(new Error('Benchmark "run" function references outer-scope variables: port')));
7
+ const kill = mock.fn();
8
+
9
+ mock.module('../executor.ts', {
10
+ namedExports: {
11
+ createExecutor: () => ({ pushAsync, kill }),
12
+ },
13
+ });
14
+
15
+ const { Benchmark } = await import('../index.ts');
16
+
17
+ const bench = Benchmark.create('feed');
18
+ bench.target('target').measure('run', () => 1);
19
+
20
+ const reports = await bench.execute();
21
+
22
+ assert.deepStrictEqual(reports, [
23
+ {
24
+ target: 'target',
25
+ measures: [
26
+ {
27
+ measure: 'run',
28
+ feeds: [
29
+ {
30
+ feed: 'feed',
31
+ data: {
32
+ count: 0,
33
+ heapUsedKB: 0,
34
+ dceWarning: false,
35
+ error: 'Benchmark "run" function references outer-scope variables: port',
36
+ },
37
+ },
38
+ ],
39
+ },
40
+ ],
41
+ },
42
+ ]);
43
+ assert.strictEqual(pushAsync.mock.callCount(), 1);
44
+ assert.strictEqual(kill.mock.callCount(), 1);
45
+
46
+ mock.restoreAll();
47
+ });
48
+ });