Depura WebAssembly con herramientas modernas

Ingvar Stepanyan
Ingvar Stepanyan

La ruta hasta ahora

Hace un año, Chrome anunció la compatibilidad inicial con la depuración nativa por WebAssembly en las Herramientas para desarrolladores de Chrome.

Demostramos compatibilidad básica con los pasos y hablamos sobre el uso de oportunidades de información de DWARF en lugar de los mapas de fuentes que se abran en el futuro:

  • Resuelve nombres de variables
  • Tipos de impresión con formato estilístico
  • Evalúa expresiones en los lenguajes de origen
  • ...y mucho más

Hoy nos complace presentar las funciones prometidas y el progreso de los equipos de Emscripten y Herramientas para desarrolladores de Chrome a lo largo de este año, en particular para las apps de C y C++.

Antes de comenzar, ten en cuenta que esta aún es una versión beta de la nueva experiencia, debes usar la versión más reciente de todas las herramientas bajo tu responsabilidad y, si tienes algún problema, infórmalo en https://bugs.chromium.org/p/chromium/issues/entry?template=DevTools+issue.

Comencemos con el mismo ejemplo simple de C que 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 compilarla, usamos Escripten más reciente y pasamos una marca -g, al igual que 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 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 auxiliar que se integre con las Herramientas para desarrolladores de Chrome y la ayude a comprender toda la información de depuración codificada en el archivo WebAssembly. Instálala desde este vínculo: goo.gle/wasm-debugging-extension.

También te recomendamos habilitar la depuración de WebAssembly en la sección Experimentos de Herramientas para desarrolladores. Abre las Herramientas para desarrolladores de Chrome, haz clic en el ícono de ajustes () en la esquina superior derecha del panel de Herramientas para desarrolladores, ve al panel Experimentos y marca la opción Depuración de WebAssembly: Habilitar la compatibilidad con DWARF.

Panel Experimentos de la configuración de Herramientas para desarrolladores

Cuando cierres la Configuración, Herramientas para desarrolladores sugerirá que se vuelva a cargar para aplicar la configuración, así que hagamos precisamente eso. Eso es todo con la configuración única.

Ahora podemos volver al panel Fuentes, habilitar Pausar en excepciones (ícono ⏸) y, luego, marcar Pausar en excepciones detectadas y volver a cargar la página. Deberías ver las Herramientas para desarrolladores detenidas en una excepción:

Captura de pantalla del panel Sources en la que se muestra cómo habilitar la opción “Detener en excepciones detectadas”

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

Herramientas para desarrolladores detenidas en la función `assert_less` y mostrando valores de `x` e `y` en la vista Scope

Ahora, si observas la vista Scope, podrás ver los nombres y valores originales de las variables en el código C/C++, y ya no tendrás que entender qué significan los nombres alterados 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 estructuras, clases, arrays, etcétera.

Compatibilidad con tipos enriquecidos

Veamos un ejemplo más complicado para explicarlos. 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 archivo único que contiene 50 líneas de código, pero esta vez también uso algunas APIs externas, como la biblioteca SD para gráficos y números complejos de la biblioteca C++ estándar.

Voy a compilarlo con la misma marca -g anterior 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 Herramientas para desarrolladores, 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 un punto de interrupción al comienzo del código.

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

Se pausaron las Herramientas para desarrolladores en la llamada “SDL_Init”.

Ya podemos ver todas las variables a la derecha, pero solo width y height se inicializan en este momento, por lo que no hay mucho que inspeccionar.

Estableceremos otro punto de interrupción dentro de nuestro bucle principal de Mandelbrot y reanudaremos la ejecución para saltar un poco hacia adelante.

Herramientas para desarrolladores detenidas dentro de los bucles anidados

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

Si deseas acceder a una propiedad profundamente anidada que es difícil de 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;

Reanudamos la ejecución varias veces y veamos cómo también cambia el x interno. Para ello, volveremos a buscar en la vista Scope, agregaremos el nombre de la variable a la lista de observación, lo evaluamos en la consola o coloca el cursor sobre la variable en el código fuente:

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

A partir de aquí, podemos recorrer o pasar declaraciones de C++ y observar cómo otras variables también cambian:

Cuadros de información y la vista Scope que muestran los valores de &quot;color&quot;, &quot;point&quot; y otras variables

Todo esto funciona muy bien cuando hay información de depuración disponible, pero ¿qué pasa 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 mismos a partir de la fuente, por lo que, al menos, actualmente no hay forma de que el depurador encuentre fuentes asociadas. Regresemos a SDL_RenderDrawColor:

Herramientas para desarrolladores que muestran la vista de desensamblado de &quot;mandelbrot.wasm&quot;

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

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

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

En primer lugar, si ya usaste la depuración de WebAssembly sin procesar, podrías notar que el desensamblado completo ahora se muestra en un solo archivo, sin tener que adivinar a qué función corresponde 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 desensamblado. Antes, solo se veían índices numéricos o, en el caso de las funciones, ningún nombre.

Ahora, generamos nombres de manera similar a otras herramientas de desensamblado, mediante el uso de sugerencias de la sección de nombres de WebAssembly, las rutas de importación y exportación y, por último, si todo lo demás falla, la generación se basa en el 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 desensamblajes un poco más legibles.

