24 puntos por xguru 2024-07-19 | 6 comentarios | Compartir por WhatsApp
  • Hace 3 años, Notion logró mejorar la velocidad de la app de Notion para Mac y Windows usando una base de datos SQLite para almacenar datos en caché en el cliente
  • Esta vez también lograron ofrecer la misma mejora a los usuarios que acceden a Notion a través del navegador
  • Este artículo analiza en profundidad cómo mejoraron el rendimiento de Notion en el navegador usando la implementación sqlite3 de WebAssembly (WASM)
  • Al usar SQLite, el tiempo de navegación entre páginas mejoró un 20% en todos los navegadores modernos
    • La diferencia fue aún más notable para usuarios cuyo tiempo de respuesta de la API era especialmente lento debido a factores externos como la conexión a internet
    • Por ejemplo, para usuarios de Australia el tiempo de navegación entre páginas fue 28% más rápido, para usuarios de China 31% y para usuarios de India 33%

Tecnología clave: OPFS y Web Workers

  • La biblioteca WASM SQLite usa una API moderna del navegador llamada Origin Private File System (OPFS) para mantener los datos entre sesiones
  • OPFS solo puede usarse dentro de Web Workers
  • Se puede pensar en un Web Worker como código que se ejecuta en un hilo separado del hilo principal, donde corre la mayor parte del JavaScript del navegador
  • Notion se empaqueta con Webpack, que ofrece una sintaxis fácil de usar para cargar Web Workers
  • Configuraron un Web Worker para crear un archivo de base de datos SQLite usando OPFS o cargar uno existente, y ejecutaron en ese Web Worker el código de caché ya existente
  • Usaron la biblioteca Comlink para manejar fácilmente el paso de mensajes entre el hilo principal y el Worker

Enfoque basado en SharedWorker

  • La arquitectura final se basa en una nueva solución presentada por Roy Hashimoto en una discusión de GitHub
    • Un enfoque que permite que solo una pestaña acceda a SQLite a la vez, mientras otras pestañas también pueden ejecutar consultas SQLite
  • ¿Cómo funciona esta nueva arquitectura?
    • En pocas palabras, cada pestaña tiene un Web Worker dedicado que puede escribir en SQLite
    • Sin embargo, en la práctica solo una pestaña puede usar realmente su Web Worker
    • El SharedWorker se encarga de administrar cuál es la "pestaña activa"
    • Si la pestaña activa se cierra, el SharedWorker sabe que debe elegir una nueva pestaña activa
  • Para ejecutar consultas SQLite, el hilo principal de cada pestaña envía la consulta al SharedWorker, y este la redirige al Worker dedicado de la pestaña activa
  • Las pestañas pueden ejecutar tantas consultas SQLite simultáneamente como quieran, y siempre se enrutan a una sola pestaña activa
  • Cada Web Worker accede a la base de datos SQLite usando una implementación OPFS SyncAccessHandle Pool VFS que funciona en todos los navegadores principales

Por qué no funcionó un enfoque más simple

  • Antes de construir la arquitectura descrita arriba, intentaron ejecutar WASM SQLite de una forma más simple: dar a cada pestaña su propio Web Worker dedicado y hacer que cada Web Worker escribiera en la base de datos SQLite
  • Sin embargo, ninguna opción era suficiente tal cual para los requisitos de Notion

Obstáculo #1: aislamiento de origen cruzado

  • Para usar OPFS vía sqlite3_vfs, el sitio debe estar en estado de "cross-origin isolation"
  • Para agregar aislamiento de origen cruzado a una página, hay que configurar varios encabezados de seguridad que restringen qué scripts se pueden cargar
  • Configurar esos encabezados puede implicar bastante trabajo
  • Notion depende de muchos scripts de terceros para operar distintas funciones de su infraestructura web, así que lograr un aislamiento de origen cruzado completo habría requerido pedir a cada proveedor que configurara nuevos encabezados y cambiara cómo funcionan sus iframes, algo difícil de hacer en la práctica
  • En pruebas, pudieron obtener datos importantes de rendimiento ofreciendo esta variante a un subconjunto de usuarios mediante Origin Trials para SharedArrayBuffer, disponibles en Chrome y Edge
  • Esos Origin Trials permitieron evitar temporalmente el requisito de aislamiento de origen cruzado

