Wie wir WebXR/WebGL-Apps profilieren
Dieser Artikel behandelt Methoden zur Profilerstellung von WebXR- und WebGL-Apps (unabhängig vom zugrunde liegenden Framework). Das Profiling ist der erste Schritt zur Optimierung Deiner Anwendung. Optimieren ohne Profiling ist verschwendete Mühe, wie in Engpässe beschrieben.
Beachte, dass der Beitrag nicht darauf eingeht, wie gefundene Leistungsprobleme behoben werden können. Es gibt viele Blog-Beiträge und Vorträge, die auf spezifische Engpässe abzielen, aber sobald Du weißt, was das Problem ist, findest Du Ressourcen dafür.
Die Metriken, für die wir ein Profil erstellen, sind CPU-Zeit, GPU-Zeit, JavaScript-Heap-Speicher und die Ladezeit der Anwendung. Ziel ist es, die umfassendste Sammlung von Informationen zur Profilerstellung kostenlos bereitzustellen. Und nein: “Poly Count” ist keine Metrik, die uns interessiert!
Inhalte
Engpässe
Ohne den Engpass Deiner App zu finden, werden Deine Optimierungsbemühungen nicht effektiv sein.
Beispiel
Stell Dir vor, Du rendert eine große Szene mit vielen Objekten. Deine App erreicht kaum 60 fps. Du kannst die Anzahl der Vertices jedes Objekts optimieren und wirst trotzdem bei 60 fps bleiben.
Die CPU ist dafür verantwortlich, der GPU den Befehl zum Rendern jedes Objekts zu senden (“Draw Call”). In diesem Fall ist es viel wahrscheinlicher, dass die CPU mit der Arbeit überlastet ist, die Draw Calls zu senden, während die GPU nur darauf wartet, dass die CPU mehr Arbeit sendet!
Aber wir wissen es nicht… bis wir messen. Dafür ist das Profiling da.
Engpasstypen
Jeder Teil einer Anwendung kann einen Engpass verursachen. Hier sind einige Beispiele für häufige Engpässe in der Häufigkeitsreihenfolge:
CPU Draw Calls: Zu viele Aufrufe an den Treiber verursachen zu viel Overhead.
Garbage: Die Anwendung läuft gut, produziert jedoch Abfall, der in regelmäßigen oder unregelmäßigen Abständen zu Stottern führt.
CPU-Logik: Deine Anwendung kann die Arbeit für die GPU nicht schnell genug bereitstellen. Oft verursacht durch schwere Physiksimulationen oder Strahlenverfolgungen.
GPU-Fragment-Shading: Die Kosten für das Shading pro Fragment sind zu hoch.
GPU-Vertex-Verarbeitung: Die Anforderungen an die Verarbeitung pro Vertex sind zu hoch.
GPU Resolve: Verursacht durch Nachbearbeitung auf mobilen GPUs.
GPU-Vertex Fetch: Der Speicher für die Vertices wird nicht schnell genug gelesen.
Jeder dieser Engpässe könnte weitere Unterengpässe haben.
Metriken
Die Metriken, für die wir ein Profil erstellen, sind:
CPU-Zeit
Die Arbeit für jede XR-Anwendung ist zwischen CPU und GPU aufgeteilt. Die CPU ist verantwortlich für die Vorbereitung der Grafikausgabe für die GPU und führt die App-Logik aus.
Jeder JavaScript-Code läuft auf der CPU, um Draw Calls vorzubereiten, Ressourcen zu laden, Physik zu simulieren, Audio zu rendern und Transformationen des Szenenbaums zu berechnen.
GPU-Zeit
Die GPU ist für die grafische Schwerstarbeit verantwortlich. Während die CPU einen Draw Call im Sinne von “Mesh X mit Shader Y mit Textur Z und Materialparametern W zeichnen” sendet, führt die GPU die eigentliche Rasterisierung und Transformationen pro Vertex durch, um die Pixel auf dem Bildschirm zu erzeugen.
Die GPU ist auch dafür verantwortlich, das endgültige Bild an den Bildschirm zu senden, und wartet auf “V-Sync”, was die Aktualisierungsrate des Bildschirms mit der Anwendung synchronisiert.
Bildrate und V-Sync
Wir verwenden nicht die Bildrate (Bilder pro Sekunde = fps), um unsere Anwendung zu profilieren.
Diese Metrik wird nur aufgeführt, um die Komplexität zu erklären, wie die Leistung mit V-Sync interagiert. Sie ist zu grob, um nützliche Urteile abzugeben.
Betrachte V-Sync als feste Deadlines: 60, 72, 90 oder mehr Deadlines pro Sekunde (die Bildrate). Wir werden “das V-Sync schaffen” als das rechtzeitige Rendern und Einreichen des Frames, um die Deadline zu treffen, bezeichnen.
Wenn Du die Deadline verpasst, werden alle Deine Arbeiten verworfen und Du musst versuchen, die nächste Deadline statt der bisherigen zu erreichen. In Umgebungen mit flexibler Bildrate kann dies bedeuten, dass Deine Treiber Dich auf die Hälfte der Bildrate herabsetzen.
Wenn Deine Anwendung nur knapp zu langsam ist, würdest Du z.B. 30 fps statt 59 fps sehen, was eine völlig andere Geschichte erzählt. Vielleicht funktioniert Deine Anwendung mit 3ms CPU- und 3ms GPU-Zeit anständig, aber weil die Arbeit zu spät beginnt, verpasst Du das V-Sync für Deine Zielbildrate– was Deine effektive Bildrate halbiert. Keine Vertexanzahl, kein Draw Call oder eine klassische Optimierung wird Dir hier helfen.
Latenz
Die Latenz, also die Zeit zwischen Eingabe (z.B. Kopfbewegung) und dem fertigen Frame, ist besonders wichtig für VR. WebXR-Implementierungen kümmern sich normalerweise darum für uns und könnten die Frame-Rückrufe etwas später planen, wenn wir unser Frame-Budget nicht verwenden, um die Latenz zu reduzieren. Wir haben daher nur begrenzt Kontrolle darüber von WebXR aus und werden dies in diesem Blog-Beitrag nicht behandeln.
Garbage Collection
JavaScript verfügt über ein Speichermanagement, das es Dir ermöglicht, Zuordnungen unbedacht und sorglos zu behandeln. Daher ist es üblich, dies zu tun.
Ohne die Notwendigkeit, Speicher zu verwalten, verlierst Du die Kontrolle darüber, festzulegen, wann Du möchtest, dass Dein Speicher verwaltet wird. Dieser Prozess wird “Garbage Collection” genannt und wird zu zufälligen Zeiten im Lebenszyklus Deiner Anwendung stattfinden–z.B. wenn Du kurz davor wärst, das V-Sync zu erreichen und gerade im Begriff warst, Deinen Frame einzureichen.
Die Garbage Collection könnte zwischen 0,1 - 10 ms dauern. Angesichts Deines üblichen VR-Frame-Budgets von 11 ms willst Du auf keinen Fall, dass dies zu zufälligen Zeiten passiert.
Wie können wir es vermeiden? Der einzige Weg besteht darin, jeglichen Müll zu vermeiden, der bereinigt werden müsste. Je weniger Du produzieren, desto kleiner und seltener werden die harten Unterbrechungen durch die Garbage Collection sein.
Ladezeit der Anwendung
Wie bei der Website-Ladezeit nimmt die Bereitschaft des Nutzers, Deine App zu nutzen, mit jeder verbrachten Sekunde des Wartens ab. Da WebXR-Anwendungen ziemlich groß sind, gibt es hier vielleicht etwas mehr Gutwillen im Vergleich zu einer Website, aber das muss nicht sein.
Indem wir die Anwendung frühzeitig starten und Ressourcen laden, die nicht sofort benötigt werden, können wir die wahrgenommene Ladezeit verkürzen.
Indem wir Assets optimieren und sicherstellen, dass unsere Servereinstellungen optimal sind, können wir die Ladezeit insgesamt verkürzen.
Und durch die Verwendung von Formaten, die weniger Parsing benötigen, können wir den Aufwand verringern, den die CPU nachdem Laden der Ressourcen leisten muss.
Tools
In diesem Beitrag behandeln wir die folgenden Tools:
- Chrome Profiler, [CPU, GPU, Garbage]
- Chrome Networking-Tab, [Ladezeit]
- OVR Profiler Tool, [CPU, GPU]
- Spector.js Browser-Erweiterung, [CPU, GPU]
- WebGL Disjoint Timer Query, [GPU]
- Wonderland Editor Profiler (nur Wonderland Engine). [CPU, GPU]
Chrome Profiler
Der in Chrome integrierte Profiler ermöglicht es Dir, die CPU-Zeit von JavaScript, Garbage Collection und sehr grob die GPU-Frame-Zeit zu profilieren.
Du findest die “Performance”-Registerkarte, indem Du zu einer beliebigen Website navigierst
und Strg + Umschalt + C
(Befehl + Umschalt + C
auf MacOS) drückst. Finde dort “Performance”.
Um eine Profilsitzung aufzunehmen, drücke die Aufnahmetaste oben links (Strg/Befehl + E
).
3-5 Sekunden sind in der Regel völlig ausreichend, da wir normalerweise an einzelnen Frames interessiert sind.
Dies funktioniert auch beim Entfernten Debuggen auf Android-Geräten wie Meta Quest oder Deinem Smartphone.
Safari hat für Mac, iOS-Geräte und das Apple Vision PRO (z.B. mit Safari, das im Apple Vision PRO-Simulator läuft) einen ähnlichen Profiler.
Chrome Memory Profiler
In Garbage Collection beschreiben wir Stottern in sonst reibungslos laufenden Anwendungen.
Um diesen Engpass zu finden, bieten Browser eine Möglichkeit, den JavaScript-Heap zu stichproben. So aktivierst Du es in Chrome vom Chrome Profiler aus:

