├── .eslintrc.json ├── .github └── workflows │ ├── ci.yml │ ├── codspeed.yml │ └── release.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .moon ├── tasks.yml ├── toolchain.yml └── workspace.yml ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc.json ├── .vscode ├── c_cpp_properties.json ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── commitlint.config.js ├── docs └── introspection.md ├── examples ├── with-javascript-cjs │ ├── benchmark-js.js │ ├── package.json │ └── tinybench.js ├── with-javascript-esm │ ├── benchmark-js.js │ ├── package.json │ └── tinybench.js ├── with-typescript-cjs │ ├── bench │ │ ├── benchmark.js │ │ │ ├── fibo.bench.ts │ │ │ ├── foobarbaz.bench.ts │ │ │ └── index.bench.ts │ │ └── tinybench │ │ │ ├── fibo.bench.ts │ │ │ ├── foobarbaz.bench.ts │ │ │ └── index.bench.ts │ ├── package.json │ ├── src │ │ ├── fibonacci.ts │ │ └── foobarbaz.ts │ └── tsconfig.json ├── with-typescript-esm │ ├── bench │ │ ├── benchmark.js │ │ │ ├── fibo.bench.ts │ │ │ ├── foobarbaz.bench.ts │ │ │ └── index.bench.ts │ │ └── tinybench │ │ │ ├── fibo.bench.ts │ │ │ ├── foobarbaz.bench.ts │ │ │ └── index.bench.ts │ ├── package.json │ ├── src │ │ ├── fibonacci.bench.ts │ │ ├── fibonacci.test.ts │ │ ├── fibonacci.ts │ │ └── foobarbaz.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── with-typescript-simple-cjs │ ├── benchmark-js.ts │ ├── package.json │ └── tinybench.ts └── with-typescript-simple-esm │ ├── benchmark-js.ts │ ├── package.json │ └── tinybench.ts ├── lerna.json ├── package.json ├── packages ├── benchmark.js-plugin │ ├── README.md │ ├── babel.config.js │ ├── benches │ │ ├── parsePr.ts │ │ └── sample.ts │ ├── jest.config.integ.js │ ├── jest.config.js │ ├── moon.yml │ ├── package.json │ ├── rollup.config.ts │ ├── src │ │ ├── __tests__ │ │ │ └── buildSuiteAdd.test.ts │ │ ├── buildSuiteAdd.ts │ │ ├── getCallingFile.ts │ │ ├── index.ts │ │ └── types.ts │ ├── tests │ │ ├── __snapshots__ │ │ │ └── index.integ.test.ts.snap │ │ ├── index.integ.test.ts │ │ ├── registerBenchmarks.ts │ │ └── registerOtherBenchmarks.ts │ ├── tsconfig.json │ └── tsconfig.test.json ├── core │ ├── .gitignore │ ├── README.md │ ├── binding.gyp │ ├── jest.config.integ.js │ ├── jest.config.js │ ├── moon.yml │ ├── package.json │ ├── rollup.config.ts │ ├── src │ │ ├── index.ts │ │ ├── introspection.ts │ │ ├── mongoMeasurement.ts │ │ ├── native_core │ │ │ ├── index.ts │ │ │ ├── linux_perf │ │ │ │ ├── linux_perf.cc │ │ │ │ ├── linux_perf.h │ │ │ │ ├── linux_perf.ts │ │ │ │ ├── linux_perf_listener.cc │ │ │ │ └── utils.h │ │ │ ├── measurement │ │ │ │ ├── measurement.cc │ │ │ │ ├── measurement.h │ │ │ │ └── measurement.ts │ │ │ └── native_core.cc │ │ ├── optimization.ts │ │ └── utils.ts │ ├── tests │ │ └── index.integ.test.ts │ ├── tracer.spec.json │ ├── tsconfig.json │ └── tsconfig.test.json ├── tinybench-plugin │ ├── README.md │ ├── benches │ │ ├── parsePr.ts │ │ └── sample.ts │ ├── jest.config.integ.js │ ├── jest.config.js │ ├── moon.yml │ ├── package.json │ ├── rollup.config.ts │ ├── src │ │ └── index.ts │ ├── tests │ │ ├── __snapshots__ │ │ │ └── index.integ.test.ts.snap │ │ ├── index.integ.test.ts │ │ ├── registerBenchmarks.ts │ │ └── registerOtherBenchmarks.ts │ ├── tsconfig.json │ └── tsconfig.test.json └── vitest-plugin │ ├── README.md │ ├── benches │ ├── flat.bench.ts │ ├── hooks.bench.ts │ ├── parsePr.bench.ts │ └── parsePr.ts │ ├── moon.yml │ ├── package.json │ ├── rollup.config.ts │ ├── src │ ├── __tests__ │ │ ├── globalSetup.test.ts │ │ ├── index.test.ts │ │ └── runner.test.ts │ ├── globalSetup.ts │ ├── index.ts │ └── runner.ts │ ├── tsconfig.json │ ├── tsconfig.test.json │ └── vitest.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── rollup.options.js ├── scripts └── release.sh ├── tsconfig.base.json └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/recommended", 6 | "plugin:import/recommended", 7 | "plugin:import/typescript" 8 | ], 9 | "ignorePatterns": [ 10 | "**/dist/**", 11 | "**/node_modules/**", 12 | "**/rollup.config.ts", 13 | "**/jest.config.js" 14 | ], 15 | "settings": { 16 | "import/parsers": { 17 | "@typescript-eslint/parser": [".ts", ".tsx"] 18 | }, 19 | "import/resolver": { 20 | "typescript": { 21 | "alwaysTryTypes": true, 22 | "project": [ 23 | "tsconfig.json", 24 | "packages/*/tsconfig.json", 25 | "packages/*/tsconfig.*.json" 26 | ] 27 | } 28 | } 29 | }, 30 | "rules": { 31 | "import/no-named-as-default": "off", 32 | "import/no-named-as-default-member": "off" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | on: 3 | push: 4 | branches: 5 | - "main" 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | check: 11 | runs-on: "ubuntu-latest" 12 | steps: 13 | - uses: "actions/checkout@v4" 14 | with: 15 | fetch-depth: 0 16 | - name: Install valgrind 17 | run: | 18 | sudo apt-get update 19 | sudo apt-get install -y valgrind 20 | - uses: pnpm/action-setup@v2 21 | - uses: actions/setup-node@v3 22 | with: 23 | cache: pnpm 24 | node-version-file: .nvmrc 25 | - run: pnpm install --frozen-lockfile --prefer-offline 26 | - run: pnpm moon check --all 27 | 28 | list-examples: 29 | runs-on: "ubuntu-latest" 30 | name: List examples 31 | outputs: 32 | examples: ${{ steps.list-examples.outputs.examples }} 33 | steps: 34 | - uses: "actions/checkout@v4" 35 | # list the directories in ./examples and output them to a github action workflow variables as a JSON array 36 | - run: | 37 | examples=$(find ./examples -maxdepth 1 -mindepth 1 -type d -printf '%f\n' | jq -R -s -c 'split("\n") | map(select(length > 0))') 38 | echo "::set-output name=examples::$examples" 39 | id: list-examples 40 | 41 | node-versions: 42 | runs-on: "ubuntu-latest" 43 | name: "${{ matrix.example }} on Node ${{ matrix.node-version }}" 44 | needs: list-examples 45 | strategy: 46 | matrix: 47 | node-version: ["16", "18", "20.5.1"] 48 | example: ${{ fromJson(needs.list-examples.outputs.examples) }} 49 | fail-fast: false 50 | steps: 51 | - uses: "actions/checkout@v4" 52 | with: 53 | fetch-depth: 0 54 | - name: Install valgrind 55 | run: | 56 | sudo apt-get update 57 | sudo apt-get install -y valgrind 58 | - uses: pnpm/action-setup@v2 59 | - uses: actions/setup-node@v3 60 | with: 61 | cache: pnpm 62 | node-version: ${{ matrix.node-version }} 63 | - run: pnpm install --frozen-lockfile --prefer-offline 64 | - run: pnpm moon run :build 65 | 66 | - name: Run benchmarks with tinybench-plugin 67 | # use version from `main` branch to always test the latest version, in real projects, use a tag, like `@v2` 68 | uses: CodSpeedHQ/action@main 69 | with: 70 | run: pnpm --filter ${{ matrix.example }} bench-tinybench 71 | env: 72 | CODSPEED_SKIP_UPLOAD: true 73 | CODSPEED_DEBUG: true 74 | - name: Run benchmarks with benchmark.js-plugin 75 | # use version from `main` branch to always test the latest version, in real projects, use a tag, like `@v2` 76 | uses: CodSpeedHQ/action@main 77 | with: 78 | run: pnpm --filter ${{ matrix.example }} bench-benchmark-js 79 | env: 80 | CODSPEED_SKIP_UPLOAD: true 81 | CODSPEED_DEBUG: true 82 | -------------------------------------------------------------------------------- /.github/workflows/codspeed.yml: -------------------------------------------------------------------------------- 1 | name: CodSpeed 2 | on: 3 | push: 4 | branches: 5 | - "main" 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | codspeed: 11 | name: Run CodSpeed 12 | runs-on: "ubuntu-latest" 13 | steps: 14 | - uses: "actions/checkout@v4" 15 | with: 16 | fetch-depth: 0 17 | - name: Install valgrind 18 | run: | 19 | sudo apt-get update 20 | sudo apt-get install -y valgrind 21 | - uses: pnpm/action-setup@v2 22 | - uses: actions/setup-node@v3 23 | with: 24 | cache: pnpm 25 | node-version-file: .nvmrc 26 | - run: pnpm install --frozen-lockfile --prefer-offline 27 | - run: pnpm moon run :build 28 | 29 | - name: Run benchmarks 30 | # use version from `main` branch to always test the latest version, in real projects, use a tag, like `@v2` 31 | uses: CodSpeedHQ/action@main 32 | with: 33 | run: | 34 | pnpm moon run --concurrency 1 :bench 35 | pnpm --workspace-concurrency 1 -r bench-tinybench 36 | pnpm --workspace-concurrency 1 -r bench-benchmark-js 37 | pnpm --workspace-concurrency 1 -r bench-vitest 38 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release on tag 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | permissions: 9 | id-token: write 10 | contents: write 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | - uses: pnpm/action-setup@v2 21 | - uses: actions/setup-node@v3 22 | with: 23 | cache: pnpm 24 | node-version-file: .nvmrc 25 | registry-url: "https://registry.npmjs.org" 26 | - run: pnpm install --frozen-lockfile --prefer-offline 27 | - name: Install valgrind 28 | run: | 29 | sudo apt-get update 30 | sudo apt-get install -y valgrind 31 | 32 | - name: Build the libraries 33 | run: pnpm moon run :build 34 | 35 | - name: Publish the libraries 36 | run: pnpm publish -r --access=public --no-git-checks 37 | env: 38 | NPM_CONFIG_PROVENANCE: true 39 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 40 | 41 | - name: Create a draft release 42 | run: | 43 | NEW_VERSION=$(pnpm lerna list --json | jq -r '.[] | select(.name == "@codspeed/core") | .version') 44 | gh release create v$NEW_VERSION --title "v$NEW_VERSION" --generate-notes -d 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Serverless directories 108 | .serverless/ 109 | 110 | # FuseBox cache 111 | .fusebox/ 112 | 113 | # DynamoDB Local files 114 | .dynamodb/ 115 | 116 | # TernJS port file 117 | .tern-port 118 | 119 | # Stores VSCode versions used for testing VSCode extensions 120 | .vscode-test 121 | 122 | 123 | pnpm-global 124 | packages/app/.env 125 | 126 | .vercel 127 | .DS_Store 128 | 129 | # moon 130 | .moon/cache 131 | .moon/docker 132 | .rollup.cache/ 133 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | pnpm commitlint --edit ${1} 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | pnpm moon run :lint :format :typecheck --affected --status=staged 5 | 6 | -------------------------------------------------------------------------------- /.moon/tasks.yml: -------------------------------------------------------------------------------- 1 | # https://moonrepo.dev/docs/config/global-project 2 | $schema: "https://moonrepo.dev/schemas/global-project.json" 3 | 4 | fileGroups: 5 | configs: 6 | - "*.config.{js,cjs,mjs,ts}" 7 | - ".eslintrc.js" 8 | - "tsconfig.*.json" 9 | 10 | sources: 11 | - "src/**/*" 12 | - "types/**/*" 13 | tests: 14 | - "tests/**/*.test.*" 15 | - "**/__tests__/**/*" 16 | dist: 17 | - "dist/**/*" 18 | 19 | tasks: 20 | format: 21 | command: "prettier --config @in(0) --ignore-path @in(1) --check ." 22 | inputs: 23 | - "/.prettierrc.json" 24 | - "/.prettierignore" 25 | - "@globs(sources)" 26 | - "@globs(tests)" 27 | - "@globs(configs)" 28 | lint: 29 | command: "eslint ." 30 | inputs: 31 | - "@globs(sources)" 32 | - "@globs(tests)" 33 | - ".eslintignore" 34 | - ".eslintrc.js" 35 | - "/.eslintrc.js" 36 | - "tsconfig.json" 37 | - "tsconfig.*.json" 38 | deps: 39 | - "build" 40 | 41 | typecheck: 42 | command: "tsc --noEmit --pretty" 43 | inputs: 44 | - "@globs(sources)" 45 | - "@globs(tests)" 46 | - "tsconfig.json" 47 | - "/tsconfig.json" 48 | - "/tsconfig.base.json" 49 | deps: 50 | - "build" 51 | 52 | build: 53 | command: "rollup -c rollup.config.ts --configPlugin typescript" 54 | inputs: 55 | - "@globs(sources)" 56 | - "rollup.config.ts" 57 | outputs: 58 | - "dist/" 59 | deps: 60 | - "^:build" 61 | env: 62 | NODE_NO_WARNINGS: "1" 63 | 64 | test: 65 | command: "jest --passWithNoTests --silent" 66 | inputs: 67 | - "@globs(sources)" 68 | - "@globs(tests)" 69 | - "@globs(configs)" 70 | - "tsconfig.json" 71 | - "/tsconfig.json" 72 | - "/tsconfig.base.json" 73 | deps: 74 | - "build" 75 | - "test/integ" 76 | 77 | test/integ: 78 | command: "jest --passWithNoTests --silent -c jest.config.integ.js" 79 | inputs: 80 | - "@globs(sources)" 81 | - "@globs(tests)" 82 | - "@globs(configs)" 83 | - "tsconfig.json" 84 | - "/tsconfig.json" 85 | - "/tsconfig.base.json" 86 | deps: 87 | - "build" 88 | 89 | clean: 90 | command: "rm -rf" 91 | args: 92 | - dist 93 | local: true 94 | options: 95 | cache: false 96 | platform: system 97 | -------------------------------------------------------------------------------- /.moon/toolchain.yml: -------------------------------------------------------------------------------- 1 | # https://moonrepo.dev/docs/config/toolchain 2 | $schema: "https://moonrepo.dev/schemas/toolchain.json" 3 | 4 | node: 5 | packageManager: "pnpm" 6 | pnpm: 7 | version: "8.6.3" 8 | 9 | dedupeOnLockfileChange: false 10 | dependencyVersionFormat: "workspace" 11 | inferTasksFromScripts: true 12 | syncProjectWorkspaceDependencies: true 13 | 14 | typescript: 15 | createMissingConfig: false 16 | rootConfigFileName: "tsconfig.json" 17 | rootOptionsConfigFileName: "tsconfig.base.json" 18 | 19 | routeOutDirToCache: false 20 | syncProjectReferences: false 21 | syncProjectReferencesToPaths: false 22 | -------------------------------------------------------------------------------- /.moon/workspace.yml: -------------------------------------------------------------------------------- 1 | # https://moonrepo.dev/docs/config/workspace 2 | $schema: 'https://moonrepo.dev/schemas/workspace.json' 3 | 4 | projects: 5 | - 'packages/*' 6 | 7 | vcs: 8 | manager: 'git' 9 | defaultBranch: 'main' 10 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18.16.0 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .rollup.cache 3 | dist 4 | generated 5 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-organize-imports"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Linux", 5 | "includePath": [ 6 | "${workspaceFolder}/**", 7 | "/usr/include/node" 8 | ], 9 | "defines": [], 10 | "compilerPath": "/usr/bin/clang", 11 | "cStandard": "c17", 12 | "cppStandard": "c++14", 13 | "intelliSenseMode": "linux-clang-x64", 14 | "configurationProvider": "ms-vscode.makefile-tools" 15 | } 16 | ], 17 | "version": 4 18 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "attach", 7 | "name": "Attach to Node", 8 | "port": 9229 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "C_Cpp.default.configurationProvider": "ms-vscode.makefile-tools", 3 | "makefile.makeDirectory": "${workspaceRoot}/packages/core/build/", 4 | "files.exclude": {}, 5 | "editor.defaultFormatter": "esbenp.prettier-vscode", 6 | "editor.formatOnSave": true, 7 | "editor.codeActionsOnSave": { 8 | "source.organizeImports": "never" // Import sorting is handled by prettier 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2023 CodSpeed 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

codspeed-node