Cuando no hay información de tipos disponible, puede ser difícil inspeccionar cualquier valor además de las primitivas. Por ejemplo, los punteros se mostrarán como números enteros regulares, sin la posibilidad 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 WebAssembly, representado por env.memory en la vista Scope para buscar bytes individuales. Esto funcionó en algunas situaciones triviales, pero no fue particularmente conveniente para expandir y no permitió volver a interpretar los datos en formatos que no fueran valores de bytes. También agregamos una función nueva para ayudar con esto: un inspector de memoria lineal.

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

Menú contextual de “env.memory” en el panel Alcance que muestra un elemento “Inspeccionar memoria”

Una vez que hagas clic, se abrirá el Inspector de memoria, en el que podrás 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 de Herramientas para desarrolladores que muestra vistas hexadecimales y ASCII de la memoria

Situaciones y advertencias avanzadas

Cómo generar perfiles del código de WebAssembly

Cuando abres Herramientas para desarrolladores, el código de WebAssembly se “nivela” 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 ni otros métodos para medir la velocidad de tu código mientras las Herramientas para desarrolladores están abiertas, ya que los números que obtengas no representarán el rendimiento real.

En su lugar, debes usar el panel Rendimiento de Herramientas para desarrolladores, que ejecutará el código a toda velocidad y te proporcionará un desglose detallado del tiempo que se dedicó a las diferentes funciones:

Panel de creación de perfiles en el que se muestran varias funciones de Wasm

Como alternativa, puedes ejecutar tu aplicación con las Herramientas para desarrolladores cerradas y abrirlas una vez que hayas terminado para inspeccionar la Consola.

Mejoraremos las situaciones de generación de perfiles en el futuro, pero, por ahora, es una advertencia. Si deseas obtener más información sobre las situaciones de niveles de WebAssembly, consulta nuestros documentos sobre la canalización de compilación de WebAssembly.

Compilación y depuración en diferentes máquinas (incluido Docker / host)

Cuando compiles en un Docker, en una máquina virtual o en un servidor de compilación remoto, es probable que te encuentres con situaciones en las que las rutas a los archivos fuente usadas durante la compilación no coincidan con las rutas 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 de acceso en las opciones de extensión de C/C++. Puedes usarlo para reasignar rutas de acceso arbitrarias y ayudar a las Herramientas para desarrolladores a ubicar las fuentes.

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

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

El primer prefijo coincidente es “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 están inhabilitadas las optimizaciones. Las optimizaciones pueden intercalar funciones entre sí, reordenar el código o quitar partes de él por completo. Todo esto puede confundir al depurador y, en consecuencia, a ti como usuario.

Si no te importa una experiencia de depuración más limitada y aún deseas depurar una compilación optimizada, la mayoría de las optimizaciones funcionarán como se espera, excepto la intercalació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, tipos definidos, variables, funciones, alcances y ubicaciones, todo lo que pueda 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 WebAssembly, puedes dividir 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 deseado:

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 auxiliar podrá ubicarlo y cargarlo cuando abras Herramientas para desarrolladores.

Cuando se combina con optimizaciones como las descritas 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 complementario local. En este caso, también 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á...

¡Vaya! Esas son muchas funciones nuevas.

Con todas esas integraciones nuevas, las Herramientas para desarrolladores de Chrome se convierten 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 multiplataforma compartida.

Sin embargo, nuestro recorrido aún no terminó. Algunas de las cosas en que trabajaremos a partir de aquí:

  • Limpiar las asperezas de la experiencia de depuración
  • Se agregó compatibilidad con formateadores de tipos personalizados
  • Estamos trabajando en mejoras en la generación de perfiles para las apps de WebAssembly.
  • Se agregó compatibilidad con la cobertura de código para facilitar la búsqueda de código sin usar.
  • Se mejoró la compatibilidad con expresiones en la evaluación de la consola.
  • Agregamos compatibilidad con más idiomas.
  • …y mucho más

Mientras tanto, ayúdanos a probar la versión beta actual en tu propio código e informa cualquier problema encontrado a https://bugs.chromium.org/p/chromium/issues/entry?template=DevTools+issue.

Descarga los canales de vista previa

Considera usar Canary, Dev o Beta de Chrome como tu navegador de desarrollo predeterminado. Estos canales de vista previa te brindan acceso a las funciones más recientes de Herramientas para desarrolladores, prueba APIs de plataformas web de vanguardia y encuentra problemas en tu sitio antes que tus usuarios.

Cómo comunicarse con el equipo de Herramientas para desarrolladores de Chrome

Usa las siguientes opciones para analizar las nuevas funciones y los cambios en la publicación, o cualquier otra cosa relacionada con Herramientas para desarrolladores.

  • Envíanos tus sugerencias o comentarios a través de crbug.com.
  • Informa un problema en Herramientas para desarrolladores mediante Más opciones   Más   > Ayuda > Informar problemas con Herramientas para desarrolladores en Herramientas para desarrolladores.
  • Envía un tweet a @ChromeDevTools.
  • Deja comentarios en los videos de YouTube de las Novedades de las Herramientas para desarrolladores o en las sugerencias de Herramientas para desarrolladores (videos de YouTube).