24 pontos por xguru 2024-07-19 | 6 comentários | Compartilhar no WhatsApp
  • Há 3 anos, a Notion melhorou com sucesso a velocidade dos apps da Notion para Mac e Windows ao usar um banco de dados SQLite para armazenar dados em cache no cliente
  • Desta vez, conseguiu levar a mesma melhoria também para os usuários que acessam a Notion pelo navegador
  • Este artigo é uma análise aprofundada de como a Notion melhorou o desempenho no navegador usando a implementação sqlite3 em WebAssembly (WASM)
  • Com o uso de SQLite, o tempo de navegação entre páginas melhorou 20% em todos os navegadores modernos
    • A diferença foi ainda mais evidente para usuários cujo tempo de resposta da API era especialmente lento devido a fatores externos, como a conexão com a internet
    • Por exemplo, para usuários na Austrália o tempo de navegação entre páginas ficou 28% mais rápido, na China 31% e na Índia 33%

Tecnologias principais: OPFS e Web Workers

  • A biblioteca WASM SQLite usa a API moderna do navegador chamada Origin Private File System (OPFS) para manter os dados entre sessões
  • O OPFS só pode ser usado em Web Workers
  • Um Web Worker pode ser entendido como um código que roda em uma thread separada da thread principal, onde a maior parte do JavaScript do navegador é executada
  • A Notion é empacotada com Webpack, que oferece uma sintaxe fácil de usar para carregar Web Workers
  • Foi configurado um Web Worker para criar ou carregar um arquivo de banco de dados SQLite existente usando OPFS, e o código de cache já existente passou a rodar nesse Web Worker
  • A biblioteca Comlink foi usada para facilitar o gerenciamento da troca de mensagens entre a thread principal e o Worker

Abordagem baseada em SharedWorker

  • A arquitetura final foi baseada em uma nova solução apresentada por Roy Hashimoto em uma discussão no GitHub
    • Uma abordagem que permite que apenas uma aba por vez acesse o SQLite, ao mesmo tempo em que outras abas também podem executar consultas SQLite
  • Como essa nova arquitetura funciona?
    • Em resumo, cada aba tem um Web Worker dedicado que pode escrever no SQLite
    • Mas, na prática, apenas uma aba pode usar de fato o Web Worker
    • O SharedWorker é responsável por gerenciar qual é a "aba ativa"
    • Quando a aba ativa é fechada, o SharedWorker sabe que precisa escolher uma nova aba ativa
  • Para executar uma consulta SQLite, a thread principal de cada aba envia a consulta ao SharedWorker, e o SharedWorker a redireciona para o Worker dedicado da aba ativa
  • As abas podem executar quantas consultas SQLite quiserem ao mesmo tempo, e tudo é sempre roteado para uma única aba ativa
  • Cada Web Worker acessa o banco de dados SQLite usando a implementação OPFS SyncAccessHandle Pool VFS, que funciona em todos os principais navegadores

Por que a abordagem simples não funcionou

  • Antes de construir a arquitetura descrita acima, foi feita uma tentativa de rodar o WASM SQLite de uma forma mais simples, com um Web Worker dedicado por aba e cada Web Worker escrevendo no banco de dados SQLite
  • No entanto, nenhuma delas era suficiente para atender às necessidades da Notion sem adaptações

Obstáculo #1: isolamento entre origens

  • Para usar OPFS via sqlite3_vfs, o site precisava estar em estado de "isolamento entre origens"
  • Para adicionar o isolamento entre origens à página, era necessário configurar alguns cabeçalhos de segurança que restringem quais scripts podem ser carregados
  • Configurar esses cabeçalhos poderia dar bastante trabalho
  • A Notion depende de muitos scripts de terceiros para várias funções da infraestrutura web, então alcançar isolamento total entre origens exigiria pedir a cada fornecedor que configurasse novos cabeçalhos e alterasse a forma como os iframes funcionam — uma exigência difícil de viabilizar na prática
  • Nos testes, foi possível obter dados importantes de desempenho oferecendo essa variante a um subconjunto de usuários por meio de Origin Trials para SharedArrayBuffer, disponíveis nos navegadores Chrome e Edge
  • Com esses Origin Trials, foi possível contornar temporariamente a exigência de isolamento entre origens

