Files
node/src/node_worker.h
T
Shelley Vohr ddc0a0aa28 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>
2026-09-04 13:28:27 +00:00

164 lines
5.6 KiB
C++

#ifndef SRC_NODE_WORKER_H_
#define SRC_NODE_WORKER_H_
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#include <optional>
#include <unordered_map>
#include "json_utils.h"
#include "node_exit_code.h"
#include "node_messaging.h"
#include "util.h"
#include "uv.h"
namespace node {
struct SnapshotData;
namespace worker {
class WorkerThreadData;
enum ResourceLimits {
kMaxYoungGenerationSizeMb,
kMaxOldGenerationSizeMb,
kCodeRangeSizeMb,
kStackSizeMb,
kTotalResourceLimitCount
};
// A worker thread, as represented in its parent thread.
class Worker : public AsyncWrap {
public:
Worker(Environment* env,
v8::Local<v8::Object> wrap,
const std::string& url,
const std::string& name,
std::shared_ptr<PerIsolateOptions> per_isolate_opts,
std::vector<std::string>&& exec_argv,
std::shared_ptr<KVStore> env_vars,
const SnapshotData* snapshot_data,
const bool is_internal);
~Worker() override;
// 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
// a custom `'error'` event before emitting `'exit'`.
void Exit(ExitCode code,
const char* error_code = nullptr,
const char* error_message = nullptr);
// Wait for the worker thread to stop (in a blocking manner).
void JoinThread();
template <typename Fn>
inline bool RequestInterrupt(Fn&& cb);
SET_NO_MEMORY_INFO()
SET_MEMORY_INFO_NAME(Worker)
SET_SELF_SIZE(Worker)
bool IsNotIndicativeOfMemoryLeakAtExit() const override;
bool is_stopped() const;
const SnapshotData* snapshot_data() const { return snapshot_data_; }
bool is_internal() const { return is_internal_; }
std::string_view name() const { return name_; }
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
static void StartThread(const v8::FunctionCallbackInfo<v8::Value>& args);
static void StopThread(const v8::FunctionCallbackInfo<v8::Value>& args);
static void HasRef(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Ref(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Unref(const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetResourceLimits(
const v8::FunctionCallbackInfo<v8::Value>& args);
v8::Local<v8::Float64Array> GetResourceLimits(v8::Isolate* isolate) const;
static void TakeHeapSnapshot(const v8::FunctionCallbackInfo<v8::Value>& args);
static void LoopIdleTime(const v8::FunctionCallbackInfo<v8::Value>& args);
static void LoopStartTime(const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetHeapStatistics(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void CpuUsage(const v8::FunctionCallbackInfo<v8::Value>& args);
static void StartCpuProfile(const v8::FunctionCallbackInfo<v8::Value>& args);
static void StopCpuProfile(const v8::FunctionCallbackInfo<v8::Value>& args);
static void StartHeapProfile(const v8::FunctionCallbackInfo<v8::Value>& args);
static void StopHeapProfile(const v8::FunctionCallbackInfo<v8::Value>& args);
private:
bool CreateEnvMessagePort(Environment* env);
static size_t NearHeapLimit(void* data, size_t current_heap_limit,
size_t initial_heap_limit);
std::shared_ptr<PerIsolateOptions> per_isolate_opts_;
std::vector<std::string> exec_argv_;
std::vector<std::string> argv_;
MultiIsolatePlatform* platform_;
v8::Isolate* isolate_ = nullptr;
std::optional<uv_thread_t> tid_; // Set while the thread is running
std::unique_ptr<InspectorParentHandle> inspector_parent_handle_;
// This mutex protects access to all variables listed below it.
mutable Mutex mutex_;
const char* custom_error_ = nullptr;
std::string custom_error_str_;
ExitCode exit_code_ = ExitCode::kNoFailure;
ThreadId thread_id_;
uintptr_t stack_base_ = 0;
// Optional name used for debugging in inspector and trace events.
std::string name_;
// Custom resource constraints:
double resource_limits_[kTotalResourceLimitCount];
void UpdateResourceConstraints(v8::ResourceConstraints* constraints);
// Full size of the thread's stack.
size_t stack_size_ = 4 * 1024 * 1024;
// Stack buffer size that is not available to the JS engine.
static constexpr size_t kStackBufferSize = 192 * 1024;
std::unique_ptr<MessagePortData> child_port_data_;
std::shared_ptr<KVStore> env_vars_;
EmbedderPreloadCallback embedder_preload_;
// A raw flag that is used by creator and worker threads to
// sync up on pre-mature termination of worker - while in the
// warmup phase. Once the worker is fully warmed up, use the
// async handle of the worker's Environment for the same purpose.
bool stopped_ = true;
bool has_ref_ = true;
uint64_t environment_flags_ = EnvironmentFlags::kNoFlags;
// The real Environment of the worker object. It has a lesser
// lifespan than the worker object itself - comes to life
// when the worker thread creates a new Environment, and gets
// destroyed alongwith the worker thread.
Environment* env_ = nullptr;
const SnapshotData* snapshot_data_ = nullptr;
const bool is_internal_;
friend class WorkerThreadData;
};
template <typename Fn>
bool Worker::RequestInterrupt(Fn&& cb) {
Mutex::ScopedLock lock(mutex_);
if (env_ == nullptr) return false;
env_->RequestInterrupt(std::move(cb));
return true;
}
} // namespace worker
} // namespace node
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#endif // SRC_NODE_WORKER_H_