From be4fca67e4b2d4339d74b99211c91e32b66e02b7 Mon Sep 17 00:00:00 2001 From: CodSpeed Bot Date: Tue, 14 Jul 2026 13:55:01 +0000 Subject: [PATCH 01/10] ci: add CodSpeed performance benchmarks --- .github/workflows/codspeed.yaml | 31 +++++++ README.md | 3 + bench/core.bench.ts | 83 +++++++++++++++++++ bench/fetch.bench.ts | 38 +++++++++ bench/peer.bench.ts | 64 ++++++++++++++ package.json | 1 + pnpm-lock.yaml | 142 ++++++++++++++++++++++++++++++++ vitest.config.ts | 6 ++ 8 files changed, 368 insertions(+) create mode 100644 .github/workflows/codspeed.yaml create mode 100644 bench/core.bench.ts create mode 100644 bench/fetch.bench.ts create mode 100644 bench/peer.bench.ts diff --git a/.github/workflows/codspeed.yaml b/.github/workflows/codspeed.yaml new file mode 100644 index 0000000..ca94ba8 --- /dev/null +++ b/.github/workflows/codspeed.yaml @@ -0,0 +1,31 @@ +name: CodSpeed Benchmarks + +on: + push: + branches: [main] + pull_request: + branches: [main] + # `workflow_dispatch` allows CodSpeed to trigger backtest + # performance analysis in order to generate initial data. + workflow_dispatch: + +permissions: + contents: read + id-token: write + +jobs: + benchmarks: + name: Run benchmarks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - uses: pnpm/action-setup@v6 + + - run: pnpm i + + - name: Run the benchmarks + uses: CodSpeedHQ/action@v4 + with: + mode: simulation + run: pnpm exec vitest bench --run diff --git a/README.md b/README.md index 534c7c1..2452737 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,9 @@ weekly downloads + + CodSpeed + MIT License diff --git a/bench/core.bench.ts b/bench/core.bench.ts new file mode 100644 index 0000000..e6f4015 --- /dev/null +++ b/bench/core.bench.ts @@ -0,0 +1,83 @@ +import type { StandardHeaders } from '@standardserver/core' +import { + flattenStandardHeader, + generateContentDisposition, + getFilenameFromContentDisposition, + isStandardHeaders, + isStandardRequest, + isStandardResponse, + mergeStandardHeaders, + parseStandardUrl, +} from '@standardserver/core' +import { bench, describe } from 'vitest' + +const sampleHeaders: StandardHeaders = { + 'content-type': 'application/json', + 'accept': ['application/json', 'text/plain'], + 'user-agent': 'standard-server-bench', + 'x-request-id': 'abc-123-def-456', + 'set-cookie': ['a=1; HttpOnly', 'b=2; Secure'], +} + +const otherHeaders: StandardHeaders = { + 'content-type': 'text/html', + 'x-custom': 'value', + 'accept': 'application/xml', +} + +const sampleRequest = { + method: 'POST', + url: '/api/v1/resource?page=1&limit=20#section', + headers: sampleHeaders, +} + +const sampleResponse = { + status: 200, + headers: sampleHeaders, +} + +const contentDisposition = generateContentDisposition('rapport final (été 2024).pdf') + +describe('core / url parsing', () => { + bench('parseStandardUrl - full url', () => { + parseStandardUrl('/api/v1/resource?page=1&limit=20#section') + }) + + bench('parseStandardUrl - pathname only', () => { + parseStandardUrl('/api/v1/resource') + }) +}) + +describe('core / headers', () => { + bench('flattenStandardHeader - array', () => { + flattenStandardHeader(['application/json', 'text/plain', 'text/html']) + }) + + bench('mergeStandardHeaders', () => { + mergeStandardHeaders(sampleHeaders, otherHeaders) + }) + + bench('isStandardHeaders', () => { + isStandardHeaders(sampleHeaders) + }) +}) + +describe('core / content-disposition', () => { + bench('generateContentDisposition', () => { + generateContentDisposition('rapport final (été 2024).pdf') + }) + + bench('getFilenameFromContentDisposition', () => { + getFilenameFromContentDisposition(contentDisposition) + }) +}) + +describe('core / validators', () => { + bench('isStandardRequest', () => { + isStandardRequest(sampleRequest) + }) + + bench('isStandardResponse', () => { + isStandardResponse(sampleResponse) + }) +}) diff --git a/bench/fetch.bench.ts b/bench/fetch.bench.ts new file mode 100644 index 0000000..dbe3c82 --- /dev/null +++ b/bench/fetch.bench.ts @@ -0,0 +1,38 @@ +import type { StandardHeaders } from '@standardserver/core' +import { toFetchHeaders, toStandardHeaders, toStandardUrl } from '@standardserver/fetch' +import { bench, describe } from 'vitest' + +const fetchHeaders = new Headers() +fetchHeaders.append('content-type', 'application/json') +fetchHeaders.append('accept', 'application/json') +fetchHeaders.append('accept', 'text/plain') +fetchHeaders.append('set-cookie', 'a=1; HttpOnly') +fetchHeaders.append('set-cookie', 'b=2; Secure') +fetchHeaders.append('user-agent', 'standard-server-bench') +fetchHeaders.append('x-request-id', 'abc-123-def-456') + +const standardHeaders: StandardHeaders = { + 'content-type': 'application/json', + 'accept': ['application/json', 'text/plain'], + 'set-cookie': ['a=1; HttpOnly', 'b=2; Secure'], + 'user-agent': 'standard-server-bench', + 'x-request-id': 'abc-123-def-456', +} + +const url = new URL('https://example.com/api/v1/resource?page=1&limit=20#section') + +describe('fetch / headers', () => { + bench('toStandardHeaders', () => { + toStandardHeaders(fetchHeaders) + }) + + bench('toFetchHeaders', () => { + toFetchHeaders(standardHeaders) + }) +}) + +describe('fetch / url', () => { + bench('toStandardUrl', () => { + toStandardUrl(url) + }) +}) diff --git a/bench/peer.bench.ts b/bench/peer.bench.ts new file mode 100644 index 0000000..b7138ad --- /dev/null +++ b/bench/peer.bench.ts @@ -0,0 +1,64 @@ +import type { PeerMessage } from '@standardserver/peer' +import { decodePeerMessage, encodePeerMessage } from '@standardserver/peer' +import { bench, describe } from 'vitest' + +const jsonMessage: PeerMessage = { + id: 'req-0001', + kind: 'request', + json: { + method: 'POST', + url: '/api/v1/resource?page=1&limit=20', + headers: { + 'content-type': 'application/json', + 'accept': ['application/json', 'text/plain'], + }, + body: { + name: 'standard-server', + values: Array.from({ length: 32 }, (_, i) => ({ index: i, label: `item-${i}` })), + }, + }, +} + +const binaryPayload = new Uint8Array(4096) +for (let i = 0; i < binaryPayload.length; i++) { + binaryPayload[i] = i % 251 +} + +const binaryMessage: PeerMessage = { + id: 'req-0002', + kind: 'octet-stream', + json: { url: '/upload' }, + binary: binaryPayload, +} + +const encodedJson = await encodePeerMessage(jsonMessage) as string +const encodedBinary = await encodePeerMessage(binaryMessage) as Uint8Array +const encodedPrefixed = await encodePeerMessage(binaryMessage, { prefix: 'ss:' }) as Uint8Array + +describe('peer / encode', () => { + bench('encodePeerMessage - json only', async () => { + await encodePeerMessage(jsonMessage) + }) + + bench('encodePeerMessage - json + binary', async () => { + await encodePeerMessage(binaryMessage) + }) + + bench('encodePeerMessage - with prefix', async () => { + await encodePeerMessage(binaryMessage, { prefix: 'ss:' }) + }) +}) + +describe('peer / decode', () => { + bench('decodePeerMessage - json only', () => { + decodePeerMessage(encodedJson) + }) + + bench('decodePeerMessage - json + binary', () => { + decodePeerMessage(encodedBinary) + }) + + bench('decodePeerMessage - with prefix', () => { + decodePeerMessage(encodedPrefixed, { prefix: 'ss:' }) + }) +}) diff --git a/package.json b/package.json index c973210..70d2db4 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ }, "devDependencies": { "@antfu/eslint-config": "^6.7.3", + "@codspeed/vitest-plugin": "^5.7.1", "@hono/node-server": "^2.0.1", "@remix-run/node-fetch-server": "^0.13.0", "@standardserver/core": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2dec20a..dff06c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@antfu/eslint-config': specifier: ^6.7.3 version: 6.7.3(@vue/compiler-sfc@3.5.25)(eslint-plugin-format@1.1.0(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(typescript@6.0.3)(vitest@4.1.5) + '@codspeed/vitest-plugin': + specifier: ^5.7.1 + version: 5.7.1(tinybench@2.9.0)(vite@7.3.0(@types/node@26.0.0)(jiti@2.6.1)(tsx@4.22.4)(yaml@2.9.0))(vitest@4.1.5) '@hono/node-server': specifier: ^2.0.1 version: 2.0.1(hono@4.11.1) @@ -269,6 +272,16 @@ packages: '@clack/prompts@0.11.0': resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==} + '@codspeed/core@5.7.1': + resolution: {integrity: sha512-7mrFkqaY14rXu3XHv3o7bK3btLK0LFRxfeK3bQC8dLduI/Ty9eq8Hv8rx04FAY/onQbRTx3rUUQg5xprYHpATQ==} + + '@codspeed/vitest-plugin@5.7.1': + resolution: {integrity: sha512-yKNqaYVxZZPJYkhVHpz30Oem4TUgcI+/IRDD9YHvcAgi0P42CL6TB/+qYjuFOWghFzTPrm3TUbNFoBsevSTbUA==} + peerDependencies: + tinybench: '>=2.9.0' + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + vitest: ^3.2 || ^4 + '@dprint/formatter@0.3.0': resolution: {integrity: sha512-N9fxCxbaBOrDkteSOzaCqwWjso5iAe+WJPsHC021JfHNj2ThInPNEF13ORDKta3llq5D1TlclODCvOvipH7bWQ==} @@ -1360,6 +1373,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'} + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -1417,6 +1434,9 @@ packages: peerDependencies: postcss: ^8.1.0 + axios@1.18.1: + resolution: {integrity: sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -2109,6 +2129,10 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} + 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==} @@ -2119,6 +2143,15 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + form-data@4.0.5: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} @@ -2247,6 +2280,10 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + human-signals@8.0.1: resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} engines: {node: '>=18.18.0'} @@ -2428,6 +2465,10 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + 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.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} @@ -2692,6 +2733,10 @@ packages: node-fetch-native@1.6.7: resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} @@ -2753,10 +2798,18 @@ 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@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + package-manager-detector@1.6.0: resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} @@ -2782,6 +2835,10 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=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-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -3042,6 +3099,10 @@ packages: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -3261,6 +3322,10 @@ packages: engines: {node: '>=20.16.0'} hasBin: true + 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==} @@ -3603,6 +3668,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'} + yoctocolors@2.1.2: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} @@ -3698,6 +3767,27 @@ snapshots: picocolors: 1.1.1 sisteransi: 1.0.5 + '@codspeed/core@5.7.1': + 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.1(tinybench@2.9.0)(vite@7.3.0(@types/node@26.0.0)(jiti@2.6.1)(tsx@4.22.4)(yaml@2.9.0))(vitest@4.1.5)': + dependencies: + '@codspeed/core': 5.7.1 + tinybench: 2.9.0 + vite: 7.3.0(@types/node@26.0.0)(jiti@2.6.1)(tsx@4.22.4)(yaml@2.9.0) + vitest: 4.1.5(@types/node@26.0.0)(@vitest/coverage-v8@4.1.5)(vite@7.3.0(@types/node@26.0.0)(jiti@2.6.1)(tsx@4.22.4)(yaml@2.9.0)) + transitivePeerDependencies: + - debug + - supports-color + '@dprint/formatter@0.3.0': {} '@dprint/markdown@0.17.8': {} @@ -4507,6 +4597,12 @@ snapshots: acorn@8.15.0: {} + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -4561,6 +4657,16 @@ snapshots: postcss: 8.5.6 postcss-value-parser: 4.2.0 + 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: {} baseline-browser-mapping@2.9.11: {} @@ -5437,6 +5543,11 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 + 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 @@ -5450,6 +5561,8 @@ snapshots: flatted@3.3.3: {} + follow-redirects@1.16.0: {} + form-data@4.0.5: dependencies: asynckit: 0.4.0 @@ -5567,6 +5680,13 @@ snapshots: html-escaper@2.0.2: {} + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + human-signals@8.0.1: {} ignore@5.3.2: {} @@ -5721,6 +5841,10 @@ snapshots: dependencies: p-locate: 5.0.0 + locate-path@7.2.0: + dependencies: + p-locate: 6.0.0 + lodash.memoize@4.1.2: {} lodash.merge@4.6.2: {} @@ -6149,6 +6273,8 @@ snapshots: node-fetch-native@1.6.7: {} + node-gyp-build@4.8.4: {} + node-releases@2.0.27: {} normalize-path@3.0.0: {} @@ -6217,10 +6343,18 @@ snapshots: dependencies: yocto-queue: 0.1.0 + p-limit@4.0.0: + dependencies: + yocto-queue: 1.2.2 + p-locate@5.0.0: dependencies: p-limit: 3.1.0 + p-locate@6.0.0: + dependencies: + p-limit: 4.0.0 + package-manager-detector@1.6.0: {} parent-module@1.0.1: @@ -6239,6 +6373,8 @@ snapshots: path-exists@4.0.0: {} + path-exists@5.0.0: {} + path-key@3.1.1: {} path-key@4.0.0: {} @@ -6464,6 +6600,8 @@ snapshots: dependencies: parse-ms: 4.0.0 + proxy-from-env@2.1.0: {} + punycode@2.3.1: {} qs@6.14.0: @@ -6703,6 +6841,8 @@ snapshots: srvx@0.10.0: {} + stack-trace@1.0.0-pre2: {} + stackback@0.0.2: {} std-env@3.10.0: {} @@ -7031,6 +7171,8 @@ snapshots: yocto-queue@0.1.0: {} + yocto-queue@1.2.2: {} + yoctocolors@2.1.2: {} zwitch@2.0.4: {} diff --git a/vitest.config.ts b/vitest.config.ts index 0c0db1a..76a8f81 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,6 +1,8 @@ +import codspeedPlugin from '@codspeed/vitest-plugin' import { defineConfig } from 'vitest/config' export default defineConfig(() => ({ + plugins: [codspeedPlugin()], test: { coverage: { include: ['packages/*/src/**'], @@ -8,9 +10,13 @@ export default defineConfig(() => ({ }, projects: [ { + plugins: [codspeedPlugin()], test: { globals: true, include: ['**/*.test.ts'], + benchmark: { + include: ['**/*.bench.ts'], + }, }, }, ], From 91caa3e7a0cf1e073b9859135d77013b094e3ff9 Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Wed, 15 Jul 2026 09:50:38 +0700 Subject: [PATCH 02/10] improve configs --- .github/workflows/codspeed.yaml | 6 ++---- package.json | 1 + tsconfig.json | 2 ++ vitest.config.ts | 4 ---- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/codspeed.yaml b/.github/workflows/codspeed.yaml index ca94ba8..8610830 100644 --- a/.github/workflows/codspeed.yaml +++ b/.github/workflows/codspeed.yaml @@ -15,7 +15,6 @@ permissions: jobs: benchmarks: - name: Run benchmarks runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 @@ -24,8 +23,7 @@ jobs: - run: pnpm i - - name: Run the benchmarks - uses: CodSpeedHQ/action@v4 + - uses: CodSpeedHQ/action@v4 with: mode: simulation - run: pnpm exec vitest bench --run + run: pnpm run bench diff --git a/package.json b/package.json index 70d2db4..0ec9780 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "type:check": "pnpm -r run type:check && tsc", "test": "vitest run", "test:coverage": "vitest run --coverage", + "bench": "vitest bench --run", "lint": "eslint --max-warnings=0 .", "lint:fix": "eslint --max-warnings=0 --fix .", "repo:fix": "sherif --fix", diff --git a/tsconfig.json b/tsconfig.json index 5de5884..e7e7b6d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "playground", "scripts", "tests", + "bench", "*", "*/*/src/**/*.bench.*", @@ -19,6 +20,7 @@ "*/*/playground", "*/*/scripts", "*/*/tests", + "*/*/bench", "*/*/*" ], "exclude": [ diff --git a/vitest.config.ts b/vitest.config.ts index 76a8f81..9bfc75e 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -10,13 +10,9 @@ export default defineConfig(() => ({ }, projects: [ { - plugins: [codspeedPlugin()], test: { globals: true, include: ['**/*.test.ts'], - benchmark: { - include: ['**/*.bench.ts'], - }, }, }, ], From 5ce89666cbb0d0705d0fcde1f0ae3557b4b4352c Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Wed, 15 Jul 2026 09:56:55 +0700 Subject: [PATCH 03/10] fix --- .github/workflows/codspeed.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codspeed.yaml b/.github/workflows/codspeed.yaml index 8610830..0e28af4 100644 --- a/.github/workflows/codspeed.yaml +++ b/.github/workflows/codspeed.yaml @@ -26,4 +26,4 @@ jobs: - uses: CodSpeedHQ/action@v4 with: mode: simulation - run: pnpm run bench + run: pnpm exec vitest bench --run From f6a11655e8484e0b67e00574dc495877f6b42d87 Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Wed, 15 Jul 2026 10:02:04 +0700 Subject: [PATCH 04/10] fix --- .github/workflows/codspeed.yaml | 2 +- vitest.config.ts | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codspeed.yaml b/.github/workflows/codspeed.yaml index 0e28af4..8610830 100644 --- a/.github/workflows/codspeed.yaml +++ b/.github/workflows/codspeed.yaml @@ -26,4 +26,4 @@ jobs: - uses: CodSpeedHQ/action@v4 with: mode: simulation - run: pnpm exec vitest bench --run + run: pnpm run bench diff --git a/vitest.config.ts b/vitest.config.ts index 9bfc75e..495c301 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,17 +2,20 @@ import codspeedPlugin from '@codspeed/vitest-plugin' import { defineConfig } from 'vitest/config' export default defineConfig(() => ({ - plugins: [codspeedPlugin()], test: { coverage: { include: ['packages/*/src/**'], - exclude: ['**.test-d.*', '**.test.*'], + exclude: ['**.test-d.*', '**.test.*', '**/*.bench.ts'], }, projects: [ { + plugins: [codspeedPlugin()], test: { globals: true, include: ['**/*.test.ts'], + benchmark: { + include: ['**/*.bench.ts'], + }, }, }, ], From 380c78963dddfcac70048a155c6ea4c0b70cbf2b Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Wed, 15 Jul 2026 18:12:25 +0700 Subject: [PATCH 05/10] e2e bench --- bench/__shared__/client-server.hono-fetch.ts | 52 ++++ .../client-server.inprogress-fetch.ts | 32 ++ .../__shared__/client-server.message-port.ts | 58 ++++ bench/__shared__/client-server.node-http.ts | 52 ++++ bench/__shared__/client-server.node-ws.ts | 110 +++++++ bench/__shared__/client-server.ts | 81 +++++ bench/__shared__/payloads.ts | 286 ++++++++++++++++++ bench/core.bench.ts | 83 ----- bench/e2e.bench.ts | 80 +++++ bench/fetch.bench.ts | 38 --- bench/peer.bench.ts | 64 ---- 11 files changed, 751 insertions(+), 185 deletions(-) create mode 100644 bench/__shared__/client-server.hono-fetch.ts create mode 100644 bench/__shared__/client-server.inprogress-fetch.ts create mode 100644 bench/__shared__/client-server.message-port.ts create mode 100644 bench/__shared__/client-server.node-http.ts create mode 100644 bench/__shared__/client-server.node-ws.ts create mode 100644 bench/__shared__/client-server.ts create mode 100644 bench/__shared__/payloads.ts delete mode 100644 bench/core.bench.ts create mode 100644 bench/e2e.bench.ts delete mode 100644 bench/fetch.bench.ts delete mode 100644 bench/peer.bench.ts diff --git a/bench/__shared__/client-server.hono-fetch.ts b/bench/__shared__/client-server.hono-fetch.ts new file mode 100644 index 0000000..7dc5718 --- /dev/null +++ b/bench/__shared__/client-server.hono-fetch.ts @@ -0,0 +1,52 @@ +import type { AddressInfo } from 'node:net' +import type { ClientServer } from './client-server' +import { serve } from '@hono/node-server' +import { toFetchBody, toFetchHeaders, toFetchResponse, toStandardLazyRequest, toStandardLazyResponse } from '@standardserver/fetch' + +/** + * Real Hono/node-server + fetch adapter round-trip. + */ +export function createHonoFetchClientServer(): ClientServer { + const clientServer: ClientServer = { + handler: async () => ({ status: 404, body: 'Not Found', headers: {} }), + request: async () => { + throw new Error('client-server not ready') + }, + } + + const server = serve({ + fetch: async (request: Request) => { + const standardRequest = toStandardLazyRequest(request) + const standardResponse = await clientServer.handler(standardRequest) + return toFetchResponse(standardResponse) + }, + port: 0, + }) + + afterAll(() => { + server.close() + }) + + const addressInfo = server.address() as AddressInfo + + clientServer.request = async (standardRequest) => { + const [body, standardHeaders] = toFetchBody(standardRequest.body, standardRequest.headers) + + const init: RequestInit = { + method: standardRequest.method, + signal: standardRequest.signal ?? null, + headers: toFetchHeaders(standardHeaders), + body: body ?? null, + } + + if (body instanceof ReadableStream) { + init.duplex = 'half' + } + + const response = await fetch(`http://localhost:${addressInfo.port}${standardRequest.url}`, init) + + return toStandardLazyResponse(response) + } + + return clientServer +} diff --git a/bench/__shared__/client-server.inprogress-fetch.ts b/bench/__shared__/client-server.inprogress-fetch.ts new file mode 100644 index 0000000..505eb36 --- /dev/null +++ b/bench/__shared__/client-server.inprogress-fetch.ts @@ -0,0 +1,32 @@ +import type { ClientServer } from './client-server' +import { toFetchBody, toFetchHeaders, toFetchResponse, toStandardLazyRequest, toStandardLazyResponse } from '@standardserver/fetch' + +/** + * In-process fetch adapter path (Request/Response conversion) without a real network hop. + */ +export function createInprogressFetchClientServer(): ClientServer { + const clientServer: ClientServer = { + handler: async () => ({ status: 404, body: 'Not Found', headers: {} }), + request: async (standardRequest) => { + const [body, standardHeaders] = toFetchBody(standardRequest.body, standardRequest.headers) + + return toStandardLazyResponse( + toFetchResponse( + await clientServer.handler( + toStandardLazyRequest( + new Request(`http://localhost${standardRequest.url}`, { + method: standardRequest.method, + signal: standardRequest.signal ?? null, + headers: toFetchHeaders(standardHeaders), + body: body ?? null, + duplex: 'half', + }), + ), + ), + ), + ) + }, + } + + return clientServer +} diff --git a/bench/__shared__/client-server.message-port.ts b/bench/__shared__/client-server.message-port.ts new file mode 100644 index 0000000..18dd0da --- /dev/null +++ b/bench/__shared__/client-server.message-port.ts @@ -0,0 +1,58 @@ +import type { PeerMessage } from '@standardserver/peer' +import type { ClientServer } from './client-server' +import { ClientPeer, decodePeerMessage, encodePeerMessage, isClientPeerSendMessage, isServerPeerSendMessage, ServerPeer } from '@standardserver/peer' + +const prefix = 'bench:' + +async function encode(message: PeerMessage) { + return encodePeerMessage(message, { prefix }) +} + +/** + * Peer adapter over `MessageChannel` (in-process, no network). + */ +export function createMessagePortClientServer(): ClientServer { + const { port1, port2 } = new MessageChannel() + + const clientPeer = new ClientPeer(async (message) => { + port1.postMessage(await encode(message)) + }) + + port1.addEventListener('message', async (event) => { + const { matched, message } = decodePeerMessage(event.data, { prefix }) + + if (!matched || !isServerPeerSendMessage(message)) { + return + } + + await clientPeer.message(message) + }) + port1.start() + + const clientServer: ClientServer = { + handler: async () => ({ status: 404, body: 'Not Found', headers: {} }), + request: async standardRequest => clientPeer.request(standardRequest), + } + + const serverPeer = new ServerPeer(async (message) => { + port2.postMessage(await encode(message)) + }) + + port2.addEventListener('message', async (event) => { + const { matched, message } = decodePeerMessage(event.data, { prefix }) + + if (!matched || !isClientPeerSendMessage(message)) { + return + } + + await serverPeer.message(message, async request => clientServer.handler(request)) + }) + port2.start() + + afterAll(() => { + port1.close() + port2.close() + }) + + return clientServer +} diff --git a/bench/__shared__/client-server.node-http.ts b/bench/__shared__/client-server.node-http.ts new file mode 100644 index 0000000..8bd9760 --- /dev/null +++ b/bench/__shared__/client-server.node-http.ts @@ -0,0 +1,52 @@ +import type { AddressInfo } from 'node:net' +import type { ClientServer } from './client-server' +import * as http from 'node:http' +import { toFetchBody, toFetchHeaders, toStandardLazyResponse } from '@standardserver/fetch' +import { sendStandardResponse, toStandardLazyRequest } from '@standardserver/node' + +/** + * Real Node.js `http` server + `fetch` client (node adapter on the server side). + */ +export function createNodeHttpClientServer(): ClientServer { + const clientServer: ClientServer = { + handler: async () => ({ status: 404, body: 'Not Found', headers: {} }), + request: async () => { + throw new Error('client-server not ready') + }, + } + + const server = http.createServer(async (req, res) => { + const standardRequest = toStandardLazyRequest(req, res) + const standardResponse = await clientServer.handler(standardRequest) + await sendStandardResponse(res, standardResponse) + }) + + server.listen(0) + + afterAll(() => { + server.close() + }) + + const addressInfo = server.address() as AddressInfo + + clientServer.request = async (standardRequest) => { + const [body, standardHeaders] = toFetchBody(standardRequest.body, standardRequest.headers) + + const init: RequestInit = { + method: standardRequest.method, + headers: toFetchHeaders(standardHeaders), + body: body ?? null, + signal: standardRequest.signal ?? null, + } + + if (body instanceof ReadableStream) { + init.duplex = 'half' + } + + const response = await fetch(`http://localhost:${addressInfo.port}${standardRequest.url}`, init) + + return toStandardLazyResponse(response) + } + + return clientServer +} diff --git a/bench/__shared__/client-server.node-ws.ts b/bench/__shared__/client-server.node-ws.ts new file mode 100644 index 0000000..56c6603 --- /dev/null +++ b/bench/__shared__/client-server.node-ws.ts @@ -0,0 +1,110 @@ +/* eslint-disable ban/ban */ +import type { PeerMessage } from '@standardserver/peer' +import type { BlobPart } from 'node:buffer' +import type { ClientServer } from './client-server' +import { ClientPeer, decodePeerMessage, encodePeerMessage, isClientPeerSendMessage, isServerPeerSendMessage, ServerPeer } from '@standardserver/peer' +import { WebSocket, WebSocketServer } from 'ws' + +const prefix = 'bench:' + +async function encode(message: PeerMessage) { + return encodePeerMessage(message, { prefix }) +} + +async function normalizeWsData(data: unknown): Promise> { + if (typeof data === 'string') { + return data + } + + if (data instanceof ArrayBuffer) { + return new Uint8Array(data) + } + + if (Array.isArray(data)) { + return await new Blob(data as BlobPart[]).bytes() as Uint8Array + } + + if (data instanceof Uint8Array) { + return new Uint8Array(data.buffer as ArrayBuffer, data.byteOffset, data.byteLength) + } + + // Node.js Buffer + const buffer = data as { buffer: ArrayBuffer, byteOffset: number, byteLength: number } + return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength) +} + +/** + * Peer adapter over a real WebSocket server/client (`ws`). + */ +export function createNodeWsClientServer(): ClientServer { + const wss = new WebSocketServer({ port: 0 }) + const address = wss.address() as WebSocket.AddressInfo + const wsc = new WebSocket(`ws://localhost:${address.port}`) + + afterAll(() => { + wss.close() + wsc.close() + }) + + const untilReady = new Promise((resolve) => { + if (wsc.readyState === WebSocket.OPEN) { + resolve() + return + } + + wsc.addEventListener('open', () => { + resolve() + }) + }) + + const clientPeer = new ClientPeer(async (message) => { + await untilReady + wsc.send(await encode(message)) + }) + + wsc.addEventListener('message', async (event) => { + const encoded = await normalizeWsData(event.data) + const { matched, message } = decodePeerMessage(encoded, { prefix }) + + if (!matched || !isServerPeerSendMessage(message)) { + return + } + + await clientPeer.message(message) + }) + + const clientServer: ClientServer = { + handler: async () => ({ status: 404, body: 'Not Found', headers: {} }), + request: async (standardRequest) => { + await untilReady + return clientPeer.request(standardRequest) + }, + } + + let sendServer: (data: string | Uint8Array) => void = () => { + throw new Error('websocket server not connected') + } + + const serverPeer = new ServerPeer(async (message) => { + sendServer(await encode(message) as string | Uint8Array) + }) + + wss.on('connection', (ws) => { + sendServer = (data) => { + ws.send(data) + } + + ws.addEventListener('message', async (event) => { + const encoded = await normalizeWsData(event.data) + const { matched, message } = decodePeerMessage(encoded, { prefix }) + + if (!matched || !isClientPeerSendMessage(message)) { + return + } + + await serverPeer.message(message, async request => clientServer.handler(request)) + }) + }) + + return clientServer +} diff --git a/bench/__shared__/client-server.ts b/bench/__shared__/client-server.ts new file mode 100644 index 0000000..8716013 --- /dev/null +++ b/bench/__shared__/client-server.ts @@ -0,0 +1,81 @@ +import type { StandardLazyRequest, StandardLazyResponse, StandardRequest, StandardResponse } from '@standardserver/core' +import { isAsyncIteratorObject } from '@standardserver/shared' + +export interface ClientServer { + handler: (request: StandardLazyRequest) => Promise + request: (request: StandardRequest) => Promise +} + +export const echoHandler: ClientServer['handler'] = async (request) => { + const body = await request.resolveBody() + + return { + status: 200, + headers: { + 'x-from': 'server', + }, + body, + } +} + +/** + * Fully consume a response body so streaming / lazy adapters are measured end-to-end. + */ +async function drainBody(body: unknown): Promise { + if (body === undefined || body === null) { + return + } + + if (isAsyncIteratorObject(body)) { + const iterator = body as AsyncGenerator + while (true) { + const result = await iterator.next() + if (result.done) { + break + } + } + return + } + + if (body instanceof ReadableStream) { + const reader = body.getReader() + try { + while (true) { + const { done } = await reader.read() + if (done) { + break + } + } + } + finally { + reader.releaseLock() + } + return + } + + if (body instanceof Blob) { + await body.arrayBuffer() + return + } + + if (body instanceof FormData) { + for (const value of body.values()) { + if (value instanceof Blob) { + await value.arrayBuffer() + } + } + return + } + + if (body instanceof URLSearchParams) { + body.toString() + } +} + +export async function roundTrip( + clientServer: ClientServer, + request: StandardRequest, +): Promise { + const response = await clientServer.request(request) + await drainBody(await response.resolveBody()) +} diff --git a/bench/__shared__/payloads.ts b/bench/__shared__/payloads.ts new file mode 100644 index 0000000..0b6600e --- /dev/null +++ b/bench/__shared__/payloads.ts @@ -0,0 +1,286 @@ +import assert from 'node:assert/strict' +import { stringifyJSON } from '@standardserver/shared' + +const KB = 1024 +const SIZES = { '1KB': KB, '100KB': 100 * KB, '10MB': 100 * 100 * KB } as const +type SizeLabel = keyof typeof SIZES +export const BODY_SIZE_ENTRIES = Object.entries(SIZES) as [SizeLabel, number][] + +function rng(seed: number): () => number { + let s = seed >>> 0 + return () => { + s = (s + 0x6D2B79F5) | 0 + let t = Math.imul(s ^ (s >>> 15), 1 | s) + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t + return ((t ^ (t >>> 14)) >>> 0) / 4294967296 + } +} + +const ALNUM = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' +const TEXT = `${ALNUM} .,;:!?@#&()[]{}/+_-` +const UNI = ['café', '東京', '🚀', 'São Paulo', '日本語', 'привет', '€99'] as const + +function pick(r: () => number, xs: readonly T[]): T { + return xs[Math.floor(r() * xs.length)]! +} + +function alnum(r: () => number, n: number): string { + if (n <= 0) + return '' + if (n > KB) { + const u = alnum(r, KB) + return u.repeat(Math.floor(n / KB)) + u.slice(0, n % KB) + } + let s = '' + for (let i = 0; i < n; i++) s += ALNUM[Math.floor(r() * ALNUM.length)]! + return s +} + +function text(r: () => number, n: number): string { + if (n <= 0) + return '' + if (n > KB) { + const u = text(r, KB) + return u.repeat(Math.floor(n / KB)) + u.slice(0, n % KB) + } + let s = '' + while (s.length < n) s += r() < 0.1 ? pick(r, UNI) : TEXT[Math.floor(r() * TEXT.length)]! + return s.slice(0, n) +} + +function bytes(n: number, seed: number): Uint8Array { + const r = rng(seed) + const b = new Uint8Array(n) as Uint8Array + for (let i = 0; i < n; i++) { + b[i] = r() < 0.15 && i > 0 ? b[i - 1]! : r() < 0.3 ? 0x20 + Math.floor(r() * 95) : Math.floor(r() * 256) + } + return b +} + +function repeatBytes(unit: Uint8Array, times: number): Uint8Array { + const out = new Uint8Array(unit.byteLength * times) as Uint8Array + for (let i = 0; i < times; i++) out.set(unit, i * unit.byteLength) + return out +} + +function jlen(v: unknown): number { + return stringifyJSON(v)!.length +} + +function padJson(value: object, size: number, seed: number): object { + const r = rng(seed) + const base = jlen({ ...value, _pad: '' }) + assert.ok(base <= size, `json core ${base} > ${size}`) + return { ...value, _pad: alnum(r, size - base) } +} + +function record(seed: number) { + const r = rng(seed) + return { + id: `rec_${alnum(r, 10)}`, + type: pick(r, ['user', 'order', 'event'] as const), + active: r() > 0.4, + score: Math.fround(r() * 100), + tags: [pick(r, UNI), alnum(r, 6)], + meta: { locale: pick(r, ['en-US', 'fr-FR', 'ja-JP']), region: pick(r, ['us-east-1', 'eu-west-1']), v: 1 + Math.floor(r() * 5) }, + profile: { + name: text(r, 12), + email: `${alnum(r, 8)}@example.com`, + bio: text(r, 32), + }, + payload: { kind: pick(r, ['snapshot', 'delta']), values: [r(), r(), r()], note: text(r, 20) }, + } +} + +function split(buf: Uint8Array, parts: number): Uint8Array[] { + const base = Math.floor(buf.byteLength / parts) + let rem = buf.byteLength - base * parts + const out: Uint8Array[] = [] + let off = 0 + for (let i = 0; i < parts; i++) { + const n = base + (rem-- > 0 ? 1 : 0) + out.push(buf.subarray(off, off + n) as Uint8Array) + off += n + } + return out +} + +function formSize(form: FormData): number { + let n = 0 + for (const v of form.values()) n += typeof v === 'string' ? new TextEncoder().encode(v).byteLength : v.size + return n +} + +function makeForm(size: number, pool: Uint8Array, seed: number): FormData { + const r = rng(seed) + const form = new FormData() + form.append('userId', `user_${alnum(r, 8)}`) + form.append('action', pick(r, ['create', 'update', 'import'])) + form.append('meta', stringifyJSON({ tags: [pick(r, UNI), pick(r, UNI)], dryRun: r() > 0.5 })!) + form.append('note', text(r, 40)) + form.append('thumb', new File([bytes(32, seed ^ 1)], 't.png', { type: 'image/png' })) + const fileSize = size - formSize(form) + assert.ok(fileSize > 0 && fileSize <= pool.byteLength) + form.append('file', new File([pool.subarray(0, fileSize)], 'data.bin', { type: 'application/octet-stream' })) + return form +} + +function makeParams(size: number, seed: number): URLSearchParams { + const r = rng(seed) + const p = new URLSearchParams({ + 'page': String(1 + Math.floor(r() * 20)), + 'sort': pick(r, ['created_at', 'score']), + 'q': alnum(r, 12), + 'filter[status]': pick(r, ['active', 'pending']), + 'cursor': alnum(r, 16), + 'data': '', + }) + const base = p.toString().length + assert.ok(base <= size) + p.set('data', alnum(r, size - base)) + const d = size - p.toString().length + if (d) + p.set('data', (p.get('data') ?? '') + alnum(r, d)) + return p +} + +interface Event { type: string, [k: string]: unknown } + +function eventSize(parts: readonly Event[]): number { + return parts.reduce((n, p) => n + jlen(p), 0) +} + +function makeEvents(size: number, seed: number): Event[] { + const r = rng(seed) + const parts: Event[] = [ + { type: 'heartbeat', ts: 1_700_000_000_000 + Math.floor(r() * 1e6), seq: Math.floor(r() * 1e6) }, + { type: 'progress', jobId: alnum(r, 8), percent: Math.floor(r() * 100), stage: pick(r, ['running', 'done']) }, + { type: 'message', id: alnum(r, 8), text: text(r, 20), tags: [pick(r, UNI)] }, + { type: 'message', id: 'pad', channel: 'pad', text: '', tags: ['pad'] }, + ] + const base = eventSize(parts) + assert.ok(base <= size, `event core ${base} > ${size}`) + parts[parts.length - 1]!.text = alnum(r, size - base) + return parts +} + +function fitJson(unit: object, size: number, seed: number): object { + for (let n = 100; n > 0; n--) { + const core = { n, items: Array.from({ length: n }, (_, i) => ({ i, data: unit })) } + if (jlen({ ...core, _pad: '' }) <= size) + return padJson(core, size, seed) + } + throw new Error(`cannot fit json under ${size}`) +} + +function fitEvents(unit: object, size: number, seed: number): Event[] { + const r = rng(seed) + const parts: Event[] = [] + for (let i = 0; i < 100; i++) { + const next = [...parts, { type: 'batch', i, data: unit }, { type: 'message', id: 'pad', text: '', tags: ['pad'] }] + if (eventSize(next) > size) + break + parts.push({ type: 'batch', i, data: unit }) + } + assert.ok(parts.length > 0) + parts.push({ type: 'message', id: 'pad', text: '', tags: ['pad'] }) + parts[parts.length - 1]!.text = alnum(r, size - eventSize(parts)) + return parts +} + +const BYTES1KB = bytes(SIZES['1KB'], 0xB001) +assert.equal(BYTES1KB.byteLength, SIZES['1KB']) +const BYTES100KB = repeatBytes(BYTES1KB, 100) +assert.equal(BYTES100KB.byteLength, SIZES['100KB']) +const BYTES10MB = repeatBytes(BYTES100KB, 100) +assert.equal(BYTES10MB.byteLength, SIZES['10MB']) + +const JSON1KB = padJson(record(0xA001), SIZES['1KB'], 0xA101) +assert.equal(jlen(JSON1KB), SIZES['1KB']) +const JSON100KB = padJson( + { items: Array.from({ length: 100 }, (_, i) => ({ i, item: record(0xA200 + i) })) }, + SIZES['100KB'], + 0xA201, +) +assert.equal(jlen(JSON100KB), SIZES['100KB']) +const JSON10MB = fitJson(JSON100KB, SIZES['10MB'], 0xA301) +assert.equal(jlen(JSON10MB), SIZES['10MB']) + +const BLOB1KB = new Blob([BYTES1KB], { type: 'application/octet-stream' }) +assert.equal(BLOB1KB.size, SIZES['1KB']) +const BLOB100KB = new Blob([BYTES100KB], { type: 'application/octet-stream' }) +assert.equal(BLOB100KB.size, SIZES['100KB']) +const BLOB10MB = new Blob([BYTES10MB], { type: 'application/octet-stream' }) +assert.equal(BLOB10MB.size, SIZES['10MB']) + +const FORM1KB = makeForm(SIZES['1KB'], BYTES1KB, 0xF001) +assert.equal(formSize(FORM1KB), SIZES['1KB']) +const FORM100KB = makeForm(SIZES['100KB'], BYTES100KB, 0xF101) +assert.equal(formSize(FORM100KB), SIZES['100KB']) +const FORM10MB = makeForm(SIZES['10MB'], BYTES10MB, 0xF201) +assert.equal(formSize(FORM10MB), SIZES['10MB']) + +const USP1KB = makeParams(SIZES['1KB'], 0xC001) +assert.equal(USP1KB.toString().length, SIZES['1KB']) +const USP100KB = makeParams(SIZES['100KB'], 0xC101) +assert.equal(USP100KB.toString().length, SIZES['100KB']) +const USP10MB = makeParams(SIZES['10MB'], 0xC201) +assert.equal(USP10MB.toString().length, SIZES['10MB']) + +const EVENTS1KB = makeEvents(SIZES['1KB'], 0xE001) +assert.equal(eventSize(EVENTS1KB), SIZES['1KB']) +const EVENTS100KB = Array.from({ length: 100 }, (_, i) => i === 0 ? EVENTS1KB : makeEvents(SIZES['1KB'], 0xE100 + i)).flat() +assert.equal(eventSize(EVENTS100KB), SIZES['100KB']) +const EVENTS10MB = fitEvents(JSON100KB, SIZES['10MB'], 0xE201) +assert.equal(eventSize(EVENTS10MB), SIZES['10MB']) + +const OCTET1KB = split(BYTES1KB, 8) +assert.equal(OCTET1KB.reduce((n, c) => n + c.byteLength, 0), SIZES['1KB']) +const OCTET100KB = split(BYTES100KB, 32) +assert.equal(OCTET100KB.reduce((n, c) => n + c.byteLength, 0), SIZES['100KB']) +const OCTET10MB = split(BYTES10MB, 64) +assert.equal(OCTET10MB.reduce((n, c) => n + c.byteLength, 0), SIZES['10MB']) + +export function asEventStream(parts: readonly Event[]): AsyncGenerator { + return (async function* () { + for (const p of parts) yield p + }()) +} + +export function asOctetStream(parts: readonly Uint8Array[]): ReadableStream> { + let i = 0 + return new ReadableStream({ + pull(c) { + if (i >= parts.length) + c.close() + else c.enqueue(parts[i++]!) + }, + }) +} + +export const BODY_PAYLOADS = { + '1KB': { + json: JSON1KB, + blob: BLOB1KB, + formData: FORM1KB, + urlSearchParams: USP1KB, + eventParts: EVENTS1KB, + octetParts: OCTET1KB, + }, + '100KB': { + json: JSON100KB, + blob: BLOB100KB, + formData: FORM100KB, + urlSearchParams: USP100KB, + eventParts: EVENTS100KB, + octetParts: OCTET100KB, + }, + '10MB': { + json: JSON10MB, + blob: BLOB10MB, + formData: FORM10MB, + urlSearchParams: USP10MB, + eventParts: EVENTS10MB, + octetParts: OCTET10MB, + }, +} as const diff --git a/bench/core.bench.ts b/bench/core.bench.ts deleted file mode 100644 index e6f4015..0000000 --- a/bench/core.bench.ts +++ /dev/null @@ -1,83 +0,0 @@ -import type { StandardHeaders } from '@standardserver/core' -import { - flattenStandardHeader, - generateContentDisposition, - getFilenameFromContentDisposition, - isStandardHeaders, - isStandardRequest, - isStandardResponse, - mergeStandardHeaders, - parseStandardUrl, -} from '@standardserver/core' -import { bench, describe } from 'vitest' - -const sampleHeaders: StandardHeaders = { - 'content-type': 'application/json', - 'accept': ['application/json', 'text/plain'], - 'user-agent': 'standard-server-bench', - 'x-request-id': 'abc-123-def-456', - 'set-cookie': ['a=1; HttpOnly', 'b=2; Secure'], -} - -const otherHeaders: StandardHeaders = { - 'content-type': 'text/html', - 'x-custom': 'value', - 'accept': 'application/xml', -} - -const sampleRequest = { - method: 'POST', - url: '/api/v1/resource?page=1&limit=20#section', - headers: sampleHeaders, -} - -const sampleResponse = { - status: 200, - headers: sampleHeaders, -} - -const contentDisposition = generateContentDisposition('rapport final (été 2024).pdf') - -describe('core / url parsing', () => { - bench('parseStandardUrl - full url', () => { - parseStandardUrl('/api/v1/resource?page=1&limit=20#section') - }) - - bench('parseStandardUrl - pathname only', () => { - parseStandardUrl('/api/v1/resource') - }) -}) - -describe('core / headers', () => { - bench('flattenStandardHeader - array', () => { - flattenStandardHeader(['application/json', 'text/plain', 'text/html']) - }) - - bench('mergeStandardHeaders', () => { - mergeStandardHeaders(sampleHeaders, otherHeaders) - }) - - bench('isStandardHeaders', () => { - isStandardHeaders(sampleHeaders) - }) -}) - -describe('core / content-disposition', () => { - bench('generateContentDisposition', () => { - generateContentDisposition('rapport final (été 2024).pdf') - }) - - bench('getFilenameFromContentDisposition', () => { - getFilenameFromContentDisposition(contentDisposition) - }) -}) - -describe('core / validators', () => { - bench('isStandardRequest', () => { - isStandardRequest(sampleRequest) - }) - - bench('isStandardResponse', () => { - isStandardResponse(sampleResponse) - }) -}) diff --git a/bench/e2e.bench.ts b/bench/e2e.bench.ts new file mode 100644 index 0000000..7eb02a0 --- /dev/null +++ b/bench/e2e.bench.ts @@ -0,0 +1,80 @@ +import { bench, describe } from 'vitest' +import { echoHandler, roundTrip } from './__shared__/client-server' +import { createHonoFetchClientServer } from './__shared__/client-server.hono-fetch' +import { createInprogressFetchClientServer } from './__shared__/client-server.inprogress-fetch' +import { createMessagePortClientServer } from './__shared__/client-server.message-port' +import { createNodeHttpClientServer } from './__shared__/client-server.node-http' +import { createNodeWsClientServer } from './__shared__/client-server.node-ws' +import { asEventStream, asOctetStream, BODY_PAYLOADS, BODY_SIZE_ENTRIES } from './__shared__/payloads' + +const adapters = [ + ['inprogress-fetch', createInprogressFetchClientServer], + ['node-http', createNodeHttpClientServer], + ['hono-fetch', createHonoFetchClientServer], + ['message-port', createMessagePortClientServer], + ['node-ws', createNodeWsClientServer], +] as const + +describe.each(adapters)('e2e client-server: $0', async (_, create) => { + const clientServer = create() + clientServer.handler = echoHandler + await roundTrip(clientServer, { method: 'GET', url: '/', headers: {} }) + + describe.each(BODY_SIZE_ENTRIES)('body size $0', (label) => { + const payloads = BODY_PAYLOADS[label] + + bench('json', async () => { + await roundTrip(clientServer, { + method: 'POST', + url: `/${label}/json`, + headers: {}, + body: payloads.json, + }) + }) + + bench('blob', async () => { + await roundTrip(clientServer, { + method: 'POST', + url: `/${label}/blob`, + headers: {}, + body: payloads.blob, + }) + }) + + bench('mixed formData', async () => { + await roundTrip(clientServer, { + method: 'POST', + url: `/${label}/form-data`, + headers: {}, + body: payloads.formData, + }) + }) + + bench('url search params', async () => { + await roundTrip(clientServer, { + method: 'POST', + url: `/${label}/url-search-params`, + headers: {}, + body: payloads.urlSearchParams, + }) + }) + + bench('event stream', async () => { + await roundTrip(clientServer, { + method: 'POST', + url: `/${label}/event-stream`, + headers: {}, + body: asEventStream(payloads.eventParts), + }) + }) + + bench('octet stream', async () => { + await roundTrip(clientServer, { + method: 'POST', + url: `/${label}/octet-stream`, + headers: {}, + body: asOctetStream(payloads.octetParts), + }) + }) + }) +}) diff --git a/bench/fetch.bench.ts b/bench/fetch.bench.ts deleted file mode 100644 index dbe3c82..0000000 --- a/bench/fetch.bench.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { StandardHeaders } from '@standardserver/core' -import { toFetchHeaders, toStandardHeaders, toStandardUrl } from '@standardserver/fetch' -import { bench, describe } from 'vitest' - -const fetchHeaders = new Headers() -fetchHeaders.append('content-type', 'application/json') -fetchHeaders.append('accept', 'application/json') -fetchHeaders.append('accept', 'text/plain') -fetchHeaders.append('set-cookie', 'a=1; HttpOnly') -fetchHeaders.append('set-cookie', 'b=2; Secure') -fetchHeaders.append('user-agent', 'standard-server-bench') -fetchHeaders.append('x-request-id', 'abc-123-def-456') - -const standardHeaders: StandardHeaders = { - 'content-type': 'application/json', - 'accept': ['application/json', 'text/plain'], - 'set-cookie': ['a=1; HttpOnly', 'b=2; Secure'], - 'user-agent': 'standard-server-bench', - 'x-request-id': 'abc-123-def-456', -} - -const url = new URL('https://example.com/api/v1/resource?page=1&limit=20#section') - -describe('fetch / headers', () => { - bench('toStandardHeaders', () => { - toStandardHeaders(fetchHeaders) - }) - - bench('toFetchHeaders', () => { - toFetchHeaders(standardHeaders) - }) -}) - -describe('fetch / url', () => { - bench('toStandardUrl', () => { - toStandardUrl(url) - }) -}) diff --git a/bench/peer.bench.ts b/bench/peer.bench.ts deleted file mode 100644 index b7138ad..0000000 --- a/bench/peer.bench.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { PeerMessage } from '@standardserver/peer' -import { decodePeerMessage, encodePeerMessage } from '@standardserver/peer' -import { bench, describe } from 'vitest' - -const jsonMessage: PeerMessage = { - id: 'req-0001', - kind: 'request', - json: { - method: 'POST', - url: '/api/v1/resource?page=1&limit=20', - headers: { - 'content-type': 'application/json', - 'accept': ['application/json', 'text/plain'], - }, - body: { - name: 'standard-server', - values: Array.from({ length: 32 }, (_, i) => ({ index: i, label: `item-${i}` })), - }, - }, -} - -const binaryPayload = new Uint8Array(4096) -for (let i = 0; i < binaryPayload.length; i++) { - binaryPayload[i] = i % 251 -} - -const binaryMessage: PeerMessage = { - id: 'req-0002', - kind: 'octet-stream', - json: { url: '/upload' }, - binary: binaryPayload, -} - -const encodedJson = await encodePeerMessage(jsonMessage) as string -const encodedBinary = await encodePeerMessage(binaryMessage) as Uint8Array -const encodedPrefixed = await encodePeerMessage(binaryMessage, { prefix: 'ss:' }) as Uint8Array - -describe('peer / encode', () => { - bench('encodePeerMessage - json only', async () => { - await encodePeerMessage(jsonMessage) - }) - - bench('encodePeerMessage - json + binary', async () => { - await encodePeerMessage(binaryMessage) - }) - - bench('encodePeerMessage - with prefix', async () => { - await encodePeerMessage(binaryMessage, { prefix: 'ss:' }) - }) -}) - -describe('peer / decode', () => { - bench('decodePeerMessage - json only', () => { - decodePeerMessage(encodedJson) - }) - - bench('decodePeerMessage - json + binary', () => { - decodePeerMessage(encodedBinary) - }) - - bench('decodePeerMessage - with prefix', () => { - decodePeerMessage(encodedPrefixed, { prefix: 'ss:' }) - }) -}) From dc1c64cbc5721d79d239b83962d244871232c80d Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Wed, 15 Jul 2026 18:52:44 +0700 Subject: [PATCH 06/10] reduce bench --- .../client-server.inprogress-fetch.ts | 32 ---------- .../__shared__/client-server.message-port.ts | 58 ------------------- bench/e2e.bench.ts | 6 +- 3 files changed, 1 insertion(+), 95 deletions(-) delete mode 100644 bench/__shared__/client-server.inprogress-fetch.ts delete mode 100644 bench/__shared__/client-server.message-port.ts diff --git a/bench/__shared__/client-server.inprogress-fetch.ts b/bench/__shared__/client-server.inprogress-fetch.ts deleted file mode 100644 index 505eb36..0000000 --- a/bench/__shared__/client-server.inprogress-fetch.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { ClientServer } from './client-server' -import { toFetchBody, toFetchHeaders, toFetchResponse, toStandardLazyRequest, toStandardLazyResponse } from '@standardserver/fetch' - -/** - * In-process fetch adapter path (Request/Response conversion) without a real network hop. - */ -export function createInprogressFetchClientServer(): ClientServer { - const clientServer: ClientServer = { - handler: async () => ({ status: 404, body: 'Not Found', headers: {} }), - request: async (standardRequest) => { - const [body, standardHeaders] = toFetchBody(standardRequest.body, standardRequest.headers) - - return toStandardLazyResponse( - toFetchResponse( - await clientServer.handler( - toStandardLazyRequest( - new Request(`http://localhost${standardRequest.url}`, { - method: standardRequest.method, - signal: standardRequest.signal ?? null, - headers: toFetchHeaders(standardHeaders), - body: body ?? null, - duplex: 'half', - }), - ), - ), - ), - ) - }, - } - - return clientServer -} diff --git a/bench/__shared__/client-server.message-port.ts b/bench/__shared__/client-server.message-port.ts deleted file mode 100644 index 18dd0da..0000000 --- a/bench/__shared__/client-server.message-port.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { PeerMessage } from '@standardserver/peer' -import type { ClientServer } from './client-server' -import { ClientPeer, decodePeerMessage, encodePeerMessage, isClientPeerSendMessage, isServerPeerSendMessage, ServerPeer } from '@standardserver/peer' - -const prefix = 'bench:' - -async function encode(message: PeerMessage) { - return encodePeerMessage(message, { prefix }) -} - -/** - * Peer adapter over `MessageChannel` (in-process, no network). - */ -export function createMessagePortClientServer(): ClientServer { - const { port1, port2 } = new MessageChannel() - - const clientPeer = new ClientPeer(async (message) => { - port1.postMessage(await encode(message)) - }) - - port1.addEventListener('message', async (event) => { - const { matched, message } = decodePeerMessage(event.data, { prefix }) - - if (!matched || !isServerPeerSendMessage(message)) { - return - } - - await clientPeer.message(message) - }) - port1.start() - - const clientServer: ClientServer = { - handler: async () => ({ status: 404, body: 'Not Found', headers: {} }), - request: async standardRequest => clientPeer.request(standardRequest), - } - - const serverPeer = new ServerPeer(async (message) => { - port2.postMessage(await encode(message)) - }) - - port2.addEventListener('message', async (event) => { - const { matched, message } = decodePeerMessage(event.data, { prefix }) - - if (!matched || !isClientPeerSendMessage(message)) { - return - } - - await serverPeer.message(message, async request => clientServer.handler(request)) - }) - port2.start() - - afterAll(() => { - port1.close() - port2.close() - }) - - return clientServer -} diff --git a/bench/e2e.bench.ts b/bench/e2e.bench.ts index 7eb02a0..65c6566 100644 --- a/bench/e2e.bench.ts +++ b/bench/e2e.bench.ts @@ -1,17 +1,13 @@ import { bench, describe } from 'vitest' import { echoHandler, roundTrip } from './__shared__/client-server' import { createHonoFetchClientServer } from './__shared__/client-server.hono-fetch' -import { createInprogressFetchClientServer } from './__shared__/client-server.inprogress-fetch' -import { createMessagePortClientServer } from './__shared__/client-server.message-port' import { createNodeHttpClientServer } from './__shared__/client-server.node-http' import { createNodeWsClientServer } from './__shared__/client-server.node-ws' import { asEventStream, asOctetStream, BODY_PAYLOADS, BODY_SIZE_ENTRIES } from './__shared__/payloads' const adapters = [ - ['inprogress-fetch', createInprogressFetchClientServer], ['node-http', createNodeHttpClientServer], ['hono-fetch', createHonoFetchClientServer], - ['message-port', createMessagePortClientServer], ['node-ws', createNodeWsClientServer], ] as const @@ -41,7 +37,7 @@ describe.each(adapters)('e2e client-server: $0', async (_, create) => { }) }) - bench('mixed formData', async () => { + bench('form data', async () => { await roundTrip(clientServer, { method: 'POST', url: `/${label}/form-data`, From 7af1998edf8ecbc6bbcd8a7ae93ecb7c7149aceb Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Wed, 15 Jul 2026 19:19:14 +0700 Subject: [PATCH 07/10] reduce bench size --- bench/__shared__/payloads.ts | 44 ++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/bench/__shared__/payloads.ts b/bench/__shared__/payloads.ts index 0b6600e..40e08fa 100644 --- a/bench/__shared__/payloads.ts +++ b/bench/__shared__/payloads.ts @@ -2,7 +2,7 @@ import assert from 'node:assert/strict' import { stringifyJSON } from '@standardserver/shared' const KB = 1024 -const SIZES = { '1KB': KB, '100KB': 100 * KB, '10MB': 100 * 100 * KB } as const +const SIZES = { '1KB': KB, '100KB': 100 * KB, '5MB': 50 * 100 * KB } as const type SizeLabel = keyof typeof SIZES export const BODY_SIZE_ENTRIES = Object.entries(SIZES) as [SizeLabel, number][] @@ -192,8 +192,8 @@ const BYTES1KB = bytes(SIZES['1KB'], 0xB001) assert.equal(BYTES1KB.byteLength, SIZES['1KB']) const BYTES100KB = repeatBytes(BYTES1KB, 100) assert.equal(BYTES100KB.byteLength, SIZES['100KB']) -const BYTES10MB = repeatBytes(BYTES100KB, 100) -assert.equal(BYTES10MB.byteLength, SIZES['10MB']) +const BYTES5MB = repeatBytes(BYTES100KB, 50) +assert.equal(BYTES5MB.byteLength, SIZES['5MB']) const JSON1KB = padJson(record(0xA001), SIZES['1KB'], 0xA101) assert.equal(jlen(JSON1KB), SIZES['1KB']) @@ -203,43 +203,43 @@ const JSON100KB = padJson( 0xA201, ) assert.equal(jlen(JSON100KB), SIZES['100KB']) -const JSON10MB = fitJson(JSON100KB, SIZES['10MB'], 0xA301) -assert.equal(jlen(JSON10MB), SIZES['10MB']) +const JSON5MB = fitJson(JSON100KB, SIZES['5MB'], 0xA301) +assert.equal(jlen(JSON5MB), SIZES['5MB']) const BLOB1KB = new Blob([BYTES1KB], { type: 'application/octet-stream' }) assert.equal(BLOB1KB.size, SIZES['1KB']) const BLOB100KB = new Blob([BYTES100KB], { type: 'application/octet-stream' }) assert.equal(BLOB100KB.size, SIZES['100KB']) -const BLOB10MB = new Blob([BYTES10MB], { type: 'application/octet-stream' }) -assert.equal(BLOB10MB.size, SIZES['10MB']) +const BLOB5MB = new Blob([BYTES5MB], { type: 'application/octet-stream' }) +assert.equal(BLOB5MB.size, SIZES['5MB']) const FORM1KB = makeForm(SIZES['1KB'], BYTES1KB, 0xF001) assert.equal(formSize(FORM1KB), SIZES['1KB']) const FORM100KB = makeForm(SIZES['100KB'], BYTES100KB, 0xF101) assert.equal(formSize(FORM100KB), SIZES['100KB']) -const FORM10MB = makeForm(SIZES['10MB'], BYTES10MB, 0xF201) -assert.equal(formSize(FORM10MB), SIZES['10MB']) +const FORM5MB = makeForm(SIZES['5MB'], BYTES5MB, 0xF201) +assert.equal(formSize(FORM5MB), SIZES['5MB']) const USP1KB = makeParams(SIZES['1KB'], 0xC001) assert.equal(USP1KB.toString().length, SIZES['1KB']) const USP100KB = makeParams(SIZES['100KB'], 0xC101) assert.equal(USP100KB.toString().length, SIZES['100KB']) -const USP10MB = makeParams(SIZES['10MB'], 0xC201) -assert.equal(USP10MB.toString().length, SIZES['10MB']) +const USP5MB = makeParams(SIZES['5MB'], 0xC201) +assert.equal(USP5MB.toString().length, SIZES['5MB']) const EVENTS1KB = makeEvents(SIZES['1KB'], 0xE001) assert.equal(eventSize(EVENTS1KB), SIZES['1KB']) const EVENTS100KB = Array.from({ length: 100 }, (_, i) => i === 0 ? EVENTS1KB : makeEvents(SIZES['1KB'], 0xE100 + i)).flat() assert.equal(eventSize(EVENTS100KB), SIZES['100KB']) -const EVENTS10MB = fitEvents(JSON100KB, SIZES['10MB'], 0xE201) -assert.equal(eventSize(EVENTS10MB), SIZES['10MB']) +const EVENTS5MB = fitEvents(JSON100KB, SIZES['5MB'], 0xE201) +assert.equal(eventSize(EVENTS5MB), SIZES['5MB']) const OCTET1KB = split(BYTES1KB, 8) assert.equal(OCTET1KB.reduce((n, c) => n + c.byteLength, 0), SIZES['1KB']) const OCTET100KB = split(BYTES100KB, 32) assert.equal(OCTET100KB.reduce((n, c) => n + c.byteLength, 0), SIZES['100KB']) -const OCTET10MB = split(BYTES10MB, 64) -assert.equal(OCTET10MB.reduce((n, c) => n + c.byteLength, 0), SIZES['10MB']) +const OCTET5MB = split(BYTES5MB, 64) +assert.equal(OCTET5MB.reduce((n, c) => n + c.byteLength, 0), SIZES['5MB']) export function asEventStream(parts: readonly Event[]): AsyncGenerator { return (async function* () { @@ -275,12 +275,12 @@ export const BODY_PAYLOADS = { eventParts: EVENTS100KB, octetParts: OCTET100KB, }, - '10MB': { - json: JSON10MB, - blob: BLOB10MB, - formData: FORM10MB, - urlSearchParams: USP10MB, - eventParts: EVENTS10MB, - octetParts: OCTET10MB, + '5MB': { + json: JSON5MB, + blob: BLOB5MB, + formData: FORM5MB, + urlSearchParams: USP5MB, + eventParts: EVENTS5MB, + octetParts: OCTET5MB, }, } as const From d7046ebb34a5e03a6783922ff243d2a16bae67da Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Wed, 15 Jul 2026 19:34:44 +0700 Subject: [PATCH 08/10] walltime mode --- .github/workflows/codspeed.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codspeed.yaml b/.github/workflows/codspeed.yaml index 8610830..874b1c1 100644 --- a/.github/workflows/codspeed.yaml +++ b/.github/workflows/codspeed.yaml @@ -15,7 +15,7 @@ permissions: jobs: benchmarks: - runs-on: ubuntu-latest + runs-on: codspeed-macro steps: - uses: actions/checkout@v7 @@ -25,5 +25,5 @@ jobs: - uses: CodSpeedHQ/action@v4 with: - mode: simulation + mode: walltime run: pnpm run bench From b1a62ad082c7a6fc55aefb4c5e66c34f449ca2c6 Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Wed, 15 Jul 2026 19:47:32 +0700 Subject: [PATCH 09/10] fix --- .github/workflows/codspeed.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codspeed.yaml b/.github/workflows/codspeed.yaml index 874b1c1..eda27aa 100644 --- a/.github/workflows/codspeed.yaml +++ b/.github/workflows/codspeed.yaml @@ -15,7 +15,7 @@ permissions: jobs: benchmarks: - runs-on: codspeed-macro + runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 From 309d2158fc944d06dc6aeb99538f5fb08fa0c3fd Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Wed, 15 Jul 2026 20:05:01 +0700 Subject: [PATCH 10/10] improve --- bench/__shared__/client-server.node-ws.ts | 8 +++----- packages/core/README.md | 3 +++ packages/fetch/README.md | 3 +++ packages/node/README.md | 3 +++ packages/peer/README.md | 3 +++ packages/shared/README.md | 3 +++ 6 files changed, 18 insertions(+), 5 deletions(-) diff --git a/bench/__shared__/client-server.node-ws.ts b/bench/__shared__/client-server.node-ws.ts index 56c6603..8083375 100644 --- a/bench/__shared__/client-server.node-ws.ts +++ b/bench/__shared__/client-server.node-ws.ts @@ -5,10 +5,8 @@ import type { ClientServer } from './client-server' import { ClientPeer, decodePeerMessage, encodePeerMessage, isClientPeerSendMessage, isServerPeerSendMessage, ServerPeer } from '@standardserver/peer' import { WebSocket, WebSocketServer } from 'ws' -const prefix = 'bench:' - async function encode(message: PeerMessage) { - return encodePeerMessage(message, { prefix }) + return encodePeerMessage(message) } async function normalizeWsData(data: unknown): Promise> { @@ -64,7 +62,7 @@ export function createNodeWsClientServer(): ClientServer { wsc.addEventListener('message', async (event) => { const encoded = await normalizeWsData(event.data) - const { matched, message } = decodePeerMessage(encoded, { prefix }) + const { matched, message } = decodePeerMessage(encoded) if (!matched || !isServerPeerSendMessage(message)) { return @@ -96,7 +94,7 @@ export function createNodeWsClientServer(): ClientServer { ws.addEventListener('message', async (event) => { const encoded = await normalizeWsData(event.data) - const { matched, message } = decodePeerMessage(encoded, { prefix }) + const { matched, message } = decodePeerMessage(encoded) if (!matched || !isClientPeerSendMessage(message)) { return diff --git a/packages/core/README.md b/packages/core/README.md index cd7dfeb..b2e5b8d 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -7,6 +7,9 @@ weekly downloads + + CodSpeed + MIT License diff --git a/packages/fetch/README.md b/packages/fetch/README.md index 18daec3..400e82e 100644 --- a/packages/fetch/README.md +++ b/packages/fetch/README.md @@ -7,6 +7,9 @@ weekly downloads + + CodSpeed + MIT License diff --git a/packages/node/README.md b/packages/node/README.md index 99cd58d..6a4fb4c 100644 --- a/packages/node/README.md +++ b/packages/node/README.md @@ -7,6 +7,9 @@ weekly downloads + + CodSpeed + MIT License diff --git a/packages/peer/README.md b/packages/peer/README.md index 2da18a0..0abfe66 100644 --- a/packages/peer/README.md +++ b/packages/peer/README.md @@ -7,6 +7,9 @@ weekly downloads + + CodSpeed + MIT License diff --git a/packages/shared/README.md b/packages/shared/README.md index a37e122..5d7e63f 100644 --- a/packages/shared/README.md +++ b/packages/shared/README.md @@ -7,6 +7,9 @@ weekly downloads + + CodSpeed + MIT License