Skip to content

Commit

Permalink
v8: add v8.startupSnapshot utils
Browse files Browse the repository at this point in the history
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 <legendecas@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
  • Loading branch information
joyeecheung authored and targos committed Jul 12, 2022
1 parent a1f581a commit 34e8331
Show file tree
Hide file tree
Showing 15 changed files with 413 additions and 29 deletions.
15 changes: 15 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -1179,6 +1179,13 @@ because the `node:domain` module has been loaded at an earlier point in time.
The stack trace is extended to include the point in time at which the
`node:domain` module had been loaded.

<a id="ERR_DUPLICATE_STARTUP_SNAPSHOT_MAIN_FUNCTION"></a>

### `ERR_DUPLICATE_STARTUP_SNAPSHOT_MAIN_FUNCTION`

[`v8.startupSnapshot.setDeserializeMainFunction()`][] could not be called
because it had already been called before.

<a id="ERR_ENCODING_INVALID_ENCODED_DATA"></a>

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

<a id="ERR_NOT_BUILDING_SNAPSHOT"></a>

### `ERR_NOT_BUILDING_SNAPSHOT`

An attempt was made to use operations that can only be used when building
V8 startup snapshot even though Node.js isn't building one.

<a id="ERR_NO_CRYPTO"></a>

### `ERR_NO_CRYPTO`
Expand Down Expand Up @@ -3501,6 +3515,7 @@ The native call from `process.cpuUsage` could not be processed.
[`url.parse()`]: url.md#urlparseurlstring-parsequerystring-slashesdenotehost
[`util.getSystemErrorName(error.errno)`]: util.md#utilgetsystemerrornameerr
[`util.parseArgs()`]: util.md#utilparseargsconfig
[`v8.startupSnapshot.setDeserializeMainFunction()`]: v8.md#v8startupsnapshotsetdeserializemainfunctioncallback-data
[`zlib`]: zlib.md
[crypto digest algorithm]: crypto.md#cryptogethashes
[debugger]: debugger.md
Expand Down
131 changes: 131 additions & 0 deletions doc/api/v8.md
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,137 @@ Called immediately after a promise continuation executes. This may be after a
Called when the promise receives a resolution or rejection value. This may
occur synchronously in the case of `Promise.resolve()` or `Promise.reject()`.

## Startup Snapshot API

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental
The `v8.startupSnapshot` interface can be used to add serialization and
deserialization hooks for custom startup snapshots. Currently the startup
snapshots can only be built into the Node.js binary from source.

```console
$ cd /path/to/node
$ ./configure --node-snapshot-main=entry.js
$ make node
# This binary contains the result of the execution of entry.js
$ out/Release/node
```

In the example above, `entry.js` can use methods from the `v8.startupSnapshot`
interface to specify how to save information for custom objects in the snapshot
during serialization and how the information can be used to synchronize these
objects during deserialization of the snapshot. For example, if the `entry.js`
contains the following script:

```cjs
'use strict';

const fs = require('fs');
const zlib = require('zlib');
const path = require('path');
const assert = require('assert');

const {
isBuildingSnapshot,
addSerializeCallback,
addDeserializeCallback,
setDeserializeMainFunction
} = require('v8').startupSnapshot;

const filePath = path.resolve(__dirname, '../x1024.txt');
const storage = {};

assert(isBuildingSnapshot());

addSerializeCallback(({ filePath }) => {
storage[filePath] = zlib.gzipSync(fs.readFileSync(filePath));
}, { filePath });

addDeserializeCallback(({ filePath }) => {
storage[filePath] = zlib.gunzipSync(storage[filePath]);
}, { filePath });

setDeserializeMainFunction(({ filePath }) => {
console.log(storage[filePath].toString());
}, { filePath });
```

The resulted binary will simply print the data deserialized from the snapshot
during start up:

```console
$ out/Release/node
# Prints content of ./test/fixtures/x1024.txt
```

