WorkerExecutionSupport.js 11 KB

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