Obstáculo #2: problemas de corrupción

  • Cuando ofrecieron OPFS vía sqlite3_vfs a un pequeño grupo de usuarios, comenzaron a aparecer bugs graves para algunos de ellos
    • Esos usuarios veían datos incorrectos en la página
    • Por ejemplo, comentarios asignados al compañero equivocado o enlaces a páginas nuevas cuya vista previa correspondía a una página completamente distinta
  • Al revisar los archivos de base de datos de los usuarios afectados, encontraron patrones que indicaban que la base de datos SQLite se había corrompido de alguna manera
    • Seleccionar filas de ciertas tablas producía errores y, al inspeccionar las filas, aparecían problemas de consistencia de datos, como múltiples filas con el mismo ID pero contenidos distintos
  • Sobre cómo la base de datos SQLite llegó a ese estado, supusieron que se debía a problemas de concurrencia
    • Había varias pestañas abiertas y cada una tenía un Web Worker dedicado con una conexión activa a la base de datos SQLite
    • La aplicación de Notion escribe con frecuencia en la caché cada vez que recibe actualizaciones del servidor, es decir, cuando varias pestañas podían estar escribiendo al mismo archivo al mismo tiempo
  • Ya usaban un enfoque con transacciones para agrupar consultas SQLite, pero sospecharon fuertemente que la corrupción se debía a la falta de manejo de concurrencia del lado de la API de OPFS
  • Por eso comenzaron a registrar los errores de corrupción e intentaron algunos parches, como agregar Web Locks y hacer que solo la pestaña enfocada escribiera en SQLite
    • Esos ajustes redujeron la tasa de corrupción, pero no lo suficiente como para volver a activar la función en tráfico de producción
    • Aun así, pudieron confirmar que los problemas de concurrencia contribuían de forma importante a la corrupción
  • Este problema no ocurría en la app de escritorio de Notion
    • En esa plataforma, solo un proceso padre escribe en SQLite
    • Se pueden abrir todas las pestañas que se quiera en la app, pero siempre hay un solo hilo accediendo al archivo de base de datos

Obstáculo #3: la alternativa solo puede ejecutarse en una pestaña a la vez

  • También evaluaron la variante OPFS SyncAccessHandle Pool VFS
    • Esta variante no requiere SharedArrayBuffer, por lo que puede usarse en Safari, Firefox y otros navegadores sin Origin Trial para SharedArrayBuffer
  • La desventaja de esta variante es que solo puede ejecutarse en una pestaña a la vez
    • Si una pestaña posterior intenta abrir la base de datos SQLite, simplemente falla con un error
  • Por un lado, eso significa que OPFS SyncAccessHandle Pool VFS no tiene los problemas de concurrencia de la variante OPFS vía sqlite3_vfs
    • Lo confirmaron al ofrecerla a un pequeño grupo de usuarios y no detectar problemas de corrupción
  • Por otro lado, no podían lanzar esta variante tal cual porque querían que todas las pestañas de los usuarios se beneficiaran del caché

Resolución del problema

  • El hecho de que ninguna variante pudiera usarse tal cual fue lo que los llevó a construir la arquitectura con SharedWorker descrita arriba
  • Esta arquitectura es compatible con cualquiera de estas variantes de SQLite
  • Al usar la variante OPFS vía sqlite3_vfs, como solo una pestaña escribe a la vez, se evita el problema de corrupción
  • Al usar la variante OPFS SyncAccessHandle Pool VFS, el SharedWorker permite que el caché esté disponible en todas las pestañas
  • Después de confirmar que esta arquitectura funcionaba con ambas variantes, que la mejora de rendimiento era visible en las métricas y que no había problemas de corrupción, llegó el momento de elegir cuál variante ofrecer definitivamente
  • Eligieron OPFS SyncAccessHandle Pool VFS porque no requiere aislamiento de origen cruzado, así que no bloquea el despliegue en navegadores distintos de Chrome y Edge

Mitigación de regresiones de rendimiento

  • Cuando comenzaron a ofrecer esta mejora a los usuarios, detectaron algunas regresiones de rendimiento que tuvieron que corregir, como tiempos de carga más lentos

