Depura WebAssembly con herramientas modernas

Ingvar Stepanyan
Ingvar Stepanyan

El camino recorrido hasta ahora

Hace un año, Chrome anunció la compatibilidad inicial con la depuración nativa de WebAssembly en Chrome DevTools.

Demostramos la compatibilidad con los pasos básicos y hablamos sobre las oportunidades que nos brinda el uso de información de DWARF en lugar de mapas de origen en el futuro:

  • Cómo resolver nombres de variables
  • Tipos de impresión con formato
  • Cómo evaluar expresiones en idiomas de origen
  • …y mucho más.

Hoy, nos complace mostrar las funciones prometidas que cobran vida y el progreso que los equipos de Emscripten y Chrome DevTools han logrado a lo largo de este año, en particular, para las apps de C y C++.

Antes de comenzar, ten en cuenta que esta sigue siendo una versión beta de la nueva experiencia. Debes usar la versión más reciente de todas las herramientas bajo tu propio riesgo. Si tienes algún problema, infórmalo en https://issues.chromium.org/issues/new?noWizard=true&template=0&component=1456350.

Comencemos con el mismo ejemplo simple de C que usamos la última vez:

#include <stdlib.h>

void assert_less(int x, int y) {
  if (x >= y) {
    abort();
  }
}

int main() {
  assert_less(10, 20);
  assert_less(30, 20);
}

Para compilarlo, usamos la versión más reciente de Emscripten y pasamos una marca -g, como en la publicación original, para incluir información de depuración:

emcc -g temp.c -o temp.html

Ahora podemos entregar la página generada desde un servidor HTTP de localhost (por ejemplo, con serve) y abrirla en la versión más reciente de Chrome Canary.

Esta vez, también necesitaremos una extensión de ayuda que se integre en DevTools de Chrome y la ayude a comprender toda la información de depuración codificada en el archivo WebAssembly. Para instalarla, ve a este vínculo: goo.gle/wasm-debugging-extension

También querrás habilitar la depuración de WebAssembly en Experiments de DevTools. Abre las herramientas para desarrolladores de Chrome, haz clic en el ícono de ajustes () en la esquina superior derecha del panel de DevTools, ve al panel Experiments y marca WebAssembly Debugging: Enable DWARF support.

Panel Experiments de la configuración de DevTools

Cuando cierres Configuración, DevTools te sugerirá que se vuelva a cargar para aplicar la configuración, así que hagámoslo. Eso es todo para la configuración única.

Ahora podemos volver al panel Sources, habilitar Pause on exceptions (ícono ⏸), luego marcar Pause on caught exceptions y volver a cargar la página. Deberías ver DevTools detenido en una excepción:

Captura de pantalla del panel Sources en la que se muestra cómo habilitar &quot;Pause on caught exceptions&quot;

De forma predeterminada, se detiene en un código de unión generado por Emscripten, pero a la derecha, puedes ver una vista Call Stack que representa el seguimiento de pila del error y puedes navegar a la línea C original que invocó abort:

DevTools se detuvo en la función &quot;assert_less&quot; y muestra los valores de &quot;x&quot; e &quot;y&quot; en la vista de alcance.

Ahora, si miras en la vista Alcance, puedes ver los nombres originales y los valores de las variables en el código C/C++, y ya no tienes que averiguar qué significan nombres dañados como $localN y cómo se relacionan con el código fuente que escribiste.

Esto se aplica no solo a los valores primitivos, como los números enteros, sino también a los tipos compuestos, como las estructuras, las clases, los arrays, etcétera.

Compatibilidad con tipos enriquecidos

Veamos un ejemplo más complicado para mostrarlos. Esta vez, dibujaremos un fractal de Mandelbrot con el siguiente código C++:

#include <SDL2/SDL.h>
#include <complex>

