Como depurar o WebAssembly com ferramentas modernas

Ingvar Stepanyan
Ingvar Stepanyan

O caminho até o momento

Um ano atrás, o Chrome anunciou suporte inicial para depuração nativa do WebAssembly no Chrome DevTools.

Demonstramos suporte básico para caminhadas e conversamos sobre oportunidades de uso de informações de DWARF em vez de mapas de origem no futuro:

  • Como resolver nomes de variáveis
  • Tipos de estilos de formatação
  • Como avaliar expressões em linguagens de origem
  • E muito mais.

Hoje, temos o prazer de mostrar os recursos prometidos ganharem vida e o progresso que as equipes do Emscripten e do Chrome DevTools fizeram neste ano, principalmente para apps C e C++.

Antes de começar, lembre-se de que esta ainda é uma versão Beta da nova experiência, você precisa usar a versão mais recente de todas as ferramentas por sua conta e risco e, se tiver algum problema, informe-o em https://bugs.chromium.org/p/chromium/issues/entry?template=DevTools+issue.

Vamos começar com o mesmo exemplo de C simples da ú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 compilá-lo, usamos o Emscripten mais recente e transmitimos uma sinalização -g, assim como na postagem original, para incluir informações de depuração:

emcc -g temp.c -o temp.html

Agora podemos exibir a página gerada de um servidor HTTP localhost (por exemplo, com serve) e abrir a página no Chrome Canary mais recente.

Desta vez, também precisaremos de uma extensão auxiliar que se integre com o Chrome DevTools e ajude a entender todas as informações de depuração codificadas no arquivo WebAssembly. Para fazer a instalação, acesse este link: goo.gle/wasm-debugging-extension

Ative também a depuração do WebAssembly nos Experimentos do DevTools. Abra o Chrome DevTools, clique no ícone de engrenagem () no canto superior direito do painel, acesse o painel Experimentos e marque Depuração da WebAssembly: ativar suporte a DWARF.

Painel &quot;Experimentos&quot; nas configurações do DevTools

Quando você fechar as Configurações, o DevTools vai sugerir a atualização para aplicar as configurações, então vamos fazer isso. Isso é tudo para a configuração única.

Agora podemos voltar ao painel Sources, ativar a opção Pausar em exceções (ícone ⏸), marcar Pausar em exceções capturadas e recarregar a página. O DevTools terá pausado em uma exceção:

Captura de tela do painel &quot;Sources&quot; mostrando como ativar a opção &quot;Pausar em exceções capturadas&quot;

Por padrão, ela é interrompida em um código agrupador gerado por Emscripten, mas, à direita, é possível conferir uma visualização de Pilha de chamadas que representa o stack trace do erro e pode navegar para a linha C original que invocou abort:

O DevTools foi pausado na função `assert_less` e mostrando valores de `x` e `y` na visualização do escopo

Na visualização Scope, você pode conferir os nomes e valores originais das variáveis no código C/C++ e não precisa mais descobrir o que significam nomes corrompidos, como $localN, e como eles estão relacionados ao código-fonte escrito.

Isso se aplica não apenas a valores primitivos, como números inteiros, mas também a tipos compostos, como estruturas, classes, matrizes etc.

Suporte a rich text

Vamos analisar um exemplo mais complicado para mostrar isso. Desta vez, renderizaremos um fractal de Mandelbrot com o seguinte 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();
}

Você pode notar que esse aplicativo ainda é bastante pequeno. Ele é um único arquivo que contém 50 linhas de código, mas também estou usando algumas APIs externas, como a biblioteca SDL para gráficos, bem como números complexos da biblioteca C++ padrão.

Vou compilá-lo com a mesma flag -g acima para incluir informações de depuração e também pedir ao Emscripten que forneça a biblioteca SDL2 e permita uma memória de tamanho arbitrariamente:

emcc -g mandelbrot.cc -o mandelbrot.html \
     -s USE_SDL=2 \
     -s ALLOW_MEMORY_GROWTH=1

Quando acesso a página gerada no navegador, posso ver a bela forma fractal com algumas cores aleatórias:

Página de demonstração

Quando abro o DevTools, mais uma vez, posso ver o arquivo C++ original. Desta vez, no entanto, não há um erro no código (ufa!), então vamos definir algum ponto de interrupção no início do código.

Quando atualizarmos a página novamente, o depurador será pausado dentro da fonte em C++:

O DevTools foi pausado na chamada a `SDL_Init`

Já podemos ver todas as variáveis à direita, mas apenas width e height são inicializados no momento, então não há muito o que inspecionar.

Vamos definir outro ponto de interrupção dentro do loop de Mandelbrot principal e retomar a execução para avançar um pouco.

DevTools pausado dentro dos loops aninhados

