diff --git a/.eslintrc.json b/.eslintrc.json index fbb4762607..156b35b556 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -17,7 +17,8 @@ "project": [ "./jsconfig.json", "./dev/jsconfig.json", - "./test/jsconfig.json" + "./test/jsconfig.json", + "./benches/jsconfig.json" ] }, "env": { diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml new file mode 100644 index 0000000000..e2a44837dd --- /dev/null +++ b/.github/workflows/bench.yml @@ -0,0 +1,32 @@ +name: Performance Benchmarks + +on: + push: + branches: [master] + pull_request: + merge_group: + workflow_dispatch: +jobs: + benchmark: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version-file: "package.json" + + - name: Install dependencies + run: npm ci + + - name: Build Libs + run: npm run build-libs + + - name: Run Benchmarks + uses: CodSpeedHQ/action@v2 + with: + token: ${{ secrets.CODSPEED_TOKEN }} + run: npm run bench diff --git a/benches/jsconfig.json b/benches/jsconfig.json new file mode 100644 index 0000000000..8b821d6843 --- /dev/null +++ b/benches/jsconfig.json @@ -0,0 +1,43 @@ +{ + "compilerOptions": { + "module": "ES2022", + "target": "ES2022", + "checkJs": true, + "moduleResolution": "node", + "strict": true, + "strictNullChecks": true, + "noImplicitAny": true, + "strictPropertyInitialization": true, + "suppressImplicitAnyIndexErrors": false, + "skipLibCheck": false, + "baseUrl": ".", + "paths": { + "*": ["../types/ext/*"], + "dev/*": ["../types/dev/*"], + "benches/*": ["../types/benches/*"], + "rollup/parseAst": ["../types/other/rollup-parse-ast"], + "ext/json-schema": ["../types/ext/json-schema"], + "json-schema": ["json-schema"] + }, + "types": [ + "chrome", + "firefox-webext-browser", + "handlebars", + "jszip", + "parse5", + "wanakana" + ] + }, + "include": [ + "**/*.js", + "../ext/**/*.js", + "../types/ext/**/*.ts", + "../types/dev/**/*.ts", + "../types/benches/**/*.ts", + "../types/other/globals.d.ts" + ], + "exclude": [ + "../node_modules", + "../dev/lib" + ] +} diff --git a/benches/language-transformer.bench.js b/benches/language-transformer.bench.js new file mode 100644 index 0000000000..9d312fb737 --- /dev/null +++ b/benches/language-transformer.bench.js @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2023-2024 Yomitan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import fs from 'fs'; +import {fileURLToPath} from 'node:url'; +import path from 'path'; +import {bench, describe} from 'vitest'; +import {parseJson} from '../dev/json.js'; +import {LanguageTransformer} from '../ext/js/language/language-transformer.js'; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); + +/** @type {import('language-transformer').LanguageTransformDescriptor} */ +const descriptor = parseJson(fs.readFileSync(path.join(dirname, '..', 'ext', 'data/language/japanese-transforms.json'), {encoding: 'utf8'})); +const languageTransformer = new LanguageTransformer(); +languageTransformer.addDescriptor(descriptor); + +describe('Language transformer basic tests', () => { + const adjectiveInflections = [ + '愛しい', + '愛しそう', + '愛しすぎる', + '愛しかったら', + '愛しかったり', + '愛しくて', + '愛しく', + '愛しくない', + '愛しさ', + '愛しかった', + '愛しくありません', + '愛しくありませんでした', + '愛しき' + ]; + + const verbInflections = [ + '食べる', + '食べます', + '食べた', + '食べました', + '食べて', + '食べられる', + '食べられる', + '食べさせる', + '食べさせられる', + '食べろ', + '食べない', + '食べません', + '食べなかった', + '食べませんでした', + '食べなくて', + '食べられない', + '食べられない', + '食べさせない', + '食べさせられない', + '食べ', + '食べれば', + '食べちゃう', + '食べちまう', + '食べなさい', + '食べそう', + '食べすぎる', + '食べたい', + '食べたら', + '食べたり', + '食べず', + '食べぬ', + '食べ', + '食べましょう', + '食べよう', + '食べとく', + '食べている', + '食べておる', + '食べてる', + '食べとる', + '食べてしまう' + ]; + + const inflectionCombinations = [ + '抱き抱えていなければ', + '抱きかかえていなければ', + '打ち込んでいませんでした', + '食べさせられたくなかった' + ]; + + bench('transformations', () => { + for (const transform of [...adjectiveInflections, ...verbInflections, ...inflectionCombinations]) { + languageTransformer.transform(transform); + } + }); +}); diff --git a/package-lock.json b/package-lock.json index 1473fe6539..3fd30bb103 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "wanakana": "^5.3.1" }, "devDependencies": { + "@codspeed/vitest-plugin": "^3.1.0", "@playwright/test": "^1.39.0", "@stylistic/eslint-plugin-ts": "^1.5.1", "@stylistic/stylelint-plugin": "^2.0.0", @@ -304,6 +305,113 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@codspeed/core": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@codspeed/core/-/core-3.1.0.tgz", + "integrity": "sha512-oYd7X46QhnRkgRbZkqAoX9i3Fwm17FpunK4Ee5RdrvRYR0Xr93ewH8/O5g6uyTPDOOqDEv1v2KRYtWhVgN+2VQ==", + "dev": true, + "dependencies": { + "axios": "^1.4.0", + "find-up": "^6.3.0", + "form-data": "^4.0.0", + "node-gyp-build": "^4.6.0" + } + }, + "node_modules/@codspeed/core/node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@codspeed/core/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@codspeed/core/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@codspeed/core/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@codspeed/core/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/@codspeed/core/node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@codspeed/vitest-plugin": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@codspeed/vitest-plugin/-/vitest-plugin-3.1.0.tgz", + "integrity": "sha512-ms11tUytiQTgB+idxZRUuCUQfgz4LaKTDJCLYm5VTSpOCUU7D5+QWvJnA8X8B9glPfR5siIK8RxrnZP4yuysKQ==", + "dev": true, + "dependencies": { + "@codspeed/core": "^3.1.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0", + "vitest": ">=1.2.2" + } + }, "node_modules/@csstools/css-parser-algorithms": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.5.0.tgz", @@ -2009,6 +2117,17 @@ "node": ">= 4.5.0" } }, + "node_modules/axios": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3225,6 +3344,26 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", @@ -4720,6 +4859,17 @@ "tslib": "^2.0.3" } }, + "node_modules/node-gyp-build": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz", + "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==", + "dev": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -5196,6 +5346,12 @@ "node": ">=6" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -7088,6 +7244,78 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "@codspeed/core": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@codspeed/core/-/core-3.1.0.tgz", + "integrity": "sha512-oYd7X46QhnRkgRbZkqAoX9i3Fwm17FpunK4Ee5RdrvRYR0Xr93ewH8/O5g6uyTPDOOqDEv1v2KRYtWhVgN+2VQ==", + "dev": true, + "requires": { + "axios": "^1.4.0", + "find-up": "^6.3.0", + "form-data": "^4.0.0", + "node-gyp-build": "^4.6.0" + }, + "dependencies": { + "find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "requires": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + } + }, + "locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "requires": { + "p-locate": "^6.0.0" + } + }, + "p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "requires": { + "yocto-queue": "^1.0.0" + } + }, + "p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "requires": { + "p-limit": "^4.0.0" + } + }, + "path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true + }, + "yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true + } + } + }, + "@codspeed/vitest-plugin": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@codspeed/vitest-plugin/-/vitest-plugin-3.1.0.tgz", + "integrity": "sha512-ms11tUytiQTgB+idxZRUuCUQfgz4LaKTDJCLYm5VTSpOCUU7D5+QWvJnA8X8B9glPfR5siIK8RxrnZP4yuysKQ==", + "dev": true, + "requires": { + "@codspeed/core": "^3.1.0" + } + }, "@csstools/css-parser-algorithms": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.5.0.tgz", @@ -8150,6 +8378,17 @@ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "dev": true }, + "axios": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", + "dev": true, + "requires": { + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -9022,6 +9261,12 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, + "follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "dev": true + }, "foreground-child": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", @@ -10085,6 +10330,12 @@ "tslib": "^2.0.3" } }, + "node-gyp-build": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz", + "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==", + "dev": true + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -10397,6 +10648,12 @@ } } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, "psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", diff --git a/package.json b/package.json index 0f8e82cd1b..c4dc2dd851 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "type": "module", "scripts": { + "bench": "vitest bench", "build": "node ./dev/bin/build.js", "build-libs": "node ./dev/bin/build-libs.js", "test": "npm run test-lint-js && npm run test-ts && npm run test-lint-css && npm run test-lint-html && npm run test-code && npm run test-build", @@ -46,6 +47,7 @@ "sourceDir": "ext" }, "devDependencies": { + "@codspeed/vitest-plugin": "^3.1.0", "@playwright/test": "^1.39.0", "@stylistic/eslint-plugin-ts": "^1.5.1", "@stylistic/stylelint-plugin": "^2.0.0", diff --git a/test/data/json.json b/test/data/json.json index fe7df57dd1..1f664587f0 100644 --- a/test/data/json.json +++ b/test/data/json.json @@ -23,6 +23,7 @@ {"path": "test/data/dictionaries/invalid-dictionary6/index.json", "ignore": true}, {"path": "test/jsconfig.json", "ignore": true}, {"path": "test/data/vitest.write.config.json", "ignore": true}, + {"path": "benches/jsconfig.json", "ignore": true}, { "path": "dev/data/manifest-variants.json", @@ -184,4 +185,4 @@ "schema": "ext/data/schemas/dictionary-term-meta-bank-v3-schema.json" } ] -} \ No newline at end of file +} diff --git a/test/json.test.js b/test/json.test.js index 54c33c347f..0689913ad9 100644 --- a/test/json.test.js +++ b/test/json.test.js @@ -36,6 +36,7 @@ function getJsconfigPath(jsconfigType) { switch (jsconfigType) { case 'dev': path = '../dev/jsconfig.json'; break; case 'test': path = '../test/jsconfig.json'; break; + case 'benches': path = '../benches/jsconfig.json'; break; default: path = '../jsconfig.json'; break; } return join(dirname, path); diff --git a/types/test/json.d.ts b/types/test/json.d.ts index 1b4fb91983..d08f9fb0ea 100644 --- a/types/test/json.d.ts +++ b/types/test/json.d.ts @@ -39,4 +39,4 @@ export type JsonFileParseInfo = { export type AjvSchema = Schema; -export type JsconfigType = 'main' | 'dev' | 'test'; +export type JsconfigType = 'main' | 'dev' | 'test' | 'benches'; diff --git a/vitest.config.js b/vitest.config.js index e0e72e6316..7603e92c15 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -14,9 +14,12 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + +import codspeedPlugin from '@codspeed/vitest-plugin'; import {configDefaults, defineConfig} from 'vitest/config'; export default defineConfig({ + plugins: [codspeedPlugin()], test: { exclude: [ ...configDefaults.exclude,