feat: update benchmarks

This commit is contained in:
Mykhailo Skrypskyi
2026-02-01 11:35:03 +02:00
parent 782e0715f0
commit 3cb3c2d6ad
29 changed files with 3125 additions and 465 deletions

View File

@@ -1,13 +0,0 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")"
# based on https://medium.com/@felipedutratine/intelligent-benchmark-with-wrk-163986c1587f
cd /tmp/
sudo apt-get install build-essential libssl-dev git -y
git clone --depth=1 https://github.com/wg/wrk.git wrk
cd wrk
sudo make
# move the executable to somewhere in your PATH, ex:
sudo cp wrk /usr/local/bin

View File

@@ -1,88 +0,0 @@
-----------------------
express
-----------------------
Running 10s test @ http://localhost:3000
1024 connections
┌─────────┬───────┬───────┬───────┬────────┬──────────┬──────────┬────────┐
│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │
├─────────┼───────┼───────┼───────┼────────┼──────────┼──────────┼────────┤
│ Latency │ 55 ms │ 58 ms │ 91 ms │ 138 ms │ 61.88 ms │ 23.95 ms │ 747 ms │
└─────────┴───────┴───────┴───────┴────────┴──────────┴──────────┴────────┘
┌───────────┬─────────┬─────────┬─────────┬─────────┬──────────┬─────────┬─────────┐
│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │
├───────────┼─────────┼─────────┼─────────┼─────────┼──────────┼─────────┼─────────┤
│ Req/Sec │ 8407 │ 8407 │ 17407 │ 17743 │ 16454.41 │ 2716.94 │ 8402 │
├───────────┼─────────┼─────────┼─────────┼─────────┼──────────┼─────────┼─────────┤
│ Bytes/Sec │ 1.81 MB │ 1.81 MB │ 3.74 MB │ 3.81 MB │ 3.54 MB │ 584 kB │ 1.81 MB │
└───────────┴─────────┴─────────┴─────────┴─────────┴──────────┴─────────┴─────────┘
Req/Bytes counts sampled once per second.
165k requests in 10.17s, 35.4 MB read
-----------------------
nest (with "@nestjs/platform-express")
-----------------------
Running 10s test @ http://localhost:3000
1024 connections
┌─────────┬───────┬───────┬───────┬───────┬──────────┬──────────┬────────┐
│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │
├─────────┼───────┼───────┼───────┼───────┼──────────┼──────────┼────────┤
│ Latency │ 61 ms │ 64 ms │ 71 ms │ 94 ms │ 65.44 ms │ 17.35 ms │ 325 ms │
└─────────┴───────┴───────┴───────┴───────┴──────────┴──────────┴────────┘
┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬────────┬─────────┐
│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼────────┼─────────┤
│ Req/Sec │ 14183 │ 14183 │ 15767 │ 15991 │ 15640 │ 501.13 │ 14182 │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼────────┼─────────┤
│ Bytes/Sec │ 3.06 MB │ 3.06 MB │ 3.41 MB │ 3.45 MB │ 3.38 MB │ 108 kB │ 3.06 MB │
└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴────────┴─────────┘
Req/Bytes counts sampled once per second.
156k requests in 10.24s, 33.8 MB read
-----------------------
fastify
-----------------------
Running 10s test @ http://localhost:3000
1024 connections
┌─────────┬───────┬───────┬───────┬───────┬──────────┬──────────┬─────────┐
│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │
├─────────┼───────┼───────┼───────┼───────┼──────────┼──────────┼─────────┤
│ Latency │ 27 ms │ 30 ms │ 39 ms │ 78 ms │ 31.62 ms │ 26.59 ms │ 1232 ms │
└─────────┴───────┴───────┴───────┴───────┴──────────┴──────────┴─────────┘
┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐
│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│ Req/Sec │ 19935 │ 19935 │ 33247 │ 34111 │ 32030.4 │ 4103.84 │ 19931 │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│ Bytes/Sec │ 3.03 MB │ 3.03 MB │ 5.05 MB │ 5.19 MB │ 4.87 MB │ 624 kB │ 3.03 MB │
└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘
Req/Bytes counts sampled once per second.
320k requests in 10.18s, 48.7 MB read
-----------------------
nest (with "@nestjs/platform-fastify")
-----------------------
Running 10s test @ http://localhost:3000
1024 connections
┌─────────┬───────┬───────┬───────┬───────┬──────────┬──────────┬────────┐
│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │
├─────────┼───────┼───────┼───────┼───────┼──────────┼──────────┼────────┤
│ Latency │ 31 ms │ 33 ms │ 38 ms │ 52 ms │ 34.41 ms │ 11.73 ms │ 245 ms │
└─────────┴───────┴───────┴───────┴───────┴──────────┴──────────┴────────┘
┌───────────┬─────────┬─────────┬────────┬─────────┬─────────┬─────────┬─────────┐
│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │
├───────────┼─────────┼─────────┼────────┼─────────┼─────────┼─────────┼─────────┤
│ Req/Sec │ 24911 │ 24911 │ 30031 │ 30335 │ 29470.4 │ 1564.48 │ 24907 │
├───────────┼─────────┼─────────┼────────┼─────────┼─────────┼─────────┼─────────┤
│ Bytes/Sec │ 3.81 MB │ 3.81 MB │ 4.6 MB │ 4.64 MB │ 4.51 MB │ 239 kB │ 3.81 MB │
└───────────┴─────────┴─────────┴────────┴─────────┴─────────┴─────────┴─────────┘
Req/Bytes counts sampled once per second.
295k requests in 10.17s, 45.1 MB read

