Files
node/lib/internal/worker.js
T
Shelley Vohr e38eede0e5 lib: load fewer builtins when bootstrapping without a snapshot
Contexts that are not deserialized from the built-in snapshot -- worker
threads, and the main context of embedders that create their own
isolate or of `node --no-node-snapshot` -- compile (with the code cache
at best) every builtin the bootstrap touches, so each eagerly required
builtin is startup time (~0.15-0.4 ms apiece). A number of them are only
required eagerly so that they end up in the snapshot, or for features
the bootstrap path never uses.

Load lazily what those paths do not need:

- is_main_thread.js: preload util, url, the ESM loader (translators,
  resolver, module_job/map, source maps, node:module, vm modules, mime,
  data_url, the TypeScript stripper), internal/blob and
  internal/dns/utils only while building a snapshot; they load on first
  use otherwise.
- fs: internal/blob (+ internal/encoding and its tables) is only used
  by fs.openAsBlob().
- internal/url: internal/data_url (+ internal/mime) is only used by the
  Buffer-returning file URL helpers.
- internal/process/execution, the CommonJS loader, esm/translators and
  esm/load: the TypeScript stripper and data: URL helpers are only
  needed for TypeScript sources / data: URLs.
- pre_execution: internal/dns/utils (+ internal/net) is only needed up
  front to validate an explicit --dns-result-order or to register the
  resolver's snapshot serializer; the default order becomes the
  variable's initializer.
- internal/worker: event_loop_utilization and error_serdes are only
  needed once a sub-worker's ELU is read or it reports an error.
- worker_threads: `locks` is defined lazily, like util's lazy exports.

Main-thread startup with the snapshot is unchanged (the same modules
are preloaded into it; the bootstrap-modules test lists are adjusted).
A bare worker compiles 95 -> 83 builtins (cold start -5%); without the
snapshot an empty CommonJS entry point compiles 76 -> 59 builtins and an
empty ES module entry point 76 -> 69.

Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65329
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
Reviewed-By: Aviv Keller <me@aviv.sh>
2026-08-22 17:07:39 +00:00

740 lines
21 KiB
JavaScript