Currently the API is only available to a Node.js instance launched from the
default snapshot, that is, the application deserialized from a user-land
snapshot cannot use these APIs again.

### `v8.startupSnapshot.addSerializeCallback(callback[, data])`

<!-- YAML
added: REPLACEME
-->

* `callback` {Function} Callback to be invoked before serialization.
* `data` {any} Optional data that will be passed to the `callback` when it
gets called.

Add a callback that will be called when the Node.js instance is about to
get serialized into a snapshot and exit. This can be used to release
resources that should not or cannot be serialized or to convert user data
into a form more suitable for serialization.

### `v8.startupSnapshot.addDeserializeCallback(callback[, data])`

<!-- YAML
added: REPLACEME
-->

* `callback` {Function} Callback to be invoked after the snapshot is
deserialized.
* `data` {any} Optional data that will be passed to the `callback` when it
gets called.

Add a callback that will be called when the Node.js instance is deserialized
from a snapshot. The `callback` and the `data` (if provided) will be
serialized into the snapshot, they can be used to re-initialize the state
of the application or to re-acquire resources that the application needs
when the application is restarted from the snapshot.

### `v8.startupSnapshot.setDeserializeMainFunction(callback[, data])`

<!-- YAML
added: REPLACEME
-->

* `callback` {Function} Callback to be invoked as the entry point after the
snapshot is deserialized.
* `data` {any} Optional data that will be passed to the `callback` when it
gets called.

This sets the entry point of the Node.js application when it is deserialized
from a snapshot. This can be called only once in the snapshot building
script. If called, the deserialized application no longer needs an additional
entry point script to start up and will simply invoke the callback along with
the deserialized data (if provided), otherwise an entry point script still
needs to be provided to the deserialized application.

### `v8.startupSnapshot.isBuildingSnapshot()`

<!-- YAML
added: REPLACEME
-->

* Returns: {boolean}

Returns true if the Node.js instance is run to build a snapshot.

[HTML structured clone algorithm]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
[Hook Callbacks]: #hook-callbacks
[V8]: https://developers.google.com/v8/
Expand Down
3 changes: 2 additions & 1 deletion lib/internal/bootstrap/pre_execution.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ function prepareMainThreadExecution(expandArgv1 = false,
setupCoverageHooks(process.env.NODE_V8_COVERAGE);
}


setupDebugEnv();