View File

@@ -1,7 +0,0 @@
'use strict';
const express = require('express');
const app = express();
app.get('/', async (req, res) => res.send('Hello world'));
app.listen(3000);

View File

@@ -1,7 +0,0 @@
'use strict';
const fastify = require('fastify')();
fastify.get('/', async (req, reply) => reply.send('Hello world'));
fastify.listen({
port: 3000
});

View File

@@ -1,14 +0,0 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
const core_1 = require('@nestjs/core');
const fastify_platform_1 = require('@nestjs/platform-fastify');
const app_module_1 = require('./nest/app.module');
core_1.NestFactory.create(
app_module_1.AppModule,
new fastify_platform_1.FastifyAdapter(),
{
logger: false,
bodyParser: false,
},
).then(app => app.listen(3000));
//# sourceMappingURL=main.js.map

View File

@@ -1,9 +0,0 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
const core_1 = require('@nestjs/core');
const app_module_1 = require('./nest/app.module');
core_1.NestFactory.create(app_module_1.AppModule, {
logger: false,
bodyParser: false,
}).then(app => app.listen(3000));
//# sourceMappingURL=main.js.map

View File

@@ -1,3 +0,0 @@
export declare class AppController {
root(): string;
}

View File

@@ -1,47 +0,0 @@
'use strict';
var __decorate =
(this && this.__decorate) ||
function(decorators, target, key, desc) {
var c = arguments.length,
r =
c < 3
? target
: desc === null
? (desc = Object.getOwnPropertyDescriptor(target, key))
: desc,
d;
if (typeof Reflect === 'object' && typeof Reflect.decorate === 'function')
r = Reflect.decorate(decorators, target, key, desc);
else
for (var i = decorators.length - 1; i >= 0; i--)
if ((d = decorators[i]))
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata =
(this && this.__metadata) ||
function(k, v) {
if (typeof Reflect === 'object' && typeof Reflect.metadata === 'function')
return Reflect.metadata(k, v);
};
Object.defineProperty(exports, '__esModule', { value: true });
const common_1 = require('@nestjs/common');
let AppController = class AppController {
root() {
return 'Hello world!';
}
};
__decorate(
[
common_1.Get(),
__metadata('design:type', Function),
__metadata('design:paramtypes', []),
__metadata('design:returntype', String),
],
AppController.prototype,
'root',
null,
);
AppController = __decorate([common_1.Controller()], AppController);
exports.AppController = AppController;
//# sourceMappingURL=app.controller.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"app.controller.js","sourceRoot":"","sources":["../src/app.controller.ts"],"names":[],"mappings":";;;;;;;;;;;AAAA,2CAAiD;AAGjD,IAAa,aAAa,GAA1B,MAAa,aAAa;IAExB,IAAI;QACF,OAAO,cAAc,CAAA;IACvB,CAAC;CACF,CAAA;AAHC;IADC,YAAG,EAAE;;;;yCAGL;AAJU,aAAa;IADzB,mBAAU,EAAE;GACA,aAAa,CAKzB;AALY,sCAAa"}

View File

@@ -1 +0,0 @@
export declare class AppModule {}

View File

@@ -1,20 +0,0 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
const common_1 = require("@nestjs/common");
const app_controller_1 = require("./app.controller");
let AppModule = class AppModule {
};
AppModule = __decorate([
common_1.Module({
imports: [],
controllers: [app_controller_1.AppController],
})
], AppModule);
exports.AppModule = AppModule;
//# sourceMappingURL=app.module.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"app.module.js","sourceRoot":"","sources":["../src/app.module.ts"],"names":[],"mappings":";;;;;;;;AAAA,2CAAwC;AACxC,qDAAiD;AAMjD,IAAa,SAAS,GAAtB,MAAa,SAAS;CAAG,CAAA;AAAZ,SAAS;IAJrB,eAAM,CAAC;QACN,OAAO,EAAE,EAAE;QACX,WAAW,EAAE,CAAC,8BAAa,CAAC;KAC7B,CAAC;GACW,SAAS,CAAG;AAAZ,8BAAS"}

View File

@@ -1 +0,0 @@
export {};

View File

@@ -1 +0,0 @@
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";;AAAA,uCAA2C;AAC3C,6CAAyC;AAEzC,KAAK,UAAU,SAAS;IACtB,MAAM,GAAG,GAAG,MAAM,kBAAW,CAAC,MAAM,CAAC,sBAAS,CAAC,CAAC;IAChD,MAAM,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AACzB,CAAC;AACD,SAAS,EAAE,CAAC"}

