@giro3d/giro3d
Version:
A JS/WebGL framework for 3D geospatial data visualization
169 lines (160 loc) • 4.63 kB
JavaScript
/*
* Copyright (c) 2015-2018, IGN France.
* Copyright (c) 2018-2026, Giro3D team.
* SPDX-License-Identifier: MIT
*/
import OperationCounter from '../core/OperationCounter';
/**
* A message to send to the worker.
*/
export class WorkerError extends Error {
constructor(messageId, message) {
super(message);
this.messageId = messageId;
}
}
export function createErrorResponse(requestId, error) {
return {
requestId,
error: error instanceof Error ? error.message : 'unknown error'
};
}
/**
* A simple Web Worker pool that can select idle workers to perform work.
*
* Additionally, idle workers are terminated after a delay to free resources.
*
* @typeParam TMessageType - The type of the messages supported by the workers.
* @typeParam TMessageMap - The map between request and response messages.
*/
export default class WorkerPool {
_workers = new Set();
_disposed = false;
_messageId = 0;
get loading() {
let result = false;
this._workers.forEach(w => {
if (w.counter.loading) {
result = true;
}
});
return result;
}
get progress() {
let sum = 0;
this._workers.forEach(w => {
sum += w.counter.progress;
});
return sum / this._workers.size;
}
constructor(options) {
this._createWorker = options.createWorker;
this._terminationDelay = options.terminationDelay ?? 10000;
if (options.concurrency != null) {
this._concurrency = options.concurrency;
} else {
this._concurrency = WorkerPool.defaultConcurrency;
}
}
static get defaultConcurrency() {
if (typeof window !== 'undefined' && window.navigator != null) {
return window.navigator.hardwareConcurrency;
} else {
return 1;
}
}
/**
* Sends a message to the first available worker, then waits for a response matching this
* message's id, then returns this response, or throw an error if an error response is received.
*/
queue(type, payload, transfer) {
if (this._disposed) {
throw new Error('this object is disposed');
}
const wrapper = this.getWorker();
wrapper.counter.increment();
if (wrapper.idleTimeout) {
clearTimeout(wrapper.idleTimeout);
wrapper.idleTimeout = null;
}
const worker = wrapper.worker;
const message = {
id: this._messageId++,
payload,
type: type
};
return new Promise((resolve, reject) => {
// eslint-disable-next-line prefer-const
let stopListening;
const onResponse = event => {
const response = event.data;
if (response.requestId === message.id) {
stopListening();
if ('error' in response) {
reject(new Error(response.error));
} else {
resolve(response.payload);
}
}
};
stopListening = () => {
wrapper.counter.decrement();
// The worker is idle, start the termination timeout. It will be cancelled
// if the worker becomes busy again before the timeout finishes.
if (!wrapper.counter.loading) {
this.startWorkerTerminationTimeout(wrapper);
}
worker.removeEventListener('message', onResponse);
};
worker.addEventListener('message', onResponse);
worker.postMessage(message, transfer ?? []);
});
}
/**
* Terminates all workers.
*/
dispose() {
if (this._disposed) {
return;
}
this._disposed = true;
this._workers.forEach(w => w.worker.terminate());
}
startWorkerTerminationTimeout(wrapper) {
const worker = wrapper.worker;
wrapper.idleTimeout = setTimeout(() => {
worker.terminate();
this._workers.delete(wrapper);
}, this._terminationDelay);
}
createWorker() {
const worker = this._createWorker();
const wrapper = {
counter: new OperationCounter(),
worker,
idleTimeout: null
};
this._workers.add(wrapper);
return wrapper;
}
getWorker() {
// Create the first worker.
if (this._workers.size === 0) {
return this.createWorker();
}
const workers = [...this._workers];
// Attempt to return the first idle worker.
const idle = workers.find(w => !w.counter.loading);
if (idle) {
return idle;
}
// No idle worker, create one if possible.
if (this._workers.size < this._concurrency) {
return this.createWorker();
}
// All workers are busy and impossible to create one, just return the least busy.
workers.sort((a, b) => a.counter.operations - b.counter.operations);
const leastBusy = workers[0];
return leastBusy;
}
}