É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 la création, en termes de langage source et de structure de projet, tout en ayant accès à des informations qui leur sont familières et pertinentes.

Cet article examine les détails techniques afin de déterminer quelles modifications ont été nécessaires dans Angular et les outils pour les développeurs 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 des outils pour les développeurs Chrome encourage les autres frameworks à adopter les nouvelles API de console et les points d'extension de mappage source afin qu'ils puissent eux aussi offrir une meilleure expérience de débogage à leurs utilisateurs.

Code de l'élément à ignorer

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 de 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 du code qu'ils ne souhaitent pas voir ni parcourir, sans avoir à configurer manuellement cela 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 "Sources" et la boîte de dialogue d'ouverture rapide. Ils peuvent également améliorer le comportement d'exécution des étapes et de la reprise dans le débogueur.

GIF animé montrant les outils de développement avant et après Notez que dans l'image après, les outils de développement affichent le code créé dans l'arborescence, ne suggère plus de fichiers de framework dans le menu "Quick Open" et affichent 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 de la carte source, les outils pour les développeurs Chrome s'en serviront pour identifier les sections du code qui doivent être ajoutées à la liste des éléments à ignorer.

Vous trouverez ci-dessous un mappage source pour un fichier généré out.js. Deux sources d'origine ont contribué à la génération du fichier de sortie: foo.js et lib.js. Le premier est quelque chose qu'un développeur de sites Web a écrit et le second est un cadre 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".
  • Comme résultats dans la boîte de dialogue d'ouverture rapide.
  • 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 index faisant référence au tableau sources: 1. Cela indique que les régions mappées à lib.js sont en fait du code tiers qui doit être automatiquement ajouté à la liste des é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 pendant le processus de compilation incluent ce champ afin d'intégrer ces nouvelles fonctionnalités dans les outils pour les développeurs Chrome.

x_google_ignoreList dans Angular

Depuis 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 que nos ingénieurs ont créé des hooks dans l'étape PROCESS_ASSETS_STAGE_DEV_TOOLING et qui renseigne le champ x_google_ignoreList dans les mappages sources pour les éléments finaux générés par Webpack 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 la 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 par la suite: il peut toujours être intéressant de connaître la "cause racine" ou le côté planification de ces opérations, mais cela ne fera pas partie de la trace de la 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 ! Ce n'est toutefois pas aussi simple dans les projets plus complexes, en particulier lorsque vous utilisez un framework doté de mécanismes de planification plus avancés (par exemple, un système qui effectue un suivi des zones, une mise en file d'attente personnalisée des tâches 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 pour le 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é asynchrone sans aucune information sur la date de planification. Il n'affiche que la trace de la pile à partir de "requestAnimationFrame", mais ne contient aucune information concernant sa planification.

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

Trace de la pile d'un code exécuté asynchrone avec des informations sur sa date de planification. 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 nommé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 pouvez 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 racines" sont affichées dans l'ordre dans la trace de la pile.

Les tâches peuvent être exécutées autant de fois que nécessaire, et la charge utile peut différer entre chaque exécution. La pile d'appel du site de planification est mémorisée jusqu'à ce que l'objet de tâche soit récupéré.

API Async Stack Tagging dans Angular

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

Lors de la planification d'une tâche, la fonction console.createTask() est utilisée lorsqu'elle est disponible. L'instance Task résultante est stockée pour être utilisée par la suite. Après avoir appelé la tâche, NgZone utilisera l'instance Task stockée pour l'exécuter.

Ces modifications ont été intégrées dans la version NgZone 0.11.8 d'Angular via les demandes d'extraction #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 HTML en JavaScript simple qui s'exécute à terme dans le navigateur. Parfois, ces types de fonctions générées reçoivent des noms peu conviviaux : soit des noms à une seule lettre après minimisation, soit des noms obscurs ou inconnus, même s'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 un mappage source comporte 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 cadre 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 devraient se déployer progressivement au fil du temps.

Lors de l'analyse des modèles HTML rédigés 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 fonctions dans le champ "Noms" des mappages sources et de référencer ces noms dans les mappages entre le code généré et le code d'origine.

Par exemple, si une fonction pour un écouteur d'événements est générée et que son nom n'est pas adapté ou a été supprimé lors de la minimisation, les mappages sources peuvent désormais inclure le nom plus convivial de cette fonction dans le champ "names" et faire référence au début du champ d'application de la fonction (c'est-à-dire à la parenthèse gauche de la liste des paramètres). Les outils pour les développeurs Chrome utiliseront ensuite ces noms pour renommer les cadres d'appel dans les traces de la pile.

Perspectives d'avenir

Utiliser Angular comme pilote d'essai pour vérifier notre travail a été une expérience merveilleuse. N'hésitez pas à nous faire part de vos commentaires sur les développeurs de frameworks et à nous faire part de vos commentaires sur ces points d'extension.

Il y a d'autres domaines que nous aimerions explorer. en particulier la manière d'améliorer l'expérience de profilage dans les outils de développement.