Fallstudie: Besseres Angular-Debugging mit Entwicklertools

Verbesserte Fehlerbehebung

In den letzten Monaten hat das Chrome-Entwicklertools-Team mit dem Angular-Team zusammengearbeitet, um Verbesserungen an der Fehlerbehebung in den Chrome-Entwicklertools vorzunehmen. Mitarbeiter aus beiden Teams haben zusammengearbeitet und Maßnahmen ergriffen, um Entwicklern das Debuggen und Profilieren von Webanwendungen aus der Erstellungsperspektive zu ermöglichen: in Bezug auf ihre Ausgangssprache und Projektstruktur und den Zugriff auf Informationen, die ihnen vertraut und relevant sind.

In diesem Beitrag erfahren Sie, welche Änderungen in den Entwicklertools von Angular und Chrome dazu erforderlich waren. Obwohl einige dieser Änderungen durch Angular demonstriert werden, können sie auch auf andere Frameworks angewendet werden. Das Chrome-Entwicklertools-Team empfiehlt andere Frameworks, die neuen Konsolen-APIs und Source Map-Erweiterungspunkte zu übernehmen, damit auch sie ihren Nutzern eine bessere Debugging-Erfahrung bieten können.

Code zum Ignorieren

Beim Debuggen von Anwendungen mit den Chrome-Entwicklertools möchten Autoren in der Regel nur nur ihren Code sehen, nicht den des zugrunde liegenden Frameworks oder eine Abhängigkeit im Ordner node_modules.

Um dies zu erreichen, hat das Entwicklertools-Team eine Erweiterung mit dem Namen x_google_ignoreList für Quellkarten eingeführt. Diese Erweiterung wird verwendet, um Quellen von Drittanbietern wie Framework-Code oder von Bundler generierten Code zu identifizieren. Wenn ein Framework diese Erweiterung verwendet, vermeiden Autoren jetzt automatisch Code, den sie nicht sehen oder nicht durchlaufen möchten, ohne dies vorher manuell konfigurieren zu müssen.

In der Praxis können die Chrome-Entwicklertools automatisch Code ausblenden, der als solcher in Stacktraces, im Quellenbaum und im Dialogfeld zum schnellen Öffnen identifiziert wurde. Außerdem lassen sich damit die Schritte und die Fortsetzung des Debuggers verbessern.

Ein animiertes GIF, auf dem die Entwicklertools vorher und nachher zu sehen sind. Beachten Sie, dass die Entwicklertools im Nachher-Image den erstellten Code in der Baumstruktur anzeigen, im Menü „Schnell öffnen“ keine Framework-Dateien mehr vorschlagen und rechts einen viel übersichtlicheren Stacktrace anzeigen.

Die Source Map-Erweiterung x_google_ignoreList

In Quellzuordnungen verweist das neue Feld x_google_ignoreList auf das Array sources und listet die Indizes aller bekannten Drittanbieterquellen in dieser Source Map auf. Beim Parsen der Source Map wird dies von den Chrome-Entwicklertools verwendet, um herauszufinden, welche Codeabschnitte auf der Ignorieren-Liste stehen sollten.

Unten sehen Sie eine Source Map für die generierte Datei out.js. Es gibt zwei ursprüngliche sources, die zur Generierung der Ausgabedatei beigetragen haben: foo.js und lib.js. Ersteres wurde von einem Website-Entwickler geschrieben und Letzteres ist ein Framework, das er verwendet hat.

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

Das sourcesContent ist für diese beiden Originalquellen enthalten und die Chrome-Entwicklertools würden diese Dateien standardmäßig im Debugger anzeigen:

  • Als Dateien in der Quellstruktur.
  • Als Ergebnisse im Dialogfeld „Schnell öffnen“ angezeigt.
  • Als zugeordnete Aufruf-Frame-Positionen in Fehler-Stacktraces, während sie an einem Haltepunkt pausiert sind und beim Stepping sind.

Es gibt eine zusätzliche Information, die jetzt in Source Maps aufgenommen werden kann, um zu ermitteln, welche dieser Quellen Erst- oder Drittanbietercode ist:

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

