Skip to content

Commit

Permalink
bootstrap: implement --snapshot-blob and --snapshot-main
Browse files Browse the repository at this point in the history
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
  • Loading branch information
joyeecheung committed Jun 2, 2021
1 parent c80ea0c commit 3f409b7
Show file tree
Hide file tree
Showing 8 changed files with 238 additions and 7 deletions.
25 changes: 25 additions & 0 deletions doc/api/cli.md
Expand Up @@ -897,6 +897,30 @@ minimum allocation from the secure heap. The minimum value is `2`.
The maximum value is the lesser of `--secure-heap` or `2147483647`.
The value given must be a power of two.

### `--snapshot-blob=path`
<!-- YAML
added: REPLACEME
-->

When used with `--snapshot-main`, `--snapshot-blob` specifies the path
where the generated snapshot blob will be written to. If not speficied,
the generated blob will be written, by default, to `snapshot.blob`
in the current working directory.

When used without `--snapshot-main`, `--snapshot-blob` specifies the
path to the blob that will be used to restore the application state.

### `--snapshot-main=path`
<!-- YAML
added: REPLACEME
-->

Specifies the path of the entry point file used to build user land
snapshot. If `--snapshot-blob` is not specified, the generated blob
will be written, by default, to `snapshot.blob` in the current working
directory. Otherwise it will be written to the path specified by
`--snapshot-blob`.

### `--throw-deprecation`
<!-- YAML
added: v0.11.14
Expand Down Expand Up @@ -1429,6 +1453,7 @@ Node.js options that are allowed are:
* `--require`, `-r`
* `--secure-heap-min`
* `--secure-heap`
* `--snapshot-blob`
* `--throw-deprecation`
* `--title`
* `--tls-cipher-list`
Expand Down
73 changes: 67 additions & 6 deletions src/node.cc
Expand Up @@ -1100,18 +1100,76 @@ int Start(int argc, char** argv) {
return result.exit_code;
}

