Skip to content

Commit 165f670

Browse files
authored
feat: add experimental snapshot support MONGOSH-1333 (#40)
1 parent 20986b7 commit 165f670

File tree

9 files changed

+271
-70
lines changed

9 files changed

+271
-70
lines changed

.github/workflows/nodejs.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ jobs:
3636
fail-fast: false
3737
matrix:
3838
node-version: [14.x, 16.x, 18.x, 19.x]
39-
shard: [1, 2, 3, 4]
39+
shard: [1, 2, 3, 4, 5]
4040
runs-on: windows-latest
4141
steps:
4242
- uses: actions/checkout@v2
@@ -61,4 +61,4 @@ jobs:
6161
- name: Sharded tests
6262
run: npm test -- -g "shard ${{ matrix.shard }}"
6363
- name: Unsharded tests
64-
run: npm test -- -g "shard [1-4]" -i
64+
run: npm test -- -g "shard [1-5]" -i

bin/boxednode.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ const argv = require('yargs')
3333
.option('use-code-cache', {
3434
alias: 'H', type: 'boolean', desc: 'Use Node.js code cache support to speed up startup'
3535
})
36+
.option('use-node-snapshot', {
37+
alias: 'S', type: 'boolean', desc: 'Use experimental Node.js snapshot support'
38+
})
3639
.example('$0 -s myProject.js -t myProject.exe -n ^14.0.0',
3740
'Create myProject.exe from myProject.js using Node.js v14')
3841
.help()
@@ -50,7 +53,8 @@ const argv = require('yargs')
5053
makeArgs: (argv.M || '').split(',').filter(Boolean),
5154
namespace: argv.N,
5255
useLegacyDefaultUvLoop: argv.useLegacyDefaultUvLoop,
53-
useCodeCache: argv.H
56+
useCodeCache: argv.H,
57+
useNodeSnapshot: argv.S
5458
});
5559
} catch (err) {
5660
console.error(err);

resources/entry-point-trampoline.js

+35-13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22
const Module = require('module');
33
const vm = require('vm');
4+
const v8 = require('v8');
45
const path = require('path');
56
const assert = require('assert');
67
const {
@@ -10,6 +11,8 @@ const {
1011
const hydatedRequireMappings =
1112
requireMappings.map(([re, reFlags, linked]) => [new RegExp(re, reFlags), linked]);
1213

14+
if (process.argv[2] === '--') process.argv.splice(2, 1);
15+
1316
if (enableBindingsPatch) {
1417
// Hack around various deficiencies in https://github.com/TooTallNate/node-bindings
1518
const fs = require('fs');
@@ -50,11 +53,24 @@ if (enableBindingsPatch) {
5053
});
5154
}
5255

56+
const outerRequire = require;
5357
module.exports = (src, codeCacheMode, codeCache) => {
5458
const __filename = process.execPath;
5559
const __dirname = path.dirname(process.execPath);
56-
const innerRequire = Module.createRequire(__filename);
60+
let innerRequire;
5761
const exports = {};
62+
const usesSnapshot = !!v8?.startupSnapshot?.isBuildingSnapshot();
63+
64+
if (usesSnapshot) {
65+
innerRequire = outerRequire; // Node.js snapshots currently do not support userland require()
66+
v8.startupSnapshot.addDeserializeCallback(() => {
67+
if (process.argv[1] === '--boxednode-snapshot-argv-fixup') {
68+
process.argv.splice(1, 1, process.execPath);
69+
}
70+
});
71+
} else {
72+
innerRequire = Module.createRequire(__filename);
73+
}
5874

5975
function require(module) {
6076
for (const [ re, linked ] of hydatedRequireMappings) {
@@ -69,7 +85,7 @@ module.exports = (src, codeCacheMode, codeCache) => {
6985
Object.setPrototypeOf(require, Object.getPrototypeOf(innerRequire));
7086

7187
process.argv.unshift(__filename);
72-
process.boxednode = {};
88+
process.boxednode = { usesSnapshot };
7389

7490
const module = {
7591
exports,
@@ -79,17 +95,23 @@ module.exports = (src, codeCacheMode, codeCache) => {
7995
path: __dirname,
8096
require
8197
};
82-
const mainFunction = vm.compileFunction(src, [
83-
'__filename', '__dirname', 'require', 'exports', 'module'
84-
], {
85-
filename: __filename,
86-
cachedData: codeCache.length > 0 ? codeCache : undefined,
87-
produceCachedData: codeCacheMode === 'generate'
88-
});
89-
if (codeCacheMode === 'generate') {
90-
assert.strictEqual(mainFunction.cachedDataProduced, true);
91-
process.stdout.write(mainFunction.cachedData);
92-
return;
98+
99+
let mainFunction;
100+
if (usesSnapshot) {
101+
mainFunction = eval(`(function(__filename, __dirname, require, exports, module) {\n${src}\n})`);
102+
} else {
103+
mainFunction = vm.compileFunction(src, [
104+
'__filename', '__dirname', 'require', 'exports', 'module'
105+
], {
106+
filename: __filename,
107+
cachedData: codeCache.length > 0 ? codeCache : undefined,
108+
produceCachedData: codeCacheMode === 'generate'
109+
});
110+
if (codeCacheMode === 'generate') {
111+
assert.strictEqual(mainFunction.cachedDataProduced, true);
112+
require('fs').writeFileSync('intermediate.out', mainFunction.cachedData);
113+
return;
114+
}
93115
}
94116

95117
process.boxednode.hasCodeCache = codeCache.length > 0;

resources/main-template.cc

+118-25
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,21 @@ using namespace v8;
2525
#define USE_OWN_LEGACY_PROCESS_INITIALIZATION 1
2626
#endif
2727

28+
// 20.0.0 will have https://github.com/nodejs/node/pull/45888, possibly the PR
29+
// will be backported to older versions but for now this is the one where we
30+
// can be sure of its presence.
31+
#if NODE_VERSION_AT_LEAST(20, 0, 0)
32+
#define NODE_VERSION_SUPPORTS_EMBEDDER_SNAPSHOT 1
33+
#endif
34+
2835
// 18.1.0 is the current minimum version that has https://github.com/nodejs/node/pull/42809,
2936
// which introduced crashes when using workers, and later 18.9.0 is the current
3037
// minimum version to contain https://github.com/nodejs/node/pull/44252, which
3138
// introcued crashes when using the vm module.
3239
// We should be able to remove this restriction again once Node.js stops relying
3340
// on global state for determining whether snapshots are enabled or not
3441
// (after https://github.com/nodejs/node/pull/45888, hopefully).
35-
#if NODE_VERSION_AT_LEAST(18, 1, 0)
42+
#if NODE_VERSION_AT_LEAST(18, 1, 0) && !defined(NODE_VERSION_SUPPORTS_EMBEDDER_SNAPSHOT)
3643
#define PASS_NO_NODE_SNAPSHOT_OPTION 1
3744
#endif
3845

@@ -45,6 +52,7 @@ void TearDownOncePerProcess();
4552
namespace boxednode {
4653
Local<String> GetBoxednodeMainScriptSource(Isolate* isolate);
4754
Local<Uint8Array> GetBoxednodeCodeCacheBuffer(Isolate* isolate);
55+
std::vector<char> GetBoxednodeSnapshotBlobVector();
4856
}
4957

5058
extern "C" {
@@ -53,11 +61,86 @@ typedef void (*register_boxednode_linked_module)(const void**, const void**);
5361
REPLACE_DECLARE_LINKED_MODULES
5462
}
5563

64+
#if __cplusplus >= 201703L
65+
[[maybe_unused]]
66+
#endif
5667
static register_boxednode_linked_module boxednode_linked_modules[] = {
5768
REPLACE_DEFINE_LINKED_MODULES
5869
nullptr // Make sure the array is not empty, for MSVC
5970
};
6071

72+
static MaybeLocal<Value> LoadBoxednodeEnvironment(Local<Context> context) {
73+
Environment* env = GetCurrentEnvironment(context);
74+
return LoadEnvironment(env,
75+
#ifdef BOXEDNODE_CONSUME_SNAPSHOT
76+
node::StartExecutionCallback{}
77+
#else
78+
[&](const StartExecutionCallbackInfo& info) -> MaybeLocal<Value> {
79+
Isolate* isolate = context->GetIsolate();
80+
HandleScope handle_scope(isolate);
81+
Local<Value> entrypoint_name = String::NewFromUtf8(
82+
isolate,
83+
REPLACE_WITH_ENTRY_POINT)
84+
.ToLocalChecked();
85+
Local<Value> entrypoint_ret;
86+
if (!info.native_require->Call(
87+
context,
88+
Null(isolate),
89+
1,
90+
&entrypoint_name
91+
).ToLocal(&entrypoint_ret)) {
92+
return {}; // JS exception.
93+
}
94+
assert(entrypoint_ret->IsFunction());
95+
Local<Value> trampoline_args[] = {
96+
boxednode::GetBoxednodeMainScriptSource(isolate),
97+
String::NewFromUtf8Literal(isolate, BOXEDNODE_CODE_CACHE_MODE),
98+
boxednode::GetBoxednodeCodeCacheBuffer(isolate),
99+
};
100+
if (entrypoint_ret.As<Function>()->Call(
101+
context,
102+
Null(isolate),
103+
sizeof(trampoline_args) / sizeof(trampoline_args[0]),
104+
trampoline_args).IsEmpty()) {
105+
return {}; // JS exception.
106+
}
107+
return Null(isolate);
108+
}
109+
#endif
110+
);
111+
}
112+
113+
#ifdef BOXEDNODE_GENERATE_SNAPSHOT
114+
static int RunNodeInstance(MultiIsolatePlatform* platform,
115+
const std::vector<std::string>& args,
116+
const std::vector<std::string>& exec_args) {
117+
int exit_code = 0;
118+
std::vector<std::string> errors;
119+
std::unique_ptr<CommonEnvironmentSetup> setup =
120+
CommonEnvironmentSetup::CreateForSnapshotting(platform, &errors, args, exec_args);
121+
122+
Isolate* isolate = setup->isolate();
123+
124+
{
125+
Locker locker(isolate);
126+
Isolate::Scope isolate_scope(isolate);
127+
128+
HandleScope handle_scope(isolate);
129+
Local<Context> context = setup->context();
130+
Context::Scope context_scope(context);
131+
if (LoadBoxednodeEnvironment(context).IsEmpty())
132+
return 1;
133+
exit_code = SpinEventLoop(setup->env()).FromMaybe(1);
134+
}
135+
136+
{
137+
FILE* fp = fopen("intermediate.out", "wb");
138+
setup->CreateSnapshot()->ToFile(fp);
139+
fclose(fp);
140+
}
141+
return exit_code;
142+
}
143+
#else // BOXEDNODE_GENERATE_SNAPSHOT
61144
static int RunNodeInstance(MultiIsolatePlatform* platform,
62145
const std::vector<std::string>& args,
63146
const std::vector<std::string>& exec_args) {
@@ -81,7 +164,13 @@ static int RunNodeInstance(MultiIsolatePlatform* platform,
81164
std::shared_ptr<ArrayBufferAllocator> allocator =
82165
ArrayBufferAllocator::Create();
83166

84-
#if NODE_VERSION_AT_LEAST(14, 0, 0)
167+
#ifdef BOXEDNODE_CONSUME_SNAPSHOT
168+
std::vector<char> snapshot_blob_vec = boxednode::GetBoxednodeSnapshotBlobVector();
169+
assert(EmbedderSnapshotData::CanUseCustomSnapshotPerIsolate());
170+
node::EmbedderSnapshotData::Pointer snapshot_blob =
171+
EmbedderSnapshotData::FromBlob(snapshot_blob_vec);
172+
Isolate* isolate = NewIsolate(allocator, loop, platform, snapshot_blob.get());
173+
#elif NODE_VERSION_AT_LEAST(14, 0, 0)
85174
Isolate* isolate = NewIsolate(allocator, loop, platform);
86175
#else
87176
Isolate* isolate = NewIsolate(allocator.get(), loop, platform);
@@ -98,12 +187,19 @@ static int RunNodeInstance(MultiIsolatePlatform* platform,
98187
// Create a node::IsolateData instance that will later be released using
99188
// node::FreeIsolateData().
100189
std::unique_ptr<IsolateData, decltype(&node::FreeIsolateData)> isolate_data(
101-
node::CreateIsolateData(isolate, loop, platform, allocator.get()),
190+
node::CreateIsolateData(isolate, loop, platform, allocator.get()
191+
#ifdef BOXEDNODE_CONSUME_SNAPSHOT
192+
, snapshot_blob.get()
193+
#endif
194+
),
102195
node::FreeIsolateData);
103196

104-
// Set up a new v8::Context.
105197
HandleScope handle_scope(isolate);
106-
Local<Context> context = node::NewContext(isolate);
198+
Local<Context> context;
199+
#ifndef BOXEDNODE_CONSUME_SNAPSHOT
200+
// Set up a new v8::Context.
201+
context = node::NewContext(isolate);
202+
107203
if (context.IsEmpty()) {
108204
fprintf(stderr, "%s: Failed to initialize V8 Context\n", args[0].c_str());
109205
return 1;
@@ -112,12 +208,20 @@ static int RunNodeInstance(MultiIsolatePlatform* platform,
112208
// The v8::Context needs to be entered when node::CreateEnvironment() and
113209
// node::LoadEnvironment() are being called.
114210
Context::Scope context_scope(context);
211+
#endif
115212

116213
// Create a node::Environment instance that will later be released using
117214
// node::FreeEnvironment().
118215
std::unique_ptr<Environment, decltype(&node::FreeEnvironment)> env(
119216
node::CreateEnvironment(isolate_data.get(), context, args, exec_args),
120217
node::FreeEnvironment);
218+
#ifdef BOXEDNODE_CONSUME_SNAPSHOT
219+
assert(context.IsEmpty());
220+
context = GetMainContext(env.get());
221+
assert(!context.IsEmpty());
222+
Context::Scope context_scope(context);
223+
#endif
224+
assert(isolate->InContext());
121225

122226
const void* node_mod;
123227
const void* napi_mod;
@@ -144,27 +248,9 @@ static int RunNodeInstance(MultiIsolatePlatform* platform,
144248
// `module.createRequire()` is being used to create one that is able to
145249
// load files from the disk, and uses the standard CommonJS file loader
146250
// instead of the internal-only `require` function.
147-
Local<Value> loadenv_ret;
148-
if (!node::LoadEnvironment(
149-
env.get(),
150-
"const path = require('path');\n"
151-
"if (process.argv[2] === '--') process.argv.splice(2, 1);\n"
152-
"return require(" REPLACE_WITH_ENTRY_POINT ")").ToLocal(&loadenv_ret)) {
251+
if (LoadBoxednodeEnvironment(context).IsEmpty()) {
153252
return 1; // There has been a JS exception.
154253
}
155-
assert(loadenv_ret->IsFunction());
156-
Local<Value> trampoline_args[] = {
157-
boxednode::GetBoxednodeMainScriptSource(isolate),
158-
String::NewFromUtf8Literal(isolate, BOXEDNODE_CODE_CACHE_MODE),
159-
boxednode::GetBoxednodeCodeCacheBuffer(isolate),
160-
};
161-
if (loadenv_ret.As<Function>()->Call(
162-
context,
163-
Null(isolate),
164-
sizeof(trampoline_args) / sizeof(trampoline_args[0]),
165-
trampoline_args).IsEmpty()) {
166-
return 1; // JS exception.
167-
}
168254

169255
{
170256
// SealHandleScope protects against handle leaks from callbacks.
@@ -221,13 +307,14 @@ static int RunNodeInstance(MultiIsolatePlatform* platform,
221307

222308
return exit_code;
223309
}
310+
#endif // BOXEDNODE_GENERATE_SNAPSHOT
224311

225312
static int BoxednodeMain(std::vector<std::string> args) {
226313
std::vector<std::string> exec_args;
227314
std::vector<std::string> errors;
228315

229316
if (args.size() > 0) {
230-
args.insert(args.begin() + 1, "--");
317+
args.insert(args.begin() + 1, "--");
231318
#ifdef PASS_NO_NODE_SNAPSHOT_OPTION
232319
args.insert(args.begin() + 1, "--no-node-snapshot");
233320
#endif
@@ -260,6 +347,12 @@ static int BoxednodeMain(std::vector<std::string> args) {
260347
exec_args = result->exec_args();
261348
#endif
262349

350+
#ifdef BOXEDNODE_CONSUME_SNAPSHOT
351+
if (args.size() > 0) {
352+
args.insert(args.begin() + 1, "--boxednode-snapshot-argv-fixup");
353+
}
354+
#endif
355+
263356
// Create a v8::Platform instance. `MultiIsolatePlatform::Create()` is a way
264357
// to create a v8::Platform instance that Node.js can use when creating
265358
// Worker threads. When no `MultiIsolatePlatform` instance is present,

0 commit comments

Comments
 (0)