diff --git a/dist/preview release/what's new.md b/dist/preview release/what's new.md index d12eed73bcb..c725ee26700 100644 --- a/dist/preview release/what's new.md +++ b/dist/preview release/what's new.md @@ -74,6 +74,7 @@ - Added `getCreationOptions` on `ThinEngine`. ([carolhmj](https://github.com/carolhmj)) - Added `CompatibilityOptions.UseOpenGLOrientationForUV` to define if the system should use OpenGL convention for UVs when creating geometry or loading .babylon files (false by default) ([Deltakosh](https://github.com/deltakosh)) - Added RuntimeError and errorCodes for runtime errors. ([jp833](https://github.com/jp833)) +- Added `AutoReleaseWorkerPool` which will automatically terminate idle workers after a specified amount of time and use them in KTX2 and Draco decoders. ([bghgary](https://github.com/bghgary)) ### Engine diff --git a/loaders/src/glTF/glTFValidation.ts b/loaders/src/glTF/glTFValidation.ts index 036709319e9..9a324d142fa 100644 --- a/loaders/src/glTF/glTFValidation.ts +++ b/loaders/src/glTF/glTFValidation.ts @@ -117,12 +117,14 @@ export class GLTFValidation { worker.removeEventListener("error", onError); worker.removeEventListener("message", onMessage); resolve(data.value); + worker.terminate(); break; } case "validate.reject": { worker.removeEventListener("error", onError); worker.removeEventListener("message", onMessage); reject(data.reason); + worker.terminate(); } } }; diff --git a/src/Meshes/Compression/dracoCompression.ts b/src/Meshes/Compression/dracoCompression.ts index a1f4d8db1b3..6d83e2a7d31 100644 --- a/src/Meshes/Compression/dracoCompression.ts +++ b/src/Meshes/Compression/dracoCompression.ts @@ -1,5 +1,5 @@ import { Tools } from "../../Misc/tools"; -import { WorkerPool } from '../../Misc/workerPool'; +import { AutoReleaseWorkerPool } from '../../Misc/workerPool'; import { Nullable } from "../../types"; import { IDisposable } from "../../scene"; import { VertexData } from "../../Meshes/mesh.vertexData"; @@ -231,7 +231,7 @@ export interface IDracoCompressionConfiguration { * @see https://www.babylonjs-playground.com/#N3EK4B#0 */ export class DracoCompression implements IDisposable { - private _workerPoolPromise?: Promise; + private _workerPoolPromise?: Promise; private _decoderModulePromise?: Promise; /** @@ -303,9 +303,9 @@ export class DracoCompression implements IDisposable { this._workerPoolPromise = decoderInfo.wasmBinaryPromise.then((decoderWasmBinary) => { const workerContent = `${decodeMesh}(${worker})()`; const workerBlobUrl = URL.createObjectURL(new Blob([workerContent], { type: "application/javascript" })); - const workerPromises = new Array>(numWorkers); - for (let i = 0; i < workerPromises.length; i++) { - workerPromises[i] = new Promise((resolve, reject) => { + + return new AutoReleaseWorkerPool(numWorkers, () => { + return new Promise((resolve, reject) => { const worker = new Worker(workerBlobUrl); const onError = (error: ErrorEvent) => { worker.removeEventListener("error", onError); @@ -332,10 +332,6 @@ export class DracoCompression implements IDisposable { } }); }); - } - - return Promise.all(workerPromises).then((workers) => { - return new WorkerPool(workers); }); }); } diff --git a/src/Misc/khronosTextureContainer2.ts b/src/Misc/khronosTextureContainer2.ts index f969453e228..2674c8fa478 100644 --- a/src/Misc/khronosTextureContainer2.ts +++ b/src/Misc/khronosTextureContainer2.ts @@ -1,7 +1,7 @@ import { InternalTexture } from "../Materials/Textures/internalTexture"; import { ThinEngine } from "../Engines/thinEngine"; import { Constants } from '../Engines/constants'; -import { WorkerPool } from './workerPool'; +import { AutoReleaseWorkerPool } from './workerPool'; import { Tools } from "./tools"; import { Nullable } from "../types"; @@ -11,10 +11,8 @@ declare var KTX2DECODER: any; * Class for loading KTX2 files */ export class KhronosTextureContainer2 { - private static _WorkerPoolPromise?: Promise; - private static _NoWorkerPromise?: Promise; - private static _Initialized: boolean; - private static _Ktx2Decoder: any; // used when no worker pool is used + private static _WorkerPoolPromise?: Promise; + private static _DecoderModulePromise?: Promise; /** * URLs to use when loading the KTX2 decoder module as well as its dependencies @@ -67,48 +65,43 @@ export class KhronosTextureContainer2 { private _engine: ThinEngine; - private static _CreateWorkerPool(numWorkers: number) { - this._Initialized = true; + private static _Initialize(numWorkers: number) { + if (KhronosTextureContainer2._WorkerPoolPromise || KhronosTextureContainer2._DecoderModulePromise) { + return; + } if (numWorkers && typeof Worker === "function") { KhronosTextureContainer2._WorkerPoolPromise = new Promise((resolve) => { const workerContent = `(${workerFunc})()`; const workerBlobUrl = URL.createObjectURL(new Blob([workerContent], { type: "application/javascript" })); - const workerPromises = new Array>(numWorkers); - for (let i = 0; i < workerPromises.length; i++) { - workerPromises[i] = new Promise((resolve, reject) => { - const worker = new Worker(workerBlobUrl); + resolve(new AutoReleaseWorkerPool(numWorkers, () => new Promise((resolve, reject) => { + const worker = new Worker(workerBlobUrl); - const onError = (error: ErrorEvent) => { + const onError = (error: ErrorEvent) => { + worker.removeEventListener("error", onError); + worker.removeEventListener("message", onMessage); + reject(error); + }; + + const onMessage = (message: MessageEvent) => { + if (message.data.action === "init") { worker.removeEventListener("error", onError); worker.removeEventListener("message", onMessage); - reject(error); - }; - - const onMessage = (message: MessageEvent) => { - if (message.data.action === "init") { - worker.removeEventListener("error", onError); - worker.removeEventListener("message", onMessage); - resolve(worker); - } - }; + resolve(worker); + } + }; - worker.addEventListener("error", onError); - worker.addEventListener("message", onMessage); + worker.addEventListener("error", onError); + worker.addEventListener("message", onMessage); - worker.postMessage({ - action: "init", - urls: KhronosTextureContainer2.URLConfig - }); + worker.postMessage({ + action: "init", + urls: KhronosTextureContainer2.URLConfig }); - } - - Promise.all(workerPromises).then((workers) => { - resolve(new WorkerPool(workers)); - }); + }))); }); } else if (typeof(KTX2DECODER) === "undefined") { - KhronosTextureContainer2._NoWorkerPromise = Tools.LoadScriptAsync(KhronosTextureContainer2.URLConfig.jsDecoderModule).then(() => { + KhronosTextureContainer2._DecoderModulePromise = Tools.LoadScriptAsync(KhronosTextureContainer2.URLConfig.jsDecoderModule).then(() => { KTX2DECODER.MSCTranscoder.UseFromWorkerThread = false; KTX2DECODER.WASMMemoryManager.LoadBinariesFromCurrentThread = true; @@ -134,10 +127,13 @@ export class KhronosTextureContainer2 { if (urls.wasmZSTDDecoder !== null) { KTX2DECODER.ZSTDDecoder.WasmModuleURL = urls.wasmZSTDDecoder; } + + return new KTX2DECODER.KTX2Decoder(); }); } else { KTX2DECODER.MSCTranscoder.UseFromWorkerThread = false; KTX2DECODER.WASMMemoryManager.LoadBinariesFromCurrentThread = true; + KhronosTextureContainer2._DecoderModulePromise = Promise.resolve(new KTX2DECODER.KTX2Decoder()); } } @@ -149,9 +145,7 @@ export class KhronosTextureContainer2 { public constructor(engine: ThinEngine, numWorkers = KhronosTextureContainer2.DefaultNumWorkers) { this._engine = engine; - if (!KhronosTextureContainer2._Initialized) { - KhronosTextureContainer2._CreateWorkerPool(numWorkers); - } + KhronosTextureContainer2._Initialize(numWorkers); } /** @hidden */ @@ -206,15 +200,10 @@ export class KhronosTextureContainer2 { }); }); }); - } - else if (KhronosTextureContainer2._NoWorkerPromise) { - return KhronosTextureContainer2._NoWorkerPromise.then(() => { + } else if (KhronosTextureContainer2._DecoderModulePromise) { + return KhronosTextureContainer2._DecoderModulePromise.then((decoder) => { return new Promise((resolve, reject) => { - if (!KhronosTextureContainer2._Ktx2Decoder) { - KhronosTextureContainer2._Ktx2Decoder = new KTX2DECODER.KTX2Decoder(); - } - - KhronosTextureContainer2._Ktx2Decoder.decode(data, caps).then((data: any) => { + decoder.decode(data, caps).then((data: any) => { this._createTexture(data, internalTexture); resolve(); }).catch((reason: any) => { @@ -224,32 +213,7 @@ export class KhronosTextureContainer2 { }); } - return new Promise((resolve, reject) => { - if (!KhronosTextureContainer2._Ktx2Decoder) { - KhronosTextureContainer2._Ktx2Decoder = new KTX2DECODER.KTX2Decoder(); - } - - KhronosTextureContainer2._Ktx2Decoder.decode(data, caps).then((data: any) => { - this._createTexture(data, internalTexture); - resolve(); - }).catch((reason: any) => { - reject({ message: reason }); - }); - }); - } - - /** - * Stop all async operations and release resources. - */ - public dispose(): void { - if (KhronosTextureContainer2._WorkerPoolPromise) { - KhronosTextureContainer2._WorkerPoolPromise.then((workerPool) => { - workerPool.dispose(); - }); - } - - delete KhronosTextureContainer2._WorkerPoolPromise; - delete KhronosTextureContainer2._NoWorkerPromise; + throw new Error("KTX2 decoder module is not available"); } protected _createTexture(data: any /* IEncodedData */, internalTexture: InternalTexture, options?: any) { @@ -327,8 +291,6 @@ export class KhronosTextureContainer2 { declare function importScripts(...urls: string[]): void; declare function postMessage(message: any, transfer?: any[]): void; -declare var KTX2DECODER: any; - function workerFunc(): void { let ktx2Decoder: any; diff --git a/src/Misc/workerPool.ts b/src/Misc/workerPool.ts index 8b58f1c2cb4..ea6ac6ee7ad 100644 --- a/src/Misc/workerPool.ts +++ b/src/Misc/workerPool.ts @@ -1,16 +1,18 @@ import { IDisposable } from "../scene"; +/** @ignore */ interface WorkerInfo { - worker: Worker; - active: boolean; + workerPromise: Promise; + idle: boolean; + timeoutId?: number; } /** * Helper class to push actions to a pool of workers. */ export class WorkerPool implements IDisposable { - private _workerInfos: Array; - private _pendingActions = new Array<(worker: Worker, onComplete: () => void) => void>(); + protected _workerInfos: Array; + protected _pendingActions = new Array<(worker: Worker, onComplete: () => void) => void>(); /** * Constructor @@ -18,8 +20,8 @@ export class WorkerPool implements IDisposable { */ constructor(workers: Array) { this._workerInfos = workers.map((worker) => ({ - worker: worker, - active: false + workerPromise: Promise.resolve(worker), + idle: true })); } @@ -28,11 +30,13 @@ export class WorkerPool implements IDisposable { */ public dispose(): void { for (const workerInfo of this._workerInfos) { - workerInfo.worker.terminate(); + workerInfo.workerPromise.then((worker) => { + worker.terminate(); + }); } - this._workerInfos = []; - this._pendingActions = []; + this._workerInfos.length = 0; + this._pendingActions.length = 0; } /** @@ -41,24 +45,112 @@ export class WorkerPool implements IDisposable { * @param action The action to perform. Call onComplete when the action is complete. */ public push(action: (worker: Worker, onComplete: () => void) => void): void { + if (!this._executeOnIdleWorker(action)) { + this._pendingActions.push(action); + } + } + + protected _executeOnIdleWorker(action: (worker: Worker, onComplete: () => void) => void): boolean { for (const workerInfo of this._workerInfos) { - if (!workerInfo.active) { + if (workerInfo.idle) { this._execute(workerInfo, action); - return; + return true; } } - this._pendingActions.push(action); + return false; } - private _execute(workerInfo: WorkerInfo, action: (worker: Worker, onComplete: () => void) => void): void { - workerInfo.active = true; - action(workerInfo.worker, () => { - workerInfo.active = false; - const nextAction = this._pendingActions.shift(); - if (nextAction) { - this._execute(workerInfo, nextAction); - } + protected _execute(workerInfo: WorkerInfo, action: (worker: Worker, onComplete: () => void) => void): void { + workerInfo.idle = false; + workerInfo.workerPromise.then((worker) => { + action(worker, () => { + const nextAction = this._pendingActions.shift(); + if (nextAction) { + this._execute(workerInfo, nextAction); + } else { + workerInfo.idle = true; + } + }); }); } } + +/** + * Options for AutoReleaseWorkerPool + */ +export interface AutoReleaseWorkerPoolOptions { + /** + * Idle time elapsed before workers are terminated. + */ + idleTimeElapsedBeforeRelease: number; +} + +/** + * Similar to the WorkerPool class except it creates and destroys workers automatically with a maximum of `maxWorkers` workers. + * Workers are terminated when it is idle for at least `idleTimeElapsedBeforeRelease` milliseconds. + */ +export class AutoReleaseWorkerPool extends WorkerPool { + /** + * Default options for the constructor. + * Override to change the defaults. + */ + public static DefaultOptions: AutoReleaseWorkerPoolOptions = { + idleTimeElapsedBeforeRelease: 1000 + }; + + private readonly _maxWorkers: number; + private readonly _createWorkerAsync: () => Promise; + private readonly _options: AutoReleaseWorkerPoolOptions; + + constructor(maxWorkers: number, createWorkerAsync: () => Promise, options = AutoReleaseWorkerPool.DefaultOptions) { + super([]); + + this._maxWorkers = maxWorkers; + this._createWorkerAsync = createWorkerAsync; + this._options = options; + } + + public push(action: (worker: Worker, onComplete: () => void) => void): void { + if (!this._executeOnIdleWorker(action)) { + if (this._workerInfos.length < this._maxWorkers) { + const workerInfo: WorkerInfo = { + workerPromise: this._createWorkerAsync(), + idle: false + }; + this._workerInfos.push(workerInfo); + this._execute(workerInfo, action); + } else { + this._pendingActions.push(action); + } + } + } + + protected _execute(workerInfo: WorkerInfo, action: (worker: Worker, onComplete: () => void) => void): void { + // Reset the idle timeout. + if (workerInfo.timeoutId) { + clearTimeout(workerInfo.timeoutId); + delete workerInfo.timeoutId; + } + + super._execute(workerInfo, (worker, onComplete) => { + action(worker, () => { + onComplete(); + + if (workerInfo.idle) { + // Schedule the worker to be terminated after the elapsed time. + workerInfo.timeoutId = setTimeout(() => { + workerInfo.workerPromise.then((worker) => { + worker.terminate(); + }); + + const indexOf = this._workerInfos.indexOf(workerInfo); + if (indexOf !== -1) { + this._workerInfos.splice(indexOf, 1); + } + }, this._options.idleTimeElapsedBeforeRelease); + } + }); + }); + } +} \ No newline at end of file