Depura WebAssembly más rápido

Philip Pfaffe
Kim-Anh Tran
Kim-Anh Tran
Eric Leese
Eric Leese
Sam Clegg

En la Chrome Dev Summit 2020, demostramos por primera vez en la Web la compatibilidad de Chrome con la depuración de aplicaciones de WebAssembly. Desde entonces, el equipo ha invertido mucha energía en hacer que la experiencia del desarrollador se ajuste a aplicaciones grandes y hasta enormes. En esta publicación, te mostraremos los controles que agregamos (o que funcionan) en las diferentes herramientas y cómo usarlos.

Depuración escalable

Retomemos desde donde lo dejamos en nuestra publicación de 2020. Este es el ejemplo que vimos en ese momento:

#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();
}

Sigue siendo un ejemplo bastante pequeño y es probable que no veas ninguno de los problemas reales que verías en una aplicación realmente grande, pero aún podemos mostrarte cuáles son las funciones nuevas. La configuración es rápida y fácil, y pruébala por tu cuenta.

En la última publicación, analizamos cómo compilar y depurar este ejemplo. Volvamos a hacerlo, pero también echemos un vistazo a //performance//:

$ emcc -sUSE_SDL=2 -g -O0 -o mandelbrot.html mandelbrot.cc -sALLOW_MEMORY_GROWTH

Este comando produce un objeto binario wasm de 3 MB. Y la mayor parte de eso, como puedes esperar, es información de depuración. Puedes verificarlo con la herramienta de llvm-objdump [1], por ejemplo:

$ llvm-objdump -h mandelbrot.wasm

mandelbrot.wasm:        file format wasm

Sections:
Idx Name          Size     VMA      Type
  0 TYPE          0000026f 00000000
  1 IMPORT        00001f03 00000000
  2 FUNCTION      0000043e 00000000
  3 TABLE         00000007 00000000
  4 MEMORY        00000007 00000000
  5 GLOBAL        00000021 00000000
  6 EXPORT        0000014a 00000000
  7 ELEM          00000457 00000000
  8 CODE          0009308a 00000000 TEXT
  9 DATA          0000e4cc 00000000 DATA
 10 name          00007e58 00000000
 11 .debug_info   000bb1c9 00000000
 12 .debug_loc    0009b407 00000000
 13 .debug_ranges 0000ad90 00000000
 14 .debug_abbrev 000136e8 00000000
 15 .debug_line   000bb3ab 00000000
 16 .debug_str    000209bd 00000000

Este resultado nos muestra todas las secciones que se encuentran en el archivo wasm generado. La mayoría son secciones estándar de WebAssembly, pero también hay varias secciones personalizadas cuyo nombre comienza con .debug_. Ahí es donde el binario contiene nuestra información de depuración. Si sumamos todos los tamaños, vemos que la información de depuración representa aproximadamente 2.3 MB de nuestro archivo de 3 MB. Si también time el comando emcc, vemos que en nuestra máquina tardó alrededor de 1.5 s en ejecutarse. Estos números son un buen modelo de referencia, pero son tan pequeños que probablemente nadie los notaría. Sin embargo, en las aplicaciones reales, el ejecutable de depuración puede alcanzar fácilmente un tamaño de GB y tardar minutos en compilarse.

Omisión de Binaryen

Cuando compilas una aplicación Wasm con Emscripten, uno de los últimos pasos de la compilación es ejecutar el optimizador Binaryen. Binaryen es un kit de herramientas de compilación que optimiza y legaliza los objetos binarios de WebAssembly. La ejecución de Binaryen como parte de la compilación es bastante costosa, pero solo es obligatoria en ciertas condiciones. En el caso de las compilaciones de depuración, podemos acelerar el tiempo de compilación de forma significativa si evitamos la necesidad de pases de Binaryen. El pase de Binaryen más común y obligatorio es para legalizar las firmas de funciones que involucran valores de números enteros de 64 bits. Si habilitas la integración de WebAssembly BigInt con -sWASM_BIGINT, podemos evitar esto.

$ emcc -sUSE_SDL=2 -g -O0 -o mandelbrot.html mandelbrot.cc -sALLOW_MEMORY_GROWTH -sWASM_BIGINT -sERROR_ON_WASM_CHANGES_AFTER_LINK

