Skip to content

Commit 539ecc7

Browse files
committed
worker: start worker threads from the built-in snapshot
Every `new Worker()` runs the whole internal bootstrap (realm, node, web exposure, thread and process-state switches) in its fresh isolate, compiling ~80 builtins with the code cache; only the main thread deserializes its principal context from the built-in snapshot. That bootstrap is about half of a worker's cold start. The bootstrapped principal context in the snapshot is nearly thread-neutral: the worker-side switch scripts (is_not_main_thread, does_not_own_process_state) are written as overrides of the main-thread ones, and the per-thread values of the `worker` binding were the only thread-specific data baked into the context. Let a worker deserialize that same context and EnvSerializeInfo and apply the two worker-side switches on top: - worker binding: threadId, threadName, isMainThread, isInternalThread, ownsProcessState and resourceLimits become lazy properties of the per-isolate template, computed from the Environment on first read. - CreateEnvironment(): when a worker (its IsolateData has a Worker) passes an empty context, deserialize kNodeMainContextIndex and run internal/bootstrap/switches/is_not_main_thread and, unless the worker owns process state, does_not_own_process_state after InitializeMainContext(). - Worker::Run(): take that path when the embedded built-in snapshot is in use (not an embedder's or a --snapshot-blob one, which has run application code), browser globals are not disabled and --no-worker-snapshot was not given; otherwise bootstrap as before. - is_not_main_thread.js: also delete _debugPause and the profiler idle notifier helpers that is_main_thread.js installs. - pre_execution: run the snapshot's deserialize callbacks (Buffer pool, default resolver, cached cwd) on worker threads too, now that a worker can come from a snapshot. - --[no-]worker-snapshot per-isolate option, documented. Sequential new Worker() -> 'online' -> terminate goes from ~20.9 ms to ~10.3 ms per worker on x64 Linux; --no-worker-snapshot restores the old number. Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
1 parent 8af1821 commit 539ecc7

10 files changed

Lines changed: 162 additions & 61 deletions

File tree

doc/api/cli.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2270,6 +2270,17 @@ added: v6.0.0
22702270

22712271
Silence all process warnings (including deprecations).
22722272

2273+
### `--no-worker-snapshot`
2274+
2275+
<!-- YAML
2276+
added: REPLACEME
2277+
-->
2278+
2279+
> Stability: 1 - Experimental
2280+
2281+
Start worker threads by running the internal bootstrap from scratch instead of
2282+
deserializing the bootstrapped context from the built-in startup snapshot.
2283+
22732284
### `--node-memory-debug`
22742285

22752286
<!-- YAML
@@ -3963,6 +3974,7 @@ one is included in the list below.
39633974
* `--no-strip-types`
39643975
* `--no-warnings`
39653976
* `--no-webstorage`
3977+
* `--no-worker-snapshot`
39663978
* `--node-memory-debug`
39673979
* `--openssl-config`
39683980
* `--openssl-legacy-provider`

doc/node.1

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,6 +1139,10 @@ For more information, see the TypeScript type-stripping documentation.
11391139
.It Fl -no-warnings
11401140
Silence all process warnings (including deprecations).
11411141
.
1142+
.It Fl -no-worker-snapshot
1143+
Start worker threads by running the internal bootstrap from scratch instead of
1144+
deserializing the bootstrapped context from the built-in startup snapshot.
1145+
.
11421146
.It Fl -node-memory-debug
11431147
Enable extra debug checks for memory leaks in Node.js internals. This is
11441148
usually only useful for developers debugging Node.js itself.
@@ -2114,6 +2118,8 @@ one is included in the list below.
21142118
.It
21152119
\fB--no-webstorage\fR
21162120
.It
2121+
\fB--no-worker-snapshot\fR
2122+
.It
21172123
\fB--node-memory-debug\fR
21182124
.It
21192125
\fB--openssl-config\fR

