WorkerExecutionSupport.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584
  1. /**
  2. * Development repository: https://github.com/kaisalmen/WWOBJLoader
  3. */
  4. /**
  5. * These instructions are used by {WorkerExecutionSupport} to build code for the web worker or to assign code
  6. *
  7. * @param {boolean} supportsStandardWorker
  8. * @param {boolean} supportsJsmWorker
  9. * @constructor
  10. */
  11. const CodeBuilderInstructions = function ( supportsStandardWorker, supportsJsmWorker, preferJsmWorker ) {
  12. this.supportsStandardWorker = supportsStandardWorker;
  13. this.supportsJsmWorker = supportsJsmWorker;
  14. this.preferJsmWorker = preferJsmWorker;
  15. this.startCode = '';
  16. this.codeFragments = [];
  17. this.importStatements = [];
  18. this.jsmWorkerUrl = null;
  19. this.defaultGeometryType = 0;
  20. };
  21. CodeBuilderInstructions.prototype = {
  22. constructor: CodeBuilderInstructions,
  23. isSupportsStandardWorker: function () {
  24. return this.supportsStandardWorker;
  25. },
  26. isSupportsJsmWorker: function () {
  27. return this.supportsJsmWorker;
  28. },
  29. isPreferJsmWorker: function () {
  30. return this.preferJsmWorker;
  31. },
  32. /**
  33. * Set the full path to the module that contains the worker code.
  34. *
  35. * @param {String} jsmWorkerUrl
  36. */
  37. setJsmWorkerUrl: function ( jsmWorkerUrl ) {
  38. if ( jsmWorkerUrl !== undefined && jsmWorkerUrl !== null ) {
  39. this.jsmWorkerUrl = jsmWorkerUrl;
  40. }
  41. },
  42. /**
  43. * Add code that is contained in addition to fragments and libraries
  44. * @param {String} startCode
  45. */
  46. addStartCode: function ( startCode ) {
  47. this.startCode = startCode;
  48. },
  49. /**
  50. * Add code fragment that is included in the provided order
  51. * @param {String} code
  52. */
  53. addCodeFragment: function ( code ) {
  54. this.codeFragments.push( code );
  55. },
  56. /**
  57. * Add full path to a library that is contained at the start of the worker via "importScripts"
  58. * @param {String} libraryPath
  59. */
  60. addLibraryImport: function ( libraryPath ) {
  61. const libraryUrl = new URL( libraryPath, window.location.href ).href;
  62. const code = 'importScripts( "' + libraryUrl + '" );';
  63. this.importStatements.push( code );
  64. },
  65. getImportStatements: function () {
  66. return this.importStatements;
  67. },
  68. getCodeFragments: function () {
  69. return this.codeFragments;
  70. },
  71. getStartCode: function () {
  72. return this.startCode;
  73. }
  74. };
  75. /**
  76. * This class provides means to transform existing parser code into a web worker. It defines a simple communication protocol
  77. * which allows to configure the worker and receive raw mesh data during execution.
  78. * @class
  79. */
  80. const WorkerExecutionSupport = function () {
  81. // check worker support first
  82. if ( window.Worker === undefined ) throw 'This browser does not support web workers!';
  83. if ( window.Blob === undefined ) throw 'This browser does not support Blob!';
  84. if ( typeof window.URL.createObjectURL !== 'function' ) throw 'This browser does not support Object creation from URL!';
  85. this._reset();
  86. };
  87. WorkerExecutionSupport.WORKER_SUPPORT_VERSION = '3.2.0';
  88. console.info( 'Using WorkerSupport version: ' + WorkerExecutionSupport.WORKER_SUPPORT_VERSION );
  89. WorkerExecutionSupport.prototype = {
  90. constructor: WorkerExecutionSupport,
  91. _reset: function () {
  92. this.logging = {
  93. enabled: false,
  94. debug: false
  95. };
  96. const scope = this;
  97. const scopeTerminate = function ( ) {
  98. scope._terminate();
  99. };
  100. this.worker = {
  101. native: null,
  102. jsmWorker: false,
  103. logging: true,
  104. workerRunner: {
  105. name: 'WorkerRunner',
  106. usesMeshDisassembler: false,
  107. defaultGeometryType: 0
  108. },
  109. terminateWorkerOnLoad: true,
  110. forceWorkerDataCopy: false,
  111. started: false,
  112. queuedMessage: null,
  113. callbacks: {
  114. onAssetAvailable: null,
  115. onLoad: null,
  116. terminate: scopeTerminate
  117. }
  118. };
  119. },
  120. /**
  121. * Enable or disable logging in general (except warn and error), plus enable or disable debug logging.
  122. *
  123. * @param {boolean} enabled True or false.
  124. * @param {boolean} debug True or false.
  125. */
  126. setLogging: function ( enabled, debug ) {
  127. this.logging.enabled = enabled === true;
  128. this.logging.debug = debug === true;
  129. this.worker.logging = enabled === true;
  130. return this;
  131. },
  132. /**
  133. * Forces all ArrayBuffers to be transferred to worker to be copied.
  134. *
  135. * @param {boolean} forceWorkerDataCopy True or false.
  136. */
  137. setForceWorkerDataCopy: function ( forceWorkerDataCopy ) {
  138. this.worker.forceWorkerDataCopy = forceWorkerDataCopy === true;
  139. return this;
  140. },
  141. /**
  142. * Request termination of worker once parser is finished.
  143. *
  144. * @param {boolean} terminateWorkerOnLoad True or false.
  145. */
  146. setTerminateWorkerOnLoad: function ( terminateWorkerOnLoad ) {
  147. this.worker.terminateWorkerOnLoad = terminateWorkerOnLoad === true;
  148. if ( this.worker.terminateWorkerOnLoad && this.isWorkerLoaded( this.worker.jsmWorker ) &&
  149. this.worker.queuedMessage === null && this.worker.started ) {
  150. if ( this.logging.enabled ) {
  151. console.info( 'Worker is terminated immediately as it is not running!' );
  152. }
  153. this._terminate();
  154. }
  155. return this;
  156. },
  157. /**
  158. * Update all callbacks.
  159. *
  160. * @param {Function} onAssetAvailable The function for processing the data, e.g. {@link MeshReceiver}.
  161. * @param {Function} [onLoad] The function that is called when parsing is complete.
  162. */
  163. updateCallbacks: function ( onAssetAvailable, onLoad ) {
  164. if ( onAssetAvailable !== undefined && onAssetAvailable !== null ) {
  165. this.worker.callbacks.onAssetAvailable = onAssetAvailable;
  166. }
  167. if ( onLoad !== undefined && onLoad !== null ) {
  168. this.worker.callbacks.onLoad = onLoad;
  169. }
  170. this._verifyCallbacks();
  171. },
  172. _verifyCallbacks: function () {
  173. if ( this.worker.callbacks.onAssetAvailable === undefined || this.worker.callbacks.onAssetAvailable === null ) {
  174. throw 'Unable to run as no "onAssetAvailable" callback is set.';
  175. }
  176. },
  177. /**
  178. * Builds the worker code according the provided Instructions.
  179. * If jsm worker code shall be built, then function may fall back to standard if lag is set
  180. *
  181. * @param {CodeBuilderInstructions} codeBuilderInstructions
  182. */
  183. buildWorker: function ( codeBuilderInstructions ) {
  184. let jsmSuccess = false;
  185. if ( codeBuilderInstructions.isSupportsJsmWorker() && codeBuilderInstructions.isPreferJsmWorker() ) {
  186. jsmSuccess = this._buildWorkerJsm( codeBuilderInstructions );
  187. }
  188. if ( ! jsmSuccess && codeBuilderInstructions.isSupportsStandardWorker() ) {
  189. this._buildWorkerStandard( codeBuilderInstructions );
  190. }
  191. },
  192. /**
  193. *
  194. * @param {CodeBuilderInstructions} codeBuilderInstructions
  195. * @return {boolean} Whether loading of jsm worker was successful
  196. * @private
  197. */
  198. _buildWorkerJsm: function ( codeBuilderInstructions ) {
  199. let jsmSuccess = true;
  200. const timeLabel = 'buildWorkerJsm';
  201. const workerAvailable = this._buildWorkerCheckPreconditions( true, timeLabel );
  202. if ( ! workerAvailable ) {
  203. try {
  204. const worker = new Worker( codeBuilderInstructions.jsmWorkerUrl.href, { type: 'module' } );
  205. this._configureWorkerCommunication( worker, true, codeBuilderInstructions.defaultGeometryType, timeLabel );
  206. } catch ( e ) {
  207. jsmSuccess = false;
  208. // Chrome throws this exception, but Firefox currently does not complain, but can't execute the worker afterwards
  209. if ( e instanceof TypeError || e instanceof SyntaxError ) {
  210. console.error( 'Modules are not supported in workers.' );
  211. }
  212. }
  213. }
  214. return jsmSuccess;
  215. },
  216. /**
  217. * Validate the status of worker code and the derived worker and specify functions that should be build when new raw mesh data becomes available and when the parser is finished.
  218. *
  219. * @param {CodeBuilderIns} buildWorkerCode The function that is invoked to create the worker code of the parser.
  220. */
  221. /**
  222. *
  223. * @param {CodeBuilderInstructions} codeBuilderInstructions
  224. * @private
  225. */
  226. _buildWorkerStandard: function ( codeBuilderInstructions ) {
  227. const timeLabel = 'buildWorkerStandard';
  228. const workerAvailable = this._buildWorkerCheckPreconditions( false, timeLabel );
  229. if ( ! workerAvailable ) {
  230. let concatenateCode = '';
  231. codeBuilderInstructions.getImportStatements().forEach( function ( element ) {
  232. concatenateCode += element + '\n';
  233. } );
  234. concatenateCode += '\n';
  235. codeBuilderInstructions.getCodeFragments().forEach( function ( element ) {
  236. concatenateCode += element + '\n';
  237. } );
  238. concatenateCode += '\n';
  239. concatenateCode += codeBuilderInstructions.getStartCode();
  240. const blob = new Blob( [ concatenateCode ], { type: 'application/javascript' } );
  241. const worker = new Worker( window.URL.createObjectURL( blob ) );
  242. this._configureWorkerCommunication( worker, false, codeBuilderInstructions.defaultGeometryType, timeLabel );
  243. }
  244. },
  245. _buildWorkerCheckPreconditions: function ( requireJsmWorker, timeLabel ) {
  246. let workerAvailable = false;
  247. if ( this.isWorkerLoaded( requireJsmWorker ) ) {
  248. workerAvailable = true;
  249. } else {
  250. if ( this.logging.enabled ) {
  251. console.info( 'WorkerExecutionSupport: Building ' + ( requireJsmWorker ? 'jsm' : 'standard' ) + ' worker code...' );
  252. console.time( timeLabel );
  253. }
  254. }
  255. return workerAvailable;
  256. },
  257. _configureWorkerCommunication: function ( worker, haveJsmWorker, defaultGeometryType, timeLabel ) {
  258. this.worker.native = worker;
  259. this.worker.jsmWorker = haveJsmWorker;
  260. const scope = this;
  261. const scopedReceiveWorkerMessage = function ( event ) {
  262. scope._receiveWorkerMessage( event );
  263. };
  264. this.worker.native.onmessage = scopedReceiveWorkerMessage;
  265. this.worker.native.onerror = scopedReceiveWorkerMessage;
  266. if ( defaultGeometryType !== undefined && defaultGeometryType !== null ) {
  267. this.worker.workerRunner.defaultGeometryType = defaultGeometryType;
  268. }
  269. if ( this.logging.enabled ) {
  270. console.timeEnd( timeLabel );
  271. }
  272. },
  273. /**
  274. * Returns if Worker code is available and complies with expectation.
  275. * @param {boolean} requireJsmWorker
  276. * @return {boolean|*}
  277. */
  278. isWorkerLoaded: function ( requireJsmWorker ) {
  279. return this.worker.native !== null &&
  280. ( ( requireJsmWorker && this.worker.jsmWorker ) || ( ! requireJsmWorker && ! this.worker.jsmWorker ) );
  281. },
  282. /**
  283. * Executed in worker scope
  284. */
  285. _receiveWorkerMessage: function ( event ) {
  286. // fast-fail in case of error
  287. if ( event.type === 'error' ) {
  288. console.error( event );
  289. return;
  290. }
  291. const payload = event.data;
  292. const workerRunnerName = this.worker.workerRunner.name;
  293. switch ( payload.cmd ) {
  294. case 'assetAvailable':
  295. this.worker.callbacks.onAssetAvailable( payload );
  296. break;
  297. case 'completeOverall':
  298. this.worker.queuedMessage = null;
  299. this.worker.started = false;
  300. if ( this.worker.callbacks.onLoad !== null ) {
  301. this.worker.callbacks.onLoad( payload.msg );
  302. }
  303. if ( this.worker.terminateWorkerOnLoad ) {
  304. if ( this.worker.logging.enabled ) {
  305. console.info( 'WorkerSupport [' + workerRunnerName + ']: Run is complete. Terminating application on request!' );
  306. }
  307. this.worker.callbacks.terminate();
  308. }
  309. break;
  310. case 'error':
  311. console.error( 'WorkerSupport [' + workerRunnerName + ']: Reported error: ' + payload.msg );
  312. this.worker.queuedMessage = null;
  313. this.worker.started = false;
  314. if ( this.worker.callbacks.onLoad !== null ) {
  315. this.worker.callbacks.onLoad( payload.msg );
  316. }
  317. if ( this.worker.terminateWorkerOnLoad ) {
  318. if ( this.worker.logging.enabled ) {
  319. console.info( 'WorkerSupport [' + workerRunnerName + ']: Run reported error. Terminating application on request!' );
  320. }
  321. this.worker.callbacks.terminate();
  322. }
  323. break;
  324. default:
  325. console.error( 'WorkerSupport [' + workerRunnerName + ']: Received unknown command: ' + payload.cmd );
  326. break;
  327. }
  328. },
  329. /**
  330. * Runs the parser with the provided configuration.
  331. *
  332. * @param {Object} payload Raw mesh description (buffers, params, materials) used to build one to many meshes.
  333. */
  334. executeParallel: function ( payload, transferables ) {
  335. payload.cmd = 'parse';
  336. payload.usesMeshDisassembler = this.worker.workerRunner.usesMeshDisassembler;
  337. payload.defaultGeometryType = this.worker.workerRunner.defaultGeometryType;
  338. if ( ! this._verifyWorkerIsAvailable( payload, transferables ) ) return;
  339. this._postMessage();
  340. },
  341. _verifyWorkerIsAvailable: function ( payload, transferables ) {
  342. this._verifyCallbacks();
  343. let ready = true;
  344. if ( this.worker.queuedMessage !== null ) {
  345. console.warn( 'Already processing message. Rejecting new run instruction' );
  346. ready = false;
  347. } else {
  348. this.worker.queuedMessage = {
  349. payload: payload,
  350. transferables: ( transferables === undefined || transferables === null ) ? [] : transferables
  351. };
  352. this.worker.started = true;
  353. }
  354. return ready;
  355. },
  356. _postMessage: function () {
  357. if ( this.worker.queuedMessage !== null ) {
  358. if ( this.worker.queuedMessage.payload.data.input instanceof ArrayBuffer ) {
  359. let transferables = [];
  360. if ( this.worker.forceWorkerDataCopy ) {
  361. transferables.push( this.worker.queuedMessage.payload.data.input.slice( 0 ) );
  362. } else {
  363. transferables.push( this.worker.queuedMessage.payload.data.input );
  364. }
  365. if ( this.worker.queuedMessage.transferables.length > 0 ) {
  366. transferables = transferables.concat( this.worker.queuedMessage.transferables );
  367. }
  368. this.worker.native.postMessage( this.worker.queuedMessage.payload, transferables );
  369. } else {
  370. this.worker.native.postMessage( this.worker.queuedMessage.payload );
  371. }
  372. }
  373. },
  374. _terminate: function () {
  375. this.worker.native.terminate();
  376. this._reset();
  377. }
  378. };
  379. export {
  380. CodeBuilderInstructions,
  381. WorkerExecutionSupport
  382. };