Beispiel
Das Folgende ist ein Profil auf Meta Quest 2 von Elysian, das auf Three.js basiert:
Du siehst, dass der “JS Heap” zwischen 14,1 MB - 23,3 MB schwankt.
Wo auch immer der Speicher plötzlich fällt, finden wir Garbage Collection. In diesem Fall ist die GC so groß, dass sie den Start des nächsten Frames verzögert und einen Frame-Drop verursacht:

Als Ergebnis müssen wir hoffen, dass der verworfene Frame aus dem letzten Frame neu projiziert wird, ansonsten tritt ein erhebliches Stottern auf. Da Animationen nicht neu projiziert werden können, wird es so oder so ein Stottern geben.
Netzwerk-Registerkarte
Die Netzwerk-Registerkarte eines jeden Browsers ist ein großartiges Tool zum Profilieren der Ladezeit Deiner Anwendung.
Du findest die Netzwerk-Registerkarte, indem Du zu einer beliebigen Website navigierst
und Strg + Umschalt + C
(Befehl + Umschalt + C
auf MacOS) drückst. Finde dort
“Netzwerk”.
Um die Netzwerkaktivität aufzuzeichnen, lade die Seite neu, während die Registerkarte geöffnet ist.
Blockierende Downloads
Wenn eine Ressource nicht sofort bei Anwendungsstart benötigt wird, sollte sie später geladen werden, da Browser nur begrenzte parallele Anfragen gleichzeitig ausführen (z.B. 6 bei Chrome).
Meta Quest Performance HUD
Beim Profilieren von VR-Renderings auf der Meta Quest kannst Du das Performance HUD verwenden.
Es wird am einfachsten über den Meta Quest Developer Hub installiert.
Dies ist besonders nützlich zum Debuggen von leistungserkennenden Problemen, da Du das Headset verwenden kannst, während Du kontinuierliches Feedback zur Leistung erhältst.
Beispiel
Im folgenden Beispiel zeigt Ayushman Johri die ausgezeichnete Leistung seines auf Wonderland Engine basierenden “Vintage Study Room” mit dem Meta Quest Performance HUD:
Hier ist ein Leistungstest mit dem @MetaQuestVR FPS-Diagramm!
— Ayushman Johri ✨ (@AyushmanJohri) August 19, 2023
Ich möchte erwähnen, dass ich mehr veraltete Frames beobachtete, als ich ohne Systemaufzeichnung erhalten würde~
Außerdem habe ich mich aus künstlerischen Gründen dafür entschieden, unkomprimierte Texturen für einige extra detaillierte Assets zu verwenden ✨
Nicht perfekt, aber ziemlich nah! pic.twitter.com/MLzWVLHItn
OVR GPU Profiler
Wenn Du auf Engpässe beim Vertex- oder Fragment-Shading stößt, wirst Du ein klareres Verständnis dafür zu schätzen wissen, was in Deinen Shadern die meiste Zeit in Anspruch nimmt.
Der ovrgpuprofiler ist ein sehr präzises Werkzeug. Wenn Du ein gewisses Verständnis von GPU-Speicher und -Architektur hast, bietet es Dir viele Einblicke.
Es wird installiert, indem es auf Dein Meta Quest-Headset über adb
installiert und über adb shell
ausgeführt wird:
1 47 unterstützte Metriken:
2 1 Clocks / Second
3 2 GPU % Bus Busy
4 3 % Vertex Fetch Stall
5 4 % Texture Fetch Stall
6 5 L1 Texture Cache Miss Per Pixel
7 6 % Texture L1 Miss
8 7 % Texture L2 Miss
9 8 % Stalled on System Memory
10 9 Pre-clipped Polygons/Second
11 10 % Prims Trivially Rejected
12 11 % Prims Clipped
(Quelle: developer.oculus.com)
Ein Beispielausgabe sieht wie folgt aus:
1$ adb shell ovrgpuprofiler -r"4,5,6"
2
3% Texture Fetch Stall : 2.449
4L1 Texture Cache Miss Per Pixel : 0.124
5% Texture L1 Miss : 20.338
6
7% Texture Fetch Stall : 2.369
8L1 Texture Cache Miss Per Pixel : 0.122
9% Texture L1 Miss : 20.130
10
11% Texture Fetch Stall : 2.580
12L1 Texture Cache Miss Per Pixel : 0.127
13% Texture L1 Miss
14
15...
(Modifiziert von: developer.oculus.com)
Du wirst oft lesen, dass das Lesen und Schreiben von Speicher normalerweise die teuersten Operationen in einem Shader-Programm sind. Ob dies zutrifft, hängt jedoch davon ab, wie cache-freundlich Deine Speicheroperationen sind.
L1 (“Level 1”) Cache-Speicher ist unglaublich schnell. Um sicherzustellen, dass Du ihn verwendest, solltest Du das Prinzip der Örtlichkeit einhalten: Greife auf Speicher zu, der nahe am zuvor abgerufenen Speicher liegt.
Spector.js
Spector.js ist eine Browser-Erweiterung für Chrome und Firefox, die das Erfassen von WebGL- Frame-Traces ermöglicht. Das Tool kann die vollständige Liste der Befehle anzeigen und Statistiken wie Vertex-Anzahlen und Draw Calls zusammenfassen.
Installiere es im Chrome Extension Store oder der Firefox Addon-Bibliothek. Alternativ kannst Du das Tool über ein HTML-Tag für Browser ohne Plugin-Unterstützung einbetten.