int main() {
  // Init SDL.
  int width = 600, height = 600;
  SDL_Init(SDL_INIT_VIDEO);
  SDL_Window* window;
  SDL_Renderer* renderer;
  SDL_CreateWindowAndRenderer(width, height, SDL_WINDOW_OPENGL, &window,
                              &renderer);

  // Generate a palette with random colors.
  enum { MAX_ITER_COUNT = 256 };
  SDL_Color palette[MAX_ITER_COUNT];
  srand(time(0));
  for (int i = 0; i < MAX_ITER_COUNT; ++i) {
    palette[i] = {
        .r = (uint8_t)rand(),
        .g = (uint8_t)rand(),
        .b = (uint8_t)rand(),
        .a = 255,
    };
  }

  // Calculate and draw the Mandelbrot set.
  std::complex<double> center(0.5, 0.5);
  double scale = 4.0;
  for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
      std::complex<double> point((double)x / width, (double)y / height);
      std::complex<double> c = (point - center) * scale;
      std::complex<double> z(0, 0);
      int i = 0;
      for (; i < MAX_ITER_COUNT - 1; i++) {
        z = z * z + c;
        if (abs(z) > 2.0)
          break;
      }
      SDL_Color color = palette[i];
      SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);
      SDL_RenderDrawPoint(renderer, x, y);
    }
  }

  // Render everything we've drawn to the canvas.
  SDL_RenderPresent(renderer);

  // SDL_Quit();
}

Puedes ver que esta aplicación sigue siendo bastante pequeña (es un solo archivo que contiene 50 líneas de código), pero esta vez también uso algunas APIs externas, como la biblioteca SDL para gráficos, así como números complejos de la biblioteca estándar de C++.

La compilaré con la misma marca -g que antes para incluir información de depuración y también le pediré a Emscripten que proporcione la biblioteca SDL2 y permita memoria de tamaño arbitrario:

emcc -g mandelbrot.cc -o mandelbrot.html \
     -s USE_SDL=2 \
     -s ALLOW_MEMORY_GROWTH=1

Cuando visito la página generada en el navegador, puedo ver la hermosa forma fractal con algunos colores aleatorios:

Página de demostración

Cuando abro DevTools, una vez más, puedo ver el archivo C++ original. Sin embargo, esta vez, no tenemos un error en el código (uf), así que establezcamos algunos puntos de interrupción al comienzo de nuestro código.

Cuando volvamos a cargar la página, el depurador se detendrá dentro de nuestra fuente de C++:

DevTools se detuvo en la llamada a &quot;SDL_Init&quot;.

Ya podemos ver todas nuestras variables a la derecha, pero solo width y height están inicializadas en este momento, por lo que no hay mucho que inspeccionar.

Establezcamos otro punto de interrupción dentro de nuestro bucle principal de Mandelbrot y reanudemos la ejecución para avanzar un poco.

DevTools se detuvo dentro de los bucles anidados

En este punto, nuestro palette se llenó con algunos colores aleatorios, y podemos expandir el array en sí, así como las estructuras SDL_Color individuales, e inspeccionar sus componentes para verificar que todo se vea bien (por ejemplo, que el canal "alpha" siempre esté configurado en opacidad completa). De manera similar, podemos expandir y verificar las partes reales e imaginarias del número complejo almacenado en la variable center.

Si deseas acceder a una propiedad anidada de forma profunda a la que, de otro modo, es difícil navegar a través de la vista Alcance, también puedes usar la evaluación de Console. Sin embargo, ten en cuenta que aún no se admiten expresiones C++ más complejas.

Panel de la consola que muestra el resultado de &quot;palette[10].r&quot;

Resumamos la ejecución varias veces y podremos ver cómo también cambia el x interno. Para ello, podemos volver a mirar la vista Scope, agregar el nombre de la variable a la lista de vigilancia, evaluarla en la consola o colocar el cursor sobre la variable en el código fuente:

Información sobre herramientas sobre la variable &quot;x&quot; en la fuente que muestra su valor &quot;3&quot;

Desde aquí, podemos ingresar o omitir instrucciones de C++ y observar cómo también cambian otras variables:

Información sobre herramientas y vista de alcance que muestran valores de &quot;color&quot;, &quot;punto&quot; y otras variables

De acuerdo, todo esto funciona muy bien cuando hay información de depuración disponible, pero ¿qué sucede si queremos depurar un código que no se compiló con las opciones de depuración?

Depuración de WebAssembly sin procesar

Por ejemplo, le pedimos a Emscripten que nos proporcionara una biblioteca de SDL compilada previamente, en lugar de compilarla nosotros desde la fuente, por lo que, al menos por el momento, el depurador no tiene forma de encontrar fuentes asociadas. Volvamos a entrar en SDL_RenderDrawColor:

DevTools muestra la vista de desmontaje de &quot;mandelbrot.wasm&quot;

Volvemos a la experiencia de depuración sin procesar de WebAssembly.