// Print stack trace on `SIGINT` if option `--trace-sigint` presents.
Expand Down Expand Up @@ -84,6 +83,8 @@ function prepareMainThreadExecution(expandArgv1 = false,
initializeDeprecations();
initializeWASI();

require('internal/v8/startup_snapshot').runDeserializeCallbacks();

if (!initialzeModules) {
return;
}
Expand Down
23 changes: 19 additions & 4 deletions lib/internal/bootstrap/switches/is_main_thread.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

const { ObjectDefineProperty } = primordials;
const rawMethods = internalBinding('process_methods');

const {
addSerializeCallback,
isBuildingSnapshot
} = require('v8').startupSnapshot;
// TODO(joyeecheung): deprecate and remove these underscore methods
process._debugProcess = rawMethods._debugProcess;
process._debugEnd = rawMethods._debugEnd;
Expand Down Expand Up @@ -134,6 +137,12 @@ function refreshStderrOnSigWinch() {
stderr._refreshSize();
}

function addCleanup(fn) {
if (isBuildingSnapshot()) {
addSerializeCallback(fn);
}
}

function getStdout() {
if (stdout) return stdout;
stdout = createWritableStdioStream(1);
Expand All @@ -145,12 +154,14 @@ function getStdout() {
process.on('SIGWINCH', refreshStdoutOnSigWinch);
}

internalBinding('mksnapshot').cleanups.push(function cleanupStdout() {
addCleanup(function cleanupStdout() {
stdout._destroy = stdoutDestroy;
stdout.destroy();
process.removeListener('SIGWINCH', refreshStdoutOnSigWinch);
stdout = undefined;
});
// No need to add deserialize callback because stdout = undefined above
// causes the stream to be lazily initialized again later.
return stdout;
}

Expand All @@ -164,12 +175,14 @@ function getStderr() {
if (stderr.isTTY) {
process.on('SIGWINCH', refreshStderrOnSigWinch);
}
internalBinding('mksnapshot').cleanups.push(function cleanupStderr() {
addCleanup(function cleanupStderr() {
stderr._destroy = stderrDestroy;
stderr.destroy();
process.removeListener('SIGWINCH', refreshStderrOnSigWinch);
stderr = undefined;
});
// No need to add deserialize callback because stderr = undefined above
// causes the stream to be lazily initialized again later.
return stderr;
}

Expand Down Expand Up @@ -256,10 +269,12 @@ function getStdin() {
}
}

internalBinding('mksnapshot').cleanups.push(function cleanupStdin() {
addCleanup(function cleanupStdin() {
stdin.destroy();
stdin = undefined;
});
// No need to add deserialize callback because stdin = undefined above
// causes the stream to be lazily initialized again later.
return stdin;
}

Expand Down
4 changes: 4 additions & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -978,6 +978,8 @@ E('ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE',
'The `domain` module is in use, which is mutually exclusive with calling ' +
'process.setUncaughtExceptionCaptureCallback()',
Error);
E('ERR_DUPLICATE_STARTUP_SNAPSHOT_MAIN_FUNCTION',
'Deserialize main function is already configured.', Error);
E('ERR_ENCODING_INVALID_ENCODED_DATA', function(encoding, ret) {
this.errno = ret;
return `The encoded data was not valid for encoding ${encoding}`;
Expand Down Expand Up @@ -1456,6 +1458,8 @@ E('ERR_NETWORK_IMPORT_BAD_RESPONSE',
"import '%s' received a bad response: %s", Error);
E('ERR_NETWORK_IMPORT_DISALLOWED',
"import of '%s' by %s is not supported: %s", Error);
E('ERR_NOT_BUILDING_SNAPSHOT',
'Operation cannot be invoked when not building startup snapshot', Error);
E('ERR_NO_CRYPTO',
'Node.js is not compiled with OpenSSL crypto support', Error);
E('ERR_NO_ICU',
Expand Down
17 changes: 7 additions & 10 deletions lib/internal/main/mksnapshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const {
const binding = internalBinding('mksnapshot');
const { NativeModule } = require('internal/bootstrap/loaders');
const {
compileSnapshotMain,
compileSerializeMain,
} = binding;

const {
Expand Down Expand Up @@ -83,7 +83,7 @@ const supportedModules = new SafeSet(new SafeArrayIterator([
'v8',
// 'vm',
// 'worker_threads',
// 'zlib',
'zlib',
]));

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

prepareMainThreadExecution(true, false);
process.once('beforeExit', function runCleanups() {
for (const cleanup of binding.cleanups) {
cleanup();
}
});

const file = process.argv[1];
const path = require('path');
const filename = path.resolve(file);
const dirname = path.dirname(filename);
const source = readFileSync(file, 'utf-8');
const snapshotMainFunction = compileSnapshotMain(filename, source);
const serializeMainFunction = compileSerializeMain(filename, source);

require('internal/v8/startup_snapshot').initializeCallbacks();

if (getOptionValue('--inspect-brk')) {
internalBinding('inspector').callAndPauseOnStart(
snapshotMainFunction, undefined,
serializeMainFunction, undefined,
requireForUserSnapshot, filename, dirname);
} else {
snapshotMainFunction(requireForUserSnapshot, filename, dirname);
serializeMainFunction(requireForUserSnapshot, filename, dirname);
}
}

Expand Down

0 comments on commit 34e8331

Please sign in to comment.