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. As pessoas das duas equipes trabalharam juntas e tomaram medidas para permitir que os desenvolvedores depurem e criem perfis de aplicativos da Web do ponto de vista da perspectiva de autoria: em termos de linguagem-fonte e estrutura do projeto, com acesso a informações conhecidas e relevantes para eles.

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

Ignorar o código da listagem

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

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

Na prática, o Chrome DevTools pode ocultar automaticamente o código identificado como tal em stack traces, a árvore de origens, a caixa de diálogo de abertura rápida e também melhorar o comportamento de acompanhamento e retomada no depurador.

Um GIF animado mostrando o DevTools antes e depois. Observe como, na imagem posterior, o DevTools mostra o código criado na árvore, não sugere mais nenhum dos arquivos do framework no menu “Quick Open” 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.

Veja abaixo um mapa de origem de 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 escreveu, e o segundo é um framework usado por ele.

{
  "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 mostraria esses arquivos por padrão no Debugger:

  • Como arquivos na árvore Sources.
  • Como resultados na caixa de diálogo de abertura rápida.
  • Como locais de frames de chamada mapeados em stack traces de erro enquanto pausado em um ponto de interrupção e durante uma caminhada.

Há mais uma informação que agora pode ser incluída nos mapas de origem para identificar qual dessas origens é código primário 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 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 desenvolvedor de framework ou bundler, verifique se os mapas de origem gerados durante o processo de compilação incluem esse campo para vincular 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 "a ignorar" (link em inglês).

Isso foi feito por uma alteração em angular-cli, criando um plug-in que se conecta ao módulo Compiler do webpack.

O plug-in webpack que nossos engenheiros criaram hooks no cenário PROCESS_ASSETS_STAGE_DEV_TOOLING e preenche o campo x_google_ignoreList nos mapas de origem para os recursos finais gerados pelo webpack e carregado pelo navegador.

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 é feito da perspectiva da máquina e não necessariamente algo que corresponda à perspectiva do desenvolvedor ou ao modelo mental dele do ambiente de execução do aplicativo. Isso é especialmente verdadeiro quando algumas operações são programadas para acontecer de forma 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.

O V8 tem um mecanismo interno para acompanhar essas tarefas assíncronas quando primitivos de programação do navegador padrão são usados, como setTimeout. Nesses casos, isso é feito por padrão, e 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 programação mais avançados, por exemplo, um que execute rastreamento de zona, enfileiramento personalizado de tarefas ou que divida 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, permitindo que os desenvolvedores do framework indiquem os locais em que as operações são programadas e onde elas são executadas.

API Async Stack Tagging

Sem a inclusão de tag assíncrona, os stack traces para códigos que são executados de maneira assíncrona e complexa por frameworks aparecem sem conexão com o código em que foram programados.

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

Com a inclusão de tag assíncrona em pilha, é possível fornecer esse contexto, e o stack trace tem a seguinte aparência:

Um stack trace de um código executado de maneira assíncrona 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 chamado console.createTask(), fornecido pela API Async Stack Tagging. A assinatura é a seguinte:

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

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

Invocar console.createTask() retorna uma instância de Task que pode ser usada mais tarde 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" são exibidas no stack trace em sequência.

As tarefas podem ser executadas quantas vezes forem necessárias, e a carga útil de trabalho pode diferir entre cada execução. A pilha de chamadas no local de agendamento será lembrada até que o objeto da tarefa seja coletado da lixeira.

API Async Stack Tagging API no Angular

No Angular, foram feitas mudanças no NgZone: 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 à NgZone 0.11.8 do Angular por meio das solicitações de envio 46693 e 46958 (links em inglês).

Frames de ligação compatíveis

As estruturas muitas vezes geram código a partir de todos os tipos de linguagens de modelos ao construir um projeto, como os modelos Angular ou JSX, que transformam código HTML em JavaScript simples que, eventualmente, é executado no navegador. Às vezes, esses tipos de funções geradas recebem nomes não muito amigáveis: nomes de letras únicas após serem minimizadas ou alguns nomes obscuros ou desconhecidos, mesmo quando não são.

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

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

Para resolver isso, o Chrome DevTools agora oferece suporte à renomeação dessas funções pelos 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 precisará exibir esse nome no stack trace.

Frames de chamada compatíveis no Angular

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

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

Como parte desse processo de geração de código, também são criados mapas de origem. No momento, estamos explorando maneiras de incluir nomes de funções no campo “names” 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 o nome for não amigável ou removido durante a minificação, os mapas de origem agora poderão incluir o nome mais simples dessa função no campo "names", e o mapeamento do 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 frames de chamada em stack traces.

No futuro

Usar o Angular como um piloto de teste para verificar nosso trabalho tem sido uma experiência maravilhosa. Queremos ouvir a opinião dos desenvolvedores de framework e enviar feedback sobre esses pontos de extensão.

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