GeezerVison is showing github.com — sea: support code cache for ESM entrypoint in SEA · nodejs/node@966b700
File treeExpand file treeCollapse file tree Open diff view settings fixtures/sea/esm-code-cache
Expand file treeCollapse file tree Open diff view settings Collapse file 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 number | Diff line number | Diff line change |
|---|
@@ -395,8 +395,7 @@ The accepted values are: | 395 | 395 |
| 396 | 396 | If the `mainFormat` field is not specified, it defaults to `"commonjs"`. | 397 | 397 |
| 398 | | -Currently, `"mainFormat": "module"` cannot be used together with `"useSnapshot"` | 399 | | -or `"useCodeCache"`. | | 398 | +Currently, `"mainFormat": "module"` cannot be used together with `"useSnapshot"`. | 400 | 399 |
| 401 | 400 | ### Module loading in the injected main script | 402 | 401 |
| |
Collapse file Copy file name to clipboardExpand all lines: src/module_wrap.cc +36-6Lines changed: 36 additions & 6 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 1 | 1 | #include "module_wrap.h" | 2 | 2 |
| | 3 | +#include "debug_utils-inl.h" | 3 | 4 | #include "env.h" | 4 | 5 | #include "memory_tracker-inl.h" | 5 | 6 | #include "node_contextify.h" | 6 | 7 | #include "node_errors.h" | 7 | 8 | #include "node_external_reference.h" | 8 | 9 | #include "node_internals.h" | 9 | 10 | #include "node_process-inl.h" | | 11 | +#include "node_sea.h" | 10 | 12 | #include "node_url.h" | 11 | 13 | #include "node_watchdog.h" | 12 | 14 | #include "util-inl.h" | @@ -365,6 +367,20 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) { | 365 | 367 | new ScriptCompiler::CachedData(data + cached_data_buf->ByteOffset(), | 366 | 368 | cached_data_buf->ByteLength()); | 367 | 369 | } | | 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 | 368 | 384 | Local<String> source_text = args[2].As<String>(); | 369 | 385 |
| 370 | 386 | bool cache_rejected = false; | @@ -389,12 +405,26 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) { | 389 | 405 | return; | 390 | 406 | } | 391 | 407 |
| 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 | + } | 398 | 428 | } | 399 | 429 |
| 400 | 430 | if (that->Set(context, | |
Collapse file Copy file name to clipboardExpand all lines: src/node_sea.cc +61-37Lines changed: 61 additions & 37 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -24,18 +24,21 @@ using v8::Array; | 24 | 24 | using v8::ArrayBuffer; | 25 | 25 | using v8::BackingStore; | 26 | 26 | using v8::Context; | | 27 | +using v8::Data; | 27 | 28 | using v8::Function; | 28 | 29 | using v8::FunctionCallbackInfo; | 29 | 30 | using v8::HandleScope; | 30 | 31 | using v8::Isolate; | 31 | 32 | using v8::Local; | 32 | 33 | using v8::LocalVector; | 33 | 34 | using v8::MaybeLocal; | | 35 | +using v8::Module; | 34 | 36 | using v8::NewStringType; | 35 | 37 | using v8::Object; | 36 | 38 | using v8::ScriptCompiler; | 37 | 39 | using v8::ScriptOrigin; | 38 | 40 | using v8::String; | | 41 | +using v8::UnboundModuleScript; | 39 | 42 | using v8::Value; | 40 | 43 |
| 41 | 44 | namespace node { | @@ -542,7 +545,7 @@ std::optional<SeaConfig> ParseSingleExecutableConfig( | 542 | 545 | "\"useCodeCache\" is redundant when \"useSnapshot\" is true\n"); | 543 | 546 | } | 544 | 547 |
| 545 | | - // TODO(joyeecheung): support ESM with useSnapshot and useCodeCache. | | 548 | + // TODO(joyeecheung): support ESM with useSnapshot. | 546 | 549 | if (result.main_format == ModuleFormat::kModule && | 547 | 550 | static_cast<bool>(result.flags & SeaFlags::kUseSnapshot)) { | 548 | 551 | FPrintF(stderr, | @@ -551,14 +554,6 @@ std::optional<SeaConfig> ParseSingleExecutableConfig( | 551 | 554 | return std::nullopt; | 552 | 555 | } | 553 | 556 |
| 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 | | - | 562 | 557 | if (result.main_path.empty()) { | 563 | 558 | FPrintF(stderr, | 564 | 559 | "\"main\" field of %s is not a non-empty string\n", | @@ -616,7 +611,8 @@ ExitCode GenerateSnapshotForSEA(const SeaConfig& config, | 616 | 611 | } | 617 | 612 |
| 618 | 613 | 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) { | 620 | 616 | RAIIIsolate raii_isolate(SnapshotBuilder::GetEmbeddedSnapshotData()); | 621 | 617 | Isolate* isolate = raii_isolate.get(); | 622 | 618 |
| @@ -647,34 +643,62 @@ std::optional<std::string> GenerateCodeCache(std::string_view main_path, | 647 | 643 | return std::nullopt; | 648 | 644 | } | 649 | 645 |
| 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)); | 671 | 697 | } | 672 | 698 |
| 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 | + } | 678 | 702 | std::string code_cache(cache->data, cache->data + cache->length); | 679 | 703 | return code_cache; | 680 | 704 | } | @@ -728,7 +752,7 @@ ExitCode GenerateSingleExecutableBlob( | 728 | 752 | std::string code_cache; | 729 | 753 | if (static_cast<bool>(config.flags & SeaFlags::kUseCodeCache)) { | 730 | 754 | std::optional<std::string> optional_code_cache = | 731 | | - GenerateCodeCache(config.main_path, main_script); | | 755 | + GenerateCodeCache(config.main_path, main_script, config.main_format); | 732 | 756 | if (!optional_code_cache.has_value()) { | 733 | 757 | FPrintF(stderr, "Cannot generate V8 code cache\n"); | 734 | 758 | return ExitCode::kGenericUserError; | |
Collapse file Copy file name to clipboard +7Lines changed: 7 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +{ | | 2 | + "main": "sea.mjs", | | 3 | + "output": "sea", | | 4 | + "mainFormat": "module", | | 5 | + "useCodeCache": true, | | 6 | + "disableExperimentalSEAWarning": true | | 7 | +} |
Collapse file Copy file name to clipboard +20Lines changed: 20 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 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 Copy file name to clipboard +34Lines changed: 34 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 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 | + }); |
|