'use strict';
const {
ArrayIsArray,
ArrayPrototypeForEach,
ArrayPrototypeMap,
ArrayPrototypePush,
AtomicsAdd,
Float64Array,
FunctionPrototypeBind,
MathMax,
NumberMAX_SAFE_INTEGER,
ObjectEntries,
Promise,
PromiseResolve,
PromiseWithResolvers,
ReflectApply,
RegExpPrototypeExec,
SafeArrayIterator,
SafeMap,
String,
StringPrototypeTrim,
Symbol,
SymbolAsyncDispose,
SymbolFor,
TypedArrayPrototypeFill,
Uint32Array,
} = primordials;
const EventEmitter = require('events');
const assert = require('internal/assert');
const path = require('path');
const errorCodes = require('internal/errors').codes;
const {
ERR_WORKER_NOT_RUNNING,
ERR_WORKER_PATH,
ERR_WORKER_UNSERIALIZABLE_ERROR,
ERR_WORKER_INVALID_EXEC_ARGV,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_OPERATION_FAILED,
} = errorCodes;
const workerIo = require('internal/worker/io');
const {
drainMessagePort,
receiveMessageOnPort,
MessageChannel,
messageTypes,
kPort,
kIncrementsPortRef,
kWaitingStreams,
kStdioWantsMoreDataCallback,
setupPortReferencing,
ReadableWorkerStdio,
WritableWorkerStdio,
} = workerIo;
const { createMainThreadPort, destroyMainThreadPort } = require('internal/worker/messaging');
const { fileURLToPath, isURL, pathToFileURL } = require('internal/url');
const {
constructSharedArrayBuffer,
kEmptyObject,
} = require('internal/util');
const {
validateArray,
validateString,
validateObject,
validateNumber,
} = require('internal/validators');
const {
throwIfBuildingSnapshot,
} = require('internal/v8/startup_snapshot');
const {
ownsProcessState,
isMainThread,
isInternalThread,
resourceLimits: resourceLimitsRaw,
threadId,
threadName,
Worker: WorkerImpl,
kMaxYoungGenerationSizeMb,
kMaxOldGenerationSizeMb,
kCodeRangeSizeMb,
kStackSizeMb,
kTotalResourceLimitCount,
} = internalBinding('worker');
const kHandle = Symbol('kHandle');
const kPublicPort = Symbol('kPublicPort');
const kDispose = Symbol('kDispose');
const kOnExit = Symbol('kOnExit');
const kOnMessage = Symbol('kOnMessage');
const kOnCouldNotSerializeErr = Symbol('kOnCouldNotSerializeErr');
const kOnErrorMessage = Symbol('kOnErrorMessage');
const kParentSideStdio = Symbol('kParentSideStdio');
const kLoopStartTime = Symbol('kLoopStartTime');
const kIsInternal = Symbol('kIsInternal');
const kIsOnline = Symbol('kIsOnline');
const kWebWorkerData = Symbol('kWebWorkerData');
const SHARE_ENV = SymbolFor('nodejs.worker_threads.SHARE_ENV');
let debug = require('internal/util/debuglog').debuglog('worker', (fn) => {
debug = fn;
});
const dc = require('diagnostics_channel');
const workerThreadsChannel = dc.channel('worker_threads');
let cwdCounter;
let normalizeHeapProfileOptions;
let normalizeCpuProfileOptions;
const environmentData = new SafeMap();
if (isMainThread) {
cwdCounter = new Uint32Array(constructSharedArrayBuffer(4));
const originalChdir = process.chdir;
process.chdir = function(path) {
originalChdir(path);
AtomicsAdd(cwdCounter, 0, 1);
};
}
function setEnvironmentData(key, value) {
if (value === undefined)
environmentData.delete(key);
else
environmentData.set(key, value);
}
function getEnvironmentData(key) {
return environmentData.get(key);
}
function assignEnvironmentData(data) {
if (data === undefined) return;
data.forEach((value, key) => {
environmentData.set(key, value);
});
}
class CPUProfileHandle {
#worker = null;
#id = null;
#promise = null;
constructor(worker, id) {
this.#worker = worker;
this.#id = id;
}
stop() {
if (this.#promise) {
return this.#promise;
}
const stopTaker = this.#worker[kHandle]?.stopCpuProfile(this.#id);
return this.#promise = new Promise((resolve, reject) => {
if (!stopTaker) return reject(new ERR_WORKER_NOT_RUNNING());
stopTaker.ondone = (err, profile) => {
if (err) {
return reject(err);
}
resolve(profile);
};
});
};
async [SymbolAsyncDispose]() {
await this.stop();
}
}
class HeapProfileHandle {
#worker = null;
#promise = null;
constructor(worker) {
this.#worker = worker;
}
stop() {
if (this.#promise) {
return this.#promise;
}
const stopTaker = this.#worker[kHandle]?.stopHeapProfile();
return this.#promise = new Promise((resolve, reject) => {
if (!stopTaker) return reject(new ERR_WORKER_NOT_RUNNING());
stopTaker.ondone = (err, profile) => {
if (err) {
return reject(err);
}
resolve(profile);
};
});
};
async [SymbolAsyncDispose]() {
await this.stop();
}
}
class Worker extends EventEmitter {
constructor(filename, options = kEmptyObject) {
throwIfBuildingSnapshot('Creating workers');
super();
const isInternal = arguments[2] === kIsInternal;
debug(
`[${threadId}] create new worker`,
filename,
options,
`isInternal: ${isInternal}`,
);
if (options.execArgv)
validateArray(options.execArgv, 'options.execArgv');
let argv;
if (options.argv) {
validateArray(options.argv, 'options.argv');
argv = ArrayPrototypeMap(options.argv, String);
}
let url, doEval;
if (isInternal) {
doEval = 'internal';
url = `node:${filename}`;
} else if (options.eval) {
if (typeof filename !== 'string') {
throw new ERR_INVALID_ARG_VALUE(
'options.eval',
options.eval,
'must be false when \'filename\' is not a string',
);
}
url = null;
doEval = 'classic';
} else if (isURL(filename) && filename.protocol === 'data:') {
url = null;
doEval = 'data-url';
filename = `${filename}`;
} else {
doEval = false;
if (isURL(filename)) {
url = filename;
filename = fileURLToPath(filename);
} else if (typeof filename !== 'string') {
throw new ERR_INVALID_ARG_TYPE(
'filename',
['string', 'URL'],
filename,
);
} else if (path.isAbsolute(filename) ||
RegExpPrototypeExec(/^\.\.?[\\/]/, filename) !== null) {
filename = path.resolve(filename);
url = pathToFileURL(filename);
} else {
throw new ERR_WORKER_PATH(filename);
}
}
let env;
if (typeof options.env === 'object' && options.env !== null) {
env = { __proto__: null };
ArrayPrototypeForEach(
ObjectEntries(options.env),
({ 0: key, 1: value }) => { env[key] = `${value}`; },
);
} else if (options.env == null) {
env = process.env;
} else if (options.env !== SHARE_ENV) {
throw new ERR_INVALID_ARG_TYPE(
'options.env',
['object', 'undefined', 'null', 'worker_threads.SHARE_ENV'],
options.env);
}
let name = 'WorkerThread';
if (options.name) {
validateString(options.name, 'options.name');
name = StringPrototypeTrim(options.name);
}
debug('instantiating Worker.', `url: ${url}`, `doEval: ${doEval}`);
// Set up the C++ handle for the worker, as well as some internal wiring.
this[kHandle] = new WorkerImpl(url,
env === process.env ? null : env,
options.execArgv,
parseResourceLimits(options.resourceLimits),
!!(options.trackUnmanagedFds ?? true),
isInternal,
name);
if (this[kHandle].invalidExecArgv) {
throw new ERR_WORKER_INVALID_EXEC_ARGV(this[kHandle].invalidExecArgv);
}
if (this[kHandle].invalidNodeOptions) {
throw new ERR_WORKER_INVALID_EXEC_ARGV(
this[kHandle].invalidNodeOptions, 'invalid NODE_OPTIONS env variable');
}
this[kHandle].onexit = (code, customErr, customErrReason) => {
this[kOnExit](code, customErr, customErrReason);
};
this[kPort] = this[kHandle].messagePort;
this[kPort].on('message', (data) => this[kOnMessage](data));
this[kPort].start();
this[kPort].unref();
this[kPort][kWaitingStreams] = 0;
debug(`[${threadId}] created Worker with ID ${this.threadId}`);
let stdin = null;
if (options.stdin)
stdin = new WritableWorkerStdio(this[kPort], 'stdin');
const stdout = new ReadableWorkerStdio(this[kPort], 'stdout');
if (!options.stdout) {
stdout[kIncrementsPortRef] = false;
pipeWithoutWarning(stdout, process.stdout);
}
const stderr = new ReadableWorkerStdio(this[kPort], 'stderr');
if (!options.stderr) {
stderr[kIncrementsPortRef] = false;
pipeWithoutWarning(stderr, process.stderr);
}
this[kParentSideStdio] = { stdin, stdout, stderr };
const mainThreadPortToWorker = createMainThreadPort(this.threadId);
const {
port1: publicPortToParent,
port2: publicPortToWorker,
} = new MessageChannel();
const transferList = [mainThreadPortToWorker, publicPortToWorker];
// If transferList is provided.
if (options.transferList)
ArrayPrototypePush(transferList,
...new SafeArrayIterator(options.transferList));
this[kPublicPort] = publicPortToParent;
ArrayPrototypeForEach(['message', 'messageerror'], (event) => {
this[kPublicPort].on(event, (message) => {
// Extract watch messages first if needed and relay events from worker thread to watcher
if (
event === 'message' &&
process.env.WATCH_REPORT_DEPENDENCIES &&
process.send
) {
const { isMainThread } = internalBinding('worker');
if (isMainThread) {
if (ArrayIsArray(message?.['watch:require'])) {
process.send({ 'watch:require': message['watch:require'] });
}
if (ArrayIsArray(message?.['watch:import'])) {
process.send({ 'watch:import': message['watch:import'] });
}
}
}
this.emit(event, message);
});
});
setupPortReferencing(this[kPublicPort], this, 'message');
this[kPort].postMessage({
argv,
type: messageTypes.LOAD_SCRIPT,
filename,
doEval,
isInternal,
cwdCounter: cwdCounter || workerIo.sharedCwdCounter,
workerData: options.workerData,
webWorkerData: options[kWebWorkerData],
environmentData,
hasStdin: !!options.stdin,
publicPort: publicPortToWorker,
mainThreadPort: mainThreadPortToWorker,
}, transferList);
// Use this to cache the Worker's loopStart value once available.
this[kLoopStartTime] = -1;
this[kIsOnline] = false;
this.performance = {
eventLoopUtilization: FunctionPrototypeBind(eventLoopUtilization, this),
};
// Actually start the new thread now that everything is in place.
this[kHandle].startThread();
process.nextTick(() => process.emit('worker', this));
if (workerThreadsChannel.hasSubscribers) {
workerThreadsChannel.publish({
worker: this,
});
}
}
[kOnExit](code, customErr, customErrReason) {
debug(`[${threadId}] hears end event for Worker ${this.threadId}`);
drainMessagePort(this[kPublicPort]);
drainMessagePort(this[kPort]);
destroyMainThreadPort(this.threadId);
this.removeAllListeners('message');
this.removeAllListeners('messageerrors');
this[kPublicPort].unref();
this[kPort].unref();
this[kDispose]();
if (customErr) {
debug(`[${threadId}] failing with custom error ${customErr} \
and with reason ${customErrReason}`);
this.emit('error', new errorCodes[customErr](customErrReason));
}
this.emit('exit', code);
this.removeAllListeners();
}
[kOnCouldNotSerializeErr]() {
this.emit('error', new ERR_WORKER_UNSERIALIZABLE_ERROR());
}
[kOnErrorMessage](serialized) {
// This is what is called for uncaught exceptions.
const { deserializeError } = require('internal/error_serdes');
const error = deserializeError(serialized);
this.emit('error', error);
}
[kOnMessage](message) {
switch (message.type) {
case messageTypes.UP_AND_RUNNING:
this[kIsOnline] = true;
return this.emit('online');
case messageTypes.COULD_NOT_SERIALIZE_ERROR:
return this[kOnCouldNotSerializeErr]();
case messageTypes.ERROR_MESSAGE:
return this[kOnErrorMessage](message.error);
case messageTypes.STDIO_PAYLOAD:
{
const { stream, chunks } = message;
const readable = this[kParentSideStdio][stream];
// This is a hot path, use a for(;;) loop
for (let i = 0; i < chunks.length; i++) {
const { chunk, encoding } = chunks[i];
readable.push(chunk, encoding);
}
return;
}
case messageTypes.STDIO_WANTS_MORE_DATA:
{
const { stream } = message;
return this[kParentSideStdio][stream][kStdioWantsMoreDataCallback]();
}
}
assert.fail(`Unknown worker message type ${message.type}`);
}
[kDispose]() {
this[kHandle].onexit = null;
this[kHandle] = null;
this[kPort] = null;
this[kPublicPort] = null;
const { stdout, stderr } = this[kParentSideStdio];
if (!stdout.readableEnded) {
debug(`[${threadId}] explicitly closes stdout for ${this.threadId}`);
stdout.push(null);
}
if (!stderr.readableEnded) {
debug(`[${threadId}] explicitly closes stderr for ${this.threadId}`);
stderr.push(null);
}
}
postMessage(...args) {
if (this[kPublicPort] === null) return;
ReflectApply(this[kPublicPort].postMessage, this[kPublicPort], args);
}
terminate() {
debug(`[${threadId}] terminates Worker with ID ${this.threadId}`);
this.ref();
if (this[kHandle] === null) return PromiseResolve();
this[kHandle].stopThread();
// Do not use events.once() here, because the 'exit' event will always be
// emitted regardless of any errors, and the point is to only resolve
// once the thread has actually stopped.
const { promise, resolve } = PromiseWithResolvers();
this.once('exit', resolve);
return promise;
}
async [SymbolAsyncDispose]() {
await this.terminate();
}
ref() {
if (this[kHandle] === null) return;
this[kHandle].ref();
this[kPublicPort].ref();
}
unref() {
if (this[kHandle] === null) return;
this[kHandle].unref();
this[kPublicPort].unref();
}
get threadId() {
if (this[kHandle] === null) return -1;
return this[kHandle].threadId;
}
get threadName() {
if (this[kHandle] === null) return null;
return this[kHandle].threadName;
}
get stdin() {
return this[kParentSideStdio].stdin;
}
get stdout() {
return this[kParentSideStdio].stdout;
}
get stderr() {
return this[kParentSideStdio].stderr;
}
get resourceLimits() {
if (this[kHandle] === null) return {};
return makeResourceLimits(this[kHandle].getResourceLimits());
}
getHeapSnapshot(options) {
const {
HeapSnapshotStream,
getHeapSnapshotOptions,
} = require('internal/heap_utils');
const optionsArray = getHeapSnapshotOptions(options);
const heapSnapshotTaker = this[kHandle]?.takeHeapSnapshot(optionsArray);
return new Promise((resolve, reject) => {
if (!heapSnapshotTaker) return reject(new ERR_WORKER_NOT_RUNNING());
heapSnapshotTaker.ondone = (handle) => {
resolve(new HeapSnapshotStream(handle));
};
});
}
getHeapStatistics() {
const taker = this[kHandle]?.getHeapStatistics();
return new Promise((resolve, reject) => {
if (!taker) return reject(new ERR_WORKER_NOT_RUNNING());
taker.ondone = (handle) => {
resolve(handle);
};
});
}
cpuUsage(prev) {
if (prev) {
validateObject(prev, 'prev');
validateNumber(prev.user, 'prev.user', 0, NumberMAX_SAFE_INTEGER);
validateNumber(prev.system, 'prev.system', 0, NumberMAX_SAFE_INTEGER);
}
if (process.platform === 'sunos') {
throw new ERR_OPERATION_FAILED('worker.cpuUsage() is not available on SunOS');
}
const taker = this[kHandle]?.cpuUsage();
return new Promise((resolve, reject) => {
if (!taker) return reject(new ERR_WORKER_NOT_RUNNING());
taker.ondone = (err, current) => {
if (err !== null) {
return reject(err);
}
if (prev) {
resolve({
user: current.user - prev.user,
system: current.system - prev.system,
});
} else {
resolve({
user: current.user,
system: current.system,
});
}
};
});
}
startCpuProfile(options) {
normalizeCpuProfileOptions ??=
require('internal/v8/cpu_profiler').normalizeCpuProfileOptions;
const {
samplingIntervalMicros,
maxSamples,
} = normalizeCpuProfileOptions(options);
const startTaker = this[kHandle]?.startCpuProfile(
samplingIntervalMicros,
maxSamples);
return new Promise((resolve, reject) => {
if (!startTaker) return reject(new ERR_WORKER_NOT_RUNNING());
startTaker.ondone = (err, id) => {
if (err) {
return reject(err);
}
resolve(new CPUProfileHandle(this, id));
};
});
}
/**
* @param {object} [options]
* @param {number} [options.sampleInterval]
* @param {number} [options.stackDepth]
* @param {boolean} [options.forceGC]
* @param {boolean} [options.includeObjectsCollectedByMajorGC]
* @param {boolean} [options.includeObjectsCollectedByMinorGC]
* @returns {Promise}
*/
startHeapProfile(options) {
normalizeHeapProfileOptions ??=
require('internal/v8/heap_profile').normalizeHeapProfileOptions;
const { sampleInterval, stackDepth, flags } =
normalizeHeapProfileOptions(options);
const startTaker = this[kHandle]?.startHeapProfile(
sampleInterval, stackDepth, flags);
return new Promise((resolve, reject) => {
if (!startTaker) return reject(new ERR_WORKER_NOT_RUNNING());
startTaker.ondone = (err) => {
if (err) {
return reject(err);
}
resolve(new HeapProfileHandle(this));
};
});
}
}
/**
* A worker which has an internal module for entry point (e.g. internal/module/esm/worker).
* Internal workers bypass the permission model.
*/
class InternalWorker extends Worker {
constructor(filename, options) {
super(filename, options, kIsInternal);
}
receiveMessageSync() {
return receiveMessageOnPort(this[kPublicPort]);
}
}
function pipeWithoutWarning(source, dest) {
const sourceMaxListeners = source._maxListeners;
const destMaxListeners = dest._maxListeners;
source.setMaxListeners(Infinity);
dest.setMaxListeners(Infinity);
source.pipe(dest);
source._maxListeners = sourceMaxListeners;
dest._maxListeners = destMaxListeners;
}
const resourceLimitsArray = new Float64Array(kTotalResourceLimitCount);
function parseResourceLimits(obj) {
const ret = resourceLimitsArray;
TypedArrayPrototypeFill(ret, -1);
if (typeof obj !== 'object' || obj === null) return ret;
if (typeof obj.maxOldGenerationSizeMb === 'number')
ret[kMaxOldGenerationSizeMb] = MathMax(obj.maxOldGenerationSizeMb, 2);
if (typeof obj.maxYoungGenerationSizeMb === 'number')
ret[kMaxYoungGenerationSizeMb] = obj.maxYoungGenerationSizeMb;
if (typeof obj.codeRangeSizeMb === 'number')
ret[kCodeRangeSizeMb] = obj.codeRangeSizeMb;
if (typeof obj.stackSizeMb === 'number')
ret[kStackSizeMb] = obj.stackSizeMb;
return ret;
}
function makeResourceLimits(float64arr) {
return {
maxYoungGenerationSizeMb: float64arr[kMaxYoungGenerationSizeMb],
maxOldGenerationSizeMb: float64arr[kMaxOldGenerationSizeMb],
codeRangeSizeMb: float64arr[kCodeRangeSizeMb],
stackSizeMb: float64arr[kStackSizeMb],
};
}
function eventLoopUtilization(util1, util2) {
const { internalEventLoopUtilization } = require('internal/perf/event_loop_utilization');
// TODO(trevnorris): Works to solve the thread-safe read/write issue of
// loopTime, but has the drawback that it can't be set until the event loop
// has had a chance to turn. So it will be impossible to read the ELU of
// a worker thread immediately after it's been created.
if (!this[kIsOnline] || !this[kHandle]) {
return { idle: 0, active: 0, utilization: 0 };
}
// Cache loopStart, since it's only written to once.
if (this[kLoopStartTime] === -1) {
this[kLoopStartTime] = this[kHandle].loopStartTime();
if (this[kLoopStartTime] === -1)
return { idle: 0, active: 0, utilization: 0 };
}
return internalEventLoopUtilization(
this[kLoopStartTime],
this[kHandle].loopIdleTime(),
util1,
util2,
);
}
module.exports = {
ownsProcessState,
kIsOnline,
isMainThread,
isInternalThread,
SHARE_ENV,
resourceLimits:
!isMainThread ? makeResourceLimits(resourceLimitsRaw) : {},
setEnvironmentData,
getEnvironmentData,
assignEnvironmentData,
threadId,
threadName,
InternalWorker,
Worker,
kPublicPort,
kWebWorkerData,
};