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 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 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. Es rápido y fácil de configurar y probar.
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 es de esperar, es información de depuración. Puedes verificar esto con la herramienta 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 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 se compila una aplicación wasm con Emscripten, uno de los pasos finales de compilación es ejecutar el optimizador Binaryen. Binaryen es un kit de herramientas de compilador que optimiza y legaliza los objetos binarios de WebAssembly (similares). 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 habilitamos la integración de BigInt de WebAssembly 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 seguir por 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 de archivos 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 de tamaño de código pequeña, pero puede acelerar los tiempos de vinculación y 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.
Quita 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 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 las 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 cuando se compila sin datos de depuración, con datos de depuración y, por último, con datos de depuración y fisión de depuración.
Cuando se dividen los datos DWARF, una parte de los datos de depuración reside junto con el binario, mientras que la parte grande se coloca en el archivo mandelbrot.dwo
(como se ilustró anteriormente).
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
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 predeterminada, es otro truco que nos ayuda a iniciar la depuración más rápido. Con ella, se almacena cierta información en el binario principal que la versión 4 predeterminada dejó fuera. 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. Presenta un índice de nombre en los datos de depuración que se generarán 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 requeriría una búsqueda exhaustiva de todos los datos de depuración para encontrar la unidad de compilación correcta que define la entidad nombrada 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 indicará que tenemos una unidad de esqueleto, lo que significa que tenemos datos incompletos en este archivo y que hay un archivo .dwo
independiente que contiene la información de depuración restante:
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
Resumen: ¿Cuál es la ventaja de usar la fisión de depuración?
Dividir la información de depuración tiene varias ventajas si se trabaja con aplicaciones grandes:
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).
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 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.
- Envíanos tus comentarios y solicitudes de funciones a crbug.com.
- Informa un problema de DevTools con Más opciones > Ayuda > Informar un problema de DevTools en DevTools.
- Twittea a @ChromeDevTools.
- Deja comentarios en los videos de YouTube sobre las novedades de DevTools o en los videos de YouTube sobre sugerencias de DevTools.