Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -2271,6 +2271,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
Expand Down Expand Up @@ -3937,6 +3948,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`
Expand Down
6 changes: 6 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -1142,6 +1142,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.
Expand Down Expand Up @@ -2105,6 +2109,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
Expand Down
6 changes: 6 additions & 0 deletions lib/internal/bootstrap/switches/is_not_main_thread.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down
26 changes: 25 additions & 1 deletion src/api/environment.cc
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,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;
Expand Down Expand Up @@ -469,12 +474,31 @@ Environment* CreateEnvironment(
FreeEnvironment(env);
return nullptr;
}
SetIsolateErrorHandlers(isolate, {});
if (!for_worker) SetIsolateErrorHandlers(isolate, {});
}

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) {
Expand Down
6 changes: 6 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1275,6 +1275,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),
Expand Down
1 change: 1 addition & 0 deletions src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ class EnvironmentOptions : public Options {

class PerIsolateOptions : public Options {
public:
bool worker_snapshot = true; // --[no-]worker-snapshot
PerIsolateOptions() = default;
PerIsolateOptions(PerIsolateOptions&&) = default;

Expand Down
150 changes: 97 additions & 53 deletions src/node_worker.cc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include "v8-profiler.h"

#include <memory>
#include <optional>
#include <string>
#include <vector>

Expand Down Expand Up @@ -293,6 +294,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_);
Expand Down Expand Up @@ -341,7 +354,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,
Expand All @@ -358,7 +379,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",
Expand All @@ -368,8 +389,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;
Expand All @@ -385,6 +406,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));
Expand Down Expand Up @@ -1414,8 +1436,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();

{
Expand Down Expand Up @@ -1520,55 +1604,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);
Expand All @@ -1578,6 +1616,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);
Expand Down
1 change: 1 addition & 0 deletions src/node_worker.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading