UNPKG

@here/harp-mapview

Version:

Functionality needed to render a map.

561 lines 22.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ConcurrentWorkerSet = exports.DEFAULT_WORKER_INITIALIZATION_TIMEOUT = exports.isLoggingMessage = void 0; /* * Copyright (C) 2019-2021 HERE Europe B.V. * Licensed under Apache 2.0, see full license in LICENSE * SPDX-License-Identifier: Apache-2.0 */ const harp_datasource_protocol_1 = require("@here/harp-datasource-protocol"); const harp_utils_1 = require("@here/harp-utils"); const THREE = require("three"); const WorkerLoader_1 = require("./workers/WorkerLoader"); const logger = harp_utils_1.LoggerManager.instance.create("ConcurrentWorkerSet"); function isLoggingMessage(message) { return message && typeof message.level === "number" && message.type === harp_utils_1.WORKERCHANNEL_MSG_TYPE; } exports.isLoggingMessage = isLoggingMessage; /** * The default number of Web Workers to use if `navigator.hardwareConcurrency` is unavailable. */ const DEFAULT_WORKER_COUNT = 2; /** * The default timeout for first message from worker. * * @see {@link WorkerLoader.startWorker} */ exports.DEFAULT_WORKER_INITIALIZATION_TIMEOUT = 10000; /** * A set of concurrent Web Workers. Acts as a Communication Peer for [[WorkerService]] instances * running in Web Workers. * * Starts and manages a certain number of web workers and provides a means to communicate * with them using various communication schemes, such as: * - [[addEventListener]] : receive a unidirectional messages * - [[broadcastMessage]] : send unidirectional broadcast message * - [[invokeRequest]] : send a request that waits for a response, with load balancing * - [[postMessage]] : send a unidirectional message, with load balancing * * The request queue holds all requests before they are stuffed into the event queue, allows for * easy (and early) cancelling of requests. The workers now only get a single new RequestMessage * when they return their previous result, or if they are idle. When they are idle, they are stored * in m_availableWorkers. */ class ConcurrentWorkerSet { /** * Creates a new `ConcurrentWorkerSet`. * * Creates as many Web Workers as specified in `options.workerCount`, from the script provided * in `options.scriptUrl`. If `options.workerCount` is not specified, the value specified in * `navigator.hardwareConcurrency` is used instead. * * The worker set is implicitly started when constructed. */ constructor(m_options) { this.m_options = m_options; this.m_workerChannelLogger = harp_utils_1.LoggerManager.instance.create("WorkerChannel"); this.m_eventListeners = new Map(); this.m_workers = new Array(); // List of idle workers that can be given the next job. It is using a LIFO scheme to reduce // memory consumption in idle workers. this.m_availableWorkers = new Array(); this.m_workerPromises = new Array(); this.m_readyPromises = new Map(); this.m_requests = new Map(); this.m_workerRequestQueue = []; this.m_nextMessageId = 0; this.m_stopped = true; this.m_referenceCount = 0; /** * Handles messages received from workers. This method is protected so that the message * reception can be simulated through an extended class, to avoid relying on real workers. * * @param workerId - The workerId of the web worker. * @param event - The event to dispatch. */ this.onWorkerMessage = (workerId, event) => { if (harp_datasource_protocol_1.WorkerServiceProtocol.isResponseMessage(event.data)) { const response = event.data; if (response.messageId === null) { logger.error(`[${this.m_options.scriptUrl}]: Bad ResponseMessage: no messageId`); return; } const entry = this.m_requests.get(response.messageId); if (entry === undefined) { logger.error(`[${this.m_options.scriptUrl}]: Bad ResponseMessage: invalid messageId`); return; } if (workerId >= 0 && workerId < this.m_workers.length) { const worker = this.m_workers[workerId]; this.m_availableWorkers.push(worker); // Check if any new work has been put into the queue. this.checkWorkerRequestQueue(); } else { logger.error(`[${this.m_options.scriptUrl}]: onWorkerMessage: invalid workerId`); } if (response.errorMessage !== undefined) { const error = new Error(response.errorMessage); if (response.errorStack !== undefined) { error.stack = response.errorStack; } entry.resolver(error); } else { entry.resolver(undefined, response.response); } } else if (harp_datasource_protocol_1.WorkerServiceProtocol.isInitializedMessage(event.data)) { const readyPromise = this.getReadyPromise(event.data.service); if (++readyPromise.count === this.m_workerPromises.length) { readyPromise.resolve(); } } else if (isLoggingMessage(event.data)) { switch (event.data.level) { case harp_utils_1.LogLevel.Trace: this.m_workerChannelLogger.trace(...event.data.message); break; case harp_utils_1.LogLevel.Debug: this.m_workerChannelLogger.debug(...event.data.message); break; case harp_utils_1.LogLevel.Log: this.m_workerChannelLogger.log(...event.data.message); break; case harp_utils_1.LogLevel.Info: this.m_workerChannelLogger.info(...event.data.message); break; case harp_utils_1.LogLevel.Warn: this.m_workerChannelLogger.warn(...event.data.message); break; case harp_utils_1.LogLevel.Error: this.m_workerChannelLogger.error(...event.data.message); break; } } else { this.eventHandler(event); } }; this.start(); } /** * Adds an external reference and increments the internal reference counter by one. * * To implement a reference-count based automatic resource cleanup, use this function with * [[removeReference]]. */ addReference() { this.m_referenceCount += 1; if (this.m_referenceCount === 1 && this.m_stopped) { this.start(); } } /** * Decrements the internal reference counter by 1. * * When the internal reference counter reaches 0, this function calls [[dispose]] to clear the * resources. * * Use with [[addReference]] to implement reference-count based automatic resource cleanup. */ removeReference() { this.m_referenceCount -= 1; if (this.m_referenceCount === 0) { this.destroy(); } } /** * Starts workers. * * Use to start workers already stopped by [[stop]] or [[destroy]] calls. * * Note: The worker set is implicitly started on construction - no need to call [[start]] on * fresh instance. * * @param options - optional, new worker set options */ start(options) { if (options !== undefined) { this.m_options = options; } if (!this.m_stopped) { throw new Error("ConcurrentWorker set already started"); } this.m_workerCount = harp_utils_1.getOptionValue(this.m_options.workerCount, typeof navigator !== "undefined" && navigator.hardwareConcurrency !== undefined ? // We need to have at least one worker THREE.MathUtils.clamp(navigator.hardwareConcurrency - 1, 1, 2) : undefined, DEFAULT_WORKER_COUNT); // Initialize the workers. The workers now have an ID to identify specific workers and // handle their busy state. const timeout = harp_utils_1.getOptionValue(this.m_options.workerConnectionTimeout, exports.DEFAULT_WORKER_INITIALIZATION_TIMEOUT); for (let workerId = 0; workerId < this.m_workerCount; ++workerId) { const workerPromise = WorkerLoader_1.WorkerLoader.startWorker(this.m_options.scriptUrl, timeout).then(worker => { const listener = (evt) => { this.onWorkerMessage(workerId, evt); }; worker.addEventListener("message", listener); this.m_workers.push(worker); this.m_availableWorkers.push(worker); return { worker, listener }; }); this.m_workerPromises.push(workerPromise); } this.m_stopped = false; } /** * The number of workers started for this worker set. The value is `undefined` until the workers * have been created. */ get workerCount() { return this.m_workerCount; } /** * Stops workers. * * Waits for all pending requests to be finished and stops all workers. * * Use [[start]] to start this worker again. * * @returns `Promise` that resolves when all workers are destroyed. */ async stop() { this.m_stopped = true; await this.waitForAllResponses().then(() => { this.terminateWorkers(); }); } /** * Destroys all workers immediately. * * Resolves all pending request promises with a `worker destroyed` error. * * Use [[start]] to start this worker again. */ destroy() { this.m_stopped = true; // respond with all pending request this.m_requests.forEach(entry => { entry.resolver(new Error("worker destroyed")); }); this.m_requests.clear(); this.m_workerRequestQueue = []; this.terminateWorkers(); // clean other stuff this.m_eventListeners.clear(); } /** * Is `true` if the workers have been terminated. */ get terminated() { return this.m_workers.length === 0; } /** * Waits for `service` to be initialized in all workers. * * Each service that starts in a worker sends an [[isInitializedMessage]] to confirm that * it has started successfully. This method resolves when all workers in a set have * `service` initialized. * * Promise is rejected if any of worker fails to start. * * @param serviceId - The service identifier. */ async connect(serviceId) { this.ensureStarted(); await Promise.all(this.m_workerPromises); return await this.getReadyPromise(serviceId).promise; } /** * Registers an event listener for events that originated in a web worker, for a given * `serviceId`. You can only set one event listener per `serviceId`. * * @param serviceId - The service to listen to. * @param callback - The callback to invoke for matching events. */ addEventListener(serviceId, callback) { this.m_eventListeners.set(serviceId, callback); } /** * Removes a previously set event listener for the given `serviceId`. * * @param serviceId - The service from which to remove the event listeners. */ removeEventListener(serviceId) { this.m_eventListeners.delete(serviceId); } /** * Invokes a request that expects a response from a random worker. * * Sends [[RequestMessage]] and resolves when a matching [[ResponseMessage]] is received from * workers. Use this function when interfacing with "RPC-like" calls to services. * * @param serviceId - The name of service, as registered with the [[WorkerClient]] instance. * @param request - The request to process. * @param transferList - An optional array of `ArrayBuffer`s to transfer to the worker context. * @param requestController - An optional [[RequestController]] to store state of cancelling. * * @returns A `Promise` that resolves with a response from the service. */ invokeRequest(serviceId, request, transferList, requestController) { this.ensureStarted(); const messageId = this.m_nextMessageId++; let resolver; const promise = new Promise((resolve, reject) => { resolver = (error, response) => { this.m_requests.delete(messageId); if (error !== undefined) { reject(error); } else { resolve(response); } }; }); this.m_requests.set(messageId, { promise, resolver: resolver }); const message = { service: serviceId, type: harp_datasource_protocol_1.WorkerServiceProtocol.ServiceMessageName.Request, messageId, request }; this.postRequestMessage(message, transferList, requestController); return promise; } /** * Invokes a request that expects responses from all workers. * * Send [[RequestMessage]] to all workers and resolves when all workers have sent a matching * [[ResponseMessage]]. Use this function to wait on request that need to happen on all workers * before proceeding (like synchronous worker service creation). * * @param serviceId - The name of service, as registered with the [[WorkerClient]] instance. * @param request - The request to process. * @param transferList - An optional array of `ArrayBuffer`s to transfer to the worker context. * * @returns Array of `Promise`s that resolves with a response from each worker (unspecified * order). */ broadcastRequest(serviceId, request, transferList) { const promises = []; for (const worker of this.m_workers) { const messageId = this.m_nextMessageId++; let resolver; const promise = new Promise((resolve, reject) => { resolver = (error, response) => { this.m_requests.delete(messageId); if (error !== undefined) { reject(error); } else { resolve(response); } }; }); promises.push(promise); this.m_requests.set(messageId, { promise, resolver: resolver }); const message = { service: serviceId, type: harp_datasource_protocol_1.WorkerServiceProtocol.ServiceMessageName.Request, messageId, request }; if (transferList !== undefined) { worker.postMessage(message, transferList); } else { worker.postMessage(message); } } return Promise.all(promises); } /** * Posts a message to all workers. * * @param message - The message to send. * @param buffers - Optional buffers to transfer to the workers. */ broadcastMessage(message, buffers) { this.ensureStarted(); if (buffers !== undefined) { this.m_workers.forEach(worker => worker.postMessage(message, buffers)); } else { this.m_workers.forEach(worker => worker.postMessage(message)); } } /** * The size of the request queue for debugging and profiling. */ get requestQueueSize() { return this.m_workerRequestQueue.length; } /** * The number of workers for debugging and profiling. */ get numWorkers() { return this.m_workers.length; } /** * The number of workers for debugging and profiling. */ get numIdleWorkers() { return this.m_availableWorkers.length; } /** * Subclasses must call this function when a worker emits an event. * * @param event - The event to dispatch. */ eventHandler(event) { if (typeof event.data.type !== "string") { return; // not an event generated by us, ignore. } this.dispatchEvent(event.data.type, event); } /** * Posts a [[WorkerServiceProtocol.RequestMessage]] to an available worker. If no worker is * available, the request is put into a queue. * * @param message - The message to send. * @param buffers - Optional buffers to transfer to the worker. * @param requestController - An optional [[RequestController]] to store state of cancelling. */ postRequestMessage(message, buffers, requestController) { this.ensureStarted(); if (this.m_workers.length === 0) { throw new Error("ConcurrentWorkerSet#postMessage: no workers started"); } // Check if the requestController has received the abort signal, in which case the request // is ignored. if (requestController !== undefined && requestController.signal.aborted) { const entry = this.m_requests.get(message.messageId); if (entry === undefined) { logger.error(`[${this.m_options.scriptUrl}]: Bad RequestMessage: invalid messageId`); return; } const err = new Error("Aborted"); err.name = "AbortError"; entry.resolver(err, undefined); return; } if (this.m_availableWorkers.length > 0) { const worker = this.m_availableWorkers.pop(); if (buffers !== undefined) { worker.postMessage(message, buffers); } else { worker.postMessage(message); } } else { // We need a priority to keep sorting stable, so we have to add a RequestController. if (requestController === undefined) { requestController = new harp_datasource_protocol_1.RequestController(0); } if (requestController.priority === 0) { // If the requests do not get a priority, they should keep their sorting order. requestController.priority = -this.m_nextMessageId; } this.m_workerRequestQueue.unshift({ message, buffers, requestController }); } } ensureStarted() { if (this.m_stopped) { throw new Error("ConcurrentWorkerSet stopped"); } } async waitForAllResponses() { const promises = new Array(); this.m_requests.forEach(entry => { promises.push(entry.promise); }); await Promise.all(promises); } dispatchEvent(id, message) { const callback = this.m_eventListeners.get(id); if (callback === undefined) { return; } // unknown event, ignore. callback(message); } terminateWorkers() { // terminate all workers this.m_workerPromises.forEach(workerPromise => { workerPromise.then(workerEntry => { if (workerEntry === undefined) { return; } workerEntry.worker.removeEventListener("message", workerEntry.listener); workerEntry.worker.terminate(); }); }); this.m_workers = []; this.m_workerPromises = []; this.m_availableWorkers = []; this.m_readyPromises.clear(); } getReadyPromise(id) { const readyPromise = this.m_readyPromises.get(id); if (readyPromise !== undefined) { return readyPromise; } const newPromise = { count: 0, promise: undefined, resolve: () => { /* placeholder */ }, reject: (error) => { newPromise.error = error; }, error: undefined }; newPromise.promise = new Promise((resolve, reject) => { const that = newPromise; if (that.error !== undefined) { reject(that.error); } else if (that.count === this.m_workerPromises.length) { resolve(); } that.resolve = resolve; that.reject = reject; }); this.m_readyPromises.set(id, newPromise); return newPromise; } /** * Check the worker request queue, if there are any queued up decoding jobs and idle workers, * they will be executed with postRequestMessage. The requests in the queue are sorted before * the request with the highest priority is selected for processing. */ checkWorkerRequestQueue() { if (this.m_workerRequestQueue.length === 0 || this.m_availableWorkers.length === 0) { return; } this.m_workerRequestQueue.sort((a, b) => { return a.requestController.priority - b.requestController.priority; }); // Get the request with the highest priority and send it (again). while (this.m_availableWorkers.length > 0 && this.m_workerRequestQueue.length > 0) { const request = this.m_workerRequestQueue.pop(); this.postRequestMessage(request.message, request.buffers, request.requestController); } } } exports.ConcurrentWorkerSet = ConcurrentWorkerSet; //# sourceMappingURL=ConcurrentWorkerSet.js.map