Profiling en PHP: medir y mejorar el rendimiento con Xdebug, Blackfire y SPX

El profiling es el proceso de medir qué partes de tu código consumen más tiempo y memoria. En PHP, las herramientas principales son Xdebug en modo profiling, Blackfire.io y SPX. Antes de optimizar, mide: la intuición sobre el cuello de botella falla más de la mitad de las veces.

microtime() y memory_get_peak_usage() para medición puntual

<?php
function medirTiempo(callable $fn, string $etiqueta): void
{
    $inicio   = microtime(true);
    $memInicio = memory_get_usage();

    $fn();

    $fin  = microtime(true);
    $mem  = memory_get_peak_usage(true);

    printf(
        "[%s] Tiempo: %.4f s | Memoria pico: %sn",
        $etiqueta,
        $fin - $inicio,
        number_format($mem / 1024 / 1024, 2) . ' MB'
    );
}

medirTiempo(function () {
    $datos = array_map(fn($i) => $i * $i, range(1, 100_000));
    sort($datos);
}, 'array_map + sort');
?>

Xdebug en modo profiling

Xdebug genera ficheros Cachegrind que analizan herramientas como KCachegrind (Linux) o QCacheGrind (macOS/Windows).

[xdebug]
zend_extension=xdebug.so
xdebug.mode=profile
xdebug.output_dir=/tmp/xdebug-profiles
xdebug.profiler_output_name=cachegrind.out.%p.%t

Cada petición genera un fichero cachegrind.out.*. Abre ese fichero en KCachegrind para ver el árbol de llamadas, el tiempo inclusive/exclusivo y el número de invocaciones por función.

Activar el profiling solo en peticiones concretas

xdebug.mode=profile
xdebug.start_with_request=trigger

Con este ajuste, Xdebug solo genera el perfil cuando la petición incluye el trigger:

# GET con query string
curl 'http://localhost/lento.php?XDEBUG_PROFILE=1'

# Cookie (para sesiones autenticadas)
curl -b 'XDEBUG_PROFILE=1' http://localhost/dashboard

Blackfire.io

Blackfire es un servicio de profiling continuo con una interfaz web muy visual. Instala el agente y la extensión:

# Instalar agente
curl -sS https://packages.blackfire.io/gpg.key | sudo tee /etc/apt/trusted.gpg.d/blackfire.asc
sudo apt install blackfire blackfire-php

# Perfilar una URL
blackfire curl http://localhost/mi-endpoint

# Comparar dos versiones
blackfire curl --reference 1 http://localhost/mi-endpoint   # línea base
# (Haz cambios en el código)
blackfire curl http://localhost/mi-endpoint                 # comparativa

Blackfire muestra un grafo de llamadas interactivo, marca los cuellos de botella en rojo y proporciona una puntuación de rendimiento. Su modo --samples 5 ejecuta 5 peticiones y promedia los resultados para reducir el ruido.

SPX (Simple PHP eXtension)

SPX es una alternativa open source a Blackfire con interfaz web local, sin cuenta externa:

git clone https://github.com/NoiseByNorthwest/php-spx
cd php-spx && phpize && ./configure && make && sudo make install
extension=spx.so
spx.http_enabled=1
spx.http_key="clave-secreta"
spx.http_ip_whitelist="127.0.0.1"

Accede a http://localhost/?SPX_KEY=clave-secreta&SPX_UI_URI=/ para ver la interfaz. Activa el profiling con el parámetro SPX_ENABLED=1 en la URL.

Interpretando los resultados

  • Wall time (tiempo de pared): tiempo real transcurrido, incluye esperas de I/O.
  • CPU time: tiempo de procesador activo, excluye esperas.
  • Self time vs. inclusive time: el self time excluye el tiempo en funciones llamadas desde esa función. Si una función tiene alto inclusive pero bajo self, el cuello de botella está en sus hijos.
  • Memoria: picos altos en funciones de array o de strings grandes.

Patrones de optimización habituales

<?php
// LENTO: consulta dentro de un bucle (N+1)
foreach ($usuarios as $usuario) {
    $pedidos = $pdo->query("SELECT * FROM pedidos WHERE user_id = {$usuario['id']}")->fetchAll();
}

// RÁPIDO: una sola consulta con IN
$ids  = array_column($usuarios, 'id');
$in   = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM pedidos WHERE user_id IN ($in)");
$stmt->execute($ids);
$pedidos = $stmt->fetchAll();

// Agrupa por user_id en PHP
$pedidosPorUsuario = [];
foreach ($pedidos as $pedido) {
    $pedidosPorUsuario[$pedido['user_id']][] = $pedido;
}
?>

OPcache como primera optimización

opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0  ; en producción

Errores comunes

  • Perfilar en desarrollo, no en producción: el overhead de Xdebug en modo profile puede ser 10×-50×. Usa Blackfire con --samples bajo o SPX con trigger para producción.
  • Optimizar lo que no importa: una función llamada 1000 veces que tarda 0,01 ms da 10 ms en total. Enfócate en las que tienen alto inclusive time en el árbol principal.
  • No comparar antes y después: guarda el perfil de referencia antes de cambiar el código; sin comparativa, no sabes si mejoró o empeoró.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP