perf es el perfilador oficial del kernel Linux. Toma muestras de lo que está ejecutando la CPU miles de veces por segundo y te dice en qué funciones, de tu aplicación, de sus bibliotecas o del kernel, se va el tiempo. En este tutorial instalarás perf en Ubuntu 24.04, lo usarás con un programa de ejemplo para localizar la función más costosa, perfilarás un proceso en marcha y todo el sistema, y generarás un flame graph para visualizar el resultado.

Requisitos previos

Para seguir esta guía necesitas:

  • Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath.
  • Un usuario no root con privilegios sudo.
  • git para descargar las herramientas de flame graphs.

Si tu servidor es una máquina virtual, es posible que los contadores de hardware (ciclos, instrucciones, fallos de caché) no estén disponibles. El perfilado de CPU funciona igualmente con eventos de software, como verás en el paso 3.

Paso 1: Instalar perf

perf se distribuye con las herramientas del kernel y debe coincidir con la versión del kernel en ejecución. Instala el paquete común y el específico de tu kernel, junto con gcc para compilar el ejemplo:

sudo apt update
sudo apt install linux-tools-common linux-tools-$(uname -r) gcc git

Si usas el kernel generic y quieres que perf se actualice con él, instala también linux-tools-generic. Comprueba la instalación:

perf --version
perf version 6.8.12

Ubuntu restringe perf a usuarios con privilegios mediante kernel.perf_event_paranoid:

sysctl kernel.perf_event_paranoid
kernel.perf_event_paranoid = 4

Con ese valor tendrás que ejecutar perf con sudo, que es lo que hace esta guía. No bajes este valor en un servidor compartido: permitiría a cualquier usuario observar la actividad de otros procesos.

Paso 2: Crear un programa de ejemplo

Para aprender a leer perf conviene un programa donde ya sabes dónde está el problema. Este ejemplo tiene dos funciones: una hace una operación costosa (una división por cada iteración interna) y otra hace un trabajo ligero.

nano hotspot.c
#include <stdio.h>
#include <stdlib.h>

#define N 200000

__attribute__((noinline))
static unsigned long slow_sum(const unsigned *data, size_t n) {
    unsigned long s = 0;
    for (size_t i = 0; i < n; i++)
        for (unsigned j = 1; j <= 2000; j++)
            s += data[i] % j;
    return s;
}

__attribute__((noinline))
static unsigned long fast_sum(const unsigned *data, size_t n) {
    unsigned long s = 0;
    for (int r = 0; r < 200; r++)
        for (size_t i = 0; i < n; i++)
            s += data[i];
    return s;
}

int main(void) {
    unsigned *data = malloc(N * sizeof *data);
    if (!data)
        return 1;
    for (size_t i = 0; i < N; i++)
        data[i] = (unsigned)rand();
    printf("%lu %lu\n", slow_sum(data, N), fast_sum(data, N));
    free(data);
    return 0;
}

Compílalo con optimizaciones, símbolos de depuración (-g) y puntero de marco (-fno-omit-frame-pointer), que permite a perf reconstruir la pila de llamadas de forma barata:

gcc -O2 -g -fno-omit-frame-pointer -o hotspot hotspot.c

La ejecución tarda unos segundos, según tu CPU.

Paso 3: Medir el proceso con perf stat

perf stat ejecuta un comando y cuenta eventos durante toda su ejecución. Es la primera medición que conviene hacer, porque dice si el programa está limitado por la CPU o pasa el tiempo esperando:

sudo perf stat ./hotspot
 Performance counter stats for './hotspot':

          5,812.44 msec task-clock                #    0.999 CPUs utilized
                 9      context-switches          #    1.548 /sec
                 0      cpu-migrations            #    0.000 /sec
               250      page-faults               #   43.012 /sec
    23,114,560,212      cycles                    #    3.977 GHz
     2,811,490,207      instructions              #    0.12  insn per cycle