Neste ponto, a palette foi preenchida com algumas cores aleatórias, e podemos expandir a própria matriz e as estruturas SDL_Color individuais e inspecionar os componentes para verificar se tudo está certo. Por exemplo, se o canal "Alfa" está sempre definido como opacidade total. Da mesma forma, podemos expandir e verificar as partes reais e imaginárias do número complexo armazenado na variável center.

Se você quiser acessar uma propriedade aninhada que seria difícil de navegar pela visualização Escopo, também é possível usar a avaliação do Console. No entanto, ainda não há suporte para expressões C++ mais complexas.

Painel do console mostrando o resultado de `palette[10].r`

Vamos retomar a execução algumas vezes e ver como o x interno também está mudando na visualização Scope, adicionando o nome da variável à lista de observação, avaliando-a no console ou passando o cursor sobre a variável no código-fonte:

Dica sobre a variável &quot;x&quot; na origem mostrando o valor &quot;3&quot;

A partir daqui, podemos adicionar ou ignorar instruções C++ e observar como outras variáveis também estão mudando:

Dicas e visualização de escopo mostrando os valores de &quot;color&quot;, &quot;point&quot; e outras variáveis

Tudo isso funciona muito bem quando há informações de depuração disponíveis, mas e se quisermos depurar um código que não foi criado com as opções de depuração?

Depuração de WebAssembly brutas

Por exemplo, pedimos ao Emscripten que fornecesse uma biblioteca SDL pré-criada para nós, em vez de compilá-la usando a origem. Então, pelo menos no momento, não há como o depurador encontrar fontes associadas. Faça login novamente para entrar no SDL_RenderDrawColor:

DevTools mostrando a visualização de desmontagem de `mandelbrot.Wasm`

Estamos de volta à experiência de depuração bruta do WebAssembly.

Parece um pouco assustador e não é algo com que a maioria dos desenvolvedores da Web vai precisar lidar, mas às vezes você pode querer depurar uma biblioteca criada sem informações de depuração, seja por ser uma biblioteca de terceiros que você não tem controle ou porque está enfrentando um desses bugs que ocorrem apenas na produção.

Para ajudar nesses casos, também fizemos algumas melhorias na experiência básica de depuração.

Primeiro, se você já usou a depuração bruta do WebAssembly antes, poderá notar que todo o código desmontado agora é mostrado em um único arquivo, sem mais adivinhar a qual função uma entrada wasm-53834e3e/ wasm-53834e3e-7 de Sources pode corresponder.

Novo esquema de geração de nomes

Também melhoramos os nomes na visualização de desmontagem. Anteriormente, você veria apenas índices numéricos ou, no caso de funções, nenhum nome.

Agora, estamos gerando nomes de maneira semelhante a outras ferramentas de desmontagem, usando dicas da seção Nome do WebAssembly, caminhos de importação/exportação e, por fim, se tudo falhar, gerando-os com base no tipo e no índice do item, como $func123. Veja na captura de tela acima como isso já ajuda a conseguir stack traces e desmontagens um pouco mais legíveis.

Quando não há informações de tipo disponíveis, pode ser difícil inspecionar valores além dos primitivos. Por exemplo, os ponteiros vão aparecer como números inteiros regulares, sem como saber o que está armazenado por trás deles na memória.

Inspeção de memória

Antes, só era possível expandir o objeto de memória WebAssembly, representado por env.memory na visualização Scope, para procurar bytes individuais. Isso funcionou em alguns cenários triviais, mas não era muito conveniente para expandir e não permitiu reinterpretar dados em formatos que não sejam valores de bytes. Também adicionamos um novo recurso para ajudar com isso: o inspetor de memória linear.

Ao clicar com o botão direito do mouse em env.memory, você verá uma nova opção chamada Inspecionar memória:

Menu de contexto em `env.memory` no painel Scope mostrando um item &quot;Inspect Memory&quot;

Depois que você clicar nele, será exibido um Inspetor de memória, em que é possível inspecionar a memória do WebAssembly em visualizações hexadecimais e ASCII, navegar até endereços específicos e interpretar os dados em formatos diferentes:

Painel do Memory Inspector no DevTools mostrando visualizações hexadecimais e ASCII da memória

Cenários e advertências avançados

Como criar o perfil do código WebAssembly

Quando você abre o DevTools, o código do WebAssembly é "em camadas" para uma versão não otimizada para permitir a depuração. Essa versão é muito mais lenta, o que significa que não é possível confiar em console.time, performance.now e outros métodos de medição da velocidade do código enquanto o DevTools está aberto, já que os números recebidos não representam o desempenho real.

Em vez disso, use o painel Performance do DevTools, que executa o código em velocidade máxima e fornece um detalhamento detalhado do tempo gasto em diferentes funções:

Painel de criação de perfil mostrando várias funções do Wasm

Como alternativa, você pode executar seu aplicativo com o DevTools fechado e abrir o aplicativo depois de terminar para inspecionar o Console.

Vamos melhorar os cenários de criação de perfil no futuro, mas, por enquanto, é uma ressalva que você precisa conhecer. Se você quiser saber mais sobre os cenários de níveis do WebAssembly, confira nossos documentos sobre o pipeline de compilação do WebAssembly.

