24 Punkte von xguru 2024-07-19 | 6 Kommentare | Auf WhatsApp teilen
  • Vor drei Jahren konnte Notion die Geschwindigkeit der Notion-App für Mac und Windows erfolgreich verbessern, indem Daten mit einer SQLite-Datenbank auf dem Client zwischengespeichert wurden
  • Diesmal konnte dieselbe Verbesserung auch Nutzern bereitgestellt werden, die über den Browser auf Notion zugreifen
  • Dieser Beitrag analysiert im Detail, wie die Performance von Notion im Browser mithilfe der sqlite3-Implementierung in WebAssembly (WASM) verbessert wurde
  • Durch den Einsatz von SQLite konnte die Seiten-Navigationszeit in allen modernen Browsern um 20 % verbessert werden
    • Besonders deutlich war der Unterschied bei Nutzern, deren API-Antwortzeiten aufgrund externer Faktoren wie der Internetverbindung besonders langsam waren
    • So war die Seiten-Navigationszeit bei Nutzern in Australien um 28 %, in China um 31 % und in Indien um 33 % schneller

Kerntechnologien: OPFS und Web Workers

  • Die WASM-SQLite-Bibliothek nutzt eine moderne Browser-API namens Origin Private File System (OPFS), um Daten sitzungsübergreifend zu speichern
  • OPFS kann nur in Web Workers verwendet werden
  • Ein Web Worker lässt sich als Code verstehen, der in einem separaten Thread ausgeführt wird, anders als der Main Thread, auf dem im Browser der Großteil von JavaScript läuft
  • Notion wird zusammen mit Webpack gebündelt, das eine leicht nutzbare Syntax zum Laden von Web Workers bereitstellt
  • Es wurde ein Web Worker eingerichtet, der mithilfe von OPFS eine SQLite-Datenbankdatei erstellt oder eine bestehende Datei lädt; in diesem Web Worker wurde der vorhandene Caching-Code ausgeführt
  • Mit der Bibliothek Comlink wurde die Nachrichtenübermittlung zwischen Main Thread und Worker einfacher verwaltet

SharedWorker-basierter Ansatz

  • Die endgültige Architektur basiert auf einer neuen Lösung, die Roy Hashimoto in einer GitHub-Diskussion vorgestellt hat
    • Ein Ansatz, bei dem immer nur ein Tab gleichzeitig auf SQLite zugreift, während SQLite-Abfragen auch aus anderen Tabs ausgeführt werden können
  • Wie funktioniert diese neue Architektur?
    • Kurz gesagt hat jeder Tab einen dedizierten Web Worker, der auf SQLite schreiben kann
    • Tatsächlich kann aber immer nur ein Tab den Web Worker aktiv nutzen
    • Der SharedWorker verwaltet, welcher Tab der „aktive Tab“ ist
    • Wenn der aktive Tab geschlossen wird, weiß der SharedWorker, dass er einen neuen aktiven Tab auswählen muss
  • Um eine SQLite-Abfrage auszuführen, sendet der Main Thread jedes Tabs die Abfrage an den SharedWorker, der sie an den dedizierten Worker des aktiven Tabs weiterleitet
  • Tabs können beliebig viele SQLite-Abfragen gleichzeitig ausführen; sie werden stets an den einen aktiven Tab geroutet
  • Jeder Web Worker greift mit einer OPFS SyncAccessHandle Pool VFS-Implementierung, die in allen großen Browsern funktioniert, auf die SQLite-Datenbank zu

Warum der einfache Ansatz nicht funktioniert hat

  • Bevor die oben beschriebene Architektur aufgebaut wurde, versuchte man zunächst, WASM SQLite auf einfachere Weise zu betreiben: mit einem dedizierten Web Worker pro Tab, wobei jeder Web Worker in die SQLite-Datenbank schreibt
  • Keiner dieser Ansätze war jedoch in der vorhandenen Form ausreichend für die Anforderungen von Notion