Obstáculo #2: problemas de corrupção

  • Quando o OPFS via sqlite3_vfs foi disponibilizado para um pequeno grupo de usuários, começaram a surgir bugs graves para alguns deles
    • Esses usuários passaram a ver dados incorretos nas páginas
    • Por exemplo, comentários atribuídos à pessoa errada ou links para novas páginas cuja prévia era de uma página completamente diferente
  • Ao examinar os arquivos de banco de dados dos usuários afetados, apareceu um padrão indicando que o banco SQLite estava corrompido de alguma forma
    • Ao selecionar linhas de certas tabelas, surgiam erros; ao inspecionar as próprias linhas, foram encontrados problemas de consistência, como múltiplas linhas com o mesmo ID, mas com conteúdos diferentes
  • Sobre como o banco SQLite chegou a esse estado, a hipótese foi de que isso ocorreu por causa de problemas de concorrência
    • Havia várias abas abertas, e cada aba tinha um Web Worker dedicado com uma conexão ativa com o banco de dados SQLite
    • O aplicativo da Notion escreve com frequência no cache sempre que recebe atualizações do servidor, ou seja, as abas acabavam escrevendo no mesmo arquivo ao mesmo tempo
  • Já estava sendo usada uma abordagem com transações para agrupar consultas SQLite em lote, mas havia forte suspeita de que a corrupção ocorria por falta de tratamento de concorrência do lado da API OPFS
  • Por isso, começaram a registrar logs dos erros de corrupção e tentaram algumas soluções paliativas, como adicionar Web Locks e permitir que apenas a aba em foco escrevesse no SQLite
    • Esses ajustes reduziram a taxa de corrupção, mas não o suficiente para reativar o recurso no tráfego de produção
    • Ainda assim, foi possível confirmar que os problemas de concorrência contribuíam significativamente para a corrupção
  • Esse problema não ocorria no app desktop da Notion
    • Nessas plataformas, apenas um único processo pai escreve no SQLite
    • É possível abrir quantas abas quiser no app, mas sempre apenas uma thread acessa o arquivo do banco de dados

Obstáculo #3: a alternativa só pode rodar em uma aba por vez

  • A variante OPFS SyncAccessHandle Pool VFS também foi avaliada
    • Essa variante não exige SharedArrayBuffer, então pode ser usada em Safari, Firefox e outros navegadores sem Origin Trial para SharedArrayBuffer
  • A desvantagem dessa variante é que ela só pode rodar em uma aba por vez
    • Se uma aba posterior tentar abrir o banco de dados SQLite, simplesmente ocorre um erro
  • Por um lado, isso significa que o OPFS SyncAccessHandle Pool VFS não sofre dos problemas de concorrência da variante OPFS via sqlite3_vfs
    • Isso foi confirmado porque não foram encontrados problemas de corrupção ao disponibilizá-la para um pequeno grupo de usuários
  • Por outro lado, essa variante não podia ser lançada como estava, porque a intenção era que todas as abas dos usuários se beneficiassem do cache

Resolvendo o problema

  • O fato de nenhuma variante poder ser usada como estava levou à construção da arquitetura com SharedWorker descrita acima
  • Essa arquitetura é compatível com uma dessas variantes de SQLite
  • Ao usar a variante OPFS via sqlite3_vfs, como apenas uma aba escreve por vez, é possível evitar os problemas de corrupção
  • Ao usar a variante OPFS SyncAccessHandle Pool VFS, o SharedWorker permite que o cache funcione em todas as abas
  • Depois de confirmar que essa arquitetura funcionava com ambas as variantes, que a melhora de desempenho era visível nas métricas e que não havia problemas de corrupção, chegou o momento de escolher qual variante seria usada em definitivo
  • Foi escolhido o OPFS SyncAccessHandle Pool VFS, porque ele não exige isolamento entre origens e, portanto, não impede o lançamento em navegadores além de Chrome e Edge

Mitigando regressões de desempenho

  • Quando essa melhoria começou a ser disponibilizada aos usuários, foram encontrados alguns problemas de regressão de desempenho que precisavam ser corrigidos, como tempos de carregamento mais lentos

