Étude de cas: Améliorer le débogage Angular avec les outils de développement

Une expérience de débogage améliorée

Au cours des derniers mois, l'équipe des outils pour les développeurs Chrome a collaboré avec l'équipe Angular pour améliorer l'expérience de débogage dans les outils pour les développeurs Chrome. Les membres des deux équipes ont travaillé ensemble et ont pris des mesures pour permettre aux développeurs de déboguer et de profiler des applications Web du point de vue de l'auteur: en termes de langue source et de structure de projet, avec un accès à des informations familières et pertinentes pour eux.

Cet article vous présente les modifications apportées à Angular et aux outils de développement Chrome pour y parvenir. Même si certaines de ces modifications sont illustrées par Angular, elles peuvent également être appliquées à d'autres frameworks. L'équipe Chrome DevTools encourage les autres frameworks à adopter les nouvelles API de console et les points d'extension de la carte source afin qu'ils puissent également offrir une meilleure expérience de débogage à leurs utilisateurs.

Code d'ignorement de la fiche

Lorsque vous déboguez des applications à l'aide des outils pour les développeurs Chrome, vous ne souhaitez généralement voir que votre code, et non celui du framework sous-jacent ni une dépendance cachée dans le dossier node_modules.

Pour ce faire, l'équipe DevTools a introduit une extension des cartes sources, appelée x_google_ignoreList. Cette extension permet d'identifier les sources tierces telles que le code de framework ou le code généré par le bundler. Lorsqu'un framework utilise cette extension, les auteurs évitent désormais automatiquement le code qu'ils ne souhaitent pas voir ou suivre sans avoir à le configurer manuellement au préalable.

En pratique, Chrome DevTools peut automatiquement masquer le code identifié comme tel dans les traces de pile, l'arborescence des sources, la boîte de dialogue "Ouvrir rapidement", et améliorer le comportement de la progression et de la reprise dans le débogueur.

GIF animé montrant DevTools avant et après. Notez que dans l'image d'après, DevTools affiche le code créé dans l'arborescence, ne suggère plus aucun fichier de framework dans le menu "Ouvrir rapidement" et affiche une trace de pile beaucoup plus claire à droite.

Extension de carte source x_google_ignoreList

Dans les maps de sources, le nouveau champ x_google_ignoreList fait référence au tableau sources et liste les indices de toutes les sources tierces connues de cette map de sources. Lors de l'analyse du mappage source, Chrome DevTools utilise cette information pour déterminer quelles sections du code doivent être ajoutées à la liste d'éléments à ignorer.

Vous trouverez ci-dessous une carte source pour un fichier out.js généré. Deux sources d'origine ont contribué à générer le fichier de sortie: foo.js et lib.js. Le premier est un code écrit par un développeur de site Web, et le second est un framework qu'il a utilisé.

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

sourcesContent est inclus pour ces deux sources d'origine, et Chrome DevTools affiche ces fichiers par défaut dans le débogueur:

  • En tant que fichiers dans l'arborescence "Sources".
  • dans la boîte de dialogue "Ouvrir rapidement".
  • En tant qu'emplacements de frame d'appel mappés dans les traces de pile d'erreur lorsque vous êtes arrêté sur un point d'arrêt et lorsque vous effectuez un pas à pas.

Une information supplémentaire peut désormais être incluse dans les maps de sources pour identifier laquelle de ces sources est du code propriétaire ou tiers:

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

Le nouveau champ x_google_ignoreList contient un seul indice faisant référence au tableau sources: 1. Cela spécifie que les régions mappées sur lib.js sont en fait du code tiers qui doit être automatiquement ajouté à la liste d'éléments à ignorer.

Dans un exemple plus complexe, illustré ci-dessous, les indices 2, 4 et 5 indiquent que les régions mappées sur lib1.ts, lib2.coffee et hmr.js sont toutes du code tiers qui doit être automatiquement ajouté à la liste d'ignorer.

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

Si vous êtes développeur de framework ou de bundler, assurez-vous que les mappages sources générés lors du processus de compilation incluent ce champ afin de pouvoir exploiter ces nouvelles fonctionnalités dans les outils de développement Chrome.

x_google_ignoreList dans Angular

À partir de la version Angular v14.1.0, le contenu des dossiers node_modules et webpack a été marqué comme "à ignorer".

Pour ce faire, nous avons modifié angular-cli en créant un plug-in qui s'associe au module Compiler de webpack.

Le plug-in webpack créé par nos ingénieurs s'intègre à l'étape PROCESS_ASSETS_STAGE_DEV_TOOLING et renseigne le champ x_google_ignoreList dans les mappages sources pour les éléments finaux générés par webpack et chargés par le navigateur.

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)));

Traces de pile associées

Les traces de pile répondent à la question "Comment suis-je arrivé ici ?", mais souvent du point de vue de la machine, et pas nécessairement de celui du développeur ni de son modèle mental de l'environnement d'exécution de l'application. Cela est particulièrement vrai lorsque certaines opérations sont planifiées pour se produire de manière asynchrone plus tard: il peut toujours être intéressant de connaître la "cause racine" ou le côté planification de ces opérations, mais c'est exactement ce qui ne figurera pas dans une trace de pile asynchrone.

