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

Amélioration de l'expérience de débogage

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 passe en revue les détails techniques pour comprendre quelles modifications ont été nécessaires dans Angular et les outils pour les développeurs Chrome. 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 la 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

Lors du débogage d'applications à l'aide des outils pour les développeurs Chrome, les auteurs ne veulent généralement voir que leur code, et non celui du framework situé en dessous ou 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 bundler. Lorsqu'un framework utilise cette extension, les auteurs évitent désormais automatiquement le code qu'ils ne souhaitent pas voir ou exécuter sans avoir à le configurer manuellement au préalable.

En pratique, les outils pour les développeurs Chrome peuvent masquer automatiquement le code identifié comme tel dans les traces de la pile, l'arborescence des sources et la boîte de dialogue d'ouverture rapide, et améliorer le comportement d'exécution des étapes 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 mappages sources, le nouveau champ x_google_ignoreList fait référence au tableau sources et répertorie les index de toutes les sources tierces connues dans ce mappage source. 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 un mappage source pour un fichier généré out.js. 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;"
}

Le fichier sourcesContent est inclus pour ces deux sources d'origine, et les outils pour les développeurs Chrome affichent 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".
  • Les emplacements des frames d'appel mappés dans les traces de la pile d'erreurs sont mis en pause au niveau d'un point d'arrêt et lors d'étapes.

Une information supplémentaire peut désormais être incluse dans les mappages sources afin d'identifier la source de 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 index 2, 4 et 5 indiquent que les régions mappées à lib1.ts, lib2.coffee et hmr.js sont toutes du code tiers qui doit être automatiquement ajouté à la liste des éléments à 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 cela, nous avons modifié angular-cli en créant un plug-in qui se connecte 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 que webpack génère et que le navigateur charge.

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 s'est déroulée l'arrivée", mais le plus souvent, c'est du point de vue de la machine, et pas nécessairement du point de vue du développeur ou 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 standards sont utilisées, comme setTimeout. Cette opération est effectuée par défaut dans ces cas de figure. Les développeurs peuvent donc 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, les outils de développement proposent un mécanisme appelé "API Async Stack Tagging" au niveau de l'objet console. Il permet aux développeurs de framework d'indiquer les emplacements où les opérations sont planifiées et celles 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 obtenue est stockée pour être utilisée par la suite. 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 résoudre ce problème, les outils pour les développeurs Chrome permettent désormais de renommer ces fonctions via des mappages 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.

Frames d'appel 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.

Ce processus de génération de code implique également la création de mappages sources. 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 framework et recevoir leurs commentaires sur ces points d'extension.

Il y a d'autres domaines que nous aimerions explorer. En particulier, comment améliorer l'expérience de profilage dans DevTools.