Estudo de caso: melhor depuração angular com o DevTools

Uma experiência de depuração aprimorada

Nos últimos meses, a equipe do Chrome DevTools colaborou com a equipe do Angular para lançar melhorias na experiência de depuração no Chrome DevTools. Pessoas de ambas as equipes trabalharam juntas e tomaram medidas para permitir que os desenvolvedores depurem e criem perfis de aplicativos da Web a partir da perspectiva de autoria: em termos de linguagem de origem e estrutura do projeto, com acesso a informações familiares e relevantes para eles.

Esta postagem examina os bastidores para entender quais mudanças no Angular e no Chrome DevTools foram necessárias para fazer isso. Mesmo que algumas dessas mudanças sejam demonstradas no Angular, elas também podem ser aplicadas a outros frameworks. A equipe do Chrome DevTools incentiva outras estruturas a adotar as novas APIs do console e pontos de extensão do mapa de origem, para que elas também possam oferecer uma melhor experiência de depuração aos usuários.

Código de lista de ignorados

Ao depurar aplicativos usando o Chrome DevTools, os autores geralmente querem ver apenas o código, não o do framework abaixo ou alguma dependência escondida na pasta node_modules.

Para isso, a equipe do DevTools introduziu uma extensão para mapas de origem, chamada x_google_ignoreList. Essa extensão é usada para identificar origens de terceiros, como código da estrutura ou código gerado pelo bundler. Agora, quando um framework usa essa extensão, os autores evitam automaticamente códigos que não querem ver ou seguir sem precisar configurar isso manualmente antes.

Na prática, o Chrome DevTools pode ocultar automaticamente o código identificado como tal nos stack traces, na árvore de origens e na caixa de diálogo Quick Open, além de melhorar o comportamento de percorrer e retomar o depurador.

Um GIF animado mostrando o antes e depois do DevTools. Observe como, na imagem posterior, o DevTools mostra o código de criação na árvore, não sugere mais nenhum dos arquivos do framework no menu “Acesso rápido” e mostra um stack trace muito mais limpo à direita.

A extensão de mapa de origem x_google_ignoreList

Nos mapas de origem, o novo campo x_google_ignoreList se refere à matriz sources e lista os índices de todas as origens de terceiros conhecidas nesse mapa de origem. Ao analisar o mapa de origem, o Chrome DevTools usará isso para descobrir quais seções do código devem ser ignoradas.

Confira abaixo um mapa de origem para um arquivo out.js gerado. Há dois sources originais que contribuíram para a geração do arquivo de saída: foo.js e lib.js. O primeiro é algo que um desenvolvedor de sites criou e o segundo é uma estrutura que eles usaram.

{
  "version" : 3,
  "file": "out.js",
  "sourceRoot": "",
  "sources": ["foo.js", "lib.js"],
  "sourcesContent": ["...", "..."],
  "names": ["src", "maps", "are", "fun"],
  "mappings": "A,AAAB;;ABCDE;"
}

O sourcesContent está incluído para ambas as fontes originais, e o Chrome DevTools exibiria esses arquivos por padrão no Debugger:

  • Como arquivos na árvore de origem.
  • Como resultado na caixa de diálogo "Quick Open".
  • Como locais de frames de chamada mapeados em stack traces de erro enquanto pausados em um ponto de interrupção e durante o passo.

Há uma informação adicional que agora pode ser incluída nos mapas de origem para identificar qual dessas origens é um código próprio ou de terceiros:

{
  ...
  "sources": ["foo.js", "lib.js"],
  "x_google_ignoreList": [1],
  ...
}

O novo campo x_google_ignoreList contém um único índice que se refere à matriz sources: 1. Isso especifica que as regiões mapeadas para lib.js são, na verdade, códigos de terceiros que precisam ser adicionadas automaticamente à lista de ignorados.

Em um exemplo mais complexo, mostrado abaixo, os índices 2, 4 e 5 especificam que as regiões mapeadas para lib1.ts, lib2.coffee e hmr.js são todos códigos de terceiros que precisam ser adicionados automaticamente à lista de ignorados.

{
  ...
  "sources": ["foo.html", "bar.css", "lib1.ts", "baz.js", "lib2.coffee", "hmr.js"],
  "x_google_ignoreList": [2, 4, 5],
  ...
}

Se você for um desenvolvedor de framework ou bundler, verifique se os mapas de origem gerados durante o processo de compilação incluem esse campo para se conectar a esses novos recursos no Chrome DevTools.

x_google_ignoreList no Angular

A partir do Angular v14.1.0, o conteúdo das pastas node_modules e webpack foi marcado como “para ignorar”.

Isso foi conseguido com uma mudança no angular-cli, criando um plug-in que se conecta ao módulo Compiler do webpack.

O plug-in webpack (link em inglês) que nossos engenheiros criaram hooks no estágio PROCESS_ASSETS_STAGE_DEV_TOOLING e preenche o campo x_google_ignoreList nos mapas de origem para os recursos finais que o webpack gera e o navegador carrega.

const map = JSON.parse(mapContent) as SourceMap;
const ignoreList = [];

for (const [index, path] of map.sources.entries()) {
  if (path.includes('/node_modules/') || path.startsWith('webpack/')) {
    ignoreList.push(index);
  }
}

map[`x_google_ignoreList`] = ignoreList;
compilation.updateAsset(name, new RawSource(JSON.stringify(map)));

Stack traces vinculados