Hürde Nr. 1: Cross-Origin-Isolation

  • Um OPFS via sqlite3_vfs zu verwenden, muss sich die Website im Zustand der „Cross-Origin-Isolation“ befinden
  • Um Cross-Origin-Isolation für eine Seite zu aktivieren, müssen einige Security-Header gesetzt werden, die einschränken, welche Skripte geladen werden dürfen
  • Diese Header zu setzen kann erhebliche Arbeit verursachen
  • Notion ist auf viele Drittanbieter-Skripte angewiesen, die verschiedene Funktionen der Web-Infrastruktur ermöglichen. Um vollständige Cross-Origin-Isolation zu erreichen, hätte man jeden Anbieter bitten müssen, neue Header zu setzen und die Funktionsweise von iframes anzupassen – eine in der Praxis schwer erfüllbare Anforderung
  • In Tests konnten dennoch wichtige Performance-Daten gewonnen werden, indem diese Variante einer Teilmenge von Nutzern über Origin Trials für SharedArrayBuffer bereitgestellt wurde, die in Chrome und Edge verfügbar sind
  • Mit diesen Origin Trials konnte die Anforderung der Cross-Origin-Isolation vorübergehend umgangen werden

Hürde Nr. 2: Beschädigungsprobleme

  • Als OPFS via sqlite3_vfs für eine kleine Gruppe von Nutzern ausgerollt wurde, traten bei einigen schwerwiegende Bugs auf
    • Diese Nutzer sahen fehlerhafte Daten auf Seiten
    • Beispielsweise Kommentare, die dem falschen Kollegen zugewiesen waren, oder Links auf neue Seiten, deren Vorschau eine völlig andere Seite zeigte
  • Bei der Untersuchung der Datenbankdateien betroffener Nutzer zeigte sich ein Muster, dass die SQLite-Datenbank auf irgendeine Weise beschädigt worden war
    • Das Auswählen von Zeilen aus bestimmten Tabellen führte zu Fehlern, und bei der Untersuchung der Zeilen selbst zeigten sich Datenkonsistenzprobleme wie mehrere Zeilen mit derselben ID, aber unterschiedlichem Inhalt
  • Als mögliche Ursache für diesen Zustand der SQLite-Datenbank wurde ein Concurrency-Problem vermutet
    • Mehrere Tabs waren geöffnet, und jeder Tab hatte einen dedizierten Web Worker mit einer aktiven Verbindung zur SQLite-Datenbank
    • Die Notion-Anwendung schreibt häufig in den Cache, nämlich immer dann, wenn Updates vom Server empfangen werden – also genau dann, wenn Tabs gleichzeitig in dieselbe Datei schreiben konnten
  • Es wurde bereits ein Transaktionsansatz verwendet, der SQLite-Abfragen gemeinsam bündelt, doch es bestand der starke Verdacht, dass die fehlende Concurrency-Behandlung auf Seiten der OPFS-API zu den Beschädigungen führte
  • Daher begann man, Beschädigungsfehler zu protokollieren, und probierte einige provisorische Ansätze aus, etwa Web Locks und die Regel, dass nur der fokussierte Tab in SQLite schreiben darf
    • Diese Anpassungen senkten die Beschädigungsrate, aber nicht genug, um die Funktion im produktiven Traffic wieder aktivieren zu können
    • Dennoch ließ sich bestätigen, dass Concurrency-Probleme erheblich zu den Beschädigungen beitrugen
  • In der Notion-Desktop-App trat dieses Problem nicht auf
    • Auf dieser Plattform schreibt nur ein einzelner Parent-Prozess in SQLite
    • In der App können beliebig viele Tabs geöffnet werden, aber immer nur ein Thread greift auf die Datenbankdatei zu

