diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml new file mode 100644 index 0000000000..400850ce3e --- /dev/null +++ b/.github/workflows/codspeed.yml @@ -0,0 +1,37 @@ +name: CodSpeed + +on: + push: + branches: [master, dev] + pull_request: + branches: [master] + # `workflow_dispatch` allows CodSpeed to trigger backtest + # performance analysis in order to generate initial data. + workflow_dispatch: + +permissions: + contents: read + id-token: write # for OpenID Connect authentication with CodSpeed + +jobs: + benchmarks: + name: Run benchmarks + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - run: pnpm install --frozen-lockfile + + - name: Run benchmarks + uses: CodSpeedHQ/action@v4 + with: + mode: simulation + run: pnpm bench diff --git a/README.md b/README.md index e92278eeba..436b3c7299 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@
diff --git a/benchmarks/core.bench.ts b/benchmarks/core.bench.ts new file mode 100644 index 0000000000..8e50c371ac --- /dev/null +++ b/benchmarks/core.bench.ts @@ -0,0 +1,60 @@ +import { bench, describe } from 'vitest' + +import { parseDuration } from '@/core/duration.js' +import { compareVersions, getRepoSlug } from '@/core/version.js' +import { inferCredentialVendor, resolveAnthropicAuthMode } from '@/core/credential-inference.js' + +// These benchmarks cover small, pure, hot-path helpers used across the +// scheduling, version-awareness and credential-inference code paths. They are +// CPU-bound and deterministic, which makes them a good fit for CodSpeed's +// simulation instrument. + +describe('parseDuration', () => { + const inputs = ['30m', '1h', '5m30s', '2h15m', '1h30m45s', ' 90s ', 'not-a-duration', ''] + + bench('parse a mix of valid and invalid durations', () => { + for (const input of inputs) { + parseDuration(input) + } + }) +}) + +describe('compareVersions', () => { + const pairs: Array<[string, string]> = [ + ['1.2.3', '1.2.4'], + ['2.0.0', '1.9.9'], + ['1.0.0-beta.1', '1.0.0'], + ['0.50.0-beta.1', '0.50.0-beta.2'], + ['v3.4.5', '3.4.5'], + ['10.0.0', '9.99.99'], + ] + + bench('compare a batch of semver pairs', () => { + for (const [a, b] of pairs) { + compareVersions(a, b) + } + }) +}) + +describe('getRepoSlug', () => { + bench('derive repo slug from package.json repository url', () => { + getRepoSlug() + }) +}) + +describe('credential inference', () => { + const cases = [ + { agent: 'claude', baseUrl: 'https://api.z.ai/v1' }, + { agent: 'codex', baseUrl: 'https://api.minimaxi.com' }, + { agent: 'opencode', baseUrl: 'https://api.moonshot.cn' }, + { agent: 'pi', baseUrl: 'https://example.com/v1' }, + { agent: 'claude', baseUrl: 'https://api.deepseek.com' }, + ] + + bench('infer vendor + anthropic auth mode for a batch of configs', () => { + for (const c of cases) { + inferCredentialVendor(c) + resolveAnthropicAuthMode({ baseUrl: c.baseUrl }) + } + }) +}) diff --git a/package.json b/package.json index 6d9c37de90..5bd2615e45 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "test:watch": "vitest", "test:e2e": "vitest run --config vitest.e2e.config.ts", "test:bbProvider": "vitest run --config vitest.bbProvider.config.ts", + "bench": "vitest bench --run --config vitest.bench.config.ts", "test:smoke": "tsx scripts/guardian/smoke.ts", "postinstall": "node scripts/fix-pty-perms.mjs" }, @@ -106,6 +107,7 @@ }, "devDependencies": { "@ai-sdk/provider": "^3.0.8", + "@codspeed/vitest-plugin": "^5.7.0", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", "@types/node": "^25.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a550b7d2d..3cc4392376 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -100,6 +100,9 @@ importers: '@ai-sdk/provider': specifier: ^3.0.8 version: 3.0.8 + '@codspeed/vitest-plugin': + specifier: ^5.7.0 + version: 5.7.0(tinybench@2.9.0)(vite@7.3.2(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0))(vitest@4.1.5) '@testing-library/react': specifier: ^16.3.2 version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -516,6 +519,16 @@ packages: '@bufbuild/protobuf@2.11.0': resolution: {integrity: sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==} + '@codspeed/core@5.7.0': + resolution: {integrity: sha512-wj2nusv9ZRxn7otwSNU3O7NxJldWtUm5UQuX0/cts2xTIQgypwevSzdyeRES4rQzlSKCGs5DKBLkXoMkAIAPFw==} + + '@codspeed/vitest-plugin@5.7.0': + resolution: {integrity: sha512-W30T/i0Md9c0Vfle4xcgxAkmy5Rc5CFQG+GfXHqeHMCVimrRwIleQ67yTd8tvWZ9ep3KFdtgHWh8A8KwwfemxQ==} + peerDependencies: + tinybench: '>=2.9.0' + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + vitest: ^3.2 || ^4 + '@csstools/color-helpers@6.0.2': resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} engines: {node: '>=20.19.0'} @@ -1814,6 +1827,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -1898,6 +1915,9 @@ packages: axios@0.32.0: resolution: {integrity: sha512-sGQArzERW2SI8IRkjuJ5y91Sm9QjiRq4Ay4kOLqpbBt5CeKDNq4g6nirJdyD+palK3yEDXnJiVXsesX66AjwyA==} + axios@1.18.1: + resolution: {integrity: sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -2525,6 +2545,10 @@ packages: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} + find-up@6.3.0: + resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + fix-dts-default-cjs-exports@1.0.1: resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} @@ -2693,6 +2717,10 @@ packages: resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} engines: {node: '>=10.19.0'} + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} @@ -3041,6 +3069,10 @@ packages: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} @@ -3268,6 +3300,10 @@ packages: encoding: optional: true + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + node-gyp@12.4.0: resolution: {integrity: sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==} engines: {node: ^20.17.0 || >=22.9.0} @@ -3357,6 +3393,14 @@ packages: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} + p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + parse5@8.0.0: resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} @@ -3364,6 +3408,10 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -3495,6 +3543,10 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + psl@1.15.0: resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} @@ -3819,6 +3871,10 @@ packages: sprintf-js@1.1.3: resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + stack-trace@1.0.0-pre2: + resolution: {integrity: sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==} + engines: {node: '>=16'} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -4438,6 +4494,10 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yocto-queue@1.2.2: + resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} + engines: {node: '>=12.20'} + zod-to-json-schema@3.25.1: resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} peerDependencies: @@ -4682,6 +4742,27 @@ snapshots: '@bufbuild/protobuf@2.11.0': {} + '@codspeed/core@5.7.0': + dependencies: + axios: 1.18.1 + find-up: 6.3.0 + form-data: 4.0.5 + node-gyp-build: 4.8.4 + stack-trace: 1.0.0-pre2 + transitivePeerDependencies: + - debug + - supports-color + + '@codspeed/vitest-plugin@5.7.0(tinybench@2.9.0)(vite@7.3.2(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0))(vitest@4.1.5)': + dependencies: + '@codspeed/core': 5.7.0 + tinybench: 2.9.0 + vite: 7.3.2(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0) + vitest: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@25.2.3)(@vitest/coverage-v8@4.1.5)(jsdom@29.0.0(@noble/hashes@2.2.0))(msw@2.14.6(@types/node@25.2.3)(typescript@5.9.3))(vite@7.3.2(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)) + transitivePeerDependencies: + - debug + - supports-color + '@csstools/color-helpers@6.0.2': {} '@csstools/css-calc@3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': @@ -5773,6 +5854,12 @@ snapshots: acorn@8.15.0: {} + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + agent-base@7.1.4: {} ai@6.0.86(zod@4.3.6): @@ -5892,6 +5979,16 @@ snapshots: transitivePeerDependencies: - debug + axios@1.18.1: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.5 + https-proxy-agent: 5.0.1 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + - supports-color + balanced-match@1.0.2: {} balanced-match@4.0.4: {} @@ -6608,6 +6705,11 @@ snapshots: transitivePeerDependencies: - supports-color + find-up@6.3.0: + dependencies: + locate-path: 7.2.0 + path-exists: 5.0.0 + fix-dts-default-cjs-exports@1.0.1: dependencies: magic-string: 0.30.21 @@ -6815,6 +6917,13 @@ snapshots: quick-lru: 5.1.1 resolve-alpn: 1.2.1 + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 @@ -7090,6 +7199,10 @@ snapshots: load-tsconfig@0.2.5: {} + locate-path@7.2.0: + dependencies: + p-locate: 6.0.0 + lodash@4.18.1: {} long@5.3.2: {} @@ -7316,6 +7429,8 @@ snapshots: optionalDependencies: encoding: 0.1.13 + node-gyp-build@4.8.4: {} + node-gyp@12.4.0: dependencies: env-paths: 2.2.1 @@ -7394,12 +7509,22 @@ snapshots: dependencies: yocto-queue: 0.1.0 + p-limit@4.0.0: + dependencies: + yocto-queue: 1.2.2 + + p-locate@6.0.0: + dependencies: + p-limit: 4.0.0 + parse5@8.0.0: dependencies: entities: 6.0.1 parseurl@1.3.3: {} + path-exists@5.0.0: {} + path-is-absolute@1.0.1: {} path-key@3.1.1: {} @@ -7537,6 +7662,8 @@ snapshots: proxy-from-env@1.1.0: {} + proxy-from-env@2.1.0: {} + psl@1.15.0: dependencies: punycode: 2.3.1 @@ -7885,6 +8012,8 @@ snapshots: sprintf-js@1.1.3: optional: true + stack-trace@1.0.0-pre2: {} + stackback@0.0.2: {} standardwebhooks@1.0.0: @@ -8502,6 +8631,8 @@ snapshots: yocto-queue@0.1.0: {} + yocto-queue@1.2.2: {} + zod-to-json-schema@3.25.1(zod@4.3.6): dependencies: zod: 4.3.6 diff --git a/vitest.bench.config.ts b/vitest.bench.config.ts new file mode 100644 index 0000000000..ea51281046 --- /dev/null +++ b/vitest.bench.config.ts @@ -0,0 +1,28 @@ +import { defineConfig } from 'vitest/config' +import { fileURLToPath } from 'node:url' +import { resolve, dirname } from 'node:path' +import codspeedPlugin from '@codspeed/vitest-plugin' + +const __dirname = dirname(fileURLToPath(import.meta.url)) + +// Mirror the workspace aliases from vitest.config.ts so benchmarks can import +// directly from `src/*` and the workspace packages' `src/*` entry points +// without a prior `dist/` build. +const workspaceAliases = { + '@': resolve(__dirname, './src'), + '@traderalice/ibkr': resolve(__dirname, './packages/ibkr/src/index.ts'), + '@traderalice/opentypebb/server': resolve(__dirname, './packages/opentypebb/src/server.ts'), + '@traderalice/opentypebb': resolve(__dirname, './packages/opentypebb/src/index.ts'), + '@traderalice/uta-protocol': resolve(__dirname, './packages/uta-protocol/src/index.ts'), +} + +export default defineConfig({ + plugins: [codspeedPlugin()], + resolve: { + alias: workspaceAliases, + }, + test: { + environment: 'node', + include: ['benchmarks/**/*.bench.ts'], + }, +})