Skip to content

Commit 2e2a6be

Browse files
authored
feat: add dce detection plugin (#131)
Signed-off-by: RafaelGSS <rafael.nunu@hotmail.com>
1 parent 53e20aa commit 2e2a6be

File tree

9 files changed

+487
-1
lines changed

9 files changed

+487
-1
lines changed

README.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ See the [examples folder](./examples/) for more common usage examples.
7777
- [`new Suite([options])`](#new-suiteoptions)
7878
- [`suite.add(name[, options], fn)`](#suiteaddname-options-fn)
7979
- [`suite.run()`](#suiterun)
80+
- [Dead Code Elimination Detection](#dead-code-elimination-detection)
81+
- [How It Works](#how-it-works)
82+
- [Configuration](#configuration)
83+
- [When DCE Warnings Appear](#when-dce-warnings-appear)
8084
- [Plugins](#plugins)
8185
- [Plugin Methods](#plugin-methods)
8286
- [Example Plugin](#example-plugin)
@@ -131,7 +135,10 @@ A `Suite` manages and executes benchmark functions. It provides two methods: `ad
131135
* `useWorkers` {boolean} Whether to run benchmarks in worker threads. **Default:** `false`.
132136
* `plugins` {Array} Array of plugin instances to use.
133137
* `repeatSuite` {number} Number of times to repeat each benchmark. Automatically set to `30` when `ttest: true`. **Default:** `1`.
138+
* `plugins` {Array} Array of plugin instances to use. **Default:** `[V8NeverOptimizePlugin]`.
134139
* `minSamples` {number} Minimum number of samples per round for all benchmarks in the suite. Can be overridden per benchmark. **Default:** `10` samples.
140+
* `detectDeadCodeElimination` {boolean} Enable dead code elimination detection. When enabled, default plugins are disabled to allow V8 optimizations. **Default:** `false`.
141+
* `dceThreshold` {number} Threshold multiplier for DCE detection. Benchmarks faster than baseline × threshold will trigger warnings. **Default:** `10`.
135142

136143
If no `reporter` is provided, results are printed to the console.
137144

@@ -179,6 +186,78 @@ Using delete property x 5,853,505 ops/sec (10 runs sampled) min..max=(169ns ...
179186

180187
Runs all added benchmarks and returns the results.
181188

189+
## Dead Code Elimination Detection
190+
191+
**bench-node** includes optional detection of dead code elimination (DCE) to help identify benchmarks that may be producing inaccurate results. When the JIT compiler optimizes away your benchmark code, it can run nearly as fast as an empty function, leading to misleading performance measurements.
192+
193+
**Important:** DCE detection is **opt-in**. When enabled, the `V8NeverOptimizePlugin` is automatically disabled to allow V8 optimizations to occur naturally. This helps catch benchmarks that would be optimized away in real-world scenarios.
194+
195+
### How It Works
196+
197+
When enabled, bench-node measures a baseline (empty function) performance before running your benchmarks. After each benchmark completes, it compares the timing against this baseline. If a benchmark runs suspiciously fast (less than 10× slower than the baseline by default), a warning is emitted.
198+
199+
### Example Warning Output
200+
201+
```
202+
⚠️ Dead Code Elimination Warnings:
203+
The following benchmarks may have been optimized away by the JIT compiler:
204+
205+
• array creation
206+
Benchmark: 3.98ns/iter
207+
Baseline: 0.77ns/iter
208+
Ratio: 5.18x of baseline
209+
Suggestion: Ensure the result is used or assign to a variable
210+
211+
ℹ️ These benchmarks are running nearly as fast as an empty function,
212+
which suggests the JIT may have eliminated the actual work.
213+
```
214+
215+
### Configuration
216+
217+
```js
218+
const { Suite, V8NeverOptimizePlugin } = require('bench-node');
219+
220+
// Enable DCE detection (disables V8NeverOptimizePlugin automatically)
221+
const suite = new Suite({
222+
detectDeadCodeElimination: true
223+
});
224+
225+
// Enable DCE detection with custom threshold (default is 10x)
226+
const strictSuite = new Suite({
227+
detectDeadCodeElimination: true,
228+
dceThreshold: 20 // Only warn if < 20x slower than baseline
229+
});
230+
231+
// Use both DCE detection AND prevent optimization
232+
// (helpful for educational purposes to see warnings even when using %NeverOptimizeFunction)
233+
const educationalSuite = new Suite({
234+
plugins: [new V8NeverOptimizePlugin()],
235+
detectDeadCodeElimination: true
236+
});
237+
```
238+
239+
### When DCE Warnings Appear
240+
241+
Common scenarios that trigger warnings:
242+
243+
```js
244+
// ❌ Result not used - will be optimized away
245+
suite.add('computation', () => {
246+
const result = Math.sqrt(144);
247+
// result is never used
248+
});
249+
250+
// ✅ Result is used - less likely to be optimized
251+
suite.add('computation', () => {
252+
const result = Math.sqrt(144);
253+
if (result !== 12) throw new Error('Unexpected');
254+
});
255+
```
256+
257+
**Note:** DCE detection only works in `'ops'` benchmark mode and when not using worker threads. It is automatically disabled for `'time'` mode and worker-based benchmarks.
258+
259+
See [examples/dce-detection/](./examples/dce-detection/) for more examples.
260+
182261
## Plugins
183262

184263
Plugins extend the functionality of the benchmark module.

examples/dce-detection/example.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
const { Suite } = require("../../lib/index");
2+
3+
// Enable DCE detection to catch benchmarks that may be optimized away
4+
const suite = new Suite({
5+
detectDeadCodeElimination: true,
6+
});
7+
8+
// Example 1: Likely to trigger DCE warning - result not used
9+
suite.add("simple addition (likely DCE)", () => {
10+
const result = 1 + 1;
11+
// result is never used - JIT might optimize this away
12+
});
13+
14+
// Example 2: Result is used - should not trigger warning
15+
suite.add("simple addition (used)", () => {
16+
const result = 1 + 1;
17+
if (result !== 2) throw new Error("Unexpected result");
18+
});
19+
20+
// Example 3: Array creation without use - likely DCE
21+
suite.add("array creation (likely DCE)", () => {
22+
const arr = new Array(100);
23+
// arr is never accessed - might be optimized away
24+
});
25+
26+
// Example 4: Array creation with access - should not trigger warning
27+
suite.add("array creation (accessed)", () => {
28+
const arr = new Array(100);
29+
arr[0] = 1; // Using the array
30+
});
31+
32+
// Example 5: Object creation without use - likely DCE
33+
suite.add("object creation (likely DCE)", () => {
34+
const obj = { x: 1, y: 2, z: 3 };
35+
// obj is never accessed
36+
});
37+
38+
// Example 6: More realistic computation
39+
suite.add("string operations", () => {
40+
const str1 = "hello";
41+
const str2 = "world";
42+
const result = str1 + " " + str2;
43+
if (!result.includes("hello")) throw new Error("Missing hello");
44+
});
45+
46+
suite.run();
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const { Suite } = require("../../lib/index");
2+
3+
// Default behavior - DCE detection is disabled, V8NeverOptimizePlugin is used
4+
// You don't need to set detectDeadCodeElimination: false explicitly
5+
const suite = new Suite();
6+
7+
// These benchmarks will run with V8NeverOptimizePlugin, so they'll be slower
8+
// but more deterministic and won't be optimized away
9+
suite.add("simple addition", () => {
10+
const result = 1 + 1;
11+
});
12+
13+
suite.add("array creation", () => {
14+
const arr = new Array(100);
15+
});
16+
17+
suite.run();
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
const { Suite } = require("../../lib/index");
2+
3+
// Enable DCE detection - this automatically disables V8NeverOptimizePlugin
4+
// In this mode, V8 optimizations occur naturally and DCE warnings help identify issues
5+
const suite = new Suite({
6+
detectDeadCodeElimination: true,
7+
});
8+
9+
// Example 1: Likely to trigger DCE warning - result not used
10+
suite.add("simple addition (likely DCE)", () => {
11+
const result = 1 + 1;
12+
// result is never used - JIT will optimize this away
13+
});
14+
15+
// Example 2: Result is used - should not trigger warning
16+
suite.add("simple addition (used)", () => {
17+
const result = 1 + 1;
18+
if (result !== 2) throw new Error("Unexpected result");
19+
});
20+
21+
// Example 3: Array creation without use - likely DCE
22+
suite.add("array creation (likely DCE)", () => {
23+
const arr = new Array(100);
24+
// arr is never accessed - will be optimized away
25+
});
26+
27+
// Example 4: Array creation with access - should not trigger warning
28+
suite.add("array creation (accessed)", () => {
29+
const arr = new Array(100);
30+
arr[0] = 1; // Using the array
31+
});
32+
33+
// Example 5: More realistic computation that takes time
34+
suite.add("actual work", () => {
35+
let sum = 0;
36+
for (let i = 0; i < 100; i++) {
37+
sum += Math.sqrt(i);
38+
}
39+
if (sum < 0) throw new Error("Impossible");
40+
});
41+
42+
suite.run();

index.d.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ export declare namespace BenchNode {
6464
repeatSuite?: number; // Number of times to repeat each benchmark (default: 1, or 30 when ttest is enabled)
6565
ttest?: boolean; // Enable t-test mode for statistical significance (auto-sets repeatSuite=30)
6666
reporterOptions?: ReporterOptions;
67+
detectDeadCodeElimination?: boolean; // Enable DCE detection, default: false
68+
dceThreshold?: number; // DCE detection threshold multiplier, default: 10
6769
}
6870

6971
interface BenchmarkOptions {
@@ -142,6 +144,29 @@ export declare namespace BenchNode {
142144
getResult(benchmarkName: string): PluginResult;
143145
toString(): string;
144146
}
147+
148+
class DeadCodeEliminationDetectionPlugin implements Plugin {
149+
constructor(options?: { threshold?: number });
150+
isSupported(): boolean;
151+
setBaseline(timePerOp: number): void;
152+
onCompleteBenchmark(
153+
result: OnCompleteBenchmarkResult,
154+
bench: Benchmark,
155+
): void;
156+
getWarning(
157+
benchmarkName: string,
158+
): { timePerOp: number; baselineTime: number; ratio: number } | undefined;
159+
getAllWarnings(): Array<{
160+
name: string;
161+
timePerOp: number;
162+
baselineTime: number;
163+
ratio: number;
164+
}>;
165+
hasWarning(benchmarkName: string): boolean;
166+
emitWarnings(): void;
167+
toString(): string;
168+
reset(): void;
169+
}
145170
}
146171

147172
export declare const textReport: BenchNode.ReporterFunction;
@@ -215,3 +240,5 @@ export declare function compareBenchmarks(
215240
sample2: number[],
216241
alpha?: number,
217242
): TTest.CompareBenchmarksResult;
243+
244+
export declare class DeadCodeEliminationDetectionPlugin extends BenchNode.DeadCodeEliminationDetectionPlugin {}

lib/index.js

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const {
2222
V8GetOptimizationStatus,
2323
V8OptimizeOnNextCallPlugin,
2424
MemoryPlugin,
25+
DeadCodeEliminationDetectionPlugin,
2526
} = require("./plugins");
2627
const {
2728
validateFunction,
@@ -122,6 +123,7 @@ class Suite {
122123
#minSamples;
123124
#repeatSuite;
124125
#ttest;
126+
#dceDetector;
125127

126128
constructor(options = {}) {
127129
this.#benchmarks = [];
@@ -140,11 +142,27 @@ class Suite {
140142

141143
this.#useWorkers = options.useWorkers || false;
142144

145+
// DCE detection is opt-in to avoid breaking changes
146+
const dceEnabled = options.detectDeadCodeElimination === true;
147+
if (dceEnabled) {
148+
this.#dceDetector = new DeadCodeEliminationDetectionPlugin(
149+
options.dceThreshold ? { threshold: options.dceThreshold } : {},
150+
);
151+
}
152+
153+
// Plugin setup: If DCE detection is enabled, default to no plugins (allow optimization)
154+
// Otherwise, use V8NeverOptimizePlugin as the default
143155
if (options?.plugins) {
144156
validateArray(options.plugins, "plugin");
145157
validatePlugins(options.plugins);
158+
this.#plugins = options.plugins;
159+
} else if (dceEnabled) {
160+
// DCE detection requires optimization to be enabled, so no default plugins
161+
this.#plugins = [];
162+
} else {
163+
// Default behavior - use V8NeverOptimizePlugin
164+
this.#plugins = [new V8NeverOptimizePlugin()];
146165
}
147-
this.#plugins = options?.plugins || [new V8NeverOptimizePlugin()];
148166

149167
this.#benchmarkMode = options.benchmarkMode || "ops";
150168
validateBenchmarkMode(this.#benchmarkMode, "options.benchmarkMode");
@@ -231,6 +249,15 @@ class Suite {
231249
throwIfNoNativesSyntax();
232250
const results = new Array(this.#benchmarks.length);
233251

252+
// Measure baseline for DCE detection (only in ops mode, not in worker mode)
253+
if (
254+
this.#dceDetector &&
255+
!this.#useWorkers &&
256+
this.#benchmarkMode === "ops"
257+
) {
258+
await this.#measureBaseline();
259+
}
260+
234261
// It doesn't make sense to warmup a fresh new instance of Worker.
235262
// TODO: support warmup directly in the Worker.
236263
if (!this.#useWorkers) {
@@ -250,6 +277,15 @@ class Suite {
250277

251278
for (let i = 0; i < this.#benchmarks.length; ++i) {
252279
const benchmark = this.#benchmarks[i];
280+
281+
// Add DCE detector to benchmark plugins if enabled
282+
if (this.#dceDetector && this.#benchmarkMode === "ops") {
283+
const originalPlugins = benchmark.plugins;
284+
benchmark.plugins = [...benchmark.plugins, this.#dceDetector];
285+
// Regenerate function string with new plugins
286+
benchmark.fnStr = createFnString(benchmark);
287+
}
288+
253289
// Warmup is calculated to reduce noise/bias on the results
254290
const initialIterations = await getInitialIterations(benchmark);
255291
debugBench(
@@ -280,9 +316,43 @@ class Suite {
280316
this.#reporter(results, this.#reporterOptions);
281317
}
282318

319+
// Emit DCE warnings after reporting
320+
if (this.#dceDetector) {
321+
this.#dceDetector.emitWarnings();
322+
}
323+
283324
return results;
284325
}
285326

327+
async #measureBaseline() {
328+
debugBench("Measuring baseline for DCE detection...");
329+
330+
// Create a minimal baseline benchmark (empty function)
331+
const baselineBench = new Benchmark(
332+
"__baseline__",
333+
() => {},
334+
0.01, // minTime
335+
0.05, // maxTime
336+
this.#plugins,
337+
1, // repeatSuite
338+
10, // minSamples
339+
);
340+
341+
const initialIterations = await getInitialIterations(baselineBench);
342+
const result = await runBenchmark(
343+
baselineBench,
344+
initialIterations,
345+
"ops",
346+
1,
347+
10,
348+
);
349+
350+
const baselineTimePerOp = (1 / result.opsSec) * 1e9; // Convert to ns
351+
debugBench(`DCE baseline: ${timer.format(baselineTimePerOp)}/iter`);
352+
353+
this.#dceDetector.setBaseline(baselineTimePerOp);
354+
}
355+
286356
async runWorkerBenchmark(benchmark, initialIterations) {
287357
return new Promise((resolve, reject) => {
288358
const workerPath = path.resolve(__dirname, "./worker-runner.js");
@@ -319,6 +389,7 @@ module.exports = {
319389
V8GetOptimizationStatus,
320390
V8OptimizeOnNextCallPlugin,
321391
MemoryPlugin,
392+
DeadCodeEliminationDetectionPlugin,
322393
chartReport,
323394
textReport,
324395
prettyReport,

lib/plugins.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ const { V8NeverOptimizePlugin } = require("./plugins/v8-never-opt");
33

44
const { V8GetOptimizationStatus } = require("./plugins/v8-print-status");
55
const { MemoryPlugin } = require("./plugins/memory");
6+
const {
7+
DeadCodeEliminationDetectionPlugin,
8+
} = require("./plugins/dce-detection");
69

710
const { validateFunction, validateArray } = require("./validators");
811

@@ -48,5 +51,6 @@ module.exports = {
4851
V8NeverOptimizePlugin,
4952
V8GetOptimizationStatus,
5053
V8OptimizeOnNextCallPlugin,
54+
DeadCodeEliminationDetectionPlugin,
5155
validatePlugins,
5256
};

0 commit comments

Comments
 (0)