Hürde Nr. 3: Die Alternative kann immer nur in einem Tab gleichzeitig laufen

  • Auch die Variante OPFS SyncAccessHandle Pool VFS wurde evaluiert
    • Diese Variante benötigt kein SharedArrayBuffer und kann daher in Safari, Firefox und anderen Browsern ohne Origin Trial für SharedArrayBuffer verwendet werden
  • Der Nachteil dieser Variante besteht darin, dass sie immer nur in einem Tab gleichzeitig ausgeführt werden kann
    • Versucht ein nachfolgender Tab, die SQLite-Datenbank zu öffnen, tritt einfach ein Fehler auf
  • Einerseits bedeutet dies, dass OPFS SyncAccessHandle Pool VFS nicht die Concurrency-Probleme der Variante OPFS via sqlite3_vfs hat
    • Das wurde bestätigt, da bei einer kleinen Nutzergruppe keine Beschädigungsprobleme festgestellt wurden
  • Andererseits konnte diese Variante nicht unverändert veröffentlicht werden, da alle Nutzer-Tabs von den Vorteilen des Cachings profitieren sollten

Lösung des Problems

  • Die Tatsache, dass keine der Varianten unverändert nutzbar war, führte zum Aufbau der oben beschriebenen SharedWorker-Architektur
  • Diese Architektur ist mit einer dieser SQLite-Varianten kompatibel
  • Bei der Verwendung der Variante OPFS via sqlite3_vfs wird das Beschädigungsproblem vermieden, weil immer nur ein Tab gleichzeitig schreibt
  • Bei der Verwendung der Variante OPFS SyncAccessHandle Pool VFS ermöglicht der SharedWorker Caching in allen Tabs
  • Nachdem bestätigt wurde, dass diese Architektur mit beiden Varianten funktioniert, messbare Performance-Verbesserungen zeigt und keine Beschädigungsprobleme verursacht, musste schließlich entschieden werden, welche Variante ausgeliefert werden soll
  • Die Wahl fiel auf OPFS SyncAccessHandle Pool VFS, weil dafür keine Cross-Origin-Isolation erforderlich ist und die Auslieferung nicht auf Chrome und Edge beschränkt bleibt

Abmilderung von Performance-Verschlechterungen

  • Als diese Verbesserung schrittweise an Nutzer ausgeliefert wurde, wurden einige Performance-Verschlechterungen entdeckt, die behoben werden mussten, etwa längere Ladezeiten

Seitenladezeit wurde langsamer

  • Die erste Beobachtung war, dass die Navigation zwischen Notion-Seiten zwar schneller wurde, das initiale Laden der Seite jedoch langsamer
    • Profiling zeigte, dass das Laden der Seite in der Regel nicht beim Datenabruf den Flaschenhals hat
    • Der App-Boot-Code von Notion führt andere Arbeiten aus – etwa JS-Parsing und App-Setup –, während auf den Abschluss des API-Calls gewartet wird, und profitiert deshalb nicht so stark vom SQLite-Caching wie die Navigation
  • Der Grund für die Verlangsamung war, dass Nutzer die WASM-SQLite-Bibliothek herunterladen und verarbeiten mussten
    • Dadurch wurde der Seitenladeprozess blockiert, sodass andere Ladeaufgaben nicht parallel stattfinden konnten
    • Da diese Bibliothek einige hundert Kilobyte groß ist, war die zusätzliche Zeit in den Metriken deutlich sichtbar
  • Zur Behebung wurde die Art, wie die Bibliothek geladen wird, leicht angepasst
    • WASM SQLite wurde vollständig asynchron geladen, ohne das Laden der Seite zu blockieren
    • Das bedeutete, dass die initialen Seitendaten in den meisten Fällen nicht aus SQLite geladen würden
    • Das war akzeptabel, weil objektiv festgestellt wurde, dass der Geschwindigkeitsgewinn durch das Laden der initialen Seite aus SQLite nicht größer ist als der Geschwindigkeitsverlust durch den Download der Bibliothek
  • Nach dieser Änderung waren die Metriken für das initiale Laden der Seite zwischen Test- und Kontrollgruppe des Experiments identisch

