├── .github └── workflows │ ├── commit-message-validation.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .npmignore ├── .release-please-manifest.json ├── CHANGELOG.md ├── README.md ├── assets ├── README.md ├── logo-text-dark.svg ├── logo-text-light.svg └── logo.svg ├── biome.json ├── doc └── Plugins.md ├── examples ├── .gitignore ├── 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 ├── default-report │ └── node.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 │ ├── 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 ├── run.sh ├── string-replace │ ├── node.js │ └── node.js.log ├── string-searching │ ├── node.js │ └── node.js.log ├── time-mode.js └── worker-threads │ └── node.js ├── lib ├── clock.js ├── histogram.js ├── index.js ├── lifecycle.js ├── plugins.js ├── plugins │ ├── memory.js │ ├── v8-never-opt.js │ ├── v8-opt.js │ └── v8-print-status.js ├── report.js ├── reporter │ ├── chart.js │ ├── csv.js │ ├── html.js │ ├── json.js │ ├── template.html │ └── text.js ├── utils │ └── styleText.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 └── worker.js /.github/workflows/commit-message-validation.yml: -------------------------------------------------------------------------------- 1 | name: Commit Message Validation 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | validate: 8 | name: Validate 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | 15 | - uses: webiny/action-conventional-commits@v1.3.0 16 | with: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: write 10 | pull-requests: write 11 | id-token: write 12 | 13 | jobs: 14 | release-please: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout Code 18 | uses: actions/checkout@v4 19 | 20 | - name: Release Please 21 | uses: google-github-actions/release-please-action@v4 22 | id: release 23 | 24 | - name: Use Node 22.x 25 | uses: actions/setup-node@v4 26 | if: ${{ steps.release.outputs.release_created }} 27 | with: 28 | node-version: 22 29 | registry-url: 'https://registry.npmjs.org' 30 | 31 | - name: Install Deps 32 | run: npm install 33 | if: ${{ steps.release.outputs.release_created }} 34 | 35 | - name: NPM Publish 36 | run: npm publish --provenance --access public 37 | if: ${{ steps.release.outputs.release_created }} 38 | env: 39 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 40 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [18, 20, 22, 23] 16 | 17 | env: 18 | CI: true 19 | 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v4 23 | 24 | - name: Set up Node.js 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | 29 | - name: Install dependencies 30 | run: npm install 31 | 32 | - name: Run linter 33 | run: npm run lint:ci 34 | 35 | - name: Run tests 36 | run: npm test 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tags 2 | .idea 3 | node_modules/ 4 | package-lock.json -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # We doesn't wan to publish the logo and other 2 | assets 3 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "0.7.0" 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.7.0](https://github.com/RafaelGSS/bench-node/compare/v0.6.0...v0.7.0) (2025-05-15) 4 | 5 | 6 | ### Features 7 | 8 | * add opsSecPerRun result ([#74](https://github.com/RafaelGSS/bench-node/issues/74)) ([34ad9e9](https://github.com/RafaelGSS/bench-node/commit/34ad9e95b23d92d80442955569f04efdafc97655)) 9 | 10 | ## [0.6.0](https://github.com/RafaelGSS/bench-node/compare/v0.5.4...v0.6.0) (2025-04-30) 11 | 12 | 13 | ### Features 14 | 15 | * add time mode benchmark ([#71](https://github.com/RafaelGSS/bench-node/issues/71)) ([9d72044](https://github.com/RafaelGSS/bench-node/commit/9d72044a13a9fd93ae5f1b97fc26ff4a7db7d5f1)) 16 | 17 | 18 | ### Documentation 19 | 20 | * add JSDoc to functions ([#69](https://github.com/RafaelGSS/bench-node/issues/69)) ([e5c6695](https://github.com/RafaelGSS/bench-node/commit/e5c66957ec737f1a8f5ecc4f204e8032daf50aca)) 21 | 22 | 23 | ### Miscellaneous Chores 24 | 25 | * start issuing semver-minor ([3a261ae](https://github.com/RafaelGSS/bench-node/commit/3a261ae94ea029e8aed68153968570eb7c38a2c1)) 26 | 27 | ## [0.5.4](https://github.com/RafaelGSS/bench-node/compare/v0.5.3...v0.5.4) (2025-03-13) 28 | 29 | 30 | ### Features 31 | 32 | * 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)) 33 | 34 | ## [0.5.3](https://github.com/RafaelGSS/bench-node/compare/v0.5.2...v0.5.3) (2025-02-24) 35 | 36 | 37 | ### Features 38 | 39 | * add min samples as param ([#65](https://github.com/RafaelGSS/bench-node/issues/65)) ([9c6812e](https://github.com/RafaelGSS/bench-node/commit/9c6812e1124f44d95f8d086cba01b5302ec5187e)) 40 | 41 | 42 | ### Miscellaneous Chores 43 | 44 | * **readme:** clean + update ([#61](https://github.com/RafaelGSS/bench-node/issues/61)) ([b5e1e8b](https://github.com/RafaelGSS/bench-node/commit/b5e1e8bb507b9f8b017a91e8c814fb1046908042)) 45 | 46 | ## [0.5.2](https://github.com/RafaelGSS/bench-node/compare/v0.5.1...v0.5.2) (2025-01-27) 47 | 48 | 49 | ### Features 50 | 51 | * **chartReport:** include node-v ([#53](https://github.com/RafaelGSS/bench-node/issues/53)) ([695842e](https://github.com/RafaelGSS/bench-node/commit/695842eb58c8b6106598a4cef26fe44a552065c6)) 52 | * improve htmlReport UX ([#60](https://github.com/RafaelGSS/bench-node/issues/60)) ([79ce533](https://github.com/RafaelGSS/bench-node/commit/79ce5335e95d531b90c4e26fde983ec1c6de9502)) 53 | 54 | 55 | ### Documentation 56 | 57 | * **readme:** add badges and links ([f85f809](https://github.com/RafaelGSS/bench-node/commit/f85f809aa0f85987d3b01ef9737ed0a855e46741)) 58 | 59 | 60 | ### Miscellaneous Chores 61 | 62 | * **"branding":** add logo ([#58](https://github.com/RafaelGSS/bench-node/issues/58)) ([3eedd06](https://github.com/RafaelGSS/bench-node/commit/3eedd064791f5804810545db247a509aea618234)) 63 | 64 | ## [0.5.1](https://github.com/RafaelGSS/bench-node/compare/v0.5.0...v0.5.1) (2025-01-19) 65 | 66 | 67 | ### Features 68 | 69 | * create csv reporter ([#38](https://github.com/RafaelGSS/bench-node/issues/38)) ([11be8e5](https://github.com/RafaelGSS/bench-node/commit/11be8e52e8cbb5e17f378a8462dd1c4bf7b35351)) 70 | * **reporter:** polish chart output ([#40](https://github.com/RafaelGSS/bench-node/issues/40)) ([91082b6](https://github.com/RafaelGSS/bench-node/commit/91082b6441cfa6ba7b195d7386d493d689e29454)) 71 | * use biome linter ([#34](https://github.com/RafaelGSS/bench-node/issues/34)) ([11c1108](https://github.com/RafaelGSS/bench-node/commit/11c11088e12d2b77547389eb0a5055ad3ff11427)) 72 | 73 | 74 | ### Bug Fixes 75 | 76 | * ignore package.json due to release-please ([7c95b0a](https://github.com/RafaelGSS/bench-node/commit/7c95b0a4fd41aa81576503ac9444a775ed498eda)) 77 | * lint package.json ([549f6ca](https://github.com/RafaelGSS/bench-node/commit/549f6ca574f4a30915e86dff9cd073b3d90def1e)) 78 | 79 | 80 | ### Miscellaneous Chores 81 | 82 | * **main:** release 0.5.1 ([#44](https://github.com/RafaelGSS/bench-node/issues/44)) ([4e51324](https://github.com/RafaelGSS/bench-node/commit/4e51324ea129c3607229aaec3b8d22ef221d0e7d)) 83 | * **main:** release 0.5.2 ([#45](https://github.com/RafaelGSS/bench-node/issues/45)) ([baf2014](https://github.com/RafaelGSS/bench-node/commit/baf20147c1f09f3e50491845e536c590db0d8aa5)) 84 | * **main:** release 0.5.3 ([4757182](https://github.com/RafaelGSS/bench-node/commit/4757182c015cfbd769ebf3969c8269120271e5b3)) 85 | 86 | 87 | ### Tests 88 | 89 | * add managed basic tests ([#36](https://github.com/RafaelGSS/bench-node/issues/36)) ([c491a32](https://github.com/RafaelGSS/bench-node/commit/c491a328329bc79b2ef8124856b162c8df0e8cfb)) 90 | 91 | 92 | ### Continuous Integration 93 | 94 | * **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)) 95 | 96 | ## [0.5.3](https://github.com/RafaelGSS/bench-node/compare/v0.5.2...v0.5.3) (2025-01-16) 97 | 98 | 99 | ### Features 100 | 101 | * add benchmark repetition ([#27](https://github.com/RafaelGSS/bench-node/issues/27)) ([d65e8aa](https://github.com/RafaelGSS/bench-node/commit/d65e8aab609b882a32331a48bb60fb81ee2db24a)) 102 | * add enough context to plugin methods to figure out bench task name ([c16cf34](https://github.com/RafaelGSS/bench-node/commit/c16cf340699cf198ca10146f30c158697afff908)) 103 | * add html reporter ([a69fdfb](https://github.com/RafaelGSS/bench-node/commit/a69fdfb7415eeb4645e7116be125ccf876d00ebc)) 104 | * add JSONReporter ([7b51c16](https://github.com/RafaelGSS/bench-node/commit/7b51c16db1446b4a2c921c2548e14462197f4779)) 105 | * code and examples ([#1](https://github.com/RafaelGSS/bench-node/issues/1)) ([503b573](https://github.com/RafaelGSS/bench-node/commit/503b573a67cf9245383da949274b30412c366084)) 106 | * create csv reporter ([#38](https://github.com/RafaelGSS/bench-node/issues/38)) ([11be8e5](https://github.com/RafaelGSS/bench-node/commit/11be8e52e8cbb5e17f378a8462dd1c4bf7b35351)) 107 | * **enrichers:** added V8NeverOptimizeEnricher and V8OptimizeOnNextCallEnricher ([16e842e](https://github.com/RafaelGSS/bench-node/commit/16e842eb5dad9703fd009979f68b4f71c98436b2)) 108 | * initial commit ([ee2d46f](https://github.com/RafaelGSS/bench-node/commit/ee2d46fc446a481c7bca731639759e4b7529c405)) 109 | * **memory-enricher:** added support to report memory heap statistics ([441b3ad](https://github.com/RafaelGSS/bench-node/commit/441b3adfee5d92cdd32cb0d4bfd5e7b49d14c2af)) 110 | * **reporter:** polish chart output ([#40](https://github.com/RafaelGSS/bench-node/issues/40)) ([91082b6](https://github.com/RafaelGSS/bench-node/commit/91082b6441cfa6ba7b195d7386d493d689e29454)) 111 | * use biome linter ([#34](https://github.com/RafaelGSS/bench-node/issues/34)) ([11c1108](https://github.com/RafaelGSS/bench-node/commit/11c11088e12d2b77547389eb0a5055ad3ff11427)) 112 | 113 | 114 | ### Bug Fixes 115 | 116 | * handle htmlReport when bench suite is uppercase ([1685144](https://github.com/RafaelGSS/bench-node/commit/16851442e3fe3a97f8e6fc5c98993c77162dc4bc)) 117 | * **lifecycle:** missing imports ([08c6064](https://github.com/RafaelGSS/bench-node/commit/08c60646736ee1236cb371143594e337b1d5f502)) 118 | * typo ([d2a36ae](https://github.com/RafaelGSS/bench-node/commit/d2a36aec5dae24b9a4e95e4f055109a73d3b6bbc)) 119 | 120 | 121 | ### Miscellaneous Chores 122 | 123 | * add exec permission to run.sh ([5d0f4ef](https://github.com/RafaelGSS/bench-node/commit/5d0f4ef72849189472b6700ddf7e56376eea61a2)) 124 | * add node_modules to ignore ([478f24c](https://github.com/RafaelGSS/bench-node/commit/478f24c3fb8cd896e28e1c87e3212269fe9e31eb)) 125 | * **examples:** added example benchmarks ([b4b50b2](https://github.com/RafaelGSS/bench-node/commit/b4b50b23def45698c854bf3bbe434d3f3a92567d)) 126 | * **gitignore:** ignore .idea folder ([e9a13ae](https://github.com/RafaelGSS/bench-node/commit/e9a13ae640fd2ec2d7e714c0c6c9240f4ab1c628)) 127 | * **main:** release 0.5.1 ([#44](https://github.com/RafaelGSS/bench-node/issues/44)) ([4e51324](https://github.com/RafaelGSS/bench-node/commit/4e51324ea129c3607229aaec3b8d22ef221d0e7d)) 128 | * **main:** release 0.5.2 ([#45](https://github.com/RafaelGSS/bench-node/issues/45)) ([baf2014](https://github.com/RafaelGSS/bench-node/commit/baf20147c1f09f3e50491845e536c590db0d8aa5)) 129 | * rename to bench-node ([2f15705](https://github.com/RafaelGSS/bench-node/commit/2f157051e3b1988ac3a8094e0fc7e4daee267a48)) 130 | * rename to nodebenchmark ([9806a31](https://github.com/RafaelGSS/bench-node/commit/9806a31c819073d705bd59c29adc35e808e61d6c)) 131 | * **run:** added script to run all examples ([6733730](https://github.com/RafaelGSS/bench-node/commit/6733730de9fa83a0b6ee7f243b1c3c0576f6f4ad)) 132 | * update rafaelgss email ([a5ec544](https://github.com/RafaelGSS/bench-node/commit/a5ec5445a777c9db12181cae70cd47def0ac56c2)) 133 | 134 | 135 | ### Code Refactoring 136 | 137 | * **lib:** from esm to commonjs ([f25d0e4](https://github.com/RafaelGSS/bench-node/commit/f25d0e40c293a07fe865f09f9bd6693b3152e5b0)) 138 | * **lib:** make the code usable outside/inside node core ([c60c80e](https://github.com/RafaelGSS/bench-node/commit/c60c80e8fd6cad52f5275419252e313e03767893)) 139 | * **validators:** added missing validators on clock ([478fc7e](https://github.com/RafaelGSS/bench-node/commit/478fc7e3456c84797cd718b2c7eeb7e876bad2bc)) 140 | 141 | 142 | ### Tests 143 | 144 | * add a test documenting the plugin signature and lifecycle ([fd379d6](https://github.com/RafaelGSS/bench-node/commit/fd379d6ed51317504255eb78a24e33db21e0b3a7)) 145 | * add basic test suite ([8349ee0](https://github.com/RafaelGSS/bench-node/commit/8349ee0f96236646776fd12843c01d1d9c806b42)) 146 | * add managed basic tests ([#36](https://github.com/RafaelGSS/bench-node/issues/36)) ([c491a32](https://github.com/RafaelGSS/bench-node/commit/c491a328329bc79b2ef8124856b162c8df0e8cfb)) 147 | * add scenario for optimized managed benchmark ([74c4db1](https://github.com/RafaelGSS/bench-node/commit/74c4db1046857f9af57c0c54cc5bf801d0195339)) 148 | * add test case for copy function ([ddf6dc7](https://github.com/RafaelGSS/bench-node/commit/ddf6dc7b4e7a695f6bff5766788b4b0d5beec527)) 149 | * fix the plugin api test ([be8ec69](https://github.com/RafaelGSS/bench-node/commit/be8ec69ff9481ce55b8e49f5732e01a468f6b5de)) 150 | * include TODO test for managed and async ([15ff469](https://github.com/RafaelGSS/bench-node/commit/15ff46924bb969d724d1f92f5611a3f4385f0d47)) 151 | * increase percentage diff on CI ([fa57188](https://github.com/RafaelGSS/bench-node/commit/fa571883f30fd7033a12e05f291fe12bf4816152)) 152 | 153 | 154 | ### Build System 155 | 156 | * move run.sh to examples folder ([08ac769](https://github.com/RafaelGSS/bench-node/commit/08ac7699032a32f3a04a252cc48ee1514fd734bd)) 157 | 158 | 159 | ### Continuous Integration 160 | 161 | * **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)) 162 | 163 | ## [0.5.2](https://github.com/RafaelGSS/bench-node/compare/v0.5.1...v0.5.2) (2025-01-16) 164 | 165 | 166 | ### Features 167 | 168 | * add benchmark repetition ([#27](https://github.com/RafaelGSS/bench-node/issues/27)) ([d65e8aa](https://github.com/RafaelGSS/bench-node/commit/d65e8aab609b882a32331a48bb60fb81ee2db24a)) 169 | * add enough context to plugin methods to figure out bench task name ([c16cf34](https://github.com/RafaelGSS/bench-node/commit/c16cf340699cf198ca10146f30c158697afff908)) 170 | * add html reporter ([a69fdfb](https://github.com/RafaelGSS/bench-node/commit/a69fdfb7415eeb4645e7116be125ccf876d00ebc)) 171 | * add JSONReporter ([7b51c16](https://github.com/RafaelGSS/bench-node/commit/7b51c16db1446b4a2c921c2548e14462197f4779)) 172 | * code and examples ([#1](https://github.com/RafaelGSS/bench-node/issues/1)) ([503b573](https://github.com/RafaelGSS/bench-node/commit/503b573a67cf9245383da949274b30412c366084)) 173 | * create csv reporter ([#38](https://github.com/RafaelGSS/bench-node/issues/38)) ([11be8e5](https://github.com/RafaelGSS/bench-node/commit/11be8e52e8cbb5e17f378a8462dd1c4bf7b35351)) 174 | * **enrichers:** added V8NeverOptimizeEnricher and V8OptimizeOnNextCallEnricher ([16e842e](https://github.com/RafaelGSS/bench-node/commit/16e842eb5dad9703fd009979f68b4f71c98436b2)) 175 | * initial commit ([ee2d46f](https://github.com/RafaelGSS/bench-node/commit/ee2d46fc446a481c7bca731639759e4b7529c405)) 176 | * **memory-enricher:** added support to report memory heap statistics ([441b3ad](https://github.com/RafaelGSS/bench-node/commit/441b3adfee5d92cdd32cb0d4bfd5e7b49d14c2af)) 177 | * **reporter:** polish chart output ([#40](https://github.com/RafaelGSS/bench-node/issues/40)) ([91082b6](https://github.com/RafaelGSS/bench-node/commit/91082b6441cfa6ba7b195d7386d493d689e29454)) 178 | * use biome linter ([#34](https://github.com/RafaelGSS/bench-node/issues/34)) ([11c1108](https://github.com/RafaelGSS/bench-node/commit/11c11088e12d2b77547389eb0a5055ad3ff11427)) 179 | 180 | 181 | ### Bug Fixes 182 | 183 | * handle htmlReport when bench suite is uppercase ([1685144](https://github.com/RafaelGSS/bench-node/commit/16851442e3fe3a97f8e6fc5c98993c77162dc4bc)) 184 | * **lifecycle:** missing imports ([08c6064](https://github.com/RafaelGSS/bench-node/commit/08c60646736ee1236cb371143594e337b1d5f502)) 185 | * typo ([d2a36ae](https://github.com/RafaelGSS/bench-node/commit/d2a36aec5dae24b9a4e95e4f055109a73d3b6bbc)) 186 | 187 | 188 | ### Miscellaneous Chores 189 | 190 | * add exec permission to run.sh ([5d0f4ef](https://github.com/RafaelGSS/bench-node/commit/5d0f4ef72849189472b6700ddf7e56376eea61a2)) 191 | * add node_modules to ignore ([478f24c](https://github.com/RafaelGSS/bench-node/commit/478f24c3fb8cd896e28e1c87e3212269fe9e31eb)) 192 | * **examples:** added example benchmarks ([b4b50b2](https://github.com/RafaelGSS/bench-node/commit/b4b50b23def45698c854bf3bbe434d3f3a92567d)) 193 | * **gitignore:** ignore .idea folder ([e9a13ae](https://github.com/RafaelGSS/bench-node/commit/e9a13ae640fd2ec2d7e714c0c6c9240f4ab1c628)) 194 | * **main:** release 0.5.1 ([#44](https://github.com/RafaelGSS/bench-node/issues/44)) ([4e51324](https://github.com/RafaelGSS/bench-node/commit/4e51324ea129c3607229aaec3b8d22ef221d0e7d)) 195 | * rename to bench-node ([2f15705](https://github.com/RafaelGSS/bench-node/commit/2f157051e3b1988ac3a8094e0fc7e4daee267a48)) 196 | * rename to nodebenchmark ([9806a31](https://github.com/RafaelGSS/bench-node/commit/9806a31c819073d705bd59c29adc35e808e61d6c)) 197 | * **run:** added script to run all examples ([6733730](https://github.com/RafaelGSS/bench-node/commit/6733730de9fa83a0b6ee7f243b1c3c0576f6f4ad)) 198 | * update rafaelgss email ([a5ec544](https://github.com/RafaelGSS/bench-node/commit/a5ec5445a777c9db12181cae70cd47def0ac56c2)) 199 | 200 | 201 | ### Code Refactoring 202 | 203 | * **lib:** from esm to commonjs ([f25d0e4](https://github.com/RafaelGSS/bench-node/commit/f25d0e40c293a07fe865f09f9bd6693b3152e5b0)) 204 | * **lib:** make the code usable outside/inside node core ([c60c80e](https://github.com/RafaelGSS/bench-node/commit/c60c80e8fd6cad52f5275419252e313e03767893)) 205 | * **validators:** added missing validators on clock ([478fc7e](https://github.com/RafaelGSS/bench-node/commit/478fc7e3456c84797cd718b2c7eeb7e876bad2bc)) 206 | 207 | 208 | ### Tests 209 | 210 | * add a test documenting the plugin signature and lifecycle ([fd379d6](https://github.com/RafaelGSS/bench-node/commit/fd379d6ed51317504255eb78a24e33db21e0b3a7)) 211 | * add basic test suite ([8349ee0](https://github.com/RafaelGSS/bench-node/commit/8349ee0f96236646776fd12843c01d1d9c806b42)) 212 | * add managed basic tests ([#36](https://github.com/RafaelGSS/bench-node/issues/36)) ([c491a32](https://github.com/RafaelGSS/bench-node/commit/c491a328329bc79b2ef8124856b162c8df0e8cfb)) 213 | * add scenario for optimized managed benchmark ([74c4db1](https://github.com/RafaelGSS/bench-node/commit/74c4db1046857f9af57c0c54cc5bf801d0195339)) 214 | * add test case for copy function ([ddf6dc7](https://github.com/RafaelGSS/bench-node/commit/ddf6dc7b4e7a695f6bff5766788b4b0d5beec527)) 215 | * fix the plugin api test ([be8ec69](https://github.com/RafaelGSS/bench-node/commit/be8ec69ff9481ce55b8e49f5732e01a468f6b5de)) 216 | * include TODO test for managed and async ([15ff469](https://github.com/RafaelGSS/bench-node/commit/15ff46924bb969d724d1f92f5611a3f4385f0d47)) 217 | * increase percentage diff on CI ([fa57188](https://github.com/RafaelGSS/bench-node/commit/fa571883f30fd7033a12e05f291fe12bf4816152)) 218 | 219 | 220 | ### Build System 221 | 222 | * move run.sh to examples folder ([08ac769](https://github.com/RafaelGSS/bench-node/commit/08ac7699032a32f3a04a252cc48ee1514fd734bd)) 223 | 224 | 225 | ### Continuous Integration 226 | 227 | * **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)) 228 | 229 | ## [0.5.1](https://github.com/RafaelGSS/bench-node/compare/v0.5.0...v0.5.1) (2025-01-14) 230 | 231 | 232 | ### Features 233 | 234 | * create csv reporter ([#38](https://github.com/RafaelGSS/bench-node/issues/38)) ([11be8e5](https://github.com/RafaelGSS/bench-node/commit/11be8e52e8cbb5e17f378a8462dd1c4bf7b35351)) 235 | * **reporter:** polish chart output ([#40](https://github.com/RafaelGSS/bench-node/issues/40)) ([91082b6](https://github.com/RafaelGSS/bench-node/commit/91082b6441cfa6ba7b195d7386d493d689e29454)) 236 | * use biome linter ([#34](https://github.com/RafaelGSS/bench-node/issues/34)) ([11c1108](https://github.com/RafaelGSS/bench-node/commit/11c11088e12d2b77547389eb0a5055ad3ff11427)) 237 | 238 | 239 | ### Tests 240 | 241 | * add managed basic tests ([#36](https://github.com/RafaelGSS/bench-node/issues/36)) ([c491a32](https://github.com/RafaelGSS/bench-node/commit/c491a328329bc79b2ef8124856b162c8df0e8cfb)) 242 | 243 | 244 | ### Continuous Integration 245 | 246 | * **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)) 247 | -------------------------------------------------------------------------------- /assets/README.md: -------------------------------------------------------------------------------- 1 | # "Branding" 2 | 3 | ## Logo 4 | 5 | | Logo | Description | 6 | | --- | --- | 7 | | ![Logo](./logo.svg) | simple logo for readme | 8 | | ![Logo](./logo-text-light.svg) | logo with text for light backgrounds | 9 | | ![Logo](./logo-text-dark.svg) | logo with text for dark backgrounds | 10 | 11 | ## Other 12 | 13 | - **Font**: [Geist](https://fonts.google.com/specimen/Geist) 14 | - **Light Green**: `#84BA64` 15 | - **Dark Green**: `#2C682C` 16 | 17 | > The design of the assets was created by [@AugustinMauroy](https://github.com/AugustinMauroy). 18 | -------------------------------------------------------------------------------- /assets/logo-text-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /assets/logo-text-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", 3 | "organizeImports": { 4 | "enabled": true 5 | }, 6 | "linter": { 7 | "enabled": true, 8 | "rules": { 9 | "recommended": true, 10 | "style": { 11 | "noParameterAssign": "off", 12 | "noUselessElse": "off" 13 | } 14 | } 15 | }, 16 | "formatter": { 17 | "enabled": true 18 | }, 19 | "files": { 20 | "ignore": ["examples/*", "test/plugin-api-doc.js", "package.json"] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /doc/Plugins.md: -------------------------------------------------------------------------------- 1 | # Plugins 2 | 3 | The benchmark module supports a flexible plugin system that 4 | allows you to extend its functionality by adding custom plugins. 5 | This documentation explains how to create, validate, and use 6 | plugins within the benchmarking framework. 7 | 8 | [V8NeverOptimizePlugin](#class-v8neveroptimizeplugin) is enabled by default. 9 | 10 | To observe how a plugin is used, see the `plugin-api-doc.js` file in tests and explore its results. 11 | 12 | ## Structure 13 | 14 | Each plugin is expected to follow a specific structure with required methods 15 | for integration into the benchmark module. The plugins are required to define 16 | the following methods: 17 | 18 | * `isSupported()`: This method checks if the plugin can run in the 19 | current environment. If the plugin uses features specific to certain 20 | environments (e.g., V8 engine features), it should return `true` if those 21 | features are available and `false` otherwise. 22 | 23 | * `toString()`: This method should return a string representation of the plugin. 24 | It’s used for logging and error messages. 25 | 26 | In addition to these required methods, plugins can optionally define other 27 | methods based on their functionality, such as `beforeClockTemplate()`, 28 | `afterClockTemplate()`, `onCompleteBenchmark()`, and more. 29 | 30 | ## Plugin Methods 31 | 32 | ### `isSupported()` (required) 33 | 34 | This method checks if the plugin's functionality is available in the 35 | current environment. For instance, if a plugin uses specific V8 engine commands, 36 | this method ensures the environment supports them. 37 | 38 | ### `beforeClockTemplate(varNames)` 39 | 40 | * `varNames` {Object} 41 | * `bench` {string} - Name for the benchmark variable. 42 | * `context` {string} - Name for the context variable. 43 | * `timer` {string} - Name for the timer variable. 44 | * `awaitOrEmpty` {string} - A string with `await` or empty string (`''`). 45 | 46 | Some plugins need to modify or prepare the code before the benchmark starts. 47 | The `beforeClockTemplate()` method allows you to inject code before the timing 48 | process begins. 49 | 50 | This method must return an array where: 51 | 52 | * The first element is a string representing the JavaScript code to be executed 53 | before the benchmark function. 54 | 55 | * The second element (optional) is a string representing a function that will 56 | wrap the benchmark function. This wrapper is used to customize how the 57 | benchmark function is called during execution. 58 | 59 | The wrapped function provides a powerful way to manipulate how the benchmark 60 | is run without directly modifying the benchmark logic. 61 | 62 | ```js 63 | beforeClockTemplate() { 64 | let code = ''; 65 | 66 | code += ` 67 | function DoNotOptimize(x) {} 68 | // Prevent DoNotOptimize from optimizing or being inlined. 69 | %NeverOptimizeFunction(DoNotOptimize); 70 | ` 71 | return [code, 'DoNotOptimize']; 72 | } 73 | ``` 74 | 75 | In this example, the plugin injects the `DoNotOptimize` function and also 76 | provides it as a wrapper for the benchmark function. 77 | 78 | ### `afterClockTemplate(varNames)` 79 | 80 | * `varNames` {Object} 81 | * `bench` {string} - Name for the benchmark variable. 82 | * `context` {string} - Name for the context variable. 83 | * `timer` {string} - Name for the timer variable. 84 | * `awaitOrEmpty` {string} - A string with `await` or empty string (`''`). 85 | 86 | After the benchmark runs, this method can inject code to gather performance data 87 | or reset configurations. It must return an array where: 88 | 89 | * The first element is a string containing the JavaScript code to be executed 90 | after the benchmark finishes. 91 | 92 | Unlike `beforeClockTemplate`, `afterClockTemplate` does not support a second 93 | element in the returned array, as it only runs cleanup or data collection code 94 | after the benchmark is executed. 95 | 96 | ### `onCompleteBenchmark(result)` 97 | 98 | * `result` {Object} 99 | * `duration` {number} - Benchmark duration 100 | * `count` {number} - Number of iterations 101 | * `context` {Object} - A object used to store results after the benchmark clock 102 | 103 | This method is called when the benchmark completes. Plugins can collect and 104 | process data from the benchmark results in this step. 105 | 106 | ### `toString()` (required) 107 | 108 | This method returns a string identifier for the plugin, typically the plugin’s 109 | name. It is used in error messages and logging. 110 | 111 | ## Example Plugins 112 | 113 | Here are examples of plugins that follow the required structure and functionality. 114 | 115 | ```js 116 | class V8OptimizeOnNextCallPlugin { 117 | isSupported() { 118 | try { 119 | new Function(`%OptimizeFunctionOnNextCall(() => {})`)(); 120 | return true; 121 | } catch (e) { 122 | return false; 123 | } 124 | } 125 | 126 | beforeClockTemplate({ awaitOrEmpty, bench }) { 127 | let code = ''; 128 | 129 | code += `%OptimizeFunctionOnNextCall(${ bench }.fn);\n`; 130 | code += `${ awaitOrEmpty }${ bench }.fn();\n`; 131 | code += `${ awaitOrEmpty }${ bench }.fn();\n`; 132 | 133 | return [code]; 134 | } 135 | 136 | toString() { 137 | return 'V8OptimizeOnNextCallPlugin'; 138 | } 139 | } 140 | ``` 141 | 142 | ## Official Plugins 143 | 144 | This is a list of official plugins that can be fetched when requiring 145 | `bench-node` module. 146 | 147 | ```js 148 | const { V8OptimizeOnNextCallPlugin, Suite } = require('bench-node'); 149 | const suite = new Suite({ 150 | plugins: [new V8OptimizeOnNextCallPlugin()], 151 | }) 152 | ``` 153 | 154 | ### Class: `V8OptimizeOnNextCallPlugin` 155 | 156 | The `V8OptimizeOnNextCallPlugin` triggers the V8 engine to optimize the 157 | function before it is called. This can improve performance in repeated 158 | benchmarks. 159 | 160 | ### Class: `V8NeverOptimizePlugin` 161 | 162 | The `V8NeverOptimizePlugin` prevents the V8 engine from optimizing or inlining 163 | a function, useful when you want to benchmark functions without any 164 | optimization. 165 | 166 | ### Class: `V8GetOptimizationStatus` 167 | 168 | The `V8GetOptimizationStatus` plugin collects the V8 engine's optimization 169 | status for a given function after it has been benchmarked. 170 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RafaelGSS/bench-node/29aa66c882723f74490a37b2b3f082b27a9c965a/examples/.gitignore -------------------------------------------------------------------------------- /examples/chart-report/node.js: -------------------------------------------------------------------------------- 1 | const { Suite, chartReport } = require('../../lib'); 2 | const assert = require('node:assert'); 3 | 4 | const suite = new Suite({ 5 | reporter: chartReport, 6 | }); 7 | 8 | suite 9 | .add('single with matcher', function () { 10 | const pattern = /[123]/g 11 | const replacements = { 1: 'a', 2: 'b', 3: 'c' } 12 | const subject = '123123123123123123123123123123123123123123123123' 13 | const r = subject.replace(pattern, m => replacements[m]) 14 | assert.ok(r); 15 | }) 16 | .add('multiple replaces', function () { 17 | const subject = '123123123123123123123123123123123123123123123123' 18 | const r = subject.replace(/1/g, 'a').replace(/2/g, 'b').replace(/3/g, 'c') 19 | assert.ok(r); 20 | }) 21 | .run(); 22 | -------------------------------------------------------------------------------- /examples/create-uint32array/node.js: -------------------------------------------------------------------------------- 1 | const { Suite } = require('../../lib'); 2 | 3 | const suite = new Suite(); 4 | 5 | suite 6 | .add(`new Uint32Array(1024)`, function () { 7 | return new Uint32Array(1024); 8 | }) 9 | .add(`new Uint32Array(1024) with 10 repetitions`, {repeatSuite: 10}, function () { 10 | return new Uint32Array(1024); 11 | }) 12 | .add(`new Uint32Array(1024) with min samples of 50`, {minSamples: 50}, function() { 13 | return new Uint32Array(1024); 14 | }) 15 | .add(`[Managed] new Uint32Array(1024)`, function (timer) { 16 | const assert = require('node:assert'); 17 | 18 | let r; 19 | 20 | timer.start(); 21 | for (let i = 0; i < timer.count; i++) { 22 | r = new Uint32Array(1024); 23 | } 24 | timer.end(timer.count); 25 | 26 | assert.ok(r); 27 | }) 28 | .run(); 29 | -------------------------------------------------------------------------------- /examples/create-uint32array/node.js.log: -------------------------------------------------------------------------------- 1 | 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 2 | [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 3 | ---------------------------------------------------------------------------- 4 | 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 5 | [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 6 | ---------------------------------------------------------------------------- 7 | 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 8 | [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 9 | ---------------------------------------------------------------------------- 10 | -------------------------------------------------------------------------------- /examples/crypto-verify/node.js: -------------------------------------------------------------------------------- 1 | const crypto = require('node:crypto'); 2 | const { readFileSync } = require('fs'); 3 | const { Suite } = require('../../lib'); 4 | 5 | const rsaPrivateKey = readFileSync(`${__dirname}/private-key.pem`, 'utf-8'); 6 | const rsaPublicKey = readFileSync(`${__dirname}/public-key.pem`, 'utf-8'); 7 | 8 | const thing = 'hello world'; 9 | const algorithm = 'RSA-SHA256'; 10 | const signature = crypto.createSign(algorithm).update(thing).sign(rsaPrivateKey, 'base64'); 11 | 12 | const suite = new Suite(); 13 | 14 | suite 15 | .add(`crypto.createVerify('${algorithm}')`, function () { 16 | var verifier = crypto.createVerify(algorithm); 17 | verifier.update(thing); 18 | verifier.verify(rsaPublicKey, signature, 'base64'); 19 | }) 20 | .add(`crypto.verify('${algorithm}')`, function () { 21 | crypto.verify(algorithm, thing, rsaPublicKey, Buffer.from(signature, 'base64')); 22 | }) 23 | .add(`[Managed] crypto.createVerify('RSA-SHA256')`, function (timer) { 24 | const crypto = require('node:crypto'); 25 | const { readFileSync } =require('node:fs'); 26 | const assert = require('node:assert'); 27 | 28 | const rsaPrivateKey = readFileSync(`${__dirname}/private-key.pem`, 'utf-8'); 29 | const rsaPublicKey = readFileSync(`${__dirname}/public-key.pem`, 'utf-8'); 30 | 31 | const thing = 'hello world' 32 | const signature = crypto.createSign('RSA-SHA256').update(thing).sign(rsaPrivateKey, 'base64') 33 | 34 | let verifier; 35 | 36 | timer.start(); 37 | for (let i = 0; i < timer.count; i++) { 38 | verifier = crypto.createVerify('RSA-SHA256') 39 | verifier.update(thing) 40 | verifier.verify(rsaPublicKey, signature, 'base64') 41 | } 42 | timer.end(timer.count); 43 | 44 | assert.ok(verifier); 45 | }) 46 | .add(`[Managed] crypto.verify('RSA-SHA256')`, function (timer) { 47 | const crypto = require('node:crypto'); 48 | const { readFileSync } = require('node:fs'); 49 | const assert = require('node:assert'); 50 | 51 | const rsaPrivateKey = readFileSync(`${__dirname}/private-key.pem`, 'utf-8'); 52 | const rsaPublicKey = readFileSync(`${__dirname}/public-key.pem`, 'utf-8'); 53 | 54 | const thing = 'hello world' 55 | const signature = crypto.createSign('RSA-SHA256').update(thing).sign(rsaPrivateKey, 'base64') 56 | 57 | let r; 58 | 59 | timer.start(); 60 | for (let i = 0; i < timer.count; i++) { 61 | r = crypto.verify('RSA-SHA256', thing, rsaPublicKey, Buffer.from(signature, 'base64')); 62 | } 63 | timer.end(timer.count); 64 | 65 | assert.ok(r); 66 | }) 67 | .run(); 68 | -------------------------------------------------------------------------------- /examples/crypto-verify/node.js.log: -------------------------------------------------------------------------------- 1 | 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 2 | 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 3 | [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 4 | [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 5 | ---------------------------------------------------------------------------- 6 | 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 7 | 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 8 | [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 9 | [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 10 | ---------------------------------------------------------------------------- 11 | 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 12 | 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 13 | [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 14 | [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 15 | ---------------------------------------------------------------------------- 16 | -------------------------------------------------------------------------------- /examples/crypto-verify/private-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAz7QweYwr7puNtc8/6jf4pADXfY2/HVq7LgI7JXkAwW5HTEjU 3 | Rvu02SYC7n/ylr86Da4hcAEAFBC2baVh7J0EnLo6D4FA3yV4s5zj7tqL+p2CdniY 4 | MDEny+uxS/9eRuLf1RIYHjSVB6wOoU4lg9FtDOQA8W09ZDLvc3/4ZSwEf9O2J5Vm 5 | qcSGaPt35eIEBS0iIMiOUCGqbUcVMZkp4JO7I65HOK/g0Scvb1LLY6f4qDsThqyi 6 | ID/NXM5LtjnztRP+9dCLT9DdwC+LeU2te6vcEwC2bPYs3nUwkZL4qqIWO6GHaavn 7 | gFKSmbmc9muVAPmYW/ffMtznKZaWCp9Li0JGuQIDAQABAoIBAQC7OI7hYSpQgEKy 8 | eUgBlcY3/tI/SD/W8+v5QuWRl4rI0ODPsG44NbcEbbECzq4al/B6WFWnoh8x9waZ 9 | uxOTts1rgKnJRBb3jc1JCcijirfWhZgNthJojkZzF9bOzDds6iAc7Zxzza3wJnVh 10 | jRFfyqzji7oV5QQLh6YzlEyQ1aaQmOKTkCDV1UDetI8iItUvq3i8EwsFmtfvXjTl 11 | aymL/xSd3R6w3WDEd8PS5ZPDoFmkt2h/4IhOo6bVXLwWBWqcopRAvzGQrc806c2l 12 | jjePwk9fddIG+vIHc8DHqaC/WzMR6iBt3K7Q9Eyu4k97qeOxB5TIEVlhxg2DJON9 13 | DlGtInXpAoGBAPje3L0xL441i7kh7GqiWtXiDgxVwdPmN5p7n755MddSxvu8894h 14 | T9/LxwAVcpolXANgyRtrl5bu/gXgznKefmVr976BzG2DY33PESf5FnHZAmixWSfz 15 | VOdfVlz+Bk/BI+nVwmeT0+8NSHJ6TJ/9NOSzI9ctUceszIHG5/Gfc2wHAoGBANWn 16 | bHNT7aphFqyUMfWyc6+h8Radq/RVMrOGeD2wRRqUTeb2Ydbx2OwB+gcwnlbsQjfS 17 | BYAa3kn1z5K5kOigB9qu6x7zuM9SMReKIlMg3NpYVyKKj+JaNF3YArFa5TWy0B26 18 | wNJ0FiiZyPq4eP5hPwM+Bed4ytIRWa54P5GeRYc/AoGAWT8ijb4rvaW6G4Ps0jiy 19 | tmzAeO/v+FtgqUeX+6helUccEH6sPYZYrHrZPFB0ro6jNprow6qLzBacheMeZcAs 20 | t5ZGW80UUFmDvkQZdOpAgEdAM+cVf9wlIGvx/psiDEvI4zxC4P4ETH/I8TSmceFN 21 | rI4JVkrsPtza4ddAqkdyDtUCgYAaoMs7dHJikccpqy6u2JbihORvVSdhRF0VUuUZ 22 | iyaRsXokFwEKsQnAIF7xFnYljzyRiHN3C+I4hZJhTw9obsmLz9EuAmI+NJg5vtWY 23 | Vrgv3mK9w1c7dtKf/5QWVqXKk4asreHqWN2KIeCSnvs1eRlJZimGN9/PXqo2vHXv 24 | yDISMQKBgE22EhVB99rEsLXDCXOUHIFGpW46ZavQc4DLXg+uIQm3y0WWyc0oyNF8 25 | 9kr1luu5QI5PV9mWpSKa3SKmc74biyxhlUp/DYuMyuSXpdDegMAKheAHVXP9ToU/ 26 | 5qhq4HBcISN3CwSAYcFcbqC3cB07T+IwX6NTEz4zel4gty8t/tsM 27 | -----END RSA PRIVATE KEY----- -------------------------------------------------------------------------------- /examples/crypto-verify/public-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz7QweYwr7puNtc8/6jf4 3 | pADXfY2/HVq7LgI7JXkAwW5HTEjURvu02SYC7n/ylr86Da4hcAEAFBC2baVh7J0E 4 | nLo6D4FA3yV4s5zj7tqL+p2CdniYMDEny+uxS/9eRuLf1RIYHjSVB6wOoU4lg9Ft 5 | DOQA8W09ZDLvc3/4ZSwEf9O2J5VmqcSGaPt35eIEBS0iIMiOUCGqbUcVMZkp4JO7 6 | I65HOK/g0Scvb1LLY6f4qDsThqyiID/NXM5LtjnztRP+9dCLT9DdwC+LeU2te6vc 7 | EwC2bPYs3nUwkZL4qqIWO6GHaavngFKSmbmc9muVAPmYW/ffMtznKZaWCp9Li0JG 8 | uQIDAQAB 9 | -----END PUBLIC KEY----- -------------------------------------------------------------------------------- /examples/csv-report/node.js: -------------------------------------------------------------------------------- 1 | const { Suite } = require('../../lib'); 2 | const assert = require('node:assert'); 3 | 4 | const suite = new Suite(); 5 | 6 | suite 7 | .add('single with matcher', function () { 8 | const pattern = /[123]/g 9 | const replacements = { 1: 'a', 2: 'b', 3: 'c' } 10 | const subject = '123123123123123123123123123123123123123123123123' 11 | const r = subject.replace(pattern, m => replacements[m]) 12 | assert.ok(r); 13 | }) 14 | .add('multiple replaces', function () { 15 | const subject = '123123123123123123123123123123123123123123123123' 16 | const r = subject.replace(/1/g, 'a').replace(/2/g, 'b').replace(/3/g, 'c') 17 | assert.ok(r); 18 | }) 19 | .run(); 20 | -------------------------------------------------------------------------------- /examples/default-report/node.js: -------------------------------------------------------------------------------- 1 | const { Suite, csvReport } = require('../../lib'); 2 | const assert = require('node:assert'); 3 | 4 | const suite = new Suite({ 5 | reporter: csvReport, 6 | }); 7 | 8 | suite 9 | .add('single with matcher', function () { 10 | const pattern = /[123]/g 11 | const replacements = { 1: 'a', 2: 'b', 3: 'c' } 12 | const subject = '123123123123123123123123123123123123123123123123' 13 | const r = subject.replace(pattern, m => replacements[m]) 14 | assert.ok(r); 15 | }) 16 | .add('multiple replaces', function () { 17 | const subject = '123123123123123123123123123123123123123123123123' 18 | const r = subject.replace(/1/g, 'a').replace(/2/g, 'b').replace(/3/g, 'c') 19 | assert.ok(r); 20 | }) 21 | .run(); 22 | -------------------------------------------------------------------------------- /examples/deleting-properties/node.js: -------------------------------------------------------------------------------- 1 | const { Suite } = require('../../lib'); 2 | 3 | const suite = new Suite(); 4 | 5 | const NullObject = function NullObject() { } 6 | NullObject.prototype = Object.create(null); 7 | %NeverOptimizeFunction(NullObject); 8 | 9 | suite 10 | .add('Using delete property', function () { 11 | const data = { x: 1, y: 2, z: 3 } 12 | delete data.y 13 | 14 | data.x 15 | data.y 16 | data.z 17 | }) 18 | .add('Using delete property (proto: null)', function () { 19 | const data = { __proto__: null, x: 1, y: 2, z: 3 } 20 | delete data.y 21 | 22 | data.x 23 | data.y 24 | data.z 25 | }) 26 | .add('Using delete property (cached proto: null)', function () { 27 | const data = new NullObject() 28 | 29 | data.x = 1 30 | data.y = 2 31 | data.z = 3 32 | 33 | delete data.y 34 | 35 | data.x 36 | data.y 37 | data.z 38 | }) 39 | .add('Using undefined assignment', function () { 40 | const data = { x: 1, y: 2, z: 3 } 41 | data.y = undefined 42 | 43 | data.x 44 | data.y 45 | data.z 46 | }) 47 | .add('Using undefined assignment (proto: null)', function () { 48 | const data = { __proto__: null, x: 1, y: 2, z: 3 } 49 | data.y = undefined 50 | 51 | data.x 52 | data.y 53 | data.z 54 | }) 55 | .add('Using undefined property (cached proto: null)', function () { 56 | const data = new NullObject() 57 | 58 | data.x = 1 59 | data.y = 2 60 | data.z = 3 61 | 62 | data.y = undefined 63 | 64 | data.x 65 | data.y 66 | data.z 67 | }) 68 | .add('[Managed] Using undefined property (cached proto: null)', function (t) { 69 | const NullObject = function () { } 70 | NullObject.prototype = Object.create(null) 71 | 72 | t.start(); 73 | for (let i = 0; i < t.count; i++) { 74 | const data = new NullObject() 75 | 76 | data.x = 1 77 | data.y = 2 78 | data.z = 3 79 | 80 | data.y = undefined 81 | 82 | data.x 83 | data.y 84 | data.z 85 | } 86 | t.end(t.count); 87 | }) 88 | .run(); 89 | -------------------------------------------------------------------------------- /examples/deleting-properties/node.js.log: -------------------------------------------------------------------------------- 1 | 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 2 | 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 3 | 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 4 | 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 5 | 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 6 | 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 7 | [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 8 | ---------------------------------------------------------------------------- 9 | 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 10 | 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 11 | 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 12 | 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 13 | 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 14 | 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 15 | [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 16 | ---------------------------------------------------------------------------- 17 | 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 18 | 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 19 | 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 20 | 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 21 | 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 22 | 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 23 | [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 24 | ---------------------------------------------------------------------------- 25 | -------------------------------------------------------------------------------- /examples/empty/node.js: -------------------------------------------------------------------------------- 1 | const { Suite } = require('../../lib'); 2 | 3 | const suite = new Suite(); 4 | 5 | suite 6 | .add(`empty`, function () {}) 7 | .add(`empty async`, async function () {}) 8 | .run(); 9 | -------------------------------------------------------------------------------- /examples/empty/node.js.log: -------------------------------------------------------------------------------- 1 | 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 2 | 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 3 | ---------------------------------------------------------------------------- 4 | 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 5 | 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 6 | ---------------------------------------------------------------------------- 7 | 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 8 | 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 9 | ---------------------------------------------------------------------------- 10 | -------------------------------------------------------------------------------- /examples/fs-read-async/node.js: -------------------------------------------------------------------------------- 1 | const { Suite } = require('../../lib'); 2 | const { readFile } = require('node:fs/promises'); 3 | const { resolve } = require('node:path'); 4 | 5 | const suite = new Suite(); 6 | 7 | const sampleFile = resolve(__dirname, 'sample-file.txt'); 8 | 9 | suite 10 | .add('readFile', async function () { 11 | const r = await readFile(sampleFile); 12 | }) 13 | .add('readFile utf-8', async function () { 14 | const r = await readFile(sampleFile, 'utf-8'); 15 | }) 16 | .add('[managed] readFile', async function (timer) { 17 | const { readFile } = require('node:fs/promises'); 18 | const { resolve } = require('node:path'); 19 | const assert = require('node:assert'); 20 | 21 | const sampleFile = resolve(__dirname, 'sample-file.txt'); 22 | let r; 23 | 24 | timer.start(); 25 | for (let i = 0; i < timer.count; i++) { 26 | r = await readFile(sampleFile); 27 | } 28 | timer.end(timer.count); 29 | 30 | assert.ok(r); 31 | }) 32 | .add('[managed] readFile utf-8', async function (timer) { 33 | const { readFile } = require('node:fs/promises'); 34 | const { resolve } = require('node:path'); 35 | const assert = require('node:assert'); 36 | 37 | const sampleFile = resolve(__dirname, 'sample-file.txt'); 38 | let r; 39 | 40 | timer.start(); 41 | for (let i = 0; i < timer.count; i++) { 42 | r = await readFile(sampleFile, 'utf-8'); 43 | } 44 | timer.end(timer.count); 45 | 46 | assert.ok(r); 47 | }) 48 | .run(); 49 | -------------------------------------------------------------------------------- /examples/fs-read-async/node.js.log: -------------------------------------------------------------------------------- 1 | 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 2 | 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 3 | [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 4 | [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 5 | ---------------------------------------------------------------------------- 6 | 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 7 | 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 8 | [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 9 | [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 10 | ---------------------------------------------------------------------------- 11 | 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 12 | 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 13 | [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 14 | [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 15 | ---------------------------------------------------------------------------- 16 | -------------------------------------------------------------------------------- /examples/fs-read-async/node.managed.js: -------------------------------------------------------------------------------- 1 | const { Suite } = require('../../lib'); 2 | 3 | const suite = new Suite(); 4 | 5 | suite 6 | .add('readFile', async function (timer) { 7 | const { readFile } = require('node:fs/promises'); 8 | const { resolve } = require('node:path'); 9 | const assert = require('node:assert'); 10 | 11 | const sampleFile = resolve(__dirname, 'sample-file.txt'); 12 | let r; 13 | 14 | timer.start(); 15 | for (let i = 0; i < timer.count; i++) { 16 | r = await readFile(sampleFile); 17 | } 18 | timer.end(timer.count); 19 | 20 | assert.ok(r); 21 | }) 22 | .add('readFile utf-8', async function (timer) { 23 | const { readFile } = require('node:fs/promises'); 24 | const { resolve } = require('node:path'); 25 | const assert = require('node:assert'); 26 | 27 | const sampleFile = resolve(__dirname, 'sample-file.txt'); 28 | let r; 29 | 30 | timer.start(); 31 | for (let i = 0; i < timer.count; i++) { 32 | r = await readFile(sampleFile, 'utf-8'); 33 | } 34 | timer.end(timer.count); 35 | 36 | assert.ok(r); 37 | }) 38 | .run(); 39 | -------------------------------------------------------------------------------- /examples/fs-read-async/node.managed.js.log: -------------------------------------------------------------------------------- 1 | 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 2 | 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 3 | ---------------------------------------------------------------------------- 4 | 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 5 | 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 6 | ---------------------------------------------------------------------------- 7 | 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 8 | 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 9 | ---------------------------------------------------------------------------- 10 | -------------------------------------------------------------------------------- /examples/fs-read-async/sample-file.txt: -------------------------------------------------------------------------------- 1 | hello -------------------------------------------------------------------------------- /examples/fs-read-sync/node.js: -------------------------------------------------------------------------------- 1 | const { Suite } = require('../../lib'); 2 | const { readFileSync } = require('node:fs'); 3 | const { resolve } = require('node:path'); 4 | 5 | const suite = new Suite(); 6 | 7 | const sampleFile = resolve(__dirname, 'sample-file.txt'); 8 | 9 | suite 10 | .add('readFileSync', function () { 11 | const r = readFileSync(sampleFile); 12 | }) 13 | .add('readFileSync utf-8', function () { 14 | const r = readFileSync(sampleFile, 'utf-8'); 15 | }) 16 | .add('[Managed] readFileSync', function (timer) { 17 | const { readFileSync } = require('node:fs'); 18 | const { resolve } = require('node:path'); 19 | const assert = require('node:assert'); 20 | 21 | const sampleFile = resolve(__dirname, 'sample-file.txt'); 22 | let r; 23 | 24 | timer.start(); 25 | for (let i = 0; i < timer.count; i++) { 26 | r = readFileSync(sampleFile); 27 | } 28 | timer.end(timer.count); 29 | 30 | assert.ok(r); 31 | }) 32 | .add('[Managed] readFileSync utf-8', function (timer) { 33 | const { readFileSync } = require('node:fs'); 34 | const { resolve } = require('node:path'); 35 | const assert = require('node:assert'); 36 | 37 | const sampleFile = resolve(__dirname, 'sample-file.txt'); 38 | let r; 39 | 40 | timer.start(); 41 | for (let i = 0; i < timer.count; i++) { 42 | r = readFileSync(sampleFile, 'utf-8'); 43 | } 44 | timer.end(timer.count); 45 | 46 | assert.ok(r); 47 | }) 48 | 49 | .run(); 50 | -------------------------------------------------------------------------------- /examples/fs-read-sync/node.js.log: -------------------------------------------------------------------------------- 1 | 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 2 | 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 3 | [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 4 | [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 5 | ---------------------------------------------------------------------------- 6 | 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 7 | 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 8 | [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 9 | [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 10 | ---------------------------------------------------------------------------- 11 | 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 12 | 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 13 | [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 14 | [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 15 | ---------------------------------------------------------------------------- 16 | -------------------------------------------------------------------------------- /examples/fs-read-sync/sample-file.txt: -------------------------------------------------------------------------------- 1 | hello -------------------------------------------------------------------------------- /examples/html-report/node.js: -------------------------------------------------------------------------------- 1 | const { Suite, htmlReport } = require('../../lib'); 2 | const assert = require('node:assert'); 3 | 4 | const suite = new Suite({ 5 | reporter: htmlReport, 6 | }); 7 | 8 | suite 9 | .add('single with matcher', function () { 10 | const pattern = /[123]/g 11 | const replacements = { 1: 'a', 2: 'b', 3: 'c' } 12 | const subject = '123123123123123123123123123123123123123123123123' 13 | const r = subject.replace(pattern, m => replacements[m]) 14 | assert.ok(r); 15 | }) 16 | .add('Multiple replaces', function () { 17 | const subject = '123123123123123123123123123123123123123123123123' 18 | const r = subject.replace(/1/g, 'a').replace(/2/g, 'b').replace(/3/g, 'c') 19 | assert.ok(r); 20 | }) 21 | .run(); 22 | -------------------------------------------------------------------------------- /examples/html-report/result.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Benchmark Visualizer 7 | 100 | 101 | 102 |
103 |

