wandres.dev
NIVEL DIOS · Síntesis y el método de depuración

La bisección como técnica general

El único algoritmo de depuración que existe, aplicado a los seis espacios donde se puede bisecar, y las condiciones que hacen que funcione o que falle.

⏱ 19 min

Si tuvieras que reducir toda la depuración a un solo procedimiento, sería este: partir el espacio de posibles causas en dos, comprobar en cuál de las dos mitades está el problema, y repetir. Es la única técnica que garantiza convergencia sin necesidad de entender el sistema, funciona en espacios de tipos muy distintos, y su eficiencia es logarítmica: veinte pasos bastan para localizar una causa entre un millón de candidatas.

🎯 Al terminar esta lección sabrás
  • Enunciar las tres condiciones que una bisección necesita para funcionar.
  • Aplicarla en los seis espacios biseccionables de una aplicación web.
  • Reconocer cuándo la bisección no es aplicable y qué hacer entonces.
  • Diseñar la comprobación que decide cada paso.

Las tres condiciones

Una bisección funciona si y solo si se cumplen tres cosas, y cuando falla es siempre porque una de las tres no se cumple.

Uno: una prueba fiable. Tiene que existir una comprobación que diga, sin ambigüedad, si el problema está presente. Si la comprobación es “me parece que va más lento”, no hay bisección posible: el ruido de la medición hace que las decisiones de cada paso sean aleatorias y el procedimiento converge a cualquier sitio.

Dos: monotonía. El problema tiene que estar en una de las dos mitades y no en las dos a la vez. Si hay dos causas independientes que producen el mismo síntoma, la bisección se vuelve loca: quitas la mitad, sigue fallando por la otra causa, y concluyes que estaba en la mitad equivocada.

Tres: independencia de las partes. Tiene que poder eliminarse una mitad sin romper la otra. En un espacio donde todo depende de todo, quitar la mitad produce fallos distintos que no informan.

Cuando la bisección no avanza, la reacción correcta no es insistir: es comprobar cuál de las tres condiciones falta.

⚠️
Cuidado

La condición que falla más a menudo es la segunda, y su síntoma es reconocible: la bisección converge y el punto al que llega no explica el síntoma completo. Cuando eso ocurre, casi siempre hay dos causas. La salida es arreglar la que has encontrado, comprobar que el síntoma cambia aunque no desaparezca, y volver a empezar el procedimiento sobre lo que queda.

Los seis espacios

Espacio uno: el tiempo, es decir, el historial. Si funcionaba y ahora no, hay un cambio culpable. El sistema de control de versiones tiene una operación específica para esto que automatiza la búsqueda binaria sobre el historial, y con un guion de comprobación puede hacerlo sola.

# Biseccion automatica sobre el historial con un guion que decide
git bisect start
git bisect bad                 # la version actual falla
git bisect good v2.14.0        # esta se sabe que iba bien

# El guion devuelve 0 si va bien y 1 si falla.
# git ejecuta, decide y salta al siguiente candidato, solo.
git bisect run ./comprobar-el-bug.sh

# Al terminar:
git bisect reset

El guion de comprobación es la parte que hay que escribir, y merece la pena aunque cueste veinte minutos: convierte una búsqueda manual de dos horas en una que se ejecuta sola mientras haces otra cosa. Si la comprobación no se puede automatizar, la bisección manual sigue siendo mucho más rápida que revisar el historial cambio a cambio.

Espacio dos: el código. Comentar la mitad, comprobar, repetir. Es la bisección más burda y sigue siendo eficaz cuando no hay nada mejor. Su versión estructurada es la reducción a un caso mínimo: quitar componentes hasta que solo quede lo imprescindible.

Espacio tres: los datos. Si falla con mil registros, prueba con quinientos. La bisección sobre los datos encuentra los problemas de escala y también los datos concretos que rompen algo: un campo nulo, un texto con un carácter inesperado, una fecha inválida.

Espacio cuatro: los recursos. Bloquear la mitad de los scripts, la mitad de las hojas de estilo, la mitad de los dominios de terceros. Es lo que hace posible el bloqueo por patrón del panel de red.

Espacio cinco: el entorno. Otro navegador, otro perfil, otra red, otro dispositivo, con extensiones y sin ellas, con caché y sin ella. Cada dimensión se biseca por separado.

Espacio seis: la configuración. Indicadores de característica, variables de entorno, opciones de compilación. Si la aplicación tiene veinte, la bisección encuentra la culpable en cinco pruebas.

Diseñar la comprobación

La calidad de la bisección depende enteramente de la comprobación que decide cada paso, y hay tres propiedades que la hacen buena.

Rápida. Se va a ejecutar entre diez y veinte veces. Si cuesta cinco minutos, la sesión entera es de una hora larga; si cuesta diez segundos, son tres minutos.

