Skip to content

Commit 728cf8c

Browse files
committed
module: cache compiled WebAssembly modules in the compile cache
Extends the module compile cache to WebAssembly modules loaded through the ES module integration. A new kWasm entry type is keyed on the module URL with the wire bytes as the hashed source, and stores the serialized CompiledWasmModule. On load, cached code is deserialized through v8::WasmModuleCompilation with the same compile options the translator uses (js-string builtins and imported string constants), falling back to compilation when V8 rejects it. As for JavaScript, the cache entry is generated right after compilation. V8 only serializes optimized tier code, so with the default lazy baseline compilation there is nothing to cache and no entry is written; a complete cache requires --no-liftoff --no-wasm-lazy-compilation, which is documented as the way to use the compile cache for WebAssembly.
1 parent 29540f2 commit 728cf8c

6 files changed

Lines changed: 362 additions & 18 deletions

File tree

‎doc/api/module.md‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -371,8 +371,9 @@ changes:
371371
372372
The module compile cache can be enabled either using the [`module.enableCompileCache()`][]
373373
method or the [`NODE_COMPILE_CACHE=dir`][] environment variable. After it is enabled,
374-
whenever Node.js compiles a CommonJS, an ECMAScript Module, or a TypeScript module, it will
375-
use on-disk [V8 code cache][] persisted in the specified directory to speed up the compilation.
374+
whenever Node.js compiles a CommonJS, an ECMAScript Module, a TypeScript module, or a
375+
WebAssembly module, it will use on-disk [V8 code cache][] persisted in the specified
376+
directory to speed up the compilation.
376377
This may slow down the first load of a module graph, but subsequent loads of the same module
377378
graph may get a significant speedup if the contents of the modules do not change.
378379
@@ -433,6 +434,25 @@ There are two ways to enable the portable mode:
433434
434435
2. Setting the environment variable: [`NODE_COMPILE_CACHE_PORTABLE=1`][]
435436
437+
### WebAssembly modules in the compile cache
438+
439+
For WebAssembly modules loaded through the ES module integration, the compile cache stores
440+
the machine code that V8 has generated for the module when it is compiled. V8 only
441+
serializes code produced by its optimizing compiler. By default WebAssembly functions are
442+
compiled lazily with a baseline compiler and only optimized once they run hot, so at the time
443+
a module is compiled there is typically no optimized code and no cache entry is written.
444+
445+
To cache WebAssembly modules, compile all functions with the optimizing compiler when the
446+
module is created:
447+
448+
```console
449+
$ NODE_COMPILE_CACHE=/path/to/cache node --no-liftoff --no-wasm-lazy-compilation app.js
450+
```
451+
452+
This makes the first, uncached load of a module slower in exchange for later loads skipping
453+
compilation entirely. Since V8 flags are part of the cache key, the same flags must be
454+
used for all processes sharing the cache.
455+
436456
### Read-only compile cache
437457
438458
A cache that was generated ahead of time, for example at build time to be

‎lib/internal/modules/esm/translators.js‎

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const {
1313
StringPrototypeReplaceAll,
1414
StringPrototypeSlice,
1515
StringPrototypeStartsWith,
16+
Uint8Array,
1617
globalThis,
1718
} = primordials;
1819

@@ -47,6 +48,7 @@ const {
4748
populateCJSExportsFromESM,
4849
} = require('internal/modules/cjs/loader');
4950
const { fileURLToPath, pathToFileURL, URL } = require('internal/url');
51+
const { isAnyArrayBuffer } = require('internal/util/types');
5052
let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
5153
debug = fn;
5254
});
@@ -62,6 +64,10 @@ const { ModuleWrap, kEvaluationPhase } = moduleWrap;
6264
const { getSourceSync } = require('internal/modules/esm/load');
6365

6466
const { parse: cjsParse } = internalBinding('cjs_lexer');
67+
const {
68+
getWasmCompileCacheEntry,
69+
saveWasmCompileCacheEntry,
70+
} = internalBinding('modules');
6571