Node.js version: v20.18.1

104 |

Platform: darwin arm64

105 |

CPU Cores: 8 vCPUs | 16.0GB Mem

106 |
107 | 108 |
109 | 110 |
111 | 112 |
113 | 114 | 115 | 116 |
117 | single-with-matcher(660,788.4 ops/sec) 118 |
119 | 120 |
121 | Multiple-replaces(578,527 ops/sec) 122 |
123 | 124 |
125 | 126 |
127 |

Benchmark done with bench-node

128 |
129 | 130 | 131 | 132 | 167 | 168 | 169 | -------------------------------------------------------------------------------- /examples/json-report/node.js: -------------------------------------------------------------------------------- 1 | const { Suite, jsonReport } = require('../../lib'); 2 | const assert = require('node:assert'); 3 | 4 | const suite = new Suite({ 5 | reporter: jsonReport, 6 | }); 7 | 8 | suite 9 | .add('single with matcher', function () { 10 | const pattern = /[123]/g 11 | const replacements = { 1: 'a', 2: 'b', 3: 'c' } 12 | const subject = '123123123123123123123123123123123123123123123123' 13 | const r = subject.replace(pattern, m => replacements[m]) 14 | assert.ok(r); 15 | }) 16 | .add('Multiple replaces', function () { 17 | const subject = '123123123123123123123123123123123123123123123123' 18 | const r = subject.replace(/1/g, 'a').replace(/2/g, 'b').replace(/3/g, 'c') 19 | assert.ok(r); 20 | }) 21 | .run(); 22 | -------------------------------------------------------------------------------- /examples/plugins/all.js: -------------------------------------------------------------------------------- 1 | const { 2 | Suite, 3 | V8GetOptimizationStatus, 4 | V8NeverOptimizePlugin, 5 | V8OptimizeOnNextCallPlugin, 6 | } = require('../../lib'); 7 | 8 | const suite = new Suite({ 9 | plugins: [ 10 | new V8GetOptimizationStatus(), 11 | new V8NeverOptimizePlugin(), 12 | // new V8OptimizeOnNextCallPlugin(), 13 | ], 14 | }); 15 | 16 | suite 17 | .add(`new Uint32Array(1024)`, function () { 18 | return new Uint32Array(1024); 19 | }) 20 | .add(`[Managed] new Uint32Array(1024)`, function (timer) { 21 | const assert = require('node:assert'); 22 | 23 | let r; 24 | 25 | timer.start(); 26 | for (let i = 0; i < timer.count; i++) { 27 | r = new Uint32Array(1024); 28 | } 29 | timer.end(timer.count); 30 | 31 | assert.ok(r); 32 | }) 33 | .run(); 34 | -------------------------------------------------------------------------------- /examples/plugins/all.js.log: -------------------------------------------------------------------------------- 1 | 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 2 | [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 3 | ---------------------------------------------------------------------------- 4 | 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 5 | [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 6 | ---------------------------------------------------------------------------- 7 | 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 8 | [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 9 | ---------------------------------------------------------------------------- 10 | -------------------------------------------------------------------------------- /examples/plugins/v8-get-opt-status.js: -------------------------------------------------------------------------------- 1 | const { Suite, V8GetOptimizationStatus } = require('../../lib'); 2 | 3 | const suite = new Suite({ 4 | plugins: [new V8GetOptimizationStatus()], 5 | }); 6 | 7 | suite 8 | .add(`new Uint32Array(1024)`, function () { 9 | return new Uint32Array(1024); 10 | }) 11 | .add(`[Managed] new Uint32Array(1024)`, function (timer) { 12 | const assert = require('node:assert'); 13 | 14 | let r; 15 | 16 | timer.start(); 17 | for (let i = 0; i < timer.count; i++) { 18 | r = new Uint32Array(1024); 19 | } 20 | timer.end(timer.count); 21 | 22 | assert.ok(r); 23 | }) 24 | .run(); 25 | -------------------------------------------------------------------------------- /examples/plugins/v8-get-opt-status.js.log: -------------------------------------------------------------------------------- 1 | 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 2 | [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 3 | ---------------------------------------------------------------------------- 4 | 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 5 | [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 6 | ---------------------------------------------------------------------------- 7 | 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 8 | [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 9 | ---------------------------------------------------------------------------- 10 | -------------------------------------------------------------------------------- /examples/plugins/v8-never-optimize.js: -------------------------------------------------------------------------------- 1 | const { Suite, V8NeverOptimizePlugin } = require('../../lib'); 2 | 3 | const suite = new Suite({ 4 | plugins: [new V8NeverOptimizePlugin()], 5 | }); 6 | 7 | suite 8 | .add(`new Uint32Array(1024)`, function () { 9 | return new Uint32Array(1024); 10 | }) 11 | .add(`[Managed] new Uint32Array(1024)`, function (timer) { 12 | const assert = require('node:assert'); 13 | 14 | let r; 15 | 16 | timer.start(); 17 | for (let i = 0; i < timer.count; i++) { 18 | r = new Uint32Array(1024); 19 | } 20 | timer.end(timer.count); 21 | 22 | assert.ok(r); 23 | }) 24 | .run(); 25 | -------------------------------------------------------------------------------- /examples/plugins/v8-never-optimize.js.log: -------------------------------------------------------------------------------- 1 | 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 2 | [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 3 | ---------------------------------------------------------------------------- 4 | 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 5 | [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 6 | ---------------------------------------------------------------------------- 7 | 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 8 | [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 9 | ---------------------------------------------------------------------------- 10 | -------------------------------------------------------------------------------- /examples/plugins/v8-optimize-next-call.js: -------------------------------------------------------------------------------- 1 | const { Suite, V8OptimizeOnNextCallPlugin } = require('../../lib'); 2 | 3 | const suite = new Suite({ 4 | plugins: [new V8OptimizeOnNextCallPlugin()], 5 | }); 6 | 7 | suite 8 | .add(`new Uint32Array(1024)`, function () { 9 | return new Uint32Array(1024); 10 | }) 11 | .add(`[Managed] new Uint32Array(1024)`, function (timer) { 12 | const assert = require('node:assert'); 13 | 14 | let r; 15 | 16 | timer.start(); 17 | for (let i = 0; i < timer.count; i++) { 18 | r = new Uint32Array(1024); 19 | } 20 | timer.end(timer.count); 21 | 22 | assert.ok(r); 23 | }) 24 | .run(); 25 | -------------------------------------------------------------------------------- /examples/plugins/v8-optimize-next-call.js.log: -------------------------------------------------------------------------------- 1 | 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 2 | [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 3 | ---------------------------------------------------------------------------- 4 | 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 5 | [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 6 | ---------------------------------------------------------------------------- 7 | 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 8 | [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 9 | ---------------------------------------------------------------------------- 10 | -------------------------------------------------------------------------------- /examples/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # clean previous logs 4 | rm -f ./**/*.log 5 | 6 | for filename in ./**/*.*js; do 7 | echo "[1] Running $filename" 8 | node --allow-natives-syntax "./$filename" | sed "s,\x1B\[[0-9;]*m,,g" >>"$filename.log" 9 | echo -e "----------------------------------------------------------------------------" >>"$filename.log" 10 | 11 | echo "[2] Running $filename" 12 | node --allow-natives-syntax "./$filename" | sed "s,\x1B\[[0-9;]*m,,g" >>"$filename.log" 13 | echo -e "----------------------------------------------------------------------------" >>"$filename.log" 14 | 15 | echo "[3] Running $filename" 16 | node --allow-natives-syntax "./$filename" | sed "s,\x1B\[[0-9;]*m,,g" >>"$filename.log" 17 | echo -e "----------------------------------------------------------------------------" >>"$filename.log" 18 | done 19 | -------------------------------------------------------------------------------- /examples/string-replace/node.js: -------------------------------------------------------------------------------- 1 | const { Suite } = require('../../lib'); 2 | 3 | const suite = new Suite(); 4 | 5 | const pattern = /[123]/g 6 | const replacements = { 1: 'a', 2: 'b', 3: 'c' } 7 | 8 | const subject = '123123123123123123123123123123123123123123123123' 9 | 10 | suite 11 | .add('single with matcher', function () { 12 | const r = subject.replace(pattern, m => replacements[m]) 13 | }) 14 | .add('multiple replaces', function () { 15 | const r = subject.replace(/1/g, 'a').replace(/2/g, 'b').replace(/3/g, 'c') 16 | }) 17 | .add('[Managed] single with matcher', function (timer) { 18 | const assert = require('node:assert'); 19 | 20 | const pattern = /[123]/g 21 | const replacements = { 1: 'a', 2: 'b', 3: 'c' } 22 | 23 | const subject = '123123123123123123123123123123123123123123123123' 24 | 25 | let r; 26 | 27 | timer.start(); 28 | for (let i = 0; i < timer.count; i++) { 29 | r = subject.replace(pattern, m => replacements[m]); 30 | } 31 | timer.end(timer.count); 32 | 33 | assert.ok(r); 34 | }) 35 | .add('[Managed] multiple replaces', function (timer) { 36 | const assert = require('node:assert'); 37 | 38 | const subject = '123123123123123123123123123123123123123123123123' 39 | 40 | let r; 41 | 42 | timer.start(); 43 | for (let i = 0; i < timer.count; i++) { 44 | r = subject.replace(/1/g, 'a').replace(/2/g, 'b').replace(/3/g, 'c'); 45 | } 46 | timer.end(timer.count); 47 | assert.ok(r); 48 | }) 49 | .run() 50 | -------------------------------------------------------------------------------- /examples/string-replace/node.js.log: -------------------------------------------------------------------------------- 1 | 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 2 | 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 3 | [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 4 | [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 5 | ---------------------------------------------------------------------------- 6 | 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 7 | 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 8 | [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 9 | [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 10 | ---------------------------------------------------------------------------- 11 | 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 12 | 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 13 | [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 14 | [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 15 | ---------------------------------------------------------------------------- 16 | -------------------------------------------------------------------------------- /examples/string-searching/node.js: -------------------------------------------------------------------------------- 1 | const { Suite } = require('../../lib'); 2 | 3 | const suite = new Suite(); 4 | 5 | const text = 'text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8' 6 | const regex = /application\/json/ 7 | 8 | suite 9 | .add('Using includes', function () { 10 | const r = text.includes('application/json') 11 | }) 12 | .add('Using indexof', function () { 13 | const r = text.indexOf('application/json') !== -1 14 | }) 15 | .add('Using cached RegExp.test', function () { 16 | const r = regex.test(text) 17 | }) 18 | .add('[Managed] Using includes', function (timer) { 19 | const assert = require('node:assert'); 20 | 21 | const text = 'text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8'; 22 | 23 | let r; 24 | 25 | timer.start(); 26 | for (let i = 0; i < timer.count; i++) { 27 | r = text.includes('application/json'); 28 | } 29 | timer.end(timer.count); 30 | 31 | assert.ok(r); 32 | }) 33 | .add('[Managed] Using indexof', function (timer) { 34 | const assert = require('node:assert'); 35 | 36 | const text = 'text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8'; 37 | 38 | let r; 39 | 40 | timer.start(); 41 | for (let i = 0; i < timer.count; i++) { 42 | r = text.indexOf('application/json') !== -1; 43 | } 44 | timer.end(timer.count); 45 | 46 | assert.ok(r); 47 | }) 48 | .add('[Managed] Using cached RegExp.test', function (timer) { 49 | const assert = require('node:assert'); 50 | 51 | const regex = /application\/json/; 52 | const text = 'text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8'; 53 | 54 | let r; 55 | 56 | timer.start(); 57 | for (let i = 0; i < timer.count; i++) { 58 | r = regex.test(text); 59 | } 60 | timer.end(timer.count); 61 | 62 | assert.ok(r); 63 | }) 64 | .run(); 65 | -------------------------------------------------------------------------------- /examples/string-searching/node.js.log: -------------------------------------------------------------------------------- 1 | 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 2 | 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 3 | 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 4 | [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 5 | [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 6 | [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 7 | ---------------------------------------------------------------------------- 8 | 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 9 | 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 10 | 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 11 | [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 12 | [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 13 | [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 14 | ---------------------------------------------------------------------------- 15 | 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 16 | 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 17 | 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 18 | [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 19 | [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 20 | [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 21 | ---------------------------------------------------------------------------- 22 | -------------------------------------------------------------------------------- /examples/time-mode.js: -------------------------------------------------------------------------------- 1 | const { Suite } = require('../lib'); 2 | 3 | const timeSuite = new Suite({ 4 | benchmarkMode: 'time' // Set mode for the entire suite 5 | }); 6 | 7 | const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)); 8 | 9 | timeSuite.add('Async Delay 100ms (time)', async () => { 10 | await delay(100); 11 | }); 12 | 13 | timeSuite.add('Sync Busy Wait 50ms (time)', () => { 14 | const start = Date.now(); 15 | while (Date.now() - start < 50); 16 | }); 17 | 18 | timeSuite.add('Quick Sync Op with 5 repeats (time)', { repeatSuite: 5 }, () => { 19 | // This will run exactly once per repeat (5 times total) 20 | // and report the average time 21 | let x = 1 + 1; 22 | }); 23 | 24 | 25 | (async () => { 26 | console.log('\nRunning benchmark suite in TIME mode...'); 27 | await timeSuite.run(); 28 | })(); 29 | -------------------------------------------------------------------------------- /examples/worker-threads/node.js: -------------------------------------------------------------------------------- 1 | const { Suite } = require('../../lib'); 2 | 3 | const suite = new Suite({ 4 | useWorkers: true, 5 | }); 6 | 7 | suite 8 | .add('Using import without node: prefix', function () { 9 | return import('fs'); 10 | }) 11 | .add('Using import with node: prefix', function () { 12 | return import('node:fs'); 13 | }) 14 | .run(); 15 | -------------------------------------------------------------------------------- /lib/clock.js: -------------------------------------------------------------------------------- 1 | const { debug, types } = require("node:util"); 2 | const { validateNumber } = require("./validators"); 3 | 4 | let debugBench = debug("benchmark", (fn) => { 5 | debugBench = fn; 6 | }); 7 | 8 | const kUnmanagedTimerResult = Symbol("kUnmanagedTimerResult"); 9 | 10 | // If the smallest time measurement is 1ns 11 | // the minimum resolution of this timer is 0.5 12 | const MIN_RESOLUTION = 0.5; 13 | 14 | class Timer { 15 | constructor() { 16 | this.now = process.hrtime.bigint; 17 | } 18 | 19 | get scale() { 20 | return 1e9; 21 | } 22 | 23 | get resolution() { 24 | return 1 / 1e9; 25 | } 26 | 27 | /** 28 | * @param {number} timeInNs 29 | * @returns {string} 30 | */ 31 | format(timeInNs) { 32 | validateNumber(timeInNs, "timeInNs", 0); 33 | 34 | if (timeInNs > 1e9) { 35 | return `${(timeInNs / 1e9).toFixed(2)}s`; 36 | } 37 | 38 | if (timeInNs > 1e6) { 39 | return `${(timeInNs / 1e6).toFixed(2)}ms`; 40 | } 41 | 42 | if (timeInNs > 1e3) { 43 | return `${(timeInNs / 1e3).toFixed(2)}us`; 44 | } 45 | 46 | return `${(timeInNs).toFixed(2)}ns`; 47 | } 48 | } 49 | 50 | const timer = new Timer(); 51 | 52 | class ManagedTimer { 53 | startTime; 54 | endTime; 55 | iterations; 56 | recommendedCount; 57 | 58 | /** 59 | * @param {number} recommendedCount 60 | */ 61 | constructor(recommendedCount) { 62 | this.recommendedCount = recommendedCount; 63 | } 64 | 65 | /** 66 | * Returns the recommended value to be used to benchmark your code 67 | * @returns {number} 68 | */ 69 | get count() { 70 | return this.recommendedCount; 71 | } 72 | 73 | /** 74 | * Starts the timer 75 | */ 76 | start() { 77 | this.startTime = timer.now(); 78 | } 79 | 80 | /** 81 | * Stops the timer 82 | * @param {number} [iterations=1] The amount of iterations that run 83 | */ 84 | end(iterations = 1) { 85 | this.endTime = timer.now(); 86 | validateNumber(iterations, "iterations", 1); 87 | this.iterations = iterations; 88 | } 89 | 90 | [kUnmanagedTimerResult](context) { 91 | if (this.startTime === undefined) 92 | throw new Error("You forgot to call .start()"); 93 | 94 | if (this.endTime === undefined) 95 | throw new Error("You forgot to call .end(count)"); 96 | 97 | return [Number(this.endTime - this.startTime), this.iterations, context]; 98 | } 99 | } 100 | 101 | function createRunUnmanagedBenchmark(bench, awaitOrEmpty) { 102 | const varNames = { 103 | awaitOrEmpty, 104 | timer: "timer", 105 | context: "context", 106 | bench: "bench", 107 | }; 108 | 109 | let code = ` 110 | let i = 0; 111 | let ${varNames.context} = {}; 112 | `; 113 | 114 | let benchFnCall = `${awaitOrEmpty}${varNames.bench}.fn()`; 115 | const wrapFunctions = []; 116 | for (const p of bench.plugins) { 117 | if (typeof p.beforeClockTemplate === "function") { 118 | const [newCode, functionToCall] = p.beforeClockTemplate(varNames); 119 | code += newCode; 120 | if (functionToCall) { 121 | wrapFunctions.push(functionToCall); 122 | } 123 | } 124 | } 125 | benchFnCall = wrapFunctions.reduce((prev, n) => { 126 | return `${n}(${prev})`; 127 | }, benchFnCall); 128 | 129 | code += ` 130 | const startedAt = ${varNames.timer}.now(); 131 | 132 | for (; i < count; i++) 133 | ${benchFnCall}; 134 | 135 | const duration = Number(${varNames.timer}.now() - startedAt); 136 | `; 137 | 138 | for (const p of bench.plugins) { 139 | if (typeof p.afterClockTemplate === "function") { 140 | [newCode] = p.afterClockTemplate(varNames); 141 | code += newCode; 142 | } 143 | } 144 | 145 | code += `return [duration, count, ${varNames.context}];`; 146 | return code; 147 | } 148 | 149 | function createRunManagedBenchmark(bench, awaitOrEmpty) { 150 | const varNames = { 151 | awaitOrEmpty, 152 | timer: "timer", 153 | context: "context", 154 | bench: "bench", 155 | }; 156 | 157 | let code = ` 158 | let i = 0; 159 | let ${varNames.context} = {}; 160 | `; 161 | 162 | let benchFnCall = `${awaitOrEmpty}${varNames.bench}.fn(${varNames.timer})`; 163 | const wrapFunctions = []; 164 | for (const p of bench.plugins) { 165 | if (typeof p.beforeClockTemplate === "function") { 166 | [newCode, functionToCall] = p.beforeClockTemplate(varNames); 167 | code += newCode; 168 | if (functionToCall) { 169 | wrapFunctions.push(functionToCall); 170 | } 171 | } 172 | } 173 | benchFnCall = wrapFunctions.reduce((prev, n) => { 174 | return `${n}(${prev})`; 175 | }, benchFnCall); 176 | 177 | code += ` 178 | ${benchFnCall}; 179 | const result = ${varNames.timer}[kUnmanagedTimerResult](${varNames.context}); 180 | `; 181 | for (const p of bench.plugins) { 182 | if (typeof p.afterClockTemplate === "function") { 183 | [newCode] = p.afterClockTemplate(varNames); 184 | code += newCode; 185 | } 186 | } 187 | 188 | code += "return result;"; 189 | return code; 190 | } 191 | 192 | const AsyncFunction = (async () => {}).constructor; 193 | const SyncFunction = (() => {}).constructor; 194 | 195 | function createFnString(bench) { 196 | const { isAsync, hasArg } = bench; 197 | 198 | const compiledFnStringFactory = hasArg 199 | ? createRunManagedBenchmark 200 | : createRunUnmanagedBenchmark; 201 | const compiledFnString = compiledFnStringFactory( 202 | bench, 203 | isAsync ? "await " : "", 204 | ); 205 | return compiledFnString; 206 | } 207 | 208 | function createRunner(bench, recommendedCount) { 209 | const { isAsync, hasArg } = bench; 210 | const compiledFnString = bench.fnStr; 211 | 212 | const createFnPrototype = isAsync ? AsyncFunction : SyncFunction; 213 | const compiledFn = createFnPrototype( 214 | "bench", 215 | "timer", 216 | "count", 217 | "kUnmanagedTimerResult", 218 | compiledFnString, 219 | ); 220 | const selectedTimer = hasArg ? new ManagedTimer(recommendedCount) : timer; 221 | const runner = compiledFn.bind( 222 | globalThis, 223 | bench, 224 | selectedTimer, 225 | recommendedCount, 226 | kUnmanagedTimerResult, 227 | ); 228 | debugBench(`Compiled Code: ${compiledFnString}`); 229 | debugBench( 230 | `Created compiled benchmark, hasArg=${hasArg}, isAsync=${isAsync}, recommendedCount=${recommendedCount}`, 231 | ); 232 | 233 | return runner; 234 | } 235 | 236 | /** 237 | * Executes a benchmark and returns the time taken and number of iterations 238 | * @param {import('./index').Benchmark} bench - The benchmark to execute 239 | * @param {number} recommendedCount - The recommended number of iterations 240 | * @param {Object} [options] - Additional options 241 | * @param {boolean} [options.timeMode=false] - If true, runs the benchmark exactly once 242 | * @returns {Promise<[number, number]>} - Returns [duration, iterations] 243 | */ 244 | async function clockBenchmark(bench, recommendedCount, options = {}) { 245 | const runner = createRunner(bench, recommendedCount); 246 | const result = await runner(); 247 | 248 | // Just to avoid issues with empty fn 249 | result[0] = Math.max(MIN_RESOLUTION, result[0]); 250 | 251 | for (const p of bench.plugins) { 252 | if (typeof p.onCompleteBenchmark === "function") { 253 | // TODO: this won't work when useWorkers=true 254 | p.onCompleteBenchmark(result, bench); 255 | } 256 | } 257 | 258 | debugBench( 259 | `Took ${timer.format(result[0])} to execute ${result[1]} iterations${options.timeMode ? " (time mode)" : ""}`, 260 | ); 261 | return result; 262 | } 263 | 264 | module.exports = { 265 | clockBenchmark, 266 | createFnString, 267 | timer, 268 | MIN_RESOLUTION, 269 | debugBench, 270 | }; 271 | -------------------------------------------------------------------------------- /lib/histogram.js: -------------------------------------------------------------------------------- 1 | const { validateNumber } = require("./validators"); 2 | 3 | /** 4 | * A class that calculates and maintains statistical measurements for a set of numeric samples. 5 | * Handles outlier removal, and calculates various statistical measures like mean, standard deviation, 6 | * coefficient of variation, and percentiles. 7 | */ 8 | class StatisticalHistogram { 9 | all = []; 10 | min; 11 | max; 12 | mean; 13 | cv; 14 | stddev; 15 | 16 | /** 17 | * @returns {number[]} 18 | */ 19 | get samples() { 20 | return this.all.slice(); 21 | } 22 | 23 | /** 24 | * @param {number} percentile 25 | * @returns {number} 26 | */ 27 | percentile(percentile) { 28 | validateNumber(percentile, "percentile"); 29 | 30 | if (Number.isNaN(percentile) || percentile < 0 || percentile > 100) 31 | throw new Error( 32 | "Invalid percentile value. Must be a number between 0 and 100.", 33 | ); 34 | 35 | if (this.all.length === 0) return 0; 36 | 37 | if (percentile === 0) return this.all[0]; 38 | 39 | return this.all[Math.ceil(this.all.length * (percentile / 100)) - 1]; 40 | } 41 | 42 | /** 43 | * @param {number} value 44 | */ 45 | record(value) { 46 | validateNumber(value, "value", 0); 47 | 48 | this.all.push(value); 49 | } 50 | 51 | finish() { 52 | this.removeOutliers(); 53 | 54 | this.calculateMinMax(); 55 | this.calculateMean(); 56 | this.calculateStd(); 57 | this.calculateCv(); 58 | } 59 | 60 | /** 61 | * References: 62 | * - https://gist.github.com/rmeissn/f5b42fb3e1386a46f60304a57b6d215a 63 | * - https://en.wikipedia.org/wiki/Interquartile_range 64 | */ 65 | removeOutliers() { 66 | this.all.sort((a, b) => a - b); 67 | 68 | const size = this.all.length; 69 | 70 | if (size < 4) return; 71 | 72 | let q1; 73 | let q3; 74 | 75 | if (((size - 1) / 4) % 1 === 0 || (size / 4) % 1 === 0) { 76 | q1 = 77 | (1 / 2) * 78 | (this.all[Math.floor(size / 4) - 1] + this.all[Math.floor(size / 4)]); 79 | q3 = 80 | (1 / 2) * 81 | (this.all[Math.ceil((size * 3) / 4) - 1] + 82 | this.all[Math.ceil((size * 3) / 4)]); 83 | } else { 84 | q1 = this.all[Math.floor(size / 4)]; 85 | q3 = this.all[Math.floor((size * 3) / 4)]; 86 | } 87 | 88 | const iqr = q3 - q1; 89 | const minValue = q1 - iqr * 1.5; 90 | const maxValue = q3 + iqr * 1.5; 91 | 92 | this.all = this.all.filter( 93 | (value) => value <= maxValue && value >= minValue, 94 | ); 95 | } 96 | 97 | calculateMinMax() { 98 | this.min = Number.POSITIVE_INFINITY; 99 | this.max = Number.NEGATIVE_INFINITY; 100 | 101 | for (let i = 0; i < this.all.length; i++) { 102 | this.min = Math.min(this.all[i], this.min); 103 | this.max = Math.max(this.all[i], this.max); 104 | } 105 | } 106 | 107 | calculateMean() { 108 | if (this.all.length === 0) { 109 | this.mean = 0; 110 | return; 111 | } 112 | 113 | if (this.all.length === 1) { 114 | this.mean = this.all[0]; 115 | return; 116 | } 117 | 118 | this.mean = 119 | this.all.reduce( 120 | (acc, value) => Math.min(Number.MAX_SAFE_INTEGER, acc + value), 121 | 0, 122 | ) / this.all.length; 123 | } 124 | 125 | calculateStd() { 126 | if (this.all.length < 2) { 127 | this.stddev = 0; 128 | return; 129 | } 130 | const variance = 131 | this.all.reduce((acc, value) => { 132 | return acc + (value - this.mean) ** 2; 133 | }, 0) / 134 | (this.all.length - 1); 135 | this.stddev = Math.sqrt(variance); 136 | } 137 | 138 | /** 139 | * References: 140 | * - https://en.wikipedia.org/wiki/Coefficient_of_variation 141 | * - https://github.com/google/benchmark/blob/159eb2d0ffb85b86e00ec1f983d72e72009ec387/src/statistics.ccL81-L88 142 | */ 143 | calculateCv() { 144 | if (this.all.length < 2 || this.mean === 0) { 145 | this.cv = 0; 146 | return; 147 | } 148 | 149 | this.cv = (this.stddev / this.mean) * 100; 150 | } 151 | } 152 | 153 | module.exports = { 154 | StatisticalHistogram, 155 | }; 156 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const { Worker } = require("node:worker_threads"); 2 | const { types } = require("node:util"); 3 | const path = require("node:path"); 4 | 5 | const { 6 | textReport, 7 | chartReport, 8 | htmlReport, 9 | jsonReport, 10 | csvReport, 11 | } = require("./report"); 12 | const { 13 | getInitialIterations, 14 | runBenchmark, 15 | runWarmup, 16 | } = require("./lifecycle"); 17 | const { debugBench, timer, createFnString } = require("./clock"); 18 | const { 19 | validatePlugins, 20 | V8NeverOptimizePlugin, 21 | V8GetOptimizationStatus, 22 | V8OptimizeOnNextCallPlugin, 23 | } = require("./plugins"); 24 | const { 25 | validateFunction, 26 | validateNumber, 27 | validateObject, 28 | validateString, 29 | validateArray, 30 | validateBenchmarkMode, 31 | } = require("./validators"); 32 | 33 | const getFunctionBody = (string) => 34 | string.substring(string.indexOf("{") + 1, string.lastIndexOf("}")); 35 | 36 | class Benchmark { 37 | name = "Benchmark"; 38 | fn; 39 | minTime; 40 | maxTime; 41 | plugins; 42 | repeatSuite; 43 | minSamples; 44 | 45 | constructor(name, fn, minTime, maxTime, plugins, repeatSuite, minSamples) { 46 | this.name = name; 47 | this.fn = fn; 48 | this.minTime = minTime; 49 | this.maxTime = maxTime; 50 | this.plugins = plugins; 51 | this.repeatSuite = repeatSuite; 52 | this.minSamples = minSamples; 53 | 54 | this.hasArg = this.fn.length >= 1; 55 | if (this.fn.length > 1) { 56 | process.emitWarning( 57 | `The benchmark "${this.name}" function should not have more than 1 argument.`, 58 | ); 59 | } 60 | 61 | this.isAsync = types.isAsyncFunction(this.fn); 62 | 63 | this.fnStr = createFnString(this); 64 | } 65 | 66 | serializeBenchmark() { 67 | // Regular functions can't be passed to worker.postMessage 68 | // So we pass the string and deserialize fnStr into a new Function 69 | // on worker 70 | this.fn = getFunctionBody(this.fn.toString()); 71 | } 72 | } 73 | 74 | const defaultBenchOptions = { 75 | // 0.05s - Arbitrary number used in some benchmark tools 76 | minTime: 0.05, 77 | // 0.5s - Arbitrary number used in some benchmark tools 78 | maxTime: 0.5, 79 | // Number of times the benchmark will be repeated 80 | repeatSuite: 1, 81 | // Number minimum of samples the each round 82 | minSamples: 10, 83 | }; 84 | 85 | function throwIfNoNativesSyntax() { 86 | if (process.execArgv.includes("--allow-natives-syntax") === false) { 87 | throw new Error( 88 | "bench-node module must be run with --allow-natives-syntax argument", 89 | ); 90 | } 91 | } 92 | 93 | class Suite { 94 | #benchmarks; 95 | #reporter; 96 | #plugins; 97 | #useWorkers; 98 | #benchmarkMode; 99 | 100 | constructor(options = {}) { 101 | this.#benchmarks = []; 102 | validateObject(options, "options"); 103 | 104 | if (options?.reporter !== undefined) { 105 | if (options?.reporter !== false && options?.reporter !== null) { 106 | validateFunction(options.reporter, "reporter"); 107 | } 108 | this.#reporter = options.reporter; 109 | } else { 110 | this.#reporter = textReport; 111 | } 112 | 113 | this.#useWorkers = options.useWorkers || false; 114 | 115 | if (options?.plugins) { 116 | validateArray(options.plugins, "plugin"); 117 | validatePlugins(options.plugins); 118 | } 119 | this.#plugins = options?.plugins || [new V8NeverOptimizePlugin()]; 120 | 121 | // Benchmark Mode setup 122 | this.#benchmarkMode = options.benchmarkMode || "ops"; // Default to 'ops' 123 | validateBenchmarkMode(this.#benchmarkMode, "options.benchmarkMode"); 124 | } 125 | 126 | add(name, options, fn) { 127 | validateString(name, "name"); 128 | if (typeof options === "function") { 129 | fn = options; 130 | options = defaultBenchOptions; 131 | } else { 132 | validateObject(options, "options"); 133 | options = { 134 | ...defaultBenchOptions, 135 | ...options, 136 | }; 137 | validateNumber( 138 | options.minTime, 139 | "options.minTime", 140 | timer.resolution * 1e3, 141 | ); 142 | validateNumber(options.maxTime, "options.maxTime", options.minTime); 143 | validateNumber( 144 | options.repeatSuite, 145 | "options.repeatSuite", 146 | options.repeatSuite, 147 | ); 148 | validateNumber( 149 | options.minSamples, 150 | "options.minSamples", 151 | options.minSamples, 152 | ); 153 | } 154 | validateFunction(fn, "fn"); 155 | 156 | const benchmark = new Benchmark( 157 | name, 158 | fn, 159 | options.minTime, 160 | options.maxTime, 161 | this.#plugins, 162 | options.repeatSuite, 163 | options.minSamples, 164 | ); 165 | this.#benchmarks.push(benchmark); 166 | return this; 167 | } 168 | 169 | async run() { 170 | throwIfNoNativesSyntax(); 171 | const results = new Array(this.#benchmarks.length); 172 | 173 | // It doesn't make sense to warmup a fresh new instance of Worker. 174 | // TODO: support warmup directly in the Worker. 175 | if (!this.#useWorkers) { 176 | // This is required to avoid variance on first benchmark run 177 | for (let i = 0; i < this.#benchmarks.length; ++i) { 178 | const benchmark = this.#benchmarks[i]; 179 | debugBench( 180 | `Warmup ${benchmark.name} with minTime=${benchmark.minTime}, maxTime=${benchmark.maxTime}`, 181 | ); 182 | const initialIteration = await getInitialIterations(benchmark); 183 | await runWarmup(benchmark, initialIteration, { 184 | minTime: 0.005, 185 | maxTime: 0.05, 186 | }); 187 | } 188 | } 189 | 190 | for (let i = 0; i < this.#benchmarks.length; ++i) { 191 | const benchmark = this.#benchmarks[i]; 192 | // Warmup is calculated to reduce noise/bias on the results 193 | const initialIterations = await getInitialIterations(benchmark); 194 | debugBench( 195 | `Starting ${benchmark.name} with mode=${this.#benchmarkMode}, minTime=${benchmark.minTime}, maxTime=${benchmark.maxTime}, repeatSuite=${benchmark.repeatSuite}, minSamples=${benchmark.minSamples}`, 196 | ); 197 | 198 | let result; 199 | if (this.#useWorkers) { 200 | if (this.#benchmarkMode === "time") { 201 | console.warn( 202 | "Warning: Worker mode currently doesn't fully support 'time' benchmarkMode.", 203 | ); 204 | } 205 | result = await this.runWorkerBenchmark(benchmark, initialIterations); 206 | } else { 207 | result = await runBenchmark( 208 | benchmark, 209 | initialIterations, 210 | this.#benchmarkMode, 211 | benchmark.repeatSuite, 212 | benchmark.minSamples, 213 | ); 214 | } 215 | results[i] = result; 216 | } 217 | 218 | if (this.#reporter) { 219 | this.#reporter(results); 220 | } 221 | 222 | return results; 223 | } 224 | 225 | async runWorkerBenchmark(benchmark, initialIterations) { 226 | return new Promise((resolve, reject) => { 227 | const workerPath = path.resolve(__dirname, "./worker-runner.js"); 228 | const worker = new Worker(workerPath); 229 | 230 | benchmark.serializeBenchmark(); 231 | worker.postMessage({ 232 | benchmark, 233 | initialIterations, 234 | benchmarkMode: this.#benchmarkMode, // Pass suite mode 235 | repeatSuite: benchmark.repeatSuite, 236 | minSamples: benchmark.minSamples, 237 | }); 238 | 239 | worker.on("message", (result) => { 240 | resolve(result); 241 | worker.terminate(); 242 | }); 243 | worker.on("error", (error) => { 244 | reject(error); 245 | worker.terminate(); 246 | }); 247 | worker.on("exit", (code) => { 248 | if (code !== 0) 249 | reject(new Error(`Worker stopped with exit code ${code}`)); 250 | }); 251 | }); 252 | } 253 | } 254 | 255 | module.exports = { 256 | Suite, 257 | V8NeverOptimizePlugin, 258 | V8GetOptimizationStatus, 259 | V8OptimizeOnNextCallPlugin, 260 | chartReport, 261 | textReport, 262 | htmlReport, 263 | jsonReport, 264 | csvReport, 265 | }; 266 | -------------------------------------------------------------------------------- /lib/lifecycle.js: -------------------------------------------------------------------------------- 1 | const { 2 | clockBenchmark, 3 | debugBench, 4 | MIN_RESOLUTION, 5 | timer, 6 | } = require("./clock"); 7 | const { StatisticalHistogram } = require("./histogram"); 8 | 9 | /** 10 | * @param {number} durationPerOp The amount of time each operation takes 11 | * @param {number} targetTime The amount of time we want the benchmark to execute 12 | */ 13 | function getItersForOpDuration(durationPerOp, targetTime) { 14 | const totalOpsForMinTime = targetTime / (durationPerOp / timer.scale); 15 | 16 | return Math.min( 17 | Number.MAX_SAFE_INTEGER, 18 | Math.max(1, Math.round(totalOpsForMinTime)), 19 | ); 20 | } 21 | 22 | function parsePluginsResult(plugins, name) { 23 | const result = []; 24 | for (const p of plugins) { 25 | result.push({ 26 | name: p.toString(), 27 | result: p.getResult?.(name) ?? "enabled", 28 | report: p.getReport?.(name) ?? "", 29 | }); 30 | } 31 | return result; 32 | } 33 | 34 | /** 35 | * Calculates and returns the initial number of iterations for a benchmark 36 | * @param {import('./index').Benchmark} bench - The benchmark object to be executed 37 | * @returns {Promise} The calculated number of initial iterations 38 | */ 39 | async function getInitialIterations(bench) { 40 | const { 0: duration, 1: realIterations } = await clockBenchmark(bench, 30); 41 | 42 | // Just to avoid issues with empty fn 43 | const durationPerOp = Math.max(MIN_RESOLUTION, duration / realIterations); 44 | debugBench( 45 | `Duration per operation on initial count: ${timer.format(durationPerOp)}`, 46 | ); 47 | 48 | // TODO: is this a correct assumpion? 49 | if (durationPerOp / timer.scale >= bench.maxTime) 50 | process.emitWarning( 51 | `The benchmark "${bench.name}" has a duration per operation greater than the maxTime.`, 52 | ); 53 | 54 | return getItersForOpDuration(durationPerOp, bench.minTime); 55 | } 56 | 57 | /** 58 | * Executes the warmup phase of a benchmark 59 | * @param {import('./index').Benchmark} bench - The benchmark object to be executed 60 | * @param {number} initialIterations - The initial number of iterations to run 61 | * @param {Object} options - Warmup options 62 | * @param {number} [options.minTime] - Minimum time for warmup, defaults to bench.minTime 63 | * @param {number} [options.maxTime] - Maximum time for warmup, defaults to bench.minTime 64 | * @returns {Promise} 65 | */ 66 | async function runWarmup(bench, initialIterations, { minTime, maxTime }) { 67 | minTime = minTime ?? bench.minTime; 68 | maxTime = maxTime ?? bench.minTime; 69 | 70 | const maxDuration = maxTime * timer.scale; 71 | const minSamples = 10; 72 | 73 | let iterations = 0; 74 | let timeSpent = 0; 75 | let samples = 0; 76 | 77 | while (timeSpent < maxDuration || samples <= minSamples) { 78 | const { 0: duration, 1: realIterations } = await clockBenchmark( 79 | bench, 80 | initialIterations, 81 | ); 82 | timeSpent += duration; 83 | 84 | iterations += realIterations; 85 | iterations = Math.min(Number.MAX_SAFE_INTEGER, iterations); 86 | 87 | // Just to avoid issues with empty fn 88 | const durationPerOp = Math.max(MIN_RESOLUTION, duration / realIterations); 89 | 90 | const minWindowTime = Math.max( 91 | 0, 92 | Math.min((maxDuration - timeSpent) / timer.scale, minTime), 93 | ); 94 | initialIterations = getItersForOpDuration(durationPerOp, minWindowTime); 95 | samples++; 96 | } 97 | } 98 | 99 | async function runBenchmarkOnce( 100 | bench, 101 | histogram, 102 | { initialIterations, maxDuration, minSamples }, 103 | benchmarkMode = "ops", 104 | ) { 105 | let iterations = 0; 106 | let timeSpent = 0; 107 | 108 | // For time mode, we want to run the benchmark exactly once 109 | if (benchmarkMode === "time") { 110 | const { 0: duration, 1: realIterations } = await clockBenchmark(bench, 1); 111 | timeSpent = duration; 112 | iterations = realIterations; 113 | 114 | // Record the duration in the histogram 115 | histogram.record(duration); 116 | 117 | return { iterations, timeSpent }; 118 | } 119 | 120 | // Ops mode - run the sampling loop 121 | while (timeSpent < maxDuration || histogram.samples.length <= minSamples) { 122 | const { 0: duration, 1: realIterations } = await clockBenchmark( 123 | bench, 124 | initialIterations, 125 | ); 126 | timeSpent += duration; 127 | 128 | iterations += realIterations; 129 | iterations = Math.min(Number.MAX_SAFE_INTEGER, iterations); 130 | 131 | // Just to avoid issues with empty fn 132 | const durationPerOp = Math.max(MIN_RESOLUTION, duration / realIterations); 133 | 134 | histogram.record(durationPerOp); 135 | 136 | const minWindowTime = Math.max( 137 | 0, 138 | Math.min((maxDuration - timeSpent) / timer.scale, bench.minTime), 139 | ); 140 | initialIterations = getItersForOpDuration(durationPerOp, minWindowTime); 141 | } 142 | 143 | return { iterations, timeSpent }; 144 | } 145 | 146 | /** 147 | * Executes a benchmark with the specified parameters 148 | * @param {import('./index').Benchmark} bench - The benchmark object to be executed 149 | * @param {number} initialIterations - The initial number of iterations to run 150 | * @param {string} benchmarkMode - The benchmark mode ('ops' or 'time') 151 | * @param {number} repeatSuite - Number of times to repeat the benchmark suite 152 | * @param {number} minSamples - Minimum number of samples to collect 153 | * @returns {Promise} The benchmark results containing operations per second or total time, iterations, histogram data and plugin results 154 | */ 155 | async function runBenchmark( 156 | bench, 157 | initialIterations, 158 | benchmarkMode, 159 | repeatSuite, 160 | minSamples, 161 | ) { 162 | const histogram = new StatisticalHistogram(); 163 | const maxDuration = bench.maxTime * timer.scale; 164 | 165 | let totalIterations = 0; 166 | let totalTimeSpent = 0; 167 | const opsSecPerRun = []; 168 | 169 | for (let i = 0; i < repeatSuite; ++i) { 170 | const { iterations, timeSpent } = await runBenchmarkOnce( 171 | bench, 172 | histogram, 173 | { 174 | initialIterations, 175 | maxDuration, 176 | minSamples, 177 | }, 178 | benchmarkMode, 179 | ); 180 | 181 | const runOpsSec = iterations / (timeSpent / timer.scale); 182 | opsSecPerRun.push(runOpsSec); 183 | 184 | totalTimeSpent += timeSpent; 185 | totalIterations += iterations; 186 | } 187 | histogram.finish(); 188 | 189 | const opsSec = totalIterations / (totalTimeSpent / timer.scale); 190 | const totalTime = totalTimeSpent / timer.scale; // Convert ns to seconds 191 | 192 | const sampleData = histogram.samples; 193 | 194 | const result = { 195 | iterations: totalIterations, 196 | // StatisticalHistogram is not a serializable object, keep raw ns for min/max 197 | histogram: { 198 | samples: sampleData.length, 199 | min: histogram.min, 200 | max: histogram.max, 201 | sampleData, 202 | }, 203 | name: bench.name, 204 | plugins: parsePluginsResult(bench.plugins, bench.name), 205 | }; 206 | 207 | // Add the appropriate metric based on the benchmark mode 208 | if (benchmarkMode === "time") { 209 | result.totalTime = totalTime / repeatSuite; // Average time per repeat 210 | debugBench( 211 | `${bench.name} completed ${repeatSuite} repeats with average time ${result.totalTime.toFixed(6)} seconds`, 212 | ); 213 | } else { 214 | result.opsSec = opsSec; 215 | result.opsSecPerRun = opsSecPerRun; 216 | debugBench( 217 | `${bench.name} completed ${sampleData.length} samples with ${opsSec.toFixed(2)} ops/sec`, 218 | ); 219 | } 220 | 221 | return result; 222 | } 223 | 224 | module.exports = { 225 | getInitialIterations, 226 | runBenchmark, 227 | runWarmup, 228 | }; 229 | -------------------------------------------------------------------------------- /lib/plugins.js: -------------------------------------------------------------------------------- 1 | const { V8OptimizeOnNextCallPlugin } = require("./plugins/v8-opt"); 2 | const { V8NeverOptimizePlugin } = require("./plugins/v8-never-opt"); 3 | 4 | const { V8GetOptimizationStatus } = require("./plugins/v8-print-status"); 5 | const { MemoryPlugin } = require("./plugins/memory"); 6 | 7 | const { validateFunction, validateArray } = require("./validators"); 8 | 9 | /** 10 | * Validates that all plugins in the array implement the required plugin interface 11 | * and that they are supported in the current environment 12 | * @param {Array} plugins - Array of plugin objects to validate 13 | * @throws {Error} If any plugin doesn't meet the requirements or isn't supported 14 | */ 15 | function validatePlugins(plugins) { 16 | for (p of plugins) { 17 | validateFunction(p.isSupported, "Plugins must have a isSupported method."); 18 | validateFunction(p.toString, "Plugins must have a toString() method."); 19 | 20 | if (!p.isSupported()) { 21 | throw new Error(`Plugin: ${p.toString()} is not supported.`); 22 | } 23 | 24 | if (typeof p.beforeClockTemplate === "function") { 25 | const result = p.beforeClockTemplate({ 26 | bench: "", 27 | awaitOrEmpty: "", 28 | context: "", 29 | timer: "", 30 | }); 31 | validateArray(result, `${p.toString()}.beforeClockTemplate()`); 32 | } 33 | 34 | if (typeof p.afterClockTemplate === "function") { 35 | const result = p.afterClockTemplate({ 36 | bench: "", 37 | awaitOrEmpty: "", 38 | context: "", 39 | timer: "", 40 | }); 41 | validateArray(result, `${p.toString()}.afterClockTemplate()`); 42 | } 43 | } 44 | } 45 | 46 | module.exports = { 47 | MemoryPlugin, 48 | V8NeverOptimizePlugin, 49 | V8GetOptimizationStatus, 50 | V8OptimizeOnNextCallPlugin, 51 | validatePlugins, 52 | }; 53 | -------------------------------------------------------------------------------- /lib/plugins/memory.js: -------------------------------------------------------------------------------- 1 | const { 2 | kStatisticalHistogramRecord, 3 | StatisticalHistogram, 4 | kStatisticalHistogramFinish, 5 | } = require("../histogram"); 6 | 7 | /** 8 | * Formats a byte value into a human-readable string with appropriate units (B, Kb, MB, GB) 9 | * @param {number} bytes - The number of bytes to format 10 | * @returns {string} Formatted string with appropriate unit suffix 11 | */ 12 | function formatBytes(bytes) { 13 | if (bytes < 1024) return `${Math.round(bytes)}B`; 14 | 15 | const kbytes = bytes / 1024; 16 | if (kbytes < 1024) return `${kbytes.toFixed(2)}Kb`; 17 | 18 | const mbytes = kbytes / 1024; 19 | if (mbytes < 1024) return `${mbytes.toFixed(2)}MB`; 20 | 21 | const gbytes = mbytes / 1024; 22 | return `${gbytes.toFixed(2)}GB`; 23 | } 24 | 25 | /** 26 | * Plugin that measures memory usage during benchmark execution 27 | * Collects heap usage statistics and provides reporting capabilities 28 | */ 29 | class MemoryPlugin { 30 | static MEMORY_BEFORE_RUN = "memoryBeforeRun"; 31 | static MEMORY_AFTER_RUN = "memoryAfterRun"; 32 | static #WARNING_REPORTED = false; 33 | 34 | /** 35 | * @type {StatisticalHistogram} 36 | */ 37 | #heapUsedHistogram; 38 | 39 | constructor() { 40 | this.reset(); 41 | } 42 | 43 | isSupported() { 44 | return typeof globalThis.gc === "function"; 45 | } 46 | 47 | beforeClockTemplate({ managed, globalThisVar, contextVar }) { 48 | if (managed && !MemoryPlugin.#WARNING_REPORTED) { 49 | MemoryPlugin.#WARNING_REPORTED = true; 50 | process.emitWarning( 51 | "The memory statistics can be inaccurate since it will include the tear-up and teardown of your benchmark.", 52 | ); 53 | } 54 | 55 | let code = ""; 56 | 57 | code += `${contextVar}.${MemoryPlugin.MEMORY_BEFORE_RUN} = 0;\n`; 58 | code += `${contextVar}.${MemoryPlugin.MEMORY_AFTER_RUN} = 0;\n`; 59 | code += `${globalThisVar}.gc();\n`; 60 | code += `${contextVar}.${MemoryPlugin.MEMORY_BEFORE_RUN} = ${globalThisVar}.process.memoryUsage();\n`; 61 | 62 | return code; 63 | } 64 | 65 | afterClockTemplate({ globalThisVar, contextVar }) { 66 | return `${contextVar}.${MemoryPlugin.MEMORY_AFTER_RUN} = ${globalThisVar}.process.memoryUsage();\n`; 67 | } 68 | 69 | onCompleteClock(result) { 70 | const realIterations = result[1]; 71 | const context = result[2]; 72 | 73 | const heapUsed = 74 | context[MemoryPlugin.MEMORY_AFTER_RUN].heapUsed - 75 | context[MemoryPlugin.MEMORY_BEFORE_RUN].heapUsed; 76 | const externalUsed = 77 | context[MemoryPlugin.MEMORY_AFTER_RUN].external - 78 | context[MemoryPlugin.MEMORY_BEFORE_RUN].external; 79 | 80 | const memoryAllocated = (heapUsed + externalUsed) / realIterations; 81 | 82 | // below 0, we just coerce to be zero 83 | this.#heapUsedHistogram[kStatisticalHistogramRecord]( 84 | Math.max(0, memoryAllocated), 85 | ); 86 | } 87 | 88 | onCompleteBenchmark() { 89 | this.#heapUsedHistogram[kStatisticalHistogramFinish](); 90 | } 91 | 92 | toString() { 93 | return "MemoryPlugin"; 94 | } 95 | 96 | getReport() { 97 | return `heap usage=${formatBytes(this.#heapUsedHistogram.mean)} (${formatBytes(this.#heapUsedHistogram.min)} ... ${formatBytes(this.#heapUsedHistogram.max)})`; 98 | } 99 | 100 | getResult() { 101 | return { 102 | proto: null, 103 | type: this.toString(), 104 | histogram: this.#heapUsedHistogram, 105 | }; 106 | } 107 | } 108 | 109 | module.exports = { 110 | MemoryPlugin, 111 | }; 112 | -------------------------------------------------------------------------------- /lib/plugins/v8-never-opt.js: -------------------------------------------------------------------------------- 1 | class V8NeverOptimizePlugin { 2 | isSupported() { 3 | try { 4 | new Function("%NeverOptimizeFunction(() => {})")(); 5 | 6 | return true; 7 | } catch (e) { 8 | return false; 9 | } 10 | } 11 | 12 | beforeClockTemplate(_varNames) { 13 | let code = ""; 14 | 15 | code += ` 16 | function DoNotOptimize(x) {} 17 | // Prevent DoNotOptimize from optimizing or being inlined. 18 | %NeverOptimizeFunction(DoNotOptimize); 19 | `; 20 | return [code, "DoNotOptimize"]; 21 | } 22 | 23 | toString() { 24 | return "V8NeverOptimizePlugin"; 25 | } 26 | 27 | getReport() { 28 | return "v8-never-optimize=true"; 29 | } 30 | } 31 | 32 | module.exports = { 33 | V8NeverOptimizePlugin, 34 | }; 35 | -------------------------------------------------------------------------------- /lib/plugins/v8-opt.js: -------------------------------------------------------------------------------- 1 | class V8OptimizeOnNextCallPlugin { 2 | isSupported() { 3 | try { 4 | new Function("%OptimizeFunctionOnNextCall(() => {})")(); 5 | 6 | return true; 7 | } catch (e) { 8 | return false; 9 | } 10 | } 11 | 12 | beforeClockTemplate({ awaitOrEmpty, bench, timer }) { 13 | let code = ""; 14 | 15 | code += `%OptimizeFunctionOnNextCall(${bench}.fn);\n`; 16 | code += `${awaitOrEmpty}${bench}.fn(${timer});\n`; 17 | code += `${awaitOrEmpty}${bench}.fn(${timer});\n`; 18 | 19 | return [code]; 20 | } 21 | 22 | getReport() { 23 | return "v8-optimize-next-call=enabled"; 24 | } 25 | 26 | toString() { 27 | return "V8OptimizeOnNextCallPlugin"; 28 | } 29 | } 30 | 31 | module.exports = { 32 | V8OptimizeOnNextCallPlugin, 33 | }; 34 | -------------------------------------------------------------------------------- /lib/plugins/v8-print-status.js: -------------------------------------------------------------------------------- 1 | function checkBitmap(value, bit) { 2 | return (value & bit) === bit; 3 | } 4 | 5 | function translateStatus(optStatus) { 6 | if (optStatus === -1) { 7 | return "unknown"; 8 | } 9 | 10 | const optStat = []; 11 | if (checkBitmap(optStatus, 2)) { 12 | optStat.push("Never Optimized"); 13 | } 14 | if (checkBitmap(optStatus, 4)) { 15 | optStat.push("Always Optimized"); 16 | } 17 | if (checkBitmap(optStatus, 8)) { 18 | optStat.push("Maybe Deopted"); 19 | } 20 | if (checkBitmap(optStatus, 16)) { 21 | optStat.push("Optimized"); 22 | } 23 | if (checkBitmap(optStatus, 32)) { 24 | optStat.push("TurboFanned"); 25 | } 26 | if (checkBitmap(optStatus, 64)) { 27 | optStat.push("Interpreted"); 28 | } 29 | if (checkBitmap(optStatus, 128)) { 30 | optStat.push("Marked for Optimization"); 31 | } 32 | if (checkBitmap(optStatus, 256)) { 33 | optStat.push("Marked for Concurrent Optimization"); 34 | } 35 | if (checkBitmap(optStatus, 512)) { 36 | optStat.push("Concurrently Optimizing"); 37 | } 38 | if (checkBitmap(optStatus, 1024)) { 39 | optStat.push("Is Executing"); 40 | } 41 | if (checkBitmap(optStatus, 2048)) { 42 | optStat.push("Topmost frame is Turbo Fanned"); 43 | } 44 | if (checkBitmap(optStatus, 4096)) { 45 | optStat.push("Lite Mode"); 46 | } 47 | if (checkBitmap(optStatus, 8192)) { 48 | optStat.push("Marked for de-optimization"); 49 | } 50 | 51 | return optStat.join(", "); 52 | } 53 | 54 | class V8GetOptimizationStatus { 55 | #optimizationStatuses = []; 56 | 57 | isSupported() { 58 | try { 59 | new Function("%GetOptimizationStatus(() => {})")(); 60 | 61 | return true; 62 | } catch (e) { 63 | return false; 64 | } 65 | } 66 | 67 | afterClockTemplate({ bench, context }) { 68 | let code = ""; 69 | code += `${context}.v8OptimizationStatus = %GetOptimizationStatus(${bench}.fn);\n`; 70 | return [code]; 71 | } 72 | 73 | onCompleteBenchmark(result) { 74 | const context = result[2]; 75 | this.#optimizationStatuses.push(context.v8OptimizationStatus); 76 | } 77 | 78 | toString() { 79 | return "V8GetOptimizationStatus"; 80 | } 81 | 82 | getReport() { 83 | const allAvailableStatus = this.#optimizationStatuses.reduce( 84 | (acc, v) => acc | v, 85 | 0, 86 | ); 87 | return `v8-opt-status="${translateStatus(allAvailableStatus)}"`; 88 | } 89 | 90 | getResult() { 91 | const allAvailableStatus = this.#optimizationStatuses.reduce( 92 | (acc, v) => acc | v, 93 | 0, 94 | ); 95 | return { 96 | type: this.toString(), 97 | optimizationStatuses: translateStatus(allAvailableStatus), 98 | }; 99 | } 100 | } 101 | 102 | module.exports = { 103 | V8GetOptimizationStatus, 104 | }; 105 | -------------------------------------------------------------------------------- /lib/report.js: -------------------------------------------------------------------------------- 1 | const { textReport } = require("./reporter/text"); 2 | const { chartReport } = require("./reporter/chart"); 3 | const { htmlReport } = require("./reporter/html"); 4 | const { jsonReport } = require("./reporter/json"); 5 | const { csvReport } = require("./reporter/csv"); 6 | 7 | /** 8 | * @typedef {Object} BenchmarkResult 9 | * @property {string} name - The name of the benchmark 10 | * @property {number} opsSec - Operations per second 11 | * @property {number} iterations - Total number of iterations run 12 | * @property {Object} histogram - Statistical data about the benchmark runs 13 | * @property {Array} plugins - Results from plugins used during benchmarking 14 | */ 15 | 16 | /** 17 | * Exports various report generators for benchmark results 18 | * @type {Object} 19 | * @property {function(BenchmarkResult[]): void} chartReport - Generates a chart visualization of benchmark results 20 | * @property {function(BenchmarkResult[]): void} textReport - Generates a text report of benchmark results 21 | * @property {function(BenchmarkResult[]): void} htmlReport - Generates an HTML report of benchmark results 22 | * @property {function(BenchmarkResult[]): string} jsonReport - Generates a JSON report of benchmark results 23 | * @property {function(BenchmarkResult[]): string} csvReport - Generates a CSV report of benchmark results 24 | */ 25 | module.exports = { 26 | chartReport, 27 | textReport, 28 | htmlReport, 29 | jsonReport, 30 | csvReport, 31 | }; 32 | -------------------------------------------------------------------------------- /lib/reporter/chart.js: -------------------------------------------------------------------------------- 1 | const { platform, arch, cpus, totalmem } = require("node:os"); 2 | const { styleText } = require("../utils/styleText"); 3 | 4 | const formatter = Intl.NumberFormat(undefined, { 5 | notation: "standard", 6 | maximumFractionDigits: 2, 7 | }); 8 | 9 | const timer = Intl.NumberFormat(undefined, { 10 | minimumFractionDigits: 3, 11 | maximumFractionDigits: 3, 12 | }); 13 | 14 | /** 15 | * Draws a bar chart representation of a benchmark result 16 | * @param {string} label - The label for the bar (benchmark name) 17 | * @param {number} value - The value to display (operations per second or time per operation) 18 | * @param {number} total - The maximum value in the dataset (for scaling) 19 | * @param {number} samples - Number of samples collected 20 | * @param {string} metric - The metric being displayed (opsSec or totalTime) 21 | * @param {number} [length=30] - Length of the bar in characters 22 | */ 23 | function drawBar(label, value, total, samples, metric, length = 30) { 24 | let percentage; 25 | let displayedValue; 26 | let displayedMetric; 27 | 28 | if (metric === "opsSec") { 29 | percentage = value / total; // Higher ops/sec is better 30 | const valueReported = value < 100 ? value.toFixed(2) : value.toFixed(0); 31 | displayedValue = styleText(["yellow"], formatter.format(valueReported)); 32 | displayedMetric = "ops/sec"; 33 | } else { 34 | // metric === 'totalTime' 35 | percentage = 1 - value / total; // Lower totalTime is better, invert percentage 36 | let timeFormatted; 37 | if (value < 0.000001) { 38 | // Less than 1 microsecond, show in nanoseconds 39 | timeFormatted = `${(value * 1000000000).toFixed(2)} ns`; 40 | } else if (value < 0.001) { 41 | // Less than 1 millisecond, show in microseconds 42 | timeFormatted = `${(value * 1000000).toFixed(2)} µs`; 43 | } else if (value < 1) { 44 | // Less than 1 second, show in milliseconds 45 | timeFormatted = `${(value * 1000).toFixed(2)} ms`; 46 | } else { 47 | // 1 second or more, show in seconds 48 | timeFormatted = `${value.toFixed(2)} s`; 49 | } 50 | displayedValue = styleText(["yellow"], timeFormatted); 51 | displayedMetric = "total time"; 52 | } 53 | 54 | const filledLength = Math.round(length * percentage); 55 | const bar = "█".repeat(filledLength) + "-".repeat(length - filledLength); 56 | 57 | const displayedSamples = styleText(["yellow"], samples.toString()); 58 | 59 | process.stdout.write( 60 | `${label.padEnd(45)} | ${bar} | ${displayedValue} ${displayedMetric} | ${displayedSamples} samples\n`, 61 | ); 62 | } 63 | 64 | const environment = { 65 | nodeVersion: `Node.js version: ${process.version}`, 66 | platform: `${platform()} ${arch()}`, 67 | hardware: `${cpus().length} vCPUs | ${(totalmem() / 1024 ** 3).toFixed(1)}GB Mem`, 68 | }; 69 | 70 | /** 71 | * Generates a chart visualization of benchmark results in the console 72 | * Displays system information and a bar chart of operations per second or time per operation 73 | * @param {import('../report').BenchmarkResult[]} results - Array of benchmark results 74 | */ 75 | function chartReport(results) { 76 | // Determine the primary metric and calculate max value for scaling 77 | const primaryMetric = 78 | results[0]?.opsSec !== undefined ? "opsSec" : "totalTime"; 79 | const maxValue = Math.max(...results.map((b) => b[primaryMetric])); 80 | 81 | process.stdout.write( 82 | `${environment.nodeVersion}\n` + 83 | `Platform: ${environment.platform}\n` + 84 | `CPU Cores: ${environment.hardware}\n\n`, 85 | ); 86 | 87 | for (const result of results) { 88 | drawBar( 89 | result.name, 90 | result[primaryMetric], 91 | maxValue, 92 | result.histogram.samples, 93 | primaryMetric, 94 | ); 95 | } 96 | } 97 | 98 | module.exports = { 99 | chartReport, 100 | }; 101 | -------------------------------------------------------------------------------- /lib/reporter/csv.js: -------------------------------------------------------------------------------- 1 | const { timer } = require("../clock"); 2 | 3 | const formatter = Intl.NumberFormat(undefined, { 4 | notation: "standard", 5 | maximumFractionDigits: 2, 6 | }); 7 | 8 | // Helper function to format time with appropriate unit for CSV 9 | const formatTimeForCSV = (time) => { 10 | if (time < 0.000001) { 11 | return `${(time * 1000000000).toFixed(2)} ns`; 12 | } else if (time < 0.001) { 13 | return `${(time * 1000000).toFixed(2)} µs`; 14 | } else if (time < 1) { 15 | return `${(time * 1000).toFixed(2)} ms`; 16 | } else { 17 | return `${time.toFixed(2)} s`; 18 | } 19 | }; 20 | 21 | function csvReport(results) { 22 | const primaryMetric = 23 | results[0]?.opsSec !== undefined ? "opsSec" : "totalTime"; 24 | const header = `name,${primaryMetric === "opsSec" ? "ops/sec" : "total time"},samples,plugins,min,max\n`; 25 | process.stdout.write(header); 26 | 27 | for (const result of results) { 28 | process.stdout.write(`${result.name},`); 29 | 30 | if (primaryMetric === "opsSec") { 31 | const opsSecReported = 32 | result.opsSec < 100 33 | ? result.opsSec.toFixed(2) 34 | : result.opsSec.toFixed(0); 35 | process.stdout.write(`"${formatter.format(opsSecReported)}",`); 36 | } else { 37 | // primaryMetric === 'totalTime' 38 | process.stdout.write(`"${formatTimeForCSV(result.totalTime)}",`); 39 | } 40 | 41 | process.stdout.write(`${result.histogram.samples},`); 42 | 43 | process.stdout.write( 44 | `"${result.plugins 45 | .filter((p) => p.report) 46 | .map((p) => p.report) 47 | .join(",")}",`, 48 | ); 49 | 50 | // For test compatibility, format min/max in microseconds 51 | const minInUs = result.histogram.min / 1000; 52 | const maxInUs = result.histogram.max / 1000; 53 | process.stdout.write(`${minInUs.toFixed(2)}us,`); 54 | process.stdout.write(`${maxInUs.toFixed(2)}us\n`); 55 | } 56 | } 57 | 58 | module.exports = { 59 | csvReport, 60 | }; 61 | -------------------------------------------------------------------------------- /lib/reporter/html.js: -------------------------------------------------------------------------------- 1 | const { platform, arch, cpus, totalmem } = require("node:os"); 2 | const fs = require("node:fs"); 3 | const path = require("node:path"); 4 | 5 | const formatter = Intl.NumberFormat(undefined, { 6 | notation: "standard", 7 | maximumFractionDigits: 3, 8 | }); 9 | 10 | const timer = Intl.NumberFormat(undefined, { 11 | minimumFractionDigits: 3, 12 | maximumFractionDigits: 3, 13 | }); 14 | 15 | const formatTime = (time) => { 16 | if (time < 0.000001) { 17 | // Less than 1 microsecond, show in nanoseconds 18 | return `${(time * 1000000000).toFixed(2)} ns`; 19 | } else if (time < 0.001) { 20 | // Less than 1 millisecond, show in microseconds 21 | return `${(time * 1000000).toFixed(2)} µs`; 22 | } else if (time < 1) { 23 | // Less than 1 second, show in milliseconds 24 | return `${(time * 1000).toFixed(2)} ms`; 25 | } else { 26 | // 1 second or more, show in seconds 27 | return `${time.toFixed(2)} s`; 28 | } 29 | }; 30 | 31 | const valueToDuration = (maxValue, value, isTimeBased, scalingFactor = 10) => { 32 | const normalizedValue = isTimeBased ? maxValue / value : value / maxValue; 33 | const baseSpeed = (1 / normalizedValue) * scalingFactor; 34 | return Math.max(baseSpeed, 2); // Normalize speed with a minimum of 2 seconds 35 | }; 36 | 37 | const generateHTML = (template, durations) => { 38 | let css = ""; 39 | let labelDiv = ""; 40 | let circleDiv = ""; 41 | let position = 20; 42 | const colors = [ 43 | "blue", 44 | "orange", 45 | "yellow", 46 | "purple", 47 | "black", 48 | "grey", 49 | "red", 50 | "green", 51 | "pink", 52 | "cyan", 53 | ]; 54 | for (const d of durations) { 55 | css += ` 56 | #label-${d.name} { 57 | top: ${position}px; 58 | } 59 | 60 | #circle-${d.name} { 61 | background-color: ${colors.shift()}; 62 | top: ${position}px; 63 | } 64 | `; 65 | circleDiv += ` 66 |
67 | ${d.name}(${d.metricValueFormatted} ${d.metricUnit}) 68 |
min: ${d.minFormatted}, max: ${d.maxFormatted} 69 |
70 | `; 71 | labelDiv += ` 72 |
73 | `; 74 | 75 | position += 80; 76 | } 77 | 78 | const environmentDiv = `

Node.js version: ${process.version}

79 |

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

80 |

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

`; 81 | 82 | return template 83 | .replaceAll("{{CONTAINER_HEIGHT}}", `${durations.length * 100}px;`) 84 | .replaceAll("{{CSS}}", css) 85 | .replaceAll("{{ENVIRONMENT_DIV}}", environmentDiv) 86 | .replaceAll("{{LABEL_DIV}}", labelDiv) 87 | .replaceAll("{{CIRCLE_DIV}}", circleDiv) 88 | .replaceAll("{{DURATIONS}}", JSON.stringify(durations)); 89 | }; 90 | 91 | const templatePath = path.join(__dirname, "template.html"); 92 | const template = fs.readFileSync(templatePath, "utf8"); 93 | 94 | function htmlReport(results) { 95 | const primaryMetric = 96 | results[0]?.opsSec !== undefined ? "opsSec" : "totalTime"; 97 | let durations; 98 | 99 | if (primaryMetric === "opsSec") { 100 | const maxOpsSec = Math.max(...results.map((b) => b.opsSec)); 101 | durations = results.map((r) => ({ 102 | name: r.name.replaceAll(" ", "-"), 103 | duration: valueToDuration(maxOpsSec, r.opsSec, false), 104 | metricValueFormatted: formatter.format(r.opsSec), 105 | metricUnit: "ops/sec", 106 | minFormatted: timer.format(r.histogram.min), // Use timer for ns format 107 | maxFormatted: timer.format(r.histogram.max), 108 | })); 109 | } else { 110 | // metric === 'totalTime' 111 | const maxTotalTime = Math.max(...results.map((b) => b.totalTime)); 112 | durations = results.map((r) => ({ 113 | name: r.name.replaceAll(" ", "-"), 114 | duration: valueToDuration(maxTotalTime, r.totalTime, true), 115 | metricValueFormatted: formatTime(r.totalTime), 116 | metricUnit: "total time", 117 | minFormatted: timer.format(r.histogram.min), // Use timer for ns format 118 | maxFormatted: timer.format(r.histogram.max), 119 | })); 120 | } 121 | 122 | const htmlContent = generateHTML(template, durations); 123 | fs.writeFileSync("result.html", htmlContent, "utf8"); 124 | process.stdout.write("HTML file has been generated: result.html\n"); 125 | } 126 | 127 | module.exports = { 128 | htmlReport, 129 | }; 130 | -------------------------------------------------------------------------------- /lib/reporter/json.js: -------------------------------------------------------------------------------- 1 | const { timer } = require("../clock"); 2 | 3 | // Helper function to format time in appropriate units 4 | const formatTime = (time) => { 5 | if (time < 0.000001) { 6 | // Less than 1 microsecond, show in nanoseconds 7 | return `${(time * 1000000000).toFixed(2)} ns`; 8 | } else if (time < 0.001) { 9 | // Less than 1 millisecond, show in microseconds 10 | return `${(time * 1000000).toFixed(2)} µs`; 11 | } else if (time < 1) { 12 | // Less than 1 second, show in milliseconds 13 | return `${(time * 1000).toFixed(2)} ms`; 14 | } else { 15 | // 1 second or more, show in seconds 16 | return `${time.toFixed(2)} s`; 17 | } 18 | }; 19 | 20 | function jsonReport(results) { 21 | const output = results.map((result) => { 22 | const baseResult = { 23 | name: result.name, 24 | runsSampled: result.histogram.samples, 25 | min: timer.format(result.histogram.min), 26 | max: timer.format(result.histogram.max), 27 | // Report anything the plugins returned 28 | plugins: result.plugins.map((p) => p.report).filter(Boolean), 29 | }; 30 | 31 | if (result.opsSec !== undefined) { 32 | const opsSecReported = 33 | result.opsSec < 100 34 | ? result.opsSec.toFixed(2) 35 | : result.opsSec.toFixed(0); 36 | baseResult.opsSec = Number(opsSecReported); 37 | } else if (result.totalTime !== undefined) { 38 | baseResult.totalTime = result.totalTime; // Total time in seconds 39 | baseResult.totalTimeFormatted = formatTime(result.totalTime); 40 | } 41 | 42 | return baseResult; 43 | }); 44 | 45 | console.log(JSON.stringify(output, null, 2)); 46 | } 47 | 48 | module.exports = { 49 | jsonReport, 50 | }; 51 | -------------------------------------------------------------------------------- /lib/reporter/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Benchmark Visualizer 7 | 82 | 83 | 84 |
85 | {{ENVIRONMENT_DIV}} 86 |
87 | 88 |
89 | {{LABEL_DIV}} 90 | 91 | {{CIRCLE_DIV}} 92 |
93 | 94 |
95 |

Benchmark done with bench-node

96 |
97 | 98 | 99 | 100 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /lib/reporter/text.js: -------------------------------------------------------------------------------- 1 | const { styleText } = require("../utils/styleText"); 2 | const { timer } = require("../clock"); 3 | 4 | const formatter = Intl.NumberFormat(undefined, { 5 | notation: "standard", 6 | maximumFractionDigits: 2, 7 | }); 8 | 9 | /** 10 | * Generates a text report of benchmark results, displaying each benchmark's name, 11 | * operations per second, number of samples, plugin results, and min/max timings 12 | * @param {import('../report').BenchmarkResult[]} results - Array of benchmark results 13 | */ 14 | function textReport(results) { 15 | for (const result of results) { 16 | process.stdout.write(result.name.padEnd(45)); 17 | process.stdout.write(" x "); 18 | 19 | if (result.opsSec !== undefined) { 20 | const opsSecReported = 21 | result.opsSec < 100 22 | ? result.opsSec.toFixed(2) 23 | : result.opsSec.toFixed(0); 24 | process.stdout.write( 25 | styleText( 26 | ["cyan", "bold"], 27 | `${formatter.format(opsSecReported)} ops/sec`, 28 | ), 29 | ); 30 | } else if (result.totalTime !== undefined) { 31 | // Format time based on magnitude: 32 | // - < 0.000001 seconds (< 1 µs): show in nanoseconds 33 | // - < 0.001 seconds (< 1 ms): show in microseconds 34 | // - < 1 second: show in milliseconds 35 | // - >= 1 second: show in seconds 36 | let timeFormatted; 37 | if (result.totalTime < 0.000001) { 38 | // Less than 1 microsecond, show in nanoseconds 39 | timeFormatted = `${(result.totalTime * 1000000000).toFixed(2)} ns`; 40 | } else if (result.totalTime < 0.001) { 41 | // Less than 1 millisecond, show in microseconds 42 | timeFormatted = `${(result.totalTime * 1000000).toFixed(2)} µs`; 43 | } else if (result.totalTime < 1) { 44 | // Less than 1 second, show in milliseconds 45 | timeFormatted = `${(result.totalTime * 1000).toFixed(2)} ms`; 46 | } else { 47 | // 1 second or more, show in seconds 48 | timeFormatted = `${result.totalTime.toFixed(2)} s`; 49 | } 50 | 51 | process.stdout.write( 52 | styleText(["cyan", "bold"], `${timeFormatted} total time`), 53 | ); 54 | } 55 | 56 | // TODO: produce confidence on stddev 57 | // process.stdout.write(result.histogram.stddev.toString()); 58 | process.stdout.write(` (${result.histogram.samples} runs sampled) `); 59 | 60 | for (const p of result.plugins) { 61 | if (p.report) { 62 | process.stdout.write(styleText("dim", `${p.report} `)); 63 | } 64 | } 65 | 66 | process.stdout.write("min..max=("); 67 | process.stdout.write( 68 | styleText("green", timer.format(result.histogram.min)), 69 | ); 70 | process.stdout.write(styleText("dim", "...")); 71 | process.stdout.write( 72 | styleText("red", `${timer.format(result.histogram.max)})`), 73 | ); 74 | process.stdout.write("\n"); 75 | } 76 | } 77 | 78 | module.exports = { 79 | textReport, 80 | }; 81 | -------------------------------------------------------------------------------- /lib/utils/styleText.js: -------------------------------------------------------------------------------- 1 | const util = require("node:util"); 2 | 3 | module.exports.styleText = 4 | typeof util.styleText === "function" 5 | ? util.styleText 6 | : (_style, text) => text; 7 | -------------------------------------------------------------------------------- /lib/validators.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates an error object with ERR_INVALID_ARG_TYPE code 3 | * @param {string} message - The error message 4 | * @returns {Error} Error with code ERR_INVALID_ARG_TYPE 5 | */ 6 | function ERR_INVALID_ARG_TYPE(message) { 7 | const err = new Error(message); 8 | err.code = "ERR_INVALID_ARG_TYPE"; 9 | return err; 10 | } 11 | 12 | /** 13 | * Creates an error object with ERR_INVALID_ARG_VALUE code 14 | * @param {string} message - The error message 15 | * @returns {Error} Error with code ERR_INVALID_ARG_VALUE 16 | */ 17 | function ERR_INVALID_ARG_VALUE(message) { 18 | const err = new Error(message); 19 | err.code = "ERR_INVALID_ARG_VALUE"; 20 | return err; 21 | } 22 | 23 | /** 24 | * Validates that a value is a number within an optional range 25 | * @param {any} value - The value to validate 26 | * @param {string} name - Name of the parameter being validated 27 | * @param {number} [min] - Optional minimum value (inclusive) 28 | * @param {number} [max] - Optional maximum value (inclusive) 29 | * @throws {Error} If validation fails 30 | */ 31 | function validateNumber(value, name, min, max) { 32 | if (typeof value !== "number") 33 | throw ERR_INVALID_ARG_TYPE( 34 | `value must be a number, name: ${name}, value: ${value}`, 35 | ); 36 | 37 | if ( 38 | (min != null && value < min) || 39 | (max != null && value > max) || 40 | ((min != null || max != null) && Number.isNaN(value)) 41 | ) { 42 | throw ERR_INVALID_ARG_VALUE( 43 | `value must be ${min != null ? `>= ${min}` : ""}${min != null && max != null ? " && " : ""}${max != null ? `<= ${max}` : ""}, name: ${name}, value: ${value}`, 44 | ); 45 | } 46 | } 47 | 48 | /** 49 | * Validates that a value is an object (not null and not an array) 50 | * @param {any} value - The value to validate 51 | * @param {string} name - Name of the parameter being validated 52 | * @throws {Error} If validation fails 53 | */ 54 | function validateObject(value, name) { 55 | if (value === null || Array.isArray(value)) { 56 | throw ERR_INVALID_ARG_TYPE( 57 | `value must be an object, name: ${name}, value: ${value}`, 58 | ); 59 | } 60 | 61 | if (typeof value !== "object") { 62 | throw ERR_INVALID_ARG_TYPE( 63 | `value must be an object, name: ${name}, value: ${value}`, 64 | ); 65 | } 66 | } 67 | 68 | /** 69 | * Validates that a value is a function 70 | * @param {any} value - The value to validate 71 | * @param {string} name - Name of the parameter being validated 72 | * @throws {Error} If validation fails 73 | */ 74 | function validateFunction(value, name) { 75 | if (typeof value !== "function") 76 | throw ERR_INVALID_ARG_TYPE( 77 | `value must be a function, name: ${name}, value: ${value}`, 78 | ); 79 | } 80 | 81 | /** 82 | * Validates that a value is a string 83 | * @param {any} value - The value to validate 84 | * @param {string} name - Name of the parameter being validated 85 | * @throws {Error} If validation fails 86 | */ 87 | function validateString(value, name) { 88 | if (typeof value !== "string") 89 | throw ERR_INVALID_ARG_TYPE( 90 | `value must be a string, name: ${name}, value: ${value}`, 91 | ); 92 | } 93 | 94 | /** 95 | * Validates that a value is one of the allowed benchmark modes 96 | * @param {any} value - The value to validate 97 | * @param {string} name - Name of the parameter being validated 98 | * @throws {Error} If validation fails 99 | */ 100 | function validateBenchmarkMode(value, name) { 101 | validateString(value, name); 102 | 103 | const validModes = ["ops", "time"]; 104 | if (!validModes.includes(value)) { 105 | throw ERR_INVALID_ARG_VALUE( 106 | `value must be one of ${validModes.join(", ")}, name: ${name}, value: ${value}`, 107 | ); 108 | } 109 | } 110 | 111 | /** 112 | * Validates that a value is an array 113 | * @param {any} value - The value to validate 114 | * @param {string} name - Name of the parameter being validated 115 | * @throws {Error} If validation fails 116 | */ 117 | function validateArray(value, name) { 118 | if (!Array.isArray(value)) 119 | throw ERR_INVALID_ARG_TYPE( 120 | `value must be a array, name: ${name}, value: ${value}`, 121 | ); 122 | } 123 | 124 | module.exports = { 125 | validateFunction, 126 | validateNumber, 127 | validateObject, 128 | validateString, 129 | validateArray, 130 | validateBenchmarkMode, 131 | }; 132 | -------------------------------------------------------------------------------- /lib/worker-runner.js: -------------------------------------------------------------------------------- 1 | const { parentPort } = require("node:worker_threads"); 2 | const { runBenchmark } = require("./lifecycle"); 3 | 4 | // Deserialize the benchmark function 5 | function deserializeBenchmark(benchmark) { 6 | benchmark.fn = new Function(benchmark.fn); 7 | } 8 | 9 | parentPort.on( 10 | "message", 11 | async ({ 12 | benchmark, 13 | initialIterations, 14 | benchmarkMode, 15 | repeatSuite, 16 | minSamples, 17 | }) => { 18 | deserializeBenchmark(benchmark); 19 | const result = await runBenchmark( 20 | benchmark, 21 | initialIterations, 22 | benchmarkMode, 23 | repeatSuite, 24 | minSamples, 25 | ); 26 | parentPort.postMessage(result); 27 | }, 28 | ); 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bench-node", 3 | "version": "0.7.0", 4 | "description": "", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "node --test --allow-natives-syntax", 8 | "lint": "biome lint .", 9 | "lint:ci": "biome ci .", 10 | "lint:fix": "biome lint --write .", 11 | "lint:force-fix": "biome lint --write --unsafe .", 12 | "format": "biome format --write ." 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/RafaelGSS/bench-node.git" 17 | }, 18 | "keywords": [ 19 | "benchmark", 20 | "nodejs" 21 | ], 22 | "author": "RafaelGSS ", 23 | "contributors": [ 24 | { 25 | "name": "H4ad", 26 | "author": true 27 | } 28 | ], 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/RafaelGSS/bench-node/issues" 32 | }, 33 | "homepage": "https://github.com/RafaelGSS/bench-node#readme", 34 | "dependencies": { 35 | "piscina": "^4.8.0" 36 | }, 37 | "devDependencies": { 38 | "@biomejs/biome": "1.9.4" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": { 3 | ".": { 4 | "release-type": "node", 5 | "bump-minor-pre-major": true, 6 | "bump-patch-for-minor-pre-major": false, 7 | "draft": false, 8 | "prerelease": false, 9 | "draft-pull-request": true, 10 | "include-component-in-tag": false, 11 | "include-v-in-tag": true, 12 | "separate-pull-requests": false, 13 | "skip-github-release": false, 14 | "versioning": "default", 15 | "pull-request-header": ":robot: I have created a release *beep* *boop*", 16 | "pull-request-title-pattern": "chore${scope}: release${component} ${version}", 17 | "changelog-path": "CHANGELOG.md", 18 | "changelog-host": "https://github.com", 19 | "changelog-type": "default", 20 | "changelog-sections": [ 21 | { 22 | "type": "feat", 23 | "section": "Features" 24 | }, 25 | { 26 | "type": "feature", 27 | "section": "Features" 28 | }, 29 | { 30 | "type": "fix", 31 | "section": "Bug Fixes" 32 | }, 33 | { 34 | "type": "perf", 35 | "section": "Performance Improvements" 36 | }, 37 | { 38 | "type": "revert", 39 | "section": "Reverts" 40 | }, 41 | { 42 | "type": "docs", 43 | "section": "Documentation" 44 | }, 45 | { 46 | "type": "style", 47 | "section": "Styles" 48 | }, 49 | { 50 | "type": "chore", 51 | "section": "Miscellaneous Chores" 52 | }, 53 | { 54 | "type": "refactor", 55 | "section": "Code Refactoring" 56 | }, 57 | { 58 | "type": "test", 59 | "section": "Tests" 60 | }, 61 | { 62 | "type": "build", 63 | "section": "Build System" 64 | }, 65 | { 66 | "type": "ci", 67 | "section": "Continuous Integration" 68 | } 69 | ] 70 | } 71 | }, 72 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" 73 | } 74 | -------------------------------------------------------------------------------- /test/async.js: -------------------------------------------------------------------------------- 1 | const { todo } = require("node:test"); 2 | 3 | todo("async tasks should behave similar to sync tasks", async () => {}); 4 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | const { Suite } = require("../lib/index"); 2 | const { describe, it, todo } = require("node:test"); 3 | const assert = require("node:assert"); 4 | const { spawnSync } = require("node:child_process"); 5 | const path = require("node:path"); 6 | 7 | function noop() {} 8 | 9 | describe("API Interface", () => { 10 | it("options should be an object", () => { 11 | for (const r of [1, "ds", null]) { 12 | assert.throws( 13 | () => { 14 | new Suite(r); 15 | }, 16 | { 17 | code: "ERR_INVALID_ARG_TYPE", 18 | }, 19 | ); 20 | } 21 | // doesNotThrow 22 | new Suite({}); 23 | }); 24 | 25 | it("reporter should be a function", () => { 26 | for (const r of [1, "ds", {}]) { 27 | assert.throws( 28 | () => { 29 | new Suite({ reporter: r }); 30 | }, 31 | { 32 | code: "ERR_INVALID_ARG_TYPE", 33 | }, 34 | ); 35 | } 36 | // doesNotThrow 37 | new Suite({ reporter: () => {} }); 38 | }); 39 | 40 | it("reporter can be false or null", () => { 41 | for (const r of [false, null]) { 42 | // doesNotThrow 43 | new Suite({ reporter: r }); 44 | } 45 | }); 46 | 47 | describe("suite.add", () => { 48 | const bench = new Suite({ reporter: noop }); 49 | it("name should be an string", () => { 50 | for (const r of [1, undefined, null, {}]) { 51 | assert.throws(() => { 52 | bench.add(r); 53 | }); 54 | } 55 | // doesNotThrow 56 | bench.add("example", noop); 57 | }); 58 | 59 | it("options should be an valid object", () => { 60 | for (const r of [1, "ds", null]) { 61 | assert.throws( 62 | () => { 63 | bench.add("name", r, noop); 64 | }, 65 | { 66 | code: "ERR_INVALID_ARG_TYPE", 67 | }, 68 | ); 69 | } 70 | }); 71 | 72 | it("minTime should be a valid number", () => { 73 | for (const r of ["ds", {}, () => {}]) { 74 | assert.throws( 75 | () => { 76 | bench.add("name", { minTime: r }, noop); 77 | }, 78 | { 79 | code: "ERR_INVALID_ARG_TYPE", 80 | }, 81 | ); 82 | } 83 | assert.throws( 84 | () => { 85 | bench.add("name", { minTime: 0 }, noop); 86 | }, 87 | { 88 | code: "ERR_INVALID_ARG_VALUE", 89 | }, 90 | ); 91 | assert.throws( 92 | () => { 93 | bench.add("name", { minTime: 0.000001 }, noop); 94 | }, 95 | { 96 | code: "ERR_INVALID_ARG_VALUE", 97 | }, 98 | ); 99 | // doesNotThrow 100 | bench.add("name", { minTime: 0.5 }, noop); 101 | }); 102 | 103 | it("maxTime should be a valid number", () => { 104 | for (const r of ["ds", {}, () => {}]) { 105 | assert.throws( 106 | () => { 107 | bench.add("name", { minTime: r }, noop); 108 | }, 109 | { 110 | code: "ERR_INVALID_ARG_TYPE", 111 | }, 112 | ); 113 | } 114 | }); 115 | it("maxTime should be greater than minTime", () => { 116 | assert.throws( 117 | () => { 118 | bench.add("name", { maxTime: 0 }, noop); 119 | }, 120 | { 121 | code: "ERR_INVALID_ARG_VALUE", 122 | }, 123 | ); 124 | assert.throws( 125 | () => { 126 | bench.add("name", { maxTime: 0.1, minTime: 0.2 }, noop); 127 | }, 128 | { 129 | code: "ERR_INVALID_ARG_VALUE", 130 | }, 131 | ); 132 | // doesNotThrow 133 | bench.add("name", { minTime: 0.01, maxTime: 0.02 }, noop); 134 | }); 135 | 136 | it("fn should be a function", () => { 137 | for (const r of ["ds", {}, 42]) { 138 | assert.throws( 139 | () => { 140 | bench.add("name", {}, r); 141 | }, 142 | { 143 | code: "ERR_INVALID_ARG_TYPE", 144 | }, 145 | ); 146 | } 147 | // doesNotThrow 148 | bench.add("name", noop); 149 | }); 150 | 151 | it("repeatSuite should be a valid number", () => { 152 | for (const r of ["ds", {}, () => {}]) { 153 | assert.throws( 154 | () => { 155 | bench.add("name", { repeatSuite: r }, noop); 156 | }, 157 | { 158 | code: "ERR_INVALID_ARG_TYPE", 159 | }, 160 | ); 161 | } 162 | }); 163 | 164 | it("minSamples should be a valid number", () => { 165 | for (const r of ["ds", {}, () => {}]) { 166 | assert.throws( 167 | () => { 168 | bench.add("name", { minSamples: r }, noop); 169 | }, 170 | { 171 | code: "ERR_INVALID_ARG_TYPE", 172 | }, 173 | ); 174 | } 175 | }); 176 | }); 177 | }); 178 | 179 | describe("simple usage", async () => { 180 | const bench = new Suite({ reporter: noop }); 181 | bench 182 | .add("foo", async () => { 183 | await new Promise((resolve) => setTimeout(resolve, 50)); 184 | }) 185 | .add("bar", async () => { 186 | await new Promise((resolve) => setTimeout(resolve, 100)); 187 | }); 188 | 189 | const [bench1, bench2] = await bench.run(); 190 | 191 | it("benchmark name should be returned in results", () => { 192 | assert.strictEqual(bench1.name, "foo"); 193 | assert.strictEqual(bench2.name, "bar"); 194 | }); 195 | 196 | it("ops/sec should match the expected duration", () => { 197 | // 1000(ms)/50 = 20 + cost of creating promises 198 | assert.ok(bench1.opsSec > 18 && bench1.opsSec <= 20); 199 | // 1000(ms)/100 = 100 + cost of creating promises 200 | assert.ok(bench2.opsSec > 8 && bench2.opsSec <= 10); 201 | }); 202 | 203 | it("tasks should have at least 10 samples", () => { 204 | assert.ok(bench1.iterations >= 10); 205 | assert.ok(bench2.iterations >= 10); 206 | }); 207 | 208 | it("opsSecPerRun should exist when repeatSuite is 1", async () => { 209 | assert.ok( 210 | Array.isArray(bench1.opsSecPerRun), 211 | "opsSecPerRun should be an array", 212 | ); 213 | assert.ok( 214 | Array.isArray(bench2.opsSecPerRun), 215 | "opsSecPerRun should be an array", 216 | ); 217 | }); 218 | }); 219 | 220 | describe("repeat suite", async () => { 221 | const repeatCount = 3; 222 | const bench = new Suite({ reporter: noop }); 223 | bench.add("Repeat ops test", { repeatSuite: repeatCount }, () => { 224 | // Simple operation 225 | const x = 1 + 1; 226 | }); 227 | 228 | const results = await bench.run(); 229 | 230 | it("should include opsSecPerRun when repeatSuite is greater than 1", async () => { 231 | assert.strictEqual(results.length, 1); 232 | assert.ok(results[0].opsSec !== undefined, "opsSec should be defined"); 233 | assert.ok( 234 | Array.isArray(results[0].opsSecPerRun), 235 | "opsSecPerRun should be an array", 236 | ); 237 | assert.strictEqual( 238 | results[0].opsSecPerRun.length, 239 | repeatCount, 240 | `opsSecPerRun should have ${repeatCount} elements`, 241 | ); 242 | for (const opsSec of results[0].opsSecPerRun) { 243 | assert.ok( 244 | typeof opsSec === "number" && !Number.isNaN(opsSec), 245 | "each element should be a valid number", 246 | ); 247 | } 248 | }); 249 | }); 250 | 251 | describe("throws when a benchmark task throw", async () => { 252 | const bench = new Suite(); 253 | const err = new Error(); 254 | 255 | bench.add("error", () => { 256 | throw err; 257 | }); 258 | assert.rejects(() => bench.run()); 259 | }); 260 | 261 | describe("when no --allow-natives-syntax", async () => { 262 | it("should throw", () => { 263 | const file = path.join(__dirname, "fixtures", "bench.js"); 264 | const { status, stderr } = spawnSync(process.execPath, [file]); 265 | assert.strictEqual(status, 1); 266 | assert.match( 267 | stderr.toString(), 268 | /bench-node module must be run with --allow-natives-syntax/, 269 | ); 270 | }); 271 | }); 272 | 273 | todo("histogram values", async () => {}); 274 | -------------------------------------------------------------------------------- /test/env.js: -------------------------------------------------------------------------------- 1 | const { describe, it, before } = require("node:test"); 2 | const assert = require("node:assert"); 3 | const { Suite } = require("../lib"); 4 | const copyBench = require("./fixtures/copy"); 5 | const { managedBench, managedOptBench } = require("./fixtures/opt-managed"); 6 | 7 | function assertMinBenchmarkDifference( 8 | results, 9 | { percentageLimit, ciPercentageLimit }, 10 | ) { 11 | assertBenchmarkDifference(results, { 12 | percentageLimit, 13 | ciPercentageLimit, 14 | greaterThan: true, 15 | }); 16 | } 17 | 18 | function assertMaxBenchmarkDifference( 19 | results, 20 | { percentageLimit, ciPercentageLimit }, 21 | ) { 22 | assertBenchmarkDifference(results, { 23 | percentageLimit, 24 | ciPercentageLimit, 25 | greaterThan: false, 26 | }); 27 | } 28 | 29 | function assertBenchmarkDifference( 30 | results, 31 | { percentageLimit, ciPercentageLimit, greaterThan }, 32 | ) { 33 | for (let i = 0; i < results.length; i++) { 34 | for (let j = 0; j < results.length; j++) { 35 | if (i !== j) { 36 | const opsSec1 = results[i].opsSec; 37 | const opsSec2 = results[j].opsSec; 38 | 39 | // Calculate the percentage difference 40 | const difference = Math.abs(opsSec1 - opsSec2); 41 | const percentageDifference = 42 | (difference / Math.min(opsSec1, opsSec2)) * 100; 43 | 44 | // Check if the percentage difference is less than or equal to 10% 45 | if (process.env.CI) { 46 | // CI runs in a shared-env so the percentage of difference 47 | // must be greather there due to high variance of hardware 48 | assert.ok( 49 | greaterThan 50 | ? percentageDifference >= ciPercentageLimit 51 | : percentageDifference <= ciPercentageLimit, 52 | `"${results[i].name}" too different from "${results[j].name}" - ${percentageDifference} != ${ciPercentageLimit}`, 53 | ); 54 | } else { 55 | assert.ok( 56 | greaterThan 57 | ? percentageDifference >= percentageLimit 58 | : percentageDifference <= percentageLimit, 59 | `${results[i].name} too different from ${results[j].name} - ${percentageDifference} != ${percentageLimit}`, 60 | ); 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | describe("Same benchmark function", () => { 68 | let results; 69 | 70 | before(async () => { 71 | results = await copyBench.run(); 72 | }); 73 | 74 | it("must have a similar benchmark result", () => { 75 | assertMaxBenchmarkDifference(results, { 76 | percentageLimit: 10, 77 | ciPercentageLimit: 30, 78 | }); 79 | }); 80 | }); 81 | 82 | describe("Managed can be V8 optimized", () => { 83 | let optResults; 84 | let results; 85 | 86 | before(async () => { 87 | optResults = await managedOptBench.run(); 88 | results = await managedBench.run(); 89 | }); 90 | 91 | it("should be more than 50% different from unmanaged", () => { 92 | assertMinBenchmarkDifference(optResults, { 93 | percentageLimit: 50, 94 | ciPercentageLimit: 30, 95 | }); 96 | }); 97 | 98 | // it('should be similar when avoiding V8 optimizatio', () => { 99 | // assertBenchmarkDifference(results, 50, 30); 100 | // }); 101 | }); 102 | 103 | describe("Workers should have parallel context", () => { 104 | let results; 105 | before(async () => { 106 | const bench = new Suite({ 107 | reporter: () => {}, 108 | useWorkers: true, 109 | benchmarkMode: "ops", 110 | }); 111 | 112 | bench 113 | .add("Import with node: prefix", () => { 114 | return import("node:fs"); 115 | }) 116 | .add("Import without node: prefix", () => { 117 | return import("node:fs"); 118 | }); 119 | results = await bench.run(); 120 | }); 121 | 122 | it("should have a similar result as they will not share import.meta.cache", () => { 123 | assertMaxBenchmarkDifference(results, { 124 | percentageLimit: 10, 125 | ciPercentageLimit: 30, 126 | }); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /test/fixtures/bench.js: -------------------------------------------------------------------------------- 1 | const { Suite } = require("../../lib"); 2 | 3 | const suite = new Suite(); 4 | 5 | suite 6 | .add("empty", () => {}) 7 | .add("empty async", async () => {}) 8 | .run(); 9 | -------------------------------------------------------------------------------- /test/fixtures/copy.js: -------------------------------------------------------------------------------- 1 | const { Suite } = require("../../lib"); 2 | 3 | const suite = new Suite({ reporter: false }); 4 | 5 | suite 6 | .add("Using includes", () => { 7 | const text = 8 | "text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8"; 9 | const r = text.includes("application/json"); 10 | }) 11 | .add("Using includes 2", () => { 12 | const text = 13 | "text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8"; 14 | const r = text.includes("application/json"); 15 | }) 16 | .add("Using includes 3", () => { 17 | const text = 18 | "text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8"; 19 | const r = text.includes("application/json"); 20 | }); 21 | 22 | module.exports = suite; 23 | -------------------------------------------------------------------------------- /test/fixtures/opt-managed.js: -------------------------------------------------------------------------------- 1 | const { Suite } = require("../../lib"); 2 | const assert = require("node:assert"); 3 | 4 | const suite = new Suite({ reporter: false }); 5 | 6 | suite 7 | .add("Using includes", () => { 8 | const text = 9 | "text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8"; 10 | const r = text.includes("application/json"); 11 | assert.ok(r); 12 | }) 13 | .add("[Managed] Using includes", (timer) => { 14 | timer.start(); 15 | for (let i = 0; i < timer.count; i++) { 16 | const text = 17 | "text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8"; 18 | const r = text.includes("application/json"); 19 | assert.ok(r); 20 | } 21 | timer.end(timer.count); 22 | }); 23 | 24 | suite.run(); 25 | 26 | const optSuite = new Suite({ reporter: false }); 27 | 28 | optSuite 29 | .add("Using includes", () => { 30 | const text = 31 | "text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8"; 32 | const r = text.includes("application/json"); 33 | // assert.ok(r) 34 | }) 35 | .add("[Managed] Using includes", (timer) => { 36 | timer.start(); 37 | for (let i = 0; i < timer.count; i++) { 38 | const text = 39 | "text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8"; 40 | const r = text.includes("application/json"); 41 | // assert.ok(r) 42 | } 43 | timer.end(timer.count); 44 | }); 45 | 46 | module.exports = { 47 | managedBench: suite, 48 | managedOptBench: optSuite, 49 | }; 50 | -------------------------------------------------------------------------------- /test/managed.js: -------------------------------------------------------------------------------- 1 | const { describe, it } = require("node:test"); 2 | const assert = require("node:assert"); 3 | const { Suite } = require("../lib/index"); 4 | 5 | describe("managed benchmarks", async () => { 6 | it("should throw when timer.start isn't called", () => { 7 | const suite = new Suite({ reporter: () => {} }); 8 | assert.rejects( 9 | async () => { 10 | suite.add("managed", (timer) => {}); 11 | await suite.run(); 12 | }, 13 | { 14 | message: "You forgot to call .start()", 15 | }, 16 | ); 17 | }); 18 | 19 | it("should throw when timer.end isn't called", () => { 20 | const suite = new Suite({ reporter: () => {} }); 21 | assert.rejects( 22 | async () => { 23 | suite.add("managed", (timer) => { 24 | timer.start(); 25 | }); 26 | await suite.run(); 27 | }, 28 | { 29 | message: "You forgot to call .end(count)", 30 | }, 31 | ); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/plugin-api-doc.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { Suite } = require("../lib/index.js"); 3 | const { describe, it } = require("node:test"); 4 | const assert = require("node:assert"); 5 | 6 | class ExamplePlugin { 7 | #aggregation = {}; 8 | constructor() {} 9 | 10 | isSupported() { 11 | return true; 12 | } 13 | 14 | beforeClockTemplate() { 15 | return [`record("- evaluated beforeClockCode");`]; 16 | } 17 | 18 | afterClockTemplate({ context }) { 19 | return [ 20 | ` 21 | ${context}.example=1; 22 | record("- evaluated afterClockCode"); 23 | `, 24 | ]; 25 | } 26 | 27 | onCompleteBenchmark([time, iterations, results], { name }) { 28 | if (undefined === this.#aggregation[name]) { 29 | this.#aggregation[name] = 0; 30 | } 31 | this.#aggregation[name] += results.example; 32 | } 33 | 34 | toString() { 35 | return "ExamplePlugin"; 36 | } 37 | 38 | getReport(name) { 39 | return `examplePlugin report for ${name}`; 40 | } 41 | 42 | getResult(name) { 43 | return { 44 | examplePluginAggregation: this.#aggregation[name], 45 | }; 46 | } 47 | } 48 | 49 | describe("plugin API", async () => { 50 | const bench = new Suite({ 51 | reporter: () => {}, 52 | plugins: [captureAll(new ExamplePlugin())], 53 | }); 54 | bench.add("task1", async () => { 55 | record("- task1"); 56 | }); 57 | bench.add("task2", async () => { 58 | record("- task2"); 59 | }); 60 | const [bench1] = await bench.run(); 61 | 62 | it("matches method signatures", async () => { 63 | const recordedMethodSignatures = getSignatures(); 64 | assert.deepStrictEqual(recordedMethodSignatures, [ 65 | "afterClockTemplate({awaitOrEmpty, bench, context, timer})", 66 | "beforeClockTemplate({awaitOrEmpty, bench, context, timer})", 67 | "getReport(string)", 68 | "getResult(string)", 69 | "isSupported()", 70 | "onCompleteBenchmark([number, number, object], {fn, fnStr, hasArg, isAsync, maxTime, minSamples, minTime, name, plugins, repeatSuite})", 71 | "toJSON(string)", 72 | "toString()", 73 | ]); 74 | }); 75 | it("produces history", async () => { 76 | printExcerptFromHistory(); 77 | }); 78 | it("aggregates results", async () => { 79 | console.log("Benchmark results plugins field:", bench1.plugins); 80 | assert(bench1.plugins[0].result.examplePluginAggregation > 1); 81 | assert.strictEqual( 82 | bench1.plugins[0].report, 83 | "examplePlugin report for task1", 84 | ); 85 | }); 86 | }); 87 | 88 | // ============================================ 89 | // Utilities to capture the methods and history. 90 | // Moved down the file to keep the test code clean. Hoisting is how they're available. 91 | // No need to look at them, stop reading now, look at the test output instead. 92 | 93 | function record(name, args) { 94 | if (args && args.length) { 95 | history.push([name, 1, JSON.stringify(Array.from(args))]); 96 | } else { 97 | const last = history[history.length - 1]; 98 | if (last && last[0] === name) { 99 | last[1]++; 100 | } else { 101 | history.push([name, 1]); 102 | } 103 | } 104 | } 105 | 106 | function printExcerptFromHistory(n = 25) { 107 | const excerpt = [ 108 | ...history.slice(0, n), 109 | ["(... redacted for brevity ...)", 1], 110 | ...history.slice(-n), 111 | ] 112 | .map( 113 | ([name, count, args]) => 114 | `${name} ${count > 1 ? "x" + count : ""}${ 115 | args ? " with args: " + args : "" 116 | }`, 117 | ) 118 | .join("\n| "); 119 | console.log("+----------------------------------"); 120 | console.log("| Plugin lifecycle log:"); 121 | console.log("+----------------------------------"); 122 | console.log("|", excerpt); 123 | console.log("+----------------------------------"); 124 | } 125 | function getSignatures() { 126 | return Object.entries(API) 127 | .map( 128 | ([name, args]) => 129 | `${name}(${Array.from(args) 130 | .map((a) => { 131 | if (!a) return ""; 132 | if (Array.isArray(a)) 133 | return "[" + a.map((a) => typeof a).join(", ") + "]"; 134 | if (typeof a === "object") 135 | return "{" + Object.keys(a).sort().join(", ") + "}"; 136 | return typeof a; 137 | }) 138 | .join(", ")})`, 139 | ) 140 | .sort(); 141 | } 142 | var history, API; 143 | function captureAll(pluginInstance) { 144 | history = []; 145 | API = {}; 146 | globalThis.record = record; // make record available in the tasks 147 | 148 | return new Proxy(pluginInstance, { 149 | get(target, prop, receiver) { 150 | return function (...args) { 151 | record(prop, args); 152 | API[prop] = args; 153 | if (typeof target[prop] === "function") { 154 | return target[prop].apply(target, args); 155 | } 156 | }; 157 | }, 158 | has(target, prop) { 159 | return true; 160 | }, 161 | }); 162 | } 163 | -------------------------------------------------------------------------------- /test/plugins.js: -------------------------------------------------------------------------------- 1 | const { 2 | Suite, 3 | V8NeverOptimizePlugin, 4 | V8GetOptimizationStatus, 5 | V8OptimizeOnNextCallPlugin, 6 | } = require("../lib/index"); 7 | const { describe, it } = require("node:test"); 8 | const assert = require("node:assert"); 9 | 10 | class InvalidPlugin {} 11 | 12 | class ValidPlugin { 13 | toString() { 14 | return ""; 15 | } 16 | isSupported() { 17 | return true; 18 | } 19 | } 20 | 21 | describe("Plugins validation", () => { 22 | it("should be an object with expected methods", () => { 23 | for (const r of [1, "ds", {}]) { 24 | assert.throws( 25 | () => { 26 | new Suite({ plugins: r }); 27 | }, 28 | { 29 | code: "ERR_INVALID_ARG_TYPE", 30 | }, 31 | ); 32 | } 33 | assert.throws( 34 | () => { 35 | new Suite({ plugins: [new InvalidPlugin()] }); 36 | }, 37 | { 38 | code: "ERR_INVALID_ARG_TYPE", 39 | }, 40 | ); 41 | // doesNotThrow 42 | new Suite({ plugins: [new ValidPlugin()] }); 43 | }); 44 | 45 | it("beforeClockTemplate should return an array", () => { 46 | class InvalidPlugin2 { 47 | beforeClockTemplate() { 48 | return "error"; 49 | } 50 | toString() { 51 | return ""; 52 | } 53 | isSupported() { 54 | return true; 55 | } 56 | } 57 | 58 | assert.throws( 59 | () => { 60 | new Suite({ plugins: [new InvalidPlugin2()] }); 61 | }, 62 | { 63 | code: "ERR_INVALID_ARG_TYPE", 64 | }, 65 | ); 66 | }); 67 | 68 | it("afterClockTemplate should return an array", () => { 69 | class InvalidPlugin2 { 70 | beforeClockTemplate() { 71 | return [""]; 72 | } 73 | afterClockTemplate() { 74 | return "error"; 75 | } 76 | toString() { 77 | return ""; 78 | } 79 | isSupported() { 80 | return true; 81 | } 82 | } 83 | 84 | assert.throws( 85 | () => { 86 | new Suite({ plugins: [new InvalidPlugin2()] }); 87 | }, 88 | { 89 | code: "ERR_INVALID_ARG_TYPE", 90 | }, 91 | ); 92 | }); 93 | }); 94 | 95 | describe("Official plugins validation", () => { 96 | it("V8NeverOptimizePlugin validation", () => { 97 | const bench = new Suite({ 98 | plugins: [new V8NeverOptimizePlugin()], 99 | }); 100 | assert.ok(bench); 101 | }); 102 | it("V8GetOptimizationStatus validation", () => { 103 | const bench = new Suite({ 104 | plugins: [new V8GetOptimizationStatus()], 105 | }); 106 | assert.ok(bench); 107 | }); 108 | it("V8OptimizeOnNextCallPlugin validation", () => { 109 | const bench = new Suite({ 110 | plugins: [new V8OptimizeOnNextCallPlugin()], 111 | }); 112 | assert.ok(bench); 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /test/reporter.js: -------------------------------------------------------------------------------- 1 | const { describe, it, before } = require("node:test"); 2 | const assert = require("node:assert"); 3 | const fs = require("node:fs"); 4 | 5 | const { 6 | Suite, 7 | chartReport, 8 | htmlReport, 9 | jsonReport, 10 | csvReport, 11 | } = require("../lib"); 12 | 13 | describe("chartReport outputs benchmark results as a bar chart", async (t) => { 14 | let output = ""; 15 | 16 | before(async () => { 17 | const originalStdoutWrite = process.stdout.write; 18 | process.stdout.write = (data) => { 19 | output += data; 20 | }; 21 | 22 | const suite = new Suite({ 23 | reporter: chartReport, 24 | }); 25 | 26 | suite 27 | .add("single with matcher", () => { 28 | const pattern = /[123]/g; 29 | const replacements = { 1: "a", 2: "b", 3: "c" }; 30 | const subject = "123123123123123123123123123123123123123123123123"; 31 | const r = subject.replace(pattern, (m) => replacements[m]); 32 | assert.ok(r); 33 | }) 34 | .add("multiple replaces", () => { 35 | const subject = "123123123123123123123123123123123123123123123123"; 36 | const r = subject 37 | .replace(/1/g, "a") 38 | .replace(/2/g, "b") 39 | .replace(/3/g, "c"); 40 | assert.ok(r); 41 | }); 42 | await suite.run(); 43 | 44 | process.stdout.write = originalStdoutWrite; 45 | }); 46 | 47 | it("should include bar chart chars", () => { 48 | assert.ok(output.includes("█")); 49 | }); 50 | 51 | it("should include ops/sec", () => { 52 | assert.ok(output.includes("ops/sec")); 53 | }); 54 | 55 | it("should include benchmark names", () => { 56 | assert.ok(output.includes("single with matcher")); 57 | assert.ok(output.includes("multiple replaces")); 58 | }); 59 | 60 | it("should include sample count", () => { 61 | assert.ok(output.includes("samples")); 62 | }); 63 | 64 | it("should include Node.js version", () => { 65 | const regex = /Node\.js version: v\d+\.\d+\.\d+/; 66 | 67 | assert.ok(output.match(regex)); 68 | }); 69 | }); 70 | 71 | describe("htmlReport should create a file", async (t) => { 72 | let output = ""; 73 | let htmlName = ""; 74 | let htmlContent = ""; 75 | 76 | before(async () => { 77 | const originalStdoutWrite = process.stdout.write; 78 | const originalWriteFileSync = fs.writeFileSync; 79 | 80 | fs.writeFileSync = (name, content) => { 81 | htmlName = name; 82 | htmlContent = content; 83 | }; 84 | 85 | process.stdout.write = (data) => { 86 | output += data; 87 | }; 88 | 89 | const suite = new Suite({ 90 | reporter: htmlReport, 91 | }); 92 | 93 | suite 94 | .add("single with matcher", () => { 95 | const pattern = /[123]/g; 96 | const replacements = { 1: "a", 2: "b", 3: "c" }; 97 | const subject = "123123123123123123123123123123123123123123123123"; 98 | const r = subject.replace(pattern, (m) => replacements[m]); 99 | assert.ok(r); 100 | }) 101 | .add("Multiple replaces", () => { 102 | const subject = "123123123123123123123123123123123123123123123123"; 103 | const r = subject 104 | .replace(/1/g, "a") 105 | .replace(/2/g, "b") 106 | .replace(/3/g, "c"); 107 | assert.ok(r); 108 | }); 109 | await suite.run(); 110 | 111 | fs.writeFileSync = originalWriteFileSync; 112 | process.stdout.write = originalStdoutWrite; 113 | }); 114 | 115 | it("should print that a HTML file has been generated", () => { 116 | assert.ok(output.includes("HTML file has been generated")); 117 | }); 118 | 119 | it("htmlName should be result.html", () => { 120 | assert.strictEqual(htmlName, "result.html"); 121 | }); 122 | 123 | it("htmlContent should not be empty", () => { 124 | assert.ok(htmlContent.length > 100); 125 | }); 126 | 127 | it("htmlContent bench suite should be used as class name", () => { 128 | assert.ok(htmlContent.includes("circle-Multiple-replaces")); 129 | assert.ok(htmlContent.includes("circle-single-with-matcher")); 130 | }); 131 | 132 | it("htmlContent should not contain replace tags {{}}", () => { 133 | assert.ok(htmlContent.includes("{{") === false); 134 | assert.ok(htmlContent.includes("}}") === false); 135 | }); 136 | }); 137 | 138 | describe("custom reporter should have access to histogram data", async () => { 139 | let output = ""; 140 | 141 | before(async () => { 142 | const originalStdoutWrite = process.stdout.write; 143 | process.stdout.write = (data) => { 144 | output += data; 145 | }; 146 | 147 | function customReporter(results) { 148 | const json = []; 149 | for (const result of results) { 150 | // Calculate the median of result.histogram.sampleData: 151 | const sorted = [...result.histogram.sampleData].sort((a, b) => a - b); 152 | const median = 153 | sorted.length % 2 === 0 154 | ? (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2 155 | : sorted[Math.floor(sorted.length / 2)]; 156 | 157 | json.push({ 158 | name: result.name, 159 | low: result.histogram.sampleData.filter((v) => v <= median), 160 | high: result.histogram.sampleData.filter((v) => v >= median), 161 | median, 162 | }); 163 | } 164 | console.log(JSON.stringify(json, null, 2)); 165 | } 166 | 167 | // Create a new Suite with the custom reporter 168 | const suite = new Suite({ 169 | reporter: customReporter, 170 | }); 171 | 172 | suite 173 | .add("single with matcher", () => { 174 | const pattern = /[123]/g; 175 | const replacements = { 1: "a", 2: "b", 3: "c" }; 176 | const subject = "123123123123123123123123123123123123123123123123"; 177 | const r = subject.replace(pattern, (m) => replacements[m]); 178 | assert.ok(r); 179 | }) 180 | .add("Multiple replaces", () => { 181 | const subject = "123123123123123123123123123123123123123123123123"; 182 | const r = subject 183 | .replace(/1/g, "a") 184 | .replace(/2/g, "b") 185 | .replace(/3/g, "c"); 186 | assert.ok(r); 187 | }); 188 | 189 | // Run the suite 190 | await suite.run(); 191 | 192 | // Restore stdout 193 | process.stdout.write = originalStdoutWrite; 194 | }); 195 | 196 | it("should print valid JSON", () => { 197 | // Verify if the output can be parsed as JSON 198 | let data; 199 | try { 200 | data = JSON.parse(output); 201 | } catch (err) { 202 | assert.fail(`Output is not valid JSON: ${err.message}`); 203 | } 204 | 205 | assert.ok(Array.isArray(data), "Output should be an array of results"); 206 | }); 207 | 208 | it("should calculate median correctly", () => { 209 | const data = JSON.parse(output); 210 | for (const result of data) { 211 | assert.strictEqual( 212 | result.low.length, 213 | result.high.length, 214 | "Same number of samples above and below median", 215 | ); 216 | } 217 | }); 218 | }); 219 | 220 | describe("jsonReport should produce valid JSON output", async () => { 221 | let output = ""; 222 | 223 | before(async () => { 224 | const originalStdoutWrite = process.stdout.write; 225 | process.stdout.write = (data) => { 226 | output += data; 227 | }; 228 | 229 | // Create a new Suite with the JSON reporter 230 | const suite = new Suite({ 231 | reporter: jsonReport, 232 | }); 233 | 234 | suite 235 | .add("single with matcher", () => { 236 | const pattern = /[123]/g; 237 | const replacements = { 1: "a", 2: "b", 3: "c" }; 238 | const subject = "123123123123123123123123123123123123123123123123"; 239 | const r = subject.replace(pattern, (m) => replacements[m]); 240 | assert.ok(r); 241 | }) 242 | .add("Multiple replaces", () => { 243 | const subject = "123123123123123123123123123123123123123123123123"; 244 | const r = subject 245 | .replace(/1/g, "a") 246 | .replace(/2/g, "b") 247 | .replace(/3/g, "c"); 248 | assert.ok(r); 249 | }); 250 | 251 | // Run the suite 252 | await suite.run(); 253 | 254 | // Restore stdout 255 | process.stdout.write = originalStdoutWrite; 256 | }); 257 | 258 | it("should print valid JSON", () => { 259 | // Verify if the output can be parsed as JSON 260 | let data; 261 | try { 262 | data = JSON.parse(output); 263 | } catch (err) { 264 | assert.fail(`Output is not valid JSON: ${err.message}`); 265 | } 266 | 267 | assert.ok(Array.isArray(data), "Output should be an array of results"); 268 | }); 269 | 270 | it("should contain the required benchmark fields", () => { 271 | const data = JSON.parse(output); 272 | 273 | // We expect the two benchmarks we added: 'single with matcher' and 'Multiple replaces' 274 | assert.strictEqual(data.length, 2, "Should have results for 2 benchmarks"); 275 | 276 | for (const entry of data) { 277 | // Ensure each entry has expected keys 278 | assert.ok(typeof entry.name === "string", "name should be a string"); 279 | assert.ok(typeof entry.opsSec === "number", "opsSec should be a number"); 280 | assert.ok( 281 | typeof entry.runsSampled === "number", 282 | "runsSampled should be a number", 283 | ); 284 | assert.ok( 285 | typeof entry.min === "string", 286 | "min should be a string (formatted time)", 287 | ); 288 | assert.ok( 289 | typeof entry.max === "string", 290 | "max should be a string (formatted time)", 291 | ); 292 | assert.ok(Array.isArray(entry.plugins), "plugins should be an array"); 293 | } 294 | }); 295 | }); 296 | 297 | describe("csvReport", () => { 298 | it("should generate valid CSV output", async (t) => { 299 | const fn = t.mock.method(process.stdout, "write"); 300 | 301 | // noop 302 | fn.mock.mockImplementation(() => {}); 303 | 304 | csvReport([ 305 | { 306 | opsSec: 749625.5652171721, 307 | iterations: 374813, 308 | histogram: { 309 | samples: 10, 310 | min: 1322.2615873857162, 311 | max: 1345.4275821344213, 312 | }, 313 | name: "single with matcher", 314 | plugins: [ 315 | { 316 | name: "V8NeverOptimizePlugin", 317 | result: "enabled", 318 | report: "v8-never-optimize=true", 319 | }, 320 | ], 321 | }, 322 | { 323 | opsSec: 634284.7401772924, 324 | iterations: 317148, 325 | histogram: { 326 | samples: 11, 327 | min: 1552.562466504839, 328 | max: 1612.7852084972462, 329 | }, 330 | name: "Multiple replaces", 331 | plugins: [ 332 | { 333 | name: "V8NeverOptimizePlugin", 334 | result: "enabled", 335 | report: "v8-never-optimize=true", 336 | }, 337 | ], 338 | }, 339 | ]); 340 | 341 | const callArgs = process.stdout.write.mock.calls.map( 342 | (call) => call.arguments[0], 343 | ); 344 | 345 | assert.strictEqual(process.stdout.write.mock.callCount(), 13); 346 | assert.deepStrictEqual(callArgs, [ 347 | "name,ops/sec,samples,plugins,min,max\n", 348 | "single with matcher,", 349 | '"749,626",', 350 | "10,", 351 | '"v8-never-optimize=true",', 352 | "1.32us,", 353 | "1.35us\n", 354 | "Multiple replaces,", 355 | '"634,285",', 356 | "11,", 357 | '"v8-never-optimize=true",', 358 | "1.55us,", 359 | "1.61us\n", 360 | ]); 361 | }); 362 | }); 363 | -------------------------------------------------------------------------------- /test/time-mode.js: -------------------------------------------------------------------------------- 1 | const { Suite } = require("../lib/index"); 2 | const { describe, it } = require("node:test"); 3 | const assert = require("node:assert"); 4 | 5 | // Helper function to create a controlled delay 6 | const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); 7 | 8 | describe("Time-based Benchmarking", () => { 9 | it("should run in 'ops' mode by default", async () => { 10 | const suite = new Suite({ reporter: false }); 11 | 12 | suite.add("Default mode test", () => { 13 | // Simple operation 14 | Math.sqrt(Math.random()); 15 | }); 16 | 17 | const results = await suite.run(); 18 | 19 | assert.strictEqual(results.length, 1); 20 | assert.ok(results[0].opsSec !== undefined, "opsSec should be defined"); 21 | assert.ok( 22 | results[0].totalTime === undefined, 23 | "totalTime should not be defined", 24 | ); 25 | assert.ok( 26 | results[0].histogram.samples > 1, 27 | "Should have multiple samples in ops mode", 28 | ); 29 | }); 30 | 31 | it("should run in 'time' mode when specified at suite level", async () => { 32 | const suite = new Suite({ 33 | reporter: false, 34 | benchmarkMode: "time", 35 | }); 36 | 37 | const delayTime = 50; // 50ms delay 38 | 39 | suite.add("Time mode test", async () => { 40 | await delay(delayTime); 41 | }); 42 | 43 | const results = await suite.run(); 44 | 45 | assert.strictEqual(results.length, 1); 46 | assert.ok( 47 | results[0].totalTime !== undefined, 48 | "totalTime should be defined", 49 | ); 50 | assert.ok(results[0].opsSec === undefined, "opsSec should not be defined"); 51 | 52 | // Verify the time is approximately correct (allow for some overhead) 53 | const measuredTime = results[0].totalTime * 1000; // Convert to ms 54 | assert.ok( 55 | measuredTime >= delayTime && measuredTime < delayTime + 20, 56 | `Measured time (${measuredTime}ms) should be close to expected delay (${delayTime}ms)`, 57 | ); 58 | 59 | // Verify there's exactly 1 sample in time mode 60 | assert.strictEqual( 61 | results[0].histogram.samples, 62 | 1, 63 | "Should have exactly 1 sample in time mode", 64 | ); 65 | }); 66 | 67 | it("should average results across repeatSuite in time mode", async () => { 68 | const suite = new Suite({ 69 | reporter: false, 70 | benchmarkMode: "time", 71 | }); 72 | 73 | const repeatCount = 5; 74 | 75 | // A very fast operation that should be consistent 76 | suite.add("Repeat time test", { repeatSuite: repeatCount }, () => { 77 | // Simple operation 78 | const x = 1 + 1; 79 | }); 80 | 81 | const results = await suite.run(); 82 | 83 | assert.strictEqual(results.length, 1); 84 | assert.ok( 85 | results[0].totalTime !== undefined, 86 | "totalTime should be defined", 87 | ); 88 | 89 | // Verify the number of samples matches repeatSuite 90 | assert.strictEqual( 91 | results[0].histogram.samples, 92 | repeatCount, 93 | `Should have exactly ${repeatCount} samples with repeatSuite=${repeatCount}`, 94 | ); 95 | }); 96 | 97 | it("should not mix modes within the same suite", async () => { 98 | // This test verifies that benchmarkMode is a suite-level setting 99 | // and cannot be overridden at the benchmark level 100 | 101 | const opsSuite = new Suite({ reporter: false }); 102 | const timeSuite = new Suite({ reporter: false, benchmarkMode: "time" }); 103 | 104 | // Add benchmarks to both suites 105 | opsSuite.add("Ops benchmark", () => Math.random()); 106 | timeSuite.add("Time benchmark", () => Math.random()); 107 | 108 | // Run both suites 109 | const opsResults = await opsSuite.run(); 110 | const timeResults = await timeSuite.run(); 111 | 112 | // Verify ops suite reports ops/sec 113 | assert.ok( 114 | opsResults[0].opsSec !== undefined, 115 | "opsSec should be defined for ops suite", 116 | ); 117 | assert.ok( 118 | opsResults[0].totalTime === undefined, 119 | "totalTime should not be defined for ops suite", 120 | ); 121 | 122 | // Verify time suite reports totalTime 123 | assert.ok( 124 | timeResults[0].totalTime !== undefined, 125 | "totalTime should be defined for time suite", 126 | ); 127 | assert.ok( 128 | timeResults[0].opsSec === undefined, 129 | "opsSec should not be defined for time suite", 130 | ); 131 | }); 132 | 133 | it("should validate benchmarkMode option", () => { 134 | // Valid modes 135 | assert.doesNotThrow(() => { 136 | new Suite({ benchmarkMode: "ops" }); 137 | new Suite({ benchmarkMode: "time" }); 138 | }); 139 | 140 | // Invalid modes 141 | assert.throws(() => new Suite({ benchmarkMode: "invalid" }), { 142 | code: "ERR_INVALID_ARG_VALUE", 143 | }); 144 | 145 | assert.throws(() => new Suite({ benchmarkMode: 123 }), { 146 | code: "ERR_INVALID_ARG_TYPE", 147 | }); 148 | }); 149 | }); 150 | -------------------------------------------------------------------------------- /test/worker.js: -------------------------------------------------------------------------------- 1 | const workerThreads = require("node:worker_threads"); 2 | const { describe, it, before, after, mock } = require("node:test"); 3 | const assert = require("node:assert"); 4 | 5 | function noop() {} 6 | 7 | describe("Using worker_threads", () => { 8 | before(async () => { 9 | mock.method(workerThreads, "Worker"); 10 | 11 | const { Suite } = require("../lib/index"); 12 | 13 | const bench = new Suite({ 14 | reporter: noop, 15 | useWorkers: true, 16 | }); 17 | 18 | bench 19 | .add("Import with node: prefix", () => { 20 | return import("node:fs"); 21 | }) 22 | .add("Import without node: prefix", () => { 23 | return import("node:fs"); 24 | }); 25 | await bench.run(); 26 | }); 27 | 28 | after(() => { 29 | mock.restoreAll(); 30 | }); 31 | 32 | it("should create a new Worker 2 times", () => { 33 | assert.strictEqual(workerThreads.Worker.mock.calls.length, 2); 34 | }); 35 | }); 36 | --------------------------------------------------------------------------------