modulo 09 / lezione 48

Performance, memoria e osservabilità

Profiling backend e flamegraph

Profilare un servizio backend (Node, Python, Java) e leggere flamegraph generati con perf, py-spy o async-profiler per individuare hot path.

55 min Debugging

In parole semplici

Lato server, la lentezza ha le stesse sotto-categorie del client (CPU, I/O, locking) ma strumenti diversi. Il flamegraph è la rappresentazione più potente: mostra dove il programma passa il tempo, raggruppato per stack di chiamata.

Imparare a leggere un flamegraph trasforma “il server è lento” in “il metodo X consuma il 47% del tempo”.

Parole nuove

  • profiler: strumento che campiona lo stack a intervalli regolari.
  • sampling profiler: registra ad intervalli, leggero ma stocastico.
  • flamegraph: visualizzazione di stack di chiamata in cui la larghezza = tempo.
  • hot path: percorso di codice dove si concentra il tempo.

Cosa impari

  • Generare un flamegraph per Node, Python o Java.
  • Leggere flamegraph distinguendo larghezza, altezza, colore.
  • Identificare hot path e formulare ipotesi di ottimizzazione.

Cosa è un flamegraph

Diagramma a barre orizzontali:

  • Asse X: l’asse del tempo, ma non temporale: è lo spazio dei campioni raccolti.
  • Asse Y: profondità dello stack. In basso main(), in alto le foglie.
  • Larghezza: percentuale di campioni in cui quella funzione era nello stack.
  • Colore: spesso casuale (per distinguere blocchi). Alcuni profiler usano colori semantici (rosso = on-CPU, blu = off-CPU).

Lettura: cerca la barra più larga in alto. È la funzione dove il programma passa tempo “alla foglia”. Sotto si vede il chi-la-chiama.

Strumenti per Node.js

0x

npm install -g 0x
0x app.js
# esegui, ctrl+c, apre browser con flamegraph

clinic.js

npm install -g clinic
clinic doctor -- node app.js
clinic flame -- node app.js
clinic bubbleprof -- node app.js

doctor analizza CPU/event loop, flame produce flamegraph, bubbleprof mostra dipendenze async.

Node nativo

node --prof app.js          # genera log
node --prof-process isolate-*.log > processed.txt

Strumenti per Python

py-spy

pip install py-spy
py-spy record -o profile.svg -- python app.py
# oppure attach a un processo in corso
py-spy top --pid 1234

Vantaggio: non richiede modifiche al codice. Anche su processi in produzione.

cProfile

python -m cProfile -o profile.out app.py
# poi:
python -m pstats profile.out

Più verboso, granulare ma meno visuale.

Strumenti per JVM

async-profiler

./profiler.sh -d 30 -f flame.html <pid>

Open source, low overhead, supporta CPU, allocations, locks.

JFR (Java Flight Recorder)

Built-in in OpenJDK 11+. Avvio:

java -XX:StartFlightRecording=duration=30s,filename=profile.jfr -jar app.jar

Analizza con JDK Mission Control.

Lettura di un flamegraph: esempio

Scenario: endpoint /utenti lento.

Flamegraph mostra:

handle_request 100%
  ├── parse_request 5%
  ├── auth_check 8%
  ├── query_database 70%
  │     └── orm_serialize 60%
  │           └── deepClone 45%   ← hot path!
  └── render_response 17%

Cosa significa:

  • 70% del tempo dell’handler è nel DB.
  • Dentro al DB, 60% è in orm_serialize (serializzazione dei risultati).
  • Dentro orm_serialize, 45% è in deepClone.

Ipotesi di ottimizzazione:

  • Hot path: deepClone. Probabilmente serializza N volte per request.
  • Indagine: c’è un JSON.parse(JSON.stringify(...)) da qualche parte? Sostituibile con clone superficiale o lazy?

Distinguere CPU-bound da I/O-bound

In flamegraph CPU-only, le pause su I/O non appaiono. Indizi:

  • Profilo “vuoto” durante il request → I/O.
  • Profilo pieno di await/Promise → forse libreria spende CPU su parsing.

Strumenti per I/O:

  • wall-clock profiler (mostra tempo reale, inclusi await): clinic bubbleprof, async-profiler con wall mode.

Workflow di indagine performance backend

  1. Identifica l’endpoint lento: log o APM (modulo 9.4).
  2. Riproduci in staging con carico simile.
  3. Profila per 30-60 secondi.
  4. Leggi flamegraph: trova le 3 barre più larghe.
  5. Formula ipotesi: per ognuna, perché è così grande?
  6. Prova ottimizzazione mirata: una alla volta.
  7. Misura di nuovo: confronto.

Ottimizzazioni tipiche

  • N+1 query: una query principale + N query per oggetto correlato. Sostituire con JOIN o eager load.
  • Serializzazione ripetuta: cache.
  • Hash/crittografia in hot path: spostare offline o asincrono.
  • JSON.parse di file enormi a ogni request: caricare in memoria una volta.
  • Algoritmi O(n²) su dataset grandi.

Esercizio guidato

Per un piccolo server Node:

  1. Scrivi un endpoint che fa un’operazione finta lenta (es. loop pesante o JSON.parse di un file grande).
  2. Esegui clinic flame -- node app.js. Carica con autocannon o curl ripetuto.
  3. Stop. Apri il flamegraph.
  4. Identifica la barra più larga in alto. Quale funzione?
  5. Ottimizza. Rifai il profilo. Differenza?

Errori frequenti del principiante

  • Ottimizzare senza profilare.
  • Confondere larghezza con altezza. Larghezza = tempo. Altezza = profondità.
  • Profilare in dev con HMR. Risultati inutilizzabili.
  • Profilare per 5 secondi. Troppo poco: il sampling non statistico.