Repository: RafaelGSS/bench-node Branch: main Commit: 0765fdc5aa64 Files: 107 Total size: 299.9 KB Directory structure: gitextract_nbr14g_8/ ├── .devcontainer/ │ └── devcontainer.json ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ ├── benchmark-comparison.yml │ ├── commit-message-validation.yml │ ├── release.yml │ ├── runner_warmer.yml │ └── test.yml ├── .gitignore ├── .npmignore ├── .release-please-manifest.json ├── BENCHMARK_COMPARISSON.md ├── CHANGELOG.md ├── Dockerfile ├── README.md ├── assets/ │ └── README.md ├── bench-node-0.14.0.tgz ├── biome.json ├── doc/ │ ├── Inconclusive.md │ └── Plugins.md ├── examples/ │ ├── .gitignore │ ├── benchmark-comparison/ │ │ ├── README.md │ │ └── comparison.js │ ├── chart-report/ │ │ └── node.js │ ├── create-uint32array/ │ │ ├── node.js │ │ └── node.js.log │ ├── crypto-verify/ │ │ ├── node.js │ │ ├── node.js.log │ │ ├── private-key.pem │ │ └── public-key.pem │ ├── csv-report/ │ │ └── node.js │ ├── dce-detection/ │ │ ├── example.js │ │ ├── with-dce-disabled.js │ │ └── without-never-optimize.js │ ├── deleting-properties/ │ │ ├── node.js │ │ └── node.js.log │ ├── empty/ │ │ ├── node.js │ │ └── node.js.log │ ├── fs-read-async/ │ │ ├── node.js │ │ ├── node.js.log │ │ ├── node.managed.js │ │ ├── node.managed.js.log │ │ └── sample-file.txt │ ├── fs-read-sync/ │ │ ├── node.js │ │ ├── node.js.log │ │ └── sample-file.txt │ ├── html-report/ │ │ ├── node.js │ │ └── result.html │ ├── json-report/ │ │ └── node.js │ ├── plugins/ │ │ ├── all.js │ │ ├── all.js.log │ │ ├── memory.js │ │ ├── v8-get-opt-status.js │ │ ├── v8-get-opt-status.js.log │ │ ├── v8-never-optimize.js │ │ ├── v8-never-optimize.js.log │ │ ├── v8-optimize-next-call.js │ │ └── v8-optimize-next-call.js.log │ ├── pretty-report/ │ │ └── node.js │ ├── run.sh │ ├── statistical-significance/ │ │ ├── README.md │ │ └── node.js │ ├── string-replace/ │ │ ├── node.js │ │ └── node.js.log │ ├── string-searching/ │ │ ├── node.js │ │ └── node.js.log │ ├── time-mode.js │ └── worker-threads/ │ └── node.js ├── index.d.ts ├── lib/ │ ├── clock.js │ ├── histogram.js │ ├── index.js │ ├── lifecycle.js │ ├── plugins/ │ │ ├── dce-detection.js │ │ ├── memory.js │ │ ├── v8-never-opt.js │ │ ├── v8-opt.js │ │ └── v8-print-status.js │ ├── plugins.js │ ├── report.js │ ├── reporter/ │ │ ├── chart.js │ │ ├── csv.js │ │ ├── html.js │ │ ├── json.js │ │ ├── pretty.js │ │ ├── template.html │ │ └── text.js │ ├── utils/ │ │ ├── analyze.js │ │ ├── styleText.js │ │ └── ttest.js │ ├── validators.js │ └── worker-runner.js ├── package.json ├── release-please-config.json ├── test/ │ ├── async.js │ ├── basic.js │ ├── env.js │ ├── fixtures/ │ │ ├── bench.js │ │ ├── copy.js │ │ └── opt-managed.js │ ├── managed.js │ ├── plugin-api-doc.js │ ├── plugins.js │ ├── reporter.js │ ├── time-mode.js │ ├── ttest.js │ └── worker.js └── types/ └── types.test-d.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .devcontainer/devcontainer.json ================================================ { "name": "Bench-node Container", "build": { "dockerfile": "../Dockerfile" } } ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: [RafaelGSS] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry polar: # Replace with a single Polar username buy_me_a_coffee: # Replace with a single Buy Me a Coffee username thanks_dev: # Replace with a single thanks.dev username custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .github/workflows/benchmark-comparison.yml ================================================ name: Benchmark Comparison on: push: branches: - main workflow_dispatch: permissions: issues: write contents: read id-token: write jobs: runner-start: runs-on: ubuntu-latest steps: - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v5 with: aws-region: us-west-2 role-to-assume: arn:aws:iam::800406105498:role/RafaelGSS-nodejs-bench-operations - name: Checkout uses: actions/checkout@v5 - name: Start Runner uses: nodesource/aws-eco-runner@v1.0.0-beta.3 with: instances_id: '["i-065f0f848eb1615ae"]' action: 'start' aws_default_region: 'us-west-2' benchmark: name: Run Benchmark Comparisons runs-on: self-hosted continue-on-error: true strategy: matrix: node-version: [20, 22, 24] steps: - name: Checkout code uses: actions/checkout@v5 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} - name: Install dependencies run: npm install --dev - name: Run basic comparison run: node --allow-natives-syntax examples/benchmark-comparison/comparison.js ================================================ FILE: .github/workflows/commit-message-validation.yml ================================================ name: Commit Message Validation on: pull_request: jobs: validate: name: Validate runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 with: fetch-depth: 0 - uses: webiny/action-conventional-commits@v1.3.0 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: branches: - main permissions: contents: write pull-requests: write id-token: write jobs: release-please: runs-on: ubuntu-latest steps: - name: Checkout Code uses: actions/checkout@v5 - name: Release Please uses: google-github-actions/release-please-action@v4 id: release - name: Use Node 22.x uses: actions/setup-node@v6 if: ${{ steps.release.outputs.release_created }} with: node-version: 22 registry-url: 'https://registry.npmjs.org' - name: Install Deps run: npm install if: ${{ steps.release.outputs.release_created }} - name: NPM Publish run: npm publish --provenance --access public if: ${{ steps.release.outputs.release_created }} env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} ================================================ FILE: .github/workflows/runner_warmer.yml ================================================ name: Cron - Keep Runners Active on: schedule: # Runs this workflow every Sunday at 12:00 UTC. You can adjust the schedule according to your needs. - cron: '0 12 * * 0' workflow_dispatch: # Allows manual triggering of the workflow permissions: issues: write contents: read id-token: write jobs: start_runner: runs-on: ubuntu-latest steps: - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v5 with: aws-region: us-west-2 role-to-assume: arn:aws:iam::800406105498:role/RafaelGSS-nodejs-bench-operations - name: Start Runner uses: nodesource/aws-eco-runner@v1.0.0-beta.3 with: instances_id: '["i-065f0f848eb1615ae"]' action: 'start' aws_default_region: 'us-west-2' keep-runner-active: runs-on: [self-hosted] steps: - name: Run task to keep the runner active run: echo "Keep the runner active" ================================================ FILE: .github/workflows/test.yml ================================================ name: Test CI on: push: branches: - main pull_request: permissions: issues: write contents: read id-token: write jobs: runner-start: runs-on: ubuntu-latest steps: - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v5 with: aws-region: us-west-2 role-to-assume: arn:aws:iam::800406105498:role/RafaelGSS-nodejs-bench-operations - name: Checkout uses: actions/checkout@v5 - name: Start Runner uses: nodesource/aws-eco-runner@v1.0.0-beta.3 with: instances_id: '["i-065f0f848eb1615ae"]' action: 'start' aws_default_region: 'us-west-2' build: runs-on: self-hosted strategy: matrix: node-version: [18, 20, 22, 24, 25] env: CI: true steps: - name: Checkout code uses: actions/checkout@v5 - name: Set up Node.js uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} - name: Install dependencies run: npm install - name: Run tests run: npm test ================================================ FILE: .gitignore ================================================ tags .idea node_modules/ package-lock.json coverage ================================================ FILE: .npmignore ================================================ # We don't want to publish the logo and others assets coverage ================================================ FILE: .release-please-manifest.json ================================================ { ".": "0.15.0" } ================================================ FILE: BENCHMARK_COMPARISSON.md ================================================ # Benchmark Library Comparison This document provides a comprehensive comparison between `bench-node` and other popular Node.js benchmarking libraries based on real benchmark results across different Node.js versions. This comparison helps identify potential errors in our benchmarking approach and provides insights for specific algorithm comparisons. ## Overview The following benchmarking libraries are compared in this document: - **bench-node** - Our library with V8 optimization control - **benchmark.js** - The most popular JavaScript benchmarking library - **mitata** - Modern, fast benchmarking library - **tinybench** - Lightweight benchmarking tool ## Test Environment All benchmarks were executed on: - **CPU**: Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz - **Platform**: Linux x64 - **Node.js versions**: 20.19.3, 22.17.0, 24.3.0 ## Detailed Benchmark Results ### Simple Operations - JIT Optimizable This test measures a simple operation that V8 can heavily optimize: #### Node.js 24.3.0 | Library | ops/sec | Margin | Relative Performance | |---------|---------|--------|---------------------| | benchmark.js | 141,211,777 | ±5.18% | **1.00x** (baseline) | | bench-node | 125,902,288 | N/A | 0.89x | | mitata | 37,131,930 | ±5.00% | 0.26x | | tinybench | 28,207,575 | ±0.10% | 0.20x | #### Node.js 22.17.0 | Library | ops/sec | Margin | Relative Performance | |---------|---------|--------|---------------------| | benchmark.js | 142,037,530 | ±3.88% | **1.00x** (baseline) | | bench-node | 141,504,337 | N/A | 1.00x | | tinybench | 17,040,169 | ±1.40% | 0.12x | | mitata | 12,382,061 | ±5.00% | 0.09x | #### Node.js 20.19.3 | Library | ops/sec | Margin | Relative Performance | |---------|---------|--------|---------------------| | **benchmark.js** | **843,084,082** | ±0.25% | **1.00x** (baseline) | | bench-node | 136,801,267 | N/A | 0.16x | | tinybench | 24,275,731 | ±0.11% | 0.03x | | mitata | 18,411,459 | ±5.00% | 0.02x | ### String Processing - Regex Test This test performs regex matching on strings: #### Cross-Version Comparison | Node.js Version | bench-node | benchmark.js | mitata | tinybench | |-----------------|------------|--------------|--------|-----------| | **24.3.0** | 4,352,775 | 4,299,609 (±0.58%) | 3,543,737 (±5.00%) | 3,654,837 (±0.40%) | | **22.17.0** | 4,349,482 | 4,382,198 (±0.49%) | 3,933,431 (±5.00%) | 3,526,797 (±0.83%) | | **20.19.3** | 4,419,500 | 4,320,038 (±0.40%) | 3,475,360 (±5.00%) | 3,698,242 (±1.85%) | ### CPU Intensive - Fibonacci Calculations #### Fibonacci(10) - Light CPU Load | Node.js Version | benchmark.js | bench-node | mitata | tinybench | |-----------------|--------------|------------|--------|-----------| | **24.3.0** | 77,510,123 (±2.38%) | 69,120,722 | 42,308,343 (±5.00%) | 26,206,646 (±0.09%) | | **22.17.0** | 56,097,219 (±1.68%) | 54,623,064 | 51,268,905 (±5.00%) | 15,451,469 (±0.09%) | | **20.19.3** | 73,590,107 (±0.23%) | 57,284,808 | 10,910,003 (±5.00%) | 22,108,774 (±0.11%) | #### Fibonacci(30) - Medium CPU Load | Node.js Version | benchmark.js | bench-node | mitata | tinybench | |-----------------|--------------|------------|--------|-----------| | **24.3.0** | 43,276,919 (±1.26%) | 40,936,562 | 30,824,240 (±5.00%) | 18,752,734 (±0.11%) | | **22.17.0** | 22,187,269 (±0.53%) | 21,316,196 | 14,024,655 (±5.00%) | 11,373,272 (±1.98%) | | **20.19.3** | 22,078,755 (±0.27%) | 21,783,805 | 21,708,455 (±5.00%) | 14,162,915 (±0.11%) | #### Fibonacci(40) - Heavy CPU Load | Node.js Version | benchmark.js | bench-node | mitata | tinybench | |-----------------|--------------|------------|--------|-----------| | **24.3.0** | 35,115,900 (±1.30%) | 33,956,245 | 26,752,990 (±5.00%) | 18,719,206 (±0.10%) | | **22.17.0** | 16,511,904 (±0.91%) | 16,112,632 | 15,599,164 (±5.00%) | 9,496,167 (±4.91%) | | **20.19.3** | 16,726,334 (±0.31%) | 16,504,271 | 16,264,129 (±5.00%) | 11,755,080 (±3.32%) | ### Fibonacci Recursive(10) - Algorithm Comparison | Node.js Version | bench-node | mitata | benchmark.js | tinybench | |-----------------|------------|--------|--------------|-----------| | **24.3.0** | **999,165** | 992,635 (±5.00%) | 986,251 (±0.31%) | 977,228 (±0.09%) | | **22.17.0** | **978,652** | 972,507 (±5.00%) | 969,517 (±0.30%) | 938,449 (±0.07%) | | **20.19.3** | **989,935** | 939,466 (±5.00%) | 973,359 (±0.19%) | 965,775 (±0.07%) | --- *Last updated: August 2025 | Based on bench-node v2.x, benchmark.js v2.1.4, mitata v1.x, tinybench v2.x* *Benchmark workflow: [benchmark-comparison.yml](https://github.com/RafaelGSS/bench-node/actions/workflows/benchmark-comparison.yml)* *Test results from Node.js versions 20.19.3, 22.17.0, and 24.3.0* ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## [0.15.0](https://github.com/RafaelGSS/bench-node/compare/v0.14.0...v0.15.0) (2026-05-08) ### Features * Add fixed-unit time format to JSON min/max output for use in ([#157](https://github.com/RafaelGSS/bench-node/issues/157)) ([38d10d1](https://github.com/RafaelGSS/bench-node/commit/38d10d1ad5bb56d66fc60e764b7eb54eaa1f5d1e)) * Expose bar width flag. ([#152](https://github.com/RafaelGSS/bench-node/issues/152)) ([83ddc1d](https://github.com/RafaelGSS/bench-node/commit/83ddc1d720feb7746598dbde9a887e96766531a2)) * Support benchmark warmup with useWorkers=true ([#154](https://github.com/RafaelGSS/bench-node/issues/154)) ([f73ed33](https://github.com/RafaelGSS/bench-node/commit/f73ed3366300993aa7189fef306df417c3da173e)) ### Bug Fixes * **plugin:** make V8NeverOptimizePlugin target benchmark functions ([#163](https://github.com/RafaelGSS/bench-node/issues/163)) ([c0975c8](https://github.com/RafaelGSS/bench-node/commit/c0975c80d35edafa8820a375e744339de772fe30)) ### Miscellaneous Chores * --test flag propagation appears to be fixed. ([#162](https://github.com/RafaelGSS/bench-node/issues/162)) ([8750bfc](https://github.com/RafaelGSS/bench-node/commit/8750bfc7b7580cd2199f8c62e75fe96462aea6e5)) * add node v25 to test matrix ([#144](https://github.com/RafaelGSS/bench-node/issues/144)) ([08526ea](https://github.com/RafaelGSS/bench-node/commit/08526ea0e561328a72862d2c0cd42717884cd6a2)) * add node v25 to test matrix ([#159](https://github.com/RafaelGSS/bench-node/issues/159)) ([946bbdc](https://github.com/RafaelGSS/bench-node/commit/946bbdc068a6f955e631470acea608648e868665)) ### Code Refactoring * Clarify the purpose and units of various numbers in lifecycle.js ([#151](https://github.com/RafaelGSS/bench-node/issues/151)) ([28e872e](https://github.com/RafaelGSS/bench-node/commit/28e872eab08a92a8c364f1885a76f1a57f7de141)) ## [0.14.0](https://github.com/RafaelGSS/bench-node/compare/v0.13.0...v0.14.0) (2025-12-17) ### Features * add dce detection plugin ([#131](https://github.com/RafaelGSS/bench-node/issues/131)) ([2e2a6be](https://github.com/RafaelGSS/bench-node/commit/2e2a6be87e7dc7562952dd53b9e34017e0822b8e)) * add t-test mode for statistical significance testing ([#133](https://github.com/RafaelGSS/bench-node/issues/133)) ([53e20aa](https://github.com/RafaelGSS/bench-node/commit/53e20aae67fbd747a05465b6ad824f56483e231a)) * Narrow the bar display by another couple of characters by using ANSI line drawing. ([#134](https://github.com/RafaelGSS/bench-node/issues/134)) ([814e88e](https://github.com/RafaelGSS/bench-node/commit/814e88e905da60d00615a2b95cfeed23b6034c7d)) ### Bug Fixes * Clock inaccuracy in libuv is causing flaky tests. ([cad6bfa](https://github.com/RafaelGSS/bench-node/commit/cad6bfac1b03f270da38c81f3d1703db95a7b0f4)) * implement support to timers to workers ([35eb144](https://github.com/RafaelGSS/bench-node/commit/35eb144908e76b73667ef6de9131c4d37425974b)) ### Miscellaneous Chores * Localize op/sec in text reporter ([#146](https://github.com/RafaelGSS/bench-node/issues/146)) ([18302e3](https://github.com/RafaelGSS/bench-node/commit/18302e369b3f430d35fe856177d0fd0a55592eaf)) ## [0.13.0](https://github.com/RafaelGSS/bench-node/compare/v0.12.0...v0.13.0) (2025-11-27) ### Features * export to<format> functions and rework some of the tests. ([960cb87](https://github.com/RafaelGSS/bench-node/commit/960cb87caf3e73c01550cf93127679991c928ef8)) ### Miscellaneous Chores * change printTree to formatTree ([d6ff418](https://github.com/RafaelGSS/bench-node/commit/d6ff418dcc50fbe256b89e9bd562521efc8b7095)) * Convert json reporter to allow json generation to string. ([cfc464c](https://github.com/RafaelGSS/bench-node/commit/cfc464c4709c1ed4415ff86d486a29dc39556708)) * Convert printResult to generate a line of output. ([4199a57](https://github.com/RafaelGSS/bench-node/commit/4199a578f29eec173b5c22a21affb80c9b415f07)) * extract toCSV from csv reporter. ([033dd4b](https://github.com/RafaelGSS/bench-node/commit/033dd4bdd8cd874d0fe985a0f6dae34666452e68)) * Extract toPretty() method ([c2e54a3](https://github.com/RafaelGSS/bench-node/commit/c2e54a36387ff812c461961724e66aa2389e4358)) * extract toText from text reporter ([db850ca](https://github.com/RafaelGSS/bench-node/commit/db850ca95b124fd8f4a1ce8dd140868be2f2de5d)) * Move lint after unit tests. ([e0c019d](https://github.com/RafaelGSS/bench-node/commit/e0c019d48c9bdd77dc934e2c5c15231fd1a3b836)) * Move short branch to top of conditional block. ([e7254b3](https://github.com/RafaelGSS/bench-node/commit/e7254b35286f58a18622c30aebea7d2e127542b7)) * Remove duplicate time code in pretty and text printers ([a977b97](https://github.com/RafaelGSS/bench-node/commit/a977b972adab5b947b290bb347c774d93ff011fd)) * Split out text only chart creation function. ([80538fa](https://github.com/RafaelGSS/bench-node/commit/80538fa6447fe542284a6f58d1d8f08c0d362696)) * Use report for tests. ([b14db92](https://github.com/RafaelGSS/bench-node/commit/b14db929b058f2a25717a0ff83f5f911c015fb66)) ## [0.12.0](https://github.com/RafaelGSS/bench-node/compare/v0.11.0...v0.12.0) (2025-11-03) ### Features * add several code refactor/fixes ([#123](https://github.com/RafaelGSS/bench-node/issues/123)) ([cf96c3d](https://github.com/RafaelGSS/bench-node/commit/cf96c3deaace1086df64dfa0424aeb5bbe90ca5f)) * Deduplicate the data summarization code. ([#120](https://github.com/RafaelGSS/bench-node/issues/120)) ([85bfd3e](https://github.com/RafaelGSS/bench-node/commit/85bfd3e8b4a7da047859c70a1ec5e43e81161c10)) * make minSamples option available per Suite creation ([#126](https://github.com/RafaelGSS/bench-node/issues/126)) ([152b945](https://github.com/RafaelGSS/bench-node/commit/152b94571e870246db75311e80b962451e2af9ca)) ### Bug Fixes * **plugins:** export plugin memory ([52bb536](https://github.com/RafaelGSS/bench-node/commit/52bb536117460cff49518881a5680b96ef06426c)) ### Miscellaneous Chores * **index.d.ts:** update exported types for plugins ([abc828f](https://github.com/RafaelGSS/bench-node/commit/abc828fd7f77fe6bb48aa239155e1ad4ca9f9e63)) * typo ([#125](https://github.com/RafaelGSS/bench-node/issues/125)) ([88aff41](https://github.com/RafaelGSS/bench-node/commit/88aff41f3b190a005fd33160b41a888f9227b3b4)) ### Code Refactoring * **plugins:** enable support for memory & fixes on types ([cd4f96e](https://github.com/RafaelGSS/bench-node/commit/cd4f96e5a48a3381242047c18fc082e293ef7723)) ### Continuous Integration * bump ci ([#124](https://github.com/RafaelGSS/bench-node/issues/124)) ([2d7ba17](https://github.com/RafaelGSS/bench-node/commit/2d7ba17eac29328a7d09393efe2692960c84c869)) ## [0.11.0](https://github.com/RafaelGSS/bench-node/compare/v0.10.0...v0.11.0) (2025-09-22) ### Features * add comparisson with other benchmark libs ([#106](https://github.com/RafaelGSS/bench-node/issues/106)) ([19de73f](https://github.com/RafaelGSS/bench-node/commit/19de73f3a3fb4f11e8ea1d746154c8f3391d0192)) * Allow for configurable column width for the chart output. ([#104](https://github.com/RafaelGSS/bench-node/issues/104)) ([315d551](https://github.com/RafaelGSS/bench-node/commit/315d551209e25827c844c0f4a301afa6e2bc276b)) * reduce NPM package size by adding files field to package.json ([#111](https://github.com/RafaelGSS/bench-node/issues/111)) ([a584269](https://github.com/RafaelGSS/bench-node/commit/a584269c8267a30a3fb9b0bb77fccf201f77381c)) ### Bug Fixes * optional iterations count for end(), add type tests ([a4dc145](https://github.com/RafaelGSS/bench-node/commit/a4dc145dee4dd53d331cd294771f83333efba7f6)) ### Documentation * add comprehensive library comparison document ([#109](https://github.com/RafaelGSS/bench-node/issues/109)) ([caa18a9](https://github.com/RafaelGSS/bench-node/commit/caa18a914f416be9f53357d2a2c8e2ca8a092c66)) ### Miscellaneous Chores * **main:** release 0.11.0.beta-1 ([b234ce7](https://github.com/RafaelGSS/bench-node/commit/b234ce76b8f03db0b4668eaddf531291819bd922)) * use active versions only for bench comparisson ([#107](https://github.com/RafaelGSS/bench-node/issues/107)) ([bf119c7](https://github.com/RafaelGSS/bench-node/commit/bf119c783df026ad40b7e26209fc7b2368b6a292)) ## [0.10.0](https://github.com/RafaelGSS/bench-node/compare/v0.9.0...v0.10.0) (2025-07-31) ### Features * Add code coverage to the test code. ([#96](https://github.com/RafaelGSS/bench-node/issues/96)) ([daff350](https://github.com/RafaelGSS/bench-node/commit/daff3509be6731e7415eaa2a8ba0b7464d1b7408)) * add fastest/slowest value for feature parity with benchmark.js ([9ad4c94](https://github.com/RafaelGSS/bench-node/commit/9ad4c9461f105f7681a77f80a2c33eea521ddba6)) * Show bar chart with 2% resolution by using partial width box characters. ([#97](https://github.com/RafaelGSS/bench-node/issues/97)) ([4c65557](https://github.com/RafaelGSS/bench-node/commit/4c65557b6472c8437eba98d4f91b52da8628d614)) ### Bug Fixes * cpus().length is broken under docker. ([#100](https://github.com/RafaelGSS/bench-node/issues/100)) ([c423cdd](https://github.com/RafaelGSS/bench-node/commit/c423cdd0220bc7317861fa387c99a230a34076ee)) ### Styles * Blue works better on light terminals and still looks good on dark. ([#95](https://github.com/RafaelGSS/bench-node/issues/95)) ([5ec0319](https://github.com/RafaelGSS/bench-node/commit/5ec0319787c08ff69ecd54c8003c715e96eafdc7)) ### Miscellaneous Chores * do not stop machine on runner_warmer ([6e0e71d](https://github.com/RafaelGSS/bench-node/commit/6e0e71dd30e4eb3bb5a1902a9eb9e6050bbccef7)) * run lint:ci on test ([#103](https://github.com/RafaelGSS/bench-node/issues/103)) ([a85e15c](https://github.com/RafaelGSS/bench-node/commit/a85e15cfb6b8bbf90a56c28aea49a3ff2e913567)) ## [0.9.0](https://github.com/RafaelGSS/bench-node/compare/v0.8.0...v0.9.0) (2025-07-17) ### Features * add reporterOptions support with printHeader opt ([#92](https://github.com/RafaelGSS/bench-node/issues/92)) ([20d34e9](https://github.com/RafaelGSS/bench-node/commit/20d34e90340d179ea20958f760f732f7e1573551)) ## [0.8.0](https://github.com/RafaelGSS/bench-node/compare/v0.7.0...v0.8.0) (2025-07-16) ### Features * add baseline and summary to pretty and text reporter ([#83](https://github.com/RafaelGSS/bench-node/issues/83)) ([3aa57cb](https://github.com/RafaelGSS/bench-node/commit/3aa57cb1e487274b141ab79a99e7ed0718cdbf63)) * add pretty-reporter and shorthand pretty: true ([#82](https://github.com/RafaelGSS/bench-node/issues/82)) ([45efe9c](https://github.com/RafaelGSS/bench-node/commit/45efe9c833cc39e7d8abfd203fbb05a4e84e9daa)) * export bench-node ts types ([#77](https://github.com/RafaelGSS/bench-node/issues/77)) ([d93f111](https://github.com/RafaelGSS/bench-node/commit/d93f111472515ff19b0fbe57bce588fdc51cfc9d)) ### Bug Fixes * fix imports in type test file ([#87](https://github.com/RafaelGSS/bench-node/issues/87)) ([defa158](https://github.com/RafaelGSS/bench-node/commit/defa158ffadceb6b8c660a802445eafe33933db5)) * use self-hosted runner for test CI ([#81](https://github.com/RafaelGSS/bench-node/issues/81)) ([a65af10](https://github.com/RafaelGSS/bench-node/commit/a65af107859946808ac8458d7b15bb9c887b12da)) ### Miscellaneous Chores * drop Node.js v23 and add v24 ([#78](https://github.com/RafaelGSS/bench-node/issues/78)) ([c2dae5f](https://github.com/RafaelGSS/bench-node/commit/c2dae5f038e3ec613aaac753091fbff5ed927b3f)) ## [0.7.0](https://github.com/RafaelGSS/bench-node/compare/v0.6.0...v0.7.0) (2025-05-15) ### Features * add opsSecPerRun result ([#74](https://github.com/RafaelGSS/bench-node/issues/74)) ([34ad9e9](https://github.com/RafaelGSS/bench-node/commit/34ad9e95b23d92d80442955569f04efdafc97655)) ## [0.6.0](https://github.com/RafaelGSS/bench-node/compare/v0.5.4...v0.6.0) (2025-04-30) ### Features * add time mode benchmark ([#71](https://github.com/RafaelGSS/bench-node/issues/71)) ([9d72044](https://github.com/RafaelGSS/bench-node/commit/9d72044a13a9fd93ae5f1b97fc26ff4a7db7d5f1)) ### Documentation * add JSDoc to functions ([#69](https://github.com/RafaelGSS/bench-node/issues/69)) ([e5c6695](https://github.com/RafaelGSS/bench-node/commit/e5c66957ec737f1a8f5ecc4f204e8032daf50aca)) ### Miscellaneous Chores * start issuing semver-minor ([3a261ae](https://github.com/RafaelGSS/bench-node/commit/3a261ae94ea029e8aed68153968570eb7c38a2c1)) ## [0.5.4](https://github.com/RafaelGSS/bench-node/compare/v0.5.3...v0.5.4) (2025-03-13) ### Features * expose histogram sample data to reporters via sampleData property ([#67](https://github.com/RafaelGSS/bench-node/issues/67)) ([833bec1](https://github.com/RafaelGSS/bench-node/commit/833bec16ae8167785e148ec939fc6211a0662822)) ## [0.5.3](https://github.com/RafaelGSS/bench-node/compare/v0.5.2...v0.5.3) (2025-02-24) ### Features * add min samples as param ([#65](https://github.com/RafaelGSS/bench-node/issues/65)) ([9c6812e](https://github.com/RafaelGSS/bench-node/commit/9c6812e1124f44d95f8d086cba01b5302ec5187e)) ### Miscellaneous Chores * **readme:** clean + update ([#61](https://github.com/RafaelGSS/bench-node/issues/61)) ([b5e1e8b](https://github.com/RafaelGSS/bench-node/commit/b5e1e8bb507b9f8b017a91e8c814fb1046908042)) ## [0.5.2](https://github.com/RafaelGSS/bench-node/compare/v0.5.1...v0.5.2) (2025-01-27) ### Features * **chartReport:** include node-v ([#53](https://github.com/RafaelGSS/bench-node/issues/53)) ([695842e](https://github.com/RafaelGSS/bench-node/commit/695842eb58c8b6106598a4cef26fe44a552065c6)) * improve htmlReport UX ([#60](https://github.com/RafaelGSS/bench-node/issues/60)) ([79ce533](https://github.com/RafaelGSS/bench-node/commit/79ce5335e95d531b90c4e26fde983ec1c6de9502)) ### Documentation * **readme:** add badges and links ([f85f809](https://github.com/RafaelGSS/bench-node/commit/f85f809aa0f85987d3b01ef9737ed0a855e46741)) ### Miscellaneous Chores * **"branding":** add logo ([#58](https://github.com/RafaelGSS/bench-node/issues/58)) ([3eedd06](https://github.com/RafaelGSS/bench-node/commit/3eedd064791f5804810545db247a509aea618234)) ## [0.5.1](https://github.com/RafaelGSS/bench-node/compare/v0.5.0...v0.5.1) (2025-01-19) ### Features * create csv reporter ([#38](https://github.com/RafaelGSS/bench-node/issues/38)) ([11be8e5](https://github.com/RafaelGSS/bench-node/commit/11be8e52e8cbb5e17f378a8462dd1c4bf7b35351)) * **reporter:** polish chart output ([#40](https://github.com/RafaelGSS/bench-node/issues/40)) ([91082b6](https://github.com/RafaelGSS/bench-node/commit/91082b6441cfa6ba7b195d7386d493d689e29454)) * use biome linter ([#34](https://github.com/RafaelGSS/bench-node/issues/34)) ([11c1108](https://github.com/RafaelGSS/bench-node/commit/11c11088e12d2b77547389eb0a5055ad3ff11427)) ### Bug Fixes * ignore package.json due to release-please ([7c95b0a](https://github.com/RafaelGSS/bench-node/commit/7c95b0a4fd41aa81576503ac9444a775ed498eda)) * lint package.json ([549f6ca](https://github.com/RafaelGSS/bench-node/commit/549f6ca574f4a30915e86dff9cd073b3d90def1e)) ### Miscellaneous Chores * **main:** release 0.5.1 ([#44](https://github.com/RafaelGSS/bench-node/issues/44)) ([4e51324](https://github.com/RafaelGSS/bench-node/commit/4e51324ea129c3607229aaec3b8d22ef221d0e7d)) * **main:** release 0.5.2 ([#45](https://github.com/RafaelGSS/bench-node/issues/45)) ([baf2014](https://github.com/RafaelGSS/bench-node/commit/baf20147c1f09f3e50491845e536c590db0d8aa5)) * **main:** release 0.5.3 ([4757182](https://github.com/RafaelGSS/bench-node/commit/4757182c015cfbd769ebf3969c8269120271e5b3)) ### Tests * add managed basic tests ([#36](https://github.com/RafaelGSS/bench-node/issues/36)) ([c491a32](https://github.com/RafaelGSS/bench-node/commit/c491a328329bc79b2ef8124856b162c8df0e8cfb)) ### Continuous Integration * **release:** add support to release via release-please ([#42](https://github.com/RafaelGSS/bench-node/issues/42)) ([5263cc6](https://github.com/RafaelGSS/bench-node/commit/5263cc68a5c854a260b68e1f5b930496153ac7fb)) ## [0.5.3](https://github.com/RafaelGSS/bench-node/compare/v0.5.2...v0.5.3) (2025-01-16) ### Features * add benchmark repetition ([#27](https://github.com/RafaelGSS/bench-node/issues/27)) ([d65e8aa](https://github.com/RafaelGSS/bench-node/commit/d65e8aab609b882a32331a48bb60fb81ee2db24a)) * add enough context to plugin methods to figure out bench task name ([c16cf34](https://github.com/RafaelGSS/bench-node/commit/c16cf340699cf198ca10146f30c158697afff908)) * add html reporter ([a69fdfb](https://github.com/RafaelGSS/bench-node/commit/a69fdfb7415eeb4645e7116be125ccf876d00ebc)) * add JSONReporter ([7b51c16](https://github.com/RafaelGSS/bench-node/commit/7b51c16db1446b4a2c921c2548e14462197f4779)) * code and examples ([#1](https://github.com/RafaelGSS/bench-node/issues/1)) ([503b573](https://github.com/RafaelGSS/bench-node/commit/503b573a67cf9245383da949274b30412c366084)) * create csv reporter ([#38](https://github.com/RafaelGSS/bench-node/issues/38)) ([11be8e5](https://github.com/RafaelGSS/bench-node/commit/11be8e52e8cbb5e17f378a8462dd1c4bf7b35351)) * **enrichers:** added V8NeverOptimizeEnricher and V8OptimizeOnNextCallEnricher ([16e842e](https://github.com/RafaelGSS/bench-node/commit/16e842eb5dad9703fd009979f68b4f71c98436b2)) * initial commit ([ee2d46f](https://github.com/RafaelGSS/bench-node/commit/ee2d46fc446a481c7bca731639759e4b7529c405)) * **memory-enricher:** added support to report memory heap statistics ([441b3ad](https://github.com/RafaelGSS/bench-node/commit/441b3adfee5d92cdd32cb0d4bfd5e7b49d14c2af)) * **reporter:** polish chart output ([#40](https://github.com/RafaelGSS/bench-node/issues/40)) ([91082b6](https://github.com/RafaelGSS/bench-node/commit/91082b6441cfa6ba7b195d7386d493d689e29454)) * use biome linter ([#34](https://github.com/RafaelGSS/bench-node/issues/34)) ([11c1108](https://github.com/RafaelGSS/bench-node/commit/11c11088e12d2b77547389eb0a5055ad3ff11427)) ### Bug Fixes * handle htmlReport when bench suite is uppercase ([1685144](https://github.com/RafaelGSS/bench-node/commit/16851442e3fe3a97f8e6fc5c98993c77162dc4bc)) * **lifecycle:** missing imports ([08c6064](https://github.com/RafaelGSS/bench-node/commit/08c60646736ee1236cb371143594e337b1d5f502)) * typo ([d2a36ae](https://github.com/RafaelGSS/bench-node/commit/d2a36aec5dae24b9a4e95e4f055109a73d3b6bbc)) ### Miscellaneous Chores * add exec permission to run.sh ([5d0f4ef](https://github.com/RafaelGSS/bench-node/commit/5d0f4ef72849189472b6700ddf7e56376eea61a2)) * add node_modules to ignore ([478f24c](https://github.com/RafaelGSS/bench-node/commit/478f24c3fb8cd896e28e1c87e3212269fe9e31eb)) * **examples:** added example benchmarks ([b4b50b2](https://github.com/RafaelGSS/bench-node/commit/b4b50b23def45698c854bf3bbe434d3f3a92567d)) * **gitignore:** ignore .idea folder ([e9a13ae](https://github.com/RafaelGSS/bench-node/commit/e9a13ae640fd2ec2d7e714c0c6c9240f4ab1c628)) * **main:** release 0.5.1 ([#44](https://github.com/RafaelGSS/bench-node/issues/44)) ([4e51324](https://github.com/RafaelGSS/bench-node/commit/4e51324ea129c3607229aaec3b8d22ef221d0e7d)) * **main:** release 0.5.2 ([#45](https://github.com/RafaelGSS/bench-node/issues/45)) ([baf2014](https://github.com/RafaelGSS/bench-node/commit/baf20147c1f09f3e50491845e536c590db0d8aa5)) * rename to bench-node ([2f15705](https://github.com/RafaelGSS/bench-node/commit/2f157051e3b1988ac3a8094e0fc7e4daee267a48)) * rename to nodebenchmark ([9806a31](https://github.com/RafaelGSS/bench-node/commit/9806a31c819073d705bd59c29adc35e808e61d6c)) * **run:** added script to run all examples ([6733730](https://github.com/RafaelGSS/bench-node/commit/6733730de9fa83a0b6ee7f243b1c3c0576f6f4ad)) * update rafaelgss email ([a5ec544](https://github.com/RafaelGSS/bench-node/commit/a5ec5445a777c9db12181cae70cd47def0ac56c2)) ### Code Refactoring * **lib:** from esm to commonjs ([f25d0e4](https://github.com/RafaelGSS/bench-node/commit/f25d0e40c293a07fe865f09f9bd6693b3152e5b0)) * **lib:** make the code usable outside/inside node core ([c60c80e](https://github.com/RafaelGSS/bench-node/commit/c60c80e8fd6cad52f5275419252e313e03767893)) * **validators:** added missing validators on clock ([478fc7e](https://github.com/RafaelGSS/bench-node/commit/478fc7e3456c84797cd718b2c7eeb7e876bad2bc)) ### Tests * add a test documenting the plugin signature and lifecycle ([fd379d6](https://github.com/RafaelGSS/bench-node/commit/fd379d6ed51317504255eb78a24e33db21e0b3a7)) * add basic test suite ([8349ee0](https://github.com/RafaelGSS/bench-node/commit/8349ee0f96236646776fd12843c01d1d9c806b42)) * add managed basic tests ([#36](https://github.com/RafaelGSS/bench-node/issues/36)) ([c491a32](https://github.com/RafaelGSS/bench-node/commit/c491a328329bc79b2ef8124856b162c8df0e8cfb)) * add scenario for optimized managed benchmark ([74c4db1](https://github.com/RafaelGSS/bench-node/commit/74c4db1046857f9af57c0c54cc5bf801d0195339)) * add test case for copy function ([ddf6dc7](https://github.com/RafaelGSS/bench-node/commit/ddf6dc7b4e7a695f6bff5766788b4b0d5beec527)) * fix the plugin api test ([be8ec69](https://github.com/RafaelGSS/bench-node/commit/be8ec69ff9481ce55b8e49f5732e01a468f6b5de)) * include TODO test for managed and async ([15ff469](https://github.com/RafaelGSS/bench-node/commit/15ff46924bb969d724d1f92f5611a3f4385f0d47)) * increase percentage diff on CI ([fa57188](https://github.com/RafaelGSS/bench-node/commit/fa571883f30fd7033a12e05f291fe12bf4816152)) ### Build System * move run.sh to examples folder ([08ac769](https://github.com/RafaelGSS/bench-node/commit/08ac7699032a32f3a04a252cc48ee1514fd734bd)) ### Continuous Integration * **release:** add support to release via release-please ([#42](https://github.com/RafaelGSS/bench-node/issues/42)) ([5263cc6](https://github.com/RafaelGSS/bench-node/commit/5263cc68a5c854a260b68e1f5b930496153ac7fb)) ## [0.5.2](https://github.com/RafaelGSS/bench-node/compare/v0.5.1...v0.5.2) (2025-01-16) ### Features * add benchmark repetition ([#27](https://github.com/RafaelGSS/bench-node/issues/27)) ([d65e8aa](https://github.com/RafaelGSS/bench-node/commit/d65e8aab609b882a32331a48bb60fb81ee2db24a)) * add enough context to plugin methods to figure out bench task name ([c16cf34](https://github.com/RafaelGSS/bench-node/commit/c16cf340699cf198ca10146f30c158697afff908)) * add html reporter ([a69fdfb](https://github.com/RafaelGSS/bench-node/commit/a69fdfb7415eeb4645e7116be125ccf876d00ebc)) * add JSONReporter ([7b51c16](https://github.com/RafaelGSS/bench-node/commit/7b51c16db1446b4a2c921c2548e14462197f4779)) * code and examples ([#1](https://github.com/RafaelGSS/bench-node/issues/1)) ([503b573](https://github.com/RafaelGSS/bench-node/commit/503b573a67cf9245383da949274b30412c366084)) * create csv reporter ([#38](https://github.com/RafaelGSS/bench-node/issues/38)) ([11be8e5](https://github.com/RafaelGSS/bench-node/commit/11be8e52e8cbb5e17f378a8462dd1c4bf7b35351)) * **enrichers:** added V8NeverOptimizeEnricher and V8OptimizeOnNextCallEnricher ([16e842e](https://github.com/RafaelGSS/bench-node/commit/16e842eb5dad9703fd009979f68b4f71c98436b2)) * initial commit ([ee2d46f](https://github.com/RafaelGSS/bench-node/commit/ee2d46fc446a481c7bca731639759e4b7529c405)) * **memory-enricher:** added support to report memory heap statistics ([441b3ad](https://github.com/RafaelGSS/bench-node/commit/441b3adfee5d92cdd32cb0d4bfd5e7b49d14c2af)) * **reporter:** polish chart output ([#40](https://github.com/RafaelGSS/bench-node/issues/40)) ([91082b6](https://github.com/RafaelGSS/bench-node/commit/91082b6441cfa6ba7b195d7386d493d689e29454)) * use biome linter ([#34](https://github.com/RafaelGSS/bench-node/issues/34)) ([11c1108](https://github.com/RafaelGSS/bench-node/commit/11c11088e12d2b77547389eb0a5055ad3ff11427)) ### Bug Fixes * handle htmlReport when bench suite is uppercase ([1685144](https://github.com/RafaelGSS/bench-node/commit/16851442e3fe3a97f8e6fc5c98993c77162dc4bc)) * **lifecycle:** missing imports ([08c6064](https://github.com/RafaelGSS/bench-node/commit/08c60646736ee1236cb371143594e337b1d5f502)) * typo ([d2a36ae](https://github.com/RafaelGSS/bench-node/commit/d2a36aec5dae24b9a4e95e4f055109a73d3b6bbc)) ### Miscellaneous Chores * add exec permission to run.sh ([5d0f4ef](https://github.com/RafaelGSS/bench-node/commit/5d0f4ef72849189472b6700ddf7e56376eea61a2)) * add node_modules to ignore ([478f24c](https://github.com/RafaelGSS/bench-node/commit/478f24c3fb8cd896e28e1c87e3212269fe9e31eb)) * **examples:** added example benchmarks ([b4b50b2](https://github.com/RafaelGSS/bench-node/commit/b4b50b23def45698c854bf3bbe434d3f3a92567d)) * **gitignore:** ignore .idea folder ([e9a13ae](https://github.com/RafaelGSS/bench-node/commit/e9a13ae640fd2ec2d7e714c0c6c9240f4ab1c628)) * **main:** release 0.5.1 ([#44](https://github.com/RafaelGSS/bench-node/issues/44)) ([4e51324](https://github.com/RafaelGSS/bench-node/commit/4e51324ea129c3607229aaec3b8d22ef221d0e7d)) * rename to bench-node ([2f15705](https://github.com/RafaelGSS/bench-node/commit/2f157051e3b1988ac3a8094e0fc7e4daee267a48)) * rename to nodebenchmark ([9806a31](https://github.com/RafaelGSS/bench-node/commit/9806a31c819073d705bd59c29adc35e808e61d6c)) * **run:** added script to run all examples ([6733730](https://github.com/RafaelGSS/bench-node/commit/6733730de9fa83a0b6ee7f243b1c3c0576f6f4ad)) * update rafaelgss email ([a5ec544](https://github.com/RafaelGSS/bench-node/commit/a5ec5445a777c9db12181cae70cd47def0ac56c2)) ### Code Refactoring * **lib:** from esm to commonjs ([f25d0e4](https://github.com/RafaelGSS/bench-node/commit/f25d0e40c293a07fe865f09f9bd6693b3152e5b0)) * **lib:** make the code usable outside/inside node core ([c60c80e](https://github.com/RafaelGSS/bench-node/commit/c60c80e8fd6cad52f5275419252e313e03767893)) * **validators:** added missing validators on clock ([478fc7e](https://github.com/RafaelGSS/bench-node/commit/478fc7e3456c84797cd718b2c7eeb7e876bad2bc)) ### Tests * add a test documenting the plugin signature and lifecycle ([fd379d6](https://github.com/RafaelGSS/bench-node/commit/fd379d6ed51317504255eb78a24e33db21e0b3a7)) * add basic test suite ([8349ee0](https://github.com/RafaelGSS/bench-node/commit/8349ee0f96236646776fd12843c01d1d9c806b42)) * add managed basic tests ([#36](https://github.com/RafaelGSS/bench-node/issues/36)) ([c491a32](https://github.com/RafaelGSS/bench-node/commit/c491a328329bc79b2ef8124856b162c8df0e8cfb)) * add scenario for optimized managed benchmark ([74c4db1](https://github.com/RafaelGSS/bench-node/commit/74c4db1046857f9af57c0c54cc5bf801d0195339)) * add test case for copy function ([ddf6dc7](https://github.com/RafaelGSS/bench-node/commit/ddf6dc7b4e7a695f6bff5766788b4b0d5beec527)) * fix the plugin api test ([be8ec69](https://github.com/RafaelGSS/bench-node/commit/be8ec69ff9481ce55b8e49f5732e01a468f6b5de)) * include TODO test for managed and async ([15ff469](https://github.com/RafaelGSS/bench-node/commit/15ff46924bb969d724d1f92f5611a3f4385f0d47)) * increase percentage diff on CI ([fa57188](https://github.com/RafaelGSS/bench-node/commit/fa571883f30fd7033a12e05f291fe12bf4816152)) ### Build System * move run.sh to examples folder ([08ac769](https://github.com/RafaelGSS/bench-node/commit/08ac7699032a32f3a04a252cc48ee1514fd734bd)) ### Continuous Integration * **release:** add support to release via release-please ([#42](https://github.com/RafaelGSS/bench-node/issues/42)) ([5263cc6](https://github.com/RafaelGSS/bench-node/commit/5263cc68a5c854a260b68e1f5b930496153ac7fb)) ## [0.5.1](https://github.com/RafaelGSS/bench-node/compare/v0.5.0...v0.5.1) (2025-01-14) ### Features * create csv reporter ([#38](https://github.com/RafaelGSS/bench-node/issues/38)) ([11be8e5](https://github.com/RafaelGSS/bench-node/commit/11be8e52e8cbb5e17f378a8462dd1c4bf7b35351)) * **reporter:** polish chart output ([#40](https://github.com/RafaelGSS/bench-node/issues/40)) ([91082b6](https://github.com/RafaelGSS/bench-node/commit/91082b6441cfa6ba7b195d7386d493d689e29454)) * use biome linter ([#34](https://github.com/RafaelGSS/bench-node/issues/34)) ([11c1108](https://github.com/RafaelGSS/bench-node/commit/11c11088e12d2b77547389eb0a5055ad3ff11427)) ### Tests * add managed basic tests ([#36](https://github.com/RafaelGSS/bench-node/issues/36)) ([c491a32](https://github.com/RafaelGSS/bench-node/commit/c491a328329bc79b2ef8124856b162c8df0e8cfb)) ### Continuous Integration * **release:** add support to release via release-please ([#42](https://github.com/RafaelGSS/bench-node/issues/42)) ([5263cc6](https://github.com/RafaelGSS/bench-node/commit/5263cc68a5c854a260b68e1f5b930496153ac7fb)) ================================================ FILE: Dockerfile ================================================ FROM node:24-trixie-slim RUN apt update && apt install git -y ================================================ FILE: README.md ================================================

