Fallstudie: Besseres Angular-Debugging mit Entwicklertools

Verbesserte Fehlerbehebung

In den letzten Monaten hat das Chrome-Entwicklertools-Team mit dem Angular-Team zusammengearbeitet, um die Fehlerbehebung in den Chrome-Entwicklertools zu verbessern. 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 für die Ignorierliste

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.

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 automatisch Code ausblenden, der in den Stacktraces, der Quellenstruktur und dem Schnellzugriff-Dialogfeld erkannt wurde. Außerdem können die Schritte und die Wiederaufnahme im Debugger verbessert werden.

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 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 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-Positionen in Fehler-Stacktraces, während sie an einem Haltepunkt pausiert wurden und dabei Schritte ausgeführt werden.

Es gibt eine weitere Information, die jetzt in Quellkarten aufgenommen werden kann, um zu identifizieren, ob es sich bei den Quellen um Code von Google oder Drittanbietern handelt:

{
  ...
  "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-Entwickler sind, achten Sie darauf, dass die während des Build-Prozesses generierten Quellzuordnungen dieses Feld enthalten, damit die neuen Funktionen in den Chrome-Entwicklertools verwendet 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 bei angular-cli erreicht, bei der ein Plug-in für das Compiler-Modul von Webpack erstellt wurde.

Das von unseren Entwicklern erstellte Webpack-Plug-in wird in die PROCESS_ASSETS_STAGE_DEV_TOOLING-Phase aufgenommen und füllt das Feld x_google_ignoreList in den Quellzuordnungen für die endgültigen Assets aus, die von Webpack generiert und im 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, um später ausgeführt zu werden: 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 verfügt intern über einen Mechanismus, mit dem solche asynchronen Aufgaben nachverfolgt werden können, wenn Standardprivilegien für die Browserplanung 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 im 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 Async Stack Tagging werden Stacktraces für Code, der von Frameworks asynchron auf komplexe Weise ausgeführt wird, ohne Verbindung zum Code angezeigt, für den er geplant wurde.

Ein Stack-Trace eines asynchron ausgeführten Codes ohne Informationen dazu, wann er geplant wurde. Es wird nur der Stack-Trace ab „requestAnimationFrame“ angezeigt, 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 lautet wie folgt:

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 der Minimierung 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 Quellzuordnung einen Namenseintrag für den Anfang eines Funktionsbereichs hat (d. h. den linken Klammer der Parameterliste), sollte im Aufrufframe dieser Name im Stacktrace angezeigt werden.

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 Funktionsnamen in das Feld „names“ von Quellkarten aufgenommen und in den Zuordnungen zwischen dem generierten Code und dem ursprünglichen Code darauf verwiesen werden 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

Es war eine wunderbare Erfahrung, Angular als Testpilot zu verwenden, um unsere Arbeit zu überprüfen. 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.