6672
const translators = new SafeMap();
6773
exports.translators = translators;
@@ -554,6 +560,12 @@ translators.set('json', function jsonStrategy(url, translateContext) {
554560
* >} [[Instance]] slot proxy for WebAssembly Module Record
555561
*/
556562
const wasmInstances = new SafeWeakMap();
563+
// Compile options per https://webassembly.github.io/esm-integration/js-api/index.html#parse-a-webassembly-module.
564+
const wasmCompileOptions = {
565+
__proto__: null,
566+
builtins: ['js-string'],
567+
importedStringConstants: 'wasm:js/string-constants',
568+
};
557569
translators.set('wasm', function(url, translateContext) {
558570
const { source } = translateContext;
559571
// WebAssembly global is not available during snapshot building, so we need to get it lazily.
@@ -562,15 +574,21 @@ translators.set('wasm', function(url, translateContext) {
562574

563575
debug(`Translating WASMModule ${url}`, translateContext);
564576

565-
let compiled;
566-
try {
567-
compiled = new WebAssembly.Module(source, {
568-
builtins: ['js-string'],
569-
importedStringConstants: 'wasm:js/string-constants',
570-
});
571-
} catch (err) {
572-
err.message = errPath(url) + ': ' + err.message;
573-
throw err;
577+
// When the compile cache is enabled, the cache entry holds the module
578+
// deserialized from cached optimized code if V8 accepted it.
579+
const cacheEntry = getWasmCompileCacheEntry(
580+
isAnyArrayBuffer(source) ? new Uint8Array(source) : source, url, wasmCompileOptions);
581+
let compiled = cacheEntry?.module;
582+
if (compiled === undefined) {
583+
try {
584+
compiled = new WebAssembly.Module(source, wasmCompileOptions);
585+
} catch (err) {
586+
err.message = errPath(url) + ': ' + err.message;
587+
throw err;
588+
}
589+
if (cacheEntry !== undefined) {
590+
saveWasmCompileCacheEntry(cacheEntry.external, compiled);
591+
}
574592
}
575593

576594
const importsList = new SafeSet();

‎src/compile_cache.cc‎

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ using v8::Local;
2323
using v8::Module;
2424
using v8::ScriptCompiler;
2525
using v8::String;
26+
using v8::WasmModuleObject;
2627

2728
namespace {
2829
std::string Uint32ToHex(uint32_t crc) {
@@ -103,6 +104,8 @@ const char* CompileCacheEntry::type_name() const {
103104
return "ESM";
104105
case CachedCodeType::kStrippedTypeScript:
105106
return "StrippedTypeScript";
107+
case CachedCodeType::kWasm:
108+
return "Wasm";
106109
default:
107110
UNREACHABLE();
108111
}
@@ -249,6 +252,20 @@ static std::string GetRelativePath(std::string_view path,
249252
CompileCacheEntry* CompileCacheHandler::GetOrInsert(Local<String> code,
250253
Local<String> filename,
251254
CachedCodeType type) {
255+
// TODO(joyeecheung): don't encode this again into UTF8. If we read the
256+
// UTF8 content on disk as raw buffer (from the JS layer, while watching out
257+
// for monkey patching), we can just hash it directly.
258+
Utf8Value code_utf8(isolate_, code);
259+
return GetOrInsert(reinterpret_cast<const uint8_t*>(code_utf8.out()),
260+
code_utf8.length(),
261+
filename,
262+
type);
263+
}
264+
265+
CompileCacheEntry* CompileCacheHandler::GetOrInsert(const uint8_t* code,
266+
size_t code_size,
267+
Local<String> filename,
268+
CachedCodeType type) {
252269
DCHECK(!compile_cache_dir_.empty());
253270

254271
Environment* env = Environment::GetCurrent(isolate_->GetCurrentContext());
@@ -275,11 +292,7 @@ CompileCacheEntry* CompileCacheHandler::GetOrInsert(Local<String> code,
275292
}
276293
uint32_t key = GetCacheKey(file_path, type);
277294

278-
// TODO(joyeecheung): don't encode this again into UTF8. If we read the
279-
// UTF8 content on disk as raw buffer (from the JS layer, while watching out
280-
// for monkey patching), we can just hash it directly.
281-
Utf8Value code_utf8(isolate_, code);
282-
uint32_t code_hash = GetHash(code_utf8.out(), code_utf8.length());
295+
uint32_t code_hash = GetHash(reinterpret_cast<const char*>(code), code_size);
283296
auto loaded = compiler_cache_store_.find(key);
284297

285298
// TODO(joyeecheung): let V8's in-isolate compilation cache take precedence.
@@ -295,7 +308,7 @@ CompileCacheEntry* CompileCacheHandler::GetOrInsert(Local<String> code,
295308
auto* result = emplaced.first->second.get();
296309

297310
result->code_hash = code_hash;
298-
result->code_size = code_utf8.length();
311+
result->code_size = code_size;
299312
result->cache_key = key;
300313
result->cache_filename =
301314
compile_cache_dir_ + kPathSeparator + Uint32ToHex(key);
@@ -318,6 +331,18 @@ ScriptCompiler::CachedData* SerializeCodeCache(Local<Module> mod) {
318331
return ScriptCompiler::CreateCodeCache(mod->GetUnboundModuleScript());
319332
}
320333

334+
// V8 only serializes code compiled by the optimizing tier, and produces
335+
// nothing if there is none, e.g. for a module that has only been compiled
336+
// with the baseline tier so far.
337+
ScriptCompiler::CachedData* SerializeCodeCache(Local<WasmModuleObject> mod) {
338+
v8::OwnedBuffer code = mod->GetCompiledModule().Serialize();
339+
if (code.size == 0) return nullptr;
340+
return new ScriptCompiler::CachedData(
341+
code.buffer.release(),
342+
static_cast<int>(code.size),
343+
ScriptCompiler::CachedData::BufferOwned);
344+
}
345+
321346
template <typename T>
322347
void CompileCacheHandler::MaybeSaveImpl(CompileCacheEntry* entry,
323348
Local<T> func_or_mod,
@@ -341,6 +366,12 @@ void CompileCacheHandler::MaybeSaveImpl(CompileCacheEntry* entry,
341366
entry->cache == nullptr ? "initializing" : "refreshing");
342367

343368
ScriptCompiler::CachedData* data = SerializeCodeCache(func_or_mod);
369+
if (data == nullptr) {
370+
Debug("[compile cache] nothing to serialize for %s %s\n",
371+
entry->type_name(),
372+
entry->source_filename);
373+
return;
374+
}
344375
DCHECK_EQ(data->buffer_policy, ScriptCompiler::CachedData::BufferOwned);
345376
entry->refreshed = true;
346377
entry->cache.reset(data);
@@ -359,6 +390,13 @@ void CompileCacheHandler::MaybeSave(CompileCacheEntry* entry,
359390
MaybeSaveImpl(entry, func, rejected);
360391
}
361392

393+
void CompileCacheHandler::MaybeSave(CompileCacheEntry* entry,
394+
Local<WasmModuleObject> mod,
395+
bool rejected) {
396+
DCHECK(entry->type == CachedCodeType::kWasm);
397+
MaybeSaveImpl(entry, mod, rejected);
398+
}
399+
362400
void CompileCacheHandler::MaybeSave(CompileCacheEntry* entry,
363401
std::string_view transpiled) {
364402
if (read_only_) {

‎src/compile_cache.h‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ class Environment;
1616
#define CACHED_CODE_TYPES(V) \
1717
V(kCommonJS, 0) \
1818
V(kESM, 1) \
19-
V(kStrippedTypeScript, 2)
19+
V(kStrippedTypeScript, 2) \
20+
V(kWasm, 3)
2021

2122
enum class CachedCodeType : uint8_t {
2223
#define V(type, value) type = value,
@@ -89,12 +90,19 @@ class CompileCacheHandler {
8990
CompileCacheEntry* GetOrInsert(v8::Local<v8::String> code,
9091
v8::Local<v8::String> filename,
9192
CachedCodeType type);
93+
CompileCacheEntry* GetOrInsert(const uint8_t* code,
94+
size_t code_size,
95+
v8::Local<v8::String> filename,
96+
CachedCodeType type);
9297
void MaybeSave(CompileCacheEntry* entry,
9398
v8::Local<v8::Function> func,
9499
bool rejected);
95100
void MaybeSave(CompileCacheEntry* entry,
96101
v8::Local<v8::Module> mod,
97102
bool rejected);
103+
void MaybeSave(CompileCacheEntry* entry,
104+
v8::Local<v8::WasmModuleObject> mod,
105+
bool rejected);
98106
void MaybeSave(CompileCacheEntry* entry, std::string_view transpiled);
99107
std::string_view cache_dir() { return compile_cache_dir_; }
100108
bool read_only() const { return read_only_; }

0 commit comments

Comments
 (0)