decocms / decocms/blocks

perf(cache): o cache de loader é por isolate e quase nunca serve (3-28% quente com 12 chaves em 10min)

Open
#536 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
5
Forks
2
Avg merge
20h 12m
Merged PRs (30d)
36

Description

Resumo

O cache de loader (createCachedLoader, sdk/cachedLoader.ts) é um Map de módulo,
ou seja por isolate. Num Worker com rotatividade alta de isolates ele quase nunca
serve: medido sob carga, com 12 chaves quentes martelizadas por 10 minutos, a
fração de respostas quentes ficou entre 3% e 28%.

Não é problema de TTL nem de cap de bytes. É a topologia: o cache morre com o isolate.

Evidência 1 — a vida do isolate

Produção (montecarlo, hora de pico, 3.808 isolates distintos numa hora), lookups de
cachedLoader por isolate:

lookups
p50 1
p90 12
p99 25
máximo 78

O isolate mediano faz um lookup e morre. Não há segunda chance de acertar.

Evidência 2 — teste de carga por camada (10 min)

Contra o worker de um site real, tráfego sustentado, 4.292 requisições, zero erro.
HTML para exercitar a borda e POST /deco/invoke/<loader> para exercitar o loader
isolado (o /deco/ faz bypass da borda por design, então é a única forma de ver
esta camada sem o edge mascarar).

camada resultado
HTML (borda, caches.default) hit 97,2%, p50 108ms
loader (/deco/invoke) p50 1739ms, p95 4468ms

A borda aquece em ~2 min e segura. O loader não aquece nunca.

Detalhe por chave — cada uma recebeu 150-190 chamadas idênticas em 10 minutos:

entrada quente p50 min
804 KB 6% 3377ms 95ms
707 KB 3% 3394ms 107ms
520 KB 4% 3352ms 793ms
367 KB 20% 2675ms 76ms
316 KB 22% 1823ms 88ms
122 KB 28% 812ms 71ms
15 KB 22% 575ms 58ms

O min mostra que o cache consegue servir em 60-110ms. Ele só quase nunca tem a
entrada.

O que já foi descartado

  • Subir o cap de bytes não resolve. 12 chaves × ~500 KB = ~6 MB, contra um cap de
    32 MB que nunca é alcançado (o p99 dos isolates segura ~12 MB). Já subimos para
    64 MB num site como folga, sem expectativa de mover hit rate — e não moveu.
  • TTL não resolve. O isolate morre muito antes de qualquer janela razoável.
  • Chave poluída já foi corrigida (#532, #533, #534). O hit rate subiu onde a chave
    era o problema; esta camada continua igual.

Proposta para discussão

Apoiar o cache de loader em algo compartilhado por colo em vez de por isolate —
caches.default é o candidato óbvio, já que a camada de borda usa e entrega 97%.

Pontos que precisam de decisão, e por isso isto é issue e não PR:

  1. Serialização. O Map guarda objetos vivos; caches.default guarda Response.
    Passa a haver custo de JSON por hit. Para uma entrada de 800 KB isso não é
    desprezível e pode comer parte do ganho — precisa medir antes de assumir.
  2. Invalidação. Hoje clearLoaderCache() limpa um Map local. Com cache
    compartilhado a limpeza vira purge por chave, e o /_cache/purge-loaders precisa
    acompanhar.
  3. SWR e single-flight. O stale-while-revalidate e a deduplicação por
    inflight são hoje in-process. Compartilhado, o single-flight vira problema
    distribuído — ou se aceita revalidação duplicada entre isolates.
  4. Híbrido? Manter o Map como L1 (grátis, mesma isolate) e caches.default
    como L2 seria o melhor dos dois, ao custo de duas camadas para raciocinar.

Segunda frente, independente

O tamanho das entradas (até 804 KB por PLP) merece atenção separada: as três
maiores foram as que menos aqueceram (3-6%) contra 20-28% das menores.

Ressalva: essa correlação é sugestiva, não causal provada — o tráfego HTML do
teste divide os mesmos isolates e também povoa o cache, então não dá para atribuir
tudo a evicção. Mas skills/knowledge/perf/edge-caching.md já diz "Entry SIZE is the
constraint, not hit rate", e estes números não contradizem.

Como reproduzir

npx -p '@decocms/parity@0.33.14' parity cache-gap \
  --url <worker-url> --dir <repo-do-site> \
  --pages 30 --loaders --loader-limit 30 --max-requests 1500

A fase 3 mede cada loader em dois modos, serial e fanned — a diferença entre os
dois é justamente o custo de fragmentação por isolate.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with sdk/cachedLoader.ts, the /_cache/purge-loaders path, and skills/knowledge/perf/edge-caching.md to understand the current Map, invalidation, SWR, and single-flight behavior. Run the provided parity cache-gap command against a worker and compare serial and fanned loader results. Done requires an agreed cache topology, measured serialization and hit-rate impact, and aligned invalidation behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
backend-api-design, cloud, performance
Issue type
Refactor
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.