Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 966b700

Browse filesBrowse files
joyeecheungaduh95
authored andcommitted
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 f0eea0f commit 966b700
Copy full SHA for 966b700

6 files changed

+159-45Lines changed: 159 additions & 45 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎doc/api/single-executable-applications.md‎

Copy file name to clipboardExpand all lines: doc/api/single-executable-applications.md
+1-2Lines changed: 1 addition & 2 deletions
  • Display the source diff
  • Display the rich diff
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

Collapse file

‎src/module_wrap.cc‎

Copy file name to clipboardExpand all lines: src/module_wrap.cc
+36-6Lines 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,
Collapse file

‎src/node_sea.cc‎

Copy file name to clipboardExpand all lines: src/node_sea.cc
+61-37Lines 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;
Collapse file
+7Lines 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+
}
Collapse file
+20Lines 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');
Collapse file
+34Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.