Criar e depurar em diferentes máquinas (incluindo Docker / host)

Ao criar em um Docker, uma máquina virtual ou em um servidor de build remoto, você provavelmente vai se deparar com situações em que os caminhos para os arquivos de origem usados durante a criação não correspondem aos caminhos no seu sistema de arquivos em que o Chrome DevTools está sendo executado. Nesse caso, os arquivos vão aparecer no painel Sources, mas não serão carregados.

Para corrigir esse problema, implementamos uma funcionalidade de mapeamento de caminho nas opções de extensão C/C++. Ele pode ser usado para remapear caminhos arbitrários e ajudar o DevTools a localizar origens.

Por exemplo, se o projeto na máquina host estiver em um caminho C:\src\my_project, mas tiver sido criado dentro de um contêiner do Docker em que esse caminho foi representado como /mnt/c/src/my_project, será possível remapeá-lo durante a depuração especificando esses caminhos como prefixos:

Página de opções da extensão de depuração C/C++

O primeiro prefixo correspondente "vence". Caso você conheça outros depuradores C++, essa opção é semelhante ao comando set substitute-path no GDB ou a uma configuração target.source-map no LLDB.

Como depurar builds otimizados

Como em outras linguagens, a depuração funciona melhor quando as otimizações estão desativadas. As otimizações podem incorporar funções inline em outro, reordenar o código ou remover partes dele. Tudo isso pode confundir o depurador e, consequentemente, você como usuário.

Se você não se importa em ter uma experiência de depuração mais limitada e ainda quer depurar um build otimizado, a maioria das otimizações vai funcionar conforme o esperado, exceto a função em linha. Planejamos resolver os problemas restantes no futuro, mas, por enquanto, use -fno-inline para desativá-lo ao compilar com otimizações no nível -O, por exemplo:

emcc -g temp.c -o temp.html \
     -O3 -fno-inline

Como separar as informações de depuração

As informações de depuração preservam muitos detalhes sobre seu código, tipos definidos, variáveis, funções, escopos e locais, e tudo que possa ser útil para o depurador. Como resultado, ele geralmente pode ser maior que o código em si.

Para acelerar o carregamento e a compilação do módulo WebAssembly, divida essas informações de depuração em um arquivo WebAssembly separado. Para fazer isso no Emscripten, transmita uma sinalização -gseparate-dwarf=… com o nome de arquivo desejado:

emcc -g temp.c -o temp.html \
     -gseparate-dwarf=temp.debug.wasm

Nesse caso, o aplicativo principal armazenará apenas um nome de arquivo temp.debug.wasm, e a extensão auxiliar poderá localizá-lo e carregá-lo quando você abrir o DevTools.

Quando combinado com otimizações como as descritas acima, esse recurso pode ser usado para enviar builds de produção quase otimizados do seu aplicativo e depois depurá-los com um arquivo secundário local. Nesse caso, também precisaremos substituir o URL armazenado para ajudar a extensão a encontrar o arquivo lateral. Por exemplo:

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]

Para continuar...

Ufa, foram muitos recursos novos!

Com todas essas novas integrações, o Chrome DevTools se torna um depurador viável e avançado não apenas para JavaScript, mas também para apps C e C++, facilitando mais do que nunca o uso de apps criados com diversas tecnologias e os levar a uma Web compartilhada entre plataformas.

No entanto, nossa jornada ainda não acabou. Algumas das coisas em que trabalharemos a partir daqui em diante:

  • Como limpar os pontos fora da curva na experiência de depuração
  • Foi adicionado suporte a formatadores de tipos personalizados.
  • Estamos trabalhando em melhorias na criação de perfil para apps WebAssembly.
  • Foi adicionado suporte à cobertura de código para facilitar a localização de códigos não utilizados.
  • Melhoria no suporte a expressões na avaliação do console.
  • Foi adicionado suporte a mais idiomas.
  • E muito mais.

Enquanto isso, ajude nossa equipe testando a versão Beta atual no seu próprio código e informando os problemas encontrados para https://bugs.chromium.org/p/chromium/issues/entry?template=DevTools+issue.

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 oferecem acesso aos recursos mais recentes do DevTools, testam as APIs modernas de plataformas da Web e encontram problemas no site antes dos usuários.

Entrar em contato com a equipe do Chrome DevTools

Use as opções a seguir para discutir os novos recursos e mudanças na publicação ou qualquer outra coisa relacionada ao DevTools.

  • Envie uma sugestão ou feedback em crbug.com.
  • Informe um problema do DevTools em Mais opções   Mais   > Ajuda > Informar problemas no DevTools.
  • Envie um tweet em @ChromeDevTools.
  • Deixe comentários nos nossos vídeos do YouTube sobre a ferramenta DevTools ou nos vídeos do YouTube com dicas sobre o DevTools.