Skip to content

Commit 513f524

Browse files
joyeecheungtargos
authored andcommitted
v8: add v8.startupSnapshot utils
This adds several APIs under the `v8.startupSnapshot` namespace for specifying hooks into the startup snapshot serialization and deserialization. - isBuildingSnapshot() - addSerializeCallback() - addDeserializeCallback() - setDeserializeMainFunction() PR-URL: #43329 Fixes: #42617 Refs: #35711 Reviewed-By: Chengzhong Wu <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent 20fa30c commit 513f524

15 files changed

+413
-29
lines changed

doc/api/errors.md

+15
Original file line numberDiff line numberDiff line change
@@ -1167,6 +1167,13 @@ because the `node:domain` module has been loaded at an earlier point in time.
11671167
The stack trace is extended to include the point in time at which the
11681168
`node:domain` module had been loaded.
11691169

1170+
<a id="ERR_DUPLICATE_STARTUP_SNAPSHOT_MAIN_FUNCTION"></a>
1171+
1172+
### `ERR_DUPLICATE_STARTUP_SNAPSHOT_MAIN_FUNCTION`
1173+
1174+
[`v8.startupSnapshot.setDeserializeMainFunction()`][] could not be called
1175+
because it had already been called before.
1176+
11701177
<a id="ERR_ENCODING_INVALID_ENCODED_DATA"></a>
11711178

11721179
### `ERR_ENCODING_INVALID_ENCODED_DATA`
@@ -2293,6 +2300,13 @@ has occurred when attempting to start the loop.
22932300
Once no more items are left in the queue, the idle loop must be suspended. This
22942301
error indicates that the idle loop has failed to stop.
22952302

2303+
<a id="ERR_NOT_BUILDING_SNAPSHOT"></a>
2304+
2305+
### `ERR_NOT_BUILDING_SNAPSHOT`
2306+
2307+
An attempt was made to use operations that can only be used when building
2308+
V8 startup snapshot even though Node.js isn't building one.
2309+
22962310
<a id="ERR_NO_CRYPTO"></a>
22972311

22982312
### `ERR_NO_CRYPTO`
@@ -3460,6 +3474,7 @@ The native call from `process.cpuUsage` could not be processed.
34603474
[`subprocess.send()`]: child_process.md#subprocesssendmessage-sendhandle-options-callback
34613475
[`util.getSystemErrorName(error.errno)`]: util.md#utilgetsystemerrornameerr
34623476
[`util.parseArgs()`]: util.md#utilparseargsconfig
3477+
[`v8.startupSnapshot.setDeserializeMainFunction()`]: v8.md#v8startupsnapshotsetdeserializemainfunctioncallback-data
34633478
[`zlib`]: zlib.md
34643479
[crypto digest algorithm]: crypto.md#cryptogethashes
34653480
[debugger]: debugger.md

doc/api/v8.md

+131
Original file line numberDiff line numberDiff line change
@@ -853,6 +853,137 @@ Called immediately after a promise continuation executes. This may be after a
853853
Called when the promise receives a resolution or rejection value. This may
854854
occur synchronously in the case of `Promise.resolve()` or `Promise.reject()`.
855855

