Como depurar o WebAssembly mais rápido

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

No Chrome Dev Summit 2020, demonstramos pela primeira vez o suporte à depuração do Chrome para aplicativos WebAssembly na Web. Desde então, a equipe investiu muita energia para adaptar a experiência do desenvolvedor a aplicativos grandes e até mesmo gigantes. Nesta postagem, vamos mostrar os botões que adicionamos (ou fizemos trabalho) nas diferentes ferramentas e como usá-los.

Depuração escalonável

Vamos continuar de onde paramos na nossa postagem de 2020. Aqui está o exemplo que estávamos analisando nessa época:

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

Esse ainda é um exemplo bastante pequeno, e você provavelmente não veria nenhum dos problemas reais que veria em um aplicativo realmente grande, mas ainda podemos mostrar quais são os novos recursos. É rápido e fácil de configurar e testar por conta própria!

Na última postagem, discutimos como compilar e depurar esse exemplo. Vamos fazer isso de novo, mas também vamos conferir o //performance//:

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

Esse comando produz um binário Wasm de 3 MB. E a maior parte disso, como é de se esperar, são informações de depuração. É possível verificar isso com a ferramenta llvm-objdump [1], por exemplo:

$ 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

Essa saída mostra todas as seções que estão no arquivo Wasm gerado. A maioria delas são seções padrão do WebAssembly, mas também há várias seções personalizadas com nomes que começam com .debug_. É aí que o binário contém as informações de depuração. Se somarmos todos os tamanhos, veremos que as informações de depuração compõem aproximadamente 2,3 MB do nosso arquivo de 3 MB. Se também aplicarmos time ao comando emcc, veremos que a execução na máquina levou cerca de 1,5 segundo. Esses números formam uma pequena linha de base, mas são tão pequenos que ninguém olharia para eles. No entanto, em aplicativos reais, o binário de depuração pode facilmente atingir um tamanho de GB e levar minutos para ser criado.

Como ignorar binários

Ao criar um aplicativo Wasm com o Emscripten, uma das etapas finais do build é executar o otimizador Binaryen. O Binaryen é um kit de ferramentas de compilação que otimiza e legaliza binários semelhantes ao WebAssembly. A execução do Binaryen como parte do build é bastante cara, mas só é necessária sob certas condições. Para builds de depuração, podemos acelerar significativamente o tempo de build se evitarmos a necessidade de passagens binárias. A passagem binária obrigatória mais comum é para legalizar assinaturas de funções envolvendo valores inteiros de 64 bits. Ao ativar a integração do WebAssembly BigInt usando -sWASM_BIGINT, podemos evitar isso.

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

Geramos a flag -sERROR_ON_WASM_CHANGES_AFTER_LINK. Ajuda a detectar quando o Binaryen está em execução e regrava o binário inesperadamente. Dessa forma, podemos ter certeza de que vamos permanecer no caminho rápido.

Mesmo que nosso exemplo seja bem pequeno, ainda podemos ver o efeito de pular a fase binária! De acordo com time, esse comando é executado em pouco menos de 1 segundo, ou seja, meio segundo mais rápido do que antes.

Ajustes avançados

Pulando a verificação de arquivos de entrada

Normalmente, ao vincular um projeto Emscripten, o emcc verifica todos os arquivos e bibliotecas do objeto de entrada. Isso é feito para implementar dependências precisas entre funções da biblioteca JavaScript e símbolos nativos em seu programa. Para projetos maiores, essa verificação extra de arquivos de entrada (usando llvm-nm) pode aumentar significativamente o tempo de vinculação.

Em vez disso, é possível executar com -sREVERSE_DEPS=all, que instrui o emcc a incluir todas as dependências nativas possíveis das funções JavaScript. Isso gera uma pequena sobrecarga de tamanho de código, mas pode acelerar o tempo de vinculação e pode ser útil para builds de depuração.

Para um projeto pequeno como o nosso exemplo, isso não faz diferença real, mas se você tiver centenas ou até milhares de arquivos de objeto no seu projeto, isso pode melhorar significativamente os tempos de vinculação.

Eliminar a seção “name”

Em projetos grandes, especialmente aqueles com muito uso de modelos C++, a seção “nome” do WebAssembly pode ser muito grande. No nosso exemplo, é apenas uma pequena fração do tamanho total do arquivo (veja a saída de llvm-objdump acima), mas, em alguns casos, pode ser muito significativo. Se a seção “name” do aplicativo for muito grande e as informações de depuração anão forem suficientes para suas necessidades de depuração, pode ser vantajoso eliminar a seção “nome”:

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

Isso eliminará a seção “name” do WebAssembly, preservando as seções de depuração do DWARF.

Fissão de depuração

Binários com muitos dados de depuração não apenas pressionam o tempo de compilação, mas também o tempo de depuração. O depurador precisa carregar os dados e criar um índice para eles, para que possa responder rapidamente a consultas, como "Qual é o tipo da variável local x?".

A fissão de depuração nos permite dividir as informações de depuração de um binário em duas partes: uma, que permanece no binário e outra, que está contida em um arquivo separado, chamado de objeto DWARF (.dwo). Para ativá-lo, transmita a flag -gsplit-dwarf para 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

Mostramos abaixo os diferentes comandos e quais arquivos são gerados pela compilação sem dados de depuração, com dados de depuração e, por fim, com dados de depuração e fissão de depuração.

