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.
- package/README.md +68 -25
- package/bin/overtake.js +1 -1
- package/build/cli.cjs +44 -33
- package/build/cli.cjs.map +1 -1
- package/build/cli.js +43 -32
- package/build/cli.js.map +1 -1
- package/build/executor.cjs +6 -3
- package/build/executor.cjs.map +1 -1
- package/build/executor.d.ts +3 -2
- package/build/executor.js +6 -3
- package/build/executor.js.map +1 -1
- package/build/gc-watcher.cjs +31 -0
- package/build/gc-watcher.cjs.map +1 -0
- package/build/gc-watcher.d.ts +9 -0
- package/build/gc-watcher.js +21 -0
- package/build/gc-watcher.js.map +1 -0
- package/build/index.cjs +9 -1
- package/build/index.cjs.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.js +9 -1
- package/build/index.js.map +1 -1
- package/build/runner.cjs +226 -18
- package/build/runner.cjs.map +1 -1
- package/build/runner.d.ts +1 -1
- package/build/runner.js +226 -18
- package/build/runner.js.map +1 -1
- package/build/types.cjs.map +1 -1
- package/build/types.d.ts +4 -0
- package/build/types.js.map +1 -1
- package/build/utils.cjs +21 -0
- package/build/utils.cjs.map +1 -1
- package/build/utils.d.ts +1 -0
- package/build/utils.js +18 -0
- package/build/utils.js.map +1 -1
- package/build/worker.cjs +95 -8
- package/build/worker.cjs.map +1 -1
- package/build/worker.js +54 -8
- package/build/worker.js.map +1 -1
- package/examples/accuracy.ts +54 -0
- package/examples/complete.ts +3 -3
- package/examples/custom-reports.ts +21 -0
- package/examples/imports.ts +47 -0
- package/examples/quick-start.ts +10 -9
- package/package.json +10 -9
- package/src/cli.ts +46 -30
- package/src/executor.ts +8 -2
- package/src/gc-watcher.ts +23 -0
- package/src/index.ts +11 -0
- package/src/runner.ts +266 -17
- package/src/types.ts +4 -0
- package/src/utils.ts +20 -0
- package/src/worker.ts +59 -9
- package/CLAUDE.md +0 -145
- package/examples/array-copy.ts +0 -17
- package/examples/object-merge.ts +0 -41
- 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 {
|
|
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
|
|
23
|
-
const teardown: TeardownFn<unknown> = teardownCode && Function(`return ${teardownCode};`)();
|
|
27
|
+
const serialize = (code?: string) => (code ? code : '() => {}');
|
|
24
28
|
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
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
|
|
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
|
package/examples/array-copy.ts
DELETED
|
@@ -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
|
-
});
|
package/examples/object-merge.ts
DELETED
|
@@ -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
|
-
});
|