Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions doc/api/module.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,8 +371,9 @@ changes:

The module compile cache can be enabled either using the [`module.enableCompileCache()`][]
method or the [`NODE_COMPILE_CACHE=dir`][] environment variable. After it is enabled,
whenever Node.js compiles a CommonJS, an ECMAScript Module, or a TypeScript module, it will
use on-disk [V8 code cache][] persisted in the specified directory to speed up the compilation.
whenever Node.js compiles a CommonJS, an ECMAScript Module, a TypeScript module, or a
WebAssembly module, it will use on-disk [V8 code cache][] persisted in the specified
directory to speed up the compilation.
This may slow down the first load of a module graph, but subsequent loads of the same module
graph may get a significant speedup if the contents of the modules do not change.

Expand Down Expand Up @@ -433,6 +434,25 @@ There are two ways to enable the portable mode:

2. Setting the environment variable: [`NODE_COMPILE_CACHE_PORTABLE=1`][]

### WebAssembly modules in the compile cache

For WebAssembly modules loaded through the ES module integration, the compile cache stores
the machine code that V8 has generated for the module when it is compiled. V8 only
serializes code produced by its optimizing compiler. By default WebAssembly functions are
compiled lazily with a baseline compiler and only optimized once they run hot, so at the time
a module is compiled there is typically no optimized code and no cache entry is written.

To cache WebAssembly modules, compile all functions with the optimizing compiler when the
module is created:

```console
$ NODE_COMPILE_CACHE=/path/to/cache node --no-liftoff --no-wasm-lazy-compilation app.js
```

This makes the first, uncached load of a module slower in exchange for later loads skipping
compilation entirely. Since V8 flags are part of the cache key, the same flags must be
used for all processes sharing the cache.

### Read-only compile cache

A cache that was generated ahead of time, for example at build time to be
Expand Down
36 changes: 27 additions & 9 deletions lib/internal/modules/esm/translators.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const {
StringPrototypeReplaceAll,
StringPrototypeSlice,
StringPrototypeStartsWith,
Uint8Array,
globalThis,
} = primordials;

Expand Down Expand Up @@ -47,6 +48,7 @@ const {
populateCJSExportsFromESM,
} = require('internal/modules/cjs/loader');
const { fileURLToPath, pathToFileURL, URL } = require('internal/url');
const { isAnyArrayBuffer } = require('internal/util/types');
let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
debug = fn;
});
Expand All @@ -63,6 +65,10 @@ const { ModuleWrap, kEvaluationPhase } = moduleWrap;
const { getSourceSync } = require('internal/modules/esm/load');

const { parse: cjsParse } = internalBinding('cjs_lexer');
const {
getWasmCompileCacheEntry,
saveWasmCompileCacheEntry,
} = internalBinding('modules');

