overtake 1.0.2 → 1.0.4
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/CLAUDE.md +145 -0
- package/README.md +170 -192
- package/build/cli.cjs +9 -12
- package/build/cli.cjs.map +1 -1
- package/build/cli.js +10 -13
- package/build/cli.js.map +1 -1
- package/examples/array-copy.ts +3 -3
- package/examples/complete.ts +95 -0
- package/examples/object-merge.ts +7 -7
- package/examples/quick-start.ts +16 -0
- package/examples/serialization.ts +22 -0
- package/package.json +19 -6
- package/src/cli.ts +9 -13
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
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/README.md
CHANGED
|
@@ -1,284 +1,262 @@
|
|
|
1
1
|
# Overtake
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
⚡ The fastest, most accurate JavaScript benchmarking library. Worker-isolated, statistically-rigorous, zero-overhead.
|
|
4
4
|
|
|
5
5
|
[![Build Status][github-image]][github-url]
|
|
6
6
|
[![NPM version][npm-image]][npm-url]
|
|
7
7
|
[![Downloads][downloads-image]][npm-url]
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
```bash
|
|
10
|
+
npm install -D overtake
|
|
11
|
+
```
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## 5-Second Quick Start
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
- [Quick Start](#quick-start)
|
|
19
|
-
- [API Guide](#api-guide)
|
|
20
|
-
- [Examples](#examples)
|
|
21
|
-
- [CLI Usage](#cli-usage)
|
|
22
|
-
- [License](#license)
|
|
15
|
+
```typescript
|
|
16
|
+
// benchmark.ts
|
|
17
|
+
const suite = benchmark('1M numbers', () => Array.from({ length: 1e6 }, (_, i) => i));
|
|
23
18
|
|
|
24
|
-
|
|
19
|
+
suite.target('for loop').measure('sum', (_, arr) => {
|
|
20
|
+
let sum = 0;
|
|
21
|
+
for (let i = 0; i < arr.length; i++) sum += arr[i];
|
|
22
|
+
return sum;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
suite.target('reduce').measure('sum', (_, arr) => arr.reduce((a, b) => a + b));
|
|
26
|
+
```
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
```bash
|
|
29
|
+
npx overtake benchmark.ts
|
|
30
|
+
|
|
31
|
+
# Output:
|
|
32
|
+
# for loop sum
|
|
33
|
+
# 1M numbers: 1,607 ops/s
|
|
34
|
+
#
|
|
35
|
+
# reduce sum
|
|
36
|
+
# 1M numbers: 238 ops/s (6.7x slower)
|
|
37
|
+
```
|
|
27
38
|
|
|
28
|
-
|
|
29
|
-
- **Memory pressure artifacts** - GC pauses and memory allocation affect timing
|
|
30
|
-
- **Cross-benchmark contamination** - Previous tests affect subsequent measurements
|
|
31
|
-
- **Insufficient sample sizes** - Results vary wildly between runs
|
|
39
|
+
## Why Overtake?
|
|
32
40
|
|
|
33
|
-
|
|
41
|
+
**The Problem**: JavaScript benchmarks lie. JIT optimizations, garbage collection, and shared state make results meaningless.
|
|
34
42
|
|
|
35
|
-
|
|
36
|
-
- **Statistical convergence** - Automatically runs until results are statistically stable
|
|
37
|
-
- **Zero-copy result collection** - Uses SharedArrayBuffer to eliminate serialization overhead
|
|
38
|
-
- **Proper warmup cycles** - Ensures JIT optimization before measurement
|
|
39
|
-
- **Concurrent execution** - Runs multiple benchmarks in parallel for faster results
|
|
43
|
+
**The Solution**: Overtake runs every benchmark in an isolated worker thread with a fresh V8 context. No contamination. No lies.
|
|
40
44
|
|
|
41
|
-
|
|
45
|
+
| Feature | Overtake | Benchmark.js | Tinybench |
|
|
46
|
+
| ----------------------- | -------------------------- | ----------------- | ----------------- |
|
|
47
|
+
| Worker isolation | ✅ Each benchmark isolated | ❌ Shared context | ❌ Shared context |
|
|
48
|
+
| Active maintenance | ✅ 2025 | ❌ Archived 2017 | ✅ 2025 |
|
|
49
|
+
| Statistical convergence | ✅ Auto-adjusts cycles | ⚠️ Manual config | ⚠️ Manual config |
|
|
50
|
+
| Zero-copy timing | ✅ SharedArrayBuffer | ❌ Serialization | ❌ Serialization |
|
|
51
|
+
| TypeScript support | ✅ Built-in | ❌ Manual setup | ⚠️ Needs config |
|
|
42
52
|
|
|
43
|
-
|
|
44
|
-
- 📊 **Statistical convergence** with configurable confidence thresholds
|
|
45
|
-
- 🔄 **Automatic warmup cycles** to stabilize JIT optimization
|
|
46
|
-
- 💻 **TypeScript support** with transpilation built-in
|
|
47
|
-
- 🎯 **Multiple comparison targets** in a single benchmark
|
|
48
|
-
- 📈 **Rich statistics** including percentiles, mean, median, mode
|
|
49
|
-
- 🖥️ **CLI and programmatic API**
|
|
50
|
-
- ⚡ **Zero-copy communication** using SharedArrayBuffer
|
|
53
|
+
## Core Concepts
|
|
51
54
|
|
|
52
|
-
|
|
55
|
+
- **Feed**: Input data to benchmark (`'1M numbers'` → array of 1 million numbers)
|
|
56
|
+
- **Target**: Implementation variant (`'for loop'` vs `'reduce'`)
|
|
57
|
+
- **Measure**: Operation to time (`'sum'` operation)
|
|
58
|
+
- **Isolation**: Each benchmark runs in a separate worker thread with fresh V8 context
|
|
53
59
|
|
|
54
|
-
|
|
60
|
+
## Installation
|
|
55
61
|
|
|
56
62
|
```bash
|
|
57
|
-
|
|
63
|
+
# npm
|
|
64
|
+
npm install -D overtake
|
|
65
|
+
|
|
66
|
+
# pnpm
|
|
67
|
+
pnpm add -D overtake
|
|
68
|
+
|
|
69
|
+
# yarn
|
|
70
|
+
yarn add -D overtake
|
|
58
71
|
```
|
|
59
72
|
|
|
60
|
-
|
|
73
|
+
## ⚠️ Critical: Dynamic Imports Required
|
|
61
74
|
|
|
62
|
-
|
|
63
|
-
|
|
75
|
+
**Benchmarks run in isolated workers. Modules MUST be imported dynamically:**
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
// ❌ WRONG - Static import won't work in worker
|
|
79
|
+
import { serialize } from 'node:v8';
|
|
80
|
+
benchmark('data', getData).target('v8', () => ({ serialize })); // serialize is undefined!
|
|
81
|
+
|
|
82
|
+
// ✅ CORRECT - Dynamic import inside target
|
|
83
|
+
benchmark('data', getData)
|
|
84
|
+
.target('v8', async () => {
|
|
85
|
+
const { serialize } = await import('node:v8');
|
|
86
|
+
return { serialize };
|
|
87
|
+
})
|
|
88
|
+
.measure('serialize', ({ serialize }, input) => serialize(input));
|
|
64
89
|
```
|
|
65
90
|
|
|
66
|
-
##
|
|
91
|
+
## Usage
|
|
67
92
|
|
|
68
|
-
###
|
|
93
|
+
### CLI Mode (Recommended)
|
|
69
94
|
|
|
70
|
-
|
|
95
|
+
When using `npx overtake`, a global `benchmark` function is provided:
|
|
71
96
|
|
|
72
97
|
```typescript
|
|
73
|
-
// benchmark.ts
|
|
74
|
-
|
|
75
|
-
.
|
|
76
|
-
.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
suite.target('reduce').measure('sum', (_, input) => {
|
|
85
|
-
return Array.from({ length: 1000 }, (_, i) => i).reduce((a, b) => a + b, 0);
|
|
86
|
-
});
|
|
98
|
+
// benchmark.ts - No imports needed!
|
|
99
|
+
benchmark('small', () => generateSmallData())
|
|
100
|
+
.feed('large', () => generateLargeData())
|
|
101
|
+
.target('algorithm A')
|
|
102
|
+
.measure('process', (_, input) => processA(input))
|
|
103
|
+
.target('algorithm B')
|
|
104
|
+
.measure('process', (_, input) => processB(input));
|
|
105
|
+
|
|
106
|
+
// No .execute() needed - CLI handles it
|
|
87
107
|
```
|
|
88
108
|
|
|
89
|
-
Run with CLI:
|
|
90
|
-
|
|
91
109
|
```bash
|
|
92
|
-
npx overtake benchmark.ts
|
|
110
|
+
npx overtake benchmark.ts --format table
|
|
93
111
|
```
|
|
94
112
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
### Core Concepts
|
|
113
|
+
### Programmatic Mode
|
|
98
114
|
|
|
99
|
-
|
|
100
|
-
2. **Feed**: Different input data sets to test with
|
|
101
|
-
3. **Target**: Different implementations to compare (e.g., "for loop" vs "reduce")
|
|
102
|
-
4. **Measure**: Specific operations to measure for each target
|
|
103
|
-
|
|
104
|
-
### Creating Benchmarks
|
|
115
|
+
For custom integration, import the Benchmark class:
|
|
105
116
|
|
|
106
117
|
```typescript
|
|
107
|
-
|
|
108
|
-
const suite = benchmark('Test name', () => generateInputData());
|
|
118
|
+
import { Benchmark, printTableReports } from 'overtake';
|
|
109
119
|
|
|
110
|
-
|
|
111
|
-
suite.feed('small dataset', () => generateSmallData()).feed('large dataset', () => generateLargeData());
|
|
120
|
+
const suite = new Benchmark('dataset', () => getData());
|
|
112
121
|
|
|
113
|
-
|
|
114
|
-
suite.target('implementation A').measure('operation', (ctx, input) => {
|
|
115
|
-
// Your code here
|
|
116
|
-
});
|
|
122
|
+
suite.target('impl').measure('op', (_, input) => process(input));
|
|
117
123
|
|
|
118
|
-
|
|
119
|
-
|
|
124
|
+
// Must explicitly execute
|
|
125
|
+
const reports = await suite.execute({
|
|
126
|
+
workers: 4,
|
|
127
|
+
reportTypes: ['ops', 'mean', 'p95'],
|
|
120
128
|
});
|
|
129
|
+
|
|
130
|
+
printTableReports(reports);
|
|
121
131
|
```
|
|
122
132
|
|
|
123
|
-
|
|
133
|
+
## API Reference
|
|
134
|
+
|
|
135
|
+
### Creating Benchmarks
|
|
124
136
|
|
|
125
137
|
```typescript
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
138
|
+
// Create with initial feed
|
|
139
|
+
benchmark('initial data', () => data)
|
|
140
|
+
.feed('more data', () => moreData) // Add more datasets
|
|
141
|
+
|
|
142
|
+
// Define what to compare
|
|
143
|
+
.target('implementation A')
|
|
144
|
+
.measure('operation', (ctx, input) => {
|
|
145
|
+
/* ... */
|
|
131
146
|
})
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
.teardown(async (ctx) => {
|
|
137
|
-
// Cleanup: runs once after measurements
|
|
138
|
-
await ctx.connection.close();
|
|
147
|
+
|
|
148
|
+
.target('implementation B')
|
|
149
|
+
.measure('operation', (ctx, input) => {
|
|
150
|
+
/* ... */
|
|
139
151
|
});
|
|
140
152
|
```
|
|
141
153
|
|
|
142
|
-
###
|
|
154
|
+
### Targets with Setup
|
|
143
155
|
|
|
144
156
|
```typescript
|
|
157
|
+
const suite = benchmark('data', () => Buffer.from('test data'));
|
|
158
|
+
|
|
145
159
|
suite
|
|
146
|
-
.target('with
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
// Runs before EACH measurement
|
|
152
|
-
prepareData(input);
|
|
160
|
+
.target('with setup', async () => {
|
|
161
|
+
// Setup runs once before measurements
|
|
162
|
+
const { createHash } = await import('node:crypto');
|
|
163
|
+
const cache = new Map();
|
|
164
|
+
return { createHash, cache }; // Available as ctx in measure
|
|
153
165
|
})
|
|
154
|
-
.
|
|
155
|
-
//
|
|
156
|
-
|
|
166
|
+
.measure('hash', ({ createHash, cache }, input) => {
|
|
167
|
+
// ctx contains setup return value
|
|
168
|
+
const hash = createHash('sha256').update(input).digest();
|
|
169
|
+
cache.set(input, hash);
|
|
170
|
+
return hash;
|
|
157
171
|
});
|
|
158
172
|
```
|
|
159
173
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
### Example 1: Array Operations Comparison
|
|
163
|
-
|
|
164
|
-
This example compares different methods for copying array elements:
|
|
174
|
+
### Preventing Garbage Collection
|
|
165
175
|
|
|
166
176
|
```typescript
|
|
167
|
-
|
|
168
|
-
const suite = benchmark('1M array of strings', () => Array.from({ length: 1_000_000 }, (_, idx) => `${idx}`))
|
|
169
|
-
.feed('1M array of numbers', () => Array.from({ length: 1_000_000 }, (_, idx) => idx))
|
|
170
|
-
.feed('1M typed array', () => new Uint32Array(1_000_000).map((_, idx) => idx));
|
|
171
|
-
|
|
172
|
-
suite.target('for loop').measure('copy half', (_, input) => {
|
|
173
|
-
const n = input?.length ?? 0;
|
|
174
|
-
const mid = n / 2;
|
|
175
|
-
for (let i = 0; i < mid; i++) {
|
|
176
|
-
input[i + mid] = input[i];
|
|
177
|
-
}
|
|
178
|
-
});
|
|
177
|
+
const suite = benchmark('data', () => [1, 2, 3, 4, 5]);
|
|
179
178
|
|
|
180
|
-
suite
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
})
|
|
179
|
+
suite
|
|
180
|
+
.target('no GC', () => {
|
|
181
|
+
const gcBlock = new Set(); // Keeps references alive
|
|
182
|
+
return { gcBlock };
|
|
183
|
+
})
|
|
184
|
+
.measure('process', ({ gcBlock }, input) => {
|
|
185
|
+
const result = input.map((x) => x * x);
|
|
186
|
+
gcBlock.add(result); // Prevent GC during measurement
|
|
187
|
+
return result;
|
|
188
|
+
});
|
|
185
189
|
```
|
|
186
190
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
- `copyWithin` is ~5x faster for typed arrays
|
|
190
|
-
- `for loop` performs consistently across all array types
|
|
191
|
-
- Regular arrays have different performance characteristics than typed arrays
|
|
192
|
-
|
|
193
|
-
### Example 2: Object Merging Strategies
|
|
191
|
+
## Examples
|
|
194
192
|
|
|
195
|
-
|
|
193
|
+
### Serialization Comparison
|
|
196
194
|
|
|
197
195
|
```typescript
|
|
198
|
-
//
|
|
199
|
-
|
|
196
|
+
// Compare V8 vs JSON serialization
|
|
197
|
+
const suite = benchmark('10K strings', () => Array.from({ length: 10_000 }, (_, i) => `string-${i}`));
|
|
200
198
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
})
|
|
199
|
+
suite
|
|
200
|
+
.target('V8', async () => {
|
|
201
|
+
const { serialize } = await import('node:v8');
|
|
202
|
+
return { serialize };
|
|
203
|
+
})
|
|
204
|
+
.measure('serialize', ({ serialize }, input) => serialize(input));
|
|
206
205
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
Object.assign(acc, obj);
|
|
210
|
-
return acc;
|
|
211
|
-
}, {});
|
|
212
|
-
});
|
|
206
|
+
suite.target('JSON').measure('serialize', (_, input) => JSON.stringify(input));
|
|
207
|
+
```
|
|
213
208
|
|
|
214
|
-
|
|
215
|
-
return Object.assign({}, ...input);
|
|
216
|
-
});
|
|
209
|
+
**[📁 More examples in `/examples`](./examples/)**
|
|
217
210
|
|
|
218
|
-
|
|
219
|
-
reportTypes: ['ops', 'mean'],
|
|
220
|
-
maxCycles: 10_000,
|
|
221
|
-
});
|
|
211
|
+
## CLI Options
|
|
222
212
|
|
|
223
|
-
|
|
213
|
+
```bash
|
|
214
|
+
npx overtake <pattern> [options]
|
|
224
215
|
```
|
|
225
216
|
|
|
226
|
-
|
|
217
|
+
| Option | Short | Description | Default |
|
|
218
|
+
| ---------------- | ----- | ---------------------------------------- | --------- |
|
|
219
|
+
| `--format` | `-f` | Output format: `simple`, `table`, `json` | `simple` |
|
|
220
|
+
| `--report-types` | `-r` | Stats to show: `ops`, `mean`, `p95`, etc | `['ops']` |
|
|
221
|
+
| `--workers` | `-w` | Concurrent workers | CPU count |
|
|
222
|
+
| `--min-cycles` | | Minimum iterations | 50 |
|
|
223
|
+
| `--max-cycles` | | Maximum iterations | 1000 |
|
|
227
224
|
|
|
228
|
-
|
|
229
|
-
- `Object.assign` with spread is most concise and performant
|
|
230
|
-
- Mutating approaches (assign in reduce) offer similar performance
|
|
225
|
+
### Example Commands
|
|
231
226
|
|
|
232
|
-
|
|
227
|
+
```bash
|
|
228
|
+
# Run all benchmarks with table output
|
|
229
|
+
npx overtake "**/*.bench.ts" -f table
|
|
233
230
|
|
|
234
|
-
|
|
231
|
+
# Show detailed statistics
|
|
232
|
+
npx overtake bench.ts -r ops mean p95 p99
|
|
235
233
|
|
|
236
|
-
|
|
237
|
-
npx overtake
|
|
234
|
+
# Output JSON for CI
|
|
235
|
+
npx overtake bench.ts -f json > results.json
|
|
238
236
|
```
|
|
239
237
|
|
|
240
|
-
|
|
238
|
+
## Troubleshooting
|
|
241
239
|
|
|
242
|
-
|
|
243
|
-
| -------------------- | ------------------------------------------------------------------------------------------------------- | --------- |
|
|
244
|
-
| `-f, --format` | Output format: `simple`, `table`, `json`, `pjson` | `simple` |
|
|
245
|
-
| `-r, --report-types` | Statistics to display: `ops`, `mean`, `median`, `mode`, `min`, `max`, `p50`, `p75`, `p90`, `p95`, `p99` | `['ops']` |
|
|
246
|
-
| `-w, --workers` | Number of concurrent worker threads | CPU count |
|
|
247
|
-
| `--warmup-cycles` | Number of warmup iterations before measurement | 20 |
|
|
248
|
-
| `--min-cycles` | Minimum measurement cycles | 50 |
|
|
249
|
-
| `--max-cycles` | Maximum measurement cycles | 1000 |
|
|
250
|
-
| `--abs-threshold` | Absolute error threshold in nanoseconds | 1000 |
|
|
251
|
-
| `--rel-threshold` | Relative error threshold (0-1) | 0.02 |
|
|
240
|
+
### "Cannot find module" in worker
|
|
252
241
|
|
|
253
|
-
|
|
242
|
+
**Solution**: Use dynamic imports inside target callbacks (see [Critical section](#️-critical-dynamic-imports-required))
|
|
254
243
|
|
|
255
|
-
|
|
256
|
-
# Run all benchmarks in a directory
|
|
257
|
-
npx overtake "src/**/*.bench.ts" -f table
|
|
244
|
+
### No output from benchmark
|
|
258
245
|
|
|
259
|
-
|
|
260
|
-
npx overtake benchmark.ts -r ops mean median p95 p99
|
|
246
|
+
**Solution**: In CLI mode, don't import Benchmark or call `.execute()`. Use the global `benchmark` function.
|
|
261
247
|
|
|
262
|
-
|
|
263
|
-
npx overtake benchmark.ts --min-cycles 100 --max-cycles 10000
|
|
248
|
+
### Results vary between runs
|
|
264
249
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
250
|
+
**Solution**: Increase `--min-cycles` for more samples, or use the `gcBlock` pattern to prevent garbage collection.
|
|
251
|
+
|
|
252
|
+
**[🐛 Report issues](https://github.com/3axap4eHko/overtake/issues)**
|
|
268
253
|
|
|
269
254
|
## License
|
|
270
255
|
|
|
271
|
-
|
|
272
|
-
Copyright (c) 2021-present Ivan Zakharchanka
|
|
256
|
+
[Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0) © 2021-2025 Ivan Zakharchanka
|
|
273
257
|
|
|
274
258
|
[npm-url]: https://www.npmjs.com/package/overtake
|
|
275
259
|
[downloads-image]: https://img.shields.io/npm/dw/overtake.svg?maxAge=43200
|
|
276
260
|
[npm-image]: https://img.shields.io/npm/v/overtake.svg?maxAge=43200
|
|
277
261
|
[github-url]: https://github.com/3axap4eHko/overtake/actions/workflows/cicd.yml
|
|
278
262
|
[github-image]: https://github.com/3axap4eHko/overtake/actions/workflows/cicd.yml/badge.svg
|
|
279
|
-
[codecov-url]: https://codecov.io/gh/3axap4eHko/overtake
|
|
280
|
-
[codecov-image]: https://codecov.io/gh/3axap4eHko/overtake/branch/master/graph/badge.svg?token=JZ8QCGH6PI
|
|
281
|
-
[codeclimate-url]: https://codeclimate.com/github/3axap4eHko/overtake/maintainability
|
|
282
|
-
[codeclimate-image]: https://api.codeclimate.com/v1/badges/0ba20f27f6db2b0fec8c/maintainability
|
|
283
|
-
[snyk-url]: https://snyk.io/test/npm/overtake/latest
|
|
284
|
-
[snyk-image]: https://img.shields.io/snyk/vulnerabilities/github/3axap4eHko/overtake.svg?maxAge=43200
|
package/build/cli.cjs
CHANGED
|
@@ -55,21 +55,18 @@ const require1 = (0, _nodemodule.createRequire)(require("url").pathToFileURL(__f
|
|
|
55
55
|
const { name, description, version } = require1('../package.json');
|
|
56
56
|
const commander = new _commander.Command();
|
|
57
57
|
const transpile = async (code)=>{
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
dynamicImport: true,
|
|
61
|
-
target: 'esnext'
|
|
62
|
-
});
|
|
63
|
-
const output = await (0, _core.print)(ast, {
|
|
64
|
-
module: {
|
|
65
|
-
type: 'es6'
|
|
66
|
-
},
|
|
58
|
+
const output = await (0, _core.transform)(code, {
|
|
59
|
+
filename: 'benchmark.ts',
|
|
67
60
|
jsc: {
|
|
68
|
-
target: 'esnext',
|
|
69
61
|
parser: {
|
|
70
|
-
syntax: 'typescript'
|
|
62
|
+
syntax: 'typescript',
|
|
63
|
+
tsx: false,
|
|
64
|
+
dynamicImport: true
|
|
71
65
|
},
|
|
72
|
-
|
|
66
|
+
target: 'esnext'
|
|
67
|
+
},
|
|
68
|
+
module: {
|
|
69
|
+
type: 'es6'
|
|
73
70
|
}
|
|
74
71
|
});
|
|
75
72
|
return output.code;
|
package/build/cli.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["import { createRequire, Module } from 'node:module';\nimport { SyntheticModule, createContext, SourceTextModule } from 'node:vm';\nimport { stat, readFile } from 'node:fs/promises';\nimport {
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["import { createRequire, Module } from 'node:module';\nimport { SyntheticModule, createContext, SourceTextModule } from 'node:vm';\nimport { stat, readFile } from 'node:fs/promises';\nimport { transform } from '@swc/core';\nimport { Command, Option } from 'commander';\nimport { glob } from 'glob';\nimport { Benchmark, printTableReports, printJSONReports, printSimpleReports, DEFAULT_REPORT_TYPES, DEFAULT_WORKERS } from './index.js';\nimport { REPORT_TYPES } from './types.js';\n\nconst require = createRequire(import.meta.url);\nconst { name, description, version } = require('../package.json');\n\nconst commander = new Command();\n\nconst transpile = async (code: string): Promise<string> => {\n const output = await transform(code, {\n filename: 'benchmark.ts',\n jsc: {\n parser: {\n syntax: 'typescript',\n tsx: false,\n dynamicImport: true,\n },\n target: 'esnext',\n },\n module: {\n type: 'es6',\n },\n });\n return output.code;\n};\n\ncommander\n .name(name)\n .description(description)\n .version(version)\n .argument('<path>', 'glob pattern to find benchmarks')\n .addOption(new Option('-r, --report-types [reportTypes...]', 'statistic types to include in the report').choices(REPORT_TYPES).default(DEFAULT_REPORT_TYPES))\n .addOption(new Option('-w, --workers [workers]', 'number of concurent workers').default(DEFAULT_WORKERS).argParser(parseInt))\n .addOption(new Option('-f, --format [format]', 'output format').default('simple').choices(['simple', 'json', 'pjson', 'table']))\n .addOption(new Option('--abs-threshold [absThreshold]', 'absolute error threshold in nanoseconds').argParser(parseInt))\n .addOption(new Option('--rel-threshold [relThreshold]', 'relative error threshold (fraction between 0 and 1)').argParser(parseInt))\n .addOption(new Option('--warmup-cycles [warmupCycles]', 'number of warmup cycles before measuring').argParser(parseInt))\n .addOption(new Option('--max-cycles [maxCycles]', 'maximum measurement cycles per feed').argParser(parseInt))\n .addOption(new Option('--min-cycles [minCycles]', 'minimum measurement cycles per feed').argParser(parseInt))\n .action(async (path, executeOptions) => {\n const files = await glob(path, { absolute: true, cwd: process.cwd() }).catch(() => []);\n for (const file of files) {\n const stats = await stat(file).catch(() => false as const);\n if (stats && stats.isFile()) {\n const content = await readFile(file, 'utf8');\n const code = await transpile(content);\n let instance: Benchmark<unknown> | undefined;\n const benchmark = (...args: Parameters<(typeof Benchmark)['create']>) => {\n if (instance) {\n throw new Error('Only one benchmark per file is supported');\n }\n instance = Benchmark.create(...args);\n return instance;\n };\n const script = new SourceTextModule(code, {\n context: createContext({ benchmark }),\n });\n const imports = new Map();\n await script.link(async (specifier: string, referencingModule) => {\n if (imports.has(specifier)) {\n return imports.get(specifier);\n }\n const mod = await import(Module.isBuiltin(specifier) ? specifier : require.resolve(specifier));\n const exportNames = Object.keys(mod);\n const imported = new SyntheticModule(\n exportNames,\n () => {\n exportNames.forEach((key) => imported.setExport(key, mod[key]));\n },\n { identifier: specifier, context: referencingModule.context },\n );\n\n imports.set(specifier, imported);\n return imported;\n });\n await script.evaluate();\n\n if (instance) {\n const reports = await instance.execute(executeOptions);\n switch (executeOptions.format) {\n case 'json':\n {\n printJSONReports(reports);\n }\n break;\n case 'pjson':\n {\n printJSONReports(reports, 2);\n }\n break;\n case 'table':\n {\n printTableReports(reports);\n }\n break;\n default:\n printSimpleReports(reports);\n }\n }\n }\n }\n });\n\ncommander.parse(process.argv);\n"],"names":["require","createRequire","name","description","version","commander","Command","transpile","code","output","transform","filename","jsc","parser","syntax","tsx","dynamicImport","target","module","type","argument","addOption","Option","choices","REPORT_TYPES","default","DEFAULT_REPORT_TYPES","DEFAULT_WORKERS","argParser","parseInt","action","path","executeOptions","files","glob","absolute","cwd","process","catch","file","stats","stat","isFile","content","readFile","instance","benchmark","args","Error","Benchmark","create","script","SourceTextModule","context","createContext","imports","Map","link","specifier","referencingModule","has","get","mod","Module","isBuiltin","resolve","exportNames","Object","keys","imported","SyntheticModule","forEach","key","setExport","identifier","set","evaluate","reports","execute","format","printJSONReports","printTableReports","printSimpleReports","parse","argv"],"mappings":"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"}
|
package/build/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire, Module } from 'node:module';
|
|
2
2
|
import { SyntheticModule, createContext, SourceTextModule } from 'node:vm';
|
|
3
3
|
import { stat, readFile } from 'node:fs/promises';
|
|
4
|
-
import {
|
|
4
|
+
import { transform } from '@swc/core';
|
|
5
5
|
import { Command, Option } from 'commander';
|
|
6
6
|
import { glob } from 'glob';
|
|
7
7
|
import { Benchmark, printTableReports, printJSONReports, printSimpleReports, DEFAULT_REPORT_TYPES, DEFAULT_WORKERS } from "./index.js";
|
|
@@ -10,21 +10,18 @@ const require = createRequire(import.meta.url);
|
|
|
10
10
|
const { name, description, version } = require('../package.json');
|
|
11
11
|
const commander = new Command();
|
|
12
12
|
const transpile = async (code)=>{
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
dynamicImport: true,
|
|
16
|
-
target: 'esnext'
|
|
17
|
-
});
|
|
18
|
-
const output = await print(ast, {
|
|
19
|
-
module: {
|
|
20
|
-
type: 'es6'
|
|
21
|
-
},
|
|
13
|
+
const output = await transform(code, {
|
|
14
|
+
filename: 'benchmark.ts',
|
|
22
15
|
jsc: {
|
|
23
|
-
target: 'esnext',
|
|
24
16
|
parser: {
|
|
25
|
-
syntax: 'typescript'
|
|
17
|
+
syntax: 'typescript',
|
|
18
|
+
tsx: false,
|
|
19
|
+
dynamicImport: true
|
|
26
20
|
},
|
|
27
|
-
|
|
21
|
+
target: 'esnext'
|
|
22
|
+
},
|
|
23
|
+
module: {
|
|
24
|
+
type: 'es6'
|
|
28
25
|
}
|
|
29
26
|
});
|
|
30
27
|
return output.code;
|
package/build/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["import { createRequire, Module } from 'node:module';\nimport { SyntheticModule, createContext, SourceTextModule } from 'node:vm';\nimport { stat, readFile } from 'node:fs/promises';\nimport {
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["import { createRequire, Module } from 'node:module';\nimport { SyntheticModule, createContext, SourceTextModule } from 'node:vm';\nimport { stat, readFile } from 'node:fs/promises';\nimport { transform } from '@swc/core';\nimport { Command, Option } from 'commander';\nimport { glob } from 'glob';\nimport { Benchmark, printTableReports, printJSONReports, printSimpleReports, DEFAULT_REPORT_TYPES, DEFAULT_WORKERS } from './index.js';\nimport { REPORT_TYPES } from './types.js';\n\nconst require = createRequire(import.meta.url);\nconst { name, description, version } = require('../package.json');\n\nconst commander = new Command();\n\nconst transpile = async (code: string): Promise<string> => {\n const output = await transform(code, {\n filename: 'benchmark.ts',\n jsc: {\n parser: {\n syntax: 'typescript',\n tsx: false,\n dynamicImport: true,\n },\n target: 'esnext',\n },\n module: {\n type: 'es6',\n },\n });\n return output.code;\n};\n\ncommander\n .name(name)\n .description(description)\n .version(version)\n .argument('<path>', 'glob pattern to find benchmarks')\n .addOption(new Option('-r, --report-types [reportTypes...]', 'statistic types to include in the report').choices(REPORT_TYPES).default(DEFAULT_REPORT_TYPES))\n .addOption(new Option('-w, --workers [workers]', 'number of concurent workers').default(DEFAULT_WORKERS).argParser(parseInt))\n .addOption(new Option('-f, --format [format]', 'output format').default('simple').choices(['simple', 'json', 'pjson', 'table']))\n .addOption(new Option('--abs-threshold [absThreshold]', 'absolute error threshold in nanoseconds').argParser(parseInt))\n .addOption(new Option('--rel-threshold [relThreshold]', 'relative error threshold (fraction between 0 and 1)').argParser(parseInt))\n .addOption(new Option('--warmup-cycles [warmupCycles]', 'number of warmup cycles before measuring').argParser(parseInt))\n .addOption(new Option('--max-cycles [maxCycles]', 'maximum measurement cycles per feed').argParser(parseInt))\n .addOption(new Option('--min-cycles [minCycles]', 'minimum measurement cycles per feed').argParser(parseInt))\n .action(async (path, executeOptions) => {\n const files = await glob(path, { absolute: true, cwd: process.cwd() }).catch(() => []);\n for (const file of files) {\n const stats = await stat(file).catch(() => false as const);\n if (stats && stats.isFile()) {\n const content = await readFile(file, 'utf8');\n const code = await transpile(content);\n let instance: Benchmark<unknown> | undefined;\n const benchmark = (...args: Parameters<(typeof Benchmark)['create']>) => {\n if (instance) {\n throw new Error('Only one benchmark per file is supported');\n }\n instance = Benchmark.create(...args);\n return instance;\n };\n const script = new SourceTextModule(code, {\n context: createContext({ benchmark }),\n });\n const imports = new Map();\n await script.link(async (specifier: string, referencingModule) => {\n if (imports.has(specifier)) {\n return imports.get(specifier);\n }\n const mod = await import(Module.isBuiltin(specifier) ? specifier : require.resolve(specifier));\n const exportNames = Object.keys(mod);\n const imported = new SyntheticModule(\n exportNames,\n () => {\n exportNames.forEach((key) => imported.setExport(key, mod[key]));\n },\n { identifier: specifier, context: referencingModule.context },\n );\n\n imports.set(specifier, imported);\n return imported;\n });\n await script.evaluate();\n\n if (instance) {\n const reports = await instance.execute(executeOptions);\n switch (executeOptions.format) {\n case 'json':\n {\n printJSONReports(reports);\n }\n break;\n case 'pjson':\n {\n printJSONReports(reports, 2);\n }\n break;\n case 'table':\n {\n printTableReports(reports);\n }\n break;\n default:\n printSimpleReports(reports);\n }\n }\n }\n }\n });\n\ncommander.parse(process.argv);\n"],"names":["createRequire","Module","SyntheticModule","createContext","SourceTextModule","stat","readFile","transform","Command","Option","glob","Benchmark","printTableReports","printJSONReports","printSimpleReports","DEFAULT_REPORT_TYPES","DEFAULT_WORKERS","REPORT_TYPES","require","url","name","description","version","commander","transpile","code","output","filename","jsc","parser","syntax","tsx","dynamicImport","target","module","type","argument","addOption","choices","default","argParser","parseInt","action","path","executeOptions","files","absolute","cwd","process","catch","file","stats","isFile","content","instance","benchmark","args","Error","create","script","context","imports","Map","link","specifier","referencingModule","has","get","mod","isBuiltin","resolve","exportNames","Object","keys","imported","forEach","key","setExport","identifier","set","evaluate","reports","execute","format","parse","argv"],"mappings":"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"}
|
package/examples/array-copy.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
const
|
|
1
|
+
const copySuite = benchmark('1M array of strings', () => Array.from({ length: 1_000_000 }, (_, idx) => `${idx}`))
|
|
2
2
|
.feed('1M array of numbers', () => Array.from({ length: 1_000_000 }, (_, idx) => idx))
|
|
3
3
|
.feed('1M typed array', () => new Uint32Array(1_000_000).map((_, idx) => idx));
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
copySuite.target('for loop').measure('copy half', (_, input) => {
|
|
6
6
|
const n = input?.length ?? 0;
|
|
7
7
|
const mid = n / 2;
|
|
8
8
|
for (let i = 0; i < mid; i++) {
|
|
@@ -10,7 +10,7 @@ suite.target('for loop').measure('copy half', (_, input) => {
|
|
|
10
10
|
}
|
|
11
11
|
});
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
copySuite.target('copyWithin').measure('copy half', (_, input) => {
|
|
14
14
|
const n = input?.length ?? 0;
|
|
15
15
|
const mid = n / 2;
|
|
16
16
|
input.copyWithin(mid, 0, mid);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Benchmark, DEFAULT_REPORT_TYPES, printJSONReports } from '../build/index.js';
|
|
2
|
+
import { randomUUID, randomInt } from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
const length = 10 ** 4;
|
|
5
|
+
// creates a benchmark suite with 3 data feeds of objects, strings and numbers
|
|
6
|
+
const suite = new Benchmark('1M array of objects', () =>
|
|
7
|
+
Array.from({ length }, (_, idx) => ({
|
|
8
|
+
string: randomUUID(),
|
|
9
|
+
number: randomInt(length),
|
|
10
|
+
boolean: idx % 3 === 0,
|
|
11
|
+
})),
|
|
12
|
+
)
|
|
13
|
+
.feed('1M array of strings', () => Array.from({ length }, () => randomUUID()))
|
|
14
|
+
.feed('1M array of numbers', () => Array.from({ length }, () => randomInt(length)));
|
|
15
|
+
|
|
16
|
+
// create a specific benchmark target this way v8Target type is aware of
|
|
17
|
+
// serialize, deserialize and serialized properties inside the context
|
|
18
|
+
const v8Target = suite.target('v8', async () => {
|
|
19
|
+
const { serialize, deserialize } = await import('node:v8');
|
|
20
|
+
const serialized: Buffer = Buffer.from([]);
|
|
21
|
+
return { serialize, deserialize, serialized };
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
v8Target
|
|
25
|
+
.measure('serialize', ({ serialize }, input) => {
|
|
26
|
+
serialize(input);
|
|
27
|
+
})
|
|
28
|
+
.pre(async (_ctx, _input) => {
|
|
29
|
+
// executed before measurement
|
|
30
|
+
})
|
|
31
|
+
.post(async (_ctx, _input) => {
|
|
32
|
+
// executed before measurement
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
v8Target
|
|
36
|
+
.measure('deserialize', ({ deserialize, serialized }) => {
|
|
37
|
+
deserialize(serialized);
|
|
38
|
+
})
|
|
39
|
+
.pre(async (ctx, input) => {
|
|
40
|
+
// since there is no serialized data pre hook prepares it
|
|
41
|
+
// it serializes before each measurement
|
|
42
|
+
ctx.serialized = ctx.serialize(input);
|
|
43
|
+
})
|
|
44
|
+
.post(async (ctx) => {
|
|
45
|
+
// clean it up to avoid GC trigger during measurement
|
|
46
|
+
ctx.serialized = undefined as unknown as Buffer;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
v8Target.teardown(async () => {
|
|
50
|
+
// teardown the benchmark if needed free up resources, clean and etc
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const jsonTarget = suite.target('json', () => {
|
|
54
|
+
const { parse, stringify } = JSON;
|
|
55
|
+
const serialized: string = '';
|
|
56
|
+
return { parse, stringify, serialized };
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
jsonTarget
|
|
60
|
+
.measure('stringify', ({ stringify }, input) => {
|
|
61
|
+
stringify(input);
|
|
62
|
+
})
|
|
63
|
+
.pre(async (_ctx, _input) => {
|
|
64
|
+
// executed before measurement
|
|
65
|
+
})
|
|
66
|
+
.post(async (_ctx, _input) => {
|
|
67
|
+
// executed before measurement
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
jsonTarget
|
|
71
|
+
.measure('parsse', ({ parse, serialized }) => {
|
|
72
|
+
parse(serialized);
|
|
73
|
+
})
|
|
74
|
+
.pre(async (ctx, input) => {
|
|
75
|
+
ctx.serialized = ctx.stringify(input);
|
|
76
|
+
})
|
|
77
|
+
.post(async (ctx) => {
|
|
78
|
+
ctx.serialized = '';
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
jsonTarget.teardown(async () => {
|
|
82
|
+
// teardown the benchmark if needed free up resources, clean and etc
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const reports = await suite.execute({
|
|
86
|
+
workers: 10,
|
|
87
|
+
warmupCycles: 20,
|
|
88
|
+
maxCycles: 100,
|
|
89
|
+
minCycles: 100,
|
|
90
|
+
absThreshold: 1_000,
|
|
91
|
+
relThreshold: 0.02,
|
|
92
|
+
reportTypes: DEFAULT_REPORT_TYPES,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
printJSONReports(reports, 2);
|
package/examples/object-merge.ts
CHANGED
|
@@ -1,39 +1,39 @@
|
|
|
1
1
|
import { Benchmark, printSimpleReports } from '../build/index.js';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const objectMergeSuite = new Benchmark('1K array of objects', () => Array.from({ length: 1_000 }, (_, idx) => ({ [idx]: idx })));
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
objectMergeSuite.target('reduce destructure').measure('data', (_, input) => {
|
|
6
6
|
input.reduce((acc, obj) => {
|
|
7
7
|
return { ...acc, ...obj };
|
|
8
8
|
}, {});
|
|
9
9
|
});
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
objectMergeSuite.target('reduce assign').measure('data', (_, input) => {
|
|
12
12
|
input.reduce((acc, obj) => {
|
|
13
13
|
Object.assign(acc, obj);
|
|
14
14
|
return acc;
|
|
15
15
|
}, {});
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
objectMergeSuite.target('forEach assign').measure('data', (_, input) => {
|
|
19
19
|
const result = {};
|
|
20
20
|
input.forEach((obj) => {
|
|
21
21
|
Object.assign(result, obj);
|
|
22
22
|
});
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
objectMergeSuite.target('for assign').measure('data', (_, input) => {
|
|
26
26
|
const result = {};
|
|
27
27
|
for (let i = 0; i < input.length; i++) {
|
|
28
28
|
Object.assign(result, input[i]);
|
|
29
29
|
}
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
objectMergeSuite.target('assign').measure('data', (_, input) => {
|
|
33
33
|
Object.assign({}, ...input);
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
const reports = await
|
|
36
|
+
const reports = await objectMergeSuite.execute({
|
|
37
37
|
reportTypes: ['ops'],
|
|
38
38
|
maxCycles: 10_000,
|
|
39
39
|
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// run using the following command
|
|
2
|
+
// npx overtake examples/quick-start.ts
|
|
3
|
+
|
|
4
|
+
const sumSuite = benchmark('1M array', () => Array.from({ length: 1_000_000 }, (_, idx) => idx));
|
|
5
|
+
|
|
6
|
+
sumSuite.target('for loop').measure('sum', (_, input) => {
|
|
7
|
+
const n = input.length;
|
|
8
|
+
let sum = 0;
|
|
9
|
+
for (let i = 0; i < n; i++) {
|
|
10
|
+
sum += input[i];
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
sumSuite.target('reduce').measure('sum', (_, input) => {
|
|
15
|
+
input.reduce((a, b) => a + b, 0);
|
|
16
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
});
|
package/package.json
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "overtake",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "NodeJS performance benchmark",
|
|
5
|
-
"main": "build/index.js",
|
|
6
|
-
"types": "build/index.d.ts",
|
|
7
5
|
"type": "module",
|
|
6
|
+
"types": "build/index.d.ts",
|
|
7
|
+
"main": "build/index.cjs",
|
|
8
|
+
"module": "build/index.js",
|
|
9
|
+
"exports": {
|
|
10
|
+
"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
|
+
}
|
|
20
|
+
},
|
|
8
21
|
"bin": {
|
|
9
22
|
"overtake": "bin/overtake.js"
|
|
10
23
|
},
|
|
@@ -32,16 +45,16 @@
|
|
|
32
45
|
"@swc/jest": "^0.2.39",
|
|
33
46
|
"@types/async": "^3.2.25",
|
|
34
47
|
"@types/jest": "^30.0.0",
|
|
35
|
-
"@types/node": "^24.
|
|
48
|
+
"@types/node": "^24.3.0",
|
|
36
49
|
"husky": "^9.1.7",
|
|
37
|
-
"inop": "^0.7.
|
|
50
|
+
"inop": "^0.7.9",
|
|
38
51
|
"jest": "^30.0.5",
|
|
39
52
|
"prettier": "^3.6.2",
|
|
40
53
|
"pretty-quick": "^4.2.2",
|
|
41
54
|
"typescript": "^5.9.2"
|
|
42
55
|
},
|
|
43
56
|
"dependencies": {
|
|
44
|
-
"@swc/core": "^1.13.
|
|
57
|
+
"@swc/core": "^1.13.5",
|
|
45
58
|
"async": "^3.2.6",
|
|
46
59
|
"commander": "^14.0.0",
|
|
47
60
|
"glob": "^11.0.3"
|
package/src/cli.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire, Module } from 'node:module';
|
|
2
2
|
import { SyntheticModule, createContext, SourceTextModule } from 'node:vm';
|
|
3
3
|
import { stat, readFile } from 'node:fs/promises';
|
|
4
|
-
import {
|
|
4
|
+
import { transform } from '@swc/core';
|
|
5
5
|
import { Command, Option } from 'commander';
|
|
6
6
|
import { glob } from 'glob';
|
|
7
7
|
import { Benchmark, printTableReports, printJSONReports, printSimpleReports, DEFAULT_REPORT_TYPES, DEFAULT_WORKERS } from './index.js';
|
|
@@ -13,22 +13,18 @@ const { name, description, version } = require('../package.json');
|
|
|
13
13
|
const commander = new Command();
|
|
14
14
|
|
|
15
15
|
const transpile = async (code: string): Promise<string> => {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
dynamicImport: true,
|
|
19
|
-
target: 'esnext',
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
const output = await print(ast, {
|
|
23
|
-
module: {
|
|
24
|
-
type: 'es6',
|
|
25
|
-
},
|
|
16
|
+
const output = await transform(code, {
|
|
17
|
+
filename: 'benchmark.ts',
|
|
26
18
|
jsc: {
|
|
27
|
-
target: 'esnext',
|
|
28
19
|
parser: {
|
|
29
20
|
syntax: 'typescript',
|
|
21
|
+
tsx: false,
|
|
22
|
+
dynamicImport: true,
|
|
30
23
|
},
|
|
31
|
-
|
|
24
|
+
target: 'esnext',
|
|
25
|
+
},
|
|
26
|
+
module: {
|
|
27
|
+
type: 'es6',
|
|
32
28
|
},
|
|
33
29
|
});
|
|
34
30
|
return output.code;
|