mirror of
https://github.com/nestjs/nest.git
synced 2026-02-21 23:11:44 +00:00
feat: update benchmarks
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
@@ -1,7 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const fastify = require('fastify')();
|
||||
fastify.get('/', async (req, reply) => reply.send('Hello world'));
|
||||
fastify.listen({
|
||||
port: 3000
|
||||
});
|
||||
@@ -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
|
||||
@@ -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
|
||||
3
benchmarks/nest/app.controller.d.ts
vendored
3
benchmarks/nest/app.controller.d.ts
vendored
@@ -1,3 +0,0 @@
|
||||
export declare class AppController {
|
||||
root(): string;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"}
|
||||
1
benchmarks/nest/app.module.d.ts
vendored
1
benchmarks/nest/app.module.d.ts
vendored
@@ -1 +0,0 @@
|
||||
export declare class AppModule {}
|
||||
@@ -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
|
||||
@@ -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"}
|
||||
1
benchmarks/nest/main.d.ts
vendored
1
benchmarks/nest/main.d.ts
vendored
@@ -1 +0,0 @@
|
||||
export {};
|
||||
@@ -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"}
|
||||
@@ -38,14 +38,14 @@ 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',
|
||||
|
||||
@@ -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) + '%';
|
||||
}
|
||||
@@ -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
2632
tools/benchmarks/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
tools/benchmarks/package.json
Normal file
18
tools/benchmarks/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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%
|
||||
32
tools/benchmarks/src/autocannon/run.ts
Normal file
32
tools/benchmarks/src/autocannon/run.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
9
tools/benchmarks/src/frameworks/express.ts
Normal file
9
tools/benchmarks/src/frameworks/express.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import express from 'express';
|
||||
|
||||
const app = express();
|
||||
|
||||
app.get('/', async (_, res) => {
|
||||
res.send('Hello world');
|
||||
});
|
||||
|
||||
app.listen(3000);
|
||||
14
tools/benchmarks/src/frameworks/fastify.ts
Normal file
14
tools/benchmarks/src/frameworks/fastify.ts
Normal 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);
|
||||
});
|
||||
13
tools/benchmarks/src/frameworks/nest-express.ts
Normal file
13
tools/benchmarks/src/frameworks/nest-express.ts
Normal 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);
|
||||
});
|
||||
13
tools/benchmarks/src/frameworks/nest-fastify.ts
Normal file
13
tools/benchmarks/src/frameworks/nest-fastify.ts
Normal 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);
|
||||
});
|
||||
9
tools/benchmarks/src/frameworks/nest/app.controller.ts
Normal file
9
tools/benchmarks/src/frameworks/nest/app.controller.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
|
||||
@Controller('/')
|
||||
export class AppController {
|
||||
@Get()
|
||||
root() {
|
||||
return 'Hello world!';
|
||||
}
|
||||
}
|
||||
8
tools/benchmarks/src/frameworks/nest/app.module.ts
Normal file
8
tools/benchmarks/src/frameworks/nest/app.module.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AppController } from './app.controller';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
controllers: [AppController],
|
||||
})
|
||||
export class AppModule {}
|
||||
349
tools/benchmarks/src/main.ts
Normal file
349
tools/benchmarks/src/main.ts
Normal 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);
|
||||
});
|
||||
21
tools/benchmarks/tsconfig.json
Normal file
21
tools/benchmarks/tsconfig.json
Normal 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,
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user