Skip to content

Commit 3561514

Browse files
joyeecheungruyadorno
authored andcommittedAug 23, 2022
bootstrap: implement --snapshot-blob and --build-snapshot
This patch introduces `--build-snapshot` and `--snapshot-blob` options for creating and using user land snapshots. For the initial iteration, user land CJS modules and ESM are not yet supported in the snapshot, so only one single file can be snapshotted (users can bundle their applications into a single script with their bundler of choice to build a snapshot though). A subset of builtins should already work, and support for more builtins are being added. This PR includes tests checking that the TypeScript compiler and the marked markdown renderer (and the builtins they use) can be snapshotted and deserialized. To generate a snapshot using `snapshot.js` as entry point and write the snapshot blob to `snapshot.blob`: ``` $ echo "globalThis.foo = 'I am from the snapshot'" > snapshot.js $ node --snapshot-blob snapshot.blob --build-snapshot snapshot.js ``` To restore application state from `snapshot.blob`, with `index.js` as the entry point script for the deserialized application: ``` $ echo "console.log(globalThis.foo)" > index.js $ node --snapshot-blob snapshot.blob index.js I am from the snapshot ``` Users can also use the `v8.startupSnapshot` API to specify an entry point at snapshot building time, thus avoiding the need of an additional entry script at deserialization time: ``` $ echo "require('v8').startupSnapshot.setDeserializeMainFunction(() => console.log('I am from the snapshot'))" > snapshot.js $ node --snapshot-blob snapshot.blob --build-snapshot snapshot.js $ node --snapshot-blob snapshot.blob I am from the snapshot ``` Note that this patch only adds functionality to the `node` executable for building run-time user-land snapshots, the generated snapshot is stored into a separate file on disk. Building a single binary with both Node.js and an embedded snapshot has already been possible with the `--node-snapshot-main` option to the `configure` script if the user compiles Node.js from source. It would be a different task to enable the `node` executable to produce a single binary that contains both Node.js and an embedded snapshot without building Node.js from source, which should be layered on top of the SEA (Single Executable Apps) initiative. Known limitations/bugs that are being fixed in the upstream: - V8 hits a DCHECK when deserializing certain mutated globals, e.g. `Error.stackTraceLimit` (it should work fine in the release build, however): https://chromium-review.googlesource.com/c/v8/v8/+/3319481 - Layout of V8's read-only heap can be inconsistent after deserialization, resulting in memory corruption: https://bugs.chromium.org/p/v8/issues/detail?id=12921 PR-URL: #38905 Refs: #35711 Reviewed-By: Chengzhong Wu <[email protected]> Reviewed-By: Matteo Collina <[email protected]>
1 parent 1b3fcf7 commit 3561514

17 files changed

+1408
-52
lines changed
 

‎doc/api/cli.md

+76
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,62 @@ If this flag is passed, the behavior can still be set to not abort through
100100
[`process.setUncaughtExceptionCaptureCallback()`][] (and through usage of the
101101
`node:domain` module that uses it).
102102

103+
### `--build-snapshot`
104+
105+
<!-- YAML
106+
added: REPLACEME
107+
-->
108+
109+
> Stability: 1 - Experimental
110+
111+
Generates a snapshot blob when the process exits and writes it to
112+
disk, which can be loaded later with `--snapshot-blob`.
113+
114+
When building the snapshot, if `--snapshot-blob` is not specified,
115+
the generated blob will be written, by default, to `snapshot.blob`
116+
in the current working directory. Otherwise it will be written to
117+
the path specified by `--snapshot-blob`.
118+
119+
```console
120+
$ echo "globalThis.foo = 'I am from the snapshot'" > snapshot.js
121+
122+
# Run snapshot.js to intialize the application and snapshot the
123+
# state of it into snapshot.blob.
124+
$ node --snapshot-blob snapshot.blob --build-snapshot snapshot.js
125+
126+
$ echo "console.log(globalThis.foo)" > index.js
127+
128+
# Load the generated snapshot and start the application from index.js.
129+
$ node --snapshot-blob snapshot.blob index.js
130+
I am from the snapshot
131+
```
132+
133+
The [`v8.startupSnapshot` API][] can be used to specify an entry point at
134+
snapshot building time, thus avoiding the need of an additional entry
135+
script at deserialization time:
136+
137+
```console
138+
$ echo "require('v8').startupSnapshot.setDeserializeMainFunction(() => console.log('I am from the snapshot'))" > snapshot.js
139+
$ node --snapshot-blob snapshot.blob --build-snapshot snapshot.js
140+
$ node --snapshot-blob snapshot.blob
141+
I am from the snapshot
142+
```
143+
144+
For more information, check out the [`v8.startupSnapshot` API][] documentation.
145+
146+
Currently the support for run-time snapshot is experimental in that:
147+
148+
1. User-land modules are not yet supported in the snapshot, so only
149+
one single file can be snapshotted. Users can bundle their applications
150+
into a single script with their bundler of choice before building
151+
a snapshot, however.
152+
2. Only a subset of the built-in modules work in the snapshot, though the
153+
Node.js core test suite checks that a few fairly complex applications
154+
can be snapshotted. Support for more modules are being added. If any
155+
crashes or buggy behaviors occur when building a snapshot, please file
156+
a report in the [Node.js issue tracker][] and link to it in the
157+
[tracking issue for user-land snapshots][].
158+
103159
### `--completion-bash`
104160