{
if (!per_process::cli_options->snapshot_main.empty()) {
SnapshotData data;
{
std::string entry;
int r =
ReadFileSync(&entry, per_process::cli_options->snapshot_main.c_str());
if (r != 0) {
const char* code = uv_err_name(r);
const char* message = uv_strerror(r);
FPrintF(stderr,
"Failed to open %s. %s: %s\n",
per_process::cli_options->snapshot_main.c_str(),
code,
message);
return 1;
}
node::SnapshotBuilder::Generate(
&data, entry, result.args, result.exec_args);
}

std::string snapshot_blob_path;
if (!per_process::cli_options->snapshot_blob.empty()) {
snapshot_blob_path = per_process::cli_options->snapshot_blob;
} else {
snapshot_blob_path = std::string("snapshot.blob");
char buf[PATH_MAX_BYTES];
size_t cwd_size = sizeof(buf);
if (uv_cwd(buf, &cwd_size)) {
snapshot_blob_path =
std::string(buf) + kPathSeparator + std::string("snapshot.blob");
}
}

FILE* fp = fopen(snapshot_blob_path.c_str(), "w");
if (fp != nullptr) {
data.ToBlob(fp);
fclose(fp);
} else {
fprintf(stderr, "Cannot open %s", snapshot_blob_path.c_str());
result.exit_code = 1;
}
} else {
SnapshotData snapshot_data;
Isolate::CreateParams params;
const std::vector<size_t>* indices = nullptr;
const EnvSerializeInfo* env_info = nullptr;
bool force_no_snapshot =
per_process::cli_options->per_isolate->no_node_snapshot;
if (!force_no_snapshot) {
v8::StartupData* blob = NodeMainInstance::GetEmbeddedSnapshotBlob();
if (blob != nullptr) {
params.snapshot_blob = blob;
indices = NodeMainInstance::GetIsolateDataIndices();
env_info = NodeMainInstance::GetEnvSerializeInfo();
// TODO(joyee): return const SnapshotData* from the generated source
if (per_process::cli_options->snapshot_blob.empty()) {
v8::StartupData* blob = NodeMainInstance::GetEmbeddedSnapshotBlob();
if (blob != nullptr) {
params.snapshot_blob = blob;
indices = NodeMainInstance::GetIsolateDataIndices();
env_info = NodeMainInstance::GetEnvSerializeInfo();
}
} else {
std::string filename = per_process::cli_options->snapshot_blob;
FILE* fp = fopen(filename.c_str(), "r");
if (fp != nullptr) {
SnapshotData::FromBlob(&snapshot_data, fp);
params.snapshot_blob = &(snapshot_data.blob);
indices = &(snapshot_data.isolate_data_indices);
env_info = &(snapshot_data.env_info);
fclose(fp);
} else {
fprintf(stderr, "Cannot open %s", filename.c_str());
result.exit_code = 1;
}
}
}
uv_loop_configure(uv_default_loop(), UV_METRICS_IDLE_TIME);
Expand All @@ -1123,6 +1181,9 @@ int Start(int argc, char** argv) {
result.exec_args,
indices);
result.exit_code = main_instance.Run(env_info);
if (snapshot_data.blob.data != nullptr) {
delete snapshot_data.blob.data;
}
}

TearDownOncePerProcess();
Expand Down
10 changes: 10 additions & 0 deletions src/node_options.cc
Expand Up @@ -687,6 +687,16 @@ PerProcessOptionsParser::PerProcessOptionsParser(
"disable Object.prototype.__proto__",
&PerProcessOptions::disable_proto,
kAllowedInEnvironment);
AddOption("--snapshot-main",
"Path to the entry point file used to build user snapshot",
&PerProcessOptions::snapshot_main,
kDisallowedInEnvironment);
AddOption("--snapshot-blob",
"Path to the snapshot blob that's either the result of snapshot"
"building, or the blob that is used to restore the application "
"state",
&PerProcessOptions::snapshot_blob,
kAllowedInEnvironment);

// 12.x renamed this inadvertently, so alias it for consistency within the
// release line, while using the original name for consistency with older
Expand Down
2 changes: 2 additions & 0 deletions src/node_options.h
Expand Up @@ -220,6 +220,8 @@ class PerProcessOptions : public Options {
bool zero_fill_all_buffers = false;
bool debug_arraybuffer_allocations = false;
std::string disable_proto;
std::string snapshot_main;
std::string snapshot_blob;

std::vector<std::string> security_reverts;
bool print_bash_completion = false;
Expand Down
63 changes: 62 additions & 1 deletion src/node_snapshotable.cc
Expand Up @@ -16,12 +16,17 @@
namespace node {

using v8::Context;
using v8::Function;
using v8::HandleScope;
using v8::Isolate;
using v8::Local;
using v8::MaybeLocal;
using v8::Object;
using v8::ScriptCompiler;
using v8::ScriptOrigin;
using v8::SnapshotCreator;
using v8::StartupData;
using v8::String;
using v8::TryCatch;
using v8::Value;

Expand Down Expand Up @@ -522,6 +527,7 @@ const EnvSerializeInfo* NodeMainInstance::GetEnvSerializeInfo() {
}

void SnapshotBuilder::Generate(SnapshotData* out,
const std::string& entry_file,
const std::vector<std::string> args,
const std::vector<std::string> exec_args) {
Isolate* isolate = Isolate::Allocate();
Expand Down Expand Up @@ -573,13 +579,62 @@ void SnapshotBuilder::Generate(SnapshotData* out,
// Run scripts in lib/internal/bootstrap/
{
TryCatch bootstrapCatch(isolate);
v8::MaybeLocal<Value> result = env->RunBootstrapping();
MaybeLocal<Value> result = env->RunBootstrapping();
if (bootstrapCatch.HasCaught()) {
PrintCaughtException(isolate, context, bootstrapCatch);
}
result.ToLocalChecked();
}

// Run the entry point file
if (!entry_file.empty()) {
TryCatch bootstrapCatch(isolate);
std::string filename_s = std::string("node:snapshot_main");
Local<String> filename =
OneByteString(isolate, filename_s.c_str(), filename_s.size());
ScriptOrigin origin(isolate, filename, 0, 0, true);
Local<String> source = ToV8Value(context, entry_file, isolate)
.ToLocalChecked()
.As<String>();
// TODO(joyee): do we need all of these? Maybe we would want a less
// internal version of them.
std::vector<Local<String>> parameters = {env->require_string(),
env->process_string(),
env->internal_binding_string(),
env->primordials_string()};
ScriptCompiler::Source script_source(source, origin);
Local<Function> fn;
if (!ScriptCompiler::CompileFunctionInContext(
context,
&script_source,
parameters.size(),
parameters.data(),
0,
nullptr,
ScriptCompiler::kEagerCompile)
.ToLocal(&fn)) {
if (bootstrapCatch.HasCaught()) {
PrintCaughtException(isolate, context, bootstrapCatch);
}
abort();
}
std::vector<Local<Value>> args = {env->native_module_require(),
env->process_object(),
env->internal_binding_loader(),
env->primordials()};
Local<Value> result;
if (!fn->Call(context, Undefined(isolate), args.size(), args.data())
.ToLocal(&result)) {
if (bootstrapCatch.HasCaught()) {
PrintCaughtException(isolate, context, bootstrapCatch);
}
abort();
}
// TODO(joyee): we could use the result for something special, like
// setting up initializers that should be invoked at snapshot
// dehydration.
}

if (per_process::enabled_debug_list.enabled(DebugCategory::MKSNAPSHOT)) {
env->PrintAllBaseObjects();
printf("Environment = %p\n", env);
Expand All @@ -606,6 +661,12 @@ void SnapshotBuilder::Generate(SnapshotData* out,
per_process::v8_platform.Platform()->UnregisterIsolate(isolate);
}

void SnapshotBuilder::Generate(SnapshotData* out,
const std::vector<std::string> args,
const std::vector<std::string> exec_args) {
Generate(out, "", args, exec_args);
}

std::string SnapshotBuilder::Generate(
const std::vector<std::string> args,
const std::vector<std::string> exec_args) {
Expand Down
6 changes: 6 additions & 0 deletions src/node_snapshotable.h
Expand Up @@ -125,6 +125,12 @@ class SnapshotBuilder {
public:
static std::string Generate(const std::vector<std::string> args,
const std::vector<std::string> exec_args);
// Generate the snapshot into out.
// entry_file should be the content of the UTF-8 encoded entry files.
static void Generate(SnapshotData* out,
const std::string& entry_file,
const std::vector<std::string> args,
const std::vector<std::string> exec_args);
static void Generate(SnapshotData* out,
const std::vector<std::string> args,
const std::vector<std::string> exec_args);
Expand Down
3 changes: 3 additions & 0 deletions test/fixtures/snapshot/mutate-fs.js
@@ -0,0 +1,3 @@
const fs = require('fs');

fs.foo = 'I am from the snapshot';
63 changes: 63 additions & 0 deletions test/parallel/test-snapshot-blob.js
@@ -0,0 +1,63 @@
'use strict';

require('../common');
const assert = require('assert');
const { spawnSync } = require('child_process');
const tmpdir = require('../common/tmpdir');
const fixtures = require('../common/fixtures');
const path = require('path');
const fs = require('fs');

tmpdir.refresh();
const blobPath = path.join(tmpdir.path, 'my-snapshot.blob');
const file = fixtures.path('snapshot', 'mutate-fs.js');

{
const child = spawnSync(process.execPath, [
'--snapshot-main',
file,
], {
cwd: tmpdir.path
});
if (child.status !== 0) {
console.log(child.stderr.toString());
console.log(child.stdout.toString());
assert.strictEqual(child.status, 0);
}
const stats = fs.statSync(path.join(tmpdir.path, 'snapshot.blob'));
assert(stats.isFile());
}

{
let child = spawnSync(process.execPath, [
'--snapshot-main',
file,
'--snapshot-blob',
blobPath,
], {
cwd: tmpdir.path
});
if (child.status !== 0) {
console.log(child.stderr.toString());
console.log(child.stdout.toString());
assert.strictEqual(child.status, 0);
}
const stats = fs.statSync(blobPath);
assert(stats.isFile());

child = spawnSync(process.execPath, [
'--snapshot-blob',
blobPath,
'-p',
'require("fs").foo',
], {
cwd: tmpdir.path
});

if (child.status !== 0) {
console.log(child.stderr.toString());
console.log(child.stdout.toString());
assert.strictEqual(child.status, 0);
}
assert(/I am from the snapshot/.test(child.stdout.toString()));
}

0 comments on commit 3f409b7

Please sign in to comment.