Fallstudie: Besseres Angular-Debugging mit Entwicklertools

Verbesserte Fehlerbehebung

In den letzten Monaten hat das Chrome DevTools-Team in Zusammenarbeit mit dem Angular-Team Verbesserungen am Debugging in den Chrome DevTools eingeführt. Mitarbeiter aus beiden Teams haben zusammengearbeitet und Schritte unternommen, um Entwicklern die Möglichkeit zu geben, Webanwendungen aus der Autorenperspektive zu debuggen und zu profilieren: in Bezug auf ihre Quellsprache und Projektstruktur, mit Zugriff auf Informationen, die ihnen vertraut und relevant sind.

In diesem Beitrag sehen wir uns an, welche Änderungen an Angular und den Chrome DevTools erforderlich waren, um dies zu erreichen. Auch wenn einige dieser Änderungen anhand von Angular veranschaulicht werden, können sie auch auf andere Frameworks angewendet werden. Das Chrome DevTools-Team empfiehlt anderen Frameworks, die neuen Console APIs und Erweiterungspunkte für Quellkarten zu verwenden, damit auch sie ihren Nutzern eine bessere Fehlerbehebung bieten können.

Code zum Ignorieren von Einträgen

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

Dazu hat das DevTools-Team eine Erweiterung für Quellzuordnungen namens x_google_ignoreList eingeführt. Mit dieser Erweiterung werden Drittanbieterquellen wie Framework-Code oder vom Bundler generierter Code identifiziert. Wenn ein Framework diese Erweiterung verwendet, wird Code, den die Autoren nicht sehen oder durchgehen möchten, jetzt automatisch ausgeblendet, ohne dass dies vorher manuell konfiguriert werden muss.

In der Praxis können die Chrome-Entwicklertools Code, der so gekennzeichnet ist, automatisch in Stack-Traces, im Quellenbaum und im Dialogfeld „Schnell öffnen“ ausblenden und auch das Verhalten beim Schritten und Fortsetzen im Debugger verbessern.

Ein animiertes GIF, das die DevTools vor und nach der Änderung zeigt. Beachten Sie, dass in der Abbildung nach der Änderung der Autorisierte Code im Baum angezeigt wird, keine Framework-Dateien mehr im Menü „Schnell öffnen“ vorgeschlagen werden und rechts ein viel übersichtlicherer Stack-Trace zu sehen ist.

Die x_google_ignoreList-Quellzuordnung

In Quellkarten bezieht sich das neue Feld x_google_ignoreList auf das Array sources und enthält die Indizes aller bekannten Drittanbieterquellen in dieser Quellkarte. Beim Parsen der Quellkarte ermitteln die Chrome-Entwicklertools anhand dieser Informationen, welche Codeabschnitte in die Ignorierliste aufgenommen werden sollen.

Unten sehen Sie eine Quellzuordnung für eine 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 Websiteentwickler geschrieben, 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;"
}

Die sourcesContent ist für beide ursprünglichen Quellen enthalten und in den Chrome-Entwicklertools werden diese Dateien standardmäßig im Debugger angezeigt:

  • Als Dateien im Stammbaum „Quellen“
  • Als Ergebnisse im Dialogfeld „Schnell öffnen“
  • Als zugeordnete Aufrufframe-Standorte in Fehlerstack-Traces, wenn die Ausführung an einem Haltepunkt pausiert oder Schritt für Schritt ausgeführt wird.

Es gibt eine weitere Information, die jetzt in Quellkarten aufgenommen werden kann, um zu identifizieren, welche dieser Quellen Code von Drittanbietern oder selbst erstellter Code 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. Damit wird angegeben, dass die lib.js zugeordneten Regionen tatsächlich Drittanbietercode sind, der automatisch der Ignorieren-Liste hinzugefügt werden sollte.

In einem komplexeren Beispiel unten geben die Indizes 2, 4 und 5 an, dass Regionen, die lib1.ts, lib2.coffee und hmr.js zugeordnet sind, Code von Drittanbietern sind, der automatisch der Ignorierliste hinzugefügt werden soll.