105161
<!-- YAML
@@ -1105,6 +1161,22 @@ minimum allocation from the secure heap. The minimum value is `2`.
11051161
The maximum value is the lesser of `--secure-heap` or `2147483647`.
11061162
The value given must be a power of two.
11071163

1164+
### `--snapshot-blob=path`
1165+
1166+
<!-- YAML
1167+
added: REPLACEME
1168+
-->
1169+
1170+
> Stability: 1 - Experimental
1171+
1172+
When used with `--build-snapshot`, `--snapshot-blob` specifies the path
1173+
where the generated snapshot blob will be written to. If not specified,
1174+
the generated blob will be written, by default, to `snapshot.blob`
1175+
in the current working directory.
1176+
1177+
When used without `--build-snapshot`, `--snapshot-blob` specifies the
1178+
path to the blob that will be used to restore the application state.
1179+
11081180
### `--test`
11091181

11101182
<!-- YAML
@@ -1727,6 +1799,7 @@ Node.js options that are allowed are:
17271799
* `--require`, `-r`
17281800
* `--secure-heap-min`
17291801
* `--secure-heap`
1802+
* `--snapshot-blob`
17301803
* `--test-only`
17311804
* `--throw-deprecation`
17321805
* `--title`
@@ -2100,6 +2173,7 @@ done
21002173
[ECMAScript module loader]: esm.md#loaders
21012174
[Fetch API]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
21022175
[Modules loaders]: packages.md#modules-loaders
2176+
[Node.js issue tracker]: https://github.com/nodejs/node/issues
21032177
[OSSL_PROVIDER-legacy]: https://www.openssl.org/docs/man3.0/man7/OSSL_PROVIDER-legacy.html
21042178
[REPL]: repl.md
21052179
[ScriptCoverage]: https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-ScriptCoverage
@@ -2130,6 +2204,7 @@ done
21302204
[`tls.DEFAULT_MAX_VERSION`]: tls.md#tlsdefault_max_version
21312205
[`tls.DEFAULT_MIN_VERSION`]: tls.md#tlsdefault_min_version
21322206
[`unhandledRejection`]: process.md#event-unhandledrejection
2207+
[`v8.startupSnapshot` API]: v8.md#startup-snapshot-api
21332208
[`worker_threads.threadId`]: worker_threads.md#workerthreadid
21342209
[conditional exports]: packages.md#conditional-exports
21352210
[context-aware]: addons.md#context-aware-addons
@@ -2145,4 +2220,5 @@ done
21452220
[security warning]: #warning-binding-inspector-to-a-public-ipport-combination-is-insecure
21462221
[semi-space]: https://www.memorymanagement.org/glossary/s.html#semi.space
21472222
[timezone IDs]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
2223+
[tracking issue for user-land snapshots]: https://github.com/nodejs/node/issues/44014
21482224
[ways that `TZ` is handled in other environments]: https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html

‎src/env.cc

+21-15
Original file line numberDiff line numberDiff line change
@@ -248,17 +248,6 @@ std::ostream& operator<<(std::ostream& output,
248248
return output;
249249
}
250250

