Como depurar o WebAssembly com ferramentas modernas

Ingvar Stepanyan
Ingvar Stepanyan

O caminho até aqui

Há um ano, o Chrome anunciou o suporte inicial à depuração nativa do WebAssembly no Chrome DevTools.

Demonstramos o suporte básico de escalonamento e falamos sobre oportunidades de uso de informações do DWARF em vez de mapas de origem abertos para nós no futuro:

  • Como resolver nomes de variáveis
  • Tipos de impressão com estilo
  • Como avaliar expressões em idiomas de origem
  • …e muito mais!

Hoje, temos o prazer de mostrar os recursos prometidos e o progresso que as equipes do Emscripten e do Chrome DevTools fizeram ao longo deste ano, em particular, 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. Se encontrar algum problema, informe-o em https://issues.chromium.org/issues/new?noWizard=true&template=0&component=1456350.

Vamos começar com o mesmo exemplo simples de C 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 fazer a compilação, usamos a versão mais recente do Emscripten e transmitimos uma flag -g, assim como na postagem original, para incluir informações de depuração:

emcc -g temp.c -o temp.html

Agora podemos oferecer a página gerada em um servidor HTTP localhost (por exemplo, com serve) e abri-la no Chrome Canary mais recente.

Desta vez, também vamos precisar de uma extensão auxiliar que se integre ao Chrome DevTools e ajude a entender todas as informações de depuração codificadas no arquivo WebAssembly. Instale a extensão neste link: goo.gle/wasm-debugging-extension

Também é recomendável ativar a depuração do WebAssembly nos Experimentos do DevTools. Abra o Chrome DevTools, clique no ícone de engrenagem () no canto superior direito do painel do DevTools, acesse o painel Experiments e marque WebAssembly Debugging: Enable DWARF support.

Painel &quot;Experiments&quot; das configurações do DevTools

Quando você fecha as Configurações, o DevTools sugere que ele seja recarregado para aplicar as configurações. Vamos fazer isso. É isso por hoje.

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

Captura de tela do painel &quot;Origens&quot; mostrando como ativar a opção &quot;Pausar nas exceções encontradas&quot;

Por padrão, ele para em um código de cola gerado pelo Emscripten, mas à direita, você pode ver uma visualização Call Stack que representa o stacktrace do erro e pode navegar até a linha C original que invocou abort:

O DevTools foi pausado na função &quot;assert_less&quot; e mostra os valores de &quot;x&quot; e &quot;y&quot; na visualização de escopo.

Agora, se você olhar na visualização Escopo, poderá conferir os nomes originais e os valores das variáveis no código C/C++, e não precisará mais descobrir o que nomes mutilados como $localN significam e como eles se relacionam com o código-fonte que você escreveu.

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 tipos avançados

Vamos conferir um exemplo mais complicado para mostrar isso. Desta vez, vamos desenhar 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();
}

Esse aplicativo ainda é bastante pequeno, um único arquivo com 50 linhas de código, mas dessa vez também estou usando algumas APIs externas, como a biblioteca SDL para gráficos e números complexos da biblioteca padrão C++.

Vou fazer a compilação com a mesma flag -g acima para incluir informações de depuração e também pedir que o Emscripten forneça a biblioteca SDL2 e permita memória de tamanho arbitrário:

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

Quando abro a página gerada no navegador, vejo a bela forma fractal com algumas cores aleatórias:

Página de demonstração

Quando abro o DevTools, posso ver o arquivo C++ original. Dessa vez, no entanto, não temos um erro no código (ufa!). Vamos definir alguns pontos de interrupção no início do código.

Quando a página for atualizada novamente, o depurador será pausado diretamente na fonte C++:

O DevTools foi pausado na chamada &quot;SDL_Init&quot;.

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

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

As DevTools foram pausadas dentro dos loops aninhados.

Nesse ponto, o palette foi preenchido com algumas cores aleatórias, e podemos expandir a matriz e as estruturas SDL_Color individuais e inspecionar os componentes para verificar se tudo está certo. Por exemplo, o canal "alpha" 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 profundamente aninhada que é difícil de navegar pela visualização Escopo, use a avaliação do Console. No entanto, expressões C++ mais complexas ainda não são compatíveis.

Painel do console mostrando o resultado de &quot;palette[10].r&quot;

Vamos retomar a execução algumas vezes e conferir como o x interno está mudando também. Para isso, basta olhar novamente na visualização Escopo, adicionar o nome da variável à lista de observação, avaliá-la no console ou passar 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 inserir ou ignorar instruções C++ e observar como outras variáveis também estão mudando:

Dicas e visualização de escopo mostrando 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 bruta do WebAssembly

Por exemplo, pedimos ao Emscripten para fornecer uma biblioteca SDL pré-construída para nós, em vez de compilarmos a partir da fonte. Portanto, pelo menos no momento, não há como o depurador encontrar as fontes associadas. Vamos entrar novamente para acessar o SDL_RenderDrawColor:

DevTools mostrando a visualização de desmontagem de &quot;mandelbrot.wasm&quot;

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

Isso pode parecer um pouco assustador e não é algo com que a maioria dos desenvolvedores da Web precisa lidar, mas, às vezes, você pode querer depurar uma biblioteca criada sem informações de depuração, seja porque é uma biblioteca de 3rd que você não tem controle ou porque você encontrou um desses bugs que ocorre 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, talvez perceba que toda a desmontagem agora é mostrada em um único arquivo. Não é mais necessário adivinhar qual função uma entrada Sources wasm-53834e3e/ wasm-53834e3e-7 possivelmente corresponde.