Puede parecer un poco aterrador y no es algo con lo que la mayoría de los desarrolladores web deban lidiar, pero, en ocasiones, es posible que desees depurar una biblioteca compilada sin información de depuración, ya sea porque es una biblioteca de terceros sobre la que no tienes control o porque te encuentras con uno de esos errores que solo se producen en producción.

Para ayudar en esos casos, también realizamos algunas mejoras en la experiencia básica de depuración.

En primer lugar, si antes usaste la depuración de WebAssembly sin procesar, es posible que notes que todo el desmontaje ahora se muestra en un solo archivo. No es necesario adivinar a qué función puede corresponder una entrada wasm-53834e3e/ wasm-53834e3e-7 de Sources.

Nuevo esquema de generación de nombres

También mejoramos los nombres en la vista de desmontaje. Anteriormente, solo veías índices numéricos o, en el caso de las funciones, ningún nombre.

Ahora generamos nombres de manera similar a otras herramientas de desmontaje. Para ello, usamos sugerencias de la sección de nombres de WebAssembly, las rutas de importación/exportación y, por último, si todo lo demás falla, los generamos en función del tipo y el índice del elemento, como $func123. Puedes ver cómo, en la captura de pantalla anterior, esto ya ayuda a obtener seguimientos de pila y desmontaje un poco más legibles.

Cuando no hay información de tipo disponible, puede ser difícil inspeccionar cualquier valor además de las primitivas; por ejemplo, los punteros aparecerán como números enteros normales, sin forma de saber qué se almacena detrás de ellos en la memoria.

Inspección de memoria

Anteriormente, solo podías expandir el objeto de memoria de WebAssembly, representado por env.memory en la vista Scope, para buscar bytes individuales. Esto funcionó en algunas situaciones triviales, pero no era particularmente conveniente para expandirse y no permitía reinterpretar los datos en formatos distintos de los valores de bytes. También agregamos una nueva función para ayudar con esto: un inspector de memoria lineal.

Si haces clic con el botón derecho en env.memory, deberías ver una nueva opción llamada Inspeccionar memoria:

Menú contextual en &quot;env.memory&quot; en el panel de alcance que muestra un elemento &quot;Inspect Memory&quot;

Una vez que hagas clic, aparecerá un Inspector de memoria, en el que puedes inspeccionar la memoria de WebAssembly en vistas hexadecimales y ASCII, navegar a direcciones específicas y, además, interpretar los datos en diferentes formatos:

Panel del Inspector de memoria en DevTools que muestra vistas hexadecimales y ASCII de la memoria

Situaciones y advertencias avanzadas

Genera perfiles del código de WebAssembly

Cuando abres Herramientas para desarrolladores, el código de WebAssembly se “reduce” a una versión no optimizada para habilitar la depuración. Esta versión es mucho más lenta, lo que significa que no puedes confiar en console.time, performance.now y otros métodos para medir la velocidad de tu código mientras DevTools está abierta, ya que las cifras que obtengas no representarán el rendimiento real en absoluto.

En su lugar, debes usar el panel Rendimiento de DevTools, que ejecutará el código a la velocidad máxima y te proporcionará un desglose detallado del tiempo dedicado a las diferentes funciones:

Panel de perfiles que muestra varias funciones de Wasm

Como alternativa, puedes ejecutar tu aplicación con DevTools cerrado y abrirlo una vez que termines para inspeccionar Console.

Mejoraremos las situaciones de generación de perfiles en el futuro, pero, por ahora, ten en cuenta esta advertencia. Si deseas obtener más información sobre las situaciones de nivelación de WebAssembly, consulta nuestra documentación sobre la canalización de compilación de WebAssembly.

Compila y depura en diferentes máquinas (incluidos Docker o el host)

Cuando compiles en Docker, una máquina virtual o un servidor de compilación remoto, es probable que te encuentres con situaciones en las que las rutas de acceso a los archivos de origen que se usan durante la compilación no coincidan con las rutas de acceso de tu propio sistema de archivos en el que se ejecutan las herramientas para desarrolladores de Chrome. En este caso, los archivos aparecerán en el panel Sources, pero no se cargarán.

Para solucionar este problema, implementamos una funcionalidad de asignación de ruta en las opciones de extensión de C/C++. Puedes usarlo para volver a asignar rutas arbitrarias y ayudar a las herramientas para desarrolladores a ubicar fuentes.

