overtake 1.0.4 → 1.1.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 +68 -25
  2. package/bin/overtake.js +1 -1
  3. package/build/cli.cjs +44 -33
  4. package/build/cli.cjs.map +1 -1
  5. package/build/cli.js +43 -32
  6. package/build/cli.js.map +1 -1
  7. package/build/executor.cjs +6 -3
  8. package/build/executor.cjs.map +1 -1
  9. package/build/executor.d.ts +3 -2
  10. package/build/executor.js +6 -3
  11. package/build/executor.js.map +1 -1
  12. package/build/gc-watcher.cjs +31 -0
  13. package/build/gc-watcher.cjs.map +1 -0
  14. package/build/gc-watcher.d.ts +9 -0
  15. package/build/gc-watcher.js +21 -0
  16. package/build/gc-watcher.js.map +1 -0
  17. package/build/index.cjs +9 -1
  18. package/build/index.cjs.map +1 -1
  19. package/build/index.d.ts +1 -1
  20. package/build/index.js +9 -1
  21. package/build/index.js.map +1 -1
  22. package/build/runner.cjs +226 -18
  23. package/build/runner.cjs.map +1 -1
  24. package/build/runner.d.ts +1 -1
  25. package/build/runner.js +226 -18
  26. package/build/runner.js.map +1 -1
  27. package/build/types.cjs.map +1 -1
  28. package/build/types.d.ts +4 -0
  29. package/build/types.js.map +1 -1
  30. package/build/utils.cjs +21 -0
  31. package/build/utils.cjs.map +1 -1
  32. package/build/utils.d.ts +1 -0
  33. package/build/utils.js +18 -0
  34. package/build/utils.js.map +1 -1
  35. package/build/worker.cjs +95 -8
  36. package/build/worker.cjs.map +1 -1
  37. package/build/worker.js +54 -8
  38. package/build/worker.js.map +1 -1
  39. package/examples/accuracy.ts +54 -0
  40. package/examples/complete.ts +3 -3
  41. package/examples/custom-reports.ts +21 -0
  42. package/examples/imports.ts +47 -0
  43. package/examples/quick-start.ts +10 -9
  44. package/package.json +10 -9
  45. package/src/cli.ts +46 -30
  46. package/src/executor.ts +8 -2
  47. package/src/gc-watcher.ts +23 -0
  48. package/src/index.ts +11 -0
  49. package/src/runner.ts +266 -17
  50. package/src/types.ts +4 -0
  51. package/src/utils.ts +20 -0
  52. package/src/worker.ts +59 -9
  53. package/CLAUDE.md +0 -145
  54. package/examples/array-copy.ts +0 -17
  55. package/examples/object-merge.ts +0 -41
  56. package/examples/serialization.ts +0 -22
