From e381dcc04d78018776411af5353a2ac24e58cc9d Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Thu, 16 Jul 2026 20:13:32 +0700 Subject: [PATCH 1/2] ci: use simulation mode in CodSpeed --- .github/workflows/codspeed.yaml | 2 +- ...r.hono-fetch.ts => client-server.fetch.ts} | 34 +- benches/__shared__/client-server.node-ws.ts | 108 ------ ...ver.node-http.ts => client-server.node.ts} | 5 +- benches/__shared__/client-server.peer.ts | 37 ++ benches/__shared__/payloads.ts | 327 +++++------------- benches/e2e.bench.ts | 111 +++--- vitest.config.ts | 18 +- 8 files changed, 195 insertions(+), 447 deletions(-) rename benches/__shared__/{client-server.hono-fetch.ts => client-server.fetch.ts} (53%) delete mode 100644 benches/__shared__/client-server.node-ws.ts rename benches/__shared__/{client-server.node-http.ts => client-server.node.ts} (90%) create mode 100644 benches/__shared__/client-server.peer.ts diff --git a/.github/workflows/codspeed.yaml b/.github/workflows/codspeed.yaml index eda27aa..8610830 100644 --- a/.github/workflows/codspeed.yaml +++ b/.github/workflows/codspeed.yaml @@ -25,5 +25,5 @@ jobs: - uses: CodSpeedHQ/action@v4 with: - mode: walltime + mode: simulation run: pnpm run bench diff --git a/benches/__shared__/client-server.hono-fetch.ts b/benches/__shared__/client-server.fetch.ts similarity index 53% rename from benches/__shared__/client-server.hono-fetch.ts rename to benches/__shared__/client-server.fetch.ts index 7dc5718..93576ff 100644 --- a/benches/__shared__/client-server.hono-fetch.ts +++ b/benches/__shared__/client-server.fetch.ts @@ -1,12 +1,7 @@ -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 { +export function createFetchClientServer(): ClientServer { const clientServer: ClientServer = { handler: async () => ({ status: 404, body: 'Not Found', headers: {} }), request: async () => { @@ -14,21 +9,6 @@ export function createHonoFetchClientServer(): ClientServer { }, } - 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) @@ -43,9 +23,15 @@ export function createHonoFetchClientServer(): ClientServer { init.duplex = 'half' } - const response = await fetch(`http://localhost:${addressInfo.port}${standardRequest.url}`, init) - - return toStandardLazyResponse(response) + return toStandardLazyResponse( + toFetchResponse( + await clientServer.handler( + toStandardLazyRequest( + new Request(`http://localhost:${standardRequest.url}`, init), + ), + ), + ), + ) } return clientServer diff --git a/benches/__shared__/client-server.node-ws.ts b/benches/__shared__/client-server.node-ws.ts deleted file mode 100644 index 8083375..0000000 --- a/benches/__shared__/client-server.node-ws.ts +++ /dev/null @@ -1,108 +0,0 @@ -/* 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' - -async function encode(message: PeerMessage) { - return encodePeerMessage(message) -} - -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) - - 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) - - if (!matched || !isClientPeerSendMessage(message)) { - return - } - - await serverPeer.message(message, async request => clientServer.handler(request)) - }) - }) - - return clientServer -} diff --git a/benches/__shared__/client-server.node-http.ts b/benches/__shared__/client-server.node.ts similarity index 90% rename from benches/__shared__/client-server.node-http.ts rename to benches/__shared__/client-server.node.ts index 8bd9760..711afd2 100644 --- a/benches/__shared__/client-server.node-http.ts +++ b/benches/__shared__/client-server.node.ts @@ -4,10 +4,7 @@ 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 { +export function createNodeClientServer(): ClientServer { const clientServer: ClientServer = { handler: async () => ({ status: 404, body: 'Not Found', headers: {} }), request: async () => { diff --git a/benches/__shared__/client-server.peer.ts b/benches/__shared__/client-server.peer.ts new file mode 100644 index 0000000..c248ed7 --- /dev/null +++ b/benches/__shared__/client-server.peer.ts @@ -0,0 +1,37 @@ +import type { ClientServer } from './client-server' +import { ClientPeer, decodePeerMessage, encodePeerMessage, isClientPeerSendMessage, isServerPeerSendMessage, ServerPeer } from '@standardserver/peer' + +export function createPeerClientServer(): ClientServer { + let clientPeer: ClientPeer + + const clientServer: ClientServer = { + handler: async () => ({ status: 404, body: 'Not Found', headers: {} }), + request: async (standardRequest) => { + return clientPeer.request(standardRequest) + }, + } + + const serverPeer = new ServerPeer(async (message) => { + const encoded = await encodePeerMessage(message) + const decoded = decodePeerMessage(encoded) + + if (!decoded.matched || !isServerPeerSendMessage(decoded.message)) { + return + } + + clientPeer.message(decoded.message) + }) + + clientPeer = new ClientPeer(async (message) => { + const encoded = await encodePeerMessage(message) + const decoded = decodePeerMessage(encoded) + + if (!decoded.matched || !isClientPeerSendMessage(decoded.message)) { + throw new Error('not found') + } + + serverPeer.message(decoded.message, clientServer.handler) + }) + + return clientServer +} diff --git a/benches/__shared__/payloads.ts b/benches/__shared__/payloads.ts index 40e08fa..3e27a11 100644 --- a/benches/__shared__/payloads.ts +++ b/benches/__shared__/payloads.ts @@ -1,100 +1,44 @@ -import assert from 'node:assert/strict' import { stringifyJSON } from '@standardserver/shared' -const KB = 1024 -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][] +const SIZE_1KB = 1024 +const SIZE_10KB = 10 * SIZE_1KB -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) +/** Simple JSON unit. */ +function createUnit(i: number) { + return { + id: i, + name: `item-${i}`, + active: true, + score: 42.5, + tags: ['a', 'b', 'c'], + meta: { version: '2.0.0', count: i, region: 'us-east-1' }, + profile: { + name: `user-${i}`, + email: `user-${i}@example.com`, + bio: 'hello world', + }, } - 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 +/** Pad a JSON value so `stringifyJSON` length equals `size`. */ +function padJson(value: object, size: number): object { + const base = stringifyJSON({ ...value, _pad: '' })!.length + if (base > size) + throw new Error(`json core ${base} > ${size}`) + return { ...value, _pad: 'x'.repeat(size - base) } } -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) } -} +const PAYLOAD_UNIT = createUnit(0) -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) }, - } -} +export const JSON_10KB = padJson( + { items: Array.from({ length: 10 }, (_, i) => createUnit(i)) }, + SIZE_10KB, +) -function split(buf: Uint8Array, parts: number): Uint8Array[] { - const base = Math.floor(buf.byteLength / parts) - let rem = buf.byteLength - base * parts +function splitBytes(size: number, parts: number): Uint8Array[] { + const buf = new Uint8Array(size) as Uint8Array + const base = Math.floor(size / parts) + let rem = size - base * parts const out: Uint8Array[] = [] let off = 0 for (let i = 0; i < parts; i++) { @@ -105,182 +49,75 @@ function split(buf: Uint8Array, parts: number): 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 +export const FORM_10KB = new FormData() +FORM_10KB.append('userId', 'user_0') +FORM_10KB.append('action', 'create') +FORM_10KB.append('meta', stringifyJSON({ tags: ['a', 'b'], dryRun: false })!) +FORM_10KB.append('note', 'hello world') +{ + const fileSize = SIZE_10KB - formContentSize(FORM_10KB) + if (fileSize <= 0) + throw new Error(`form core >= ${SIZE_10KB}`) + FORM_10KB.append('file', new File([new Uint8Array(fileSize)], 'data.bin', { type: 'application/octet-stream' })) +} + +export const USP_10KB = new URLSearchParams({ + 'page': '1', + 'sort': 'created_at', + 'q': 'search', + 'filter[status]': 'active', + 'data': '', +}) +{ + const base = USP_10KB.toString().length + USP_10KB.set('data', 'x'.repeat(SIZE_10KB - base)) + const d = SIZE_10KB - USP_10KB.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 + USP_10KB.set('data', (USP_10KB.get('data') ?? '') + 'x'.repeat(d)) } -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)) +/** Events totaling 10KB of stringified JSON. */ +function makeEvents(size: number): object[] { + const parts: object[] = [PAYLOAD_UNIT, PAYLOAD_UNIT, PAYLOAD_UNIT, { type: 'pad', text: '' }] + const core = parts.slice(0, -1).reduce((n, p) => n + stringifyJSON(p)!.length, 0) + const padBase = stringifyJSON({ type: 'pad', text: '' })!.length + if (core + padBase > size) + throw new Error(`event core ${core + padBase} > ${size}`) + parts[parts.length - 1] = { type: 'pad', text: 'x'.repeat(size - core - padBase) } 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 BYTES5MB = repeatBytes(BYTES100KB, 50) -assert.equal(BYTES5MB.byteLength, SIZES['5MB']) - -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 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 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 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 USP5MB = makeParams(SIZES['5MB'], 0xC201) -assert.equal(USP5MB.toString().length, SIZES['5MB']) +export const EVENTS_10KB = makeEvents(SIZE_10KB) -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 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 OCTET5MB = split(BYTES5MB, 64) -assert.equal(OCTET5MB.reduce((n, c) => n + c.byteLength, 0), SIZES['5MB']) - -export function asEventStream(parts: readonly Event[]): AsyncGenerator { +/** Fresh async generator over prebuilt event parts (one-shot per call). */ +export function asAsyncIteratorObject(parts: readonly unknown[]): AsyncGenerator { return (async function* () { - for (const p of parts) yield p + for (const part of parts) + yield part }()) } -export function asOctetStream(parts: readonly Uint8Array[]): ReadableStream> { +/** Fresh ReadableStream over prebuilt octet chunks (one-shot per call). */ +export function asReadableStream(parts: readonly Uint8Array[]): ReadableStream> { let i = 0 return new ReadableStream({ - pull(c) { + pull(controller) { if (i >= parts.length) - c.close() - else c.enqueue(parts[i++]!) + controller.close() + else + controller.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, - }, - '5MB': { - json: JSON5MB, - blob: BLOB5MB, - formData: FORM5MB, - urlSearchParams: USP5MB, - eventParts: EVENTS5MB, - octetParts: OCTET5MB, - }, -} as const diff --git a/benches/e2e.bench.ts b/benches/e2e.bench.ts index 65c6566..3f06429 100644 --- a/benches/e2e.bench.ts +++ b/benches/e2e.bench.ts @@ -1,76 +1,81 @@ import { bench, describe } from 'vitest' import { echoHandler, roundTrip } from './__shared__/client-server' -import { createHonoFetchClientServer } from './__shared__/client-server.hono-fetch' -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' +import { createFetchClientServer } from './__shared__/client-server.fetch' +import { createNodeClientServer } from './__shared__/client-server.node' +import { createPeerClientServer } from './__shared__/client-server.peer' +import { + asAsyncIteratorObject, + asReadableStream, + BLOB_10KB, + BYTES_10KB, + EVENTS_10KB, + FORM_10KB, + JSON_10KB, + USP_10KB, +} from './__shared__/payloads' const adapters = [ - ['node-http', createNodeHttpClientServer], - ['hono-fetch', createHonoFetchClientServer], - ['node-ws', createNodeWsClientServer], + ['node-adapter', createNodeClientServer], + ['fetch-adapter', createFetchClientServer], + ['peer-adapter', createPeerClientServer], ] as const describe.each(adapters)('e2e client-server: $0', async (_, create) => { const clientServer = create() clientServer.handler = echoHandler - await roundTrip(clientServer, { method: 'GET', url: '/', headers: {} }) + await roundTrip(clientServer, { method: 'GET', url: '/', headers: {} }) // ensure client-server is ready - 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('json 10KB', async () => { + await roundTrip(clientServer, { + method: 'POST', + url: '/10KB/json', + headers: {}, + body: JSON_10KB, }) + }) - bench('blob', async () => { - await roundTrip(clientServer, { - method: 'POST', - url: `/${label}/blob`, - headers: {}, - body: payloads.blob, - }) + bench('blob 10KB', async () => { + await roundTrip(clientServer, { + method: 'POST', + url: '/10KB/blob', + headers: {}, + body: BLOB_10KB, }) + }) - bench('form data', async () => { - await roundTrip(clientServer, { - method: 'POST', - url: `/${label}/form-data`, - headers: {}, - body: payloads.formData, - }) + bench('form data 10KB', async () => { + await roundTrip(clientServer, { + method: 'POST', + url: '/10KB/form-data', + headers: {}, + body: FORM_10KB, }) + }) - bench('url search params', async () => { - await roundTrip(clientServer, { - method: 'POST', - url: `/${label}/url-search-params`, - headers: {}, - body: payloads.urlSearchParams, - }) + bench('url search params 10KB', async () => { + await roundTrip(clientServer, { + method: 'POST', + url: '/10KB/url-search-params', + headers: {}, + body: USP_10KB, }) + }) - bench('event stream', async () => { - await roundTrip(clientServer, { - method: 'POST', - url: `/${label}/event-stream`, - headers: {}, - body: asEventStream(payloads.eventParts), - }) + bench('event stream 10KB', async () => { + await roundTrip(clientServer, { + method: 'POST', + url: '/10KB/event-stream', + headers: {}, + body: asAsyncIteratorObject(EVENTS_10KB), }) + }) - bench('octet stream', async () => { - await roundTrip(clientServer, { - method: 'POST', - url: `/${label}/octet-stream`, - headers: {}, - body: asOctetStream(payloads.octetParts), - }) + bench('octet stream 10KB', async () => { + await roundTrip(clientServer, { + method: 'POST', + url: '/10KB/octet-stream', + headers: {}, + body: asReadableStream(BYTES_10KB), }) }) }) diff --git a/vitest.config.ts b/vitest.config.ts index 495c301..91a70e0 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,22 +2,16 @@ import codspeedPlugin from '@codspeed/vitest-plugin' import { defineConfig } from 'vitest/config' export default defineConfig(() => ({ + plugins: [codspeedPlugin()], test: { + globals: true, + include: ['**/*.test.ts'], coverage: { include: ['packages/*/src/**'], exclude: ['**.test-d.*', '**.test.*', '**/*.bench.ts'], }, - projects: [ - { - plugins: [codspeedPlugin()], - test: { - globals: true, - include: ['**/*.test.ts'], - benchmark: { - include: ['**/*.bench.ts'], - }, - }, - }, - ], + benchmark: { + include: ['**/*.bench.ts'], + }, }, })) From f5eda3999a739d195d7a22c55bc3cb0e2e4d00df Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Thu, 16 Jul 2026 20:15:36 +0700 Subject: [PATCH 2/2] improve name --- .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..de63dc4 100644 --- a/.github/workflows/codspeed.yaml +++ b/.github/workflows/codspeed.yaml @@ -1,4 +1,4 @@ -name: CodSpeed Benchmarks +name: CodSpeed on: push: