Bootcamp i backendutveckling med Node.js · Lektion

CPU-profilering och flame graphs för heta kodvägar

Spela in CPU-profiler och läs flame graphs för att hitta funktionerna som tar mest tid.

Lektion 3 av 413 steg

CPU-profilering och flame graphs för heta kodvägar är en gratis lektion i Bootcamp i backendutveckling med Node.js på CoddyKit. Detta är lektion 3 av 4. Ni kan läsa hela lektionen gratis nedan och sedan öva praktiskt i webbläsaren med en inbyggd kodredigerare och en AI-handledare som är tillgänglig dygnet runt. Den ingår i lärvägen för Bootcamp i backendutveckling med Node.js, och Era framsteg synkroniseras mellan webben och CoddyKit-appen. Kursen i Bootcamp i backendutveckling med Node.js innehåller totalt 4 lektioner.

Varför CPU-profilering är viktigt

När en Node.js-backend känns långsam beror det vanligtvis på en av två saker: processen väntar (I/O, databas eller nätverk), eller så beräknar den (förbrukar CPU på den enda huvudtråden). En CPU-profil visar exakt vart den senare typen av tid tar vägen.

  • Den samplar anropsstacken med en fast frekvens (V8 använder cirka 1 000 Hz, ett sampel per millisekund).
  • Varje sampel registrerar den funktion som körs just då och hela stacken av anropare.
  • Funktioner som förekommer i många sampel är Era heta kodvägar — den kod som är värd att optimera.

Eftersom Node.js kör JavaScript i en enda tråd kan en enda het funktion blockera varje inkommande request. Profilering hittar den åt Er i stället för att Ni behöver gissa.

Egen tid kontra total tid

Alla profilerare skiljer mellan två tal per funktion, och att blanda ihop dem är det vanligaste misstaget vid profilering.

  • Egen tid (även kallad exklusiv tid): tiden som ägnas åt att köra funktionens egen kod, exklusive de underordnade funktioner som den anropade.
  • Total tid (även kallad inklusiv tid): egen tid plus all tid som tillbringas i anropade funktioner.

En funktion som har hög total tid kan bara vara en samordnare som anropar dyra underordnade funktioner. Funktionen som har hög egen tid är där CPU:n faktiskt arbetar hårt. Optimera först utifrån egen tid.

Spela in en profil från CLI

Det snabbaste sättet att profilera ett skript är att använda den inbyggda V8-flaggan. Inga extra paket och inga kodändringar behövs.

  • node --prof app.js skriver en rå isolate-*.log-fil.
  • node --prof-process isolate-*.log > profile.txt omvandlar den till en läsbar sammanfattning med en uppdelning av ticks.

Sammanfattningen grupperar ticks efter JavaScript, C++ och GC och listar de tyngsta funktionerna. Den består endast av text, så för visuella flame graphs använder vi inspector-protokollet härnäst. Nedan finns en CPU-bunden arbetsbelastning som Ni kan profilera på detta sätt.

function isPrime(n) {
  if (n < 2) return false;
  for (let i = 2; i * i <= n; i++) {
    if (n % i === 0) return false;
  }
  return true;
}

function countPrimes(limit) {
  let count = 0;
  for (let n = 0; n < limit; n++) {
    if (isPrime(n)) count++;
  }
  return count;
}

console.log(countPrimes(2_000_000));

Spela in programmatiskt med Inspectorn

För en långkörande server vill Ni ofta profilera ett specifikt tidsintervall. Den inbyggda modulen inspector låter Er starta och stoppa V8:s CPU-profilerare inifrån koden och spara en .cpuprofile-fil.

  • Öppna en Session, anslut den och aktivera domänen Profiler.
  • Anropa Profiler.start, kör belastningen och anropa sedan Profiler.stop.
  • Den returnerade profilen är JSON som Ni skriver till disk och läser in i Chrome DevTools eller VS Code.
const inspector = require('node:inspector');
const fs = require('node:fs');
const session = new inspector.Session();
session.connect();

function work() {
  let sum = 0;
  for (let i = 0; i < 5e7; i++) sum += Math.sqrt(i);
  return sum;
}

session.post('Profiler.enable', () => {
  session.post('Profiler.start', () => {
    work();
    session.post('Profiler.stop', (err, { profile }) => {
      fs.writeFileSync('./work.cpuprofile', JSON.stringify(profile));
      console.log('Saved work.cpuprofile');
      session.disconnect();
    });
  });
});

Vad en flame graph faktiskt visar

En flame graph omvandlar stackproverna till en bild. Läs den så här:

  • X-axeln visar INTE tid — den visar mängden stackar. Bredden = hur många prover som innehöll den stackraden, alltså hur mycket CPU den använde.
  • Y-axeln visar stackdjupet. Ramen längst ned är anroparen; ramarna ovanpå är dess anropade funktioner.
  • En bred ram betyder att en funktion (och dess underordnade funktioner) använde mycket CPU. Breda ramar längst upp med få ramar ovanför sig är löven som utför det faktiska arbetet.

