Skip to content

Commit 7fb2600

Browse files
codebytereaduh95
authored andcommitted
src: allow building the snapshot on top of a V8 startup blob
CommonEnvironmentSetup::CreateForSnapshotting() always created the SnapshotCreator without a startup blob, so V8 set up its heap from scratch. That is impossible when the V8 that Node.js is linked against only carries the deserializer (external startup data, as Chromium builds it), and it puts the resulting snapshot on a different read-only heap lineage than isolates the embedder creates from its own blob. Embedders in that situation cannot use the snapshot support at all today. Add SnapshotConfig::base_blob, an optional caller-owned v8::StartupData that is handed to the SnapshotCreator so that V8 deserializes its heap from that blob and Node.js adds its isolate data and contexts on top, the way Blink's context snapshot is built. node_mksnapshot accepts --v8-snapshot-blob=<file> for hosts that build Node.js with such a V8; Node.js's own build never passes it and is unchanged. Consuming the result needs no changes. embedtest can create a plain V8 startup blob and build the embedder snapshot on top of one, and a test round-trips argv through a snapshot built that way. Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com> PR-URL: #65374 Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 286cfd6 commit 7fb2600

5 files changed

Lines changed: 155 additions & 5 deletions

File tree

src/api/embed_helpers.cc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ CommonEnvironmentSetup::CommonEnvironmentSetup(
131131
isolate = impl_->isolate = Isolate::Allocate(GetOrCreateIsolateGroup());
132132
platform->RegisterIsolate(isolate, loop);
133133

134+
if (snapshot_config != nullptr && snapshot_config->base_blob != nullptr) {
135+
params.snapshot_blob = snapshot_config->base_blob;
136+
}
134137
impl_->snapshot_creator.emplace(isolate, params);
135138
isolate->SetCaptureStackTraceForUncaughtExceptions(
136139
true,

src/node.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,13 @@ struct SnapshotConfig {
673673
// the snapshot builder can execute asynchronous operations as long as they
674674
// are run to completion when the snapshot is taken.
675675
std::optional<std::string> builder_script_path;
676+
677+
// A V8 startup blob (as produced by V8's mksnapshot) to build the snapshot
678+
// on top of, instead of setting up the V8 heap from scratch. Needed when
679+
// the V8 that Node.js is linked against can only deserialize (external
680+
// startup data), and to keep the result on the same read-only heap lineage
681+
// as the embedder's other isolates. Caller-owned; must outlive the setup.
682+
const v8::StartupData* base_blob = nullptr;
676683
};
677684

678685
struct InspectorParentHandle {

test/embedding/embedtest.cc

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,52 @@ static int RunNodeInstance(MultiIsolatePlatform* platform,
2828
const std::vector<std::string>& args,
2929
const std::vector<std::string>& exec_args);
3030

31+
// --create-v8-startup-blob <file>: a plain V8 startup blob (what V8's
32+
// mksnapshot produces), to test building the Node.js snapshot on top of one.
33+
static int CreateV8StartupBlob(MultiIsolatePlatform* platform,
34+
const std::string& path) {
35+
std::unique_ptr<v8::ArrayBuffer::Allocator> allocator(
36+
v8::ArrayBuffer::Allocator::NewDefaultAllocator());
37+
v8::Isolate::CreateParams params;
38+
params.array_buffer_allocator = allocator.get();
39+
uv_loop_t loop;
40+
assert(uv_loop_init(&loop) == 0);
41+
v8::Isolate* isolate = v8::Isolate::Allocate();
42+
platform->RegisterIsolate(isolate, &loop);
43+
v8::StartupData blob;
44+
{
45+
v8::SnapshotCreator creator(isolate, params);
46+
{
47+
v8::HandleScope handle_scope(isolate);
48+
creator.SetDefaultContext(v8::Context::New(isolate));
49+
}
50+
blob =
51+
creator.CreateBlob(v8::SnapshotCreator::FunctionCodeHandling::kClear);
52+
}
53+
bool platform_finished = false;
54+
platform->AddIsolateFinishedCallback(
55+
isolate,
56+
[](void* data) {
57+
bool* finished = static_cast<bool*>(data);
58+
*finished = true;
59+
},
60+
&platform_finished);
61+
platform->DisposeIsolate(isolate);
62+
while (!platform_finished) uv_run(&loop, UV_RUN_ONCE);
63+
uv_loop_close(&loop);
64+
assert(blob.data != nullptr);
65+
FILE* fp = fopen(path.c_str(), "wb");
66+
assert(fp != nullptr);
67+
size_t written = fwrite(blob.data, blob.raw_size, 1, fp);
68+
assert(written == 1);
69+
fclose(fp);
70+
delete[] blob.data;
71+
return 0;
72+
}
73+
74+
static std::vector<char> base_blob_bytes;
75+
static v8::StartupData base_blob{nullptr, 0};
76+
3177
NODE_MAIN(int argc, node::argv_type raw_argv[]) {
3278
char** argv = nullptr;
3379
node::FixupMain(argc, raw_argv, &argv);
@@ -112,6 +158,27 @@ int RunNodeInstance(MultiIsolatePlatform* platform,
112158
assert(i + 1 < args.size());
113159
snapshot_blob_path = args[i + 1];
114160
i++;
161+
} else if (arg == "--create-v8-startup-blob") {
162+
assert(i + 1 < args.size());
163+
return CreateV8StartupBlob(platform, args[i + 1]);
164+
} else if (arg == "--embedder-snapshot-base-blob") {
165+
assert(i + 1 < args.size());
166+
FILE* fp = fopen(args[i + 1].c_str(), "rb");
167+
assert(fp != nullptr);
168+
fseek(fp, 0, SEEK_END);
169+
base_blob_bytes.resize(ftell(fp));
170+
fseek(fp, 0, SEEK_SET);
171+
size_t read =
172+
fread(base_blob_bytes.data(), base_blob_bytes.size(), 1, fp);
173+
assert(read == 1);
174+
fclose(fp);
175+
base_blob = {base_blob_bytes.data(),
176+
static_cast<int>(base_blob_bytes.size())};
177+
if (!snapshot_config.has_value()) {
178+
snapshot_config = node::SnapshotConfig{};
179+
}
180+
snapshot_config.value().base_blob = &base_blob;
181+
i++;
115182
} else {
116183
filtered_args.push_back(arg);
117184
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
'use strict';
2+
3+
// SnapshotConfig::base_blob: the embedder snapshot can be built on top of an
4+
// existing V8 startup blob instead of a heap set up from scratch.
5+
6+
const common = require('../common');
7+
const tmpdir = require('../common/tmpdir');
8+
const assert = require('assert');
9+
const fs = require('fs');
10+
const fixtures = require('../common/fixtures');
11+
const {
12+
spawnSyncAndAssert,
13+
spawnSyncAndExitWithoutError,
14+
} = require('../common/child_process');
15+
16+
const embedtest = common.resolveBuiltBinary('embedtest');
17+
const snapshotFixture = fixtures.path('snapshot', 'echo-args.js');
18+
const v8Blob = tmpdir.resolve('v8.blob');
19+
const nodeBlob = tmpdir.resolve('node-on-v8.blob');
20+
const buildSnapshotExecArgs = [
21+
`eval(require("fs").readFileSync(${JSON.stringify(snapshotFixture)}, "utf8"))`,
22+
'arg1', 'arg2',
23+
];
24+
25+
tmpdir.refresh();
26+
27+
spawnSyncAndExitWithoutError(embedtest, ['--', '--create-v8-startup-blob', v8Blob], { cwd: tmpdir.path });
28+
assert.ok(fs.statSync(v8Blob).size > 0);
29+
30+
spawnSyncAndExitWithoutError(
31+
embedtest,
32+
['--', ...buildSnapshotExecArgs, '--embedder-snapshot-blob', nodeBlob,
33+
'--embedder-snapshot-base-blob', v8Blob, '--embedder-snapshot-create'],
34+
{ cwd: tmpdir.path });
35+
assert.ok(fs.statSync(nodeBlob).size > fs.statSync(v8Blob).size);
36+
37+
spawnSyncAndAssert(
38+
embedtest,
39+
['--', 'arg3', 'arg4', '--embedder-snapshot-blob', nodeBlob],
40+
{ cwd: tmpdir.path },
41+
{
42+
stdout(output) {
43+
assert.deepStrictEqual(JSON.parse(output), {
44+
originalArgv: [embedtest, '__node_anonymous_main', ...buildSnapshotExecArgs],
45+
currentArgv: [embedtest, embedtest, 'arg3', 'arg4'],
46+
});
47+
return true;
48+
},
49+
});

tools/snapshot/node_mksnapshot.cc

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,18 +53,41 @@ int main(int argc, char* argv[]) {
5353
return BuildSnapshot(argc, argv);
5454
}
5555

56+
static const char kBaseBlobFlag[] = "--v8-snapshot-blob=";
57+
5658
int BuildSnapshot(int argc, char* argv[]) {
57-
if (argc < 2) {
58-
std::cerr << "Usage: " << argv[0] << " <path/to/output.cc>\n";
59-
std::cerr << " " << argv[0] << " --build-snapshot "
59+
std::vector<std::string> args(argv, argv + argc);
60+
// --v8-snapshot-blob=<file>: build on top of this V8 startup blob (for
61+
// hosts whose V8 uses external startup data) instead of from scratch.
62+
std::string base_blob_bytes;
63+
v8::StartupData base_blob{nullptr, 0};
64+
for (auto it = args.begin(); it != args.end(); ++it) {
65+
if (it->starts_with(kBaseBlobFlag)) {
66+
std::string path = it->substr(sizeof(kBaseBlobFlag) - 1);
67+
args.erase(it);
68+
if (node::ReadFileSync(path.c_str(), &base_blob_bytes) != 0) {
69+
std::cerr << "Cannot read V8 snapshot blob " << path << "\n";
70+
return 1;
71+
}
72+
base_blob = {base_blob_bytes.data(),
73+
static_cast<int>(base_blob_bytes.size())};
74+
break;
75+
}
76+
}
77+
78+
if (args.size() < 2) {
79+
std::cerr
80+
<< "Usage: " << argv[0]
81+
<< " [--v8-snapshot-blob=<path/to/blob.bin>] <path/to/output.cc>\n";
82+
std::cerr << " " << argv[0]
83+
<< " [--v8-snapshot-blob=<path/to/blob.bin>] --build-snapshot "
6084
<< "<path/to/script.js> <path/to/output.cc>\n";
6185
return 1;
6286
}
6387

6488
std::shared_ptr<node::InitializationResult> result =
6589
node::InitializeOncePerProcess(
66-
std::vector<std::string>(argv, argv + argc),
67-
node::ProcessInitializationFlags::kGeneratePredictableSnapshot);
90+
args, node::ProcessInitializationFlags::kGeneratePredictableSnapshot);
6891

6992
if (result->exit_code() != 0) {
7093
for (const std::string& error : result->errors()) {
@@ -94,6 +117,7 @@ int BuildSnapshot(int argc, char* argv[]) {
94117

95118
node::SnapshotConfig snapshot_config;
96119
snapshot_config.builder_script_path = builder_script_path;
120+
if (base_blob.data != nullptr) snapshot_config.base_blob = &base_blob;
97121

98122
#ifdef NODE_USE_NODE_CODE_CACHE
99123
snapshot_config.flags = node::SnapshotFlags::kDefault;

0 commit comments

Comments
 (0)