...
       5.818203114 seconds time elapsed

       5.809882000 seconds user
       0.001998000 seconds sys

Cómo leerlo:

  • CPUs utilized cerca de 1 en un programa de un solo hilo significa que está limitado por la CPU. Un valor muy por debajo indica que espera a disco, red o bloqueos, y entonces strace o las herramientas de E/S son mejores que un perfil de CPU.
  • insn per cycle (instrucciones por ciclo) bajo, por debajo de 1, sugiere que la CPU espera a memoria o a instrucciones lentas como la división de este ejemplo.
  • user frente a sys: casi todo el tiempo está en modo usuario, en el código del programa.

Si en tu máquina virtual aparece <not supported> en cycles e instructions, el hipervisor no expone los contadores de hardware. El resto de la guía funciona igual con el evento de software cpu-clock.

Paso 4: Localizar las funciones costosas con perf record y perf report

perf record toma muestras periódicas de qué se está ejecutando y las guarda en perf.data. Usa -F 99 (99 muestras por segundo, suficiente y con poca sobrecarga) y -g para registrar la pila de llamadas:

sudo perf record -F 99 -g ./hotspot
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.067 MB perf.data (577 samples) ]

Si tu VPS no tiene contadores de hardware, añade -e cpu-clock para muestrear con el temporizador:

sudo perf record -e cpu-clock -F 99 -g ./hotspot

Analiza el resultado en modo texto. --no-children asigna a cada función solo el tiempo que pasa en su propio código, sin sumar el de las funciones a las que llama, que es lo más directo para encontrar el punto caliente:

sudo perf report --stdio --no-children
# Overhead  Command  Shared Object     Symbol
# ........  .......  ................  ...........................
#
    96.53%  hotspot  hotspot           [.] slow_sum
            |
            ---__libc_start_main
               main
               slow_sum

     2.95%  hotspot  hotspot           [.] fast_sum
     0.17%  hotspot  [kernel.kallsyms] [k] clear_page_erms
...

[.] indica código de usuario y [k] código del kernel. El informe confirma que slow_sum consume el 96 % del tiempo, aunque fast_sum recorre los datos 200 veces: el coste está en la división dentro del bucle interno, no en el número de accesos a memoria.

Para ver qué líneas exactas de la función son costosas, usa perf annotate. Gracias a -g al compilar, perf muestra el código fuente junto a las instrucciones:

sudo perf annotate --stdio slow_sum | head -n 40

Las instrucciones con mayor porcentaje aparecerán junto a la instrucción de división (div) que genera data[i] % j.

Sin --stdio, perf report abre una interfaz interactiva: navega con las flechas, pulsa Enter para expandir una función y a para anotarla.

Paso 5: Perfilar un proceso en marcha o todo el sistema

En un servidor real normalmente no lanzas el programa desde perf, sino que investigas un servicio que ya consume CPU. Primero, perf top muestra en tiempo real las funciones más activas de todo el sistema, como top pero por función:

sudo perf top

Pulsa q para salir. Si ves que un proceso concreto domina, obtén su PID (por ejemplo con pgrep -a nginx) y graba 30 segundos de muestras solo de ese proceso. El comando sleep 30 marca la duración de la captura:

sudo perf record -F 99 -g -p your_pid -- sleep 30
sudo perf report --stdio --no-children | head -n 50

Cuando no sabes qué proceso es el responsable, graba todo el sistema con -a y ordena el informe por proceso y biblioteca:

sudo perf record -F 99 -a -g -- sleep 30
sudo perf report --stdio --no-children --sort comm,dso | head -n 30
# Overhead  Command          Shared Object
    41.20%  php-fpm8.3       libphp8.3.so
    18.75%  mysqld           mysqld
     9.10%  php-fpm8.3       [kernel.kallsyms]
...

Este resumen te dice rápidamente si el tiempo se va en la aplicación, en la base de datos o en el kernel (red, sistema de ficheros).