Agregamos la marca -sERROR_ON_WASM_CHANGES_AFTER_LINK para mayor seguridad. Ayuda a detectar cuándo se ejecuta Binaryen y vuelve a escribir el objeto binario de forma inesperada. De esta manera, podemos asegurarnos de mantenernos en la ruta rápida.

Aunque nuestro ejemplo es bastante pequeño, aún podemos ver el efecto de omitir Binaryen. Según time, este comando se ejecuta en poco menos de 1 segundo, es decir, medio segundo más rápido que antes.

Ajustes avanzados

Cómo omitir el análisis del archivo de entrada

Por lo general, cuando se vincula un proyecto de Emscripten, emcc analiza todos los archivos y bibliotecas de objetos de entrada. Esto se hace para implementar dependencias precisas entre las funciones de la biblioteca de JavaScript y los símbolos nativos en tu programa. En el caso de proyectos más grandes, este análisis adicional de archivos de entrada (con llvm-nm) puede aumentar significativamente el tiempo de vinculación.

En su lugar, puedes ejecutarlo con -sREVERSE_DEPS=all, que le indica a emcc que incluya todas las dependencias nativas posibles de las funciones de JavaScript. Esto tiene una sobrecarga pequeña del tamaño de código, pero puede acelerar los tiempos de vinculación y puede ser útil para compilaciones de depuración.

Para un proyecto tan pequeño como nuestro ejemplo, esto no hace ninguna diferencia real, pero si tienes cientos o incluso miles de archivos de objetos en tu proyecto, puede mejorar significativamente los tiempos de vinculación.

Quitar la sección "name"

En proyectos grandes, en especial aquellos con mucho uso de plantillas de C++, la sección "name" de WebAssembly puede ser muy grande. En nuestro ejemplo, es solo una pequeña fracción del tamaño total del archivo (consulta el resultado de llvm-objdump anterior), pero en algunos casos puede ser muy significativo. Si la sección "name" de tu aplicación es muy grande y la información de depuración de enano es suficiente para tus necesidades de depuración, puede ser ventajoso quitar la sección "name":

$ emstrip --no-strip-all --remove-section=name mandelbrot.wasm

Esto quitará la sección “name” de WebAssembly y conservará las secciones de depuración de DWARF.

Fisión de depuración

Los objetos binarios con muchos datos de depuración no solo ejercen presión sobre el tiempo de compilación, sino también sobre el tiempo de depuración. El depurador debe cargar los datos y crear un índice para ellos, de modo que pueda responder rápidamente a consultas, como "¿Cuál es el tipo de la variable local x?".

La fisión de depuración nos permite dividir la información de depuración de un objeto binario en dos partes: una que permanece en el objeto binario y otra que se encuentra en un archivo separado, llamado objeto DWARF (.dwo). Para habilitarlo, pasa la marca -gsplit-dwarf a Emscripten:

$ emcc -sUSE_SDL=2 -g -gsplit-dwarf -gdwarf-5 -O0 -o mandelbrot.html mandelbrot.cc  -sALLOW_MEMORY_GROWTH -sWASM_BIGINT -sERROR_ON_WASM_CHANGES_AFTER_LINK

A continuación, mostramos los diferentes comandos y los archivos que se generan mediante la compilación sin datos de depuración, con datos de depuración y, por último, con datos de depuración y fisión de depuración.

los diferentes comandos y los archivos que se generan

Cuando se dividen los datos DWARF, una parte de los datos de depuración reside junto con el objeto binario, mientras que la parte grande se coloca en el archivo mandelbrot.dwo (como se ilustra más arriba).

Para mandelbrot, solo tenemos un archivo fuente, pero, por lo general, los proyectos son más grandes y contienen más de un archivo. La fisión de depuración genera un archivo .dwo para cada uno de ellos. Para que la versión beta actual del depurador (0.1.6.1615) pueda cargar esta información de depuración dividida, debemos agruparla en un llamado paquete DWARF (.dwp) de la siguiente manera:

$ emdwp -e mandelbrot.wasm -o mandelbrot.dwp

agrupa archivos dwo en un paquete DWARF

