diff --git a/src/api/embed_helpers.cc b/src/api/embed_helpers.cc index c1bcb3948c4c..929998a7f239 100644 --- a/src/api/embed_helpers.cc +++ b/src/api/embed_helpers.cc @@ -131,6 +131,9 @@ CommonEnvironmentSetup::CommonEnvironmentSetup( isolate = impl_->isolate = Isolate::Allocate(GetOrCreateIsolateGroup()); platform->RegisterIsolate(isolate, loop); + if (snapshot_config != nullptr && snapshot_config->base_blob != nullptr) { + params.snapshot_blob = snapshot_config->base_blob; + } impl_->snapshot_creator.emplace(isolate, params); isolate->SetCaptureStackTraceForUncaughtExceptions( true, diff --git a/src/node.h b/src/node.h index bf6537e3bfe6..8f366bfdd543 100644 --- a/src/node.h +++ b/src/node.h @@ -672,6 +672,13 @@ struct SnapshotConfig { // the snapshot builder can execute asynchronous operations as long as they // are run to completion when the snapshot is taken. std::optional builder_script_path; + + // A V8 startup blob (as produced by V8's mksnapshot) to build the snapshot + // on top of, instead of setting up the V8 heap from scratch. Needed when + // the V8 that Node.js is linked against can only deserialize (external + // startup data), and to keep the result on the same read-only heap lineage + // as the embedder's other isolates. Caller-owned; must outlive the setup. + const v8::StartupData* base_blob = nullptr; }; struct InspectorParentHandle { diff --git a/test/embedding/embedtest.cc b/test/embedding/embedtest.cc index 982eed74f954..045c01211cf2 100644 --- a/test/embedding/embedtest.cc +++ b/test/embedding/embedtest.cc @@ -28,6 +28,52 @@ static int RunNodeInstance(MultiIsolatePlatform* platform, const std::vector& args, const std::vector& exec_args); +// --create-v8-startup-blob : a plain V8 startup blob (what V8's +// mksnapshot produces), to test building the Node.js snapshot on top of one. +static int CreateV8StartupBlob(MultiIsolatePlatform* platform, + const std::string& path) { + std::unique_ptr allocator( + v8::ArrayBuffer::Allocator::NewDefaultAllocator()); + v8::Isolate::CreateParams params; + params.array_buffer_allocator = allocator.get(); + uv_loop_t loop; + assert(uv_loop_init(&loop) == 0); + v8::Isolate* isolate = v8::Isolate::Allocate(); + platform->RegisterIsolate(isolate, &loop); + v8::StartupData blob; + { + v8::SnapshotCreator creator(isolate, params); + { + v8::HandleScope handle_scope(isolate); + creator.SetDefaultContext(v8::Context::New(isolate)); + } + blob = + creator.CreateBlob(v8::SnapshotCreator::FunctionCodeHandling::kClear); + } + bool platform_finished = false; + platform->AddIsolateFinishedCallback( + isolate, + [](void* data) { + bool* finished = static_cast(data); + *finished = true; + }, + &platform_finished); + platform->DisposeIsolate(isolate); + while (!platform_finished) uv_run(&loop, UV_RUN_ONCE); + uv_loop_close(&loop); + assert(blob.data != nullptr); + FILE* fp = fopen(path.c_str(), "wb"); + assert(fp != nullptr); + size_t written = fwrite(blob.data, blob.raw_size, 1, fp); + assert(written == 1); + fclose(fp); + delete[] blob.data; + return 0; +} + +static std::vector base_blob_bytes; +static v8::StartupData base_blob{nullptr, 0}; + NODE_MAIN(int argc, node::argv_type raw_argv[]) { char** argv = nullptr; node::FixupMain(argc, raw_argv, &argv); @@ -112,6 +158,27 @@ int RunNodeInstance(MultiIsolatePlatform* platform, assert(i + 1 < args.size()); snapshot_blob_path = args[i + 1]; i++; + } else if (arg == "--create-v8-startup-blob") { + assert(i + 1 < args.size()); + return CreateV8StartupBlob(platform, args[i + 1]); + } else if (arg == "--embedder-snapshot-base-blob") { + assert(i + 1 < args.size()); + FILE* fp = fopen(args[i + 1].c_str(), "rb"); + assert(fp != nullptr); + fseek(fp, 0, SEEK_END); + base_blob_bytes.resize(ftell(fp)); + fseek(fp, 0, SEEK_SET); + size_t read = + fread(base_blob_bytes.data(), base_blob_bytes.size(), 1, fp); + assert(read == 1); + fclose(fp); + base_blob = {base_blob_bytes.data(), + static_cast(base_blob_bytes.size())}; + if (!snapshot_config.has_value()) { + snapshot_config = node::SnapshotConfig{}; + } + snapshot_config.value().base_blob = &base_blob; + i++; } else { filtered_args.push_back(arg); } diff --git a/test/embedding/test-embedding-snapshot-base-blob.js b/test/embedding/test-embedding-snapshot-base-blob.js new file mode 100644 index 000000000000..af67319297ff --- /dev/null +++ b/test/embedding/test-embedding-snapshot-base-blob.js @@ -0,0 +1,49 @@ +'use strict'; + +// SnapshotConfig::base_blob: the embedder snapshot can be built on top of an +// existing V8 startup blob instead of a heap set up from scratch. + +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const fs = require('fs'); +const fixtures = require('../common/fixtures'); +const { + spawnSyncAndAssert, + spawnSyncAndExitWithoutError, +} = require('../common/child_process'); + +const embedtest = common.resolveBuiltBinary('embedtest'); +const snapshotFixture = fixtures.path('snapshot', 'echo-args.js'); +const v8Blob = tmpdir.resolve('v8.blob'); +const nodeBlob = tmpdir.resolve('node-on-v8.blob'); +const buildSnapshotExecArgs = [ + `eval(require("fs").readFileSync(${JSON.stringify(snapshotFixture)}, "utf8"))`, + 'arg1', 'arg2', +]; + +tmpdir.refresh(); + +spawnSyncAndExitWithoutError(embedtest, ['--', '--create-v8-startup-blob', v8Blob], { cwd: tmpdir.path }); +assert.ok(fs.statSync(v8Blob).size > 0); + +spawnSyncAndExitWithoutError( + embedtest, + ['--', ...buildSnapshotExecArgs, '--embedder-snapshot-blob', nodeBlob, + '--embedder-snapshot-base-blob', v8Blob, '--embedder-snapshot-create'], + { cwd: tmpdir.path }); +assert.ok(fs.statSync(nodeBlob).size > fs.statSync(v8Blob).size); + +spawnSyncAndAssert( + embedtest, + ['--', 'arg3', 'arg4', '--embedder-snapshot-blob', nodeBlob], + { cwd: tmpdir.path }, + { + stdout(output) { + assert.deepStrictEqual(JSON.parse(output), { + originalArgv: [embedtest, '__node_anonymous_main', ...buildSnapshotExecArgs], + currentArgv: [embedtest, embedtest, 'arg3', 'arg4'], + }); + return true; + }, + }); diff --git a/tools/snapshot/node_mksnapshot.cc b/tools/snapshot/node_mksnapshot.cc index 0842cba257d0..05e264b390f7 100644 --- a/tools/snapshot/node_mksnapshot.cc +++ b/tools/snapshot/node_mksnapshot.cc @@ -53,18 +53,41 @@ int main(int argc, char* argv[]) { return BuildSnapshot(argc, argv); } +static const char kBaseBlobFlag[] = "--v8-snapshot-blob="; + int BuildSnapshot(int argc, char* argv[]) { - if (argc < 2) { - std::cerr << "Usage: " << argv[0] << " \n"; - std::cerr << " " << argv[0] << " --build-snapshot " + std::vector args(argv, argv + argc); + // --v8-snapshot-blob=: build on top of this V8 startup blob (for + // hosts whose V8 uses external startup data) instead of from scratch. + std::string base_blob_bytes; + v8::StartupData base_blob{nullptr, 0}; + for (auto it = args.begin(); it != args.end(); ++it) { + if (it->starts_with(kBaseBlobFlag)) { + std::string path = it->substr(sizeof(kBaseBlobFlag) - 1); + args.erase(it); + if (node::ReadFileSync(path.c_str(), &base_blob_bytes) != 0) { + std::cerr << "Cannot read V8 snapshot blob " << path << "\n"; + return 1; + } + base_blob = {base_blob_bytes.data(), + static_cast(base_blob_bytes.size())}; + break; + } + } + + if (args.size() < 2) { + std::cerr + << "Usage: " << argv[0] + << " [--v8-snapshot-blob=] \n"; + std::cerr << " " << argv[0] + << " [--v8-snapshot-blob=] --build-snapshot " << " \n"; return 1; } std::shared_ptr result = node::InitializeOncePerProcess( - std::vector(argv, argv + argc), - node::ProcessInitializationFlags::kGeneratePredictableSnapshot); + args, node::ProcessInitializationFlags::kGeneratePredictableSnapshot); if (result->exit_code() != 0) { for (const std::string& error : result->errors()) { @@ -94,6 +117,7 @@ int BuildSnapshot(int argc, char* argv[]) { node::SnapshotConfig snapshot_config; snapshot_config.builder_script_path = builder_script_path; + if (base_blob.data != nullptr) snapshot_config.base_blob = &base_blob; #ifdef NODE_USE_NODE_CODE_CACHE snapshot_config.flags = node::SnapshotFlags::kDefault;