diff --git a/doc/api/errors.md b/doc/api/errors.md
index 14b6996c7ad235..ed07dbb095e3a5 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -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.
+
+
+### `ERR_DUPLICATE_STARTUP_SNAPSHOT_MAIN_FUNCTION`
+
+[`v8.startupSnapshot.setDeserializeMainFunction()`][] could not be called
+because it had already been called before.
+
### `ERR_ENCODING_INVALID_ENCODED_DATA`
@@ -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.
+
+
+### `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.
+
### `ERR_NO_CRYPTO`
@@ -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
diff --git a/doc/api/v8.md b/doc/api/v8.md
index 15f085e04c49f2..0e58bad9dbe5c4 100644
--- a/doc/api/v8.md
+++ b/doc/api/v8.md
@@ -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
+
+
+
+> 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])`
+
+
+
+* `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])`
+
+
+
+* `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])`
+
+
+
+* `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()`
+
+
+
+* 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/
diff --git a/lib/internal/bootstrap/pre_execution.js b/lib/internal/bootstrap/pre_execution.js
index e2f3037ffd486c..b304b69c0486d3 100644
--- a/lib/internal/bootstrap/pre_execution.js
+++ b/lib/internal/bootstrap/pre_execution.js
@@ -54,7 +54,6 @@ function prepareMainThreadExecution(expandArgv1 = false,
setupCoverageHooks(process.env.NODE_V8_COVERAGE);
}
-
setupDebugEnv();
// Print stack trace on `SIGINT` if option `--trace-sigint` presents.
@@ -85,6 +84,8 @@ function prepareMainThreadExecution(expandArgv1 = false,
initializeDeprecations();
initializeWASI();
+ require('internal/v8/startup_snapshot').runDeserializeCallbacks();
+
if (!initialzeModules) {
return;
}
diff --git a/lib/internal/bootstrap/switches/is_main_thread.js b/lib/internal/bootstrap/switches/is_main_thread.js
index b7bd79e09c4acf..5022c99c4fc43a 100644
--- a/lib/internal/bootstrap/switches/is_main_thread.js
+++ b/lib/internal/bootstrap/switches/is_main_thread.js
@@ -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;
@@ -133,6 +136,12 @@ function refreshStderrOnSigWinch() {
stderr._refreshSize();
}
+function addCleanup(fn) {
+ if (isBuildingSnapshot()) {
+ addSerializeCallback(fn);
+ }
+}
+
function getStdout() {
if (stdout) return stdout;
stdout = createWritableStdioStream(1);
@@ -144,12 +153,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;
}
@@ -163,12 +174,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;
}
@@ -255,10 +268,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;
}
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index 9289d50c008e13..10356ca762e719 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -967,6 +967,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}`;
@@ -1445,6 +1447,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',
diff --git a/lib/internal/main/mksnapshot.js b/lib/internal/main/mksnapshot.js
index 3e1515a2d2e05e..616a436e0a9483 100644
--- a/lib/internal/main/mksnapshot.js
+++ b/lib/internal/main/mksnapshot.js
@@ -9,7 +9,7 @@ const {
const binding = internalBinding('mksnapshot');
const { NativeModule } = require('internal/bootstrap/loaders');
const {
- compileSnapshotMain,
+ compileSerializeMain,
} = binding;
const {
@@ -83,7 +83,7 @@ const supportedModules = new SafeSet(new SafeArrayIterator([
'v8',
// 'vm',
// 'worker_threads',
- // 'zlib',
+ 'zlib',
]));
const warnedModules = new SafeSet();
@@ -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);
}
}
diff --git a/lib/internal/v8/startup_snapshot.js b/lib/internal/v8/startup_snapshot.js
new file mode 100644
index 00000000000000..6a6a6c47e85708
--- /dev/null
+++ b/lib/internal/v8/startup_snapshot.js
@@ -0,0 +1,111 @@
+'use strict';
+
+const {
+ validateFunction,
+} = require('internal/validators');
+const {
+ ERR_NOT_BUILDING_SNAPSHOT,
+ ERR_DUPLICATE_STARTUP_SNAPSHOT_MAIN_FUNCTION
+} = require('internal/errors');
+
+const {
+ setSerializeCallback,
+ setDeserializeCallback,
+ setDeserializeMainFunction: _setDeserializeMainFunction,
+ markBootstrapComplete
+} = internalBinding('mksnapshot');
+
+function isBuildingSnapshot() {
+ // For now this is the only way to build a snapshot.
+ return require('internal/options').getOptionValue('--build-snapshot');
+}
+
+function throwIfNotBuildingSnapshot() {
+ if (!isBuildingSnapshot()) {
+ throw new ERR_NOT_BUILDING_SNAPSHOT();
+ }
+}
+
+const deserializeCallbacks = [];
+let deserializeCallbackIsSet = false;
+function runDeserializeCallbacks() {
+ while (deserializeCallbacks.length > 0) {
+ const { 0: callback, 1: data } = deserializeCallbacks.shift();
+ callback(data);
+ }
+}
+
+function addDeserializeCallback(callback, data) {
+ throwIfNotBuildingSnapshot();
+ validateFunction(callback, 'callback');
+ if (!deserializeCallbackIsSet) {
+ // TODO(joyeecheung): when the main function handling is done in JS,
+ // the deserialize callbacks can always be invoked. For now only
+ // store it in C++ when it's actually used to avoid unnecessary
+ // C++ -> JS costs.
+ setDeserializeCallback(runDeserializeCallbacks);
+ deserializeCallbackIsSet = true;
+ }
+ deserializeCallbacks.push([callback, data]);
+}
+
+const serializeCallbacks = [];
+function runSerializeCallbacks() {
+ while (serializeCallbacks.length > 0) {
+ const { 0: callback, 1: data } = serializeCallbacks.shift();
+ callback(data);
+ }
+ // Remove the hooks from the snapshot.
+ require('v8').startupSnapshot = undefined;
+}
+
+function addSerializeCallback(callback, data) {
+ throwIfNotBuildingSnapshot();
+ validateFunction(callback, 'callback');
+ serializeCallbacks.push([callback, data]);
+}
+
+function initializeCallbacks() {
+ // Only run the serialize callbacks in snapshot building mode, otherwise
+ // they throw.
+ if (isBuildingSnapshot()) {
+ setSerializeCallback(runSerializeCallbacks);
+ }
+}
+
+let deserializeMainIsSet = false;
+function setDeserializeMainFunction(callback, data) {
+ throwIfNotBuildingSnapshot();
+ // TODO(joyeecheung): In lib/internal/bootstrap/node.js, create a default
+ // main function to run the lib/internal/main scripts and make sure that
+ // the main function set in the snapshot building process takes precedence.
+ validateFunction(callback, 'callback');
+ if (deserializeMainIsSet) {
+ throw new ERR_DUPLICATE_STARTUP_SNAPSHOT_MAIN_FUNCTION();
+ }
+ deserializeMainIsSet = true;
+
+ _setDeserializeMainFunction(function deserializeMain() {
+ const {
+ prepareMainThreadExecution
+ } = require('internal/bootstrap/pre_execution');
+
+ // This should be in sync with run_main_module.js until we make that
+ // a built-in main function.
+ prepareMainThreadExecution(true);
+ markBootstrapComplete();
+ callback(data);
+ });
+}
+
+module.exports = {
+ initializeCallbacks,
+ runDeserializeCallbacks,
+ // Exposed to require('v8').startupSnapshot
+ namespace: {
+ addDeserializeCallback,
+ addSerializeCallback,
+ setDeserializeMainFunction,
+ isBuildingSnapshot
+ }
+};
diff --git a/lib/v8.js b/lib/v8.js
index 40db7808fcd611..1a8b4bce2fccd6 100644
--- a/lib/v8.js
+++ b/lib/v8.js
@@ -40,6 +40,9 @@ const {
Serializer,
Deserializer
} = internalBinding('serdes');
+const {
+ namespace: startupSnapshot
+} = require('internal/v8/startup_snapshot');
let profiler = {};
if (internalBinding('config').hasInspector) {
@@ -372,4 +375,5 @@ module.exports = {
serialize,
writeHeapSnapshot,
promiseHooks,
+ startupSnapshot
};
diff --git a/src/api/embed_helpers.cc b/src/api/embed_helpers.cc
index 8e2fc67695b875..bd0459f20b1b3e 100644
--- a/src/api/embed_helpers.cc
+++ b/src/api/embed_helpers.cc
@@ -3,6 +3,7 @@
#include "debug_utils-inl.h"
using v8::Context;
+using v8::Function;
using v8::Global;
using v8::HandleScope;
using v8::Isolate;
@@ -44,6 +45,13 @@ Maybe SpinEventLoop(Environment* env) {
if (EmitProcessBeforeExit(env).IsNothing())
break;
+ {
+ HandleScope handle_scope(isolate);
+ if (env->RunSnapshotSerializeCallback().IsEmpty()) {
+ break;
+ }
+ }
+
// Emit `beforeExit` if the loop became alive either after emitting
// event, or after running some callbacks.
more = uv_loop_alive(env->event_loop());
@@ -54,6 +62,11 @@ Maybe SpinEventLoop(Environment* env) {
if (env->is_stopping()) return Nothing();
env->set_trace_sync_io(false);
+ // Clear the serialize callback even though the JS-land queue should
+ // be empty this point so that the deserialized instance won't
+ // attempt to call into JS again.
+ env->set_snapshot_serialize_callback(Local());
+
env->PrintInfoForSnapshotIfDebug();
env->VerifyNoStrongBaseObjects();
return EmitProcessExit(env);
diff --git a/src/env.cc b/src/env.cc
index e21ee5efab1d19..65467587ffcdd8 100644
--- a/src/env.cc
+++ b/src/env.cc
@@ -34,6 +34,7 @@ using v8::Array;
using v8::Boolean;
using v8::Context;
using v8::EmbedderGraph;
+using v8::EscapableHandleScope;
using v8::Function;
using v8::FunctionTemplate;
using v8::HandleScope;
@@ -671,6 +672,26 @@ void Environment::PrintSyncTrace() const {
isolate(), stack_trace_limit(), StackTrace::kDetailed));
}
+MaybeLocal Environment::RunSnapshotSerializeCallback() const {
+ EscapableHandleScope handle_scope(isolate());
+ if (!snapshot_serialize_callback().IsEmpty()) {
+ Context::Scope context_scope(context());
+ return handle_scope.EscapeMaybe(snapshot_serialize_callback()->Call(
+ context(), v8::Undefined(isolate()), 0, nullptr));
+ }
+ return handle_scope.Escape(Undefined(isolate()));
+}
+
+MaybeLocal Environment::RunSnapshotDeserializeMain() const {
+ EscapableHandleScope handle_scope(isolate());
+ if (!snapshot_deserialize_main().IsEmpty()) {
+ Context::Scope context_scope(context());
+ return handle_scope.EscapeMaybe(snapshot_deserialize_main()->Call(
+ context(), v8::Undefined(isolate()), 0, nullptr));
+ }
+ return handle_scope.Escape(Undefined(isolate()));
+}
+
void Environment::RunCleanup() {
started_cleanup_ = true;
TRACE_EVENT0(TRACING_CATEGORY_NODE1(environment), "RunCleanup");
diff --git a/src/env.h b/src/env.h
index c188b263470819..730a710c911cc5 100644
--- a/src/env.h
+++ b/src/env.h
@@ -556,6 +556,9 @@ class NoArrayBufferZeroFillScope {
V(promise_hook_handler, v8::Function) \
V(promise_reject_callback, v8::Function) \
V(script_data_constructor_function, v8::Function) \
+ V(snapshot_serialize_callback, v8::Function) \
+ V(snapshot_deserialize_callback, v8::Function) \
+ V(snapshot_deserialize_main, v8::Function) \
V(source_map_cache_getter, v8::Function) \
V(tick_callback_function, v8::Function) \
V(timers_callback_function, v8::Function) \
@@ -1331,6 +1334,10 @@ class Environment : public MemoryRetainer {
void RunWeakRefCleanup();
+ v8::MaybeLocal RunSnapshotSerializeCallback() const;
+ v8::MaybeLocal RunSnapshotDeserializeCallback() const;
+ v8::MaybeLocal RunSnapshotDeserializeMain() const;
+
// Strings and private symbols are shared across shared contexts
// The getters simply proxy to the per-isolate primitive.
#define VP(PropertyName, StringValue) V(v8::Private, PropertyName)
diff --git a/src/node.cc b/src/node.cc
index 31c3e14846343f..78e93c74d3c3c4 100644
--- a/src/node.cc
+++ b/src/node.cc
@@ -485,6 +485,14 @@ MaybeLocal StartExecution(Environment* env, StartExecutionCallback cb) {
return scope.EscapeMaybe(cb(info));
}
+ // TODO(joyeecheung): move these conditions into JS land and let the
+ // deserialize main function take precedence. For workers, we need to
+ // move the pre-execution part into a different file that can be
+ // reused when dealing with user-defined main functions.
+ if (!env->snapshot_deserialize_main().IsEmpty()) {
+ return env->RunSnapshotDeserializeMain();
+ }
+
if (env->worker_context() != nullptr) {
return StartExecution(env, "internal/main/worker_thread");
}
diff --git a/src/node_snapshotable.cc b/src/node_snapshotable.cc
index 1fc374842ff5c6..2bc30b20bcdeac 100644
--- a/src/node_snapshotable.cc
+++ b/src/node_snapshotable.cc
@@ -457,7 +457,7 @@ void SerializeBindingData(Environment* env,
namespace mksnapshot {
-static void CompileSnapshotMain(const FunctionCallbackInfo& args) {
+void CompileSerializeMain(const FunctionCallbackInfo& args) {
CHECK(args[0]->IsString());
Local filename = args[0].As();
Local source = args[1].As();
@@ -485,23 +485,46 @@ static void CompileSnapshotMain(const FunctionCallbackInfo& args) {
}
}
-static void Initialize(Local