View File

@@ -38,18 +38,18 @@ export default tseslint.config(
'@typescript-eslint/no-require-imports': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-non-null-asserted-optional-chain': 'warn',
"@typescript-eslint/no-misused-promises": [
"error",
'@typescript-eslint/no-misused-promises': [
'error',
{
"checksVoidReturn": false,
"checksConditionals": false
}
checksVoidReturn: false,
checksConditionals: false,
},
],
"@typescript-eslint/require-await": "off",
'@typescript-eslint/require-await': 'off',
'@typescript-eslint/prefer-promise-reject-errors': 'off',
'@typescript-eslint/no-base-to-string': 'off',
'@typescript-eslint/unbound-method': 'off',
'@typescript-eslint/only-throw-error': 'off',
},
},
);
);

View File

@@ -1,148 +0,0 @@
import { codechecks, CodeChecksReport } from '@codechecks/client';
import * as bytes from 'bytes';
import { Benchmarks, getBenchmarks, LIBS } from './get-benchmarks';
const markdownTable = require('markdown-table');
const benchmarksKey = 'nest/performance-benchmark';
export default async function checkBenchmarks() {
const currentBenchmarks = await getBenchmarks();
await codechecks.saveValue(benchmarksKey, currentBenchmarks);
if (!codechecks.isPr()) {
return;
}
const baselineBenchmarks =
await codechecks.getValue<Benchmarks>(benchmarksKey);
const report = getCodechecksReport(currentBenchmarks, baselineBenchmarks);
await codechecks.report(report);
}
function getCodechecksReport(
current: Benchmarks,
baseline: Benchmarks | undefined,
): CodeChecksReport {
const diff = getDiff(current, baseline);
const shortDescription = getShortDescription(baseline, diff);
const longDescription = getLongDescription(current, baseline, diff);
return {
name: 'Benchmarks',
status: 'success',
shortDescription,
longDescription,
};
}
function getShortDescription(
baseline: Benchmarks | undefined,
diff: BenchmarksDiff,
): string {
if (!baseline) {
return 'New benchmarks generated';
}
const avgDiff = getAverageDiff(diff);
if (avgDiff > 0) {
return `Performance improved by ${avgDiff.toFixed(
2,
)}% on average, good job!`;
}
if (avgDiff === 0) {
return `No changes in performance detected`;
}
if (avgDiff < 0) {
return `Performance decreased by ${avgDiff.toFixed(
2,
)}% on average, be careful!`;
}
}
function getLongDescription(
current: Benchmarks,
baseline: Benchmarks | undefined,
diff: BenchmarksDiff,
): string {
function printTableRow(id: string, label: string): string[] {
return [
label,
current[id].requestsPerSec.toFixed(0),
current[id].transferPerSec,
baseline ? formatPerc(diff[id].requestsPerSecDiff) : '-',
baseline ? formatPerc(diff[id].transferPerSecDiff) : '-',
];
}
const table = [
['', 'Req/sec', 'Trans/sec', 'Req/sec DIFF', 'Trans/sec DIFF'],
printTableRow('nest', 'Nest-Express'),
printTableRow('nest-fastify', 'Nest-Fastify'),
printTableRow('express', 'Express'),
printTableRow('fastify', 'Fastify'),
];
return markdownTable(table);
}
function getDiff(
current: Benchmarks,
baseline: Benchmarks | undefined,
): BenchmarksDiff {
const diff: BenchmarksDiff = {};
for (const l of LIBS) {
if (!baseline) {
diff[l] = undefined;
continue;
}
const currentValue = current[l];
const baselineValue = baseline[l];
diff[l] = {
requestsPerSecDiff: getRequestDiff(
currentValue.requestsPerSec,
baselineValue.requestsPerSec,
),
transferPerSecDiff: getTransferDiff(
currentValue.transferPerSec,
baselineValue.transferPerSec,
),
};
}
return diff;
}
function getTransferDiff(
currentTransfer: string,
baselineTransfer: string,
): number {
return 1 - bytes.parse(currentTransfer) / bytes.parse(baselineTransfer);
}
function getAverageDiff(diff: BenchmarksDiff) {
return (
(diff['nest'].transferPerSecDiff +
diff['nest'].requestsPerSecDiff +
diff['nest-fastify'].transferPerSecDiff +
diff['nest-fastify'].requestsPerSecDiff) /
4
);
}
function getRequestDiff(currentRequest: number, baselineRequest: number) {
return 1 - currentRequest / baselineRequest;
}
interface BenchmarkDiff {
transferPerSecDiff: number | undefined;
requestsPerSecDiff: number | undefined;
}
interface BenchmarksDiff {
[lib: string]: BenchmarkDiff;
}
function formatPerc(n: number) {
return (n > 0 ? '+' : '') + (n * 100).toFixed(2) + '%';
}

