diff --git a/src/loading/binary-loader.ts b/src/loading/binary-loader.ts index c7d6564b..e9a626c9 100644 --- a/src/loading/binary-loader.ts +++ b/src/loading/binary-loader.ts @@ -6,7 +6,7 @@ import { Box3, BufferAttribute, BufferGeometry, Uint8BufferAttribute, Vector3 } import { PointAttributeName, PointAttributeType } from '../point-attributes'; import { PointCloudOctreeGeometryNode } from '../point-cloud-octree-geometry-node'; import { handleEmptyBuffer, handleFailedRequest } from '../utils/utils'; -import { WorkerPool } from '../utils/worker-pool'; +import { WorkerPool, WorkerType } from '../utils/worker-pool'; import { Version } from '../version'; import { GetUrlFn, XhrRequest } from './types'; @@ -48,10 +48,7 @@ export class BinaryLoader { xhrRequest: XhrRequest; callbacks: Callback[]; - public static readonly WORKER_POOL = new WorkerPool( - 32, - require('../workers/binary-decoder.worker.js').default, - ); + public static readonly WORKER_POOL = WorkerPool.getInstance(); constructor({ getUrl = s => Promise.resolve(s), @@ -111,7 +108,7 @@ export class BinaryLoader { return; } - BinaryLoader.WORKER_POOL.getWorker().then(autoTerminatingWorker => { + BinaryLoader.WORKER_POOL.getWorker(WorkerType.BINARY_DECODER_WORKER).then(autoTerminatingWorker => { const pointAttributes = node.pcoGeometry.pointAttributes; const numPoints = buffer.byteLength / pointAttributes.byteSize; @@ -122,7 +119,7 @@ export class BinaryLoader { autoTerminatingWorker.worker.onmessage = (e: WorkerResponse) => { if (this.disposed) { resolve(); - BinaryLoader.WORKER_POOL.releaseWorker(autoTerminatingWorker); + BinaryLoader.WORKER_POOL.releaseWorker(WorkerType.BINARY_DECODER_WORKER, autoTerminatingWorker); return; } @@ -145,7 +142,7 @@ export class BinaryLoader { this.callbacks.forEach(callback => callback(node)); resolve(); - BinaryLoader.WORKER_POOL.releaseWorker(autoTerminatingWorker); + BinaryLoader.WORKER_POOL.releaseWorker(WorkerType.BINARY_DECODER_WORKER, autoTerminatingWorker); }; const message = { diff --git a/src/loading2/octree-loader.ts b/src/loading2/octree-loader.ts index a603d0e4..956193e7 100644 --- a/src/loading2/octree-loader.ts +++ b/src/loading2/octree-loader.ts @@ -4,7 +4,7 @@ import { GetUrlFn, XhrRequest } from '../loading/types'; import { OctreeGeometry } from './octree-geometry'; import { OctreeGeometryNode } from './octree-geometry-node'; import { PointAttribute, PointAttributes, PointAttributeTypes } from './point-attributes'; -import { WorkerPool, WorkerType } from './worker-pool'; +import { WorkerPool, WorkerType } from '../utils/worker-pool'; import { buildUrl, extractBasePath } from './utils'; // Buffer files for DEFAULT encoding @@ -95,14 +95,14 @@ export class NodeLoader { } const workerType = this.metadata.encoding === 'GLTF' ? WorkerType.DECODER_WORKER_GLTF : WorkerType.DECODER_WORKER; - const worker = this.workerPool.getWorker(workerType); + const autoTerminatingWorker = await this.workerPool.getWorker(workerType); - worker.onmessage = (e) => { + autoTerminatingWorker.worker.onmessage = (e) => { const data = e.data; const buffers = data.attributeBuffers; - this.workerPool.returnWorker(workerType, worker); + this.workerPool.releaseWorker(workerType, autoTerminatingWorker); const geometry = new BufferGeometry(); @@ -168,7 +168,7 @@ export class NodeLoader { numPoints: numPoints }; - worker.postMessage(message, [message.buffer]); + autoTerminatingWorker.worker.postMessage(message, [message.buffer]); } catch (e) { node.loaded = false; node.loading = false; @@ -371,7 +371,7 @@ export interface Metadata { export class OctreeLoader { - workerPool: WorkerPool = new WorkerPool(); + workerPool: WorkerPool = WorkerPool.getInstance(); basePath = ''; hierarchyPath = ''; diff --git a/src/loading2/worker-pool.ts b/src/loading2/worker-pool.ts deleted file mode 100644 index e71be262..00000000 --- a/src/loading2/worker-pool.ts +++ /dev/null @@ -1,52 +0,0 @@ - -// Create enums for different types of workers -export enum WorkerType { - DECODER_WORKER = 'DECODER_WORKER', - DECODER_WORKER_GLTF = 'DECODER_WORKER_GLTF', -} - -// Worker JS names: 'BinaryDecoderWorker.js', 'DEMWorker.js', 'EptBinaryDecoderWorker.js', 'EptLaszipDecoderWorker.js', -// EptZstandardDecoder_preamble.js', 'EptZstandardDecoderWorker.js', 'LASDecoderWorker.js', 'LASLAZWorker.js', 'LazLoaderWorker.js' - -function createWorker(type: WorkerType): Worker { - // console.log(type) - switch (type) { - case WorkerType.DECODER_WORKER: { - const DecoderWorker = require('./decoder.worker.js').default; - return new DecoderWorker(); - } - case WorkerType.DECODER_WORKER_GLTF: { - const DecoderWorker_GLTF = require('./gltf-decoder.worker.js').default; - return new DecoderWorker_GLTF(); - } - default: - throw new Error('Unknown worker type'); - } -} - -export class WorkerPool { - // Workers will be an object that has a key for each worker type and the value is an array of Workers that can be empty - private workers: { [key in WorkerType]: Worker[] } = {DECODER_WORKER: [], DECODER_WORKER_GLTF: []}; - - getWorker(workerType: WorkerType): Worker { - // Throw error if workerType is not recognized - if (this.workers[workerType] === undefined) { - throw new Error('Unknown worker type'); - } - // Given a worker URL, if URL does not exist in the worker object, create a new array with the URL as a key - if (this.workers[workerType].length === 0) { - const worker = createWorker(workerType); - this.workers[workerType].push(worker); - } - const worker = this.workers[workerType].pop(); - if (worker === undefined) { // Typescript needs this - throw new Error('No workers available'); - } - // Return the last worker in the array and remove it from the array - return worker; - } - - returnWorker(workerType: WorkerType, worker: Worker) { - this.workers[workerType].push(worker); - } -} diff --git a/src/potree.ts b/src/potree.ts index cd97d739..d948df6a 100644 --- a/src/potree.ts +++ b/src/potree.ts @@ -17,7 +17,7 @@ import { PERSPECTIVE_CAMERA, } from './constants'; import { FEATURES } from './features'; -import { BinaryLoader, GetUrlFn, loadPOC } from './loading'; +import { GetUrlFn, loadPOC } from './loading'; import { loadOctree } from './loading2/load-octree'; import { ClipMode } from './materials'; import { PointCloudOctree } from './point-cloud-octree'; @@ -28,6 +28,7 @@ import { IPointCloudGeometryNode, IPointCloudTreeNode, IPotree, IVisibilityUpdat import { BinaryHeap } from './utils/binary-heap'; import { Box3Helper } from './utils/box3-helper'; import { LRU } from './utils/lru'; +import { WorkerPool } from './utils/worker-pool'; export class QueueItem { constructor( @@ -117,11 +118,11 @@ export class Potree implements IPotree { } static set maxLoaderWorkers(value: number) { - BinaryLoader.WORKER_POOL.maxWorkers = value; + WorkerPool.getInstance().maxWorkersPerPool = value; } static get maxLoaderWorkers(): number { - return BinaryLoader.WORKER_POOL.maxWorkers; + return WorkerPool.getInstance().maxWorkersPerPool; } private updateVisibility( diff --git a/src/utils/worker-pool.ts b/src/utils/worker-pool.ts index 5577635b..3ad91db8 100644 --- a/src/utils/worker-pool.ts +++ b/src/utils/worker-pool.ts @@ -1,74 +1,54 @@ -import { AsyncBlockingQueue } from './async-blocking-queue'; +import { AutoTerminatingWorker, WorkerQueue } from './worker-queue'; -export class AutoTerminatingWorker { - private timeoutId: number | undefined = undefined; - private terminated: boolean = false; +export enum WorkerType { + // Potree 1 workers + BINARY_DECODER_WORKER = 'BINARY_DECODER_WORKER', - constructor(private wrappedWorker: Worker, private maxIdle: number) {} + // Potree 2 workers + DECODER_WORKER = 'DECODER_WORKER', + DECODER_WORKER_GLTF = 'DECODER_WORKER_GLTF', +} - public get worker(): Worker { - return this.wrappedWorker; - } +export const DEFAULT_MAX_WORKERS_PER_POOL = 32; - get isTerminated(): boolean { - return this.terminated; - } +export class WorkerPool { + public _maxWorkersPerPool = DEFAULT_MAX_WORKERS_PER_POOL; + + private static instance: WorkerPool | undefined; + private constructor() {} + + private pool: { [key in WorkerType]: WorkerQueue } = { + BINARY_DECODER_WORKER: new WorkerQueue( + this._maxWorkersPerPool, + require('../workers/binary-decoder.worker.js').default, + ), + DECODER_WORKER: new WorkerQueue( + this._maxWorkersPerPool, + require('../loading2/decoder.worker.js').default, + ), + DECODER_WORKER_GLTF: new WorkerQueue( + this._maxWorkersPerPool, + require('../loading2/gltf-decoder.worker.js').default, + ), + }; + + static getInstance(): WorkerPool { + if (!this.instance) { + this.instance = new WorkerPool(); + } - markIdle(): void { - this.timeoutId = window.setTimeout(() => { - this.terminated = true; - this.wrappedWorker.terminate(); - }, this.maxIdle); + return this.instance; } - markInUse(): void { - if (this.timeoutId) { - window.clearTimeout(this.timeoutId); - } + set maxWorkersPerPool(count: number) { + Object.entries(this.pool).forEach(([_, pool]) => (pool.maxWorkers = count)); } -} - -export class WorkerPool { - /** - * The maximum amount of idle time that can elapse before a worker from this pool is automatically terminated - */ - private static readonly POOL_MAX_IDLE = 7000; - private pool = new AsyncBlockingQueue(); - private poolSize = 0; - - constructor(public maxWorkers: number, private workerType: any) {} - - /** - * Returns a worker promise which is resolved when one is available. - */ - public getWorker(): Promise { - // If the number of active workers is smaller than the maximum, return a new one. - // Otherwise, return a promise for worker from the pool. - if (this.poolSize < this.maxWorkers) { - this.poolSize++; - return Promise.resolve( - new AutoTerminatingWorker(new this.workerType(), WorkerPool.POOL_MAX_IDLE), - ); - } else { - return this.pool.dequeue().then(worker => { - worker.markInUse(); - // If the dequeued worker has been terminated, decrease the pool size and make a recursive call to get a new worker - if (worker.isTerminated) { - this.poolSize--; - return this.getWorker(); - } - return worker; - }); - } + public getWorker(workerType: WorkerType): Promise { + return this.pool[workerType].getWorker(); } - /** - * Releases a Worker back into the pool - * @param worker - */ - public releaseWorker(worker: AutoTerminatingWorker): void { - worker.markIdle(); - this.pool.enqueue(worker); + public releaseWorker(workerType: WorkerType, worker: AutoTerminatingWorker): void { + return this.pool[workerType].releaseWorker(worker); } } diff --git a/src/utils/worker-queue.ts b/src/utils/worker-queue.ts new file mode 100644 index 00000000..270e5725 --- /dev/null +++ b/src/utils/worker-queue.ts @@ -0,0 +1,74 @@ +import { AsyncBlockingQueue } from './async-blocking-queue'; + +export class AutoTerminatingWorker { + private timeoutId: number | undefined = undefined; + private terminated: boolean = false; + + constructor(private wrappedWorker: Worker, private maxIdle: number) {} + + public get worker(): Worker { + return this.wrappedWorker; + } + + get isTerminated(): boolean { + return this.terminated; + } + + markIdle(): void { + this.timeoutId = window.setTimeout(() => { + this.terminated = true; + this.wrappedWorker.terminate(); + }, this.maxIdle); + } + + markInUse(): void { + if (this.timeoutId) { + window.clearTimeout(this.timeoutId); + } + } +} + +export class WorkerQueue { + /** + * The maximum amount of idle time that can elapse before a worker from this pool is automatically terminated + */ + private static readonly QUEUE_MAX_IDLE = 7000; + + private queue = new AsyncBlockingQueue(); + private queueSize = 0; + + constructor(public maxWorkers: number, private workerType: any) {} + + /** + * Returns a worker promise which is resolved when one is available. + */ + public getWorker(): Promise { + // If the number of active workers is smaller than the maximum, return a new one. + // Otherwise, return a promise for worker from the pool. + if (this.queueSize < this.maxWorkers) { + this.queueSize++; + return Promise.resolve( + new AutoTerminatingWorker(new this.workerType(), WorkerQueue.QUEUE_MAX_IDLE), + ); + } else { + return this.queue.dequeue().then(worker => { + worker.markInUse(); + // If the dequeued worker has been terminated, decrease the pool size and make a recursive call to get a new worker + if (worker.isTerminated) { + this.queueSize--; + return this.getWorker(); + } + return worker; + }); + } + } + + /** + * Releases a Worker back into the pool + * @param worker + */ + public releaseWorker(worker: AutoTerminatingWorker): void { + worker.markIdle(); + this.queue.enqueue(worker); + } +}