package/src/utils.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { transform } from '@swc/core';
2
+
1
3
  export const abs = (value: bigint) => {
2
4
  if (value < 0n) {
3
5
  return -value;
@@ -63,3 +65,21 @@ export class ScaledBigInt {
63
65
  return Number(div(this.value, this.scale));
64
66
  }
65
67
  }
68
+
69
+ export const transpile = async (code: string): Promise<string> => {
70
+ const output = await transform(code, {
71
+ filename: 'benchmark.ts',
72
+ jsc: {
73
+ parser: {
74
+ syntax: 'typescript',
75
+ tsx: false,
76
+ dynamicImport: true,
77
+ },
78
+ target: 'esnext',
79
+ },
80
+ module: {
81
+ type: 'es6',
82
+ },
83
+ });
84
+ return output.code;
85
+ };
package/src/worker.ts CHANGED
@@ -1,8 +1,12 @@
1
1
  import { workerData } from 'node:worker_threads';
2
+ import { SourceTextModule, SyntheticModule, createContext } from 'node:vm';
3
+ import { createRequire } from 'node:module';
4
+ import { fileURLToPath } from 'node:url';
2
5
  import { benchmark } from './runner.js';
3
- import { SetupFn, TeardownFn, StepFn, WorkerOptions } from './types.js';
6
+ import { WorkerOptions } from './types.js';
4
7
 
5
8
  const {
9
+ baseUrl,
6
10
  setupCode,
7
11
  teardownCode,
8
12
  preCode,
@@ -14,19 +18,66 @@ const {
14
18
  minCycles,
15
19
  absThreshold,
16
20
  relThreshold,
21
+ gcObserver = true,
17
22
 
18
23
  durationsSAB,
19
24
  controlSAB,
20
25
  }: WorkerOptions = workerData;
21
26
 
22
- const setup: SetupFn<unknown> = setupCode && Function(`return ${setupCode};`)();
23
- const teardown: TeardownFn<unknown> = teardownCode && Function(`return ${teardownCode};`)();
27
+ const serialize = (code?: string) => (code ? code : '() => {}');
24
28
 
25
- const pre: StepFn<unknown, unknown> = preCode && Function(`return ${preCode};`)();
26
- const run: StepFn<unknown, unknown> = runCode && Function(`return ${runCode};`)();
27
- const post: StepFn<unknown, unknown> = postCode && Function(`return ${postCode};`)();
29
+ const source = `
30
+ export const setup = ${serialize(setupCode)};
31
+ export const teardown = ${serialize(teardownCode)};
32
+ export const pre = ${serialize(preCode)};
33
+ export const run = ${serialize(runCode)};
34
+ export const post = ${serialize(postCode)};
35
+ `;
28
36
 
29
- const exitCode = await benchmark({
37
+ const context = createContext({ console, Buffer });
38
+ const imports = new Map<string, SyntheticModule>();
39
+ const mod = new SourceTextModule(source, {
40
+ identifier: baseUrl,
41
+ context,
42
+ initializeImportMeta(meta) {
43
+ meta.url = baseUrl;
44
+ },
45
+ importModuleDynamically(specifier, referencingModule) {
46
+ const base = referencingModule.identifier ?? baseUrl;
47
+ const resolveFrom = createRequire(fileURLToPath(base));
48
+ return import(resolveFrom.resolve(specifier));
49
+ },
50
+ });
51
+
52
+ await mod.link(async (specifier, referencingModule) => {
53
+ const base = referencingModule.identifier ?? baseUrl;
54
+ const resolveFrom = createRequire(fileURLToPath(base));
55
+ const target = resolveFrom.resolve(specifier);
56
+ const cached = imports.get(target);
57
+ if (cached) return cached;
58
+
59
+ const importedModule = await import(target);
60
+ const exportNames = Object.keys(importedModule);
61
+ const imported = new SyntheticModule(
62
+ exportNames,
63
+ () => {
64
+ exportNames.forEach((key) => imported.setExport(key, importedModule[key]));
65
+ },
66
+ { identifier: target, context: referencingModule.context },
67
+ );
68
+ imports.set(target, imported);
69
+ return imported;
70
+ });
71
+
72
+ await mod.evaluate();
73
+ const { setup, teardown, pre, run, post } = mod.namespace as any;
74
+
75
+ if (!run) {
76
+ throw new Error('Benchmark run function is required');
77
+ }
78
+
79
+ process.exitCode = await benchmark({
80
+ baseUrl,
30
81
  setup,
31
82
  teardown,
32
83
  pre,
@@ -38,9 +89,8 @@ const exitCode = await benchmark({
38
89
  minCycles,
39
90
  absThreshold,
40
91
  relThreshold,
92
+ gcObserver,
41
93
 
42
94
  durationsSAB,
43
95
  controlSAB,
44
96
  });
45
-
46
- process.exit(exitCode);
package/CLAUDE.md DELETED
@@ -1,145 +0,0 @@
1
- # CLAUDE.md
2
-
3
- This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
-
5
- ## Project Overview
6
-
7
- Overtake is a high-precision JavaScript/TypeScript benchmarking library that uses worker thread isolation and statistical convergence to provide accurate performance measurements. It solves common benchmarking problems like JIT optimization interference and cross-benchmark contamination.
8
-
9
- ## Development Commands
10
-
11
- ```bash
12
- # Build the project (uses inop for transpilation + tsc for declarations only)
13
- npm run build
14
- # or with pnpm
15
- pnpm build
16
-
17
- # Run tests (uses Jest with SWC transpilation - no test files currently exist)
18
- npm test
19
-
20
- # Execute benchmarks via CLI
21
- npx overtake "examples/*.ts" -f table -r ops mean p95
22
-
23
- # Run a single benchmark file
24
- npx overtake examples/quick-start.ts
25
-
26
- # Start the CLI directly (requires argument)
27
- npm start examples/quick-start.ts
28
-
29
- # Format code with Prettier (via pre-commit hook)
30
- npx prettier --write "src/**/*.ts" "examples/**/*.ts"
31
-
32
- # Note: Package manager is pnpm@10.14.0 (see packageManager field in package.json)
33
- ```
34
-
35
- ## Architecture
36
-
37
- ### Core Components
38
-
39
- 1. **Benchmark Class** (`src/index.ts`): Main API with fluent interface for defining benchmarks using feed → target → measure pattern
40
- 2. **Executor** (`src/executor.ts`): Worker thread pool management using async queues and SharedArrayBuffer for zero-copy communication
41
- 3. **Worker** (`src/worker.ts`): Isolated execution environment with fresh V8 context per benchmark
42
- 4. **Runner** (`src/runner.ts`): Handles warmup cycles, statistical convergence detection, and timing collection
43
- 5. **Reporter** (`src/reporter.ts`): Statistical analysis and result formatting (ops/sec, percentiles, mean/median/mode)
44
-
45
- ### Key Design Patterns
46
-
47
- - **Worker Thread Isolation**: Each benchmark runs in separate thread to prevent contamination
48
- - **SharedArrayBuffer Communication**: Zero-copy data transfer for high-precision bigint timing
49
- - **Statistical Convergence**: Automatic cycle adjustment based on configurable confidence thresholds
50
- - **Dynamic Code Execution**: Function serialization across threads with VM module sandboxing
51
-
52
- ### API Structure
53
-
54
- ```typescript
55
- // Fluent API pattern
56
- benchmark('name', feedFunction)
57
- .target('implementation')
58
- .measure('operation', measureFunction)
59
- .execute() // Returns Promise<Report[]>
60
- ```
61
-
62
- ## Technical Requirements
63
-
64
- - Node.js >=22 (uses modern features like VM modules)
65
- - ES modules only (no CommonJS)
66
- - TypeScript with ESNext target
67
- - Uses SWC for transpilation (not tsc for builds)
68
-
69
- ## Important Implementation Notes
70
-
71
- - **CRITICAL**: All imports must be dynamic inside target callbacks since they run in worker threads:
72
- ```typescript
73
- // CORRECT - dynamic import inside target
74
- .target('V8', async () => {
75
- const { serialize } = await import('node:v8');
76
- return { serialize };
77
- })
78
-
79
- // WRONG - static import at top level
80
- import { serialize } from 'node:v8';
81
- .target('V8', () => ({ serialize }))
82
- ```
83
- - Timing uses `process.hrtime.bigint()` for nanosecond precision
84
- - Worker threads communicate via SharedArrayBuffer to minimize overhead
85
- - Build process uses `inop` tool for transpilation followed by tsc for declarations only
86
- - Test files should match `*.spec.ts` or `*.test.ts` patterns (currently no tests exist)
87
-
88
- ## API Usage Modes
89
-
90
- ### CLI Mode (Global `benchmark` function)
91
- - Used when running via `npx overtake file.ts`
92
- - CLI provides global `benchmark` function automatically
93
- - No imports needed, no `.execute()` call required
94
- - Results printed based on CLI flags
95
-
96
- Example (examples/quick-start.ts):
97
- ```typescript
98
- const suite = benchmark('name', () => data);
99
- suite.target('impl').measure('op', (ctx, input) => { /* ... */ });
100
- ```
101
-
102
- ### Programmatic Mode (Import Benchmark class)
103
- - Used for standalone scripts with custom execution
104
- - Must import Benchmark class and printer functions
105
- - Must call `.execute()` and handle results
106
-
107
- Example (examples/complete.ts):
108
- ```typescript
109
- import { Benchmark, printJSONReports } from '../build/index.js';
110
- const suite = new Benchmark('name', () => data);
111
- // ... define targets and measures
112
- const reports = await suite.execute({ /* options */ });
113
- printJSONReports(reports, 2);
114
- ```
115
-
116
- ## Common Issues and Solutions
117
-
118
- ### TypeScript Transpilation
119
- - CLI uses SWC's `transform` function to strip TypeScript types
120
- - If you see "Missing initializer in const declaration", it means TypeScript wasn't transpiled
121
- - The transpiler in `src/cli.ts` must use `transform`, not `parse`/`print`
122
-
123
- ### Benchmarks Not Running
124
- - CLI mode: Ensure using global `benchmark` function (no import)
125
- - Programmatic mode: Must call `.execute()` and handle results
126
- - Check that worker count doesn't exceed CPU cores
127
-
128
- ### Memory Management Patterns
129
- - Use `gcBlock` Set to prevent garbage collection during measurements:
130
- ```typescript
131
- .target('name', () => {
132
- const gcBlock = new Set(); // Prevents GC
133
- return { gcBlock };
134
- })
135
- .measure('op', ({ gcBlock }, input) => {
136
- gcBlock.add(result); // Keep reference alive
137
- })
138
- ```
139
-
140
- ## Code Quality Tools
141
-
142
- - **Formatter**: Prettier (config in `.prettierrc`) - runs automatically on commit via husky pre-commit hook
143
- - **Test Runner**: Jest with SWC transpilation (config in `jest.config.js`)
144
- - **TypeScript Config**: Strict mode enabled, targets ESNext with NodeNext modules
145
- - **No linter or type-check commands**: Add these if needed in the future
@@ -1,17 +0,0 @@
1
- const copySuite = benchmark('1M array of strings', () => Array.from({ length: 1_000_000 }, (_, idx) => `${idx}`))
2
- .feed('1M array of numbers', () => Array.from({ length: 1_000_000 }, (_, idx) => idx))
3
- .feed('1M typed array', () => new Uint32Array(1_000_000).map((_, idx) => idx));
4
-
5
- copySuite.target('for loop').measure('copy half', (_, input) => {
6
- const n = input?.length ?? 0;
7
- const mid = n / 2;
8
- for (let i = 0; i < mid; i++) {
9
- input[i + mid] = input[i];
10
- }
11
- });
12
-
13
- copySuite.target('copyWithin').measure('copy half', (_, input) => {
14
- const n = input?.length ?? 0;
15
- const mid = n / 2;
16
- input.copyWithin(mid, 0, mid);
17
- });
@@ -1,41 +0,0 @@
1
- import { Benchmark, printSimpleReports } from '../build/index.js';
2
-
3
- const objectMergeSuite = new Benchmark('1K array of objects', () => Array.from({ length: 1_000 }, (_, idx) => ({ [idx]: idx })));
4
-
5
- objectMergeSuite.target('reduce destructure').measure('data', (_, input) => {
6
- input.reduce((acc, obj) => {
7
- return { ...acc, ...obj };
8
- }, {});
9
- });
10
-
11
- objectMergeSuite.target('reduce assign').measure('data', (_, input) => {
12
- input.reduce((acc, obj) => {
13
- Object.assign(acc, obj);
14
- return acc;
15
- }, {});
16
- });
17
-
18
- objectMergeSuite.target('forEach assign').measure('data', (_, input) => {
19
- const result = {};
20
- input.forEach((obj) => {
21
- Object.assign(result, obj);
22
- });
23
- });
24
-
25
- objectMergeSuite.target('for assign').measure('data', (_, input) => {
26
- const result = {};
27
- for (let i = 0; i < input.length; i++) {
28
- Object.assign(result, input[i]);
29
- }
30
- });
31
-
32
- objectMergeSuite.target('assign').measure('data', (_, input) => {
33
- Object.assign({}, ...input);
34
- });
35
-
36
- const reports = await objectMergeSuite.execute({
37
- reportTypes: ['ops'],
38
- maxCycles: 10_000,
39
- });
40
-
41
- printSimpleReports(reports);
@@ -1,22 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
-
3
- const serializeSuite = benchmark('1K strings', () => Array.from({ length: 10_000 }, () => randomUUID()));
4
-
5
- const v8Target = serializeSuite.target('V8', async () => {
6
- const { serialize, deserialize } = await import('node:v8');
7
- const gcBlock = new Set();
8
- return { serialize, deserialize, gcBlock };
9
- });
10
-
11
- v8Target.measure('serialize', ({ serialize, gcBlock }, input) => {
12
- gcBlock.add(serialize(input));
13
- });
14
-
15
- serializeSuite
16
- .target('JSON', () => {
17
- const gcBlock = new Set();
18
- return { gcBlock };
19
- })
20
- .measure('serialize', ({ gcBlock }, input) => {
21
- gcBlock.add(JSON.stringify(input));
22
- });