Färgerna är vanligtvis slumpmässiga och saknar betydelse — dra inga slutsatser av dem. Leta efter de bredaste platåerna, inte de högsta tornen.

Flame graph kontra flame chart

De här ser likadana ut, men besvarar olika frågor, och Chrome DevTools visar båda.

  • Flame chart (tidslinjen "Performance" i DevTools): x-axeln visar väggurstid, från vänster till höger. Utmärkt för att se när något hände och i vilken ordning händelser inträffade.
  • Flame graph (aggregerad): identiska ramar slås ihop och sorteras efter bredd. Utmärkt för att se vilken funktion som är het under hela körningen, oavsett när den kördes.

För att hitta heta kodvägar vill du använda den aggregerade flame graphen: en funktion som anropas 10 000 gånger vid utspridda tidpunkter visas som en enda tjock stapel i stället för 10 000 osynliga smala streck.

Generera flame graphs med 0x

Verktyget 0x startar och övervakar processen, samlar in en profil och skapar en interaktiv HTML-baserad flame graph i ett enda steg — idealiskt för Node.js-tjänster.

  • npx 0x app.js kör appen och öppnar en flame graph i webbläsaren när processen avslutas.
  • För en server skickar du belastning mot den (till exempel med autocannon) medan 0x spelar in. Stoppa sedan processen för att generera grafen.

I 0x-visningen kan du klicka på valfri ram för att zooma och söka efter namn för att markera varje ställe där en funktion förekommer. Nedan finns en liten HTTP-server som är lämplig att profilera under belastning.

const http = require('node:http');

function renderRow(i) {
  return '<tr><td>' + i + '</td><td>' + (i * i) + '</td></tr>';
}

http.createServer((req, res) => {
  let html = '<table>';
  for (let i = 0; i < 5000; i++) {
    html += renderRow(i);
  }
  html += '</table>';
  res.setHeader('Content-Type', 'text/html');
  res.end(html);
}).listen(3000, () => console.log('listening on 3000'));

Läsa grafen: hitta det bredaste lövet

Ett systematiskt sätt att hitta den heta kodvägen i valfri flame graph:

  • Skanna grafens övre kant (lövr amarna). Det här är funktionerna som faktiskt kördes när proverna togs.
  • Hitta det bredaste lövet eller den bredaste platån. Den enskilda ramen utgör din största andel egen tid.
  • Följ den nedåt för att se anropskedjan som leder dit — det visar vad du bör ändra.

Se upp för brus från ramverk: ramar som (anonymous), module.exports eller interna delar av körmiljön är ofta breda eftersom allt passerar genom dem. Ignorera breda orkestreringsramar och fokusera på breda lövramar.

Identifiera belastning från skräpsamling

Flame graphs visar inte bara er kod — de blottlägger även V8-körmiljön. Om ni ser breda ramar med namn som GC, Scavenge eller Mark-Compact används CPU:n till att samla in skräp, inte till att köra logik.

  • En stor GC-bredd betyder vanligtvis att ni allokerar för många kortlivade objekt i den heta kodvägen (strängkonkatenering i loopar eller skapande av closures eller arrayer för varje begäran).
  • Lösningen är sällan att "optimera funktionen" — den är att "allokera mindre": återanvänd buffertar, allokera arrayer i förväg och undvik objektliteraler i varje iteration.

Exemplet nedan allokerar ett nytt objekt vid varje iteration, det klassiska mönstret som får GC-ramar att lysa upp.

function process(n) {
  const results = [];
  for (let i = 0; i < n; i++) {
    // a new object every iteration -> GC pressure
    results.push({ id: i, squared: i * i, label: 'item-' + i });
  }
  let sum = 0;
  for (const r of results) sum += r.squared;
  return sum;
}

console.log(process(1_000_000));

Ledtrådar om deoptimering och inlining

V8 kompilerar heta funktioner till optimerad maskinkod. När en funktion tvingas tillbaka till långsammare bytekod är den deoptimerad, vilket syns som oväntat breda ramar.

  • Kör med node --trace-deopt app.js för att logga varje deoptimering med dess orsak (t.ex. ändrade objektformer eller blandade typer i en array).
  • node --trace-opt app.js visar vilka funktioner som optimerades.
  • Om funktionsargumenten förblir monomorfa (alltid har samma form eller typ) kan V8 fortsätta att optimera och inline:a dem.

Funktionen nedan förblir snabb eftersom den alltid tar emot tal; om ni skickar en sträng till den utlöses en deoptimering och en bredare ram i profilen.

