mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
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: https://github.com/nodejs/node/pull/65336 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
This commit is contained in:
10 files changed
+162
-61
No files matched your search
@@ -2402,6 +2402,17 @@ added: v6.0.0
|
||||
|
||||
Silence all process warnings (including deprecations).
|
||||
|
||||
### `--no-worker-snapshot`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
Start worker threads by running the internal bootstrap from scratch instead of
|
||||
deserializing the bootstrapped context from the built-in startup snapshot.
|
||||
|
||||
### `--node-memory-debug`
|
||||
|
||||
<!-- YAML
|
||||
@@ -4102,6 +4113,7 @@ one is included in the list below.
|
||||
* `--no-strip-types`
|
||||
* `--no-warnings`
|
||||
* `--no-webstorage`
|
||||
* `--no-worker-snapshot`
|
||||
* `--node-memory-debug`
|
||||
* `--openssl-config`
|
||||
* `--openssl-legacy-provider`
|
||||
|
||||
@@ -1209,6 +1209,10 @@ For more information, see the TypeScript type-stripping documentation.
|
||||
.It Fl -no-warnings
|
||||
Silence all process warnings (including deprecations).
|
||||
.
|
||||
.It Fl -no-worker-snapshot
|
||||
Start worker threads by running the internal bootstrap from scratch instead of
|
||||
deserializing the bootstrapped context from the built-in startup snapshot.
|
||||
.
|
||||
.It Fl -node-memory-debug
|
||||
Enable extra debug checks for memory leaks in Node.js internals. This is
|
||||
usually only useful for developers debugging Node.js itself.
|
||||
@@ -2198,6 +2202,8 @@ one is included in the list below.
|
||||
.It
|
||||
\fB--no-webstorage\fR
|
||||
.It
|
||||
\fB--no-worker-snapshot\fR
|
||||
.It
|
||||
\fB--node-memory-debug\fR
|
||||
.It
|
||||
\fB--openssl-config\fR
|
||||
|
||||
@@ -6,6 +6,12 @@ const {
|
||||
|
||||
delete process._debugProcess;
|
||||
delete process._debugEnd;
|
||||
// Also drop the other main-thread-only helpers is_main_thread.js installs, so
|
||||
// that this switch can be applied on top of a context bootstrapped for the
|
||||
// main thread (as when a worker starts from the built-in snapshot).
|
||||
delete process._debugPause;
|
||||
delete process._startProfilerIdleNotifier;
|
||||
delete process._stopProfilerIdleNotifier;
|
||||
|
||||
function defineStream(name, getter) {
|
||||
ObjectDefineProperty(process, name, {
|
||||
|
||||
@@ -163,14 +163,12 @@ function prepareExecution(options) {
|
||||
// channel. This needs to be done before any user code gets executed
|
||||
// (including preload modules).
|
||||
initializeClusterIPC();
|
||||
|
||||
// TODO(joyeecheung): do this for worker threads as well.
|
||||
runDeserializeCallbacks();
|
||||
} else {
|
||||
assert(!internalBinding('worker').isMainThread);
|
||||
// The setup should be called in LOAD_SCRIPT message handler.
|
||||
assert(!initializeModules);
|
||||
}
|
||||
runDeserializeCallbacks();
|
||||
|
||||
const { initializeExtensionFormatMap } = require('internal/modules/esm/get_format');
|
||||
initializeExtensionFormatMap();
|
||||
|
||||
@@ -425,6 +425,11 @@ Environment* CreateEnvironment(
|
||||
|
||||
const bool use_snapshot = context.IsEmpty();
|
||||
const EnvSerializeInfo* env_snapshot_info = nullptr;
|
||||
// A worker thread (its IsolateData knows its Worker) deserializes the same
|
||||
// bootstrapped principal context the main thread uses and then has the
|
||||
// worker-side bootstrap switches applied on top of it (they are written as
|
||||
// overrides of the main-thread setup).
|
||||
const bool for_worker = isolate_data->worker_context() != nullptr;
|
||||
if (use_snapshot) {
|
||||
CHECK_NOT_NULL(isolate_data->snapshot_data());
|
||||
env_snapshot_info = &isolate_data->snapshot_data()->env_info;
|
||||
@@ -466,6 +471,25 @@ Environment* CreateEnvironment(
|
||||
Context::Scope context_scope(context);
|
||||
env->InitializeMainContext(context, env_snapshot_info);
|
||||
|
||||
if (use_snapshot && for_worker) {
|
||||
// The deserialized context went through is_main_thread /
|
||||
// does_own_process_state when the snapshot was built; the worker-side
|
||||
// switches redefine exactly those pieces (stdio getters, signal wiring,
|
||||
// process.abort/chdir/umask/..., debug helpers).
|
||||
if (env->principal_realm()
|
||||
->ExecuteBootstrapper(
|
||||
"internal/bootstrap/switches/is_not_main_thread")
|
||||
.IsEmpty() ||
|
||||
(!env->owns_process_state() &&
|
||||
env->principal_realm()
|
||||
->ExecuteBootstrapper(
|
||||
"internal/bootstrap/switches/does_not_own_process_state")
|
||||
.IsEmpty())) {
|
||||
FreeEnvironment(env);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
#if HAVE_INSPECTOR
|
||||
if (env->should_create_inspector()) {
|
||||
if (inspector_parent_handle) {
|
||||
|
||||
@@ -1366,6 +1366,12 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
|
||||
|
||||
PerIsolateOptionsParser::PerIsolateOptionsParser(
|
||||
const EnvironmentOptionsParser& eop) {
|
||||
AddOption("--worker-snapshot",
|
||||
"start worker threads from the bootstrapped context in the "
|
||||
"built-in startup snapshot",
|
||||
BOOL_FIELD(worker_snapshot),
|
||||
kAllowedInEnvvar,
|
||||
true);
|
||||
AddOption("--track-heap-objects",
|
||||
"track heap object allocations for heap snapshots",
|
||||
BOOL_FIELD(track_heap_objects),
|
||||
|
||||
@@ -319,6 +319,7 @@ class EnvironmentOptions : public Options {
|
||||
|
||||
class PerIsolateOptions : public Options {
|
||||
public:
|
||||
bool worker_snapshot = true; // --[no-]worker-snapshot
|
||||
PerIsolateOptions() = default;
|
||||
PerIsolateOptions(PerIsolateOptions&&) = default;
|
||||
|
||||
|
||||
+97
-53
@@ -16,6 +16,7 @@
|
||||
#include "v8-profiler.h"
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -292,6 +293,18 @@ size_t Worker::NearHeapLimit(void* data, size_t current_heap_limit,
|
||||
return new_limit;
|
||||
}
|
||||
|
||||
// Can this worker start by deserializing the bootstrapped principal context
|
||||
// from the embedded snapshot (plus the worker-side switches) instead of
|
||||
// bootstrapping from scratch? Only that snapshot qualifies: an embedder's own
|
||||
// or a --snapshot-blob one has run application code in its main context.
|
||||
// --no-worker-snapshot opts out; kNoBrowserGlobals changes the bootstrap.
|
||||
bool Worker::UseWorkerContextSnapshot() const {
|
||||
return snapshot_data_ != nullptr &&
|
||||
snapshot_data_ == SnapshotBuilder::GetEmbeddedSnapshotData() &&
|
||||
!(environment_flags_ & EnvironmentFlags::kNoBrowserGlobals) &&
|
||||
per_process::cli_options->per_isolate->worker_snapshot;
|
||||
}
|
||||
|
||||
void Worker::Run() {
|
||||
std::string trace_name = "[worker " + std::to_string(thread_id_.id) + "]" +
|
||||
(name_ == "" ? "" : " " + name_);
|
||||
@@ -340,7 +353,15 @@ void Worker::Run() {
|
||||
// resource constraints, we need something in place to handle it,
|
||||
// though.
|
||||
TryCatch try_catch(isolate_);
|
||||
if (snapshot_data_ != nullptr) {
|
||||
if (UseWorkerContextSnapshot()) {
|
||||
// Leave `context` empty: CreateEnvironment() deserializes the
|
||||
// bootstrapped principal context (kNodeMainContextIndex) and the
|
||||
// Environment state that goes with it, applies the worker-side
|
||||
// switches, and skips RunBootstrapping().
|
||||
Debug(this,
|
||||
"Worker %llu deserializes the bootstrapped context\n",
|
||||
thread_id_.id);
|
||||
} else if (snapshot_data_ != nullptr) {
|
||||
Debug(this,
|
||||
"Worker %llu uses context from snapshot %d\n",
|
||||
thread_id_.id,
|
||||
@@ -357,7 +378,7 @@ void Worker::Run() {
|
||||
this, "Worker %llu builds context from scratch\n", thread_id_.id);
|
||||
context = NewContext(isolate_);
|
||||
}
|
||||
if (context.IsEmpty()) {
|
||||
if (context.IsEmpty() && !UseWorkerContextSnapshot()) {
|
||||
// TODO(joyeecheung): maybe this should be kBootstrapFailure instead?
|
||||
Exit(ExitCode::kGenericUserError,
|
||||
"ERR_WORKER_INIT_FAILED",
|
||||
@@ -367,8 +388,8 @@ void Worker::Run() {
|
||||
}
|
||||
|
||||
if (is_stopped()) return;
|
||||
CHECK(!context.IsEmpty());
|
||||
Context::Scope context_scope(context);
|
||||
std::optional<Context::Scope> context_scope;
|
||||
if (!context.IsEmpty()) context_scope.emplace(context);
|
||||
{
|
||||
#if HAVE_INSPECTOR
|
||||
environment_flags_ |= EnvironmentFlags::kNoWaitForInspectorFrontend;
|
||||
@@ -384,6 +405,7 @@ void Worker::Run() {
|
||||
name_));
|
||||
if (is_stopped()) return;
|
||||
CHECK_NOT_NULL(env_);
|
||||
if (!context_scope) context_scope.emplace(env_->context());
|
||||
env_->set_env_vars(std::move(env_vars_));
|
||||
SetProcessExitHandler(env_.get(), [this](Environment*, int exit_code) {
|
||||
Exit(static_cast<ExitCode>(exit_code));
|
||||
@@ -1416,8 +1438,70 @@ void GetEnvMessagePort(const FunctionCallbackInfo<Value>& args) {
|
||||
}
|
||||
}
|
||||
|
||||
// Per-thread values of the `worker` binding are lazy properties of the
|
||||
// per-isolate template, so that a bootstrapped context carries none of them
|
||||
// and can be deserialized by any thread.
|
||||
void ThreadIdGetter(Local<v8::Name>,
|
||||
const v8::PropertyCallbackInfo<Value>& info) {
|
||||
Environment* env = Environment::GetCurrent(info);
|
||||
info.GetReturnValue().Set(static_cast<double>(env->thread_id()));
|
||||
}
|
||||
|
||||
void ThreadNameGetter(Local<v8::Name>,
|
||||
const v8::PropertyCallbackInfo<Value>& info) {
|
||||
Environment* env = Environment::GetCurrent(info);
|
||||
Local<String> name;
|
||||
if (String::NewFromUtf8(info.GetIsolate(),
|
||||
env->thread_name().data(),
|
||||
NewStringType::kNormal,
|
||||
env->thread_name().size())
|
||||
.ToLocal(&name)) {
|
||||
info.GetReturnValue().Set(name);
|
||||
}
|
||||
}
|
||||
|
||||
void IsMainThreadGetter(Local<v8::Name>,
|
||||
const v8::PropertyCallbackInfo<Value>& info) {
|
||||
info.GetReturnValue().Set(Environment::GetCurrent(info)->is_main_thread());
|
||||
}
|
||||
|
||||
void IsInternalThreadGetter(Local<v8::Name>,
|
||||
const v8::PropertyCallbackInfo<Value>& info) {
|
||||
Worker* worker =
|
||||
Environment::GetCurrent(info)->isolate_data()->worker_context();
|
||||
info.GetReturnValue().Set(worker != nullptr && worker->is_internal());
|
||||
}
|
||||
|
||||
void OwnsProcessStateGetter(Local<v8::Name>,
|
||||
const v8::PropertyCallbackInfo<Value>& info) {
|
||||
info.GetReturnValue().Set(
|
||||
Environment::GetCurrent(info)->owns_process_state());
|
||||
}
|
||||
|
||||
void ResourceLimitsGetter(Local<v8::Name>,
|
||||
const v8::PropertyCallbackInfo<Value>& info) {
|
||||
Environment* env = Environment::GetCurrent(info);
|
||||
if (env->worker_context() != nullptr) {
|
||||
info.GetReturnValue().Set(
|
||||
env->worker_context()->GetResourceLimits(info.GetIsolate()));
|
||||
}
|
||||
}
|
||||
|
||||
void CreateWorkerPerIsolateProperties(IsolateData* isolate_data,
|
||||
Local<ObjectTemplate> target) {
|
||||
{
|
||||
Isolate* isolate = isolate_data->isolate();
|
||||
auto lazy = [&](const char* name, v8::AccessorNameGetterCallback getter) {
|
||||
target->SetLazyDataProperty(OneByteString(isolate, name), getter);
|
||||
};
|
||||
lazy("threadId", ThreadIdGetter);
|
||||
lazy("threadName", ThreadNameGetter);
|
||||
lazy("isMainThread", IsMainThreadGetter);
|
||||
lazy("isInternalThread", IsInternalThreadGetter);
|
||||
lazy("ownsProcessState", OwnsProcessStateGetter);
|
||||
lazy("resourceLimits", ResourceLimitsGetter);
|
||||
}
|
||||
|
||||
Isolate* isolate = isolate_data->isolate();
|
||||
|
||||
{
|
||||
@@ -1522,55 +1606,9 @@ void CreateWorkerPerContextProperties(Local<Object> target,
|
||||
Local<Value> unused,
|
||||
Local<Context> context,
|
||||
void* priv) {
|
||||
Environment* env = Environment::GetCurrent(context);
|
||||
Isolate* isolate = env->isolate();
|
||||
|
||||
target
|
||||
->Set(env->context(),
|
||||
env->thread_id_string(),
|
||||
Number::New(isolate, static_cast<double>(env->thread_id())))
|
||||
.Check();
|
||||
|
||||
target
|
||||
->Set(env->context(),
|
||||
env->thread_name_string(),
|
||||
String::NewFromUtf8(isolate,
|
||||
env->thread_name().data(),
|
||||
NewStringType::kNormal,
|
||||
env->thread_name().size())
|
||||
.ToLocalChecked())
|
||||
.Check();
|
||||
|
||||
target
|
||||
->Set(env->context(),
|
||||
FIXED_ONE_BYTE_STRING(isolate, "isMainThread"),
|
||||
Boolean::New(isolate, env->is_main_thread()))
|
||||
.Check();
|
||||
|
||||
Worker* worker = env->isolate_data()->worker_context();
|
||||
bool is_internal = worker != nullptr && worker->is_internal();
|
||||
|
||||
// Set the is_internal property
|
||||
target
|
||||
->Set(env->context(),
|
||||
FIXED_ONE_BYTE_STRING(isolate, "isInternalThread"),
|
||||
Boolean::New(isolate, is_internal))
|
||||
.Check();
|
||||
|
||||
target
|
||||
->Set(env->context(),
|
||||
FIXED_ONE_BYTE_STRING(isolate, "ownsProcessState"),
|
||||
Boolean::New(isolate, env->owns_process_state()))
|
||||
.Check();
|
||||
|
||||
if (!env->is_main_thread()) {
|
||||
target
|
||||
->Set(env->context(),
|
||||
FIXED_ONE_BYTE_STRING(isolate, "resourceLimits"),
|
||||
env->worker_context()->GetResourceLimits(isolate))
|
||||
.Check();
|
||||
}
|
||||
|
||||
// threadId, threadName, isMainThread, isInternalThread, ownsProcessState
|
||||
// and resourceLimits are lazy properties of the per-isolate template (see
|
||||
// CreateWorkerPerIsolateProperties).
|
||||
NODE_DEFINE_CONSTANT(target, kMaxYoungGenerationSizeMb);
|
||||
NODE_DEFINE_CONSTANT(target, kMaxOldGenerationSizeMb);
|
||||
NODE_DEFINE_CONSTANT(target, kCodeRangeSizeMb);
|
||||
@@ -1580,6 +1618,12 @@ void CreateWorkerPerContextProperties(Local<Object> target,
|
||||
|
||||
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
|
||||
registry->Register(GetEnvMessagePort);
|
||||
registry->Register(ThreadIdGetter);
|
||||
registry->Register(ThreadNameGetter);
|
||||
registry->Register(IsMainThreadGetter);
|
||||
registry->Register(IsInternalThreadGetter);
|
||||
registry->Register(OwnsProcessStateGetter);
|
||||
registry->Register(ResourceLimitsGetter);
|
||||
registry->Register(Worker::New);
|
||||
registry->Register(Worker::StartThread);
|
||||
registry->Register(Worker::StopThread);
|
||||
|
||||
@@ -42,6 +42,7 @@ class Worker : public AsyncWrap {
|
||||
|
||||
// Run the worker. This is only called from the worker thread.
|
||||
void Run();
|
||||
bool UseWorkerContextSnapshot() const;
|
||||
|
||||
// Forcibly exit the thread with a specified exit code. This may be called
|
||||
// from any thread. `error_code` and `error_message` can be used to create
|
||||
|
||||
@@ -118,12 +118,14 @@ expected.atRunTime = new Set([
|
||||
|
||||
const { isMainThread } = require('worker_threads');
|
||||
// Binaries built without the snapshot (e.g. cross-compiled) and
|
||||
// --no-node-snapshot bootstrap the main context from scratch, like a worker.
|
||||
const mainContextFromSnapshot = isMainThread &&
|
||||
// --no-node-snapshot bootstrap the context from scratch; so do workers under
|
||||
// --no-worker-snapshot.
|
||||
const contextFromSnapshot =
|
||||
process.config.variables.node_use_node_snapshot &&
|
||||
!process.execArgv.includes('--no-node-snapshot');
|
||||
!process.execArgv.includes('--no-node-snapshot') &&
|
||||
(isMainThread || !process.execArgv.includes('--no-worker-snapshot'));
|
||||
|
||||
if (mainContextFromSnapshot) {
|
||||
if (contextFromSnapshot) {
|
||||
[
|
||||
'Internal Binding cjs_lexer',
|
||||
'NativeModule internal/modules/esm/assert',
|
||||
@@ -148,7 +150,8 @@ if (mainContextFromSnapshot) {
|
||||
} else if (isMainThread) {
|
||||
expected.beforePreExec.delete(getFormatNativeModule);
|
||||
expected.atRunTime.add(getFormatNativeModule);
|
||||
} else { // Worker.
|
||||
}
|
||||
if (!isMainThread) {
|
||||
[
|
||||
'NativeModule diagnostics_channel',
|
||||
'NativeModule internal/abort_controller',
|
||||
|
||||
Reference in new issue
Block a user