La carga de página se volvió más lenta

  • El primer hallazgo fue que navegar entre páginas de Notion se volvió más rápido, pero la carga inicial de página se hizo más lenta
    • El profiling mostró que la carga de página normalmente no está limitada por la obtención de datos
    • El código de arranque de la app de Notion ejecuta otras tareas mientras espera que termine la llamada a la API, como parseo de JS, configuración de la app, etc., por lo que no se beneficia tanto del caché SQLite como la navegación
  • La razón de la lentitud era que los usuarios tenían que descargar y procesar la biblioteca WASM SQLite
    • Eso bloqueaba el proceso de carga de página, impidiendo que otras tareas de carga ocurrieran al mismo tiempo
    • Como esta biblioteca pesa varios cientos de kilobytes, el tiempo adicional se notaba claramente en las métricas
  • Para resolverlo, modificaron un poco la forma de cargar la biblioteca
    • Cargaron WASM SQLite de forma completamente asíncrona para no bloquear la carga de página
    • Eso significaba que era poco probable que los datos iniciales de la página se cargaran desde SQLite
    • Les pareció aceptable, porque objetivamente concluyeron que la mejora de velocidad por cargar la página inicial desde SQLite no compensaba la pérdida causada por descargar la biblioteca
  • Después de aplicar el cambio, las métricas de carga inicial de página quedaron iguales entre el grupo de prueba y el grupo de control del experimento

Los dispositivos lentos no se benefician del caché

  • Otro fenómeno que vieron en las métricas fue que el tiempo mediano para navegar de una página de Notion a otra era más rápido, pero el tiempo del percentil 95 era más lento
    • Ciertos dispositivos, como teléfonos móviles con un navegador apuntando a Notion, no se beneficiaban del caché e incluso empeoraban
  • Encontraron la respuesta a este enigma en una investigación previa realizada por el equipo móvil
    • Cuando implementaron este caché en la app móvil nativa, algunos dispositivos, como teléfonos Android antiguos, leían muy lentamente desde disco
    • Por lo tanto, no se podía asumir que cargar datos desde la caché en disco sería más rápido que cargarlos desde la API
  • Esa investigación móvil ya había llevado a una lógica donde dos solicitudes asíncronas, SQLite y API, "compiten" durante la carga de página
    • Simplemente reimplementaron esa lógica en la ruta de código para los clics de navegación
    • Eso igualó el percentil 95 de los tiempos de navegación entre los dos grupos del experimento

Conclusión

  • Llevar las mejoras de rendimiento de SQLite a Notion en el navegador tuvo sus propias dificultades
  • En particular, se enfrentaron a una serie de incógnitas relacionadas con tecnologías nuevas y sacaron varias lecciones en el proceso:
    • OPFS no maneja la concurrencia de forma elegante por defecto. Los desarrolladores deben ser conscientes de ello y diseñar en consecuencia
    • Web Workers y SharedWorkers (y su primo no mencionado aquí, Service Workers) tienen capacidades distintas, y puede ser útil combinarlos cuando sea necesario
    • A primavera de 2024, no es fácil implementar completamente el aislamiento de origen cruzado en una aplicación web sofisticada, especialmente si usa scripts de terceros
  • Como resultado de almacenar datos en caché con SQLite en el navegador para los usuarios, vieron la mejora de 20% en el tiempo de navegación mencionada antes, sin observar deterioro en otras métricas
    • Lo importante es que no observaron problemas causados por corrupción de SQLite
    • Consideran que el éxito y la estabilidad de este enfoque final se deben al equipo a cargo de la implementación oficial de SQLite en WASM, así como a Roy Hashimoto por ofrecer al público un enfoque experimental

6 comentarios

 
[Este comentario fue ocultado.]
 
cometkim 2024-07-19

Por eso, los servicios que tienen que colaborar con terceros deberían salir desde su primer lanzamiento con el aislamiento entre orígenes activado...

 
freedomzero 2024-07-20

Oh, cometkim, qué gusto verte.

 
sixmen 2024-07-19

En Firefox, cuando abro una página de Notion, se queda congelada y no puedo usarla. ¿Será por esto?.. (La app de Notion sí funciona bien, así que por ahora la estoy usando)

 
hellworld 2024-07-20

Probablemente sea así. Enda también solo admitía escritura de archivos locales en Chrome y Edge.

 
freedomzero 2024-07-20

Me pasó algo así en una laptop Linux vieja; parece que abrirlo en modo privado funcionaba.