os diferentes comandos e quais arquivos são gerados

Ao dividir os dados DWARF, uma parte dos dados de depuração fica com o binário, enquanto a parte grande é colocada no arquivo mandelbrot.dwo (como ilustrado acima).

Para mandelbrot, temos apenas um arquivo de origem, mas geralmente os projetos são maiores do que isso e incluem mais de um arquivo. A fissão de depuração gera um arquivo .dwo para cada um deles. Para que a versão Beta atual do depurador (0.1.6.1615) possa carregar essas informações de depuração divididas, precisamos agrupar tudo isso em um pacote chamado DWARF (.dwp), desta forma:

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

agrupar arquivos dwo em um pacote DWARF

Construir o pacote DWARF usando objetos individuais tem a vantagem de que você só precisa disponibilizar um arquivo extra! No momento, estamos trabalhando para carregar todos os objetos individuais em uma versão futura.

O que é DWARF 5?

Talvez você tenha notado que inserimos outra flag no comando emcc acima, -gdwarf-5. Ativar a versão 5 dos símbolos DWARF, que atualmente não é o padrão, é outro truque para nos ajudar a começar a depurar mais rapidamente. Com ele, algumas informações são armazenadas no binário principal que a versão 4 padrão deixou de fora. Especificamente, podemos determinar o conjunto completo de arquivos de origem apenas a partir do binário principal. Isso permite que o depurador execute ações básicas, como mostrar a árvore de origem completa e definir pontos de interrupção sem carregar e analisar os dados completos do símbolo. Isso torna a depuração com símbolos de divisão muito mais rápida, então estamos sempre usando as sinalizações de linha de comando -gsplit-dwarf e -gdwarf-5 juntas.

Com o formato de depuração DWARF5, também temos acesso a outro recurso útil. Ela introduz um índice de nomes nos dados de depuração que serão gerados ao transmitir a flag -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 uma sessão de depuração, as pesquisas de símbolo geralmente ocorrem pesquisando uma entidade pelo nome, por exemplo, ao procurar uma variável ou um tipo. O índice de nome acelera essa pesquisa apontando diretamente para a unidade de compilação que define esse nome. Sem um índice de nome, uma pesquisa exaustiva de todos os dados de depuração seria necessária para encontrar a unidade de compilação correta que define a entidade nomeada que estamos procurando.

Para curiosos: analisar os dados de depuração

Você pode usar llvm-dwarfdump para observar os dados do DWARF. Vamos tentar:

llvm-dwarfdump mandelbrot.wasm

Isso nos dá uma visão geral sobre as "Unidades de compilação" (de forma geral, os arquivos de origem) para as quais temos informações de depuração. Neste exemplo, só temos as informações de depuração de mandelbrot.cc. As informações gerais nos informarão que temos uma unidade de esqueleto, o que significa apenas que temos dados incompletos nesse arquivo, e que há um arquivo .dwo separado com as informações de depuração restantes:

mandelbrot.wasm e informações de depuração

Você também pode dar uma olhada em outras tabelas nesse arquivo, por exemplo, Na tabela de linhas, que mostra o mapeamento do bytecode Wasm para linhas C++ (tente usar llvm-dwarfdump -debug-line).

Também podemos dar uma olhada nas informações de depuração contidas no arquivo .dwo separado:

llvm-dwarfdump mandelbrot.dwo

mandelbrot.wasm e informações de depuração

Texto longo, leia o resumo: Qual é a vantagem de usar a fissão de depuração?

Há diversas vantagens em dividir as informações de depuração ao trabalhar com aplicativos grandes:

  1. Vinculação mais rápida: o vinculador não precisa mais analisar todas as informações de depuração. Os vinculadores geralmente precisam analisar todos os dados DWARF que estão no binário. Ao remover grandes partes das informações de depuração em arquivos separados, os vinculadores lidam com binários menores, o que resulta em tempos de vinculação mais rápidos (especialmente para aplicativos grandes).

  2. Depuração mais rápida: o depurador pode pular a análise dos símbolos adicionais em arquivos .dwo/.dwp para algumas pesquisas de símbolos. Para algumas pesquisas (como solicitações no mapeamento de linhas dos arquivos Wasm para C++), não precisamos analisar os outros dados de depuração. Isso economiza tempo, sem a necessidade de carregar e analisar os dados de depuração adicionais.

1: se você não tiver uma versão recente do llvm-objdump no sistema e estiver usando emsdk, procure essa versão no diretório emsdk/upstream/bin.

Fazer o download dos canais de visualização

Use o Chrome Canary, Dev ou Beta como seu navegador de desenvolvimento padrão. Esses canais de pré-lançamento dão acesso aos recursos mais recentes do DevTools, testam APIs modernas da plataforma Web e encontram problemas no seu site antes que os usuários o façam!

Entrar em contato com a equipe do Chrome DevTools

Use as opções a seguir para discutir os novos recursos e mudanças na postagem ou qualquer outro assunto relacionado ao DevTools.

  • Envie uma sugestão ou feedback pelo site crbug.com.
  • Informe um problema do DevTools usando Mais opções   Mais > Ajuda > Relate problemas no DevTools no DevTools.
  • Tuíte em @ChromeDevTools.
  • Deixe comentários nos vídeos do YouTube sobre as novidades do DevTools ou nos vídeos do YouTube com dicas sobre o DevTools.