View File

@@ -1,77 +0,0 @@
import wrkPkg = require('wrk');
import { spawn } from 'child_process';
import { join } from 'path';
export interface Benchmarks {
[lib: string]: WrkResults;
}
const wrk = (options: any) =>
new Promise<WrkResults>((resolve, reject) =>
wrkPkg(options, (err: any, result: any) =>
err ? reject(err) : resolve(result),
),
);
const sleep = (time: number) =>
new Promise(resolve => setTimeout(resolve, time));
const BENCHMARK_PATH = join(__dirname, '../../benchmarks');
export const LIBS = ['express', 'fastify', 'nest', 'nest-fastify'];
async function runBenchmarkOfLib(lib: string): Promise<WrkResults> {
const libPath = join(BENCHMARK_PATH, `${lib}.js`);
const process = spawn('node', [libPath], {
detached: true,
});
process.stdout!.on('data', data => {
console.log(`stdout: ${data}`);
});
process.stderr!.on('data', data => {
console.log(`stderr: ${data}`);
});
process.unref();
await sleep(2000);
const result = await wrk({
threads: 8,
duration: '10s',
connections: 1024,
url: 'http://localhost:3000',
});
process.kill();
return result;
}
export async function getBenchmarks() {
const results: Benchmarks = {};
for await (const lib of LIBS) {
const result = await runBenchmarkOfLib(lib);
results[lib] = result;
}
return results;
}
interface WrkResults {
transferPerSec: string;
requestsPerSec: number;
connectErrors: string;
readErrors: string;
writeErrors: string;
timeoutErrors: string;
requestsTotal: number;
durationActual: string;
transferTotal: string;
latencyAvg: string;
latencyStdev: string;
latencyMax: string;
latencyStdevPerc: number;
rpsAvg: string;
rpsStdev: string;
rpsMax: string;
rpsStdevPerc: number;
}

