Skip to content

Commit 5dec186

Browse files
joyeecheungRafaelGSS
authored andcommitted
src: support snapshot in single executable applications
This patch adds snapshot support to single executable applications. To build a snapshot from the main script when preparing the blob that will be injected into the single executable application, add `"useSnapshot": true` to the configuration passed to `--experimental-sea-config`. For example: ``` { "main": "snapshot.js", "output": "sea-prep.blob", "useSnapshot": true } ``` The main script used to build the snapshot must invoke `v8.startupSnapshot.setDeserializeMainFunction()` to configure the entry point. The generated startup snapshot would be part of the preparation blob and get injected into the final executable. When the single executable application is launched, instead of running the `main` script from scratch, Node.js would instead deserialize the snapshot to get to the state initialized during build-time directly. PR-URL: #46824 Refs: nodejs/single-executable#57 Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Darshan Sen <[email protected]>
1 parent d92b013 commit 5dec186

9 files changed

+322
-42
lines changed

doc/api/single-executable-applications.md

+40-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
added:
77
- v19.7.0
88
- v18.16.0
9+
changes:
10+
- version: REPLACEME
11+
pr-url: https://github.com/nodejs/node/pull/46824
12+
description: Added support for "useSnapshot".
913
-->
1014

1115
> Stability: 1 - Experimental: This feature is being designed and will change.
@@ -169,14 +173,46 @@ The configuration currently reads the following top-level fields:
169173
{
170174
"main": "/path/to/bundled/script.js",
171175
"output": "/path/to/write/the/generated/blob.blob",
172-
"disableExperimentalSEAWarning": true // Default: false
176+
"disableExperimentalSEAWarning": true, // Default: false
177+
"useSnapshot": false // Default: false
173178
}
174179
```
175180
176181
If the paths are not absolute, Node.js will use the path relative to the
177182
current working directory. The version of the Node.js binary used to produce
178183
the blob must be the same as the one to which the blob will be injected.
179184
185+
### Startup snapshot support
186+
187+
The `useSnapshot` field can be used to enable startup snapshot support. In this
188+
case the `main` script would not be when the final executable is launched.
189+
Instead, it would be run when the single executable application preparation
190+
blob is generated on the building machine. The generated preparation blob would
191+
then include a snapshot capturing the states initialized by the `main` script.
192+
The final executable with the preparation blob injected would deserialize
193+
the snapshot at run time.
194+
195+
When `useSnapshot` is true, the main script must invoke the
196+
[`v8.startupSnapshot.setDeserializeMainFunction()`][] API to configure code
197+
that needs to be run when the final executable is launched by the users.
198+
199+
The typical pattern for an application to use snapshot in a single executable
200+
application is:
201+
202+
1. At build time, on the building machine, the main script is run to
203+
initialize the heap to a state that's ready to take user input. The script
204+
should also configure a main function with
205+
[`v8.startupSnapshot.setDeserializeMainFunction()`][]. This function will be
206+
compiled and serialized into the snapshot, but not invoked at build time.
207+
2. At run time, the main function will be run on top of the deserialized heap
208+
on the user machine to process user input and generate output.
209+
210+
The general constraints of the startup snapshot scripts also apply to the main
211+
script when it's used to build snapshot for the single executable application,
212+
and the main script can use the [`v8.startupSnapshot` API][] to adapt to
213+
these constraints. See
214+
[documentation about startup snapshot support in Node.js][].
215+
180216
## Notes
181217
182218
### `require(id)` in the injected module is not file based
@@ -249,6 +285,9 @@ to help us document them.
249285
[`process.execPath`]: process.md#processexecpath
250286
[`require()`]: modules.md#requireid
251287
[`require.main`]: modules.md#accessing-the-main-module
288+
[`v8.startupSnapshot.setDeserializeMainFunction()`]: v8.md#v8startupsnapshotsetdeserializemainfunctioncallback-data
289+
[`v8.startupSnapshot` API]: v8.md#startup-snapshot-api
290+
[documentation about startup snapshot support in Node.js]: cli.md#--build-snapshot
252291
[fuse]: https://www.electronjs.org/docs/latest/tutorial/fuses
253292
[postject]: https://github.com/nodejs/postject
254293
[signtool]: https://learn.microsoft.com/en-us/windows/win32/seccrypto/signtool

lib/internal/main/mksnapshot.js

+9
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ const {
1616
anonymousMainPath,
1717
} = internalBinding('mksnapshot');
1818

19+
const { isExperimentalSeaWarningNeeded } = internalBinding('sea');
20+
21+
const { emitExperimentalWarning } = require('internal/util');
22+
1923
const {
2024
getOptionValue,
2125
} = require('internal/options');
@@ -126,6 +130,7 @@ function requireForUserSnapshot(id) {
126130
return require(normalizedId);
127131
}
128132

133+
129134
function main() {
130135
prepareMainThreadExecution(true, false);
131136
initializeCallbacks();
@@ -167,6 +172,10 @@ function main() {
167172

168173
const serializeMainArgs = [process, requireForUserSnapshot, minimalRunCjs];
169174

175+
if (isExperimentalSeaWarningNeeded()) {
176+
emitExperimentalWarning('Single executable application');
177+
}
178+
170179
if (getOptionValue('--inspect-brk')) {
171180
internalBinding('inspector').callAndPauseOnStart(
172181
runEmbedderEntryPoint, undefined, ...serializeMainArgs);

lib/internal/process/pre_execution.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ function patchProcessObject(expandArgv1) {
175175
__proto__: null,
176176
enumerable: true,
177177
// Only set it to true during snapshot building.
178-
configurable: getOptionValue('--build-snapshot'),
178+
configurable: isBuildingSnapshot(),
179179
value: process.argv[0],
180180
});
181181

src/node.cc

+60-23
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,17 @@ MaybeLocal<Value> StartExecution(Environment* env, StartExecutionCallback cb) {
292292

293293
CHECK(!env->isolate_data()->is_building_snapshot());
294294

295+
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
296+
if (sea::IsSingleExecutable()) {
297+
sea::SeaResource sea = sea::FindSingleExecutableResource();
298+
// The SEA preparation blob building process should already enforce this,
299+
// this check is just here to guard against the unlikely case where
300+
// the SEA preparation blob has been manually modified by someone.
301+
CHECK_IMPLIES(sea.use_snapshot(),
302+
!env->snapshot_deserialize_main().IsEmpty());
303+
}
304+
#endif
305+
295306
// TODO(joyeecheung): move these conditions into JS land and let the
296307
// deserialize main function take precedence. For workers, we need to
297308
// move the pre-execution part into a different file that can be
@@ -1198,49 +1209,66 @@ ExitCode GenerateAndWriteSnapshotData(const SnapshotData** snapshot_data_ptr,
11981209
return exit_code;
11991210
}
12001211

1201-
ExitCode LoadSnapshotDataAndRun(const SnapshotData** snapshot_data_ptr,
1202-
const InitializationResultImpl* result) {
1203-
ExitCode exit_code = result->exit_code_enum();
1212+
bool LoadSnapshotData(const SnapshotData** snapshot_data_ptr) {
12041213
// nullptr indicates there's no snapshot data.
12051214
DCHECK_NULL(*snapshot_data_ptr);
1215+
1216+
bool is_sea = false;
1217+
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
1218+
if (sea::IsSingleExecutable()) {
1219+
is_sea = true;
1220+
sea::SeaResource sea = sea::FindSingleExecutableResource();
1221+
if (sea.use_snapshot()) {
1222+
std::unique_ptr<SnapshotData> read_data =
1223+
std::make_unique<SnapshotData>();
1224+
std::string_view snapshot = sea.main_code_or_snapshot;
1225+
if (SnapshotData::FromBlob(read_data.get(), snapshot)) {
1226+
*snapshot_data_ptr = read_data.release();
1227+
return true;
1228+
} else {
1229+
fprintf(stderr, "Invalid snapshot data in single executable binary\n");
1230+
return false;
1231+
}
1232+
}
1233+
}
1234+
#endif
1235+
12061236
// --snapshot-blob indicates that we are reading a customized snapshot.
1207-
if (!per_process::cli_options->snapshot_blob.empty()) {
1237+
// Ignore it when we are loading from SEA.
1238+
if (!is_sea && !per_process::cli_options->snapshot_blob.empty()) {
12081239
std::string filename = per_process::cli_options->snapshot_blob;
12091240
FILE* fp = fopen(filename.c_str(), "rb");
12101241
if (fp == nullptr) {
12111242
fprintf(stderr, "Cannot open %s", filename.c_str());
1212-
exit_code = ExitCode::kStartupSnapshotFailure;
1213-
return exit_code;
1243+
return false;
12141244
}
12151245
std::unique_ptr<SnapshotData> read_data = std::make_unique<SnapshotData>();
12161246
bool ok = SnapshotData::FromFile(read_data.get(), fp);
12171247
fclose(fp);
12181248
if (!ok) {
1219-
// If we fail to read the customized snapshot,
1220-
// simply exit with kStartupSnapshotFailure.
1221-
exit_code = ExitCode::kStartupSnapshotFailure;
1222-
return exit_code;
1249+
return false;
12231250
}
12241251
*snapshot_data_ptr = read_data.release();
1225-
} else if (per_process::cli_options->node_snapshot) {
1226-
// If --snapshot-blob is not specified, we are reading the embedded
1227-
// snapshot, but we will skip it if --no-node-snapshot is specified.
1252+
return true;
1253+
}
1254+
1255+
if (per_process::cli_options->node_snapshot) {
1256+
// If --snapshot-blob is not specified or if the SEA contains no snapshot,
1257+
// we are reading the embedded snapshot, but we will skip it if
1258+
// --no-node-snapshot is specified.
12281259
const node::SnapshotData* read_data =
12291260
SnapshotBuilder::GetEmbeddedSnapshotData();
1230-
if (read_data != nullptr && read_data->Check()) {
1261+
if (read_data != nullptr) {
1262+
if (!read_data->Check()) {
1263+
return false;
1264+
}
12311265
// If we fail to read the embedded snapshot, treat it as if Node.js
12321266
// was built without one.
12331267
*snapshot_data_ptr = read_data;
12341268
}
12351269
}
12361270

1237-
NodeMainInstance main_instance(*snapshot_data_ptr,
1238-
uv_default_loop(),
1239-
per_process::v8_platform.Platform(),
1240-
result->args(),
1241-
result->exec_args());
1242-
exit_code = main_instance.Run();
1243-
return exit_code;
1271+
return true;
12441272
}
12451273

12461274
static ExitCode StartInternal(int argc, char** argv) {
@@ -1275,7 +1303,8 @@ static ExitCode StartInternal(int argc, char** argv) {
12751303

12761304
std::string sea_config = per_process::cli_options->experimental_sea_config;
12771305
if (!sea_config.empty()) {
1278-
return sea::BuildSingleExecutableBlob(sea_config);
1306+
return sea::BuildSingleExecutableBlob(
1307+
sea_config, result->args(), result->exec_args());
12791308
}
12801309

12811310
// --build-snapshot indicates that we are in snapshot building mode.
@@ -1290,7 +1319,15 @@ static ExitCode StartInternal(int argc, char** argv) {
12901319
}
12911320

12921321
// Without --build-snapshot, we are in snapshot loading mode.
1293-
return LoadSnapshotDataAndRun(&snapshot_data, result.get());
1322+
if (!LoadSnapshotData(&snapshot_data)) {
1323+
return ExitCode::kStartupSnapshotFailure;
1324+
}
1325+
NodeMainInstance main_instance(snapshot_data,
1326+
uv_default_loop(),
1327+
per_process::v8_platform.Platform(),
1328+
result->args(),
1329+
result->exec_args());
1330+
return main_instance.Run();
12941331
}
12951332

12961333
int Start(int argc, char** argv) {

src/node_main_instance.cc

+7-3
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,16 @@ void NodeMainInstance::Run(ExitCode* exit_code, Environment* env) {
9292
bool runs_sea_code = false;
9393
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
9494
if (sea::IsSingleExecutable()) {
95-
runs_sea_code = true;
9695
sea::SeaResource sea = sea::FindSingleExecutableResource();
97-
std::string_view code = sea.code;
98-
LoadEnvironment(env, code);
96+
if (!sea.use_snapshot()) {
97+
runs_sea_code = true;
98+
std::string_view code = sea.main_code_or_snapshot;
99+
LoadEnvironment(env, code);
100+
}
99101
}
100102
#endif
103+
// Either there is already a snapshot main function from SEA, or it's not
104+
// a SEA at all.
101105
if (!runs_sea_code) {
102106
LoadEnvironment(env, StartExecutionCallback{});
103107
}

0 commit comments

Comments
 (0)