Skip to content

Commit ac150b9

Browse files
committedJun 6, 2022
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()
1 parent 7a71ae8 commit ac150b9

15 files changed

+401
-28
lines changed
 

‎doc/api/errors.md

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

1182+
<a id="ERR_DUPLICATE_STARTUP_SNAPSHOT_MAIN_FUNCTION"></a>
1183+
1184+
### `ERR_DUPLICATE_STARTUP_SNAPSHOT_MAIN_FUNCTION`
1185+
1186+
[`v8.startupSnapshot.setDeserializeMainFunction()`][] could not be called
1187+
because it had already been called before.
1188+
11821189
<a id="ERR_ENCODING_INVALID_ENCODED_DATA"></a>
11831190

11841191
### `ERR_ENCODING_INVALID_ENCODED_DATA`
@@ -2314,6 +2321,13 @@ has occurred when attempting to start the loop.
23142321
Once no more items are left in the queue, the idle loop must be suspended. This
23152322
error indicates that the idle loop has failed to stop.
23162323

2324+
<a id="ERR_NOT_BUILDING_SNAPSHOT"></a>
2325+
2326+
### `ERR_NOT_BUILDING_SNAPSHOT`
2327+
2328+
An attempt was made to use operations that can only be used when building
2329+
V8 startup snapshot even though Node.js isn't building one.
2330+
23172331
<a id="ERR_NO_CRYPTO"></a>
23182332

23192333
### `ERR_NO_CRYPTO`
@@ -3501,6 +3515,7 @@ The native call from `process.cpuUsage` could not be processed.
35013515
[`url.parse()`]: url.md#urlparseurlstring-parsequerystring-slashesdenotehost
35023516
[`util.getSystemErrorName(error.errno)`]: util.md#utilgetsystemerrornameerr
35033517
[`util.parseArgs()`]: util.md#utilparseargsconfig
3518+
[`v8.startupSnapshot.setDeserializeMainFunction()`]: v8.md#v8startupsnapshotsetdeserializemainfunctioncallback-data
35043519
[`zlib`]: zlib.md
35053520
[crypto digest algorithm]: crypto.md#cryptogethashes
35063521
[debugger]: debugger.md

‎doc/api/v8.md

+123
Original file line numberDiff line numberDiff line change
@@ -876,6 +876,129 @@ Called immediately after a promise continuation executes. This may be after a
876876
Called when the promise receives a resolution or rejection value. This may
877877
occur synchronously in the case of `Promise.resolve()` or `Promise.reject()`.
878878

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

‎lib/internal/bootstrap/pre_execution.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ function prepareMainThreadExecution(expandArgv1 = false,
5454
setupCoverageHooks(process.env.NODE_V8_COVERAGE);
5555
}
5656

57-
5857
setupDebugEnv();
5958

6059
// Print stack trace on `SIGINT` if option `--trace-sigint` presents.
@@ -85,6 +84,8 @@ function prepareMainThreadExecution(expandArgv1 = false,
8584
initializeDeprecations();
8685
initializeWASI();
8786

87+
require('internal/v8/startup_snapshot').runDeserializeCallbacks();
88+
8889
if (!initialzeModules) {
8990
return;
9091
}

‎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;
@@ -133,6 +136,12 @@ function refreshStderrOnSigWinch() {
133136
stderr._refreshSize();
134137
}
135138

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

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

@@ -163,12 +174,14 @@ function getStderr() {
163174
if (stderr.isTTY) {
164175
process.on('SIGWINCH', refreshStderrOnSigWinch);
165176
}
166-
internalBinding('mksnapshot').cleanups.push(function cleanupStderr() {
177+
addCleanup(function cleanupStderr() {
167178
stderr._destroy = stderrDestroy;
168179
stderr.destroy();
169180
process.removeListener('SIGWINCH', refreshStderrOnSigWinch);
170181
stderr = undefined;
171182
});
183+
// No need to add deserialize callback because stderr = undefined above
184+
// causes the stream to be lazily initialized again later.
172185
return stderr;
173186
}
174187

@@ -255,10 +268,12 @@ function getStdin() {
255268
}
256269
}
257270

258-
internalBinding('mksnapshot').cleanups.push(function cleanupStdin() {
271+
addCleanup(function cleanupStdin() {
259272
stdin.destroy();
260273
stdin = undefined;
261274
});
275+
// No need to add deserialize callback because stdin = undefined above
276+
// causes the stream to be lazily initialized again later.
262277
return stdin;
263278
}
264279

‎lib/internal/errors.js

+4
Original file line numberDiff line numberDiff line change
@@ -967,6 +967,8 @@ E('ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE',
967967
'The `domain` module is in use, which is mutually exclusive with calling ' +
968968
'process.setUncaughtExceptionCaptureCallback()',
969969
Error);
970+
E('ERR_DUPLICATE_STARTUP_SNAPSHOT_MAIN_FUNCTION',
971+
'Deserialize main function is already configured.', Error);
970972
E('ERR_ENCODING_INVALID_ENCODED_DATA', function(encoding, ret) {
971973
this.errno = ret;
972974
return `The encoded data was not valid for encoding ${encoding}`;
@@ -1445,6 +1447,8 @@ E('ERR_NETWORK_IMPORT_BAD_RESPONSE',
14451447
"import '%s' received a bad response: %s", Error);
14461448
E('ERR_NETWORK_IMPORT_DISALLOWED',
14471449
"import of '%s' by %s is not supported: %s", Error);
1450+
E('ERR_NOT_BUILDING_SNAPSHOT',
1451+
'Operation cannot be invoked when not building startup snapshot', Error);
14481452
E('ERR_NO_CRYPTO',
14491453
'Node.js is not compiled with OpenSSL crypto support', Error);
14501454
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)
Please sign in to comment.