diff --git a/frameworks/nestjs/Dockerfile b/frameworks/nestjs/Dockerfile new file mode 100644 index 000000000..f19f4ef18 --- /dev/null +++ b/frameworks/nestjs/Dockerfile @@ -0,0 +1,20 @@ +FROM node:26-trixie-slim AS build +RUN apt-get update && \ + apt-get install -y --no-install-recommends ca-certificates && \ + rm -rf /var/lib/apt/lists/* +WORKDIR /app +COPY package.json tsconfig.json ./ +RUN npm install +COPY src ./src +RUN npx tsc && npm prune --omit=dev + +FROM node:26-trixie-slim +RUN apt-get update && \ + apt-get install -y --no-install-recommends ca-certificates curl && \ + rm -rf /var/lib/apt/lists/* +WORKDIR /app +COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/dist ./dist +ENV NODE_ENV=production +EXPOSE 8080 +CMD ["node", "dist/main.js"] diff --git a/frameworks/nestjs/README.md b/frameworks/nestjs/README.md new file mode 100644 index 000000000..e598effe7 --- /dev/null +++ b/frameworks/nestjs/README.md @@ -0,0 +1,26 @@ +# nestjs + +NestJS 11 on the Express platform adapter, default configuration. + +## Stack + +- **Language:** TypeScript 5.9 on Node 26 +- **Framework:** NestJS 11 (`@nestjs/platform-express`) +- **Build:** Two-stage, `node:26-trixie-slim` runtime + +## Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/pipeline` | GET | Returns `ok` (plain text) | +| `/baseline11` | GET | Sums query parameter values | +| `/baseline11` | POST | Sums query parameters + request body | +| `/json/{count}?m=N` | GET | First `count` dataset items with `total = price * quantity * m` | +| `/upload` | POST | Reads the body and returns the byte count | + +## Notes + +- Controller with the Nest routing and `@Param` / `@Query` / `@Header` decorators +- Compression through the `compression` middleware, as the Nest docs recommend +- Body parsers are off and the POST endpoints read the raw stream, since they only sum or count what arrives +- The cluster module is used for multi-core scaling, one worker per available CPU diff --git a/frameworks/nestjs/meta.json b/frameworks/nestjs/meta.json new file mode 100644 index 000000000..d41910c82 --- /dev/null +++ b/frameworks/nestjs/meta.json @@ -0,0 +1,19 @@ +{ + "display_name": "nestjs", + "language": "TS", + "type": "flagship", + "mode": "standard", + "engine": "nodehttp", + "description": "NestJS 11 on the Express platform adapter, default configuration. Routing and parameter decorators through the Nest API, controller return values serialized by the framework, gzip through the compression middleware the Nest docs recommend.", + "repo": "https://github.com/nestjs/nest", + "enabled": true, + "tests": [ + "baseline", + "pipelined", + "limited-conn", + "json", + "json-comp", + "upload" + ], + "maintainers": [] +} diff --git a/frameworks/nestjs/package.json b/frameworks/nestjs/package.json new file mode 100644 index 000000000..6a89e3cca --- /dev/null +++ b/frameworks/nestjs/package.json @@ -0,0 +1,18 @@ +{ + "name": "httparena-nestjs", + "private": true, + "dependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0", + "@nestjs/platform-express": "^11.0.0", + "compression": "^1.8.0", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@types/compression": "^1.7.5", + "@types/express": "^5.0.0", + "@types/node": "^26.0.0", + "typescript": "^5.9.0" + } +} diff --git a/frameworks/nestjs/src/app.controller.ts b/frameworks/nestjs/src/app.controller.ts new file mode 100644 index 000000000..a9ad2c93d --- /dev/null +++ b/frameworks/nestjs/src/app.controller.ts @@ -0,0 +1,104 @@ +import { Controller, Get, Header, Param, Post, Query, Req } from '@nestjs/common'; +import { Request } from 'express'; +import { readFileSync } from 'node:fs'; + +interface Rating { + score: number; + count: number; +} + +interface DatasetItem { + id: number; + name: string; + category: string; + price: number; + quantity: number; + active: boolean; + tags: string[]; + rating: Rating; +} + +let dataset: DatasetItem[] = []; +try { + dataset = JSON.parse(readFileSync(process.env.DATASET_PATH || '/data/dataset.json', 'utf8')); +} catch { + dataset = []; +} + +function sumQuery(query: Record): number { + let sum = 0; + for (const value of Object.values(query)) { + const n = parseInt(String(value), 10); + if (!Number.isNaN(n)) sum += n; + } + return sum; +} + +// The default body parsers are disabled in main.ts, so the POST endpoints read +// the raw stream: they only sum or count what arrives. +function readBody(req: Request): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + req.on('data', (chunk: Buffer) => chunks.push(chunk)); + req.on('end', () => resolve(Buffer.concat(chunks))); + req.on('error', reject); + }); +} + +function countBody(req: Request): Promise { + return new Promise((resolve, reject) => { + let size = 0; + req.on('data', (chunk: Buffer) => { + size += chunk.length; + }); + req.on('end', () => resolve(size)); + req.on('error', reject); + }); +} + +@Controller() +export class AppController { + @Get('pipeline') + @Header('Content-Type', 'text/plain') + pipeline(): string { + return 'ok'; + } + + @Get('baseline11') + @Header('Content-Type', 'text/plain') + baselineGet(@Query() query: Record): string { + return String(sumQuery(query)); + } + + @Post('baseline11') + @Header('Content-Type', 'text/plain') + async baselinePost( + @Query() query: Record, + @Req() req: Request, + ): Promise { + let total = sumQuery(query); + const n = parseInt((await readBody(req)).toString().trim(), 10); + if (!Number.isNaN(n)) total += n; + return String(total); + } + + @Get('json/:count') + jsonItems(@Param('count') rawCount: string, @Query('m') rawM?: string) { + let count = parseInt(rawCount, 10) || 0; + if (count < 0) count = 0; + if (count > dataset.length) count = dataset.length; + const m = parseInt(rawM ?? '1', 10) || 1; + + const items = dataset.slice(0, count).map((item) => ({ + ...item, + total: item.price * item.quantity * m, + })); + return { items, count }; + } + + @Post('upload') + @Header('Content-Type', 'text/plain') + async upload(@Req() req: Request): Promise { + return String(await countBody(req)); + } +} diff --git a/frameworks/nestjs/src/app.module.ts b/frameworks/nestjs/src/app.module.ts new file mode 100644 index 000000000..848d4aaa7 --- /dev/null +++ b/frameworks/nestjs/src/app.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; +import { AppController } from './app.controller'; + +@Module({ + controllers: [AppController], +}) +export class AppModule {} diff --git a/frameworks/nestjs/src/main.ts b/frameworks/nestjs/src/main.ts new file mode 100644 index 000000000..1be7556ac --- /dev/null +++ b/frameworks/nestjs/src/main.ts @@ -0,0 +1,35 @@ +import 'reflect-metadata'; +import { NestFactory } from '@nestjs/core'; +import cluster from 'node:cluster'; +import { readFileSync } from 'node:fs'; +import os from 'node:os'; +import compression from 'compression'; +import { AppModule } from './app.module'; + +function getCPUCount(): number { + try { + const max = readFileSync('/sys/fs/cgroup/cpu.max', 'utf8').trim(); + const [quota, period] = max.split(' '); + if (quota !== 'max') { + const cgroup = Math.floor(Number(quota) / Number(period)); + if (cgroup >= 1) return cgroup; + } + } catch { + // no cgroup limit, fall back to the host CPUs + } + return os.availableParallelism ? os.availableParallelism() : os.cpus().length; +} + +async function bootstrap() { + // bodyParser off: the POST endpoints read the raw stream themselves. + const app = await NestFactory.create(AppModule, { bodyParser: false, logger: false }); + app.use(compression()); + await app.listen(8080); +} + +if (cluster.isPrimary) { + const workers = getCPUCount(); + for (let i = 0; i < workers; i++) cluster.fork(); +} else { + bootstrap(); +} diff --git a/frameworks/nestjs/tsconfig.json b/frameworks/nestjs/tsconfig.json new file mode 100644 index 000000000..0d7efb486 --- /dev/null +++ b/frameworks/nestjs/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2023", + "moduleResolution": "node", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "esModuleInterop": true, + "skipLibCheck": true, + "strict": true, + "outDir": "./dist" + }, + "include": ["src/**/*"] +} diff --git a/site/data/frameworks.json b/site/data/frameworks.json index 7f879a980..35d45fb1b 100644 --- a/site/data/frameworks.json +++ b/site/data/frameworks.json @@ -602,6 +602,14 @@ } ] }, + "nestjs": { + "dir": "nestjs", + "description": "NestJS 11 on the Express platform adapter, default configuration. Routing and parameter decorators through the Nest API, controller return values serialized by the framework, gzip through the compression middleware the Nest docs recommend.", + "repo": "https://github.com/nestjs/nest", + "type": "flagship", + "engine": "nodehttp", + "mode": "standard" + }, "ngx-php": { "dir": "ngx-php", "description": "Embedded PHP scripting language module for nginx.", diff --git a/site/data/results/nestjs.json b/site/data/results/nestjs.json new file mode 100644 index 000000000..7936502ef --- /dev/null +++ b/site/data/results/nestjs.json @@ -0,0 +1,243 @@ +{ + "framework": "nestjs", + "results": { + "baseline-4096": { + "framework": "nestjs", + "language": "TS", + "rps": 413334, + "avg_latency": "8.06ms", + "p99_latency": "22.10ms", + "cpu": "6414.6%", + "memory": "9.3GiB", + "connections": 4096, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "100.61MB/s", + "input_bw": "31.93MB/s", + "reconnects": 0, + "status_2xx": 2066673, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "baseline-512": { + "framework": "nestjs", + "language": "TS", + "rps": 430068, + "avg_latency": "1.19ms", + "p99_latency": "5.41ms", + "cpu": "6519.0%", + "memory": "8.6GiB", + "connections": 512, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "104.60MB/s", + "input_bw": "33.22MB/s", + "reconnects": 0, + "status_2xx": 2150340, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "json-4096": { + "framework": "nestjs", + "language": "TS", + "rps": 366074, + "avg_latency": "4.15ms", + "p99_latency": "31.10ms", + "cpu": "6354.0%", + "memory": "8.8GiB", + "connections": 4096, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "1.29GB/s", + "input_bw": "17.46MB/s", + "reconnects": 72871, + "status_2xx": 1830371, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "json-comp-16384": { + "framework": "nestjs", + "language": "TS", + "rps": 52963, + "avg_latency": "91.18ms", + "p99_latency": "751.70ms", + "cpu": "6474.9%", + "memory": "8.8GiB", + "connections": 16384, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "77.07MB/s", + "input_bw": "3.94MB/s", + "reconnects": 8076, + "status_2xx": 264819, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "json-comp-4096": { + "framework": "nestjs", + "language": "TS", + "rps": 50402, + "avg_latency": "62.38ms", + "p99_latency": "273.30ms", + "cpu": "6471.3%", + "memory": "8.0GiB", + "connections": 4096, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "73.37MB/s", + "input_bw": "3.75MB/s", + "reconnects": 8376, + "status_2xx": 252011, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "json-comp-512": { + "framework": "nestjs", + "language": "TS", + "rps": 60658, + "avg_latency": "8.44ms", + "p99_latency": "39.10ms", + "cpu": "6450.9%", + "memory": "7.8GiB", + "connections": 512, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "88.32MB/s", + "input_bw": "4.51MB/s", + "reconnects": 12045, + "status_2xx": 303290, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "limited-conn-4096": { + "framework": "nestjs", + "language": "TS", + "rps": 256857, + "avg_latency": "2.73ms", + "p99_latency": "21.40ms", + "cpu": "4495.7%", + "memory": "8.7GiB", + "connections": 4096, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "62.52MB/s", + "input_bw": "19.84MB/s", + "reconnects": 128426, + "status_2xx": 1284288, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "limited-conn-512": { + "framework": "nestjs", + "language": "TS", + "rps": 244377, + "avg_latency": "2.09ms", + "p99_latency": "21.10ms", + "cpu": "4466.9%", + "memory": "8.7GiB", + "connections": 512, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "59.49MB/s", + "input_bw": "18.88MB/s", + "reconnects": 122192, + "status_2xx": 1221885, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "pipelined-4096": { + "framework": "nestjs", + "language": "TS", + "rps": 880666, + "avg_latency": "59.89ms", + "p99_latency": "185.10ms", + "cpu": "6533.9%", + "memory": "9.4GiB", + "connections": 4096, + "threads": 64, + "duration": "5s", + "pipeline": 16, + "bandwidth": "211.57MB/s", + "reconnects": 0, + "status_2xx": 4403333, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "pipelined-512": { + "framework": "nestjs", + "language": "TS", + "rps": 884996, + "avg_latency": "9.27ms", + "p99_latency": "12.80ms", + "cpu": "6613.7%", + "memory": "9.3GiB", + "connections": 512, + "threads": 64, + "duration": "5s", + "pipeline": 16, + "bandwidth": "212.61MB/s", + "reconnects": 0, + "status_2xx": 4424981, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "upload-256": { + "framework": "nestjs", + "language": "TS", + "rps": 1855, + "avg_latency": "134.98ms", + "p99_latency": "646.90ms", + "cpu": "6531.2%", + "memory": "7.4GiB", + "connections": 256, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "474.79KB/s", + "input_bw": "14.71GB/s", + "reconnects": 1814, + "status_2xx": 9276, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "upload-32": { + "framework": "nestjs", + "language": "TS", + "rps": 1780, + "avg_latency": "17.95ms", + "p99_latency": "63.10ms", + "cpu": "5912.6%", + "memory": "6.7GiB", + "connections": 32, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "455.66KB/s", + "input_bw": "14.12GB/s", + "reconnects": 1784, + "status_2xx": 8900, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + } + } +} diff --git a/site/static/logs/baseline/4096/nestjs.log b/site/static/logs/baseline/4096/nestjs.log new file mode 100644 index 000000000..e69de29bb diff --git a/site/static/logs/baseline/512/nestjs.log b/site/static/logs/baseline/512/nestjs.log new file mode 100644 index 000000000..e69de29bb diff --git a/site/static/logs/json-comp/16384/nestjs.log b/site/static/logs/json-comp/16384/nestjs.log new file mode 100644 index 000000000..e69de29bb diff --git a/site/static/logs/json-comp/4096/nestjs.log b/site/static/logs/json-comp/4096/nestjs.log new file mode 100644 index 000000000..e69de29bb diff --git a/site/static/logs/json-comp/512/nestjs.log b/site/static/logs/json-comp/512/nestjs.log new file mode 100644 index 000000000..e69de29bb diff --git a/site/static/logs/json/4096/nestjs.log b/site/static/logs/json/4096/nestjs.log new file mode 100644 index 000000000..e69de29bb diff --git a/site/static/logs/limited-conn/4096/nestjs.log b/site/static/logs/limited-conn/4096/nestjs.log new file mode 100644 index 000000000..e69de29bb diff --git a/site/static/logs/limited-conn/512/nestjs.log b/site/static/logs/limited-conn/512/nestjs.log new file mode 100644 index 000000000..e69de29bb diff --git a/site/static/logs/pipelined/4096/nestjs.log b/site/static/logs/pipelined/4096/nestjs.log new file mode 100644 index 000000000..e69de29bb diff --git a/site/static/logs/pipelined/512/nestjs.log b/site/static/logs/pipelined/512/nestjs.log new file mode 100644 index 000000000..e69de29bb diff --git a/site/static/logs/upload/256/nestjs.log b/site/static/logs/upload/256/nestjs.log new file mode 100644 index 000000000..e69de29bb diff --git a/site/static/logs/upload/32/nestjs.log b/site/static/logs/upload/32/nestjs.log new file mode 100644 index 000000000..e69de29bb