V8 dispose en interne d'un mécanisme permettant de suivre ces tâches asynchrones lorsque des primitives de planification de navigateur standard sont utilisées, telles que setTimeout. Dans ces cas, cela est effectué par défaut, de sorte que les développeurs puissent déjà les inspecter. Toutefois, dans les projets plus complexes, ce n'est pas aussi simple, en particulier lorsque vous utilisez un framework avec des mécanismes de planification plus avancés, par exemple un framework qui effectue le suivi des zones, la mise en file d'attente de tâches personnalisées ou qui divise les mises à jour en plusieurs unités de travail exécutées au fil du temps.

Pour résoudre ce problème, DevTools expose un mécanisme appelé "API de taggage de pile asynchrone" sur l'objet console, qui permet aux développeurs de framework d'indiquer à la fois les emplacements où les opérations sont planifiées et les emplacements où elles sont exécutées.

API Async Stack Tagging

Sans le taggage de pile asynchrone, les traces de pile du code exécuté de manière asynchrone de manière complexe par les frameworks s'affichent sans connexion au code où il a été planifié.

Trace de la pile d'un code exécuté de manière asynchrone, sans aucune information sur le moment où il a été planifié. Il n'affiche que la trace de la pile à partir de "requestAnimationFrame", mais ne contient aucune information sur le moment où elle a été planifiée.

Avec le taggage de pile asynchrone, vous pouvez fournir ce contexte. La trace de la pile se présente comme suit:

Trace de la pile d'un code exécuté de manière asynchrone, avec des informations sur le moment où il a été planifié. Notez que, contrairement à la précédente, elle inclut "businessLogic" et "schedule" dans la trace de la pile.

Pour ce faire, utilisez une nouvelle méthode console appelée console.createTask(), fournie par l'API Async Stack Tagging. Sa signature est la suivante:

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

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

L'appel de console.createTask() renvoie une instance Task que vous pourrez utiliser ultérieurement pour exécuter le code asynchrone.

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

// Task Execution
task.run(f);

Les opérations asynchrones peuvent également être imbriquées, et les "causes profondes" s'affichent dans la trace de la pile de manière séquentielle.

Les tâches peuvent être exécutées un nombre illimité de fois, et la charge utile de travail peut varier à chaque exécution. La pile d'appels sur le site de planification est conservée jusqu'à ce que l'objet de tâche soit collecté.

API Async Stack Tagging dans Angular

Dans Angular, des modifications ont été apportées à NgZone, le contexte d'exécution d'Angular qui persiste entre les tâches asynchrones.

Lors de la planification d'une tâche, console.createTask() est utilisé si disponible. L'instance Task générée est stockée pour une utilisation ultérieure. Lors de l'appel de la tâche, NgZone utilisera l'instance Task stockée pour l'exécuter.

Ces modifications ont été apportées à NgZone 0.11.8 d'Angular via les requêtes pull 46693 et 46958.

Cadres d'appel conviviaux

Les frameworks génèrent souvent du code à partir de toutes sortes de langages de création de modèles lors de la création d'un projet, tels que des modèles Angular ou JSX qui transforment du code ressemblant à du code HTML en code JavaScript pur qui s'exécute finalement dans le navigateur. Parfois, ces types de fonctions générées sont associées à des noms peu pratiques : des noms à une seule lettre après avoir été minifiés, ou des noms obscurs ou inconnus, même lorsqu'ils ne le sont pas.

Dans Angular, il n'est pas rare de voir des cadres d'appel avec des noms tels que AppComponent_Template_app_button_handleClick_1_listener dans les traces de pile.

Capture d&#39;écran de la trace de la pile avec un nom de fonction généré automatiquement.

Pour y remédier, les outils pour les développeurs Chrome permettent désormais de renommer ces fonctions via des cartes sources. Si une carte source contient une entrée de nom pour le début d'un champ d'application de fonction (c'est-à-dire la parenthèse gauche de la liste des paramètres), le bloc d'appel doit afficher ce nom dans la trace de la pile.

Cadres d'appels conviviaux dans Angular

Le changement de nom des frames d'appel dans Angular est un effort continu. Ces améliorations seront déployées progressivement au fil du temps.

Lors de l'analyse des modèles HTML écrits par les auteurs, le compilateur Angular génère du code TypeScript, qui est finalement transpilé en code JavaScript que le navigateur charge et exécute.

Dans le cadre de ce processus de génération de code, des mappages de sources sont également créés. Nous étudions actuellement des moyens d'inclure les noms de fonction dans le champ "names" des maps sources et de les référencer dans les mappages entre le code généré et le code d'origine.

Par exemple, si une fonction pour un écouteur d'événement est générée et que son nom n'est pas convivial ou est supprimé lors de la minification, les mappages de source peuvent désormais inclure le nom plus convivial de cette fonction dans le champ "names", et le mappage du début du champ d'application de la fonction peut désormais faire référence à ce nom (c'est-à-dire la parenthèse gauche de la liste des paramètres). Chrome DevTools utilisera ensuite ces noms pour renommer les cadres d'appel dans les traces de pile.

Perspectives

L'utilisation d'Angular en tant que pilote de test pour vérifier notre travail a été une expérience formidable. Nous aimerions connaître l'avis des développeurs de frameworks et recevoir leurs commentaires sur ces points d'extension.

Nous aimerions explorer d'autres domaines. En particulier, comment améliorer l'expérience de profilage dans DevTools.