Aktiviere das Tool zuerst über den Add-ons Button oben, und nimm dann einen Frame auf, indem Du den roten Aufnahme-Button klickst.

Du kannst sehen, dass Wonderland Engine hier viele Objekte in insgesamt 11 Draw Calls zeichnet. Bei jedem anderen Framework wird diese Zahl um ein Vielfaches höher sein.
Disjoint Timer Query
Die EXT_disjoint_timer_query-Erweiterung ist eine WebGL-Erweiterung, die es ermöglicht, die GPU-Zeit für eine Gruppe von WebGL-Befehlen zu messen. Sie ist nur auf Chrome [August 2023] gut unterstützt und wenn das “WebGL Debug Extensions”-Chrome-Flag aktiviert ist.
Da alle Befehle asynchron auf der GPU ausgeführt werden, müssen die Zeitmessungen ebenfalls asynchron geplant werden.
Wonderland Editor Profiler
Der Wonderland Editor verfügt über ein integriertes Profilierungstool, das hilft, zu verstehen, wo die Leistung Deiner WebXR-App möglicherweise leidet.
Abschließende Worte
Jedes Framework hat seine speziellen Leistungsmerkmale. Die meisten sind zuerst durch Draw Calls begrenzt und dann durch Fragmente–kein Grund, low-poly zu gehen, wenn nicht bei 90 fps!
Wir haben die Wonderland Engine von Grund auf so konzipiert, dass die meisten dieser Engpässe vermieden werden– es ist bis zu 120k USD Jahresumsatz kostenlos. Kontakt hier hier für Enterprise Lizenzen und Support.
Probiere die Wonderland Engine aus und beginne Zeit zu sparen bei der Optimierung.
