Skip to content

Commit 28bc446

Browse files
committed
bootstrap: implement --snapshot-blob and --snapshot-main
This patch introduces --snapshot-main and --snapshot-blob options for creating and using user land snapshots. At the time of the creation of this patch, user land CJS modules and ESM are not yet supported in the snapshot, but a subset of builtins should already work (in particular modules loaded during bootstrap are shipped in the built-in snapshot, so they should work in user land snapshot as well), and support for more builtins are being added. To generate a snapshot using main.js as entry point and write the snapshot blob to snapshot.blob: $ node --snapshot-main main.js --snapshot-blob snapshot.blob To restore application state from snapshot.blob: $ node --snapshot-blob snapshot.blob
1 parent abe40e8 commit 28bc446

File tree

8 files changed

+237
-7
lines changed

8 files changed

+237
-7
lines changed

doc/api/cli.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -897,6 +897,30 @@ minimum allocation from the secure heap. The minimum value is `2`.
897897
The maximum value is the lesser of `--secure-heap` or `2147483647`.
898898
The value given must be a power of two.
899899

900+
### `--snapshot-blob=path`
901+
<!-- YAML
902+
added: REPLACEME
903+
-->
904+
905+
When used with `--snapshot-main`, `--snapshot-blob` specifies the path
906+
where the generated snapshot blob will be written to. If not speficied,
907+
the generated blob will be written, by default, to `snapshot.blob`
908+
in the current working directory.
909+
910+
When used without `--snapshot-main`, `--snapshot-blob` specifies the
911+
path to the blob that will be used to restore the application state.
912+
913+
### `--snapshot-main=path`
914+
<!-- YAML
915+
added: REPLACEME
916+
-->
917+
918+
Specifies the path of the entry point file used to build user land
919+
snapshot. If `--snapshot-blob` is not specified, the generated blob
920+
will be written, by default, to `snapshot.blob` in the current working
921+
directory. Otherwise it will be written to the path specified by
922+
`--snapshot-blob`.
923+
900924
### `--throw-deprecation`
901925
<!-- YAML
902926
added: v0.11.14

src/node.cc

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,18 +1100,76 @@ int Start(int argc, char** argv) {
11001100
return result.exit_code;
11011101
}
11021102