2632
tools/benchmarks/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
{
"name": "@nestjs/benchmarks",
"version": "1.0.0",
"description": "Nest - modern, fast, powerful node.js web framework (@benchmarks)",
"license": "ISC",
"scripts": {
"benchmarks": "tsc && node dist/main.js"
},
"dependencies": {
"@nestjs/platform-express": "^11.1.12",
"@nestjs/platform-fastify": "^11.1.12"
},
"devDependencies": {
"@types/autocannon": "^7.12.7",
"autocannon": "^8.0.0",
"typescript": "^5.9.3"
}
}

View File

@@ -1,20 +0,0 @@
Short description (shown on main PR screen): Performance improved 0.13% on average.
Long description (after clicking details):
| | Req/sec | Trans/sec | Req/sec DIFF | Trans/sec DIFF | Req vs Express | Trans vs Fastify |
| -------------- | ------- | --------- | ------------ | -------------- | -------------- | ---------------- |
| NestJS-Express | 3.37MB | 16375.58 | +0.15% | +0.14% | 80.62% | 80.37% |
| NestJS-Fastify | 4.78MB | 32728.51 | +0.12% | +0.12 | 64.76% | 64.25% |
| Express | 4.18MB | 20374.59 | 0% | 0% | - | - |
| Fastify | 7.38MB | 50938 | 0% | 0% | - | - |
## Explanations:
Short description: average of all diffs for NestJS-\* so: `(0.15 + 0.14 + 0.12 + 0.12) / 4`
Long description:
`req/sec DIFF` and `Trans/sec DIFF` is in comparison to the baseline on target branch (master).
Req vs express is calculated as perf compared to NOT using nestjs so: 80.62% = (3.37/4.18) \* 100%

View File