function add(a, b) {
  return a + b;
}

let total = 0;
for (let i = 0; i < 1e7; i++) {
  total = add(total, i); // monomorphic: always numbers, stays optimized
}
console.log(total);

Ett repeterbart arbetsflöde för profilering

Sätt ihop delarna till en loop som ni kan köra på valfri backend:

  • Återskapa belastningen deterministiskt (ett benchmark eller återuppspelad trafik), så att två profiler går att jämföra.
  • Spela in en profil (--prof, sessionen i inspector eller 0x).
  • Läs flame graphen: den bredaste lövramen = mest egen tid = första målet.
  • Åtgärda exakt den funktionen och ändra inget annat.
  • Profilera på nytt under samma belastning och bekräfta att den tjocka stapeln har krympt.

Ändra en sak per iteration. Om ni åtgärdar tre funktioner samtidigt vet ni inte vilken ändring som hjälpte — och en av dem kan ha gjort saker sämre.

Snabb kontroll: läsa grafen

Ni profilerade en långsam endpoint. I den aggregerade flame graphen är er requesthanterare den bredaste ramen, men den har höga staplar av underordnade funktioner ovanför sig. En liten lövram nära toppen, JSON.stringify, är också mycket bred. Vilken funktion bör ni optimera först?

Sammanfattning

Ni kan nu hitta och åtgärda heta CPU-kodvägar i en Node.js-backend:

  • Egen tid (exklusiv) är där CPU:n förbrukas; (inklusiv) omfattar underordnade funktioner. Optimera utifrån egen tid.
  • Spela in med node --prof, den inbyggda inspector-sessionen eller 0x för en interaktiv flame graph.
  • I en flame graph visar x-axeln antalet prover (CPU), inte tid, och y-axeln visar stackdjupet. Leta efter den bredaste lövramen.
  • Breda GC/Scavenge-ramar betyder allokeringsbelastning — allokera mindre i stället för att mikrooptimera logiken.
  • Använd --trace-deopt för att upptäcka deoptimeringar som orsakas av polymorf kod eller kod som ändrar form.
  • Följ loopen: återskapa, spela in, läs, åtgärda en sak och profilera på nytt.
Gratis att börja

Lär dig JavaScript med en AI-lärare – gratis

Skriv och kör riktig kod i webbläsaren, få omedelbar hjälp av en AI-lärare dygnet runt och fortsätt där du slutade – på webben eller i appen.

Kurser
22
Lektioner
92

Vanliga frågor

Är lektionen ”CPU-profilering och flame graphs för heta kodvägar” gratis?

Ja – hela texten till ”CPU-profilering och flame graphs för heta kodvägar” kan läsas gratis här på webben. Om Ni vill öva interaktivt med en inbyggd kodredigerare och en AI-handledare som är tillgänglig dygnet runt och låsa upp resten av kursen i Bootcamp i backendutveckling med Node.js, kan Ni uppgradera till CoddyKit PRO. Kursen i Bootcamp i backendutveckling med Node.js innehåller totalt 4 lektioner.

Vad lär jag mig i ”CPU-profilering och flame graphs för heta kodvägar”?

Spela in CPU-profiler och läs flame graphs för att hitta funktionerna som tar mest tid. Ni övar på Bootcamp i backendutveckling med Node.js med praktisk kod som körs direkt i webbläsaren, medan en AI-handledare som är tillgänglig dygnet runt svarar på Era frågor under lektionen.

Behöver jag någon erfarenhet för att börja lära mig Bootcamp i backendutveckling med Node.js?

Du behöver inga förkunskaper. Utbildningen i Bootcamp i backendutveckling med Node.js på CoddyKit är upplagd för allt från nybörjare till avancerade elever, så att du kan börja här eller från början och gå fram i din egen takt. Detta är lektion 3 av 4.

Hur lång tid tar lektionen ”CPU-profilering och flame graphs för heta kodvägar”?

De flesta CoddyKit-lektioner tar cirka 5–10 minuter. Varje lektion är kort och interaktiv, så att du gör stadiga framsteg och kan fortsätta precis där du slutade – på webben eller i appen.

Kan jag skriva och köra kod i den här Bootcamp i backendutveckling med Node.js-lektionen?

Ja. Varje Bootcamp i backendutveckling med Node.js-lektion innehåller en inbyggd kodredigerare, så att du kan skriva och köra riktig kod direkt i webbläsaren och få omedelbar AI-feedback – utan lokal installation.

Alla lektioner i den här kursen

  1. V8-heapen, generationsbaserad GC och objektslivslängder
  2. Samla in och jämför heap-ögonblicksbilder
  3. CPU-profilering och flame graphs för heta kodvägar
  4. Identifiera och åtgärda vanliga läckagemönster
← Tillbaka till Bootcamp i backendutveckling med Node.js