const translators = new SafeMap();
exports.translators = translators;
Expand Down Expand Up @@ -555,6 +561,12 @@ translators.set('json', function jsonStrategy(url, translateContext) {
* >} [[Instance]] slot proxy for WebAssembly Module Record
*/
const wasmInstances = new SafeWeakMap();
// Compile options per https://webassembly.github.io/esm-integration/js-api/index.html#parse-a-webassembly-module.
const wasmCompileOptions = {
__proto__: null,
builtins: ['js-string'],
importedStringConstants: 'wasm:js/string-constants',
};
translators.set('wasm', function(url, translateContext) {
const { source } = translateContext;
// WebAssembly global is not available during snapshot building, so we need to get it lazily.
Expand All @@ -566,15 +578,21 @@ translators.set('wasm', function(url, translateContext) {

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

let compiled;
try {
compiled = new WebAssembly.Module(source, {
builtins: ['js-string'],
importedStringConstants: 'wasm:js/string-constants',
});
} catch (err) {
err.message = errPath(url) + ': ' + err.message;
throw err;
// When the compile cache is enabled, the cache entry holds the module
// deserialized from cached optimized code if V8 accepted it.
const cacheEntry = getWasmCompileCacheEntry(
isAnyArrayBuffer(source) ? new Uint8Array(source) : source, url, wasmCompileOptions);
let compiled = cacheEntry?.module;
if (compiled === undefined) {
try {
compiled = new WebAssembly.Module(source, wasmCompileOptions);
} catch (err) {
err.message = errPath(url) + ': ' + err.message;
throw err;
}
if (cacheEntry !== undefined) {
saveWasmCompileCacheEntry(cacheEntry.external, compiled);
}
}

const importsList = new SafeSet();
Expand Down
50 changes: 44 additions & 6 deletions src/compile_cache.cc
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ using v8::Local;
using v8::Module;
using v8::ScriptCompiler;
using v8::String;
using v8::WasmModuleObject;

namespace {
std::string Uint32ToHex(uint32_t crc) {
Expand Down Expand Up @@ -103,6 +104,8 @@ const char* CompileCacheEntry::type_name() const {
return "ESM";
case CachedCodeType::kStrippedTypeScript:
return "StrippedTypeScript";
case CachedCodeType::kWasm:
return "Wasm";
default:
UNREACHABLE();
}
Expand Down Expand Up @@ -249,6 +252,20 @@ static std::string GetRelativePath(std::string_view path,
CompileCacheEntry* CompileCacheHandler::GetOrInsert(Local<String> code,
Local<String> filename,
CachedCodeType type) {
// TODO(joyeecheung): don't encode this again into UTF8. If we read the
// UTF8 content on disk as raw buffer (from the JS layer, while watching out
// for monkey patching), we can just hash it directly.
Utf8Value code_utf8(isolate_, code);
return GetOrInsert(reinterpret_cast<const uint8_t*>(code_utf8.out()),
code_utf8.length(),
filename,
type);
}

CompileCacheEntry* CompileCacheHandler::GetOrInsert(const uint8_t* code,
size_t code_size,
Local<String> filename,
CachedCodeType type) {
DCHECK(!compile_cache_dir_.empty());

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

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

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

result->code_hash = code_hash;
result->code_size = code_utf8.length();
result->code_size = code_size;
result->cache_key = key;
result->cache_filename =
compile_cache_dir_ + kPathSeparator + Uint32ToHex(key);
Expand All @@ -318,6 +331,18 @@ ScriptCompiler::CachedData* SerializeCodeCache(Local<Module> mod) {
return ScriptCompiler::CreateCodeCache(mod->GetUnboundModuleScript());
}

// V8 only serializes code compiled by the optimizing tier, and produces
// nothing if there is none, e.g. for a module that has only been compiled
// with the baseline tier so far.
ScriptCompiler::CachedData* SerializeCodeCache(Local<WasmModuleObject> mod) {
v8::OwnedBuffer code = mod->GetCompiledModule().Serialize();
if (code.size == 0) return nullptr;
return new ScriptCompiler::CachedData(
code.buffer.release(),
static_cast<int>(code.size),
ScriptCompiler::CachedData::BufferOwned);
}

template <typename T>
void CompileCacheHandler::MaybeSaveImpl(CompileCacheEntry* entry,
Local<T> func_or_mod,
Expand All @@ -341,6 +366,12 @@ void CompileCacheHandler::MaybeSaveImpl(CompileCacheEntry* entry,
entry->cache == nullptr ? "initializing" : "refreshing");

ScriptCompiler::CachedData* data = SerializeCodeCache(func_or_mod);
if (data == nullptr) {
Debug("[compile cache] nothing to serialize for %s %s\n",
entry->type_name(),
entry->source_filename);
return;
}
DCHECK_EQ(data->buffer_policy, ScriptCompiler::CachedData::BufferOwned);
entry->refreshed = true;
entry->cache.reset(data);
Expand All @@ -359,6 +390,13 @@ void CompileCacheHandler::MaybeSave(CompileCacheEntry* entry,
MaybeSaveImpl(entry, func, rejected);
}

void CompileCacheHandler::MaybeSave(CompileCacheEntry* entry,
Local<WasmModuleObject> mod,
bool rejected) {
DCHECK(entry->type == CachedCodeType::kWasm);
MaybeSaveImpl(entry, mod, rejected);
}

void CompileCacheHandler::MaybeSave(CompileCacheEntry* entry,
std::string_view transpiled) {
if (read_only_) {
Expand Down
10 changes: 9 additions & 1 deletion src/compile_cache.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ class Environment;
#define CACHED_CODE_TYPES(V) \
V(kCommonJS, 0) \
V(kESM, 1) \
V(kStrippedTypeScript, 2)
V(kStrippedTypeScript, 2) \
V(kWasm, 3)

enum class CachedCodeType : uint8_t {
#define V(type, value) type = value,
Expand Down Expand Up @@ -89,12 +90,19 @@ class CompileCacheHandler {
CompileCacheEntry* GetOrInsert(v8::Local<v8::String> code,
v8::Local<v8::String> filename,
CachedCodeType type);
CompileCacheEntry* GetOrInsert(const uint8_t* code,
size_t code_size,
v8::Local<v8::String> filename,
CachedCodeType type);
void MaybeSave(CompileCacheEntry* entry,
v8::Local<v8::Function> func,
bool rejected);
void MaybeSave(CompileCacheEntry* entry,
v8::Local<v8::Module> mod,
bool rejected);
void MaybeSave(CompileCacheEntry* entry,
v8::Local<v8::WasmModuleObject> mod,
bool rejected);
void MaybeSave(CompileCacheEntry* entry, std::string_view transpiled);
std::string_view cache_dir() { return compile_cache_dir_; }
bool read_only() const { return read_only_; }
Expand Down
Loading
Loading