@@ -0,0 +1,32 @@
import autocannon from 'autocannon';
export type RunOptions = autocannon.Options & {
/**
* When true, prints live progress to stdout via `autocannon.track(instance)`.
* Default: false (quiet)
*/
verbose?: boolean;
};
/**
* Runs an autocannon benchmark.
*
* By default this is quiet (no live table/progress). Pass `{ verbose: true }`
* to enable `autocannon.track(...)` output.
*/
export const run = (options: RunOptions) =>
new Promise<autocannon.Result>((resolve, reject) => {
const { verbose = false, ...autocannonOptions } = options;
const instance = autocannon(autocannonOptions, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
if (verbose) {
autocannon.track(instance);
}
});

View File

@@ -0,0 +1,9 @@
import express from 'express';
const app = express();
app.get('/', async (_, res) => {
res.send('Hello world');
});
app.listen(3000);

View File

@@ -0,0 +1,14 @@
import Fastify from 'fastify';
const fastify = Fastify({
logger: false,
});
fastify.get('/', async (_, reply) => reply.send('Hello world'));
fastify
.listen({
port: 3000,
})
.catch(err => {
console.error(err);
process.exit(1);
});

View File

@@ -0,0 +1,13 @@
import { NestFactory } from '@nestjs/core';
import { ExpressAdapter } from '@nestjs/platform-express';
import { AppModule } from './nest/app.module';
NestFactory.create(AppModule, new ExpressAdapter(), {
logger: false,
bodyParser: false,
})
.then(app => app.listen(3000))
.catch(error => {
console.error('Error starting Nest.js application:', error);
});

View File

@@ -0,0 +1,13 @@
import { NestFactory } from '@nestjs/core';
import { FastifyAdapter } from '@nestjs/platform-fastify';
import { AppModule } from './nest/app.module';
NestFactory.create(AppModule, new FastifyAdapter(), {
logger: false,
bodyParser: false,
})
.then(app => app.listen(3000))
.catch(error => {
console.error('Error starting Nest.js application:', error);
});

View File

@@ -0,0 +1,9 @@
import { Controller, Get } from '@nestjs/common';
@Controller('/')
export class AppController {
@Get()
root() {
return 'Hello world!';
}
}

View File

@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
@Module({
imports: [],
controllers: [AppController],
})
export class AppModule {}

View File

@@ -0,0 +1,349 @@
import { fork, ChildProcess } from 'node:child_process';
import { join } from 'node:path';
import { run } from './autocannon/run';
type Framework = 'express' | 'fastify' | 'nest-express' | 'nest-fastify';
type Args = {
connections: number;
duration: number;
pipelining: number;
verbose: boolean;
port: number;
path: string;
};
type BenchmarkSummary = {
requestsPerSecAvg: number;
latencyAvgMs: number;
latencyP99Ms: number;
throughputBytesPerSecAvg: number;
};
const DEFAULTS: Args = {
connections: 100,
duration: 10,
pipelining: 10,
verbose: false,
port: 3000,
path: '/',
};
function parseNumber(value: string | undefined, fallback: number): number {
if (!value) return fallback;
const n = Number(value);
return Number.isFinite(n) && n > 0 ? n : fallback;
}
function parseArgs(argv: string[]): Args {
const args: Args = { ...DEFAULTS };
const only: Framework[] = [];
for (let i = 0; i < argv.length; i++) {
const token = argv[i];
if (token === '--verbose' || token === '-v') {
args.verbose = true;
continue;
}
if (token === '--connections' || token === '-c') {
args.connections = parseNumber(argv[++i], args.connections);
continue;
}
if (token === '--duration' || token === '-d') {
args.duration = parseNumber(argv[++i], args.duration);
continue;
}
if (token === '--pipelining' || token === '-p') {
args.pipelining = parseNumber(argv[++i], args.pipelining);
continue;
}
if (token === '--port') {
args.port = parseNumber(argv[++i], args.port);
continue;
}
if (token === '--path') {
args.path = argv[++i] ?? args.path;
continue;
}
if (token === '--help' || token === '-h') {
printHelpAndExit(0);
}
}
return args;
}
function printHelpAndExit(code: number): never {
// Keep this simple so `npm run benchmarks` works without extra args.
// You can optionally narrow execution with `--framework` or `--only`.
// Example: `npm run benchmarks -- --framework fastify`
// Example: `npm run benchmarks -- --only express,fastify`
// Tuning: `--connections`, `--duration`, `--pipelining`, `--port`, `--path`
// Notes: The benchmark spawns the framework server as a child process and
// runs autocannon against it.
//
// eslint-disable-next-line no-console
console.log(
`
Usage:
npm run benchmarks [-- --verbose]
npm run benchmarks [-- --connections 100] [-- --duration 10] [-- --pipelining 10]
npm run benchmarks [-- --port 3000] [-- --path /]
Defaults:
verbose: ${DEFAULTS.verbose}
connections: ${DEFAULTS.connections}
duration: ${DEFAULTS.duration}
pipelining: ${DEFAULTS.pipelining}
port: ${DEFAULTS.port}
path: ${DEFAULTS.path}
`.trim(),
);
process.exit(code);
}
function frameworkEntry(framework: Framework): string {
return join(__dirname, '.', 'frameworks', framework);
}
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function waitForServer(url: string, timeoutMs = 5_000): Promise<void> {
const start = Date.now();
// Use global fetch if available (Node 18+). If not, just wait a bit.
const hasFetch = typeof (globalThis as any).fetch === 'function';
while (Date.now() - start < timeoutMs) {
try {
if (hasFetch) {
const res = await (globalThis as any).fetch(url, { method: 'GET' });
if (res && (res.status === 200 || res.status === 404)) return;
} else {
// best-effort fallback
await sleep(250);
return;
}
} catch {
// server not ready yet
}
await sleep(100);
}
throw new Error(`Server did not become ready in ${timeoutMs}ms: ${url}`);
}
function killChild(child: ChildProcess): void {
if (!child || child.killed) return;
// Try graceful termination first.
try {
child.kill('SIGTERM');
} catch {
// ignore
}
// Force kill if still alive shortly after.
setTimeout(() => {
try {
if (!child.killed) child.kill('SIGKILL');
} catch {
// ignore
}
}, 1_000).unref?.();
}
function toNumber(value: unknown, fallback = 0): number {
const n = typeof value === 'number' ? value : Number(value);
return Number.isFinite(n) ? n : fallback;
}
function summarizeResult(result: any): BenchmarkSummary {
return {
requestsPerSecAvg: toNumber(result?.requests?.average),
latencyAvgMs: toNumber(result?.latency?.average),
latencyP99Ms: toNumber(result?.latency?.p99),
throughputBytesPerSecAvg: toNumber(result?.throughput?.average),
};
}
function pctChange(newValue: number, oldValue: number): number | null {
if (
!Number.isFinite(newValue) ||
!Number.isFinite(oldValue) ||
oldValue === 0
)
return null;
return ((newValue - oldValue) / oldValue) * 100;
}
function fmtPct(p: number | null, digits = 1): string {
if (p == null) return 'n/a';
const sign = p >= 0 ? '+' : '';
return `${sign}${p.toFixed(digits)}%`;
}
function fmtNum(n: number, digits = 2): string {
if (!Number.isFinite(n)) return 'n/a';
return n.toFixed(digits);
}
function fmtBytesPerSec(n: number): string {
if (!Number.isFinite(n)) return 'n/a';
const abs = Math.abs(n);
if (abs >= 1024 ** 3) return `${(n / 1024 ** 3).toFixed(2)} GB/s`;
if (abs >= 1024 ** 2) return `${(n / 1024 ** 2).toFixed(2)} MB/s`;
if (abs >= 1024) return `${(n / 1024).toFixed(2)} KB/s`;
return `${n.toFixed(0)} B/s`;
}
function printComparison(
baseName: string,
base: BenchmarkSummary | undefined,
nestName: string,
nest: BenchmarkSummary | undefined,
): void {
// eslint-disable-next-line no-console
console.log(`\n--- Comparison: ${baseName} <-> ${nestName} ---`);
if (!base || !nest) {
// eslint-disable-next-line no-console
console.log(
`Missing results: ${baseName}=${base ? 'ok' : 'missing'} ${nestName}=${nest ? 'ok' : 'missing'}`,
);
return;
}
const rpsPct = pctChange(nest.requestsPerSecAvg, base.requestsPerSecAvg);
const latAvgPct = pctChange(nest.latencyAvgMs, base.latencyAvgMs);
const latP99Pct = pctChange(nest.latencyP99Ms, base.latencyP99Ms);
const thrPct = pctChange(
nest.throughputBytesPerSecAvg,
base.throughputBytesPerSecAvg,
);
// eslint-disable-next-line no-console
console.log(
`Requests/sec avg: ${fmtNum(base.requestsPerSecAvg)} -> ${fmtNum(nest.requestsPerSecAvg)} (${fmtPct(rpsPct)})`,
);
// eslint-disable-next-line no-console
console.log(
`Latency avg (ms): ${fmtNum(base.latencyAvgMs)} -> ${fmtNum(nest.latencyAvgMs)} (${fmtPct(latAvgPct)})`,
);
// eslint-disable-next-line no-console
console.log(
`Latency p99 (ms): ${fmtNum(base.latencyP99Ms)} -> ${fmtNum(nest.latencyP99Ms)} (${fmtPct(latP99Pct)})`,
);
// eslint-disable-next-line no-console
console.log(
`Throughput avg: ${fmtBytesPerSec(base.throughputBytesPerSecAvg)} -> ${fmtBytesPerSec(nest.throughputBytesPerSecAvg)} (${fmtPct(thrPct)})`,
);
}
async function runOne(
framework: Framework,
args: Args,
): Promise<BenchmarkSummary> {
const child = fork(frameworkEntry(framework), {
stdio: 'ignore',
});
const url = `http://localhost:${args.port}${args.path.startsWith('/') ? args.path : `/${args.path}`}`;
try {
await waitForServer(url);
// eslint-disable-next-line no-console
console.log(`\n=== ${framework.toUpperCase()} ===`);
// eslint-disable-next-line no-console
console.log(`Target: ${url}`);
// eslint-disable-next-line no-console
console.log(
`connections=${args.connections} duration=${args.duration} pipelining=${args.pipelining}`,
);
// eslint-disable-next-line no-console
console.log(`Warmup: ${framework.toUpperCase()}`);
await run({
url,
connections: args.connections,
duration: args.duration,
pipelining: args.pipelining,
verbose: args.verbose,
});
// eslint-disable-next-line no-console
console.log(`Warmup ended: ${framework.toUpperCase()}`);
const result = (await run({
url,
connections: args.connections,
duration: args.duration,
pipelining: args.pipelining,
verbose: args.verbose,
})) as any;
const summary = summarizeResult(result);
// Keep output readable and stable across autocannon versions.
// eslint-disable-next-line no-console
console.log(`Requests/sec: ${summary.requestsPerSecAvg}`);
// eslint-disable-next-line no-console
console.log(
`Latency (ms): avg=${summary.latencyAvgMs} p99=${summary.latencyP99Ms}`,
);
// eslint-disable-next-line no-console
console.log(`Throughput (B/s): avg=${summary.throughputBytesPerSecAvg}`);
return summary;
} finally {
killChild(child);
}
}
async function main(): Promise<void> {
const args = parseArgs(process.argv.slice(2));
const targets: Framework[] = [
'express',
'nest-express',
'fastify',
'nest-fastify',
];
const results: Partial<Record<Framework, BenchmarkSummary>> = {};
// Run sequentially to avoid port conflicts (both frameworks listen on the same port).
for (const fw of targets) {
results[fw] = await runOne(fw, args);
// small cooldown between runs
await sleep(250);
}
// Compare raw framework vs Nest adapter for same underlying HTTP server
printComparison(
'EXPRESS',
results['express'],
'NEST-EXPRESS',
results['nest-express'],
);
printComparison(
'FASTIFY',
results['fastify'],
'NEST-FASTIFY',
results['nest-fastify'],
);
}
main().catch(err => {
// eslint-disable-next-line no-console
console.error(err);
process.exit(1);
});

View File

@@ -0,0 +1,21 @@
{
"$schema": "https://www.schemastore.org/tsconfig",
"_version": "18.2.0",
"compilerOptions": {
"lib": ["es2023"],
"module": "node16",
"target": "es2022",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"moduleResolution": "node16",
"outDir": "dist",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"sourceMap": true,
},
}