O carregamento da página ficou mais lento

  • A primeira descoberta foi que a movimentação entre páginas da Notion ficou mais rápida, mas o carregamento inicial da página ficou mais lento
    • A análise de profiling mostrou que, em geral, o carregamento da página não fica limitado pela busca de dados
    • O código de inicialização do app da Notion executa outras tarefas enquanto espera a conclusão das chamadas de API, como parsing de JS e configuração do app, então ele não se beneficia do cache SQLite tanto quanto a navegação
  • A lentidão aconteceu porque o usuário precisava baixar e processar a biblioteca WASM SQLite
    • Isso bloqueava o processo de carregamento da página e impedia que outras tarefas de carregamento acontecessem ao mesmo tempo
    • Como essa biblioteca tem algumas centenas de kilobytes, o tempo adicional apareceu de forma perceptível nas métricas
  • Para resolver isso, a forma de carregar a biblioteca foi ajustada levemente
    • O WASM SQLite passou a ser carregado de forma totalmente assíncrona, sem bloquear o carregamento da página
    • Isso significava que os dados da página inicial quase nunca seriam carregados a partir do SQLite
    • Isso foi considerado aceitável, porque foi avaliado de forma objetiva que o ganho de velocidade ao carregar a página inicial a partir do SQLite não compensava a perda causada pelo download da biblioteca
  • Após aplicar a mudança, as métricas de carregamento inicial da página ficaram iguais entre o grupo de teste e o grupo de controle do experimento

Dispositivos lentos não se beneficiavam do cache

  • Outro comportamento encontrado nas métricas foi que, embora o tempo mediano de navegação entre páginas da Notion tenha melhorado, o tempo no percentil 95 ficou pior
    • Certos dispositivos, como celulares com navegador apontando para a Notion, não se beneficiavam do cache e na verdade apresentavam piora
  • A resposta para esse enigma veio de uma investigação anterior conduzida pela equipe mobile
    • Quando esse cache foi implementado no aplicativo mobile nativo, alguns dispositivos, como celulares Android antigos, tinham leitura de disco muito lenta
    • Portanto, não era possível assumir que carregar dados do cache em disco seria mais rápido do que carregar os mesmos dados via API
  • Como resultado dessa investigação mobile, já existia na navegação de páginas uma lógica que fazia duas requisições assíncronas (SQLite e API) "competirem" entre si
    • Essa lógica foi simplesmente reimplementada no caminho de código dos cliques de navegação
    • Isso fez com que o percentil 95 do tempo de navegação ficasse igual entre os dois grupos do experimento

Conclusão

  • Levar ao navegador as melhorias de desempenho do SQLite para a Notion teve seus próprios desafios
  • Em especial, surgiram várias incógnitas ligadas a tecnologias novas, e algumas lições foram aprendidas ao longo do processo:
    • O OPFS, por padrão, não lida com concorrência de forma elegante. Os desenvolvedores precisam estar atentos a isso e projetar suas soluções levando esse fator em conta
    • Web Workers e SharedWorkers (e seus parentes Service Workers, não abordados neste artigo) têm capacidades diferentes, e pode ser útil combiná-los quando necessário
    • Até a primavera de 2024, implementar totalmente o isolamento entre origens em uma aplicação web sofisticada não é algo simples, especialmente quando há uso de scripts de terceiros
  • Como resultado do cache de dados com SQLite no navegador para os usuários, foi observada a melhora de 20% no tempo de navegação mencionada anteriormente, sem piora em outras métricas
    • O mais importante é que não foram observados problemas causados por corrupção do SQLite
    • Considera-se que o sucesso e a estabilidade dessa abordagem final se devem à equipe responsável pela implementação oficial em WASM do SQLite, assim como a Roy Hashimoto por disponibilizar ao público essa abordagem experimental

6 comentários

 
[Este comentário foi ocultado.]
 
cometkim 2024-07-19

É por isso que serviços que precisam colaborar com terceiros já deveriam sair com isolamento entre origens ativado desde o primeiro lançamento...

 
freedomzero 2024-07-20

Opa, cometkim, que bom te ver por aqui

 
sixmen 2024-07-19

Quando abro uma página do Notion no Firefox, ela trava e fica impossível de usar. Será que é por causa disso..? (O app do Notion funciona bem, então por enquanto estou usando ele)

 
hellworld 2024-07-20

Provavelmente é isso. O Enda também só oferece suporte à gravação de arquivos locais no Chrome e no Edge.

 
freedomzero 2024-07-20

Aconteceu comigo em um laptop Linux antigo; bastava abrir no modo privado.