Os stack traces respondem à pergunta "como cheguei até aqui", mas, muitas vezes, isso é da perspectiva da máquina, e não necessariamente de algo que corresponda à perspectiva do desenvolvedor ou do modelo mental do tempo de execução do aplicativo. Isso é especialmente verdadeiro quando algumas operações são programadas para acontecer de maneira assíncrona mais tarde: ainda pode ser interessante saber a "causa raiz" ou o lado da programação dessas operações, mas isso é exatamente algo que não fará parte de um stack trace assíncrono.

Internamente, o V8 tem um mecanismo para acompanhar essas tarefas assíncronas quando os primitivos de programação padrão do navegador são usados, como setTimeout. Nesses casos, isso é feito por padrão. Assim, os desenvolvedores já podem inspecioná-los. Mas, em projetos mais complexos, isso não é tão simples quanto isso, especialmente ao usar um framework com mecanismos de agendamento mais avançados — por exemplo, um que executa rastreamento de zona, enfileiramento de tarefas personalizadas ou que divide atualizações em várias unidades de trabalho executadas ao longo do tempo.

Para resolver isso, o DevTools expõe um mecanismo chamado "API Async Stack Tagging" no objeto console, que permite que os desenvolvedores de framework indiquem os locais em que as operações estão programadas e onde são executadas.

API Async Stack Tagging

Sem a inclusão de tags assíncronas, os stack traces do código que são executados de maneira assíncrona de maneiras complexas pelos frameworks aparecem sem conexão com o código em que foram programados.

Um stack trace de um código assíncrono executado sem informações sobre quando ele foi programado. Ele mostra apenas o stack trace a partir de "requestAnimationFrame", mas não contém informações de quando foi programado.

Com a inclusão de tag Async Stack, é possível fornecer esse contexto, e o stack trace fica assim:

Um stack trace de um código assíncrono executado com informações sobre quando ele foi programado. Observe como, ao contrário de antes, ele inclui `businessLogic` e `schedule` no stack trace.

Para fazer isso, use um novo método console (link em inglês) chamado console.createTask(), fornecido pela API Async Stack Tagging. Sua assinatura é a seguinte:

interface Console {
  createTask(name: string): Task;
}

interface Task {
  run<T>(f: () => T): T;
}

Invocar console.createTask() retorna uma instância Task que pode ser usada posteriormente para executar o código assíncrono.

// Task Creation
const task = console.createTask(name);

// Task Execution
task.run(f);

As operações assíncronas também podem ser aninhadas, e as "causas raiz" serão exibidas no stack trace em sequência.

As tarefas podem ser executadas quantas vezes você quiser, e a carga útil de trabalho pode ser diferente entre cada execução. A pilha de chamadas no site de agendamento será lembrada até que o objeto da tarefa seja coletado da lixeira.

API Async Stack Tagging no Angular

No Angular, foram feitas alterações no NgZone, o contexto de execução do Angular que persiste em tarefas assíncronas.

Ao programar uma tarefa, ele usa console.createTask() quando disponível. A instância Task resultante é armazenada para uso posterior. Ao invocar a tarefa, o NgZone usará a instância Task armazenada para executá-la.

Essas mudanças chegaram ao NgZone 0.11.8 do Angular usando solicitações de envio #46693 e 46958.

Frames de chamada amigáveis

Os frameworks geralmente geram códigos de todos os tipos de linguagens de modelo ao construir um projeto, como modelos Angular ou JSX que transformam código HTML em JavaScript simples que é executado no navegador. Às vezes, esses tipos de funções geradas recebem nomes pouco amigáveis, como nomes com uma única letra depois de minificados ou alguns nomes obscuros ou desconhecidos, mesmo quando não são.

No Angular, não é incomum ver frames de chamadas com nomes como AppComponent_Template_app_button_handleClick_1_listener nos stack traces.

Captura de tela do stack trace com o nome de uma função gerado automaticamente.

Para resolver isso, o Chrome DevTools agora oferece suporte para renomear essas funções usando mapas de origem. Se um mapa de origem tiver uma entrada de nome para o início de um escopo de função (ou seja, o parêntese esquerdo da lista de parâmetros), o frame de chamada vai mostrar esse nome no stack trace.

Frames de chamada amigáveis no Angular

A renomeação de frames de chamadas no Angular é um esforço contínuo. Esperamos que essas melhorias sejam implementadas gradualmente ao longo do tempo.

Ao analisar os modelos HTML que os autores escreveram, o compilador Angular gera o código TypeScript, que é transformado em código JavaScript que o navegador carrega e executa.

Como parte desse processo de geração de código, os mapas de origem também são criados. No momento, estamos explorando maneiras de incluir nomes de funções no campo “nomes” dos mapas de origem e fazer referência a esses nomes nos mapeamentos entre o código gerado e o código original.

Por exemplo, se uma função para um listener de eventos for gerada e seu nome for incompatível ou removido durante a minificação, os mapas de origem agora poderão incluir o nome mais simples para essa função no campo “names”, e o mapeamento para o início do escopo da função agora poderá se referir a esse nome (ou seja, o parêntese esquerdo da lista de parâmetros). O Chrome DevTools usará esses nomes para renomear os frames de chamada nos rastreamentos de pilha.

No futuro

Usar o Angular como piloto de teste para conferir se nosso trabalho tem sido uma experiência incrível. Queremos saber a opinião dos desenvolvedores de frameworks e enviar feedback sobre esses pontos de extensão.

Há mais áreas que gostaríamos de explorar. Em particular, como melhorar a experiência de criação de perfil no DevTools.