Skip to content

Commit 3f409b7

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 c80ea0c commit 3f409b7

File tree

8 files changed

+238
-7
lines changed

8 files changed

+238
-7
lines changed

doc/api/cli.md

+25
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
@@ -1429,6 +1453,7 @@ Node.js options that are allowed are:
14291453
* `--require`, `-r`
14301454
* `--secure-heap-min`
14311455
* `--secure-heap`
1456+
* `--snapshot-blob`
14321457
* `--throw-deprecation`
14331458
* `--title`
14341459
* `--tls-cipher-list`

src/node.cc

+67-6
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

+10
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+
kDisallowedInEnvironment);
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

+2
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

+62-1
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

@@ -522,6 +527,7 @@ const EnvSerializeInfo* NodeMainInstance::GetEnvSerializeInfo() {
522527
}
523528

524529
void SnapshotBuilder::Generate(SnapshotData* out,
530+
const std::string& entry_file,
525531
const std::vector<std::string> args,
526532
const std::vector<std::string> exec_args) {
527533
Isolate* isolate = Isolate::Allocate();
@@ -573,13 +579,62 @@ void SnapshotBuilder::Generate(SnapshotData* out,
573579
// Run scripts in lib/internal/bootstrap/
574580
{
575581
TryCatch bootstrapCatch(isolate);
576-
v8::MaybeLocal<Value> result = env->RunBootstrapping();
582+
MaybeLocal<Value> result = env->RunBootstrapping();
577583
if (bootstrapCatch.HasCaught()) {
578584
PrintCaughtException(isolate, context, bootstrapCatch);
579585
}
580586
result.ToLocalChecked();
581587
}
582588

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

664+
void SnapshotBuilder::Generate(SnapshotData* out,
665+
const std::vector<std::string> args,
666+
const std::vector<std::string> exec_args) {
667+
Generate(out, "", args, exec_args);
668+
}
669+
609670
std::string SnapshotBuilder::Generate(
610671
const std::vector<std::string> args,
611672
const std::vector<std::string> exec_args) {

src/node_snapshotable.h

+6
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

+3
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

+63
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)