Skip to content

Commit 9d72044

Browse files
authored
feat: add time mode benchmark (#71)
Fixes: #63
1 parent e5c6695 commit 9d72044

File tree

15 files changed

+591
-79
lines changed

15 files changed

+591
-79
lines changed

README.md

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<a href="#plugins">Plugins</a>&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;
1414
<a href="#using-reporter">Using Reporter</a>&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;
1515
<a href="#setup-and-teardown">Setup and Teardown</a>&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;
16+
<a href="#benchmark-modes">Benchmark Modes</a>&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;
1617
<a href="#writing-javascript-mistakes">Writing JavaScript Mistakes</a>
1718
</p>
1819

@@ -78,7 +79,9 @@ See the [examples folder](./examples/) for more common usage examples.
7879
3. [Custom Reporter](#custom-reporter)
7980
4. [Setup and Teardown](#setup-and-teardown)
8081
1. [Managed Benchmarks](#managed-benchmarks)
81-
82+
5. [Benchmark Modes](#benchmark-modes)
83+
1. [Operations Mode (Default)](#operations-mode)
84+
2. [Time Mode](#time-mode)
8285
## Class: `Suite`
8386

8487
> Stability: 1.1 Active Development
@@ -94,6 +97,11 @@ A `Suite` manages and executes benchmark functions. It provides two methods: `ad
9497
* `opsSec` {string} Operations per second.
9598
* `iterations` {Number} Number of iterations.
9699
* `histogram` {Histogram} Histogram instance.
100+
* `benchmarkMode` {string} Benchmark mode to use. Can be 'ops' or 'time'. **Default:** `'ops'`.
101+
* `'ops'` - Measures operations per second (traditional benchmarking).
102+
* `'time'` - Measures actual execution time for a single run.
103+
* `useWorkers` {boolean} Whether to run benchmarks in worker threads. **Default:** `false`.
104+
* `plugins` {Array} Array of plugin instances to use.
97105

98106
If no `reporter` is provided, results are printed to the console.
99107

@@ -130,7 +138,8 @@ Using delete property x 5,853,505 ops/sec (10 runs sampled) min..max=(169ns ...
130138
### `suite.run()`
131139

132140
* Returns: `{Promise<Array<Object>>}` An array of benchmark results, each containing:
133-
* `opsSec` {number} Operations per second.
141+
* `opsSec` {number} Operations per second (Only in 'ops' mode).
142+
* `totalTime` {number} Total execution time in seconds (Only in 'time' mode).
134143
* `iterations` {number} Number of executions of `fn`.
135144
* `histogram` {Histogram} Histogram of benchmark iterations.
136145
* `name` {string} Benchmark name.
@@ -481,6 +490,79 @@ const suite = new Suite({
481490
});
482491
```
483492

493+
## Benchmark Modes
494+
495+
`bench-node` supports multiple benchmarking modes to measure code performance in different ways.
496+
497+
### Operations Mode
498+
499+
Operations mode (default) measures how many operations can be performed in a given timeframe.
500+
This is the traditional benchmarking approach that reports results in operations per second (ops/sec).
501+
502+
This mode is best for:
503+
- Comparing relative performance between different implementations
504+
- Measuring throughput of small, fast operations
505+
- Traditional microbenchmarking
506+
507+
Example output:
508+
```
509+
String concatenation x 12,345,678 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(81ns...85ns)
510+
```
511+
512+
### Time Mode
513+
514+
Time mode measures the actual time taken to execute a function exactly once.
515+
This mode is useful when you want to measure the real execution time for operations that have a known, fixed duration.
516+
517+
This mode is best for:
518+
- Costly operations where multiple instructions are executed in a single run
519+
- Benchmarking operations with predictable timing
520+
- Verifying performance guarantees for time-sensitive functions
521+
522+
To use time mode, set the `benchmarkMode` option to `'time'` when creating a Suite:
523+
524+
```js
525+
const { Suite } = require('bench-node');
526+
527+
const timeSuite = new Suite({
528+
benchmarkMode: 'time' // Enable time mode
529+
});
530+
531+
// Create a function that takes a predictable amount of time
532+
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
533+
534+
timeSuite.add('Async Delay 100ms', async () => {
535+
await delay(100);
536+
});
537+
538+
timeSuite.add('Sync Busy Wait 50ms', () => {
539+
const start = Date.now();
540+
while (Date.now() - start < 50);
541+
});
542+
543+
// Optional: Run the benchmark multiple times with repeatSuite
544+
timeSuite.add('Quick Operation with 5 repeats', { repeatSuite: 5 }, () => {
545+
// This will run exactly once per repeat (5 times total)
546+
// and report the average time
547+
let x = 1 + 1;
548+
});
549+
550+
(async () => {
551+
await timeSuite.run();
552+
})();
553+
```
554+
555+
In time mode, results include `totalTime` (in seconds) instead of `opsSec`.
556+
557+
Example output:
558+
```
559+
Async Delay 100ms x 0.1003s (1 sample) v8-never-optimize=true
560+
Sync Busy Wait 50ms x 0.0502s (1 sample) v8-never-optimize=true
561+
Quick Operation with 5 repeats x 0.0000s (5 samples) v8-never-optimize=true
562+
```
563+
564+
See [examples/time-mode.js](./examples/time-mode.js) for a complete example.
565+
484566
## Writing JavaScript Mistakes
485567

486568
When working on JavaScript micro-benchmarks, it’s easy to forget that modern engines use

biome.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"rules": {
99
"recommended": true,
1010
"style": {
11-
"noParameterAssign": "off"
11+
"noParameterAssign": "off",
12+
"noUselessElse": "off"
1213
}
1314
}
1415
},

examples/time-mode.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
const { Suite } = require('../lib');
2+
3+
const timeSuite = new Suite({
4+
benchmarkMode: 'time' // Set mode for the entire suite
5+
});
6+
7+
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
8+
9+
timeSuite.add('Async Delay 100ms (time)', async () => {
10+
await delay(100);
11+
});
12+
13+
timeSuite.add('Sync Busy Wait 50ms (time)', () => {
14+
const start = Date.now();
15+
while (Date.now() - start < 50);
16+
});
17+
18+
timeSuite.add('Quick Sync Op with 5 repeats (time)', { repeatSuite: 5 }, () => {
19+
// This will run exactly once per repeat (5 times total)
20+
// and report the average time
21+
let x = 1 + 1;
22+
});
23+
24+
25+
(async () => {
26+
console.log('\nRunning benchmark suite in TIME mode...');
27+
await timeSuite.run();
28+
})();

lib/clock.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,11 +233,21 @@ function createRunner(bench, recommendedCount) {
233233
return runner;
234234
}
235235

236-
async function clockBenchmark(bench, recommendedCount) {
236+
/**
237+
* Executes a benchmark and returns the time taken and number of iterations
238+
* @param {import('./index').Benchmark} bench - The benchmark to execute
239+
* @param {number} recommendedCount - The recommended number of iterations
240+
* @param {Object} [options] - Additional options
241+
* @param {boolean} [options.timeMode=false] - If true, runs the benchmark exactly once
242+
* @returns {Promise<[number, number]>} - Returns [duration, iterations]
243+
*/
244+
async function clockBenchmark(bench, recommendedCount, options = {}) {
237245
const runner = createRunner(bench, recommendedCount);
238246
const result = await runner();
247+
239248
// Just to avoid issues with empty fn
240249
result[0] = Math.max(MIN_RESOLUTION, result[0]);
250+
241251
for (const p of bench.plugins) {
242252
if (typeof p.onCompleteBenchmark === "function") {
243253
// TODO: this won't work when useWorkers=true
@@ -246,7 +256,7 @@ async function clockBenchmark(bench, recommendedCount) {
246256
}
247257

248258
debugBench(
249-
`Took ${timer.format(result[0])} to execute ${result[1]} iterations`,
259+
`Took ${timer.format(result[0])} to execute ${result[1]} iterations${options.timeMode ? " (time mode)" : ""}`,
250260
);
251261
return result;
252262
}

lib/index.js

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const {
2727
validateObject,
2828
validateString,
2929
validateArray,
30+
validateBenchmarkMode,
3031
} = require("./validators");
3132

3233
const getFunctionBody = (string) =>
@@ -94,10 +95,12 @@ class Suite {
9495
#reporter;
9596
#plugins;
9697
#useWorkers;
98+
#benchmarkMode;
9799

98100
constructor(options = {}) {
99101
this.#benchmarks = [];
100102
validateObject(options, "options");
103+
101104
if (options?.reporter !== undefined) {
102105
if (options?.reporter !== false && options?.reporter !== null) {
103106
validateFunction(options.reporter, "reporter");
@@ -108,11 +111,16 @@ class Suite {
108111
}
109112

110113
this.#useWorkers = options.useWorkers || false;
114+
111115
if (options?.plugins) {
112116
validateArray(options.plugins, "plugin");
113117
validatePlugins(options.plugins);
114118
}
115119
this.#plugins = options?.plugins || [new V8NeverOptimizePlugin()];
120+
121+
// Benchmark Mode setup
122+
this.#benchmarkMode = options.benchmarkMode || "ops"; // Default to 'ops'
123+
validateBenchmarkMode(this.#benchmarkMode, "options.benchmarkMode");
116124
}
117125

118126
add(name, options, fn) {
@@ -184,16 +192,22 @@ class Suite {
184192
// Warmup is calculated to reduce noise/bias on the results
185193
const initialIterations = await getInitialIterations(benchmark);
186194
debugBench(
187-
`Starting ${benchmark.name} with minTime=${benchmark.minTime}, maxTime=${benchmark.maxTime}, repeatSuite=${benchmark.repeatSuite}, minSamples=${benchmark.minSamples}`,
195+
`Starting ${benchmark.name} with mode=${this.#benchmarkMode}, minTime=${benchmark.minTime}, maxTime=${benchmark.maxTime}, repeatSuite=${benchmark.repeatSuite}, minSamples=${benchmark.minSamples}`,
188196
);
189197

190198
let result;
191199
if (this.#useWorkers) {
200+
if (this.#benchmarkMode === "time") {
201+
console.warn(
202+
"Warning: Worker mode currently doesn't fully support 'time' benchmarkMode.",
203+
);
204+
}
192205
result = await this.runWorkerBenchmark(benchmark, initialIterations);
193206
} else {
194207
result = await runBenchmark(
195208
benchmark,
196209
initialIterations,
210+
this.#benchmarkMode,
197211
benchmark.repeatSuite,
198212
benchmark.minSamples,
199213
);
@@ -204,35 +218,35 @@ class Suite {
204218
if (this.#reporter) {
205219
this.#reporter(results);
206220
}
221+
207222
return results;
208223
}
209224

210-
runWorkerBenchmark(benchmark, initialIterations) {
211-
benchmark.serializeBenchmark();
212-
const worker = new Worker(path.join(__dirname, "./worker-runner.js"));
213-
214-
worker.postMessage({
215-
benchmark,
216-
initialIterations,
217-
repeatSuite: benchmark.repeatSuite,
218-
minSamples: benchmark.minSamples,
219-
});
225+
async runWorkerBenchmark(benchmark, initialIterations) {
220226
return new Promise((resolve, reject) => {
227+
const workerPath = path.resolve(__dirname, "./worker-runner.js");
228+
const worker = new Worker(workerPath);
229+
230+
benchmark.serializeBenchmark();
231+
worker.postMessage({
232+
benchmark,
233+
initialIterations,
234+
benchmarkMode: this.#benchmarkMode, // Pass suite mode
235+
repeatSuite: benchmark.repeatSuite,
236+
minSamples: benchmark.minSamples,
237+
});
238+
221239
worker.on("message", (result) => {
222240
resolve(result);
223-
// TODO: await?
224241
worker.terminate();
225242
});
226-
227-
worker.on("error", (err) => {
228-
reject(err);
243+
worker.on("error", (error) => {
244+
reject(error);
229245
worker.terminate();
230246
});
231-
232247
worker.on("exit", (code) => {
233-
if (code !== 0) {
248+
if (code !== 0)
234249
reject(new Error(`Worker stopped with exit code ${code}`));
235-
}
236250
});
237251
});
238252
}

0 commit comments

Comments
 (0)