From 396e25283531b1728d1ff4517cf362bca0fd34c6 Mon Sep 17 00:00:00 2001 From: U9G Date: Sun, 30 Aug 2026 17:44:51 -0400 Subject: [PATCH] Cache compiled protocols on disk across processes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every process recompiles each protocol state from minecraft-data JSON: loading protocol.json, walking it in addProtocol, generating code and evaling it — and the eval'd result can never hit V8's on-disk compile cache. For a full 26.1 client (four states, both directions) that is ~340ms of setup per process on an idle M-series Mac, repeated by every bot, test child and CLI run. Pass a cacheFile to protodef's compileProtoDefSync (new in protodef) keyed by nmp/protodef/minecraft-data versions plus the protocol key and customPackets, so a stale file is unreachable and never needs explicit invalidation. On a hit, loading the cached module also skips mcData.protocol and addProtocol entirely, and protodef enables the V8 compile cache for the require, cutting protocol setup to ~170ms. Default cache dir is os.tmpdir()/node-minecraft-protocol-cache; NMP_PROTOCOL_CACHE_DIR overrides it, =0 disables. The nbt types are registered before the cache attempt (their natives are needed to load a cached protocol) and again after addProtocol on a miss, preserving the type precedence of the previous ordering — verified by comparing all compiled type functions against the old code modulo generated variable names. --- src/transforms/serializer.js | 38 ++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/src/transforms/serializer.js b/src/transforms/serializer.js index 76f7ef27..ca22805d 100644 --- a/src/transforms/serializer.js +++ b/src/transforms/serializer.js @@ -11,6 +11,23 @@ const merge = require('lodash.merge') const minecraftData = require('minecraft-data') const protocols = {} +// The generated code is fully determined by these versions plus the protocol +// key and customPackets, so together they make a stale cache file unreachable. +function protocolCacheFile (state, direction, version, customPackets) { + const cacheDir = process.env.NMP_PROTOCOL_CACHE_DIR ?? + require('path').join(require('os').tmpdir(), 'node-minecraft-protocol-cache') + if (cacheDir === '0') return null + const inputs = [ + require('../../package.json').version, + require('protodef/package.json').version, + require('minecraft-data/package.json').version, + JSON.stringify(customPackets ?? {}) + ].join(';') + const hash = require('crypto').createHash('sha1').update(inputs).digest('hex').slice(0, 12) + const name = `${version}-${state}-${direction}-${hash}.js`.replace(/[^a-zA-Z0-9.-]/g, '_') + return require('path').join(cacheDir, name) +} + function createProtocol (state, direction, version, customPackets, compiled = true) { const key = `${state};${direction};${version}${compiled ? ';c' : ''}` if (protocols[key]) { return protocols[key] } @@ -24,18 +41,31 @@ function createProtocol (state, direction, version, customPackets, compiled = tr throw new Error(`Unsupported protocol version '${versionInfo.version}' (attempted to use '${mcData.version.version}' data); try updating your packages with 'npm update'`) } - const mergedProtocol = merge(mcData.protocol, customPackets?.[mcData.version.majorVersion] ?? {}) - if (compiled) { const compiler = new ProtoDefCompiler() compiler.addTypes(require('../datatypes/compiler-minecraft')) - compiler.addProtocol(mergedProtocol, [state, direction]) nbt.addTypesToCompiler('big', compiler) - const proto = compiler.compileProtoDefSync() + const cacheFile = protocolCacheFile(state, direction, version, customPackets) + let proto + if (cacheFile) { + // A hit also skips loading mcData.protocol and walking it in + // addProtocol, which cost as much as the compile itself. + try { proto = compiler.loadCompiledProtoDefSync(cacheFile) } catch {} + } + if (!proto) { + const mergedProtocol = merge(mcData.protocol, customPackets?.[mcData.version.majorVersion] ?? {}) + compiler.addProtocol(mergedProtocol, [state, direction]) + // Registered a second time: the nbt schemas must override the types the + // protocol declares as native (the pre-cache call above only provides + // the natives needed to load a cached protocol). + nbt.addTypesToCompiler('big', compiler) + proto = compiler.compileProtoDefSync(cacheFile ? { cacheFile } : {}) + } protocols[key] = proto return proto } + const mergedProtocol = merge(mcData.protocol, customPackets?.[mcData.version.majorVersion] ?? {}) const proto = new ProtoDef(false) proto.addTypes(minecraft) proto.addProtocol(mergedProtocol, [state, direction])