Déboguer WebAssembly avec des outils modernes

Ingvar Stepanyan
Ingvar Stepanyan

Le chemin parcouru jusqu'à présent

Il y a un an, Chrome a annoncé la prise en charge initiale du débogage WebAssembly natif dans les outils pour les développeurs Chrome.

Nous avons présenté la prise en charge de la progression de base et évoqué les opportunités que l'utilisation d'informations DWARF au lieu de cartes sources nous ouvrira à l'avenir:

  • Résoudre les noms de variables
  • Types d'impression élégante
  • Évaluer des expressions dans les langues sources
  • … et bien plus encore !

Aujourd'hui, nous sommes ravis de présenter les fonctionnalités promises et les progrès réalisés par les équipes Emscripten et Chrome DevTools au cours de cette année, en particulier pour les applications C et C++.

Avant de commencer, veuillez garder à l'esprit qu'il s'agit toujours d'une version bêta de la nouvelle expérience. Vous devez utiliser la dernière version de tous les outils à vos risques et périls. Si vous rencontrez des problèmes, veuillez les signaler sur https://issues.chromium.org/issues/new?noWizard=true&template=0&component=1456350.

Commençons par le même exemple C simple que la dernière fois:

#include <stdlib.h>

void assert_less(int x, int y) {
  if (x >= y) {
    abort();
  }
}

int main() {
  assert_less(10, 20);
  assert_less(30, 20);
}

Pour le compiler, nous utilisons la dernière version d'Emscripten et transmettons un indicateur -g, comme dans le post d'origine, pour inclure des informations de débogage:

emcc -g temp.c -o temp.html

Nous pouvons maintenant diffuser la page générée à partir d'un serveur HTTP localhost (par exemple, avec serve) et l'ouvrir dans la dernière version de Chrome Canary.

Cette fois, nous aurons également besoin d'une extension d'assistance qui s'intègre aux outils de développement Chrome et l'aide à comprendre toutes les informations de débogage encodées dans le fichier WebAssembly. Veuillez l'installer en accédant à ce lien: goo.gle/wasm-debugging-extension.

Vous devez également activer le débogage WebAssembly dans les tests de DevTools. Ouvrez les outils pour les développeurs Chrome, cliquez sur l'icône en forme de roue dentée  en haut à droite du volet des outils pour les développeurs, accédez au panneau Tests et cochez WebAssembly Debugging: Enable DWARF support (Débogage WebAssembly : activer la prise en charge de DWARF).

Volet &quot;Tests&quot; des paramètres DevTools

Lorsque vous fermez Settings (Paramètres), les outils de développement vous suggèrent de les recharger pour appliquer les paramètres. C'est ce que nous allons faire. C'est tout pour la configuration ponctuelle.

Nous pouvons maintenant revenir au panneau Sources, activer Mettre en pause en cas d'exception (icône ⏸), puis cocher Mettre en pause en cas d'exception détectée et actualiser la page. Les outils pour les développeurs devraient être suspendus sur une exception:

Capture d&#39;écran du panneau &quot;Sources&quot; montrant comment activer &quot;Suspendre sur les exceptions interceptées&quot;

Par défaut, il s'arrête sur un code de liaison généré par Emscripten, mais à droite, vous pouvez voir une vue Call Stack (Pile d'appels) représentant la trace de la pile de l'erreur et accéder à la ligne C d'origine qui a appelé abort:

DevTools est mis en pause dans la fonction &quot;assert_less&quot; et affiche les valeurs de &quot;x&quot; et &quot;y&quot; dans la vue &quot;Champ d&#39;application&quot;.

Si vous regardez dans la vue Champ d'application, vous pouvez voir les noms et les valeurs d'origine des variables dans le code C/C++. Vous n'avez plus à essayer de comprendre la signification de noms tronqués comme $localN et leur relation avec le code source que vous avez écrit.

Cela s'applique non seulement aux valeurs primitives telles que les entiers, mais également aux types composés tels que les structures, les classes, les tableaux, etc.

Compatibilité avec les types enrichis

Examinons un exemple plus complexe pour les illustrer. Cette fois, nous allons dessiner un fractal de Mandelbrot avec le code C++ suivant:

#include <SDL2/SDL.h>
#include <complex>