lib/internal/bootstrap/switches/is_not_main_thread.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ const {
66

77
delete process._debugProcess;
88
delete process._debugEnd;
9+
// Also drop the other main-thread-only helpers is_main_thread.js installs, so
10+
// that this switch can be applied on top of a context bootstrapped for the
11+
// main thread (as when a worker starts from the built-in snapshot).
12+
delete process._debugPause;
13+
delete process._startProfilerIdleNotifier;
14+
delete process._stopProfilerIdleNotifier;
915

1016
function defineStream(name, getter) {
1117
ObjectDefineProperty(process, name, {

lib/internal/process/pre_execution.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,14 +163,12 @@ function prepareExecution(options) {
163163
// channel. This needs to be done before any user code gets executed
164164
// (including preload modules).
165165
initializeClusterIPC();
166-
167-
// TODO(joyeecheung): do this for worker threads as well.
168-
runDeserializeCallbacks();
169166
} else {
170167
assert(!internalBinding('worker').isMainThread);
171168
// The setup should be called in LOAD_SCRIPT message handler.
172169
assert(!initializeModules);
173170
}
171+
runDeserializeCallbacks();
174172

175173
const { initializeExtensionFormatMap } = require('internal/modules/esm/get_format');
176174
initializeExtensionFormatMap();

src/api/environment.cc

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,11 @@ Environment* CreateEnvironment(
425425

426426
const bool use_snapshot = context.IsEmpty();
427427
const EnvSerializeInfo* env_snapshot_info = nullptr;
428+
// A worker thread (its IsolateData knows its Worker) deserializes the same
429+
// bootstrapped principal context the main thread uses and then has the
430+
// worker-side bootstrap switches applied on top of it (they are written as
431+
// overrides of the main-thread setup).
432+
const bool for_worker = isolate_data->worker_context() != nullptr;
428433
if (use_snapshot) {
429434
CHECK_NOT_NULL(isolate_data->snapshot_data());
430435
env_snapshot_info = &isolate_data->snapshot_data()->env_info;
@@ -466,6 +471,25 @@ Environment* CreateEnvironment(
466471
Context::Scope context_scope(context);
467472
env->InitializeMainContext(context, env_snapshot_info);
468473

474+
if (use_snapshot && for_worker) {
475+
// The deserialized context went through is_main_thread /
476+
// does_own_process_state when the snapshot was built; the worker-side
477+
// switches redefine exactly those pieces (stdio getters, signal wiring,
478+
// process.abort/chdir/umask/..., debug helpers).
479+
if (env->principal_realm()
480+
->ExecuteBootstrapper(
481+
"internal/bootstrap/switches/is_not_main_thread")
482+
.IsEmpty() ||
483+
(!env->owns_process_state() &&
484+
env->principal_realm()
485+
->ExecuteBootstrapper(
486+
"internal/bootstrap/switches/does_not_own_process_state")
487+
.IsEmpty())) {
488+
FreeEnvironment(env);
489+
return nullptr;
490+
}
491+
}
492+
469493
#if HAVE_INSPECTOR
470494
if (env->should_create_inspector()) {
471495
if (inspector_parent_handle) {

src/node_options.cc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1272,6 +1272,12 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
12721272

12731273
PerIsolateOptionsParser::PerIsolateOptionsParser(
12741274
const EnvironmentOptionsParser& eop) {
1275+
AddOption("--worker-snapshot",
1276+
"start worker threads from the bootstrapped context in the "
1277+
"built-in startup snapshot",
1278+
BOOL_FIELD(worker_snapshot),
1279+
kAllowedInEnvvar,
1280+
true);
12751281
AddOption("--track-heap-objects",
12761282
"track heap object allocations for heap snapshots",
12771283
BOOL_FIELD(track_heap_objects),

src/node_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,7 @@ class EnvironmentOptions : public Options {
310310

311311
class PerIsolateOptions : public Options {
312312
public:
313+
bool worker_snapshot = true; // --[no-]worker-snapshot
313314
PerIsolateOptions() = default;
314315
PerIsolateOptions(PerIsolateOptions&&) = default;
315316

src/node_worker.cc

Lines changed: 97 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include "v8-profiler.h"
1717

1818
#include <memory>
19+
#include <optional>
1920
#include <string>
2021
#include <vector>
2122

@@ -291,6 +292,18 @@ size_t Worker::NearHeapLimit(void* data, size_t current_heap_limit,
291292
return new_limit;
292293
}
293294

295+
// Can this worker start by deserializing the bootstrapped principal context
296+
// from the embedded snapshot (plus the worker-side switches) instead of
297+
// bootstrapping from scratch? Only that snapshot qualifies: an embedder's own
298+
// or a --snapshot-blob one has run application code in its main context.
299+
// --no-worker-snapshot opts out; kNoBrowserGlobals changes the bootstrap.
300+
bool Worker::UseWorkerContextSnapshot() const {
301+
return snapshot_data_ != nullptr &&
302+
snapshot_data_ == SnapshotBuilder::GetEmbeddedSnapshotData() &&
303+
!(environment_flags_ & EnvironmentFlags::kNoBrowserGlobals) &&
304+
per_process::cli_options->per_isolate->worker_snapshot;
305+
}
306+
294307
void Worker::Run() {
295308
std::string trace_name = "[worker " + std::to_string(thread_id_.id) + "]" +
296309
(name_ == "" ? "" : " " + name_);
@@ -339,7 +352,15 @@ void Worker::Run() {
339352
// resource constraints, we need something in place to handle it,
340353
// though.
341354
TryCatch try_catch(isolate_);
342-
if (snapshot_data_ != nullptr) {
355+
if (UseWorkerContextSnapshot()) {
356+
// Leave `context` empty: CreateEnvironment() deserializes the
357+
// bootstrapped principal context (kNodeMainContextIndex) and the
358+
// Environment state that goes with it, applies the worker-side
359+
// switches, and skips RunBootstrapping().
360+
Debug(this,
361+
"Worker %llu deserializes the bootstrapped context\n",
362+
thread_id_.id);
363+
} else if (snapshot_data_ != nullptr) {
343364
Debug(this,
344365
"Worker %llu uses context from snapshot %d\n",
345366
thread_id_.id,
@@ -356,7 +377,7 @@ void Worker::Run() {
356377
this, "Worker %llu builds context from scratch\n", thread_id_.id);
357378
context = NewContext(isolate_);
358379
}
359-
if (context.IsEmpty()) {
380+
if (context.IsEmpty() && !UseWorkerContextSnapshot()) {
360381
// TODO(joyeecheung): maybe this should be kBootstrapFailure instead?
361382
Exit(ExitCode::kGenericUserError,
362383
"ERR_WORKER_INIT_FAILED",
@@ -366,8 +387,8 @@ void Worker::Run() {
366387
}
367388

368389
if (is_stopped()) return;
369-
CHECK(!context.IsEmpty());
370-
Context::Scope context_scope(context);
390+
std::optional<Context::Scope> context_scope;
391+
if (!context.IsEmpty()) context_scope.emplace(context);
371392
{
372393
#if HAVE_INSPECTOR
373394
environment_flags_ |= EnvironmentFlags::kNoWaitForInspectorFrontend;
@@ -383,6 +404,7 @@ void Worker::Run() {
383404
name_));
384405
if (is_stopped()) return;
385406
CHECK_NOT_NULL(env_);
407+
if (!context_scope) context_scope.emplace(env_->context());
386408
env_->set_env_vars(std::move(env_vars_));
387409
SetProcessExitHandler(env_.get(), [this](Environment*, int exit_code) {
388410
Exit(static_cast<ExitCode>(exit_code));
@@ -1412,8 +1434,70 @@ void GetEnvMessagePort(const FunctionCallbackInfo<Value>& args) {
14121434
}
14131435
}
14141436

1437+
// Per-thread values of the `worker` binding are lazy properties of the
1438+
// per-isolate template, so that a bootstrapped context carries none of them
1439+
// and can be deserialized by any thread.
1440+
void ThreadIdGetter(Local<v8::Name>,
1441+
const v8::PropertyCallbackInfo<Value>& info) {
1442+
Environment* env = Environment::GetCurrent(info);
1443+
info.GetReturnValue().Set(static_cast<double>(env->thread_id()));
1444+
}
1445+
1446+
void ThreadNameGetter(Local<v8::Name>,
1447+
const v8::PropertyCallbackInfo<Value>& info) {
1448+
Environment* env = Environment::GetCurrent(info);
1449+
Local<String> name;
1450+
if (String::NewFromUtf8(info.GetIsolate(),
1451+
env->thread_name().data(),
1452+
NewStringType::kNormal,
1453+
env->thread_name().size())
1454+
.ToLocal(&name)) {
1455+
info.GetReturnValue().Set(name);
1456+
}
1457+
}
1458+
1459+
void IsMainThreadGetter(Local<v8::Name>,
1460+
const v8::PropertyCallbackInfo<Value>& info) {
1461+
info.GetReturnValue().Set(Environment::GetCurrent(info)->is_main_thread());
1462+
}
1463+
1464+
void IsInternalThreadGetter(Local<v8::Name>,
1465+
const v8::PropertyCallbackInfo<Value>& info) {
1466+
Worker* worker =
1467+
Environment::GetCurrent(info)->isolate_data()->worker_context();
1468+
info.GetReturnValue().Set(worker != nullptr && worker->is_internal());
1469+
}
1470+
1471+
void OwnsProcessStateGetter(Local<v8::Name>,
1472+
const v8::PropertyCallbackInfo<Value>& info) {
1473+
info.GetReturnValue().Set(
1474+
Environment::GetCurrent(info)->owns_process_state());
1475+
}
1476+
1477+
void ResourceLimitsGetter(Local<v8::Name>,
1478+
const v8::PropertyCallbackInfo<Value>& info) {
1479+
Environment* env = Environment::GetCurrent(info);
1480+
if (env->worker_context() != nullptr) {
1481+
info.GetReturnValue().Set(
1482+
env->worker_context()->GetResourceLimits(info.GetIsolate()));
1483+
}
1484+
}
1485+
14151486
void CreateWorkerPerIsolateProperties(IsolateData* isolate_data,
14161487
Local<ObjectTemplate> target) {
1488+
{
1489+
Isolate* isolate = isolate_data->isolate();
1490+
auto lazy = [&](const char* name, v8::AccessorNameGetterCallback getter) {
1491+
target->SetLazyDataProperty(OneByteString(isolate, name), getter);
1492+
};
1493+
lazy("threadId", ThreadIdGetter);
1494+
lazy("threadName", ThreadNameGetter);
1495+
lazy("isMainThread", IsMainThreadGetter);
1496+
lazy("isInternalThread", IsInternalThreadGetter);
1497+
lazy("ownsProcessState", OwnsProcessStateGetter);
1498+
lazy("resourceLimits", ResourceLimitsGetter);
1499+
}
1500+
14171501
Isolate* isolate = isolate_data->isolate();
14181502

14191503
{
@@ -1518,55 +1602,9 @@ void CreateWorkerPerContextProperties(Local<Object> target,
15181602
Local<Value> unused,
15191603
Local<Context> context,
15201604
void* priv) {
1521-
Environment* env = Environment::GetCurrent(context);
1522-
Isolate* isolate = env->isolate();
1523-
1524-
target
1525-
->Set(env->context(),
1526-
env->thread_id_string(),
1527-
Number::New(isolate, static_cast<double>(env->thread_id())))
1528-
.Check();
1529-
1530-
target
1531-
->Set(env->context(),
1532-
env->thread_name_string(),
1533-
String::NewFromUtf8(isolate,
1534-
env->thread_name().data(),
1535-
NewStringType::kNormal,
1536-
env->thread_name().size())
1537-
.ToLocalChecked())
1538-
.Check();
1539-
1540-
target
1541-
->Set(env->context(),
1542-
FIXED_ONE_BYTE_STRING(isolate, "isMainThread"),
1543-
Boolean::New(isolate, env->is_main_thread()))
1544-
.Check();
1545-
1546-
Worker* worker = env->isolate_data()->worker_context();
1547-
bool is_internal = worker != nullptr && worker->is_internal();
1548-
1549-
// Set the is_internal property
1550-
target
1551-
->Set(env->context(),
1552-
FIXED_ONE_BYTE_STRING(isolate, "isInternalThread"),
1553-
Boolean::New(isolate, is_internal))
1554-
.Check();
1555-
1556-
target
1557-
->Set(env->context(),
1558-
FIXED_ONE_BYTE_STRING(isolate, "ownsProcessState"),
1559-
Boolean::New(isolate, env->owns_process_state()))
1560-
.Check();
1561-
1562-
if (!env->is_main_thread()) {
1563-
target
1564-
->Set(env->context(),
1565-
FIXED_ONE_BYTE_STRING(isolate, "resourceLimits"),
1566-
env->worker_context()->GetResourceLimits(isolate))
1567-
.Check();
1568-
}
1569-
1605+
// threadId, threadName, isMainThread, isInternalThread, ownsProcessState
1606+
// and resourceLimits are lazy properties of the per-isolate template (see
1607+
// CreateWorkerPerIsolateProperties).
15701608
NODE_DEFINE_CONSTANT(target, kMaxYoungGenerationSizeMb);
15711609
NODE_DEFINE_CONSTANT(target, kMaxOldGenerationSizeMb);
15721610
NODE_DEFINE_CONSTANT(target, kCodeRangeSizeMb);
@@ -1576,6 +1614,12 @@ void CreateWorkerPerContextProperties(Local<Object> target,
15761614

15771615
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
15781616
registry->Register(GetEnvMessagePort);
1617+
registry->Register(ThreadIdGetter);
1618+
registry->Register(ThreadNameGetter);
1619+
registry->Register(IsMainThreadGetter);
1620+
registry->Register(IsInternalThreadGetter);
1621+
registry->Register(OwnsProcessStateGetter);
1622+
registry->Register(ResourceLimitsGetter);
15791623
registry->Register(Worker::New);
15801624
registry->Register(Worker::StartThread);
15811625
registry->Register(Worker::StopThread);

src/node_worker.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class Worker : public AsyncWrap {
4242

4343
// Run the worker. This is only called from the worker thread.
4444
void Run();
45+
bool UseWorkerContextSnapshot() const;
4546

4647
// Forcibly exit the thread with a specified exit code. This may be called
4748
// from any thread. `error_code` and `error_message` can be used to create

test/parallel/test-bootstrap-modules.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,14 @@ expected.atRunTime = new Set([
118118

119119
const { isMainThread } = require('worker_threads');
120120
// Binaries built without the snapshot (e.g. cross-compiled) and
121-
// --no-node-snapshot bootstrap the main context from scratch, like a worker.
122-
const mainContextFromSnapshot = isMainThread &&
121+
// --no-node-snapshot bootstrap the context from scratch; so do workers under
122+
// --no-worker-snapshot.
123+
const contextFromSnapshot =
123124
process.config.variables.node_use_node_snapshot &&
124-
!process.execArgv.includes('--no-node-snapshot');
125+
!process.execArgv.includes('--no-node-snapshot') &&
126+
(isMainThread || !process.execArgv.includes('--no-worker-snapshot'));
125127

126-
if (mainContextFromSnapshot) {
128+
if (contextFromSnapshot) {
127129
[
128130
'Internal Binding cjs_lexer',
129131
'NativeModule internal/modules/esm/assert',
@@ -148,7 +150,8 @@ if (mainContextFromSnapshot) {
148150
} else if (isMainThread) {
149151
expected.beforePreExec.delete(getFormatNativeModule);
150152
expected.atRunTime.add(getFormatNativeModule);
151-
} else { // Worker.
153+
}
154+
if (!isMainThread) {
152155
[
153156
'NativeModule diagnostics_channel',
154157
'NativeModule internal/abort_controller',

0 commit comments

Comments
 (0)