Pilas de llamadas incompletas

Ubuntu 24.04 compila sus paquetes con puntero de marco, así que -g reconstruye bien las pilas de la mayoría de programas del sistema. Si perfilas un binario compilado sin él (muchos binarios de terceros), las pilas aparecerán cortadas o con [unknown]. En ese caso usa el método DWARF, que es más preciso pero genera ficheros mucho más grandes:

sudo perf record -F 99 --call-graph dwarf -p your_pid -- sleep 10

Paso 6: Generar un flame graph

Un flame graph representa todas las pilas muestreadas en una sola imagen: cada rectángulo es una función, el ancho es proporcional al tiempo que aparece en las muestras y las funciones que llama se apilan encima. Las funciones más anchas en la parte superior son las que consumen la CPU directamente.

Descarga los scripts de Brendan Gregg, el autor de la técnica:

git clone https://github.com/brendangregg/FlameGraph.git ~/FlameGraph

Convierte las muestras de perf.data en texto, agrupa las pilas y genera el SVG:

sudo perf script > out.perf
~/FlameGraph/stackcollapse-perf.pl out.perf > out.folded
~/FlameGraph/flamegraph.pl out.folded > flamegraph.svg

Comprueba que se ha generado:

ls -lh flamegraph.svg

Descarga flamegraph.svg a tu equipo, por ejemplo con scp, y ábrelo en un navegador. Es interactivo: al hacer clic en una función se amplía su rama y puedes buscar funciones por nombre.

scp your_user@your_server_ip:~/flamegraph.svg .

Para comparar antes y después de una optimización, genera un flame graph en cada estado con la misma duración de captura y compáralos, o usa difffolded.pl del mismo repositorio para obtener un flame graph diferencial.

Paso 7: Validar una optimización

El objetivo de perfilar es cambiar algo y comprobar que mejora. En el ejemplo, el cálculo de slow_sum no puede evitar la división, pero en una aplicación real la corrección típica es sacar trabajo repetido de un bucle, cachear un resultado o cambiar de algoritmo. Después de cada cambio:

  1. Repite perf stat varias veces con -r para obtener la media y la variación:

    sudo perf stat -r 5 ./hotspot
    
  2. Compara el tiempo total (seconds time elapsed) y la variación indicada con +-.

  3. Repite perf record y perf report para ver si el punto caliente ha cambiado de sitio.

Cambia una sola cosa cada vez; si cambias varias, no sabrás cuál ha tenido efecto.

Solución de problemas

WARNING: perf not found for kernel .... El paquete linux-tools no corresponde al kernel en ejecución, normalmente tras una actualización del kernel sin reiniciar o al revés. Instala linux-tools-$(uname -r) o reinicia al kernel nuevo.

Access to performance monitoring and observability operations is limited. Estás ejecutando perf sin sudo con kernel.perf_event_paranoid en 4. Usa sudo.

<not supported> en ciclos e instrucciones, o perf record sin muestras. La máquina virtual no expone la PMU. Usa -e cpu-clock.

Funciones como [unknown] o direcciones hexadecimales. El binario no tiene tabla de símbolos (se ha aplicado strip) o no tiene puntero de marco. Instala los símbolos de depuración del paquete si existen, compila con -g -fno-omit-frame-pointer o usa --call-graph dwarf.

perf.data enorme. Reduce la frecuencia (-F 49), la duración de la captura o evita --call-graph dwarf salvo que lo necesites.

Conclusión

Has usado perf stat para saber si un proceso está limitado por la CPU, perf record y perf report para encontrar la función responsable, perf annotate para bajar hasta la línea de código, y has perfilado procesos en marcha y todo el sistema antes de visualizarlo en un flame graph. Como siguientes pasos, puedes combinar perf con una prueba de carga con wrk para perfilar un servicio web bajo tráfico, usar strace cuando perf stat indique que el proceso espera en lugar de calcular, o explorar perf sched para analizar latencias del planificador.