3 | 4 | Node.js libraries to create CodSpeed benchmarks 5 | 6 | [![CI](https://github.com/CodSpeedHQ/codspeed-node/actions/workflows/ci.yml/badge.svg)](https://github.com/CodSpeedHQ/codspeed-node/actions/workflows/ci.yml) 7 | [![npm (scoped)](https://img.shields.io/npm/v/@codspeed/core)](https://www.npmjs.com/org/codspeed) 8 | [![Discord](https://img.shields.io/badge/chat%20on-discord-7289da.svg)](https://discord.com/invite/MxpaCfKSqF) 9 | [![CodSpeed Badge](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/CodSpeedHQ/codspeed-node) 10 | 11 |
12 | 13 | ## Documentation 14 | 15 | Check out the [documentation](https://docs.codspeed.io/benchmarks/nodejs) for complete integration instructions. 16 | 17 | ## Packages 18 | 19 | This mono-repo contains the integration packages for using CodSpeed with Node.js: 20 | 21 | - [`@codspeed/vitest-plugin`](./packages/vitest-plugin): vitest compatibility layer for CodSpeed 22 | - [`@codspeed/tinybench-plugin`](./packages/tinybench-plugin): tinybench compatibility layer for CodSpeed 23 | - [`@codspeed/benchmark.js-plugin`](./packages/benchmark.js-plugin): Benchmark.js compatibility layer for CodSpeed 24 | - [`@codspeed/core`](./packages/core): The core library used to integrate with Codspeed runners 25 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ["@commitlint/config-conventional"] }; 2 | -------------------------------------------------------------------------------- /docs/introspection.md: -------------------------------------------------------------------------------- 1 | ## Testing introspection locally 2 | 3 | 1. Inside `codspeed-node` directory, run `export PATH="$pwd/../action/dist/bin:$PATH"`. 4 | This will ensure that the action's `dist/bin/node` file will be used instead of the 5 | system's `node` binary. 6 | 7 | 2. Replace the `CodSpeedHQ/action` grep filter with `CodSpeedHQ` in the `dist/bin/node`. 8 | Since we used `../action` in the `export PATH=...` command, the original grep filter will 9 | not work. 10 | 11 | 3. Run your command with the correct flags in `codspeed-node`, for example 12 | 13 | ```bash 14 | CI=1 CODSPEED_DEBUG=true pnpm --filter with-typescript-esm bench-tinybench 15 | ``` 16 | -------------------------------------------------------------------------------- /examples/with-javascript-cjs/benchmark-js.js: -------------------------------------------------------------------------------- 1 | const { withCodSpeed } = require("@codspeed/benchmark.js-plugin"); 2 | const Benchmark = require("benchmark"); 3 | 4 | const suite = withCodSpeed(new Benchmark.Suite()); 5 | 6 | suite 7 | .add("RegExp#test", function () { 8 | /o/.test("Hello World!"); 9 | }) 10 | .add("String#indexOf", function () { 11 | "Hello World!".indexOf("o") > -1; 12 | }) 13 | // add listeners 14 | .on("cycle", function (event) { 15 | console.log(String(event.target)); 16 | }) 17 | // run async 18 | .run({ async: true }); 19 | -------------------------------------------------------------------------------- /examples/with-javascript-cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-javascript-cjs", 3 | "private": true, 4 | "scripts": { 5 | "bench-benchmark-js": "node benchmark-js.js", 6 | "bench-tinybench": "node tinybench.js" 7 | }, 8 | "devDependencies": { 9 | "@codspeed/benchmark.js-plugin": "workspace:*", 10 | "@codspeed/tinybench-plugin": "workspace:*", 11 | "benchmark": "^2.1.4", 12 | "tinybench": "^2.5.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/with-javascript-cjs/tinybench.js: -------------------------------------------------------------------------------- 1 | const { withCodSpeed } = require("@codspeed/tinybench-plugin"); 2 | const { Bench } = require("tinybench"); 3 | 4 | const bench = withCodSpeed(new Bench({ time: 100 })); 5 | 6 | bench 7 | .add("switch 1", () => { 8 | let a = 1; 9 | let b = 2; 10 | const c = a; 11 | a = b; 12 | b = c; 13 | }) 14 | .add("switch 2", () => { 15 | let a = 1; 16 | let b = 10; 17 | a = b + a; 18 | b = a - b; 19 | a = b - a; 20 | }); 21 | 22 | bench.run().then(() => { 23 | console.table(bench.table()); 24 | }); 25 | -------------------------------------------------------------------------------- /examples/with-javascript-esm/benchmark-js.js: -------------------------------------------------------------------------------- 1 | import { withCodSpeed } from "@codspeed/benchmark.js-plugin"; 2 | import Benchmark from "benchmark"; 3 | 4 | const suite = withCodSpeed(new Benchmark.Suite()); 5 | 6 | suite 7 | .add("RegExp#test", function () { 8 | /o/.test("Hello World!"); 9 | }) 10 | .add("String#indexOf", function () { 11 | "Hello World!".indexOf("o") > -1; 12 | }) 13 | // add listeners 14 | .on("cycle", function (event) { 15 | console.log(String(event.target)); 16 | }) 17 | // run async 18 | .run({ async: true }); 19 | -------------------------------------------------------------------------------- /examples/with-javascript-esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-javascript-esm", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "bench-benchmark-js": "node benchmark-js.js", 7 | "bench-tinybench": "node tinybench.js" 8 | }, 9 | "devDependencies": { 10 | "@codspeed/benchmark.js-plugin": "workspace:*", 11 | "@codspeed/tinybench-plugin": "workspace:*", 12 | "benchmark": "^2.1.4", 13 | "tinybench": "^2.5.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/with-javascript-esm/tinybench.js: -------------------------------------------------------------------------------- 1 | import { withCodSpeed } from "@codspeed/tinybench-plugin"; 2 | import { Bench } from "tinybench"; 3 | 4 | const bench = withCodSpeed(new Bench({ time: 100 })); 5 | 6 | bench 7 | .add("switch 1", () => { 8 | let a = 1; 9 | let b = 2; 10 | const c = a; 11 | a = b; 12 | b = c; 13 | }) 14 | .add("switch 2", () => { 15 | let a = 1; 16 | let b = 10; 17 | a = b + a; 18 | b = a - b; 19 | a = b - a; 20 | }); 21 | 22 | bench.run().then(() => { 23 | console.table(bench.table()); 24 | }); 25 | -------------------------------------------------------------------------------- /examples/with-typescript-cjs/bench/benchmark.js/fibo.bench.ts: -------------------------------------------------------------------------------- 1 | import type { WithCodSpeedSuite } from "@codspeed/benchmark.js-plugin"; 2 | import { 3 | iterativeFibonacci, 4 | recursiveCachedFibonacci, 5 | recursiveFibonacci, 6 | } from "../../src/fibonacci"; 7 | 8 | export function registerFiboBenchmarks(suite: WithCodSpeedSuite) { 9 | suite 10 | .add("test_recursive_fibo_10", () => { 11 | recursiveFibonacci(10); 12 | }) 13 | .add("test_recursive_fibo_20", () => { 14 | recursiveFibonacci(20); 15 | }); 16 | 17 | suite 18 | .add("test_recursive_cached_fibo_10", () => { 19 | recursiveCachedFibonacci(10); 20 | }) 21 | .add("test_recursive_cached_fibo_20", () => { 22 | recursiveCachedFibonacci(20); 23 | }) 24 | .add("test_recursive_cached_fibo_30", () => { 25 | recursiveCachedFibonacci(30); 26 | }); 27 | 28 | suite 29 | .add("test_iterative_fibo_10", () => { 30 | iterativeFibonacci(10); 31 | }) 32 | .add("test_iterative_fibo_100", () => { 33 | iterativeFibonacci(100); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /examples/with-typescript-cjs/bench/benchmark.js/foobarbaz.bench.ts: -------------------------------------------------------------------------------- 1 | import type { WithCodSpeedSuite } from "@codspeed/benchmark.js-plugin"; 2 | import { baz } from "../../src/foobarbaz"; 3 | 4 | export function registerFoobarbazBenchmarks(suite: WithCodSpeedSuite) { 5 | suite 6 | .add("test sync baz 10", () => { 7 | baz(10); 8 | }) 9 | .add("test sync baz 100", () => { 10 | baz(100); 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /examples/with-typescript-cjs/bench/benchmark.js/index.bench.ts: -------------------------------------------------------------------------------- 1 | import { withCodSpeed } from "@codspeed/benchmark.js-plugin"; 2 | import Benchmark from "benchmark"; 3 | import { registerFiboBenchmarks } from "./fibo.bench"; 4 | import { registerFoobarbazBenchmarks } from "./foobarbaz.bench"; 5 | 6 | export const suite = withCodSpeed(new Benchmark.Suite()); 7 | 8 | (async () => { 9 | registerFiboBenchmarks(suite); 10 | registerFoobarbazBenchmarks(suite); 11 | 12 | suite.on("cycle", function (event: Benchmark.Event) { 13 | console.log(String(event.target)); 14 | }); 15 | 16 | await suite.run({ async: true }); 17 | })(); 18 | -------------------------------------------------------------------------------- /examples/with-typescript-cjs/bench/tinybench/fibo.bench.ts: -------------------------------------------------------------------------------- 1 | import { Bench } from "tinybench"; 2 | import { 3 | iterativeFibonacci, 4 | recursiveCachedFibonacci, 5 | recursiveFibonacci, 6 | } from "../../src/fibonacci"; 7 | 8 | export function registerFiboBenchmarks(bench: Bench) { 9 | bench 10 | .add("test_recursive_fibo_10", () => { 11 | recursiveFibonacci(10); 12 | }) 13 | .add("test_recursive_fibo_20", () => { 14 | recursiveFibonacci(20); 15 | }); 16 | 17 | bench 18 | .add("test_recursive_cached_fibo_10", () => { 19 | recursiveCachedFibonacci(10); 20 | }) 21 | .add("test_recursive_cached_fibo_20", () => { 22 | recursiveCachedFibonacci(20); 23 | }) 24 | .add("test_recursive_cached_fibo_30", () => { 25 | recursiveCachedFibonacci(30); 26 | }); 27 | 28 | bench 29 | .add("test_iterative_fibo_10", () => { 30 | iterativeFibonacci(10); 31 | }) 32 | .add("test_iterative_fibo_100", () => { 33 | iterativeFibonacci(100); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /examples/with-typescript-cjs/bench/tinybench/foobarbaz.bench.ts: -------------------------------------------------------------------------------- 1 | import { Bench } from "tinybench"; 2 | import { baz } from "../../src/foobarbaz"; 3 | 4 | export function registerFoobarbazBenchmarks(bench: Bench) { 5 | bench 6 | .add("test sync baz 10", () => { 7 | baz(10); 8 | }) 9 | .add("test sync baz 100", () => { 10 | baz(100); 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /examples/with-typescript-cjs/bench/tinybench/index.bench.ts: -------------------------------------------------------------------------------- 1 | import { withCodSpeed } from "@codspeed/tinybench-plugin"; 2 | import { Bench } from "tinybench"; 3 | import { registerFiboBenchmarks } from "./fibo.bench"; 4 | import { registerFoobarbazBenchmarks } from "./foobarbaz.bench"; 5 | 6 | export const bench = withCodSpeed(new Bench()); 7 | 8 | (async () => { 9 | registerFiboBenchmarks(bench); 10 | registerFoobarbazBenchmarks(bench); 11 | 12 | await bench.run(); 13 | console.table(bench.table()); 14 | })(); 15 | -------------------------------------------------------------------------------- /examples/with-typescript-cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-typescript-cjs", 3 | "private": true, 4 | "scripts": { 5 | "bench-benchmark-js": "node -r esbuild-register bench/benchmark.js/index.bench.ts", 6 | "bench-tinybench": "node -r esbuild-register bench/tinybench/index.bench.ts" 7 | }, 8 | "devDependencies": { 9 | "@codspeed/benchmark.js-plugin": "workspace:*", 10 | "@codspeed/tinybench-plugin": "workspace:*", 11 | "@types/benchmark": "^2.1.2", 12 | "benchmark": "^2.1.4", 13 | "esbuild-register": "^3.4.2", 14 | "tinybench": "^2.5.0", 15 | "typescript": "^5.1.3" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/with-typescript-cjs/src/fibonacci.ts: -------------------------------------------------------------------------------- 1 | export function recursiveFibonacci(n: number): number { 2 | if (n < 2) { 3 | return n; 4 | } 5 | return recursiveFibonacci(n - 1) + recursiveFibonacci(n - 2); 6 | } 7 | 8 | export function recursiveCachedFibonacci(n: number) { 9 | const cache: Record = { 0: 0, 1: 1 }; 10 | const fiboInner = (n: number) => { 11 | if (n in cache) { 12 | return cache[n]; 13 | } 14 | cache[n] = fiboInner(n - 1) + fiboInner(n - 2); 15 | return cache[n]; 16 | }; 17 | return fiboInner(n); 18 | } 19 | 20 | export function iterativeFibonacci(n: number) { 21 | let a = 0; 22 | let b = 1; 23 | let c = 0; 24 | for (let i = 0; i < n; i++) { 25 | c = a + b; 26 | a = b; 27 | b = c; 28 | } 29 | return a; 30 | } 31 | -------------------------------------------------------------------------------- /examples/with-typescript-cjs/src/foobarbaz.ts: -------------------------------------------------------------------------------- 1 | // Sync version 2 | function foo(n: number) { 3 | let result = 0; 4 | for (let i = 0; i < n; i++) { 5 | result += 1; 6 | } 7 | return result; 8 | } 9 | 10 | function bar(n: number) { 11 | foo(n); 12 | } 13 | 14 | export function baz(n: number) { 15 | bar(n); 16 | } 17 | -------------------------------------------------------------------------------- /examples/with-typescript-cjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2023"], 4 | "module": "Node16", 5 | "target": "es2022", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "moduleResolution": "Node" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/with-typescript-esm/bench/benchmark.js/fibo.bench.ts: -------------------------------------------------------------------------------- 1 | import type { WithCodSpeedSuite } from "@codspeed/benchmark.js-plugin"; 2 | import { 3 | iterativeFibonacci, 4 | recursiveCachedFibonacci, 5 | recursiveFibonacci, 6 | } from "../../src/fibonacci"; 7 | 8 | export function registerFiboBenchmarks(suite: WithCodSpeedSuite) { 9 | suite 10 | .add("test_recursive_fibo_10", () => { 11 | recursiveFibonacci(10); 12 | }) 13 | .add("test_recursive_fibo_20", () => { 14 | recursiveFibonacci(20); 15 | }); 16 | 17 | suite 18 | .add("test_recursive_cached_fibo_10", () => { 19 | recursiveCachedFibonacci(10); 20 | }) 21 | .add("test_recursive_cached_fibo_20", () => { 22 | recursiveCachedFibonacci(20); 23 | }) 24 | .add("test_recursive_cached_fibo_30", () => { 25 | recursiveCachedFibonacci(30); 26 | }); 27 | 28 | suite 29 | .add("test_iterative_fibo_10", () => { 30 | iterativeFibonacci(10); 31 | }) 32 | .add("test_iterative_fibo_100", () => { 33 | iterativeFibonacci(100); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /examples/with-typescript-esm/bench/benchmark.js/foobarbaz.bench.ts: -------------------------------------------------------------------------------- 1 | import type { WithCodSpeedSuite } from "@codspeed/benchmark.js-plugin"; 2 | import { baz } from "../../src/foobarbaz"; 3 | 4 | export function registerFoobarbazBenchmarks(suite: WithCodSpeedSuite) { 5 | suite 6 | .add("test sync baz 10", () => { 7 | baz(10); 8 | }) 9 | .add("test sync baz 100", () => { 10 | baz(100); 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /examples/with-typescript-esm/bench/benchmark.js/index.bench.ts: -------------------------------------------------------------------------------- 1 | import { withCodSpeed } from "@codspeed/benchmark.js-plugin"; 2 | import Benchmark from "benchmark"; 3 | import { registerFiboBenchmarks } from "./fibo.bench"; 4 | import { registerFoobarbazBenchmarks } from "./foobarbaz.bench"; 5 | 6 | export const suite = withCodSpeed(new Benchmark.Suite()); 7 | 8 | (async () => { 9 | registerFiboBenchmarks(suite); 10 | registerFoobarbazBenchmarks(suite); 11 | 12 | suite.on("cycle", function (event: Benchmark.Event) { 13 | console.log(String(event.target)); 14 | }); 15 | 16 | await suite.run({ async: true }); 17 | })(); 18 | -------------------------------------------------------------------------------- /examples/with-typescript-esm/bench/tinybench/fibo.bench.ts: -------------------------------------------------------------------------------- 1 | import { Bench } from "tinybench"; 2 | import { 3 | iterativeFibonacci, 4 | recursiveCachedFibonacci, 5 | recursiveFibonacci, 6 | } from "../../src/fibonacci"; 7 | 8 | export function registerFiboBenchmarks(bench: Bench) { 9 | bench 10 | .add("test_recursive_fibo_10", () => { 11 | recursiveFibonacci(10); 12 | }) 13 | .add("test_recursive_fibo_20", () => { 14 | recursiveFibonacci(20); 15 | }); 16 | 17 | bench 18 | .add("test_recursive_cached_fibo_10", () => { 19 | recursiveCachedFibonacci(10); 20 | }) 21 | .add("test_recursive_cached_fibo_20", () => { 22 | recursiveCachedFibonacci(20); 23 | }) 24 | .add("test_recursive_cached_fibo_30", () => { 25 | recursiveCachedFibonacci(30); 26 | }); 27 | 28 | bench 29 | .add("test_iterative_fibo_10", () => { 30 | iterativeFibonacci(10); 31 | }) 32 | .add("test_iterative_fibo_100", () => { 33 | iterativeFibonacci(100); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /examples/with-typescript-esm/bench/tinybench/foobarbaz.bench.ts: -------------------------------------------------------------------------------- 1 | import { Bench } from "tinybench"; 2 | import { baz } from "../../src/foobarbaz"; 3 | 4 | export function registerFoobarbazBenchmarks(bench: Bench) { 5 | bench 6 | .add("test sync baz 10", () => { 7 | baz(10); 8 | }) 9 | .add("test sync baz 100", () => { 10 | baz(100); 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /examples/with-typescript-esm/bench/tinybench/index.bench.ts: -------------------------------------------------------------------------------- 1 | import { withCodSpeed } from "@codspeed/tinybench-plugin"; 2 | import { Bench } from "tinybench"; 3 | import { registerFiboBenchmarks } from "./fibo.bench"; 4 | import { registerFoobarbazBenchmarks } from "./foobarbaz.bench"; 5 | 6 | export const bench = withCodSpeed(new Bench()); 7 | 8 | (async () => { 9 | registerFiboBenchmarks(bench); 10 | registerFoobarbazBenchmarks(bench); 11 | 12 | await bench.run(); 13 | console.table(bench.table()); 14 | })(); 15 | -------------------------------------------------------------------------------- /examples/with-typescript-esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-typescript-esm", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "bench-benchmark-js": "node --loader esbuild-register/loader -r esbuild-register bench/benchmark.js/index.bench.ts", 7 | "bench-tinybench": "node --loader esbuild-register/loader -r esbuild-register bench/tinybench/index.bench.ts", 8 | "bench-vitest": "vitest bench --run" 9 | }, 10 | "devDependencies": { 11 | "@codspeed/benchmark.js-plugin": "workspace:*", 12 | "@codspeed/tinybench-plugin": "workspace:*", 13 | "@codspeed/vitest-plugin": "workspace:*", 14 | "@types/benchmark": "^2.1.2", 15 | "benchmark": "^2.1.4", 16 | "esbuild-register": "^3.4.2", 17 | "tinybench": "^2.5.0", 18 | "typescript": "^5.1.3", 19 | "vitest": "^1.2.2" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/with-typescript-esm/src/fibonacci.bench.ts: -------------------------------------------------------------------------------- 1 | import { bench, describe } from "vitest"; 2 | import { iterativeFibonacci } from "./fibonacci"; 3 | 4 | describe("iterativeFibonacci", () => { 5 | bench("fibo 10", () => { 6 | iterativeFibonacci(10); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /examples/with-typescript-esm/src/fibonacci.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { iterativeFibonacci } from "./fibonacci"; 3 | 4 | describe("iterativeFibonacci", () => { 5 | it("should return the correct value", () => { 6 | expect(iterativeFibonacci(1)).toBe(1); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /examples/with-typescript-esm/src/fibonacci.ts: -------------------------------------------------------------------------------- 1 | export function recursiveFibonacci(n: number): number { 2 | if (n < 2) { 3 | return n; 4 | } 5 | return recursiveFibonacci(n - 1) + recursiveFibonacci(n - 2); 6 | } 7 | 8 | export function recursiveCachedFibonacci(n: number) { 9 | const cache: Record = { 0: 0, 1: 1 }; 10 | const fiboInner = (n: number) => { 11 | if (n in cache) { 12 | return cache[n]; 13 | } 14 | cache[n] = fiboInner(n - 1) + fiboInner(n - 2); 15 | return cache[n]; 16 | }; 17 | return fiboInner(n); 18 | } 19 | 20 | export function iterativeFibonacci(n: number) { 21 | let a = 0; 22 | let b = 1; 23 | let c = 0; 24 | for (let i = 0; i < n; i++) { 25 | c = a + b; 26 | a = b; 27 | b = c; 28 | } 29 | return a; 30 | } 31 | -------------------------------------------------------------------------------- /examples/with-typescript-esm/src/foobarbaz.ts: -------------------------------------------------------------------------------- 1 | // Sync version 2 | function foo(n: number) { 3 | let result = 0; 4 | for (let i = 0; i < n; i++) { 5 | result += 1; 6 | } 7 | return result; 8 | } 9 | 10 | function bar(n: number) { 11 | foo(n); 12 | } 13 | 14 | export function baz(n: number) { 15 | bar(n); 16 | } 17 | -------------------------------------------------------------------------------- /examples/with-typescript-esm/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2023"], 4 | "module": "ESNext", 5 | "verbatimModuleSyntax": true, 6 | "target": "es2022", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "moduleResolution": "Node" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/with-typescript-esm/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import codspeedPlugin from "@codspeed/vitest-plugin"; 2 | import { defineConfig } from "vitest/config"; 3 | 4 | export default defineConfig({ 5 | plugins: [codspeedPlugin()], 6 | test: { 7 | benchmark: { 8 | exclude: ["**/bench/**/*", "**/node_modules/**/*"], 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /examples/with-typescript-simple-cjs/benchmark-js.ts: -------------------------------------------------------------------------------- 1 | import { withCodSpeed } from "@codspeed/benchmark.js-plugin"; 2 | import Benchmark from "benchmark"; 3 | 4 | const suite = withCodSpeed(new Benchmark.Suite()); 5 | 6 | suite 7 | .add("RegExp#test", function () { 8 | /o/.test("Hello World!"); 9 | }) 10 | .add("String#indexOf", function () { 11 | "Hello World!".indexOf("o") > -1; 12 | }) 13 | // add listeners 14 | .on("cycle", function (event: Benchmark.Event) { 15 | console.log(String(event.target)); 16 | }) 17 | // run async 18 | .run({ async: true }); 19 | -------------------------------------------------------------------------------- /examples/with-typescript-simple-cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-typescript-simple-cjs", 3 | "private": true, 4 | "scripts": { 5 | "bench-benchmark-js": "node -r esbuild-register benchmark-js.ts", 6 | "bench-tinybench": "node -r esbuild-register tinybench.ts" 7 | }, 8 | "devDependencies": { 9 | "@codspeed/benchmark.js-plugin": "workspace:*", 10 | "@codspeed/tinybench-plugin": "workspace:*", 11 | "@types/benchmark": "^2.1.2", 12 | "benchmark": "^2.1.4", 13 | "esbuild-register": "^3.4.2", 14 | "tinybench": "^2.5.0", 15 | "typescript": "^5.1.3" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/with-typescript-simple-cjs/tinybench.ts: -------------------------------------------------------------------------------- 1 | import { withCodSpeed } from "@codspeed/tinybench-plugin"; 2 | import { Bench } from "tinybench"; 3 | 4 | const bench = withCodSpeed(new Bench({ time: 100 })); 5 | 6 | bench 7 | .add("switch 1", () => { 8 | let a = 1; 9 | let b = 2; 10 | const c = a; 11 | a = b; 12 | b = c; 13 | }) 14 | .add("switch 2", () => { 15 | let a = 1; 16 | let b = 10; 17 | a = b + a; 18 | b = a - b; 19 | a = b - a; 20 | }); 21 | 22 | bench.run().then(() => { 23 | console.table(bench.table()); 24 | }); 25 | -------------------------------------------------------------------------------- /examples/with-typescript-simple-esm/benchmark-js.ts: -------------------------------------------------------------------------------- 1 | import { withCodSpeed } from "@codspeed/benchmark.js-plugin"; 2 | import Benchmark from "benchmark"; 3 | 4 | const suite = withCodSpeed(new Benchmark.Suite()); 5 | 6 | suite 7 | .add("RegExp#test", function () { 8 | /o/.test("Hello World!"); 9 | }) 10 | .add("String#indexOf", function () { 11 | "Hello World!".indexOf("o") > -1; 12 | }) 13 | // add listeners 14 | .on("cycle", function (event: Benchmark.Event) { 15 | console.log(String(event.target)); 16 | }) 17 | // run async 18 | .run({ async: true }); 19 | -------------------------------------------------------------------------------- /examples/with-typescript-simple-esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-typescript-simple-esm", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "bench-benchmark-js": "node --loader esbuild-register/loader -r esbuild-register benchmark-js.ts", 7 | "bench-tinybench": "node --loader esbuild-register/loader -r esbuild-register tinybench.ts" 8 | }, 9 | "devDependencies": { 10 | "@codspeed/benchmark.js-plugin": "workspace:*", 11 | "@codspeed/tinybench-plugin": "workspace:*", 12 | "@types/benchmark": "^2.1.2", 13 | "benchmark": "^2.1.4", 14 | "esbuild-register": "^3.4.2", 15 | "tinybench": "^2.5.0", 16 | "typescript": "^5.1.3" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/with-typescript-simple-esm/tinybench.ts: -------------------------------------------------------------------------------- 1 | import { withCodSpeed } from "@codspeed/tinybench-plugin"; 2 | import { Bench } from "tinybench"; 3 | 4 | const bench = withCodSpeed(new Bench({ time: 100 })); 5 | 6 | bench 7 | .add("switch 1", () => { 8 | let a = 1; 9 | let b = 2; 10 | const c = a; 11 | a = b; 12 | b = c; 13 | }) 14 | .add("switch 2", () => { 15 | let a = 1; 16 | let b = 10; 17 | a = b + a; 18 | b = a - b; 19 | a = b - a; 20 | }); 21 | 22 | bench.run().then(() => { 23 | console.table(bench.table()); 24 | }); 25 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmClient": "pnpm", 3 | "useWorkspaces": true, 4 | "packages": ["packages/*"], 5 | "$schema": "node_modules/lerna/schemas/lerna-schema.json", 6 | "version": "4.0.1" 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "devDependencies": { 5 | "@commitlint/cli": "^17.5.1", 6 | "@commitlint/config-conventional": "^17.4.4", 7 | "@moonrepo/cli": "^1.19.3", 8 | "@rollup/plugin-commonjs": "^25.0.7", 9 | "@rollup/plugin-json": "^6.0.1", 10 | "@rollup/plugin-node-resolve": "^15.2.3", 11 | "@rollup/plugin-typescript": "^11.1.5", 12 | "@types/jest": "^29.5.0", 13 | "@types/node": "^18.15.11", 14 | "@typescript-eslint/eslint-plugin": "^5.58.0", 15 | "@typescript-eslint/parser": "^5.58.0", 16 | "esbuild": "^0.17.16", 17 | "esbuild-register": "^3.4.2", 18 | "eslint": "^7.32.0", 19 | "eslint-import-resolver-typescript": "^3.5.5", 20 | "eslint-plugin-import": "^2.27.5", 21 | "husky": "^7.0.4", 22 | "jest": "^29.5.0", 23 | "jest-config": "^29.5.0", 24 | "lerna": "^6.6.1", 25 | "prettier": "^2.8.7", 26 | "prettier-plugin-organize-imports": "^3.2.2", 27 | "rollup": "^4.4.1", 28 | "rollup-plugin-dts": "^6.1.0", 29 | "rollup-plugin-esbuild": "^6.1.0", 30 | "ts-jest": "^29.1.0", 31 | "tslib": "^2.5.0", 32 | "typescript": "4.9.4" 33 | }, 34 | "packageManager": "pnpm@8.6.3", 35 | "engines": { 36 | "node": "18.16.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/benchmark.js-plugin/README.md: -------------------------------------------------------------------------------- 1 |
2 |

@codspeed/benchmark.js-plugin

3 | 4 | Benchmark.js compatibility layer for CodSpeed 5 | 6 | [![CI](https://github.com/CodSpeedHQ/codspeed-node/actions/workflows/ci.yml/badge.svg)](https://github.com/CodSpeedHQ/codspeed-node/actions/workflows/ci.yml) 7 | [![npm (scoped)](https://img.shields.io/npm/v/@codspeed/benchmark.js-plugin)](https://www.npmjs.com/package/@codspeed/benchmark.js-plugin) 8 | [![Discord](https://img.shields.io/badge/chat%20on-discord-7289da.svg)](https://discord.com/invite/MxpaCfKSqF) 9 | [![CodSpeed Badge](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/CodSpeedHQ/codspeed-node) 10 | 11 |
12 | 13 | ## Documentation 14 | 15 | Check out the [documentation](https://docs.codspeed.io/benchmarks/nodejs/benchmark.js) for complete integration instructions. 16 | 17 | ## Installation 18 | 19 | First, install the plugin [`@codspeed/benchmark.js-plugin`](https://www.npmjs.com/package/@codspeed/benchmark.js-plugin) and `benchmark.js` (if not already installed): 20 | 21 | ```sh 22 | npm install --save-dev @codspeed/benchmark.js-plugin benchmark.js 23 | ``` 24 | 25 | or with `yarn`: 26 | 27 | ```sh 28 | yarn add --dev @codspeed/benchmark.js-plugin benchmark.js 29 | ``` 30 | 31 | or with `pnpm`: 32 | 33 | ```sh 34 | pnpm add --save-dev @codspeed/benchmark.js-plugin benchmark.js 35 | ``` 36 | 37 | ## Usage 38 | 39 | Let's create a fibonacci function and benchmark it with benchmark.js and the CodSpeed plugin: 40 | 41 | ```js title="benches/bench.mjs" 42 | import Benchmark from "benchmark"; 43 | import { withCodSpeed } from "@codspeed/benchmark.js-plugin"; 44 | 45 | function fibonacci(n) { 46 | if (n < 2) { 47 | return n; 48 | } 49 | return fibonacci(n - 1) + fibonacci(n - 2); 50 | } 51 | 52 | const suite = withCodSpeed(new Benchmark.Suite()); 53 | 54 | suite 55 | .add("fibonacci10", () => { 56 | fibonacci(10); 57 | }) 58 | .add("fibonacci15", () => { 59 | fibonacci(15); 60 | }) 61 | .on("cycle", function (event: Benchmark.Event) { 62 | console.log(String(event.target)); 63 | }) 64 | .run(); 65 | ``` 66 | 67 | Here, a few things are happening: 68 | 69 | - We create a simple recursive fibonacci function. 70 | - We create a new `Benchmark.Suite` instance with CodSpeed support by using the **`withCodSpeed`** helper. This step is **critical** to enable CodSpeed on your benchmarks. 71 | - We add two benchmarks to the suite and launch it, benching our `fibonacci` function with 10 and 15. 72 | 73 | Now, we can run our benchmarks locally to make sure everything is working as expected: 74 | 75 | ```sh 76 | $ node benches/bench.mjs 77 | [CodSpeed] 2 benches detected but no instrumentation found 78 | [CodSpeed] falling back to benchmark.js 79 | 80 | fibonacci10 x 2,155,187 ops/sec ±0.50% (96 runs sampled) 81 | fibonacci15 x 194,742 ops/sec ±0.48% (95 runs sampled) 82 | ``` 83 | 84 | And... Congrats🎉, CodSpeed is installed in your benchmarking suite! Locally, CodSpeed will fallback to tinybench since the instrumentation is only available in the CI environment for now. 85 | 86 | You can now [run those benchmarks in your CI](https://docs.codspeed.io/benchmarks/nodejs/benchmark.js#running-the-benchmarks-in-your-ci) to continuously get consistent performance measurements. 87 | -------------------------------------------------------------------------------- /packages/benchmark.js-plugin/babel.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-undef 2 | module.exports = { 3 | presets: [["@babel/preset-env", { targets: { node: "current" } }]], 4 | }; 5 | -------------------------------------------------------------------------------- /packages/benchmark.js-plugin/benches/parsePr.ts: -------------------------------------------------------------------------------- 1 | interface PullRequest { 2 | number: number; 3 | title: string; 4 | body: string; 5 | } 6 | 7 | function sendEvent(numberOfOperations: number): void { 8 | for (let i = 0; i < numberOfOperations; i++) { 9 | let a = i; 10 | a = a + 1; 11 | } 12 | } 13 | 14 | function logMetrics( 15 | numberOfOperations: number, 16 | numberOfDeepOperations: number 17 | ): void { 18 | for (let i = 0; i < numberOfOperations; i++) { 19 | for (let i = 0; i < numberOfOperations; i++) { 20 | let a = i; 21 | a = a + 1; 22 | a = a + 1; 23 | } 24 | sendEvent(numberOfDeepOperations); 25 | } 26 | } 27 | 28 | function parseTitle(title: string): void { 29 | logMetrics(10, 10); 30 | modifyTitle(title); 31 | } 32 | 33 | function modifyTitle(title: string): void { 34 | for (let i = 0; i < 100; i++) { 35 | let a = i; 36 | a = a + 1 + title.length; 37 | } 38 | } 39 | 40 | function prepareParsingBody(body: string): void { 41 | for (let i = 0; i < 100; i++) { 42 | let a = i; 43 | a = a + 1; 44 | } 45 | parseBody(body); 46 | } 47 | 48 | function parseBody(body: string): void { 49 | logMetrics(10, 10); 50 | for (let i = 0; i < 200; i++) { 51 | let a = i; 52 | a = a + 1; 53 | } 54 | parseIssueFixed(body); 55 | } 56 | 57 | function parseIssueFixed(body: string): number | null { 58 | const prefix = "fixes #"; 59 | const index = body.indexOf(prefix); 60 | if (index === -1) { 61 | return null; 62 | } 63 | 64 | const start = index + prefix.length; 65 | let end = start; 66 | while (end < body.length && /\d/.test(body[end])) { 67 | end += 1; 68 | } 69 | return parseInt(body.slice(start, end)); 70 | } 71 | 72 | export default function parsePr(pullRequest: PullRequest): void { 73 | parseTitle(pullRequest.title); 74 | prepareParsingBody(pullRequest.body); 75 | } 76 | -------------------------------------------------------------------------------- /packages/benchmark.js-plugin/benches/sample.ts: -------------------------------------------------------------------------------- 1 | import Benchmark from "benchmark"; 2 | import { withCodSpeed } from ".."; 3 | import parsePr from "./parsePr"; 4 | 5 | const LONG_BODY = 6 | new Array(1_000) 7 | .fill( 8 | "Lorem ipsum dolor sit amet consectetur adipisicing elit. Sunt, earum. Atque architecto vero veniam est tempora fugiat sint quo praesentium quia. Autem, veritatis omnis beatae iste delectus recusandae animi non." 9 | ) 10 | .join("\n") + "fixes #123"; 11 | 12 | const suite = withCodSpeed(new Benchmark.Suite()); 13 | 14 | suite 15 | .add("RegExp#test", function () { 16 | /o/.test("Hello World!"); 17 | }) 18 | .add("String#indexOf", function () { 19 | "Hello World!".indexOf("o") > -1; 20 | }) 21 | .add("short body", () => { 22 | parsePr({ body: "fixes #123", title: "test", number: 124 }); 23 | }) 24 | .add("long body", () => { 25 | parsePr({ body: LONG_BODY, title: "test", number: 124 }); 26 | }) 27 | .add("short body 2", () => { 28 | parsePr({ body: "fixes #123", title: "test", number: 124 }); 29 | }) 30 | .add("short body 3", () => { 31 | parsePr({ body: "fixes #123", title: "test", number: 124 }); 32 | }) 33 | .add("short body 4", () => { 34 | parsePr({ body: "fixes #123", title: "test", number: 124 }); 35 | }) 36 | .add("short body 5", () => { 37 | parsePr({ body: "fixes #123", title: "test", number: 124 }); 38 | }) 39 | // add listeners 40 | .on("cycle", function (event: Benchmark.Event) { 41 | console.log(String(event.target)); 42 | }) 43 | // run async 44 | .run({ async: true }); 45 | -------------------------------------------------------------------------------- /packages/benchmark.js-plugin/jest.config.integ.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | // eslint-disable-next-line no-undef 3 | module.exports = { 4 | preset: "ts-jest", 5 | testEnvironment: "node", 6 | transform: { 7 | "^.+\\.tsx?$": [ 8 | "ts-jest", 9 | { 10 | tsconfig: "tsconfig.test.json", 11 | }, 12 | ], 13 | }, 14 | testPathIgnorePatterns: [ 15 | "/node_modules/", 16 | "/src/", 17 | "/.rollup.cache/", 18 | ], 19 | }; 20 | -------------------------------------------------------------------------------- /packages/benchmark.js-plugin/jest.config.js: -------------------------------------------------------------------------------- 1 | const esmModules = [ 2 | "find-up", 3 | "locate-path", 4 | "p-locate", 5 | "p-limit", 6 | "yocto-queue", 7 | "path-exists", 8 | "stack-trace", 9 | ]; 10 | 11 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 12 | module.exports = { 13 | preset: "ts-jest", 14 | testEnvironment: "node", 15 | transform: { 16 | "^.+\\.tsx?$": ["ts-jest"], 17 | // transform js with babel-jest 18 | "^.+\\.js$": "babel-jest", 19 | }, 20 | testPathIgnorePatterns: [ 21 | "/node_modules/", 22 | "/tests/", 23 | "/.rollup.cache/", 24 | ], 25 | transformIgnorePatterns: [ 26 | `node_modules/(?!(?:.pnpm/)?(${esmModules.join("|")}))`, 27 | ], 28 | }; 29 | -------------------------------------------------------------------------------- /packages/benchmark.js-plugin/moon.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | bench: 3 | command: node -r esbuild-register benches/sample.ts 4 | inputs: 5 | - "benches/**" 6 | local: true 7 | platform: "system" 8 | options: 9 | cache: false 10 | -------------------------------------------------------------------------------- /packages/benchmark.js-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@codspeed/benchmark.js-plugin", 3 | "version": "4.0.1", 4 | "description": "Benchmark.js compatibility layer for CodSpeed", 5 | "keywords": [ 6 | "codspeed", 7 | "benchmark", 8 | "benchmark.js", 9 | "performance" 10 | ], 11 | "main": "dist/index.cjs.js", 12 | "module": "dist/index.es5.js", 13 | "types": "dist/index.d.ts", 14 | "files": [ 15 | "dist" 16 | ], 17 | "author": "Arthur Pastel ", 18 | "repository": "https://github.com/CodSpeedHQ/codspeed-node", 19 | "homepage": "https://codspeed.io", 20 | "license": "Apache-2.0", 21 | "devDependencies": { 22 | "@babel/preset-env": "^7.22.5", 23 | "@types/benchmark": "^2.1.2", 24 | "@types/lodash": "^4.14.195", 25 | "@types/stack-trace": "^0.0.30", 26 | "benchmark": "^2.1.4", 27 | "jest-mock-extended": "^3.0.4" 28 | }, 29 | "dependencies": { 30 | "@codspeed/core": "workspace:^4.0.1", 31 | "lodash": "^4.17.10", 32 | "stack-trace": "1.0.0-pre2" 33 | }, 34 | "peerDependencies": { 35 | "benchmark": "^2.1.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/benchmark.js-plugin/rollup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "rollup"; 2 | import { declarationsPlugin, jsPlugins } from "../../rollup.options"; 3 | import pkg from "./package.json" assert { type: "json" }; 4 | 5 | const entrypoint = "src/index.ts"; 6 | 7 | export default defineConfig([ 8 | { 9 | input: entrypoint, 10 | output: [ 11 | { 12 | file: pkg.types, 13 | format: "es", 14 | sourcemap: true, 15 | }, 16 | ], 17 | plugins: declarationsPlugin({ compilerOptions: { composite: false } }), 18 | }, 19 | { 20 | input: entrypoint, 21 | output: [ 22 | { 23 | file: pkg.main, 24 | format: "cjs", 25 | sourcemap: true, 26 | }, 27 | { file: pkg.module, format: "es", sourcemap: true }, 28 | ], 29 | plugins: jsPlugins(pkg.version), 30 | external: ["@codspeed/core"], 31 | }, 32 | ]); 33 | -------------------------------------------------------------------------------- /packages/benchmark.js-plugin/src/__tests__/buildSuiteAdd.test.ts: -------------------------------------------------------------------------------- 1 | import { Suite } from "benchmark"; 2 | import buildSuiteAdd from "../buildSuiteAdd"; 3 | import { CodSpeedBenchmark } from "../types"; 4 | 5 | describe("buildSuiteAdd", () => { 6 | let emptyBench: () => void; 7 | let suite: Suite; 8 | 9 | beforeEach(() => { 10 | emptyBench = () => { 11 | return; 12 | }; 13 | suite = new Suite(); 14 | }); 15 | 16 | it("should register benchmark name when using (options: Options)", () => { 17 | suite.add = buildSuiteAdd(suite); 18 | suite.add({ name: "test", fn: emptyBench }); 19 | suite.forEach((bench: CodSpeedBenchmark) => 20 | expect(bench.uri).toBe( 21 | "packages/benchmark.js-plugin/src/__tests__/buildSuiteAdd.test.ts::test" 22 | ) 23 | ); 24 | }); 25 | 26 | it("should register benchmark name when using (fn: Function, options?: Options)", () => { 27 | suite.add = buildSuiteAdd(suite); 28 | suite.add(emptyBench, { name: "test" }); 29 | suite.forEach((bench: CodSpeedBenchmark) => 30 | expect(bench.uri).toBe( 31 | "packages/benchmark.js-plugin/src/__tests__/buildSuiteAdd.test.ts::test" 32 | ) 33 | ); 34 | }); 35 | 36 | it("should register benchmark name when using (name: string, options?: Options)", () => { 37 | suite.add = buildSuiteAdd(suite); 38 | suite.add("test", { fn: emptyBench }); 39 | suite.forEach((bench: CodSpeedBenchmark) => 40 | expect(bench.uri).toBe( 41 | "packages/benchmark.js-plugin/src/__tests__/buildSuiteAdd.test.ts::test" 42 | ) 43 | ); 44 | }); 45 | 46 | it("should register benchmark name when using (name: string, fn: Function, options?: Options)", () => { 47 | suite.add = buildSuiteAdd(suite); 48 | suite.add("test", emptyBench); 49 | suite.forEach((bench: CodSpeedBenchmark) => 50 | expect(bench.uri).toBe( 51 | "packages/benchmark.js-plugin/src/__tests__/buildSuiteAdd.test.ts::test" 52 | ) 53 | ); 54 | }); 55 | 56 | it("should register benchmark name when suite name is defined", () => { 57 | suite.name = "suite"; 58 | suite.add = buildSuiteAdd(suite); 59 | suite.add("test", emptyBench); 60 | suite.forEach((bench: CodSpeedBenchmark) => 61 | expect(bench.uri).toBe( 62 | "packages/benchmark.js-plugin/src/__tests__/buildSuiteAdd.test.ts::suite::test" 63 | ) 64 | ); 65 | }); 66 | 67 | it("should call rawAdd with options object", () => { 68 | const rawAdd = jest.fn(); 69 | suite.add = rawAdd; 70 | suite.add = buildSuiteAdd(suite); 71 | const options = { name: "test", delay: 100 }; 72 | suite.add(options); 73 | expect(rawAdd).toHaveBeenCalledWith(options); 74 | }); 75 | 76 | it("should call rawAdd with function and options object", () => { 77 | const rawAdd = jest.fn(); 78 | suite.add = rawAdd; 79 | suite.add = buildSuiteAdd(suite); 80 | const fn = emptyBench; 81 | const options = { name: "test", delay: 100 }; 82 | suite.add("test", fn, options); 83 | expect(rawAdd).toHaveBeenCalledWith("test", fn, { 84 | ...options, 85 | uri: "packages/benchmark.js-plugin/src/__tests__/buildSuiteAdd.test.ts::test", 86 | }); 87 | }); 88 | 89 | it("should call rawAdd with name and options object", () => { 90 | const rawAdd = jest.fn(); 91 | suite.add = rawAdd; 92 | suite.add = buildSuiteAdd(suite); 93 | const options = { name: "test", delay: 100 }; 94 | suite.add("test", options); 95 | expect(rawAdd).toHaveBeenCalledWith("test", options); 96 | }); 97 | 98 | it("should call rawAdd with function and undefined options", () => { 99 | const rawAdd = jest.fn(); 100 | suite.add = rawAdd; 101 | suite.add = buildSuiteAdd(suite); 102 | const fn = emptyBench; 103 | suite.add("test", fn); 104 | expect(rawAdd).toHaveBeenCalledWith("test", fn, { 105 | uri: "packages/benchmark.js-plugin/src/__tests__/buildSuiteAdd.test.ts::test", 106 | }); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /packages/benchmark.js-plugin/src/buildSuiteAdd.ts: -------------------------------------------------------------------------------- 1 | import { Options, Suite } from "benchmark"; 2 | import { isFunction, isPlainObject } from "lodash"; 3 | import getCallingFile from "./getCallingFile"; 4 | 5 | function isOptions(options: unknown): options is Options { 6 | return isPlainObject(options); 7 | } 8 | 9 | export default function buildSuiteAdd(suite: Suite) { 10 | const rawAdd = suite.add; 11 | const suiteName = suite.name; 12 | 13 | function registerBenchmarkName(name: string) { 14 | const callingFile = getCallingFile(2); // [here, suite.add, actual caller] 15 | let uri = callingFile; 16 | if (suiteName !== undefined) { 17 | uri += `::${suiteName}`; 18 | } 19 | uri += `::${name}`; 20 | 21 | return uri; 22 | } 23 | 24 | function add(options: Options): Suite; 25 | // eslint-disable-next-line @typescript-eslint/ban-types 26 | function add(fn: Function, options?: Options): Suite; 27 | function add(name: string, options?: Options): Suite; 28 | // eslint-disable-next-line @typescript-eslint/ban-types 29 | function add(name: string, fn: Function, options?: Options): Suite; 30 | function add(name: unknown, fn?: unknown, opts?: unknown) { 31 | // 1 argument: (options: Options) 32 | if (isOptions(name)) { 33 | if (name.name !== undefined) { 34 | const rawFn = name.fn; 35 | if (typeof rawFn === "function") { 36 | const uri = registerBenchmarkName(name.name); 37 | const options = Object.assign({}, name, { uri }); 38 | return rawAdd.bind(suite)(options); 39 | } 40 | } 41 | return rawAdd.bind(suite)(name); 42 | } 43 | 44 | // 2 arguments: (fn: Function, options?: Options) 45 | if (isFunction(name) && (isOptions(fn) || fn === undefined)) { 46 | if (fn !== undefined) { 47 | if (fn.name !== undefined) { 48 | const uri = registerBenchmarkName(fn.name); 49 | const options = Object.assign({}, fn, { uri }); 50 | return rawAdd.bind(suite)(name, options); 51 | } 52 | } 53 | return rawAdd.bind(suite)(name, fn); 54 | } 55 | 56 | // 2 arguments: (name: string, options?: Options) 57 | if (typeof name === "string" && (isOptions(fn) || fn === undefined)) { 58 | if (fn !== undefined && typeof fn.fn === "function") { 59 | const uri = registerBenchmarkName(name); 60 | const options = Object.assign({}, fn, { uri }); 61 | return rawAdd.bind(suite)(name, options); 62 | } 63 | return rawAdd.bind(suite)(name, fn); 64 | } 65 | 66 | // 3 arguments: (name: string, fn: Function, options?: Options) 67 | if ( 68 | typeof name === "string" && 69 | isFunction(fn) && 70 | (isOptions(opts) || opts === undefined) 71 | ) { 72 | const uri = registerBenchmarkName(name); 73 | const options = Object.assign({}, opts ?? {}, { uri }); 74 | return rawAdd.bind(suite)(name, fn, options); 75 | } 76 | } 77 | 78 | return add; 79 | } 80 | -------------------------------------------------------------------------------- /packages/benchmark.js-plugin/src/getCallingFile.ts: -------------------------------------------------------------------------------- 1 | import { getGitDir } from "@codspeed/core"; 2 | import path from "path"; 3 | import { get as getStackTrace } from "stack-trace"; 4 | import { fileURLToPath } from "url"; 5 | 6 | export default function getCallingFile(depth: number): string { 7 | const stack = getStackTrace(); 8 | let callingFile = stack[depth + 1].getFileName(); 9 | const gitDir = getGitDir(callingFile); 10 | if (gitDir === undefined) { 11 | throw new Error("Could not find a git repository"); 12 | } 13 | if (callingFile.startsWith("file://")) { 14 | callingFile = fileURLToPath(callingFile); 15 | } 16 | return path.relative(gitDir, callingFile); 17 | } 18 | -------------------------------------------------------------------------------- /packages/benchmark.js-plugin/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Measurement, 3 | mongoMeasurement, 4 | optimizeFunction, 5 | optimizeFunctionSync, 6 | setupCore, 7 | SetupInstrumentsRequestBody, 8 | SetupInstrumentsResponse, 9 | teardownCore, 10 | tryIntrospect, 11 | } from "@codspeed/core"; 12 | import Benchmark from "benchmark"; 13 | import buildSuiteAdd from "./buildSuiteAdd"; 14 | import getCallingFile from "./getCallingFile"; 15 | import { CodSpeedBenchmark } from "./types"; 16 | 17 | declare const __VERSION__: string; 18 | 19 | tryIntrospect(); 20 | 21 | interface WithCodSpeedBenchmark 22 | extends Omit< 23 | Benchmark, 24 | "run" | "abort" | "clone" | "compare" | "emit" | "off" | "on" | "reset" 25 | > { 26 | abort(): WithCodSpeedBenchmark; 27 | clone(options: Benchmark.Options): WithCodSpeedBenchmark; 28 | compare(benchmark: Benchmark): number; 29 | off( 30 | type?: string, 31 | listener?: CallableFunction 32 | ): Benchmark | Promise; 33 | off(types: string[]): WithCodSpeedBenchmark; 34 | on(type?: string, listener?: CallableFunction): WithCodSpeedBenchmark; 35 | on(types: string[]): WithCodSpeedBenchmark; 36 | reset(): WithCodSpeedBenchmark; 37 | // Makes run an async function 38 | run(options?: Benchmark.Options): Benchmark | Promise; 39 | } 40 | 41 | export interface WithCodSpeedSuite 42 | extends Omit< 43 | Benchmark.Suite, 44 | | "run" 45 | | "abort" 46 | | "clone" 47 | | "compare" 48 | | "emit" 49 | | "off" 50 | | "on" 51 | | "reset" 52 | | "add" 53 | | "filter" 54 | | "each" 55 | | "forEach" 56 | > { 57 | abort(): WithCodSpeedSuite; 58 | add( 59 | name: string, 60 | fn: CallableFunction | string, 61 | options?: Benchmark.Options 62 | ): WithCodSpeedSuite; 63 | add( 64 | fn: CallableFunction | string, 65 | options?: Benchmark.Options 66 | ): WithCodSpeedSuite; 67 | add(name: string, options?: Benchmark.Options): WithCodSpeedSuite; 68 | add(options: Benchmark.Options): WithCodSpeedSuite; 69 | clone(options: Benchmark.Options): WithCodSpeedSuite; 70 | filter(callback: CallableFunction | string): WithCodSpeedSuite; 71 | off(type?: string, callback?: CallableFunction): WithCodSpeedSuite; 72 | off(types: string[]): WithCodSpeedSuite; 73 | on(type?: string, callback?: CallableFunction): WithCodSpeedSuite; 74 | on(types: string[]): WithCodSpeedSuite; 75 | reset(): WithCodSpeedSuite; 76 | each(callback: CallableFunction): WithCodSpeedSuite; 77 | forEach(callback: CallableFunction): WithCodSpeedSuite; 78 | 79 | run(options?: Benchmark.Options): Benchmark.Suite | Promise; 80 | } 81 | 82 | export function withCodSpeed(suite: Benchmark): WithCodSpeedBenchmark; 83 | export function withCodSpeed(suite: Benchmark.Suite): WithCodSpeedSuite; 84 | export function withCodSpeed(item: unknown): unknown { 85 | if ((item as { length?: number }).length === undefined) { 86 | return withCodSpeedBenchmark(item as Benchmark); 87 | } else { 88 | return withCodSpeedSuite(item as Benchmark.Suite); 89 | } 90 | } 91 | 92 | function withCodSpeedBenchmark(bench: Benchmark): WithCodSpeedBenchmark { 93 | if (!Measurement.isInstrumented()) { 94 | const rawRun = bench.run; 95 | bench.run = (options?: Benchmark.Options) => { 96 | console.warn( 97 | `[CodSpeed] bench detected but no instrumentation found, falling back to benchmark.js` 98 | ); 99 | return rawRun.bind(bench)(options); 100 | }; 101 | return bench; 102 | } 103 | const callingFile = getCallingFile(2); // [here, withCodSpeed, actual caller] 104 | const codspeedBench = bench as BenchmarkWithOptions; 105 | if (codspeedBench.name !== undefined) { 106 | codspeedBench.uri = `${callingFile}::${bench.name}`; 107 | } 108 | // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/ban-ts-comment 109 | // @ts-ignore 110 | bench.run = async function (options?: Benchmark.Options): Promise { 111 | await runBenchmarks({ 112 | benches: [codspeedBench], 113 | baseUri: callingFile, 114 | benchmarkCompletedListeners: bench.listeners("complete"), 115 | options, 116 | }); 117 | return bench; 118 | }; 119 | return bench; 120 | } 121 | 122 | function withCodSpeedSuite(suite: Benchmark.Suite): WithCodSpeedSuite { 123 | if (!Measurement.isInstrumented()) { 124 | const rawRun = suite.run; 125 | suite.run = (options?: Benchmark.Options) => { 126 | console.warn( 127 | `[CodSpeed] ${suite.length} benches detected but no instrumentation found, falling back to benchmark.js` 128 | ); 129 | return rawRun.bind(suite)(options); 130 | }; 131 | return suite as WithCodSpeedSuite; 132 | } 133 | suite.add = buildSuiteAdd(suite); 134 | const callingFile = getCallingFile(2); // [here, withCodSpeed, actual caller] 135 | // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/ban-ts-comment 136 | // @ts-ignore 137 | suite.run = async function ( 138 | options?: Benchmark.Options 139 | ): Promise { 140 | const suiteName = suite.name; 141 | const benches = this as unknown as BenchmarkWithOptions[]; 142 | let baseUri = callingFile; 143 | if (suiteName !== undefined) { 144 | baseUri += `::${suiteName}`; 145 | } 146 | await runBenchmarks({ 147 | benches, 148 | baseUri, 149 | benchmarkCompletedListeners: suite.listeners("complete"), 150 | options, 151 | }); 152 | return suite; 153 | }; 154 | return suite as WithCodSpeedSuite; 155 | } 156 | 157 | type BenchmarkWithOptions = CodSpeedBenchmark & { options: Benchmark.Options }; 158 | 159 | interface RunBenchmarksOptions { 160 | benches: BenchmarkWithOptions[]; 161 | baseUri: string; 162 | benchmarkCompletedListeners: CallableFunction[]; 163 | options?: Benchmark.Options; 164 | } 165 | 166 | async function runBenchmarks({ 167 | benches, 168 | baseUri, 169 | benchmarkCompletedListeners, 170 | }: RunBenchmarksOptions): Promise { 171 | console.log(`[CodSpeed] running with @codspeed/benchmark.js v${__VERSION__}`); 172 | setupCore(); 173 | for (let i = 0; i < benches.length; i++) { 174 | const bench = benches[i]; 175 | const uri = bench.uri ?? `${baseUri}::unknown_${i}`; 176 | const isAsync = bench.options.async || bench.options.defer; 177 | let benchPayload; 178 | if (bench.options.defer) { 179 | benchPayload = () => { 180 | return new Promise((resolve, reject) => { 181 | (bench.fn as CallableFunction)({ resolve, reject }); 182 | }); 183 | }; 184 | } else if (bench.options.async) { 185 | benchPayload = async () => { 186 | await (bench.fn as CallableFunction)(); 187 | }; 188 | } else { 189 | benchPayload = bench.fn as CallableFunction; 190 | } 191 | 192 | if (typeof bench.options.setup === "function") { 193 | await bench.options.setup(); 194 | } 195 | 196 | if (isAsync) { 197 | await optimizeFunction(benchPayload); 198 | await mongoMeasurement.start(uri); 199 | global.gc?.(); 200 | await (async function __codspeed_root_frame__() { 201 | Measurement.startInstrumentation(); 202 | await benchPayload(); 203 | Measurement.stopInstrumentation(uri); 204 | })(); 205 | await mongoMeasurement.stop(uri); 206 | } else { 207 | optimizeFunctionSync(benchPayload); 208 | await mongoMeasurement.start(uri); 209 | (function __codspeed_root_frame__() { 210 | Measurement.startInstrumentation(); 211 | benchPayload(); 212 | Measurement.stopInstrumentation(uri); 213 | })(); 214 | await mongoMeasurement.stop(uri); 215 | } 216 | 217 | if (typeof bench.options.teardown === "function") { 218 | await bench.options.teardown(); 219 | } 220 | 221 | console.log(` ✔ Measured ${uri}`); 222 | benchmarkCompletedListeners.forEach((listener) => listener()); 223 | } 224 | teardownCore(); 225 | console.log(`[CodSpeed] Done running ${benches.length} benches.`); 226 | } 227 | 228 | /** 229 | * Dynamically setup the CodSpeed instruments. 230 | */ 231 | export async function setupInstruments( 232 | body: SetupInstrumentsRequestBody 233 | ): Promise { 234 | if (!Measurement.isInstrumented()) { 235 | console.warn("[CodSpeed] No instrumentation found, using default mongoUrl"); 236 | 237 | return { remoteAddr: body.mongoUrl }; 238 | } 239 | 240 | return await mongoMeasurement.setupInstruments(body); 241 | } 242 | -------------------------------------------------------------------------------- /packages/benchmark.js-plugin/src/types.ts: -------------------------------------------------------------------------------- 1 | import Benchmark, { Options } from "benchmark"; 2 | 3 | export interface CodSpeedBenchOptions extends Options { 4 | uri: string; 5 | } 6 | 7 | export interface CodSpeedBenchmark extends Benchmark { 8 | uri: string; 9 | } 10 | -------------------------------------------------------------------------------- /packages/benchmark.js-plugin/tests/__snapshots__/index.integ.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Benchmark check console output(instrumented=false) 1`] = ` 4 | { 5 | "log": [], 6 | "warn": [ 7 | [ 8 | "[CodSpeed] bench detected but no instrumentation found, falling back to benchmark.js", 9 | ], 10 | ], 11 | } 12 | `; 13 | 14 | exports[`Benchmark check console output(instrumented=true) 1`] = ` 15 | { 16 | "log": [ 17 | [ 18 | " ✔ Measured packages/benchmark.js-plugin/tests/index.integ.test.ts::RegExpSingle", 19 | ], 20 | [ 21 | "[CodSpeed] Done running 1 benches.", 22 | ], 23 | ], 24 | "warn": [], 25 | } 26 | `; 27 | 28 | exports[`Benchmark.Suite check console output(instrumented=false) 1`] = ` 29 | { 30 | "log": [], 31 | "warn": [ 32 | [ 33 | "[CodSpeed] 2 benches detected but no instrumentation found, falling back to benchmark.js", 34 | ], 35 | ], 36 | } 37 | `; 38 | 39 | exports[`Benchmark.Suite check console output(instrumented=true) 1`] = ` 40 | { 41 | "log": [ 42 | [ 43 | " ✔ Measured packages/benchmark.js-plugin/tests/index.integ.test.ts::thesuite::RegExp", 44 | ], 45 | [ 46 | " ✔ Measured packages/benchmark.js-plugin/tests/index.integ.test.ts::thesuite::unknown_1", 47 | ], 48 | [ 49 | "[CodSpeed] Done running 2 benches.", 50 | ], 51 | ], 52 | "warn": [], 53 | } 54 | `; 55 | -------------------------------------------------------------------------------- /packages/benchmark.js-plugin/tests/index.integ.test.ts: -------------------------------------------------------------------------------- 1 | import { mockDeep, mockReset } from "jest-mock-extended"; 2 | const mockCore = mockDeep(); 3 | 4 | import * as core from "@codspeed/core"; 5 | import Benchmark from "benchmark"; 6 | import { withCodSpeed } from ".."; 7 | import { registerBenchmarks } from "./registerBenchmarks"; 8 | import { registerOtherBenchmarks } from "./registerOtherBenchmarks"; 9 | 10 | jest.mock("@codspeed/core", () => { 11 | mockCore.getGitDir = jest.requireActual("@codspeed/core").getGitDir; 12 | return mockCore; 13 | }); 14 | 15 | beforeEach(() => { 16 | mockReset(mockCore); 17 | jest.clearAllMocks(); 18 | }); 19 | 20 | const benchOptions: Benchmark.Options = { 21 | maxTime: 0.01, 22 | }; 23 | 24 | describe("Benchmark", () => { 25 | it("simple benchmark", async () => { 26 | mockCore.Measurement.isInstrumented.mockReturnValue(false); 27 | const bench = withCodSpeed( 28 | new Benchmark( 29 | "RegExp", 30 | function () { 31 | /o/.test("Hello World!"); 32 | }, 33 | benchOptions 34 | ) 35 | ); 36 | const onComplete = jest.fn(); 37 | bench.on("complete", onComplete); 38 | await bench.run(); 39 | expect(onComplete).toHaveBeenCalled(); 40 | expect(mockCore.Measurement.startInstrumentation).not.toHaveBeenCalled(); 41 | expect(mockCore.Measurement.stopInstrumentation).not.toHaveBeenCalled(); 42 | }); 43 | it("check core methods are called", async () => { 44 | mockCore.Measurement.isInstrumented.mockReturnValue(true); 45 | 46 | const bench = withCodSpeed( 47 | new Benchmark( 48 | "RegExpSingle", 49 | function () { 50 | /o/.test("Hello World!"); 51 | }, 52 | benchOptions 53 | ) 54 | ); 55 | const onComplete = jest.fn(); 56 | bench.on("complete", onComplete); 57 | await bench.run(); 58 | expect(onComplete).toHaveBeenCalled(); 59 | expect(mockCore.Measurement.startInstrumentation).toHaveBeenCalled(); 60 | expect(mockCore.Measurement.stopInstrumentation).toHaveBeenCalledWith( 61 | "packages/benchmark.js-plugin/tests/index.integ.test.ts::RegExpSingle" 62 | ); 63 | }); 64 | it("check error handling", async () => { 65 | mockCore.Measurement.isInstrumented.mockReturnValue(true); 66 | const bench = withCodSpeed( 67 | new Benchmark( 68 | "throwing", 69 | () => { 70 | throw new Error("test"); 71 | }, 72 | benchOptions 73 | ) 74 | ); 75 | await expect(bench.run()).rejects.toThrowError("test"); 76 | }); 77 | it.each([true, false])( 78 | "check console output(instrumented=%p) ", 79 | async (instrumented) => { 80 | const logSpy = jest.spyOn(console, "log"); 81 | const warnSpy = jest.spyOn(console, "warn"); 82 | mockCore.Measurement.isInstrumented.mockReturnValue(instrumented); 83 | await withCodSpeed( 84 | new Benchmark( 85 | "RegExpSingle", 86 | function () { 87 | /o/.test("Hello World!"); 88 | }, 89 | benchOptions 90 | ) 91 | ).run(); 92 | if (instrumented) { 93 | expect(logSpy).toHaveBeenCalledWith( 94 | expect.stringContaining( 95 | "[CodSpeed] running with @codspeed/benchmark.js v" 96 | ) 97 | ); 98 | expect({ 99 | log: logSpy.mock.calls.slice(1), 100 | warn: warnSpy.mock.calls, 101 | }).toMatchSnapshot(); 102 | } else { 103 | expect({ 104 | log: logSpy.mock.calls.slice(1), 105 | warn: warnSpy.mock.calls, 106 | }).toMatchSnapshot(); 107 | } 108 | } 109 | ); 110 | it("should call setup and teardown", async () => { 111 | mockCore.Measurement.isInstrumented.mockReturnValue(true); 112 | const setup = jest.fn(); 113 | const teardown = jest.fn(); 114 | const bench = withCodSpeed( 115 | new Benchmark( 116 | "RegExpSingle", 117 | function () { 118 | /o/.test("Hello World!"); 119 | }, 120 | { ...benchOptions, setup, teardown } 121 | ) 122 | ); 123 | await bench.run(); 124 | expect(setup).toHaveBeenCalled(); 125 | expect(teardown).toHaveBeenCalled(); 126 | }); 127 | }); 128 | 129 | describe("Benchmark.Suite", () => { 130 | it("simple suite", async () => { 131 | mockCore.Measurement.isInstrumented.mockReturnValue(false); 132 | const suite = withCodSpeed(new Benchmark.Suite()); 133 | suite.add( 134 | "RegExp", 135 | function () { 136 | /o/.test("Hello World!"); 137 | }, 138 | benchOptions 139 | ); 140 | const onComplete = jest.fn(); 141 | suite.on("complete", onComplete); 142 | await suite.run({ maxTime: 0.1, initCount: 1 }); 143 | expect(onComplete).toHaveBeenCalled(); 144 | expect(mockCore.Measurement.startInstrumentation).not.toHaveBeenCalled(); 145 | expect(mockCore.Measurement.stopInstrumentation).not.toHaveBeenCalled(); 146 | }); 147 | it("check core methods are called", async () => { 148 | mockCore.Measurement.isInstrumented.mockReturnValue(true); 149 | const suite = withCodSpeed(new Benchmark.Suite()).add( 150 | "RegExp", 151 | function () { 152 | /o/.test("Hello World!"); 153 | }, 154 | benchOptions 155 | ); 156 | const onComplete = jest.fn(); 157 | suite.on("complete", onComplete); 158 | await suite.run({ maxTime: 0.1, initCount: 1 }); 159 | expect(mockCore.Measurement.startInstrumentation).toHaveBeenCalled(); 160 | expect(mockCore.Measurement.stopInstrumentation).toHaveBeenCalledWith( 161 | "packages/benchmark.js-plugin/tests/index.integ.test.ts::RegExp" 162 | ); 163 | }); 164 | it("check suite name is in the uri", async () => { 165 | mockCore.Measurement.isInstrumented.mockReturnValue(true); 166 | await withCodSpeed(new Benchmark.Suite("thesuite")) 167 | .add( 168 | "RegExp", 169 | function () { 170 | /o/.test("Hello World!"); 171 | }, 172 | benchOptions 173 | ) 174 | .add(() => { 175 | /o/.test("Hello World!"); 176 | }, benchOptions) 177 | .run(); 178 | expect(mockCore.Measurement.stopInstrumentation).toHaveBeenCalledWith( 179 | "packages/benchmark.js-plugin/tests/index.integ.test.ts::thesuite::RegExp" 180 | ); 181 | expect(mockCore.Measurement.stopInstrumentation).toHaveBeenCalledWith( 182 | "packages/benchmark.js-plugin/tests/index.integ.test.ts::thesuite::unknown_1" 183 | ); 184 | }); 185 | it("check error handling", async () => { 186 | mockCore.Measurement.isInstrumented.mockReturnValue(true); 187 | const bench = withCodSpeed(new Benchmark.Suite("thesuite")).add( 188 | "throwing", 189 | () => { 190 | throw new Error("test"); 191 | } 192 | ); 193 | await expect(bench.run()).rejects.toThrowError("test"); 194 | }); 195 | it.each([true, false])( 196 | "check console output(instrumented=%p) ", 197 | async (instrumented) => { 198 | const logSpy = jest.spyOn(console, "log"); 199 | const warnSpy = jest.spyOn(console, "warn"); 200 | mockCore.Measurement.isInstrumented.mockReturnValue(instrumented); 201 | await withCodSpeed(new Benchmark.Suite("thesuite")) 202 | .add( 203 | "RegExp", 204 | function () { 205 | /o/.test("Hello World!"); 206 | }, 207 | benchOptions 208 | ) 209 | .add(() => { 210 | /o/.test("Hello World!"); 211 | }, benchOptions) 212 | .run(); 213 | if (instrumented) { 214 | expect(logSpy).toHaveBeenCalledWith( 215 | expect.stringContaining( 216 | "[CodSpeed] running with @codspeed/benchmark.js v" 217 | ) 218 | ); 219 | expect({ 220 | log: logSpy.mock.calls.slice(1), 221 | warn: warnSpy.mock.calls, 222 | }).toMatchSnapshot(); 223 | } else { 224 | expect({ 225 | log: logSpy.mock.calls.slice(1), 226 | warn: warnSpy.mock.calls, 227 | }).toMatchSnapshot(); 228 | } 229 | } 230 | ); 231 | it("check nested file path is in the uri when bench is registered in another file", async () => { 232 | mockCore.Measurement.isInstrumented.mockReturnValue(true); 233 | const suite = withCodSpeed(new Benchmark.Suite("thesuite")); 234 | registerBenchmarks(suite); 235 | const onComplete = jest.fn(); 236 | suite.on("complete", onComplete); 237 | await suite.run({ maxTime: 0.1, initCount: 1 }); 238 | expect(mockCore.Measurement.startInstrumentation).toHaveBeenCalled(); 239 | expect(mockCore.Measurement.stopInstrumentation).toHaveBeenCalledWith( 240 | "packages/benchmark.js-plugin/tests/registerBenchmarks.ts::thesuite::RegExp" 241 | ); 242 | }); 243 | it("check that benchmarks with same name have different URIs when registered in different files", async () => { 244 | mockCore.Measurement.isInstrumented.mockReturnValue(true); 245 | const suite = withCodSpeed(new Benchmark.Suite("thesuite")); 246 | registerBenchmarks(suite); 247 | registerOtherBenchmarks(suite); 248 | const onComplete = jest.fn(); 249 | suite.on("complete", onComplete); 250 | await suite.run({ maxTime: 0.1, initCount: 1 }); 251 | expect(mockCore.Measurement.startInstrumentation).toHaveBeenCalled(); 252 | expect(mockCore.Measurement.stopInstrumentation).toHaveBeenCalledWith( 253 | "packages/benchmark.js-plugin/tests/registerBenchmarks.ts::thesuite::RegExp" 254 | ); 255 | expect(mockCore.Measurement.stopInstrumentation).toHaveBeenCalledWith( 256 | "packages/benchmark.js-plugin/tests/registerOtherBenchmarks.ts::thesuite::RegExp" 257 | ); 258 | }); 259 | it("should call setupCore and teardownCore only once after run()", async () => { 260 | mockCore.Measurement.isInstrumented.mockReturnValue(true); 261 | const suite = withCodSpeed(new Benchmark.Suite("thesuite")); 262 | registerBenchmarks(suite); 263 | registerOtherBenchmarks(suite); 264 | 265 | expect(mockCore.setupCore).not.toHaveBeenCalled(); 266 | expect(mockCore.teardownCore).not.toHaveBeenCalled(); 267 | 268 | await suite.run({ maxTime: 0.1, initCount: 1 }); 269 | 270 | expect(mockCore.setupCore).toHaveBeenCalledTimes(1); 271 | expect(mockCore.teardownCore).toHaveBeenCalledTimes(1); 272 | }); 273 | it("should call setup and teardown", async () => { 274 | mockCore.Measurement.isInstrumented.mockReturnValue(true); 275 | const setup = jest.fn(); 276 | const teardown = jest.fn(); 277 | 278 | const suite = withCodSpeed(new Benchmark.Suite("thesuite")).add( 279 | "RegExpSingle", 280 | function () { 281 | /o/.test("Hello World!"); 282 | }, 283 | { ...benchOptions, setup, teardown } 284 | ); 285 | await suite.run(); 286 | 287 | expect(setup).toHaveBeenCalled(); 288 | expect(teardown).toHaveBeenCalled(); 289 | }); 290 | }); 291 | -------------------------------------------------------------------------------- /packages/benchmark.js-plugin/tests/registerBenchmarks.ts: -------------------------------------------------------------------------------- 1 | import type { WithCodSpeedSuite } from ".."; 2 | 3 | export function registerBenchmarks(suite: WithCodSpeedSuite) { 4 | suite.add( 5 | "RegExp", 6 | function () { 7 | /o/.test("Hello World!"); 8 | }, 9 | { maxTime: 0.1 } 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /packages/benchmark.js-plugin/tests/registerOtherBenchmarks.ts: -------------------------------------------------------------------------------- 1 | import type { WithCodSpeedSuite } from ".."; 2 | 3 | export function registerOtherBenchmarks(suite: WithCodSpeedSuite) { 4 | suite.add( 5 | "RegExp", 6 | function () { 7 | /o/.test("Hello World!"); 8 | }, 9 | { maxTime: 0.1 } 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /packages/benchmark.js-plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "rootDir": "src", 6 | "types": ["jest", "node"], 7 | "typeRoots": ["node_modules/@types", "../../node_modules/@types"] 8 | }, 9 | "references": [{ "path": "../core" }], 10 | "include": ["src/**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/benchmark.js-plugin/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["jest", "node"] 5 | }, 6 | "include": ["tests/**/*.ts", "benches/**/*.ts", "jest.config.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | prebuilds 3 | generated -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 |
2 |

@codspeed/core

3 | 4 | The core Node library used to integrate with Codspeed runners 5 | 6 | [![CI](https://github.com/CodSpeedHQ/codspeed-node/actions/workflows/ci.yml/badge.svg)](https://github.com/CodSpeedHQ/codspeed-node/actions/workflows/ci.yml) 7 | [![npm (scoped)](https://img.shields.io/npm/v/@codspeed/core)](https://www.npmjs.com/package/@codspeed/core) 8 | [![Discord](https://img.shields.io/badge/chat%20on-discord-7289da.svg)](https://discord.com/invite/MxpaCfKSqF) 9 | [![CodSpeed Badge](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/CodSpeedHQ/codspeed-node) 10 | 11 |
12 | 13 | For now, this package should not be used directly. Instead, use one of the integration packages: 14 | 15 | - [`@codspeed/vitest-plugin`](../vitest-plugin): vitest compatibility layer for CodSpeed 16 | - [`@codspeed/tinybench-plugin`](../tinybench-plugin): tinybench compatibility layer for CodSpeed 17 | - [`@codspeed/benchmark.js-plugin`](../benchmark.js-plugin): Benchmark.js compatibility layer for CodSpeed 18 | -------------------------------------------------------------------------------- /packages/core/binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "native_core", 5 | "cflags!": [ 6 | "-fno-exceptions" 7 | ], 8 | "cflags_cc!": [ 9 | "-fno-exceptions" 10 | ], 11 | "sources": [ 12 | "src/native_core/measurement/measurement.cc", 13 | "src/native_core/linux_perf/linux_perf.cc", 14 | "src/native_core/linux_perf/linux_perf_listener.cc", 15 | "src/native_core/native_core.cc" 16 | ], 17 | "include_dirs": [ 18 | "/src/", 17 | "/.rollup.cache/", 18 | ], 19 | }; 20 | -------------------------------------------------------------------------------- /packages/core/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: "ts-jest", 4 | testEnvironment: "node", 5 | transform: { 6 | "^.+\\.tsx?$": [ 7 | "ts-jest", 8 | { 9 | tsconfig: "tsconfig.test.json", 10 | }, 11 | ], 12 | }, 13 | testPathIgnorePatterns: [ 14 | "/node_modules/", 15 | "/tests/", 16 | "/.rollup.cache/", 17 | ], 18 | }; 19 | -------------------------------------------------------------------------------- /packages/core/moon.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | clean: 3 | args: 4 | - build 5 | - generated/openapi 6 | build: 7 | deps: 8 | - build-native-addon 9 | - build-tracer-client 10 | 11 | build-native-addon: 12 | command: prebuildify --napi --strip 13 | inputs: 14 | - "src/native_core/**/*.cc" 15 | - "src/native_core/**/*.h" 16 | - "binding.gyp" 17 | outputs: 18 | - "prebuilds" 19 | 20 | build-tracer-client: 21 | inputs: 22 | - "./tracer.spec.json" 23 | outputs: 24 | - "src/generated/openapi" 25 | command: openapi --client axios --input ./tracer.spec.json --name MongoTracer --output ./src/generated/openapi 26 | 27 | typecheck: 28 | deps: 29 | - build-tracer-client 30 | 31 | lint: 32 | deps: 33 | - build-tracer-client 34 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@codspeed/core", 3 | "version": "4.0.1", 4 | "description": "The core Node library used to integrate with Codspeed runners", 5 | "keywords": [ 6 | "codspeed", 7 | "benchmark", 8 | "performance" 9 | ], 10 | "files": [ 11 | "dist", 12 | "prebuilds" 13 | ], 14 | "main": "dist/index.cjs.js", 15 | "module": "dist/index.es5.js", 16 | "types": "dist/index.d.ts", 17 | "gypfile": true, 18 | "author": "Arthur Pastel ", 19 | "repository": "https://github.com/CodSpeedHQ/codspeed-node", 20 | "homepage": "https://codspeed.io", 21 | "license": "Apache-2.0", 22 | "devDependencies": { 23 | "@types/find-up": "^4.0.0", 24 | "node-addon-api": "^5.1.0", 25 | "node-gyp": "^9.3.1", 26 | "openapi-typescript-codegen": "^0.23.0", 27 | "prebuildify": "^5.0.1" 28 | }, 29 | "dependencies": { 30 | "axios": "^1.4.0", 31 | "find-up": "^6.3.0", 32 | "form-data": "^4.0.0", 33 | "node-gyp-build": "^4.6.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/core/rollup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "rollup"; 2 | import { declarationsPlugin, jsPlugins } from "../../rollup.options"; 3 | 4 | import pkg from "./package.json" assert { type: "json" }; 5 | const entrypoint = "src/index.ts"; 6 | 7 | export default defineConfig([ 8 | { 9 | input: entrypoint, 10 | output: [ 11 | { 12 | file: pkg.types, 13 | format: "es", 14 | sourcemap: true, 15 | }, 16 | ], 17 | plugins: declarationsPlugin({ compilerOptions: { composite: false } }), 18 | }, 19 | { 20 | input: entrypoint, 21 | output: [ 22 | { 23 | file: pkg.main, 24 | format: "cjs", 25 | sourcemap: true, 26 | }, 27 | { file: pkg.module, format: "es", sourcemap: true }, 28 | ], 29 | plugins: jsPlugins(pkg.version), 30 | }, 31 | ]); 32 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | import { checkV8Flags } from "./introspection"; 2 | import { MongoMeasurement } from "./mongoMeasurement"; 3 | import native_core from "./native_core"; 4 | 5 | declare const __VERSION__: string; 6 | 7 | const linuxPerf = new native_core.LinuxPerf(); 8 | 9 | export const isBound = native_core.isBound; 10 | 11 | export const mongoMeasurement = new MongoMeasurement(); 12 | 13 | export const setupCore = () => { 14 | native_core.Measurement.stopInstrumentation( 15 | `Metadata: codspeed-node ${__VERSION__}` 16 | ); 17 | linuxPerf.start(); 18 | checkV8Flags(); 19 | }; 20 | 21 | export const teardownCore = () => { 22 | linuxPerf.stop(); 23 | }; 24 | 25 | export type { 26 | SetupInstrumentsRequestBody, 27 | SetupInstrumentsResponse, 28 | } from "./generated/openapi"; 29 | export { getV8Flags, tryIntrospect } from "./introspection"; 30 | export { optimizeFunction, optimizeFunctionSync } from "./optimization"; 31 | export * from "./utils"; 32 | export const Measurement = native_core.Measurement; 33 | -------------------------------------------------------------------------------- /packages/core/src/introspection.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSync } from "fs"; 2 | 3 | const CUSTOM_INTROSPECTION_EXIT_CODE = 0; 4 | 5 | export const getV8Flags = () => { 6 | const nodeVersionMajor = parseInt(process.version.slice(1).split(".")[0]); 7 | 8 | const flags = [ 9 | "--hash-seed=1", 10 | "--random-seed=1", 11 | "--no-opt", 12 | "--predictable", 13 | "--predictable-gc-schedule", 14 | "--interpreted-frames-native-stack", 15 | "--allow-natives-syntax", 16 | "--expose-gc", 17 | "--no-concurrent-sweeping", 18 | "--max-old-space-size=4096", 19 | ]; 20 | if (nodeVersionMajor < 18) { 21 | flags.push("--no-randomize-hashes"); 22 | } 23 | if (nodeVersionMajor < 20) { 24 | flags.push("--no-scavenge-task"); 25 | } 26 | return flags; 27 | }; 28 | 29 | export const tryIntrospect = () => { 30 | if (process.env.__CODSPEED_NODE_CORE_INTROSPECTION_PATH__ !== undefined) { 31 | const introspectionMetadata = { 32 | flags: getV8Flags(), 33 | }; 34 | writeFileSync( 35 | process.env.__CODSPEED_NODE_CORE_INTROSPECTION_PATH__, 36 | JSON.stringify(introspectionMetadata) 37 | ); 38 | process.exit(CUSTOM_INTROSPECTION_EXIT_CODE); 39 | } 40 | }; 41 | 42 | export const checkV8Flags = () => { 43 | const requiredFlags = getV8Flags(); 44 | const actualFlags = process.execArgv; 45 | const missingFlags = requiredFlags.filter( 46 | (flag) => !actualFlags.includes(flag) 47 | ); 48 | if (missingFlags.length > 0) { 49 | console.warn( 50 | `[CodSpeed] missing required flags: ${missingFlags.join(", ")}` 51 | ); 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /packages/core/src/mongoMeasurement.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MongoTracer, 3 | SetupInstrumentsRequestBody, 4 | SetupInstrumentsResponse, 5 | } from "./generated/openapi"; 6 | 7 | export type { SetupInstrumentsRequestBody }; 8 | 9 | export class MongoMeasurement { 10 | private tracerClient: MongoTracer | undefined; 11 | 12 | constructor() { 13 | const serverUrl = process.env.CODSPEED_MONGO_INSTR_SERVER_ADDRESS; 14 | 15 | if (serverUrl !== undefined) { 16 | this.tracerClient = new MongoTracer({ 17 | BASE: serverUrl, 18 | }); 19 | } 20 | } 21 | 22 | public async setupInstruments( 23 | body: SetupInstrumentsRequestBody 24 | ): Promise { 25 | if (this.tracerClient === undefined) { 26 | throw new Error("MongoDB Instrumentation is not enabled"); 27 | } 28 | return await this.tracerClient.instruments.setup(body); 29 | } 30 | 31 | public async start(uri: string) { 32 | if (this.tracerClient !== undefined) { 33 | await this.tracerClient.instrumentation.start({ 34 | uri, 35 | }); 36 | } 37 | } 38 | 39 | public async stop(uri: string) { 40 | if (this.tracerClient !== undefined) { 41 | await this.tracerClient.instrumentation.stop({ 42 | uri, 43 | }); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/core/src/native_core/index.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { LinuxPerf } from "./linux_perf/linux_perf"; 3 | import { Measurement } from "./measurement/measurement"; 4 | interface NativeCore { 5 | Measurement: Measurement; 6 | LinuxPerf: typeof LinuxPerf; 7 | } 8 | 9 | interface NativeCoreWithBindingStatus extends NativeCore { 10 | isBound: boolean; 11 | } 12 | 13 | let native_core: NativeCoreWithBindingStatus; 14 | try { 15 | // eslint-disable-next-line @typescript-eslint/no-var-requires 16 | const nativeCore = require("node-gyp-build")( 17 | path.dirname(__dirname) 18 | ) as NativeCore; 19 | native_core = { 20 | ...nativeCore, 21 | isBound: true, 22 | }; 23 | } catch (e) { 24 | native_core = { 25 | Measurement: { 26 | isInstrumented: () => false, 27 | // eslint-disable-next-line @typescript-eslint/no-empty-function 28 | startInstrumentation: () => {}, 29 | // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function 30 | stopInstrumentation: (at) => {}, 31 | }, 32 | LinuxPerf: class LinuxPerf { 33 | start() { 34 | return false; 35 | } 36 | stop() { 37 | return false; 38 | } 39 | }, 40 | 41 | isBound: false, 42 | }; 43 | } 44 | 45 | export default native_core; 46 | -------------------------------------------------------------------------------- /packages/core/src/native_core/linux_perf/linux_perf.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "linux_perf.h" 4 | #include 5 | 6 | namespace codspeed_native { 7 | 8 | Napi::Object LinuxPerf::Initialize(Napi::Env env, Napi::Object exports) { 9 | Napi::Function func = DefineClass(env, "LinuxPerf", 10 | {InstanceMethod("start", &LinuxPerf::Start), 11 | InstanceMethod("stop", &LinuxPerf::Stop)}); 12 | 13 | exports.Set("LinuxPerf", func); 14 | return exports; 15 | } 16 | 17 | LinuxPerf::LinuxPerf(const Napi::CallbackInfo &info) 18 | : Napi::ObjectWrap(info) { 19 | handler = nullptr; 20 | } 21 | 22 | Napi::Value LinuxPerf::Start(const Napi::CallbackInfo &info) { 23 | if (handler == nullptr) { 24 | v8::Isolate *isolate = v8::Isolate::GetCurrent(); 25 | handler = new LinuxPerfHandler(isolate); 26 | handler->Enable(); 27 | return Napi::Boolean::New(info.Env(), true); 28 | } 29 | return Napi::Boolean::New(info.Env(), false); 30 | } 31 | 32 | Napi::Value LinuxPerf::Stop(const Napi::CallbackInfo &info) { 33 | if (handler != nullptr) { 34 | handler->Disable(); 35 | delete handler; 36 | handler = nullptr; 37 | return Napi::Boolean::New(info.Env(), true); 38 | } 39 | return Napi::Boolean::New(info.Env(), false); 40 | } 41 | 42 | } // namespace codspeed_native -------------------------------------------------------------------------------- /packages/core/src/native_core/linux_perf/linux_perf.h: -------------------------------------------------------------------------------- 1 | #ifndef __LINUX_PERF_H 2 | #define __LINUX_PERF_H 3 | 4 | #include "v8-profiler.h" 5 | #include 6 | #include 7 | #include 8 | 9 | namespace codspeed_native { 10 | 11 | class LinuxPerfHandler : public v8::CodeEventHandler { 12 | public: 13 | explicit LinuxPerfHandler(v8::Isolate *isolate); 14 | ~LinuxPerfHandler() override; 15 | 16 | void Handle(v8::CodeEvent *code_event) override; 17 | 18 | private: 19 | std::ofstream mapFile; 20 | std::string FormatName(v8::CodeEvent *code_event); 21 | v8::Isolate *isolate_; 22 | }; 23 | 24 | class LinuxPerf : public Napi::ObjectWrap { 25 | public: 26 | static Napi::Object Initialize(Napi::Env env, Napi::Object exports); 27 | 28 | LinuxPerf(const Napi::CallbackInfo &info); 29 | ~LinuxPerf() = default; 30 | 31 | Napi::Value Start(const Napi::CallbackInfo &info); 32 | Napi::Value Stop(const Napi::CallbackInfo &info); 33 | 34 | LinuxPerfHandler *handler; 35 | }; 36 | 37 | } // namespace codspeed_native 38 | 39 | #endif // __LINUX_PERF_H -------------------------------------------------------------------------------- /packages/core/src/native_core/linux_perf/linux_perf.ts: -------------------------------------------------------------------------------- 1 | export declare class LinuxPerf { 2 | constructor(); 3 | start(): boolean; 4 | stop(): boolean; 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/src/native_core/linux_perf/linux_perf_listener.cc: -------------------------------------------------------------------------------- 1 | #include "linux_perf.h" 2 | #include "utils.h" 3 | #include 4 | #include 5 | 6 | namespace codspeed_native { 7 | 8 | LinuxPerfHandler::LinuxPerfHandler(v8::Isolate *isolate) 9 | : v8::CodeEventHandler(isolate) { 10 | isolate_ = isolate; 11 | int pid = static_cast(uv_os_getpid()); 12 | mapFile.open("/tmp/perf-" + std::to_string(pid) + ".map"); 13 | } 14 | 15 | LinuxPerfHandler::~LinuxPerfHandler() { mapFile.close(); } 16 | 17 | std::string LinuxPerfHandler::FormatName(v8::CodeEvent *code_event) { 18 | std::string name = std::string(code_event->GetComment()); 19 | if (name.empty()) { 20 | name = v8LocalStringToString(code_event->GetFunctionName()); 21 | } 22 | return name; 23 | } 24 | 25 | void LinuxPerfHandler::Handle(v8::CodeEvent *code_event) { 26 | mapFile << std::hex << code_event->GetCodeStartAddress() << " " 27 | << code_event->GetCodeSize() << " "; 28 | mapFile << v8::CodeEvent::GetCodeEventTypeName(code_event->GetCodeType()) 29 | << ":" << FormatName(code_event) << " " 30 | << v8LocalStringToString(code_event->GetScriptName()) << std::dec 31 | << ":" << code_event->GetScriptLine() << ":" 32 | << code_event->GetScriptColumn() << std::endl; 33 | } 34 | 35 | } // namespace codspeed_native 36 | -------------------------------------------------------------------------------- /packages/core/src/native_core/linux_perf/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef LINUX_PERF_UTILS_H 2 | #define LINUX_PERF_UTILS_H 3 | 4 | #include "v8-profiler.h" 5 | 6 | static inline std::string 7 | v8LocalStringToString(v8::Local v8String) { 8 | std::string buffer(v8String->Utf8Length(v8::Isolate::GetCurrent()) + 1, 0); 9 | v8String->WriteUtf8(v8::Isolate::GetCurrent(), &buffer[0], 10 | v8String->Utf8Length(v8::Isolate::GetCurrent()) + 1); 11 | // Sanitize name, removing unwanted \0 resulted from WriteUtf8 12 | return std::string(buffer.c_str()); 13 | } 14 | 15 | #endif // LINUX_PERF_UTILS_H -------------------------------------------------------------------------------- /packages/core/src/native_core/measurement/measurement.cc: -------------------------------------------------------------------------------- 1 | #include "measurement.h" 2 | #include 3 | 4 | namespace codspeed_native { 5 | namespace Measurement { 6 | 7 | Napi::Boolean IsInstrumented(const Napi::CallbackInfo &info) { 8 | Napi::Env env = info.Env(); 9 | uint depth = RUNNING_ON_VALGRIND; 10 | return Napi::Boolean::New(env, depth > 0); 11 | } 12 | 13 | void StartInstrumentation(const Napi::CallbackInfo &info) { 14 | CALLGRIND_ZERO_STATS; 15 | CALLGRIND_START_INSTRUMENTATION; 16 | return; 17 | } 18 | 19 | void StopInstrumentation(const Napi::CallbackInfo &info) { 20 | CALLGRIND_STOP_INSTRUMENTATION; 21 | Napi::Env env = info.Env(); 22 | if (info.Length() != 1) { 23 | Napi::TypeError::New(env, "Wrong number of arguments") 24 | .ThrowAsJavaScriptException(); 25 | return; 26 | } 27 | if (!info[0].IsString()) { 28 | Napi::TypeError::New(env, "Wrong arguments").ThrowAsJavaScriptException(); 29 | return; 30 | } 31 | 32 | std::string pos = info[0].As().Utf8Value(); 33 | CALLGRIND_DUMP_STATS_AT(&pos.c_str()[0]); 34 | return; 35 | } 36 | 37 | Napi::Object Initialize(Napi::Env env, Napi::Object exports) { 38 | Napi::Object measurementObj = Napi::Object::New(env); 39 | 40 | measurementObj.Set(Napi::String::New(env, "isInstrumented"), 41 | Napi::Function::New(env, IsInstrumented)); 42 | measurementObj.Set(Napi::String::New(env, "startInstrumentation"), 43 | Napi::Function::New(env, StartInstrumentation)); 44 | measurementObj.Set(Napi::String::New(env, "stopInstrumentation"), 45 | Napi::Function::New(env, StopInstrumentation)); 46 | 47 | exports.Set(Napi::String::New(env, "Measurement"), measurementObj); 48 | 49 | return exports; 50 | } 51 | 52 | } // namespace Measurement 53 | } // namespace codspeed_native -------------------------------------------------------------------------------- /packages/core/src/native_core/measurement/measurement.h: -------------------------------------------------------------------------------- 1 | #ifndef MEASUREMENT_H 2 | #define MEASUREMENT_H 3 | 4 | #include 5 | 6 | namespace codspeed_native { 7 | namespace Measurement { 8 | 9 | Napi::Boolean IsInstrumented(const Napi::CallbackInfo &info); 10 | void StartInstrumentation(const Napi::CallbackInfo &info); 11 | void StopInstrumentation(const Napi::CallbackInfo &info); 12 | Napi::Object Initialize(Napi::Env env, Napi::Object exports); 13 | 14 | } // namespace Measurement 15 | } // namespace codspeed_native 16 | 17 | #endif // MEASUREMENT_H 18 | -------------------------------------------------------------------------------- /packages/core/src/native_core/measurement/measurement.ts: -------------------------------------------------------------------------------- 1 | export interface Measurement { 2 | isInstrumented: () => boolean; 3 | startInstrumentation: () => void; 4 | stopInstrumentation: (pos: string) => void; 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/src/native_core/native_core.cc: -------------------------------------------------------------------------------- 1 | #include "linux_perf/linux_perf.h" 2 | #include "measurement/measurement.h" 3 | #include 4 | 5 | namespace codspeed_native { 6 | 7 | Napi::Object Initialize(Napi::Env env, Napi::Object exports) { 8 | codspeed_native::LinuxPerf::Initialize(env, exports); 9 | codspeed_native::Measurement::Initialize(env, exports); 10 | 11 | return exports; 12 | } 13 | 14 | NODE_API_MODULE(native_core, Initialize) 15 | 16 | } // namespace codspeed_native 17 | -------------------------------------------------------------------------------- /packages/core/src/optimization.ts: -------------------------------------------------------------------------------- 1 | export const optimizeFunction = async (fn: CallableFunction) => { 2 | // Source: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#optimization-killers 3 | // a total of 7 calls seems to be the sweet spot 4 | await fn(); 5 | await fn(); 6 | await fn(); 7 | await fn(); 8 | await fn(); 9 | await fn(); 10 | eval("%OptimizeFunctionOnNextCall(fn)"); 11 | await fn(); // optimize 12 | }; 13 | 14 | export const optimizeFunctionSync = (fn: CallableFunction) => { 15 | // Source: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#optimization-killers 16 | // a total of 7 calls seems to be the sweet spot 17 | fn(); 18 | fn(); 19 | fn(); 20 | fn(); 21 | fn(); 22 | fn(); 23 | eval("%OptimizeFunctionOnNextCall(fn)"); 24 | fn(); // optimize 25 | }; 26 | -------------------------------------------------------------------------------- /packages/core/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { findUpSync, Options as FindupOptions } from "find-up"; 2 | import { dirname } from "path"; 3 | 4 | export function getGitDir(path: string): string | undefined { 5 | const dotGitPath = findUpSync(".git", { 6 | cwd: path, 7 | type: "directory", 8 | } as FindupOptions); 9 | return dotGitPath ? dirname(dotGitPath) : undefined; 10 | } 11 | 12 | /** 13 | * Log debug messages if the environment variable `CODSPEED_DEBUG` is set. 14 | */ 15 | export function logDebug(...args: unknown[]) { 16 | if (process.env.CODSPEED_DEBUG) { 17 | console.log(...args); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/core/tests/index.integ.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | export {}; // Make this a module 3 | 4 | beforeEach(() => { 5 | jest.resetModules(); 6 | }); 7 | 8 | describe("with bindings", () => { 9 | it("should be bound", () => { 10 | const isBound = require("..").isBound as boolean; 11 | expect(isBound).toBe(true); 12 | }); 13 | }); 14 | 15 | describe("without bindings", () => { 16 | const initialEnv = process.env; 17 | beforeAll(() => { 18 | process.env.npm_config_arch = "unknown"; 19 | }); 20 | afterAll(() => { 21 | process.env = initialEnv; 22 | }); 23 | it("should not be bound", () => { 24 | const isBound = require("..").isBound as boolean; 25 | expect(isBound).toBe(false); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /packages/core/tracer.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.3", 3 | "info": { 4 | "title": "CodSpeed MongoDB Tracer", 5 | "description": "Instrumentation API for CodSpeed Tracer", 6 | "version": "0.2.0" 7 | }, 8 | "paths": { 9 | "/benchmark/start": { 10 | "post": { 11 | "tags": ["instrumentation"], 12 | "operationId": "start", 13 | "requestBody": { 14 | "content": { 15 | "application/json": { 16 | "schema": { 17 | "$ref": "#/components/schemas/InstrumentationRequestBody" 18 | } 19 | } 20 | }, 21 | "required": true 22 | }, 23 | "responses": { 24 | "200": { 25 | "description": "successful operation", 26 | "content": { 27 | "application/json": { 28 | "schema": { 29 | "$ref": "#/components/schemas/InstrumentationStatus" 30 | } 31 | } 32 | } 33 | }, 34 | "4XX": { 35 | "$ref": "#/components/responses/Error" 36 | }, 37 | "5XX": { 38 | "$ref": "#/components/responses/Error" 39 | } 40 | } 41 | } 42 | }, 43 | "/benchmark/stop": { 44 | "post": { 45 | "tags": ["instrumentation"], 46 | "operationId": "stop", 47 | "requestBody": { 48 | "content": { 49 | "application/json": { 50 | "schema": { 51 | "$ref": "#/components/schemas/InstrumentationRequestBody" 52 | } 53 | } 54 | }, 55 | "required": true 56 | }, 57 | "responses": { 58 | "200": { 59 | "description": "successful operation", 60 | "content": { 61 | "application/json": { 62 | "schema": { 63 | "$ref": "#/components/schemas/InstrumentationStatus" 64 | } 65 | } 66 | } 67 | }, 68 | "4XX": { 69 | "$ref": "#/components/responses/Error" 70 | }, 71 | "5XX": { 72 | "$ref": "#/components/responses/Error" 73 | } 74 | } 75 | } 76 | }, 77 | "/instruments/setup": { 78 | "post": { 79 | "tags": ["instruments"], 80 | "summary": "Start the instruments (proxy and aggregator) for the given `body.mongo_url`.", 81 | "description": "If other endpoints of the instrumentation server are called before this one, they will likely fail as the proxy and aggregator are not running yet.", 82 | "operationId": "setup", 83 | "requestBody": { 84 | "content": { 85 | "application/json": { 86 | "schema": { 87 | "$ref": "#/components/schemas/SetupInstrumentsRequestBody" 88 | } 89 | } 90 | }, 91 | "required": true 92 | }, 93 | "responses": { 94 | "200": { 95 | "description": "successful operation", 96 | "content": { 97 | "application/json": { 98 | "schema": { 99 | "$ref": "#/components/schemas/SetupInstrumentsResponse" 100 | } 101 | } 102 | } 103 | }, 104 | "4XX": { 105 | "$ref": "#/components/responses/Error" 106 | }, 107 | "5XX": { 108 | "$ref": "#/components/responses/Error" 109 | } 110 | } 111 | } 112 | }, 113 | "/status": { 114 | "get": { 115 | "tags": ["instrumentation"], 116 | "operationId": "status", 117 | "responses": { 118 | "200": { 119 | "description": "successful operation", 120 | "content": { 121 | "application/json": { 122 | "schema": { 123 | "$ref": "#/components/schemas/InstrumentationStatus" 124 | } 125 | } 126 | } 127 | }, 128 | "4XX": { 129 | "$ref": "#/components/responses/Error" 130 | }, 131 | "5XX": { 132 | "$ref": "#/components/responses/Error" 133 | } 134 | } 135 | } 136 | }, 137 | "/terminate": { 138 | "post": { 139 | "tags": ["instrumentation"], 140 | "operationId": "terminate", 141 | "responses": { 142 | "200": { 143 | "description": "successful operation", 144 | "content": { 145 | "application/json": { 146 | "schema": { 147 | "$ref": "#/components/schemas/AggregatorStore" 148 | } 149 | } 150 | } 151 | }, 152 | "4XX": { 153 | "$ref": "#/components/responses/Error" 154 | }, 155 | "5XX": { 156 | "$ref": "#/components/responses/Error" 157 | } 158 | } 159 | } 160 | } 161 | }, 162 | "components": { 163 | "responses": { 164 | "Error": { 165 | "description": "Error", 166 | "content": { 167 | "application/json": { 168 | "schema": { 169 | "$ref": "#/components/schemas/Error" 170 | } 171 | } 172 | } 173 | } 174 | }, 175 | "schemas": { 176 | "AggregatorMetadata": { 177 | "type": "object", 178 | "properties": { 179 | "name": { 180 | "type": "string" 181 | }, 182 | "version": { 183 | "type": "string" 184 | } 185 | }, 186 | "required": ["name", "version"] 187 | }, 188 | "AggregatorStore": { 189 | "type": "object", 190 | "properties": { 191 | "metadata": { 192 | "nullable": true, 193 | "allOf": [ 194 | { 195 | "$ref": "#/components/schemas/AggregatorMetadata" 196 | } 197 | ] 198 | }, 199 | "queries": { 200 | "type": "object", 201 | "additionalProperties": { 202 | "type": "array", 203 | "items": { 204 | "$ref": "#/components/schemas/MongoQuery" 205 | } 206 | } 207 | } 208 | }, 209 | "required": ["queries"] 210 | }, 211 | "Document": { 212 | "type": "object" 213 | }, 214 | "Error": { 215 | "description": "Error information from a response.", 216 | "type": "object", 217 | "properties": { 218 | "error_code": { 219 | "type": "string" 220 | }, 221 | "message": { 222 | "type": "string" 223 | }, 224 | "request_id": { 225 | "type": "string" 226 | } 227 | }, 228 | "required": ["message", "request_id"] 229 | }, 230 | "InstrumentationRequestBody": { 231 | "type": "object", 232 | "properties": { 233 | "uri": { 234 | "type": "string" 235 | } 236 | }, 237 | "required": ["uri"] 238 | }, 239 | "InstrumentationStatus": { 240 | "type": "object", 241 | "properties": { 242 | "currentUri": { 243 | "nullable": true, 244 | "type": "string" 245 | } 246 | } 247 | }, 248 | "MongoQuery": { 249 | "type": "object", 250 | "properties": { 251 | "collection": { 252 | "type": "string" 253 | }, 254 | "database": { 255 | "type": "string" 256 | }, 257 | "explanation": { 258 | "nullable": true, 259 | "allOf": [ 260 | { 261 | "$ref": "#/components/schemas/Document" 262 | } 263 | ] 264 | }, 265 | "op": { 266 | "type": "string" 267 | }, 268 | "query_documents": { 269 | "type": "array", 270 | "items": { 271 | "$ref": "#/components/schemas/Document" 272 | } 273 | }, 274 | "response_documents": { 275 | "type": "array", 276 | "items": { 277 | "$ref": "#/components/schemas/Document" 278 | } 279 | } 280 | }, 281 | "required": [ 282 | "collection", 283 | "database", 284 | "op", 285 | "query_documents", 286 | "response_documents" 287 | ] 288 | }, 289 | "SetupInstrumentsRequestBody": { 290 | "type": "object", 291 | "properties": { 292 | "mongoUrl": { 293 | "description": "The full `MONGO_URL` that is usually used to connect to the database.", 294 | "type": "string" 295 | } 296 | }, 297 | "required": ["mongoUrl"] 298 | }, 299 | "SetupInstrumentsResponse": { 300 | "type": "object", 301 | "properties": { 302 | "remoteAddr": { 303 | "description": "The patched `MONGO_URL` that should be used to connect to the database.", 304 | "type": "string" 305 | } 306 | }, 307 | "required": ["remoteAddr"] 308 | } 309 | } 310 | }, 311 | "tags": [ 312 | { 313 | "name": "instrumentation" 314 | }, 315 | { 316 | "name": "instruments" 317 | } 318 | ] 319 | } 320 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "rootDir": "src" 6 | }, 7 | "include": ["src/**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["jest", "node"] 5 | }, 6 | "include": ["tests/**/*.ts", "benches/**/*.ts", "jest.config.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/tinybench-plugin/README.md: -------------------------------------------------------------------------------- 1 |
2 |

@codspeed/tinybench-plugin

3 | 4 | tinybench compatibility layer for CodSpeed 5 | 6 | [![CI](https://github.com/CodSpeedHQ/codspeed-node/actions/workflows/ci.yml/badge.svg)](https://github.com/CodSpeedHQ/codspeed-node/actions/workflows/ci.yml) 7 | [![npm (scoped)](https://img.shields.io/npm/v/@codspeed/tinybench-plugin)](https://www.npmjs.com/package/@codspeed/tinybench-plugin) 8 | [![Discord](https://img.shields.io/badge/chat%20on-discord-7289da.svg)](https://discord.com/invite/MxpaCfKSqF) 9 | [![CodSpeed Badge](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/CodSpeedHQ/codspeed-node) 10 | 11 |
12 | 13 | ## Documentation 14 | 15 | Check out the [documentation](https://docs.codspeed.io/benchmarks/nodejs/tinybench) for complete integration instructions. 16 | 17 | ## Installation 18 | 19 | First, install the plugin [`@codspeed/tinybench-plugin`](https://www.npmjs.com/package/@codspeed/tinybench-plugin) and `tinybench` (if not already installed): 20 | 21 | ```sh 22 | npm install --save-dev @codspeed/tinybench-plugin tinybench 23 | ``` 24 | 25 | or with `yarn`: 26 | 27 | ```sh 28 | yarn add --dev @codspeed/tinybench-plugin tinybench 29 | ``` 30 | 31 | or with `pnpm`: 32 | 33 | ```sh 34 | pnpm add --save-dev @codspeed/tinybench-plugin tinybench 35 | ``` 36 | 37 | ## Usage 38 | 39 | Let's create a fibonacci function and benchmark it with tinybench and the CodSpeed plugin: 40 | 41 | ```js title="benches/bench.mjs" 42 | import { Bench } from "tinybench"; 43 | import { withCodSpeed } from "@codspeed/tinybench-plugin"; 44 | 45 | function fibonacci(n) { 46 | if (n < 2) { 47 | return n; 48 | } 49 | return fibonacci(n - 1) + fibonacci(n - 2); 50 | } 51 | 52 | const bench = withCodSpeed(new Bench()); 53 | 54 | bench 55 | .add("fibonacci10", () => { 56 | fibonacci(10); 57 | }) 58 | .add("fibonacci15", () => { 59 | fibonacci(15); 60 | }); 61 | 62 | await bench.run(); 63 | console.table(bench.table()); 64 | ``` 65 | 66 | Here, a few things are happening: 67 | 68 | - We create a simple recursive fibonacci function. 69 | - We create a new `Bench` instance with CodSpeed support by using the **`withCodSpeed`** helper. This step is **critical** to enable CodSpeed on your benchmarks. 70 | 71 | - We add two benchmarks to the suite and launch it, benching our `fibonacci` function for 10 and 15. 72 | 73 | Now, we can run our benchmarks locally to make sure everything is working as expected: 74 | 75 | ```sh 76 | $ node benches/bench.mjs 77 | [CodSpeed] 2 benches detected but no instrumentation found 78 | [CodSpeed] falling back to tinybench 79 | 80 | ┌─────────┬───────────────┬─────────────┬───────────────────┬──────────┬─────────┐ 81 | │ (index) │ Task Name │ ops/sec │ Average Time (ns) │ Margin │ Samples │ 82 | ├─────────┼───────────────┼─────────────┼───────────────────┼──────────┼─────────┤ 83 | │ 0 │ 'fibonacci10' │ '1,810,236' │ 552.4139857896414 │ '±0.18%' │ 905119 │ 84 | │ 1 │ 'fibonacci15' │ '177,516' │ 5633.276191749634 │ '±0.14%' │ 88759 │ 85 | └─────────┴───────────────┴─────────────┴───────────────────┴──────────┴─────────┘ 86 | ``` 87 | 88 | And... Congrats🎉, CodSpeed is installed in your benchmarking suite! Locally, CodSpeed will fallback to tinybench since the instrumentation is only available in the CI environment for now. 89 | 90 | You can now [run those benchmarks in your CI](https://docs.codspeed.io/benchmarks/nodejs/tinybench#running-the-benchmarks-in-your-ci) to continuously get consistent performance measurements. 91 | -------------------------------------------------------------------------------- /packages/tinybench-plugin/benches/parsePr.ts: -------------------------------------------------------------------------------- 1 | interface PullRequest { 2 | number: number; 3 | title: string; 4 | body: string; 5 | } 6 | 7 | function sendEvent(numberOfOperations: number): void { 8 | for (let i = 0; i < numberOfOperations; i++) { 9 | let a = i; 10 | a = a + 1; 11 | } 12 | } 13 | 14 | function logMetrics( 15 | numberOfOperations: number, 16 | numberOfDeepOperations: number 17 | ): void { 18 | for (let i = 0; i < numberOfOperations; i++) { 19 | for (let i = 0; i < numberOfOperations; i++) { 20 | let a = i; 21 | a = a + 1; 22 | a = a + 1; 23 | } 24 | sendEvent(numberOfDeepOperations); 25 | } 26 | } 27 | 28 | function parseTitle(title: string): void { 29 | logMetrics(10, 10); 30 | modifyTitle(title); 31 | } 32 | 33 | function modifyTitle(title: string): void { 34 | for (let i = 0; i < 100; i++) { 35 | let a = i; 36 | a = a + 1 + title.length; 37 | } 38 | } 39 | 40 | function prepareParsingBody(body: string): void { 41 | for (let i = 0; i < 100; i++) { 42 | let a = i; 43 | a = a + 1; 44 | } 45 | parseBody(body); 46 | } 47 | 48 | function parseBody(body: string): void { 49 | logMetrics(10, 10); 50 | for (let i = 0; i < 200; i++) { 51 | let a = i; 52 | a = a + 1; 53 | } 54 | parseIssueFixed(body); 55 | } 56 | 57 | function parseIssueFixed(body: string): number | null { 58 | const prefix = "fixes #"; 59 | const index = body.indexOf(prefix); 60 | if (index === -1) { 61 | return null; 62 | } 63 | 64 | const start = index + prefix.length; 65 | let end = start; 66 | while (end < body.length && /\d/.test(body[end])) { 67 | end += 1; 68 | } 69 | return parseInt(body.slice(start, end)); 70 | } 71 | 72 | export default function parsePr(pullRequest: PullRequest): void { 73 | parseTitle(pullRequest.title); 74 | prepareParsingBody(pullRequest.body); 75 | } 76 | -------------------------------------------------------------------------------- /packages/tinybench-plugin/benches/sample.ts: -------------------------------------------------------------------------------- 1 | import { Bench } from "tinybench"; 2 | import { withCodSpeed } from ".."; 3 | import parsePr from "./parsePr"; 4 | 5 | const LONG_BODY = 6 | new Array(1_000) 7 | .fill( 8 | "Lorem ipsum dolor sit amet consectetur adipisicing elit. Sunt, earum. Atque architecto vero veniam est tempora fugiat sint quo praesentium quia. Autem, veritatis omnis beatae iste delectus recusandae animi non." 9 | ) 10 | .join("\n") + "fixes #123"; 11 | 12 | const bench = withCodSpeed(new Bench({ time: 100 })); 13 | 14 | bench 15 | .add("switch 1", () => { 16 | let a = 1; 17 | let b = 2; 18 | const c = a; 19 | a = b; 20 | b = c; 21 | }) 22 | .add("switch 2", () => { 23 | let a = 1; 24 | let b = 10; 25 | a = b + a; 26 | b = a - b; 27 | a = b - a; 28 | }) 29 | .add("short body", () => { 30 | parsePr({ body: "fixes #123", title: "test", number: 124 }); 31 | }) 32 | .add("long body", () => { 33 | parsePr({ body: LONG_BODY, title: "test", number: 124 }); 34 | }); 35 | 36 | bench.run().then(() => { 37 | console.table(bench.table()); 38 | }); 39 | -------------------------------------------------------------------------------- /packages/tinybench-plugin/jest.config.integ.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | // eslint-disable-next-line no-undef 3 | module.exports = { 4 | preset: "ts-jest", 5 | testEnvironment: "node", 6 | transform: { 7 | "^.+\\.tsx?$": [ 8 | "ts-jest", 9 | { 10 | tsconfig: "tsconfig.test.json", 11 | }, 12 | ], 13 | }, 14 | testPathIgnorePatterns: [ 15 | "/node_modules/", 16 | "/src/", 17 | "/.rollup.cache/", 18 | ], 19 | }; 20 | -------------------------------------------------------------------------------- /packages/tinybench-plugin/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: "ts-jest", 4 | testEnvironment: "node", 5 | transform: { 6 | "^.+\\.tsx?$": [ 7 | "ts-jest", 8 | { 9 | tsconfig: "tsconfig.test.json", 10 | }, 11 | ], 12 | }, 13 | testPathIgnorePatterns: [ 14 | "/node_modules/", 15 | "/tests/", 16 | "/.rollup.cache/", 17 | ], 18 | }; 19 | -------------------------------------------------------------------------------- /packages/tinybench-plugin/moon.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | bench: 3 | command: node -r esbuild-register benches/sample.ts 4 | inputs: 5 | - "benches/**" 6 | local: true 7 | platform: "system" 8 | options: 9 | cache: false 10 | -------------------------------------------------------------------------------- /packages/tinybench-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@codspeed/tinybench-plugin", 3 | "version": "4.0.1", 4 | "description": "tinybench compatibility layer for CodSpeed", 5 | "keywords": [ 6 | "codspeed", 7 | "benchmark", 8 | "tinybench", 9 | "performance" 10 | ], 11 | "main": "dist/index.cjs.js", 12 | "module": "dist/index.es5.js", 13 | "types": "dist/index.d.ts", 14 | "files": [ 15 | "dist" 16 | ], 17 | "author": "Arthur Pastel ", 18 | "repository": "https://github.com/CodSpeedHQ/codspeed-node", 19 | "homepage": "https://codspeed.io", 20 | "license": "Apache-2.0", 21 | "devDependencies": { 22 | "@types/stack-trace": "^0.0.30", 23 | "jest-mock-extended": "^3.0.4", 24 | "tinybench": "^2.5.0" 25 | }, 26 | "dependencies": { 27 | "@codspeed/core": "workspace:^4.0.1", 28 | "stack-trace": "1.0.0-pre2" 29 | }, 30 | "peerDependencies": { 31 | "tinybench": "^2.3.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/tinybench-plugin/rollup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "rollup"; 2 | import { declarationsPlugin, jsPlugins } from "../../rollup.options"; 3 | import pkg from "./package.json" assert { type: "json" }; 4 | 5 | const entrypoint = "src/index.ts"; 6 | 7 | export default defineConfig([ 8 | { 9 | input: entrypoint, 10 | output: [ 11 | { 12 | file: pkg.types, 13 | format: "es", 14 | sourcemap: true, 15 | }, 16 | ], 17 | plugins: declarationsPlugin({ compilerOptions: { composite: false } }), 18 | }, 19 | { 20 | input: entrypoint, 21 | output: [ 22 | { 23 | file: pkg.main, 24 | format: "cjs", 25 | sourcemap: true, 26 | }, 27 | { file: pkg.module, format: "es", sourcemap: true }, 28 | ], 29 | plugins: jsPlugins(pkg.version), 30 | external: ["@codspeed/core"], 31 | }, 32 | ]); 33 | -------------------------------------------------------------------------------- /packages/tinybench-plugin/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getGitDir, 3 | Measurement, 4 | mongoMeasurement, 5 | optimizeFunction, 6 | setupCore, 7 | SetupInstrumentsRequestBody, 8 | SetupInstrumentsResponse, 9 | teardownCore, 10 | tryIntrospect, 11 | } from "@codspeed/core"; 12 | import path from "path"; 13 | import { get as getStackTrace } from "stack-trace"; 14 | import { Bench, Task } from "tinybench"; 15 | import { fileURLToPath } from "url"; 16 | 17 | declare const __VERSION__: string; 18 | 19 | tryIntrospect(); 20 | 21 | type CodSpeedBenchOptions = Task["opts"] & { 22 | uri: string; 23 | }; 24 | 25 | function isCodSpeedBenchOptions( 26 | options: Task["opts"] 27 | ): options is CodSpeedBenchOptions { 28 | return "uri" in options; 29 | } 30 | 31 | export function withCodSpeed(bench: Bench): Bench { 32 | if (!Measurement.isInstrumented()) { 33 | const rawRun = bench.run; 34 | bench.run = async () => { 35 | console.warn( 36 | `[CodSpeed] ${bench.tasks.length} benches detected but no instrumentation found, falling back to tinybench` 37 | ); 38 | return await rawRun.bind(bench)(); 39 | }; 40 | return bench; 41 | } 42 | 43 | const rawAdd = bench.add; 44 | bench.add = (name, fn, opts: CodSpeedBenchOptions) => { 45 | const callingFile = getCallingFile(); 46 | const uri = `${callingFile}::${name}`; 47 | const options = Object.assign({}, opts ?? {}, { uri }); 48 | return rawAdd.bind(bench)(name, fn, options); 49 | }; 50 | const rootCallingFile = getCallingFile(); 51 | 52 | bench.run = async () => { 53 | console.log(`[CodSpeed] running with @codspeed/tinybench v${__VERSION__}`); 54 | setupCore(); 55 | for (const task of bench.tasks) { 56 | const uri = isCodSpeedBenchOptions(task.opts) 57 | ? task.opts.uri 58 | : `${rootCallingFile}::${task.name}`; 59 | 60 | await task.opts.beforeAll?.call(task); 61 | 62 | // run optimizations 63 | await optimizeFunction(async () => { 64 | await task.opts.beforeEach?.call(task); 65 | await task.fn(); 66 | await task.opts.afterEach?.call(task); 67 | }); 68 | 69 | // run instrumented benchmark 70 | await task.opts.beforeEach?.call(task); 71 | 72 | await mongoMeasurement.start(uri); 73 | global.gc?.(); 74 | await (async function __codspeed_root_frame__() { 75 | Measurement.startInstrumentation(); 76 | await task.fn(); 77 | Measurement.stopInstrumentation(uri); 78 | })(); 79 | await mongoMeasurement.stop(uri); 80 | 81 | await task.opts.afterEach?.call(task); 82 | 83 | await task.opts.afterAll?.call(task); 84 | 85 | // print results 86 | console.log(` ✔ Measured ${uri}`); 87 | } 88 | teardownCore(); 89 | console.log(`[CodSpeed] Done running ${bench.tasks.length} benches.`); 90 | return bench.tasks; 91 | }; 92 | return bench; 93 | } 94 | 95 | function getCallingFile(): string { 96 | const stack = getStackTrace(); 97 | let callingFile = stack[2].getFileName(); // [here, withCodSpeed, actual caller] 98 | const gitDir = getGitDir(callingFile); 99 | if (gitDir === undefined) { 100 | throw new Error("Could not find a git repository"); 101 | } 102 | if (callingFile.startsWith("file://")) { 103 | callingFile = fileURLToPath(callingFile); 104 | } 105 | return path.relative(gitDir, callingFile); 106 | } 107 | 108 | /** 109 | * Dynamically setup the CodSpeed instruments. 110 | */ 111 | export async function setupInstruments( 112 | body: SetupInstrumentsRequestBody 113 | ): Promise { 114 | if (!Measurement.isInstrumented()) { 115 | console.warn("[CodSpeed] No instrumentation found, using default mongoUrl"); 116 | 117 | return { remoteAddr: body.mongoUrl }; 118 | } 119 | 120 | return await mongoMeasurement.setupInstruments(body); 121 | } 122 | -------------------------------------------------------------------------------- /packages/tinybench-plugin/tests/__snapshots__/index.integ.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Benchmark.Suite check console output(instrumented=false) 1`] = ` 4 | { 5 | "log": [], 6 | "warn": [ 7 | [ 8 | "[CodSpeed] 2 benches detected but no instrumentation found, falling back to tinybench", 9 | ], 10 | ], 11 | } 12 | `; 13 | 14 | exports[`Benchmark.Suite check console output(instrumented=true) 1`] = ` 15 | { 16 | "log": [ 17 | [ 18 | " ✔ Measured packages/tinybench-plugin/tests/index.integ.test.ts::RegExp", 19 | ], 20 | [ 21 | " ✔ Measured packages/tinybench-plugin/tests/index.integ.test.ts::RegExp2", 22 | ], 23 | [ 24 | "[CodSpeed] Done running 2 benches.", 25 | ], 26 | ], 27 | "warn": [], 28 | } 29 | `; 30 | -------------------------------------------------------------------------------- /packages/tinybench-plugin/tests/index.integ.test.ts: -------------------------------------------------------------------------------- 1 | import { mockDeep, mockReset } from "jest-mock-extended"; 2 | const mockCore = mockDeep(); 3 | 4 | import * as core from "@codspeed/core"; 5 | import { Bench } from "tinybench"; 6 | import { withCodSpeed } from ".."; 7 | import { registerBenchmarks } from "./registerBenchmarks"; 8 | import { registerOtherBenchmarks } from "./registerOtherBenchmarks"; 9 | 10 | jest.mock("@codspeed/core", () => { 11 | mockCore.getGitDir = jest.requireActual("@codspeed/core").getGitDir; 12 | return mockCore; 13 | }); 14 | 15 | beforeEach(() => { 16 | mockReset(mockCore); 17 | jest.clearAllMocks(); 18 | }); 19 | 20 | describe("Benchmark.Suite", () => { 21 | it("simple suite", async () => { 22 | mockCore.Measurement.isInstrumented.mockReturnValue(false); 23 | const bench = withCodSpeed(new Bench({ time: 100 })); 24 | const onComplete = jest.fn(); 25 | bench.add("RegExp", function () { 26 | /o/.test("Hello World!"); 27 | }); 28 | bench.getTask("RegExp")?.addEventListener("complete", onComplete); 29 | await bench.run(); 30 | 31 | expect(onComplete).toHaveBeenCalled(); 32 | expect(mockCore.mongoMeasurement.start).not.toHaveBeenCalled(); 33 | expect(mockCore.mongoMeasurement.stop).not.toHaveBeenCalled(); 34 | expect(mockCore.Measurement.startInstrumentation).not.toHaveBeenCalled(); 35 | expect(mockCore.Measurement.stopInstrumentation).not.toHaveBeenCalled(); 36 | }); 37 | it("check core methods are called", async () => { 38 | mockCore.Measurement.isInstrumented.mockReturnValue(true); 39 | await withCodSpeed(new Bench()) 40 | .add("RegExp", function () { 41 | /o/.test("Hello World!"); 42 | }) 43 | .run(); 44 | 45 | expect(mockCore.mongoMeasurement.start).toHaveBeenCalledWith( 46 | "packages/tinybench-plugin/tests/index.integ.test.ts::RegExp" 47 | ); 48 | expect(mockCore.mongoMeasurement.stop).toHaveBeenCalledTimes(1); 49 | expect(mockCore.Measurement.startInstrumentation).toHaveBeenCalled(); 50 | expect(mockCore.Measurement.stopInstrumentation).toHaveBeenCalledWith( 51 | "packages/tinybench-plugin/tests/index.integ.test.ts::RegExp" 52 | ); 53 | }); 54 | it("check suite name is in the uri", async () => { 55 | mockCore.Measurement.isInstrumented.mockReturnValue(true); 56 | await withCodSpeed(new Bench()) 57 | .add("RegExp", function () { 58 | /o/.test("Hello World!"); 59 | }) 60 | .add("RegExp2", () => { 61 | /o/.test("Hello World!"); 62 | }) 63 | .run(); 64 | 65 | expect(mockCore.mongoMeasurement.start).toHaveBeenCalledWith( 66 | "packages/tinybench-plugin/tests/index.integ.test.ts::RegExp" 67 | ); 68 | expect(mockCore.mongoMeasurement.start).toHaveBeenCalledWith( 69 | "packages/tinybench-plugin/tests/index.integ.test.ts::RegExp2" 70 | ); 71 | expect(mockCore.mongoMeasurement.stop).toHaveBeenCalledTimes(2); 72 | expect(mockCore.Measurement.stopInstrumentation).toHaveBeenCalledWith( 73 | "packages/tinybench-plugin/tests/index.integ.test.ts::RegExp" 74 | ); 75 | expect(mockCore.Measurement.stopInstrumentation).toHaveBeenCalledWith( 76 | "packages/tinybench-plugin/tests/index.integ.test.ts::RegExp2" 77 | ); 78 | }); 79 | it("check error handling", async () => { 80 | mockCore.Measurement.isInstrumented.mockReturnValue(true); 81 | const bench = withCodSpeed(new Bench()); 82 | bench.add("throwing", async () => { 83 | throw new Error("test"); 84 | }); 85 | await expect(bench.run()).rejects.toThrowError("test"); 86 | }); 87 | it.each([true, false])( 88 | "check console output(instrumented=%p) ", 89 | async (instrumented) => { 90 | const logSpy = jest.spyOn(console, "log"); 91 | const warnSpy = jest.spyOn(console, "warn"); 92 | mockCore.Measurement.isInstrumented.mockReturnValue(instrumented); 93 | await withCodSpeed(new Bench({ time: 100 })) 94 | .add("RegExp", function () { 95 | /o/.test("Hello World!"); 96 | }) 97 | .add("RegExp2", () => { 98 | /o/.test("Hello World!"); 99 | }) 100 | .run(); 101 | // Check that the first log contains "[CodSpeed] running with @codspeed/tinybench v" 102 | if (instrumented) { 103 | expect(logSpy).toHaveBeenCalledWith( 104 | expect.stringContaining( 105 | "[CodSpeed] running with @codspeed/tinybench v" 106 | ) 107 | ); 108 | expect({ 109 | log: logSpy.mock.calls.slice(1), 110 | warn: warnSpy.mock.calls, 111 | }).toMatchSnapshot(); 112 | } else { 113 | expect({ 114 | log: logSpy.mock.calls, 115 | warn: warnSpy.mock.calls, 116 | }).toMatchSnapshot(); 117 | } 118 | } 119 | ); 120 | it("check nested file path is in the uri when bench is registered in another file", async () => { 121 | mockCore.Measurement.isInstrumented.mockReturnValue(true); 122 | const bench = withCodSpeed(new Bench()); 123 | registerBenchmarks(bench); 124 | await bench.run(); 125 | expect(mockCore.Measurement.startInstrumentation).toHaveBeenCalled(); 126 | expect(mockCore.Measurement.stopInstrumentation).toHaveBeenCalledWith( 127 | "packages/tinybench-plugin/tests/registerBenchmarks.ts::RegExp" 128 | ); 129 | }); 130 | // TODO: this is not supported at the moment as tinybench does not support tasks with same name 131 | // remove `.failing` when tinybench supports it 132 | it.failing( 133 | "check that benchmarks with same name have different URIs when registered in different files", 134 | async () => { 135 | mockCore.Measurement.isInstrumented.mockReturnValue(true); 136 | const bench = withCodSpeed(new Bench()); 137 | registerBenchmarks(bench); 138 | registerOtherBenchmarks(bench); 139 | await bench.run(); 140 | expect(mockCore.Measurement.startInstrumentation).toHaveBeenCalled(); 141 | expect(mockCore.Measurement.stopInstrumentation).toHaveBeenCalledWith( 142 | "packages/tinybench-plugin/tests/registerBenchmarks.ts::RegExp" 143 | ); 144 | expect(mockCore.Measurement.stopInstrumentation).toHaveBeenCalledWith( 145 | "packages/tinybench-plugin/tests/registerOtherBenchmarks.ts::RegExp" 146 | ); 147 | } 148 | ); 149 | 150 | it("should run before and after hooks", async () => { 151 | mockCore.Measurement.isInstrumented.mockReturnValue(true); 152 | mockCore.optimizeFunction.mockImplementation(async (fn) => { 153 | await fn(); 154 | }); 155 | const beforeAll = jest.fn(); 156 | const beforeEach = jest.fn(); 157 | const afterEach = jest.fn(); 158 | const afterAll = jest.fn(); 159 | 160 | await withCodSpeed(new Bench()) 161 | .add( 162 | "RegExp", 163 | function () { 164 | /o/.test("Hello World!"); 165 | }, 166 | { afterAll, afterEach, beforeAll, beforeEach } 167 | ) 168 | .add( 169 | "RegExp2", 170 | () => { 171 | /o/.test("Hello World!"); 172 | }, 173 | { afterAll, afterEach, beforeAll, beforeEach } 174 | ) 175 | .run(); 176 | 177 | // since the optimization is running the benchmark once before the actual run, the each hooks are called twice 178 | expect(beforeEach).toHaveBeenCalledTimes(4); 179 | expect(afterEach).toHaveBeenCalledTimes(4); 180 | 181 | expect(beforeAll).toHaveBeenCalledTimes(2); 182 | expect(afterAll).toHaveBeenCalledTimes(2); 183 | }); 184 | 185 | it("should call setupCore and teardownCore only once after run()", async () => { 186 | mockCore.Measurement.isInstrumented.mockReturnValue(true); 187 | const bench = withCodSpeed(new Bench()) 188 | .add("RegExp", function () { 189 | /o/.test("Hello World!"); 190 | }) 191 | .add("RegExp2", () => { 192 | /o/.test("Hello World!"); 193 | }); 194 | 195 | expect(mockCore.setupCore).not.toHaveBeenCalled(); 196 | expect(mockCore.teardownCore).not.toHaveBeenCalled(); 197 | 198 | await bench.run(); 199 | 200 | expect(mockCore.setupCore).toHaveBeenCalledTimes(1); 201 | expect(mockCore.teardownCore).toHaveBeenCalledTimes(1); 202 | }); 203 | }); 204 | -------------------------------------------------------------------------------- /packages/tinybench-plugin/tests/registerBenchmarks.ts: -------------------------------------------------------------------------------- 1 | import type { Bench } from "tinybench"; 2 | 3 | export function registerBenchmarks(bench: Bench) { 4 | bench.add("RegExp", function () { 5 | /o/.test("Hello World!"); 6 | }); 7 | } 8 | -------------------------------------------------------------------------------- /packages/tinybench-plugin/tests/registerOtherBenchmarks.ts: -------------------------------------------------------------------------------- 1 | import type { Bench } from "tinybench"; 2 | 3 | export function registerOtherBenchmarks(bench: Bench) { 4 | bench.add("RegExp", function () { 5 | /o/.test("Hello World!"); 6 | }); 7 | } 8 | -------------------------------------------------------------------------------- /packages/tinybench-plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "rootDir": "src", 6 | "typeRoots": ["node_modules/@types", "../../node_modules/@types"] 7 | }, 8 | "references": [{ "path": "../core" }], 9 | "include": ["src/**/*.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/tinybench-plugin/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["jest", "node"] 5 | }, 6 | "include": ["tests/**/*.ts", "benches/**/*.ts", "jest.config.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/vitest-plugin/README.md: -------------------------------------------------------------------------------- 1 |
2 |

@codspeed/vitest-plugin

3 | 4 | [Vitest](https://vitest.dev) plugin for [CodSpeed](https://codspeed.io) 5 | 6 | [![CI](https://github.com/CodSpeedHQ/codspeed-node/actions/workflows/ci.yml/badge.svg)](https://github.com/CodSpeedHQ/codspeed-node/actions/workflows/ci.yml) 7 | [![npm (scoped)](https://img.shields.io/npm/v/@codspeed/tinybench-plugin)](https://www.npmjs.com/package/@codspeed/tinybench-plugin) 8 | [![Discord](https://img.shields.io/badge/chat%20on-discord-7289da.svg)](https://discord.com/invite/MxpaCfKSqF) 9 | [![CodSpeed Badge](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/CodSpeedHQ/codspeed-node) 10 | 11 |
12 | 13 | ## Documentation 14 | 15 | Check out the [documentation](https://docs.codspeed.io/benchmarks/nodejs/vitest) for complete integration instructions. 16 | 17 | ## Installation 18 | 19 | First, install the plugin [`@codspeed/vitest-plugin`](https://www.npmjs.com/package/@codspeed/vitest-plugin) and `vitest` (if not already installed): 20 | 21 | > [!NOTE] 22 | > The CodSpeed plugin is only compatible with 23 | > [vitest@1.2.2](https://www.npmjs.com/package/vitest/v/1.2.2) 24 | > and above. 25 | 26 | ```sh 27 | npm install --save-dev @codspeed/vitest-plugin vitest 28 | ``` 29 | 30 | or with `yarn`: 31 | 32 | ```sh 33 | yarn add --dev @codspeed/vitest-plugin vitest 34 | ``` 35 | 36 | or with `pnpm`: 37 | 38 | ```sh 39 | pnpm add --save-dev @codspeed/vitest-plugin vitest 40 | ``` 41 | 42 | ## Usage 43 | 44 | Let's create a fibonacci function and benchmark it with `vitest.bench`: 45 | 46 | ```ts title="benches/fibo.bench.ts" 47 | import { describe, bench } from "vitest"; 48 | 49 | function fibonacci(n: number): number { 50 | if (n < 2) { 51 | return n; 52 | } 53 | return fibonacci(n - 1) + fibonacci(n - 2); 54 | } 55 | 56 | describe("fibonacci", () => { 57 | bench("fibonacci10", () => { 58 | fibonacci(10); 59 | }); 60 | 61 | bench("fibonacci15", () => { 62 | fibonacci(15); 63 | }); 64 | }); 65 | ``` 66 | 67 | Create or update your `vitest.config.ts` file to use the CodSpeed runner: 68 | 69 | ```ts title="vitest.config.ts" 70 | import { defineConfig } from "vitest/config"; 71 | import codspeedPlugin from "@codspeed/vitest-plugin"; 72 | 73 | export default defineConfig({ 74 | plugins: [codspeedPlugin()], 75 | // ... 76 | }); 77 | ``` 78 | 79 | Finally, run your benchmarks (here with `pnpm`): 80 | 81 | ```bash 82 | $ pnpm vitest bench --run 83 | [CodSpeed] bench detected but no instrumentation found, falling back to default vitest runner 84 | 85 | ... Regular `vitest bench` output 86 | ``` 87 | 88 | And... Congrats 🎉, CodSpeed is installed in your benchmarking suite! Locally, CodSpeed will fallback to vitest since the instrumentation is only available in the CI environment for now. 89 | 90 | You can now [run those benchmarks in your CI](https://docs.codspeed.io/benchmarks/nodejs/vitest#running-the-benchmarks-in-your-ci) to continuously get consistent performance measurements. 91 | -------------------------------------------------------------------------------- /packages/vitest-plugin/benches/flat.bench.ts: -------------------------------------------------------------------------------- 1 | import { bench, describe } from "vitest"; 2 | import parsePr from "./parsePr"; 3 | 4 | const LONG_BODY = 5 | new Array(1_000) 6 | .fill( 7 | "Lorem ipsum dolor sit amet consectetur adipisicing elit. Sunt, earum. Atque architecto vero veniam est tempora fugiat sint quo praesentium quia. Autem, veritatis omnis beatae iste delectus recusandae animi non." 8 | ) 9 | .join("\n") + "fixes #123"; 10 | 11 | describe("parsePr", () => { 12 | bench("short body", () => { 13 | parsePr({ body: "fixes #123", title: "test-1", number: 1 }); 14 | }); 15 | 16 | bench("long body", () => { 17 | parsePr({ body: LONG_BODY, title: "test-2", number: 2 }); 18 | }); 19 | }); 20 | 21 | function fibo(n: number): number { 22 | if (n < 2) return 1; 23 | return fibo(n - 1) + fibo(n - 2); 24 | } 25 | 26 | describe("fibo", () => { 27 | bench("fibo 10", () => { 28 | fibo(10); 29 | }); 30 | bench("fibo 15", () => { 31 | fibo(15); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/vitest-plugin/benches/hooks.bench.ts: -------------------------------------------------------------------------------- 1 | import { 2 | afterAll, 3 | afterEach, 4 | beforeAll, 5 | beforeEach, 6 | bench, 7 | describe, 8 | expect, 9 | } from "vitest"; 10 | 11 | let count = -1; 12 | 13 | beforeAll(() => { 14 | count += 1; 15 | }); 16 | 17 | beforeEach(() => { 18 | count += 1; 19 | }); 20 | 21 | // the count is multiplied by 2 because the bench function is called twice with codspeed (once for the optimization and once for the actual measurement) 22 | bench("one", () => { 23 | expect(count).toBe(1 * 2); 24 | }); 25 | 26 | describe("level1", () => { 27 | bench("two", () => { 28 | expect(count).toBe(2 * 2); 29 | }); 30 | 31 | bench("three", () => { 32 | expect(count).toBe(3 * 2); 33 | }); 34 | 35 | describe("level 2", () => { 36 | beforeEach(() => { 37 | count += 1; 38 | }); 39 | 40 | bench("five", () => { 41 | expect(count).toBe(5 * 2); 42 | }); 43 | 44 | describe("level 3", () => { 45 | bench("seven", () => { 46 | expect(count).toBe(7 * 2); 47 | }); 48 | }); 49 | }); 50 | 51 | describe("level 2 bench nested beforeAll", () => { 52 | beforeAll(() => { 53 | count = 0; 54 | }); 55 | 56 | bench("one", () => { 57 | expect(count).toBe(1 * 2); 58 | }); 59 | }); 60 | 61 | bench("two", () => { 62 | expect(count).toBe(2 * 2); 63 | }); 64 | }); 65 | 66 | describe("hooks cleanup", () => { 67 | let cleanUpCount = 0; 68 | describe("run", () => { 69 | beforeAll(() => { 70 | cleanUpCount += 10; 71 | }); 72 | beforeEach(() => { 73 | cleanUpCount += 1; 74 | }); 75 | afterEach(() => { 76 | cleanUpCount -= 1; 77 | }); 78 | afterAll(() => { 79 | cleanUpCount -= 10; 80 | }); 81 | 82 | bench("one", () => { 83 | expect(cleanUpCount).toBe(11); 84 | }); 85 | bench("two", () => { 86 | expect(cleanUpCount).toBe(11); 87 | }); 88 | }); 89 | bench("end", () => { 90 | expect(cleanUpCount).toBe(0); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /packages/vitest-plugin/benches/parsePr.bench.ts: -------------------------------------------------------------------------------- 1 | import { bench, describe } from "vitest"; 2 | import parsePr from "./parsePr"; 3 | 4 | const LONG_BODY = 5 | new Array(1_000) 6 | .fill( 7 | "Lorem ipsum dolor sit amet consectetur adipisicing elit. Sunt, earum. Atque architecto vero veniam est tempora fugiat sint quo praesentium quia. Autem, veritatis omnis beatae iste delectus recusandae animi non." 8 | ) 9 | .join("\n") + "fixes #123"; 10 | 11 | describe("parsePr", () => { 12 | bench("short body", () => { 13 | parsePr({ body: "fixes #123", title: "test", number: 124 }); 14 | }); 15 | 16 | bench("long body", () => { 17 | parsePr({ body: LONG_BODY, title: "test", number: 124 }); 18 | }); 19 | 20 | describe("nested suite", () => { 21 | bench("short body", () => { 22 | parsePr({ body: "fixes #123", title: "test", number: 124 }); 23 | }); 24 | 25 | bench("long body", () => { 26 | parsePr({ body: LONG_BODY, title: "test", number: 124 }); 27 | }); 28 | 29 | describe("deeply nested suite", () => { 30 | bench("short body", () => { 31 | parsePr({ body: "fixes #123", title: "test", number: 124 }); 32 | }); 33 | }); 34 | }); 35 | }); 36 | 37 | describe("another parsePr", () => { 38 | bench("short body", () => { 39 | parsePr({ body: "fixes #123", title: "test", number: 124 }); 40 | }); 41 | 42 | bench("long body", () => { 43 | parsePr({ body: LONG_BODY, title: "test", number: 124 }); 44 | }); 45 | 46 | describe("nested suite", () => { 47 | bench("short body", () => { 48 | parsePr({ body: "fixes #123", title: "test", number: 124 }); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /packages/vitest-plugin/benches/parsePr.ts: -------------------------------------------------------------------------------- 1 | interface PullRequest { 2 | number: number; 3 | title: string; 4 | body: string; 5 | } 6 | 7 | function sendEvent(numberOfOperations: number): void { 8 | for (let i = 0; i < numberOfOperations; i++) { 9 | let a = i; 10 | a = a + 1; 11 | } 12 | } 13 | 14 | function logMetrics( 15 | numberOfOperations: number, 16 | numberOfDeepOperations: number 17 | ): void { 18 | for (let i = 0; i < numberOfOperations; i++) { 19 | for (let i = 0; i < numberOfOperations; i++) { 20 | let a = i; 21 | a = a + 1; 22 | a = a + 1; 23 | } 24 | sendEvent(numberOfDeepOperations); 25 | } 26 | } 27 | 28 | function parseTitle(title: string): void { 29 | logMetrics(10, 10); 30 | modifyTitle(title); 31 | } 32 | 33 | function modifyTitle(title: string): void { 34 | for (let i = 0; i < 100; i++) { 35 | let a = i; 36 | a = a + 1 + title.length; 37 | } 38 | } 39 | 40 | function prepareParsingBody(body: string): void { 41 | for (let i = 0; i < 100; i++) { 42 | let a = i; 43 | a = a + 1; 44 | } 45 | parseBody(body); 46 | } 47 | 48 | function parseBody(body: string): void { 49 | logMetrics(10, 10); 50 | for (let i = 0; i < 200; i++) { 51 | let a = i; 52 | a = a + 1; 53 | } 54 | parseIssueFixed(body); 55 | } 56 | 57 | function parseIssueFixed(body: string): number | null { 58 | const prefix = "fixes #"; 59 | const index = body.indexOf(prefix); 60 | if (index === -1) { 61 | return null; 62 | } 63 | 64 | const start = index + prefix.length; 65 | let end = start; 66 | while (end < body.length && /\d/.test(body[end])) { 67 | end += 1; 68 | } 69 | return parseInt(body.slice(start, end)); 70 | } 71 | 72 | export default function parsePr(pullRequest: PullRequest): void { 73 | parseTitle(pullRequest.title); 74 | prepareParsingBody(pullRequest.body); 75 | } 76 | -------------------------------------------------------------------------------- /packages/vitest-plugin/moon.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | bench: 3 | command: vitest bench --run 4 | inputs: 5 | - "benches/**" 6 | local: true 7 | options: 8 | cache: false 9 | 10 | test: 11 | command: vitest --run 12 | inputs: 13 | - "./vitest.config.ts" 14 | 15 | test/integ: 16 | command: noop 17 | -------------------------------------------------------------------------------- /packages/vitest-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@codspeed/vitest-plugin", 3 | "version": "4.0.1", 4 | "description": "vitest plugin for CodSpeed", 5 | "keywords": [ 6 | "codspeed", 7 | "benchmark", 8 | "vitest", 9 | "performance" 10 | ], 11 | "module": "./dist/index.mjs", 12 | "types": "./dist/index.d.ts", 13 | "exports": { 14 | ".": { 15 | "types": "./dist/index.d.ts", 16 | "import": "./dist/index.mjs" 17 | } 18 | }, 19 | "type": "module", 20 | "files": [ 21 | "dist" 22 | ], 23 | "author": "Adrien Cacciaguerra ", 24 | "repository": "https://github.com/CodSpeedHQ/codspeed-node", 25 | "homepage": "https://codspeed.io", 26 | "license": "Apache-2.0", 27 | "scripts": { 28 | "bench": "vitest bench" 29 | }, 30 | "dependencies": { 31 | "@codspeed/core": "workspace:^4.0.1" 32 | }, 33 | "peerDependencies": { 34 | "vite": "^4.2.0 || ^5.0.0 || ^6.0.0", 35 | "vitest": ">=1.2.2" 36 | }, 37 | "devDependencies": { 38 | "@total-typescript/shoehorn": "^0.1.1", 39 | "execa": "^8.0.1", 40 | "vite": "^5.0.0", 41 | "vitest": "^1.2.2" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/vitest-plugin/rollup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "rollup"; 2 | import { declarationsPlugin, jsPlugins } from "../../rollup.options"; 3 | import pkg from "./package.json" assert { type: "json" }; 4 | 5 | export default defineConfig([ 6 | { 7 | input: "src/index.ts", 8 | output: { file: pkg.module, format: "es" }, 9 | plugins: jsPlugins(pkg.version), 10 | external: ["@codspeed/core", /^vitest/], 11 | }, 12 | { 13 | input: "src/index.ts", 14 | output: { file: pkg.types, format: "es" }, 15 | plugins: declarationsPlugin({ compilerOptions: { composite: false } }), 16 | }, 17 | { 18 | input: "src/globalSetup.ts", 19 | output: { file: "dist/globalSetup.mjs", format: "es" }, 20 | plugins: jsPlugins(pkg.version), 21 | external: ["@codspeed/core", /^vitest/], 22 | }, 23 | { 24 | input: "src/runner.ts", 25 | output: { file: "dist/runner.mjs", format: "es" }, 26 | plugins: jsPlugins(pkg.version), 27 | external: ["@codspeed/core", /^vitest/], 28 | }, 29 | ]); 30 | -------------------------------------------------------------------------------- /packages/vitest-plugin/src/__tests__/globalSetup.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from "vitest"; 2 | import globalSetup from "../globalSetup"; 3 | 4 | console.log = vi.fn(); 5 | 6 | describe("globalSetup", () => { 7 | it("should log the correct message on setup and teardown, and fail when teardown is called twice", async () => { 8 | const teardown = globalSetup(); 9 | 10 | expect(console.log).toHaveBeenCalledWith( 11 | "[CodSpeed] @codspeed/vitest-plugin v1.0.0 - setup" 12 | ); 13 | 14 | teardown(); 15 | 16 | expect(console.log).toHaveBeenCalledWith( 17 | "[CodSpeed] @codspeed/vitest-plugin v1.0.0 - teardown" 18 | ); 19 | 20 | expect(() => teardown()).toThrowError("teardown called twice"); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/vitest-plugin/src/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { fromPartial } from "@total-typescript/shoehorn"; 2 | import { describe, expect, it, vi } from "vitest"; 3 | import codspeedPlugin from "../index"; 4 | 5 | const coreMocks = vi.hoisted(() => { 6 | return { 7 | Measurement: { 8 | isInstrumented: vi.fn(), 9 | startInstrumentation: vi.fn(), 10 | stopInstrumentation: vi.fn(), 11 | }, 12 | }; 13 | }); 14 | 15 | const resolvedCodSpeedPlugin = codspeedPlugin(); 16 | const applyPluginFunction = resolvedCodSpeedPlugin.apply; 17 | if (typeof applyPluginFunction !== "function") 18 | throw new Error("applyPluginFunction is not a function"); 19 | 20 | vi.mock("@codspeed/core", async (importOriginal) => { 21 | const mod = await importOriginal(); 22 | return { ...mod, ...coreMocks }; 23 | }); 24 | 25 | console.warn = vi.fn(); 26 | 27 | describe("codSpeedPlugin", () => { 28 | it("should have a name", async () => { 29 | expect(resolvedCodSpeedPlugin.name).toBe("codspeed:vitest"); 30 | }); 31 | 32 | it("should enforce to run after the other plugins", async () => { 33 | expect(resolvedCodSpeedPlugin.enforce).toBe("post"); 34 | }); 35 | 36 | describe("apply", () => { 37 | it("should not apply the plugin when the mode is not benchmark", async () => { 38 | const applyPlugin = applyPluginFunction( 39 | {}, 40 | fromPartial({ mode: "test" }) 41 | ); 42 | 43 | expect(applyPlugin).toBe(false); 44 | }); 45 | 46 | it("should apply the plugin when there is no instrumentation", async () => { 47 | coreMocks.Measurement.isInstrumented.mockReturnValue(false); 48 | 49 | const applyPlugin = applyPluginFunction( 50 | {}, 51 | fromPartial({ mode: "benchmark" }) 52 | ); 53 | 54 | expect(console.warn).toHaveBeenCalledWith( 55 | "[CodSpeed] bench detected but no instrumentation found" 56 | ); 57 | expect(applyPlugin).toBe(true); 58 | }); 59 | 60 | it("should apply the plugin when there is instrumentation", async () => { 61 | coreMocks.Measurement.isInstrumented.mockReturnValue(true); 62 | 63 | const applyPlugin = applyPluginFunction( 64 | {}, 65 | fromPartial({ mode: "benchmark" }) 66 | ); 67 | 68 | expect(applyPlugin).toBe(true); 69 | }); 70 | }); 71 | 72 | it("should apply the codspeed config", async () => { 73 | const config = resolvedCodSpeedPlugin.config; 74 | if (typeof config !== "function") 75 | throw new Error("config is not a function"); 76 | 77 | expect(config({}, fromPartial({}))).toStrictEqual({ 78 | test: { 79 | globalSetup: [ 80 | expect.stringContaining("packages/vitest-plugin/src/globalSetup.ts"), 81 | ], 82 | pool: "forks", 83 | poolOptions: { 84 | forks: { 85 | execArgv: [ 86 | "--hash-seed=1", 87 | "--random-seed=1", 88 | "--no-opt", 89 | "--predictable", 90 | "--predictable-gc-schedule", 91 | "--interpreted-frames-native-stack", 92 | "--allow-natives-syntax", 93 | "--expose-gc", 94 | "--no-concurrent-sweeping", 95 | "--max-old-space-size=4096", 96 | "--no-scavenge-task", 97 | ], 98 | }, 99 | }, 100 | runner: expect.stringContaining("packages/vitest-plugin/src/runner.ts"), 101 | }, 102 | }); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /packages/vitest-plugin/src/__tests__/runner.test.ts: -------------------------------------------------------------------------------- 1 | import { fromPartial } from "@total-typescript/shoehorn"; 2 | import { describe, expect, it, Suite, vi } from "vitest"; 3 | import { getBenchFn } from "vitest/suite"; 4 | import CodSpeedRunner from "../runner"; 5 | 6 | const coreMocks = vi.hoisted(() => { 7 | return { 8 | Measurement: { 9 | startInstrumentation: vi.fn(), 10 | stopInstrumentation: vi.fn(), 11 | }, 12 | setupCore: vi.fn(), 13 | teardownCore: vi.fn(), 14 | mongoMeasurement: { 15 | start: vi.fn(), 16 | stop: vi.fn(), 17 | }, 18 | }; 19 | }); 20 | 21 | global.eval = vi.fn(); 22 | 23 | vi.mock("@codspeed/core", async (importOriginal) => { 24 | const mod = await importOriginal(); 25 | return { ...mod, ...coreMocks }; 26 | }); 27 | 28 | console.log = vi.fn(); 29 | 30 | vi.mock("vitest/suite", async (importOriginal) => { 31 | const actual = await importOriginal(); 32 | return { 33 | ...actual, 34 | getBenchFn: vi.fn(), 35 | }; 36 | }); 37 | const mockedGetBenchFn = vi.mocked(getBenchFn); 38 | 39 | describe("CodSpeedRunner", () => { 40 | it("should run the bench functions only twice", async () => { 41 | const benchFn = vi.fn(); 42 | mockedGetBenchFn.mockReturnValue(benchFn); 43 | 44 | const runner = new CodSpeedRunner(fromPartial({})); 45 | const suite = fromPartial({ 46 | filepath: __filename, 47 | name: "test suite", 48 | tasks: [{ mode: "run", meta: { benchmark: true }, name: "test bench" }], 49 | }); 50 | suite.tasks[0].suite = suite; 51 | await runner.runSuite(suite); 52 | 53 | // setup 54 | expect(coreMocks.setupCore).toHaveBeenCalledTimes(1); 55 | expect(console.log).toHaveBeenCalledWith( 56 | "[CodSpeed] running suite packages/vitest-plugin/src/__tests__/runner.test.ts" 57 | ); 58 | 59 | // run 60 | expect(coreMocks.mongoMeasurement.start).toHaveBeenCalledWith( 61 | "packages/vitest-plugin/src/__tests__/runner.test.ts::test bench" 62 | ); 63 | expect(coreMocks.Measurement.startInstrumentation).toHaveBeenCalledTimes(1); 64 | expect(benchFn).toHaveBeenCalledTimes(8); 65 | expect(coreMocks.Measurement.stopInstrumentation).toHaveBeenCalledTimes(1); 66 | expect(coreMocks.mongoMeasurement.stop).toHaveBeenCalledTimes(1); 67 | expect(console.log).toHaveBeenCalledWith( 68 | "[CodSpeed] packages/vitest-plugin/src/__tests__/runner.test.ts::test bench done" 69 | ); 70 | 71 | // teardown 72 | expect(console.log).toHaveBeenCalledWith( 73 | "[CodSpeed] running suite packages/vitest-plugin/src/__tests__/runner.test.ts done" 74 | ); 75 | expect(coreMocks.teardownCore).toHaveBeenCalledTimes(1); 76 | }); 77 | 78 | it("should run nested suites", async () => { 79 | const benchFn = vi.fn(); 80 | mockedGetBenchFn.mockReturnValue(benchFn); 81 | 82 | const runner = new CodSpeedRunner(fromPartial({})); 83 | const rootSuite = fromPartial({ 84 | filepath: __filename, 85 | name: "test suite", 86 | tasks: [ 87 | { 88 | type: "suite", 89 | name: "nested suite", 90 | mode: "run", 91 | tasks: [ 92 | { 93 | mode: "run", 94 | meta: { benchmark: true }, 95 | name: "test bench", 96 | }, 97 | ], 98 | }, 99 | ], 100 | }); 101 | rootSuite.tasks[0].suite = rootSuite; 102 | // @ts-expect-error type is not narrow enough, but it is fine 103 | rootSuite.tasks[0].tasks[0].suite = rootSuite.tasks[0]; 104 | 105 | await runner.runSuite(rootSuite); 106 | 107 | // setup 108 | expect(coreMocks.setupCore).toHaveBeenCalledTimes(1); 109 | expect(console.log).toHaveBeenCalledWith( 110 | "[CodSpeed] running suite packages/vitest-plugin/src/__tests__/runner.test.ts" 111 | ); 112 | 113 | // run 114 | expect(coreMocks.mongoMeasurement.start).toHaveBeenCalledWith( 115 | "packages/vitest-plugin/src/__tests__/runner.test.ts::nested suite::test bench" 116 | ); 117 | expect(coreMocks.Measurement.startInstrumentation).toHaveBeenCalledTimes(1); 118 | expect(benchFn).toHaveBeenCalledTimes(8); 119 | expect(coreMocks.Measurement.stopInstrumentation).toHaveBeenCalledTimes(1); 120 | expect(coreMocks.mongoMeasurement.stop).toHaveBeenCalledTimes(1); 121 | expect(console.log).toHaveBeenCalledWith( 122 | "[CodSpeed] packages/vitest-plugin/src/__tests__/runner.test.ts::nested suite::test bench done" 123 | ); 124 | 125 | // teardown 126 | expect(console.log).toHaveBeenCalledWith( 127 | "[CodSpeed] running suite packages/vitest-plugin/src/__tests__/runner.test.ts done" 128 | ); 129 | expect(coreMocks.teardownCore).toHaveBeenCalledTimes(1); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /packages/vitest-plugin/src/globalSetup.ts: -------------------------------------------------------------------------------- 1 | declare const __VERSION__: string; 2 | 3 | /** 4 | * @deprecated 5 | * TODO: try to use something like `updateTask` from `@vitest/runner` instead to use the output 6 | * of vitest instead console.log but at the moment, `updateTask` is not exposed 7 | */ 8 | function logCodSpeed(message: string) { 9 | console.log(`[CodSpeed] ${message}`); 10 | } 11 | 12 | let teardownHappened = false; 13 | 14 | export default function () { 15 | logCodSpeed(`@codspeed/vitest-plugin v${__VERSION__} - setup`); 16 | 17 | return () => { 18 | if (teardownHappened) throw new Error("teardown called twice"); 19 | teardownHappened = true; 20 | 21 | logCodSpeed(`@codspeed/vitest-plugin v${__VERSION__} - teardown`); 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /packages/vitest-plugin/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getV8Flags, 3 | Measurement, 4 | mongoMeasurement, 5 | SetupInstrumentsRequestBody, 6 | SetupInstrumentsResponse, 7 | } from "@codspeed/core"; 8 | import { join } from "path"; 9 | import { Plugin } from "vite"; 10 | import { UserConfig } from "vitest/config"; 11 | 12 | // get this file's directory path from import.meta.url 13 | const __dirname = new URL(".", import.meta.url).pathname; 14 | const isFileInTs = import.meta.url.endsWith(".ts"); 15 | 16 | function getCodSpeedFileFromName(name: string) { 17 | const fileExtension = isFileInTs ? "ts" : "mjs"; 18 | 19 | return join(__dirname, `${name}.${fileExtension}`); 20 | } 21 | 22 | export default function codspeedPlugin(): Plugin { 23 | return { 24 | name: "codspeed:vitest", 25 | apply(_, { mode }) { 26 | if (mode !== "benchmark") { 27 | return false; 28 | } 29 | if (!Measurement.isInstrumented()) { 30 | console.warn("[CodSpeed] bench detected but no instrumentation found"); 31 | } 32 | return true; 33 | }, 34 | enforce: "post", 35 | config(): UserConfig { 36 | return { 37 | test: { 38 | pool: "forks", 39 | poolOptions: { 40 | forks: { 41 | execArgv: getV8Flags(), 42 | }, 43 | }, 44 | runner: getCodSpeedFileFromName("runner"), 45 | globalSetup: [getCodSpeedFileFromName("globalSetup")], 46 | }, 47 | }; 48 | }, 49 | }; 50 | } 51 | 52 | /** 53 | * Dynamically setup the CodSpeed instruments. 54 | */ 55 | export async function setupInstruments( 56 | body: SetupInstrumentsRequestBody 57 | ): Promise { 58 | if (!Measurement.isInstrumented()) { 59 | console.warn("[CodSpeed] No instrumentation found, using default mongoUrl"); 60 | 61 | return { remoteAddr: body.mongoUrl }; 62 | } 63 | 64 | return await mongoMeasurement.setupInstruments(body); 65 | } 66 | -------------------------------------------------------------------------------- /packages/vitest-plugin/src/runner.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getGitDir, 3 | logDebug, 4 | Measurement, 5 | mongoMeasurement, 6 | optimizeFunction, 7 | setupCore, 8 | teardownCore, 9 | } from "@codspeed/core"; 10 | import path from "path"; 11 | import { Benchmark, chai, Suite, Task } from "vitest"; 12 | import { NodeBenchmarkRunner } from "vitest/runners"; 13 | import { getBenchFn, getHooks } from "vitest/suite"; 14 | 15 | type SuiteHooks = ReturnType; 16 | 17 | function getSuiteHooks(suite: Suite, name: keyof SuiteHooks) { 18 | return getHooks(suite)?.[name] ?? []; 19 | } 20 | 21 | export async function callSuiteHook( 22 | suite: Suite, 23 | currentTask: Task, 24 | name: T 25 | ): Promise { 26 | if (name === "beforeEach" && suite?.suite) { 27 | await callSuiteHook(suite.suite, currentTask, name); 28 | } 29 | 30 | const hooks = getSuiteHooks(suite, name); 31 | 32 | await Promise.all(hooks.map((fn) => fn())); 33 | 34 | if (name === "afterEach" && suite?.suite) { 35 | await callSuiteHook(suite.suite, currentTask, name); 36 | } 37 | } 38 | 39 | const currentFileName = 40 | typeof __filename === "string" 41 | ? __filename 42 | : new URL("runner.mjs", import.meta.url).pathname; 43 | 44 | /** 45 | * @deprecated 46 | * TODO: try to use something like `updateTask` from `@vitest/runner` instead to use the output 47 | * of vitest instead console.log but at the moment, `updateTask` is not exposed 48 | */ 49 | function logCodSpeed(message: string) { 50 | console.log(`[CodSpeed] ${message}`); 51 | } 52 | 53 | async function runBench(benchmark: Benchmark, currentSuiteName: string) { 54 | const uri = `${currentSuiteName}::${benchmark.name}`; 55 | const fn = getBenchFn(benchmark); 56 | 57 | await callSuiteHook(benchmark.suite, benchmark, "beforeEach"); 58 | try { 59 | await optimizeFunction(fn); 60 | } catch (e) { 61 | // if the error is not an assertion error, we want to fail the run 62 | // we allow assertion errors because we want to be able to use `expect` in the benchmark to allow for better authoring 63 | // assertions are allowed to fail in the optimization phase since it might be linked to stateful code 64 | if (!(e instanceof chai.AssertionError)) { 65 | throw e; 66 | } 67 | } 68 | await callSuiteHook(benchmark.suite, benchmark, "afterEach"); 69 | 70 | await callSuiteHook(benchmark.suite, benchmark, "beforeEach"); 71 | await mongoMeasurement.start(uri); 72 | global.gc?.(); 73 | await (async function __codspeed_root_frame__() { 74 | Measurement.startInstrumentation(); 75 | // @ts-expect-error we do not need to bind the function to an instance of tinybench's Bench 76 | await fn(); 77 | Measurement.stopInstrumentation(uri); 78 | })(); 79 | await mongoMeasurement.stop(uri); 80 | await callSuiteHook(benchmark.suite, benchmark, "afterEach"); 81 | 82 | logCodSpeed(`${uri} done`); 83 | } 84 | 85 | async function runBenchmarkSuite(suite: Suite, parentSuiteName?: string) { 86 | const currentSuiteName = parentSuiteName 87 | ? parentSuiteName + "::" + suite.name 88 | : suite.name; 89 | 90 | // do not call `beforeAll` if we are in the root suite, since it is already called by vitest 91 | // see https://github.com/vitest-dev/vitest/blob/1fee63f2598edc228017f18eca325f85ee54aee0/packages/runner/src/run.ts#L293 92 | if (parentSuiteName !== undefined) { 93 | await callSuiteHook(suite, suite, "beforeAll"); 94 | } 95 | 96 | for (const task of suite.tasks) { 97 | if (task.mode !== "run") continue; 98 | 99 | if (task.meta?.benchmark) { 100 | await runBench(task as Benchmark, currentSuiteName); 101 | } else if (task.type === "suite") { 102 | await runBenchmarkSuite(task, currentSuiteName); 103 | } 104 | } 105 | 106 | // do not call `afterAll` if we are in the root suite, since it is already called by vitest 107 | // see https://github.com/vitest-dev/vitest/blob/1fee63f2598edc228017f18eca325f85ee54aee0/packages/runner/src/run.ts#L324 108 | if (parentSuiteName !== undefined) { 109 | await callSuiteHook(suite, suite, "afterAll"); 110 | } 111 | } 112 | 113 | function patchRootSuiteWithFullFilePath(suite: Suite) { 114 | if (suite.filepath === undefined) { 115 | throw new Error("filepath is undefined is the root suite"); 116 | } 117 | const gitDir = getGitDir(suite.filepath); 118 | if (gitDir === undefined) { 119 | throw new Error("Could not find a git repository"); 120 | } 121 | suite.name = path.relative(gitDir, suite.filepath); 122 | } 123 | 124 | class CodSpeedRunner extends NodeBenchmarkRunner { 125 | async runSuite(suite: Suite): Promise { 126 | logDebug(`PROCESS PID: ${process.pid} in ${currentFileName}`); 127 | setupCore(); 128 | 129 | patchRootSuiteWithFullFilePath(suite); 130 | 131 | logCodSpeed(`running suite ${suite.name}`); 132 | 133 | await runBenchmarkSuite(suite); 134 | logCodSpeed(`running suite ${suite.name} done`); 135 | 136 | teardownCore(); 137 | } 138 | } 139 | 140 | export default CodSpeedRunner; 141 | -------------------------------------------------------------------------------- /packages/vitest-plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "rootDir": "src", 6 | "typeRoots": ["node_modules/@types", "../../node_modules/@types"] 7 | }, 8 | "references": [{ "path": "../core" }], 9 | "include": ["src/**/*.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/vitest-plugin/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["jest", "node"] 5 | }, 6 | "include": ["tests/**/*.ts", "benches/**/*.ts", "jest.config.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/vitest-plugin/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | import codspeedPlugin from "./dist/index.mjs"; 3 | 4 | export default defineConfig({ 5 | // @ts-expect-error - TODO: investigate why importing from '.' wants to import only "main" field and thus fail 6 | plugins: [codspeedPlugin()], 7 | define: { 8 | __VERSION__: JSON.stringify("1.0.0"), 9 | }, 10 | test: { 11 | exclude: ["**/tests/**/*", "**/.rollup.cache/**/*"], 12 | mockReset: true, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "examples/*" 3 | - "packages/*" 4 | -------------------------------------------------------------------------------- /rollup.options.js: -------------------------------------------------------------------------------- 1 | import commonjs from "@rollup/plugin-commonjs"; 2 | import json from "@rollup/plugin-json"; 3 | import resolve from "@rollup/plugin-node-resolve"; 4 | import dts from "rollup-plugin-dts"; 5 | import esbuild from "rollup-plugin-esbuild"; 6 | 7 | /** 8 | * @typedef {import('rollup-plugin-dts').Options} DtsOptions 9 | */ 10 | 11 | /** 12 | * @param {DtsOptions} options 13 | */ 14 | export const declarationsPlugin = (options) => [dts(options)]; 15 | 16 | export const jsPlugins = (version) => [ 17 | json(), 18 | esbuild({ 19 | define: { 20 | __VERSION__: '"' + version + '"', 21 | }, 22 | }), 23 | commonjs(), 24 | resolve(), 25 | ]; 26 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Usage: ./scripts/release.sh 3 | set -ex 4 | 5 | # Fail if not on main 6 | if [ "$(git rev-parse --abbrev-ref HEAD)" != "main" ]; then 7 | echo "Not on default branch" 8 | exit 1 9 | fi 10 | 11 | if [ $# -ne 1 ]; then 12 | echo "Usage: ./release.sh " 13 | exit 1 14 | fi 15 | 16 | # Fail if there are any unstaged changes left 17 | git diff --exit-code 18 | 19 | pnpm lerna version $1 --force-publish --no-private 20 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "moduleResolution": "node", 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "lib": ["esnext"], 8 | "types": ["node"], 9 | "strict": true, 10 | "allowSyntheticDefaultImports": true, 11 | "experimentalDecorators": true, 12 | "sourceMap": true, 13 | "emitDecoratorMetadata": true, 14 | "skipLibCheck": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "esModuleInterop": true, 17 | "resolveJsonModule": true, 18 | "isolatedModules": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "include": [], 4 | "exclude": ["**/node_modules"], 5 | "references": [ 6 | { 7 | "path": "packages/benchmark.js-plugin" 8 | }, 9 | { 10 | "path": "packages/tinybench-plugin" 11 | }, 12 | { 13 | "path": "packages/vitest-plugin" 14 | }, 15 | { 16 | "path": "packages/core" 17 | } 18 | ] 19 | } 20 | --------------------------------------------------------------------------------