We use cookies to enhance your experience on the site
CodeWorlds

Profiling and Benchmarking NestJS - diagnosing the Empire's power

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.

Benchmarking with autocannon

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 --inspect Profiling

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 -> Performance

Flame Graphs - visualizing hot spots

Flame 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 - memory analysis

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}

Identifying bottlenecks - practical workflow

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!"

Go to CodeWorlds