Das neue Feld x_google_ignoreList enthält einen einzelnen Index, der sich auf das Array sources bezieht: 1. Hiermit wird angegeben, dass die lib.js zugeordneten Regionen Drittanbietercode sind, der automatisch der Ignorierliste hinzugefügt werden sollte.

In einem komplexeren Beispiel (siehe unten) geben die Indizes 2, 4 und 5 an, dass die den lib1.ts, lib2.coffee und hmr.js zugeordneten Regionen Drittanbietercode sind, der automatisch der Ignorierliste hinzugefügt werden sollte.

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

Wenn Sie ein Framework- oder Bundler-Entwickler sind, achten Sie darauf, dass die während des Build-Prozesses generierten Quellzuordnungen dieses Feld enthalten, um diese neuen Funktionen in den Chrome-Entwicklertools nutzen zu können.

x_google_ignoreList in Angular

Ab Version 14.1.0 von Angular wurde der Inhalt der Ordner node_modules und webpack mit „zu ignorieren“ gekennzeichnet.

Dies wurde durch eine Änderung in angular-cli durch das Erstellen eines Plug-ins, das in das Compiler-Modul von Webpack eingebunden erreicht.

Das von unseren Entwicklern erstellte Webpack-Plug-in wird in die PROCESS_ASSETS_STAGE_DEV_TOOLING-Phase eingebunden und füllt das x_google_ignoreList-Feld in den Source Maps für die endgültigen Assets, die Webpack generiert und im Browser geladen wird.

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

Verknüpfte Stacktraces

Stacktraces beantworten die Frage, wie ich hierher gekommen bin, aber oft geschieht dies aus der Perspektive des Computers und nicht unbedingt etwas, das der Perspektive des Entwicklers oder seinem mentalen Modell der Anwendungslaufzeit entspricht. Dies gilt insbesondere, wenn einige Vorgänge so geplant sind, dass sie später asynchron ausgeführt werden: Es könnte dennoch interessant sein, die „Grundursache“ oder die Planungsseite solcher Vorgänge zu kennen, aber genau das ist nicht Teil eines asynchronen Stacktrace.

V8 verfügt intern über einen Mechanismus, mit dem solche asynchronen Aufgaben erfasst werden können, wenn die Standardfunktionen der Browserplanung verwendet werden, z. B. setTimeout. In diesen Fällen ist dies standardmäßig der Fall, sodass die Entwickler sie bereits prüfen können. Bei komplexeren Projekten ist es jedoch nicht so einfach, insbesondere wenn Sie ein Framework mit erweiterten Planungsmechanismen verwenden, z. B. eines, das Zonenverfolgung, benutzerdefinierte Aufgabenwarteschlangen oder Aktualisierungen in mehrere Arbeitseinheiten unterteilt, die im Laufe der Zeit ausgeführt werden.

Um dieses Problem zu beheben, stellen die Entwicklertools einen Mechanismus namens „Async Stack Tagging API“ im console-Objekt bereit, mit dem Framework-Entwickler sowohl die Orte angeben können, an denen Vorgänge geplant sind, als auch, wo diese Vorgänge ausgeführt werden.

Die Async Stack Tagging API

Ohne asynchrones Stack-Tagging werden Stacktraces für Code, der von Frameworks auf komplexe Weise asynchron ausgeführt wird, ohne Verbindung zu dem Code angezeigt, für den er geplant wurde.

Ein Stacktrace von asynchronem ausgeführtem Code ohne Informationen zum Zeitpunkt der Planung. Sie zeigt nur den Stacktrace ab „requestAnimationFrame“ an, enthält aber keine Informationen zum Zeitpunkt der Planung.

Mit dem asynchronen Stack-Tagging ist es möglich, diesen Kontext bereitzustellen. Der Stacktrace sieht dann so aus:

Ein Stacktrace von asynchronem ausgeführtem Code mit Informationen darüber, wann er geplant wurde. Beachten Sie, dass im Stacktrace im Gegensatz dazu „businessLogic“ und „schedule“ enthalten sind.

Verwenden Sie dazu die neue console-Methode namens console.createTask(), die von der Async Stack Tagging API bereitgestellt wird. Ihre Signatur lautet:

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

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

Wenn Sie console.createTask() aufrufen, wird eine Task-Instanz zurückgegeben, die Sie später zum Ausführen des asynchronen Codes verwenden können.

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

