Flame graphs: el perfil de CPU de un vistazo
Cómo un flame graph pliega miles de pilas de muestreo en una sola imagen donde el ancho es tiempo de CPU y la altura es profundidad de pila, el flujo canónico perf record hacia stackcollapse hacia flamegraph, la elección del método de desenrollado entre marcos, DWARF y LBR, y las variantes diferencial, off-CPU e icicle.
Un perf report con diez mil pilas de llamadas es un muro de texto que nadie lee entero. El ojo humano no está hecho para tablas de porcentajes anidados, pero sí para las formas. El flame graph, inventado por Brendan Gregg, hace la traducción: toma esas miles de pilas muestreadas y las pliega en una única imagen donde cada función es una caja, el ancho de la caja es la proporción de tiempo que la CPU pasó allí, y la altura es la profundidad de la pila. Lo que en texto exige minutos de rastreo, en la imagen salta a la vista en segundos: las mesetas anchas de la cima son exactamente donde arde el procesador.
- Leer un flame graph: eje X población de muestras, eje Y profundidad de pila, ancho proporción de tiempo.
- Ejecutar el flujo
perf recordhaciastackcollapse-perf.plhaciaflamegraph.pl. - Elegir el método de desenrollado de pila entre marcos, DWARF y LBR.
- Reconocer las variantes diferencial, off-CPU e icicle.
Anatomía de un flame graph
La regla de lectura es contraintuitiva y hay que grabársela. El eje Y es profundidad de pila: la caja de abajo es la raíz —main o el hilo del kernel— y cada caja encima es una función llamada por la de debajo. El eje X no es tiempo: las cajas se ordenan alfabéticamente para fundir las pilas idénticas, de modo que el ancho de una caja es la fracción de muestras en que esa función aparecía en la pila, es decir, la proporción de tiempo de CPU que consumió ella y sus hijas. No hay flujo temporal de izquierda a derecha; hay población.
De ahí se sigue cómo se interpreta. Buscas las cajas anchas, no las altas: una torre estrecha y altísima es una pila profunda que casi no consume tiempo, mientras que una meseta ancha en la cima es una función hoja que está quemando la CPU ahora mismo, sin llamar a nadie más. Ese es el sospechoso. Una caja ancha en la base con muchas hijas estrechas reparte su coste; una caja ancha arriba lo concentra.
Ancho = tiempo
La proporción de muestras. Ancho es caro; busca lo ancho, ignora lo estrecho.
Alto = profundidad
Cada nivel es un marco de pila. Alto no significa lento, solo anidado.
Meseta en la cima
Función hoja que arde: consume CPU sin llamar a nadie. El sospechoso.
X no es tiempo
Las cajas se ordenan alfabeticamente para fundir pilas, no cronologicamente.
El flujo canónico
Un flame graph nace de tres transformaciones sobre un perfil de perf. Primero muestreas con pilas; luego despliegas cada muestra a una línea de texto; luego pliegas las pilas idénticas contándolas; y finalmente dibujas el SVG. Cada paso es una herramienta pequeña encadenada por tuberías, en la mejor tradición Unix.
# 1. muestrear a 99 Hz, toda la maquina, con pilas, durante 30 s
perf record -F 99 -a -g -- sleep 30
# 2. desplegar el perf.data binario a texto, una linea por marco
perf script > perfiles.txt
# 3. plegar cada pila a una sola linea con su cuenta al final
./stackcollapse-perf.pl perfiles.txt > perfiles.folded
# 4. dibujar el SVG interactivo
./flamegraph.pl perfiles.folded > flame.svg
El formato plegado intermedio es transparente y merece verse: cada línea es una pila única, con sus funciones separadas por punto y coma de la raíz a la hoja, y al final el número de muestras que la recorrieron.
mi_prog;main;parse_line;hash_bucket;crc32 482
mi_prog;main;parse_line;memmove 217
mi_prog;main;flush_output 95
Ese texto plano es lo que flamegraph.pl convierte en cajas: cada segmento entre puntos y comas es un nivel, y la cuenta final fija el ancho. Porque el formato es tan simple, cualquier fuente de pilas —no solo perf— puede alimentar el dibujante, y por eso los flame graphs se generalizaron a memoria, bloqueos y esperas.
flowchart LR REC[perf record -F 99 -g] --> SCRIPT[perf script] SCRIPT --> COLLAPSE[stackcollapse-perf.pl] COLLAPSE --> FOLDED[pilas plegadas y contadas] FOLDED --> FG[flamegraph.pl] FG --> SVG[flame.svg interactivo]
En los núcleos y las herramientas de 2026 hay un atajo integrado: perf script report flamegraph genera directamente un flame graph interactivo en HTML con D3, sin scripts externos, y GUIs como Hotspot leen el perf.data y lo pintan al vuelo. Pero conocer la tubería clásica sigue importando, porque es la que se adapta a cualquier fuente de pilas.
Desenrollar la pila: marcos, DWARF o LBR
Todo depende de que perf sepa reconstruir la pila de llamadas en cada muestra, y hay tres métodos con compromisos distintos. Con marcos de pila (--call-graph fp) sigues el encadenamiento del registro de base; es baratísimo, pero exige que el código se compilara con -fno-omit-frame-pointer, algo que las distribuciones abandonaron durante años por rendimiento y han vuelto a activar. Con DWARF (--call-graph dwarf) perf copia un trozo de la pila en cada muestra y la desenrolla después con la información de depuración; funciona sin marcos, pero infla el perf.data y pesa más. Con LBR (--call-graph lbr) usas el registro hardware de últimos saltos del procesador; es preciso y ligero, pero de profundidad limitada.
# DWARF: pilas correctas aunque el binario no tenga frame pointers
perf record --call-graph dwarf -F 99 -g ./mi_programa
# LBR: desenrollado por hardware, sin necesidad de frame pointers
perf record --call-graph lbr -F 99 ./mi_programa
Si tu flame graph aparece truncado o lleno de [unknown], casi siempre es el desenrollado: faltan marcos de pila y no pediste DWARF. La elección del método es la decisión técnica que más determina la calidad del gráfico.
Variantes: diferencial, off-CPU e icicle
El flame graph básico mide tiempo en CPU. Sus parientes cubren otras preguntas. El diferencial superpone dos perfiles —antes y después de un cambio— y pinta en rojo lo que creció y en azul lo que menguó, ideal para ver qué empeoró tras un despliegue. El off-CPU invierte la pregunta: en vez de dónde arde la CPU, mide dónde el hilo estaba bloqueado esperando —E/S, un cerrojo, el planificador—, y se construye rastreando las conmutaciones de contexto con el planificador o con una herramienta BPF como offcputime. El icicle es simplemente el mismo gráfico invertido, con la raíz arriba, útil para agrupar por hoja común.
# tiempo fuera de CPU: donde los hilos se bloquean, via BPF
sudo offcputime-bpfcc -df 30 > offcpu.folded
./flamegraph.pl --title "Off-CPU" --colors=io offcpu.folded > offcpu.svg
Juntos, el flame graph on-CPU y el off-CPU cubren el tiempo entero de un hilo: lo que pasa trabajando y lo que pasa esperando. Un servicio lento cuyo flame graph on-CPU está casi vacío te está diciendo que el problema no es cómputo, sino espera, y que debes mirar el off-CPU.
Durante una década las distribuciones compilaban sin marcos de pila para ganar un pequeño porcentaje de rendimiento, y a cambio los flame graphs salían rotos salvo que recompilaras o usaras DWARF. Desde 2023 y 2024, Fedora y Ubuntu volvieron a activar -fno-omit-frame-pointer por defecto tras concluir que la observabilidad valía ese coste. Si trabajas en una distribución reciente, el desenrollado por marcos vuelve a ser fiable y barato; si no, DWARF es tu red de seguridad.
Piensa en la hazaña perceptiva que es un flame graph, porque ahí se esconde una idea que trasciende el perfilado. Un perfil de CPU es un objeto de dimensión endiablada: decenas de miles de muestras, cada una un vector que es una pila de llamadas de profundidad variable, un punto en un espacio de secuencias de funciones. Ningún humano puede leer esa nube de datos cruda. El flame graph la proyecta a dos dimensiones eligiendo con astucia qué preservar y qué descartar: sacrifica por completo el tiempo —el eje X deja de ser cronología y pasa a ser población agregada— y a cambio conserva lo único que importa para optimizar, que es dónde se concentra la masa de muestras. Esa renuncia es exactamente lo que hace legible la imagen: al fundir todas las apariciones de una misma pila en una sola caja cuyo ancho es su frecuencia, convierte una distribución de probabilidad sobre pilas de llamadas en un paisaje que el sistema visual humano parsea de un vistazo, porque estamos exquisitamente afinados para detectar la caja ancha, la meseta, la anomalía de forma. Aquí está la lección que se lleva uno más allá de perf: los datos no se entienden acumulándolos ni listándolos, sino encontrando la proyección que descarta lo irrelevante y hace saltar a la vista lo que importa. El flame graph es una demostración de que la visualización correcta no es decoración sobre los números, sino un acto cognitivo: traslada el trabajo del razonamiento lento y secuencial —leer diez mil pilas— a la percepción rápida y paralela —ver una forma—. Optimizar rendimiento, al final, no es cuestión de líneas de código sino de dónde se concentra la población de tu ejecución, y el genio del flame graph es hacer visible esa concentración invisible.
- Genera un flame graph completo de una carga tuya siguiendo los cuatro pasos, ábrelo en el navegador y localiza la meseta más ancha de la cima; nombra la función que arde.
- Inspecciona el fichero
.foldedintermedio y explica, tomando una línea, cómo su cuenta final se traduce en el ancho de las cajas del SVG. - Repite la captura con
--call-graph fpy con--call-graph dwarf, compara la calidad de las pilas y explica por qué una versión muestra[unknown]donde la otra no. - Introduce una regresión de rendimiento a propósito, captura un segundo perfil y produce un flame graph diferencial que pinte en rojo la función que empeoraste.
- Perfila un servicio que sospeches limitado por E/S, comprueba que su flame graph on-CPU sale casi vacío, y captura entonces uno off-CPU que revele dónde se bloquea de verdad.