From bc009d0c10c3df7a15a3bf57146d977bf1d0a83b Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Wed, 26 Jul 2023 15:40:35 +0530 Subject: [PATCH] sea: add support for V8 bytecode-only caching Refs: https://github.com/nodejs/single-executable/issues/73 Signed-off-by: Darshan Sen PR-URL: https://github.com/nodejs/node/pull/48191 Fixes: https://github.com/nodejs/single-executable/issues/73 Reviewed-By: Yagiz Nizipli Reviewed-By: Stephen Belanger Reviewed-By: Joyee Cheung --- doc/api/single-executable-applications.md | 18 +- lib/internal/modules/cjs/loader.js | 10 +- lib/internal/util/embedding.js | 9 +- src/json_parser.cc | 21 +- src/json_parser.h | 4 +- src/node_contextify.cc | 16 ++ src/node_contextify.h | 6 + src/node_sea.cc | 187 +++++++++++++++++- src/node_sea.h | 5 + src/node_snapshotable.cc | 11 +- src/util.cc | 18 ++ src/util.h | 13 ++ test/fixtures/errors/force_colors.snapshot | 4 +- test/fixtures/sea.js | 6 + ...e-executable-application-use-code-cache.js | 60 ++++++ 15 files changed, 347 insertions(+), 41 deletions(-) create mode 100644 test/sequential/test-single-executable-application-use-code-cache.js diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index 607158643cbc3c..41a45e9dcb1cab 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -10,6 +10,9 @@ changes: - version: REPLACEME pr-url: https://github.com/nodejs/node/pull/46824 description: Added support for "useSnapshot". + - version: REPLACEME + pr-url: https://github.com/nodejs/node/pull/48191 + description: Added support for "useCodeCache". --> > Stability: 1 - Experimental: This feature is being designed and will change. @@ -174,7 +177,8 @@ The configuration currently reads the following top-level fields: "main": "/path/to/bundled/script.js", "output": "/path/to/write/the/generated/blob.blob", "disableExperimentalSEAWarning": true, // Default: false - "useSnapshot": false // Default: false + "useSnapshot": false, // Default: false + "useCodeCache": true // Default: false } ``` @@ -213,6 +217,18 @@ and the main script can use the [`v8.startupSnapshot` API][] to adapt to these constraints. See [documentation about startup snapshot support in Node.js][]. +### V8 code cache support + +When `useCodeCache` is set to `true` in the configuration, during the generation +of the single executable preparation blob, Node.js will compile the `main` +script to generate the V8 code cache. The generated code cache would be part of +the preparation blob and get injected into the final executable. When the single +executable application is launched, instead of compiling the `main` script from +scratch, Node.js would use the code cache to speed up the compilation, then +execute the script, which would improve the startup performance. + +**Note:** `import()` does not work when `useCodeCache` is `true`. + ## Notes ### `require(id)` in the injected module is not file based diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 6e847d0a5c6033..19a7d7e671f5ab 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -1123,7 +1123,7 @@ Module.prototype.require = function(id) { let resolvedArgv; let hasPausedEntry = false; let Script; -function wrapSafe(filename, content, cjsModuleInstance) { +function wrapSafe(filename, content, cjsModuleInstance, codeCache) { if (patched) { const wrapper = Module.wrap(content); if (Script === undefined) { @@ -1158,6 +1158,7 @@ function wrapSafe(filename, content, cjsModuleInstance) { '__dirname', ], { filename, + cachedData: codeCache, importModuleDynamically(specifier, _, importAssertions) { const cascadedLoader = getCascadedLoader(); return cascadedLoader.import(specifier, normalizeReferrerURL(filename), @@ -1165,6 +1166,13 @@ function wrapSafe(filename, content, cjsModuleInstance) { }, }); + // The code cache is used for SEAs only. + if (codeCache && + result.cachedDataRejected !== false && + internalBinding('sea').isSea()) { + process.emitWarning('Code cache data rejected.'); + } + // Cache the source map for the module if present. if (result.sourceMapURL) { maybeCacheSourceMap(filename, content, this, false, undefined, result.sourceMapURL); diff --git a/lib/internal/util/embedding.js b/lib/internal/util/embedding.js index e2e67202477bc7..be310f401ad115 100644 --- a/lib/internal/util/embedding.js +++ b/lib/internal/util/embedding.js @@ -1,7 +1,8 @@ 'use strict'; -const { codes: { ERR_UNKNOWN_BUILTIN_MODULE } } = require('internal/errors'); const { BuiltinModule: { normalizeRequirableId } } = require('internal/bootstrap/realm'); const { Module, wrapSafe } = require('internal/modules/cjs/loader'); +const { codes: { ERR_UNKNOWN_BUILTIN_MODULE } } = require('internal/errors'); +const { getCodeCache, getCodePath, isSea } = internalBinding('sea'); // This is roughly the same as: // @@ -15,7 +16,11 @@ const { Module, wrapSafe } = require('internal/modules/cjs/loader'); function embedderRunCjs(contents) { const filename = process.execPath; - const compiledWrapper = wrapSafe(filename, contents); + const compiledWrapper = wrapSafe( + isSea() ? getCodePath() : filename, + contents, + undefined, + getCodeCache()); const customModule = new Module(filename, null); customModule.filename = filename; diff --git a/src/json_parser.cc b/src/json_parser.cc index a9973c099087e5..1e19e174833fa5 100644 --- a/src/json_parser.cc +++ b/src/json_parser.cc @@ -4,7 +4,6 @@ #include "util-inl.h" namespace node { -using v8::ArrayBuffer; using v8::Context; using v8::Isolate; using v8::Local; @@ -12,26 +11,8 @@ using v8::Object; using v8::String; using v8::Value; -static Isolate* NewIsolate(v8::ArrayBuffer::Allocator* allocator) { - Isolate* isolate = Isolate::Allocate(); - CHECK_NOT_NULL(isolate); - per_process::v8_platform.Platform()->RegisterIsolate(isolate, - uv_default_loop()); - Isolate::CreateParams params; - params.array_buffer_allocator = allocator; - Isolate::Initialize(isolate, params); - return isolate; -} - -void JSONParser::FreeIsolate(Isolate* isolate) { - per_process::v8_platform.Platform()->UnregisterIsolate(isolate); - isolate->Dispose(); -} - JSONParser::JSONParser() - : allocator_(ArrayBuffer::Allocator::NewDefaultAllocator()), - isolate_(NewIsolate(allocator_.get())), - handle_scope_(isolate_.get()), + : handle_scope_(isolate_.get()), context_(isolate_.get(), Context::New(isolate_.get())), context_scope_(context_.Get(isolate_.get())) {} diff --git a/src/json_parser.h b/src/json_parser.h index 555f539acf3076..3978a24222eb03 100644 --- a/src/json_parser.h +++ b/src/json_parser.h @@ -24,9 +24,7 @@ class JSONParser { private: // We might want a lighter-weight JSON parser for this use case. But for now // using V8 is good enough. - static void FreeIsolate(v8::Isolate* isolate); - std::unique_ptr allocator_; - DeleteFnPtr isolate_; + RAIIIsolate isolate_; v8::HandleScope handle_scope_; v8::Global context_; v8::Context::Scope context_scope_; diff --git a/src/node_contextify.cc b/src/node_contextify.cc index f8bd2d9b7cd71d..ee68ed12795740 100644 --- a/src/node_contextify.cc +++ b/src/node_contextify.cc @@ -935,6 +935,22 @@ Maybe StoreCodeCacheResult( return Just(true); } +// TODO(RaisinTen): Reuse in ContextifyContext::CompileFunction(). +MaybeLocal CompileFunction(Local context, + Local filename, + Local content, + std::vector>* parameters) { + ScriptOrigin script_origin(context->GetIsolate(), filename, 0, 0, true); + ScriptCompiler::Source script_source(content, script_origin); + + return ScriptCompiler::CompileFunction(context, + &script_source, + parameters->size(), + parameters->data(), + 0, + nullptr); +} + bool ContextifyScript::InstanceOf(Environment* env, const Local& value) { return !value.IsEmpty() && diff --git a/src/node_contextify.h b/src/node_contextify.h index 3160160521e0fe..9a0cbe07d6e660 100644 --- a/src/node_contextify.h +++ b/src/node_contextify.h @@ -210,6 +210,12 @@ v8::Maybe StoreCodeCacheResult( bool produce_cached_data, std::unique_ptr new_cached_data); +v8::MaybeLocal CompileFunction( + v8::Local context, + v8::Local filename, + v8::Local content, + std::vector>* parameters); + } // namespace contextify } // namespace node diff --git a/src/node_sea.cc b/src/node_sea.cc index b9eabef8196750..c595afbc753a79 100644 --- a/src/node_sea.cc +++ b/src/node_sea.cc @@ -4,11 +4,14 @@ #include "debug_utils-inl.h" #include "env-inl.h" #include "json_parser.h" +#include "node_contextify.h" +#include "node_errors.h" #include "node_external_reference.h" #include "node_internals.h" #include "node_snapshot_builder.h" #include "node_union_bytes.h" #include "node_v8_platform-inl.h" +#include "util-inl.h" // The POSTJECT_SENTINEL_FUSE macro is a string of random characters selected by // the Node.js project that is present only once in the entire binary. It is @@ -27,10 +30,19 @@ #if !defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION) using node::ExitCode; +using v8::ArrayBuffer; +using v8::BackingStore; using v8::Context; +using v8::DataView; +using v8::Function; using v8::FunctionCallbackInfo; +using v8::HandleScope; +using v8::Isolate; using v8::Local; +using v8::NewStringType; using v8::Object; +using v8::ScriptCompiler; +using v8::String; using v8::Value; namespace node { @@ -76,6 +88,12 @@ size_t SeaSerializer::Write(const SeaResource& sea) { written_total += WriteArithmetic(flags); DCHECK_EQ(written_total, SeaResource::kHeaderSize); + Debug("Write SEA code path %p, size=%zu\n", + sea.code_path.data(), + sea.code_path.size()); + written_total += + WriteStringView(sea.code_path, StringLogMode::kAddressAndContent); + Debug("Write SEA resource %s %p, size=%zu\n", sea.use_snapshot() ? "snapshot" : "code", sea.main_code_or_snapshot.data(), @@ -84,6 +102,14 @@ size_t SeaSerializer::Write(const SeaResource& sea) { WriteStringView(sea.main_code_or_snapshot, sea.use_snapshot() ? StringLogMode::kAddressOnly : StringLogMode::kAddressAndContent); + + if (sea.code_cache.has_value()) { + Debug("Write SEA resource code cache %p, size=%zu\n", + sea.code_cache->data(), + sea.code_cache->size()); + written_total += + WriteStringView(sea.code_cache.value(), StringLogMode::kAddressOnly); + } return written_total; } @@ -109,6 +135,11 @@ SeaResource SeaDeserializer::Read() { Debug("Read SEA flags %x\n", static_cast(flags)); CHECK_EQ(read_total, SeaResource::kHeaderSize); + std::string_view code_path = + ReadStringView(StringLogMode::kAddressAndContent); + Debug( + "Read SEA code path %p, size=%zu\n", code_path.data(), code_path.size()); + bool use_snapshot = static_cast(flags & SeaFlags::kUseSnapshot); std::string_view code = ReadStringView(use_snapshot ? StringLogMode::kAddressOnly @@ -118,7 +149,15 @@ SeaResource SeaDeserializer::Read() { use_snapshot ? "snapshot" : "code", code.data(), code.size()); - return {flags, code}; + + std::string_view code_cache; + if (static_cast(flags & SeaFlags::kUseCodeCache)) { + code_cache = ReadStringView(StringLogMode::kAddressOnly); + Debug("Read SEA resource code cache %p, size=%zu\n", + code_cache.data(), + code_cache.size()); + } + return {flags, code_path, code, code_cache}; } std::string_view FindSingleExecutableBlob() { @@ -167,6 +206,10 @@ bool IsSingleExecutable() { return postject_has_resource(); } +void IsSea(const FunctionCallbackInfo& args) { + args.GetReturnValue().Set(IsSingleExecutable()); +} + void IsExperimentalSeaWarningNeeded(const FunctionCallbackInfo& args) { bool is_building_sea = !per_process::cli_options->experimental_sea_config.empty(); @@ -185,6 +228,54 @@ void IsExperimentalSeaWarningNeeded(const FunctionCallbackInfo& args) { sea_resource.flags & SeaFlags::kDisableExperimentalSeaWarning)); } +void GetCodeCache(const FunctionCallbackInfo& args) { + if (!IsSingleExecutable()) { + return; + } + + Isolate* isolate = args.GetIsolate(); + + SeaResource sea_resource = FindSingleExecutableResource(); + + if (!static_cast(sea_resource.flags & SeaFlags::kUseCodeCache)) { + return; + } + + std::shared_ptr backing_store = ArrayBuffer::NewBackingStore( + const_cast( + static_cast(sea_resource.code_cache->data())), + sea_resource.code_cache->length(), + [](void* /* data */, size_t /* length */, void* /* deleter_data */) { + // The code cache data blob is not freed here because it is a static + // blob which is not allocated by the BackingStore allocator. + }, + nullptr); + Local array_buffer = ArrayBuffer::New(isolate, backing_store); + Local data_view = + DataView::New(array_buffer, 0, array_buffer->ByteLength()); + + args.GetReturnValue().Set(data_view); +} + +void GetCodePath(const FunctionCallbackInfo& args) { + DCHECK(IsSingleExecutable()); + + Isolate* isolate = args.GetIsolate(); + + SeaResource sea_resource = FindSingleExecutableResource(); + + Local code_path; + if (!String::NewFromUtf8(isolate, + sea_resource.code_path.data(), + NewStringType::kNormal, + sea_resource.code_path.length()) + .ToLocal(&code_path)) { + return; + } + + args.GetReturnValue().Set(code_path); +} + std::tuple FixupArgsForSEA(int argc, char** argv) { // Repeats argv[0] at position 1 on argv as a replacement for the missing // entry point file path. @@ -269,6 +360,17 @@ std::optional ParseSingleExecutableConfig( result.flags |= SeaFlags::kUseSnapshot; } + std::optional use_code_cache = + parser.GetTopLevelBoolField("useCodeCache"); + if (!use_code_cache.has_value()) { + FPrintF( + stderr, "\"useCodeCache\" field of %s is not a Boolean\n", config_path); + return std::nullopt; + } + if (use_code_cache.value()) { + result.flags |= SeaFlags::kUseCodeCache; + } + return result; } @@ -307,6 +409,59 @@ ExitCode GenerateSnapshotForSEA(const SeaConfig& config, return ExitCode::kNoFailure; } +std::optional GenerateCodeCache(std::string_view main_path, + std::string_view main_script) { + RAIIIsolate raii_isolate; + Isolate* isolate = raii_isolate.get(); + + HandleScope handle_scope(isolate); + Local context = Context::New(isolate); + Context::Scope context_scope(context); + + errors::PrinterTryCatch bootstrapCatch( + isolate, errors::PrinterTryCatch::kPrintSourceLine); + + Local filename; + if (!String::NewFromUtf8(isolate, + main_path.data(), + NewStringType::kNormal, + main_path.length()) + .ToLocal(&filename)) { + return std::nullopt; + } + + Local content; + if (!String::NewFromUtf8(isolate, + main_script.data(), + NewStringType::kNormal, + main_script.length()) + .ToLocal(&content)) { + return std::nullopt; + } + + std::vector> parameters = { + FIXED_ONE_BYTE_STRING(isolate, "exports"), + FIXED_ONE_BYTE_STRING(isolate, "require"), + FIXED_ONE_BYTE_STRING(isolate, "module"), + FIXED_ONE_BYTE_STRING(isolate, "__filename"), + FIXED_ONE_BYTE_STRING(isolate, "__dirname"), + }; + + // TODO(RaisinTen): Using the V8 code cache prevents us from using `import()` + // in the SEA code. Support it. + // Refs: https://github.com/nodejs/node/pull/48191#discussion_r1213271430 + Local fn; + if (!contextify::CompileFunction(context, filename, content, ¶meters) + .ToLocal(&fn)) { + return std::nullopt; + } + + std::unique_ptr cache{ + ScriptCompiler::CreateCodeCacheForFunction(fn)}; + std::string code_cache(cache->data, cache->data + cache->length); + return code_cache; +} + ExitCode GenerateSingleExecutableBlob( const SeaConfig& config, const std::vector& args, @@ -331,11 +486,33 @@ ExitCode GenerateSingleExecutableBlob( } } + std::optional optional_code_cache = + GenerateCodeCache(config.main_path, main_script); + if (!optional_code_cache.has_value()) { + FPrintF(stderr, "Cannot generate V8 code cache\n"); + return ExitCode::kGenericUserError; + } + + std::optional optional_sv_code_cache; + std::string code_cache; + if (static_cast(config.flags & SeaFlags::kUseCodeCache)) { + std::optional optional_code_cache = + GenerateCodeCache(config.main_path, main_script); + if (!optional_code_cache.has_value()) { + FPrintF(stderr, "Cannot generate V8 code cache\n"); + return ExitCode::kGenericUserError; + } + code_cache = optional_code_cache.value(); + optional_sv_code_cache = code_cache; + } + SeaResource sea{ config.flags, + config.main_path, builds_snapshot_from_main ? std::string_view{snapshot_blob.data(), snapshot_blob.size()} - : std::string_view{main_script.data(), main_script.size()}}; + : std::string_view{main_script.data(), main_script.size()}, + optional_sv_code_cache}; SeaSerializer serializer; serializer.Write(sea); @@ -374,14 +551,20 @@ void Initialize(Local target, Local unused, Local context, void* priv) { + SetMethod(context, target, "isSea", IsSea); SetMethod(context, target, "isExperimentalSeaWarningNeeded", IsExperimentalSeaWarningNeeded); + SetMethod(context, target, "getCodePath", GetCodePath); + SetMethod(context, target, "getCodeCache", GetCodeCache); } void RegisterExternalReferences(ExternalReferenceRegistry* registry) { + registry->Register(IsSea); registry->Register(IsExperimentalSeaWarningNeeded); + registry->Register(GetCodePath); + registry->Register(GetCodeCache); } } // namespace sea diff --git a/src/node_sea.h b/src/node_sea.h index f9eb5ca79d4543..c443de9e4d0adc 100644 --- a/src/node_sea.h +++ b/src/node_sea.h @@ -6,10 +6,12 @@ #if !defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION) #include +#include #include #include #include #include + #include "node_exit_code.h" namespace node { @@ -24,11 +26,14 @@ enum class SeaFlags : uint32_t { kDefault = 0, kDisableExperimentalSeaWarning = 1 << 0, kUseSnapshot = 1 << 1, + kUseCodeCache = 1 << 2, }; struct SeaResource { SeaFlags flags = SeaFlags::kDefault; + std::string_view code_path; std::string_view main_code_or_snapshot; + std::optional code_cache; bool use_snapshot() const; static constexpr size_t kHeaderSize = sizeof(kMagic) + sizeof(SeaFlags); diff --git a/src/node_snapshotable.cc b/src/node_snapshotable.cc index 797cd16d87c603..f4434103ec5fcd 100644 --- a/src/node_snapshotable.cc +++ b/src/node_snapshotable.cc @@ -42,7 +42,6 @@ using v8::MaybeLocal; using v8::Object; using v8::ObjectTemplate; using v8::ScriptCompiler; -using v8::ScriptOrigin; using v8::SnapshotCreator; using v8::StartupData; using v8::String; @@ -1259,7 +1258,6 @@ void CompileSerializeMain(const FunctionCallbackInfo& args) { Local source = args[1].As(); Isolate* isolate = args.GetIsolate(); Local context = isolate->GetCurrentContext(); - ScriptOrigin origin(isolate, filename, 0, 0, true); // TODO(joyeecheung): do we need all of these? Maybe we would want a less // internal version of them. std::vector> parameters = { @@ -1267,15 +1265,8 @@ void CompileSerializeMain(const FunctionCallbackInfo& args) { FIXED_ONE_BYTE_STRING(isolate, "__filename"), FIXED_ONE_BYTE_STRING(isolate, "__dirname"), }; - ScriptCompiler::Source script_source(source, origin); Local fn; - if (ScriptCompiler::CompileFunction(context, - &script_source, - parameters.size(), - parameters.data(), - 0, - nullptr, - ScriptCompiler::kNoCompileOptions) + if (contextify::CompileFunction(context, filename, source, ¶meters) .ToLocal(&fn)) { args.GetReturnValue().Set(fn); } diff --git a/src/util.cc b/src/util.cc index f28f906b4b7b6b..0523a3c8e00847 100644 --- a/src/util.cc +++ b/src/util.cc @@ -28,6 +28,7 @@ #include "node_errors.h" #include "node_internals.h" #include "node_util.h" +#include "node_v8_platform-inl.h" #include "string_bytes.h" #include "uv.h" @@ -55,6 +56,7 @@ static std::atomic_int seq = {0}; // Sequence number for diagnostic filenames. namespace node { +using v8::ArrayBuffer; using v8::ArrayBufferView; using v8::Context; using v8::FunctionTemplate; @@ -629,4 +631,20 @@ Local UnionBytes::ToStringChecked(Isolate* isolate) const { } } +RAIIIsolate::RAIIIsolate() + : allocator_{ArrayBuffer::Allocator::NewDefaultAllocator()} { + isolate_ = Isolate::Allocate(); + CHECK_NOT_NULL(isolate_); + per_process::v8_platform.Platform()->RegisterIsolate(isolate_, + uv_default_loop()); + Isolate::CreateParams params; + params.array_buffer_allocator = allocator_.get(); + Isolate::Initialize(isolate_, params); +} + +RAIIIsolate::~RAIIIsolate() { + per_process::v8_platform.Platform()->UnregisterIsolate(isolate_); + isolate_->Dispose(); +} + } // namespace node diff --git a/src/util.h b/src/util.h index c2ae527d6f5123..303489e624ce97 100644 --- a/src/util.h +++ b/src/util.h @@ -958,6 +958,19 @@ void SetConstructorFunction(v8::Isolate* isolate, SetConstructorFunctionFlag flag = SetConstructorFunctionFlag::SET_CLASS_NAME); +// Simple RAII class to spin up a v8::Isolate instance. +class RAIIIsolate { + public: + RAIIIsolate(); + ~RAIIIsolate(); + + v8::Isolate* get() const { return isolate_; } + + private: + std::unique_ptr allocator_; + v8::Isolate* isolate_; +}; + } // namespace node #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/test/fixtures/errors/force_colors.snapshot b/test/fixtures/errors/force_colors.snapshot index daef7905a6be19..be1d45d0d8e8ba 100644 --- a/test/fixtures/errors/force_colors.snapshot +++ b/test/fixtures/errors/force_colors.snapshot @@ -4,8 +4,8 @@ throw new Error('Should include grayed stack trace') Error: Should include grayed stack trace at Object. (/test*force_colors.js:1:7) - at Module._compile (node:internal*modules*cjs*loader:1233:14) - at Module._extensions..js (node:internal*modules*cjs*loader:1287:10) + at Module._compile (node:internal*modules*cjs*loader:1241:14) + at Module._extensions..js (node:internal*modules*cjs*loader:1295:10)  at Module.load (node:internal*modules*cjs*loader:1091:32)  at Module._load (node:internal*modules*cjs*loader:938:12)  at Function.executeUserEntryPoint [as runMain] (node:internal*modules*run_main:83:12) diff --git a/test/fixtures/sea.js b/test/fixtures/sea.js index 4e1f37ce5d8fad..e7b7f46ff00a86 100644 --- a/test/fixtures/sea.js +++ b/test/fixtures/sea.js @@ -5,12 +5,15 @@ const createdRequire = createRequire(__filename); // because we set NODE_TEST_DIR=/Users/iojs/node-tmp on Jenkins CI. const { expectWarning, mustNotCall } = createdRequire(process.env.COMMON_DIRECTORY); +// This additionally makes sure that no unexpected warnings are emitted. if (createdRequire('./sea-config.json').disableExperimentalSEAWarning) { process.on('warning', mustNotCall()); } else { expectWarning('ExperimentalWarning', 'Single executable application is an experimental feature and ' + 'might change at any time'); + // Any unexpected warning would throw this error: + // https://github.com/nodejs/node/blob/c301404105a7256b79a0b8c4522ce47af96dfa17/test/common/index.js#L697-L700. } // Should be possible to require core modules that optionally require the @@ -18,6 +21,9 @@ if (createdRequire('./sea-config.json').disableExperimentalSEAWarning) { const { deepStrictEqual, strictEqual, throws } = require('assert'); const { dirname } = require('node:path'); +// Checks that the source filename is used in the error stack trace. +strictEqual(new Error('lol').stack.split('\n')[1], ' at sea.js:25:13'); + // Should be possible to require a core module that requires using the "node:" // scheme. { diff --git a/test/sequential/test-single-executable-application-use-code-cache.js b/test/sequential/test-single-executable-application-use-code-cache.js new file mode 100644 index 00000000000000..6d45fcf289a772 --- /dev/null +++ b/test/sequential/test-single-executable-application-use-code-cache.js @@ -0,0 +1,60 @@ +'use strict'; + +require('../common'); + +const { + injectAndCodeSign, + skipIfSingleExecutableIsNotSupported, +} = require('../common/sea'); + +skipIfSingleExecutableIsNotSupported(); + +// This tests the creation of a single executable application which uses the +// V8 code cache. + +const fixtures = require('../common/fixtures'); +const tmpdir = require('../common/tmpdir'); +const { copyFileSync, writeFileSync, existsSync } = require('fs'); +const { execFileSync } = require('child_process'); +const { join } = require('path'); +const { strictEqual } = require('assert'); +const assert = require('assert'); + +const inputFile = fixtures.path('sea.js'); +const requirableFile = join(tmpdir.path, 'requirable.js'); +const configFile = join(tmpdir.path, 'sea-config.json'); +const seaPrepBlob = join(tmpdir.path, 'sea-prep.blob'); +const outputFile = join(tmpdir.path, process.platform === 'win32' ? 'sea.exe' : 'sea'); + +tmpdir.refresh(); + +writeFileSync(requirableFile, ` +module.exports = { + hello: 'world', +}; +`); + +writeFileSync(configFile, ` +{ + "main": "sea.js", + "output": "sea-prep.blob", + "useCodeCache": true +} +`); + +// Copy input to working directory +copyFileSync(inputFile, join(tmpdir.path, 'sea.js')); +execFileSync(process.execPath, ['--experimental-sea-config', 'sea-config.json'], { + cwd: tmpdir.path +}); + +assert(existsSync(seaPrepBlob)); + +copyFileSync(process.execPath, outputFile); +injectAndCodeSign(outputFile, seaPrepBlob); + +const singleExecutableApplicationOutput = execFileSync( + outputFile, + [ '-a', '--b=c', 'd' ], + { env: { COMMON_DIRECTORY: join(__dirname, '..', 'common') } }); +strictEqual(singleExecutableApplicationOutput.toString(), 'Hello, world! 😊\n');