int main() {
  // Init SDL.
  int width = 600, height = 600;
  SDL_Init(SDL_INIT_VIDEO);
  SDL_Window* window;
  SDL_Renderer* renderer;
  SDL_CreateWindowAndRenderer(width, height, SDL_WINDOW_OPENGL, &window,
                              &renderer);

  // Generate a palette with random colors.
  enum { MAX_ITER_COUNT = 256 };
  SDL_Color palette[MAX_ITER_COUNT];
  srand(time(0));
  for (int i = 0; i < MAX_ITER_COUNT; ++i) {
    palette[i] = {
        .r = (uint8_t)rand(),
        .g = (uint8_t)rand(),
        .b = (uint8_t)rand(),
        .a = 255,
    };
  }

  // Calculate and draw the Mandelbrot set.
  std::complex<double> center(0.5, 0.5);
  double scale = 4.0;
  for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
      std::complex<double> point((double)x / width, (double)y / height);
      std::complex<double> c = (point - center) * scale;
      std::complex<double> z(0, 0);
      int i = 0;
      for (; i < MAX_ITER_COUNT - 1; i++) {
        z = z * z + c;
        if (abs(z) > 2.0)
          break;
      }
      SDL_Color color = palette[i];
      SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);
      SDL_RenderDrawPoint(renderer, x, y);
    }
  }

  // Render everything we've drawn to the canvas.
  SDL_RenderPresent(renderer);

  // SDL_Quit();
}

Vous pouvez constater que cette application est encore assez petite. Il s'agit d'un seul fichier contenant 50 lignes de code. Toutefois, cette fois, j'utilise également des API externes, comme la bibliothèque SDL pour les graphiques, ainsi que les nombres complexes de la bibliothèque standard C++.

Je vais le compiler avec le même indicateur -g que ci-dessus pour inclure des informations de débogage. Je vais également demander à Emscripten de fournir la bibliothèque SDL2 et d'autoriser une mémoire de taille arbitraire:

emcc -g mandelbrot.cc -o mandelbrot.html \
     -s USE_SDL=2 \
     -s ALLOW_MEMORY_GROWTH=1

Lorsque j'accède à la page générée dans le navigateur, je vois la belle forme fractale avec des couleurs aléatoires:

Page de démonstration

Lorsque j'ouvre DevTools, je vois à nouveau le fichier C++ d'origine. Cette fois, cependant, il n'y a pas d'erreur dans le code (ouf !). Nous allons donc définir un point d'arrêt au début de notre code.

Lorsque nous actualisons à nouveau la page, le débogueur se met en pause dans notre source C++:

Les outils de développement ont été mis en pause à l&#39;appel de SDL_Init.

Nous pouvons déjà voir toutes nos variables à droite, mais seules width et height sont initialisées pour le moment. Il n'y a donc pas grand-chose à inspecter.

Définissons un autre point d'arrêt dans notre boucle principale Mandelbrot, puis reprenons l'exécution pour avancer un peu.

DevTools mis en pause dans les boucles imbriquées

À ce stade, notre palette a été rempli de couleurs aléatoires. Nous pouvons développer le tableau lui-même, ainsi que les structures SDL_Color individuelles, et inspecter leurs composants pour vérifier que tout est correct (par exemple, que le canal "alpha" est toujours défini sur une opacité complète). De même, nous pouvons développer et vérifier les parties réelle et imaginaire du nombre complexe stocké dans la variable center.

Si vous souhaitez accéder à une propriété profondément imbriquée qui est autrement difficile à atteindre via la vue Champ d'application, vous pouvez également utiliser l'évaluation de la console. Notez toutefois que les expressions C++ plus complexes ne sont pas encore prises en charge.

Panneau de la console affichant le résultat de &quot;palette[10].r&quot;

Reprenons l'exécution plusieurs fois pour voir comment l'x interne change également. Pour ce faire, consultez à nouveau la vue Champ d'application, ajoutez le nom de la variable à la liste de surveillance, évaluez-la dans la console ou pointez sur la variable dans le code source:

Info-bulle sur la variable &quot;x&quot; dans la source affichant sa valeur &quot;3&quot;

À partir de là, nous pouvons exécuter des étapes ou ignorer des instructions C++, et observer comment les autres variables changent également:

Vue des info-bulles et de la portée affichant les valeurs de &quot;color&quot;, &quot;point&quot; et d&#39;autres variables

Tout fonctionne parfaitement lorsqu'une information de débogage est disponible, mais que se passe-t-il si nous voulons déboguer un code qui n'a pas été compilé avec les options de débogage ?

Débogage WebAssembly brut

Par exemple, nous avons demandé à Emscripten de nous fournir une bibliothèque SDL précompilée au lieu de la compiler nous-mêmes à partir de la source. Par conséquent, le débogueur ne peut pas trouver les sources associées, du moins pour le moment. Revenons à la SDL_RenderDrawColor:

DevTools affichant la vue d&#39;assemblage de &quot;mandelbrot.wasm&quot;

Nous revenons à l'expérience de débogage WebAssembly brute.

