Skip to content

Commit f7e1092

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(); skip the isolate error-handler reset. - Worker::Run(): take that path when the built-in (kDefault) snapshot is in use, 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. - --[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 30bff4a commit f7e1092

8 files changed

Lines changed: 155 additions & 54 deletions

File tree

doc/api/cli.md

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

22722272
Silence all process warnings (including deprecations).
22732273

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

22762287
<!-- YAML
@@ -3937,6 +3948,7 @@ one is included in the list below.
39373948
* `--no-strip-types`
39383949
* `--no-warnings`
39393950
* `--no-webstorage`
3951+
* `--no-worker-snapshot`
39403952
* `--node-memory-debug`
39413953
* `--openssl-config`
39423954
* `--openssl-legacy-provider`

doc/node.1

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,6 +1142,10 @@ For more information, see the TypeScript type-stripping documentation.
11421142
.It Fl -no-warnings
11431143
Silence all process warnings (including deprecations).
11441144
.
1145+
.It Fl -no-worker-snapshot
1146+
Start worker threads by running the internal bootstrap from scratch instead of
1147+
deserializing the bootstrapped context from the built-in startup snapshot.
1148+
.
11451149
.It Fl -node-memory-debug
11461150
Enable extra debug checks for memory leaks in Node.js internals. This is
11471151
usually only useful for developers debugging Node.js itself.
@@ -2105,6 +2109,8 @@ one is included in the list below.
21052109
.It
21062110
\fB--no-webstorage\fR
21072111
.It
2112+
\fB--no-worker-snapshot\fR
2113+
.It
21082114
\fB--node-memory-debug\fR
21092115
.It
21102116
\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, {

src/api/environment.cc

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,11 @@ Environment* CreateEnvironment(
433433

434434
const bool use_snapshot = context.IsEmpty();
435435
const EnvSerializeInfo* env_snapshot_info = nullptr;
436+
// A worker thread (its IsolateData knows its Worker) deserializes the same
437+
// bootstrapped principal context the main thread uses and then has the
438+
// worker-side bootstrap switches applied on top of it (they are written as
439+
// overrides of the main-thread setup).
440+
const bool for_worker = isolate_data->worker_context() != nullptr;
436441
if (use_snapshot) {
437442
CHECK_NOT_NULL(isolate_data->snapshot_data());
438443
env_snapshot_info = &isolate_data->snapshot_data()->env_info;
@@ -469,12 +474,31 @@ Environment* CreateEnvironment(
469474
FreeEnvironment(env);
470475
return nullptr;
471476
}
472-
SetIsolateErrorHandlers(isolate, {});
477+
if (!for_worker) SetIsolateErrorHandlers(isolate, {});
473478
}
474479

475480
Context::Scope context_scope(context);
476481
env->InitializeMainContext(context, env_snapshot_info);
477482

483+
if (use_snapshot && for_worker) {
484+
// The deserialized context went through is_main_thread /
485+
// does_own_process_state when the snapshot was built; the worker-side
486+
// switches redefine exactly those pieces (stdio getters, signal wiring,
487+
// process.abort/chdir/umask/..., debug helpers).
488+
if (env->principal_realm()
489+
->ExecuteBootstrapper(
490+
"internal/bootstrap/switches/is_not_main_thread")
491+
.IsEmpty() ||
492+
(!env->owns_process_state() &&
493+
env->principal_realm()
494+
->ExecuteBootstrapper(
495+
"internal/bootstrap/switches/does_not_own_process_state")
496+
.IsEmpty())) {
497+
FreeEnvironment(env);
498+
return nullptr;
499+
}
500+
}
501+
478502
#if HAVE_INSPECTOR
479503
if (env->should_create_inspector()) {
480504
if (inspector_parent_handle) {

src/node_options.cc

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

12761276
PerIsolateOptionsParser::PerIsolateOptionsParser(
12771277
const EnvironmentOptionsParser& eop) {
1278+
AddOption("--worker-snapshot",
1279+
"start worker threads from the bootstrapped context in the "
1280+
"built-in startup snapshot",
1281+
BOOL_FIELD(worker_snapshot),
1282+
kAllowedInEnvvar,
1283+
true);
12781284
AddOption("--track-heap-objects",
12791285
"track heap object allocations for heap snapshots",
12801286
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: 98 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

@@ -293,6 +294,20 @@ size_t Worker::NearHeapLimit(void* data, size_t current_heap_limit,
293294
return new_limit;
294295
}
295296

297+
// Can this worker start by deserializing the bootstrapped principal context
298+
// from the built-in snapshot (plus the worker-side switches) instead of
299+
// bootstrapping from scratch? Requires the built-in snapshot (a customized
300+
// one has run the application's entry point in that context), the same
301+
// bootstrap shape (browser globals present), and no --no-worker-snapshot.
302+
bool Worker::UseWorkerContextSnapshot() const {
303+
if (snapshot_data_ == nullptr) return false;
304+
if (snapshot_data_->metadata.type != SnapshotMetadata::Type::kDefault)
305+
return false;
306+
if (environment_flags_ & EnvironmentFlags::kNoBrowserGlobals) return false;
307+
if (!per_process::cli_options->per_isolate->worker_snapshot) return false;
308+
return true;
309+
}
310+
296311
void Worker::Run() {
297312
std::string trace_name = "[worker " + std::to_string(thread_id_.id) + "]" +
298313
(name_ == "" ? "" : " " + name_);
@@ -341,7 +356,15 @@ void Worker::Run() {
341356
// resource constraints, we need something in place to handle it,
342357
// though.
343358
TryCatch try_catch(isolate_);
344-
if (snapshot_data_ != nullptr) {
359+
if (UseWorkerContextSnapshot()) {
360+
// Leave `context` empty: CreateEnvironment() deserializes the
361+
// bootstrapped principal context (kNodeMainContextIndex) and the
362+
// Environment state that goes with it, applies the worker-side
363+
// switches, and skips RunBootstrapping().
364+
Debug(this,
365+
"Worker %llu deserializes the bootstrapped context\n",
366+
thread_id_.id);
367+
} else if (snapshot_data_ != nullptr) {
345368
Debug(this,
346369
"Worker %llu uses context from snapshot %d\n",
347370
thread_id_.id,
@@ -358,7 +381,7 @@ void Worker::Run() {
358381
this, "Worker %llu builds context from scratch\n", thread_id_.id);
359382
context = NewContext(isolate_);
360383
}
361-
if (context.IsEmpty()) {
384+
if (context.IsEmpty() && !UseWorkerContextSnapshot()) {
362385
// TODO(joyeecheung): maybe this should be kBootstrapFailure instead?
363386
Exit(ExitCode::kGenericUserError,
364387
"ERR_WORKER_INIT_FAILED",
@@ -368,8 +391,8 @@ void Worker::Run() {
368391
}
369392

370393
if (is_stopped()) return;
371-
CHECK(!context.IsEmpty());
372-
Context::Scope context_scope(context);
394+
std::optional<Context::Scope> context_scope;
395+
if (!context.IsEmpty()) context_scope.emplace(context);
373396
{
374397
#if HAVE_INSPECTOR
375398
environment_flags_ |= EnvironmentFlags::kNoWaitForInspectorFrontend;
@@ -385,6 +408,7 @@ void Worker::Run() {
385408
name_));
386409
if (is_stopped()) return;
387410
CHECK_NOT_NULL(env_);
411+
if (!context_scope) context_scope.emplace(env_->context());
388412
env_->set_env_vars(std::move(env_vars_));
389413
SetProcessExitHandler(env_.get(), [this](Environment*, int exit_code) {
390414
Exit(static_cast<ExitCode>(exit_code));
@@ -1414,8 +1438,69 @@ void GetEnvMessagePort(const FunctionCallbackInfo<Value>& args) {
14141438
}
14151439
}
14161440

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

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

15791618
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
15801619
registry->Register(GetEnvMessagePort);
1620+
registry->Register(ThreadIdGetter);
1621+
registry->Register(ThreadNameGetter);
1622+
registry->Register(IsMainThreadGetter);
1623+
registry->Register(IsInternalThreadGetter);
1624+
registry->Register(OwnsProcessStateGetter);
1625+
registry->Register(ResourceLimitsGetter);
15811626
registry->Register(Worker::New);
15821627
registry->Register(Worker::StartThread);
15831628
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

0 commit comments

Comments
 (0)