Bench Node logo Bench Node

Install   |    Usage   |    Suite   |    Plugins   |    Using Reporter   |    Setup and Teardown   |    Benchmark Modes   |    Writing JavaScript Mistakes

[![npm package][npm-img]][npm-url] [![Build Status][build-img]][build-url] [![Downloads][downloads-img]][downloads-url] [![Issues][issues-img]][issues-url] The `bench-node` module allows you to measure operations per second of Node.js code blocks. ## Install ```bash $ npm install bench-node ``` ## Usage ```cjs const { Suite } = require('bench-node'); const suite = new Suite(); suite.add('Using delete property', () => { const data = { x: 1, y: 2, z: 3 }; delete data.y; data.x; data.y; data.z; }); suite.run() ``` ```bash $ node --allow-natives-syntax my-benchmark.js Using delete property x 3,326,913 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(0ns ... 0ns) p75=0ns p99=0ns ``` This module uses V8 deoptimization to help ensure that the code block is not optimized away, producing accurate benchmarks -- but not realistic. See the [Writing JavaScript Microbenchmark Mistakes](#writing-javascript-mistakes) section for more details. The [`bench-node-cli`](https://github.com/RafaelGSS/bench-node-cli) tool allows you to execute a `bench-node` benchmark from any location, eliminating the need to install the `bench-node` package locally. Simply use the following command to run your benchmark: ```bash npx bench-node-cli my-benchmark.js ``` See the [examples folder](./examples/) for more common usage examples. ## Table of Contents - [Install](#install) - [Usage](#usage) - [Table of Contents](#table-of-contents) - [Sponsors](#sponsors) - [Class: `Suite`](#class-suite) - [`new Suite([options])`](#new-suiteoptions) - [`suite.add(name[, options], fn)`](#suiteaddname-options-fn) - [`suite.run()`](#suiterun) - [Dead Code Elimination Detection](#dead-code-elimination-detection) - [How It Works](#how-it-works) - [Configuration](#configuration) - [When DCE Warnings Appear](#when-dce-warnings-appear) - [Plugins](#plugins) - [Plugin Methods](#plugin-methods) - [Example Plugin](#example-plugin) - [Using Reporter](#using-reporter) - [`textReport` (Default)](#textreport-default) - [`chartReport`](#chartreport) - [`htmlReport`](#htmlreport) - [`jsonReport`](#jsonreport) - [CSV Reporter](#csv-reporter) - [Pretty Reporter](#pretty-reporter) - [Custom Reporter](#custom-reporter) - [Setup and Teardown](#setup-and-teardown) - [Managed Benchmarks](#managed-benchmarks) - [Worker Threads](#worker-threads) - [Benchmark Modes](#benchmark-modes) - [Operations Mode](#operations-mode) - [Time Mode](#time-mode) - [Baseline Comparisons](#baseline-comparisons) - [Statistical Significance Testing](#statistical-significance-testing-t-test) - [Direct API Usage](#direct-api-usage) - [Fixing Inconclusive Tests](#fixing-inconclusive-tests) - [Writing JavaScript Mistakes](#writing-javascript-mistakes) ## Sponsors Test machines are generously sponsored by [NodeSource](https://nodesource.com/). NodeSource logo ## Class: `Suite` > Stability: 1.1 Active Development A `Suite` manages and executes benchmark functions. It provides two methods: `add()` and `run()`. ### `new Suite([options])` * `options` {Object} Configuration options for the suite. * `reporter` {Function} Callback function for reporting results. Receives two arguments: * `results` {Object[]} Array of benchmark results: * `name` {string} Benchmark name. * `opsSec` {string} Operations per second. * `iterations` {Number} Number of iterations. * `histogram` {Histogram} Histogram instance. * `ttest` {boolean} Enable Welch's t-test for statistical significance testing. Automatically sets `repeatSuite=30`. **Default:** `false`. * `reporterOptions` {Object} Reporter-specific options. * `printHeader` {boolean} Whether to print system information header. **Default:** `true`. * `labelWidth` {number} Width for benchmark labels in output. **Default:** `45`. * `alpha` {number} Significance level for t-test (e.g., 0.05 for 95% confidence). **Default:** `0.05`. * `benchmarkMode` {string} Benchmark mode to use. Can be 'ops' or 'time'. **Default:** `'ops'`. * `'ops'` - Measures operations per second (traditional benchmarking). * `'time'` - Measures actual execution time for a single run. * `useWorkers` {boolean} Whether to run benchmarks in worker threads. **Default:** `false`. * `plugins` {Array} Array of plugin instances to use. * `repeatSuite` {number} Number of times to repeat each benchmark. Automatically set to `30` when `ttest: true`. **Default:** `1`. * `plugins` {Array} Array of plugin instances to use. **Default:** `[V8NeverOptimizePlugin]`. * `minSamples` {number} Minimum number of samples per round for all benchmarks in the suite. Can be overridden per benchmark. **Default:** `10` samples. * `detectDeadCodeElimination` {boolean} Enable dead code elimination detection. When enabled, default plugins are disabled to allow V8 optimizations. **Default:** `false`. * `dceThreshold` {number} Threshold multiplier for DCE detection. Benchmarks faster than baseline × threshold will trigger warnings. **Default:** `10`. If no `reporter` is provided, results are printed to the console. ```js const { Suite } = require('bench-node'); const suite = new Suite(); ``` If you don't want results to be printed to the console, `false` and `null` can be used ```js const { Suite } = require('bench-node'); const suite = new Suite({ reporter: false }); ``` ### `suite.add(name[, options], fn)` * `name` {string} The name of the benchmark, displayed when reporting results. * `options` {Object} Configuration options for the benchmark. Supported properties: * `minTime` {number} The minimum duration of each sampling interval. **Default:** `0.05` seconds. * `maxTime` {number} Maximum duration for the benchmark to run. **Default:** `0.5` seconds. * `repeatSuite` {number} Number of times to repeat benchmark to run. **Default:** `1` times. * `minSamples` {number} Number minimum of samples the each round. **Default:** `10` samples. * `baseline` {boolean} Mark this benchmark as the baseline for comparison. Only one benchmark per suite can be baseline. **Default:** `false`. * `fn` {Function|AsyncFunction} The benchmark function. Can be synchronous or asynchronous. * Returns: {Suite} Adds a benchmark function to the suite. ```bash $ node --allow-natives-syntax my-benchmark.js Using delete property x 5,853,505 ops/sec (10 runs sampled) min..max=(169ns ... 171ns) ``` ### `suite.run()` * Returns: `{Promise>}` An array of benchmark results, each containing: * `opsSec` {number} Operations per second (Only in 'ops' mode). * `opsSecPerRun` {Array} Array of operations per second (useful when repeatSuite > 1). * `totalTime` {number} Total execution time in seconds (Only in 'time' mode). * `iterations` {number} Number of executions of `fn`. * `histogram` {Histogram} Histogram of benchmark iterations. * `name` {string} Benchmark name. * `plugins` {Object} Object with plugin results if any plugins are active. Runs all added benchmarks and returns the results. ## Dead Code Elimination Detection **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. **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. ### How It Works 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. ### Example Warning Output ``` ⚠️ Dead Code Elimination Warnings: The following benchmarks may have been optimized away by the JIT compiler: • array creation Benchmark: 3.98ns/iter Baseline: 0.77ns/iter Ratio: 5.18x of baseline Suggestion: Ensure the result is used or assign to a variable ℹ️ These benchmarks are running nearly as fast as an empty function, which suggests the JIT may have eliminated the actual work. ``` ### Configuration ```js const { Suite, V8NeverOptimizePlugin } = require('bench-node'); // Enable DCE detection (disables V8NeverOptimizePlugin automatically) const suite = new Suite({ detectDeadCodeElimination: true }); // Enable DCE detection with custom threshold (default is 10x) const strictSuite = new Suite({ detectDeadCodeElimination: true, dceThreshold: 20 // Only warn if < 20x slower than baseline }); // Use both DCE detection AND prevent optimization // (helpful for educational purposes to see warnings even when using %NeverOptimizeFunction) const educationalSuite = new Suite({ plugins: [new V8NeverOptimizePlugin()], detectDeadCodeElimination: true }); ``` ### When DCE Warnings Appear Common scenarios that trigger warnings: ```js // ❌ Result not used - will be optimized away suite.add('computation', () => { const result = Math.sqrt(144); // result is never used }); // ✅ Result is used - less likely to be optimized suite.add('computation', () => { const result = Math.sqrt(144); if (result !== 12) throw new Error('Unexpected'); }); ``` **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. See [examples/dce-detection/](./examples/dce-detection/) for more examples. ## Plugins Plugins extend the functionality of the benchmark module. See [Plugins](./doc/Plugins.md) for details. ### Plugin Methods - **`isSupported()`**: Checks if the plugin can run in the current environment. - **`beforeClockTemplate(varNames)`**: Injects code before the benchmark starts. Returns an array with: * `Code` {string} JavaScript code to execute. * `Wrapper` {string} (optional) Function to wrap the benchmark function. - **`afterClockTemplate(varNames)`**: Injects code after the benchmark finishes. Returns an array with: * `Code` {string} JavaScript code to execute. - **`onCompleteBenchmark(result, bench)`**: Called when the benchmark completes, allowing plugins to process results. - **`toString()`**: Returns a string identifier for the plugin. - **`getReport(benchmarkName)`**: Returns a string to be displayed in the benchmark result line. - **`getResult(benchmarkName)`**: Returns the data that can be used by the reporter. - **`reset()`**: Resets the plugin state, to avoid carrying over data between benchmarks. ### Example Plugin ```js class V8OptimizeOnNextCallPlugin { isSupported() { try { new Function(`%OptimizeFunctionOnNextCall(() => {})`)(); return true; } catch (e) { return false; } } beforeClockTemplate({ awaitOrEmpty, bench }) { let code = ''; code += `%OptimizeFunctionOnNextCall(${bench}.fn);\n`; code += `${awaitOrEmpty}${bench}.fn();\n`; code += `${awaitOrEmpty}${bench}.fn();\n`; return [code]; } toString() { return 'V8OptimizeOnNextCallPlugin'; } } ``` ## Using Reporter This module exports two reporters that control how benchmark results are displayed: a detailed `textReport` for statistical analysis, and a visual `chartReport` that displays a bar graph in the terminal. ### `textReport` (Default) The `textReport` is the default reporter, which provides simple statistical information about each benchmark result. It includes the number of operations per second, the number of runs sampled, min...max, and enabled plugins. **Example Output**: ``` single with matcher x 710,248 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(1.40us...1.42us) multiple replaces x 604,713 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(1.64us...1.70us) ``` Here’s how you can explicitly pass it as a reporter: ```cjs const { Suite, textReport } = require('bench-node'); const suite = new Suite({ reporter: textReport, // Optional, since this is the default }); ``` ### `chartReport` The `chartReport` reporter provides a graphical representation of benchmark results in the form of a bar chart, making it easier to visualize the relative performance of each benchmark. It scales the bars based on the highest operations per second (ops/sec) value, and displays the results incrementally as they are collected. Example output: ``` Node.js version: v23.6.1 Platform: darwin arm64 CPU Cores: 8 vCPUs | 16.0GB Mem single with matcher | ██████████████████████████████ | 709,321 ops/sec | 10 samples multiple replaces | ██████████████████████████---- | 606,401 ops/sec | 11 samples ``` Usage: ```cjs const { Suite, chartReport } = require('bench-node'); const suite = new Suite({ reporter: chartReport, // Optionally control header display reporterOptions: { printHeader: true // Set to false to hide system info header } }); ``` The `reporterOptions.printHeader` setting controls whether the system information (Node.js version, platform, and CPU cores) appears at the top of the output. This is useful when running multiple suites in sequence and you want to avoid duplicate headers. ### `htmlReport` The `htmlReport` generates an interactive HTML visualization of benchmark results. It transforms benchmark data into a visual format, such as speed circle animations, making it easier to interpret and share performance insights. Example output: https://github.com/user-attachments/assets/b2b98175-6648-4af4-8319-63f3ebbc729e Usage: ```cjs const { Suite, htmlReport } = require('bench-node'); const suite = new Suite({ reporter: htmlReport, }); ``` ### `jsonReport` The `jsonReport` plugin provides benchmark results in **JSON format**. It includes key performance metrics—such as `opsSec`, `runsSampled`, `min` and `max` times, and any reporter data from your **plugins**—so you can easily store, parse, or share the information. Example output: ```json [ { "name": "single with matcher", "opsSec": 180000, "runsSampled": 50, "min": "13.20μs", "max": "82.57μs", "plugins": [] }, { "name": "Multiple replaces", "opsSec": 170000, "runsSampled": 50, "min": "15.31μs", "max": "77.49μs", "plugins": [] } ] ``` **Usage:** ```cjs const { Suite, jsonReport } = require('bench-node'); const suite = new Suite({ reporter: jsonReport, }); ``` ### CSV Reporter The `csvReport` plugin generates benchmark results in CSV format. It includes columns for key performance metrics like `ops/sec`, `samples`, `min` and `max` times, as well as any reporter data provided by your plugins. Example output: ```csv name,ops/sec,samples,plugins,min,max Using delete property,"7,732,517",10,v8-never-optimize=true,127.91ns,129.95ns Using delete property (proto: null),"24,636,631",10,v8-never-optimize=true,39.57ns,40.91ns Using delete property (cached proto: null),"7,497,893",11,v8-never-optimize=true,132.25ns,134.89ns Using undefined assignment,"132,093,600",11,v8-never-optimize=true,7.53ns,7.64ns Using undefined assignment (proto: null),"28,231,374",9,v8-never-optimize=true,35.27ns,35.42ns Using undefined property (cached proto: null),"60,843,193",10,v8-never-optimize=true,16.24ns,16.65ns [Managed] Using undefined property (cached proto: null),"35,394,060",10,v8-never-optimize=true,27.90ns,28.54ns ``` **Usage:** ```cjs const { Suite, csvReport } = require('bench-node'); const suite = new Suite({ reporter: csvReport, }); ``` ### Pretty Reporter The `prettyReport` provides a beautiful, hierarchical view of your benchmark results. **Usage:** ```javascript const { Suite } = require('bench-node'); // You can either pass the reporter function directly... const suiteWithReporter = new Suite({ reporter: prettyReport, }); // ...or use the `pretty` option for convenience. const suiteWithPrettyOption = new Suite({ pretty: true, }); suite .add('my-group/my-benchmark', () => { //... }) .add('my-group/my-benchmark-2', () => { //... }) .add('second-group/baseline', () => { //... }) .run(); ``` **Sample Output:** ``` System Information: Node.js: v22.15.0 OS: darwin 24.5.0 CPU: Apple M2 Benchmark results (3 total): Plugins enabled: V8NeverOptimizePlugin ├─ my-group │ ├─ my-benchmark 1,461,380 ops/sec (10 runs sampled) min..max=(682.30ns...685.79ns) │ └─ my-benchmark-2 2,270,973 ops/sec (10 runs sampled) min..max=(437.22ns...444.12ns) └─ second-group └─ baseline 637,787 ops/sec (11 runs sampled) min..max=(1.54us...1.59us) ``` ### Custom Reporter Customize data reporting by providing a `reporter` function when creating the `Suite`: ```js const { Suite } = require('bench-node'); function reporter(results) { for (const result of results) { console.log(`Benchmark: ${result.name}`); console.log(`Operations per second: ${result.opsSec}`); console.log(`Iterations: ${result.iterations}`); console.log(`Histogram: ${result.histogram}`); } } const suite = new Suite({ reporter }); suite.add('Using delete to remove property from object', () => { const data = { x: 1, y: 2, z: 3 }; delete data.y; data.x; data.y; data.z; }); suite.run(); ``` ```bash $ node --allow-natives-syntax my-benchmark.js Benchmark: Using delete to remove property from object - 6,032,212 ops/sec ``` ## Setup and Teardown Control the benchmark function's setup and teardown using the timer argument: ```js const { Suite } = require('bench-node'); const { readFileSync, writeFileSync, rmSync } = require('node:fs'); const suite = new Suite(); suite.add('readFileSync', (timer) => { const randomFile = Date.now(); const filePath = `./${randomFile}.txt`; writeFileSync(filePath, Math.random().toString()); timer.start(); readFileSync(filePath, 'utf8'); timer.end(); rmSync(filePath); }).run(); ``` For advanced setups, use the timer argument to start and end timing explicitly: ```js const { Suite } = require('bench-node'); const { readFileSync, writeFileSync, rmSync } = require('node:fs'); const suite = new Suite(); suite.add('readFileSync', (timer) => { const randomFile = Date.now(); const filePath = `./${randomFile}.txt`; writeFileSync(filePath, Math.random().toString()); timer.start(); for (let i = 0; i < timer.count; i++) { readFileSync(filePath, 'utf8'); } timer.end(timer.count); rmSync(filePath); }); suite.run(); ``` > [!WARNING] > When using the `timer`, the setup will also be deoptimized. > As a result, if you compare this approach with one that uses functions outside > the benchmark function, the results may not match. > See: [Deleting Properties Example](./examples/deleting-properties/node.js). Ensure you call `.start()` and `.end()` methods when using the timer argument, or an `ERR_BENCHMARK_MISSING_OPERATION` error will be thrown. ### Managed Benchmarks In regular benchmarks (when `timer` is not used), you run the benchmarked function in a loop, and the timing is managed implicitly. This means each iteration of the benchmarked function is measured directly. The downside is that optimizations like inlining or caching might affect the timing, especially for fast operations. Example: ```cjs suite.add('Using includes', () => { const text = 'text/html,...'; const r = text.includes('application/json'); }); ``` Here, `DoNotOptimize` is called inside the generated loop for regular benchmarks (assuming `V8NeverOptimizePlugin` is being used), and the benchmark function is also marked with `%NeverOptimizeFunction`. Together, these prevent V8 from optimizing the benchmark function and from treating the returned value as irrelevant. Managed benchmarks explicitly handle timing through `start()` and `end()` calls around the benchmarked code. This encapsulates the entire set of iterations in one timed block, which can result in tighter measurement with less overhead. However, it can lead to over-optimistic results, especially if the timer’s start and stop calls are placed outside of the loop, allowing V8 to over-optimize the entire block. Example: ```cjs suite.add('[Managed] Using includes', (timer) => { timer.start(); for (let i = 0; i < timer.count; i++) { const text = 'text/html,...'; const r = text.includes('application/json'); assert.ok(r); // Ensure the result is used so it doesn't get V8 optimized away } timer.end(timer.count); }); ``` In this case, `DoNotOptimize` is applied to the benchmark function's return value, outside the user-managed loop. It does not consume the local `r` value from each iteration. That's why `assert.ok(r)` has been used: it makes the per-iteration result observable inside the timed block. > [!NOTE] > V8 assumptions can change any time soon. Therefore, it's crucial to investigate > results between versions of V8/Node.js. ### Worker Threads > Stability: 1.0 (Experimental) `bench-node` provides experimental support for **Worker Threads**. When you set `useWorkers: true`, the library runs each benchmark in a separate worker thread, ensuring that one benchmark does not affect another. Usage is straightforward: ```cjs const suite = new Suite({ useWorkers: true, }); ``` ## Benchmark Modes `bench-node` supports multiple benchmarking modes to measure code performance in different ways. ### Operations Mode Operations mode (default) measures how many operations can be performed in a given timeframe. This is the traditional benchmarking approach that reports results in operations per second (ops/sec). This mode is best for: - Comparing relative performance between different implementations - Measuring throughput of small, fast operations - Traditional microbenchmarking Example output: ``` String concatenation x 12,345,678 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(81ns...85ns) ``` ### Time Mode Time mode measures the actual time taken to execute a function exactly once. This mode is useful when you want to measure the real execution time for operations that have a known, fixed duration. This mode is best for: - Costly operations where multiple instructions are executed in a single run - Benchmarking operations with predictable timing - Verifying performance guarantees for time-sensitive functions To use time mode, set the `benchmarkMode` option to `'time'` when creating a Suite: ```js const { Suite } = require('bench-node'); const timeSuite = new Suite({ benchmarkMode: 'time' // Enable time mode }); // Create a function that takes a predictable amount of time const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)); timeSuite.add('Async Delay 100ms', async () => { await delay(100); }); timeSuite.add('Sync Busy Wait 50ms', () => { const start = Date.now(); while (Date.now() - start < 50); }); // Optional: Run the benchmark multiple times with repeatSuite timeSuite.add('Quick Operation with 5 repeats', { repeatSuite: 5 }, () => { // This will run exactly once per repeat (5 times total) // and report the average time let x = 1 + 1; }); (async () => { await timeSuite.run(); })(); ``` In time mode, results include `totalTime` (in seconds) instead of `opsSec`. Example output: ``` Async Delay 100ms x 0.1003s (1 sample) v8-never-optimize=true Sync Busy Wait 50ms x 0.0502s (1 sample) v8-never-optimize=true Quick Operation with 5 repeats x 0.0000s (5 samples) v8-never-optimize=true ``` See [examples/time-mode.js](./examples/time-mode.js) for a complete example. ## Baseline Comparisons You can mark one benchmark as a baseline to compare all other benchmarks against it: ```js const { Suite } = require('bench-node'); const suite = new Suite(); suite .add('baseline', { baseline: true }, () => { // baseline implementation const arr = [1, 2, 3]; arr.includes(2); }) .add('alternative', () => { // alternative implementation const arr = [1, 2, 3]; arr.indexOf(2) !== -1; }); suite.run(); ``` Example output with baseline: ``` baseline x 52,832,865 ops/sec (10 runs sampled) min..max=(18.50ns...19.22ns) alternative x 53,550,219 ops/sec (11 runs sampled) min..max=(18.26ns...18.89ns) Summary (vs. baseline): baseline (baseline) alternative (1.01x faster) ``` ## Statistical Significance Testing (T-Test) > Stability: 1.0 (Experimental) When comparing benchmarks, especially on machines with high variance (cloud VMs, shared environments), raw ops/sec differences may not be meaningful. `bench-node` provides **Welch's t-test** to determine if performance differences are statistically significant. Welch's t-test is preferred over Student's t-test because it doesn't assume equal variances between the two samples, which is common in benchmark scenarios. ### Enabling T-Test Mode Enable t-test mode with `ttest: true`. This automatically sets `repeatSuite=30` to collect enough independent samples for reliable statistical analysis (per the Central Limit Theorem): ```js const { Suite } = require('bench-node'); const suite = new Suite({ ttest: true, // Enables t-test and auto-sets repeatSuite=30 }); suite .add('baseline', { baseline: true }, () => { let sum = 0; for (let i = 0; i < 100; i++) sum += i; }) .add('optimized', () => { let sum = (99 * 100) / 2; // Gauss formula }); suite.run(); ``` Example output: ``` T-Test Mode: Enabled (repeatSuite=30) baseline x 1,234,567 ops/sec (300 runs sampled) min..max=(810.05ns...812.45ns) optimized x 9,876,543 ops/sec (305 runs sampled) min..max=(101.23ns...102.87ns) Summary (vs. baseline): baseline (baseline) optimized (8.00x faster) *** Significance: * p<0.05, ** p<0.01, *** p<0.001 ``` The asterisks indicate significance level: - `***` = p < 0.001 (0.1% risk of false positive) - `**` = p < 0.01 (1% risk of false positive) - `*` = p < 0.05 (5% risk of false positive) - (no stars) = not statistically significant This helps identify when a benchmark shows a difference due to random variance vs. a real performance improvement. **How it works**: With `ttest: true`, each benchmark runs 30 times independently (via `repeatSuite=30`). The t-test compares the 30 ops/sec values from the baseline against the 30 ops/sec values from each test benchmark. This accounts for run-to-run variance within that benchmark session. **Note**: Running the entire benchmark suite multiple times may still show variance in absolute numbers due to system-level factors (CPU frequency scaling, thermal throttling, background processes). The t-test helps determine if differences are statistically significant within each benchmark session, but results can vary between separate benchmark runs due to changing system conditions. See also: [Fixing Inconclusive Tests](doc/Inconclusive.md). ### Direct API Usage You can also use the t-test utilities directly for custom analysis: ```js const { welchTTest, compareBenchmarks } = require('bench-node'); // Raw sample data from two benchmarks (e.g., timing samples in nanoseconds) const baseline = [100, 102, 99, 101, 100, 98, 103, 99, 100, 101]; const optimized = [50, 51, 49, 52, 50, 48, 51, 49, 50, 51]; // High-level comparison const result = compareBenchmarks(optimized, baseline, 0.05); console.log(result); // { // significant: true, // pValue: 0.00001, // confidence: '99.99%', // stars: '***', // difference: 'faster', // tStatistic: 45.2, // degreesOfFreedom: 17.8 // } // Low-level Welch's t-test const ttest = welchTTest(optimized, baseline); console.log(ttest); // { // tStatistic: 45.2, // degreesOfFreedom: 17.8, // pValue: 0.00001, // significant: true, // mean1: 50.1, // mean2: 100.3, // variance1: 1.43, // variance2: 2.23 // } ``` #### Interpreting Results - **`significant: true`** - The performance difference is statistically significant at the given alpha level - **`pValue`** - Probability that the observed difference occurred by chance (lower = more confident) - **`confidence`** - Confidence level (e.g., "99.95%" means 99.95% confident the difference is real) - **`stars`** - Visual indicator of significance: `'***'` (p<0.001), `'**'` (p<0.01), `'*'` (p<0.05), or `''` (not significant) - **`difference`** - Whether the first sample is `'faster'`, `'slower'`, or `'same'` as the second A common threshold is `alpha = 0.05` (95% confidence). If `pValue < alpha`, the difference is significant. ## Writing JavaScript Mistakes When working on JavaScript micro-benchmarks, it’s easy to forget that modern engines use multiple tiers of Just-In-Time (JIT) compilation and sometimes even entirely different optimizations. The results you get from a simple timing loop often aren’t representative of how your code will behave under real-world conditions, especially once the browser or runtime has adjusted for frequent function calls. Caching, tail call optimizations, and hidden class transformations can all distort your measurements, leading to overblown claims about performance improvements that might never materialize in production. That’s why **bench-node** was created—to provide a stable and consistent way to compare small snippets of code. By default, it tells V8 to never optimize the benchmark function with `%NeverOptimizeFunction(bench.fn)` and consumes the returned value with a non-optimized `DoNotOptimize` helper so the JIT compiler doesn’t remove dead code. However, even this approach can’t fully replicate real-world scenarios in which V8 optimizations and unpredictable workloads impact performance. Think of bench-node as a helpful tool for quick comparisons rather than a guarantee of what you’ll see in production. [build-img]:https://github.com/RafaelGSS/bench-node/actions/workflows/release.yml/badge.svg [build-url]:https://github.com/RafaelGSS/bench-node/actions/workflows/release.yml [downloads-img]:https://img.shields.io/npm/dt/bench-node [downloads-url]:https://www.npmtrends.com/bench-node [npm-img]:https://img.shields.io/npm/v/bench-node [npm-url]:https://www.npmjs.com/package/bench-node [issues-img]:https://img.shields.io/github/issues/RafaelGSS/bench-node [issues-url]:https://github.com/RafaelGSS/bench-node/issues ================================================ FILE: assets/README.md ================================================ # "Branding" ## Logo | Logo | Description | | --- | --- | | ![Logo](./logo.svg) | simple logo for readme | | ![Logo](./logo-text-light.svg) | logo with text for light backgrounds | | ![Logo](./logo-text-dark.svg) | logo with text for dark backgrounds | ## Other - **Font**: [Geist](https://fonts.google.com/specimen/Geist) - **Light Green**: `#84BA64` - **Dark Green**: `#2C682C` > The design of the assets was created by [@AugustinMauroy](https://github.com/AugustinMauroy). ================================================ FILE: biome.json ================================================ { "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", "organizeImports": { "enabled": true }, "linter": { "enabled": true, "rules": { "recommended": true, "suspicious": { "noExplicitAny": "off" }, "style": { "noParameterAssign": "off", "noUselessElse": "off" } } }, "formatter": { "enabled": true }, "files": { "ignore": [ "examples/*", "coverage/*", "test/plugin-api-doc.js", "package.json" ] } } ================================================ FILE: doc/Inconclusive.md ================================================ # Fixing Inconclusive Tests t-tests are looking at the distribution of both sets of results and trying to determine if they overlap in a way that makes the average value significant or just noise in the results. A run with a bimodal distribution for instance, caused by problems with the machine the tests are running on or the NodeJS runtime doing things in the background. Here are a few causes. ## Random Input Data Variability in the inputs between runs can lead to big changes in the runtime of an algorithm. Particularly with code that sorts, filters, or conditionally operates on input data, feeding them certain combinations of data will result in wildly different run times from one loop to the next or occasionally from one sample to the next. The Central Limit Theorem (that over a long enough time a situation will revert to the mean), does not invalidate the existence of the Gambler's Paradox (that it will revert to the mean before I become bankrupt). It is better to do your fuzzing in fuzz tests and pick representative data for your benchmarks. Partially informed by the results of your fuzz tests, and other bug reports. ## Underprovisioned VM, Oversubscribed hardware For a clean micro benchmark, we generally want to be the only one using the machine at the time. There are a number of known issues running benchmarks on machines that are thermally throttling, or on cheap VMs that use best-effort to allocate CPU time to the running processes. In particular, docker images with `cpu-shares` are especially poor targets for running benchmarks because the quota might expire for one timeslice in the middle of one test or between benchmarks in a single Suite. This creates an unfair advantage for the first test, and/or lots of noise in the results. We are currently investigating ways to detect this sort of noise, and analyzing if the t-tests are sufficient to do so. ## Epicycles in GC or JIT compilation If the warmup time is insufficient to get V8 to optimize the code, it may kick in during the middle of a sample, which will introduce a bimodal distribution of answers (before, and after). There is currently not a way to adjust the warmup time of `bench-node`, but should be added as a feature. One of the nastiest performance issues to detect in garbage collected code is allocation epicycles. This happens when early parts of a calculation create lots of temporary data but not sufficient to cross the incremental or full GC threshold, so that the next function in a call sequence routinely gets hit with exceeding the threshold. This is especially common in code that generates a JSON or HTML response to a series of calculations - it is the single biggest allocation in the sequence, but it gets blamed in the performance report for the lion's share of the CPU time. If you change the `minTime` up or down, that will alter the number of iterations per sample which may smooth out the results. You can also try increasing `minSamples` to get more samples. But also take this as a suggestion that your code may have a performance bug that is worth prioritizing. Forcing GC in the teardown or setup methods between subsequent tests in a single Suite may help with some situations when reordering the tests results in differences in runtime, but for the more general case, you may need to review the code under test to make it 'play well' both with benchmarking and in production systems. In production code, particularly where p9# values are used as a fitness test, it is sometimes better to chose the algorithm with more consistent runtime over the one with supposedly better average runtime. This can also be true where DDOS scenarios are possible - the attacker will always chose the worst, most assymetric request to send to your machine, and mean response time will not matter one whit. If `bench-node` is complaining, the problem may not be `bench-node`. ================================================ FILE: doc/Plugins.md ================================================ # Plugins The benchmark module supports a flexible plugin system that allows you to extend its functionality by adding custom plugins. This documentation explains how to create, validate, and use plugins within the benchmarking framework. [V8NeverOptimizePlugin](#class-v8neveroptimizeplugin) is enabled by default. To observe how a plugin is used, see the `plugin-api-doc.js` file in tests and explore its results. ## Structure Each plugin is expected to follow a specific structure with required methods for integration into the benchmark module. The plugins are required to define the following methods: * `isSupported()`: This method checks if the plugin can run in the current environment. If the plugin uses features specific to certain environments (e.g., V8 engine features), it should return `true` if those features are available and `false` otherwise. * `toString()`: This method should return a string representation of the plugin. It’s used for logging and error messages. In addition to these required methods, plugins can optionally define other methods based on their functionality, such as `beforeClockTemplate()`, `afterClockTemplate()`, `onCompleteBenchmark()`, and more. ## Plugin Methods ### `isSupported()` (required) This method checks if the plugin's functionality is available in the current environment. For instance, if a plugin uses specific V8 engine commands, this method ensures the environment supports them. ### `beforeClockTemplate(varNames)` * `varNames` {Object} * `bench` {string} - Name for the benchmark variable. * `context` {string} - Name for the context variable. * `timer` {string} - Name for the timer variable. * `awaitOrEmpty` {string} - A string with `await` or empty string (`''`). Some plugins need to modify or prepare the code before the benchmark starts. The `beforeClockTemplate()` method allows you to inject code before the timing process begins. This method must return an array where: * The first element is a string representing the JavaScript code to be executed before the benchmark function. * The second element (optional) is a string representing a function that will wrap the benchmark function. This wrapper is used to customize how the benchmark function is called during execution. The wrapped function provides a powerful way to manipulate how the benchmark is run without directly modifying the benchmark logic. ```js beforeClockTemplate({ bench }) { let code = ''; code += ` function DoNotOptimize(x) {} // Prevent the benchmark function and result consumer from optimizing or being inlined. %NeverOptimizeFunction(${bench}.fn); %NeverOptimizeFunction(DoNotOptimize); ` return [code, 'DoNotOptimize']; } ``` In this example, the plugin injects the `DoNotOptimize` function and also provides it as a wrapper for the benchmark function result. The benchmark function itself is marked with `%NeverOptimizeFunction(${bench}.fn)`, while the `DoNotOptimize` wrapper consumes the returned value so the benchmark expression does not become observationally irrelevant. These two protections address different parts of the generated code: `%NeverOptimizeFunction(${bench}.fn)` targets the function under test, and `DoNotOptimize(bench.fn())` targets the value returned by each call. ### `afterClockTemplate(varNames)` * `varNames` {Object} * `bench` {string} - Name for the benchmark variable. * `context` {string} - Name for the context variable. * `timer` {string} - Name for the timer variable. * `awaitOrEmpty` {string} - A string with `await` or empty string (`''`). After the benchmark runs, this method can inject code to gather performance data or reset configurations. It must return an array where: * The first element is a string containing the JavaScript code to be executed after the benchmark finishes. Unlike `beforeClockTemplate`, `afterClockTemplate` does not support a second element in the returned array, as it only runs cleanup or data collection code after the benchmark is executed. ### `onCompleteBenchmark(result)` * `result` {Object} * `duration` {number} - Benchmark duration * `count` {number} - Number of iterations * `context` {Object} - A object used to store results after the benchmark clock This method is called when the benchmark completes. Plugins can collect and process data from the benchmark results in this step. ### `toString()` (required) This method returns a string identifier for the plugin, typically the plugin’s name. It is used in error messages and logging. ## Example Plugins Here are examples of plugins that follow the required structure and functionality. ```js class V8OptimizeOnNextCallPlugin { isSupported() { try { new Function(`%OptimizeFunctionOnNextCall(() => {})`)(); return true; } catch (e) { return false; } } beforeClockTemplate({ awaitOrEmpty, bench }) { let code = ''; code += `%OptimizeFunctionOnNextCall(${ bench }.fn);\n`; code += `${ awaitOrEmpty }${ bench }.fn();\n`; code += `${ awaitOrEmpty }${ bench }.fn();\n`; return [code]; } toString() { return 'V8OptimizeOnNextCallPlugin'; } } ``` ## Official Plugins This is a list of official plugins that can be fetched when requiring `bench-node` module. ```js const { V8OptimizeOnNextCallPlugin, Suite } = require('bench-node'); const suite = new Suite({ plugins: [new V8OptimizeOnNextCallPlugin()], }) ``` ### Class: `V8OptimizeOnNextCallPlugin` The `V8OptimizeOnNextCallPlugin` triggers the V8 engine to optimize the function before it is called. This can improve performance in repeated benchmarks. ### Class: `V8NeverOptimizePlugin` The `V8NeverOptimizePlugin` prevents the V8 engine from optimizing or inlining the benchmark function. It also wraps the benchmark result in a non-optimized `DoNotOptimize` helper so V8 cannot treat an unused return value as irrelevant. ### Class: `V8GetOptimizationStatus` The `V8GetOptimizationStatus` plugin collects the V8 engine's optimization status for a given function after it has been benchmarked. ================================================ FILE: examples/.gitignore ================================================ ================================================ FILE: examples/benchmark-comparison/README.md ================================================ # Benchmark Comparison Framework This directory contains a framework for comparing `bench-node` against other popular benchmark libraries: - [Benchmark.js](https://benchmarkjs.com/) - [Mitata](https://github.com/evanwashere/mitata) - [Tinybench](https://github.com/tinylibs/tinybench) ## Test Cases The comparison focuses on four key test scenarios: 1. **JIT Optimizable** - Simple operations that the JavaScript JIT compiler might optimize away 2. **Regex Test** - Regular expression pattern matching operations 3. **Fibonacci** - CPU-bound recursive calculation ## Running the Comparisons To run any of the comparison scripts, use: ```bash node --allow-natives-syntax examples/benchmark-comparison/comparison.js ``` > **Note:** All bench-node scripts must be run with the `--allow-natives-syntax` flag. ================================================ FILE: examples/benchmark-comparison/comparison.js ================================================ const { Suite } = require('../../lib'); const Benchmark = require('benchmark'); const { bench, run: mitataRun } = require('mitata'); const { Bench } = require('tinybench'); const testSuites = { 'simple-operations': { name: 'Simple Operations', tests: { 'jit-optimizable': { name: 'JIT Optimizable', fn: function() { const x = 1; const y = 2; return x + y; } } } }, 'string-processing': { name: 'String Processing', tests: { 'regex-test': { name: 'Regex Test', fn: function() { const pattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; const emails = [ 'test@example.com', 'invalid-email', 'another.test@example.co.uk', 'john.doe123@sub.domain.com' ]; return emails.map(email => pattern.test(email)); } } } }, 'cpu-intensive': { name: 'CPU Intensive', tests: { 'fibonacci-10': { name: 'Fibonacci(10)', fn: function() { const n = 10; let a = 0, b = 1; for (let i = 2; i <= n; i++) { [a, b] = [b, a + b]; } return b; } }, 'fibonacci-30': { name: 'Fibonacci(30)', fn: function() { const n = 30; let a = 0, b = 1; for (let i = 2; i <= n; i++) { [a, b] = [b, a + b]; } return b; } }, 'fibonacci-40': { name: 'Fibonacci(40)', fn: function() { const n = 40; let a = 0, b = 1; for (let i = 2; i <= n; i++) { [a, b] = [b, a + b]; } return b; } }, 'fibonacci-recursive-10': { name: 'Fibonacci Recursive(10)', fn: function() { function fibRecursive(n) { if (n <= 1) return n; return fibRecursive(n - 1) + fibRecursive(n - 2); } return fibRecursive(10); } } } } }; function formatNumber(num) { return new Intl.NumberFormat().format(Math.round(num)); } async function runBenchNode() { console.log('\n🔍 Running bench-node tests... (detailed output hidden)'); // Save original console.log const originalConsoleLog = console.log; // Replace with no-op function console.log = () => {}; const results = []; try { for (const [suiteKey, suite] of Object.entries(testSuites)) { const benchSuite = new Suite({ name: suite.name }); Object.entries(suite.tests).forEach(([testKey, test]) => { benchSuite.add(test.name, test.fn); }); try { const suiteResults = await benchSuite.run(); // Collect results suiteResults.forEach(result => { // Extract ops/sec from the bench-node result object // The opsSec field contains the operations per second value let opsPerSec = 0; if (typeof result.opsSec === 'number') { // Direct extraction from the opsSec field opsPerSec = result.opsSec; } else if (typeof result.hz === 'number') { // Fallback to hz if available opsPerSec = result.hz; } else if (result.stats && typeof result.stats.mean === 'number') { // Mean is in nanoseconds, convert to ops/sec opsPerSec = 1_000_000_000 / result.stats.mean; } else if (typeof result.avg === 'number') { // Avg is in seconds, convert to ops/sec opsPerSec = 1 / result.avg; } results.push({ tool: 'bench-node', suite: suite.name, test: result.name, opsPerSec: opsPerSec, margin: result.rme || 0 }); }); } catch (error) { // Temporarily restore console.log for error reporting const tempConsole = console.log; console.log = originalConsoleLog; console.log(`Error running benchmark suite ${suite.name}: ${error.message}`); console.log = tempConsole; // Add placeholder results for this suite so we don't lose the comparison Object.entries(suite.tests).forEach(([testKey, test]) => { results.push({ tool: 'bench-node', suite: suite.name, test: test.name, opsPerSec: 0, margin: 0 }); }); } } } catch (error) { // Temporarily restore console.log for error reporting const tempConsole = console.log; console.log = originalConsoleLog; console.log(`Error in bench-node benchmarks: ${error.message}`); console.log = tempConsole; } // Restore original console.log console.log = originalConsoleLog; return results; } async function runBenchmarkJS() { console.log('\n🔍 Running benchmark.js tests... (detailed output hidden)'); // Save original console.log const originalConsoleLog = console.log; // Replace with no-op function console.log = () => {}; const results = []; try { for (const [suiteKey, suite] of Object.entries(testSuites)) { await new Promise((resolve) => { const benchSuite = new Benchmark.Suite(suite.name); Object.entries(suite.tests).forEach(([testKey, test]) => { benchSuite.add(test.name, test.fn); }); benchSuite .on('cycle', (event) => { // Collect result for each test results.push({ tool: 'benchmark.js', suite: suite.name, test: event.target.name, opsPerSec: event.target.hz, margin: event.target.stats.rme }); }) .on('complete', function() { resolve(); }) .run(); }); } } catch (error) { // Temporarily restore console.log for error reporting const tempConsole = console.log; console.log = originalConsoleLog; console.log(`Error in benchmark.js benchmarks: ${error.message}`); console.log = tempConsole; } // Restore original console.log console.log = originalConsoleLog; return results; } async function runMitata() { console.log('\n🔍 Running mitata tests... (detailed output hidden)'); // Save original console.log const originalConsoleLog = console.log; // Replace with no-op function console.log = () => {}; // Create a results collection const results = []; try { // Store test to name mappings for result collection const testMap = new Map(); for (const [suiteKey, suite] of Object.entries(testSuites)) { // Estimate performance based on test function execution time Object.entries(suite.tests).forEach(([testKey, test]) => { const fullName = `${suite.name}: ${test.name}`; bench(fullName, test.fn); // Create a test mapping testMap.set(fullName, { suite: suite.name, test: test.name }); // Do a simple performance estimate (not as accurate as real benchmark) // This is a fallback since we can't easily extract mitata results const start = process.hrtime.bigint(); const iterations = 1000; for (let i = 0; i < iterations; i++) { test.fn(); } const end = process.hrtime.bigint(); const avgNs = Number(end - start) / iterations; const estOpsPerSec = 1_000_000_000 / avgNs; // Add estimated result results.push({ tool: 'mitata', suite: suite.name, test: test.name, opsPerSec: estOpsPerSec, margin: 5 // Rough estimate of margin }); }); } // Run mitata benchmarks silently await mitataRun(); } catch (error) { // Temporarily restore console.log for error reporting const tempConsole = console.log; console.log = originalConsoleLog; console.log(`Error in mitata benchmarks: ${error.message}`); console.log = tempConsole; } // Restore original console.log console.log = originalConsoleLog; return results; } async function runTinyBench() { console.log('\n🔍 Running tinybench tests... (detailed output hidden)'); // Save original console.log const originalConsoleLog = console.log; // Replace with no-op function console.log = () => {}; const allResults = []; try { // Create benchmarks for each suite for (const [suiteKey, suite] of Object.entries(testSuites)) { const benchmark = new Bench(); // Add tests to suite Object.entries(suite.tests).forEach(([testKey, test]) => { benchmark.add(test.name, test.fn); }); // Run benchmarks await benchmark.run(); // Format and collect results const suiteResults = benchmark.tasks.map(({ name, result }) => ({ name, ops: result?.hz || 0, margin: result?.rme ? `±${result?.rme.toFixed(2)}%` : 'N/A' })); suiteResults.forEach(result => { // Extract numeric margin value from the margin string (e.g., '±1.20%' -> 1.20) let marginValue = 0; if (result.margin && result.margin !== 'N/A') { const match = result.margin.match(/[0-9.]+/); if (match) { marginValue = parseFloat(match[0]); } } allResults.push({ tool: 'tinybench', suite: suite.name, test: result.name, opsPerSec: result.ops, margin: marginValue }); }); } } catch (error) { // Temporarily restore console.log for error reporting const tempConsole = console.log; console.log = originalConsoleLog; console.log(`Error in tinybench benchmarks: ${error.message}`); console.log = tempConsole; } // Restore original console.log console.log = originalConsoleLog; return allResults; } function compareResults(results) { console.log('\n📈 BENCHMARK COMPARISON SUMMARY'); console.log('============================'); // Group results by suite and test const resultsBySuiteAndTest = {}; results.forEach(result => { // Ensure we have valid ops/sec values for comparison if (isNaN(result.opsPerSec) || result.opsPerSec === undefined || result.opsPerSec === null) { result.opsPerSec = 0; } const key = `${result.suite} - ${result.test}`; if (!resultsBySuiteAndTest[key]) { resultsBySuiteAndTest[key] = []; } resultsBySuiteAndTest[key].push(result); }); // For each test case, compare across tools Object.entries(resultsBySuiteAndTest).forEach(([testCase, testResults]) => { console.log(`\n📏 Test Case: ${testCase}`); const comparisonTable = testResults.map(result => ({ 'Tool': result.tool, 'ops/sec': formatNumber(result.opsPerSec || 0), 'margin': result.margin ? `±${result.margin.toFixed(2)}%` : 'N/A', 'relative': '1x' })); // Find the fastest tool for this test (with valid non-zero result) const validResults = testResults.filter(r => r.opsPerSec > 0); const fastest = validResults.length > 0 ? validResults.reduce((max, result) => result.opsPerSec > max.opsPerSec ? result : max, validResults[0]) : { opsPerSec: 1 }; // Calculate relative performance comparisonTable.forEach(row => { const result = testResults.find(r => r.tool === row['Tool']); if (result && result.opsPerSec > 0 && fastest.opsPerSec > 0) { const relative = result.opsPerSec / fastest.opsPerSec; row['relative'] = `${relative.toFixed(2)}x`; } else { row['relative'] = 'N/A'; } }); // Sort by performance (descending) comparisonTable.sort((a, b) => { const aResult = testResults.find(r => r.tool === a['Tool']); const bResult = testResults.find(r => r.tool === b['Tool']); const aSpeed = aResult && aResult.opsPerSec > 0 ? aResult.opsPerSec : 0; const bSpeed = bResult && bResult.opsPerSec > 0 ? bResult.opsPerSec : 0; return bSpeed - aSpeed; }); console.table(comparisonTable); }); } async function main() { console.log('🚀 BENCHMARK COMPARISON'); console.log('======================'); console.log('Comparing: bench-node, benchmark.js, mitata, tinybench\n'); try { const results = []; // Run all benchmarks and collect results const benchNodeResults = await runBenchNode(); const benchmarkJsResults = await runBenchmarkJS(); const mitataResults = await runMitata(); const tinyBenchResults = await runTinyBench(); // Combine all results results.push(...benchNodeResults); results.push(...benchmarkJsResults); results.push(...mitataResults); results.push(...tinyBenchResults); // Compare results across all tools compareResults(results); console.log('\n✅ All benchmarks completed successfully!'); } catch (error) { console.error('❌ Error running benchmarks:', error); } } main(); ================================================ FILE: examples/chart-report/node.js ================================================ const { Suite, chartReport } = require('../../lib'); const assert = require('node:assert'); async function runSuiteOne() { const suite = new Suite({ reporter: chartReport, }); await suite .add('test 1', function () { const pattern = /[123]/g }) .add('test 2', function () { const subject = '123123123123123123123123123123123123123123123123' const r = subject.replace(/1/g, 'a').replace(/2/g, 'b').replace(/3/g, 'c') }) .run(); } async function runSuiteTwo() { const suite = new Suite({ reporter: chartReport, reporterOptions: { printHeader: false } }); await suite .add('test 3', function () { const pattern = /[123]/g }) .add('test 4', function () { const subject = '123123123123123123123123123123123123123123123123' const r = subject.replace(/1/g, 'a').replace(/2/g, 'b').replace(/3/g, 'c') assert.ok(r) }) .run(); } async function main() { await runSuiteOne() process.stdout.write('\n\n') await runSuiteTwo() } main() ================================================ FILE: examples/create-uint32array/node.js ================================================ const { Suite } = require('../../lib'); const suite = new Suite(); suite .add(`new Uint32Array(1024)`, { baseline: true }, function () { return new Uint32Array(1024); }) .add(`new Uint32Array(1024) with 10 repetitions`, {repeatSuite: 10}, function () { return new Uint32Array(1024); }) .add(`new Uint32Array(1024) with min samples of 50`, {minSamples: 50}, function() { return new Uint32Array(1024); }) .add(`[Managed] new Uint32Array(1024)`, function (timer) { const assert = require('node:assert'); let r; timer.start(); for (let i = 0; i < timer.count; i++) { r = new Uint32Array(1024); } timer.end(timer.count); assert.ok(r); }) .run(); ================================================ FILE: examples/create-uint32array/node.js.log ================================================ new Uint32Array(1024) x 2,930,763 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(332.08ns ... 355.52ns) p75=344.36ns p99=355.52ns [Managed] new Uint32Array(1024) x 2,934,651 ops/sec (10 runs sampled) v8-never-optimize=true min..max=(335.94ns ... 357.76ns) p75=340.16ns p99=357.76ns ---------------------------------------------------------------------------- new Uint32Array(1024) x 2,967,727 ops/sec (9 runs sampled) v8-never-optimize=true min..max=(330.75ns ... 337.55ns) p75=334.98ns p99=337.55ns [Managed] new Uint32Array(1024) x 3,029,150 ops/sec (10 runs sampled) v8-never-optimize=true min..max=(326.51ns ... 341.28ns) p75=331.01ns p99=341.28ns ---------------------------------------------------------------------------- new Uint32Array(1024) x 2,920,851 ops/sec (12 runs sampled) v8-never-optimize=true min..max=(332.88ns ... 363.08ns) p75=348.19ns p99=363.08ns [Managed] new Uint32Array(1024) x 3,032,256 ops/sec (12 runs sampled) v8-never-optimize=true min..max=(327.13ns ... 343.75ns) p75=329.19ns p99=343.75ns ---------------------------------------------------------------------------- ================================================ FILE: examples/crypto-verify/node.js ================================================ const crypto = require('node:crypto'); const { readFileSync } = require('fs'); const { Suite } = require('../../lib'); const rsaPrivateKey = readFileSync(`${__dirname}/private-key.pem`, 'utf-8'); const rsaPublicKey = readFileSync(`${__dirname}/public-key.pem`, 'utf-8'); const thing = 'hello world'; const algorithm = 'RSA-SHA256'; const signature = crypto.createSign(algorithm).update(thing).sign(rsaPrivateKey, 'base64'); const suite = new Suite(); suite .add(`crypto.createVerify('${algorithm}')`, function () { var verifier = crypto.createVerify(algorithm); verifier.update(thing); verifier.verify(rsaPublicKey, signature, 'base64'); }) .add(`crypto.verify('${algorithm}')`, function () { crypto.verify(algorithm, thing, rsaPublicKey, Buffer.from(signature, 'base64')); }) .add(`[Managed] crypto.createVerify('RSA-SHA256')`, function (timer) { const crypto = require('node:crypto'); const { readFileSync } =require('node:fs'); const assert = require('node:assert'); const rsaPrivateKey = readFileSync(`${__dirname}/private-key.pem`, 'utf-8'); const rsaPublicKey = readFileSync(`${__dirname}/public-key.pem`, 'utf-8'); const thing = 'hello world' const signature = crypto.createSign('RSA-SHA256').update(thing).sign(rsaPrivateKey, 'base64') let verifier; timer.start(); for (let i = 0; i < timer.count; i++) { verifier = crypto.createVerify('RSA-SHA256') verifier.update(thing) verifier.verify(rsaPublicKey, signature, 'base64') } timer.end(timer.count); assert.ok(verifier); }) .add(`[Managed] crypto.verify('RSA-SHA256')`, function (timer) { const crypto = require('node:crypto'); const { readFileSync } = require('node:fs'); const assert = require('node:assert'); const rsaPrivateKey = readFileSync(`${__dirname}/private-key.pem`, 'utf-8'); const rsaPublicKey = readFileSync(`${__dirname}/public-key.pem`, 'utf-8'); const thing = 'hello world' const signature = crypto.createSign('RSA-SHA256').update(thing).sign(rsaPrivateKey, 'base64') let r; timer.start(); for (let i = 0; i < timer.count; i++) { r = crypto.verify('RSA-SHA256', thing, rsaPublicKey, Buffer.from(signature, 'base64')); } timer.end(timer.count); assert.ok(r); }) .run(); ================================================ FILE: examples/crypto-verify/node.js.log ================================================ crypto.createVerify('RSA-SHA256') x 10,925 ops/sec (12 runs sampled) v8-never-optimize=true min..max=(88.50us ... 93.51us) p75=91.54us p99=93.51us crypto.verify('RSA-SHA256') x 10,988 ops/sec (9 runs sampled) v8-never-optimize=true min..max=(90.93us ... 91.84us) p75=91.58us p99=91.84us [Managed] crypto.createVerify('RSA-SHA256') x 10,520 ops/sec (9 runs sampled) v8-never-optimize=true min..max=(88.44us ... 92.23us) p75=91.47us p99=92.23us [Managed] crypto.verify('RSA-SHA256') x 10,948 ops/sec (9 runs sampled) v8-never-optimize=true min..max=(89.98us ... 92.36us) p75=91.49us p99=92.36us ---------------------------------------------------------------------------- crypto.createVerify('RSA-SHA256') x 10,807 ops/sec (8 runs sampled) v8-never-optimize=true min..max=(91.66us ... 92.58us) p75=92.23us p99=92.58us crypto.verify('RSA-SHA256') x 10,964 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(88.95us ... 92.46us) p75=91.83us p99=92.46us [Managed] crypto.createVerify('RSA-SHA256') x 10,954 ops/sec (10 runs sampled) v8-never-optimize=true min..max=(90.08us ... 92.88us) p75=91.72us p99=92.88us [Managed] crypto.verify('RSA-SHA256') x 10,990 ops/sec (9 runs sampled) v8-never-optimize=true min..max=(90.31us ... 92.43us) p75=91.43us p99=92.43us ---------------------------------------------------------------------------- crypto.createVerify('RSA-SHA256') x 10,890 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(87.88us ... 94.08us) p75=92.96us p99=94.08us crypto.verify('RSA-SHA256') x 10,906 ops/sec (9 runs sampled) v8-never-optimize=true min..max=(91.03us ... 92.45us) p75=91.66us p99=92.45us [Managed] crypto.createVerify('RSA-SHA256') x 11,050 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(87.94us ... 92.09us) p75=91.88us p99=92.09us [Managed] crypto.verify('RSA-SHA256') x 10,990 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(90.29us ... 92.13us) p75=91.92us p99=92.13us ---------------------------------------------------------------------------- ================================================ FILE: examples/crypto-verify/private-key.pem ================================================ -----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAz7QweYwr7puNtc8/6jf4pADXfY2/HVq7LgI7JXkAwW5HTEjU Rvu02SYC7n/ylr86Da4hcAEAFBC2baVh7J0EnLo6D4FA3yV4s5zj7tqL+p2CdniY MDEny+uxS/9eRuLf1RIYHjSVB6wOoU4lg9FtDOQA8W09ZDLvc3/4ZSwEf9O2J5Vm qcSGaPt35eIEBS0iIMiOUCGqbUcVMZkp4JO7I65HOK/g0Scvb1LLY6f4qDsThqyi ID/NXM5LtjnztRP+9dCLT9DdwC+LeU2te6vcEwC2bPYs3nUwkZL4qqIWO6GHaavn gFKSmbmc9muVAPmYW/ffMtznKZaWCp9Li0JGuQIDAQABAoIBAQC7OI7hYSpQgEKy eUgBlcY3/tI/SD/W8+v5QuWRl4rI0ODPsG44NbcEbbECzq4al/B6WFWnoh8x9waZ uxOTts1rgKnJRBb3jc1JCcijirfWhZgNthJojkZzF9bOzDds6iAc7Zxzza3wJnVh jRFfyqzji7oV5QQLh6YzlEyQ1aaQmOKTkCDV1UDetI8iItUvq3i8EwsFmtfvXjTl aymL/xSd3R6w3WDEd8PS5ZPDoFmkt2h/4IhOo6bVXLwWBWqcopRAvzGQrc806c2l jjePwk9fddIG+vIHc8DHqaC/WzMR6iBt3K7Q9Eyu4k97qeOxB5TIEVlhxg2DJON9 DlGtInXpAoGBAPje3L0xL441i7kh7GqiWtXiDgxVwdPmN5p7n755MddSxvu8894h T9/LxwAVcpolXANgyRtrl5bu/gXgznKefmVr976BzG2DY33PESf5FnHZAmixWSfz VOdfVlz+Bk/BI+nVwmeT0+8NSHJ6TJ/9NOSzI9ctUceszIHG5/Gfc2wHAoGBANWn bHNT7aphFqyUMfWyc6+h8Radq/RVMrOGeD2wRRqUTeb2Ydbx2OwB+gcwnlbsQjfS BYAa3kn1z5K5kOigB9qu6x7zuM9SMReKIlMg3NpYVyKKj+JaNF3YArFa5TWy0B26 wNJ0FiiZyPq4eP5hPwM+Bed4ytIRWa54P5GeRYc/AoGAWT8ijb4rvaW6G4Ps0jiy tmzAeO/v+FtgqUeX+6helUccEH6sPYZYrHrZPFB0ro6jNprow6qLzBacheMeZcAs t5ZGW80UUFmDvkQZdOpAgEdAM+cVf9wlIGvx/psiDEvI4zxC4P4ETH/I8TSmceFN rI4JVkrsPtza4ddAqkdyDtUCgYAaoMs7dHJikccpqy6u2JbihORvVSdhRF0VUuUZ iyaRsXokFwEKsQnAIF7xFnYljzyRiHN3C+I4hZJhTw9obsmLz9EuAmI+NJg5vtWY Vrgv3mK9w1c7dtKf/5QWVqXKk4asreHqWN2KIeCSnvs1eRlJZimGN9/PXqo2vHXv yDISMQKBgE22EhVB99rEsLXDCXOUHIFGpW46ZavQc4DLXg+uIQm3y0WWyc0oyNF8 9kr1luu5QI5PV9mWpSKa3SKmc74biyxhlUp/DYuMyuSXpdDegMAKheAHVXP9ToU/ 5qhq4HBcISN3CwSAYcFcbqC3cB07T+IwX6NTEz4zel4gty8t/tsM -----END RSA PRIVATE KEY----- ================================================ FILE: examples/crypto-verify/public-key.pem ================================================ -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz7QweYwr7puNtc8/6jf4 pADXfY2/HVq7LgI7JXkAwW5HTEjURvu02SYC7n/ylr86Da4hcAEAFBC2baVh7J0E nLo6D4FA3yV4s5zj7tqL+p2CdniYMDEny+uxS/9eRuLf1RIYHjSVB6wOoU4lg9Ft DOQA8W09ZDLvc3/4ZSwEf9O2J5VmqcSGaPt35eIEBS0iIMiOUCGqbUcVMZkp4JO7 I65HOK/g0Scvb1LLY6f4qDsThqyiID/NXM5LtjnztRP+9dCLT9DdwC+LeU2te6vc EwC2bPYs3nUwkZL4qqIWO6GHaavngFKSmbmc9muVAPmYW/ffMtznKZaWCp9Li0JG uQIDAQAB -----END PUBLIC KEY----- ================================================ FILE: examples/csv-report/node.js ================================================ const { Suite } = require('../../lib'); const assert = require('node:assert'); const suite = new Suite(); suite .add('single with matcher', function () { const pattern = /[123]/g const replacements = { 1: 'a', 2: 'b', 3: 'c' } const subject = '123123123123123123123123123123123123123123123123' const r = subject.replace(pattern, m => replacements[m]) assert.ok(r); }) .add('multiple replaces', function () { const subject = '123123123123123123123123123123123123123123123123' const r = subject.replace(/1/g, 'a').replace(/2/g, 'b').replace(/3/g, 'c') assert.ok(r); }) .run(); ================================================ FILE: examples/dce-detection/example.js ================================================ const { Suite } = require("../../lib/index"); // Enable DCE detection to catch benchmarks that may be optimized away const suite = new Suite({ detectDeadCodeElimination: true, }); // Example 1: Likely to trigger DCE warning - result not used suite.add("simple addition (likely DCE)", () => { const result = 1 + 1; // result is never used - JIT might optimize this away }); // Example 2: Result is used - should not trigger warning suite.add("simple addition (used)", () => { const result = 1 + 1; if (result !== 2) throw new Error("Unexpected result"); }); // Example 3: Array creation without use - likely DCE suite.add("array creation (likely DCE)", () => { const arr = new Array(100); // arr is never accessed - might be optimized away }); // Example 4: Array creation with access - should not trigger warning suite.add("array creation (accessed)", () => { const arr = new Array(100); arr[0] = 1; // Using the array }); // Example 5: Object creation without use - likely DCE suite.add("object creation (likely DCE)", () => { const obj = { x: 1, y: 2, z: 3 }; // obj is never accessed }); // Example 6: More realistic computation suite.add("string operations", () => { const str1 = "hello"; const str2 = "world"; const result = str1 + " " + str2; if (!result.includes("hello")) throw new Error("Missing hello"); }); suite.run(); ================================================ FILE: examples/dce-detection/with-dce-disabled.js ================================================ const { Suite } = require("../../lib/index"); // Default behavior - DCE detection is disabled, V8NeverOptimizePlugin is used // You don't need to set detectDeadCodeElimination: false explicitly const suite = new Suite(); // These benchmarks will run with V8NeverOptimizePlugin, so they'll be slower // but more deterministic and won't be optimized away suite.add("simple addition", () => { const result = 1 + 1; }); suite.add("array creation", () => { const arr = new Array(100); }); suite.run(); ================================================ FILE: examples/dce-detection/without-never-optimize.js ================================================ const { Suite } = require("../../lib/index"); // Enable DCE detection - this automatically disables V8NeverOptimizePlugin // In this mode, V8 optimizations occur naturally and DCE warnings help identify issues const suite = new Suite({ detectDeadCodeElimination: true, }); // Example 1: Likely to trigger DCE warning - result not used suite.add("simple addition (likely DCE)", () => { const result = 1 + 1; // result is never used - JIT will optimize this away }); // Example 2: Result is used - should not trigger warning suite.add("simple addition (used)", () => { const result = 1 + 1; if (result !== 2) throw new Error("Unexpected result"); }); // Example 3: Array creation without use - likely DCE suite.add("array creation (likely DCE)", () => { const arr = new Array(100); // arr is never accessed - will be optimized away }); // Example 4: Array creation with access - should not trigger warning suite.add("array creation (accessed)", () => { const arr = new Array(100); arr[0] = 1; // Using the array }); // Example 5: More realistic computation that takes time suite.add("actual work", () => { let sum = 0; for (let i = 0; i < 100; i++) { sum += Math.sqrt(i); } if (sum < 0) throw new Error("Impossible"); }); suite.run(); ================================================ FILE: examples/deleting-properties/node.js ================================================ const { Suite } = require('../../lib'); const suite = new Suite(); const NullObject = function NullObject() { } NullObject.prototype = Object.create(null); %NeverOptimizeFunction(NullObject); suite .add('Using delete property', function () { const data = { x: 1, y: 2, z: 3 } delete data.y data.x data.y data.z }) .add('Using delete property (proto: null)', function () { const data = { __proto__: null, x: 1, y: 2, z: 3 } delete data.y data.x data.y data.z }) .add('Using delete property (cached proto: null)', function () { const data = new NullObject() data.x = 1 data.y = 2 data.z = 3 delete data.y data.x data.y data.z }) .add('Using undefined assignment', function () { const data = { x: 1, y: 2, z: 3 } data.y = undefined data.x data.y data.z }) .add('Using undefined assignment (proto: null)', function () { const data = { __proto__: null, x: 1, y: 2, z: 3 } data.y = undefined data.x data.y data.z }) .add('Using undefined property (cached proto: null)', function () { const data = new NullObject() data.x = 1 data.y = 2 data.z = 3 data.y = undefined data.x data.y data.z }) .add('[Managed] Using undefined property (cached proto: null)', function (t) { const NullObject = function () { } NullObject.prototype = Object.create(null) t.start(); for (let i = 0; i < t.count; i++) { const data = new NullObject() data.x = 1 data.y = 2 data.z = 3 data.y = undefined data.x data.y data.z } t.end(t.count); }) .run(); ================================================ FILE: examples/deleting-properties/node.js.log ================================================ Using delete property x 7,763,866 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(128.11ns ... 129.99ns) p75=129.03ns p99=129.99ns Using delete property (proto: null) x 22,946,068 ops/sec (10 runs sampled) v8-never-optimize=true min..max=(43.13ns ... 44.11ns) p75=43.74ns p99=44.11ns Using delete property (cached proto: null) x 7,331,951 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(135.64ns ... 137.39ns) p75=136.52ns p99=137.39ns Using undefined assignment x 133,449,849 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(7.45ns ... 7.56ns) p75=7.53ns p99=7.56ns Using undefined assignment (proto: null) x 25,432,632 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(38.72ns ... 40.06ns) p75=39.60ns p99=40.06ns Using undefined property (cached proto: null) x 58,871,059 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(16.92ns ... 17.12ns) p75=17.01ns p99=17.12ns [Managed] Using undefined property (cached proto: null) x 35,463,712 ops/sec (10 runs sampled) v8-never-optimize=true min..max=(28.05ns ... 28.43ns) p75=28.25ns p99=28.43ns ---------------------------------------------------------------------------- Using delete property x 6,669,856 ops/sec (10 runs sampled) v8-never-optimize=true min..max=(149.14ns ... 151.02ns) p75=150.12ns p99=151.02ns Using delete property (proto: null) x 15,242,131 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(64.77ns ... 66.38ns) p75=66.25ns p99=66.38ns Using delete property (cached proto: null) x 5,920,843 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(167.87ns ... 170.33ns) p75=169.07ns p99=170.33ns Using undefined assignment x 78,647,245 ops/sec (12 runs sampled) v8-never-optimize=true min..max=(12.62ns ... 12.83ns) p75=12.70ns p99=12.83ns Using undefined assignment (proto: null) x 16,278,910 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(60.79ns ... 62.25ns) p75=61.57ns p99=62.25ns Using undefined property (cached proto: null) x 27,262,884 ops/sec (10 runs sampled) v8-never-optimize=true min..max=(36.54ns ... 36.65ns) p75=36.62ns p99=36.65ns [Managed] Using undefined property (cached proto: null) x 28,068,785 ops/sec (12 runs sampled) v8-never-optimize=true min..max=(35.07ns ... 35.79ns) p75=35.67ns p99=35.79ns ---------------------------------------------------------------------------- Using delete property x 7,632,095 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(129.63ns ... 132.85ns) p75=131.19ns p99=132.85ns Using delete property (proto: null) x 23,040,357 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(43.06ns ... 44.20ns) p75=43.56ns p99=44.20ns Using delete property (cached proto: null) x 7,222,103 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(137.05ns ... 139.88ns) p75=139.18ns p99=139.88ns Using undefined assignment x 133,133,677 ops/sec (10 runs sampled) v8-never-optimize=true min..max=(7.49ns ... 7.53ns) p75=7.52ns p99=7.53ns Using undefined assignment (proto: null) x 25,217,884 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(39.24ns ... 40.48ns) p75=39.89ns p99=40.48ns Using undefined property (cached proto: null) x 58,899,972 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(16.94ns ... 17.03ns) p75=16.99ns p99=17.03ns [Managed] Using undefined property (cached proto: null) x 34,904,120 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(28.34ns ... 29.89ns) p75=28.84ns p99=29.89ns ---------------------------------------------------------------------------- ================================================ FILE: examples/empty/node.js ================================================ const { Suite } = require('../../lib'); const suite = new Suite(); suite .add(`empty`, function () {}) .add(`empty async`, async function () {}) .run(); ================================================ FILE: examples/empty/node.js.log ================================================ empty x 218,984,985 ops/sec (12 runs sampled) v8-never-optimize=true min..max=(4.53ns ... 4.66ns) p75=4.58ns p99=4.66ns empty async x 20,898,403 ops/sec (9 runs sampled) v8-never-optimize=true min..max=(47.46ns ... 48.57ns) p75=48.04ns p99=48.57ns ---------------------------------------------------------------------------- empty x 220,571,877 ops/sec (12 runs sampled) v8-never-optimize=true min..max=(4.49ns ... 4.61ns) p75=4.58ns p99=4.61ns empty async x 20,925,022 ops/sec (14 runs sampled) v8-never-optimize=true min..max=(37.43ns ... 48.60ns) p75=48.03ns p99=48.60ns ---------------------------------------------------------------------------- empty x 218,923,690 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(4.52ns ... 4.64ns) p75=4.59ns p99=4.64ns empty async x 20,792,931 ops/sec (12 runs sampled) v8-never-optimize=true min..max=(42.94ns ... 49.45ns) p75=48.54ns p99=49.45ns ---------------------------------------------------------------------------- ================================================ FILE: examples/fs-read-async/node.js ================================================ const { Suite } = require('../../lib'); const { readFile } = require('node:fs/promises'); const { resolve } = require('node:path'); const suite = new Suite(); const sampleFile = resolve(__dirname, 'sample-file.txt'); suite .add('readFile', async function () { const r = await readFile(sampleFile); }) .add('readFile utf-8', async function () { const r = await readFile(sampleFile, 'utf-8'); }) .add('[managed] readFile', async function (timer) { const { readFile } = require('node:fs/promises'); const { resolve } = require('node:path'); const assert = require('node:assert'); const sampleFile = resolve(__dirname, 'sample-file.txt'); let r; timer.start(); for (let i = 0; i < timer.count; i++) { r = await readFile(sampleFile); } timer.end(timer.count); assert.ok(r); }) .add('[managed] readFile utf-8', async function (timer) { const { readFile } = require('node:fs/promises'); const { resolve } = require('node:path'); const assert = require('node:assert'); const sampleFile = resolve(__dirname, 'sample-file.txt'); let r; timer.start(); for (let i = 0; i < timer.count; i++) { r = await readFile(sampleFile, 'utf-8'); } timer.end(timer.count); assert.ok(r); }) .run(); ================================================ FILE: examples/fs-read-async/node.js.log ================================================ readFile x 23,167 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(42.59us ... 43.59us) p75=43.42us p99=43.59us readFile utf-8 x 23,021 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(43.14us ... 43.92us) p75=43.64us p99=43.92us [managed] readFile x 23,386 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(42.63us ... 43.04us) p75=42.89us p99=43.04us [managed] readFile utf-8 x 23,302 ops/sec (10 runs sampled) v8-never-optimize=true min..max=(42.76us ... 43.30us) p75=43.01us p99=43.30us ---------------------------------------------------------------------------- readFile x 23,177 ops/sec (10 runs sampled) v8-never-optimize=true min..max=(42.70us ... 43.54us) p75=43.31us p99=43.54us readFile utf-8 x 23,155 ops/sec (10 runs sampled) v8-never-optimize=true min..max=(42.34us ... 43.53us) p75=43.15us p99=43.53us [managed] readFile x 23,538 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(42.22us ... 42.82us) p75=42.59us p99=42.82us [managed] readFile utf-8 x 23,458 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(42.11us ... 43.20us) p75=42.86us p99=43.20us ---------------------------------------------------------------------------- readFile x 23,235 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(42.73us ... 43.41us) p75=43.17us p99=43.41us readFile utf-8 x 23,130 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(42.90us ... 44.40us) p75=43.62us p99=44.40us [managed] readFile x 23,490 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(42.40us ... 42.89us) p75=42.66us p99=42.89us [managed] readFile utf-8 x 23,553 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(41.99us ... 42.81us) p75=42.58us p99=42.81us ---------------------------------------------------------------------------- ================================================ FILE: examples/fs-read-async/node.managed.js ================================================ const { Suite } = require('../../lib'); const suite = new Suite(); suite .add('readFile', async function (timer) { const { readFile } = require('node:fs/promises'); const { resolve } = require('node:path'); const assert = require('node:assert'); const sampleFile = resolve(__dirname, 'sample-file.txt'); let r; timer.start(); for (let i = 0; i < timer.count; i++) { r = await readFile(sampleFile); } timer.end(timer.count); assert.ok(r); }) .add('readFile utf-8', async function (timer) { const { readFile } = require('node:fs/promises'); const { resolve } = require('node:path'); const assert = require('node:assert'); const sampleFile = resolve(__dirname, 'sample-file.txt'); let r; timer.start(); for (let i = 0; i < timer.count; i++) { r = await readFile(sampleFile, 'utf-8'); } timer.end(timer.count); assert.ok(r); }) .run(); ================================================ FILE: examples/fs-read-async/node.managed.js.log ================================================ readFile x 23,066 ops/sec (10 runs sampled) v8-never-optimize=true min..max=(42.59us ... 43.63us) p75=43.20us p99=43.63us readFile utf-8 x 23,334 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(42.21us ... 43.70us) p75=42.96us p99=43.70us ---------------------------------------------------------------------------- readFile x 23,145 ops/sec (10 runs sampled) v8-never-optimize=true min..max=(42.64us ... 43.18us) p75=43.04us p99=43.18us readFile utf-8 x 23,324 ops/sec (10 runs sampled) v8-never-optimize=true min..max=(42.54us ... 43.80us) p75=42.93us p99=43.80us ---------------------------------------------------------------------------- readFile x 23,124 ops/sec (10 runs sampled) v8-never-optimize=true min..max=(42.36us ... 43.30us) p75=42.99us p99=43.30us readFile utf-8 x 23,277 ops/sec (12 runs sampled) v8-never-optimize=true min..max=(41.50us ... 43.88us) p75=42.98us p99=43.88us ---------------------------------------------------------------------------- ================================================ FILE: examples/fs-read-async/sample-file.txt ================================================ hello ================================================ FILE: examples/fs-read-sync/node.js ================================================ const { Suite } = require('../../lib'); const { readFileSync } = require('node:fs'); const { resolve } = require('node:path'); const suite = new Suite(); const sampleFile = resolve(__dirname, 'sample-file.txt'); suite .add('readFileSync', function () { const r = readFileSync(sampleFile); }) .add('readFileSync utf-8', function () { const r = readFileSync(sampleFile, 'utf-8'); }) .add('[Managed] readFileSync', function (timer) { const { readFileSync } = require('node:fs'); const { resolve } = require('node:path'); const assert = require('node:assert'); const sampleFile = resolve(__dirname, 'sample-file.txt'); let r; timer.start(); for (let i = 0; i < timer.count; i++) { r = readFileSync(sampleFile); } timer.end(timer.count); assert.ok(r); }) .add('[Managed] readFileSync utf-8', function (timer) { const { readFileSync } = require('node:fs'); const { resolve } = require('node:path'); const assert = require('node:assert'); const sampleFile = resolve(__dirname, 'sample-file.txt'); let r; timer.start(); for (let i = 0; i < timer.count; i++) { r = readFileSync(sampleFile, 'utf-8'); } timer.end(timer.count); assert.ok(r); }) .run(); ================================================ FILE: examples/fs-read-sync/node.js.log ================================================ readFileSync x 198,828 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(5.01us ... 5.06us) p75=5.04us p99=5.06us readFileSync utf-8 x 203,828 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(4.88us ... 4.94us) p75=4.92us p99=4.94us [Managed] readFileSync x 196,632 ops/sec (10 runs sampled) v8-never-optimize=true min..max=(5.05us ... 5.08us) p75=5.07us p99=5.08us [Managed] readFileSync utf-8 x 203,265 ops/sec (9 runs sampled) v8-never-optimize=true min..max=(4.89us ... 4.92us) p75=4.91us p99=4.92us ---------------------------------------------------------------------------- readFileSync x 198,461 ops/sec (9 runs sampled) v8-never-optimize=true min..max=(5.00us ... 5.05us) p75=5.02us p99=5.05us readFileSync utf-8 x 205,171 ops/sec (10 runs sampled) v8-never-optimize=true min..max=(4.82us ... 4.86us) p75=4.86us p99=4.86us [Managed] readFileSync x 193,061 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(5.05us ... 5.48us) p75=5.30us p99=5.48us [Managed] readFileSync utf-8 x 204,046 ops/sec (10 runs sampled) v8-never-optimize=true min..max=(4.86us ... 4.92us) p75=4.91us p99=4.92us ---------------------------------------------------------------------------- readFileSync x 196,533 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(5.02us ... 5.20us) p75=5.10us p99=5.20us readFileSync utf-8 x 205,213 ops/sec (10 runs sampled) v8-never-optimize=true min..max=(4.83us ... 4.89us) p75=4.86us p99=4.89us [Managed] readFileSync x 183,333 ops/sec (12 runs sampled) v8-never-optimize=true min..max=(5.06us ... 6.46us) p75=5.28us p99=6.46us [Managed] readFileSync utf-8 x 201,593 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(4.90us ... 5.07us) p75=5.06us p99=5.07us ---------------------------------------------------------------------------- ================================================ FILE: examples/fs-read-sync/sample-file.txt ================================================ hello ================================================ FILE: examples/html-report/node.js ================================================ const { Suite, htmlReport } = require('../../lib'); const assert = require('node:assert'); const suite = new Suite({ reporter: htmlReport, }); suite .add('single with matcher', function () { const pattern = /[123]/g const replacements = { 1: 'a', 2: 'b', 3: 'c' } const subject = '123123123123123123123123123123123123123123123123' const r = subject.replace(pattern, m => replacements[m]) assert.ok(r); }) .add('Multiple replaces', function () { const subject = '123123123123123123123123123123123123123123123123' const r = subject.replace(/1/g, 'a').replace(/2/g, 'b').replace(/3/g, 'c') assert.ok(r); }) .run(); ================================================ FILE: examples/html-report/result.html ================================================ Benchmark Visualizer

