Skip to content

Add cacheFile option to compileProtoDefSync - #175

Open
u9g wants to merge 1 commit into
ProtoDef-io:masterfrom
u9g:compile-cache
Open

Add cacheFile option to compileProtoDefSync#175
u9g wants to merge 1 commit into
ProtoDef-io:masterfrom
u9g:compile-cache

Conversation

@u9g

@u9g u9g commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Problem

compileProtoDefSync() evals the generated read/write/sizeOf code, so every consumer process pays full code generation and a full V8 parse of the generated source on startup — eval can never hit V8's on-disk compile cache.

Change

compileProtoDefSync({ cacheFile }):

  • first run: generate as usual, write the generated module to cacheFile (temp file + rename, so a concurrent process never requires a half-written file), then load it
  • later runs: require the file and skip generation
  • the load path calls module.enableCompileCache() (Node 22.8+; wrapped in try/catch so older Node degrades to a plain require, and NODE_DISABLE_COMPILE_CACHE=1 opts out) — this is where most of the win is: V8 caches the parsed protocol code across processes, which the eval path structurally cannot
  • any read or write failure falls back silently to the current in-process compile

Measured with node-minecraft-protocol on an idle M-series Mac, full Minecraft Java 26.1 client protocol setup (4 states × both directions): ~340ms → ~170ms per process once caches are warm.

The cached module exports (native, PartialReadError) => ctx factories, mirroring the locals the existing eval closes over — no globals. Same idea as bedrock-protocol's pregenerated data/<version>/{read,write,size}.js files (which require global.PartialReadError today), offered as a first-class API it could migrate to.

Invalidation is deliberately the caller's job: the caller knows what determines the generated code (protocol JSON, custom types, package versions) and encodes it in the file path. node-minecraft-protocol is the intended first consumer — PR to follow once this lands.

Verification

npm test: 500 passing (3 new: cache write, cache load with byte-identical output, unwritable-path fallback), standard clean.

compileProtoDefSync evals the generated code, so every consumer process
regenerates and reparses the full protocol on startup, and eval is
invisible to V8's on-disk compile cache.

With { cacheFile } the compiler saves the generated module to that path
on first compile and loads it back with require on later runs. The
require path calls module.enableCompileCache() (Node 22.8+, harmless
no-op earlier), so V8 caches the parsed protocol across processes —
that, not skipping generation, is where most of the time goes: a full
Minecraft Java 26.1 client (four states, both directions via
node-minecraft-protocol) drops from ~340ms to ~170ms of protocol setup
per process on an idle M-series Mac.

The cached module exports (native, PartialReadError) => factories,
mirroring the locals the eval closes over, so no globals are involved
(bedrock-protocol's pregenerated data files need global.PartialReadError
today and could migrate to this). Invalidation stays with the caller,
who knows what determines the generated code; any load or write failure
falls back to the normal in-process compile.
@u9g

u9g commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

Should we add a default file path? i didn’t in this pr but if we did then everyone who uses protodef gets this caching for free

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