Langsame Geräte profitieren nicht vom Caching

  • Ein weiteres Phänomen in den Metriken war, dass die mediane Zeit für die Navigation von einer Notion-Seite zu einer anderen zwar sank, die Zeit im 95. Perzentil jedoch anstieg
    • Bestimmte Geräte, etwa Mobiltelefone mit einem Browser, der auf Notion zugreift, profitierten nicht vom Caching und wurden stattdessen sogar langsamer
  • Die Antwort auf dieses Rätsel fand sich in einer früheren Untersuchung des Mobile-Teams
    • Bei der Implementierung dieses Cachings in der nativen Mobile-App wurde festgestellt, dass manche Geräte – etwa ältere Android-Smartphones – sehr langsam von der Festplatte lesen
    • Daher konnte nicht davon ausgegangen werden, dass das Laden von Daten aus dem Disk-Cache schneller ist als das Laden derselben Daten über die API
  • Als Ergebnis dieser Mobile-Untersuchung gab es bereits Logik, die beim Laden einer Seite zwei asynchrone Requests – SQLite und API – gegeneinander „rennen“ lässt
    • Diese Logik wurde im Codepfad für Navigationsklicks einfach erneut implementiert
    • Dadurch wurde das 95. Perzentil der Navigationszeit zwischen den beiden Experimentgruppen gleichgezogen

Fazit

  • Die Performance-Verbesserungen von SQLite im Browser zu Notion zu bringen, brachte eigene Herausforderungen mit sich
  • Vor allem im Zusammenhang mit neuer Technologie gab es eine Reihe unbekannter Probleme, aus denen einige Lehren gezogen wurden:
    • OPFS behandelt Concurrency nicht von Haus aus elegant. Entwickler sollten sich dessen bewusst sein und entsprechend designen
    • Web Workers und SharedWorkers (sowie ihre in diesem Beitrag nicht erwähnten Verwandten, die Service Workers) haben unterschiedliche Fähigkeiten, und es kann nützlich sein, sie bei Bedarf zu kombinieren
    • Stand Frühjahr 2024 ist es nicht einfach, Cross-Origin-Isolation in einer anspruchsvollen Web-Anwendung vollständig umzusetzen – insbesondere bei Verwendung von Drittanbieter-Skripten
  • Durch das Zwischenspeichern von Daten mit SQLite im Browser wurde für Nutzer die bereits erwähnte Verbesserung der Navigationszeit um 20 % erreicht, ohne Verschlechterungen bei anderen Metriken zu beobachten
    • Entscheidend ist, dass keine Probleme durch SQLite-Beschädigungen beobachtet wurden
    • Der Erfolg und die Stabilität dieses finalen Ansatzes sind nach Einschätzung von Notion dem Team hinter der offiziellen WASM-Implementierung von SQLite sowie Roy Hashimoto zu verdanken, der experimentelle Ansätze öffentlich zugänglich gemacht hat

6 Kommentare

 
[Dieser Kommentar wurde ausgeblendet.]
 
cometkim 2024-07-19

Deshalb sollten Services, die mit Drittanbietern zusammenarbeiten müssen, von der ersten Veröffentlichung an die Cross-Origin-Isolation aktivieren ...

 
freedomzero 2024-07-20

Oh, cometkim, freut mich, dich zu sehen :)

 
sixmen 2024-07-19

Wenn ich in Firefox eine Notion-Seite öffne, friert sie ein und ist unbenutzbar – liegt das vielleicht daran? .. (Die Notion-App funktioniert gut, daher nutze ich vorerst die.)

 
hellworld 2024-07-20

Vermutlich ist das so. Enda unterstützt lokales Schreiben von Dateien ebenfalls nur in Chrome & Edge.

 
freedomzero 2024-07-20

Das hatte ich früher auf alten Linux-Laptops auch schon; im privaten Modus gestartet hat es funktioniert.