Skip to content

perf(admin): take the meta schema out of the isolate (−41 MB warm) - #545

Open
igoramf wants to merge 3 commits into
mainfrom
osaka
Open

igoramf wants to merge 3 commits into
mainfrom
osaka

Conversation

@igoramf

@igoramf igoramf commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

The admin JSON Schema costs ~40 MB of a 128 MB Workers isolate on a large site (montecarlo: 1690 blocks), and only GET /live/_meta reads it — nothing on the render path does. V8 keeps the module's source text and materialises the JSON.parse argument as a second independent string, both two-byte because the content is accented, so a 10.6 MB chunk is paid roughly four times over.

This mirrors what fastDeploy already does for the decofile: meta:<id> + meta:etag:<id> seeded by deco-sync-blocks-to-kv, decoVitePlugin({ metaFromKV: true }) stubbing meta.gen out of the server bundle, and handleMeta streaming kv.get(..., { type: "stream" }) straight into the Response — nothing parses the payload, since materialising it even as a string would reintroduce most of the cost. The ETag is precomputed at build under its own small key, both so If-None-Match (what admin actually polls with) costs one tiny read and because hashing JSON.stringify(schema) is impossible once the schema has left the isolate; the same value is merged into the payload's etag field so the existing {...schema, etag} wire format survives a byte-for-byte passthrough.

Measured on montecarlo, production build in workerd, warm over 8 real routes: 70.5 MB → 29.5 MB. (The first commit, making the eager options.meta() load lazy, measured at 0 MB on its own — the source text and the string literal are both materialised at module load, before evaluation. It is kept because it makes handleMeta async, which is what lets it await KV, and because it removes a floating promise that made /live/_meta nondeterministic.)

Degrades safely, unlike the decofile stub: with no keys handleMeta falls back to the bundled schema; with the bundle stubbed and no keys it answers 503 and only the admin loses the schema — the site keeps serving. The read side is registered from createDecoWorkerEntry rather than a setup call sites must remember, since montecarlo never calls setupTanstackFastDeploy and a getter nobody calls is the same bug as no getter. next is unaffected: it never registers a KV getter, so it keeps the in-bundle path.

🤖 Generated with Claude Code

createAdminSetup fired the `meta` thunk unconditionally during bootstrap,
despite the comment claiming it was lazy. On a large site the composed
schema is tens of MB of heap per isolate (montecarlo: ~40 MB of a 128 MB
Workers budget) and nothing on the render path reads it — only
GET /live/_meta does.

setMetaProvider stores the thunk; handleMeta resolves it on first request
and memoises it per isolate. A failed load answers 503 and clears the memo
so the next request retries.

Note this only drops the parsed schema — the meta.gen module's source text
is still retained by V8 because it ships in the server bundle. Removing
that needs the schema to leave the bundle entirely (static asset or KV).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@igoramf
igoramf requested a review from a team September 9, 2026 17:20
The admin JSON Schema costs ~40 MB of a 128 MB Workers isolate on a large
site (montecarlo: 1690 blocks) and only GET /live/_meta reads it. V8 keeps
the module's source text AND materialises the JSON.parse argument as a
second string, both two-byte because the content is accented — so a 10.6 MB
chunk is paid roughly four times over.

Mirrors what fastDeploy already does for the decofile:

- `meta:<id>` + `meta:etag:<id>` alongside `decofile:<id>`, seeded by
  deco-sync-blocks-to-kv (automatic when it finds the file; --no-meta opts
  out).
- `decoVitePlugin({ metaFromKV: true })` stubs meta.gen out of the server
  bundle. Independent of fastDeploy — different artefact, different seed.
- handleMeta streams `kv.get(..., { type: "stream" })` straight into the
  Response. Nothing parses the payload; materialising it even as a string
  would reintroduce most of the cost.
- The ETag is precomputed at build under its own small key, so If-None-Match
  (what admin actually polls with) costs one tiny read, and because hashing
  JSON.stringify(schema) is impossible once the schema is not in the isolate.
  The same value is merged into the payload's `etag` field so the wire format
  survives a byte-for-byte passthrough.

Registered from createDecoWorkerEntry rather than a setup call sites must
remember — montecarlo never calls setupTanstackFastDeploy, and a getter
nobody calls is the same bug as no getter.

Degrades safely: no keys means handleMeta falls back to the bundled schema;
bundle stubbed with no keys means 503 on /live/_meta only — the site itself
keeps serving.

Measured on montecarlo, production build in workerd, warm over 8 real routes:
70.5 MB -> 29.5 MB.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@igoramf igoramf changed the title perf(admin): load the meta schema lazily instead of at boot perf(admin): take the meta schema out of the isolate (−41 MB warm) Sep 9, 2026
The read path was keyed off "are the keys present", which meant any site
whose build ran deco-sync-blocks-to-kv would silently start serving
/live/_meta from KV — opted in or not, since the seed is unconditional.
That is the shape the decofile path already rejects on purpose
(isFastDeployEnabled: binding a namespace must not by itself flip a site
onto the KV path).

`metaFromKV` is now the single switch for both sides, via a
`__DECO_META_FROM_KV__` define mirroring `__DECO_BLOCKS_STUBBED__`. With the
flag off, KV is never read and nothing changes for existing sites; with it
on, there is no state where the bundle has no schema but the reader declines
to fetch one.

Verified end to end against montecarlo (production build in workerd, all KV
keys seeded): flag off leaves /live/_meta on its previous behaviour and never
touches KV; flag on serves 200 from KV with a stable ETag and 304 on
If-None-Match, with only a 65-byte stub in place of the 10.6 MB chunk.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant