Skip to content

Commit 9ff27fd

Browse files
authored
sea: support code cache for ESM entrypoint in SEA
The initial support for ESM entrypoint in SEA didn't support code cache. This patch implements that by following a path similar to how code cache in CJS SEA entrypoint is supported: at build time we generate the code cache from C++ and put it into the sea blob, and at runtime we consume it via a special case in compilation routines - for CJS this was CompileFunctionForCJSLoader, in the case of SourceTextModule, it's in Module::New. PR-URL: #62158 Refs: #61813 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Anna Henningsen <anna@addaleax.net>
1 parent c96c3da commit 9ff27fd

File tree

6 files changed

+159
-45
lines changed

6 files changed

+159
-45
lines changed

doc/api/single-executable-applications.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -395,8 +395,7 @@ The accepted values are:
395395

396396
If the `mainFormat` field is not specified, it defaults to `"commonjs"`.
397397

398-
Currently, `"mainFormat": "module"` cannot be used together with `"useSnapshot"`
399-
or `"useCodeCache"`.
398+
Currently, `"mainFormat": "module"` cannot be used together with `"useSnapshot"`.
400399

401400
### Module loading in the injected main script
402401

src/module_wrap.cc

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
#include "module_wrap.h"
22

3+
#include "debug_utils-inl.h"
34
#include "env.h"
45
#include "memory_tracker-inl.h"
56
#include "node_contextify.h"
67
#include "node_errors.h"
78
#include "node_external_reference.h"
89
#include "node_internals.h"
910
#include "node_process-inl.h"
11+
#include "node_sea.h"
1012
#include "node_url.h"
1113
#include "node_watchdog.h"
1214
#include "util-inl.h"
@@ -365,6 +367,20 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
365367
new ScriptCompiler::CachedData(data + cached_data_buf->ByteOffset(),
366368
cached_data_buf->ByteLength());
367369
}
370+
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
371+
// For embedder ESM in a SEA, use the bundled code cache if available.
372+
if (id_symbol == realm->isolate_data()->embedder_module_hdo() &&
373+
sea::IsSingleExecutable()) {
374+
sea::SeaResource sea = sea::FindSingleExecutableResource();
375+
if (sea.use_code_cache()) {
376+
std::string_view data = sea.code_cache.value();
377+
user_cached_data = new ScriptCompiler::CachedData(
378+
reinterpret_cast<const uint8_t*>(data.data()),
379+
static_cast<int>(data.size()),
380+
ScriptCompiler::CachedData::BufferNotOwned);
381+
}
382+
}
383+
#endif // !DISABLE_SINGLE_EXECUTABLE_APPLICATION
368384
Local<String> source_text = args[2].As<String>();
369385

370386
bool cache_rejected = false;
@@ -389,12 +405,26 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
389405
return;
390406
}
391407

392-
if (user_cached_data.has_value() && user_cached_data.value() != nullptr &&
393-
cache_rejected) {
394-
THROW_ERR_VM_MODULE_CACHED_DATA_REJECTED(
395-
realm, "cachedData buffer was rejected");
396-
try_catch.ReThrow();
397-
return;
408+
if (user_cached_data.has_value() && user_cached_data.value() != nullptr) {
409+
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
410+
if (id_symbol == realm->isolate_data()->embedder_module_hdo() &&
411+
sea::IsSingleExecutable()) {
412+
if (cache_rejected) {
413+
per_process::Debug(DebugCategory::SEA,
414+
"SEA module code cache rejected\n");
415+
ProcessEmitWarningSync(realm->env(), "Code cache data rejected.");
416+
} else {
417+
per_process::Debug(DebugCategory::SEA,
418+
"SEA module code cache accepted\n");
419+
}
420+
} else // NOLINT(readability/braces)
421+
#endif // !DISABLE_SINGLE_EXECUTABLE_APPLICATION
422+
if (cache_rejected) {
423+
THROW_ERR_VM_MODULE_CACHED_DATA_REJECTED(
424+
realm, "cachedData buffer was rejected");
425+
try_catch.ReThrow();
426+
return;
427+
}
398428
}
399429

400430
if (that->Set(context,

src/node_sea.cc

Lines changed: 61 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,21 @@ using v8::Array;
2424
using v8::ArrayBuffer;
2525
using v8::BackingStore;
2626
using v8::Context;
27+
using v8::Data;
2728
using v8::Function;
2829
using v8::FunctionCallbackInfo;
2930
using v8::HandleScope;
3031
using v8::Isolate;
3132
using v8::Local;
3233
using v8::LocalVector;
3334
using v8::MaybeLocal;
35+
using v8::Module;
3436
using v8::NewStringType;
3537
using v8::Object;
3638
using v8::ScriptCompiler;
3739
using v8::ScriptOrigin;
3840
using v8::String;
41+
using v8::UnboundModuleScript;
3942
using v8::Value;
4043

4144
namespace node {
@@ -542,7 +545,7 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
542545
"\"useCodeCache\" is redundant when \"useSnapshot\" is true\n");
543546
}
544547

545-
// TODO(joyeecheung): support ESM with useSnapshot and useCodeCache.
548+
// TODO(joyeecheung): support ESM with useSnapshot.
546549
if (result.main_format == ModuleFormat::kModule &&
547550
static_cast<bool>(result.flags & SeaFlags::kUseSnapshot)) {
548551
FPrintF(stderr,
@@ -551,14 +554,6 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
551554
return std::nullopt;
552555
}
553556

554-
if (result.main_format == ModuleFormat::kModule &&
555-
static_cast<bool>(result.flags & SeaFlags::kUseCodeCache)) {
556-
FPrintF(stderr,
557-
"\"mainFormat\": \"module\" is not supported when "
558-
"\"useCodeCache\" is true\n");
559-
return std::nullopt;
560-
}
561-
562557
if (result.main_path.empty()) {
563558
FPrintF(stderr,
564559
"\"main\" field of %s is not a non-empty string\n",
@@ -616,7 +611,8 @@ ExitCode GenerateSnapshotForSEA(const SeaConfig& config,
616611
}
617612

618613
std::optional<std::string> GenerateCodeCache(std::string_view main_path,
619-
std::string_view main_script) {
614+
std::string_view main_script,
615+
ModuleFormat format) {
620616
RAIIIsolate raii_isolate(SnapshotBuilder::GetEmbeddedSnapshotData());
621617
Isolate* isolate = raii_isolate.get();
622618

@@ -647,34 +643,62 @@ std::optional<std::string> GenerateCodeCache(std::string_view main_path,
647643
return std::nullopt;
648644
}
649645

650-
LocalVector<String> parameters(
651-
isolate,
652-
{
653-
FIXED_ONE_BYTE_STRING(isolate, "exports"),
654-
FIXED_ONE_BYTE_STRING(isolate, "require"),
655-
FIXED_ONE_BYTE_STRING(isolate, "module"),
656-
FIXED_ONE_BYTE_STRING(isolate, "__filename"),
657-
FIXED_ONE_BYTE_STRING(isolate, "__dirname"),
658-
});
659-
ScriptOrigin script_origin(filename, 0, 0, true);
660-
ScriptCompiler::Source script_source(content, script_origin);
661-
MaybeLocal<Function> maybe_fn =
662-
ScriptCompiler::CompileFunction(context,
663-
&script_source,
664-
parameters.size(),
665-
parameters.data(),
666-
0,
667-
nullptr);
668-
Local<Function> fn;
669-
if (!maybe_fn.ToLocal(&fn)) {
670-
return std::nullopt;
646+
std::unique_ptr<ScriptCompiler::CachedData> cache;
647+
648+
if (format == ModuleFormat::kModule) {
649+
// Using empty host defined options is fine as it is not part of the cache
650+
// key and will be reset after deserialization.
651+
ScriptOrigin origin(filename,
652+
0, // line offset
653+
0, // column offset
654+
true, // is cross origin
655+
-1, // script id
656+
Local<Value>(), // source map URL
657+
false, // is opaque
658+
false, // is WASM
659+
true, // is ES Module
660+
Local<Data>()); // host defined options
661+
ScriptCompiler::Source source(content, origin);
662+
Local<Module> module;
663+
if (!ScriptCompiler::CompileModule(isolate, &source).ToLocal(&module)) {
664+
return std::nullopt;
665+
}
666+
Local<UnboundModuleScript> unbound = module->GetUnboundModuleScript();
667+
cache.reset(ScriptCompiler::CreateCodeCache(unbound));
668+
} else {
669+
// TODO(RaisinTen): Using the V8 code cache prevents us from using
670+
// `import()` in the SEA code. Support it. Refs:
671+
// https://github.com/nodejs/node/pull/48191#discussion_r1213271430
672+
// TODO(joyeecheung): this likely has been fixed by
673+
// https://chromium-review.googlesource.com/c/v8/v8/+/5401780 - add a test
674+
// and update docs.
675+
LocalVector<String> parameters(
676+
isolate,
677+
{
678+
FIXED_ONE_BYTE_STRING(isolate, "exports"),
679+
FIXED_ONE_BYTE_STRING(isolate, "require"),
680+
FIXED_ONE_BYTE_STRING(isolate, "module"),
681+
FIXED_ONE_BYTE_STRING(isolate, "__filename"),
682+
FIXED_ONE_BYTE_STRING(isolate, "__dirname"),
683+
});
684+
ScriptOrigin script_origin(filename, 0, 0, true);
685+
ScriptCompiler::Source script_source(content, script_origin);
686+
Local<Function> fn;
687+
if (!ScriptCompiler::CompileFunction(context,
688+
&script_source,
689+
parameters.size(),
690+
parameters.data(),
691+
0,
692+
nullptr)
693+
.ToLocal(&fn)) {
694+
return std::nullopt;
695+
}
696+
cache.reset(ScriptCompiler::CreateCodeCacheForFunction(fn));
671697
}
672698

673-
// TODO(RaisinTen): Using the V8 code cache prevents us from using `import()`
674-
// in the SEA code. Support it.
675-
// Refs: https://github.com/nodejs/node/pull/48191#discussion_r1213271430
676-
std::unique_ptr<ScriptCompiler::CachedData> cache{
677-
ScriptCompiler::CreateCodeCacheForFunction(fn)};
699+
if (!cache) {
700+
return std::nullopt;
701+
}
678702
std::string code_cache(cache->data, cache->data + cache->length);
679703
return code_cache;
680704
}
@@ -728,7 +752,7 @@ ExitCode GenerateSingleExecutableBlob(
728752
std::string code_cache;
729753
if (static_cast<bool>(config.flags & SeaFlags::kUseCodeCache)) {
730754
std::optional<std::string> optional_code_cache =
731-
GenerateCodeCache(config.main_path, main_script);
755+
GenerateCodeCache(config.main_path, main_script, config.main_format);
732756
if (!optional_code_cache.has_value()) {
733757
FPrintF(stderr, "Cannot generate V8 code cache\n");
734758
return ExitCode::kGenericUserError;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"main": "sea.mjs",
3+
"output": "sea",
4+
"mainFormat": "module",
5+
"useCodeCache": true,
6+
"disableExperimentalSEAWarning": true
7+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import assert from 'node:assert';
2+
import { createRequire } from 'node:module';
3+
import { pathToFileURL } from 'node:url';
4+
import { dirname } from 'node:path';
5+
6+
// Test createRequire with process.execPath.
7+
const assert2 = createRequire(process.execPath)('node:assert');
8+
assert.strictEqual(assert2.strict, assert.strict);
9+
10+
// Test import.meta properties.
11+
assert.strictEqual(import.meta.url, pathToFileURL(process.execPath).href);
12+
assert.strictEqual(import.meta.filename, process.execPath);
13+
assert.strictEqual(import.meta.dirname, dirname(process.execPath));
14+
assert.strictEqual(import.meta.main, true);
15+
16+
// Test import() with a built-in module.
17+
const { strict } = await import('node:assert');
18+
assert.strictEqual(strict, assert.strict);
19+
20+
console.log('ESM SEA with code cache executed successfully');
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
'use strict';
2+
3+
// This tests the creation of a single executable application with an ESM
4+
// entry point using "mainFormat": "module" and "useCodeCache": true.
5+
6+
require('../common');
7+
8+
const {
9+
buildSEA,
10+
skipIfBuildSEAIsNotSupported,
11+
} = require('../common/sea');
12+
13+
skipIfBuildSEAIsNotSupported();
14+
15+
const tmpdir = require('../common/tmpdir');
16+
const fixtures = require('../common/fixtures');
17+
const { spawnSyncAndExitWithoutError } = require('../common/child_process');
18+
19+
tmpdir.refresh();
20+
21+
const outputFile = buildSEA(fixtures.path('sea', 'esm-code-cache'));
22+
23+
spawnSyncAndExitWithoutError(
24+
outputFile,
25+
{
26+
env: {
27+
NODE_DEBUG_NATIVE: 'SEA',
28+
...process.env,
29+
},
30+
},
31+
{
32+
stdout: /ESM SEA with code cache executed successfully/,
33+
stderr: /SEA module code cache accepted/,
34+
});

0 commit comments

Comments
 (0)