Legion commander! Even the best-trained Roman army needs regular inspections - checking the soldiers' condition, the state of armament, and strategy effectiveness. In NestJS, profiling and benchmarking is the same kind of inspection - you diagnose where your application is losing time, memory, and resources.
autocannon is a load testing tool - like sending thousands of messengers simultaneously to check the throughput of the Empire's roads:1// benchmark.ts - skrypt benchmarkowy
2// Instalacja: npm install -g autocannon
3
4// Running from the terminal:
5// autocannon -c 100 -d 30 -p 10 http://localhost:3000/legions
6//
7// -c 100 = 100 parallel connections (100 messengers at once)
8// -d 30 = test lasts 30 seconds
9// -p 10 = 10 pipelined requests per connection
10
11// Result:
12// Stat 2.5% 50% 97.5% 99% Avg Stdev Max
13// Latency 12ms 25ms 89ms 120ms 32.4ms 18.2ms 250ms
14// Req/Sec 850 1200 1450 1500 1180 185 1520
15//
16// 35,400 requests in 30s, 12.5 MB read
17
18// Programmatic use of autocannon in tests
19import * as autocannon from 'autocannon';
20
21async function runBenchmark() {
22 const result = await autocannon({
23 url: 'http://localhost:3000/legions',
24 connections: 100, // 100 parallel connections
25 duration: 10, // 10 seconds of testing
26 pipelining: 10, // 10 requests in pipeline
27 headers: {
28 'Authorization': 'Bearer test-token',
29 },
30 });
31
32 console.log('=== Benchmark Results ===');
33 console.log('Requests/sec:', result.requests.average);
34 console.log('Latency avg:', result.latency.average, 'ms');
35 console.log('Latency p99:', result.latency.p99, 'ms');
36 console.log('Throughput:', result.throughput.average, 'bytes/sec');
37 console.log('Errors:', result.errors);
38 console.log('Timeouts:', result.timeouts);
39
40 // Success criteria
41 if (result.latency.p99 > 200) {
42 console.warn('WARNING: p99 latency exceeds 200ms!');
43 }
44 if (result.errors > 0) {
45 console.error('ERROR: Errors occurred during the test!');
46 }
47}
48
49runBenchmark();Node.js has a built-in profiler that allows you to analyze what the CPU is doing while the application is running:
1// Starting NestJS with the profiler:
2// node --inspect dist/main.js
3//
4// Then open Chrome: chrome://inspect
5// Click "Open dedicated DevTools for Node"
6//
7// In the "Performance" tab:
8// 1. Click "Record"
9// 2. Perform operations on the API (send requests)
10// 3. Click "Stop"
11// 4. Analyze the flame graph
12
13// Programmatic CPU profiling
14import { Session } from 'inspector';
15import { writeFileSync } from 'fs';
16
17class CpuProfiler {
18 private session: Session;
19
20 constructor() {
21 this.session = new Session();
22 this.session.connect();
23 }
24
25 async startProfiling(): Promise<void> {
26 await new Promise<void>((resolve) => {
27 this.session.post('Profiler.enable', () => {
28 this.session.post('Profiler.start', () => {
29 console.log('CPU profiling started...');
30 resolve();
31 });
32 });
33 });
34 }
35
36 async stopProfiling(filename: string): Promise<void> {
37 return new Promise((resolve) => {
38 this.session.post('Profiler.stop', (err, { profile }) => {
39 if (!err) {
40 writeFileSync(filename, JSON.stringify(profile));
41 console.log('CPU profile saved to: ' + filename);
42 }
43 resolve();
44 });
45 });
46 }
47}
48
49// Usage:
50// const profiler = new CpuProfiler();
51// await profiler.startProfiling();
52// ... perform operations ...
53// await profiler.stopProfiling('cpu-profile.cpuprofile');
54// Open the file in Chrome DevTools -> PerformanceFlame graphs visually show which functions take the most CPU time - like a heat map of a battle showing where the heaviest fighting is:
1// Generating a flame graph with clinic.js
2// npm install -g clinic
3
4// 1. clinic doctor - general diagnosis
5// clinic doctor -- node dist/main.js
6// (in a second terminal: autocannon -c 100 -d 10 http://localhost:3000/legions)
7// Ctrl+C -> opens HTML report
8
9// 2. clinic flame - flame graph
10// clinic flame -- node dist/main.js
11// (load the application as above)
12// Result: interaktywny flame graph
13
14// 3. clinic bubbleprof - async operations analysis
15// clinic bubbleprof -- node dist/main.js
16// Result: visualization of async delays (e.g., DB queries)
17
18// Interpreting the Flame Graph:
19// - Block width = CPU time (wider = longer)
20// - Height = call stack depth
21// - Red color = hot path (lots of time)
22// - Look for wide blocks at the bottom - those are bottlenecks!Heap snapshots are memory dumps of the application - like a census of the Empire showing who occupies how much space:
1// heap-snapshot.service.ts
2import { Injectable, Logger } from '@nestjs/common';
3import * as v8 from 'v8';
4import { writeFileSync } from 'fs';
5
6@Injectable()
7export class HeapSnapshotService {
8 private readonly logger = new Logger(HeapSnapshotService.name);
9
10 // Take a memory snapshot
11 takeSnapshot(filename?: string): string {
12 const snapshotFile = filename ||
13 'heap-' + new Date().toISOString().replace(/[:.]/g, '-') + '.heapsnapshot';
14
15 const snapshotStream = v8.writeHeapSnapshot(snapshotFile);
16 this.logger.log('Heap snapshot saved: ' + snapshotStream);
17
18 return snapshotStream;
19 }
20
21 // Get heap statistics
22 getHeapStatistics() {
23 const stats = v8.getHeapStatistics();
24 return {
25 totalHeapSize: Math.round(stats.total_heap_size / 1024 / 1024) + ' MB',
26 usedHeapSize: Math.round(stats.used_heap_size / 1024 / 1024) + ' MB',
27 heapSizeLimit: Math.round(stats.heap_size_limit / 1024 / 1024) + ' MB',
28 mallocedMemory: Math.round(stats.malloced_memory / 1024 / 1024) + ' MB',
29 usagePercent: ((stats.used_heap_size / stats.heap_size_limit) * 100).toFixed(1) + '%',
30 };
31 }
32
33 // Comparing two snapshots to detect memory leaks
34 compareSnapshots() {
35 const before = v8.getHeapStatistics();
36 // ... perform operations ...
37 const after = v8.getHeapStatistics();
38
39 const diff = {
40 heapGrowth: after.used_heap_size - before.used_heap_size,
41 objectGrowth: after.number_of_native_contexts - before.number_of_native_contexts,
42 };
43
44 if (diff.heapGrowth > 10 * 1024 * 1024) { // > 10MB growth
45 this.logger.warn('Potential memory leak! Heap grew by ' +
46 Math.round(diff.heapGrowth / 1024 / 1024) + ' MB');
47 }
48
49 return diff;
50 }
51}Here is the complete diagnostic workflow used by an experienced Empire engineer:
1// performance-diagnostic.service.ts
2import { Injectable, Logger } from '@nestjs/common';
3
4@Injectable()
5export class PerformanceDiagnosticService {
6 private readonly logger = new Logger('PerformanceDiagnostic');
7
8 // Step 1: Measure endpoint times
9 async measureEndpoint(name: string, operation: () => Promise<any>) {
10 const start = process.hrtime.bigint();
11 const result = await operation();
12 const end = process.hrtime.bigint();
13 const durationMs = Number(end - start) / 1_000_000;
14
15 this.logger.log(name + ': ' + durationMs.toFixed(2) + 'ms');
16
17 if (durationMs > 200) {
18 this.logger.warn(name + ' exceeds 200ms - needs optimization!');
19 }
20
21 return { result, durationMs };
22 }
23
24 // Step 2: Check memory usage
25 checkMemoryUsage() {
26 const usage = process.memoryUsage();
27 return {
28 rss: Math.round(usage.rss / 1024 / 1024) + ' MB',
29 heapUsed: Math.round(usage.heapUsed / 1024 / 1024) + ' MB',
30 heapTotal: Math.round(usage.heapTotal / 1024 / 1024) + ' MB',
31 external: Math.round(usage.external / 1024 / 1024) + ' MB',
32 };
33 }
34
35 // Step 3: Monitor event loop delay
36 measureEventLoopDelay(): Promise<number> {
37 return new Promise((resolve) => {
38 const start = Date.now();
39 setImmediate(() => {
40 const delay = Date.now() - start;
41 if (delay > 10) {
42 this.logger.warn('Event loop delay: ' + delay + 'ms');
43 }
44 resolve(delay);
45 });
46 });
47 }
48
49 // Step 4: Full diagnostic report
50 async generateDiagnosticReport() {
51 const memory = this.checkMemoryUsage();
52 const eventLoopDelay = await this.measureEventLoopDelay();
53 const uptime = process.uptime();
54
55 return {
56 timestamp: new Date().toISOString(),
57 uptime: Math.round(uptime) + 's',
58 memory,
59 eventLoopDelay: eventLoopDelay + 'ms',
60 nodeVersion: process.version,
61 platform: process.platform,
62 };
63 }
64}Profiling and benchmarking are the eyes and ears of the Empire on the performance battlefield. Just as a wise strategist analyzes the battlefield before attacking, a good engineer profiles the application before optimizing. Remember Praetor Augustus's rule: "Don't optimize what you haven't measured!"