Cela peut sembler un peu effrayant et la plupart des développeurs Web n'auront jamais à s'en occuper, mais il est possible que vous souhaitiez déboguer une bibliothèque compilée sans informations de débogage, que ce soit parce qu'il s'agit d'une bibliothèque tierce sur laquelle vous n'avez aucun contrôle ou parce que vous rencontrez l'un de ces bugs qui ne se produit que en production.

Pour vous aider dans ces cas, nous avons également apporté des améliorations à l'expérience de débogage de base.

Tout d'abord, si vous avez déjà utilisé le débogage WebAssembly brut, vous remarquerez peut-être que l'intégralité du désassemblage est désormais affichée dans un seul fichier. Vous n'aurez plus à deviner à quelle fonction une entrée wasm-53834e3e/ wasm-53834e3e-7 Sources correspond.

Nouveau schéma de génération de noms

Nous avons également amélioré les noms dans la vue d'assemblage. Auparavant, vous ne voyiez que des indices numériques ou, dans le cas des fonctions, aucun nom du tout.

Nous générons maintenant des noms de manière similaire aux autres outils de désassemblage, en utilisant des indices de la section sur le nom WebAssembly, des chemins d'importation/d'exportation et, enfin, en cas d'échec, en les générant en fonction du type et de l'indice de l'élément, comme $func123. Dans la capture d'écran ci-dessus, vous pouvez voir comment cela permet déjà d'obtenir des traces de pile et un désassemblage légèrement plus lisibles.

Lorsque les informations de type ne sont pas disponibles, il peut être difficile d'inspecter d'autres valeurs que les primitives. Par exemple, les pointeurs s'affichent sous la forme d'entiers standards, sans aucun moyen de savoir ce qui est stocké derrière eux en mémoire.

Inspection de la mémoire

Auparavant, vous ne pouviez développer que l'objet de mémoire WebAssembly, représenté par env.memory dans la vue Champ d'application, pour rechercher des octets individuels. Cela fonctionnait dans certains scénarios triviaux, mais n'était pas particulièrement pratique à développer et ne permettait pas de réinterpréter les données dans des formats autres que les valeurs d'octet. Nous avons également ajouté une nouvelle fonctionnalité pour vous aider: un inspecteur de mémoire linéaire.

Si vous effectuez un clic droit sur env.memory, une nouvelle option appelée Inspecter la mémoire devrait s'afficher:

Menu contextuel sur &quot;env.memory&quot; dans le volet &quot;Champ d&#39;application&quot; affichant un élément &quot;Inspecter la mémoire&quot;

Un outil d'inspection de la mémoire s'affiche alors, dans lequel vous pouvez inspecter la mémoire WebAssembly dans les vues hexadécimale et ASCII, accéder à des adresses spécifiques et interpréter les données dans différents formats:

Volet de l&#39;outil d&#39;inspection de la mémoire dans DevTools affichant des vues hexadécimales et ASCII de la mémoire

Scénarios et mises en garde avancés

Profiler du code WebAssembly

Lorsque vous ouvrez les outils de développement, le code WebAssembly est "dégradé" vers une version non optimisée pour permettre le débogage. Cette version est beaucoup plus lente, ce qui signifie que vous ne pouvez pas vous fier à console.time, performance.now et à d'autres méthodes de mesure de la vitesse de votre code lorsque DevTools est ouvert, car les chiffres que vous obtenez ne représentent pas du tout les performances réelles.

Utilisez plutôt le panneau "Performances" de DevTools, qui exécute le code à pleine vitesse et vous fournit une répartition détaillée du temps passé dans différentes fonctions:

Panneau de profilage affichant différentes fonctions Wasm

Vous pouvez également exécuter votre application avec les outils pour les développeurs fermés, puis les ouvrir une fois terminé pour inspecter la console.

Nous allons améliorer les scénarios de profilage à l'avenir, mais pour le moment, il s'agit d'un point à prendre en compte. Pour en savoir plus sur les scénarios de hiérarchisation WebAssembly, consultez notre documentation sur le pipeline de compilation WebAssembly.

Compilation et débogage sur différentes machines (y compris Docker / hôte)

Lorsque vous effectuez une compilation dans un Docker, une machine virtuelle ou sur un serveur de compilation distant, vous rencontrerez probablement des situations où les chemins d'accès aux fichiers sources utilisés lors de la compilation ne correspondent pas aux chemins de votre propre système de fichiers où les outils de développement Chrome s'exécutent. Dans ce cas, les fichiers s'affichent dans le panneau Sources, mais ne se chargent pas.

Pour résoudre ce problème, nous avons implémenté une fonctionnalité de mappage de chemin d'accès dans les options d'extension C/C++. Vous pouvez l'utiliser pour remapper des chemins arbitraires et aider les outils de développement à localiser des sources.