Node.js version: v20.18.1

Platform: darwin arm64

CPU Cores: 8 vCPUs | 16.0GB Mem

single-with-matcher(660,788.4 ops/sec)
Multiple-replaces(578,527 ops/sec)

Benchmark done with bench-node

================================================ FILE: examples/json-report/node.js ================================================ const { Suite, jsonReport } = require('../../lib'); const assert = require('node:assert'); const suite = new Suite({ reporter: jsonReport, }); suite .add('single with matcher', function () { const pattern = /[123]/g const replacements = { 1: 'a', 2: 'b', 3: 'c' } const subject = '123123123123123123123123123123123123123123123123' const r = subject.replace(pattern, m => replacements[m]) assert.ok(r); }) .add('Multiple replaces', function () { const subject = '123123123123123123123123123123123123123123123123' const r = subject.replace(/1/g, 'a').replace(/2/g, 'b').replace(/3/g, 'c') assert.ok(r); }) .run(); ================================================ FILE: examples/plugins/all.js ================================================ const { Suite, V8GetOptimizationStatus, V8NeverOptimizePlugin, V8OptimizeOnNextCallPlugin, MemoryPlugin, } = require('../../lib'); const suite = new Suite({ plugins: [ new V8GetOptimizationStatus(), new V8NeverOptimizePlugin(), new MemoryPlugin(), new V8OptimizeOnNextCallPlugin(), ], }); suite .add(`new Uint32Array(1024)`, function () { return new Uint32Array(1024); }) .add(`[Managed] new Uint32Array(1024)`, function (timer) { const assert = require('node:assert'); let r; timer.start(); for (let i = 0; i < timer.count; i++) { r = new Uint32Array(1024); } timer.end(timer.count); assert.ok(r); }) .run(); ================================================ FILE: examples/plugins/all.js.log ================================================ new Uint32Array(1024) x 2,395,395 ops/sec (14 runs sampled) v8-opt-status="Optimized, Interpreted, Marked for Optimization, Concurrently Optimizing" v8-never-optimize=true min..max=(302.80ns ... 435.20ns) p75=420.50ns p99=435.20ns [Managed] new Uint32Array(1024) x 1,878,469 ops/sec (12 runs sampled) v8-opt-status="Optimized, Interpreted, Marked for Optimization, Concurrently Optimizing" v8-never-optimize=true min..max=(217.82ns ... 577.91ns) p75=430.46ns p99=577.91ns ---------------------------------------------------------------------------- new Uint32Array(1024) x 2,282,799 ops/sec (13 runs sampled) v8-opt-status="Optimized, Interpreted, Marked for Optimization, Concurrently Optimizing" v8-never-optimize=true min..max=(339.06ns ... 479.78ns) p75=449.29ns p99=479.78ns [Managed] new Uint32Array(1024) x 2,219,660 ops/sec (10 runs sampled) v8-opt-status="Optimized, Interpreted, Marked for Optimization, Concurrently Optimizing" v8-never-optimize=true min..max=(383.40ns ... 548.65ns) p75=461.81ns p99=548.65ns ---------------------------------------------------------------------------- new Uint32Array(1024) x 2,362,479 ops/sec (11 runs sampled) v8-opt-status="Optimized, Interpreted, Marked for Optimization, Concurrently Optimizing" v8-never-optimize=true min..max=(384.64ns ... 456.35ns) p75=440.57ns p99=456.35ns [Managed] new Uint32Array(1024) x 2,272,352 ops/sec (10 runs sampled) v8-opt-status="Optimized, Interpreted, Marked for Optimization, Concurrently Optimizing" v8-never-optimize=true min..max=(421.95ns ... 451.89ns) p75=443.17ns p99=451.89ns ---------------------------------------------------------------------------- ================================================ FILE: examples/plugins/memory.js ================================================ const { Suite, MemoryPlugin} = require('../../lib'); const suite = new Suite({ plugins: [new MemoryPlugin()], }); suite .add(`new Uint32Array(1024)`, function () { return new Uint32Array(1024); }) .add(`[Managed] new Uint32Array(1024)`, function (timer) { const assert = require('node:assert'); let r; timer.start(); for (let i = 0; i < timer.count; i++) { r = new Uint32Array(1024); } timer.end(timer.count); assert.ok(r); }) .run(); ================================================ FILE: examples/plugins/v8-get-opt-status.js ================================================ const { Suite, V8GetOptimizationStatus } = require('../../lib'); const suite = new Suite({ plugins: [new V8GetOptimizationStatus()], }); suite .add(`new Uint32Array(1024)`, function () { return new Uint32Array(1024); }) .add(`[Managed] new Uint32Array(1024)`, function (timer) { const assert = require('node:assert'); let r; timer.start(); for (let i = 0; i < timer.count; i++) { r = new Uint32Array(1024); } timer.end(timer.count); assert.ok(r); }) .run(); ================================================ FILE: examples/plugins/v8-get-opt-status.js.log ================================================ new Uint32Array(1024) x 2,353,498 ops/sec (10 runs sampled) v8-opt-status="Optimized, Interpreted, Marked for Optimization, Concurrently Optimizing" min..max=(387.20ns ... 435.11ns) p75=426.25ns p99=435.11ns [Managed] new Uint32Array(1024) x 2,295,396 ops/sec (12 runs sampled) v8-opt-status="Optimized, Interpreted, Marked for Optimization, Concurrently Optimizing" min..max=(346.78ns ... 471.27ns) p75=450.57ns p99=471.27ns ---------------------------------------------------------------------------- new Uint32Array(1024) x 2,206,492 ops/sec (11 runs sampled) v8-opt-status="Optimized, Interpreted, Marked for Optimization, Concurrently Optimizing" min..max=(413.34ns ... 498.95ns) p75=457.27ns p99=498.95ns [Managed] new Uint32Array(1024) x 2,308,235 ops/sec (10 runs sampled) v8-opt-status="Optimized, Interpreted, Marked for Optimization, Concurrently Optimizing" min..max=(410.95ns ... 472.38ns) p75=444.99ns p99=472.38ns ---------------------------------------------------------------------------- new Uint32Array(1024) x 2,765,470 ops/sec (11 runs sampled) v8-opt-status="Optimized, Interpreted, Marked for Optimization, Concurrently Optimizing" min..max=(343.99ns ... 396.13ns) p75=386.55ns p99=396.13ns [Managed] new Uint32Array(1024) x 2,773,144 ops/sec (11 runs sampled) v8-opt-status="Optimized, Interpreted, Marked for Optimization, Concurrently Optimizing" min..max=(351.75ns ... 377.92ns) p75=369.54ns p99=377.92ns ---------------------------------------------------------------------------- ================================================ FILE: examples/plugins/v8-never-optimize.js ================================================ const { Suite, V8NeverOptimizePlugin } = require('../../lib'); const suite = new Suite({ plugins: [new V8NeverOptimizePlugin()], }); suite .add(`new Uint32Array(1024)`, function () { return new Uint32Array(1024); }) .add(`[Managed] new Uint32Array(1024)`, function (timer) { const assert = require('node:assert'); let r; timer.start(); for (let i = 0; i < timer.count; i++) { r = new Uint32Array(1024); } timer.end(timer.count); assert.ok(r); }) .run(); ================================================ FILE: examples/plugins/v8-never-optimize.js.log ================================================ new Uint32Array(1024) x 2,535,349 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(357.30ns ... 442.55ns) p75=416.14ns p99=442.55ns [Managed] new Uint32Array(1024) x 2,646,454 ops/sec (12 runs sampled) v8-never-optimize=true min..max=(356.81ns ... 405.88ns) p75=382.20ns p99=405.88ns ---------------------------------------------------------------------------- new Uint32Array(1024) x 2,785,937 ops/sec (12 runs sampled) v8-never-optimize=true min..max=(350.61ns ... 368.86ns) p75=360.60ns p99=368.86ns [Managed] new Uint32Array(1024) x 2,535,974 ops/sec (10 runs sampled) v8-never-optimize=true min..max=(353.44ns ... 462.38ns) p75=447.16ns p99=462.38ns ---------------------------------------------------------------------------- new Uint32Array(1024) x 2,336,195 ops/sec (9 runs sampled) v8-never-optimize=true min..max=(380.49ns ... 453.11ns) p75=431.40ns p99=453.11ns [Managed] new Uint32Array(1024) x 2,624,328 ops/sec (10 runs sampled) v8-never-optimize=true min..max=(341.92ns ... 432.82ns) p75=411.22ns p99=432.82ns ---------------------------------------------------------------------------- ================================================ FILE: examples/plugins/v8-optimize-next-call.js ================================================ const { Suite, V8OptimizeOnNextCallPlugin } = require('../../lib'); const suite = new Suite({ plugins: [new V8OptimizeOnNextCallPlugin()], }); suite .add(`new Uint32Array(1024)`, function () { return new Uint32Array(1024); }) .add(`[Managed] new Uint32Array(1024)`, function (timer) { const assert = require('node:assert'); let r; timer.start(); for (let i = 0; i < timer.count; i++) { r = new Uint32Array(1024); } timer.end(timer.count); assert.ok(r); }) .run(); ================================================ FILE: examples/plugins/v8-optimize-next-call.js.log ================================================ new Uint32Array(1024) x 2,812,339 ops/sec (13 runs sampled) v8-optimize-next-call=enabled min..max=(314.34ns ... 373.62ns) p75=359.22ns p99=373.62ns [Managed] new Uint32Array(1024) x 2,763,965 ops/sec (11 runs sampled) v8-optimize-next-call=enabled min..max=(348.80ns ... 381.77ns) p75=373.99ns p99=381.77ns ---------------------------------------------------------------------------- new Uint32Array(1024) x 2,827,020 ops/sec (11 runs sampled) v8-optimize-next-call=enabled min..max=(298.24ns ... 365.66ns) p75=354.33ns p99=365.66ns [Managed] new Uint32Array(1024) x 2,775,090 ops/sec (12 runs sampled) v8-optimize-next-call=enabled min..max=(349.78ns ... 378.17ns) p75=360.17ns p99=378.17ns ---------------------------------------------------------------------------- new Uint32Array(1024) x 2,793,090 ops/sec (9 runs sampled) v8-optimize-next-call=enabled min..max=(342.17ns ... 352.58ns) p75=350.66ns p99=352.58ns [Managed] new Uint32Array(1024) x 2,751,982 ops/sec (10 runs sampled) v8-optimize-next-call=enabled min..max=(352.72ns ... 385.30ns) p75=367.30ns p99=385.30ns ---------------------------------------------------------------------------- ================================================ FILE: examples/pretty-report/node.js ================================================ const { Suite, prettyReport } = require('../../lib'); const assert = require('node:assert'); const suite = new Suite({ reporter: prettyReport, }); suite .add('my-group/my-benchmark', () => { // A slower benchmark for (let i = 0; i < 10000; i++) {} }) .add('my-group/my-benchmark-2', { baseline: true }, function () { // The baseline for (let i = 0; i < 1000; i++) {} }) .add('second-group/another-benchmark', function () { // A faster benchmark for (let i = 0; i < 100; i++) {} }) .run(); ================================================ FILE: examples/run.sh ================================================ #!/bin/bash # clean previous logs rm -f ./**/*.log for filename in ./**/*.*js; do echo "[1] Running $filename" node --allow-natives-syntax "./$filename" | sed "s,\x1B\[[0-9;]*m,,g" >>"$filename.log" echo -e "----------------------------------------------------------------------------" >>"$filename.log" echo "[2] Running $filename" node --allow-natives-syntax "./$filename" | sed "s,\x1B\[[0-9;]*m,,g" >>"$filename.log" echo -e "----------------------------------------------------------------------------" >>"$filename.log" echo "[3] Running $filename" node --allow-natives-syntax "./$filename" | sed "s,\x1B\[[0-9;]*m,,g" >>"$filename.log" echo -e "----------------------------------------------------------------------------" >>"$filename.log" done ================================================ FILE: examples/statistical-significance/README.md ================================================ # Statistical Significance Testing (T-Test) This example demonstrates how to use Welch's t-test to determine if benchmark differences are statistically significant. ## The Problem When running benchmarks on shared or cloud environments, results can vary due to: - CPU throttling - Background processes - Memory pressure - Cache effects A benchmark might show one implementation as "1.05x faster", but is that a real improvement or just noise? ## The Solution Enable t-test mode with `ttest: true`: ```js const { Suite } = require('bench-node'); const suite = new Suite({ ttest: true, // Automatically sets repeatSuite=30 }); suite.add('baseline', { baseline: true }, () => { // ... }); suite.add('alternative', () => { // ... }); ``` When `ttest: true` is set, the suite automatically: 1. Sets `repeatSuite=30` for all benchmarks (can be overridden) 2. Runs Welch's t-test to compare results against baseline 3. Displays significance stars in the output ## Understanding the Output The output will show significance stars next to comparisons: ``` Summary (vs. baseline): baseline/for-loop (baseline) forEach (1.80x slower) *** for-of-loop (1.09x slower) *** reduce (1.06x faster) ** Significance: * p<0.05, ** p<0.01, *** p<0.001 ``` - `***` = p < 0.001 - Very high confidence (99.9%) the difference is real - `**` = p < 0.01 - High confidence (99%) the difference is real - `*` = p < 0.05 - Moderate confidence (95%) the difference is real - (no stars) = Not statistically significant - difference may be noise ## When to Use 1. **Comparing similar implementations** - Is the "optimization" actually faster? 2. **CI/CD pipelines** - Detect real regressions vs. flaky results 3. **Cloud/shared environments** - High variance requires statistical validation 4. **Small differences** - 5% faster could be noise or real ## Run the Example ```bash node --allow-natives-syntax node.js ``` ## Sample Output ``` baseline/for-loop x 85,009,221 ops/sec (311 runs sampled) reduce x 89,853,937 ops/sec (321 runs sampled) for-of-loop x 78,268,434 ops/sec (302 runs sampled) forEach x 47,249,597 ops/sec (334 runs sampled) Summary (vs. baseline): baseline/for-loop (baseline) forEach (1.80x slower) *** for-of-loop (1.09x slower) *** reduce (1.06x faster) ** Significance: * p<0.05, ** p<0.01, *** p<0.001 ``` ================================================ FILE: examples/statistical-significance/node.js ================================================ /** * Statistical Significance Example * * This example demonstrates how to use Welch's t-test to determine * if benchmark differences are statistically significant. * * When running benchmarks, especially on shared/cloud environments, * small performance differences may just be random noise. The t-test * helps identify when a difference is real vs. just variance. * * Run with: node --allow-natives-syntax node.js */ const { Suite } = require('../../lib'); // Enable t-test mode - this automatically sets repeatSuite=30 for all benchmarks const suite = new Suite({ ttest: true, }); // Baseline: Simple array sum using for loop suite.add('baseline/for-loop', { baseline: true }, () => { const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; let sum = 0; for (let i = 0; i < arr.length; i++) { sum += arr[i]; } return sum; }); // Alternative 1: Using reduce (typically slower due to function call overhead) suite.add('reduce', () => { const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; return arr.reduce((acc, val) => acc + val, 0); }); // Alternative 2: for-of loop (similar performance to for loop) suite.add('for-of-loop', () => { const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; let sum = 0; for (const val of arr) { sum += val; } return sum; }); // Alternative 3: forEach (slower due to function call per element) suite.add('forEach', () => { const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; let sum = 0; arr.forEach((val) => { sum += val; }); return sum; }); suite.run(); ================================================ FILE: examples/string-replace/node.js ================================================ const { Suite } = require('../../lib'); const suite = new Suite(); const pattern = /[123]/g const replacements = { 1: 'a', 2: 'b', 3: 'c' } const subject = '123123123123123123123123123123123123123123123123' suite .add('single with matcher', function () { const r = subject.replace(pattern, m => replacements[m]) }) .add('multiple replaces', function () { const r = subject.replace(/1/g, 'a').replace(/2/g, 'b').replace(/3/g, 'c') }) .add('[Managed] single with matcher', function (timer) { const assert = require('node:assert'); const pattern = /[123]/g const replacements = { 1: 'a', 2: 'b', 3: 'c' } const subject = '123123123123123123123123123123123123123123123123' let r; timer.start(); for (let i = 0; i < timer.count; i++) { r = subject.replace(pattern, m => replacements[m]); } timer.end(timer.count); assert.ok(r); }) .add('[Managed] multiple replaces', function (timer) { const assert = require('node:assert'); const subject = '123123123123123123123123123123123123123123123123' let r; timer.start(); for (let i = 0; i < timer.count; i++) { r = subject.replace(/1/g, 'a').replace(/2/g, 'b').replace(/3/g, 'c'); } timer.end(timer.count); assert.ok(r); }) .run() ================================================ FILE: examples/string-replace/node.js.log ================================================ single with matcher x 752,020 ops/sec (10 runs sampled) v8-never-optimize=true min..max=(1.32us ... 1.34us) p75=1.33us p99=1.34us multiple replaces x 642,602 ops/sec (10 runs sampled) v8-never-optimize=true min..max=(1.54us ... 1.55us) p75=1.55us p99=1.55us [Managed] single with matcher x 774,049 ops/sec (12 runs sampled) v8-never-optimize=true min..max=(1.28us ... 1.30us) p75=1.29us p99=1.30us [Managed] multiple replaces x 645,958 ops/sec (12 runs sampled) v8-never-optimize=true min..max=(1.54us ... 1.58us) p75=1.55us p99=1.58us ---------------------------------------------------------------------------- single with matcher x 748,221 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(1.33us ... 1.35us) p75=1.35us p99=1.35us multiple replaces x 639,390 ops/sec (10 runs sampled) v8-never-optimize=true min..max=(1.55us ... 1.57us) p75=1.56us p99=1.57us [Managed] single with matcher x 765,396 ops/sec (12 runs sampled) v8-never-optimize=true min..max=(1.30us ... 1.32us) p75=1.31us p99=1.32us [Managed] multiple replaces x 644,492 ops/sec (9 runs sampled) v8-never-optimize=true min..max=(1.55us ... 1.55us) p75=1.55us p99=1.55us ---------------------------------------------------------------------------- single with matcher x 742,699 ops/sec (9 runs sampled) v8-never-optimize=true min..max=(1.32us ... 1.34us) p75=1.33us p99=1.34us multiple replaces x 640,209 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(1.54us ... 1.57us) p75=1.56us p99=1.57us [Managed] single with matcher x 774,824 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(1.29us ... 1.30us) p75=1.29us p99=1.30us [Managed] multiple replaces x 642,062 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(1.54us ... 1.60us) p75=1.57us p99=1.60us ---------------------------------------------------------------------------- ================================================ FILE: examples/string-searching/node.js ================================================ const { Suite } = require('../../lib'); const suite = new Suite(); const text = 'text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8' const regex = /application\/json/ suite .add('Using includes', function () { const r = text.includes('application/json') }) .add('Using indexof', function () { const r = text.indexOf('application/json') !== -1 }) .add('Using cached RegExp.test', function () { const r = regex.test(text) }) .add('[Managed] Using includes', function (timer) { const assert = require('node:assert'); const text = 'text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8'; let r; timer.start(); for (let i = 0; i < timer.count; i++) { r = text.includes('application/json'); } timer.end(timer.count); assert.ok(r); }) .add('[Managed] Using indexof', function (timer) { const assert = require('node:assert'); const text = 'text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8'; let r; timer.start(); for (let i = 0; i < timer.count; i++) { r = text.indexOf('application/json') !== -1; } timer.end(timer.count); assert.ok(r); }) .add('[Managed] Using cached RegExp.test', function (timer) { const assert = require('node:assert'); const regex = /application\/json/; const text = 'text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8'; let r; timer.start(); for (let i = 0; i < timer.count; i++) { r = regex.test(text); } timer.end(timer.count); assert.ok(r); }) .run(); ================================================ FILE: examples/string-searching/node.js.log ================================================ Using includes x 131,465,887 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(7.53ns ... 7.61ns) p75=7.59ns p99=7.61ns Using indexof x 130,628,503 ops/sec (10 runs sampled) v8-never-optimize=true min..max=(7.62ns ... 7.67ns) p75=7.65ns p99=7.67ns Using cached RegExp.test x 19,855,444 ops/sec (9 runs sampled) v8-never-optimize=true min..max=(50.32ns ... 50.38ns) p75=50.37ns p99=50.38ns [Managed] Using includes x 2,259,413,999 ops/sec (17 runs sampled) v8-never-optimize=true min..max=(0.50ns ... 0.50ns) p75=0.50ns p99=0.50ns [Managed] Using indexof x 2,262,452,858 ops/sec (17 runs sampled) v8-never-optimize=true min..max=(0.50ns ... 0.50ns) p75=0.50ns p99=0.50ns [Managed] Using cached RegExp.test x 24,771,962 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(40.11ns ... 40.43ns) p75=40.37ns p99=40.43ns ---------------------------------------------------------------------------- Using includes x 131,872,369 ops/sec (12 runs sampled) v8-never-optimize=true min..max=(7.53ns ... 7.70ns) p75=7.63ns p99=7.70ns Using indexof x 131,276,601 ops/sec (10 runs sampled) v8-never-optimize=true min..max=(7.56ns ... 7.67ns) p75=7.59ns p99=7.67ns Using cached RegExp.test x 19,684,556 ops/sec (12 runs sampled) v8-never-optimize=true min..max=(50.51ns ... 51.39ns) p75=50.75ns p99=51.39ns [Managed] Using includes x 2,279,136,862 ops/sec (17 runs sampled) v8-never-optimize=true min..max=(0.50ns ... 0.50ns) p75=0.50ns p99=0.50ns [Managed] Using indexof x 2,259,877,221 ops/sec (17 runs sampled) v8-never-optimize=true min..max=(0.50ns ... 0.50ns) p75=0.50ns p99=0.50ns [Managed] Using cached RegExp.test x 24,313,713 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(40.92ns ... 41.19ns) p75=41.19ns p99=41.19ns ---------------------------------------------------------------------------- Using includes x 132,464,212 ops/sec (10 runs sampled) v8-never-optimize=true min..max=(7.52ns ... 7.56ns) p75=7.56ns p99=7.56ns Using indexof x 132,177,669 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(7.52ns ... 7.67ns) p75=7.60ns p99=7.67ns Using cached RegExp.test x 19,854,899 ops/sec (8 runs sampled) v8-never-optimize=true min..max=(50.28ns ... 50.36ns) p75=50.32ns p99=50.36ns [Managed] Using includes x 2,259,179,846 ops/sec (17 runs sampled) v8-never-optimize=true min..max=(0.50ns ... 0.50ns) p75=0.50ns p99=0.50ns [Managed] Using indexof x 2,181,069,609 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(0.50ns ... 0.50ns) p75=0.50ns p99=0.50ns [Managed] Using cached RegExp.test x 23,308,043 ops/sec (10 runs sampled) v8-never-optimize=true min..max=(40.23ns ... 41.24ns) p75=40.69ns p99=41.24ns ---------------------------------------------------------------------------- ================================================ FILE: examples/time-mode.js ================================================ const { Suite } = require('../lib'); const timeSuite = new Suite({ benchmarkMode: 'time' // Set mode for the entire suite }); const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)); timeSuite.add('Async Delay 100ms (time)', async () => { await delay(100); }); timeSuite.add('Sync Busy Wait 50ms (time)', () => { const start = Date.now(); while (Date.now() - start < 50); }); timeSuite.add('Quick Sync Op with 5 repeats (time)', { repeatSuite: 5 }, () => { // This will run exactly once per repeat (5 times total) // and report the average time let x = 1 + 1; }); (async () => { console.log('\nRunning benchmark suite in TIME mode...'); await timeSuite.run(); })(); ================================================ FILE: examples/worker-threads/node.js ================================================ const { Suite } = require('../../lib'); const suite = new Suite({ useWorkers: true, }); suite .add('Using import without node: prefix', function () { return import('fs'); }) .add('Using import with node: prefix', function () { return import('node:fs'); }) .add('async test', async function (timer) { timer.start(); let i = 0; while (i++ < timer.count) { await import("node:fs"); } timer.end(timer.count); }) .run(); ================================================ FILE: index.d.ts ================================================ // Type definitions for bench-node /// import type { Histogram } from "node:perf_hooks"; export declare namespace BenchNode { class Benchmark { name: string; fn: any; minTime: number; maxTime: number; plugins: Plugin[]; repeatSuite: number; minSamples: number; baseline: boolean; constructor( name: string, fn: any, minTime: number, maxTime: number, plugins: Plugin[], repeatSuite: number, minSamples: number, baseline?: boolean, ); serializeBenchmark(): void; } interface PluginHookVarNames { awaitOrEmpty: string; bench: string; context: string; timer: string; managed: boolean; } interface BenchmarkResult { name: string; opsSec?: number; // Only in 'ops' mode opsSecPerRun?: number[]; // Useful when repeatSuite > 1 totalTime?: number; // Total execution time in seconds (Only in 'time' mode) iterations: number; histogram: Histogram; plugins?: Record; // Object with plugin results } type ReporterFunction = (results: BenchmarkResult[]) => void; interface ReporterOptions { printHeader?: boolean; labelWidth?: number; ttest?: boolean; // Passed automatically when Suite ttest option is enabled alpha?: number; // Significance level for t-test (default: 0.05) } interface SuiteOptions { reporter?: ReporterFunction | false | null; benchmarkMode?: "ops" | "time"; useWorkers?: boolean; plugins?: Plugin[]; minSamples?: number; // Minimum number of samples per round for all benchmarks repeatSuite?: number; // Number of times to repeat each benchmark (default: 1, or 30 when ttest is enabled) ttest?: boolean; // Enable t-test mode for statistical significance (auto-sets repeatSuite=30) reporterOptions?: ReporterOptions; detectDeadCodeElimination?: boolean; // Enable DCE detection, default: false dceThreshold?: number; // DCE detection threshold multiplier, default: 10 } interface BenchmarkOptions { minTime?: number; // Minimum duration in seconds maxTime?: number; // Maximum duration in seconds repeatSuite?: number; // Number of times to repeat benchmark minSamples?: number; // Minimum number of samples per round } type BenchmarkFunction = (timer?: { start: () => void; end: (iterations?: number) => void; count: number; }) => void | Promise; type OnCompleteBenchmarkResult = [ duration: number, count: number, context: Record, ]; type PluginResult = { type: string; [key: string]: any; }; interface Plugin { isSupported?(): boolean; beforeClockTemplate?(varNames: PluginHookVarNames): string[]; afterClockTemplate?(varNames: PluginHookVarNames): string[]; onCompleteBenchmark?( result: OnCompleteBenchmarkResult, bench: Benchmark, ): void; toString?(): string; getReport?(benchmarkName: string): string; getResult?(benchmarkName: string): PluginResult; reset?(): void; } class Suite { constructor(options?: SuiteOptions); add(name: string, fn: BenchmarkFunction): this; add(name: string, options: BenchmarkOptions, fn: BenchmarkFunction): this; run(): Promise; } class V8NeverOptimizePlugin implements Plugin { isSupported(): boolean; beforeClockTemplate(varNames: PluginHookVarNames): string[]; toString(): string; getReport(benchmarkName: string): string; } class V8GetOptimizationStatus implements Plugin { isSupported(): boolean; afterClockTemplate(varNames: PluginHookVarNames): string[]; onCompleteBenchmark(result: OnCompleteBenchmarkResult): void; toString(): string; getReport(benchmarkName: string): string; getResult?(benchmarkName: string): PluginResult; } class V8OptimizeOnNextCallPlugin implements Plugin { isSupported(): boolean; beforeClockTemplate(varNames: PluginHookVarNames): string[]; toString(): string; getReport(): string; } class MemoryPlugin implements Plugin { isSupported(): boolean; beforeClockTemplate(varNames: PluginHookVarNames): string[]; afterClockTemplate(varNames: PluginHookVarNames): string[]; onCompleteBenchmark(result: OnCompleteBenchmarkResult): void; getReport(benchmarkName: string): string; getResult(benchmarkName: string): PluginResult; toString(): string; } class DeadCodeEliminationDetectionPlugin implements Plugin { constructor(options?: { threshold?: number }); isSupported(): boolean; setBaseline(timePerOp: number): void; onCompleteBenchmark( result: OnCompleteBenchmarkResult, bench: Benchmark, ): void; getWarning( benchmarkName: string, ): { timePerOp: number; baselineTime: number; ratio: number } | undefined; getAllWarnings(): Array<{ name: string; timePerOp: number; baselineTime: number; ratio: number; }>; hasWarning(benchmarkName: string): boolean; emitWarnings(): void; toString(): string; reset(): void; } } export declare const textReport: BenchNode.ReporterFunction; export declare const chartReport: BenchNode.ReporterFunction; export declare const prettyReport: BenchNode.ReporterFunction; export declare const htmlReport: BenchNode.ReporterFunction; export declare const jsonReport: BenchNode.ReporterFunction; export declare const csvReport: BenchNode.ReporterFunction; export declare class Suite extends BenchNode.Suite {} export declare class V8NeverOptimizePlugin extends BenchNode.V8NeverOptimizePlugin {} export declare class V8GetOptimizationStatus extends BenchNode.V8GetOptimizationStatus {} export declare class V8OptimizeOnNextCallPlugin extends BenchNode.V8OptimizeOnNextCallPlugin {} export declare class MemoryPlugin extends BenchNode.MemoryPlugin {} // Statistical T-Test utilities export declare namespace TTest { interface WelchTTestResult { tStatistic: number; degreesOfFreedom: number; pValue: number; significant: boolean; mean1: number; mean2: number; variance1: number; variance2: number; } interface CompareBenchmarksResult { significant: boolean; pValue: number; confidence: string; stars: "***" | "**" | "*" | ""; difference: "faster" | "slower" | "same"; tStatistic: number; degreesOfFreedom: number; } } /** * Returns significance stars based on p-value thresholds. * @param pValue - The p-value from statistical test * @returns Stars indicating significance level ('***', '**', '*', or '') */ export declare function getSignificanceStars( pValue: number, ): "***" | "**" | "*" | ""; /** * Performs Welch's t-test for two independent samples. * Does not assume equal variances between the samples. * @param sample1 - First sample array * @param sample2 - Second sample array * @returns Test results including t-statistic, degrees of freedom, p-value, and significance */ export declare function welchTTest( sample1: number[], sample2: number[], ): TTest.WelchTTestResult; /** * Determines if two benchmark results are statistically different * using Welch's t-test at a given significance level. * @param sample1 - Sample data from first benchmark * @param sample2 - Sample data from second benchmark * @param alpha - Significance level (default 0.05 for 95% confidence) * @returns Comparison result with significance info */ export declare function compareBenchmarks( sample1: number[], sample2: number[], alpha?: number, ): TTest.CompareBenchmarksResult; export declare class DeadCodeEliminationDetectionPlugin extends BenchNode.DeadCodeEliminationDetectionPlugin {} ================================================ FILE: lib/clock.js ================================================ const { debuglog } = require("node:util"); const { validateNumber } = require("./validators"); const debugBench = debuglog("benchmark"); const kUnmanagedTimerResult = Symbol("kUnmanagedTimerResult"); // If the smallest time measurement is 1ns // the minimum resolution of this timer is 0.5 const MIN_RESOLUTION = 0.5; class Timer { constructor() { this.now = process.hrtime.bigint; } get scale() { return 1e9; } get resolution() { return 1 / 1e9; } /** * @param {number} timeInNs * @returns {string} */ format(timeInNs) { validateNumber(timeInNs, "timeInNs", 0); if (timeInNs > 1e9) { return `${(timeInNs / 1e9).toFixed(2)}s`; } if (timeInNs > 1e6) { return `${(timeInNs / 1e6).toFixed(2)}ms`; } if (timeInNs > 1e3) { return `${(timeInNs / 1e3).toFixed(2)}us`; } return `${(timeInNs).toFixed(2)}ns`; } } const timer = new Timer(); class ManagedTimer { startTime; endTime; iterations; recommendedCount; /** * @param {number} recommendedCount */ constructor(recommendedCount) { this.recommendedCount = recommendedCount; } /** * Returns the recommended value to be used to benchmark your code * @returns {number} */ get count() { return this.recommendedCount; } /** * Starts the timer */ start() { this.startTime = timer.now(); } /** * Stops the timer * @param {number} [iterations=1] The amount of iterations that run */ end(iterations = 1) { this.endTime = timer.now(); validateNumber(iterations, "iterations", 1); this.iterations = iterations; } [kUnmanagedTimerResult](context) { if (this.startTime === undefined) throw new Error("You forgot to call .start()"); if (this.endTime === undefined) throw new Error("You forgot to call .end(count)"); return [Number(this.endTime - this.startTime), this.iterations, context]; } } function createRunUnmanagedBenchmark(bench, awaitOrEmpty) { const varNames = { awaitOrEmpty, timer: "timer", context: "context", bench: "bench", managed: false, }; let code = ` let i = 0; let ${varNames.context} = {}; `; let benchFnCall = `${awaitOrEmpty}${varNames.bench}.fn()`; const wrapFunctions = []; for (const p of bench.plugins) { if (typeof p.beforeClockTemplate === "function") { const [newCode, functionToCall] = p.beforeClockTemplate(varNames); code += newCode; if (functionToCall) { wrapFunctions.push(functionToCall); } } } benchFnCall = wrapFunctions.reduce((prev, n) => { return `${n}(${prev})`; }, benchFnCall); code += ` const startedAt = ${varNames.timer}.now(); for (; i < count; i++) ${benchFnCall}; const duration = Number(${varNames.timer}.now() - startedAt); `; for (const p of bench.plugins) { if (typeof p.afterClockTemplate === "function") { const [newCode] = p.afterClockTemplate(varNames); code += newCode; } } code += `return [duration, count, ${varNames.context}];`; return code; } function createRunManagedBenchmark(bench, awaitOrEmpty) { const varNames = { awaitOrEmpty, timer: "timer", context: "context", bench: "bench", managed: true, }; let code = ` let i = 0; let ${varNames.context} = {}; `; let benchFnCall = `${awaitOrEmpty}${varNames.bench}.fn(${varNames.timer})`; const wrapFunctions = []; for (const p of bench.plugins) { if (typeof p.beforeClockTemplate === "function") { const [newCode, functionToCall] = p.beforeClockTemplate(varNames); code += newCode; if (functionToCall) { wrapFunctions.push(functionToCall); } } } benchFnCall = wrapFunctions.reduce((prev, n) => { return `${n}(${prev})`; }, benchFnCall); code += ` ${benchFnCall}; const result = ${varNames.timer}[kUnmanagedTimerResult](${varNames.context}); `; for (const p of bench.plugins) { if (typeof p.afterClockTemplate === "function") { const [newCode] = p.afterClockTemplate(varNames); code += newCode; } } code += "return result;"; return code; } const AsyncFunction = (async () => {}).constructor; const SyncFunction = (() => {}).constructor; function createFnString(bench) { const { isAsync, hasArg } = bench; const compiledFnStringFactory = hasArg ? createRunManagedBenchmark : createRunUnmanagedBenchmark; const compiledFnString = compiledFnStringFactory( bench, isAsync ? "await " : "", ); return compiledFnString; } function createRunner(bench, recommendedCount) { const { isAsync, hasArg } = bench; const compiledFnString = bench.fnStr; const createFnPrototype = isAsync ? AsyncFunction : SyncFunction; const compiledFn = createFnPrototype( "bench", "timer", "count", "kUnmanagedTimerResult", compiledFnString, ); const selectedTimer = hasArg ? new ManagedTimer(recommendedCount) : timer; const runner = compiledFn.bind( globalThis, bench, selectedTimer, recommendedCount, kUnmanagedTimerResult, ); debugBench(`Compiled Code: ${compiledFnString}`); debugBench( `Created compiled benchmark, hasArg=${hasArg}, isAsync=${isAsync}, recommendedCount=${recommendedCount}`, ); return runner; } /** * Executes a benchmark and returns the time taken and number of iterations * @param {import('./index').Benchmark} bench - The benchmark to execute * @param {number} recommendedCount - The recommended number of iterations * @param {Object} [options] - Additional options * @param {boolean} [options.timeMode=false] - If true, runs the benchmark exactly once * @returns {Promise<[number, number]>} - Returns [duration, iterations] */ async function clockBenchmark(bench, recommendedCount, options = {}) { const runner = createRunner(bench, recommendedCount); const result = await runner(); // Just to avoid issues with empty fn result[0] = Math.max(MIN_RESOLUTION, result[0]); for (const p of bench.plugins) { if (typeof p.onCompleteBenchmark === "function") { // TODO: this won't work when useWorkers=true p.onCompleteBenchmark(result, bench); } } debugBench( `Took ${timer.format(result[0])} to execute ${result[1]} iterations${options.timeMode ? " (time mode)" : ""}`, ); return result; } module.exports = { clockBenchmark, createFnString, timer, MIN_RESOLUTION, debugBench, }; ================================================ FILE: lib/histogram.js ================================================ const { validateNumber } = require("./validators"); /** * A class that calculates and maintains statistical measurements for a set of numeric samples. * Handles outlier removal, and calculates various statistical measures like mean, standard deviation, * coefficient of variation, and percentiles. */ class StatisticalHistogram { all = []; min; max; mean; cv; stddev; finished = false; /** * @returns {number[]} */ get samples() { return this.all.slice(); } /** * @param {number} percentile * @returns {number} */ percentile(percentile) { validateNumber(percentile, "percentile"); if (Number.isNaN(percentile) || percentile < 0 || percentile > 100) throw new Error( "Invalid percentile value. Must be a number between 0 and 100.", ); if (this.all.length === 0) return 0; if (percentile === 0) return this.all[0]; return this.all[Math.ceil(this.all.length * (percentile / 100)) - 1]; } /** * @param {number} value */ record(value) { validateNumber(value, "value", 0); this.all.push(value); } finish() { if (this.finished) return; this.finished = true; this.removeOutliers(); this.calculateMinMax(); this.calculateMean(); this.calculateStd(); this.calculateCv(); } /** * References: * - https://gist.github.com/rmeissn/f5b42fb3e1386a46f60304a57b6d215a * - https://en.wikipedia.org/wiki/Interquartile_range */ removeOutliers() { this.all.sort((a, b) => a - b); const size = this.all.length; if (size < 4) return; let q1; let q3; if (((size - 1) / 4) % 1 === 0 || (size / 4) % 1 === 0) { q1 = (1 / 2) * (this.all[Math.floor(size / 4) - 1] + this.all[Math.floor(size / 4)]); q3 = (1 / 2) * (this.all[Math.ceil((size * 3) / 4) - 1] + this.all[Math.ceil((size * 3) / 4)]); } else { q1 = this.all[Math.floor(size / 4)]; q3 = this.all[Math.floor((size * 3) / 4)]; } const iqr = q3 - q1; const minValue = q1 - iqr * 1.5; const maxValue = q3 + iqr * 1.5; this.all = this.all.filter( (value) => value <= maxValue && value >= minValue, ); } calculateMinMax() { this.min = Number.POSITIVE_INFINITY; this.max = Number.NEGATIVE_INFINITY; for (let i = 0; i < this.all.length; i++) { this.min = Math.min(this.all[i], this.min); this.max = Math.max(this.all[i], this.max); } } calculateMean() { if (this.all.length === 0) { this.mean = 0; return; } if (this.all.length === 1) { this.mean = this.all[0]; return; } this.mean = this.all.reduce( (acc, value) => Math.min(Number.MAX_SAFE_INTEGER, acc + value), 0, ) / this.all.length; } calculateStd() { if (this.all.length < 2) { this.stddev = 0; return; } const variance = this.all.reduce((acc, value) => { return acc + (value - this.mean) ** 2; }, 0) / (this.all.length - 1); this.stddev = Math.sqrt(variance); } /** * References: * - https://en.wikipedia.org/wiki/Coefficient_of_variation * - https://github.com/google/benchmark/blob/159eb2d0ffb85b86e00ec1f983d72e72009ec387/src/statistics.ccL81-L88 */ calculateCv() { if (this.all.length < 2 || this.mean === 0) { this.cv = 0; return; } this.cv = (this.stddev / this.mean) * 100; } } module.exports = { StatisticalHistogram, }; ================================================ FILE: lib/index.js ================================================ const { Worker } = require("node:worker_threads"); const { types } = require("node:util"); const path = require("node:path"); const { textReport, chartReport, htmlReport, jsonReport, csvReport, prettyReport, } = require("./report"); const { getInitialIterations, runBenchmark, runWarmup, } = require("./lifecycle"); const { debugBench, timer, createFnString } = require("./clock"); const { validatePlugins, V8NeverOptimizePlugin, V8GetOptimizationStatus, V8OptimizeOnNextCallPlugin, MemoryPlugin, DeadCodeEliminationDetectionPlugin, } = require("./plugins"); const { validateFunction, validateNumber, validateObject, validateString, validateArray, validateBenchmarkMode, validateBoolean, } = require("./validators"); const { welchTTest, compareBenchmarks, getSignificanceStars, } = require("./utils/ttest"); const getFunctionBody = (string) => string.substring(string.indexOf("{") + 1, string.lastIndexOf("}")); class Benchmark { name = "Benchmark"; fn; minTime; maxTime; plugins; repeatSuite; minSamples; baseline = false; constructor( name, fn, minTime, maxTime, plugins, repeatSuite, minSamples, baseline = false, ) { this.name = name; this.fn = fn; this.minTime = minTime; this.maxTime = maxTime; this.plugins = plugins; this.repeatSuite = repeatSuite; this.minSamples = minSamples; this.baseline = baseline; this.hasArg = this.fn.length >= 1; if (this.fn.length > 1) { process.emitWarning( `The benchmark "${this.name}" function should not have more than 1 argument.`, ); } this.isAsync = types.isAsyncFunction(this.fn); this.fnStr = createFnString(this); } serializeBenchmark() { // Regular functions can't be passed to worker.postMessage // So we pass the string and deserialize fnStr into a new Function // on worker this.fn = getFunctionBody(this.fn.toString()); } } const defaultBenchOptions = { // 0.05s - Arbitrary number used in some benchmark tools minTime: 0.05, // 0.5s - Arbitrary number used in some benchmark tools maxTime: 0.5, // Number of times the benchmark will be repeated repeatSuite: 1, // Number minimum of samples the each round minSamples: 10, }; // Minimum repeatSuite runs required for reliable t-test results const MIN_REPEAT_FOR_TTEST = 30; function throwIfNoNativesSyntax() { if (process.execArgv.includes("--allow-natives-syntax") === false) { throw new Error( "bench-node module must be run with --allow-natives-syntax argument", ); } } class Suite { #benchmarks; #reporter; #plugins; #useWorkers; #benchmarkMode; #reporterOptions; #minSamples; #repeatSuite; #ttest; #dceDetector; constructor(options = {}) { this.#benchmarks = []; validateObject(options, "options"); if (options?.reporter !== undefined) { if (options?.reporter !== false && options?.reporter !== null) { validateFunction(options.reporter, "reporter"); } this.#reporter = options.reporter; } else if (options?.pretty === true) { this.#reporter = prettyReport; } else { this.#reporter = textReport; } this.#useWorkers = options.useWorkers || false; // DCE detection is opt-in to avoid breaking changes const dceEnabled = options.detectDeadCodeElimination === true; if (dceEnabled) { this.#dceDetector = new DeadCodeEliminationDetectionPlugin( options.dceThreshold ? { threshold: options.dceThreshold } : {}, ); } // Plugin setup: If DCE detection is enabled, default to no plugins (allow optimization) // Otherwise, use V8NeverOptimizePlugin as the default if (options?.plugins) { validateArray(options.plugins, "plugin"); validatePlugins(options.plugins); this.#plugins = options.plugins; } else if (dceEnabled) { // DCE detection requires optimization to be enabled, so no default plugins this.#plugins = []; } else { // Default behavior - use V8NeverOptimizePlugin this.#plugins = [new V8NeverOptimizePlugin()]; } this.#benchmarkMode = options.benchmarkMode || "ops"; validateBenchmarkMode(this.#benchmarkMode, "options.benchmarkMode"); this.#reporterOptions = options.reporterOptions || { printHeader: true, }; if (options.ttest !== undefined) { validateBoolean(options.ttest, "options.ttest"); } this.#ttest = options.ttest ?? false; let repeatSuite = defaultBenchOptions.repeatSuite; if (options.repeatSuite !== undefined) { validateNumber(options.repeatSuite, "options.repeatSuite", 1); repeatSuite = options.repeatSuite; } else if (this.#ttest) { repeatSuite = MIN_REPEAT_FOR_TTEST; } this.#repeatSuite = repeatSuite; if (this.#ttest) { this.#reporterOptions.ttest = true; } let minSamples = defaultBenchOptions.minSamples; if (options.minSamples !== undefined) { validateNumber(options.minSamples, "options.minSamples", 1); minSamples = options.minSamples; } this.#minSamples = minSamples; } add(name, options, fn) { validateString(name, "name"); if (typeof options === "function") { fn = options; options = { ...defaultBenchOptions, minSamples: this.#minSamples, repeatSuite: this.#repeatSuite, }; } else { validateObject(options, "options"); options = { ...defaultBenchOptions, minSamples: this.#minSamples, repeatSuite: this.#repeatSuite, ...options, }; // Enforce strict minimum (> 1e-6s). Using EPSILON to make boundary exclusive. validateNumber( options.minTime, "options.minTime", timer.resolution * 1e3 + Number.EPSILON, ); validateNumber(options.maxTime, "options.maxTime", options.minTime); validateNumber(options.repeatSuite, "options.repeatSuite", 1); validateNumber(options.minSamples, "options.minSamples", 1); } validateFunction(fn, "fn"); const { baseline = false } = options || {}; if (baseline && this.#benchmarks.some((b) => b.baseline)) { throw new Error("There is already a baseline benchmark"); } const benchmark = new Benchmark( name, fn, options.minTime, options.maxTime, this.#plugins, options.repeatSuite, options.minSamples, baseline, ); this.#benchmarks.push(benchmark); return this; } async run() { throwIfNoNativesSyntax(); const results = new Array(this.#benchmarks.length); // Measure baseline for DCE detection (only in ops mode, not in worker mode) if ( this.#dceDetector && !this.#useWorkers && this.#benchmarkMode === "ops" ) { await this.#measureBaseline(); } // It doesn't make sense to warmup a fresh new instance of Worker. // TODO: Should this be folded into the main loop? if (!this.#useWorkers) { // This is required to avoid variance on first benchmark run for (let i = 0; i < this.#benchmarks.length; ++i) { const benchmark = this.#benchmarks[i]; debugBench( `Warmup ${benchmark.name} with minTime=${benchmark.minTime}, maxTime=${benchmark.maxTime}`, ); const initialIteration = await getInitialIterations(benchmark); await runWarmup(benchmark, initialIteration, { minTime: 0.005, maxTime: 0.05, }); } } for (let i = 0; i < this.#benchmarks.length; ++i) { const benchmark = this.#benchmarks[i]; // Add DCE detector to benchmark plugins if enabled if (this.#dceDetector && this.#benchmarkMode === "ops") { const originalPlugins = benchmark.plugins; benchmark.plugins = [...benchmark.plugins, this.#dceDetector]; // Regenerate function string with new plugins benchmark.fnStr = createFnString(benchmark); } // Warmup is calculated to reduce noise/bias on the results const initialIterations = await getInitialIterations(benchmark); debugBench( `Starting ${benchmark.name} with mode=${this.#benchmarkMode}, minTime=${benchmark.minTime}, maxTime=${benchmark.maxTime}, repeatSuite=${benchmark.repeatSuite}, minSamples=${benchmark.minSamples}`, ); let result; if (this.#useWorkers) { if (this.#benchmarkMode === "time") { console.warn( "Warning: Worker mode currently doesn't fully support 'time' benchmarkMode.", ); } result = await this.runWorkerBenchmark(benchmark, initialIterations); } else { result = await runBenchmark( benchmark, initialIterations, this.#benchmarkMode, benchmark.repeatSuite, benchmark.minSamples, ); } results[i] = result; } if (this.#reporter) { this.#reporter(results, this.#reporterOptions); } // Emit DCE warnings after reporting if (this.#dceDetector) { this.#dceDetector.emitWarnings(); } return results; } async #measureBaseline() { debugBench("Measuring baseline for DCE detection..."); // Create a minimal baseline benchmark (empty function) const baselineBench = new Benchmark( "__baseline__", () => {}, 0.01, // minTime 0.05, // maxTime this.#plugins, 1, // repeatSuite 10, // minSamples ); const initialIterations = await getInitialIterations(baselineBench); const result = await runBenchmark( baselineBench, initialIterations, "ops", 1, 10, ); const baselineTimePerOp = (1 / result.opsSec) * 1e9; // Convert to ns debugBench(`DCE baseline: ${timer.format(baselineTimePerOp)}/iter`); this.#dceDetector.setBaseline(baselineTimePerOp); } async runWorkerBenchmark(benchmark, initialIterations) { return new Promise((resolve, reject) => { const workerPath = path.resolve(__dirname, "./worker-runner.js"); const worker = new Worker(workerPath); benchmark.serializeBenchmark(); worker.postMessage({ benchmark, initialIterations, benchmarkMode: this.#benchmarkMode, // Pass suite mode repeatSuite: benchmark.repeatSuite, minSamples: benchmark.minSamples, }); worker.on("message", (result) => { resolve(result); worker.terminate(); }); worker.on("error", (error) => { reject(error); worker.terminate(); }); worker.on("exit", (code) => { if (code !== 0) reject(new Error(`Worker stopped with exit code ${code}`)); }); }); } } module.exports = { Suite, V8NeverOptimizePlugin, V8GetOptimizationStatus, V8OptimizeOnNextCallPlugin, MemoryPlugin, DeadCodeEliminationDetectionPlugin, chartReport, textReport, prettyReport, htmlReport, jsonReport, csvReport, // Statistical utilities welchTTest, compareBenchmarks, getSignificanceStars, }; ================================================ FILE: lib/lifecycle.js ================================================ const { clockBenchmark, debugBench, MIN_RESOLUTION, timer, } = require("./clock"); const { StatisticalHistogram } = require("./histogram"); /** * @param {number} durationPerOp The amount of time each operation takes, in timer.scale * @param {number} targetTime The amount of time we want the benchmark to execute, in seconds * @return {number} - a suggested iteration count >= 1 */ function getItersForOpDuration(durationPerOp, targetTime) { const secondsPerOp = durationPerOp / timer.scale; const opsForTargetTime = Math.round(targetTime / secondsPerOp); return Math.min(Number.MAX_SAFE_INTEGER, Math.max(1, opsForTargetTime)); } function parsePluginsResult(plugins, name) { const result = []; for (const p of plugins) { result.push({ name: p.toString(), result: p.getResult?.(name) ?? "enabled", report: p.getReport?.(name) ?? "", }); } return result; } /** * Calculates and returns the initial number of iterations for a benchmark * @param {import('./index').Benchmark} bench - The benchmark object to be executed * @returns {Promise} The calculated number of initial iterations */ async function getInitialIterations(bench) { const { 0: duration, 1: realIterations } = await clockBenchmark(bench, 30); // Just to avoid issues with empty fn const durationPerOp = Math.max(MIN_RESOLUTION, duration / realIterations); debugBench( `Duration per operation on initial count: ${timer.format(durationPerOp)}`, ); // TODO: is this a correct assumpion? if (durationPerOp > bench.maxTime * timer.scale) process.emitWarning( `The benchmark "${bench.name}" has a duration per operation greater than the maxTime.`, ); return getItersForOpDuration(durationPerOp, bench.minTime); } /** * Executes the warmup phase of a benchmark * @param {import('./index').Benchmark} bench - The benchmark object to be executed * @param {number} initialIterations - The initial number of iterations to run * @param {Object} options - Warmup options * @param {number} [options.minTime] - Minimum time for warmup, in seconds. Defaults to bench.minTime * @param {number} [options.maxTime] - Maximum time for warmup, in seconds. Defaults to bench.minTime * @returns {Promise} */ async function runWarmup(bench, initialIterations, { minTime, maxTime }) { minTime = minTime ?? bench.minTime; maxTime = maxTime ?? bench.minTime; const maxDuration = maxTime * timer.scale; const minSamples = 10; let iterations = 0; let timeSpent = 0; let samples = 0; while (timeSpent < maxDuration || samples <= minSamples) { const { 0: duration, 1: realIterations } = await clockBenchmark( bench, initialIterations, ); timeSpent += duration; iterations += realIterations; iterations = Math.min(Number.MAX_SAFE_INTEGER, iterations); // Just to avoid issues with empty fn const durationPerOp = Math.max(MIN_RESOLUTION, duration / realIterations); const remainingTime = Math.max(0, (maxDuration - timeSpent) / timer.scale); const targetTime = Math.min(remainingTime, minTime); initialIterations = getItersForOpDuration(durationPerOp, targetTime); samples++; } } async function runBenchmarkOnce( bench, histogram, { initialIterations, maxDuration, minSamples }, benchmarkMode = "ops", ) { let iterations = 0; let timeSpent = 0; // For time mode, we want to run the benchmark exactly once if (benchmarkMode === "time") { const { 0: duration, 1: realIterations } = await clockBenchmark(bench, 1); timeSpent = duration; iterations = realIterations; // Record the duration in the histogram histogram.record(duration); return { iterations, timeSpent }; } // Ops mode - run the sampling loop while (timeSpent < maxDuration || histogram.samples.length <= minSamples) { const { 0: duration, 1: realIterations } = await clockBenchmark( bench, initialIterations, ); timeSpent += duration; iterations = Math.min(Number.MAX_SAFE_INTEGER, iterations + realIterations); // Just to avoid issues with empty fn const durationPerOp = Math.max(MIN_RESOLUTION, duration / realIterations); histogram.record(durationPerOp); const remainingTime = Math.max(0, (maxDuration - timeSpent) / timer.scale); const targetTime = Math.min(remainingTime, bench.minTime); initialIterations = getItersForOpDuration(durationPerOp, targetTime); } return { iterations, timeSpent }; } /** * Executes a benchmark with the specified parameters * @param {import('./index').Benchmark} bench - The benchmark object to be executed * @param {number} initialIterations - The initial number of iterations to run * @param {string} benchmarkMode - The benchmark mode ('ops' or 'time') * @param {number} repeatSuite - Number of times to repeat the benchmark suite * @param {number} minSamples - Minimum number of samples to collect * @returns {Promise} The benchmark results containing operations per second or total time, iterations, histogram data and plugin results */ async function runBenchmark( bench, initialIterations, benchmarkMode, repeatSuite, minSamples, ) { const histogram = new StatisticalHistogram(); const maxDuration = bench.maxTime * timer.scale; let totalIterations = 0; let totalTimeSpent = 0; const opsSecPerRun = []; for (let i = 0; i < repeatSuite; ++i) { const { iterations, timeSpent } = await runBenchmarkOnce( bench, histogram, { initialIterations, maxDuration, minSamples, }, benchmarkMode, ); const runOpsSec = iterations / (timeSpent / timer.scale); opsSecPerRun.push(runOpsSec); totalTimeSpent += timeSpent; totalIterations += iterations; } histogram.finish(); const totalTime = totalTimeSpent / timer.scale; // Convert ns to seconds const opsSec = totalIterations / totalTime; const sampleData = histogram.samples; const result = { iterations: totalIterations, // StatisticalHistogram is not a serializable object, keep raw ns for min/max histogram: { samples: sampleData.length, min: histogram.min, max: histogram.max, sampleData, }, name: bench.name, plugins: parsePluginsResult(bench.plugins, bench.name), baseline: bench.baseline, }; // Add the appropriate metric based on the benchmark mode if (benchmarkMode === "time") { result.totalTime = totalTime / repeatSuite; // Average time per repeat debugBench( `${bench.name} completed ${repeatSuite} repeats with average time ${result.totalTime.toFixed(6)} seconds`, ); } else { result.opsSec = opsSec; result.opsSecPerRun = opsSecPerRun; debugBench( `${bench.name} completed ${sampleData.length} samples with ${opsSec.toFixed(2)} ops/sec`, ); } // since the instance is shared across benchmarks, reset it after use for (const plugin of bench.plugins) { plugin.reset?.(); } return result; } module.exports = { getInitialIterations, runBenchmark, runWarmup, }; ================================================ FILE: lib/plugins/dce-detection.js ================================================ const { timer } = require("../clock"); /** * Plugin that detects potential dead code elimination (DCE) in benchmarks. * Compares benchmark timings against a baseline (empty function) to identify * suspiciously fast benchmarks that may have been optimized away by the JIT. * * This helps educate users about benchmark quality and potential issues. */ class DeadCodeEliminationDetectionPlugin { #baselineTimePerOp = null; #warnings = new Map(); #threshold = 10; // Warn if benchmark is less than 10x slower than baseline constructor(options = {}) { // Allow customizing the threshold if (options.threshold !== undefined) { this.#threshold = options.threshold; } } isSupported() { return true; // Works everywhere } /** * Stores baseline measurement for comparison * @param {number} timePerOp - Time per operation in nanoseconds */ setBaseline(timePerOp) { this.#baselineTimePerOp = timePerOp; } /** * Called after each benchmark completes to check for DCE */ onCompleteBenchmark(result, bench) { if (this.#baselineTimePerOp === null) { // No baseline yet, skip detection return; } const [duration, iterations] = result; const timePerOp = duration / iterations; // Check if this benchmark is suspiciously fast compared to baseline if (timePerOp < this.#baselineTimePerOp * this.#threshold) { this.#warnings.set(bench.name, { timePerOp, baselineTime: this.#baselineTimePerOp, ratio: timePerOp / this.#baselineTimePerOp, }); } } /** * Get warning for a specific benchmark */ getWarning(benchmarkName) { return this.#warnings.get(benchmarkName); } /** * Get all warnings */ getAllWarnings() { return Array.from(this.#warnings.entries()).map(([name, data]) => ({ name, ...data, })); } /** * Check if a benchmark has a warning */ hasWarning(benchmarkName) { return this.#warnings.has(benchmarkName); } /** * Emit warnings to console */ emitWarnings() { if (this.#warnings.size === 0) { return; } console.log("\n⚠️ Dead Code Elimination Warnings:"); console.log( "The following benchmarks may have been optimized away by the JIT compiler:\n", ); for (const [name, data] of this.#warnings.entries()) { console.log(` • ${name}`); console.log(` Benchmark: ${timer.format(data.timePerOp)}/iter`); console.log(` Baseline: ${timer.format(data.baselineTime)}/iter`); console.log(` Ratio: ${data.ratio.toFixed(2)}x of baseline`); console.log( " Suggestion: Ensure the result is used or assign to a variable\n", ); } console.log( "ℹ️ These benchmarks are running nearly as fast as an empty function,", ); console.log( " which suggests the JIT may have eliminated the actual work.\n", ); } toString() { return "DeadCodeEliminationDetectionPlugin"; } reset() { // Don't reset - we want to accumulate warnings across all benchmarks // They will be emitted once at the end of the suite run } } module.exports = { DeadCodeEliminationDetectionPlugin, }; ================================================ FILE: lib/plugins/memory.js ================================================ const { StatisticalHistogram } = require("../histogram"); /** * Formats a byte value into a human-readable string with appropriate units (B, Kb, MB, GB) * @param {number} bytes - The number of bytes to format * @returns {string} Formatted string with appropriate unit suffix */ function formatBytes(bytes) { if (bytes < 1024) return `${Math.round(bytes)}B`; const kbytes = bytes / 1024; if (kbytes < 1024) return `${kbytes.toFixed(2)}Kb`; const mbytes = kbytes / 1024; if (mbytes < 1024) return `${mbytes.toFixed(2)}MB`; const gbytes = mbytes / 1024; return `${gbytes.toFixed(2)}GB`; } /** * Plugin that measures memory usage during benchmark execution * Collects heap usage statistics and provides reporting capabilities */ class MemoryPlugin { static MEMORY_BEFORE_RUN = "memoryBeforeRun"; static MEMORY_AFTER_RUN = "memoryAfterRun"; static #WARNING_REPORTED = false; /** * @type {StatisticalHistogram} */ #heapUsedHistogram = new StatisticalHistogram(); isSupported() { return typeof globalThis.gc === "function"; } beforeClockTemplate({ managed, context }) { if (managed && !MemoryPlugin.#WARNING_REPORTED) { MemoryPlugin.#WARNING_REPORTED = true; process.emitWarning( "The memory statistics can be inaccurate since it will include the tear-up and teardown of your benchmark.", ); } let code = ""; code += `${context}.${MemoryPlugin.MEMORY_BEFORE_RUN} = 0;\n`; code += `${context}.${MemoryPlugin.MEMORY_AFTER_RUN} = 0;\n`; code += "globalThis.gc();\n"; code += `${context}.${MemoryPlugin.MEMORY_BEFORE_RUN} = globalThis.process.memoryUsage();\n`; return [code]; } afterClockTemplate({ context }) { return [ `${context}.${MemoryPlugin.MEMORY_AFTER_RUN} = globalThis.process.memoryUsage();\n`, ]; } onCompleteBenchmark([, realIterations, context]) { const heapUsed = context[MemoryPlugin.MEMORY_AFTER_RUN].heapUsed - context[MemoryPlugin.MEMORY_BEFORE_RUN].heapUsed; const externalUsed = context[MemoryPlugin.MEMORY_AFTER_RUN].external - context[MemoryPlugin.MEMORY_BEFORE_RUN].external; const memoryAllocated = (heapUsed + externalUsed) / realIterations; // below 0, we just coerce to be zero this.#heapUsedHistogram.record(Math.max(0, memoryAllocated)); } toString() { return "MemoryPlugin"; } getReport() { this.#heapUsedHistogram.finish(); return `heap usage=${formatBytes(this.#heapUsedHistogram.mean)} (${formatBytes(this.#heapUsedHistogram.min)} ... ${formatBytes(this.#heapUsedHistogram.max)})`; } getResult() { return { proto: null, type: this.toString(), histogram: this.#heapUsedHistogram, }; } reset() { this.#heapUsedHistogram = new StatisticalHistogram(); } } module.exports = { MemoryPlugin, }; ================================================ FILE: lib/plugins/v8-never-opt.js ================================================ class V8NeverOptimizePlugin { isSupported() { try { new Function("%NeverOptimizeFunction(() => {})")(); return true; } catch (e) { return false; } } beforeClockTemplate({ bench }) { let code = ""; code += ` function DoNotOptimize(x) {} // Prevent the benchmark function and result consumer from optimizing or being inlined. %NeverOptimizeFunction(${bench}.fn); %NeverOptimizeFunction(DoNotOptimize); `; return [code, "DoNotOptimize"]; } toString() { return "V8NeverOptimizePlugin"; } getReport() { return "v8-never-optimize=true"; } } module.exports = { V8NeverOptimizePlugin, }; ================================================ FILE: lib/plugins/v8-opt.js ================================================ class V8OptimizeOnNextCallPlugin { isSupported() { try { new Function("%OptimizeFunctionOnNextCall(() => {})")(); return true; } catch (e) { return false; } } beforeClockTemplate({ awaitOrEmpty, bench, timer }) { let code = ""; code += `%OptimizeFunctionOnNextCall(${bench}.fn);\n`; code += `${awaitOrEmpty}${bench}.fn(${timer});\n`; code += `${awaitOrEmpty}${bench}.fn(${timer});\n`; return [code]; } getReport() { return "v8-optimize-next-call=enabled"; } toString() { return "V8OptimizeOnNextCallPlugin"; } } module.exports = { V8OptimizeOnNextCallPlugin, }; ================================================ FILE: lib/plugins/v8-print-status.js ================================================ function checkBitmap(value, bit) { return (value & bit) === bit; } function translateStatus(optStatus) { if (optStatus === -1) { return "unknown"; } const optStat = []; if (checkBitmap(optStatus, 2)) { optStat.push("Never Optimized"); } if (checkBitmap(optStatus, 4)) { optStat.push("Always Optimized"); } if (checkBitmap(optStatus, 8)) { optStat.push("Maybe Deopted"); } if (checkBitmap(optStatus, 16)) { optStat.push("Optimized"); } if (checkBitmap(optStatus, 32)) { optStat.push("TurboFanned"); } if (checkBitmap(optStatus, 64)) { optStat.push("Interpreted"); } if (checkBitmap(optStatus, 128)) { optStat.push("Marked for Optimization"); } if (checkBitmap(optStatus, 256)) { optStat.push("Marked for Concurrent Optimization"); } if (checkBitmap(optStatus, 512)) { optStat.push("Concurrently Optimizing"); } if (checkBitmap(optStatus, 1024)) { optStat.push("Is Executing"); } if (checkBitmap(optStatus, 2048)) { optStat.push("Topmost frame is Turbo Fanned"); } if (checkBitmap(optStatus, 4096)) { optStat.push("Lite Mode"); } if (checkBitmap(optStatus, 8192)) { optStat.push("Marked for de-optimization"); } return optStat.join(", "); } class V8GetOptimizationStatus { #optimizationStatuses = []; isSupported() { try { new Function("%GetOptimizationStatus(() => {})")(); return true; } catch (e) { return false; } } afterClockTemplate({ bench, context }) { let code = ""; code += `${context}.v8OptimizationStatus = %GetOptimizationStatus(${bench}.fn);\n`; return [code]; } onCompleteBenchmark(result) { const context = result[2]; this.#optimizationStatuses.push(context.v8OptimizationStatus); } toString() { return "V8GetOptimizationStatus"; } getReport() { const allAvailableStatus = this.#optimizationStatuses.reduce( (acc, v) => acc | v, 0, ); return `v8-opt-status="${translateStatus(allAvailableStatus)}"`; } getResult() { const allAvailableStatus = this.#optimizationStatuses.reduce( (acc, v) => acc | v, 0, ); return { type: this.toString(), optimizationStatuses: translateStatus(allAvailableStatus), }; } reset() { this.#optimizationStatuses = []; } } module.exports = { V8GetOptimizationStatus, }; ================================================ FILE: lib/plugins.js ================================================ const { V8OptimizeOnNextCallPlugin } = require("./plugins/v8-opt"); const { V8NeverOptimizePlugin } = require("./plugins/v8-never-opt"); const { V8GetOptimizationStatus } = require("./plugins/v8-print-status"); const { MemoryPlugin } = require("./plugins/memory"); const { DeadCodeEliminationDetectionPlugin, } = require("./plugins/dce-detection"); const { validateFunction, validateArray } = require("./validators"); /** * Validates that all plugins in the array implement the required plugin interface * and that they are supported in the current environment * @param {Array} plugins - Array of plugin objects to validate * @throws {Error} If any plugin doesn't meet the requirements or isn't supported */ function validatePlugins(plugins) { for (const p of plugins) { validateFunction(p.isSupported, "Plugins must have a isSupported method."); validateFunction(p.toString, "Plugins must have a toString() method."); if (!p.isSupported()) { throw new Error(`Plugin: ${p.toString()} is not supported.`); } if (typeof p.beforeClockTemplate === "function") { const result = p.beforeClockTemplate({ bench: "", awaitOrEmpty: "", context: "", timer: "", }); validateArray(result, `${p.toString()}.beforeClockTemplate()`); } if (typeof p.afterClockTemplate === "function") { const result = p.afterClockTemplate({ bench: "", awaitOrEmpty: "", context: "", timer: "", }); validateArray(result, `${p.toString()}.afterClockTemplate()`); } } } module.exports = { MemoryPlugin, V8NeverOptimizePlugin, V8GetOptimizationStatus, V8OptimizeOnNextCallPlugin, DeadCodeEliminationDetectionPlugin, validatePlugins, }; ================================================ FILE: lib/report.js ================================================ const textReport = require("./reporter/text"); const prettyReport = require("./reporter/pretty"); const chartReport = require("./reporter/chart"); const htmlReport = require("./reporter/html"); const jsonReport = require("./reporter/json"); const csvReport = require("./reporter/csv"); /** * @typedef {Object} BenchmarkResult * @property {string} name - The name of the benchmark * @property {number} opsSec - Operations per second * @property {number} iterations - Total number of iterations run * @property {Object} histogram - Statistical data about the benchmark runs * @property {Array} plugins - Results from plugins used during benchmarking */ /** * Exports various report generators for benchmark results * @type {Object} * @property {function(BenchmarkResult[]): void} chartReport - Generates a chart visualization of benchmark results * @property {function(BenchmarkResult[]): void} textReport - Generates a text report of benchmark results * @property {function(BenchmarkResult[]): void} htmlReport - Generates an HTML report of benchmark results * @property {function(BenchmarkResult[]): string} jsonReport - Generates a JSON report of benchmark results * @property {function(BenchmarkResult[]): string} csvReport - Generates a CSV report of benchmark results */ module.exports = { ...chartReport, ...textReport, ...prettyReport, ...htmlReport, ...jsonReport, ...csvReport, }; ================================================ FILE: lib/reporter/chart.js ================================================ const { platform, arch, availableParallelism, totalmem } = require("node:os"); const { styleText } = require("../utils/styleText"); const { analyze } = require("../utils/analyze.js"); /** * Draws a bar chart representation of a benchmark result * @param {string} label - The label for the bar (benchmark name) * @param {number} value - The value to display (operations per second or time per operation) * @param {number} total - The maximum value in the dataset (for scaling) * @param {number} samples - Number of samples collected * @param {string} metric - The metric being displayed (opsSec or totalTime) * @param {number} width - Length of the bar in characters * @param {string} [comment=""] - optional additional comment */ function drawBar(label, value, total, samples, metric, width, comment = "") { let percentage; let displayedValue; let displayedMetric; if (metric === "opsSec") { percentage = value / total; // Higher ops/sec is better const valueReported = value < 100 ? Number(value.toFixed(2)) : Math.round(value); displayedValue = styleText(["yellow"], formatter.format(valueReported)); displayedMetric = "ops/sec"; } else { // metric === 'totalTime' percentage = 1 - value / total; // Lower totalTime is better, invert percentage let timeFormatted; if (value < 0.000001) { // Less than 1 microsecond, show in nanoseconds timeFormatted = `${(value * 1000000000).toFixed(2)} ns`; } else if (value < 0.001) { // Less than 1 millisecond, show in microseconds timeFormatted = `${(value * 1000000).toFixed(2)} µs`; } else if (value < 1) { // Less than 1 second, show in milliseconds timeFormatted = `${(value * 1000).toFixed(2)} ms`; } else { // 1 second or more, show in seconds timeFormatted = `${value.toFixed(2)} s`; } displayedValue = styleText(["yellow"], timeFormatted); displayedMetric = "total time"; } const ratio = width * percentage; const filledLength = Math.floor(ratio); const fraction = ratio % 1; const partial = fraction >= 0.5 ? "▌" : ""; const bar = "█".repeat(filledLength) + partial + "─".repeat(width - filledLength - partial.length); const displayedSamples = `${styleText(["yellow"], samples.toString().padStart(2))} samples`; return `${label} ▏${bar}▕ ${displayedValue} ${displayedMetric} | ${displayedSamples} ${comment}\n`; } const formatter = Intl.NumberFormat(undefined, { notation: "standard", maximumFractionDigits: 2, }); const timer = Intl.NumberFormat(undefined, { minimumFractionDigits: 3, maximumFractionDigits: 3, }); const environment = { nodeVersion: `Node.js version: ${process.version}`, platform: `${platform()} ${arch()}`, hardware: `${availableParallelism()} vCPUs | ${(totalmem() / 1024 ** 3).toFixed(1)}GB Mem`, }; /** * Outputs a chart visualization of benchmark results in the console * Displays system information and a bar chart of operations per second or time per operation * @param {BenchmarkResult[]} results - Array of benchmark results * @param options {object} layout options */ function chartReport(results, options = { labelWidth: 45, printHeader: true }) { process.stdout.write(toChart(results, options)); } /** * Generates a chart visualization of benchmark results in the console * Displays system information and a bar chart of operations per second or time per operation * @param {BenchmarkResult[]} results - Array of benchmark results * @param options {object} layout options */ function toChart( results, options = { labelWidth: 45, printHeader: true, barWidth: 25 }, ) { // Determine the primary metric and calculate max value for scaling const primaryMetric = results[0]?.opsSec !== undefined ? "opsSec" : "totalTime"; const maxValue = Math.max(...results.map((b) => b[primaryMetric])); let text = ""; if (options.printHeader) { text += `${environment.nodeVersion}\n`; text += `Platform: ${environment.platform}\n`; text += `CPU Cores: ${environment.hardware}\n\n`; } const hasBaseline = results.find((result) => result.baseline) !== undefined; results = analyze(results, false); if (hasBaseline) { text += styleText("bold", "\nSummary (vs. baseline):\n"); } const maxNameLength = Math.max(...results.map((r) => r.name.length)); const columnWidth = Math.max(maxNameLength, options.labelWidth ?? 45); for (const result of results) { let comment = ""; if (hasBaseline) { if (result.baseline) { comment = styleText("magenta", "(baseline)"); } else { const isSignificant = result.significanceTest?.significant ?? true; const isFaster = !result.comparison.startsWith("-"); const comparisonText = isFaster ? `(${result.comparison}x faster)` : `(${result.comparison.slice(1)}x slower)`; if (isSignificant) { comment = styleText(isFaster ? "green" : "red", comparisonText); } else { comment = styleText("dim", comparisonText); } } } text += drawBar( result.name.padEnd(columnWidth), result[primaryMetric], maxValue, result.histogram.samples, primaryMetric, options.barWidth ?? 25, comment, ); } return text; } module.exports = { chartReport, toChart, }; ================================================ FILE: lib/reporter/csv.js ================================================ const { summarize } = require("../utils/analyze"); const formatter = Intl.NumberFormat(undefined, { notation: "standard", maximumFractionDigits: 2, }); /** * Outputs JSON formatted results the console * @param {BenchmarkResult[]} results - Array of benchmark results */ function csvReport(results) { process.stdout.write(toCSV(results)); } /** * Outputs CSV formatted results as a string * @param {BenchmarkResult[]} results - Array of benchmark results * @returns {string} */ function toCSV(results) { const primaryMetric = results[0]?.opsSec !== undefined ? "opsSec" : "totalTime"; let text = `name,${primaryMetric === "opsSec" ? "ops/sec" : "total time"},samples,plugins,min,max\n`; const output = summarize(results); for (const entry of output) { text += `${entry.name},`; if (primaryMetric === "opsSec") { text += `"${formatter.format(entry.opsSec)}",`; } else { text += `${entry.totalTimeFormatted},`; } text += `${entry.runsSampled},`; text += `"${entry.plugins.join(",")}",`; // For test compatibility, format min/max in microseconds const minInUs = entry.min / 1000; const maxInUs = entry.max / 1000; text += `${minInUs.toFixed(2)}us,`; text += `${maxInUs.toFixed(2)}us\n`; } return text; } module.exports = { csvReport, toCSV, }; ================================================ FILE: lib/reporter/html.js ================================================ const { platform, arch, availableParallelism, totalmem } = require("node:os"); const fs = require("node:fs"); const path = require("node:path"); const { summarize } = require("../utils/analyze"); const { timer } = require("../clock"); const formatter = Intl.NumberFormat(undefined, { notation: "standard", maximumFractionDigits: 3, }); const timeNumberFormat = Intl.NumberFormat(undefined, { minimumFractionDigits: 3, maximumFractionDigits: 3, }); const valueToDuration = (maxValue, value, isTimeBased, scalingFactor = 10) => { const normalizedValue = isTimeBased ? maxValue / value : value / maxValue; const baseSpeed = (1 / normalizedValue) * scalingFactor; return Math.max(baseSpeed, 2); // Normalize speed with a minimum of 2 seconds }; const generateHTML = (template, durations) => { let css = ""; let labelDiv = ""; let circleDiv = ""; let position = 20; const colors = [ "blue", "orange", "yellow", "purple", "black", "grey", "red", "green", "pink", "cyan", ]; for (const d of durations) { css += ` #label-${d.name} { top: ${position}px; } #circle-${d.name} { background-color: ${colors.shift()}; top: ${position}px; } `; circleDiv += `
${d.name}(${d.metricValueFormatted} ${d.metricUnit})
min: ${d.minFormatted}, max: ${d.maxFormatted}
`; labelDiv += `
`; position += 80; } const environmentDiv = `

Node.js version: ${process.version}

Platform: ${platform()} ${arch()}

CPU Cores: ${availableParallelism()} vCPUs | ${(totalmem() / 1024 ** 3).toFixed(1)}GB Mem

`; return template .replaceAll("{{CONTAINER_HEIGHT}}", `${durations.length * 100}px;`) .replaceAll("{{CSS}}", css) .replaceAll("{{ENVIRONMENT_DIV}}", environmentDiv) .replaceAll("{{LABEL_DIV}}", labelDiv) .replaceAll("{{CIRCLE_DIV}}", circleDiv) .replaceAll("{{DURATIONS}}", JSON.stringify(durations)); }; const templatePath = path.join(__dirname, "template.html"); const template = fs.readFileSync(templatePath, "utf8"); function htmlReport(results) { const summary = summarize(results); const primaryMetric = results[0]?.opsSec !== undefined ? "opsSec" : "totalTime"; let durations; if (primaryMetric === "opsSec") { const maxOpsSec = Math.max(...summary.map((b) => b.opsSec)); durations = summary.map((r) => ({ name: r.name.replaceAll(" ", "-"), duration: valueToDuration(maxOpsSec, r.opsSec, false), metricValueFormatted: formatter.format(r.opsSec), metricUnit: "ops/sec", minFormatted: timer.format(r.min), // Use timer for ns format maxFormatted: timer.format(r.max), })); } else { // metric === 'totalTime' const maxTotalTime = Math.max(...summary.map((b) => b.totalTime)); durations = summary.map((r) => ({ name: r.name.replaceAll(" ", "-"), duration: valueToDuration(maxTotalTime, r.totalTime, true), metricValueFormatted: r.totalTimeFormatted, metricUnit: "total time", minFormatted: timer.format(r.min), // Use timer for ns format maxFormatted: timer.format(r.max), })); } const htmlContent = generateHTML(template, durations); fs.writeFileSync("result.html", htmlContent, "utf8"); process.stdout.write("HTML file has been generated: result.html\n"); } module.exports = { htmlReport, }; ================================================ FILE: lib/reporter/json.js ================================================ const { summarize } = require("../utils/analyze"); /** * Outputs JSON formatted results the console * @param {BenchmarkResult[]} results - Array of benchmark results */ function jsonReport(results) { process.stdout.write(toJSON(results)); } /** * Outputs JSON formatted results as a string * @param {BenchmarkResult[]} results - Array of benchmark results * @returns {string} */ function toJSON(results) { const output = summarize(results).map((result) => ({ name: result.name, runsSampled: result.runsSampled, min: result.minFormatted, minNS: result.min, max: result.maxFormatted, maxNS: result.max, plugins: result.plugins.map((p) => p.report).filter(Boolean), opsSec: result.opsSec, totalTime: result.totalTime, totalTimeFormatted: result.totalTimeFormatted, })); return JSON.stringify(output, null, 2); } module.exports = { jsonReport, toJSON, }; ================================================ FILE: lib/reporter/pretty.js ================================================ const { styleText } = require("../utils/styleText"); const os = require("node:os"); const { analyze, summarize } = require("../utils/analyze.js"); const formatter = Intl.NumberFormat(undefined, { notation: "standard", maximumFractionDigits: 2, }); const BOX_VERTICAL = "│"; const BOX_HORIZONTAL = "─"; const BOX_CORNER_BOTTOM_LEFT = "└"; const BOX_TEE_RIGHT = "├"; /** * Pretty print format a report. * @param {BenchmarkResult[]} results */ function prettyReport(results, options = {}) { process.stdout.write(toPretty(results, options)); } /** * Pretty print format a report. * @param {BenchmarkResult[]} results * @returns {string} the formatted report */ function toPretty(results, options = {}) { if (results.length === 0) { return ""; } const cpu = os.cpus()[0]; const ttest = options?.ttest ?? false; let text = styleText("bold", "\nSystem Information:\n"); text += ` Node.js: ${process.version}\n`; text += ` OS: ${os.platform()} ${os.release()}\n`; text += ` CPU: ${cpu.model}\n`; if (ttest) { text += styleText("cyan", "\nT-Test Mode: Enabled (repeatSuite=30)\n"); } const hasBaseline = results.find((result) => result.baseline) !== undefined; if (hasBaseline) { text += `\nLegend: ${styleText("magenta", "■")} Baseline\n\n`; } text += styleText("bold", `Benchmark results (${results.length} total):\n`); const firstResult = results[0]; if (firstResult && firstResult.plugins.length > 0) { const pluginNames = firstResult.plugins.map((p) => p.name).join(", "); text += styleText("dim", `Plugins enabled: ${pluginNames}\n`); } const tree = buildTree(results); text += formatTree(tree); if (hasBaseline) { text += styleText("bold", "\n\nSummary (vs. baseline):\n"); const sortedResults = analyze(results, true, { ttest, alpha: options?.alpha ?? 0.05, }); const maxNameLength = Math.max(...sortedResults.map((r) => r.name.length)); for (const result of sortedResults) { const namePart = ` ${result.name.padEnd(maxNameLength)} `; text += namePart; if (result.baseline) { text += styleText("magenta", "(baseline)"); } else { const isSignificant = result.significanceTest?.significant ?? true; const isFaster = !result.comparison.startsWith("-"); const comparisonText = isFaster ? `(${result.comparison}x faster)` : `(${result.comparison.slice(1)}x slower)`; if (isSignificant) { text += styleText(isFaster ? "green" : "red", comparisonText); } else { text += styleText("dim", comparisonText); } } if (result.significanceTest) { const sig = result.significanceTest; if (sig.stars) { text += styleText("cyan", ` ${sig.stars}`); } } text += "\n"; } if (ttest) { text += styleText( "dim", "\n Significance: * p<0.05, ** p<0.01, *** p<0.001\n", ); } } text += "\n"; return text; } function buildTree(results) { const tree = { children: new Map() }; const summary = summarize(results); for (const result of summary) { const parts = result.name.split("/"); let currentNode = tree; for (let i = 0; i < parts.length; i++) { const part = parts[i]; if (!currentNode.children.has(part)) { currentNode.children.set(part, { children: new Map() }); } currentNode = currentNode.children.get(part); if (i === parts.length - 1) { currentNode.result = result; } } } return tree; } function formatTree(node, prefix = "", isLast = true) { const children = Array.from(node.children.entries()); let tree = ""; for (let i = 0; i < children.length; i++) { const [name, childNode] = children[i]; const isCurrentLast = i === children.length - 1; const connector = isCurrentLast ? `${BOX_CORNER_BOTTOM_LEFT}${BOX_HORIZONTAL}` : `${BOX_TEE_RIGHT}${BOX_HORIZONTAL}`; const prefixStyled = styleText("dim", `${prefix}${connector}`); tree += prefixStyled; if (!childNode.result) { // This is a group tree += ` ${styleText(["bold", "yellow"], name)}`; } else { const style = ["bold"]; if (childNode.result.baseline) { style.push("magenta"); } const nameStyled = ` ${styleText(style, name)}`; tree += nameStyled; const prefixUnstyled = `${prefix}${connector}`; const nameUnstyled = ` ${name}`; const unstyledLength = prefixUnstyled.length + nameUnstyled.length; tree += resultLine(childNode.result, unstyledLength); } tree += "\n"; const newPrefix = `${prefix}${isCurrentLast ? " " : `${BOX_VERTICAL} `}`; if (childNode.children.size > 0) { tree += formatTree(childNode, newPrefix, isCurrentLast); } } return tree; } function resultLine(result, prefixLength) { const PADDING = 55; const padding = PADDING - prefixLength > 0 ? PADDING - prefixLength : 0; let line = " ".repeat(padding); const color = "blue"; if (result.opsSec !== undefined) { line += styleText(["bold"], `${formatter.format(result.opsSec)} ops/sec`); } else if (result.totalTime !== undefined) { line += styleText([color, "bold"], `${result.timeFormatted} total time`); } if (result.runsSampled) { line += ` (${result.runsSampled} runs sampled) `; line += "min..max=("; line += styleText("green", result.minFormatted); line += styleText("dim", "..."); line += styleText("red", result.maxFormatted); line += ")"; } return line; } module.exports = { prettyReport, toPretty, }; ================================================ FILE: lib/reporter/template.html ================================================ Benchmark Visualizer
{{ENVIRONMENT_DIV}}
{{LABEL_DIV}} {{CIRCLE_DIV}}

Benchmark done with bench-node

================================================ FILE: lib/reporter/text.js ================================================ const { styleText } = require("../utils/styleText"); const { analyze, summarize } = require("../utils/analyze.js"); const formatter = Intl.NumberFormat(undefined, { notation: "standard", maximumFractionDigits: 2, }); /** * Generates a text report of benchmark results, displaying each benchmark's name, * operations per second, number of samples, plugin results, and min/max timings * @param {import('../report').BenchmarkResult[]} results - Array of benchmark results * @param options {object} layout options */ function textReport(results, options = {}) { process.stdout.write(toText(results, options)); } function toText(results, options = {}) { const summary = summarize(results); const ttest = options?.ttest ?? false; let text = ""; if (ttest) { text += styleText("cyan", "T-Test Mode: Enabled (repeatSuite=30)\n\n"); } for (const result of summary) { text += result.name.padEnd(options.labelWidth ?? 45); text += " x "; if (result.opsSec !== undefined) { text += styleText(["blue", "bold"], `${localize(result.opsSec)} ops/sec`); } else if (result.totalTime !== undefined) { text += styleText(["blue", "bold"], `${result.timeFormatted} total time`); } // TODO: produce confidence on stddev text += ` (${result.runsSampled} runs sampled) `; for (const p of result.plugins) { if (p.report) { text += styleText("dim", `${p.report} `); } } text += "min..max=("; text += styleText("green", result.minFormatted); text += styleText("dim", "..."); text += styleText("red", result.maxFormatted); text += ")\n"; } const baselineResult = results.find((result) => result.baseline); if (baselineResult) { text += styleText("bold", "\nSummary (vs. baseline):\n"); const sortedResults = analyze(results, true, { ttest, alpha: options?.alpha ?? 0.05, }); const maxNameLength = Math.max(...sortedResults.map((r) => r.name.length)); for (const result of sortedResults) { const namePart = ` ${result.name.padEnd(maxNameLength)} `; text += namePart; if (result.baseline) { text += styleText("magenta", "(baseline)"); } else if (result.comparison !== undefined) { const isSignificant = result.significanceTest?.significant ?? true; const isFaster = !result.comparison.startsWith("-"); const comparisonText = isFaster ? `(${result.comparison}x faster)` : `(${result.comparison.slice(1)}x slower)`; if (isSignificant) { text += styleText(isFaster ? "green" : "red", comparisonText); } else { text += styleText("dim", comparisonText); } if (result.significanceTest) { const sig = result.significanceTest; if (sig.stars) { text += styleText("cyan", ` ${sig.stars}`); } } } text += "\n"; } if (ttest) { text += styleText( "dim", "\n Significance: * p<0.05, ** p<0.01, *** p<0.001\n", ); } } return text; } const numberFormat = new Intl.NumberFormat( process.env.BENCH_NODE_LOCALE_ID || "en-US", { style: "decimal", useGrouping: true, }, ); function localize(number) { return numberFormat.format(number); } module.exports = { textReport, toText, }; ================================================ FILE: lib/utils/analyze.js ================================================ const { timer } = require("../clock"); const { compareBenchmarks } = require("./ttest"); function analyze(results, sorted = true, options = {}) { const { ttest = false, alpha = 0.05 } = options; const baselineResult = results.find((result) => result.baseline); const output = [...results]; if (baselineResult?.opsSec === undefined) { return output; } const baselineHz = baselineResult.opsSec; const baselineSamples = baselineResult.histogram?.sampleData; let min = Number.MAX_SAFE_INTEGER; let max = Number.MIN_SAFE_INTEGER; let fastest; let slowest; for (const result of output) { const benchmarkHz = result.opsSec; if (benchmarkHz === undefined) { continue; } if (benchmarkHz > max) { max = benchmarkHz; fastest = result; } if (benchmarkHz < min) { min = benchmarkHz; slowest = result; } if (!result.baseline) { if (benchmarkHz > baselineHz) { result.comparison = (benchmarkHz / baselineHz).toFixed(2); } else { result.comparison = `-${(baselineHz / benchmarkHz).toFixed(2)}`; } if (ttest) { const resultSamples = result.opsSecPerRun; const baselineSamplesForTest = baselineResult.opsSecPerRun; if ( baselineSamplesForTest?.length >= 30 && resultSamples?.length >= 30 ) { const ttestResult = compareBenchmarks( resultSamples, baselineSamplesForTest, alpha, ); result.significanceTest = { significant: ttestResult.significant, pValue: ttestResult.pValue, confidence: ttestResult.confidence, stars: ttestResult.stars, }; } } } } if (fastest !== undefined) { fastest.fastest = true; } if (slowest !== undefined) { slowest.slowest = true; } if (sorted) { output.sort((a, b) => { if (a.baseline) return -1; if (b.baseline) return 1; return (a.opsSec || 0) - (b.opsSec || 0); }); } return output; } const formatter = Intl.NumberFormat(undefined, { notation: "standard", maximumFractionDigits: 2, }); // Helper function to format time in appropriate units const formatTime = (time) => { if (time < 0.000001) { // Less than 1 microsecond, show in nanoseconds return `${(time * 1000000000).toFixed(2)} ns`; } else if (time < 0.001) { // Less than 1 millisecond, show in microseconds return `${(time * 1000000).toFixed(2)} µs`; } else if (time < 1) { // Less than 1 second, show in milliseconds return `${(time * 1000).toFixed(2)} ms`; } else { // 1 second or more, show in seconds return `${time.toFixed(2)} s`; } }; function summarize(results) { return results.map((result) => { const baseResult = { name: result.name, runsSampled: result.histogram.samples, min: result.histogram.min, max: result.histogram.max, minFormatted: timer.format(result.histogram.min), // Use timer for ns format maxFormatted: timer.format(result.histogram.max), // Report anything the plugins returned plugins: result.plugins.map((p) => p.report).filter(Boolean), }; if (result.opsSec !== undefined) { const opsSecReported = result.opsSec < 100 ? result.opsSec.toFixed(2) : result.opsSec.toFixed(0); baseResult.opsSec = Number(opsSecReported); } else if (result.totalTime !== undefined) { baseResult.totalTime = result.totalTime; // Total time in seconds baseResult.totalTimeFormatted = formatTime(result.totalTime); } return baseResult; }); } module.exports = { analyze, summarize, }; ================================================ FILE: lib/utils/styleText.js ================================================ const util = require("node:util"); module.exports.styleText = typeof util.styleText === "function" ? util.styleText : (_style, text) => text; ================================================ FILE: lib/utils/ttest.js ================================================ // Welch's t-test implementation for benchmark comparison function mean(arr) { if (arr.length === 0) return 0; return arr.reduce((sum, val) => sum + val, 0) / arr.length; } // Sample variance with Bessel's correction function variance(arr, arrMean) { if (arr.length < 2) return 0; const m = arrMean !== undefined ? arrMean : mean(arr); return arr.reduce((sum, val) => sum + (val - m) ** 2, 0) / (arr.length - 1); } // Abramowitz and Stegun approximation function erf(x) { const sign = x >= 0 ? 1 : -1; x = Math.abs(x); const a1 = 0.254829592; const a2 = -0.284496736; const a3 = 1.421413741; const a4 = -1.453152027; const a5 = 1.061405429; const p = 0.3275911; const t = 1.0 / (1.0 + p * x); const y = 1.0 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-x * x); return sign * y; } // Lanczos approximation function lnGamma(z) { const g = 7; const c = [ 0.99999999999980993, 676.5203681218851, -1259.1392167224028, 771.32342877765313, -176.61502916214059, 12.507343278686905, -0.13857109526572012, 9.9843695780195716e-6, 1.5056327351493116e-7, ]; if (z < 0.5) { return Math.log(Math.PI / Math.sin(Math.PI * z)) - lnGamma(1 - z); } z -= 1; let x = c[0]; for (let i = 1; i < g + 2; i++) { x += c[i] / (z + i); } const t = z + g + 0.5; return ( 0.5 * Math.log(2 * Math.PI) + (z + 0.5) * Math.log(t) - t + Math.log(x) ); } // Incomplete beta function for t-distribution CDF function incompleteBeta(a, b, x) { if (x === 0) return 0; if (x === 1) return 1; // Symmetry relation for stability if (x > (a + 1) / (a + b + 2)) { return 1 - incompleteBeta(b, a, 1 - x); } const lnBeta = lnGamma(a) + lnGamma(b) - lnGamma(a + b); const front = Math.exp(Math.log(x) * a + Math.log(1 - x) * b - lnBeta) / a; // Lentz's algorithm const maxIterations = 200; const epsilon = 1e-14; let f = 1; let c = 1; let d = 0; for (let m = 0; m <= maxIterations; m++) { let numerator; if (m === 0) { numerator = 1; } else if (m % 2 === 0) { const k = m / 2; numerator = (k * (b - k) * x) / ((a + 2 * k - 1) * (a + 2 * k)); } else { const k = (m - 1) / 2; numerator = -((a + k) * (a + b + k) * x) / ((a + 2 * k) * (a + 2 * k + 1)); } d = 1 + numerator * d; if (Math.abs(d) < epsilon) d = epsilon; d = 1 / d; c = 1 + numerator / c; if (Math.abs(c) < epsilon) c = epsilon; const delta = c * d; f *= delta; if (Math.abs(delta - 1) < epsilon) { break; } } return front * (f - 1); } function tDistCdf(t, df) { const x = df / (df + t * t); const prob = 0.5 * incompleteBeta(df / 2, 0.5, x); return t >= 0 ? 1 - prob : prob; } function welchTTest(sample1, sample2) { const n1 = sample1.length; const n2 = sample2.length; if (n1 < 2 || n2 < 2) { return { tStatistic: 0, degreesOfFreedom: 0, pValue: 1, significant: false, mean1: n1 > 0 ? mean(sample1) : 0, mean2: n2 > 0 ? mean(sample2) : 0, variance1: 0, variance2: 0, }; } const mean1 = mean(sample1); const mean2 = mean(sample2); const var1 = variance(sample1, mean1); const var2 = variance(sample2, mean2); // Standard error of the difference const se1 = var1 / n1; const se2 = var2 / n2; const seTotal = se1 + se2; // Handle edge case where both variances are 0 if (seTotal === 0) { return { tStatistic: 0, degreesOfFreedom: n1 + n2 - 2, pValue: mean1 === mean2 ? 1 : 0, significant: mean1 !== mean2, mean1, mean2, variance1: var1, variance2: var2, }; } // Welch's t-statistic const t = (mean1 - mean2) / Math.sqrt(seTotal); // Welch-Satterthwaite degrees of freedom approximation const df = (seTotal * seTotal) / ((se1 * se1) / (n1 - 1) + (se2 * se2) / (n2 - 1)); // Two-tailed p-value const pValue = 2 * (1 - tDistCdf(Math.abs(t), df)); return { tStatistic: t, degreesOfFreedom: df, pValue, significant: pValue < 0.05, // 95% confidence level mean1, mean2, variance1: var1, variance2: var2, }; } function getSignificanceStars(pValue) { if (pValue < 0.001) return "***"; if (pValue < 0.01) return "**"; if (pValue < 0.05) return "*"; return ""; } function compareBenchmarks(sample1, sample2, alpha = 0.05) { const result = welchTTest(sample1, sample2); let difference = "same"; if (result.significant) { difference = result.mean1 > result.mean2 ? "faster" : "slower"; } const confidence = ((1 - result.pValue) * 100).toFixed(2); const stars = getSignificanceStars(result.pValue); return { significant: result.pValue < alpha, pValue: result.pValue, confidence: `${confidence}%`, stars, difference, tStatistic: result.tStatistic, degreesOfFreedom: result.degreesOfFreedom, }; } module.exports = { mean, variance, welchTTest, compareBenchmarks, tDistCdf, getSignificanceStars, }; ================================================ FILE: lib/validators.js ================================================ /** * Creates an error object with ERR_INVALID_ARG_TYPE code * @param {string} message - The error message * @returns {Error} Error with code ERR_INVALID_ARG_TYPE */ function ERR_INVALID_ARG_TYPE(message) { const err = new Error(message); err.code = "ERR_INVALID_ARG_TYPE"; return err; } /** * Creates an error object with ERR_INVALID_ARG_VALUE code * @param {string} message - The error message * @returns {Error} Error with code ERR_INVALID_ARG_VALUE */ function ERR_INVALID_ARG_VALUE(message) { const err = new Error(message); err.code = "ERR_INVALID_ARG_VALUE"; return err; } /** * Validates that a value is a number within an optional range * @param {any} value - The value to validate * @param {string} name - Name of the parameter being validated * @param {number} [min] - Optional minimum value (inclusive) * @param {number} [max] - Optional maximum value (inclusive) * @throws {Error} If validation fails */ function validateNumber(value, name, min, max) { if (typeof value !== "number") throw ERR_INVALID_ARG_TYPE( `value must be a number, name: ${name}, value: ${value}`, ); if ( (min != null && value < min) || (max != null && value > max) || ((min != null || max != null) && Number.isNaN(value)) ) { throw ERR_INVALID_ARG_VALUE( `value must be ${min != null ? `>= ${min}` : ""}${min != null && max != null ? " && " : ""}${max != null ? `<= ${max}` : ""}, name: ${name}, value: ${value}`, ); } } /** * Validates that a value is an object (not null and not an array) * @param {any} value - The value to validate * @param {string} name - Name of the parameter being validated * @throws {Error} If validation fails */ function validateObject(value, name) { if (value === null || Array.isArray(value)) { throw ERR_INVALID_ARG_TYPE( `value must be an object, name: ${name}, value: ${value}`, ); } if (typeof value !== "object") { throw ERR_INVALID_ARG_TYPE( `value must be an object, name: ${name}, value: ${value}`, ); } } /** * Validates that a value is a function * @param {any} value - The value to validate * @param {string} name - Name of the parameter being validated * @throws {Error} If validation fails */ function validateFunction(value, name) { if (typeof value !== "function") throw ERR_INVALID_ARG_TYPE( `value must be a function, name: ${name}, value: ${value}`, ); } /** * Validates that a value is a string * @param {any} value - The value to validate * @param {string} name - Name of the parameter being validated * @throws {Error} If validation fails */ function validateString(value, name) { if (typeof value !== "string") throw ERR_INVALID_ARG_TYPE( `value must be a string, name: ${name}, value: ${value}`, ); } /** * Validates that a value is one of the allowed benchmark modes * @param {any} value - The value to validate * @param {string} name - Name of the parameter being validated * @throws {Error} If validation fails */ function validateBenchmarkMode(value, name) { validateString(value, name); const validModes = ["ops", "time"]; if (!validModes.includes(value)) { throw ERR_INVALID_ARG_VALUE( `value must be one of ${validModes.join(", ")}, name: ${name}, value: ${value}`, ); } } /** * Validates that a value is an array * @param {any} value - The value to validate * @param {string} name - Name of the parameter being validated * @throws {Error} If validation fails */ function validateArray(value, name) { if (!Array.isArray(value)) throw ERR_INVALID_ARG_TYPE( `value must be a array, name: ${name}, value: ${value}`, ); } /** * Validates that a value is a boolean * @param {any} value - The value to validate * @param {string} name - Name of the parameter being validated * @throws {Error} If validation fails */ function validateBoolean(value, name) { if (typeof value !== "boolean") throw ERR_INVALID_ARG_TYPE( `value must be a boolean, name: ${name}, value: ${value}`, ); } module.exports = { validateFunction, validateNumber, validateObject, validateString, validateArray, validateBenchmarkMode, validateBoolean, }; ================================================ FILE: lib/worker-runner.js ================================================ const { parentPort } = require("node:worker_threads"); const { runBenchmark, getInitialIterations, runWarmup, } = require("./lifecycle"); const { debugBench } = require("./clock"); const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor; // Deserialize the benchmark function function deserializeBenchmark(benchmark) { const { isAsync, hasArg } = benchmark; const fnPrototype = isAsync ? AsyncFunction : Function; if (hasArg) { benchmark.fn = new fnPrototype("timer", benchmark.fn); } else { benchmark.fn = new fnPrototype(benchmark.fn); } } parentPort.on( "message", async ({ benchmark, initialIterations, benchmarkMode, repeatSuite, minSamples, }) => { deserializeBenchmark(benchmark); debugBench( `Warmup ${benchmark.name} with minTime=${benchmark.minTime}, maxTime=${benchmark.maxTime}`, ); const initialIteration = await getInitialIterations(benchmark); await runWarmup(benchmark, initialIteration, { minTime: 0.005, maxTime: 0.05, }); const result = await runBenchmark( benchmark, initialIterations, benchmarkMode, repeatSuite, minSamples, ); parentPort.postMessage(result); }, ); ================================================ FILE: package.json ================================================ { "name": "bench-node", "version": "0.15.0", "description": "", "main": "lib/index.js", "types": "index.d.ts", "scripts": { "test": "c8 node --test --allow-natives-syntax --expose-gc && npm run lint:ci", "test:types": "tsd -f types/types.test-d.ts", "lint": "biome lint .", "lint:ci": "biome ci .", "lint:fix": "biome lint --write .", "lint:force-fix": "biome lint --write --unsafe .", "format": "biome format --write ." }, "repository": { "type": "git", "url": "git+https://github.com/RafaelGSS/bench-node.git" }, "keywords": [ "benchmark", "nodejs" ], "author": "RafaelGSS ", "contributors": [ { "name": "H4ad", "author": true } ], "license": "MIT", "bugs": { "url": "https://github.com/RafaelGSS/bench-node/issues" }, "homepage": "https://github.com/RafaelGSS/bench-node#readme", "dependencies": { "piscina": "^4.8.0" }, "devDependencies": { "@biomejs/biome": "1.9.4", "@types/node": "^20", "benchmark": "^2.1.4", "c8": "^10.1.3", "mitata": "^1.0.34", "tinybench": "^4.1.0", "tsd": "^0.31.0" }, "c8": { "reporter": "lcov" }, "files": [ "lib/", "index.d.ts" ] } ================================================ FILE: release-please-config.json ================================================ { "packages": { ".": { "release-type": "node", "bump-minor-pre-major": true, "bump-patch-for-minor-pre-major": false, "draft": false, "prerelease": false, "draft-pull-request": true, "include-component-in-tag": false, "include-v-in-tag": true, "separate-pull-requests": false, "skip-github-release": false, "versioning": "default", "pull-request-header": ":robot: I have created a release *beep* *boop*", "pull-request-title-pattern": "chore${scope}: release${component} ${version}", "changelog-path": "CHANGELOG.md", "changelog-host": "https://github.com", "changelog-type": "default", "changelog-sections": [ { "type": "feat", "section": "Features" }, { "type": "feature", "section": "Features" }, { "type": "fix", "section": "Bug Fixes" }, { "type": "perf", "section": "Performance Improvements" }, { "type": "revert", "section": "Reverts" }, { "type": "docs", "section": "Documentation" }, { "type": "style", "section": "Styles" }, { "type": "chore", "section": "Miscellaneous Chores" }, { "type": "refactor", "section": "Code Refactoring" }, { "type": "test", "section": "Tests" }, { "type": "build", "section": "Build System" }, { "type": "ci", "section": "Continuous Integration" } ] } }, "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" } ================================================ FILE: test/async.js ================================================ const { todo } = require("node:test"); todo("async tasks should behave similar to sync tasks", async () => {}); ================================================ FILE: test/basic.js ================================================ const { Suite } = require("../lib/index"); const { describe, it, todo } = require("node:test"); const assert = require("node:assert"); const { spawnSync } = require("node:child_process"); const path = require("node:path"); function noop() {} describe("API Interface", () => { it("options should be an object", () => { for (const r of [1, "ds", null]) { assert.throws( () => { new Suite(r); }, { code: "ERR_INVALID_ARG_TYPE", }, ); } // doesNotThrow new Suite({}); }); it("reporter should be a function", () => { for (const r of [1, "ds", {}]) { assert.throws( () => { new Suite({ reporter: r }); }, { code: "ERR_INVALID_ARG_TYPE", }, ); } // doesNotThrow new Suite({ reporter: () => {} }); }); it("reporter can be false or null", () => { for (const r of [false, null]) { // doesNotThrow new Suite({ reporter: r }); } }); it("suite-level minSamples should be a valid number", () => { for (const r of ["ds", {}, () => {}]) { assert.throws( () => { new Suite({ minSamples: r }); }, { code: "ERR_INVALID_ARG_TYPE", }, ); } // doesNotThrow new Suite({ minSamples: 20 }); }); describe("suite.add", () => { const bench = new Suite({ reporter: noop }); it("name should be an string", () => { for (const r of [1, undefined, null, {}]) { assert.throws(() => { bench.add(r); }); } // doesNotThrow bench.add("example", noop); }); it("options should be an valid object", () => { for (const r of [1, "ds", null]) { assert.throws( () => { bench.add("name", r, noop); }, { code: "ERR_INVALID_ARG_TYPE", }, ); } }); it("minTime should be a valid number", () => { for (const r of ["ds", {}, () => {}]) { assert.throws( () => { bench.add("name", { minTime: r }, noop); }, { code: "ERR_INVALID_ARG_TYPE", }, ); } assert.throws( () => { bench.add("name", { minTime: 0 }, noop); }, { code: "ERR_INVALID_ARG_VALUE", }, ); assert.throws( () => { bench.add("name", { minTime: 0.000001 }, noop); }, { code: "ERR_INVALID_ARG_VALUE", }, ); // doesNotThrow bench.add("name", { minTime: 0.5 }, noop); }); it("maxTime should be a valid number", () => { for (const r of ["ds", {}, () => {}]) { assert.throws( () => { bench.add("name", { minTime: r }, noop); }, { code: "ERR_INVALID_ARG_TYPE", }, ); } }); it("maxTime should be greater than minTime", () => { assert.throws( () => { bench.add("name", { maxTime: 0 }, noop); }, { code: "ERR_INVALID_ARG_VALUE", }, ); assert.throws( () => { bench.add("name", { maxTime: 0.1, minTime: 0.2 }, noop); }, { code: "ERR_INVALID_ARG_VALUE", }, ); // doesNotThrow bench.add("name", { minTime: 0.01, maxTime: 0.02 }, noop); }); it("fn should be a function", () => { for (const r of ["ds", {}, 42]) { assert.throws( () => { bench.add("name", {}, r); }, { code: "ERR_INVALID_ARG_TYPE", }, ); } // doesNotThrow bench.add("name", noop); }); it("repeatSuite should be a valid number", () => { for (const r of ["ds", {}, () => {}]) { assert.throws( () => { bench.add("name", { repeatSuite: r }, noop); }, { code: "ERR_INVALID_ARG_TYPE", }, ); } }); it("minSamples should be a valid number", () => { for (const r of ["ds", {}, () => {}]) { assert.throws( () => { bench.add("name", { minSamples: r }, noop); }, { code: "ERR_INVALID_ARG_TYPE", }, ); } }); }); }); describe("simple usage", async () => { const bench = new Suite({ reporter: noop }); bench .add("foo", async () => { await new Promise((resolve) => setTimeout(resolve, 50)); }) .add("bar", async () => { await new Promise((resolve) => setTimeout(resolve, 100)); }); const [bench1, bench2] = await bench.run(); it("benchmark name should be returned in results", () => { assert.strictEqual(bench1.name, "foo"); assert.strictEqual(bench2.name, "bar"); }); it("ops/sec should match the expected duration", () => { // 1000(ms)/50 = 20 + cost of creating promises assert.ok(bench1.opsSec > 18 && bench1.opsSec <= 20); // 1000(ms)/100 = 100 + cost of creating promises assert.ok(bench2.opsSec > 8 && bench2.opsSec <= 10); }); it("tasks should have at least 10 samples", () => { assert.ok(bench1.iterations >= 10); assert.ok(bench2.iterations >= 10); }); it("opsSecPerRun should exist when repeatSuite is 1", async () => { assert.ok( Array.isArray(bench1.opsSecPerRun), "opsSecPerRun should be an array", ); assert.ok( Array.isArray(bench2.opsSecPerRun), "opsSecPerRun should be an array", ); }); }); describe("repeat suite", async () => { const repeatCount = 3; const bench = new Suite({ reporter: noop }); bench.add("Repeat ops test", { repeatSuite: repeatCount }, () => { // Simple operation const x = 1 + 1; }); const results = await bench.run(); it("should include opsSecPerRun when repeatSuite is greater than 1", async () => { assert.strictEqual(results.length, 1); assert.ok(results[0].opsSec !== undefined, "opsSec should be defined"); assert.ok( Array.isArray(results[0].opsSecPerRun), "opsSecPerRun should be an array", ); assert.strictEqual( results[0].opsSecPerRun.length, repeatCount, `opsSecPerRun should have ${repeatCount} elements`, ); for (const opsSec of results[0].opsSecPerRun) { assert.ok( typeof opsSec === "number" && !Number.isNaN(opsSec), "each element should be a valid number", ); } }); }); describe("throws when a benchmark task throw", async () => { const bench = new Suite(); const err = new Error(); bench.add("error", () => { throw err; }); assert.rejects(() => bench.run()); }); describe("when no --allow-natives-syntax", async () => { it("should throw", () => { const file = path.join(__dirname, "fixtures", "bench.js"); const { status, stderr } = spawnSync(process.execPath, [file]); assert.strictEqual(status, 1); assert.match( stderr.toString(), /bench-node module must be run with --allow-natives-syntax/, ); }); }); todo("histogram values", async () => {}); ================================================ FILE: test/env.js ================================================ const { describe, it, before } = require("node:test"); const assert = require("node:assert"); const { Suite } = require("../lib"); const copyBench = require("./fixtures/copy"); const { managedBench, managedOptBench } = require("./fixtures/opt-managed"); function assertMinBenchmarkDifference( results, { percentageLimit, ciPercentageLimit }, ) { assertBenchmarkDifference(results, { percentageLimit, ciPercentageLimit, greaterThan: true, }); } function assertMaxBenchmarkDifference( results, { percentageLimit, ciPercentageLimit }, ) { assertBenchmarkDifference(results, { percentageLimit, ciPercentageLimit, greaterThan: false, }); } function assertBenchmarkDifference( results, { percentageLimit, ciPercentageLimit, greaterThan }, ) { for (let i = 0; i < results.length; i++) { for (let j = 0; j < results.length; j++) { if (i !== j) { const opsSec1 = results[i].opsSec; const opsSec2 = results[j].opsSec; // Calculate the percentage difference const difference = Math.abs(opsSec1 - opsSec2); const percentageDifference = (difference / Math.min(opsSec1, opsSec2)) * 100; // Check if the percentage difference is less than or equal to 10% if (process.env.CI) { // CI runs in a shared-env so the percentage of difference // must be greather there due to high variance of hardware assert.ok( greaterThan ? percentageDifference >= ciPercentageLimit : percentageDifference <= ciPercentageLimit, `"${results[i].name}" too different from "${results[j].name}" - ${percentageDifference} != ${ciPercentageLimit} - ${opsSec1} x ${opsSec2}`, ); } else { assert.ok( greaterThan ? percentageDifference >= percentageLimit : percentageDifference <= percentageLimit, `${results[i].name} too different from ${results[j].name} - ${percentageDifference} != ${percentageLimit}`, ); } } } } } // TODO: on small machines the results are discrepant. Fix it. if (!process.env.CI) { describe("Same benchmark function", () => { let results; before(async () => { results = await copyBench.run(); }); it("must have a similar benchmark result", () => { assertMaxBenchmarkDifference(results, { percentageLimit: 10, ciPercentageLimit: 30, }); }); }); } describe("Managed can be V8 optimized", () => { let optResults; let results; before(async () => { optResults = await managedOptBench.run(); results = await managedBench.run(); }); it("should be more than 50% different from unmanaged", () => { assertMinBenchmarkDifference(optResults, { percentageLimit: 50, ciPercentageLimit: 30, }); }); // it('should be similar when avoiding V8 optimizatio', () => { // assertBenchmarkDifference(results, 50, 30); // }); }); describe("Workers should have parallel context", () => { let results; before(async () => { const bench = new Suite({ reporter: () => {}, useWorkers: true, benchmarkMode: "ops", }); bench .add("Import with node: prefix", () => { return import("node:fs"); }) .add("Import without node: prefix", () => { return import("node:fs"); }); results = await bench.run(); }); it("should have a similar result as they will not share import.meta.cache", () => { assertMaxBenchmarkDifference(results, { percentageLimit: 10, ciPercentageLimit: 30, }); }); }); ================================================ FILE: test/fixtures/bench.js ================================================ const { Suite } = require("../../lib"); const suite = new Suite(); suite .add("empty", () => {}) .add("empty async", async () => {}) .run(); ================================================ FILE: test/fixtures/copy.js ================================================ const { Suite } = require("../../lib"); const suite = new Suite({ reporter: false }); suite .add("Using includes", { minSamples: 20 }, () => { const text = "text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8"; const r = text.includes("application/json"); }) .add("Using includes 2", { minSamples: 20 }, () => { const text = "text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8"; const r = text.includes("application/json"); }) .add("Using includes 3", { minSamples: 20 }, () => { const text = "text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8"; const r = text.includes("application/json"); }); module.exports = suite; ================================================ FILE: test/fixtures/opt-managed.js ================================================ const { Suite } = require("../../lib"); const assert = require("node:assert"); const suite = new Suite({ reporter: false }); suite .add("Using includes", () => { const text = "text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8"; const r = text.includes("application/json"); assert.ok(r); }) .add("[Managed] Using includes", (timer) => { timer.start(); for (let i = 0; i < timer.count; i++) { const text = "text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8"; const r = text.includes("application/json"); assert.ok(r); } timer.end(timer.count); }); suite.run(); const optSuite = new Suite({ reporter: false }); optSuite .add("Using includes", () => { const text = "text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8"; const r = text.includes("application/json"); // assert.ok(r) }) .add("[Managed] Using includes", (timer) => { timer.start(); for (let i = 0; i < timer.count; i++) { const text = "text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8"; const r = text.includes("application/json"); // assert.ok(r) } timer.end(timer.count); }); module.exports = { managedBench: suite, managedOptBench: optSuite, }; ================================================ FILE: test/managed.js ================================================ const { describe, it } = require("node:test"); const assert = require("node:assert"); const { Suite } = require("../lib/index"); describe("managed benchmarks", async () => { it("should throw when timer.start isn't called", () => { const suite = new Suite({ reporter: () => {} }); assert.rejects( async () => { suite.add("managed", (timer) => {}); await suite.run(); }, { message: "You forgot to call .start()", }, ); }); it("should throw when timer.end isn't called", () => { const suite = new Suite({ reporter: () => {} }); assert.rejects( async () => { suite.add("managed", (timer) => { timer.start(); }); await suite.run(); }, { message: "You forgot to call .end(count)", }, ); }); }); ================================================ FILE: test/plugin-api-doc.js ================================================ // @ts-check const { Suite } = require("../lib/index.js"); const { describe, it } = require("node:test"); const assert = require("node:assert"); class ExamplePlugin { #aggregation = {}; constructor() { } isSupported() { return true; } beforeClockTemplate() { return [`record("- evaluated beforeClockCode");`]; } afterClockTemplate({ context }) { return [ ` ${context}.example=1; record("- evaluated afterClockCode"); `, ]; } onCompleteBenchmark([time, iterations, results], { name }) { if (undefined === this.#aggregation[name]) { this.#aggregation[name] = 0; } this.#aggregation[name] += results.example; } toString() { return "ExamplePlugin"; } getReport(name) { return `examplePlugin report for ${name}`; } getResult(name) { return { examplePluginAggregation: this.#aggregation[name], }; } } describe("plugin API", async () => { const bench = new Suite({ reporter: () => { }, plugins: [captureAll(new ExamplePlugin())], }); bench.add("task1", async () => { record("- task1"); }); bench.add("task2", async () => { record("- task2"); }); const [bench1] = await bench.run(); it("matches method signatures", async () => { const recordedMethodSignatures = getSignatures(); assert.deepStrictEqual(recordedMethodSignatures, [ "afterClockTemplate({awaitOrEmpty, bench, context, managed, timer})", "beforeClockTemplate({awaitOrEmpty, bench, context, managed, timer})", "getReport(string)", "getResult(string)", "isSupported()", "onCompleteBenchmark([number, number, object], {baseline, fn, fnStr, hasArg, isAsync, maxTime, minSamples, minTime, name, plugins, repeatSuite})", "reset()", "toJSON(string)", "toString()", ]); }); it("produces history", async () => { printExcerptFromHistory(); }); it("aggregates results", async () => { console.log("Benchmark results plugins field:", bench1.plugins); assert(bench1.plugins[0].result.examplePluginAggregation > 1); assert.strictEqual( bench1.plugins[0].report, "examplePlugin report for task1", ); }); }); // ============================================ // Utilities to capture the methods and history. // Moved down the file to keep the test code clean. Hoisting is how they're available. // No need to look at them, stop reading now, look at the test output instead. function record(name, args) { if (args && args.length) { history.push([name, 1, JSON.stringify(Array.from(args))]); } else { const last = history[history.length - 1]; if (last && last[0] === name) { last[1]++; } else { history.push([name, 1]); } } } function printExcerptFromHistory(n = 25) { const excerpt = [ ...history.slice(0, n), ["(... redacted for brevity ...)", 1], ...history.slice(-n), ] .map( ([name, count, args]) => `${name} ${count > 1 ? "x" + count : ""}${args ? " with args: " + args : "" }`, ) .join("\n| "); console.log("+----------------------------------"); console.log("| Plugin lifecycle log:"); console.log("+----------------------------------"); console.log("|", excerpt); console.log("+----------------------------------"); } function getSignatures() { return Object.entries(API) .map( ([name, args]) => `${name}(${Array.from(args) .map((a) => { if (!a) return ""; if (Array.isArray(a)) return "[" + a.map((a) => typeof a).join(", ") + "]"; if (typeof a === "object") return "{" + Object.keys(a).sort().join(", ") + "}"; return typeof a; }) .join(", ")})`, ) .sort(); } var history, API; function captureAll(pluginInstance) { history = []; API = {}; globalThis.record = record; // make record available in the tasks return new Proxy(pluginInstance, { get(target, prop, receiver) { return function (...args) { record(prop, args); API[prop] = args; if (typeof target[prop] === "function") { return target[prop].apply(target, args); } }; }, has(target, prop) { return true; }, }); } ================================================ FILE: test/plugins.js ================================================ const { Suite, V8NeverOptimizePlugin, V8GetOptimizationStatus, V8OptimizeOnNextCallPlugin, MemoryPlugin, DeadCodeEliminationDetectionPlugin, } = require("../lib/index"); const { describe, it } = require("node:test"); const assert = require("node:assert"); class InvalidPlugin {} class ValidPlugin { toString() { return ""; } isSupported() { return true; } } describe("Plugins validation", () => { it("should be an object with expected methods", () => { for (const r of [1, "ds", {}]) { assert.throws( () => { new Suite({ plugins: r }); }, { code: "ERR_INVALID_ARG_TYPE", }, ); } assert.throws( () => { new Suite({ plugins: [new InvalidPlugin()] }); }, { code: "ERR_INVALID_ARG_TYPE", }, ); // doesNotThrow new Suite({ plugins: [new ValidPlugin()] }); }); it("beforeClockTemplate should return an array", () => { class InvalidPlugin2 { beforeClockTemplate() { return "error"; } toString() { return ""; } isSupported() { return true; } } assert.throws( () => { new Suite({ plugins: [new InvalidPlugin2()] }); }, { code: "ERR_INVALID_ARG_TYPE", }, ); }); it("afterClockTemplate should return an array", () => { class InvalidPlugin2 { beforeClockTemplate() { return [""]; } afterClockTemplate() { return "error"; } toString() { return ""; } isSupported() { return true; } } assert.throws( () => { new Suite({ plugins: [new InvalidPlugin2()] }); }, { code: "ERR_INVALID_ARG_TYPE", }, ); }); }); describe("Official plugins validation", () => { it("V8NeverOptimizePlugin validation", () => { const bench = new Suite({ plugins: [new V8NeverOptimizePlugin()], }); assert.ok(bench); }); it("V8NeverOptimizePlugin prevents benchmark function optimization", () => { const plugin = new V8NeverOptimizePlugin(); const [code, wrapper] = plugin.beforeClockTemplate({ bench: "bench", awaitOrEmpty: "", context: "context", timer: "timer", }); assert.strictEqual(wrapper, "DoNotOptimize"); assert.match(code, /%NeverOptimizeFunction\(bench\.fn\);/); assert.match(code, /%NeverOptimizeFunction\(DoNotOptimize\);/); }); it("V8GetOptimizationStatus validation", () => { const bench = new Suite({ plugins: [new V8GetOptimizationStatus()], }); assert.ok(bench); }); it("V8OptimizeOnNextCallPlugin validation", () => { const bench = new Suite({ plugins: [new V8OptimizeOnNextCallPlugin()], }); assert.ok(bench); }); it("MemoryPlugin validation", () => { const bench = new Suite({ plugins: [new MemoryPlugin()], }); assert.ok(bench); }); it("DeadCodeEliminationDetectionPlugin validation", () => { const bench = new Suite({ plugins: [new DeadCodeEliminationDetectionPlugin()], }); assert.ok(bench); }); }); describe("DeadCodeEliminationDetectionPlugin", () => { it("should initialize with default threshold", () => { const plugin = new DeadCodeEliminationDetectionPlugin(); assert.ok(plugin); assert.strictEqual(plugin.isSupported(), true); }); it("should accept custom threshold", () => { const plugin = new DeadCodeEliminationDetectionPlugin({ threshold: 20 }); assert.ok(plugin); }); it("should set and use baseline", () => { const plugin = new DeadCodeEliminationDetectionPlugin(); plugin.setBaseline(100); // 100ns baseline assert.strictEqual(plugin.hasWarning("test"), false); }); it("should detect suspiciously fast benchmarks", () => { const plugin = new DeadCodeEliminationDetectionPlugin({ threshold: 10 }); plugin.setBaseline(100); // 100ns baseline // Simulate a benchmark that's only 5x slower than baseline (should warn) const fastResult = [500, 1]; // 500ns total, 1 iteration = 500ns/iter plugin.onCompleteBenchmark(fastResult, { name: "fast-bench" }); assert.strictEqual(plugin.hasWarning("fast-bench"), true); const warning = plugin.getWarning("fast-bench"); assert.ok(warning); assert.strictEqual(warning.timePerOp, 500); assert.strictEqual(warning.baselineTime, 100); assert.strictEqual(warning.ratio, 5); }); it("should not warn for legitimate benchmarks", () => { const plugin = new DeadCodeEliminationDetectionPlugin({ threshold: 10 }); plugin.setBaseline(100); // 100ns baseline // Simulate a benchmark that's 50x slower than baseline (should not warn) const slowResult = [5000, 1]; // 5000ns total, 1 iteration = 5000ns/iter plugin.onCompleteBenchmark(slowResult, { name: "slow-bench" }); assert.strictEqual(plugin.hasWarning("slow-bench"), false); }); it("should track multiple warnings", () => { const plugin = new DeadCodeEliminationDetectionPlugin({ threshold: 10 }); plugin.setBaseline(100); plugin.onCompleteBenchmark([200, 1], { name: "bench1" }); plugin.onCompleteBenchmark([300, 1], { name: "bench2" }); const allWarnings = plugin.getAllWarnings(); assert.strictEqual(allWarnings.length, 2); assert.strictEqual(allWarnings[0].name, "bench1"); assert.strictEqual(allWarnings[1].name, "bench2"); }); it("should create Suite with DCE detection enabled", () => { // Just verify Suite accepts the option const suite = new Suite({ detectDeadCodeElimination: true, dceThreshold: 15, reporter: false, }); assert.ok(suite); }); }); ================================================ FILE: test/reporter.js ================================================ const { describe, it, before } = require("node:test"); const assert = require("node:assert"); const fs = require("node:fs"); const { Suite } = require("../lib"); const { chartReport, toChart, htmlReport, jsonReport, toJSON, csvReport, toCSV, prettyReport, toPretty, textReport, toText, } = require("../lib/report"); const { analyze, summarize } = require("../lib/utils/analyze.js"); describe("chartReport", () => { describe("outputs benchmark results as a bar chart", async (t) => { let output = ""; before(async () => { const originalStdoutWrite = process.stdout.write; process.stdout.write = (data) => { output += data; }; const suite = new Suite({ reporter: chartReport, }); suite .add("single with matcher", () => { const pattern = /[123]/g; const replacements = { 1: "a", 2: "b", 3: "c" }; const subject = "123123123123123123123123123123123123123123123123"; const r = subject.replace(pattern, (m) => replacements[m]); assert.ok(r); }) .add("multiple replaces", () => { const subject = "123123123123123123123123123123123123123123123123"; const r = subject .replace(/1/g, "a") .replace(/2/g, "b") .replace(/3/g, "c"); assert.ok(r); }); await suite.run(); process.stdout.write = originalStdoutWrite; }); it("should include bar chart chars", () => { assert.ok(output.includes("█")); }); it("should include ops/sec", () => { assert.ok(output.includes("ops/sec")); }); it("should include benchmark names", () => { assert.ok(output.includes("single with matcher")); assert.ok(output.includes("multiple replaces")); }); it("should include sample count", () => { assert.ok(output.includes("samples")); }); it("should include Node.js version", () => { const regex = /Node\.js version: v\d+\.\d+\.\d+/; assert.ok(output.match(regex)); }); }); describe("with long names", async (t) => { let output = ""; before(async () => { const originalStdoutWrite = process.stdout.write; process.stdout.write = (data) => { output += data; }; const suite = new Suite({ reporter: chartReport, }); suite .add("single with matcher looooooooooooooooooooooooooong", () => { const pattern = /[123]/g; const replacements = { 1: "a", 2: "b", 3: "c" }; const subject = "123123123123123123123123123123123123123123123123"; const r = subject.replace(pattern, (m) => replacements[m]); assert.ok(r); }) .add("multiple replaces", () => { const subject = "123123123123123123123123123123123123123123123123"; const r = subject .replace(/1/g, "a") .replace(/2/g, "b") .replace(/3/g, "c"); assert.ok(r); }); await suite.run(); process.stdout.write = originalStdoutWrite; }); it("should pad out benchmark names", () => { assert.ok(output.includes("oong ▏█")); assert.ok(output.includes("multiple replaces".padEnd(51))); }); }); describe("respects reporterOptions.printHeader", async (t) => { let outputWithHeader = ""; let outputWithoutHeader = ""; before(async () => { const originalStdoutWrite = process.stdout.write; // Test with default settings (printHeader: true) process.stdout.write = (data) => { outputWithHeader += data; }; const suiteWithHeader = new Suite({ reporter: chartReport, reporterOptions: { printHeader: true, }, }); suiteWithHeader.add("test benchmark", () => { const a = 1 + 1; assert.strictEqual(a, 2); }); await suiteWithHeader.run(); // Test with printHeader: false outputWithoutHeader = ""; process.stdout.write = (data) => { outputWithoutHeader += data; }; const suiteWithoutHeader = new Suite({ reporter: chartReport, reporterOptions: { printHeader: false, }, }); suiteWithoutHeader.add("test benchmark", () => { const a = 1 + 1; assert.strictEqual(a, 2); }); await suiteWithoutHeader.run(); process.stdout.write = originalStdoutWrite; }); it("should include Node.js version when printHeader is true", () => { const regex = /Node\.js version: v\d+\.\d+\.\d+/; assert.ok(outputWithHeader.match(regex)); }); it("should include Platform when printHeader is true", () => { assert.ok(outputWithHeader.includes("Platform:")); }); it("should include CPU Cores when printHeader is true", () => { assert.ok(outputWithHeader.includes("CPU Cores:")); }); it("should NOT include Node.js version when printHeader is false", () => { const regex = /Node\.js version: v\d+\.\d+\.\d+/; assert.ok(!outputWithoutHeader.match(regex)); }); it("should NOT include Platform when printHeader is false", () => { assert.ok(!outputWithoutHeader.includes("Platform:")); }); it("should NOT include CPU Cores when printHeader is false", () => { assert.ok(!outputWithoutHeader.includes("CPU Cores:")); }); it("should still include benchmark data with or without header", () => { // Both outputs should still have benchmark bars and results assert.ok(outputWithHeader.includes("█")); assert.ok(outputWithoutHeader.includes("█")); assert.ok(outputWithHeader.includes("test benchmark")); assert.ok(outputWithoutHeader.includes("test benchmark")); }); }); }); describe("htmlReport should create a file", async (t) => { let output = ""; let htmlName = ""; let htmlContent = ""; before(async () => { const originalStdoutWrite = process.stdout.write; const originalWriteFileSync = fs.writeFileSync; fs.writeFileSync = (name, content) => { htmlName = name; htmlContent = content; }; process.stdout.write = (data) => { output += data; }; const suite = new Suite({ reporter: htmlReport, }); suite .add("single with matcher", () => { const pattern = /[123]/g; const replacements = { 1: "a", 2: "b", 3: "c" }; const subject = "123123123123123123123123123123123123123123123123"; const r = subject.replace(pattern, (m) => replacements[m]); assert.ok(r); }) .add("Multiple replaces", () => { const subject = "123123123123123123123123123123123123123123123123"; const r = subject .replace(/1/g, "a") .replace(/2/g, "b") .replace(/3/g, "c"); assert.ok(r); }); await suite.run(); fs.writeFileSync = originalWriteFileSync; process.stdout.write = originalStdoutWrite; }); it("should print that a HTML file has been generated", () => { assert.ok(output.includes("HTML file has been generated")); }); it("htmlName should be result.html", () => { assert.strictEqual(htmlName, "result.html"); }); it("htmlContent should not be empty", () => { assert.ok(htmlContent.length > 100); }); it("htmlContent bench suite should be used as class name", () => { assert.ok(htmlContent.includes("circle-Multiple-replaces")); assert.ok(htmlContent.includes("circle-single-with-matcher")); }); it("htmlContent should not contain replace tags {{}}", () => { assert.ok(htmlContent.includes("{{") === false); assert.ok(htmlContent.includes("}}") === false); }); }); describe("prettyReport outputs a beautiful report", async (t) => { let output = ""; // biome-ignore lint/suspicious/noControlCharactersInRegex: This is required to strip ANSI color codes from the output for testing. const stripAnsi = (str) => str.replace(/\x1b\[[0-9;]*m/g, ""); before(async () => { const originalStdoutWrite = process.stdout.write; process.stdout.write = (data) => { output += data; }; const suite = new Suite({ reporter: prettyReport, }); suite .add("suite/single with matcher", () => { const pattern = /[123]/g; const replacements = { 1: "a", 2: "b", 3: "c" }; const subject = "123123123123123123123123123123123123123123123123"; const r = subject.replace(pattern, (m) => replacements[m]); assert.ok(r); }) .add("suite/multiple replaces", () => { const subject = "123123123123123123123123123123123123123123123123"; const r = subject .replace(/1/g, "a") .replace(/2/g, "b") .replace(/3/g, "c"); assert.ok(r); }) .add("suite/nested/deeper/test", () => { assert.ok(true); }); await suite.run(); process.stdout.write = originalStdoutWrite; }); it("should include system information", () => { assert.ok(output.includes("System Information:")); assert.ok(output.includes("Node.js:")); assert.ok(output.includes("OS:")); assert.ok(output.includes("CPU:")); }); it("should include benchmark results header", () => { assert.ok(output.includes("Benchmark results")); }); it("should include tree structure characters", () => { assert.ok(output.includes("└─")); assert.ok(output.includes("├─")); }); it("should correctly group tests with slashes", () => { const stripped = stripAnsi(output); assert.ok(stripped.includes("suite")); assert.ok(stripped.includes("single with matcher")); assert.ok(stripped.includes("multiple replaces")); assert.ok(stripped.includes("nested")); assert.ok(stripped.includes("deeper")); assert.ok(stripped.includes("test")); }); }); describe("analyze", async (t) => { let analysis; before(async () => { // Create a new Suite with the pretty reporter const suite = new Suite({}); // Add benchmarks with one being the baseline suite .add("baseline-test", { baseline: true }, () => { // Medium-speed operation for (let i = 0; i < 10000; i++) {} }) .add("other-test", () => { // Faster operation for (let i = 0; i < 1000; i++) {} }) .add("faster-test", () => { // Slower operation for (let i = 0; i < 100; i++) {} }); // Run the suite const results = await suite.run(); analysis = analyze(results, false); }); it("annotates the fastest result", () => { assert.equal(analysis[2].fastest, true, "fastest result"); }); it("annotates the slowest result", () => { assert.equal(analysis[0].slowest, true, "slowest result"); }); }); describe("summarize", async (t) => { let results; before(async () => { // Create a new Suite with the pretty reporter const suite = new Suite({}); // Add benchmarks with one being the baseline suite .add("baseline-test", { baseline: true }, () => { // Medium-speed operation for (let i = 0; i < 10000; i++) {} }) .add("other-test", () => { // Faster operation for (let i = 0; i < 1000; i++) {} }) .add("faster-test", () => { // Slower operation for (let i = 0; i < 100; i++) {} }); // Run the suite results = await suite.run(); }); it("should contain the required benchmark fields", () => { const data = summarize(results); // We expect the two benchmarks we added: 'single with matcher' and 'Multiple replaces' assert.strictEqual(data.length, 3, "Should have results for 3 benchmarks"); for (const entry of data) { // Ensure each entry has expected keys assert.ok(typeof entry.name === "string", "name should be a string"); assert.ok(typeof entry.opsSec === "number", "opsSec should be a number"); assert.ok( typeof entry.runsSampled === "number", "runsSampled should be a number", ); assert.ok(typeof entry.min === "number", "min should be a number"); assert.ok(typeof entry.max === "number", "max should be a number"); assert.ok( typeof entry.minFormatted === "string", "minFormatted should be a string (formatted time)", ); assert.ok( typeof entry.maxFormatted === "string", "maxFormatted should be a string (formatted time)", ); assert.ok(Array.isArray(entry.plugins), "plugins should be an array"); } }); }); describe("baseline comparisons", async (t) => { let results; before(async () => { // Create a new Suite with the pretty reporter const suite = new Suite({}); // Add benchmarks with one being the baseline suite .add("baseline-test", { baseline: true }, () => { // Medium-speed operation for (let i = 0; i < 1000; i++) {} }) .add("faster-test", () => { // Faster operation for (let i = 0; i < 100; i++) {} }) .add("slower-test", () => { // Slower operation for (let i = 0; i < 10000; i++) {} }); // Run the suite results = await suite.run(); }); describe("analyze", () => { let analysis; before(() => { analysis = analyze(results); }); it("annotates the fastest result", () => { assert.equal(results[1].fastest, true, "fastest result"); }); it("annotates the slowest result", () => { assert.equal(results[2].slowest, true, "slowest result"); }); }); describe("for pretty report", async (t) => { let output = ""; before(async () => { output = toPretty(results); }); it("should include a summary section", () => { assert.ok(output.includes("Summary (vs. baseline):")); }); it("should show 'faster' comparison in summary", () => { const summary = output.split("Summary (vs. baseline):")[1]; assert.ok(summary.includes("faster")); }); it("should show 'slower' comparison in summary", () => { const summary = output.split("Summary (vs. baseline):")[1]; assert.ok(summary.includes("slower")); }); }); describe("for text Report", async (t) => { let output = ""; before(async () => { output = toText(results); }); it("should include a summary section", () => { assert.ok(output.includes("Summary (vs. baseline):")); }); it("should show 'faster' comparison in summary", () => { const summary = output.split("Summary (vs. baseline):")[1]; assert.ok(summary.includes("faster")); }); it("should show 'slower' comparison in summary", () => { const summary = output.split("Summary (vs. baseline):")[1]; assert.ok(summary.includes("slower")); }); }); describe("for chart Report", async (t) => { let output = ""; before(async () => { output = toChart(results, {}); }); it("should include a summary section", () => { assert.ok(output.includes("Summary (vs. baseline):")); }); it("should show 'faster' comparison in summary", () => { const summary = output.split("Summary (vs. baseline):")[1]; assert.ok(summary.includes("faster")); }); it("should show 'slower' comparison in summary", () => { const summary = output.split("Summary (vs. baseline):")[1]; assert.ok(summary.includes("slower")); }); it("uses the default column width for the name", () => { const summary = output.split("Summary (vs. baseline):")[1]; assert.ok( // 123456789012345678901234567890123456789012345 ▏ summary.includes("baseline-test ▏"), ); }); it("can adjust the display widths to suite", () => { const inputs = [ { iterations: 1635480, histogram: { samples: 11, min: 301.5006542361793, max: 313.07250487172166, sampleData: [ 301.5006542361793, 301.5593469278305, 302.8870084803949, 304.6617804001423, 304.71883159667146, 305.0352153017722, 306.6694294422758, 306.9953128406366, 309.27860394347147, 310.3016037436935, 313.07250487172166, ], }, name: "baseline-test", baseline: true, opsSec: 3268352.671656186, opsSecPerRun: [3268352.671656186], }, ]; output = toChart(inputs, { labelWidth: 20, barWidth: 10 }); const summary = output.split("Summary (vs. baseline):")[1]; // 12345678901234567890 ▏1234567890 ▏ assert.ok(summary.includes("baseline-test ▏██████████▕ ")); }); }); }); describe("Suite with pretty: true option", async (t) => { let output = ""; before(async () => { const originalStdoutWrite = process.stdout.write; process.stdout.write = (data) => { output += data; }; const suite = new Suite({ pretty: true, }); suite.add("test", () => {}); await suite.run(); process.stdout.write = originalStdoutWrite; }); it("should use the pretty reporter", () => { assert.ok(output.includes("System Information:")); }); }); describe("custom reporter should have access to histogram data", async () => { let output = ""; before(async () => { const originalStdoutWrite = process.stdout.write; process.stdout.write = (data) => { output += data; }; function customReporter(results) { const json = []; for (const result of results) { // Calculate the median of result.histogram.sampleData: const sorted = [...result.histogram.sampleData].sort((a, b) => a - b); const median = sorted.length % 2 === 0 ? (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2 : sorted[Math.floor(sorted.length / 2)]; json.push({ name: result.name, low: result.histogram.sampleData.filter((v) => v <= median), high: result.histogram.sampleData.filter((v) => v >= median), median, }); } console.log(JSON.stringify(json, null, 2)); } // Create a new Suite with the custom reporter const suite = new Suite({ reporter: customReporter, }); suite .add("single with matcher", () => { const pattern = /[123]/g; const replacements = { 1: "a", 2: "b", 3: "c" }; const subject = "123123123123123123123123123123123123123123123123"; const r = subject.replace(pattern, (m) => replacements[m]); assert.ok(r); }) .add("Multiple replaces", () => { const subject = "123123123123123123123123123123123123123123123123"; const r = subject .replace(/1/g, "a") .replace(/2/g, "b") .replace(/3/g, "c"); assert.ok(r); }); // Run the suite await suite.run(); // Restore stdout process.stdout.write = originalStdoutWrite; }); it("should print valid JSON", () => { // Verify if the output can be parsed as JSON let data; try { data = JSON.parse(output); } catch (err) { assert.fail(`Output is not valid JSON: ${err.message}`); } assert.ok(Array.isArray(data), "Output should be an array of results"); }); it("should calculate median correctly", () => { const data = JSON.parse(output); for (const result of data) { assert.strictEqual( result.low.length, result.high.length, "Same number of samples above and below median", ); } }); }); describe("toJSON", () => { let results; before(async () => { const suite = new Suite({}); suite .add("single with matcher", () => { const pattern = /[123]/g; const replacements = { 1: "a", 2: "b", 3: "c" }; const subject = "123123123123123123123123123123123123123123123123"; const r = subject.replace(pattern, (m) => replacements[m]); assert.ok(r); }) .add("Multiple replaces", () => { const subject = "123123123123123123123123123123123123123123123123"; const r = subject .replace(/1/g, "a") .replace(/2/g, "b") .replace(/3/g, "c"); assert.ok(r); }); // Run the suite results = await suite.run(); }); it("should run", () => { toJSON(results); }); it("should print valid JSON", () => { const output = toJSON(results); const data = JSON.parse(output); assert.ok(Array.isArray(data), "Output should be an array of results"); }); it("should contain the required benchmark fields", () => { const output = toJSON(results); const data = JSON.parse(output); // We expect the two benchmarks we added: 'single with matcher' and 'Multiple replaces' assert.strictEqual(data.length, 2, "Should have results for 2 benchmarks"); for (const entry of data) { // Ensure each entry has expected keys assert.ok(typeof entry.name === "string", "name should be a string"); assert.ok(typeof entry.opsSec === "number", "opsSec should be a number"); assert.ok( typeof entry.runsSampled === "number", "runsSampled should be a number", ); assert.ok( typeof entry.min === "string", "min should be a string (formatted time)", ); assert.ok( typeof entry.minNS === "number", "minNS should be a number (time in NS)", ); assert.ok( typeof entry.max === "string", "max should be a string (formatted time)", ); assert.ok( typeof entry.maxNS === "number", "maxNS should be a number (time in NS)", ); assert.ok(Array.isArray(entry.plugins), "plugins should be an array"); } }); }); describe("jsonReport", () => { let output = ""; before(async () => { const originalStdoutWrite = process.stdout.write; process.stdout.write = (data) => { output += data; }; // Create a new Suite with the JSON reporter const suite = new Suite({ reporter: jsonReport, }); suite .add("single with matcher", () => { const pattern = /[123]/g; const replacements = { 1: "a", 2: "b", 3: "c" }; const subject = "123123123123123123123123123123123123123123123123"; const r = subject.replace(pattern, (m) => replacements[m]); assert.ok(r); }) .add("Multiple replaces", () => { const subject = "123123123123123123123123123123123123123123123123"; const r = subject .replace(/1/g, "a") .replace(/2/g, "b") .replace(/3/g, "c"); assert.ok(r); }); // Run the suite await suite.run(); // Restore stdout process.stdout.write = originalStdoutWrite; }); it("should print valid JSON", () => { // Verify if the output can be parsed as JSON let data; try { data = JSON.parse(output); } catch (err) { assert.fail(`Output is not valid JSON: ${err.message}`); } assert.ok(Array.isArray(data), "Output should be an array of results"); }); }); describe("toCSV", () => { it("should generate valid CSV output", async (t) => { const result = toCSV([ { opsSec: 749625.5652171721, iterations: 374813, histogram: { samples: 10, min: 1322.2615873857162, max: 1345.4275821344213, }, name: "single with matcher", plugins: [ { name: "V8NeverOptimizePlugin", result: "enabled", report: "v8-never-optimize=true", }, ], }, { opsSec: 634284.7401772924, iterations: 317148, histogram: { samples: 11, min: 1552.562466504839, max: 1612.7852084972462, }, name: "Multiple replaces", plugins: [ { name: "V8NeverOptimizePlugin", result: "enabled", report: "v8-never-optimize=true", }, ], }, ]); assert.deepStrictEqual( result, "name,ops/sec,samples,plugins,min,max\n" + 'single with matcher,"749,626",10,"v8-never-optimize=true",1.32us,1.35us\n' + 'Multiple replaces,"634,285",11,"v8-never-optimize=true",1.55us,1.61us\n', ); }); }); describe("csvReport", () => { it("should generate valid CSV output", async (t) => { const fn = t.mock.method(process.stdout, "write"); // noop fn.mock.mockImplementation(() => {}); csvReport([ { opsSec: 749625.5652171721, iterations: 374813, histogram: { samples: 10, min: 1322.2615873857162, max: 1345.4275821344213, }, name: "single with matcher", plugins: [ { name: "V8NeverOptimizePlugin", result: "enabled", report: "v8-never-optimize=true", }, ], }, { opsSec: 634284.7401772924, iterations: 317148, histogram: { samples: 11, min: 1552.562466504839, max: 1612.7852084972462, }, name: "Multiple replaces", plugins: [ { name: "V8NeverOptimizePlugin", result: "enabled", report: "v8-never-optimize=true", }, ], }, ]); const callArgs = process.stdout.write.mock.calls.map( (call) => call.arguments[0], ); assert.strictEqual(process.stdout.write.mock.callCount(), 1); assert.deepStrictEqual(callArgs, [ "name,ops/sec,samples,plugins,min,max\n" + 'single with matcher,"749,626",10,"v8-never-optimize=true",1.32us,1.35us\n' + 'Multiple replaces,"634,285",11,"v8-never-optimize=true",1.55us,1.61us\n', ]); }); }); ================================================ FILE: test/time-mode.js ================================================ const { Suite } = require("../lib/index"); const { describe, it } = require("node:test"); const assert = require("node:assert"); // Helper function to create a controlled delay const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); describe("Time-based Benchmarking", () => { it("should run in 'ops' mode by default", async () => { const suite = new Suite({ reporter: false }); suite.add("Default mode test", () => { // Simple operation Math.sqrt(Math.random()); }); const results = await suite.run(); assert.strictEqual(results.length, 1); assert.ok(results[0].opsSec !== undefined, "opsSec should be defined"); assert.ok( results[0].totalTime === undefined, "totalTime should not be defined", ); assert.ok( results[0].histogram.samples > 1, "Should have multiple samples in ops mode", ); }); it("should run in 'time' mode when specified at suite level", async () => { const suite = new Suite({ reporter: false, benchmarkMode: "time", }); const delayTime = 50; // 50ms delay suite.add("Time mode test", async () => { await delay(delayTime); }); const results = await suite.run(); assert.strictEqual(results.length, 1); assert.ok( results[0].totalTime !== undefined, "totalTime should be defined", ); assert.ok(results[0].opsSec === undefined, "opsSec should not be defined"); // Verify the time is approximately correct (allow for some overhead) const measuredTime = results[0].totalTime * 1000; // Convert to ms assert.ok( measuredTime >= delayTime - 0.999 && measuredTime < delayTime + 20, `Measured time (${measuredTime}ms) should be close to expected delay (${delayTime}ms)`, ); // Verify there's exactly 1 sample in time mode assert.strictEqual( results[0].histogram.samples, 1, "Should have exactly 1 sample in time mode", ); }); it("should average results across repeatSuite in time mode", async () => { const suite = new Suite({ reporter: false, benchmarkMode: "time", }); const repeatCount = 5; // A very fast operation that should be consistent suite.add("Repeat time test", { repeatSuite: repeatCount }, () => { // Simple operation const x = 1 + 1; }); const results = await suite.run(); assert.strictEqual(results.length, 1); assert.ok( results[0].totalTime !== undefined, "totalTime should be defined", ); // Verify the number of samples matches repeatSuite assert.strictEqual( results[0].histogram.samples, repeatCount, `Should have exactly ${repeatCount} samples with repeatSuite=${repeatCount}`, ); }); it("should not mix modes within the same suite", async () => { // This test verifies that benchmarkMode is a suite-level setting // and cannot be overridden at the benchmark level const opsSuite = new Suite({ reporter: false }); const timeSuite = new Suite({ reporter: false, benchmarkMode: "time" }); // Add benchmarks to both suites opsSuite.add("Ops benchmark", () => Math.random()); timeSuite.add("Time benchmark", () => Math.random()); // Run both suites const opsResults = await opsSuite.run(); const timeResults = await timeSuite.run(); // Verify ops suite reports ops/sec assert.ok( opsResults[0].opsSec !== undefined, "opsSec should be defined for ops suite", ); assert.ok( opsResults[0].totalTime === undefined, "totalTime should not be defined for ops suite", ); // Verify time suite reports totalTime assert.ok( timeResults[0].totalTime !== undefined, "totalTime should be defined for time suite", ); assert.ok( timeResults[0].opsSec === undefined, "opsSec should not be defined for time suite", ); }); it("should validate benchmarkMode option", () => { // Valid modes assert.doesNotThrow(() => { new Suite({ benchmarkMode: "ops" }); new Suite({ benchmarkMode: "time" }); }); // Invalid modes assert.throws(() => new Suite({ benchmarkMode: "invalid" }), { code: "ERR_INVALID_ARG_VALUE", }); assert.throws(() => new Suite({ benchmarkMode: 123 }), { code: "ERR_INVALID_ARG_TYPE", }); }); }); ================================================ FILE: test/ttest.js ================================================ const { describe, it } = require("node:test"); const assert = require("node:assert"); const { mean, variance, welchTTest, compareBenchmarks, tDistCdf, getSignificanceStars, } = require("../lib/utils/ttest"); describe("T-Test Utilities", () => { describe("mean", () => { it("should return 0 for empty array", () => { assert.strictEqual(mean([]), 0); }); it("should calculate mean correctly", () => { assert.strictEqual(mean([1, 2, 3, 4, 5]), 3); assert.strictEqual(mean([10, 20, 30]), 20); assert.strictEqual(mean([5]), 5); }); it("should handle negative numbers", () => { assert.strictEqual(mean([-1, 0, 1]), 0); assert.strictEqual(mean([-10, -5, 0, 5, 10]), 0); }); }); describe("variance", () => { it("should return 0 for arrays with less than 2 elements", () => { assert.strictEqual(variance([]), 0); assert.strictEqual(variance([5]), 0); }); it("should calculate sample variance correctly (with Bessel's correction)", () => { // Sample variance with n-1 denominator // Mean = (2+4+4+4+5+5+7+9) / 8 = 5 // Variance = [(2-5)² + (4-5)² + (4-5)² + (4-5)² + (5-5)² + (5-5)² + (7-5)² + (9-5)²] / (8-1) // = [9 + 1 + 1 + 1 + 0 + 0 + 4 + 16] / 7 = 32/7 ≈ 4.571 const arr = [2, 4, 4, 4, 5, 5, 7, 9]; const m = mean(arr); const expectedVariance = 32 / 7; // ≈ 4.571 assert.ok(Math.abs(variance(arr, m) - expectedVariance) < 0.01); }); it("should work without pre-calculated mean", () => { const arr = [2, 4, 4, 4, 5, 5, 7, 9]; assert.ok(Math.abs(variance(arr) - 32 / 7) < 0.01); }); }); describe("tDistCdf", () => { it("should return 0.5 for t=0", () => { const result = tDistCdf(0, 10); assert.ok(Math.abs(result - 0.5) < 0.001); }); it("should approach 1 for large positive t", () => { const result = tDistCdf(10, 30); assert.ok(result > 0.999); }); it("should approach 0 for large negative t", () => { const result = tDistCdf(-10, 30); assert.ok(result < 0.001); }); it("should return approximately correct values for known t-values", () => { // For t=1.96 with large df, should be approximately 0.975 (two-tailed 95%) const result = tDistCdf(1.96, 100); assert.ok(Math.abs(result - 0.975) < 0.01); }); }); describe("welchTTest", () => { it("should return non-significant for identical samples", () => { const sample1 = [10, 10, 10, 10, 10]; const sample2 = [10, 10, 10, 10, 10]; const result = welchTTest(sample1, sample2); assert.strictEqual(result.tStatistic, 0); assert.strictEqual(result.significant, false); assert.strictEqual(result.pValue, 1); }); it("should return significant for clearly different samples", () => { // Two clearly different distributions const sample1 = [100, 101, 102, 99, 100, 101, 100, 99, 102, 100]; const sample2 = [50, 51, 49, 50, 52, 48, 51, 49, 50, 51]; const result = welchTTest(sample1, sample2); assert.ok(result.significant); assert.ok(result.pValue < 0.05); assert.ok(result.tStatistic > 0); // sample1 > sample2 }); it("should handle samples with different sizes", () => { const sample1 = [100, 101, 102, 99, 100]; const sample2 = [50, 51, 49, 50, 52, 48, 51, 49, 50, 51, 50, 49]; const result = welchTTest(sample1, sample2); assert.ok(result.significant); assert.ok(result.pValue < 0.05); }); it("should return safe values for insufficient samples", () => { const result = welchTTest([1], [2]); assert.strictEqual(result.tStatistic, 0); assert.strictEqual(result.pValue, 1); assert.strictEqual(result.significant, false); }); it("should handle edge case with zero variance", () => { const sample1 = [5, 5, 5, 5, 5]; const sample2 = [3, 3, 3, 3, 3]; const result = welchTTest(sample1, sample2); // Different means, zero variance - should be significant assert.strictEqual(result.significant, true); assert.strictEqual(result.pValue, 0); }); }); describe("compareBenchmarks", () => { it("should indicate no significant difference for similar samples", () => { // Samples with overlapping distributions const sample1 = [100, 102, 98, 101, 99, 100, 101, 99, 100, 102]; const sample2 = [99, 101, 100, 98, 102, 100, 99, 101, 100, 98]; const result = compareBenchmarks(sample1, sample2); assert.strictEqual(result.significant, false); assert.strictEqual(result.difference, "same"); }); it("should indicate faster when first sample is significantly higher", () => { const sample1 = [200, 201, 199, 202, 198, 200, 201, 199, 200, 202]; const sample2 = [100, 101, 99, 100, 102, 98, 101, 99, 100, 101]; const result = compareBenchmarks(sample1, sample2); assert.strictEqual(result.significant, true); assert.strictEqual(result.difference, "faster"); assert.ok(result.pValue < 0.05); }); it("should indicate slower when first sample is significantly lower", () => { const sample1 = [50, 51, 49, 50, 52, 48, 51, 49, 50, 51]; const sample2 = [100, 101, 99, 100, 102, 98, 101, 99, 100, 101]; const result = compareBenchmarks(sample1, sample2); assert.strictEqual(result.significant, true); assert.strictEqual(result.difference, "slower"); assert.ok(result.pValue < 0.05); }); it("should respect custom alpha level", () => { // Create samples where p-value is between 0.01 and 0.05 const sample1 = [100, 102, 104, 101, 103, 100, 102, 101, 103, 100]; const sample2 = [98, 99, 97, 98, 100, 96, 99, 97, 98, 99]; const result005 = compareBenchmarks(sample1, sample2, 0.05); const result001 = compareBenchmarks(sample1, sample2, 0.01); // With alpha=0.05, might be significant; with alpha=0.01, might not be // The exact behavior depends on the p-value assert.ok(result005.pValue === result001.pValue); }); it("should include confidence percentage in result", () => { const sample1 = [100, 101, 102, 99, 100]; const sample2 = [50, 51, 49, 50, 52]; const result = compareBenchmarks(sample1, sample2); assert.ok(result.confidence.endsWith("%")); assert.ok(result.tStatistic !== undefined); assert.ok(result.stars !== undefined); }); it("should include significance stars based on p-value", () => { // Highly significant difference (p < 0.001) const sample1 = [200, 201, 199, 202, 198, 200, 201, 199, 200, 202]; const sample2 = [100, 101, 99, 100, 102, 98, 101, 99, 100, 101]; const result = compareBenchmarks(sample1, sample2); assert.strictEqual(result.stars, "***"); assert.ok(result.degreesOfFreedom !== undefined); }); }); describe("getSignificanceStars", () => { it("should return *** for p < 0.001", () => { assert.strictEqual(getSignificanceStars(0.0001), "***"); assert.strictEqual(getSignificanceStars(0.0009), "***"); }); it("should return ** for 0.001 <= p < 0.01", () => { assert.strictEqual(getSignificanceStars(0.001), "**"); assert.strictEqual(getSignificanceStars(0.005), "**"); assert.strictEqual(getSignificanceStars(0.009), "**"); }); it("should return * for 0.01 <= p < 0.05", () => { assert.strictEqual(getSignificanceStars(0.01), "*"); assert.strictEqual(getSignificanceStars(0.03), "*"); assert.strictEqual(getSignificanceStars(0.049), "*"); }); it("should return empty string for p >= 0.05", () => { assert.strictEqual(getSignificanceStars(0.05), ""); assert.strictEqual(getSignificanceStars(0.1), ""); assert.strictEqual(getSignificanceStars(0.5), ""); assert.strictEqual(getSignificanceStars(1), ""); }); }); }); describe("T-Test Integration with analyze", () => { const { analyze } = require("../lib/utils/analyze"); it("should not include significanceTest when ttest is false", () => { const results = [ { name: "baseline", opsSec: 100, baseline: true, histogram: { sampleData: [100, 101, 99, 100, 102] }, }, { name: "test", opsSec: 200, histogram: { sampleData: [200, 201, 199, 200, 202] }, }, ]; const analyzed = analyze(results, true, { ttest: false }); const testResult = analyzed.find((r) => r.name === "test"); assert.strictEqual(testResult.significanceTest, undefined); }); it("should include significanceTest when ttest is true and opsSecPerRun >= 30", () => { // Generate 30+ opsSecPerRun samples (from repeatSuite) const baselineOpsSecPerRun = Array.from( { length: 30 }, (_, i) => 100 + (i % 3) - 1, ); const testOpsSecPerRun = Array.from( { length: 30 }, (_, i) => 200 + (i % 3) - 1, ); const results = [ { name: "baseline", opsSec: 100, baseline: true, opsSecPerRun: baselineOpsSecPerRun, }, { name: "test", opsSec: 200, opsSecPerRun: testOpsSecPerRun, }, ]; const analyzed = analyze(results, true, { ttest: true }); const testResult = analyzed.find((r) => r.name === "test"); assert.ok(testResult.significanceTest !== undefined); assert.ok(typeof testResult.significanceTest.significant === "boolean"); assert.ok(typeof testResult.significanceTest.pValue === "number"); assert.ok(typeof testResult.significanceTest.confidence === "string"); }); it("should not include significanceTest without opsSecPerRun", () => { const results = [ { name: "baseline", opsSec: 100, baseline: true, }, { name: "test", opsSec: 200, }, ]; const analyzed = analyze(results, true, { ttest: true }); const testResult = analyzed.find((r) => r.name === "test"); // Should not throw, and significanceTest should not be set (no opsSecPerRun) assert.strictEqual(testResult.significanceTest, undefined); }); it("should not include significanceTest when opsSecPerRun < 30", () => { const results = [ { name: "baseline", opsSec: 100, baseline: true, opsSecPerRun: Array.from({ length: 10 }, () => 100), }, { name: "test", opsSec: 200, opsSecPerRun: Array.from({ length: 10 }, () => 200), }, ]; const analyzed = analyze(results, true, { ttest: true }); const testResult = analyzed.find((r) => r.name === "test"); // Should not throw, and significanceTest should not be set (not enough samples) assert.strictEqual(testResult.significanceTest, undefined); }); it("should detect significant difference between clearly different benchmarks", () => { // Generate 30+ opsSecPerRun with clearly different means const baselineOpsSecPerRun = Array.from( { length: 30 }, (_, i) => 100 + (i % 5) - 2, ); const fastOpsSecPerRun = Array.from( { length: 30 }, (_, i) => 200 + (i % 5) - 2, ); const results = [ { name: "baseline", opsSec: 100, baseline: true, opsSecPerRun: baselineOpsSecPerRun, }, { name: "fast", opsSec: 200, opsSecPerRun: fastOpsSecPerRun, }, ]; const analyzed = analyze(results, true, { ttest: true }); const fastResult = analyzed.find((r) => r.name === "fast"); assert.ok(fastResult.significanceTest.significant); assert.ok(fastResult.significanceTest.pValue < 0.05); }); it("should not mark as significant when differences are within noise", () => { // Same benchmark run twice - should have similar results with high variance overlap // Generate 30+ opsSecPerRun with overlapping distributions const baselineOpsSecPerRun = Array.from( { length: 30 }, (_, i) => 100 + ((i % 5) - 2) * 2, ); const similarOpsSecPerRun = Array.from( { length: 30 }, (_, i) => 101 + ((i % 5) - 2) * 2, ); const results = [ { name: "baseline", opsSec: 100, baseline: true, opsSecPerRun: baselineOpsSecPerRun, }, { name: "similar", opsSec: 101, // Very close to baseline opsSecPerRun: similarOpsSecPerRun, }, ]; const analyzed = analyze(results, true, { ttest: true }); const similarResult = analyzed.find((r) => r.name === "similar"); // With overlapping distributions, should not be significant assert.strictEqual(similarResult.significanceTest.significant, false); }); }); describe("Statistical significance requires repeatSuite >= 30", () => { const { analyze } = require("../lib/utils/analyze"); it("should only compute significance when repeatSuite provides 30+ samples", () => { // With 30+ opsSecPerRun, significance should be computed const results = [ { name: "baseline", opsSec: 100, baseline: true, opsSecPerRun: Array.from({ length: 30 }, () => 100), }, { name: "test", opsSec: 200, opsSecPerRun: Array.from({ length: 30 }, () => 200), }, ]; const analyzed = analyze(results, true, { ttest: true }); const testResult = analyzed.find((r) => r.name === "test"); assert.ok(testResult.significanceTest !== undefined); }); it("should not compute significance when repeatSuite < 30", () => { // With fewer than 30 opsSecPerRun, significance should not be computed const results = [ { name: "baseline", opsSec: 100, baseline: true, opsSecPerRun: Array.from({ length: 10 }, () => 100), }, { name: "test", opsSec: 200, opsSecPerRun: Array.from({ length: 10 }, () => 200), }, ]; const analyzed = analyze(results, true, { ttest: true }); const testResult = analyzed.find((r) => r.name === "test"); assert.strictEqual(testResult.significanceTest, undefined); }); }); ================================================ FILE: test/worker.js ================================================ const workerThreads = require("node:worker_threads"); const { describe, it, before, after, mock } = require("node:test"); const assert = require("node:assert"); function noop() {} describe("Using worker_threads", () => { before(async () => { mock.method(workerThreads, "Worker"); const { Suite } = require("../lib/index"); const bench = new Suite({ reporter: noop, useWorkers: true, }); bench .add("Import with node: prefix", () => { return import("node:fs"); }) .add("Import without node: prefix", () => { return import("node:fs"); }) .add("async test", async () => { return import("node:fs"); }) .add("async with timer", async (timer) => { timer.start(); let i = 0; while (i++ < timer.count) { await import("node:fs"); } timer.end(timer.count); }); await bench.run(); }); after(() => { mock.restoreAll(); }); it("should create a new Worker 4 times", () => { assert.strictEqual(workerThreads.Worker.mock.calls.length, 4); }); }); ================================================ FILE: types/types.test-d.ts ================================================ import type { Histogram } from "node:perf_hooks"; import { expectAssignable, expectNotAssignable, expectType } from "tsd"; import { MemoryPlugin, Suite, V8GetOptimizationStatus, V8NeverOptimizePlugin, V8OptimizeOnNextCallPlugin, chartReport, csvReport, htmlReport, jsonReport, prettyReport, textReport, } from ".."; import type { BenchNode } from ".."; expectType(new Suite()); expectType( new Suite({ reporter: textReport, benchmarkMode: "ops", useWorkers: true, plugins: [new V8NeverOptimizePlugin()], }), ); expectType(new Suite({ reporter: false })); expectType(new Suite({ reporter: null })); expectType(new Suite({ minSamples: 20 })); expectAssignable({}); expectAssignable({ reporter: chartReport }); expectAssignable({ benchmarkMode: "time" }); expectAssignable({ useWorkers: false }); expectAssignable({ plugins: [new V8GetOptimizationStatus()], }); expectAssignable({ minSamples: 20 }); expectNotAssignable({ unknownOption: "test" }); expectNotAssignable({ reporter: "not-a-function" }); expectNotAssignable({ minSamples: "not-a-number" }); expectAssignable({}); expectAssignable({ minTime: 0.1 }); expectAssignable({ maxTime: 1 }); expectAssignable({ repeatSuite: 2 }); expectAssignable({ minSamples: 5 }); expectNotAssignable({ minTime: "not-a-number" }); // Test Suite.add method const suite = new Suite(); expectType( suite.add("sync benchmark", () => { const arr = []; for (let i = 0; i < 1000; i++) { arr.push(i); } }), ); expectType( suite.add("async benchmark", async () => { await new Promise((resolve) => setTimeout(resolve, 10)); }), ); expectType( suite.add( "benchmark with options", { minTime: 0.1, maxTime: 1, repeatSuite: 2, minSamples: 5 }, () => { /* ... */ }, ), ); const managedBenchFn: BenchNode.BenchmarkFunction = (timer) => { if (timer) { expectType<() => void>(timer.start); expectType<(iterations?: number) => void>(timer.end); expectType(timer.count); timer.start(); for (let i = 0; i < timer.count; i++) { // some operation } } }; expectType(suite.add("managed benchmark", managedBenchFn)); // Test Suite.run method expectType>(suite.run()); suite.run().then((results) => { expectType(results); if (results.length > 0) { const result = results[0]; expectType(result.name); expectType(result.opsSec); expectType(result.opsSecPerRun); expectType(result.totalTime); expectType(result.iterations); expectType(result.histogram); expectType | undefined>(result.plugins); if (result.plugins?.V8GetOptimizationStatus) { expectType( result.plugins.V8GetOptimizationStatus.optimizationStatuses, ); } } }); // Test ReporterFunction const sampleResults: BenchNode.BenchmarkResult[] = [ { name: "sample", iterations: 100, histogram: {} as Histogram, // Cast for simplicity in type test opsSec: 10000, opsSecPerRun: [10000], totalTime: 0.1, plugins: { MyPlugin: { data: "value" } }, }, ]; expectType(textReport(sampleResults)); expectType(chartReport(sampleResults)); expectType(htmlReport(sampleResults)); expectType(jsonReport(sampleResults)); expectType(csvReport(sampleResults)); expectType(prettyReport(sampleResults)); // Test Plugins const plugin1 = new V8NeverOptimizePlugin(); expectAssignable(plugin1); if (plugin1.isSupported?.()) { expectType(plugin1.isSupported()); const varNames: BenchNode.PluginHookVarNames = { awaitOrEmpty: "", bench: "fn", context: "context", timer: "timer", managed: false, }; expectType(plugin1.beforeClockTemplate(varNames)); expectType(plugin1.toString()); } const plugin2 = new V8GetOptimizationStatus(); expectAssignable(plugin2); if (plugin2.isSupported?.()) { expectType(plugin2.isSupported()); const varNames: BenchNode.PluginHookVarNames = { awaitOrEmpty: "", bench: "fn", context: "context", timer: "timer", managed: false, }; const benchmarkResult: BenchNode.OnCompleteBenchmarkResult = [0, 0, {}]; expectType(plugin2.afterClockTemplate(varNames)); expectType(plugin2.onCompleteBenchmark(benchmarkResult)); expectType(plugin2.toString()); } const plugin3 = new V8OptimizeOnNextCallPlugin(); expectAssignable(plugin3); if (plugin3.isSupported?.()) { expectType(plugin3.isSupported()); const varNames: BenchNode.PluginHookVarNames = { awaitOrEmpty: "", bench: "fn", context: "context", timer: "timer", managed: false, }; expectType(plugin3.beforeClockTemplate(varNames)); expectType(plugin3.toString()); } const plugin4 = new MemoryPlugin(); expectAssignable(plugin4); if (plugin4.isSupported?.()) { expectType(plugin3.isSupported()); const varNames: BenchNode.PluginHookVarNames = { awaitOrEmpty: "", bench: "fn", context: "context", timer: "timer", managed: false, }; const benchmarkResult: BenchNode.OnCompleteBenchmarkResult = [0, 0, {}]; expectType(plugin4.beforeClockTemplate(varNames)); expectType(plugin4.afterClockTemplate(varNames)); expectType(plugin4.onCompleteBenchmark(benchmarkResult)); expectType(plugin4.toString()); expectType(plugin4.getReport("test")); expectType(plugin4.getResult("test")); } expectAssignable(new Suite()); expectAssignable(new V8NeverOptimizePlugin()); expectAssignable( new V8GetOptimizationStatus(), ); expectAssignable( new V8OptimizeOnNextCallPlugin(), ); expectAssignable(new MemoryPlugin());