856+
## Startup Snapshot API
857+
858+
<!-- YAML
859+
added: REPLACEME
860+
-->
861+
862+
> Stability: 1 - Experimental
863+
864+
The `v8.startupSnapshot` interface can be used to add serialization and
865+
deserialization hooks for custom startup snapshots. Currently the startup
866+
snapshots can only be built into the Node.js binary from source.
867+
868+
```console
869+
$ cd /path/to/node
870+
$ ./configure --node-snapshot-main=entry.js
871+
$ make node
872+
# This binary contains the result of the execution of entry.js
873+
$ out/Release/node
874+
```
875+
876+
In the example above, `entry.js` can use methods from the `v8.startupSnapshot`
877+
interface to specify how to save information for custom objects in the snapshot
878+
during serialization and how the information can be used to synchronize these
879+
objects during deserialization of the snapshot. For example, if the `entry.js`
880+
contains the following script:
881+
882+
```cjs
883+
'use strict';
884+
885+
const fs = require('fs');
886+
const zlib = require('zlib');
887+
const path = require('path');
888+
const assert = require('assert');
889+
890+
const {
891+
isBuildingSnapshot,
892+
addSerializeCallback,
893+
addDeserializeCallback,
894+
setDeserializeMainFunction
895+
} = require('v8').startupSnapshot;
896+
897+
const filePath = path.resolve(__dirname, '../x1024.txt');
898+
const storage = {};
899+
900+
assert(isBuildingSnapshot());
901+
902+
addSerializeCallback(({ filePath }) => {
903+
storage[filePath] = zlib.gzipSync(fs.readFileSync(filePath));
904+
}, { filePath });
905+
906+
addDeserializeCallback(({ filePath }) => {
907+
storage[filePath] = zlib.gunzipSync(storage[filePath]);
908+
}, { filePath });
909+
910+
setDeserializeMainFunction(({ filePath }) => {
911+
console.log(storage[filePath].toString());
912+
}, { filePath });
913+
```
914+
915+
The resulted binary will simply print the data deserialized from the snapshot
916+
during start up:
917+
918+
```console
919+
$ out/Release/node
920+
# Prints content of ./test/fixtures/x1024.txt
921+
```
922+
923+
Currently the API is only available to a Node.js instance launched from the
924+
default snapshot, that is, the application deserialized from a user-land
925+
snapshot cannot use these APIs again.
926+
927+
### `v8.startupSnapshot.addSerializeCallback(callback[, data])`
928+
929+
<!-- YAML
930+
added: REPLACEME
931+
-->
932+
933+
* `callback` {Function} Callback to be invoked before serialization.
934+
* `data` {any} Optional data that will be passed to the `callback` when it
935+
gets called.
936+
937+
Add a callback that will be called when the Node.js instance is about to
938+
get serialized into a snapshot and exit. This can be used to release
939+
resources that should not or cannot be serialized or to convert user data
940+
into a form more suitable for serialization.
941+
942+
### `v8.startupSnapshot.addDeserializeCallback(callback[, data])`
943+
944+
<!-- YAML
945+
added: REPLACEME
946+
-->
947+
948+
* `callback` {Function} Callback to be invoked after the snapshot is
949+
deserialized.
950+
* `data` {any} Optional data that will be passed to the `callback` when it
951+
gets called.
952+
953+
Add a callback that will be called when the Node.js instance is deserialized
954+
from a snapshot. The `callback` and the `data` (if provided) will be
955+
serialized into the snapshot, they can be used to re-initialize the state
956+
of the application or to re-acquire resources that the application needs
957+
when the application is restarted from the snapshot.
958+
959+
### `v8.startupSnapshot.setDeserializeMainFunction(callback[, data])`
960+
961+
<!-- YAML
962+
added: REPLACEME
963+
-->
964+
965+
* `callback` {Function} Callback to be invoked as the entry point after the
966+
snapshot is deserialized.
967+
* `data` {any} Optional data that will be passed to the `callback` when it
968+
gets called.
969+
970+
This sets the entry point of the Node.js application when it is deserialized
971+
from a snapshot. This can be called only once in the snapshot building
972+
script. If called, the deserialized application no longer needs an additional
973+
entry point script to start up and will simply invoke the callback along with
974+
the deserialized data (if provided), otherwise an entry point script still
975+
needs to be provided to the deserialized application.
976+
977+
### `v8.startupSnapshot.isBuildingSnapshot()`
978+
979+
<!-- YAML
980+
added: REPLACEME
981+
-->
982+
983+
* Returns: {boolean}
984+
985+
Returns true if the Node.js instance is run to build a snapshot.
986+
856987
[HTML structured clone algorithm]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
857988
[Hook Callbacks]: #hook-callbacks
858989
[V8]: https://developers.google.com/v8/

lib/internal/bootstrap/pre_execution.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ function prepareMainThreadExecution(expandArgv1 = false,
5151
setupCoverageHooks(process.env.NODE_V8_COVERAGE);
5252
}
5353

54-
5554
setupDebugEnv();
5655