Novo esquema de geração de nomes

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

Agora estamos gerando nomes de forma semelhante a outras ferramentas de desmontagem, usando dicas da seção de nome do WebAssembly, caminhos de importação/exportação e, por fim, se tudo mais falhar, gerando com base no tipo e no índice do item, como $func123. Você pode ver como, na captura de tela acima, isso já ajuda a conseguir stacktraces e desmontagens um pouco mais legíveis.

Quando não há informações de tipo disponíveis, pode ser difícil inspecionar qualquer valor além dos primitivos. Por exemplo, os ponteiros aparecem como números inteiros normais, sem nenhuma maneira de saber o que está armazenado neles na memória.

Inspeção de memória

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

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

Menu de contexto em &quot;env.memory&quot; no painel &quot;Escopo&quot; mostrando um item &quot;Inspecionar memória&quot;

Ao clicar, um Memory Inspector será aberto. Nele, você pode inspecionar a memória do WebAssembly em visualizações hexadecimais e ASCII, navegar até endereços específicos e interpretar os dados em diferentes formatos:

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

Cenários avançados e ressalvas

Como criar o perfil do código WebAssembly

Quando você abre o DevTools, o código do WebAssembly é "rebaixado" para uma versão não otimizada para permitir a depuração. Essa versão é muito mais lenta, o que significa que você não pode confiar em console.time, performance.now e outros métodos de medição da velocidade do código enquanto as Ferramentas do desenvolvedor estão abertas, porque os números recebidos não representam a performance real.

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

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

Como alternativa, é possível executar o aplicativo com o DevTools fechado e abrir quando terminar para inspecionar o console.

Vamos melhorar os cenários de criação de perfil no futuro, mas, por enquanto, é importante considerar isso. Se você quiser saber mais sobre cenários de hierarquia do WebAssembly, consulte a documentação sobre o pipeline de compilação do WebAssembly.

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

Ao criar em um Docker, em uma máquina virtual ou em um servidor de build remoto, é provável que você encontre situações em que os caminhos para os arquivos de origem usados durante o build não correspondem aos caminhos no seu próprio sistema de arquivos em que o Chrome DevTools está sendo executado. Nesse caso, os arquivos aparecem no painel Origens, mas não são carregados.

Para corrigir esse problema, implementamos uma funcionalidade de mapeamento de caminho nas opções de extensão do C/C++. Você pode usá-lo para remapear caminhos arbitrários e ajudar o DevTools a localizar fontes.

Por exemplo, se o projeto na máquina host estiver em um caminho C:\src\my_project, mas tiver sido criado em um contêiner do Docker em que esse caminho foi representado como /mnt/c/src/my_project, ele poderá ser remapeado 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". Se você já conhece outros depuradores C++, essa opção é semelhante ao comando set substitute-path no GDB ou a uma configuração target.source-map no LLDB.

Depurar builds otimizados

Como em qualquer outro idioma, a depuração funciona melhor se as otimizações estiverem desativadas. As otimizações podem inlinear funções uma na outra, 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 importar com uma experiência de depuração mais limitada e ainda quiser depurar um build otimizado, a maioria das otimizações vai funcionar como esperado, exceto a in-line de função. Planejamos resolver os problemas restantes no futuro, mas, por enquanto, use -fno-inline para desativá-lo ao compilar com qualquer otimização de 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 o código, tipos, variáveis, funções, escopos e locais definidos, ou seja, tudo o que pode ser útil para o depurador. Como resultado, ele geralmente pode ser maior do 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 flag -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 vai 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 a descrita acima, esse recurso pode ser usado para enviar builds de produção quase otimizados do aplicativo e, mais tarde, depurá-los com um arquivo local. Nesse caso, também será necessário substituir o URL armazenado para ajudar a extensão a encontrar o arquivo secundário, 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]

Continua depois…

Ufa, foram muitos recursos novos!

Com todas essas novas integrações, o Chrome DevTools se torna um depurador viável e poderoso, não apenas para JavaScript, mas também para apps C e C++, tornando mais fácil do que nunca criar apps, desenvolvidos em várias tecnologias, e levá-los a uma Web compartilhada e multiplataforma.

No entanto, nossa jornada ainda não acabou. Confira algumas das coisas em que vamos trabalhar daqui em diante:

  • Limpeza das arestas ásperas na experiência de depuração.
  • Adição de suporte a formatadores de tipo personalizados.
  • Estamos trabalhando em melhorias na criação de perfis para apps do WebAssembly.
  • Adição de suporte à cobertura de código para facilitar a localização de código não usado.
  • Melhoria no suporte a expressões na avaliação do console.
  • Adicionamos suporte a mais idiomas.
  • … e mais!

Enquanto isso, ajude a melhorar o produto testando a versão Beta atual no seu próprio código e informando os problemas encontrados em https://issues.chromium.org/issues/new?noWizard=true&template=0&component=1456350.

Fazer o download dos canais de visualização

Use o Chrome Canary, Dev ou Beta como navegador de desenvolvimento padrão. Esses canais de visualização dão acesso aos recursos mais recentes do DevTools, permitem testar APIs de plataforma da Web de última geração e ajudam a encontrar problemas no seu site antes que os usuários.

Entre em contato com a equipe do Chrome DevTools

Use as opções a seguir para discutir os novos recursos, atualizações ou qualquer outra coisa relacionada ao DevTools.