WorkerExecutionSupport.js 13 KB

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