251-
std::ostream& operator<<(std::ostream& output,
252-
const std::vector<PropInfo>& vec) {
253-
output << "{\n";
254-
for (const auto& info : vec) {
255-
output << " { \"" << info.name << "\", " << std::to_string(info.id) << ", "
256-
<< std::to_string(info.index) << " },\n";
257-
}
258-
output << "}";
259-
return output;
260-
}
261-
262251
std::ostream& operator<<(std::ostream& output,
263252
const IsolateDataSerializeInfo& i) {
264253
output << "{\n"
@@ -298,7 +287,7 @@ IsolateDataSerializeInfo IsolateData::Serialize(SnapshotCreator* creator) {
298287
for (size_t i = 0; i < AsyncWrap::PROVIDERS_LENGTH; i++)
299288
info.primitive_values.push_back(creator->AddData(async_wrap_provider(i)));
300289

301-
size_t id = 0;
290+
uint32_t id = 0;
302291
#define V(PropertyName, TypeName) \
303292
do { \
304293
Local<TypeName> field = PropertyName(); \
@@ -352,7 +341,7 @@ void IsolateData::DeserializeProperties(const IsolateDataSerializeInfo* info) {
352341

353342
const std::vector<PropInfo>& values = info->template_values;
354343
i = 0; // index to the array
355-
size_t id = 0;
344+
uint32_t id = 0;
356345
#define V(PropertyName, TypeName) \
357346
do { \
358347
if (values.size() > i && id == values[i].id) { \
@@ -1482,6 +1471,7 @@ std::ostream& operator<<(std::ostream& output,
14821471
AsyncHooks::SerializeInfo AsyncHooks::Serialize(Local<Context> context,
14831472
SnapshotCreator* creator) {
14841473
SerializeInfo info;
1474+
// TODO(joyeecheung): some of these probably don't need to be serialized.
14851475
info.async_ids_stack = async_ids_stack_.Serialize(context, creator);
14861476
info.fields = fields_.Serialize(context, creator);
14871477
info.async_id_fields = async_id_fields_.Serialize(context, creator);
@@ -1676,7 +1666,7 @@ EnvSerializeInfo Environment::Serialize(SnapshotCreator* creator) {
16761666
info.should_abort_on_uncaught_toggle =
16771667
should_abort_on_uncaught_toggle_.Serialize(ctx, creator);
16781668

1679-
size_t id = 0;
1669+
uint32_t id = 0;
16801670
#define V(PropertyName, TypeName) \
16811671
do { \
16821672
Local<TypeName> field = PropertyName(); \
@@ -1693,6 +1683,22 @@ EnvSerializeInfo Environment::Serialize(SnapshotCreator* creator) {
16931683
return info;
16941684
}
16951685

1686+
std::ostream& operator<<(std::ostream& output,
1687+
const std::vector<PropInfo>& vec) {
1688+
output << "{\n";
1689+
for (const auto& info : vec) {
1690+
output << " " << info << ",\n";
1691+
}
1692+
output << "}";
1693+
return output;
1694+
}
1695+
1696+
std::ostream& operator<<(std::ostream& output, const PropInfo& info) {
1697+
output << "{ \"" << info.name << "\", " << std::to_string(info.id) << ", "
1698+
<< std::to_string(info.index) << " }";
1699+
return output;
1700+
}
1701+
16961702
std::ostream& operator<<(std::ostream& output,
16971703
const std::vector<std::string>& vec) {
16981704
output << "{\n";
@@ -1774,7 +1780,7 @@ void Environment::DeserializeProperties(const EnvSerializeInfo* info) {
17741780

17751781
const std::vector<PropInfo>& values = info->persistent_values;
17761782
size_t i = 0; // index to the array
1777-
size_t id = 0;
1783+
uint32_t id = 0;
17781784
#define V(PropertyName, TypeName) \
17791785
do { \
17801786
if (values.size() > i && id == values[i].id) { \

‎src/env.h

+8-3
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,7 @@ typedef size_t SnapshotIndex;
580580

581581
struct PropInfo {
582582
std::string name; // name for debugging
583-
size_t id; // In the list - in case there are any empty entries
583+
uint32_t id; // In the list - in case there are any empty entries
584584
SnapshotIndex index; // In the snapshot
585585
};
586586

@@ -987,8 +987,9 @@ struct EnvSerializeInfo {
987987
struct SnapshotData {
988988
enum class DataOwnership { kOwned, kNotOwned };
989989

990-
static const size_t kNodeBaseContextIndex = 0;
991-
static const size_t kNodeMainContextIndex = kNodeBaseContextIndex + 1;
990+
static const uint32_t kMagic = 0x143da19;
991+
static const SnapshotIndex kNodeBaseContextIndex = 0;
992+
static const SnapshotIndex kNodeMainContextIndex = kNodeBaseContextIndex + 1;
992993

993994
DataOwnership data_ownership = DataOwnership::kOwned;
994995

@@ -1000,12 +1001,16 @@ struct SnapshotData {
10001001
// TODO(joyeecheung): there should be a vector of env_info once we snapshot
10011002
// the worker environments.
10021003
EnvSerializeInfo env_info;
1004+
10031005
// A vector of built-in ids and v8::ScriptCompiler::CachedData, this can be
10041006
// shared across Node.js instances because they are supposed to share the
10051007
// read only space. We use native_module::CodeCacheInfo because
10061008
// v8::ScriptCompiler::CachedData is not copyable.
10071009
std::vector<native_module::CodeCacheInfo> code_cache;
10081010

1011+
void ToBlob(FILE* out) const;
1012+
static void FromBlob(SnapshotData* out, FILE* in);
1013+
10091014
~SnapshotData();
10101015

10111016
SnapshotData(const SnapshotData&) = delete;

‎src/node.cc

+111-22
Original file line numberDiff line numberDiff line change
@@ -1148,38 +1148,127 @@ void TearDownOncePerProcess() {
11481148
per_process::v8_platform.Dispose();
11491149
}
11501150

1151+
int GenerateAndWriteSnapshotData(const SnapshotData** snapshot_data_ptr,
1152+
InitializationResult* result) {
1153+
// nullptr indicates there's no snapshot data.
1154+
DCHECK_NULL(*snapshot_data_ptr);
1155+
1156+
// node:embedded_snapshot_main indicates that we are using the
1157+
// embedded snapshot and we are not supposed to clean it up.
1158+
if (result->args[1] == "node:embedded_snapshot_main") {
1159+
*snapshot_data_ptr = SnapshotBuilder::GetEmbeddedSnapshotData();
1160+
if (*snapshot_data_ptr == nullptr) {
1161+
// The Node.js binary is built without embedded snapshot
1162+
fprintf(stderr,
1163+
"node:embedded_snapshot_main was specified as snapshot "
1164+
"entry point but Node.js was built without embedded "
1165+
"snapshot.\n");
1166+
result->exit_code = 1;
1167+
return result->exit_code;
1168+
}
1169+
} else {
1170+
// Otherwise, load and run the specified main script.
1171+
std::unique_ptr<SnapshotData> generated_data =
1172+
std::make_unique<SnapshotData>();
1173+
result->exit_code = node::SnapshotBuilder::Generate(
1174+
generated_data.get(), result->args, result->exec_args);
1175+
if (result->exit_code == 0) {
1176+
*snapshot_data_ptr = generated_data.release();
1177+
} else {
1178+
return result->exit_code;
1179+
}
1180+
}
1181+
1182+
// Get the path to write the snapshot blob to.
1183+
std::string snapshot_blob_path;
1184+
if (!per_process::cli_options->snapshot_blob.empty()) {
1185+
snapshot_blob_path = per_process::cli_options->snapshot_blob;
1186+
} else {
1187+
// Defaults to snapshot.blob in the current working directory.
1188+
snapshot_blob_path = std::string("snapshot.blob");
1189+
}
1190+
1191+
FILE* fp = fopen(snapshot_blob_path.c_str(), "wb");
1192+
if (fp != nullptr) {
1193+
(*snapshot_data_ptr)->ToBlob(fp);
1194+
fclose(fp);
1195+
} else {
1196+
fprintf(stderr,
1197+
"Cannot open %s for writing a snapshot.\n",
1198+
snapshot_blob_path.c_str());
1199+
result->exit_code = 1;
1200+
}
1201+
return result->exit_code;
1202+
}
1203+
1204+
int LoadSnapshotDataAndRun(const SnapshotData** snapshot_data_ptr,
1205+
InitializationResult* result) {
1206+
// nullptr indicates there's no snapshot data.
1207+
DCHECK_NULL(*snapshot_data_ptr);
1208+
// --snapshot-blob indicates that we are reading a customized snapshot.
1209+
if (!per_process::cli_options->snapshot_blob.empty()) {
1210+
std::string filename = per_process::cli_options->snapshot_blob;
1211+
FILE* fp = fopen(filename.c_str(), "rb");
1212+
if (fp == nullptr) {
1213+
fprintf(stderr, "Cannot open %s", filename.c_str());
1214+
result->exit_code = 1;
1215+
return result->exit_code;
1216+
}
1217+
std::unique_ptr<SnapshotData> read_data = std::make_unique<SnapshotData>();
1218+
SnapshotData::FromBlob(read_data.get(), fp);
1219+
*snapshot_data_ptr = read_data.release();
1220+
fclose(fp);
1221+
} else if (per_process::cli_options->node_snapshot) {
1222+
// If --snapshot-blob is not specified, we are reading the embedded
1223+
// snapshot, but we will skip it if --no-node-snapshot is specified.
1224+
*snapshot_data_ptr = SnapshotBuilder::GetEmbeddedSnapshotData();
1225+
}
1226+
1227+
if ((*snapshot_data_ptr) != nullptr) {
1228+
NativeModuleLoader::RefreshCodeCache((*snapshot_data_ptr)->code_cache);
1229+
}
1230+
NodeMainInstance main_instance(*snapshot_data_ptr,
1231+
uv_default_loop(),
1232+
per_process::v8_platform.Platform(),
1233+
result->args,
1234+
result->exec_args);
1235+
result->exit_code = main_instance.Run();
1236+
return result->exit_code;
1237+
}
1238+
11511239
int Start(int argc, char** argv) {
11521240
InitializationResult result = InitializeOncePerProcess(argc, argv);
11531241
if (result.early_return) {
11541242
return result.exit_code;
11551243
}
11561244

1157-
if (per_process::cli_options->build_snapshot) {
1158-
fprintf(stderr,
1159-
"--build-snapshot is not yet supported in the node binary\n");
1160-
return 1;
1161-
}
1245+
DCHECK_EQ(result.exit_code, 0);
1246+
const SnapshotData* snapshot_data = nullptr;
11621247

1163-
{
1164-
bool use_node_snapshot = per_process::cli_options->node_snapshot;
1165-
const SnapshotData* snapshot_data =
1166-
use_node_snapshot ? SnapshotBuilder::GetEmbeddedSnapshotData()
1167-
: nullptr;
1168-
uv_loop_configure(uv_default_loop(), UV_METRICS_IDLE_TIME);
1169-
1170-
if (snapshot_data != nullptr) {
1171-
NativeModuleLoader::RefreshCodeCache(snapshot_data->code_cache);
1248+
auto cleanup_process = OnScopeLeave([&]() {
1249+
TearDownOncePerProcess();
1250+
1251+
if (snapshot_data != nullptr &&
1252+
snapshot_data->data_ownership == SnapshotData::DataOwnership::kOwned) {
1253+
delete snapshot_data;
1254+
}
1255+
});
1256+
1257+
uv_loop_configure(uv_default_loop(), UV_METRICS_IDLE_TIME);
1258+
1259+
// --build-snapshot indicates that we are in snapshot building mode.
1260+
if (per_process::cli_options->build_snapshot) {
1261+
if (result.args.size() < 2) {
1262+
fprintf(stderr,
1263+
"--build-snapshot must be used with an entry point script.\n"
1264+
"Usage: node --build-snapshot /path/to/entry.js\n");
1265+
return 9;
11721266
}
1173-
NodeMainInstance main_instance(snapshot_data,
1174-
uv_default_loop(),
1175-
per_process::v8_platform.Platform(),
1176-
result.args,
1177-
result.exec_args);
1178-
result.exit_code = main_instance.Run();
1267+
return GenerateAndWriteSnapshotData(&snapshot_data, &result);
11791268
}
11801269

1181-
TearDownOncePerProcess();
1182-
return result.exit_code;
1270+
// Without --build-snapshot, we are in snapshot loading mode.
1271+
return LoadSnapshotDataAndRun(&snapshot_data, &result);
11831272
}
11841273

11851274
int Stop(Environment* env) {

‎src/node_internals.h

+19
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,25 @@ std::string Basename(const std::string& str, const std::string& extension);
405405

406406
node_module napi_module_to_node_module(const napi_module* mod);
407407

408+
std::ostream& operator<<(std::ostream& output,
409+
const std::vector<SnapshotIndex>& v);
410+
std::ostream& operator<<(std::ostream& output,
411+
const std::vector<std::string>& vec);
412+
std::ostream& operator<<(std::ostream& output,
413+
const std::vector<PropInfo>& vec);
414+
std::ostream& operator<<(std::ostream& output, const PropInfo& d);
415+
std::ostream& operator<<(std::ostream& output, const EnvSerializeInfo& d);
416+
std::ostream& operator<<(std::ostream& output,
417+
const ImmediateInfo::SerializeInfo& d);
418+
std::ostream& operator<<(std::ostream& output,
419+
const TickInfo::SerializeInfo& d);
420+
std::ostream& operator<<(std::ostream& output,
421+
const AsyncHooks::SerializeInfo& d);
422+
423+
namespace performance {
424+
std::ostream& operator<<(std::ostream& output,
425+
const PerformanceState::SerializeInfo& d);
426+
}
408427
} // namespace node
409428

410429
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

‎src/node_options.cc

+7
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,13 @@ PerProcessOptionsParser::PerProcessOptionsParser(
764764
"", // It's a debug-only option.
765765
&PerProcessOptions::node_snapshot,
766766
kAllowedInEnvironment);
767+
AddOption("--snapshot-blob",
768+
"Path to the snapshot blob that's either the result of snapshot"
769+
"building, or the blob that is used to restore the application "
770+
"state",
771+
&PerProcessOptions::snapshot_blob,
772+
kAllowedInEnvironment);
773+
767774
// 12.x renamed this inadvertently, so alias it for consistency within the
768775
// release line, while using the original name for consistency with older
769776
// release lines.

‎src/node_options.h

+1
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ class PerProcessOptions : public Options {
237237
// snapshot used in different isolates in the same process to be the same.
238238
// Therefore --node-snapshot is a per-process option.
239239
bool node_snapshot = true;
240+
std::string snapshot_blob;
240241

241242
std::vector<std::string> security_reverts;
242243
bool print_bash_completion = false;

‎src/node_snapshotable.cc

+711-12
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
'use strict';
2+
3+
const fs = require('fs');
4+
const assert = require('assert');
5+
6+
assert.strictEqual(fs.foo, 'I am from the snapshot');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
'use strict';
2+
3+
const zlib = require('zlib');
4+
const fs = require('fs');
5+
const assert = require('assert');
6+
7+
const fixture = process.env.NODE_TEST_FIXTURE;
8+
const mode = process.env.NODE_TEST_MODE;
9+
const file = fs.readFileSync(fixture);
10+
const result = zlib.gunzipSync(file);
11+
12+
console.log(`Result length = ${result.byteLength}`);
13+
console.log('NODE_TEST_MODE:', mode);
14+
if (mode === 'snapshot') {
15+
globalThis.NODE_TEST_DATA = result;
16+
} else if (mode === 'verify') {
17+
assert.deepStrictEqual(globalThis.NODE_TEST_DATA, result);
18+
} else {
19+
assert.fail('Unknown mode');
20+
}

‎test/fixtures/snapshot/mutate-fs.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
'use strict';
2+
3+
const fs = require('fs');
4+
5+
fs.foo = 'I am from the snapshot';

‎test/parallel/test-snapshot-api.js

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
'use strict';
2+
3+
// This tests snapshot JS API
4+
5+
require('../common');
6+
const assert = require('assert');
7+
const { spawnSync } = require('child_process');
8+
const tmpdir = require('../common/tmpdir');
9+
const fixtures = require('../common/fixtures');
10+
const path = require('path');
11+
const fs = require('fs');
12+
13+
tmpdir.refresh();
14+
const blobPath = path.join(tmpdir.path, 'snapshot.blob');
15+
const entry = fixtures.path('snapshot', 'v8-startup-snapshot-api.js');
16+
{
17+
const child = spawnSync(process.execPath, [
18+
'--snapshot-blob',
19+
blobPath,
20+
'--build-snapshot',
21+
entry,
22+
], {
23+
cwd: tmpdir.path
24+
});
25+
if (child.status !== 0) {
26+
console.log(child.stderr.toString());
27+
console.log(child.stdout.toString());
28+
assert.strictEqual(child.status, 0);
29+
}
30+
const stats = fs.statSync(path.join(tmpdir.path, 'snapshot.blob'));
31+
assert(stats.isFile());
32+
}
33+
34+
{
35+
const child = spawnSync(process.execPath, [
36+
'--snapshot-blob',
37+
blobPath,
38+
], {
39+
cwd: tmpdir.path,
40+
env: {
41+
...process.env,
42+
}
43+
});
44+
45+
const stdout = child.stdout.toString().trim();
46+
const file = fs.readFileSync(fixtures.path('x1024.txt'), 'utf8');
47+
assert.strictEqual(stdout, file);
48+
assert.strictEqual(child.status, 0);
49+
}

‎test/parallel/test-snapshot-basic.js

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
'use strict';
2+
3+
// This tests that user land snapshots works when the instance restored from
4+
// the snapshot is launched with --help, --check
5+
6+
require('../common');
7+
const assert = require('assert');
8+
const { spawnSync } = require('child_process');
9+
const tmpdir = require('../common/tmpdir');
10+
const fixtures = require('../common/fixtures');
11+
const path = require('path');
12+
const fs = require('fs');
13+
14+
tmpdir.refresh();
15+
16+
let snapshotScript = 'node:embedded_snapshot_main';
17+
if (!process.config.variables.node_use_node_snapshot) {
18+
// Check that Node.js built without an embedded snapshot
19+
// exits with 1 when node:embedded_snapshot_main is specified
20+
// as snapshot entry point.
21+
const child = spawnSync(process.execPath, [
22+
'--build-snapshot',
23+
snapshotScript,
24+
], {
25+
cwd: tmpdir.path
26+
});
27+
28+
assert.match(
29+
child.stderr.toString(),
30+
/Node\.js was built without embedded snapshot/);
31+
assert.strictEqual(child.status, 1);
32+
33+
snapshotScript = fixtures.path('empty.js');
34+
}
35+
36+
// By default, the snapshot blob path is cwd/snapshot.blob.
37+
{
38+
// Create the snapshot.
39+
const child = spawnSync(process.execPath, [
40+
'--build-snapshot',
41+
snapshotScript,
42+
], {
43+
cwd: tmpdir.path
44+
});
45+
if (child.status !== 0) {
46+
console.log(child.stderr.toString());
47+
console.log(child.stdout.toString());
48+
console.log(child.signal);
49+
assert.strictEqual(child.status, 0);
50+
}
51+
const stats = fs.statSync(path.join(tmpdir.path, 'snapshot.blob'));
52+
assert(stats.isFile());
53+
}
54+
55+
tmpdir.refresh();
56+
const blobPath = path.join(tmpdir.path, 'my-snapshot.blob');
57+
{
58+
// Create the snapshot.
59+
const child = spawnSync(process.execPath, [
60+
'--snapshot-blob',
61+
blobPath,
62+
'--build-snapshot',
63+
snapshotScript,
64+
], {
65+
cwd: tmpdir.path
66+
});
67+
if (child.status !== 0) {
68+
console.log(child.stderr.toString());
69+
console.log(child.stdout.toString());
70+
console.log(child.signal);
71+
assert.strictEqual(child.status, 0);
72+
}
73+
const stats = fs.statSync(blobPath);
74+
assert(stats.isFile());
75+
}
76+
77+
{
78+
// Check --help.
79+
const child = spawnSync(process.execPath, [
80+
'--snapshot-blob',
81+
blobPath,
82+
'--help',
83+
], {
84+
cwd: tmpdir.path
85+
});
86+
87+
if (child.status !== 0) {
88+
console.log(child.stderr.toString());
89+
console.log(child.stdout.toString());
90+
console.log(child.signal);
91+
assert.strictEqual(child.status, 0);
92+
}
93+
94+
assert(child.stdout.toString().includes('--help'));
95+
}
96+
97+
{
98+
// Check -c.
99+
const child = spawnSync(process.execPath, [
100+
'--snapshot-blob',
101+
blobPath,
102+
'-c',
103+
fixtures.path('snapshot', 'marked.js'),
104+
], {
105+
cwd: tmpdir.path
106+
});
107+
108+
// Check that it is a noop.
109+
assert.strictEqual(child.stdout.toString().trim(), '');
110+
assert.strictEqual(child.stderr.toString().trim(), '');
111+
assert.strictEqual(child.status, 0);
112+
}
+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
'use strict';
2+
3+
// This tests that user land snapshots works when the instance restored from
4+
// the snapshot is launched as a CJS module.
5+
6+
require('../common');
7+
const assert = require('assert');
8+
const { spawnSync } = require('child_process');
9+
const tmpdir = require('../common/tmpdir');
10+
const fixtures = require('../common/fixtures');
11+
const path = require('path');
12+
const fs = require('fs');
13+
14+
tmpdir.refresh();
15+
const blobPath = path.join(tmpdir.path, 'snapshot.blob');
16+
const file = fixtures.path('snapshot', 'mutate-fs.js');
17+
const checkFile = fixtures.path('snapshot', 'check-mutate-fs.js');
18+
19+
{
20+
// Create the snapshot.
21+
const child = spawnSync(process.execPath, [
22+
'--snapshot-blob',
23+
blobPath,
24+
'--build-snapshot',
25+
file,
26+
], {
27+
cwd: tmpdir.path
28+
});
29+
if (child.status !== 0) {
30+
console.log(child.stderr.toString());
31+
console.log(child.stdout.toString());
32+
assert.strictEqual(child.status, 0);
33+
}
34+
const stats = fs.statSync(blobPath);
35+
assert(stats.isFile());
36+
}
37+
38+
{
39+
// Run the check file as a CJS module
40+
const child = spawnSync(process.execPath, [
41+
'--snapshot-blob',
42+
blobPath,
43+
checkFile,
44+
], {
45+
cwd: tmpdir.path
46+
});
47+
48+
if (child.status !== 0) {
49+
console.log(child.stderr.toString());
50+
console.log(child.stdout.toString());
51+
assert.strictEqual(child.status, 0);
52+
}
53+
}

‎test/parallel/test-snapshot-error.js

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
'use strict';
2+
3+
// This tests that the errors in the snapshot script can be handled
4+
// properly.
5+
6+
require('../common');
7+
const assert = require('assert');
8+
const { spawnSync } = require('child_process');
9+
const tmpdir = require('../common/tmpdir');
10+
const fixtures = require('../common/fixtures');
11+
const path = require('path');
12+
const fs = require('fs');
13+
14+
tmpdir.refresh();
15+
const blobPath = path.join(tmpdir.path, 'snapshot.blob');
16+
const entry = fixtures.path('snapshot', 'error.js');
17+
18+
// --build-snapshot should be run with an entry point.
19+
{
20+
const child = spawnSync(process.execPath, [
21+
'--snapshot-blob',
22+
blobPath,
23+
'--build-snapshot',
24+
], {
25+
cwd: tmpdir.path
26+
});
27+
const stderr = child.stderr.toString();
28+
console.log(child.status);
29+
console.log(stderr);
30+
console.log(child.stdout.toString());
31+
assert.strictEqual(child.status, 9);
32+
assert.match(stderr,
33+
/--build-snapshot must be used with an entry point script/);
34+
assert(!fs.existsSync(path.join(tmpdir.path, 'snapshot.blob')));
35+
}
36+
37+
// Loading a non-existent snapshot should fail.
38+
{
39+
const child = spawnSync(process.execPath, [
40+
'--snapshot-blob',
41+
blobPath,
42+
entry,
43+
], {
44+
cwd: tmpdir.path
45+
});
46+
const stderr = child.stderr.toString();
47+
console.log(child.status);
48+
console.log(stderr);
49+
console.log(child.stdout.toString());
50+
assert.strictEqual(child.status, 1);
51+
assert.match(stderr, /Cannot open/);
52+
assert(!fs.existsSync(path.join(tmpdir.path, 'snapshot.blob')));
53+
}
54+
55+
56+
// Running an script that throws an error should result in an exit code of 1.
57+
{
58+
const child = spawnSync(process.execPath, [
59+
'--snapshot-blob',
60+
blobPath,
61+
'--build-snapshot',
62+
entry,
63+
], {
64+
cwd: tmpdir.path
65+
});
66+
const stderr = child.stderr.toString();
67+
console.log(child.status);
68+
console.log(stderr);
69+
console.log(child.stdout.toString());
70+
assert.strictEqual(child.status, 1);
71+
assert.match(stderr, /error\.js:1/);
72+
assert(!fs.existsSync(path.join(tmpdir.path, 'snapshot.blob')));
73+
}

‎test/parallel/test-snapshot-eval.js

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
'use strict';
2+
3+
// This tests that user land snapshots works when the instance restored from
4+
// the snapshot is launched with -p and -e
5+
6+
require('../common');
7+
const assert = require('assert');
8+
const { spawnSync } = require('child_process');
9+
const tmpdir = require('../common/tmpdir');
10+
const fixtures = require('../common/fixtures');
11+
const path = require('path');
12+
const fs = require('fs');
13+
14+
tmpdir.refresh();
15+
const blobPath = path.join(tmpdir.path, 'snapshot.blob');
16+
const file = fixtures.path('snapshot', 'mutate-fs.js');
17+
18+
{
19+
// Create the snapshot.
20+
const child = spawnSync(process.execPath, [
21+
'--snapshot-blob',
22+
blobPath,
23+
'--build-snapshot',
24+
file,
25+
], {
26+
cwd: tmpdir.path
27+
});
28+
if (child.status !== 0) {
29+
console.log(child.stderr.toString());
30+
console.log(child.stdout.toString());
31+
assert.strictEqual(child.status, 0);
32+
}
33+
const stats = fs.statSync(blobPath);
34+
assert(stats.isFile());
35+
}
36+
37+
{
38+
// Check -p works.
39+
const child = spawnSync(process.execPath, [
40+
'--snapshot-blob',
41+
blobPath,
42+
'-p',
43+
'require("fs").foo',
44+
], {
45+
cwd: tmpdir.path
46+
});
47+
48+
if (child.status !== 0) {
49+
console.log(child.stderr.toString());
50+
console.log(child.stdout.toString());
51+
assert.strictEqual(child.status, 0);
52+
}
53+
assert(/I am from the snapshot/.test(child.stdout.toString()));
54+
}
55+
56+
{
57+
// Check -e works.
58+
const child = spawnSync(process.execPath, [
59+
'--snapshot-blob',
60+
blobPath,
61+
'-e',
62+
'console.log(require("fs").foo)',
63+
], {
64+
cwd: tmpdir.path
65+
});
66+
67+
if (child.status !== 0) {
68+
console.log(child.stderr.toString());
69+
console.log(child.stdout.toString());
70+
assert.strictEqual(child.status, 0);
71+
}
72+
assert(/I am from the snapshot/.test(child.stdout.toString()));
73+
}

‎test/parallel/test-snapshot-gzip.js

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
'use strict';
2+
3+
// This tests that a program that decompresses a gzip file and saves the
4+
// content can be snapshotted and deserialized properly.
5+
6+
require('../common');
7+
const assert = require('assert');
8+
const { spawnSync } = require('child_process');
9+
const tmpdir = require('../common/tmpdir');
10+
const fixtures = require('../common/fixtures');
11+
const path = require('path');
12+
const fs = require('fs');
13+
14+
tmpdir.refresh();
15+
const blobPath = path.join(tmpdir.path, 'snapshot.blob');
16+
const file = fixtures.path('snapshot', 'decompress-gzip-sync.js');
17+
18+
{
19+
// By default, the snapshot blob path is snapshot.blob at cwd
20+
const child = spawnSync(process.execPath, [
21+
'--snapshot-blob',
22+
blobPath,
23+
'--build-snapshot',
24+
file,
25+
], {
26+
env: {
27+
...process.env,
28+
NODE_TEST_FIXTURE: fixtures.path('person.jpg.gz'),
29+
NODE_TEST_MODE: 'snapshot'
30+
},
31+
cwd: tmpdir.path
32+
});
33+
const stderr = child.stderr.toString();
34+
const stdout = child.stdout.toString();
35+
console.log(stderr);
36+
console.log(stdout);
37+
assert.strictEqual(child.status, 0);
38+
39+
const stats = fs.statSync(path.join(tmpdir.path, 'snapshot.blob'));
40+
assert(stats.isFile());
41+
assert(stdout.includes('NODE_TEST_MODE: snapshot'));
42+
}
43+
44+
{
45+
const child = spawnSync(process.execPath, [
46+
'--snapshot-blob',
47+
blobPath,
48+
file,
49+
], {
50+
env: {
51+
...process.env,
52+
NODE_TEST_FIXTURE: fixtures.path('person.jpg.gz'),
53+
NODE_TEST_MODE: 'verify'
54+
},
55+
cwd: tmpdir.path
56+
});
57+
const stderr = child.stderr.toString();
58+
const stdout = child.stdout.toString();
59+
console.log(stderr);
60+
console.log(stdout);
61+
assert.strictEqual(child.status, 0);
62+
assert(stdout.includes('NODE_TEST_MODE: verify'));
63+
}

0 commit comments

Comments
 (0)
Please sign in to comment.