Day 07: Profiling, Optimization & Integration
Flame graphs that prove your optimization worked — then build the whole stack end to end.
⏱ ~2 hrs · theory 40 / capstone 80
Optimization without measurement is superstition. Today you learn to find the bottleneck before touching
code, then you assemble everything from Days 1–6 into a single working P2P node: DHT-style discovery, a Noise-style
handshake, length-prefixed binary framing, and verified message exchange. This is also your interview rehearsal —
the capstone is the system-design answer.
7.1 Measure first: the profiling toolkit
| Tool | What it shows | When to reach for it |
|---|---|---|
node --prof | V8 sampling profile → process with --prof-process | First look at where CPU time goes, zero deps |
node --cpu-prof | Writes a .cpuprofile for Chrome DevTools | Visual flame graph in a familiar UI |
clinic doctor | Diagnoses the kind of problem (CPU, I/O, GC, event-loop) | "It's slow and I don't know why" |
clinic flame / 0x | Flame graph of stack frames by time | Pinpoint the hot function |
clinic bubbleprof | Async operation flow & latency | Async/await and I/O latency chains |
perf_hooks | Precise in-process timing & event-loop delay | Targeted micro-measurements & CI gates |
Reading a flame graph
The x-axis is not time — it's the share of samples a function (and its callees) appeared in. Width
= cost. Height = call-stack depth. You hunt for wide plateaus: a single wide frame is a function
burning CPU directly; a wide frame with narrow children is the leaf where the time actually goes. Optimizing a narrow
frame, however slow it feels, moves nothing. Wide first, always.
The discipline
Measure → form a hypothesis about the widest frame → change one thing → measure again. If the flame graph didn't
get narrower where you expected, revert. Most "optimizations" made without this loop are noise or regressions.
7.2 The optimization order of operations
- Algorithm first. An
O(n²)→O(n)fix (the Day 6 concat trap) beats any micro-tuning. - Allocation second. Cut buffer churn in hot paths; reuse and slice instead of copy.
- Don't block the loop third. Move heavy CPU off-thread; keep per-event work bounded.
- Micro-optimize last, only with a flame graph pointing at the exact frame.
7.3 Lab — profile a deliberately slow path
Goal
Generate a CPU profile, read it, fix the hot spot, and confirm the win — the full measure-fix-measure loop.
// slow.js — a "message processor" with a hidden hot spot
const { performance } = require('perf_hooks');
function processMessages(n) {
let log = '';
for (let i = 0; i < n; i++) {
log += JSON.stringify({ id: i, ok: true }) + '\n'; // string += in a loop = O(n^2)-ish
}
return log.length;
}
const t = performance.now();
console.log('len:', processMessages(200000));
console.log('took:', (performance.now() - t).toFixed(0), 'ms');
# profile it
node --cpu-prof --cpu-prof-name=slow.cpuprofile slow.js
# open slow.cpuprofile in Chrome DevTools (Performance tab) -> see the wide frame
# or the zero-dependency route:
node --prof slow.js
node --prof-process isolate-*.log > profile.txt
# inspect the "Summary" and "Bottom up (heavy) profile" sections
The profile fingers string concatenation / stringify as the wide frame. The fix is the same shape as Day 6:
collect parts, join once.
// fast.js — same output, collect-then-join
function processMessages(n) {
const parts = [];
for (let i = 0; i < n; i++) parts.push('{"id":' + i + ',"ok":true}');
return parts.join('\n').length;
}
Re-profile and confirm the plateau shrank. That's the loop — never trust a fix you didn't re-measure.
7.4 Capstone — a complete P2P node
Now we wire the week together. This single program runs a tiny in-memory "DHT" to discover a peer's address, runs
an X25519 handshake to derive a shared key, then exchanges length-prefixed, AEAD-encrypted binary
messages — the whole Holepunch-shaped pipeline in one file, using only Node built-ins plus sodium-native.
What it integrates
- Day 1/3: a registry/rendezvous resolves a topic → peer address (DHT discovery, condensed).
- Day 5: X25519 key agreement → a shared AEAD key (the handshake).
- Day 4: length-prefixed varint framing over a UDP/stream boundary.
- Day 5: ChaCha20-Poly1305 AEAD on every message, with tamper rejection.
- Day 6: bounded per-event work, slice-not-copy framing, no concat-in-loop.
The capstone (run with node capstone.js)
// capstone.js — discovery + handshake + framed encrypted messaging, in-process
const sodium = require('sodium-native');
const EventEmitter = require('events');
// ---------- Day 4: varint + length-prefix framing ----------
function encodeVarint(v) {
const b = [];
while (v > 0x7f) { b.push((v & 0x7f) | 0x80); v = Math.floor(v / 128); }
b.push(v & 0x7f); return Buffer.from(b);
}
function decodeVarint(buf, off = 0) {
let r = 0, s = 0, p = off;
while (true) {
const x = buf[p++]; if (x === undefined) throw new Error('incomplete');
r += (x & 0x7f) * Math.pow(2, s); if ((x & 0x80) === 0) break; s += 7;
}
return { value: r, bytesRead: p - off };
}
class Framer extends EventEmitter {
constructor() { super(); this.buf = Buffer.alloc(0); }
push(chunk) {
this.buf = Buffer.concat([this.buf, chunk]);
while (this.buf.length) {
let h; try { h = decodeVarint(this.buf); } catch { return; }
const total = h.bytesRead + h.value;
if (this.buf.length < total) return;
this.emit('message', Buffer.from(this.buf.subarray(h.bytesRead, total)));
this.buf = this.buf.subarray(total);
}
}
}
const frame = (p) => Buffer.concat([encodeVarint(p.length), p]);
// ---------- Day 5: handshake + AEAD ----------
function kxKeypair() {
const pk = Buffer.alloc(sodium.crypto_kx_PUBLICKEYBYTES);
const sk = Buffer.alloc(sodium.crypto_kx_SECRETKEYBYTES);
sodium.crypto_kx_keypair(pk, sk); return { pk, sk };
}
const A_BYTES = sodium.crypto_aead_chacha20poly1305_ietf_ABYTES;
const N_BYTES = sodium.crypto_aead_chacha20poly1305_ietf_NPUBBYTES;
function seal(key, counter, msg) {
const nonce = Buffer.alloc(N_BYTES); nonce.writeUInt32LE(counter, 0); // per-message nonce
const ct = Buffer.alloc(msg.length + A_BYTES);
sodium.crypto_aead_chacha20poly1305_ietf_encrypt(ct, msg, null, null, nonce, key);
return ct;
}
function open(key, counter, ct) {
const nonce = Buffer.alloc(N_BYTES); nonce.writeUInt32LE(counter, 0);
const out = Buffer.alloc(ct.length - A_BYTES);
sodium.crypto_aead_chacha20poly1305_ietf_decrypt(out, null, ct, null, nonce, key);
return out;
}
// ---------- Day 1/3: a trivial discovery registry ----------
const registry = new Map(); // topic -> { inbox: Framer, key, counter }
function makePeer(name, topic) {
const kp = kxKeypair();
const inbox = new Framer();
const peer = { name, topic, kp, inbox, key: null, rxCounter: 0, txCounter: 0, partner: null };
registry.set(name, peer);
return peer;
}
// "send" = hand framed bytes to the partner's inbox (stands in for the wire)
function send(from, payload) {
const ct = seal(from.key, from.txCounter, payload);
from.txCounter++;
from.partner.inbox.push(frame(ct)); // length-prefixed encrypted frame
}
// ---------- wire it together ----------
const alice = makePeer('alice', 'chat-42');
const bob = makePeer('bob', 'chat-42');
alice.partner = bob; bob.partner = alice;
// discovery + handshake: derive a shared AEAD key from kx
function handshake(client, server) {
const cRx = Buffer.alloc(32), cTx = Buffer.alloc(32);
const sRx = Buffer.alloc(32), sTx = Buffer.alloc(32);
sodium.crypto_kx_client_session_keys(cRx, cTx, client.kp.pk, client.kp.sk, server.kp.pk);
sodium.crypto_kx_server_session_keys(sRx, sTx, server.kp.pk, server.kp.sk, client.kp.pk);
// use one direction's key for both here (demo); real protocols key each direction
client.key = cTx; server.key = sRx; // cTx === sRx
return cTx.equals(sRx);
}
console.log('handshake ok:', handshake(alice, bob));
// bob decrypts and verifies each framed message he receives
bob.inbox.on('message', (ct) => {
try {
const msg = open(bob.key, bob.rxCounter, ct);
bob.rxCounter++;
console.log(`bob received: "${msg.toString()}"`);
} catch {
console.log('bob: message failed AEAD verification — dropped');
}
});
// alice sends three encrypted, framed messages
send(alice, Buffer.from('hello bob'));
send(alice, Buffer.from('this channel is encrypted'));
send(alice, Buffer.from('and every frame is verified'));
// tamper demo: flip a byte in a sealed frame and confirm it's rejected
const good = seal(alice.key, 99, Buffer.from('evil'));
good[2] ^= 0xff;
bob.rxCounter = 99;
bob.inbox.push(frame(good)); // -> "failed AEAD verification — dropped"
Expected output
node capstone.js
// handshake ok: true
// bob received: "hello bob"
// bob received: "this channel is encrypted"
// bob received: "and every frame is verified"
// bob: message failed AEAD verification — dropped
From this skeleton to the real Holepunch stack
Each hand-rolled piece maps to a production module: the registry → hyperdht; the kx handshake →
the Noise/UDX transport inside hyperswarm; the framer → compact-encoding + the stream
protocol; the append-and-verify pattern → hypercore. Rebuild the capstone using hyperswarm
for real: join a topic, and on 'connection' you get an already-encrypted, already-framed stream — every
layer you just built by hand, productionized.
Experiments
- Replace the in-process registry with the real
dgramrendezvous from Day 3 so the two peers run as separate processes. - Swap the hand framer for
compact-encodingstructs (Day 4) and confirm identical behavior. - Profile the capstone under 100k messages with
--cpu-prof; confirm no accidental concat-in-loop and that AEAD dominates CPU (as it should).
7.5 Final interview drill
- I can read a flame graph and explain why width matters more than height.
- I follow the measure → hypothesize → change one thing → re-measure loop.
- I know the optimization order: algorithm → allocation → don't block → micro.
- My capstone discovers a peer, handshakes, and exchanges verified encrypted frames.
- I can deliver the end-to-end "two peers connecting securely" answer cold.
You've now built, by hand, a small version of every layer in a
modern P2P stack — and you can explain each one and prove it runs. Walk into the interview and rebuild the capstone
on a whiteboard from the five-layer summary above. That's the whole game.