// Task Execution
task.run(f);

Die asynchronen Vorgänge können auch verschachtelt sein und die „Ursachen“ werden der Reihe nach im Stacktrace angezeigt.

Aufgaben können beliebig oft ausgeführt werden und die Arbeitsnutzlast kann sich bei jeder Ausführung unterscheiden. Der Aufrufstack am Planungsstandort wird gespeichert, bis das Aufgabenobjekt durch die automatische Speicherbereinigung bereinigt wird.

Die Async Stack Tagging API in Angular

In Angular wurden Änderungen an NgZone vorgenommen, dem Ausführungskontext von Angular, der auch über asynchrone Aufgaben hinweg bestehen bleibt.

Beim Planen einer Aufgabe wird console.createTask() verwendet, sofern verfügbar. Die resultierende Task-Instanz wird zur weiteren Verwendung gespeichert. Beim Aufrufen der Aufgabe verwendet NgZone die gespeicherte Task-Instanz, um sie auszuführen.

Diese Änderungen landeten über Pull-Anfragen #46693 und #46958 in der NgZone-Version 0.11.8 von Angular.

Angepasste Anruf-Frames

Frameworks generieren beim Erstellen eines Projekts oft Code aus allen Arten von Vorlagensprachen. Dazu gehören beispielsweise Angular- oder JSX-Vorlagen, die HTML-ähnlichen Code in einfachen JavaScript-Code umwandeln, der schließlich im Browser ausgeführt wird. Manchmal erhalten solche generierten Funktionen Namen, die nicht besonders freundlich sind – entweder Namen aus einzelnen Buchstaben, nachdem sie reduziert wurden, oder unbekannte oder unbekannte Namen, auch wenn dies nicht der Fall ist.

In Angular sind Aufrufframes mit Namen wie AppComponent_Template_app_button_handleClick_1_listener in Stacktraces nicht ungewöhnlich.

Screenshot des Stacktrace mit einem automatisch generierten Funktionsnamen

Aus diesem Grund unterstützen die Chrome-Entwicklertools jetzt das Umbenennen dieser Funktionen über Source Maps. Wenn eine Quellzuordnung einen Namenseintrag für den Anfang eines Funktionsbereichs enthält (d. h. die linke Klammer der Parameterliste), sollte der Aufruf-Frame diesen Namen im Stacktrace anzeigen.

Optimierte Anruf-Frames in Angular

Das Umbenennen von Anruf-Frames in Angular ist ein fortlaufender Prozess. Wir gehen davon aus, dass diese Verbesserungen im Laufe der Zeit nach und nach sichtbar werden.

Beim Parsen der von den Autoren verfassten HTML-Vorlagen generiert der Angular-Compiler TypeScript-Code, der schließlich in JavaScript-Code umgewandelt wird, der vom Browser geladen und ausgeführt wird.

Im Rahmen dieses Codegenerierungsprozesses werden auch Quellzuordnungen erstellt. Wir suchen derzeit nach Möglichkeiten, Funktionsnamen in das Feld "names" von Quellzuordnungen aufzunehmen und auf diese Namen in den Zuordnungen zwischen dem generierten Code und dem ursprünglichen Code zu verweisen.

Wenn beispielsweise eine Funktion für einen Event-Listener generiert wird und ihr Name entweder nicht angezeigt oder bei der Reduzierung entfernt wird, können Quellzuordnungen jetzt den leichteren Namen für diese Funktion im Feld „names“ enthalten. Die Zuordnung für den Anfang des Funktionsbereichs kann sich dann auf diesen Namen beziehen (d. h. die linke Klammer der Parameterliste). Die Chrome-Entwicklertools verwenden diese Namen dann, um Aufrufframes in Stacktraces umzubenennen.

Zukunftspläne

Der Einsatz von Angular als Testpilot, um unsere Arbeit zu verifizieren, war eine tolle Erfahrung. Wir würden uns freuen, von Framework-Entwicklern zu hören und uns Feedback zu diesen Erweiterungspunkten zu geben.

Es gibt noch mehr Bereiche, die wir gerne erkunden würden. Insbesondere geht es darum, wie die Profilerstellung in den Entwicklertools verbessert werden kann.