UNPKG

nstdlib-nightly

Version:

Node.js standard library converted to runtime-agnostic ES modules.

537 lines (473 loc) 15.8 kB
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/internal/worker.js import EventEmitter from "nstdlib/lib/events"; import * as assert from "nstdlib/lib/internal/assert"; import * as path from "nstdlib/lib/path"; import { internalEventLoopUtilization } from "nstdlib/lib/internal/perf/event_loop_utilization"; import { codes as errorCodes } from "nstdlib/lib/internal/errors"; import * as workerIo from "nstdlib/lib/internal/worker/io"; import { createMainThreadPort, destroyMainThreadPort, } from "nstdlib/lib/internal/worker/messaging"; import { deserializeError } from "nstdlib/lib/internal/error_serdes"; import { fileURLToPath, isURL, pathToFileURL } from "nstdlib/lib/internal/url"; import { kEmptyObject } from "nstdlib/lib/internal/util"; import { validateArray, validateString } from "nstdlib/lib/internal/validators"; import { throwIfBuildingSnapshot } from "nstdlib/lib/internal/v8/startup_snapshot"; import { ownsProcessState, isMainThread, resourceLimits as resourceLimitsRaw, threadId, Worker as WorkerImpl, kMaxYoungGenerationSizeMb, kMaxOldGenerationSizeMb, kCodeRangeSizeMb, kStackSizeMb, kTotalResourceLimitCount, } from "nstdlib/stub/binding/worker"; import * as dc from "nstdlib/lib/diagnostics_channel"; import * as __hoisted_internal_heap_utils__ from "nstdlib/lib/internal/heap_utils"; 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, } = errorCodes; const { drainMessagePort, receiveMessageOnPort, MessageChannel, messageTypes, kPort, kIncrementsPortRef, kWaitingStreams, kStdioWantsMoreDataCallback, setupPortReferencing, ReadableWorkerStdio, WritableWorkerStdio, } = workerIo; 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 SHARE_ENV = Symbol.for("nodejs.worker_threads.SHARE_ENV"); const workerThreadsChannel = dc.channel("worker_threads"); let cwdCounter; const environmentData = new Map(); // SharedArrayBuffers can be disabled with --enable-sharedarraybuffer-per-context. if (isMainThread && SharedArrayBuffer !== undefined) { cwdCounter = new Uint32Array(new SharedArrayBuffer(4)); const originalChdir = process.chdir; process.chdir = function (path) { AtomicsAdd(cwdCounter, 0, 1); originalChdir(path); }; } 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 Worker extends EventEmitter { constructor(filename, options = kEmptyObject) { throwIfBuildingSnapshot("Creating workers"); super(); const isInternal = arguments[2] === kIsInternal; { /* debug */ } if (options.execArgv) validateArray(options.execArgv, "options.execArgv"); let argv; if (options.argv) { validateArray(options.argv, "options.argv"); argv = Array.prototype.map.call(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 = "module"; filename = `import ${JSONStringify(`${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) || RegExp.prototype.exec.call(/^\.\.?[\\/]/, 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 }; Array.prototype.forEach.call( Object.entries(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 = ""; if (options.name) { validateString(options.name, "options.name"); name = String.prototype.trim.call(options.name); } { /* debug */ } // 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 */ } 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) Array.prototype.push.call( transferList, ...new (Array.prototype[Symbol.iterator]())(options.transferList), ); this[kPublicPort] = publicPortToParent; Array.prototype.forEach.call(["message", "messageerror"], (event) => { this[kPublicPort].on(event, (message) => 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, 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: Function.prototype.bind.call( 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 */ } 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 */ } 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 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]; Array.prototype.forEach.call(chunks, ({ chunk, encoding }) => { 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 */ } stdout.push(null); } if (!stderr.readableEnded) { { /* debug */ } stderr.push(null); } } postMessage(...args) { if (this[kPublicPort] === null) return; ReflectApply(this[kPublicPort].postMessage, this[kPublicPort], args); } terminate(callback) { { /* debug */ } this.ref(); if (typeof callback === "function") { process.emitWarning( "Passing a callback to worker.terminate() is deprecated. " + "It returns a Promise instead.", "DeprecationWarning", "DEP0132", ); if (this[kHandle] === null) return Promise.resolve(); this.once("exit", (exitCode) => callback(null, exitCode)); } if (this[kHandle] === null) return Promise.resolve(); 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. return new Promise((resolve) => { this.once("exit", resolve); }); } 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 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 } = __hoisted_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)); }; }); } } /** * 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] = Math.max(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) { // 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, ); } export { ownsProcessState }; export { kIsOnline }; export { isMainThread }; export { SHARE_ENV }; const _export_resourceLimits_ = !isMainThread ? makeResourceLimits(resourceLimitsRaw) : {}; export { _export_resourceLimits_ as resourceLimits }; export { setEnvironmentData }; export { getEnvironmentData }; export { assignEnvironmentData }; export { threadId }; export { InternalWorker }; export { Worker };