La compilación del paquete DWARF a partir de los objetos individuales tiene la ventaja de que solo debes entregar un archivo adicional. Actualmente, estamos trabajando para cargar también todos los objetos individuales en una versión futura.

¿Qué sucede con DWARF 5?

Es posible que hayas notado que agregamos otra marca al comando emcc anterior, -gdwarf-5. Habilitar la versión 5 de los símbolos DWARF, que actualmente no es la versión predeterminada, es otro truco para ayudarnos a comenzar la depuración más rápido. Con él, se almacena cierta información en el objeto binario principal que omitió la versión 4 predeterminada. Específicamente, podemos determinar el conjunto completo de archivos de origen solo desde el binario principal. Esto permite que el depurador realice acciones básicas, como mostrar el árbol de origen completo y establecer puntos de interrupción sin cargar ni analizar los datos de símbolos completos. Esto hace que la depuración con símbolos divididos sea mucho más rápida, por lo que siempre usamos las marcas de línea de comandos -gsplit-dwarf y -gdwarf-5 juntas.

Con el formato de depuración DWARF5 también tenemos acceso a otra función útil. Introduce un índice de nombre en los datos de depuración que se generará cuando se pase la marca -gpubnames:

$ emcc -sUSE_SDL=2 -g -gdwarf-5 -gsplit-dwarf -gpubnames -O0 -o mandelbrot.html mandelbrot.cc -sALLOW_MEMORY_GROWTH -sWASM_BIGINT -sERROR_ON_WASM_CHANGES_AFTER_LINK

Durante una sesión de depuración, las búsquedas de símbolos suelen ocurrir cuando se busca una entidad por nombre, p. ej., cuando se busca una variable o un tipo. El índice de nombres acelera esta búsqueda, ya que apunta directamente a la unidad de compilación que define ese nombre. Sin un índice de nombres, se necesitaría una búsqueda exhaustiva de todos los datos de depuración para encontrar la unidad de compilación correcta que defina la entidad denominada que buscamos.

Para los curiosos: Cómo ver los datos de depuración

Puedes usar llvm-dwarfdump para echar un vistazo a los datos de DWARF. Probemos esto:

llvm-dwarfdump mandelbrot.wasm

Esto nos brinda una descripción general de las "unidades de compilación" (en términos generales, los archivos fuente) para las que tenemos información de depuración. En este ejemplo, solo tenemos la información de depuración de mandelbrot.cc. La información general nos permitirá saber que tenemos una unidad básica, lo que solo significa que tenemos datos incompletos en este archivo, y que hay un archivo .dwo independiente que contiene la información de depuración restante:

mandelbrot.wasm y la información de depuración

También puedes consultar otras tablas de este archivo, p. ej., la tabla de líneas que muestra la asignación del código de bytes de wasm a líneas de C++ (prueba usar llvm-dwarfdump -debug-line).

También podemos ver la información de depuración que se incluye en el archivo .dwo independiente:

llvm-dwarfdump mandelbrot.dwo

mandelbrot.wasm y la información de depuración

Resumen: ¿Cuál es la ventaja de usar la fisión de depuración?

Existen varias ventajas a la hora de dividir la información de depuración si se trabaja con aplicaciones grandes:

  1. Vinculación más rápida: El vinculador ya no necesita analizar toda la información de depuración. Por lo general, los vinculadores deben analizar todos los datos DWARF que se encuentran en el objeto binario. Cuando se quitan grandes partes de la información de depuración en archivos separados, los enlazadores se ocupan de objetos binarios más pequeños, lo que genera tiempos de vinculación más rápidos (especialmente para aplicaciones grandes).

  2. Depuración más rápida: El depurador puede omitir el análisis de los símbolos adicionales en los archivos .dwo/.dwp para algunas búsquedas de símbolos. Para algunas búsquedas (como las solicitudes en la asignación de líneas de archivos wasm a C++), no es necesario que analicemos los datos de depuración adicionales. Esto nos ahorra tiempo, ya que no es necesario cargar ni analizar los datos de depuración adicionales.

1: Si no tienes una versión reciente de llvm-objdump en tu sistema y usas emsdk, puedes encontrarla en el directorio emsdk/upstream/bin.

Descarga los canales de vista previa

Considera usar Chrome Canary, Dev o Beta como navegadores de desarrollo predeterminados. 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.