diff --git a/benches/static-file-handler.bench.ts b/benches/static-file-handler.bench.ts index 9d4d900d1..f6645195f 100644 --- a/benches/static-file-handler.bench.ts +++ b/benches/static-file-handler.bench.ts @@ -1,18 +1,62 @@ import type { StandardLazyRequest } from '@standard-server/core' +import type { Stats } from 'node:fs' import { Buffer } from 'node:buffer' -import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs' -import { tmpdir } from 'node:os' import path from 'node:path' +import { Readable } from 'node:stream' import { StaticFileHandlerPlugin } from '@orpc/node' import { RPCHandlerCodec, StandardHandler } from '@orpc/server/standard' import { bench } from 'vitest' -import { drainBody } from './__shared__/payloads' -const rootDir = mkdtempSync(path.join(tmpdir(), 'orpc-static-file-bench-')) +/** + * CodSpeed measures a single call, so files are served from memory: real fs calls hop through the + * libuv thread pool and the event loop, which lets thread scheduling and unrelated callbacks + * (like vitest's worker IPC) land in the measurement and flip results between runs. + * The shared payloads are not imported either, since building them changes how much V8 compilation + * lands in the first measured call. + */ +vi.mock('node:fs', async importOriginal => ({ ...await importOriginal(), createReadStream })) +vi.mock('node:fs/promises', async importOriginal => ({ ...await importOriginal(), stat, realpath })) -writeFileSync(path.join(rootDir, 'file.txt'), Buffer.alloc(10 * 1024, 'a')) -mkdirSync(path.join(rootDir, 'deeply', 'nested', 'dir'), { recursive: true }) -writeFileSync(path.join(rootDir, 'deeply', 'nested', 'dir', 'file.txt'), Buffer.alloc(10 * 1024, 'a')) +const rootDir = path.resolve('/static') +const mtime = new Date('2024-01-01T00:00:00.000Z') + +const files = new Map([ + [path.join(rootDir, 'file.txt'), Buffer.alloc(10 * 1024, 'a')], + [path.join(rootDir, 'deeply', 'nested', 'dir', 'file.txt'), Buffer.alloc(10 * 1024, 'a')], +]) + +function notFound(filePath: string): Error { + return Object.assign(new Error(`ENOENT: no such file or directory, '${filePath}'`), { code: 'ENOENT' }) +} + +async function stat(filePath: string): Promise { + const content = files.get(filePath) + + if (content === undefined) { + throw notFound(filePath) + } + + return { size: content.length, mtime, isFile: () => true, isDirectory: () => false } as Stats +} + +async function realpath(filePath: string): Promise { + if (filePath !== rootDir && !files.has(filePath)) { + throw notFound(filePath) + } + + return filePath +} + +function createReadStream(filePath: string, { start = 0, end = Infinity }: { start?: number, end?: number } = {}): Readable { + const chunk = files.get(filePath)!.subarray(start, end + 1) + + return new Readable({ + read() { + this.push(chunk) + this.push(null) + }, + }) +} const handler = new StandardHandler(new RPCHandlerCodec({}, {}), { plugins: [new StaticFileHandlerPlugin({ rootDir })], @@ -27,6 +71,14 @@ function createRequest(url: `/${string}`, headers: Record = {}): } } +async function drainBody(body: unknown): Promise { + const reader = (body as ReadableStream).getReader() + + while (!(await reader.read()).done) { + // discard the chunk + } +} + const { response } = await handler.handle(createRequest('/file.txt'), { context: {} }) await drainBody(response!.body) const etag = response!.headers.etag as string