├── .clean-publish ├── .commitlintrc.json ├── .editorconfig ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── cr.yml │ ├── qa.yml │ ├── release.yml │ └── size-limit.yml ├── .gitignore ├── .nano-staged.json ├── .simple-git-hooks.json ├── .size-limit.json ├── AUTHORS ├── LICENSE ├── README.md ├── banner.svg ├── docs ├── .nojekyll ├── assets │ ├── hierarchy.js │ ├── highlight.css │ ├── icons.js │ ├── icons.svg │ ├── main.js │ ├── navigation.js │ ├── search.js │ └── style.css ├── classes │ ├── Bench.html │ └── Task.html ├── enums │ └── JSRuntime.html ├── functions │ ├── hrtimeNow.html │ └── nToMs.html ├── hierarchy.html ├── index.html ├── interfaces │ ├── BenchEventsMap.html │ ├── BenchOptions.html │ ├── FnOptions.html │ ├── Statistics.html │ ├── TaskEventsMap.html │ └── TaskResult.html ├── types │ ├── BenchEvent.html │ ├── BenchEvents.html │ ├── EventListener.html │ ├── Fn.html │ ├── FnHook.html │ ├── Hook.html │ └── TaskEvents.html └── variables │ └── now.html ├── eslint.config.js ├── examples ├── package.json ├── src │ ├── simple-bun.ts │ ├── simple-gc.ts │ └── simple.ts └── tsconfig.json ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── src ├── bench.ts ├── constants.ts ├── event.ts ├── index.ts ├── task.ts ├── types.ts └── utils.ts ├── test ├── index.test.ts ├── isFnAsyncResource.test.ts ├── sequential.test.ts └── utils.test.ts ├── tsconfig.json ├── tsdown.config.ts └── typedoc.json /.clean-publish: -------------------------------------------------------------------------------- 1 | { 2 | "withoutPublish": true, 3 | "tempDir": "package" 4 | } 5 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { "extends": ["@commitlint/config-conventional"] } 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | 11 | [*.{js,jsx,cjs,mjs,ts,tsx}] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | polar: tinylibs 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'daily' 7 | labels: 8 | - 'github-actions' 9 | - package-ecosystem: 'npm' 10 | directories: 11 | - '/' 12 | schedule: 13 | interval: 'daily' 14 | groups: 15 | regular: 16 | update-types: 17 | - 'patch' 18 | - 'minor' 19 | exclude-patterns: 20 | - 'typescript' 21 | typescript: 22 | update-types: 23 | - 'patch' 24 | - 'minor' 25 | - 'major' 26 | patterns: 27 | - 'typescript' 28 | typescript-eslint: 29 | update-types: 30 | - 'major' 31 | patterns: 32 | - 'typescript-eslint' 33 | - '@typescript-eslint/*' 34 | size-limit: 35 | update-types: 36 | - 'major' 37 | patterns: 38 | - 'size-limit' 39 | - '@size-limit/*' 40 | eslint: 41 | update-types: 42 | - 'major' 43 | patterns: 44 | - 'eslint' 45 | - '@eslint/*' 46 | labels: 47 | - 'dependencies' 48 | versioning-strategy: increase 49 | -------------------------------------------------------------------------------- /.github/workflows/cr.yml: -------------------------------------------------------------------------------- 1 | name: Publish Any Commit 2 | on: 3 | pull_request: 4 | types: [opened, synchronize, reopened] 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | if: github.repository == 'tinylibs/tinybench' 10 | 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | 15 | - name: Install pnpm 16 | uses: pnpm/action-setup@v4 17 | 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: 22 21 | cache: pnpm 22 | 23 | - name: Install dependencies 24 | run: pnpm install --frozen-lockfile 25 | 26 | - name: Build 27 | run: pnpm build 28 | 29 | - run: pnpx pkg-pr-new publish 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | -------------------------------------------------------------------------------- /.github/workflows/qa.yml: -------------------------------------------------------------------------------- 1 | name: QA 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | types: [opened, synchronize, reopened] 7 | merge_group: 8 | branches: [main] 9 | 10 | jobs: 11 | qa: 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: 16 | - ubuntu-latest 17 | - macos-latest 18 | - windows-latest 19 | node-version: 20 | - 18 21 | - 20 22 | - 22 23 | - latest 24 | name: Node.js ${{ matrix.node-version }} QA on ${{ matrix.os }} 25 | steps: 26 | - uses: actions/checkout@v4 27 | 28 | - name: Install pnpm 29 | uses: pnpm/action-setup@v4 30 | 31 | - name: Setup node 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version: ${{ matrix.node-version }} 35 | cache: pnpm 36 | 37 | - name: Setup Bun 38 | uses: oven-sh/setup-bun@v2 39 | with: 40 | bun-version: latest 41 | 42 | - name: Install dependencies 43 | run: pnpm install --frozen-lockfile 44 | 45 | - name: Build 46 | run: pnpm build 47 | 48 | - name: Type check 49 | if: matrix.node-version == 22 50 | run: pnpm typecheck 51 | 52 | - name: Lint 53 | if: matrix.node-version == 22 54 | run: pnpm lint 55 | 56 | - name: Run unit tests 57 | run: pnpm test 58 | env: 59 | FORCE_COLOR: 2 60 | 61 | - name: Run examples 62 | run: cd examples && pnpm all 63 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | build-release: 12 | runs-on: ubuntu-latest 13 | if: github.repository == 'tinylibs/tinybench' 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - uses: pnpm/action-setup@v4 18 | 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: 22 22 | cache: pnpm 23 | 24 | - uses: oven-sh/setup-bun@v2 25 | with: 26 | bun-version: latest 27 | 28 | - name: Install dependencies 29 | run: pnpm install --ignore-scripts --frozen-lockfile 30 | 31 | - name: Build 32 | run: pnpm build 33 | 34 | - name: Type check 35 | run: pnpm typecheck 36 | 37 | - name: Lint 38 | run: pnpm lint 39 | 40 | - name: Run unit tests 41 | run: pnpm test 42 | env: 43 | FORCE_COLOR: 2 44 | 45 | - name: Run examples 46 | run: cd examples && pnpm all 47 | publish-npm: 48 | runs-on: ubuntu-latest 49 | needs: build-release 50 | steps: 51 | - uses: actions/checkout@v4 52 | 53 | - uses: pnpm/action-setup@v4 54 | 55 | - uses: actions/setup-node@v4 56 | with: 57 | node-version: 22 58 | registry-url: https://registry.npmjs.org/ 59 | cache: pnpm 60 | 61 | - name: Install dependencies 62 | run: pnpm install --ignore-scripts --frozen-lockfile 63 | 64 | - name: Read package.json version 65 | id: package-version 66 | uses: jaywcjlove/github-action-package@main 67 | 68 | - name: Publish release 69 | run: pnpm publish --no-git-checks 70 | if: ${{ contains(steps.package-version.outputs.version, '-') == false }} 71 | env: 72 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 73 | 74 | - name: Publish release candidate 75 | if: ${{ contains(steps.package-version.outputs.version, '-rc') == true }} 76 | run: pnpm publish --no-git-checks --tag next 77 | env: 78 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 79 | 80 | - name: Publish beta release 81 | if: ${{ contains(steps.package-version.outputs.version, '-beta') == true }} 82 | run: pnpm publish --no-git-checks --tag beta 83 | env: 84 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 85 | 86 | - name: Publish alpha release 87 | if: ${{ contains(steps.package-version.outputs.version, '-alpha') == true }} 88 | run: pnpm publish --no-git-checks --tag alpha 89 | env: 90 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 91 | changelog: 92 | runs-on: ubuntu-latest 93 | needs: publish-npm 94 | permissions: 95 | contents: write 96 | steps: 97 | - uses: actions/checkout@v4 98 | with: 99 | fetch-depth: 0 100 | 101 | - uses: actions/setup-node@v4 102 | with: 103 | node-version: 22 104 | 105 | - run: npx changelogithub 106 | env: 107 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 108 | publish-documentation: 109 | runs-on: ubuntu-latest 110 | needs: changelog 111 | 112 | permissions: 113 | contents: write 114 | 115 | steps: 116 | - name: Checkout 117 | uses: actions/checkout@v4 118 | with: 119 | ref: ${{ github.event.repository.default_branch }} 120 | 121 | - name: Setup pnpm 122 | uses: pnpm/action-setup@v4 123 | 124 | - name: Setup Node.js 125 | uses: actions/setup-node@v4 126 | with: 127 | node-version: 22 128 | cache: pnpm 129 | 130 | - name: Generate documentation 131 | run: | 132 | pnpm install --ignore-scripts --frozen-lockfile 133 | pnpm typedoc 134 | 135 | - name: Commit and push changes 136 | if: github.ref == 'refs/heads/${{ github.event.repository.default_branch }}' 137 | env: 138 | COMMIT_MESSAGE: 'docs: publish documentation' 139 | COMMIT_AUTHOR: Documentation Bot 140 | COMMIT_EMAIL: documentation-bot@users.noreply.github.com 141 | run: | 142 | git config --local user.name "${{ env.COMMIT_AUTHOR }}" 143 | git config --local user.email "${{ env.COMMIT_EMAIL }}" 144 | git pull 145 | git add ./docs 146 | git commit -a -m "${{ env.COMMIT_MESSAGE }}" 147 | git push 148 | -------------------------------------------------------------------------------- /.github/workflows/size-limit.yml: -------------------------------------------------------------------------------- 1 | name: 'size' 2 | on: 3 | pull_request: 4 | types: [opened, synchronize, reopened] 5 | 6 | jobs: 7 | size: 8 | runs-on: ubuntu-latest 9 | env: 10 | CI_JOB_NUMBER: 1 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: Install pnpm 15 | uses: pnpm/action-setup@v4 16 | 17 | - uses: andresz1/size-limit-action@v1 18 | with: 19 | github_token: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | .pnp 4 | .pnp.js 5 | 6 | # testing 7 | coverage 8 | 9 | # build 10 | dist 11 | build 12 | package 13 | 14 | # dotenv environment variables file 15 | .env 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | # logs 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # cache 27 | .eslintcache 28 | 29 | # jetbrains 30 | .idea 31 | 32 | # misc 33 | .DS_Store 34 | .orig 35 | -------------------------------------------------------------------------------- /.nano-staged.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.{ts,cts,mts,js,cjs,mjs}": ["eslint --cache --fix"] 3 | } 4 | -------------------------------------------------------------------------------- /.simple-git-hooks.json: -------------------------------------------------------------------------------- 1 | { 2 | "commit-msg": "pnpm exec commitlint --edit \"$1\"", 3 | "pre-commit": "pnpm exec nano-staged" 4 | } 5 | -------------------------------------------------------------------------------- /.size-limit.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "path": "dist/index.js", 4 | "limit": "12 kB" 5 | } 6 | ] 7 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | M. Bagher Abiat (https://github.com/aslemammad) 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Tinylibs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tinybench 🔎 2 | 3 | [![CI](https://github.com/tinylibs/tinybench/actions/workflows/qa.yml/badge.svg?branch=main)](https://github.com/tinylibs/tinybench/actions/workflows/qa.yml) 4 | [![NPM version](https://badgen.net/npm/v/tinybench?icon=npm)](https://www.npmjs.com/package/tinybench) 5 | [![Discord](https://badgen.net/discord/online-members/c3UUYNcHrU?icon=discord&label=discord&color=green)](https://discord.gg/c3UUYNcHrU) 6 | [![neostandard Javascript Code Style]()](https://github.com/neostandard/neostandard) 7 | 8 | Benchmark your code easily with Tinybench, a simple, tiny and light-weight `10KB` (`2KB` minified and gzipped) benchmarking library! 9 | You can run your benchmarks in multiple JavaScript runtimes, Tinybench is completely based on the Web APIs with proper timing using 10 | `process.hrtime` or `performance.now`. 11 | 12 | - Accurate and precise timing based on the environment 13 | - Statistically analyzed latency and throughput values: standard deviation, margin of error, variance, percentiles, etc. 14 | - Concurrency support 15 | - `Event` and `EventTarget` compatible events 16 | - No dependencies 17 | 18 | _In case you need more tiny libraries like tinypool or tinyspy, please consider submitting an [RFC](https://github.com/tinylibs/rfcs)_ 19 | 20 | ## Installing 21 | 22 | ```shell 23 | $ npm install -D tinybench 24 | ``` 25 | 26 | ## Usage 27 | 28 | You can start benchmarking by instantiating the `Bench` class and adding benchmark tasks to it. 29 | 30 | ```js 31 | import { Bench } from 'tinybench' 32 | 33 | const bench = new Bench({ name: 'simple benchmark', time: 100 }) 34 | 35 | bench 36 | .add('faster task', () => { 37 | console.log('I am faster') 38 | }) 39 | .add('slower task', async () => { 40 | await new Promise(resolve => setTimeout(resolve, 1)) // we wait 1ms :) 41 | console.log('I am slower') 42 | }) 43 | 44 | await bench.run() 45 | 46 | console.log(bench.name) 47 | console.table(bench.table()) 48 | 49 | // Output: 50 | // simple benchmark 51 | // ┌─────────┬───────────────┬───────────────────┬───────────────────────┬────────────────────────┬────────────────────────┬─────────┐ 52 | // │ (index) │ Task name │ Latency avg (ns) │ Latency med (ns) │ Throughput avg (ops/s) │ Throughput med (ops/s) │ Samples │ 53 | // ├─────────┼───────────────┼───────────────────┼───────────────────────┼────────────────────────┼────────────────────────┼─────────┤ 54 | // │ 0 │ 'faster task' │ '63768 ± 4.02%' │ '58954 ± 15255.00' │ '18562 ± 1.67%' │ '16962 ± 4849' │ 1569 │ 55 | // │ 1 │ 'slower task' │ '1542543 ± 7.14%' │ '1652502 ± 167851.00' │ '808 ± 19.65%' │ '605 ± 67' │ 65 │ 56 | // └─────────┴───────────────┴───────────────────┴───────────────────────┴────────────────────────┴────────────────────────┴─────────┘ 57 | ``` 58 | 59 | The `add` method accepts a task name and a task function, so it can benchmark 60 | it! This method returns a reference to the Bench instance, so it's possible to 61 | use it to create an another task for that instance. 62 | 63 | Note that the task name should always be unique in an instance, because Tinybench stores the tasks based 64 | on their names in a `Map`. 65 | 66 | Also note that `tinybench` does not log any result by default. You can extract the relevant stats 67 | from `bench.tasks` or any other API after running the benchmark, and process them however you want. 68 | 69 | More usage examples can be found in the [examples](./examples/) directory. 70 | 71 | ## Docs 72 | 73 | ### [`Bench`](https://tinylibs.github.io/tinybench/classes/Bench.html) 74 | 75 | ### [`Task`](https://tinylibs.github.io/tinybench/classes/Task.html) 76 | 77 | ### [`TaskResult`](https://tinylibs.github.io/tinybench/interfaces/TaskResult.html) 78 | 79 | ### `Events` 80 | 81 | Both the `Task` and `Bench` classes extend the `EventTarget` object. So you can attach listeners to different types of events in each class instance using the universal `addEventListener` and `removeEventListener` methods. 82 | 83 | #### [`BenchEvents`](https://tinylibs.github.io/tinybench/types/BenchEvents.html) 84 | 85 | ```js 86 | // runs on each benchmark task's cycle 87 | bench.addEventListener('cycle', (evt) => { 88 | const task = evt.task!; 89 | }); 90 | ``` 91 | 92 | #### [`TaskEvents`](https://tinylibs.github.io/tinybench/types/TaskEvents.html) 93 | 94 | ```js 95 | // runs only on this benchmark task's cycle 96 | task.addEventListener('cycle', (evt) => { 97 | const task = evt.task!; 98 | }); 99 | ``` 100 | 101 | ### [`BenchEvent`](https://tinylibs.github.io/tinybench/types/BenchEvent.html) 102 | 103 | ## `process.hrtime` 104 | 105 | if you want more accurate results for nodejs with `process.hrtime`, then import 106 | the `hrtimeNow` function from the library and pass it to the `Bench` options. 107 | 108 | ```ts 109 | import { hrtimeNow } from 'tinybench' 110 | ``` 111 | 112 | It may make your benchmarks slower. 113 | 114 | ## Concurrency 115 | 116 | - When `mode` is set to `null` (default), concurrency is disabled. 117 | - When `mode` is set to 'task', each task's iterations (calls of a task function) run concurrently. 118 | - When `mode` is set to 'bench', different tasks within the bench run concurrently. Concurrent cycles. 119 | 120 | ```ts 121 | bench.threshold = 10 // The maximum number of concurrent tasks to run. Defaults to Number.POSITIVE_INFINITY. 122 | bench.concurrency = 'task' // The concurrency mode to determine how tasks are run. 123 | await bench.run() 124 | ``` 125 | 126 | ## Prior art 127 | 128 | - [Benchmark.js](https://github.com/bestiejs/benchmark.js) 129 | - [mitata](https://github.com/evanwashere/mitata/) 130 | - [tatami-ng](https://github.com/poolifier/tatami-ng) 131 | - [Bema](https://github.com/prisma-labs/bema) 132 | 133 | ## Authors 134 | 135 | |
Mohammad Bagher
| 136 | | ------------------------------------------------------------------------------------------------------------------------------------------------ | 137 | 138 | ## Credits 139 | 140 | |
Uzlopak
|
poyoho
| 141 | | ------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | 142 | 143 | ## Contributing 144 | 145 | Feel free to create issues/discussions and then PRs for the project! 146 | 147 | ## Sponsors 148 | 149 | Your sponsorship can make a huge difference in continuing our work in open source! 150 | 151 |