1103-
{
1103+
if (!per_process::cli_options->snapshot_main.empty()) {
1104+
SnapshotData data;
1105+
{
1106+
std::string entry;
1107+
int r =
1108+
ReadFileSync(&entry, per_process::cli_options->snapshot_main.c_str());
1109+
if (r != 0) {
1110+
const char* code = uv_err_name(r);
1111+
const char* message = uv_strerror(r);
1112+
FPrintF(stderr,
1113+
"Failed to open %s. %s: %s\n",
1114+
per_process::cli_options->snapshot_main.c_str(),
1115+
code,
1116+
message);
1117+
return 1;
1118+
}
1119+
node::SnapshotBuilder::Generate(
1120+
&data, entry, result.args, result.exec_args);
1121+
}
1122+
1123+
std::string snapshot_blob_path;
1124+
if (!per_process::cli_options->snapshot_blob.empty()) {
1125+
snapshot_blob_path = per_process::cli_options->snapshot_blob;
1126+
} else {
1127+
snapshot_blob_path = std::string("snapshot.blob");
1128+
char buf[PATH_MAX_BYTES];
1129+
size_t cwd_size = sizeof(buf);
1130+
if (uv_cwd(buf, &cwd_size)) {
1131+
snapshot_blob_path =
1132+
std::string(buf) + kPathSeparator + std::string("snapshot.blob");
1133+
}
1134+
}
1135+
1136+
FILE* fp = fopen(snapshot_blob_path.c_str(), "w");
1137+
if (fp != nullptr) {
1138+
data.ToBlob(fp);
1139+
fclose(fp);
1140+
} else {
1141+
fprintf(stderr, "Cannot open %s", snapshot_blob_path.c_str());
1142+
result.exit_code = 1;
1143+
}
1144+
} else {
1145+
SnapshotData snapshot_data;
11041146
Isolate::CreateParams params;
11051147
const std::vector<size_t>* indices = nullptr;
11061148
const EnvSerializeInfo* env_info = nullptr;
11071149
bool force_no_snapshot =
11081150
per_process::cli_options->per_isolate->no_node_snapshot;
11091151
if (!force_no_snapshot) {
1110-
v8::StartupData* blob = NodeMainInstance::GetEmbeddedSnapshotBlob();
1111-
if (blob != nullptr) {
1112-
params.snapshot_blob = blob;
1113-
indices = NodeMainInstance::GetIsolateDataIndices();
1114-
env_info = NodeMainInstance::GetEnvSerializeInfo();
1152+
// TODO(joyee): return const SnapshotData* from the generated source
1153+
if (per_process::cli_options->snapshot_blob.empty()) {
1154+
v8::StartupData* blob = NodeMainInstance::GetEmbeddedSnapshotBlob();
1155+
if (blob != nullptr) {
1156+
params.snapshot_blob = blob;
1157+
indices = NodeMainInstance::GetIsolateDataIndices();
1158+
env_info = NodeMainInstance::GetEnvSerializeInfo();
1159+
}
1160+
} else {
1161+
std::string filename = per_process::cli_options->snapshot_blob;
1162+
FILE* fp = fopen(filename.c_str(), "r");
1163+
if (fp != nullptr) {
1164+
SnapshotData::FromBlob(&snapshot_data, fp);
1165+
params.snapshot_blob = &(snapshot_data.blob);
1166+
indices = &(snapshot_data.isolate_data_indices);
1167+
env_info = &(snapshot_data.env_info);
1168+
fclose(fp);
1169+
} else {
1170+
fprintf(stderr, "Cannot open %s", filename.c_str());
1171+
result.exit_code = 1;
1172+
}
11151173
}
11161174
}
11171175
uv_loop_configure(uv_default_loop(), UV_METRICS_IDLE_TIME);
@@ -1123,6 +1181,9 @@ int Start(int argc, char** argv) {
11231181
result.exec_args,
11241182
indices);
11251183
result.exit_code = main_instance.Run(env_info);
1184+
if (snapshot_data.blob.data != nullptr) {
1185+
delete snapshot_data.blob.data;
1186+
}
11261187
}
11271188

11281189
TearDownOncePerProcess();

src/node_options.cc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,16 @@ PerProcessOptionsParser::PerProcessOptionsParser(
687687
"disable Object.prototype.__proto__",
688688
&PerProcessOptions::disable_proto,
689689
kAllowedInEnvironment);
690+
AddOption("--snapshot-main",
691+
"Path to the entry point file used to build user snapshot",
692+
&PerProcessOptions::snapshot_main,
693+
kAllowedInEnvironment);
694+
AddOption("--snapshot-blob",
695+
"Path to the snapshot blob that's either the result of snapshot"
696+
"building, or the blob that is used to restore the application "
697+
"state",
698+
&PerProcessOptions::snapshot_blob,
699+
kAllowedInEnvironment);
690700

691701
// 12.x renamed this inadvertently, so alias it for consistency within the
692702
// release line, while using the original name for consistency with older

src/node_options.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,8 @@ class PerProcessOptions : public Options {
220220
bool zero_fill_all_buffers = false;
221221
bool debug_arraybuffer_allocations = false;
222222
std::string disable_proto;
223+
std::string snapshot_main;
224+
std::string snapshot_blob;
223225

224226
std::vector<std::string> security_reverts;
225227
bool print_bash_completion = false;

src/node_snapshotable.cc

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,17 @@
1616
namespace node {
1717

1818
using v8::Context;
19+
using v8::Function;
1920
using v8::HandleScope;
2021
using v8::Isolate;
2122
using v8::Local;
23+
using v8::MaybeLocal;
2224
using v8::Object;
25+
using v8::ScriptCompiler;
26+
using v8::ScriptOrigin;
2327
using v8::SnapshotCreator;
2428
using v8::StartupData;
29+
using v8::String;
2530
using v8::TryCatch;
2631
using v8::Value;
2732

@@ -526,6 +531,7 @@ const EnvSerializeInfo* NodeMainInstance::GetEnvSerializeInfo() {
526531
}
527532

528533
void SnapshotBuilder::Generate(SnapshotData* out,
534+
const std::string& entry_file,
529535
const std::vector<std::string> args,
530536
const std::vector<std::string> exec_args) {
531537
Isolate* isolate = Isolate::Allocate();
@@ -577,13 +583,62 @@ void SnapshotBuilder::Generate(SnapshotData* out,
577583
// Run scripts in lib/internal/bootstrap/
578584
{
579585
TryCatch bootstrapCatch(isolate);
580-
v8::MaybeLocal<Value> result = env->RunBootstrapping();
586+
MaybeLocal<Value> result = env->RunBootstrapping();
581587
if (bootstrapCatch.HasCaught()) {
582588
PrintCaughtException(isolate, context, bootstrapCatch);
583589
}
584590
result.ToLocalChecked();
585591
}
586592

593+
// Run the entry point file
594+
if (!entry_file.empty()) {
595+
TryCatch bootstrapCatch(isolate);
596+
std::string filename_s = std::string("node:snapshot_main");
597+
Local<String> filename =
598+
OneByteString(isolate, filename_s.c_str(), filename_s.size());
599+
ScriptOrigin origin(isolate, filename, 0, 0, true);
600+
Local<String> source = ToV8Value(context, entry_file, isolate)
601+
.ToLocalChecked()
602+
.As<String>();
603+
// TODO(joyee): do we need all of these? Maybe we would want a less
604+
// internal version of them.
605+
std::vector<Local<String>> parameters = {env->require_string(),
606+
env->process_string(),
607+
env->internal_binding_string(),
608+
env->primordials_string()};
609+
ScriptCompiler::Source script_source(source, origin);
610+
Local<Function> fn;
611+
if (!ScriptCompiler::CompileFunctionInContext(
612+
context,
613+
&script_source,
614+
parameters.size(),
615+
parameters.data(),
616+
0,
617+
nullptr,
618+
ScriptCompiler::kEagerCompile)
619+
.ToLocal(&fn)) {
620+
if (bootstrapCatch.HasCaught()) {
621+
PrintCaughtException(isolate, context, bootstrapCatch);
622+
}
623+
abort();
624+
}
625+
std::vector<Local<Value>> args = {env->native_module_require(),
626+
env->process_object(),
627+
env->internal_binding_loader(),
628+
env->primordials()};
629+
Local<Value> result;
630+
if (!fn->Call(context, Undefined(isolate), args.size(), args.data())
631+
.ToLocal(&result)) {
632+
if (bootstrapCatch.HasCaught()) {
633+
PrintCaughtException(isolate, context, bootstrapCatch);
634+
}
635+
abort();
636+
}
637+
// TODO(joyee): we could use the result for something special, like
638+
// setting up initializers that should be invoked at snapshot
639+
// dehydration.
640+
}
641+
587642
if (per_process::enabled_debug_list.enabled(DebugCategory::MKSNAPSHOT)) {
588643
env->PrintAllBaseObjects();
589644
printf("Environment = %p\n", env);
@@ -610,6 +665,12 @@ void SnapshotBuilder::Generate(SnapshotData* out,
610665
per_process::v8_platform.Platform()->UnregisterIsolate(isolate);
611666
}
612667

668+
void SnapshotBuilder::Generate(SnapshotData* out,
669+
const std::vector<std::string> args,
670+
const std::vector<std::string> exec_args) {
671+
Generate(out, "", args, exec_args);
672+
}
673+
613674
std::string SnapshotBuilder::Generate(
614675
const std::vector<std::string> args,
615676
const std::vector<std::string> exec_args) {

src/node_snapshotable.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,12 @@ class SnapshotBuilder {
125125
public:
126126
static std::string Generate(const std::vector<std::string> args,
127127
const std::vector<std::string> exec_args);
128+
// Generate the snapshot into out.
129+
// entry_file should be the content of the UTF-8 encoded entry files.
130+
static void Generate(SnapshotData* out,
131+
const std::string& entry_file,
132+
const std::vector<std::string> args,
133+
const std::vector<std::string> exec_args);
128134
static void Generate(SnapshotData* out,
129135
const std::vector<std::string> args,
130136
const std::vector<std::string> exec_args);

test/fixtures/snapshot/mutate-fs.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const fs = require('fs');
2+
3+
fs.foo = 'I am from the snapshot';

test/parallel/test-snapshot-blob.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
'use strict';
2+
3+
require('../common');
4+
const assert = require('assert');
5+
const { spawnSync } = require('child_process');
6+
const tmpdir = require('../common/tmpdir');
7+
const fixtures = require('../common/fixtures');
8+
const path = require('path');
9+
const fs = require('fs');
10+
11+
tmpdir.refresh();
12+
const blobPath = path.join(tmpdir.path, 'my-snapshot.blob');
13+
const file = fixtures.path('snapshot', 'mutate-fs.js');
14+
15+
{
16+
const child = spawnSync(process.execPath, [
17+
'--snapshot-main',
18+
file,
19+
], {
20+
cwd: tmpdir.path
21+
});
22+
if (child.status !== 0) {
23+
console.log(child.stderr.toString());
24+
console.log(child.stdout.toString());
25+
assert.strictEqual(child.status, 0);
26+
}
27+
const stats = fs.statSync(path.join(tmpdir.path, 'snapshot.blob'));
28+
assert(stats.isFile());
29+
}
30+
31+
{
32+
let child = spawnSync(process.execPath, [
33+
'--snapshot-main',
34+
file,
35+
'--snapshot-blob',
36+
blobPath,
37+
], {
38+
cwd: tmpdir.path
39+
});
40+
if (child.status !== 0) {
41+
console.log(child.stderr.toString());
42+
console.log(child.stdout.toString());
43+
assert.strictEqual(child.status, 0);
44+
}
45+
const stats = fs.statSync(blobPath);
46+
assert(stats.isFile());
47+
48+
child = spawnSync(process.execPath, [
49+
'--snapshot-blob',
50+
blobPath,
51+
'-p',
52+
'require("fs").foo',
53+
], {
54+
cwd: tmpdir.path
55+
});
56+
57+
if (child.status !== 0) {
58+
console.log(child.stderr.toString());
59+
console.log(child.stdout.toString());
60+
assert.strictEqual(child.status, 0);
61+
}
62+
assert(/I am from the snapshot/.test(child.stdout.toString()));
63+
}

0 commit comments

Comments
 (0)