From bcab0580c5a19a5c105979e38d593abdb02aa589 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Mon, 12 Dec 2022 18:18:37 +0100 Subject: [PATCH 1/7] src: add snapshot support for embedder API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add experimental support for loading snapshots in the embedder API by adding a public opaque wrapper for our `SnapshotData` struct and allowing embedders to pass it to the relevant setup functions. Where applicable, use these helpers to deduplicate existing code in Node.js’s startup path. This has shown a 40 % startup performance increase for a real-world application, even with the somewhat limited current support for built-in modules. The documentation includes a note about no guarantees for API or ABI stability for this feature while it is experimental. --- src/api/embed_helpers.cc | 65 +++++++++- src/api/environment.cc | 106 +++++++++++++--- src/env-inl.h | 4 + src/env.cc | 41 +++--- src/env.h | 15 +-- src/node.h | 118 ++++++++++++++++++ src/node_contextify.cc | 4 +- src/node_internals.h | 3 +- src/node_main_instance.cc | 51 ++------ src/node_snapshotable.cc | 11 ++ src/node_worker.cc | 4 +- test/embedding/embedtest.cc | 36 ++++-- test/embedding/test-embedding.js | 53 +++++++- .../fixtures/snapshot/create-worker-and-vm.js | 20 +++ test/fixtures/snapshot/echo-args.js | 12 ++ 15 files changed, 433 insertions(+), 110 deletions(-) create mode 100644 test/fixtures/snapshot/create-worker-and-vm.js create mode 100644 test/fixtures/snapshot/echo-args.js diff --git a/src/api/embed_helpers.cc b/src/api/embed_helpers.cc index f0f92c690eb63c..3944890e443885 100644 --- a/src/api/embed_helpers.cc +++ b/src/api/embed_helpers.cc @@ -1,6 +1,7 @@ -#include "node.h" -#include "env-inl.h" #include "debug_utils-inl.h" +#include "env-inl.h" +#include "node.h" +#include "node_snapshot_builder.h" using v8::Context; using v8::Function; @@ -86,8 +87,9 @@ struct CommonEnvironmentSetup::Impl { CommonEnvironmentSetup::CommonEnvironmentSetup( MultiIsolatePlatform* platform, std::vector* errors, + const EmbedderSnapshotData* snapshot_data, std::function make_env) - : impl_(new Impl()) { + : impl_(new Impl()) { CHECK_NOT_NULL(platform); CHECK_NOT_NULL(errors); @@ -104,16 +106,25 @@ CommonEnvironmentSetup::CommonEnvironmentSetup( loop->data = this; impl_->allocator = ArrayBufferAllocator::Create(); - impl_->isolate = NewIsolate(impl_->allocator, &impl_->loop, platform); + impl_->isolate = + NewIsolate(impl_->allocator, &impl_->loop, platform, snapshot_data); Isolate* isolate = impl_->isolate; { Locker locker(isolate); Isolate::Scope isolate_scope(isolate); impl_->isolate_data.reset(CreateIsolateData( - isolate, loop, platform, impl_->allocator.get())); + isolate, loop, platform, impl_->allocator.get(), snapshot_data)); HandleScope handle_scope(isolate); + if (snapshot_data) { + impl_->env.reset(make_env(this)); + if (impl_->env) { + impl_->context.Reset(isolate, impl_->env->context()); + } + return; + } + Local context = NewContext(isolate); impl_->context.Reset(isolate, context); if (context.IsEmpty()) { @@ -126,6 +137,12 @@ CommonEnvironmentSetup::CommonEnvironmentSetup( } } +CommonEnvironmentSetup::CommonEnvironmentSetup( + MultiIsolatePlatform* platform, + std::vector* errors, + std::function make_env) + : CommonEnvironmentSetup(platform, errors, nullptr, make_env) {} + CommonEnvironmentSetup::~CommonEnvironmentSetup() { if (impl_->isolate != nullptr) { Isolate* isolate = impl_->isolate; @@ -189,4 +206,42 @@ v8::Local CommonEnvironmentSetup::context() const { return impl_->context.Get(impl_->isolate); } +void EmbedderSnapshotData::DeleteSnapshotData::operator()( + const EmbedderSnapshotData* data) const { + CHECK_IMPLIES(data->owns_impl_, data->impl_); + if (data->owns_impl_ && + data->impl_->data_ownership == SnapshotData::DataOwnership::kOwned) { + delete data->impl_; + } + delete data; +} + +EmbedderSnapshotData::Pointer EmbedderSnapshotData::BuiltinSnapshotData() { + return EmbedderSnapshotData::Pointer{new EmbedderSnapshotData( + SnapshotBuilder::GetEmbeddedSnapshotData(), false)}; +} + +EmbedderSnapshotData::Pointer EmbedderSnapshotData::FromFile(FILE* in) { + SnapshotData* snapshot_data = new SnapshotData(); + CHECK_EQ(snapshot_data->data_ownership, SnapshotData::DataOwnership::kOwned); + EmbedderSnapshotData::Pointer result{ + new EmbedderSnapshotData(snapshot_data, true)}; + if (!SnapshotData::FromBlob(snapshot_data, in)) { + return {}; + } + return result; +} + +EmbedderSnapshotData::EmbedderSnapshotData(const SnapshotData* impl, + bool owns_impl) + : impl_(impl), owns_impl_(owns_impl) {} + +bool EmbedderSnapshotData::CanUseCustomSnapshotPerIsolate() { +#ifdef NODE_V8_SHARED_RO_HEAP + return false; +#else + return true; +#endif +} + } // namespace node diff --git a/src/api/environment.cc b/src/api/environment.cc index 406455c7263573..5033f573521cf2 100644 --- a/src/api/environment.cc +++ b/src/api/environment.cc @@ -9,6 +9,7 @@ #include "node_platform.h" #include "node_realm-inl.h" #include "node_shadow_realm.h" +#include "node_snapshot_builder.h" #include "node_v8_platform-inl.h" #include "node_wasm_web_api.h" #include "uv.h" @@ -307,9 +308,15 @@ void SetIsolateUpForNode(v8::Isolate* isolate) { Isolate* NewIsolate(Isolate::CreateParams* params, uv_loop_t* event_loop, MultiIsolatePlatform* platform, - bool has_snapshot_data) { + const SnapshotData* snapshot_data, + const IsolateSettings& settings) { Isolate* isolate = Isolate::Allocate(); if (isolate == nullptr) return nullptr; + + if (snapshot_data != nullptr) { + SnapshotBuilder::InitializeIsolateParams(snapshot_data, params); + } + #ifdef NODE_V8_SHARED_RO_HEAP { // In shared-readonly-heap mode, V8 requires all snapshots used for @@ -328,12 +335,12 @@ Isolate* NewIsolate(Isolate::CreateParams* params, SetIsolateCreateParamsForNode(params); Isolate::Initialize(isolate, *params); - if (!has_snapshot_data) { + if (snapshot_data == nullptr) { // If in deserialize mode, delay until after the deserialization is // complete. - SetIsolateUpForNode(isolate); + SetIsolateUpForNode(isolate, settings); } else { - SetIsolateMiscHandlers(isolate, {}); + SetIsolateMiscHandlers(isolate, settings); } return isolate; @@ -341,25 +348,60 @@ Isolate* NewIsolate(Isolate::CreateParams* params, Isolate* NewIsolate(ArrayBufferAllocator* allocator, uv_loop_t* event_loop, - MultiIsolatePlatform* platform) { + MultiIsolatePlatform* platform, + const EmbedderSnapshotData* snapshot_data, + const IsolateSettings& settings) { Isolate::CreateParams params; if (allocator != nullptr) params.array_buffer_allocator = allocator; - return NewIsolate(¶ms, event_loop, platform); + return NewIsolate(¶ms, + event_loop, + platform, + SnapshotData::FromEmbedderWrapper(snapshot_data), + settings); } Isolate* NewIsolate(std::shared_ptr allocator, uv_loop_t* event_loop, - MultiIsolatePlatform* platform) { + MultiIsolatePlatform* platform, + const EmbedderSnapshotData* snapshot_data, + const IsolateSettings& settings) { Isolate::CreateParams params; if (allocator) params.array_buffer_allocator_shared = allocator; - return NewIsolate(¶ms, event_loop, platform); + return NewIsolate(¶ms, + event_loop, + platform, + SnapshotData::FromEmbedderWrapper(snapshot_data), + settings); +} + +Isolate* NewIsolate(ArrayBufferAllocator* allocator, + uv_loop_t* event_loop, + MultiIsolatePlatform* platform) { + return NewIsolate(allocator, event_loop, platform, nullptr); +} + +Isolate* NewIsolate(std::shared_ptr allocator, + uv_loop_t* event_loop, + MultiIsolatePlatform* platform) { + return NewIsolate(allocator, event_loop, platform, nullptr); +} + +IsolateData* CreateIsolateData( + Isolate* isolate, + uv_loop_t* loop, + MultiIsolatePlatform* platform, + ArrayBufferAllocator* allocator, + const EmbedderSnapshotData* embedder_snapshot_data) { + const SnapshotData* snapshot_data = + SnapshotData::FromEmbedderWrapper(embedder_snapshot_data); + return new IsolateData(isolate, loop, platform, allocator, snapshot_data); } IsolateData* CreateIsolateData(Isolate* isolate, uv_loop_t* loop, MultiIsolatePlatform* platform, ArrayBufferAllocator* allocator) { - return new IsolateData(isolate, loop, platform, allocator); + return CreateIsolateData(isolate, loop, platform, allocator, nullptr); } void FreeIsolateData(IsolateData* isolate_data) { @@ -387,13 +429,45 @@ Environment* CreateEnvironment( EnvironmentFlags::Flags flags, ThreadId thread_id, std::unique_ptr inspector_parent_handle) { - Isolate* isolate = context->GetIsolate(); + Isolate* isolate = isolate_data->isolate(); HandleScope handle_scope(isolate); - Context::Scope context_scope(context); + + const bool use_snapshot = context.IsEmpty(); + const EnvSerializeInfo* env_snapshot_info = nullptr; + if (use_snapshot) { + CHECK_NOT_NULL(isolate_data->snapshot_data()); + env_snapshot_info = &isolate_data->snapshot_data()->env_info; + } + // TODO(addaleax): This is a much better place for parsing per-Environment // options than the global parse call. - Environment* env = new Environment( - isolate_data, context, args, exec_args, nullptr, flags, thread_id); + Environment* env = new Environment(isolate_data, + isolate, + args, + exec_args, + env_snapshot_info, + flags, + thread_id); + CHECK_NOT_NULL(env); + + if (use_snapshot) { + context = Context::FromSnapshot(isolate, + SnapshotData::kNodeMainContextIndex, + {DeserializeNodeInternalFields, env}) + .ToLocalChecked(); + + CHECK(!context.IsEmpty()); + Context::Scope context_scope(context); + + if (InitializeContextRuntime(context).IsNothing()) { + FreeEnvironment(env); + return nullptr; + } + SetIsolateErrorHandlers(isolate, {}); + } + + Context::Scope context_scope(context); + env->InitializeMainContext(context, env_snapshot_info); #if HAVE_INSPECTOR if (env->should_create_inspector()) { @@ -407,7 +481,7 @@ Environment* CreateEnvironment( } #endif - if (env->principal_realm()->RunBootstrapping().IsEmpty()) { + if (!use_snapshot && env->principal_realm()->RunBootstrapping().IsEmpty()) { FreeEnvironment(env); return nullptr; } @@ -492,6 +566,10 @@ ArrayBufferAllocator* GetArrayBufferAllocator(IsolateData* isolate_data) { return isolate_data->node_allocator(); } +Local GetMainContext(Environment* env) { + return env->context(); +} + MultiIsolatePlatform* GetMultiIsolatePlatform(Environment* env) { return GetMultiIsolatePlatform(env->isolate_data()); } diff --git a/src/env-inl.h b/src/env-inl.h index 4dd4ee5e1d7c9e..98aa46142d21a8 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -69,6 +69,10 @@ inline MultiIsolatePlatform* IsolateData::platform() const { return platform_; } +inline const SnapshotData* IsolateData::snapshot_data() const { + return snapshot_data_; +} + inline void IsolateData::set_worker_context(worker::Worker* context) { CHECK_NULL(worker_context_); // Should be set only once. worker_context_ = context; diff --git a/src/env.cc b/src/env.cc index 88d5b24ffc1764..692a344703a196 100644 --- a/src/env.cc +++ b/src/env.cc @@ -472,19 +472,20 @@ IsolateData::IsolateData(Isolate* isolate, uv_loop_t* event_loop, MultiIsolatePlatform* platform, ArrayBufferAllocator* node_allocator, - const IsolateDataSerializeInfo* isolate_data_info) + const SnapshotData* snapshot_data) : isolate_(isolate), event_loop_(event_loop), node_allocator_(node_allocator == nullptr ? nullptr : node_allocator->GetImpl()), - platform_(platform) { + platform_(platform), + snapshot_data_(snapshot_data) { options_.reset( new PerIsolateOptions(*(per_process::cli_options->per_isolate))); - if (isolate_data_info == nullptr) { + if (snapshot_data == nullptr) { CreateProperties(); } else { - DeserializeProperties(isolate_data_info); + DeserializeProperties(&snapshot_data->isolate_data_info); } } @@ -675,14 +676,23 @@ Environment::Environment(IsolateData* isolate_data, thread_id_(thread_id.id == static_cast(-1) ? AllocateEnvironmentThreadId().id : thread_id.id) { + constexpr bool is_shared_ro_heap = #ifdef NODE_V8_SHARED_RO_HEAP - if (!is_main_thread()) { + true; +#else + false; +#endif + if (is_shared_ro_heap && !is_main_thread()) { + // If this is a Worker thread and we are in shared-readonly-heap mode, + // we can always safely use the parent's Isolate's code cache. CHECK_NOT_NULL(isolate_data->worker_context()); - // TODO(addaleax): Adjust for the embedder API snapshot support changes builtin_loader()->CopySourceAndCodeCacheReferenceFrom( isolate_data->worker_context()->env()->builtin_loader()); + } else if (isolate_data->snapshot_data() != nullptr) { + // ... otherwise, if a snapshot was provided, use its code cache. + builtin_loader()->RefreshCodeCache( + isolate_data->snapshot_data()->code_cache); } -#endif // We'll be creating new objects so make sure we've entered the context. HandleScope handle_scope(isolate); @@ -747,23 +757,6 @@ Environment::Environment(IsolateData* isolate_data, } } -Environment::Environment(IsolateData* isolate_data, - Local context, - const std::vector& args, - const std::vector& exec_args, - const EnvSerializeInfo* env_info, - EnvironmentFlags::Flags flags, - ThreadId thread_id) - : Environment(isolate_data, - context->GetIsolate(), - args, - exec_args, - env_info, - flags, - thread_id) { - InitializeMainContext(context, env_info); -} - void Environment::InitializeMainContext(Local context, const EnvSerializeInfo* env_info) { principal_realm_ = std::make_unique( diff --git a/src/env.h b/src/env.h index 2619e57e44d6b9..b18e4f2cb23318 100644 --- a/src/env.h +++ b/src/env.h @@ -126,7 +126,7 @@ class NODE_EXTERN_PRIVATE IsolateData : public MemoryRetainer { uv_loop_t* event_loop, MultiIsolatePlatform* platform = nullptr, ArrayBufferAllocator* node_allocator = nullptr, - const IsolateDataSerializeInfo* isolate_data_info = nullptr); + const SnapshotData* snapshot_data = nullptr); SET_MEMORY_INFO_NAME(IsolateData) SET_SELF_SIZE(IsolateData) void MemoryInfo(MemoryTracker* tracker) const override; @@ -134,6 +134,7 @@ class NODE_EXTERN_PRIVATE IsolateData : public MemoryRetainer { inline uv_loop_t* event_loop() const; inline MultiIsolatePlatform* platform() const; + inline const SnapshotData* snapshot_data() const; inline std::shared_ptr options(); inline void set_options(std::shared_ptr options); @@ -205,6 +206,7 @@ class NODE_EXTERN_PRIVATE IsolateData : public MemoryRetainer { uv_loop_t* const event_loop_; NodeArrayBufferAllocator* const node_allocator_; MultiIsolatePlatform* platform_; + const SnapshotData* snapshot_data_; std::shared_ptr options_; worker::Worker* worker_context_ = nullptr; }; @@ -520,6 +522,9 @@ struct SnapshotData { // and the caller should not consume the snapshot data. bool Check() const; static bool FromBlob(SnapshotData* out, FILE* in); + static const SnapshotData* FromEmbedderWrapper( + const EmbedderSnapshotData* data); + EmbedderSnapshotData::Pointer AsEmbedderWrapper() const; ~SnapshotData(); }; @@ -610,14 +615,6 @@ class Environment : public MemoryRetainer { ThreadId thread_id); void InitializeMainContext(v8::Local context, const EnvSerializeInfo* env_info); - // Create an Environment and initialize the provided principal context for it. - Environment(IsolateData* isolate_data, - v8::Local context, - const std::vector& args, - const std::vector& exec_args, - const EnvSerializeInfo* env_info, - EnvironmentFlags::Flags flags, - ThreadId thread_id); ~Environment() override; void InitializeLibuv(); diff --git a/src/node.h b/src/node.h index 561af155561923..ce570e5273836f 100644 --- a/src/node.h +++ b/src/node.h @@ -127,6 +127,8 @@ struct napi_module; // terminally confused when it's done in node_internals.h namespace node { +struct SnapshotData; + namespace tracing { class TracingController; @@ -473,6 +475,65 @@ struct IsolateSettings { allow_wasm_code_generation_callback = nullptr; }; +// Represents a startup snapshot blob, e.g. created by passing +// --node-snapshot-main=entry.js to the configure script at build time, +// or by running Node.js with the --build-snapshot option. +// +// If used, the snapshot *must* have been built with the same Node.js +// version and V8 flags as the version that is currently running, and will +// be rejected otherwise. +// The same EmbedderSnapshotData instance *must* be passed to both +// `NewIsolate()` and `CreateIsolateData()`. The first `Environment` instance +// should be created with an empty `context` argument and will then +// use the main context included in the snapshot blob. It can be retrieved +// using `GetMainContext()`. `LoadEnvironment` can receive an empty +// `StartExecutionCallback` in this case. +// If V8 was configured with the shared-readonly-heap option, it requires +// all snapshots used to create `Isolate` instances to be identical. +// This option *must* be unset by embedders who wish to use the startup +// feature during the build step by passing the --disable-shared-readonly-heap +// flag to the configure script. +// +// Snapshots are an *experimental* feature. In particular, the embedder API +// exposed through this class is subject to change or removal between Node.js +// versions, including possible API and ABI breakage. +class EmbedderSnapshotData { + public: + struct DeleteSnapshotData { + void operator()(const EmbedderSnapshotData*) const; + }; + using Pointer = + std::unique_ptr; + + // Return an EmbedderSnapshotData object that refers to the built-in + // snapshot of Node.js. This can have been configured through e.g. + // --node-snapshot-main=entry.js. + static Pointer BuiltinSnapshotData(); + + // Return an EmbedderSnapshotData object that is based on an input file. + // Calling this method will not consume but not close the FILE* handle. + // The FILE* handle can be closed immediately following this call. + // If the snapshot is invalid, this returns an empty pointer. + static Pointer FromFile(FILE* in); + + // Returns whether custom snapshots can be used. Currently, this means + // that V8 was configured without the shared-readonly-heap feature. + static bool CanUseCustomSnapshotPerIsolate(); + + EmbedderSnapshotData(const EmbedderSnapshotData&) = delete; + EmbedderSnapshotData& operator=(const EmbedderSnapshotData&) = delete; + EmbedderSnapshotData(EmbedderSnapshotData&&) = delete; + EmbedderSnapshotData& operator=(EmbedderSnapshotData&&) = delete; + + protected: + EmbedderSnapshotData(const SnapshotData* impl, bool owns_impl); + + private: + const SnapshotData* impl_; + bool owns_impl_; + friend struct SnapshotData; +}; + // Overriding IsolateSettings may produce unexpected behavior // in Node.js core functionality, so proceed at your own risk. NODE_EXTERN void SetIsolateUpForNode(v8::Isolate* isolate, @@ -489,10 +550,23 @@ NODE_EXTERN void SetIsolateUpForNode(v8::Isolate* isolate); NODE_EXTERN v8::Isolate* NewIsolate(ArrayBufferAllocator* allocator, struct uv_loop_s* event_loop, MultiIsolatePlatform* platform = nullptr); +// TODO(addaleax): Merge with the function definition above. +NODE_EXTERN v8::Isolate* NewIsolate(ArrayBufferAllocator* allocator, + struct uv_loop_s* event_loop, + MultiIsolatePlatform* platform, + const EmbedderSnapshotData* snapshot_data, + const IsolateSettings& settings = {}); NODE_EXTERN v8::Isolate* NewIsolate( std::shared_ptr allocator, struct uv_loop_s* event_loop, MultiIsolatePlatform* platform); +// TODO(addaleax): Merge with the function definition above. +NODE_EXTERN v8::Isolate* NewIsolate( + std::shared_ptr allocator, + struct uv_loop_s* event_loop, + MultiIsolatePlatform* platform, + const EmbedderSnapshotData* snapshot_data, + const IsolateSettings& settings = {}); // Creates a new context with Node.js-specific tweaks. NODE_EXTERN v8::Local NewContext( @@ -512,6 +586,13 @@ NODE_EXTERN IsolateData* CreateIsolateData( struct uv_loop_s* loop, MultiIsolatePlatform* platform = nullptr, ArrayBufferAllocator* allocator = nullptr); +// TODO(addaleax): Merge with the function definition above. +NODE_EXTERN IsolateData* CreateIsolateData( + v8::Isolate* isolate, + struct uv_loop_s* loop, + MultiIsolatePlatform* platform, + ArrayBufferAllocator* allocator, + const EmbedderSnapshotData* snapshot_data); NODE_EXTERN void FreeIsolateData(IsolateData* isolate_data); struct ThreadId { @@ -571,6 +652,8 @@ struct InspectorParentHandle { // TODO(addaleax): Maybe move per-Environment options parsing here. // Returns nullptr when the Environment cannot be created e.g. there are // pending JavaScript exceptions. +// `context` may be empty if an `EmbedderSnapshotData` instance was provided +// to `NewIsolate()` and `CreateIsolateData()`. NODE_EXTERN Environment* CreateEnvironment( IsolateData* isolate_data, v8::Local context, @@ -624,6 +707,9 @@ NODE_EXTERN void DefaultProcessExitHandler(Environment* env, int exit_code); NODE_EXTERN Environment* GetCurrentEnvironment(v8::Local context); NODE_EXTERN IsolateData* GetEnvironmentIsolateData(Environment* env); NODE_EXTERN ArrayBufferAllocator* GetArrayBufferAllocator(IsolateData* data); +// This is mostly useful for Environment* instances that were created through +// a snapshot and have a main context that was read from that snapshot. +NODE_EXTERN v8::Local GetMainContext(Environment* env); NODE_EXTERN void OnFatalError(const char* location, const char* message); NODE_EXTERN void PromiseRejectCallback(v8::PromiseRejectMessage message); @@ -730,6 +816,12 @@ class NODE_EXTERN CommonEnvironmentSetup { MultiIsolatePlatform* platform, std::vector* errors, EnvironmentArgs&&... env_args); + template + static std::unique_ptr CreateWithSnapshot( + MultiIsolatePlatform* platform, + std::vector* errors, + const EmbedderSnapshotData* snapshot_data, + EnvironmentArgs&&... env_args); struct uv_loop_s* event_loop() const; std::shared_ptr array_buffer_allocator() const; @@ -750,6 +842,11 @@ class NODE_EXTERN CommonEnvironmentSetup { MultiIsolatePlatform*, std::vector*, std::function); + CommonEnvironmentSetup( + MultiIsolatePlatform*, + std::vector*, + const EmbedderSnapshotData*, + std::function); }; // Implementation for CommonEnvironmentSetup::Create @@ -768,6 +865,27 @@ std::unique_ptr CommonEnvironmentSetup::Create( if (!errors->empty()) ret.reset(); return ret; } +// Implementation for ::CreateWithSnapshot -- the ::Create() method +// could call this with a nullptr snapshot_data in a major version. +template +std::unique_ptr +CommonEnvironmentSetup::CreateWithSnapshot( + MultiIsolatePlatform* platform, + std::vector* errors, + const EmbedderSnapshotData* snapshot_data, + EnvironmentArgs&&... env_args) { + auto ret = std::unique_ptr(new CommonEnvironmentSetup( + platform, + errors, + snapshot_data, + [&](const CommonEnvironmentSetup* setup) -> Environment* { + return CreateEnvironment(setup->isolate_data(), + setup->context(), + std::forward(env_args)...); + })); + if (!errors->empty()) ret.reset(); + return ret; +} /* Converts a unixtime to V8 Date */ NODE_DEPRECATED("Use v8::Date::New() directly", diff --git a/src/node_contextify.cc b/src/node_contextify.cc index ed552ddd559f51..28b3266f20b070 100644 --- a/src/node_contextify.cc +++ b/src/node_contextify.cc @@ -117,9 +117,7 @@ BaseObjectPtr ContextifyContext::New( InitializeGlobalTemplates(env->isolate_data()); Local object_template = env->contextify_global_template(); DCHECK(!object_template.IsEmpty()); - bool use_node_snapshot = per_process::cli_options->node_snapshot; - const SnapshotData* snapshot_data = - use_node_snapshot ? SnapshotBuilder::GetEmbeddedSnapshotData() : nullptr; + const SnapshotData* snapshot_data = env->isolate_data()->snapshot_data(); MicrotaskQueue* queue = options.microtask_queue_wrap diff --git a/src/node_internals.h b/src/node_internals.h index 23b8c37c614c40..df90781f58e9a2 100644 --- a/src/node_internals.h +++ b/src/node_internals.h @@ -304,7 +304,8 @@ void DefineZlibConstants(v8::Local target); v8::Isolate* NewIsolate(v8::Isolate::CreateParams* params, uv_loop_t* event_loop, MultiIsolatePlatform* platform, - bool has_snapshot_data = false); + const SnapshotData* snapshot_data = nullptr, + const IsolateSettings& settings = {}); // This overload automatically picks the right 'main_script_id' if no callback // was provided by the embedder. v8::MaybeLocal StartExecution(Environment* env, diff --git a/src/node_main_instance.cc b/src/node_main_instance.cc index a40866b482c326..81804656c51093 100644 --- a/src/node_main_instance.cc +++ b/src/node_main_instance.cc @@ -71,22 +71,18 @@ NodeMainInstance::NodeMainInstance(const SnapshotData* snapshot_data, isolate_params_(std::make_unique()), snapshot_data_(snapshot_data) { isolate_params_->array_buffer_allocator = array_buffer_allocator_.get(); - if (snapshot_data != nullptr) { - SnapshotBuilder::InitializeIsolateParams(snapshot_data, - isolate_params_.get()); - } - isolate_ = NewIsolate( - isolate_params_.get(), event_loop, platform, snapshot_data != nullptr); + isolate_ = + NewIsolate(isolate_params_.get(), event_loop, platform, snapshot_data); CHECK_NOT_NULL(isolate_); // If the indexes are not nullptr, we are not deserializing - isolate_data_ = std::make_unique( - isolate_, - event_loop, - platform, - array_buffer_allocator_.get(), - snapshot_data == nullptr ? nullptr : &(snapshot_data->isolate_data_info)); + isolate_data_.reset( + CreateIsolateData(isolate_, + event_loop, + platform, + array_buffer_allocator_.get(), + snapshot_data->AsEmbedderWrapper().get())); isolate_data_->max_young_gen_size = isolate_params_->constraints.max_young_generation_size_in_bytes(); @@ -152,33 +148,10 @@ NodeMainInstance::CreateMainEnvironment(ExitCode* exit_code) { DeleteFnPtr env; if (snapshot_data_ != nullptr) { - env.reset(new Environment(isolate_data_.get(), - isolate_, - args_, - exec_args_, - &(snapshot_data_->env_info), - EnvironmentFlags::kDefaultFlags, - {})); -#ifdef NODE_V8_SHARED_RO_HEAP - // TODO(addaleax): Do this as part of creating the Environment - // once we store the SnapshotData* itself on IsolateData. - env->builtin_loader()->RefreshCodeCache(snapshot_data_->code_cache); -#endif - context = Context::FromSnapshot(isolate_, - SnapshotData::kNodeMainContextIndex, - {DeserializeNodeInternalFields, env.get()}) - .ToLocalChecked(); - - CHECK(!context.IsEmpty()); - Context::Scope context_scope(context); - - CHECK(InitializeContextRuntime(context).IsJust()); - SetIsolateErrorHandlers(isolate_, {}); - env->InitializeMainContext(context, &(snapshot_data_->env_info)); -#if HAVE_INSPECTOR - env->InitializeInspector({}); -#endif - + env.reset(CreateEnvironment(isolate_data_.get(), + Local(), // read from snapshot + args_, + exec_args_)); #if HAVE_OPENSSL crypto::InitCryptoOnce(isolate_); #endif // HAVE_OPENSSL diff --git a/src/node_snapshotable.cc b/src/node_snapshotable.cc index f74bbc0bc41ae9..ee09b3e13240ab 100644 --- a/src/node_snapshotable.cc +++ b/src/node_snapshotable.cc @@ -853,6 +853,15 @@ void SnapshotData::ToBlob(FILE* out) const { w.Debug("SnapshotData::ToBlob() Wrote %d bytes\n", written_total); } +const SnapshotData* SnapshotData::FromEmbedderWrapper( + const EmbedderSnapshotData* data) { + return data != nullptr ? data->impl_ : nullptr; +} + +EmbedderSnapshotData::Pointer SnapshotData::AsEmbedderWrapper() const { + return EmbedderSnapshotData::Pointer{new EmbedderSnapshotData(this, false)}; +} + bool SnapshotData::FromBlob(SnapshotData* out, FILE* in) { FileReader r(in); r.Debug("SnapshotData::FromBlob()\n"); @@ -1062,6 +1071,8 @@ const std::vector& SnapshotBuilder::CollectExternalReferences() { void SnapshotBuilder::InitializeIsolateParams(const SnapshotData* data, Isolate::CreateParams* params) { + CHECK_NULL(params->external_references); + CHECK_NULL(params->snapshot_blob); params->external_references = CollectExternalReferences().data(); params->snapshot_blob = const_cast(&(data->v8_snapshot_blob_data)); diff --git a/src/node_worker.cc b/src/node_worker.cc index b84a39cce330d7..66ab3dfd8ceb33 100644 --- a/src/node_worker.cc +++ b/src/node_worker.cc @@ -584,9 +584,7 @@ void Worker::New(const FunctionCallbackInfo& args) { exec_argv_out = env->exec_argv(); } - bool use_node_snapshot = per_process::cli_options->node_snapshot; - const SnapshotData* snapshot_data = - use_node_snapshot ? SnapshotBuilder::GetEmbeddedSnapshotData() : nullptr; + const SnapshotData* snapshot_data = env->isolate_data()->snapshot_data(); Worker* worker = new Worker(env, args.This(), diff --git a/test/embedding/embedtest.cc b/test/embedding/embedtest.cc index 2ad8afd42845dd..ea3af217319ef7 100644 --- a/test/embedding/embedtest.cc +++ b/test/embedding/embedtest.cc @@ -4,6 +4,8 @@ // Note: This file is being referred to from doc/api/embedding.md, and excerpts // from it are included in the documentation. Try to keep these in sync. +// Snapshot support is not part of the embedder API docs yet due to its +// experimental nature, although it is of course documented in node.h. using node::CommonEnvironmentSetup; using node::Environment; @@ -55,9 +57,22 @@ int RunNodeInstance(MultiIsolatePlatform* platform, const std::vector& exec_args) { int exit_code = 0; + node::EmbedderSnapshotData::Pointer snapshot; + auto snapshot_arg_it = + std::find(args.begin(), args.end(), "--embedder-snapshot-blob"); + if (snapshot_arg_it < args.end() - 1) { + FILE* fp = fopen((snapshot_arg_it + 1)->c_str(), "r"); + assert(fp != nullptr); + snapshot = node::EmbedderSnapshotData::FromFile(fp); + fclose(fp); + } + std::vector errors; std::unique_ptr setup = - CommonEnvironmentSetup::Create(platform, &errors, args, exec_args); + snapshot + ? CommonEnvironmentSetup::CreateWithSnapshot( + platform, &errors, snapshot.get(), args, exec_args) + : CommonEnvironmentSetup::Create(platform, &errors, args, exec_args); if (!setup) { for (const std::string& err : errors) fprintf(stderr, "%s: %s\n", args[0].c_str(), err.c_str()); @@ -73,13 +88,18 @@ int RunNodeInstance(MultiIsolatePlatform* platform, HandleScope handle_scope(isolate); Context::Scope context_scope(setup->context()); - MaybeLocal loadenv_ret = node::LoadEnvironment( - env, - "const publicRequire =" - " require('module').createRequire(process.cwd() + '/');" - "globalThis.require = publicRequire;" - "globalThis.embedVars = { nön_ascıı: '🏳️‍🌈' };" - "require('vm').runInThisContext(process.argv[1]);"); + MaybeLocal loadenv_ret; + if (snapshot) { + loadenv_ret = node::LoadEnvironment(env, node::StartExecutionCallback{}); + } else { + loadenv_ret = node::LoadEnvironment( + env, + "const publicRequire =" + " require('module').createRequire(process.cwd() + '/');" + "globalThis.require = publicRequire;" + "globalThis.embedVars = { nön_ascıı: '🏳️‍🌈' };" + "require('vm').runInThisContext(process.argv[1]);"); + } if (loadenv_ret.IsEmpty()) // There has been a JS exception. return 1; diff --git a/test/embedding/test-embedding.js b/test/embedding/test-embedding.js index 2fbaaf0ef81a49..c21c39b28e99dd 100644 --- a/test/embedding/test-embedding.js +++ b/test/embedding/test-embedding.js @@ -1,17 +1,26 @@ 'use strict'; const common = require('../common'); const fixtures = require('../common/fixtures'); +const tmpdir = require('../common/tmpdir'); const assert = require('assert'); const child_process = require('child_process'); const path = require('path'); +const fs = require('fs'); +tmpdir.refresh(); common.allowGlobals(global.require); common.allowGlobals(global.embedVars); -let binary = `out/${common.buildType}/embedtest`; -if (common.isWindows) { - binary += '.exe'; + +function resolveBuiltBinary(bin) { + let binary = `out/${common.buildType}/${bin}`; + if (common.isWindows) { + binary += '.exe'; + } + return path.resolve(__dirname, '..', '..', binary); } -binary = path.resolve(__dirname, '..', '..', binary); + +const binary = resolveBuiltBinary('embedtest'); +const standaloneNodeBinary = resolveBuiltBinary('node'); assert.strictEqual( child_process.spawnSync(binary, ['console.log(42)']) @@ -41,3 +50,39 @@ const fixturePath = JSON.stringify(fixtures.path('exit.js')); assert.strictEqual( child_process.spawnSync(binary, [`require(${fixturePath})`, 92]).status, 92); + +// Basic snapshot support +{ + const snapshotFixture = fixtures.path('snapshot', 'echo-args.js'); + const blobPath = path.join(tmpdir.path, 'embedder-snapshot.blob'); + const buildSnapshotArgs = [snapshotFixture, 'arg1', 'arg2']; + const runEmbeddedArgs = ['--embedder-snapshot-blob', blobPath, 'arg3', 'arg4']; + + fs.rmSync(blobPath, { force: true }); + assert.strictEqual(child_process.spawnSync(standaloneNodeBinary, [ + '--snapshot-blob', blobPath, '--build-snapshot', ...buildSnapshotArgs, + ], { + cwd: tmpdir.path, + }).status, 0); + const spawnResult = child_process.spawnSync(binary, ['--', ...runEmbeddedArgs]); + assert.deepStrictEqual(JSON.parse(spawnResult.stdout), { + originalArgv: [standaloneNodeBinary, ...buildSnapshotArgs], + currentArgv: [binary, ...runEmbeddedArgs], + }); +} + +// Create workers and vm contexts after deserialization +{ + const snapshotFixture = fixtures.path('snapshot', 'create-worker-and-vm.js'); + const blobPath = path.join(tmpdir.path, 'embedder-snapshot.blob'); + + fs.rmSync(blobPath, { force: true }); + assert.strictEqual(child_process.spawnSync(standaloneNodeBinary, [ + '--snapshot-blob', blobPath, '--build-snapshot', snapshotFixture, + ], { + cwd: tmpdir.path, + }).status, 0); + assert.strictEqual( + child_process.spawnSync(binary, ['--', '--embedder-snapshot-blob', blobPath]).status, + 0); +} diff --git a/test/fixtures/snapshot/create-worker-and-vm.js b/test/fixtures/snapshot/create-worker-and-vm.js new file mode 100644 index 00000000000000..4dd2aa940247af --- /dev/null +++ b/test/fixtures/snapshot/create-worker-and-vm.js @@ -0,0 +1,20 @@ +const { + setDeserializeMainFunction, +} = require('v8').startupSnapshot; +const assert = require('assert'); + +setDeserializeMainFunction(() => { + const vm = require('vm'); + const { Worker } = require('worker_threads'); + assert.strictEqual(vm.runInNewContext('21+21'), 42); + const worker = new Worker( + 'require("worker_threads").parentPort.postMessage({value: 21 + 21})', + { eval: true }); + + const messages = []; + worker.on('message', message => messages.push(message)); + + process.on('beforeExit', () => { + assert.deepStrictEqual(messages, [{value:42}]); + }) +}); diff --git a/test/fixtures/snapshot/echo-args.js b/test/fixtures/snapshot/echo-args.js new file mode 100644 index 00000000000000..0aed46223e92a0 --- /dev/null +++ b/test/fixtures/snapshot/echo-args.js @@ -0,0 +1,12 @@ +const { + setDeserializeMainFunction, +} = require('v8').startupSnapshot; + +const originalArgv = [...process.argv]; + +setDeserializeMainFunction(() => { + console.log(JSON.stringify({ + currentArgv: process.argv, + originalArgv + })); +}); From 2e9b27ae96a2e496921fc310fbbeb5cfdd81c4ea Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Tue, 24 Jan 2023 21:35:27 +0100 Subject: [PATCH 2/7] src: make build_snapshot a per-Isolate option, rather than a global one --- src/node.cc | 4 ++-- src/node_options.cc | 10 +++++----- src/node_options.h | 2 +- src/node_snapshotable.cc | 2 +- tools/snapshot/node_mksnapshot.cc | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/node.cc b/src/node.cc index c5b6c2b8444499..af680ed5b658d9 100644 --- a/src/node.cc +++ b/src/node.cc @@ -307,7 +307,7 @@ MaybeLocal StartExecution(Environment* env, StartExecutionCallback cb) { return StartExecution(env, "internal/main/inspect"); } - if (per_process::cli_options->build_snapshot) { + if (env->isolate_data()->options()->build_snapshot) { return StartExecution(env, "internal/main/mksnapshot"); } @@ -1221,7 +1221,7 @@ static ExitCode StartInternal(int argc, char** argv) { uv_loop_configure(uv_default_loop(), UV_METRICS_IDLE_TIME); // --build-snapshot indicates that we are in snapshot building mode. - if (per_process::cli_options->build_snapshot) { + if (per_process::cli_options->per_isolate->build_snapshot) { if (result->args().size() < 2) { fprintf(stderr, "--build-snapshot must be used with an entry point script.\n" diff --git a/src/node_options.cc b/src/node_options.cc index af2b79ad0b86fd..e9c5ac2b190728 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -777,6 +777,11 @@ PerIsolateOptionsParser::PerIsolateOptionsParser( Implies("--experimental-shadow-realm", "--harmony-shadow-realm"); Implies("--harmony-shadow-realm", "--experimental-shadow-realm"); ImpliesNot("--no-harmony-shadow-realm", "--experimental-shadow-realm"); + AddOption("--build-snapshot", + "Generate a snapshot blob when the process exits." + " Currently only supported in the node_mksnapshot binary.", + &PerIsolateOptions::build_snapshot, + kDisallowedInEnvvar); Insert(eop, &PerIsolateOptions::get_per_env_options); } @@ -815,11 +820,6 @@ PerProcessOptionsParser::PerProcessOptionsParser( "disable Object.prototype.__proto__", &PerProcessOptions::disable_proto, kAllowedInEnvvar); - AddOption("--build-snapshot", - "Generate a snapshot blob when the process exits." - " Currently only supported in the node_mksnapshot binary.", - &PerProcessOptions::build_snapshot, - kDisallowedInEnvvar); AddOption("--node-snapshot", "", // It's a debug-only option. &PerProcessOptions::node_snapshot, diff --git a/src/node_options.h b/src/node_options.h index 7d2c1f98560709..07ec47d861c77f 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -224,6 +224,7 @@ class PerIsolateOptions : public Options { bool report_on_signal = false; bool experimental_shadow_realm = false; std::string report_signal = "SIGUSR2"; + bool build_snapshot = false; inline EnvironmentOptions* get_per_env_options(); void CheckOptions(std::vector* errors, std::vector* argv) override; @@ -248,7 +249,6 @@ class PerProcessOptions : public Options { bool zero_fill_all_buffers = false; bool debug_arraybuffer_allocations = false; std::string disable_proto; - bool build_snapshot = false; // We enable the shared read-only heap which currently requires that the // snapshot used in different isolates in the same process to be the same. // Therefore --node-snapshot is a per-process option. diff --git a/src/node_snapshotable.cc b/src/node_snapshotable.cc index ee09b3e13240ab..49a1f45efacee5 100644 --- a/src/node_snapshotable.cc +++ b/src/node_snapshotable.cc @@ -1116,7 +1116,7 @@ ExitCode SnapshotBuilder::Generate(SnapshotData* out, // It's only possible to be kDefault in node_mksnapshot. SnapshotMetadata::Type snapshot_type = - per_process::cli_options->build_snapshot + per_process::cli_options->per_isolate->build_snapshot ? SnapshotMetadata::Type::kFullyCustomized : SnapshotMetadata::Type::kDefault; diff --git a/tools/snapshot/node_mksnapshot.cc b/tools/snapshot/node_mksnapshot.cc index 7e69b5053d15c2..d6d92ab156da62 100644 --- a/tools/snapshot/node_mksnapshot.cc +++ b/tools/snapshot/node_mksnapshot.cc @@ -72,7 +72,7 @@ int BuildSnapshot(int argc, char* argv[]) { CHECK_EQ(result->exit_code(), 0); std::string out_path; - if (node::per_process::cli_options->build_snapshot) { + if (node::per_process::cli_options->per_isolate->build_snapshot) { out_path = result->args()[2]; } else { out_path = result->args()[1]; From 598221af42cab911d6f9a71b6381581d90459a27 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Tue, 24 Jan 2023 21:35:52 +0100 Subject: [PATCH 3/7] src: allow snapshotting from the embedder API --- lib/internal/main/mksnapshot.js | 27 +++-- src/api/embed_helpers.cc | 95 +++++++++++++++-- src/env-inl.h | 10 ++ src/env.h | 8 ++ src/node.cc | 17 +++- src/node.h | 32 +++++- src/node_main_instance.cc | 35 ------- src/node_main_instance.h | 27 ----- src/node_snapshot_builder.h | 7 +- src/node_snapshotable.cc | 170 +++++++++++++++---------------- test/embedding/embedtest.cc | 38 +++++-- test/embedding/test-embedding.js | 25 +++-- 12 files changed, 300 insertions(+), 191 deletions(-) diff --git a/lib/internal/main/mksnapshot.js b/lib/internal/main/mksnapshot.js index d7ad9324114637..6607d34138b910 100644 --- a/lib/internal/main/mksnapshot.js +++ b/lib/internal/main/mksnapshot.js @@ -12,6 +12,7 @@ const { const binding = internalBinding('mksnapshot'); const { BuiltinModule } = require('internal/bootstrap/loaders'); const { + getEmbedderEntryFunction, compileSerializeMain, } = binding; @@ -119,14 +120,21 @@ function main() { prepareMainThreadExecution } = require('internal/process/pre_execution'); - prepareMainThreadExecution(true, false); + let serializeMainFunction = getEmbedderEntryFunction(); + const serializeMainArgs = [requireForUserSnapshot]; - const file = process.argv[1]; - const path = require('path'); - const filename = path.resolve(file); - const dirname = path.dirname(filename); - const source = readFileSync(file, 'utf-8'); - const serializeMainFunction = compileSerializeMain(filename, source); + if (serializeMainFunction) { // embedded case + prepareMainThreadExecution(false, false); + } else { + prepareMainThreadExecution(true, false); + const file = process.argv[1]; + const path = require('path'); + const filename = path.resolve(file); + const dirname = path.dirname(filename); + const source = readFileSync(file, 'utf-8'); + serializeMainFunction = compileSerializeMain(filename, source); + serializeMainArgs.push(filename, dirname); + } const { initializeCallbacks, @@ -146,10 +154,9 @@ function main() { if (getOptionValue('--inspect-brk')) { internalBinding('inspector').callAndPauseOnStart( - serializeMainFunction, undefined, - requireForUserSnapshot, filename, dirname); + serializeMainFunction, undefined, ...serializeMainArgs); } else { - serializeMainFunction(requireForUserSnapshot, filename, dirname); + serializeMainFunction(...serializeMainArgs); } addSerializeCallback(() => { diff --git a/src/api/embed_helpers.cc b/src/api/embed_helpers.cc index 3944890e443885..d25239d4f3f5a3 100644 --- a/src/api/embed_helpers.cc +++ b/src/api/embed_helpers.cc @@ -14,6 +14,7 @@ using v8::Locker; using v8::Maybe; using v8::Nothing; using v8::SealHandleScope; +using v8::SnapshotCreator; namespace node { @@ -78,16 +79,18 @@ struct CommonEnvironmentSetup::Impl { MultiIsolatePlatform* platform = nullptr; uv_loop_t loop; std::shared_ptr allocator; + std::optional snapshot_creator; Isolate* isolate = nullptr; DeleteFnPtr isolate_data; DeleteFnPtr env; - Global context; + Global main_context; }; CommonEnvironmentSetup::CommonEnvironmentSetup( MultiIsolatePlatform* platform, std::vector* errors, const EmbedderSnapshotData* snapshot_data, + bool is_snapshotting, std::function make_env) : impl_(new Impl()) { CHECK_NOT_NULL(platform); @@ -105,28 +108,42 @@ CommonEnvironmentSetup::CommonEnvironmentSetup( } loop->data = this; - impl_->allocator = ArrayBufferAllocator::Create(); - impl_->isolate = - NewIsolate(impl_->allocator, &impl_->loop, platform, snapshot_data); - Isolate* isolate = impl_->isolate; + Isolate* isolate; + if (is_snapshotting) { + const std::vector& external_references = + SnapshotBuilder::CollectExternalReferences(); + isolate = impl_->isolate = Isolate::Allocate(); + // Must be done before the SnapshotCreator creation so that the + // memory reducer can be initialized. + platform->RegisterIsolate(isolate, loop); + impl_->snapshot_creator.emplace(isolate, external_references.data()); + isolate->SetCaptureStackTraceForUncaughtExceptions( + true, 10, v8::StackTrace::StackTraceOptions::kDetailed); + SetIsolateMiscHandlers(isolate, {}); + } else { + impl_->allocator = ArrayBufferAllocator::Create(); + isolate = impl_->isolate = + NewIsolate(impl_->allocator, &impl_->loop, platform, snapshot_data); + } { Locker locker(isolate); Isolate::Scope isolate_scope(isolate); impl_->isolate_data.reset(CreateIsolateData( isolate, loop, platform, impl_->allocator.get(), snapshot_data)); + impl_->isolate_data->options()->build_snapshot = is_snapshotting; HandleScope handle_scope(isolate); if (snapshot_data) { impl_->env.reset(make_env(this)); if (impl_->env) { - impl_->context.Reset(isolate, impl_->env->context()); + impl_->main_context.Reset(isolate, impl_->env->context()); } return; } Local context = NewContext(isolate); - impl_->context.Reset(isolate, context); + impl_->main_context.Reset(isolate, context); if (context.IsEmpty()) { errors->push_back("Failed to initialize V8 Context"); return; @@ -141,7 +158,37 @@ CommonEnvironmentSetup::CommonEnvironmentSetup( MultiIsolatePlatform* platform, std::vector* errors, std::function make_env) - : CommonEnvironmentSetup(platform, errors, nullptr, make_env) {} + : CommonEnvironmentSetup(platform, errors, nullptr, false, make_env) {} + +std::unique_ptr +CommonEnvironmentSetup::CreateForSnapshotting( + MultiIsolatePlatform* platform, + std::vector* errors, + const std::vector& args, + const std::vector& exec_args) { + // It's not guaranteed that a context that goes through + // v8_inspector::V8Inspector::contextCreated() is runtime-independent, + // so do not start the inspector on the main context when building + // the default snapshot. + uint64_t env_flags = + EnvironmentFlags::kDefaultFlags | EnvironmentFlags::kNoCreateInspector; + + auto ret = std::unique_ptr(new CommonEnvironmentSetup( + platform, + errors, + nullptr, + true, + [&](const CommonEnvironmentSetup* setup) -> Environment* { + return CreateEnvironment( + setup->isolate_data(), + setup->context(), + args, + exec_args, + static_cast(env_flags)); + })); + if (!errors->empty()) ret.reset(); + return ret; +} CommonEnvironmentSetup::~CommonEnvironmentSetup() { if (impl_->isolate != nullptr) { @@ -150,7 +197,7 @@ CommonEnvironmentSetup::~CommonEnvironmentSetup() { Locker locker(isolate); Isolate::Scope isolate_scope(isolate); - impl_->context.Reset(); + impl_->main_context.Reset(); impl_->env.reset(); impl_->isolate_data.reset(); } @@ -160,7 +207,10 @@ CommonEnvironmentSetup::~CommonEnvironmentSetup() { *static_cast(data) = true; }, &platform_finished); impl_->platform->UnregisterIsolate(isolate); - isolate->Dispose(); + if (impl_->snapshot_creator.has_value()) + impl_->snapshot_creator.reset(); + else + isolate->Dispose(); // Wait until the platform has cleaned up all relevant resources. while (!platform_finished) @@ -173,6 +223,21 @@ CommonEnvironmentSetup::~CommonEnvironmentSetup() { delete impl_; } +EmbedderSnapshotData::Pointer CommonEnvironmentSetup::CreateSnapshot() { + CHECK_NOT_NULL(snapshot_creator()); + SnapshotData* snapshot_data = new SnapshotData(); + EmbedderSnapshotData::Pointer result{ + new EmbedderSnapshotData(snapshot_data, true)}; + + auto exit_code = SnapshotBuilder::CreateSnapshot( + snapshot_data, + this, + static_cast(SnapshotMetadata::Type::kFullyCustomized)); + if (exit_code != ExitCode::kNoFailure) return {}; + + return result; +} + Maybe SpinEventLoop(Environment* env) { Maybe result = SpinEventLoopInternal(env); if (result.IsNothing()) { @@ -203,7 +268,11 @@ Environment* CommonEnvironmentSetup::env() const { } v8::Local CommonEnvironmentSetup::context() const { - return impl_->context.Get(impl_->isolate); + return impl_->main_context.Get(impl_->isolate); +} + +v8::SnapshotCreator* CommonEnvironmentSetup::snapshot_creator() { + return impl_->snapshot_creator ? &impl_->snapshot_creator.value() : nullptr; } void EmbedderSnapshotData::DeleteSnapshotData::operator()( @@ -232,6 +301,10 @@ EmbedderSnapshotData::Pointer EmbedderSnapshotData::FromFile(FILE* in) { return result; } +void EmbedderSnapshotData::ToFile(FILE* out) const { + impl_->ToBlob(out); +} + EmbedderSnapshotData::EmbedderSnapshotData(const SnapshotData* impl, bool owns_impl) : impl_(impl), owns_impl_(owns_impl) {} diff --git a/src/env-inl.h b/src/env-inl.h index 98aa46142d21a8..2426b51cb2e1d4 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -444,6 +444,16 @@ inline builtins::BuiltinLoader* Environment::builtin_loader() { return &builtin_loader_; } +inline const Environment::EmbedderMksnapshotEntryFn& +Environment::embedder_mksnapshot_entry_point() const { + return embedder_mksnapshot_entry_point_; +} + +inline void Environment::set_embedder_mksnapshot_entry_point( + Environment::EmbedderMksnapshotEntryFn&& fn) { + embedder_mksnapshot_entry_point_ = std::move(fn); +} + inline double Environment::new_async_id() { async_hooks()->async_id_fields()[AsyncHooks::kAsyncIdCounter] += 1; return async_hooks()->async_id_fields()[AsyncHooks::kAsyncIdCounter]; diff --git a/src/env.h b/src/env.h index b18e4f2cb23318..88ff7b62b9b7e0 100644 --- a/src/env.h +++ b/src/env.h @@ -961,6 +961,13 @@ class Environment : public MemoryRetainer { #endif // HAVE_INSPECTOR + using EmbedderMksnapshotEntryFn = + std::function)>; + inline const EmbedderMksnapshotEntryFn& embedder_mksnapshot_entry_point() + const; + inline void set_embedder_mksnapshot_entry_point( + EmbedderMksnapshotEntryFn&& fn); + inline void set_process_exit_handler( std::function&& handler); @@ -1144,6 +1151,7 @@ class Environment : public MemoryRetainer { std::unique_ptr principal_realm_ = nullptr; builtins::BuiltinLoader builtin_loader_; + EmbedderMksnapshotEntryFn embedder_mksnapshot_entry_point_; // Used by allocate_managed_buffer() and release_managed_buffer() to keep // track of the BackingStore for a given pointer. diff --git a/src/node.cc b/src/node.cc index af680ed5b658d9..3870a280888ad5 100644 --- a/src/node.cc +++ b/src/node.cc @@ -127,6 +127,7 @@ namespace node { using v8::EscapableHandleScope; +using v8::Function; using v8::Isolate; using v8::Local; using v8::MaybeLocal; @@ -276,13 +277,25 @@ MaybeLocal StartExecution(Environment* env, StartExecutionCallback cb) { if (cb != nullptr) { EscapableHandleScope scope(env->isolate()); - if (StartExecution(env, "internal/main/environment").IsEmpty()) return {}; - StartExecutionCallbackInfo info = { env->process_object(), env->builtin_module_require(), }; + if (env->isolate_data()->options()->build_snapshot) { + // TODO(addaleax): pass the callback to the main script more directly, + // e.g. by making StartExecution(env, builtin) parametrizable + env->set_embedder_mksnapshot_entry_point([&](Local require_fn) { + info.native_require = require_fn; + cb(info); + }); + auto reset_entry_point = + OnScopeLeave([&]() { env->set_embedder_mksnapshot_entry_point({}); }); + + return StartExecution(env, "internal/main/mksnapshot"); + } + + if (StartExecution(env, "internal/main/environment").IsEmpty()) return {}; return scope.EscapeMaybe(cb(info)); } diff --git a/src/node.h b/src/node.h index ce570e5273836f..d05c2cf8682241 100644 --- a/src/node.h +++ b/src/node.h @@ -511,11 +511,16 @@ class EmbedderSnapshotData { static Pointer BuiltinSnapshotData(); // Return an EmbedderSnapshotData object that is based on an input file. - // Calling this method will not consume but not close the FILE* handle. + // Calling this method will consume but not close the FILE* handle. // The FILE* handle can be closed immediately following this call. // If the snapshot is invalid, this returns an empty pointer. static Pointer FromFile(FILE* in); + // Write this EmbedderSnapshotData object to an output file. + // Calling this method will not close the FILE* handle. + // The FILE* handle can be closed immediately following this call. + void ToFile(FILE* out) const; + // Returns whether custom snapshots can be used. Currently, this means // that V8 was configured without the shared-readonly-heap feature. static bool CanUseCustomSnapshotPerIsolate(); @@ -532,6 +537,7 @@ class EmbedderSnapshotData { const SnapshotData* impl_; bool owns_impl_; friend struct SnapshotData; + friend class CommonEnvironmentSetup; }; // Overriding IsolateSettings may produce unexpected behavior @@ -823,7 +829,29 @@ class NODE_EXTERN CommonEnvironmentSetup { const EmbedderSnapshotData* snapshot_data, EnvironmentArgs&&... env_args); + // Create an embedding setup which will be used for creating a snapshot + // using CreateSnapshot(). + // + // This will create and attach a v8::SnapshotCreator to this instance, + // and the same restrictions apply to this instance that also apply to + // other V8 snapshotting environments. + // Not all Node.js APIs are supported in this case. Currently, there is + // no support for native/host objects other than Node.js builtins + // in the snapshot. + // + // Snapshots are an *experimental* feature. In particular, the embedder API + // exposed through this class is subject to change or removal between Node.js + // versions, including possible API and ABI breakage. + static std::unique_ptr CreateForSnapshotting( + MultiIsolatePlatform* platform, + std::vector* errors, + const std::vector& args = {}, + const std::vector& exec_args = {}); + EmbedderSnapshotData::Pointer CreateSnapshot(); + struct uv_loop_s* event_loop() const; + v8::SnapshotCreator* snapshot_creator(); + // Empty for snapshotting environments. std::shared_ptr array_buffer_allocator() const; v8::Isolate* isolate() const; IsolateData* isolate_data() const; @@ -846,6 +874,7 @@ class NODE_EXTERN CommonEnvironmentSetup { MultiIsolatePlatform*, std::vector*, const EmbedderSnapshotData*, + bool is_snapshotting, std::function); }; @@ -878,6 +907,7 @@ CommonEnvironmentSetup::CreateWithSnapshot( platform, errors, snapshot_data, + false, [&](const CommonEnvironmentSetup* setup) -> Environment* { return CreateEnvironment(setup->isolate_data(), setup->context(), diff --git a/src/node_main_instance.cc b/src/node_main_instance.cc index 81804656c51093..5a4d127ffe3e43 100644 --- a/src/node_main_instance.cc +++ b/src/node_main_instance.cc @@ -29,34 +29,6 @@ using v8::Isolate; using v8::Local; using v8::Locker; -NodeMainInstance::NodeMainInstance(Isolate* isolate, - uv_loop_t* event_loop, - MultiIsolatePlatform* platform, - const std::vector& args, - const std::vector& exec_args) - : args_(args), - exec_args_(exec_args), - array_buffer_allocator_(nullptr), - isolate_(isolate), - platform_(platform), - isolate_data_(nullptr), - snapshot_data_(nullptr) { - isolate_data_ = - std::make_unique(isolate_, event_loop, platform, nullptr); - - SetIsolateMiscHandlers(isolate_, {}); -} - -std::unique_ptr NodeMainInstance::Create( - Isolate* isolate, - uv_loop_t* event_loop, - MultiIsolatePlatform* platform, - const std::vector& args, - const std::vector& exec_args) { - return std::unique_ptr( - new NodeMainInstance(isolate, event_loop, platform, args, exec_args)); -} - NodeMainInstance::NodeMainInstance(const SnapshotData* snapshot_data, uv_loop_t* event_loop, MultiIsolatePlatform* platform, @@ -88,13 +60,6 @@ NodeMainInstance::NodeMainInstance(const SnapshotData* snapshot_data, isolate_params_->constraints.max_young_generation_size_in_bytes(); } -void NodeMainInstance::Dispose() { - // This should only be called on a main instance that does not own its - // isolate. - CHECK_NULL(isolate_params_); - platform_->DrainTasks(isolate_); -} - NodeMainInstance::~NodeMainInstance() { if (isolate_params_ == nullptr) { return; diff --git a/src/node_main_instance.h b/src/node_main_instance.h index 43b6a8d64e409f..2752ff4d0ebc07 100644 --- a/src/node_main_instance.h +++ b/src/node_main_instance.h @@ -22,33 +22,6 @@ struct SnapshotData; // We may be able to create an abstract class to reuse some of the routines. class NodeMainInstance { public: - // To create a main instance that does not own the isolate, - // The caller needs to do: - // - // Isolate* isolate = Isolate::Allocate(); - // platform->RegisterIsolate(isolate, loop); - // isolate->Initialize(...); - // isolate->Enter(); - // std::unique_ptr main_instance = - // NodeMainInstance::Create(isolate, loop, args, exec_args); - // - // When tearing it down: - // - // main_instance->Cleanup(); // While the isolate is entered - // isolate->Exit(); - // isolate->Dispose(); - // platform->UnregisterIsolate(isolate); - // - // After calling Dispose() the main_instance is no longer accessible. - static std::unique_ptr Create( - v8::Isolate* isolate, - uv_loop_t* event_loop, - MultiIsolatePlatform* platform, - const std::vector& args, - const std::vector& exec_args); - - void Dispose(); - // Create a main instance that owns the isolate NodeMainInstance(const SnapshotData* snapshot_data, uv_loop_t* event_loop, diff --git a/src/node_snapshot_builder.h b/src/node_snapshot_builder.h index 3367cd3d7a6c68..f8cd900b2bdaa4 100644 --- a/src/node_snapshot_builder.h +++ b/src/node_snapshot_builder.h @@ -31,9 +31,14 @@ class NODE_EXTERN_PRIVATE SnapshotBuilder { static void InitializeIsolateParams(const SnapshotData* data, v8::Isolate::CreateParams* params); - private: static const std::vector& CollectExternalReferences(); + static ExitCode CreateSnapshot( + SnapshotData* out, + CommonEnvironmentSetup* setup, + /*SnapshotMetadata::Type*/ uint8_t snapshot_type); + + private: static std::unique_ptr registry_; }; } // namespace node diff --git a/src/node_snapshotable.cc b/src/node_snapshotable.cc index 49a1f45efacee5..93b6178d31ce9a 100644 --- a/src/node_snapshotable.cc +++ b/src/node_snapshotable.cc @@ -32,6 +32,7 @@ using v8::FunctionCallbackInfo; using v8::HandleScope; using v8::Isolate; using v8::Local; +using v8::MaybeLocal; using v8::Object; using v8::ObjectTemplate; using v8::ScriptCompiler; @@ -1081,38 +1082,15 @@ void SnapshotBuilder::InitializeIsolateParams(const SnapshotData* data, ExitCode SnapshotBuilder::Generate(SnapshotData* out, const std::vector args, const std::vector exec_args) { - const std::vector& external_references = - CollectExternalReferences(); - Isolate* isolate = Isolate::Allocate(); - // Must be done before the SnapshotCreator creation so that the - // memory reducer can be initialized. - per_process::v8_platform.Platform()->RegisterIsolate(isolate, - uv_default_loop()); - - SnapshotCreator creator(isolate, external_references.data()); - - isolate->SetCaptureStackTraceForUncaughtExceptions( - true, 10, v8::StackTrace::StackTraceOptions::kDetailed); - - Environment* env = nullptr; - std::unique_ptr main_instance = - NodeMainInstance::Create(isolate, - uv_default_loop(), - per_process::v8_platform.Platform(), - args, - exec_args); - - // The cleanups should be done in case of an early exit due to errors. - auto cleanup = OnScopeLeave([&]() { - // Must be done while the snapshot creator isolate is entered i.e. the - // creator is still alive. The snapshot creator destructor will destroy - // the isolate. - if (env != nullptr) { - FreeEnvironment(env); - } - main_instance->Dispose(); - per_process::v8_platform.Platform()->UnregisterIsolate(isolate); - }); + std::vector errors; + auto setup = CommonEnvironmentSetup::CreateForSnapshotting( + per_process::v8_platform.Platform(), &errors, args, exec_args); + if (!setup) { + for (const std::string& err : errors) + fprintf(stderr, "%s: %s\n", args[0].c_str(), err.c_str()); + return ExitCode::kBootstrapFailure; + } + Isolate* isolate = setup->isolate(); // It's only possible to be kDefault in node_mksnapshot. SnapshotMetadata::Type snapshot_type = @@ -1131,6 +1109,51 @@ ExitCode SnapshotBuilder::Generate(SnapshotData* out, } }); + // Initialize the main instance context. + { + Context::Scope context_scope(setup->context()); + Environment* env = setup->env(); + + // If --build-snapshot is true, lib/internal/main/mksnapshot.js would be + // loaded via LoadEnvironment() to execute process.argv[1] as the entry + // point (we currently only support this kind of entry point, but we + // could also explore snapshotting other kinds of execution modes + // in the future). + if (snapshot_type == SnapshotMetadata::Type::kFullyCustomized) { +#if HAVE_INSPECTOR + env->InitializeInspector({}); +#endif + if (LoadEnvironment(env, StartExecutionCallback{}).IsEmpty()) { + return ExitCode::kGenericUserError; + } + // FIXME(joyeecheung): right now running the loop in the snapshot + // builder seems to introduces inconsistencies in JS land that need to + // be synchronized again after snapshot restoration. + ExitCode exit_code = + SpinEventLoopInternal(env).FromMaybe(ExitCode::kGenericUserError); + if (exit_code != ExitCode::kNoFailure) { + return exit_code; + } + } + } + } + + return CreateSnapshot(out, setup.get(), static_cast(snapshot_type)); +} + +ExitCode SnapshotBuilder::CreateSnapshot(SnapshotData* out, + CommonEnvironmentSetup* setup, + uint8_t snapshot_type_u8) { + SnapshotMetadata::Type snapshot_type = + static_cast(snapshot_type_u8); + Isolate* isolate = setup->isolate(); + Environment* env = setup->env(); + SnapshotCreator* creator = setup->snapshot_creator(); + + { + HandleScope scope(isolate); + Local main_context = setup->context(); + // The default context with only things created by V8. Local default_context = Context::New(isolate); @@ -1138,7 +1161,7 @@ ExitCode SnapshotBuilder::Generate(SnapshotData* out, Local vm_context; { Local global_template = - main_instance->isolate_data()->contextify_global_template(); + setup->isolate_data()->contextify_global_template(); CHECK(!global_template.IsEmpty()); if (!contextify::ContextifyContext::CreateV8Context( isolate, global_template, nullptr, nullptr) @@ -1156,63 +1179,17 @@ ExitCode SnapshotBuilder::Generate(SnapshotData* out, } ResetContextSettingsBeforeSnapshot(base_context); - Local main_context = NewContext(isolate); - if (main_context.IsEmpty()) { - return ExitCode::kBootstrapFailure; - } - // Initialize the main instance context. { Context::Scope context_scope(main_context); - // Create the environment. - // It's not guaranteed that a context that goes through - // v8_inspector::V8Inspector::contextCreated() is runtime-independent, - // so do not start the inspector on the main context when building - // the default snapshot. - uint64_t env_flags = EnvironmentFlags::kDefaultFlags | - EnvironmentFlags::kNoCreateInspector; - - env = CreateEnvironment(main_instance->isolate_data(), - main_context, - args, - exec_args, - static_cast(env_flags)); - - // This already ran scripts in lib/internal/bootstrap/, if it fails return - if (env == nullptr) { - return ExitCode::kBootstrapFailure; - } - // If --build-snapshot is true, lib/internal/main/mksnapshot.js would be - // loaded via LoadEnvironment() to execute process.argv[1] as the entry - // point (we currently only support this kind of entry point, but we - // could also explore snapshotting other kinds of execution modes - // in the future). - if (snapshot_type == SnapshotMetadata::Type::kFullyCustomized) { -#if HAVE_INSPECTOR - env->InitializeInspector({}); -#endif - if (LoadEnvironment(env, StartExecutionCallback{}).IsEmpty()) { - return ExitCode::kGenericUserError; - } - // FIXME(joyeecheung): right now running the loop in the snapshot - // builder seems to introduces inconsistencies in JS land that need to - // be synchronized again after snapshot restoration. - ExitCode exit_code = - SpinEventLoopInternal(env).FromMaybe(ExitCode::kGenericUserError); - if (exit_code != ExitCode::kNoFailure) { - return exit_code; - } - } - if (per_process::enabled_debug_list.enabled(DebugCategory::MKSNAPSHOT)) { env->ForEachRealm([](Realm* realm) { realm->PrintInfoForSnapshot(); }); printf("Environment = %p\n", env); } // Serialize the native states - out->isolate_data_info = - main_instance->isolate_data()->Serialize(&creator); - out->env_info = env->Serialize(&creator); + out->isolate_data_info = setup->isolate_data()->Serialize(creator); + out->env_info = env->Serialize(creator); #ifdef NODE_USE_NODE_CODE_CACHE // Regenerate all the code cache. @@ -1235,19 +1212,19 @@ ExitCode SnapshotBuilder::Generate(SnapshotData* out, // Global handles to the contexts can't be disposed before the // blob is created. So initialize all the contexts before adding them. // TODO(joyeecheung): figure out how to remove this restriction. - creator.SetDefaultContext(default_context); - size_t index = creator.AddContext(vm_context); + creator->SetDefaultContext(default_context); + size_t index = creator->AddContext(vm_context); CHECK_EQ(index, SnapshotData::kNodeVMContextIndex); - index = creator.AddContext(base_context); + index = creator->AddContext(base_context); CHECK_EQ(index, SnapshotData::kNodeBaseContextIndex); - index = creator.AddContext(main_context, - {SerializeNodeContextInternalFields, env}); + index = creator->AddContext(main_context, + {SerializeNodeContextInternalFields, env}); CHECK_EQ(index, SnapshotData::kNodeMainContextIndex); } // Must be out of HandleScope out->v8_snapshot_blob_data = - creator.CreateBlob(SnapshotCreator::FunctionCodeHandling::kKeep); + creator->CreateBlob(SnapshotCreator::FunctionCodeHandling::kKeep); // We must be able to rehash the blob when we restore it or otherwise // the hash seed would be fixed by V8, introducing a vulnerability. @@ -1448,6 +1425,22 @@ void SerializeSnapshotableObjects(Realm* realm, namespace mksnapshot { +void GetEmbedderEntryFunction(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Isolate* isolate = env->isolate(); + if (!env->embedder_mksnapshot_entry_point()) return; + MaybeLocal jsfn = Function::New( + isolate->GetCurrentContext(), + [](const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Local require_fn = args[0]; + CHECK(require_fn->IsFunction()); + CHECK(env->embedder_mksnapshot_entry_point()); + env->embedder_mksnapshot_entry_point()(require_fn.As()); + }); + if (!jsfn.IsEmpty()) args.GetReturnValue().Set(jsfn.ToLocalChecked()); +} + void CompileSerializeMain(const FunctionCallbackInfo& args) { CHECK(args[0]->IsString()); Local filename = args[0].As(); @@ -1501,6 +1494,8 @@ void Initialize(Local target, Local unused, Local context, void* priv) { + SetMethod( + context, target, "getEmbedderEntryFunction", GetEmbedderEntryFunction); SetMethod(context, target, "compileSerializeMain", CompileSerializeMain); SetMethod(context, target, "setSerializeCallback", SetSerializeCallback); SetMethod(context, target, "setDeserializeCallback", SetDeserializeCallback); @@ -1511,6 +1506,7 @@ void Initialize(Local target, } void RegisterExternalReferences(ExternalReferenceRegistry* registry) { + registry->Register(GetEmbedderEntryFunction); registry->Register(CompileSerializeMain); registry->Register(SetSerializeCallback); registry->Register(SetDeserializeCallback); diff --git a/test/embedding/embedtest.cc b/test/embedding/embedtest.cc index ea3af217319ef7..8ccfcda7b9396d 100644 --- a/test/embedding/embedtest.cc +++ b/test/embedding/embedtest.cc @@ -1,3 +1,6 @@ +#ifdef NDEBUG +#undef NDEBUG +#endif #include "node.h" #include "uv.h" #include @@ -58,9 +61,12 @@ int RunNodeInstance(MultiIsolatePlatform* platform, int exit_code = 0; node::EmbedderSnapshotData::Pointer snapshot; + auto snapshot_build_mode_it = + std::find(args.begin(), args.end(), "--embedder-snapshot-create"); auto snapshot_arg_it = std::find(args.begin(), args.end(), "--embedder-snapshot-blob"); - if (snapshot_arg_it < args.end() - 1) { + if (snapshot_arg_it < args.end() - 1 && + snapshot_build_mode_it == args.end()) { FILE* fp = fopen((snapshot_arg_it + 1)->c_str(), "r"); assert(fp != nullptr); snapshot = node::EmbedderSnapshotData::FromFile(fp); @@ -69,9 +75,11 @@ int RunNodeInstance(MultiIsolatePlatform* platform, std::vector errors; std::unique_ptr setup = - snapshot - ? CommonEnvironmentSetup::CreateWithSnapshot( - platform, &errors, snapshot.get(), args, exec_args) + snapshot ? CommonEnvironmentSetup::CreateWithSnapshot( + platform, &errors, snapshot.get(), args, exec_args) + : snapshot_build_mode_it != args.end() + ? CommonEnvironmentSetup::CreateForSnapshotting( + platform, &errors, args, exec_args) : CommonEnvironmentSetup::Create(platform, &errors, args, exec_args); if (!setup) { for (const std::string& err : errors) @@ -94,9 +102,12 @@ int RunNodeInstance(MultiIsolatePlatform* platform, } else { loadenv_ret = node::LoadEnvironment( env, - "const publicRequire =" - " require('module').createRequire(process.cwd() + '/');" - "globalThis.require = publicRequire;" + // Snapshots do not support fs require()s (yet) + "if (!require('v8').startupSnapshot.isBuildingSnapshot()) {" + " const publicRequire =" + " require('module').createRequire(process.cwd() + '/');" + " globalThis.require = publicRequire;" + "} else globalThis.require = require;" "globalThis.embedVars = { nön_ascıı: '🏳️‍🌈' };" "require('vm').runInThisContext(process.argv[1]);"); } @@ -105,9 +116,20 @@ int RunNodeInstance(MultiIsolatePlatform* platform, return 1; exit_code = node::SpinEventLoop(env).FromMaybe(1); + } + + if (snapshot_arg_it < args.end() - 1 && + snapshot_build_mode_it != args.end()) { + snapshot = setup->CreateSnapshot(); + assert(snapshot); - node::Stop(env); + FILE* fp = fopen((snapshot_arg_it + 1)->c_str(), "w"); + assert(fp != nullptr); + snapshot->ToFile(fp); + fclose(fp); } + node::Stop(env); + return exit_code; } diff --git a/test/embedding/test-embedding.js b/test/embedding/test-embedding.js index c21c39b28e99dd..86dafc9c3b5d48 100644 --- a/test/embedding/test-embedding.js +++ b/test/embedding/test-embedding.js @@ -20,7 +20,6 @@ function resolveBuiltBinary(bin) { } const binary = resolveBuiltBinary('embedtest'); -const standaloneNodeBinary = resolveBuiltBinary('node'); assert.strictEqual( child_process.spawnSync(binary, ['console.log(42)']) @@ -53,32 +52,40 @@ assert.strictEqual( // Basic snapshot support { - const snapshotFixture = fixtures.path('snapshot', 'echo-args.js'); + // readSync + eval since snapshots don't support userland require() (yet) + const snapshotFixture = fixtures.readSync(['snapshot', 'echo-args.js'], 'utf8'); const blobPath = path.join(tmpdir.path, 'embedder-snapshot.blob'); - const buildSnapshotArgs = [snapshotFixture, 'arg1', 'arg2']; + const buildSnapshotArgs = [ + `eval(${JSON.stringify(snapshotFixture)})`, 'arg1', 'arg2', + '--embedder-snapshot-blob', blobPath, '--embedder-snapshot-create', + ]; const runEmbeddedArgs = ['--embedder-snapshot-blob', blobPath, 'arg3', 'arg4']; fs.rmSync(blobPath, { force: true }); - assert.strictEqual(child_process.spawnSync(standaloneNodeBinary, [ - '--snapshot-blob', blobPath, '--build-snapshot', ...buildSnapshotArgs, + assert.strictEqual(child_process.spawnSync(binary, [ + '--', ...buildSnapshotArgs, ], { cwd: tmpdir.path, }).status, 0); const spawnResult = child_process.spawnSync(binary, ['--', ...runEmbeddedArgs]); assert.deepStrictEqual(JSON.parse(spawnResult.stdout), { - originalArgv: [standaloneNodeBinary, ...buildSnapshotArgs], + originalArgv: [binary, ...buildSnapshotArgs], currentArgv: [binary, ...runEmbeddedArgs], }); } // Create workers and vm contexts after deserialization { - const snapshotFixture = fixtures.path('snapshot', 'create-worker-and-vm.js'); + const snapshotFixture = fixtures.readSync(['snapshot', 'create-worker-and-vm.js'], 'utf8'); const blobPath = path.join(tmpdir.path, 'embedder-snapshot.blob'); + const buildSnapshotArgs = [ + `eval(${JSON.stringify(snapshotFixture)})`, + '--embedder-snapshot-blob', blobPath, '--embedder-snapshot-create', + ]; fs.rmSync(blobPath, { force: true }); - assert.strictEqual(child_process.spawnSync(standaloneNodeBinary, [ - '--snapshot-blob', blobPath, '--build-snapshot', snapshotFixture, + assert.strictEqual(child_process.spawnSync(binary, [ + '--', ...buildSnapshotArgs, ], { cwd: tmpdir.path, }).status, 0); From 7e72ecc536584e86ca4bea82558ef6c1d0780dd5 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Thu, 2 Feb 2023 12:25:32 +0100 Subject: [PATCH 4/7] fixup! src: allow snapshotting from the embedder API --- src/api/embed_helpers.cc | 7 ++++--- src/node.h | 10 ++++++++-- test/embedding/embedtest.cc | 2 +- test/embedding/test-embedding.js | 12 ++++++++---- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/api/embed_helpers.cc b/src/api/embed_helpers.cc index d25239d4f3f5a3..5e65cc0011cd7c 100644 --- a/src/api/embed_helpers.cc +++ b/src/api/embed_helpers.cc @@ -90,7 +90,7 @@ CommonEnvironmentSetup::CommonEnvironmentSetup( MultiIsolatePlatform* platform, std::vector* errors, const EmbedderSnapshotData* snapshot_data, - bool is_snapshotting, + uint32_t flags, std::function make_env) : impl_(new Impl()) { CHECK_NOT_NULL(platform); @@ -109,7 +109,7 @@ CommonEnvironmentSetup::CommonEnvironmentSetup( loop->data = this; Isolate* isolate; - if (is_snapshotting) { + if (flags & Flags::kIsForSnapshotting) { const std::vector& external_references = SnapshotBuilder::CollectExternalReferences(); isolate = impl_->isolate = Isolate::Allocate(); @@ -131,7 +131,8 @@ CommonEnvironmentSetup::CommonEnvironmentSetup( Isolate::Scope isolate_scope(isolate); impl_->isolate_data.reset(CreateIsolateData( isolate, loop, platform, impl_->allocator.get(), snapshot_data)); - impl_->isolate_data->options()->build_snapshot = is_snapshotting; + impl_->isolate_data->options()->build_snapshot = + !!(flags & Flags::kIsForSnapshotting); HandleScope handle_scope(isolate); if (snapshot_data) { diff --git a/src/node.h b/src/node.h index d05c2cf8682241..1bfb909e1797ec 100644 --- a/src/node.h +++ b/src/node.h @@ -864,8 +864,14 @@ class NODE_EXTERN CommonEnvironmentSetup { CommonEnvironmentSetup& operator=(CommonEnvironmentSetup&&) = delete; private: + enum Flags : uint32_t { + kNoFlags = 0, + kIsForSnapshotting = 1, + }; + struct Impl; Impl* impl_; + CommonEnvironmentSetup( MultiIsolatePlatform*, std::vector*, @@ -874,7 +880,7 @@ class NODE_EXTERN CommonEnvironmentSetup { MultiIsolatePlatform*, std::vector*, const EmbedderSnapshotData*, - bool is_snapshotting, + uint32_t flags, std::function); }; @@ -907,7 +913,7 @@ CommonEnvironmentSetup::CreateWithSnapshot( platform, errors, snapshot_data, - false, + Flags::kNoFlags, [&](const CommonEnvironmentSetup* setup) -> Environment* { return CreateEnvironment(setup->isolate_data(), setup->context(), diff --git a/test/embedding/embedtest.cc b/test/embedding/embedtest.cc index 8ccfcda7b9396d..528509e31cc3c5 100644 --- a/test/embedding/embedtest.cc +++ b/test/embedding/embedtest.cc @@ -102,7 +102,7 @@ int RunNodeInstance(MultiIsolatePlatform* platform, } else { loadenv_ret = node::LoadEnvironment( env, - // Snapshots do not support fs require()s (yet) + // Snapshots do not support userland require()s (yet) "if (!require('v8').startupSnapshot.isBuildingSnapshot()) {" " const publicRequire =" " require('module').createRequire(process.cwd() + '/');" diff --git a/test/embedding/test-embedding.js b/test/embedding/test-embedding.js index 86dafc9c3b5d48..197158e78f2e03 100644 --- a/test/embedding/test-embedding.js +++ b/test/embedding/test-embedding.js @@ -50,13 +50,17 @@ assert.strictEqual( child_process.spawnSync(binary, [`require(${fixturePath})`, 92]).status, 92); +function getReadFileCodeForPath(path) { + return `(require("fs").readFileSync(${JSON.stringify(path)}, "utf8"))`; +} + // Basic snapshot support { // readSync + eval since snapshots don't support userland require() (yet) - const snapshotFixture = fixtures.readSync(['snapshot', 'echo-args.js'], 'utf8'); + const snapshotFixture = fixtures.path('snapshot', 'echo-args.js'); const blobPath = path.join(tmpdir.path, 'embedder-snapshot.blob'); const buildSnapshotArgs = [ - `eval(${JSON.stringify(snapshotFixture)})`, 'arg1', 'arg2', + `eval(${getReadFileCodeForPath(snapshotFixture)})`, 'arg1', 'arg2', '--embedder-snapshot-blob', blobPath, '--embedder-snapshot-create', ]; const runEmbeddedArgs = ['--embedder-snapshot-blob', blobPath, 'arg3', 'arg4']; @@ -76,10 +80,10 @@ assert.strictEqual( // Create workers and vm contexts after deserialization { - const snapshotFixture = fixtures.readSync(['snapshot', 'create-worker-and-vm.js'], 'utf8'); + const snapshotFixture = fixtures.path('snapshot', 'create-worker-and-vm.js'); const blobPath = path.join(tmpdir.path, 'embedder-snapshot.blob'); const buildSnapshotArgs = [ - `eval(${JSON.stringify(snapshotFixture)})`, + `eval(${getReadFileCodeForPath(snapshotFixture)})`, '--embedder-snapshot-blob', blobPath, '--embedder-snapshot-create', ]; From a4981baf63a135f361d648b461a61a74d2731d85 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Thu, 2 Feb 2023 12:32:56 +0100 Subject: [PATCH 5/7] fixup! src: allow snapshotting from the embedder API --- src/env-inl.h | 4 ++-- src/env.h | 10 +++------- src/node.cc | 16 +++++----------- src/node_snapshotable.cc | 3 ++- 4 files changed, 12 insertions(+), 21 deletions(-) diff --git a/src/env-inl.h b/src/env-inl.h index 2426b51cb2e1d4..0ca495bd58df6f 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -444,13 +444,13 @@ inline builtins::BuiltinLoader* Environment::builtin_loader() { return &builtin_loader_; } -inline const Environment::EmbedderMksnapshotEntryFn& +inline const StartExecutionCallback& Environment::embedder_mksnapshot_entry_point() const { return embedder_mksnapshot_entry_point_; } inline void Environment::set_embedder_mksnapshot_entry_point( - Environment::EmbedderMksnapshotEntryFn&& fn) { + StartExecutionCallback&& fn) { embedder_mksnapshot_entry_point_ = std::move(fn); } diff --git a/src/env.h b/src/env.h index 88ff7b62b9b7e0..0d432378322ebc 100644 --- a/src/env.h +++ b/src/env.h @@ -961,12 +961,8 @@ class Environment : public MemoryRetainer { #endif // HAVE_INSPECTOR - using EmbedderMksnapshotEntryFn = - std::function)>; - inline const EmbedderMksnapshotEntryFn& embedder_mksnapshot_entry_point() - const; - inline void set_embedder_mksnapshot_entry_point( - EmbedderMksnapshotEntryFn&& fn); + inline const StartExecutionCallback& embedder_mksnapshot_entry_point() const; + inline void set_embedder_mksnapshot_entry_point(StartExecutionCallback&& fn); inline void set_process_exit_handler( std::function&& handler); @@ -1151,7 +1147,7 @@ class Environment : public MemoryRetainer { std::unique_ptr principal_realm_ = nullptr; builtins::BuiltinLoader builtin_loader_; - EmbedderMksnapshotEntryFn embedder_mksnapshot_entry_point_; + StartExecutionCallback embedder_mksnapshot_entry_point_; // Used by allocate_managed_buffer() and release_managed_buffer() to keep // track of the BackingStore for a given pointer. diff --git a/src/node.cc b/src/node.cc index 3870a280888ad5..c9fb6afd05b178 100644 --- a/src/node.cc +++ b/src/node.cc @@ -127,7 +127,6 @@ namespace node { using v8::EscapableHandleScope; -using v8::Function; using v8::Isolate; using v8::Local; using v8::MaybeLocal; @@ -277,18 +276,10 @@ MaybeLocal StartExecution(Environment* env, StartExecutionCallback cb) { if (cb != nullptr) { EscapableHandleScope scope(env->isolate()); - StartExecutionCallbackInfo info = { - env->process_object(), - env->builtin_module_require(), - }; - if (env->isolate_data()->options()->build_snapshot) { // TODO(addaleax): pass the callback to the main script more directly, // e.g. by making StartExecution(env, builtin) parametrizable - env->set_embedder_mksnapshot_entry_point([&](Local require_fn) { - info.native_require = require_fn; - cb(info); - }); + env->set_embedder_mksnapshot_entry_point(std::move(cb)); auto reset_entry_point = OnScopeLeave([&]() { env->set_embedder_mksnapshot_entry_point({}); }); @@ -296,7 +287,10 @@ MaybeLocal StartExecution(Environment* env, StartExecutionCallback cb) { } if (StartExecution(env, "internal/main/environment").IsEmpty()) return {}; - return scope.EscapeMaybe(cb(info)); + return scope.EscapeMaybe(cb({ + env->process_object(), + env->builtin_module_require(), + })); } // TODO(joyeecheung): move these conditions into JS land and let the diff --git a/src/node_snapshotable.cc b/src/node_snapshotable.cc index 93b6178d31ce9a..3d307e85f58a92 100644 --- a/src/node_snapshotable.cc +++ b/src/node_snapshotable.cc @@ -1436,7 +1436,8 @@ void GetEmbedderEntryFunction(const FunctionCallbackInfo& args) { Local require_fn = args[0]; CHECK(require_fn->IsFunction()); CHECK(env->embedder_mksnapshot_entry_point()); - env->embedder_mksnapshot_entry_point()(require_fn.As()); + env->embedder_mksnapshot_entry_point()( + {env->process_object(), require_fn.As()}); }); if (!jsfn.IsEmpty()) args.GetReturnValue().Set(jsfn.ToLocalChecked()); } From 689f6038c66aab9ce8f2de4a72810aa70639f658 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Thu, 2 Feb 2023 12:52:36 +0100 Subject: [PATCH 6/7] fixup! fixup! src: allow snapshotting from the embedder API --- src/node_snapshotable.cc | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/node_snapshotable.cc b/src/node_snapshotable.cc index 3d307e85f58a92..75c6db47072e8c 100644 --- a/src/node_snapshotable.cc +++ b/src/node_snapshotable.cc @@ -1429,16 +1429,16 @@ void GetEmbedderEntryFunction(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); Isolate* isolate = env->isolate(); if (!env->embedder_mksnapshot_entry_point()) return; - MaybeLocal jsfn = Function::New( - isolate->GetCurrentContext(), - [](const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - Local require_fn = args[0]; - CHECK(require_fn->IsFunction()); - CHECK(env->embedder_mksnapshot_entry_point()); - env->embedder_mksnapshot_entry_point()( - {env->process_object(), require_fn.As()}); - }); + MaybeLocal jsfn = + Function::New(isolate->GetCurrentContext(), + [](const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Local require_fn = args[0]; + CHECK(require_fn->IsFunction()); + CHECK(env->embedder_mksnapshot_entry_point()); + env->embedder_mksnapshot_entry_point()( + {env->process_object(), require_fn.As()}); + }); if (!jsfn.IsEmpty()) args.GetReturnValue().Set(jsfn.ToLocalChecked()); } From a136c282a9e454929dd6265bf35afefa97591f97 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Fri, 3 Feb 2023 16:44:26 +0100 Subject: [PATCH 7/7] fixup! fixup! fixup! src: allow snapshotting from the embedder API --- src/api/embed_helpers.cc | 2 +- src/node.h | 6 +++--- src/node_options.cc | 3 +-- test/embedding/embedtest.cc | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/api/embed_helpers.cc b/src/api/embed_helpers.cc index 5e65cc0011cd7c..02c4fdf1b53a44 100644 --- a/src/api/embed_helpers.cc +++ b/src/api/embed_helpers.cc @@ -132,7 +132,7 @@ CommonEnvironmentSetup::CommonEnvironmentSetup( impl_->isolate_data.reset(CreateIsolateData( isolate, loop, platform, impl_->allocator.get(), snapshot_data)); impl_->isolate_data->options()->build_snapshot = - !!(flags & Flags::kIsForSnapshotting); + impl_->snapshot_creator.has_value(); HandleScope handle_scope(isolate); if (snapshot_data) { diff --git a/src/node.h b/src/node.h index 1bfb909e1797ec..285eacf9f6afd4 100644 --- a/src/node.h +++ b/src/node.h @@ -823,7 +823,7 @@ class NODE_EXTERN CommonEnvironmentSetup { std::vector* errors, EnvironmentArgs&&... env_args); template - static std::unique_ptr CreateWithSnapshot( + static std::unique_ptr CreateFromSnapshot( MultiIsolatePlatform* platform, std::vector* errors, const EmbedderSnapshotData* snapshot_data, @@ -900,11 +900,11 @@ std::unique_ptr CommonEnvironmentSetup::Create( if (!errors->empty()) ret.reset(); return ret; } -// Implementation for ::CreateWithSnapshot -- the ::Create() method +// Implementation for ::CreateFromSnapshot -- the ::Create() method // could call this with a nullptr snapshot_data in a major version. template std::unique_ptr -CommonEnvironmentSetup::CreateWithSnapshot( +CommonEnvironmentSetup::CreateFromSnapshot( MultiIsolatePlatform* platform, std::vector* errors, const EmbedderSnapshotData* snapshot_data, diff --git a/src/node_options.cc b/src/node_options.cc index e9c5ac2b190728..c1f97a5d9207eb 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -778,8 +778,7 @@ PerIsolateOptionsParser::PerIsolateOptionsParser( Implies("--harmony-shadow-realm", "--experimental-shadow-realm"); ImpliesNot("--no-harmony-shadow-realm", "--experimental-shadow-realm"); AddOption("--build-snapshot", - "Generate a snapshot blob when the process exits." - " Currently only supported in the node_mksnapshot binary.", + "Generate a snapshot blob when the process exits.", &PerIsolateOptions::build_snapshot, kDisallowedInEnvvar); diff --git a/test/embedding/embedtest.cc b/test/embedding/embedtest.cc index 528509e31cc3c5..ef07056d5bd688 100644 --- a/test/embedding/embedtest.cc +++ b/test/embedding/embedtest.cc @@ -75,7 +75,7 @@ int RunNodeInstance(MultiIsolatePlatform* platform, std::vector errors; std::unique_ptr setup = - snapshot ? CommonEnvironmentSetup::CreateWithSnapshot( + snapshot ? CommonEnvironmentSetup::CreateFromSnapshot( platform, &errors, snapshot.get(), args, exec_args) : snapshot_build_mode_it != args.end() ? CommonEnvironmentSetup::CreateForSnapshotting(