Por ejemplo, si el proyecto en tu máquina host se encuentra en una ruta C:\src\my_project, pero se compiló dentro de un contenedor de Docker en el que esa ruta se representó como /mnt/c/src/my_project, puedes volver a asignarla durante la depuración especificando esas rutas como prefijos:

Página de opciones de la extensión de depuración de C/C++

El primer prefijo que coincida "gana". Si conoces otros depuradores de C++, esta opción es similar al comando set substitute-path en GDB o a una configuración target.source-map en LLDB.

Cómo depurar compilaciones optimizadas

Al igual que con cualquier otro lenguaje, la depuración funciona mejor si se inhabilitan las optimizaciones. Las optimizaciones pueden intercalar funciones una dentro de otra, reordenar el código o quitar partes del código por completo, y todo esto puede confundir al depurador y, en consecuencia, a ti como usuario.

Si no te importa tener una experiencia de depuración más limitada y aún quieres depurar una compilación optimizada, la mayoría de las optimizaciones funcionarán como se espera, excepto la inserción de funciones. Planeamos abordar los problemas restantes en el futuro, pero, por ahora, usa -fno-inline para inhabilitarlo cuando compiles con cualquier optimización a nivel de -O, p.ej.:

emcc -g temp.c -o temp.html \
     -O3 -fno-inline

Separación de la información de depuración

La información de depuración conserva muchos detalles sobre tu código, los tipos definidos, las variables, las funciones, los alcances y las ubicaciones, todo lo que podría ser útil para el depurador. Como resultado, a menudo puede ser más grande que el código en sí.

Para acelerar la carga y la compilación del módulo de WebAssembly, te recomendamos que dividas esta información de depuración en un archivo WebAssembly independiente. Para hacerlo en Emscripten, pasa una marca -gseparate-dwarf=… con el nombre de archivo que desees:

emcc -g temp.c -o temp.html \
     -gseparate-dwarf=temp.debug.wasm

En este caso, la aplicación principal solo almacenará un nombre de archivo temp.debug.wasm, y la extensión de ayuda podrá encontrarlo y cargarlo cuando abras DevTools.

Cuando se combina con optimizaciones como las que se describieron anteriormente, esta función incluso se puede usar para enviar compilaciones de producción casi optimizadas de tu aplicación y, luego, depurarlas con un archivo lateral local. En este caso, además, tendremos que anular la URL almacenada para ayudar a la extensión a encontrar el archivo lateral, por ejemplo:

emcc -g temp.c -o temp.html \
     -O3 -fno-inline \
     -gseparate-dwarf=temp.debug.wasm \
     -s SEPARATE_DWARF_URL=file://[local path to temp.debug.wasm]

Continuará…

¡Uf! Eso fue mucha información sobre las funciones nuevas.

Con todas esas integraciones nuevas, Chrome DevTools se convierte en un depurador viable y potente, no solo para JavaScript, sino también para apps de C y C++, lo que facilita más que nunca tomar apps compiladas en una variedad de tecnologías y llevarlas a una Web compartida y multiplataforma.

Sin embargo, nuestro viaje aún no termina. Estas son algunas de las funciones en las que trabajaremos a partir de ahora:

  • Se corrigieron los problemas de la experiencia de depuración.
  • Se agregó compatibilidad con los formateadores de tipos personalizados.
  • Se están realizando mejoras en la generación de perfiles para apps de WebAssembly.
  • Se agregó compatibilidad con la cobertura de código para facilitar la búsqueda de código no utilizado.
  • Se mejoró la compatibilidad con las expresiones en la evaluación de la consola.
  • Se agregó compatibilidad con más idiomas.
  • …y mucho más

Mientras tanto, ayúdanos probando la versión beta actual en tu propio código y, luego, informa cualquier problema que encuentres en https://issues.chromium.org/issues/new?noWizard=true&template=0&component=1456350.

Descarga los canales de vista previa

Considera usar Chrome Canary, Dev o Beta como tu navegador de desarrollo predeterminado. Estos canales de versión preliminar te brindan acceso a las funciones más recientes de DevTools, te permiten probar las APIs de plataformas web de vanguardia y te ayudan a encontrar problemas en tu sitio antes que tus usuarios.

Comunícate con el equipo de Chrome DevTools

Usa las siguientes opciones para hablar sobre las funciones nuevas, las actualizaciones o cualquier otro tema relacionado con DevTools.