152 | 153 | 154 | 155 |

156 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /docs/assets/hierarchy.js: -------------------------------------------------------------------------------- 1 | window.hierarchyData = "eJyrVirKzy8pVrKKjtVRKkpNy0lNLsnMzytWsqqurQUAmx4Kpg==" -------------------------------------------------------------------------------- /docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #001080; 3 | --dark-hl-0: #9CDCFE; 4 | --light-hl-1: #000000; 5 | --dark-hl-1: #D4D4D4; 6 | --light-hl-2: #098658; 7 | --dark-hl-2: #B5CEA8; 8 | --light-hl-3: #0000FF; 9 | --dark-hl-3: #569CD6; 10 | --light-code-background: #FFFFFF; 11 | --dark-code-background: #1E1E1E; 12 | } 13 | 14 | @media (prefers-color-scheme: light) { :root { 15 | --hl-0: var(--light-hl-0); 16 | --hl-1: var(--light-hl-1); 17 | --hl-2: var(--light-hl-2); 18 | --hl-3: var(--light-hl-3); 19 | --code-background: var(--light-code-background); 20 | } } 21 | 22 | @media (prefers-color-scheme: dark) { :root { 23 | --hl-0: var(--dark-hl-0); 24 | --hl-1: var(--dark-hl-1); 25 | --hl-2: var(--dark-hl-2); 26 | --hl-3: var(--dark-hl-3); 27 | --code-background: var(--dark-code-background); 28 | } } 29 | 30 | :root[data-theme='light'] { 31 | --hl-0: var(--light-hl-0); 32 | --hl-1: var(--light-hl-1); 33 | --hl-2: var(--light-hl-2); 34 | --hl-3: var(--light-hl-3); 35 | --code-background: var(--light-code-background); 36 | } 37 | 38 | :root[data-theme='dark'] { 39 | --hl-0: var(--dark-hl-0); 40 | --hl-1: var(--dark-hl-1); 41 | --hl-2: var(--dark-hl-2); 42 | --hl-3: var(--dark-hl-3); 43 | --code-background: var(--dark-code-background); 44 | } 45 | 46 | .hl-0 { color: var(--hl-0); } 47 | .hl-1 { color: var(--hl-1); } 48 | .hl-2 { color: var(--hl-2); } 49 | .hl-3 { color: var(--hl-3); } 50 | pre, code { background: var(--code-background); } 51 | -------------------------------------------------------------------------------- /docs/assets/icons.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | addIcons(); 3 | function addIcons() { 4 | if (document.readyState === "loading") return document.addEventListener("DOMContentLoaded", addIcons); 5 | const svg = document.body.appendChild(document.createElementNS("http://www.w3.org/2000/svg", "svg")); 6 | svg.innerHTML = `MMNEPVFCICPMFPCPTTAAATR`; 7 | svg.style.display = "none"; 8 | if (location.protocol === "file:") updateUseElements(); 9 | } 10 | 11 | function updateUseElements() { 12 | document.querySelectorAll("use").forEach(el => { 13 | if (el.getAttribute("href").includes("#icon-")) { 14 | el.setAttribute("href", el.getAttribute("href").replace(/.*#/, "#")); 15 | } 16 | }); 17 | } 18 | })() -------------------------------------------------------------------------------- /docs/assets/icons.svg: -------------------------------------------------------------------------------- 1 | MMNEPVFCICPMFPCPTTAAATR -------------------------------------------------------------------------------- /docs/assets/navigation.js: -------------------------------------------------------------------------------- 1 | window.navigationData = "eJyNlE1PwzAMhv9LzhVlhQ3oEYkKIQbSthviELpMjdo6Ve1uIMR/Rw3QNOlHuPr181iylbx8MhLvxGL2sN00QLIULGAVp4zFTEBTYtgFZxmVBQtYLmHP4uuvoGNvBaSZ4dKCIwoMddmmFlGf23HMh1hbnaO09u4ogHDNK8NLIFEfePo3uWuxZdFy5cqeK5IKcFr12zAnSmDO0qVzii1xkkgyHXeYeE7Sbm9+OVaHT7UR2BQ06fmJvQvW44yEPirrSA5/fnO1WEbjF5+WuFsZWHTXo0QSIGrXY4U+UwIunoCfuVcqH3Jt1ceOkf/hzKVd2iQ+B6iTgY+8lvytEBiCOtnkRR/K6vbDeOqjhwZS/QLCLrQFq8v+1J1a4xisgwH4+g2d7JWb" -------------------------------------------------------------------------------- /docs/assets/search.js: -------------------------------------------------------------------------------- 1 | window.searchData = "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"; -------------------------------------------------------------------------------- /docs/functions/hrtimeNow.html: -------------------------------------------------------------------------------- 1 | hrtimeNow | tinybench - v4.0.1
tinybench - v4.0.1
    Preparing search index...

    Function hrtimeNow

    • Returns the current high resolution timestamp in milliseconds using process.hrtime.bigint().

      2 |

      Returns number

      the current high resolution timestamp in milliseconds

      3 |
    4 | -------------------------------------------------------------------------------- /docs/functions/nToMs.html: -------------------------------------------------------------------------------- 1 | nToMs | tinybench - v4.0.1
    tinybench - v4.0.1
      Preparing search index...

      Function nToMs

      • Converts nanoseconds to milliseconds.

        2 |

        Parameters

        • ns: number

          the nanoseconds to convert

          3 |

        Returns number

        the milliseconds

        4 |
      5 | -------------------------------------------------------------------------------- /docs/hierarchy.html: -------------------------------------------------------------------------------- 1 | tinybench - v4.0.1
      tinybench - v4.0.1
        Preparing search index...

        tinybench - v4.0.1

        Hierarchy Summary

        2 | -------------------------------------------------------------------------------- /docs/interfaces/FnOptions.html: -------------------------------------------------------------------------------- 1 | FnOptions | tinybench - v4.0.1
        tinybench - v4.0.1
          Preparing search index...

          Interface FnOptions

          the task function options

          2 |
          interface FnOptions {
              afterAll?: FnHook;
              afterEach?: FnHook;
              beforeAll?: FnHook;
              beforeEach?: FnHook;
          }
          Index

          Properties

          Properties

          afterAll?: FnHook

          An optional function that is run after all iterations of this task end

          7 |
          afterEach?: FnHook

          An optional function that is run after each iteration of this task

          8 |
          beforeAll?: FnHook

          An optional function that is run before iterations of this task begin

          9 |
          beforeEach?: FnHook

          An optional function that is run before each iteration of this task

          10 |
          11 | -------------------------------------------------------------------------------- /docs/interfaces/TaskEventsMap.html: -------------------------------------------------------------------------------- 1 | TaskEventsMap | tinybench - v4.0.1
          tinybench - v4.0.1
            Preparing search index...

            Interface TaskEventsMap

            interface TaskEventsMap {
                abort: EventListener;
                complete: EventListener;
                cycle: EventListener;
                error: EventListener;
                reset: EventListener;
                start: EventListener;
                warmup: EventListener;
            }
            Index

            Properties

            abort 2 | complete 3 | cycle 4 | error 5 | reset 6 | start 7 | warmup 8 |

            Properties

            complete: EventListener
            9 | -------------------------------------------------------------------------------- /docs/types/BenchEvent.html: -------------------------------------------------------------------------------- 1 | BenchEvent | tinybench - v4.0.1
            tinybench - v4.0.1
              Preparing search index...

              Type Alias BenchEvent

              BenchEvent: Event & { error?: Error; task?: Task }

              bench event

              2 |
              3 | -------------------------------------------------------------------------------- /docs/types/BenchEvents.html: -------------------------------------------------------------------------------- 1 | BenchEvents | tinybench - v4.0.1
              tinybench - v4.0.1
                Preparing search index...

                Type Alias BenchEvents

                BenchEvents:
                    | "abort"
                    | "add"
                    | "complete"
                    | "cycle"
                    | "error"
                    | "remove"
                    | "reset"
                    | "start"
                    | "warmup"

                Bench events

                2 |
                3 | -------------------------------------------------------------------------------- /docs/types/EventListener.html: -------------------------------------------------------------------------------- 1 | EventListener | tinybench - v4.0.1
                tinybench - v4.0.1
                  Preparing search index...

                  Type Alias EventListener

                  EventListener: (evt: BenchEvent) => void

                  event listener

                  2 |

                  Type declaration

                  3 | -------------------------------------------------------------------------------- /docs/types/Fn.html: -------------------------------------------------------------------------------- 1 | Fn | tinybench - v4.0.1
                  tinybench - v4.0.1
                    Preparing search index...

                    Type Alias Fn

                    Fn: () => Promise<unknown> | unknown

                    the task function

                    2 |

                    Type declaration

                      • (): Promise<unknown> | unknown
                      • Returns Promise<unknown> | unknown

                    3 | -------------------------------------------------------------------------------- /docs/types/FnHook.html: -------------------------------------------------------------------------------- 1 | FnHook | tinybench - v4.0.1
                    tinybench - v4.0.1
                      Preparing search index...

                      Type Alias FnHook

                      FnHook: (this: Task, mode?: "run" | "warmup") => Promise<void> | void

                      The task hook function signature 2 | If warmup is enabled, the hook will be called twice, once for the warmup and once for the run.

                      3 |

                      Type declaration

                        • (this: Task, mode?: "run" | "warmup"): Promise<void> | void
                        • Parameters

                          • this: Task
                          • Optionalmode: "run" | "warmup"

                            the mode where the hook is being called

                            4 |

                          Returns Promise<void> | void

                      5 | -------------------------------------------------------------------------------- /docs/types/Hook.html: -------------------------------------------------------------------------------- 1 | Hook | tinybench - v4.0.1
                      tinybench - v4.0.1
                        Preparing search index...

                        Type Alias Hook

                        Hook: (task?: Task, mode?: "run" | "warmup") => Promise<void> | void

                        The hook function signature 2 | If warmup is enabled, the hook will be called twice, once for the warmup and once for the run.

                        3 |

                        Type declaration

                          • (task?: Task, mode?: "run" | "warmup"): Promise<void> | void
                          • Parameters

                            • Optionaltask: Task

                              the task instance

                              4 |
                            • Optionalmode: "run" | "warmup"

                              the mode where the hook is being called

                              5 |

                            Returns Promise<void> | void

                        6 | -------------------------------------------------------------------------------- /docs/types/TaskEvents.html: -------------------------------------------------------------------------------- 1 | TaskEvents | tinybench - v4.0.1
                        tinybench - v4.0.1
                          Preparing search index...

                          Type Alias TaskEvents

                          TaskEvents:
                              | "abort"
                              | "complete"
                              | "cycle"
                              | "error"
                              | "reset"
                              | "start"
                              | "warmup"

                          task events

                          2 |
                          3 | -------------------------------------------------------------------------------- /docs/variables/now.html: -------------------------------------------------------------------------------- 1 | now | tinybench - v4.0.1
                          tinybench - v4.0.1
                            Preparing search index...

                            Variable nowConst

                            now: () => number = performanceNow

                            Type declaration

                              • (): number
                              • Returns the current high resolution millisecond timestamp, where 0 represents the start of the current node process.

                                2 |

                                Returns number

                                v8.5.0

                                3 |
                            4 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import cspellConfigs from '@cspell/eslint-plugin/configs' 2 | import js from '@eslint/js' 3 | import jsdoc from 'eslint-plugin-jsdoc' 4 | import perfectionist from 'eslint-plugin-perfectionist' 5 | import { defineConfig } from 'eslint/config' 6 | import neostandard, { plugins } from 'neostandard' 7 | 8 | export default defineConfig([ 9 | { 10 | ignores: ['docs/**', 'package/**'], 11 | }, 12 | cspellConfigs.recommended, 13 | { 14 | rules: { 15 | '@cspell/spellchecker': [ 16 | 'warn', 17 | { 18 | autoFix: true, 19 | cspell: { 20 | words: [ 21 | 'evanwashere', 22 | 'fastly', 23 | 'lagon', 24 | 'lockdown', 25 | 'quickjs', 26 | 'moddable', 27 | 'neostandard', 28 | 'spidermonkey', 29 | 'workerd', 30 | ], 31 | }, 32 | }, 33 | ], 34 | }, 35 | }, 36 | js.configs.recommended, 37 | jsdoc.configs['flat/recommended-typescript'], 38 | ...plugins['typescript-eslint'].config( 39 | { 40 | extends: [ 41 | ...plugins['typescript-eslint'].configs.strictTypeChecked, 42 | ...plugins['typescript-eslint'].configs.stylisticTypeChecked, 43 | ], 44 | languageOptions: { 45 | parserOptions: { 46 | projectService: true, 47 | tsconfigRootDir: import.meta.dirname, 48 | }, 49 | }, 50 | }, 51 | { 52 | files: ['**/*.js', '**/*.mjs', '**/*.cjs'], 53 | ...plugins['typescript-eslint'].configs.disableTypeChecked, 54 | } 55 | ), 56 | perfectionist.configs['recommended-natural'], 57 | ...neostandard({ 58 | noJsx: true, 59 | ts: true, 60 | }), 61 | ]) 62 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "packageManager": "pnpm@10.11.0", 6 | "volta": { 7 | "node": "22.16.0", 8 | "pnpm": "10.11.0" 9 | }, 10 | "scripts": { 11 | "all": "run-s example:*", 12 | "example:simple": "tsx src/simple.ts", 13 | "simple:dev": "tsx --watch src/simple.ts", 14 | "example:simple-gc": "tsx --expose_gc src/simple-gc.ts", 15 | "simple-gc:dev": "tsx --watch --expose_gc src/simple-gc.ts", 16 | "example:simple-bun": "bun run src/simple-bun.ts" 17 | }, 18 | "license": "ISC", 19 | "devDependencies": { 20 | "npm-run-all2": "^8.0.4", 21 | "tsx": "^4.19.4" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/src/simple-bun.ts: -------------------------------------------------------------------------------- 1 | import { Bench, nToMs } from '../../src' 2 | 3 | const bench = new Bench({ 4 | name: 'simple benchmark bun', 5 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access 6 | now: () => nToMs(Bun.nanoseconds()), 7 | setup: (_task, mode) => { 8 | // Run the garbage collector before warmup at each cycle 9 | if (mode === 'warmup') { 10 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access 11 | Bun.gc(true) 12 | } 13 | }, 14 | time: 100, 15 | }) 16 | 17 | bench 18 | .add('faster task', () => { 19 | console.log('I am faster') 20 | }) 21 | .add('slower task', async () => { 22 | await new Promise(resolve => setTimeout(resolve, 1)) // we wait 1ms :) 23 | console.log('I am slower') 24 | }) 25 | 26 | await bench.run() 27 | 28 | console.log(bench.name) 29 | console.table(bench.table()) 30 | 31 | // Output: 32 | // simple benchmark bun 33 | // ┌───┬─────────────┬──────────────────┬────────────────────┬────────────────────────┬────────────────────────┬─────────┐ 34 | // │ │ Task name │ Latency avg (ns) │ Latency med (ns) │ Throughput avg (ops/s) │ Throughput med (ops/s) │ Samples │ 35 | // ├───┼─────────────┼──────────────────┼────────────────────┼────────────────────────┼────────────────────────┼─────────┤ 36 | // │ 0 │ faster task │ 42717 ± 2.33% │ 38646 ± 6484.00 │ 25736 ± 1.02% │ 25876 ± 4643 │ 2341 │ 37 | // │ 1 │ slower task │ 1443410 ± 10.93% │ 1368739 ± 82721.50 │ 976 ± 35.61% │ 731 ± 42 │ 72 │ 38 | // └───┴─────────────┴──────────────────┴────────────────────┴────────────────────────┴────────────────────────┴─────────┘ 39 | -------------------------------------------------------------------------------- /examples/src/simple-gc.ts: -------------------------------------------------------------------------------- 1 | import { Bench } from '../../src' 2 | 3 | const bench = new Bench({ 4 | name: 'simple benchmark gc', 5 | setup: (_task, mode) => { 6 | // Run the garbage collector before warmup at each cycle 7 | if (mode === 'warmup' && typeof globalThis.gc === 'function') { 8 | globalThis.gc() 9 | } 10 | }, 11 | time: 100, 12 | }) 13 | 14 | bench 15 | .add('faster task', () => { 16 | console.log('I am faster') 17 | }) 18 | .add('slower task', async () => { 19 | await new Promise(resolve => setTimeout(resolve, 1)) // we wait 1ms :) 20 | console.log('I am slower') 21 | }) 22 | 23 | await bench.run() 24 | 25 | console.log(bench.name) 26 | console.table(bench.table()) 27 | 28 | // Output: 29 | // simple benchmark gc 30 | // ┌─────────┬───────────────┬───────────────────┬───────────────────────┬────────────────────────┬────────────────────────┬─────────┐ 31 | // │ (index) │ Task name │ Latency avg (ns) │ Latency med (ns) │ Throughput avg (ops/s) │ Throughput med (ops/s) │ Samples │ 32 | // ├─────────┼───────────────┼───────────────────┼───────────────────────┼────────────────────────┼────────────────────────┼─────────┤ 33 | // │ 0 │ 'faster task' │ '51687 ± 3.07%' │ '53147 ± 17317.50' │ '22478 ± 1.46%' │ '18816 ± 5605' │ 1936 │ 34 | // │ 1 │ 'slower task' │ '1605805 ± 7.06%' │ '1713970 ± 121077.00' │ '793 ± 20.79%' │ '583 ± 39' │ 64 │ 35 | // └─────────┴───────────────┴───────────────────┴───────────────────────┴────────────────────────┴────────────────────────┴─────────┘ 36 | -------------------------------------------------------------------------------- /examples/src/simple.ts: -------------------------------------------------------------------------------- 1 | import { Bench } from '../../src' 2 | 3 | const bench = new Bench({ name: 'simple benchmark', time: 100 }) 4 | 5 | bench 6 | .add('faster task', () => { 7 | console.log('I am faster') 8 | }) 9 | .add('slower task', async () => { 10 | await new Promise(resolve => setTimeout(resolve, 1)) // we wait 1ms :) 11 | console.log('I am slower') 12 | }) 13 | 14 | await bench.run() 15 | 16 | console.log(bench.name) 17 | console.table(bench.table()) 18 | 19 | // Output: 20 | // simple benchmark 21 | // ┌─────────┬───────────────┬───────────────────┬───────────────────────┬────────────────────────┬────────────────────────┬─────────┐ 22 | // │ (index) │ Task name │ Latency avg (ns) │ Latency med (ns) │ Throughput avg (ops/s) │ Throughput med (ops/s) │ Samples │ 23 | // ├─────────┼───────────────┼───────────────────┼───────────────────────┼────────────────────────┼────────────────────────┼─────────┤ 24 | // │ 0 │ 'faster task' │ '63768 ± 4.02%' │ '58954 ± 15255.00' │ '18562 ± 1.67%' │ '16962 ± 4849' │ 1569 │ 25 | // │ 1 │ 'slower task' │ '1542543 ± 7.14%' │ '1652502 ± 167851.00' │ '808 ± 19.65%' │ '605 ± 67' │ 65 │ 26 | // └─────────┴───────────────┴───────────────────┴───────────────────────┴────────────────────────┴────────────────────────┴─────────┘ 27 | -------------------------------------------------------------------------------- /examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["./src/*.ts"], 4 | "compilerOptions": { 5 | "target": "ESNext", 6 | "module": "ESNext" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tinybench", 3 | "version": "4.0.1", 4 | "description": "🔎 A simple, tiny and lightweight benchmarking library!", 5 | "type": "module", 6 | "packageManager": "pnpm@10.11.0", 7 | "volta": { 8 | "node": "22.16.0", 9 | "pnpm": "10.11.0" 10 | }, 11 | "engines": { 12 | "node": ">=18.0.0" 13 | }, 14 | "publishConfig": { 15 | "directory": "package" 16 | }, 17 | "scripts": { 18 | "prepare": "pnpm exec simple-git-hooks", 19 | "dev": "tsdown --watch", 20 | "build": "tsdown", 21 | "prepublishOnly": "pnpm build && rm -rf ./package && clean-publish", 22 | "postpublish": "rm -rf ./package", 23 | "typecheck": "tsc --noEmit", 24 | "typedoc": "typedoc", 25 | "lint": "eslint --cache src test examples eslint.config.js tsdown.config.ts", 26 | "lint:fix": "eslint --cache --fix src test examples eslint.config.js tsdown.config.ts", 27 | "release": "bumpp package.json --commit --push --tag", 28 | "test": "vitest --retry=5 --run" 29 | }, 30 | "main": "./dist/index.js", 31 | "module": "./dist/index.js", 32 | "types": "./dist/index.d.ts", 33 | "exports": { 34 | ".": { 35 | "types": "./dist/index.d.ts", 36 | "default": "./dist/index.js" 37 | } 38 | }, 39 | "files": [ 40 | "dist/**" 41 | ], 42 | "repository": "tinylibs/tinybench", 43 | "license": "MIT", 44 | "devDependencies": { 45 | "@commitlint/cli": "^19.8.1", 46 | "@commitlint/config-conventional": "^19.8.1", 47 | "@cspell/eslint-plugin": "^9.0.2", 48 | "@eslint/js": "^9.28.0", 49 | "@size-limit/preset-small-lib": "^11.2.0", 50 | "@size-limit/time": "^11.2.0", 51 | "@types/node": "^22.15.29", 52 | "bumpp": "^10.1.1", 53 | "changelogithub": "^13.15.0", 54 | "clean-publish": "^5.2.1", 55 | "eslint": "^9.28.0", 56 | "eslint-plugin-jsdoc": "^50.7.0", 57 | "eslint-plugin-perfectionist": "^4.13.0", 58 | "nano-staged": "^0.8.0", 59 | "neostandard": "^0.12.1", 60 | "p-limit": "^6.2.0", 61 | "simple-git-hooks": "^2.13.0", 62 | "size-limit": "^11.2.0", 63 | "tsdown": "^0.12.6", 64 | "typedoc": "^0.28.5", 65 | "typescript": "~5.8.3", 66 | "vitest": "^3.1.4" 67 | }, 68 | "keywords": [ 69 | "benchmark", 70 | "tinylibs", 71 | "tiny" 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - examples 3 | -------------------------------------------------------------------------------- /src/bench.ts: -------------------------------------------------------------------------------- 1 | import pLimit from 'p-limit' 2 | 3 | import type { 4 | AddEventListenerOptionsArgument, 5 | BenchEvents, 6 | BenchEventsMap, 7 | BenchOptions, 8 | Fn, 9 | FnOptions, 10 | RemoveEventListenerOptionsArgument, 11 | TaskResult, 12 | } from './types' 13 | 14 | import { 15 | defaultMinimumIterations, 16 | defaultMinimumTime, 17 | defaultMinimumWarmupIterations, 18 | defaultMinimumWarmupTime, 19 | emptyFunction, 20 | } from './constants' 21 | import { createBenchEvent } from './event' 22 | import { Task } from './task' 23 | import { 24 | formatNumber, 25 | invariant, 26 | type JSRuntime, 27 | mToNs, 28 | now, 29 | runtime, 30 | runtimeVersion, 31 | } from './utils' 32 | 33 | /** 34 | * The Bench class keeps track of the benchmark tasks and controls them. 35 | */ 36 | export class Bench extends EventTarget { 37 | /** 38 | * Executes tasks concurrently based on the specified concurrency mode. 39 | * 40 | * - When `mode` is set to `null` (default), concurrency is disabled. 41 | * - When `mode` is set to 'task', each task's iterations (calls of a task function) run concurrently. 42 | * - When `mode` is set to 'bench', different tasks within the bench run concurrently. Concurrent cycles. 43 | */ 44 | concurrency: 'bench' | 'task' | null = null 45 | 46 | /** 47 | * The benchmark name. 48 | */ 49 | readonly name?: string 50 | 51 | /** 52 | * The options. 53 | */ 54 | readonly opts: Readonly 55 | 56 | /** 57 | * The JavaScript runtime environment. 58 | */ 59 | readonly runtime: 'unknown' | JSRuntime 60 | 61 | /** 62 | * The JavaScript runtime version. 63 | */ 64 | readonly runtimeVersion: string 65 | 66 | /** 67 | * The maximum number of concurrent tasks to run @default Number.POSITIVE_INFINITY 68 | */ 69 | threshold = Number.POSITIVE_INFINITY 70 | 71 | /** 72 | * tasks results as an array 73 | * @returns the tasks results as an array 74 | */ 75 | get results (): (Readonly | undefined)[] { 76 | return [...this._tasks.values()].map(task => task.result) 77 | } 78 | 79 | /** 80 | * tasks as an array 81 | * @returns the tasks as an array 82 | */ 83 | get tasks (): Task[] { 84 | return [...this._tasks.values()] 85 | } 86 | 87 | /** 88 | * the task map 89 | */ 90 | private readonly _tasks = new Map() 91 | 92 | constructor (options: BenchOptions = {}) { 93 | super() 94 | this.name = options.name 95 | delete options.name 96 | this.runtime = runtime 97 | this.runtimeVersion = runtimeVersion 98 | this.opts = { 99 | ...{ 100 | iterations: defaultMinimumIterations, 101 | now, 102 | setup: emptyFunction, 103 | teardown: emptyFunction, 104 | throws: false, 105 | time: defaultMinimumTime, 106 | warmup: true, 107 | warmupIterations: defaultMinimumWarmupIterations, 108 | warmupTime: defaultMinimumWarmupTime, 109 | }, 110 | ...options, 111 | } 112 | 113 | if (this.opts.signal) { 114 | this.opts.signal.addEventListener( 115 | 'abort', 116 | () => { 117 | this.dispatchEvent(createBenchEvent('abort')) 118 | }, 119 | { once: true } 120 | ) 121 | } 122 | } 123 | 124 | /** 125 | * add a benchmark task to the task map 126 | * @param name - the task name 127 | * @param fn - the task function 128 | * @param fnOpts - the task function options 129 | * @returns the Bench instance 130 | * @throws if the task already exists 131 | */ 132 | add (name: string, fn: Fn, fnOpts: FnOptions = {}): this { 133 | if (!this._tasks.has(name)) { 134 | const task = new Task(this, name, fn, fnOpts) 135 | this._tasks.set(name, task) 136 | this.dispatchEvent(createBenchEvent('add', task)) 137 | } else { 138 | throw new Error(`Task "${name}" already exists`) 139 | } 140 | return this 141 | } 142 | 143 | addEventListener( 144 | type: K, 145 | listener: BenchEventsMap[K], 146 | options?: AddEventListenerOptionsArgument 147 | ): void { 148 | super.addEventListener(type, listener, options) 149 | } 150 | 151 | /** 152 | * get a task based on the task name 153 | * @param name - the task name 154 | * @returns the Task instance 155 | */ 156 | getTask (name: string): Task | undefined { 157 | return this._tasks.get(name) 158 | } 159 | 160 | /** 161 | * remove a benchmark task from the task map 162 | * @param name - the task name 163 | * @returns the Bench instance 164 | */ 165 | remove (name: string): this { 166 | const task = this.getTask(name) 167 | if (task) { 168 | this.dispatchEvent(createBenchEvent('remove', task)) 169 | this._tasks.delete(name) 170 | } 171 | return this 172 | } 173 | 174 | removeEventListener( 175 | type: K, 176 | listener: BenchEventsMap[K], 177 | options?: RemoveEventListenerOptionsArgument 178 | ): void { 179 | super.removeEventListener(type, listener, options) 180 | } 181 | 182 | /** 183 | * reset tasks and remove their result 184 | */ 185 | reset (): void { 186 | this.dispatchEvent(createBenchEvent('reset')) 187 | for (const task of this._tasks.values()) { 188 | task.reset() 189 | } 190 | } 191 | 192 | /** 193 | * run the added tasks that were registered using the {@link add} method 194 | * @returns the tasks array 195 | */ 196 | async run (): Promise { 197 | if (this.opts.warmup) { 198 | await this.warmupTasks() 199 | } 200 | let values: Task[] = [] 201 | this.dispatchEvent(createBenchEvent('start')) 202 | if (this.concurrency === 'bench') { 203 | const limit = pLimit(this.threshold) 204 | const promises: Promise[] = [] 205 | for (const task of this._tasks.values()) { 206 | promises.push(limit(task.run.bind(task))) 207 | } 208 | values = await Promise.all(promises) 209 | } else { 210 | for (const task of this._tasks.values()) { 211 | values.push(await task.run()) 212 | } 213 | } 214 | this.dispatchEvent(createBenchEvent('complete')) 215 | return values 216 | } 217 | 218 | /** 219 | * run the added tasks that were registered using the {@link add} method (sync version) 220 | * @returns the tasks array 221 | */ 222 | runSync (): Task[] { 223 | invariant( 224 | this.concurrency === null, 225 | 'Cannot use `concurrency` option when using `runSync`' 226 | ) 227 | if (this.opts.warmup) { 228 | this.warmupTasksSync() 229 | } 230 | const values: Task[] = [] 231 | this.dispatchEvent(createBenchEvent('start')) 232 | for (const task of this._tasks.values()) { 233 | values.push(task.runSync()) 234 | } 235 | this.dispatchEvent(createBenchEvent('complete')) 236 | return values 237 | } 238 | 239 | /** 240 | * table of the tasks results 241 | * @param convert - an optional callback to convert the task result to a table record 242 | * @returns the tasks results as an array of table records 243 | */ 244 | table ( 245 | convert?: (task: Task) => Record 246 | ): (null | Record)[] { 247 | return this.tasks.map(task => { 248 | if (task.result) { 249 | const { error, latency, throughput } = task.result 250 | /* eslint-disable perfectionist/sort-objects */ 251 | return error 252 | ? { 253 | 'Task name': task.name, 254 | Error: error.message, 255 | Stack: error.stack, 256 | } 257 | : (convert?.(task) ?? { 258 | 'Task name': task.name, 259 | 'Latency avg (ns)': `${formatNumber(mToNs(latency.mean), 5, 2)} \xb1 ${latency.rme.toFixed(2)}%`, 260 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 261 | 'Latency med (ns)': `${formatNumber(mToNs(latency.p50!), 5, 2)} \xb1 ${formatNumber(mToNs(latency.mad!), 5, 2)}`, 262 | 'Throughput avg (ops/s)': `${Math.round(throughput.mean).toString()} \xb1 ${throughput.rme.toFixed(2)}%`, 263 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 264 | 'Throughput med (ops/s)': `${Math.round(throughput.p50!).toString()} \xb1 ${Math.round(throughput.mad!).toString()}`, 265 | Samples: latency.samples.length, 266 | }) 267 | /* eslint-enable perfectionist/sort-objects */ 268 | } 269 | return null 270 | }) 271 | } 272 | 273 | /** 274 | * warmup the benchmark tasks. 275 | */ 276 | private async warmupTasks (): Promise { 277 | this.dispatchEvent(createBenchEvent('warmup')) 278 | if (this.concurrency === 'bench') { 279 | const limit = pLimit(this.threshold) 280 | const promises: Promise[] = [] 281 | for (const task of this._tasks.values()) { 282 | promises.push(limit(task.warmup.bind(task))) 283 | } 284 | await Promise.all(promises) 285 | } else { 286 | for (const task of this._tasks.values()) { 287 | await task.warmup() 288 | } 289 | } 290 | } 291 | 292 | /** 293 | * warmup the benchmark tasks (sync version) 294 | */ 295 | private warmupTasksSync (): void { 296 | this.dispatchEvent(createBenchEvent('warmup')) 297 | for (const task of this._tasks.values()) { 298 | task.warmupSync() 299 | } 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /src/event.ts: -------------------------------------------------------------------------------- 1 | import type { Task } from './task' 2 | import type { BenchEvents } from './types' 3 | 4 | const createBenchEvent = (eventType: BenchEvents, target?: Task) => { 5 | const event = new Event(eventType) 6 | if (target) { 7 | Object.defineProperty(event, 'task', { 8 | configurable: false, 9 | enumerable: true, 10 | value: target, 11 | writable: false, 12 | }) 13 | } 14 | return event 15 | } 16 | 17 | const createErrorEvent = (target: Task, error: Error) => { 18 | const event = new Event('error') 19 | Object.defineProperty(event, 'task', { 20 | configurable: false, 21 | enumerable: true, 22 | value: target, 23 | writable: false, 24 | }) 25 | Object.defineProperty(event, 'error', { 26 | configurable: false, 27 | enumerable: true, 28 | value: error, 29 | writable: false, 30 | }) 31 | return event 32 | } 33 | 34 | export { createBenchEvent, createErrorEvent } 35 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { Bench } from './bench' 2 | export { Task } from './task' 3 | export type { 4 | BenchEvent, 5 | BenchEvents, 6 | BenchEventsMap, 7 | BenchOptions, 8 | EventListener, 9 | Fn, 10 | FnHook, 11 | FnOptions, 12 | Hook, 13 | Statistics, 14 | TaskEvents, 15 | TaskEventsMap, 16 | TaskResult, 17 | } from './types' 18 | export { hrtimeNow, now, nToMs } from './utils' 19 | export type { JSRuntime } from './utils' 20 | -------------------------------------------------------------------------------- /src/task.ts: -------------------------------------------------------------------------------- 1 | import pLimit from 'p-limit' 2 | 3 | import type { Bench } from './bench' 4 | import type { 5 | AddEventListenerOptionsArgument, 6 | Fn, 7 | FnOptions, 8 | RemoveEventListenerOptionsArgument, 9 | TaskEvents, 10 | TaskEventsMap, 11 | TaskResult, 12 | } from './types' 13 | 14 | import { createBenchEvent, createErrorEvent } from './event' 15 | import { 16 | getStatisticsSorted, 17 | invariant, 18 | isFnAsyncResource, 19 | isPromiseLike, 20 | } from './utils' 21 | 22 | /** 23 | * A class that represents each benchmark task in Tinybench. It keeps track of the 24 | * results, name, the task function, the number times the task function has been executed, ... 25 | */ 26 | export class Task extends EventTarget { 27 | /** 28 | * The task name 29 | */ 30 | readonly name: string 31 | 32 | /** 33 | * The result object 34 | */ 35 | result: Readonly | undefined 36 | 37 | /** 38 | * The number of times the task function has been executed 39 | */ 40 | runs = 0 41 | 42 | /** 43 | * The task synchronous status 44 | */ 45 | private readonly async: boolean 46 | 47 | /** 48 | * The Bench instance reference 49 | */ 50 | private readonly bench: Bench 51 | 52 | /** 53 | * The task function 54 | */ 55 | private readonly fn: Fn 56 | 57 | /** 58 | * The task function options 59 | */ 60 | private readonly fnOpts: Readonly 61 | 62 | constructor (bench: Bench, name: string, fn: Fn, fnOpts: FnOptions = {}) { 63 | super() 64 | this.bench = bench 65 | this.name = name 66 | this.fn = fn 67 | this.fnOpts = fnOpts 68 | this.async = isFnAsyncResource(fn) 69 | // TODO: support signal in Tasks 70 | } 71 | 72 | addEventListener( 73 | type: K, 74 | listener: TaskEventsMap[K], 75 | options?: AddEventListenerOptionsArgument 76 | ): void { 77 | super.addEventListener(type, listener, options) 78 | } 79 | 80 | removeEventListener( 81 | type: K, 82 | listener: TaskEventsMap[K], 83 | options?: RemoveEventListenerOptionsArgument 84 | ): void { 85 | super.removeEventListener(type, listener, options) 86 | } 87 | 88 | /** 89 | * reset the task to make the `Task.runs` a zero-value and remove the `Task.result` object property 90 | * @internal 91 | */ 92 | reset (): void { 93 | this.dispatchEvent(createBenchEvent('reset', this)) 94 | this.runs = 0 95 | this.result = undefined 96 | } 97 | 98 | /** 99 | * run the current task and write the results in `Task.result` object property 100 | * @returns the current task 101 | * @internal 102 | */ 103 | async run (): Promise { 104 | if (this.result?.error) { 105 | return this 106 | } 107 | this.dispatchEvent(createBenchEvent('start', this)) 108 | await this.bench.opts.setup?.(this, 'run') 109 | const { error, samples: latencySamples } = (await this.benchmark( 110 | 'run', 111 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 112 | this.bench.opts.time!, 113 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 114 | this.bench.opts.iterations! 115 | )) as { error?: Error; samples?: number[] } 116 | await this.bench.opts.teardown?.(this, 'run') 117 | 118 | this.processRunResult({ error, latencySamples }) 119 | 120 | return this 121 | } 122 | 123 | /** 124 | * run the current task and write the results in `Task.result` object property (sync version) 125 | * @returns the current task 126 | * @internal 127 | */ 128 | runSync (): this { 129 | if (this.result?.error) { 130 | return this 131 | } 132 | 133 | invariant( 134 | this.bench.concurrency === null, 135 | 'Cannot use `concurrency` option when using `runSync`' 136 | ) 137 | this.dispatchEvent(createBenchEvent('start', this)) 138 | 139 | const setupResult = this.bench.opts.setup?.(this, 'run') 140 | invariant( 141 | !isPromiseLike(setupResult), 142 | '`setup` function must be sync when using `runSync()`' 143 | ) 144 | 145 | const { error, samples: latencySamples } = this.benchmarkSync( 146 | 'run', 147 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 148 | this.bench.opts.time!, 149 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 150 | this.bench.opts.iterations! 151 | ) as { error?: Error; samples?: number[] } 152 | 153 | const teardownResult = this.bench.opts.teardown?.(this, 'run') 154 | invariant( 155 | !isPromiseLike(teardownResult), 156 | '`teardown` function must be sync when using `runSync()`' 157 | ) 158 | 159 | this.processRunResult({ error, latencySamples }) 160 | 161 | return this 162 | } 163 | 164 | /** 165 | * warmup the current task 166 | * @internal 167 | */ 168 | async warmup (): Promise { 169 | if (this.result?.error) { 170 | return 171 | } 172 | this.dispatchEvent(createBenchEvent('warmup', this)) 173 | await this.bench.opts.setup?.(this, 'warmup') 174 | const { error } = (await this.benchmark( 175 | 'warmup', 176 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 177 | this.bench.opts.warmupTime!, 178 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 179 | this.bench.opts.warmupIterations! 180 | )) as { error?: Error } 181 | await this.bench.opts.teardown?.(this, 'warmup') 182 | 183 | this.postWarmup(error) 184 | } 185 | 186 | /** 187 | * warmup the current task (sync version) 188 | * @internal 189 | */ 190 | warmupSync (): void { 191 | if (this.result?.error) { 192 | return 193 | } 194 | 195 | this.dispatchEvent(createBenchEvent('warmup', this)) 196 | 197 | const setupResult = this.bench.opts.setup?.(this, 'warmup') 198 | invariant( 199 | !isPromiseLike(setupResult), 200 | '`setup` function must be sync when using `runSync()`' 201 | ) 202 | 203 | const { error } = this.benchmarkSync( 204 | 'warmup', 205 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 206 | this.bench.opts.warmupTime!, 207 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 208 | this.bench.opts.warmupIterations! 209 | ) as { error?: Error } 210 | 211 | const teardownResult = this.bench.opts.teardown?.(this, 'warmup') 212 | invariant( 213 | !isPromiseLike(teardownResult), 214 | '`teardown` function must be sync when using `runSync()`' 215 | ) 216 | 217 | this.postWarmup(error) 218 | } 219 | 220 | private async benchmark ( 221 | mode: 'run' | 'warmup', 222 | time: number, 223 | iterations: number 224 | ): Promise<{ error?: unknown; samples?: number[] }> { 225 | if (this.fnOpts.beforeAll != null) { 226 | try { 227 | await this.fnOpts.beforeAll.call(this, mode) 228 | } catch (error) { 229 | return { error } 230 | } 231 | } 232 | 233 | // TODO: factor out 234 | let totalTime = 0 // ms 235 | const samples: number[] = [] 236 | const benchmarkTask = async () => { 237 | if (this.fnOpts.beforeEach != null) { 238 | await this.fnOpts.beforeEach.call(this, mode) 239 | } 240 | 241 | let taskTime = 0 // ms; 242 | if (this.async) { 243 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 244 | const taskStart = this.bench.opts.now!() 245 | // eslint-disable-next-line no-useless-call 246 | await this.fn.call(this) 247 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 248 | taskTime = this.bench.opts.now!() - taskStart 249 | } else { 250 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 251 | const taskStart = this.bench.opts.now!() 252 | // eslint-disable-next-line no-useless-call 253 | this.fn.call(this) 254 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 255 | taskTime = this.bench.opts.now!() - taskStart 256 | } 257 | 258 | samples.push(taskTime) 259 | totalTime += taskTime 260 | 261 | if (this.fnOpts.afterEach != null) { 262 | await this.fnOpts.afterEach.call(this, mode) 263 | } 264 | } 265 | 266 | try { 267 | const limit = pLimit(this.bench.threshold) // only for task level concurrency 268 | const promises: Promise[] = [] // only for task level concurrency 269 | while ( 270 | // eslint-disable-next-line no-unmodified-loop-condition 271 | (totalTime < time || 272 | samples.length + limit.activeCount + limit.pendingCount < iterations) && 273 | !this.bench.opts.signal?.aborted 274 | ) { 275 | if (this.bench.concurrency === 'task') { 276 | promises.push(limit(benchmarkTask)) 277 | } else { 278 | await benchmarkTask() 279 | } 280 | } 281 | if (!this.bench.opts.signal?.aborted && promises.length > 0) { 282 | await Promise.all(promises) 283 | } 284 | } catch (error) { 285 | return { error } 286 | } 287 | 288 | if (this.fnOpts.afterAll != null) { 289 | try { 290 | await this.fnOpts.afterAll.call(this, mode) 291 | } catch (error) { 292 | return { error } 293 | } 294 | } 295 | return { samples } 296 | } 297 | 298 | private benchmarkSync ( 299 | mode: 'run' | 'warmup', 300 | time: number, 301 | iterations: number 302 | ): { error?: unknown; samples?: number[] } { 303 | if (this.fnOpts.beforeAll != null) { 304 | try { 305 | const beforeAllResult = this.fnOpts.beforeAll.call(this, mode) 306 | invariant( 307 | !isPromiseLike(beforeAllResult), 308 | '`beforeAll` function must be sync when using `runSync()`' 309 | ) 310 | } catch (error) { 311 | return { error } 312 | } 313 | } 314 | 315 | // TODO: factor out 316 | let totalTime = 0 // ms 317 | const samples: number[] = [] 318 | const benchmarkTask = () => { 319 | if (this.fnOpts.beforeEach != null) { 320 | const beforeEachResult = this.fnOpts.beforeEach.call(this, mode) 321 | invariant( 322 | !isPromiseLike(beforeEachResult), 323 | '`beforeEach` function must be sync when using `runSync()`' 324 | ) 325 | } 326 | 327 | let taskTime = 0 // ms; 328 | 329 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 330 | const taskStart = this.bench.opts.now!() 331 | // eslint-disable-next-line no-useless-call 332 | const result = this.fn.call(this) 333 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 334 | taskTime = this.bench.opts.now!() - taskStart 335 | 336 | invariant( 337 | !isPromiseLike(result), 338 | 'task function must be sync when using `runSync()`' 339 | ) 340 | 341 | samples.push(taskTime) 342 | totalTime += taskTime 343 | 344 | if (this.fnOpts.afterEach != null) { 345 | const afterEachResult = this.fnOpts.afterEach.call(this, mode) 346 | invariant( 347 | !isPromiseLike(afterEachResult), 348 | '`afterEach` function must be sync when using `runSync()`' 349 | ) 350 | } 351 | } 352 | 353 | try { 354 | while ( 355 | // eslint-disable-next-line no-unmodified-loop-condition 356 | totalTime < time || 357 | samples.length < iterations 358 | ) { 359 | benchmarkTask() 360 | } 361 | } catch (error) { 362 | return { error } 363 | } 364 | 365 | if (this.fnOpts.afterAll != null) { 366 | try { 367 | const afterAllResult = this.fnOpts.afterAll.call(this, mode) 368 | invariant( 369 | !isPromiseLike(afterAllResult), 370 | '`afterAll` function must be sync when using `runSync()`' 371 | ) 372 | } catch (error) { 373 | return { error } 374 | } 375 | } 376 | return { samples } 377 | } 378 | 379 | /** 380 | * merge into the result object values 381 | * @param result - the task result object to merge with the current result object values 382 | */ 383 | private mergeTaskResult (result: Partial): void { 384 | this.result = Object.freeze({ 385 | ...this.result, 386 | ...result, 387 | }) as Readonly 388 | } 389 | 390 | private postWarmup (error: Error | undefined): void { 391 | if (error) { 392 | this.mergeTaskResult({ error }) 393 | this.dispatchEvent(createErrorEvent(this, error)) 394 | this.bench.dispatchEvent(createErrorEvent(this, error)) 395 | if (this.bench.opts.throws) { 396 | throw error 397 | } 398 | } 399 | } 400 | 401 | private processRunResult ({ 402 | error, 403 | latencySamples, 404 | }: { 405 | error?: Error 406 | latencySamples?: number[] 407 | }): void { 408 | if (latencySamples) { 409 | this.runs = latencySamples.length 410 | const totalTime = latencySamples.reduce((a, b) => a + b, 0) 411 | 412 | // Latency statistics 413 | const latencyStatistics = getStatisticsSorted( 414 | latencySamples.sort((a, b) => a - b) 415 | ) 416 | 417 | // Throughput statistics 418 | const throughputSamples = latencySamples 419 | .map(sample => 420 | sample !== 0 ? 1000 / sample : 1000 / latencyStatistics.mean 421 | ) // Use latency average as imputed sample 422 | .sort((a, b) => a - b) 423 | const throughputStatistics = getStatisticsSorted(throughputSamples) 424 | 425 | if (this.bench.opts.signal?.aborted) { 426 | return 427 | } 428 | 429 | this.mergeTaskResult({ 430 | critical: latencyStatistics.critical, 431 | df: latencyStatistics.df, 432 | hz: throughputStatistics.mean, 433 | latency: latencyStatistics, 434 | max: latencyStatistics.max, 435 | mean: latencyStatistics.mean, 436 | min: latencyStatistics.min, 437 | moe: latencyStatistics.moe, 438 | p75: latencyStatistics.p75, 439 | p99: latencyStatistics.p99, 440 | p995: latencyStatistics.p995, 441 | p999: latencyStatistics.p999, 442 | period: totalTime / this.runs, 443 | rme: latencyStatistics.rme, 444 | runtime: this.bench.runtime, 445 | runtimeVersion: this.bench.runtimeVersion, 446 | samples: latencyStatistics.samples, 447 | sd: latencyStatistics.sd, 448 | sem: latencyStatistics.sem, 449 | throughput: throughputStatistics, 450 | totalTime, 451 | variance: latencyStatistics.variance, 452 | }) 453 | } 454 | 455 | if (error) { 456 | this.mergeTaskResult({ error }) 457 | this.dispatchEvent(createErrorEvent(this, error)) 458 | this.bench.dispatchEvent(createErrorEvent(this, error)) 459 | if (this.bench.opts.throws) { 460 | throw error 461 | } 462 | } 463 | 464 | this.dispatchEvent(createBenchEvent('cycle', this)) 465 | this.bench.dispatchEvent(createBenchEvent('cycle', this)) 466 | // cycle and complete are equal in Task 467 | this.dispatchEvent(createBenchEvent('complete', this)) 468 | } 469 | } 470 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Task } from '../src/task' 2 | import type { JSRuntime } from './utils' 3 | 4 | export type AddEventListenerOptionsArgument = Parameters< 5 | typeof EventTarget.prototype.addEventListener 6 | >[2] 7 | 8 | /** 9 | * bench event 10 | */ 11 | export type BenchEvent = Event & { error?: Error; task?: Task } 12 | 13 | /** 14 | * Bench events 15 | */ 16 | export type BenchEvents = 17 | | 'abort' // when a signal aborts 18 | | 'add' // when a task gets added to the Bench instance 19 | | 'complete' // when running a benchmark finishes 20 | | 'cycle' // when running each benchmark task gets done 21 | | 'error' // when the benchmark task throws 22 | | 'remove' // when a task gets removed of the Bench instance 23 | | 'reset' // when the reset method gets called 24 | | 'start' // when running the benchmarks gets started 25 | | 'warmup' // when the benchmarks start getting warmed up 26 | 27 | export interface BenchEventsMap { 28 | abort: EventListener 29 | add: EventListener 30 | complete: EventListener 31 | cycle: EventListener 32 | error: EventListener 33 | remove: EventListener 34 | reset: EventListener 35 | start: EventListener 36 | warmup: EventListener 37 | } 38 | 39 | /** 40 | * Both the `Task` and `Bench` objects extend the `EventTarget` object. 41 | * So you can attach a listeners to different types of events to each class instance 42 | * using the universal `addEventListener` and `removeEventListener` methods. 43 | */ 44 | 45 | /** 46 | * bench options 47 | */ 48 | export interface BenchOptions { 49 | /** 50 | * number of times that a task should run if even the time option is finished @default 64 51 | */ 52 | iterations?: number 53 | 54 | /** 55 | * benchmark name 56 | */ 57 | name?: string 58 | 59 | /** 60 | * function to get the current timestamp in milliseconds 61 | */ 62 | now?: () => number 63 | 64 | /** 65 | * setup function to run before each benchmark task (cycle) 66 | */ 67 | setup?: Hook 68 | 69 | /** 70 | * An AbortSignal for aborting the benchmark 71 | */ 72 | signal?: AbortSignal 73 | 74 | /** 75 | * teardown function to run after each benchmark task (cycle) 76 | */ 77 | teardown?: Hook 78 | 79 | /** 80 | * Throws if a task fails @default false 81 | */ 82 | throws?: boolean 83 | 84 | /** 85 | * time needed for running a benchmark task (milliseconds) @default 1000 86 | */ 87 | time?: number 88 | 89 | /** 90 | * warmup benchmark @default true 91 | */ 92 | warmup?: boolean 93 | 94 | /** 95 | * warmup iterations @default 16 96 | */ 97 | warmupIterations?: number 98 | 99 | /** 100 | * warmup time (milliseconds) @default 250 101 | */ 102 | warmupTime?: number 103 | } 104 | 105 | /** 106 | * event listener 107 | */ 108 | export type EventListener = (evt: BenchEvent) => void 109 | 110 | /** 111 | * the task function 112 | */ 113 | // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents 114 | export type Fn = () => Promise | unknown 115 | 116 | /** 117 | * The task hook function signature. 118 | * If warmup is enabled, the hook will be called twice, once for the warmup and once for the run. 119 | * @param mode the mode where the hook is being called 120 | */ 121 | export type FnHook = ( 122 | this: Task, 123 | mode?: 'run' | 'warmup' 124 | ) => Promise | void 125 | 126 | /** 127 | * the task function options 128 | */ 129 | export interface FnOptions { 130 | /** 131 | * An optional function that is run after all iterations of this task end 132 | */ 133 | afterAll?: FnHook 134 | 135 | /** 136 | * An optional function that is run after each iteration of this task 137 | */ 138 | afterEach?: FnHook 139 | 140 | /** 141 | * An optional function that is run before iterations of this task begin 142 | */ 143 | beforeAll?: FnHook 144 | 145 | /** 146 | * An optional function that is run before each iteration of this task 147 | */ 148 | beforeEach?: FnHook 149 | } 150 | 151 | /** 152 | * The hook function signature. 153 | * If warmup is enabled, the hook will be called twice, once for the warmup and once for the run. 154 | * @param task the task instance 155 | * @param mode the mode where the hook is being called 156 | */ 157 | export type Hook = ( 158 | task?: Task, 159 | mode?: 'run' | 'warmup' 160 | ) => Promise | void 161 | 162 | // @types/node doesn't have these types globally, and we don't want to bring "dom" lib for everyone 163 | export type RemoveEventListenerOptionsArgument = Parameters< 164 | typeof EventTarget.prototype.removeEventListener 165 | >[2] 166 | 167 | /** 168 | * the statistics object 169 | */ 170 | export interface Statistics { 171 | /** 172 | * mean/average absolute deviation 173 | */ 174 | aad: number | undefined 175 | 176 | /** 177 | * critical value 178 | */ 179 | critical: number 180 | 181 | /** 182 | * degrees of freedom 183 | */ 184 | df: number 185 | 186 | /** 187 | * median absolute deviation 188 | */ 189 | mad: number | undefined 190 | 191 | /** 192 | * the maximum value 193 | */ 194 | max: number 195 | 196 | /** 197 | * mean/average 198 | */ 199 | mean: number 200 | 201 | /** 202 | * the minimum value 203 | */ 204 | min: number 205 | 206 | /** 207 | * margin of error 208 | */ 209 | moe: number 210 | 211 | /** 212 | * p50/median percentile 213 | */ 214 | p50: number | undefined 215 | 216 | /** 217 | * p75 percentile 218 | */ 219 | p75: number | undefined 220 | 221 | /** 222 | * p99 percentile 223 | */ 224 | p99: number | undefined 225 | 226 | /** 227 | * p995 percentile 228 | */ 229 | p995: number | undefined 230 | 231 | /** 232 | * p999 percentile 233 | */ 234 | p999: number | undefined 235 | 236 | /** 237 | * relative margin of error 238 | */ 239 | rme: number 240 | 241 | /** 242 | * samples 243 | */ 244 | samples: number[] 245 | 246 | /** 247 | * standard deviation 248 | */ 249 | sd: number 250 | 251 | /** 252 | * standard error of the mean/average (a.k.a. the standard deviation of the distribution of the sample mean/average) 253 | */ 254 | sem: number 255 | 256 | /** 257 | * variance 258 | */ 259 | variance: number 260 | } 261 | 262 | /** 263 | * task events 264 | */ 265 | export type TaskEvents = 266 | | 'abort' 267 | | 'complete' 268 | | 'cycle' 269 | | 'error' 270 | | 'reset' 271 | | 'start' 272 | | 'warmup' 273 | 274 | export interface TaskEventsMap { 275 | abort: EventListener 276 | complete: EventListener 277 | cycle: EventListener 278 | error: EventListener 279 | reset: EventListener 280 | start: EventListener 281 | warmup: EventListener 282 | } 283 | /** 284 | * the task result object 285 | */ 286 | export interface TaskResult { 287 | /** 288 | * the latency samples critical value 289 | * @deprecated use `.latency.critical` instead 290 | */ 291 | critical: number 292 | 293 | /** 294 | * the latency samples degrees of freedom 295 | * @deprecated use `.latency.df` instead 296 | */ 297 | df: number 298 | 299 | /** 300 | * the last task error that was thrown 301 | */ 302 | error?: Error 303 | 304 | /** 305 | * the number of operations per second 306 | * @deprecated use `.throughput.mean` instead 307 | */ 308 | hz: number 309 | 310 | /** 311 | * the task latency statistics 312 | */ 313 | latency: Statistics 314 | 315 | /** 316 | * the maximum latency samples value 317 | * @deprecated use `.latency.max` instead 318 | */ 319 | max: number 320 | 321 | /** 322 | * the latency samples mean/average 323 | * @deprecated use `.latency.mean` instead 324 | */ 325 | mean: number 326 | 327 | /** 328 | * the minimum latency samples value 329 | * @deprecated use `.latency.min` instead 330 | */ 331 | min: number 332 | 333 | /** 334 | * the latency samples margin of error 335 | * @deprecated use `.latency.moe` instead 336 | */ 337 | moe: number 338 | 339 | /** 340 | * the latency samples p75 percentile 341 | * @deprecated use `.latency.p75` instead 342 | */ 343 | p75: number 344 | 345 | /** 346 | * the latency samples p99 percentile 347 | * @deprecated use `.latency.p99` instead 348 | */ 349 | p99: number 350 | 351 | /** 352 | * the latency samples p995 percentile 353 | * @deprecated use `.latency.p995` instead 354 | */ 355 | p995: number 356 | 357 | /** 358 | * the latency samples p999 percentile 359 | * @deprecated use `.latency.p999` instead 360 | */ 361 | p999: number 362 | 363 | /** 364 | * how long each operation takes (ms) 365 | */ 366 | period: number 367 | 368 | /** 369 | * the latency samples relative margin of error 370 | * @deprecated use `.latency.rme` instead 371 | */ 372 | rme: number 373 | 374 | /** 375 | * the JavaScript runtime environment 376 | */ 377 | runtime: 'unknown' | JSRuntime 378 | 379 | /** 380 | * the JavaScript runtime version 381 | */ 382 | runtimeVersion: string 383 | 384 | /** 385 | * latency samples (ms) 386 | * @deprecated use `.latency.samples` instead 387 | */ 388 | samples: number[] 389 | 390 | /** 391 | * the latency samples standard deviation 392 | * @deprecated use `.latency.sd` instead 393 | */ 394 | sd: number 395 | 396 | /** 397 | * the latency standard error of the mean (a.k.a. the standard deviation of the distribution of the sample mean/average) 398 | * @deprecated use `.latency.sem` instead 399 | */ 400 | sem: number 401 | 402 | /** 403 | * the task throughput statistics 404 | */ 405 | throughput: Statistics 406 | 407 | /** 408 | * the time to run the task benchmark cycle (ms) 409 | */ 410 | totalTime: number 411 | 412 | /** 413 | * the latency samples variance 414 | * @deprecated use `.latency.variance` instead 415 | */ 416 | variance: number 417 | } 418 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | // Portions copyright evanwashere. 2024. All Rights Reserved. 2 | // eslint-disable-next-line @cspell/spellchecker 3 | // Portions copyright QuiiBz. 2023-2024. All Rights Reserved. 4 | 5 | import type { Fn, Statistics } from './types' 6 | 7 | import { emptyFunction, tTable } from './constants' 8 | 9 | /** 10 | * The JavaScript runtime environment. 11 | * @see https://runtime-keys.proposal.wintercg.org/ 12 | */ 13 | export enum JSRuntime { 14 | browser = 'browser', 15 | bun = 'bun', 16 | deno = 'deno', 17 | 'edge-light' = 'edge-light', 18 | fastly = 'fastly', 19 | hermes = 'hermes', 20 | jsc = 'jsc', 21 | lagon = 'lagon', 22 | moddable = 'moddable', 23 | netlify = 'netlify', 24 | node = 'node', 25 | 'quickjs-ng' = 'quickjs-ng', 26 | spidermonkey = 'spidermonkey', 27 | v8 = 'v8', 28 | workerd = 'workerd', 29 | } 30 | 31 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unnecessary-condition 32 | const isBun = !!(globalThis as any).Bun || !!globalThis.process?.versions?.bun 33 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access 34 | const isDeno = !!(globalThis as any).Deno 35 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 36 | const isNode = globalThis.process?.release?.name === 'node' 37 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access 38 | const isHermes = !!(globalThis as any).HermesInternal 39 | const isWorkerd = 40 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access 41 | (globalThis as any).navigator?.userAgent === 'Cloudflare-Workers' 42 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call 43 | const isQuickJsNg = !!(globalThis as any).navigator?.userAgent 44 | ?.toLowerCase?.() 45 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 46 | ?.includes?.('quickjs-ng') 47 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access 48 | const isNetlify = typeof (globalThis as any).Netlify === 'object' 49 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access 50 | const isEdgeLight = typeof (globalThis as any).EdgeRuntime === 'string' 51 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access 52 | const isLagon = !!(globalThis as any).__lagon__ 53 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access 54 | const isFastly = !!(globalThis as any).fastly 55 | const isModdable = 56 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access 57 | !!(globalThis as any).$262 && 58 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access 59 | !!(globalThis as any).lockdown && 60 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access 61 | !!(globalThis as any).AsyncDisposableStack 62 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access 63 | const isV8 = !!(globalThis as any).d8 64 | const isSpiderMonkey = 65 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access 66 | !!(globalThis as any).inIon && !!(globalThis as any).performance?.mozMemory 67 | const isJsc = 68 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access 69 | !!(globalThis as any).$ && 70 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @cspell/spellchecker 71 | 'IsHTMLDDA' in (globalThis as any).$ 72 | const isBrowser = 73 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access 74 | !!(globalThis as any).window && !!(globalThis as any).navigator 75 | 76 | export const runtime: 'unknown' | JSRuntime = (() => { 77 | if (isBun) return JSRuntime.bun 78 | if (isDeno) return JSRuntime.deno 79 | if (isNode) return JSRuntime.node 80 | if (isHermes) return JSRuntime.hermes 81 | if (isNetlify) return JSRuntime.netlify 82 | if (isEdgeLight) return JSRuntime['edge-light'] 83 | if (isLagon) return JSRuntime.lagon 84 | if (isFastly) return JSRuntime.fastly 85 | if (isWorkerd) return JSRuntime.workerd 86 | if (isQuickJsNg) return JSRuntime['quickjs-ng'] 87 | if (isModdable) return JSRuntime.moddable 88 | if (isV8) return JSRuntime.v8 89 | if (isSpiderMonkey) return JSRuntime.spidermonkey 90 | if (isJsc) return JSRuntime.jsc 91 | if (isBrowser) return JSRuntime.browser 92 | return 'unknown' 93 | })() 94 | 95 | export const runtimeVersion: string = (() => { 96 | if (runtime === JSRuntime.bun) { 97 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access 98 | return (globalThis as any).Bun?.version as string 99 | } 100 | if (runtime === JSRuntime.deno) { 101 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access 102 | return (globalThis as any).Deno?.version?.deno as string 103 | } 104 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 105 | if (runtime === JSRuntime.node) return globalThis.process?.versions?.node 106 | if (runtime === JSRuntime.hermes) { 107 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call 108 | return (globalThis as any).HermesInternal?.getRuntimeProperties?.()?.[ 109 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 110 | 'OSS Release Version' 111 | ] as string 112 | } 113 | if (runtime === JSRuntime.v8) { 114 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call 115 | return (globalThis as any).version?.() as string 116 | } 117 | if (runtime === JSRuntime['quickjs-ng']) { 118 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call 119 | return (globalThis as any).navigator?.userAgent?.split?.('/')[1] as string 120 | } 121 | return 'unknown' 122 | })() 123 | 124 | /** 125 | * Converts nanoseconds to milliseconds. 126 | * @param ns - the nanoseconds to convert 127 | * @returns the milliseconds 128 | */ 129 | export const nToMs = (ns: number) => ns / 1e6 130 | 131 | /** 132 | * Converts milliseconds to nanoseconds. 133 | * @param ms - the milliseconds to convert 134 | * @returns the nanoseconds 135 | */ 136 | export const mToNs = (ms: number) => ms * 1e6 137 | 138 | /** 139 | * @param x number to format 140 | * @param targetDigits number of digits in the output to aim for 141 | * @param maxFractionDigits hard limit for the number of digits after the decimal dot 142 | * @returns formatted number 143 | */ 144 | export const formatNumber = ( 145 | x: number, 146 | targetDigits: number, 147 | maxFractionDigits: number 148 | ): string => { 149 | // Round large numbers to integers, but not to multiples of 10. 150 | // The actual number of significant digits may be more than `targetDigits`. 151 | if (Math.abs(x) >= 10 ** targetDigits) { 152 | return x.toFixed() 153 | } 154 | 155 | // Round small numbers to have `maxFractionDigits` digits after the decimal dot. 156 | // The actual number of significant digits may be less than `targetDigits`. 157 | if (Math.abs(x) < 10 ** (targetDigits - maxFractionDigits)) { 158 | return x.toFixed(maxFractionDigits) 159 | } 160 | 161 | // Round medium magnitude numbers to have exactly `targetDigits` significant digits. 162 | return x.toPrecision(targetDigits) 163 | } 164 | 165 | let hrtimeBigint: () => bigint 166 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access 167 | if (typeof (globalThis as any).process?.hrtime?.bigint === 'function') { 168 | hrtimeBigint = globalThis.process.hrtime.bigint.bind(process.hrtime) 169 | } else { 170 | hrtimeBigint = () => { 171 | throw new Error('hrtime.bigint() is not supported in this JS environment') 172 | } 173 | } 174 | /** 175 | * Returns the current high resolution timestamp in milliseconds using `process.hrtime.bigint()`. 176 | * @returns the current high resolution timestamp in milliseconds 177 | */ 178 | export const hrtimeNow = () => nToMs(Number(hrtimeBigint())) 179 | 180 | const performanceNow = performance.now.bind(performance) 181 | export const now = performanceNow 182 | 183 | /** 184 | * Checks if a value is a promise-like object. 185 | * @param maybePromiseLike - the value to check 186 | * @returns true if the value is a promise-like object 187 | */ 188 | export const isPromiseLike = ( 189 | maybePromiseLike: unknown 190 | ): maybePromiseLike is PromiseLike => 191 | maybePromiseLike !== null && 192 | typeof maybePromiseLike === 'object' && 193 | typeof (maybePromiseLike as PromiseLike).then === 'function' 194 | 195 | type AsyncFunctionType = (...args: A) => PromiseLike 196 | 197 | /** 198 | * An async function check helper only considering runtime support async syntax 199 | * @param fn - the function to check 200 | * @returns true if the function is an async function 201 | */ 202 | const isAsyncFunction = ( 203 | fn: Fn | null | undefined 204 | ): fn is AsyncFunctionType => 205 | // eslint-disable-next-line @typescript-eslint/no-empty-function 206 | fn?.constructor === (async () => {}).constructor 207 | 208 | /** 209 | * An async function check helper considering runtime support async syntax and promise return 210 | * @param fn - the function to check 211 | * @returns true if the function is an async function or returns a promise 212 | */ 213 | export const isFnAsyncResource = (fn: Fn | null | undefined): boolean => { 214 | if (fn == null) { 215 | return false 216 | } 217 | if (isAsyncFunction(fn)) { 218 | return true 219 | } 220 | try { 221 | const fnCall = fn() 222 | const promiseLike = isPromiseLike(fnCall) 223 | if (promiseLike) { 224 | // silence promise rejection 225 | try { 226 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 227 | (fnCall as Promise).then(emptyFunction)?.catch(emptyFunction) 228 | } catch { 229 | // ignore 230 | } 231 | } 232 | return promiseLike 233 | } catch { 234 | return false 235 | } 236 | } 237 | 238 | /** 239 | * Computes the average of a sample. 240 | * @param samples - the sample 241 | * @returns the average of the sample 242 | * @throws if the sample is empty 243 | */ 244 | const average = (samples: number[]) => { 245 | if (samples.length === 0) { 246 | throw new Error('samples must not be empty') 247 | } 248 | return samples.reduce((a, b) => a + b, 0) / samples.length || 0 249 | } 250 | 251 | /** 252 | * Computes the variance of a sample with Bessel's correction. 253 | * @param samples - the sample 254 | * @param avg - the average of the sample 255 | * @returns the variance of the sample 256 | */ 257 | const variance = (samples: number[], avg = average(samples)) => { 258 | const result = samples.reduce((sum, n) => sum + (n - avg) ** 2, 0) 259 | return result / (samples.length - 1) || 0 260 | } 261 | 262 | /** 263 | * Computes the q-quantile of a sorted sample. 264 | * @param samples - the sorted sample 265 | * @param q - the quantile to compute 266 | * @returns the q-quantile of the sample 267 | * @throws if the sample is empty 268 | */ 269 | const quantileSorted = (samples: number[], q: number) => { 270 | if (samples.length === 0) { 271 | throw new Error('samples must not be empty') 272 | } 273 | if (q < 0 || q > 1) { 274 | throw new Error('q must be between 0 and 1') 275 | } 276 | if (q === 0) { 277 | return samples[0] 278 | } 279 | if (q === 1) { 280 | return samples[samples.length - 1] 281 | } 282 | const base = (samples.length - 1) * q 283 | const baseIndex = Math.floor(base) 284 | if (samples[baseIndex + 1] != null) { 285 | return ( 286 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 287 | samples[baseIndex]! + 288 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 289 | (base - baseIndex) * (samples[baseIndex + 1]! - samples[baseIndex]!) 290 | ) 291 | } 292 | return samples[baseIndex] 293 | } 294 | 295 | /** 296 | * Computes the median of a sorted sample. 297 | * @param samples - the sorted sample 298 | * @returns the median of the sample 299 | */ 300 | const medianSorted = (samples: number[]) => quantileSorted(samples, 0.5) 301 | 302 | /** 303 | * Computes the median of a sample. 304 | * @param samples - the sample 305 | * @returns the median of the sample 306 | */ 307 | const median = (samples: number[]) => { 308 | return medianSorted(samples.sort((a, b) => a - b)) 309 | } 310 | 311 | /** 312 | * Computes the absolute deviation of a sample given an aggregation. 313 | * @param samples - the sample 314 | * @param aggFn - the aggregation function to use 315 | * @param aggValue - the aggregated value to use 316 | * @returns the absolute deviation of the sample given the aggregation 317 | */ 318 | const absoluteDeviation = ( 319 | samples: number[], 320 | aggFn: (arr: number[]) => number | undefined, 321 | aggValue = aggFn(samples) 322 | ) => { 323 | const absoluteDeviations: number[] = [] 324 | 325 | for (const sample of samples) { 326 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 327 | absoluteDeviations.push(Math.abs(sample - aggValue!)) 328 | } 329 | 330 | return aggFn(absoluteDeviations) 331 | } 332 | 333 | /** 334 | * Computes the statistics of a sample. 335 | * The sample must be sorted. 336 | * @param samples - the sorted sample 337 | * @returns the statistics of the sample 338 | * @throws if the sample is empty 339 | */ 340 | export const getStatisticsSorted = (samples: number[]): Statistics => { 341 | const mean = average(samples) 342 | const vr = variance(samples, mean) 343 | const sd = Math.sqrt(vr) 344 | const sem = sd / Math.sqrt(samples.length) 345 | const df = samples.length - 1 346 | // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing, @typescript-eslint/no-non-null-assertion 347 | const critical = tTable[(Math.round(df) || 1).toString()] || tTable.infinity! 348 | const moe = sem * critical 349 | const rme = (moe / mean) * 100 350 | const p50 = medianSorted(samples) 351 | return { 352 | aad: absoluteDeviation(samples, average, mean), 353 | critical, 354 | df, 355 | mad: absoluteDeviation(samples, median, p50), 356 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 357 | max: samples[df]!, 358 | mean, 359 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 360 | min: samples[0]!, 361 | moe, 362 | p50, 363 | p75: quantileSorted(samples, 0.75), 364 | p99: quantileSorted(samples, 0.99), 365 | p995: quantileSorted(samples, 0.995), 366 | p999: quantileSorted(samples, 0.999), 367 | rme, 368 | samples, 369 | sd, 370 | sem, 371 | variance: vr, 372 | } 373 | } 374 | 375 | export const invariant = (condition: boolean, message: string): void => { 376 | if (!condition) { 377 | throw new Error(message) 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /test/isFnAsyncResource.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest' 2 | 3 | import { isFnAsyncResource } from '../src/utils' 4 | 5 | test('isFnAsyncResource undefined', () => { 6 | expect(isFnAsyncResource(undefined)).toBe(false) 7 | }) 8 | 9 | test('isFnAsyncResource null', () => { 10 | expect(isFnAsyncResource(null)).toBe(false) 11 | }) 12 | 13 | test('isFnAsyncResource sync', () => { 14 | expect(isFnAsyncResource(() => 1)).toBe(false) 15 | }) 16 | 17 | test('isFnAsyncResource async', () => { 18 | expect(isFnAsyncResource(async () => Promise.resolve(1))).toBe(true) 19 | }) 20 | 21 | test('isFnAsyncResource promise', () => { 22 | expect(isFnAsyncResource(() => Promise.resolve(1))).toBe(true) 23 | }) 24 | 25 | test('isFnAsyncResource promise with error', () => { 26 | expect(isFnAsyncResource(() => Promise.reject(new Error('error')))).toBe(true) 27 | }) 28 | 29 | test('isFnAsyncResource async promise with error', () => { 30 | expect( 31 | isFnAsyncResource(async () => Promise.reject(new Error('error'))) 32 | ).toBe(true) 33 | }) 34 | 35 | test('isFnAsyncResource promiseLike', () => { 36 | expect( 37 | isFnAsyncResource(() => ({ 38 | then: () => 1, 39 | })) 40 | ).toBe(true) 41 | expect( 42 | isFnAsyncResource(() => ({ then: async () => Promise.resolve(1) })) 43 | ).toBe(true) 44 | expect(isFnAsyncResource(() => ({ then: () => Promise.resolve(1) }))).toBe( 45 | true 46 | ) 47 | expect( 48 | isFnAsyncResource(() => ({ 49 | then: () => Promise.reject(new Error('error')), 50 | })) 51 | ).toBe(true) 52 | expect( 53 | isFnAsyncResource(() => ({ 54 | then: async () => Promise.reject(new Error('error')), 55 | })) 56 | ).toBe(true) 57 | }) 58 | -------------------------------------------------------------------------------- /test/sequential.test.ts: -------------------------------------------------------------------------------- 1 | import { randomInt } from 'node:crypto' 2 | import { setTimeout } from 'node:timers/promises' 3 | import { expect, test } from 'vitest' 4 | 5 | import { Bench } from '../src' 6 | 7 | test.each(['warmup', 'run'])('%s sequential', async mode => { 8 | const iterations = 1 9 | const sequentialBench = new Bench({ 10 | iterations, 11 | throws: true, 12 | time: 0, 13 | warmup: mode === 'warmup', 14 | warmupIterations: iterations, 15 | warmupTime: 0, 16 | }) 17 | 18 | const benchTasks: string[] = [] 19 | sequentialBench 20 | .add('sample 1', async () => { 21 | await setTimeout(randomInt(0, 100)) 22 | benchTasks.push('sample 1') 23 | if (mode === 'run') { 24 | expect(benchTasks).toStrictEqual(['sample 1']) 25 | } 26 | }) 27 | .add('sample 2', async () => { 28 | await setTimeout(randomInt(0, 100)) 29 | benchTasks.push('sample 2') 30 | if (mode === 'run') { 31 | expect(benchTasks).toStrictEqual(['sample 1', 'sample 2']) 32 | } 33 | }) 34 | .add('sample 3', async () => { 35 | await setTimeout(randomInt(0, 100)) 36 | benchTasks.push('sample 3') 37 | if (mode === 'run') { 38 | expect(benchTasks).toStrictEqual(['sample 1', 'sample 2', 'sample 3']) 39 | } 40 | }) 41 | 42 | const tasks = await sequentialBench.run() 43 | 44 | if (mode === 'warmup') { 45 | expect(benchTasks).toStrictEqual([ 46 | 'sample 1', 47 | 'sample 2', 48 | 'sample 3', 49 | 'sample 1', 50 | 'sample 2', 51 | 'sample 3', 52 | ]) 53 | } else if (mode === 'run') { 54 | expect(benchTasks).toStrictEqual(['sample 1', 'sample 2', 'sample 3']) 55 | } 56 | expect(tasks.length).toBe(3) 57 | expect(benchTasks.length).toBeGreaterThanOrEqual(tasks.length) 58 | expect(tasks[0]?.name).toBe('sample 1') 59 | expect(tasks[1]?.name).toBe('sample 2') 60 | expect(tasks[2]?.name).toBe('sample 3') 61 | }) 62 | 63 | test.each(['warmup', 'run'])('%s bench concurrency', async mode => { 64 | const iterations = 128 65 | const concurrentBench = new Bench({ 66 | iterations, 67 | throws: true, 68 | time: 0, 69 | warmup: mode === 'warmup', 70 | warmupIterations: iterations, 71 | warmupTime: 0, 72 | }) 73 | expect(concurrentBench.threshold).toBe(Number.POSITIVE_INFINITY) 74 | expect(concurrentBench.concurrency).toBeNull() 75 | concurrentBench.concurrency = 'bench' 76 | expect(concurrentBench.concurrency).toBe('bench') 77 | 78 | let shouldBeDefined1: true | undefined 79 | let shouldBeDefined2: true | undefined 80 | 81 | let shouldNotBeDefinedFirst1: true | undefined 82 | let shouldNotBeDefinedFirst2: true | undefined 83 | concurrentBench 84 | .add('sample 1', async () => { 85 | shouldBeDefined1 = true 86 | await setTimeout(100) 87 | shouldNotBeDefinedFirst1 = true 88 | }) 89 | .add('sample 2', async () => { 90 | shouldBeDefined2 = true 91 | await setTimeout(100) 92 | shouldNotBeDefinedFirst2 = true 93 | }) 94 | 95 | // not awaited on purpose 96 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 97 | concurrentBench.run() 98 | 99 | await setTimeout(0) 100 | expect(shouldBeDefined1).toBeDefined() 101 | expect(shouldBeDefined2).toBeDefined() 102 | expect(shouldNotBeDefinedFirst1).toBeUndefined() 103 | expect(shouldNotBeDefinedFirst2).toBeUndefined() 104 | await setTimeout(100) 105 | expect(shouldNotBeDefinedFirst1).toBeDefined() 106 | expect(shouldNotBeDefinedFirst2).toBeDefined() 107 | }) 108 | 109 | test.each(['warmup', 'run'])('%s task concurrency', async mode => { 110 | const iterations = 16 111 | const concurrentBench = new Bench({ 112 | iterations, 113 | throws: true, 114 | time: 0, 115 | warmup: mode === 'warmup', 116 | warmupIterations: iterations, 117 | warmupTime: 0, 118 | }) 119 | expect(concurrentBench.threshold).toBe(Number.POSITIVE_INFINITY) 120 | expect(concurrentBench.concurrency).toBeNull() 121 | 122 | const taskName = 'sample 1' 123 | let runs = 0 124 | 125 | concurrentBench.add(taskName, async () => { 126 | runs++ 127 | if (concurrentBench.concurrency === 'task') { 128 | await setTimeout(10) 129 | // all task function should be here after 10ms 130 | expect([iterations, 2 * iterations]).toContain(runs) 131 | } 132 | }) 133 | 134 | await concurrentBench.run() 135 | 136 | for (const result of concurrentBench.results) { 137 | expect(result?.error).toMatchObject(/AssertionError/) 138 | } 139 | expect(concurrentBench.getTask(taskName)?.runs).toEqual(iterations) 140 | expect(runs).toEqual(mode === 'run' ? iterations : 2 * iterations) 141 | concurrentBench.reset() 142 | runs = 0 143 | expect(concurrentBench.threshold).toBe(Number.POSITIVE_INFINITY) 144 | expect(concurrentBench.concurrency).toBeNull() 145 | 146 | concurrentBench.concurrency = 'task' 147 | expect(concurrentBench.threshold).toBe(Number.POSITIVE_INFINITY) 148 | expect(concurrentBench.concurrency).toBe('task') 149 | 150 | await concurrentBench.run() 151 | 152 | for (const result of concurrentBench.results) { 153 | expect(result?.error).toBeUndefined() 154 | } 155 | expect(concurrentBench.getTask(taskName)?.runs).toEqual(iterations) 156 | expect(runs).toEqual(mode === 'run' ? iterations : 2 * iterations) 157 | }) 158 | -------------------------------------------------------------------------------- /test/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest' 2 | 3 | import { formatNumber, getStatisticsSorted } from '../src/utils' 4 | 5 | test('formatting integers', () => { 6 | expect(formatNumber(123456, 5, 2)).toBe('123456') 7 | expect(formatNumber(12345, 5, 2)).toBe('12345') 8 | expect(formatNumber(1234, 5, 2)).toBe('1234.0') 9 | expect(formatNumber(123, 5, 2)).toBe('123.00') 10 | expect(formatNumber(12, 5, 2)).toBe('12.00') 11 | expect(formatNumber(1, 5, 2)).toBe('1.00') 12 | expect(formatNumber(0, 5, 2)).toBe('0.00') 13 | expect(formatNumber(-1, 5, 2)).toBe('-1.00') 14 | }) 15 | 16 | test('formatting floats', () => { 17 | expect(formatNumber(123456.789, 5, 2)).toBe('123457') 18 | expect(formatNumber(12345.6789, 5, 2)).toBe('12346') 19 | expect(formatNumber(1234.56789, 5, 2)).toBe('1234.6') 20 | expect(formatNumber(123.456789, 5, 2)).toBe('123.46') 21 | expect(formatNumber(12.3456789, 5, 2)).toBe('12.35') 22 | expect(formatNumber(-12.3456789, 5, 2)).toBe('-12.35') 23 | }) 24 | 25 | test('statistics', () => { 26 | let stats = getStatisticsSorted([1, 2, 3, 4, 5, 6]) 27 | expect(stats.min).toBe(1) 28 | expect(stats.max).toBe(6) 29 | expect(stats.df).toBe(5) 30 | expect(stats.critical).toBeCloseTo(2.571) 31 | expect(stats.mean).toBe(3.5) 32 | expect(stats.variance).toBe(3.5) 33 | expect(stats.sd).toBeCloseTo(1.87) 34 | expect(stats.sem).toBeCloseTo(0.76) 35 | expect(stats.moe).toBeCloseTo(1.96) 36 | expect(stats.rme).toBe((stats.moe / stats.mean) * 100) 37 | expect(stats.p50).toBe(3.5) 38 | expect(stats.p75).toBe(4.75) 39 | expect(stats.p99).toBe(5.95) 40 | expect(stats.p995).toBe(5.975) 41 | expect(stats.p999).toBe(5.995) 42 | expect(stats.mad).toBe(1.5) 43 | expect(stats.aad).toBe(1.5) 44 | stats = getStatisticsSorted([1, 2, 3, 4, 5, 6, 7]) 45 | expect(stats.min).toBe(1) 46 | expect(stats.max).toBe(7) 47 | expect(stats.df).toBe(6) 48 | expect(stats.critical).toBeCloseTo(2.447) 49 | expect(stats.mean).toBe(4) 50 | expect(stats.variance).toBeCloseTo(4.666) 51 | expect(stats.sd).toBeCloseTo(2.16) 52 | expect(stats.sem).toBeCloseTo(0.816) 53 | expect(stats.moe).toBeCloseTo(1.997) 54 | expect(stats.rme).toBe((stats.moe / stats.mean) * 100) 55 | expect(stats.p50).toBe(4) 56 | expect(stats.p75).toBe(5.5) 57 | expect(stats.p99).toBeCloseTo(6.939) 58 | expect(stats.p995).toBe(6.97) 59 | expect(stats.p999).toBe(6.994) 60 | expect(stats.mad).toBe(2) 61 | expect(stats.aad).toBe(1.7142857142857142) 62 | }) 63 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "strict": true, 5 | "moduleResolution": "Bundler", 6 | "noEmit": true, 7 | "noUncheckedIndexedAccess": true, 8 | "skipLibCheck": true, 9 | "lib": [] 10 | }, 11 | "include": ["src", "test", "tsdown.config.ts"], 12 | "exclude": ["node_modules", "dist"], 13 | "removeComments": true, 14 | "newLine": "lf" 15 | } 16 | -------------------------------------------------------------------------------- /tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsdown/config' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | minify: { 6 | compress: true, 7 | mangle: true, 8 | }, 9 | }) 10 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://typedoc.org/schema.json", 3 | "tsconfig": "./tsconfig.json", 4 | "entryPoints": ["./src"], 5 | "out": "./docs", 6 | "readme": "none", 7 | "includeVersion": true 8 | } 9 | --------------------------------------------------------------------------------