Depura WebAssembly más rápido

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

En la Cumbre de desarrolladores de Chrome de 2020, presentamos por primera vez la compatibilidad con la depuración de Chrome para aplicaciones de WebAssembly en la Web. Desde entonces, el equipo ha invertido mucho energía en hacer que la experiencia de los desarrolladores se adapte a aplicaciones grandes y hasta grandes. En esta publicación, te mostraremos los controles que agregamos (o hicimos funcionar) 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 estábamos analizando 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();
}

Aún es un ejemplo bastante pequeño y es probable que no veas ninguno de los problemas reales que verías en una aplicación muy 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, discutimos cómo compilar y depurar este ejemplo. Volvamos a hacerlo, pero analicemos también //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

En este resultado, se muestran todas las secciones que se encuentran en el archivo wasm generado. La mayoría de ellas son secciones estándar de WebAssembly, pero también hay varias secciones personalizadas cuyo nombre comienza con .debug_. Ahí es donde el objeto binario contiene nuestra información de depuración. Si sumamos todos los tamaños, veremos 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, veremos que en nuestra máquina tardó aproximadamente 1.5 s en ejecutarse. Estos números forman una pequeña referencia, pero son tan pequeños que probablemente nadie las vigila. Sin embargo, en aplicaciones reales, el objeto binario de depuración puede alcanzar fácilmente un tamaño en GB y tardar minutos en compilarse.

Omitir objeto binario

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 necesaria en ciertas condiciones. Para compilaciones de depuración, podemos acelerar significativamente el tiempo de compilación si evitamos la necesidad de pases de Binaryen. El pase de Binaryen más común requerido es para legalizar firmas de funciones que involucran valores 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

Se arroja la marca -sERROR_ON_WASM_CHANGES_AFTER_LINK por si acaso. Ayuda a detectar cuándo Binaryen se ejecuta y vuelve a escribir el objeto binario de forma inesperada. De esta manera, podemos asegurarnos de mantenernos en el camino rápido.

Aunque nuestro ejemplo es relativamente pequeño, podemos ver el efecto de omitir Binaryen. Según time, este comando se ejecuta un poco menos de 1 s, así que 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 analizará todos los archivos y las bibliotecas de objetos de entrada. Lo hace para implementar dependencias precisas entre las funciones de la biblioteca JavaScript y los símbolos nativos de tu programa. Para proyectos más grandes, este análisis adicional de los archivos de entrada (mediante llvm-nm) puede aumentar considerablemente el tiempo de vinculación.

En su lugar, es posible 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 de tamaño de código pequeña, 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 tiene cientos o incluso miles de archivos de objetos en su proyecto, puede mejorar significativamente los tiempos de vinculación.

Quitar la sección "name"

En proyectos grandes, especialmente en aquellos con mucho uso de plantillas C++, la sección “name” de WebAssembly puede ser muy grande. En nuestro ejemplo, es solo una pequeña fracción del tamaño del archivo total (consulta el resultado de llvm-objdump más arriba), pero, en algunos casos, puede ser muy importante. 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 beneficioso eliminar 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.

Depurar fisión

Los objetos binarios con muchos datos de depuración no solo ejercen presión en el tiempo de compilación, sino también en el de depuración. El depurador debe cargar los datos y crear un índice para ellos, de modo que pueda responder consultas rápidamente, como "¿Cuál es el tipo de 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 está contenida en un archivo de objeto DWARF (.dwo) separado. Se puede habilitar pasando 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 los datos de depuración y la fisión de depuración.

los diferentes comandos y qué archivos 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 de origen, pero, en general, los proyectos son más grandes que este y, además, incluyen 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 agrupar todo en un paquete llamado DWARF (.dwp) de la siguiente manera:

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

agrupar archivos dwo en un paquete DWARF

Compilar el paquete DWARF a partir de los objetos individuales tiene la ventaja de que solo necesitas entregar un archivo adicional. 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 colocamos otra marca en el 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 a partir del objeto binario principal. Esto permite que el depurador realice acciones básicas, como mostrar el árbol de fuentes 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 de división sea mucho más rápida, por lo que siempre usaremos 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 realizarse 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 apuntando 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: Consultar los datos de depuración

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

llvm-dwarfdump mandelbrot.wasm

Esto nos da una descripción general de las “unidades de compilación” (en términos generales, los archivos de origen) de 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 e información de depuración

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

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

llvm-dwarfdump mandelbrot.dwo

mandelbrot.wasm e 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 necesitan analizar todos los datos DWARF que se encuentran en el objeto binario. Al eliminar grandes partes de la información de depuración en archivos separados, los vinculadores trabajan con objetos binarios más pequeños, lo que da como resultado tiempos de vinculación más rápidos (sobre todo en aplicaciones grandes).

  2. Depuración más rápida: El depurador puede omitir el análisis de símbolos adicionales en los archivos .dwo o .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 necesitamos analizar los datos de depuración adicionales. Esto nos ahorra tiempo, ya que no necesitamos cargar y 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 vista previa te brindan acceso a las funciones más recientes de Herramientas para desarrolladores, prueban API de plataforma web de vanguardia y detectan problemas en tu sitio antes que los usuarios.

Comunicarse con el equipo de Herramientas para desarrolladores de Chrome

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

  • Para enviarnos sugerencias o comentarios, accede a crbug.com.
  • Informa un problema en Herramientas para desarrolladores con Más opciones   Más > Ayuda > Informa problemas de Herramientas para desarrolladores en Herramientas para desarrolladores.
  • Twittea a @ChromeDevTools.
  • Deja comentarios en nuestros videos de YouTube de Herramientas para desarrolladores o en videos de YouTube de las Sugerencias de las Herramientas para desarrolladores.