Binaria. Sí o no, sin grados. “Tarda un poco más” no sirve. Si el síntoma es de rendimiento, hay que convertirlo en binario con un umbral: por encima de tal valor, falla.

Aislada. No debe depender de nada que cambie entre ejecuciones. Estado limpio, datos fijos, red controlada.

// Comprobacion binaria para bisecar un problema de rendimiento
async function elBugEstaPresente() {
  // 1. Estado limpio: la comprobacion no debe depender de la anterior
  localStorage.clear();
  sessionStorage.clear();
  const caches_ = await caches.keys();
  await Promise.all(caches_.map(c => caches.delete(c)));

  // 2. Provocar el escenario, siempre igual
  const t0 = performance.now();
  await accionQueSospechas();
  const duracion = performance.now() - t0;

  // 3. Decision binaria con un umbral claramente por encima del ruido
  const UMBRAL_MS = 800;
  const presente = duracion > UMBRAL_MS;
  console.log(duracion.toFixed(0) + ' ms ->', presente ? 'BUG PRESENTE' : 'ok');
  return presente;
}

async function accionQueSospechas() {
  // Sustituir por el flujo real. Debe ser identico en cada ejecucion.
  document.querySelector('[data-test="abrir-lista"]')?.click();
  await new Promise(r => setTimeout(r, 50));
  await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
}

// Ejecutarla varias veces antes de bisecar, para conocer su fiabilidad
async function validarLaComprobacion(veces = 5) {
  const resultados = [];
  for (let i = 0; i < veces; i++) resultados.push(await elBugEstaPresente());
  const todosIguales = resultados.every(r => r === resultados[0]);
  console.log(todosIguales
    ? 'La comprobacion es estable. Se puede bisecar.'
    : 'La comprobacion es inestable. Bisecar con ella dara un resultado aleatorio.');
  return todosIguales;
}

La validación previa de la comprobación es el paso que casi nadie da y el que evita el peor resultado posible de una bisección: converger con confianza a un punto equivocado. Cinco ejecuciones sobre el mismo estado tienen que dar el mismo resultado cinco veces; si no, la bisección va a mentir.

Cuándo no es aplicable

Tres situaciones en las que hay que usar otra cosa.

El problema es intermitente y no se ha conseguido hacer determinista. Sin prueba fiable no hay bisección. La tarea previa es conseguir el determinismo: exagerar condiciones, fijar el tiempo y el azar, controlar la red.

El espacio no es divisible. Si el sistema es un bloque donde nada se puede quitar sin que todo deje de funcionar, no hay mitades. La salida es instrumentar en lugar de dividir: puntos de observación repartidos que digan por dónde pasa el flujo.

El problema es de comprensión, no de localización. A veces sabes perfectamente dónde ocurre y no entiendes por qué. Ahí la bisección no aporta nada porque ya has convergido; lo que hace falta es leer y razonar.

La bisección funciona sin entender el sistema, y esa es exactamente la razón de que sea la técnica más valiosa que puedes tener

La propiedad más subestimada de la bisección es que no requiere ningún conocimiento del sistema en el que se aplica. No necesitas saber qué hace ese código, cómo está organizado el proyecto, quién escribió qué ni por qué. Solo necesitas una prueba que distinga bien de mal y una forma de partir el espacio en dos. Eso la convierte en la única técnica que funciona igual de bien en el código que conoces de memoria y en un proyecto ajeno que abriste hace una hora, y explica por qué alguien con experiencia puede llegar a un sistema desconocido y localizar un bug antes que quien lleva dos años trabajando en él: no está usando conocimiento, está usando método, y el método escala donde el conocimiento no llega. De ahí salen dos consecuencias prácticas que merece la pena adoptar. La primera es que la primera pregunta ante un bug desconocido no debería ser “qué hace este código” sino “cómo puedo partir esto en dos”. La tentación de leer código para entender antes de actuar es fortísima y casi siempre es más lenta: leer un módulo de mil líneas cuesta media hora, y una bisección sobre el mismo módulo lo reduce a veinte líneas en cinco minutos, tras lo cual leer es trivial. Entender es más barato después de acotar, no antes. Y la segunda es que la inversión que más rendimiento da en un proyecto no es documentación sino biseccionabilidad: un historial con cambios pequeños y coherentes que se pueden probar uno a uno; indicadores de característica que permiten desactivar funcionalidades sin recompilar; datos de prueba que se pueden generar en cualquier volumen; un entorno que se levanta en segundos; una batería de comprobaciones que se pueden ejecutar sobre cualquier versión. Cada una de esas cosas parece infraestructura y en realidad es capacidad de diagnóstico, porque cada una añade una dimensión más sobre la que bisecar. Un proyecto con seis espacios biseccionables tiene bugs que se localizan en minutos; uno con cero tiene bugs que se investigan leyendo, y esos se miden en días.