Skip to content

Commit ddc0a0a

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> PR-URL: #65336 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
1 parent 5ceeb6e commit ddc0a0a

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
@@ -2402,6 +2402,17 @@ added: v6.0.0
24022402

24032403
Silence all process warnings (including deprecations).
24042404

2405+
### `--no-worker-snapshot`
2406+
2407+
<!-- YAML
2408+
added: REPLACEME
2409+
-->
2410+
2411+
> Stability: 1 - Experimental
2412+
2413+
Start worker threads by running the internal bootstrap from scratch instead of
2414+
deserializing the bootstrapped context from the built-in startup snapshot.
2415+
24052416
### `--node-memory-debug`
24062417

24072418
<!-- YAML
@@ -4102,6 +4113,7 @@ one is included in the list below.
41024113
* `--no-strip-types`
41034114
* `--no-warnings`
41044115
* `--no-webstorage`
4116+
* `--no-worker-snapshot`
41054117
* `--node-memory-debug`
41064118
* `--openssl-config`
41074119
* `--openssl-legacy-provider`

doc/node.1

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1209,6 +1209,10 @@ For more information, see the TypeScript type-stripping documentation.
12091209
.It Fl -no-warnings
12101210
Silence all process warnings (including deprecations).
12111211
.
1212+
.It Fl -no-worker-snapshot
1213+
Start worker threads by running the internal bootstrap from scratch instead of
1214+
deserializing the bootstrapped context from the built-in startup snapshot.
1215+
.
12121216
.It Fl -node-memory-debug
12131217
Enable extra debug checks for memory leaks in Node.js internals. This is
12141218
usually only useful for developers debugging Node.js itself.
@@ -2198,6 +2202,8 @@ one is included in the list below.
21982202
.It
21992203
\fB--no-webstorage\fR
22002204
.It
2205+
\fB--no-worker-snapshot\fR
2206+
.It
22012207
\fB--node-memory-debug\fR
22022208
.It
22032209
\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
@@ -1366,6 +1366,12 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
13661366

13671367
PerIsolateOptionsParser::PerIsolateOptionsParser(
13681368
const EnvironmentOptionsParser& eop) {
1369+
AddOption("--worker-snapshot",
1370+
"start worker threads from the bootstrapped context in the "
1371+
"built-in startup snapshot",
1372+
BOOL_FIELD(worker_snapshot),
1373+
kAllowedInEnvvar,
1374+
true);
13691375
AddOption("--track-heap-objects",
13701376
"track heap object allocations for heap snapshots",
13711377
BOOL_FIELD(track_heap_objects),

src/node_options.h

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

320320
class PerIsolateOptions : public Options {
321321
public:
322+
bool worker_snapshot = true; // --[no-]worker-snapshot
322323
PerIsolateOptions() = default;
323324
PerIsolateOptions(PerIsolateOptions&&) = default;
324325

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

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

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

369390
if (is_stopped()) return;
370-
CHECK(!context.IsEmpty());
371-
Context::Scope context_scope(context);
391+
std::optional<Context::Scope> context_scope;
392+
if (!context.IsEmpty()) context_scope.emplace(context);
372393
{
373394
#if HAVE_INSPECTOR
374395
environment_flags_ |= EnvironmentFlags::kNoWaitForInspectorFrontend;
@@ -384,6 +405,7 @@ void Worker::Run() {
384405
name_));
385406
if (is_stopped()) return;
386407
CHECK_NOT_NULL(env_);
408+
if (!context_scope) context_scope.emplace(env_->context());
387409
env_->set_env_vars(std::move(env_vars_));
388410
SetProcessExitHandler(env_.get(), [this](Environment*, int exit_code) {
389411
Exit(static_cast<ExitCode>(exit_code));
@@ -1416,8 +1438,70 @@ void GetEnvMessagePort(const FunctionCallbackInfo<Value>& args) {
14161438
}
14171439
}
14181440

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

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

15811619
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
15821620
registry->Register(GetEnvMessagePort);
1621+
registry->Register(ThreadIdGetter);
1622+
registry->Register(ThreadNameGetter);
1623+
registry->Register(IsMainThreadGetter);
1624+
registry->Register(IsInternalThreadGetter);
1625+
registry->Register(OwnsProcessStateGetter);
1626+
registry->Register(ResourceLimitsGetter);
15831627
registry->Register(Worker::New);
15841628
registry->Register(Worker::StartThread);
15851629
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)