perf(cache): o cache de loader é por isolate e quase nunca serve (3-28% quente com 12 chaves em 10min)
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:
- Serialização. O
Mapguarda objetos vivos;caches.defaultguardaResponse.
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. - Invalidação. Hoje
clearLoaderCache()limpa umMaplocal. Com cache
compartilhado a limpeza vira purge por chave, e o/_cache/purge-loadersprecisa
acompanhar. - SWR e single-flight. O
stale-while-revalidatee a deduplicação por
inflightsão hoje in-process. Compartilhado, o single-flight vira problema
distribuído — ou se aceita revalidação duplicada entre isolates. - Híbrido? Manter o
Mapcomo L1 (grátis, mesma isolate) ecaches.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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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