lunes, 30 de noviembre de 2015

Cuando el backtrace no dice nada...

Una nueva versión de ZinjaI se aproxima. En realidad, las vacaciones se aproximan, y eso probablemente haga que se aproximen nuevas versiones de todo. Pero hoy les cuento de ZinjaI. No hay grandes super-novedades en esta versión. Al menos no hasta el momento. Sí hay un millón de pequeños cambios y correcciones que juntos suman. Pero entre lo poco destacable hay tres "pequeñas" mejoras en la interfaz de depuración que pueden ser muy útiles.

Siempre digo que si uno ve explotar su programa, debe, sin pensarlo, por instinto, presionar F5 (Depuración->Iniciar) y repetir la corrida. Es decir, lo primero que hay que hacer, es reventarlo otra vez, pero ahora con el depurador. La única excepción a esta regla se da cuando tenemos activada por defecto la generación de core-dumps (solo en GNU/Linux, activable en ZinjaI desde Archivo->Preferencias...->Depuración->Avanzado). En ese caso, lo primero que podemos hacer es cargar el core dump (Depuración->Más->Cargar Volcado de Memoria...). En cualquiera de las dos situaciones, el objetivo es inspeccionar el estado del programa en el momento justo de la explosión.

El backtrace tiene todo lo necesario para detectar el problema

En la mayoría de los casos, casi todo lo que necesitamos está en el trazado inverso. El trazado inverso (o backtrace) dice quién llamó a quién (hablando de métodos y funciones) y con qué argumentos. Es decir, cómo llegamos a ese punto en el que explota. Si el programa está bien modularizado eso puede ser suficiente. Además, muchos errores comunes se hacen evidentes allí. Por ejemplo, si invocamos un método con un puntero nulo, en la lista de argumentos veremos "this=0x0", y no habrá dudas de que eso será un error. Y en el 99% de los casos en los que el backtrace solo no alcanza, un par de inspecciones en el frame adecuado terminan de resolver el misterio.

Pero ¿qué pasa cuando no hay información útil en el backtrace? ¿o cuando directamente no hay backtrace? Esto no es tan raro. Puede deberse a dos situaciones. Una es que metimos tanto la pata con el manejo de memoria (usando punteros o índices incorrectos) que destruimos el stack (el área de memoria donde el programa guarda la información que muestra el backtrace). Otra es cuando la "explosión" no es para el depurador una salida del todo anormal. Por ejemplo, si el programa tiene algún mecanismo de salida propio al detectar el error (asserts, manejadores de señales, ifs clásicos puestos a mano, excepciones, trucos raros de biblitecas que usamos, etc). En cualquier caso, el resultados es que a veces no vemos el backtrace que queremos.

El backtrace no aporta información útil

Si cuando el depurador detecta la "explosión", o la finalización del programa, no tenemos un trazado útil, por el motivo que sea, en general el paso 2 es tratar de acercarnos con puntos de interrupción, y paso a paso, lo más que podamos al punto del error, para analizar esos instantes previos. Supongamos un programa simple, que tiene solo su función main, con algún ejercicio adentro y nada más. Y que alguna de las acciones de ese ejercicio genera una de estas explosiones. Entonces, lo que hay que hacer es ejecutar paso a paso el main hasta que el programa explote. Cuando el programa explote, habremos visto hasta donde el main avanzó sin problemas, y cual es la línea de la cual nunca volvió. Entonces, tendremos que volver a ejecutar el programa, justo hasta esa línea, y analizar qué sucede allí, justo antes de que vuelva a explotar.

En un programa que consta de solo una función, esto es trivial. En un programa real, esto es mucho más complicado. No siempre es simple determinar cual es la función a analizar, no siempre se puede analizar todo, a veces cuesta (tiempo por ejemplo) llegar al punto del error en la ejecución, etc. Y para peor, a veces explota cuando no lo esperamos, y entonces resulta que no habíamos prestado mucha atención a dónde estabamos antes de la explosión y tenemos que repetir el experimento.

Bien, luego de tanta introducción, tengo tres funcionalidaes nuevas para facilitar y/o evitar esta tarea. Una es la opción "Ver trazado anterior" del menú contextual del trazado inverso. Cuando venimos avanzando paso a paso de forma inesperada y el programa de pronto explota, esta opción nos permite volver a ver el trazado anterior al actual. El actual será el de la explosión, el anterior será el último válido que vimos antes de que explote. Un detalle importante: cuando vemos un backtrace que no es el actual, no podemos inspeccionar expresiones o variables para ese estado anterior. Es decir, ZinjaI solo guarda el backtrace, pero no todo el estado, porque eso sería carísimo.

Podemos volver a ver el último backtrace, o comenzar a registrar un historial completo

La siguiente funcionalidad, también en el menú contextual del panel de trazado inverso, es la opción "Generar historial". Este ítem abre una nueva ventana, que irá registrando todos los trazados que veamos, en todas las detenciones del depurador, y nos permitirá volver a ver cualquiera de ellas. Entonces, si no tenemos idea de dónde se da la explosión, podemos abrir esta ventana, avanzar paso a paso a lo bestia, y luego de que explote, volver hacia atrás en el tiempo (ya no un solo paso, sino varios) para ver cómo llegamos a ese punto, linea por linea.

La ventana de historial permite volver a ver todos los backtraces anteriores.
El punto de ejecución se marca en gris por no ser el actual, sino uno del historial.

Y para completarla, y que esta ventana sea un poquito más útil, ahora el paso a paso se puede hacer automáticamente. En el menú "Depuración" tenemos una opción nueva que dice "Repetir Step In/Over Automáticamente". Cuando esté activada, como su nombre lo indica, los comandos Step In y Step Over se repetirán automáticamente hasta que el programa explote, termine, o llegue a un punto de interrupción.

Este sería en realidad el primer paso. Entonces la secuencia queda: Detener el programa antes de
que explote, activar la repetición automática, iniciar un historial de backtrace, y darle a "step in".

Entonces, el proceso para ver dónde explotó cuando el backtrace final no es útil sería el siguiente. Poner un punto de interrupción al comienzo de la función sospechosa. Activar la repetición automática de los Steps. Abrir una ventana de historial de backtrace. Y click en Step In, o en Step Over. El programa comenzará a avanzar paso a paso automáticamente, lo más rápido que pueda, y registrando el backtrace completo en la ventana del historial para cada paso. Cuando reviente, podremos reconstruir todo el camino.

Aquí describí el escenario en el cual me basé para pensar estas nuevas funcionalidades, pero supongo que con el tiempo les encontraremos otros usos adicionales. Me queda como trabajo pendiente ver cómo se combina esto con múltiples hilos (si el historial sigue a uno solo, a todos, o qué, por el momento creo que hace una mezcla). Mientras tanto, manténganse atentos a las actualizaciones. Los cambios ya están el repositorio, y muy pronto estarán disponibles también en una nueva versión "estable".

No hay comentarios:

Publicar un comentario