É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 chargée des outils pour les développeurs Chrome a collaboré avec l'équipe Angular afin d'améliorer l'expérience de débogage dans les outils pour les développeurs Chrome. Les membres de ces deux équipes ont travaillé ensemble et 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, c'est-à-dire en termes de langage source et de structure de projet, avec un accès à des informations qu'ils connaissent et pertinentes.

Dans cet article, nous allons vous expliquer en détail quelles modifications ont été nécessaires dans Angular et les outils pour les développeurs Chrome pour parvenir à ce résultat. Bien que certaines de ces modifications soient illustrées dans Angular, elles peuvent également s'appliquer à d'autres frameworks. L'équipe des outils pour les développeurs Chrome encourage les autres frameworks à adopter les nouvelles API de la console et les points d'extension des cartes sources afin qu'ils puissent, eux aussi, offrir une meilleure expérience de débogage à leurs utilisateurs.

Ignorer le code d'annonce

Lorsqu'ils déboguent des 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 sous-jacent ou une dépendance cachée dans le dossier node_modules.

Pour ce faire, l'équipe DevTools a lancé une extension appelée x_google_ignoreList pour les cartes sources. Cette extension permet d'identifier les sources tierces, comme le code du 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 à l'avance.

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. Ils peuvent aussi améliorer le comportement d'exécution intermédiaire et de reprise dans le débogueur.

GIF animé montrant les outils de développement avant et après Dans l'image after, les outils de développement affichent le code créé dans l'arborescence, ne suggère plus aucun des fichiers du framework dans le menu "Quick Open" (Ouverture rapide) et affichent une trace de pile beaucoup plus claire à droite.

.

Extension de carte source x_google_ignoreList

Dans les cartes sources, le nouveau champ x_google_ignoreList fait référence au tableau sources et liste les index de toutes les sources tierces connues de cette carte. Lors de l'analyse de la carte source, les outils pour les développeurs Chrome s'en serviront pour déterminer quelles sections du code doivent être ajoutées à la liste des éléments à ignorer.

Vous trouverez ci-dessous un mappage 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 ce qu'un développeur de sites Web a écrit 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 les outils pour les développeurs Chrome affichent ces fichiers par défaut dans le débogueur:

  • Sous forme de fichiers dans l'arborescence des sources
  • En tant que résultats dans la boîte de dialogue d'ouverture rapide.
  • En tant qu'emplacements des cadres d'appel mappés dans les traces de la pile d'erreurs, pendant la pause sur un point d'arrêt et lors de l'exécution d'une étape.

Une information supplémentaire peut maintenant être incluse dans les cartes sources afin d'identifier la source correspondant à du 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. Ce champ 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 (voir 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 un développeur de frameworks ou de bundler, assurez-vous que les cartes sources générées au cours du processus de compilation incluent ce champ afin de pouvoir exploiter ces nouvelles fonctionnalités dans les outils pour les développeurs Chrome.

x_google_ignoreList dans Angular

Depuis la version Angular v14.1.0, le contenu des dossiers node_modules et webpack est marqué comme "à ignorer".

Pour ce faire, une modification a été apportée à angular-cli en créant un plug-in qui se connecte au module Compiler de webpack.

Le plug-in webpack que nos ingénieurs a créé effectue des hooks à l'étape PROCESS_ASSETS_STAGE_DEV_TOOLING et remplit le champ x_google_ignoreList dans les mappages source pour les éléments finaux générés par webpack et le navigateur chargé.

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 la pile répondent à la question "Comment en suis-je arrivé jusqu'ici", mais souvent du point de vue de la machine, et pas nécessairement du point de vue du développeur ou de son modèle mental d'exécution de l'application. C'est particulièrement vrai lorsque certaines opérations sont planifiées pour se produire ultérieurement de manière asynchrone: il peut toujours être intéressant de connaître la "cause" ou le côté planification de ces opérations, mais cela ne fera pas partie d'une trace de pile asynchrone.

V8 dispose, en interne, d'un mécanisme permettant d'effectuer le suivi de ces tâches asynchrones lorsque des primitives de planification de navigateur standards, telles que setTimeout, sont utilisées. Dans ce cas, cette opération est effectuée par défaut, afin que les développeurs puissent déjà les inspecter. Mais dans les projets plus complexes, ce n'est pas aussi simple que cela, en particulier lors de l'utilisation d'un framework doté de mécanismes de planification plus avancés, par exemple un framework qui effectue un suivi de zone, 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 exposent un mécanisme appelé "API Async Stack Tagging" sur l'objet console, qui permet aux développeurs de frameworks d'indiquer à la fois les emplacements où les opérations sont planifiées et celui où ces opérations 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 par des frameworks complexes s'affichent sans lien avec le code où elle a été planifiée.

Trace de la pile d'un code asynchrone exécuté sans informations sur la date de planification Il n'affiche que la trace de la pile à partir de "requestAnimationFrame", mais ne contient aucune information depuis 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 asynchrone exécuté avec des informations sur la date de planification Notez que, contrairement à ce qui précède, 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. Voici sa signature:

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 ensuite utiliser 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 principales" 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 de travail peut varier d'une exécution à l'autre. La pile d'appel du site de planification sera mémorisée jusqu'à ce que l'objet de la tâche soit ramassé.

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, la valeur console.createTask() est utilisée lorsqu'elle est disponible. L'instance Task obtenue est stockée pour une utilisation ultérieure. Lorsque la tâche est appelée, NgZone utilise l'instance Task stockée pour l'exécuter.

Ces modifications ont été introduites dans la zone NgZone 0.11.8 d'Angular via des demandes d'extraction n° 46693 et n° 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, comme les modèles Angular ou JSX qui transforment du code HTML en JavaScript simple qui s'exécute finalement dans le navigateur. Parfois, ces types de fonctions générées reçoivent des noms peu conviviaux, qu'il s'agisse de noms à une seule lettre après leur réduction ou de 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 cartes 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 de paramètres), le frame d'appel doit afficher ce nom dans la trace de la pile.

Trames d'appel conviviales dans Angular

Renommer les frames d'appel dans Angular est un effort continu. Ces améliorations devraient être apporté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 ensuite transpilé en code JavaScript que le navigateur charge et exécute.

Dans le cadre de ce processus de génération de code, des cartes sources sont également créées. Nous étudions actuellement des moyens d'inclure des noms de fonctions dans le champ "names" 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 est non convivial ou supprimé lors de la minimisation, les mappages sources peuvent désormais inclure le nom plus convivial de cette fonction dans le champ "names". Le mappage pour le 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 de 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

Nous avons utilisé Angular en tant que pilote d'essai pour vérifier notre travail, ce qui a été une expérience formidable. Nous aimerions recueillir l'avis de développeurs de frameworks et nous faire part de 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 les outils de développement.