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

Uma experiência de depuração melhorada

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 das duas equipes trabalharam juntas e tomaram medidas para permitir que os desenvolvedores debuguem e criem perfis de aplicativos da Web na perspectiva de criação: em termos de linguagem de origem e estrutura do projeto, com acesso a informações familiares e relevantes para eles.

Esta postagem mostra quais mudanças no Angular e no Chrome DevTools foram necessárias para 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 outros frameworks a adotar as novas APIs do console e os pontos de extensão do mapa de origem para que eles 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 apenas o código deles, não o do framework 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 fontes de terceiros, como código de framework ou código gerado pelo bundler. Quando um framework usa essa extensão, os autores evitam automaticamente o código que não querem ver ou percorrer sem precisar configurar isso manualmente antes.

Na prática, o Chrome DevTools pode ocultar automaticamente o código identificado como tal em stack traces, na árvore "Sources", na caixa de diálogo "Quick Open" e também melhorar o comportamento de execução e retomada no depurador.

Um GIF animado mostrando o DevTools antes e depois. Observe como, na imagem depois, as Ferramentas do desenvolvedor mostram o código criado pelo autor na árvore, não sugerem mais nenhum dos arquivos do framework no menu "Abrir rapidamente" e mostram um stack trace muito mais limpo à direita.

.

A extensão do 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 fontes de terceiros conhecidas nesse mapa. Ao analisar o mapa de origem, as Ferramentas para desenvolvedores do Chrome vão usar isso para descobrir quais seções do código devem ser ignoradas.

Confira abaixo um mapa de origem para um arquivo gerado out.js. Há dois sources originais que contribuíram para gerar o arquivo de saída: foo.js e lib.js. O primeiro é algo que um desenvolvedor de sites escreveu, e o segundo é uma estrutura que ele usou.

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

O sourcesContent é incluído para ambas as origens originais, e o Chrome DevTools mostra esses arquivos por padrão no depurador:

  • Como arquivos na árvore de origens.
  • Como resultados na caixa de diálogo "Abrir rapidamente".
  • Como locais de quadro de chamada mapeados em rastreamentos de pilha de erros enquanto pausado em um ponto de interrupção e durante a execução.

Agora é possível incluir mais uma informação nos mapas de origem para identificar quais delas são códigos próprios 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 adicionados 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ê é um desenvolvedor de framework ou bundler, verifique se os mapas de origem gerados durante o processo de build incluem esse campo para se conectar a esses novos recursos no Chrome DevTools.

x_google_ignoreList no Angular

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

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

O plug-in do webpack criado pelos nossos engenheiros se conecta ao estágio PROCESS_ASSETS_STAGE_DEV_TOOLING e preenche o campo x_google_ignoreList nos mapas de origem dos recursos finais gerados pelo webpack e carregados 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 aqui”, mas muitas vezes isso é da perspectiva da máquina e não necessariamente algo que corresponda à perspectiva do desenvolvedor ou ao modelo mental dele do tempo de execução do aplicativo. Isso é especialmente verdadeiro quando algumas operações são programadas para ocorrer 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 faz parte de um stack trace assíncrono.

O V8 tem um mecanismo interno para acompanhar essas tarefas assíncronas quando são usadas primitivas de programação padrão do navegador, como setTimeout. Isso é feito por padrão nesses casos, para que os desenvolvedores possam inspecionar. Mas, em projetos mais complexos, não é tão simples assim, especialmente quando se usa uma estrutura com mecanismos de programação mais avançados, por exemplo, que realiza o rastreamento de zonas, a fila de tarefas personalizadas ou que divide as atualizações em várias unidades de trabalho executadas ao longo do tempo.

Para resolver isso, as Ferramentas do desenvolvedor expõem um mecanismo chamado "API de inclusão de marcação de pilha assíncrona" no objeto console, que permite que os desenvolvedores de framework indiquem os locais em que as operações são programadas e executadas.

A API Async Stack Tagging

Sem a inclusão de tags de pilha assíncrona, os rastros de pilha para códigos executados de forma assíncrona de maneiras complexas por frameworks aparecem sem conexão com o código em que foram programados.

Um stack trace de algum código executado de forma 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 sobre quando foi programado.

Com a inclusão de tags de pilha assíncrona, é possível fornecer esse contexto, e o stack trace fica assim:

Um stack trace de algum código executado de forma assíncrona com informações sobre quando ele foi programado. Observe que, ao contrário do anterior, ele inclui "businessLogic" e "schedule" no stack trace.

Para 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;
}

A invocação de console.createTask() retorna uma instância de 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" vão ser mostradas no stack trace em sequência.

As tarefas podem ser executadas várias vezes, e o payload do trabalho pode ser diferente em cada execução. A pilha de chamadas no site de programação será lembrada até que o objeto da tarefa seja coletado da lixeira.

A API Async Stack Tagging no Angular

No Angular, foram feitas mudanças 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 vai usar a instância Task armazenada para executá-la.

Essas mudanças foram lançadas no NgZone 0.11.8 do Angular por meio dos pull requests #46693 e #46958.

Frames de chamada amigáveis

Os frameworks geralmente geram código de todos os tipos de linguagens de modelagem ao criar um projeto, como modelos Angular ou JSX que transformam código com aparência de HTML em JavaScript simples que é executado no navegador. Às vezes, esses tipos de funções geradas recebem nomes que não são muito amigáveis, como nomes de uma única letra após a minificação ou nomes obscuros ou desconhecidos, mesmo quando não são.

No Angular, não é incomum encontrar frames de chamada com nomes como AppComponent_Template_app_button_handleClick_1_listener em rastros de pilha.

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

Para resolver esse problema, o Chrome DevTools agora oferece suporte à renomeação dessas 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

Renomear frames de chamada no Angular é um esforço contínuo. Esperamos que essas melhorias sejam lançadas gradualmente ao longo do tempo.

Ao analisar os modelos HTML criados pelos autores, o compilador do Angular gera código TypeScript, que é transpilado para 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 analisando maneiras de incluir nomes de função no campo "names" dos mapas de origem e fazer referência a esses nomes nos mapeamentos entre o código gerado e o original.

Por exemplo, se uma função para um listener de eventos for gerada e o nome dela for removido ou não for amigável durante a minificação, os mapas de origem agora poderão incluir o nome mais amigável dessa 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). As Ferramentas do desenvolvedor do Chrome vão usar esses nomes para renomear frames de chamada em stack traces.

No futuro

Usar o Angular como piloto de teste para verificar nosso trabalho foi uma experiência maravilhosa. Queremos saber a opinião dos desenvolvedores de framework e receber feedback sobre esses pontos de extensão.

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