{
  ...
  "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 entwickeln, müssen Sie dafür sorgen, dass die während des Build-Prozesses generierten Quellzuordnungen dieses Feld enthalten, damit diese neuen Funktionen in den Chrome DevTools genutzt werden können.

x_google_ignoreList in Angular

Ab Angular v14.1.0 sind die Inhalte der Ordner node_modules und webpack als „zu ignorieren“ gekennzeichnet.

Dies wurde durch eine Änderung in angular-cli erreicht, indem ein Plug-in erstellt wurde, das an das Compiler-Modul von webpack angehängt wird

Das von unseren Entwicklern erstellte Webpack-Plug-in wird in der Phase PROCESS_ASSETS_STAGE_DEV_TOOLING eingebunden und füllt das Feld x_google_ignoreList in den Quellkarten für die endgültigen Assets aus, die von Webpack generiert und vom Browser geladen werden.

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

Stack-Traces beantworten die Frage „Wie bin ich hierher gekommen?“, aber oft aus der Perspektive des Computers und nicht unbedingt aus der Perspektive des Entwicklers oder seinem mentalen Modell der Anwendungslaufzeit. Das gilt insbesondere, wenn einige Vorgänge asynchron geplant sind. Es kann zwar interessant sein, die „Ursache“ oder die Planungsseite solcher Vorgänge zu kennen, aber genau das ist nicht Teil eines asynchronen Stack-Traces.

V8 hat intern einen Mechanismus, um solche asynchronen Aufgaben im Auge zu behalten, wenn standardmäßige Scheduling-Primitive des Browsers wie setTimeout verwendet werden. In diesen Fällen geschieht dies standardmäßig, sodass die Entwickler sie bereits prüfen können. Bei komplexeren Projekten ist das jedoch nicht so einfach, insbesondere wenn ein Framework mit erweiterten Planungsmechanismen verwendet wird, z. B. ein Framework, das Zonen-Tracking, benutzerdefinierte Aufgabenwarteschlangen oder die Aufteilung von Updates in mehrere Arbeitseinheiten ausführt, die im Laufe der Zeit ausgeführt werden.

Um dies zu beheben, stellt DevTools für das console-Objekt einen Mechanismus namens „Async Stack Tagging API“ bereit, mit dem Framework-Entwickler sowohl die Stellen angeben können, an denen Vorgänge geplant werden, als auch die Stellen, an denen diese Vorgänge ausgeführt werden.

Async Stack Tagging API

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

Ein Stack-Trace eines asynchron ausgeführten Codes ohne Informationen dazu, wann er geplant wurde. Er zeigt nur den Stack-Trace ab „requestAnimationFrame“ an, enthält aber keine Informationen dazu, wann er geplant wurde.

Mit dem Async-Stack-Tagging ist es möglich, diesen Kontext anzugeben. Der Stack-Trace sieht dann so aus:

Ein Stack-Trace eines asynchron ausgeführten Codes mit Informationen dazu, wann er geplant wurde. Beachten Sie, dass im Gegensatz zum vorherigen Stack-Trace jetzt „businessLogic“ und „schedule“ enthalten sind.

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

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

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

Wenn Sie console.createTask() aufrufen, wird eine Task-Instanz zurückgegeben, mit der Sie später den asynchronen Code ausführen können.

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

// Task Execution
task.run(f);

Die asynchronen Vorgänge können auch verschachtelt sein. Die „Ursachen“ werden im Stack-Trace nacheinander angezeigt.

Aufgaben können beliebig oft ausgeführt werden und die Arbeitsnutzlast kann bei jedem Durchlauf unterschiedlich sein. Der Aufrufstapel an der Planungsstelle wird gespeichert, bis das Aufgabenobjekt durch die Garbage Collection gelöscht wird.

Die Async Stack Tagging API in Angular

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

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

Diese Änderungen wurden über die Pull-Requests #46693 und #46958 in NgZone 0.11.8 von Angular übernommen.

Frames für freundliche Anrufe

Frameworks generieren beim Erstellen eines Projekts häufig Code aus allen Arten von Vorlagensprachen, z. B. Angular- oder JSX-Vorlagen, die HTML-ähnlichen Code in einfachen JavaScript-Code umwandeln, der schließlich im Browser ausgeführt wird. Manchmal werden diesen generierten Funktionen nicht sehr nutzerfreundliche Namen gegeben – entweder ein einzelner Buchstabe nach dem Minimieren oder unklare oder unbekannte Namen, auch wenn sie es nicht sind.

In Angular sind Aufrufframes mit Namen wie AppComponent_Template_app_button_handleClick_1_listener in Stack-Traces keine Seltenheit.

Screenshot eines Stack-Traces mit einem automatisch generierten Funktionsnamen

In den Chrome-Entwicklertools können Sie diese Funktionen jetzt über Quellkarten umbenennen. Wenn eine Quellkarte einen Namenseintrag für den Beginn eines Funktionsbereichs enthält (d. h. das linke eckige Klammernzeichen der Parameterliste), sollte der Aufrufframe diesen Namen im Stack-Trace anzeigen.

Freundliche Aufruf-Frames in Angular

Das Umbenennen von Aufrufframes in Angular ist ein fortlaufender Prozess. Wir gehen davon aus, dass diese Verbesserungen nach und nach eingeführt werden.

Beim Parsen der von den Autoren geschriebenen 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 Codegenerierungsvorgangs werden auch Quellkarten erstellt. Wir prüfen derzeit, wie wir Funktionsnamen in das Feld „names“ von Quellkarten aufnehmen und auf diese Namen in den Zuordnungen zwischen dem generierten Code und dem ursprünglichen Code verweisen können.

Wenn beispielsweise eine Funktion für einen Ereignis-Listener generiert wird und ihr Name entweder schwer verständlich ist oder während der Minimierung entfernt wird, können Quellkarten jetzt den verständlicheren Namen für diese Funktion im Feld „names“ enthalten. Die Zuordnung für den Anfang des Funktionsumfangs kann sich jetzt auf diesen Namen beziehen (d. h. das linke eckige Klammernzeichen der Parameterliste). In den Chrome-Entwicklertools werden diese Namen dann verwendet, um Aufrufframes in Stack-Traces umzubenennen.

Zukunftspläne

Die Verwendung von Angular als Testpilot zur Überprüfung unserer Arbeit war eine tolle Erfahrung. Wir freuen uns über Feedback von Framework-Entwicklern zu diesen Erweiterungspunkten. Bitte senden Sie uns Ihr Feedback.

Es gibt noch weitere Bereiche, die wir untersuchen möchten. Insbesondere geht es darum, wie das Profiling in den DevTools verbessert werden kann.