5756
// Print stack trace on `SIGINT` if option `--trace-sigint` presents.
@@ -82,6 +81,8 @@ function prepareMainThreadExecution(expandArgv1 = false,
8281
initializeDeprecations();
8382
initializeWASI();
8483

84+
require('internal/v8/startup_snapshot').runDeserializeCallbacks();
85+
8586
if (!initialzeModules) {
8687
return;
8788
}

lib/internal/bootstrap/switches/is_main_thread.js

+19-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
const { ObjectDefineProperty } = primordials;
44
const rawMethods = internalBinding('process_methods');
5-
5+
const {
6+
addSerializeCallback,
7+
isBuildingSnapshot
8+
} = require('v8').startupSnapshot;
69
// TODO(joyeecheung): deprecate and remove these underscore methods
710
process._debugProcess = rawMethods._debugProcess;
811
process._debugEnd = rawMethods._debugEnd;
@@ -134,6 +137,12 @@ function refreshStderrOnSigWinch() {
134137
stderr._refreshSize();
135138
}
136139

140+
function addCleanup(fn) {
141+
if (isBuildingSnapshot()) {
142+
addSerializeCallback(fn);
143+
}
144+
}
145+
137146
function getStdout() {
138147
if (stdout) return stdout;
139148
stdout = createWritableStdioStream(1);
@@ -145,12 +154,14 @@ function getStdout() {
145154
process.on('SIGWINCH', refreshStdoutOnSigWinch);
146155
}
147156

148-
internalBinding('mksnapshot').cleanups.push(function cleanupStdout() {
157+
addCleanup(function cleanupStdout() {
149158
stdout._destroy = stdoutDestroy;
150159
stdout.destroy();
151160
process.removeListener('SIGWINCH', refreshStdoutOnSigWinch);
152161
stdout = undefined;
153162
});
163+
// No need to add deserialize callback because stdout = undefined above
164+
// causes the stream to be lazily initialized again later.
154165
return stdout;
155166
}
156167

@@ -164,12 +175,14 @@ function getStderr() {
164175
if (stderr.isTTY) {
165176
process.on('SIGWINCH', refreshStderrOnSigWinch);
166177
}
167-
internalBinding('mksnapshot').cleanups.push(function cleanupStderr() {
178+
addCleanup(function cleanupStderr() {
168179
stderr._destroy = stderrDestroy;
169180
stderr.destroy();
170181
process.removeListener('SIGWINCH', refreshStderrOnSigWinch);
171182
stderr = undefined;
172183
});
184+
// No need to add deserialize callback because stderr = undefined above
185+
// causes the stream to be lazily initialized again later.
173186
return stderr;
174187
}
175188

@@ -260,10 +273,12 @@ function getStdin() {
260273
}
261274
}
262275

263-
internalBinding('mksnapshot').cleanups.push(function cleanupStdin() {
276+
addCleanup(function cleanupStdin() {
264277
stdin.destroy();
265278
stdin = undefined;
266279
});
280+
// No need to add deserialize callback because stdin = undefined above
281+
// causes the stream to be lazily initialized again later.
267282
return stdin;
268283
}
269284

lib/internal/errors.js

+4
Original file line numberDiff line numberDiff line change
@@ -1001,6 +1001,8 @@ E('ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE',
10011001
'The `domain` module is in use, which is mutually exclusive with calling ' +
10021002
'process.setUncaughtExceptionCaptureCallback()',
10031003
Error);
1004+
E('ERR_DUPLICATE_STARTUP_SNAPSHOT_MAIN_FUNCTION',
1005+
'Deserialize main function is already configured.', Error);
10041006
E('ERR_ENCODING_INVALID_ENCODED_DATA', function(encoding, ret) {
10051007
this.errno = ret;
10061008
return `The encoded data was not valid for encoding ${encoding}`;
@@ -1460,6 +1462,8 @@ E('ERR_NETWORK_IMPORT_BAD_RESPONSE',
14601462
"import '%s' received a bad response: %s", Error);
14611463
E('ERR_NETWORK_IMPORT_DISALLOWED',
14621464
"import of '%s' by %s is not supported: %s", Error);
1465+
E('ERR_NOT_BUILDING_SNAPSHOT',
1466+
'Operation cannot be invoked when not building startup snapshot', Error);
14631467
E('ERR_NO_CRYPTO',
14641468
'Node.js is not compiled with OpenSSL crypto support', Error);
14651469
E('ERR_NO_ICU',

lib/internal/main/mksnapshot.js

+7-10
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const {
99
const binding = internalBinding('mksnapshot');
1010
const { NativeModule } = require('internal/bootstrap/loaders');
1111
const {
12-
compileSnapshotMain,
12+
compileSerializeMain,
1313
} = binding;
1414

1515
const {
@@ -83,7 +83,7 @@ const supportedModules = new SafeSet(new SafeArrayIterator([
8383
'v8',
8484
// 'vm',
8585
// 'worker_threads',
86-
// 'zlib',
86+
'zlib',
8787
]));
8888

8989
const warnedModules = new SafeSet();
@@ -117,25 +117,22 @@ function main() {
117117
} = require('internal/bootstrap/pre_execution');
118118

119119
prepareMainThreadExecution(true, false);
120-
process.once('beforeExit', function runCleanups() {
121-
for (const cleanup of binding.cleanups) {
122-
cleanup();
123-
}
124-
});
125120

126121
const file = process.argv[1];
127122
const path = require('path');
128123
const filename = path.resolve(file);
129124
const dirname = path.dirname(filename);
130125
const source = readFileSync(file, 'utf-8');
131-
const snapshotMainFunction = compileSnapshotMain(filename, source);
126+
const serializeMainFunction = compileSerializeMain(filename, source);
127+
128+
require('internal/v8/startup_snapshot').initializeCallbacks();
132129

133130
if (getOptionValue('--inspect-brk')) {
134131
internalBinding('inspector').callAndPauseOnStart(
135-
snapshotMainFunction, undefined,
132+
serializeMainFunction, undefined,
136133
requireForUserSnapshot, filename, dirname);
137134
} else {
138-
snapshotMainFunction(requireForUserSnapshot, filename, dirname);
135+
serializeMainFunction(requireForUserSnapshot, filename, dirname);
139136
}
140137
}
141138

0 commit comments

Comments
 (0)