Par exemple, si le projet sur votre machine hôte se trouve sous un chemin C:\src\my_project, mais qu'il a été créé dans un conteneur Docker où ce chemin était représenté par /mnt/c/src/my_project, vous pouvez le remapper lors du débogage en spécifiant ces chemins en tant que préfixes:

Page &quot;Options&quot; de l&#39;extension de débogage C/C++

Le premier préfixe correspondant "gagne". Si vous connaissez d'autres débogueurs C++, cette option est semblable à la commande set substitute-path dans GDB ou à un paramètre target.source-map dans LLDB.

Déboguer des builds optimisés

Comme pour les autres langages, le débogage fonctionne mieux si les optimisations sont désactivées. Les optimisations peuvent intégrer des fonctions les unes aux autres, réorganiser le code ou supprimer complètement des parties du code. Tout cela peut prêter à confusion pour le débogueur et, par conséquent, pour vous en tant qu'utilisateur.

Si vous n'êtes pas gêné par une expérience de débogage plus limitée et que vous souhaitez toujours déboguer un build optimisé, la plupart des optimisations fonctionneront comme prévu, à l'exception de l'intégration de fonction. Nous prévoyons de résoudre les problèmes restants à l'avenir, mais pour l'instant, veuillez utiliser -fno-inline pour le désactiver lors de la compilation avec des optimisations de niveau -O, par exemple :

emcc -g temp.c -o temp.html \
     -O3 -fno-inline

Séparer les informations de débogage

Les informations de débogage conservent de nombreux détails sur votre code, les types définis, les variables, les fonctions, les portées et les emplacements, tout ce qui peut être utile au débogueur. Par conséquent, il peut souvent être plus volumineux que le code lui-même.

Pour accélérer le chargement et la compilation du module WebAssembly, vous pouvez diviser ces informations de débogage en fichier WebAssembly distinct. Pour ce faire dans Emscripten, transmettez un indicateur -gseparate-dwarf=… avec le nom de fichier souhaité:

emcc -g temp.c -o temp.html \
     -gseparate-dwarf=temp.debug.wasm

Dans ce cas, l'application principale ne stocke qu'un nom de fichier temp.debug.wasm, et l'extension d'assistance peut le localiser et le charger lorsque vous ouvrez DevTools.

Combinée à des optimisations comme décrit ci-dessus, cette fonctionnalité peut même être utilisée pour publier des builds de production presque optimisés de votre application, puis les déboguer avec un fichier secondaire local. Dans ce cas, nous devons également remplacer l'URL stockée pour aider l'extension à trouver le fichier secondaire, par exemple:

emcc -g temp.c -o temp.html \
     -O3 -fno-inline \
     -gseparate-dwarf=temp.debug.wasm \
     -s SEPARATE_DWARF_URL=file://[local path to temp.debug.wasm]

À suivre…

Ouf, c'était beaucoup de nouvelles fonctionnalités !

Avec toutes ces nouvelles intégrations, les outils pour les développeurs Chrome deviennent un débogueur viable et puissant, non seulement pour JavaScript, mais aussi pour les applications C et C++. Il est ainsi plus facile que jamais de prendre des applications, conçues dans diverses technologies, et de les transférer vers un Web partagé et multiplate-forme.

Toutefois, notre parcours n'est pas encore terminé. Voici quelques-unes des choses sur lesquelles nous allons travailler à l'avenir:

  • Nettoyage des aspérités de l'expérience de débogage.
  • Ajout de la prise en charge des formateurs de type personnalisés.
  • Amélioration du profilage pour les applications WebAssembly.
  • Ajout de la prise en charge de la couverture du code pour faciliter la recherche de code inutilisé.
  • Amélioration de la compatibilité avec les expressions dans l'évaluation de la console.
  • Ajout de la prise en charge de plus de langues.
  • Et bien d'autres…

En attendant, veuillez nous aider en essayant la version bêta actuelle sur votre propre code et en signalant tout problème détecté sur https://issues.chromium.org/issues/new?noWizard=true&template=0&component=1456350.

Télécharger les canaux de prévisualisation

Envisagez d'utiliser Chrome Canary, Dev ou Bêta comme navigateur de développement par défaut. Ces canaux de prévisualisation vous donnent accès aux dernières fonctionnalités de DevTools, vous permettent de tester les API de plate-forme Web de pointe et vous aident à détecter les problèmes sur votre site avant vos utilisateurs.

Contacter l'équipe des outils pour les développeurs Chrome

Utilisez les options suivantes pour discuter des nouvelles fonctionnalités, des mises à jour ou de tout autre élément lié aux outils pour les développeurs.