TaskProcessor.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. import Uri from "../ThirdParty/Uri.js";
  2. import buildModuleUrl from "./buildModuleUrl.js";
  3. import defaultValue from "./defaultValue.js";
  4. import defer from "./defer.js";
  5. import defined from "./defined.js";
  6. import destroyObject from "./destroyObject.js";
  7. import DeveloperError from "./DeveloperError.js";
  8. import Event from "./Event.js";
  9. import FeatureDetection from "./FeatureDetection.js";
  10. import isCrossOriginUrl from "./isCrossOriginUrl.js";
  11. import Resource from "./Resource.js";
  12. import RuntimeError from "./RuntimeError.js";
  13. function canTransferArrayBuffer() {
  14. if (!defined(TaskProcessor._canTransferArrayBuffer)) {
  15. const worker = new Worker(
  16. getWorkerUrl("Workers/transferTypedArrayTest.js")
  17. );
  18. worker.postMessage = defaultValue(
  19. worker.webkitPostMessage,
  20. worker.postMessage
  21. );
  22. const value = 99;
  23. const array = new Int8Array([value]);
  24. try {
  25. // postMessage might fail with a DataCloneError
  26. // if transferring array buffers is not supported.
  27. worker.postMessage(
  28. {
  29. array: array,
  30. },
  31. [array.buffer]
  32. );
  33. } catch (e) {
  34. TaskProcessor._canTransferArrayBuffer = false;
  35. return TaskProcessor._canTransferArrayBuffer;
  36. }
  37. const deferred = defer();
  38. worker.onmessage = function (event) {
  39. const array = event.data.array;
  40. // some versions of Firefox silently fail to transfer typed arrays.
  41. // https://bugzilla.mozilla.org/show_bug.cgi?id=841904
  42. // Check to make sure the value round-trips successfully.
  43. const result = defined(array) && array[0] === value;
  44. deferred.resolve(result);
  45. worker.terminate();
  46. TaskProcessor._canTransferArrayBuffer = result;
  47. };
  48. TaskProcessor._canTransferArrayBuffer = deferred.promise;
  49. }
  50. return TaskProcessor._canTransferArrayBuffer;
  51. }
  52. const taskCompletedEvent = new Event();
  53. function completeTask(processor, data) {
  54. --processor._activeTasks;
  55. const id = data.id;
  56. if (!defined(id)) {
  57. // This is not one of ours.
  58. return;
  59. }
  60. const deferreds = processor._deferreds;
  61. const deferred = deferreds[id];
  62. if (defined(data.error)) {
  63. let error = data.error;
  64. if (error.name === "RuntimeError") {
  65. error = new RuntimeError(data.error.message);
  66. error.stack = data.error.stack;
  67. } else if (error.name === "DeveloperError") {
  68. error = new DeveloperError(data.error.message);
  69. error.stack = data.error.stack;
  70. }
  71. taskCompletedEvent.raiseEvent(error);
  72. deferred.reject(error);
  73. } else {
  74. taskCompletedEvent.raiseEvent();
  75. deferred.resolve(data.result);
  76. }
  77. delete deferreds[id];
  78. }
  79. function getWorkerUrl(moduleID) {
  80. let url = buildModuleUrl(moduleID);
  81. if (isCrossOriginUrl(url)) {
  82. //to load cross-origin, create a shim worker from a blob URL
  83. const script = `importScripts("${url}");`;
  84. let blob;
  85. try {
  86. blob = new Blob([script], {
  87. type: "application/javascript",
  88. });
  89. } catch (e) {
  90. const BlobBuilder =
  91. window.BlobBuilder ||
  92. window.WebKitBlobBuilder ||
  93. window.MozBlobBuilder ||
  94. window.MSBlobBuilder;
  95. const blobBuilder = new BlobBuilder();
  96. blobBuilder.append(script);
  97. blob = blobBuilder.getBlob("application/javascript");
  98. }
  99. const URL = window.URL || window.webkitURL;
  100. url = URL.createObjectURL(blob);
  101. }
  102. return url;
  103. }
  104. let bootstrapperUrlResult;
  105. function getBootstrapperUrl() {
  106. if (!defined(bootstrapperUrlResult)) {
  107. bootstrapperUrlResult = getWorkerUrl("Workers/cesiumWorkerBootstrapper.js");
  108. }
  109. return bootstrapperUrlResult;
  110. }
  111. function createWorker(processor) {
  112. const worker = new Worker(getBootstrapperUrl());
  113. worker.postMessage = defaultValue(
  114. worker.webkitPostMessage,
  115. worker.postMessage
  116. );
  117. const bootstrapMessage = {
  118. loaderConfig: {
  119. paths: {
  120. Workers: buildModuleUrl("Workers"),
  121. },
  122. baseUrl: buildModuleUrl.getCesiumBaseUrl().url,
  123. },
  124. workerModule: processor._workerPath,
  125. };
  126. worker.postMessage(bootstrapMessage);
  127. worker.onmessage = function (event) {
  128. completeTask(processor, event.data);
  129. };
  130. return worker;
  131. }
  132. function getWebAssemblyLoaderConfig(processor, wasmOptions) {
  133. const config = {
  134. modulePath: undefined,
  135. wasmBinaryFile: undefined,
  136. wasmBinary: undefined,
  137. };
  138. // Web assembly not supported, use fallback js module if provided
  139. if (!FeatureDetection.supportsWebAssembly()) {
  140. if (!defined(wasmOptions.fallbackModulePath)) {
  141. throw new RuntimeError(
  142. `This browser does not support Web Assembly, and no backup module was provided for ${processor._workerPath}`
  143. );
  144. }
  145. config.modulePath = buildModuleUrl(wasmOptions.fallbackModulePath);
  146. return Promise.resolve(config);
  147. }
  148. config.modulePath = buildModuleUrl(wasmOptions.modulePath);
  149. config.wasmBinaryFile = buildModuleUrl(wasmOptions.wasmBinaryFile);
  150. return Resource.fetchArrayBuffer({
  151. url: config.wasmBinaryFile,
  152. }).then(function (arrayBuffer) {
  153. config.wasmBinary = arrayBuffer;
  154. return config;
  155. });
  156. }
  157. /**
  158. * A wrapper around a web worker that allows scheduling tasks for a given worker,
  159. * returning results asynchronously via a promise.
  160. *
  161. * The Worker is not constructed until a task is scheduled.
  162. *
  163. * @alias TaskProcessor
  164. * @constructor
  165. *
  166. * @param {String} workerPath The Url to the worker. This can either be an absolute path or relative to the Cesium Workers folder.
  167. * @param {Number} [maximumActiveTasks=Number.POSITIVE_INFINITY] The maximum number of active tasks. Once exceeded,
  168. * scheduleTask will not queue any more tasks, allowing
  169. * work to be rescheduled in future frames.
  170. */
  171. function TaskProcessor(workerPath, maximumActiveTasks) {
  172. const uri = new Uri(workerPath);
  173. this._workerPath =
  174. uri.scheme().length !== 0 && uri.fragment().length === 0
  175. ? workerPath
  176. : TaskProcessor._workerModulePrefix + workerPath;
  177. this._maximumActiveTasks = defaultValue(
  178. maximumActiveTasks,
  179. Number.POSITIVE_INFINITY
  180. );
  181. this._activeTasks = 0;
  182. this._deferreds = {};
  183. this._nextID = 0;
  184. }
  185. const emptyTransferableObjectArray = [];
  186. /**
  187. * Schedule a task to be processed by the web worker asynchronously. If there are currently more
  188. * tasks active than the maximum set by the constructor, will immediately return undefined.
  189. * Otherwise, returns a promise that will resolve to the result posted back by the worker when
  190. * finished.
  191. *
  192. * @param {Object} parameters Any input data that will be posted to the worker.
  193. * @param {Object[]} [transferableObjects] An array of objects contained in parameters that should be
  194. * transferred to the worker instead of copied.
  195. * @returns {Promise.<Object>|undefined} Either a promise that will resolve to the result when available, or undefined
  196. * if there are too many active tasks,
  197. *
  198. * @example
  199. * const taskProcessor = new Cesium.TaskProcessor('myWorkerPath');
  200. * const promise = taskProcessor.scheduleTask({
  201. * someParameter : true,
  202. * another : 'hello'
  203. * });
  204. * if (!Cesium.defined(promise)) {
  205. * // too many active tasks - try again later
  206. * } else {
  207. * promise.then(function(result) {
  208. * // use the result of the task
  209. * });
  210. * }
  211. */
  212. TaskProcessor.prototype.scheduleTask = function (
  213. parameters,
  214. transferableObjects
  215. ) {
  216. if (!defined(this._worker)) {
  217. this._worker = createWorker(this);
  218. }
  219. if (this._activeTasks >= this._maximumActiveTasks) {
  220. return undefined;
  221. }
  222. ++this._activeTasks;
  223. const processor = this;
  224. return Promise.resolve(canTransferArrayBuffer()).then(function (
  225. canTransferArrayBuffer
  226. ) {
  227. if (!defined(transferableObjects)) {
  228. transferableObjects = emptyTransferableObjectArray;
  229. } else if (!canTransferArrayBuffer) {
  230. transferableObjects.length = 0;
  231. }
  232. const id = processor._nextID++;
  233. const deferred = defer();
  234. processor._deferreds[id] = deferred;
  235. processor._worker.postMessage(
  236. {
  237. id: id,
  238. parameters: parameters,
  239. canTransferArrayBuffer: canTransferArrayBuffer,
  240. },
  241. transferableObjects
  242. );
  243. return deferred.promise;
  244. });
  245. };
  246. /**
  247. * Posts a message to a web worker with configuration to initialize loading
  248. * and compiling a web assembly module asychronously, as well as an optional
  249. * fallback JavaScript module to use if Web Assembly is not supported.
  250. *
  251. * @param {Object} [webAssemblyOptions] An object with the following properties:
  252. * @param {String} [webAssemblyOptions.modulePath] The path of the web assembly JavaScript wrapper module.
  253. * @param {String} [webAssemblyOptions.wasmBinaryFile] The path of the web assembly binary file.
  254. * @param {String} [webAssemblyOptions.fallbackModulePath] The path of the fallback JavaScript module to use if web assembly is not supported.
  255. * @returns {Promise.<Object>} A promise that resolves to the result when the web worker has loaded and compiled the web assembly module and is ready to process tasks.
  256. */
  257. TaskProcessor.prototype.initWebAssemblyModule = function (webAssemblyOptions) {
  258. if (!defined(this._worker)) {
  259. this._worker = createWorker(this);
  260. }
  261. const deferred = defer();
  262. const processor = this;
  263. const worker = this._worker;
  264. getWebAssemblyLoaderConfig(this, webAssemblyOptions).then(function (
  265. wasmConfig
  266. ) {
  267. return Promise.resolve(canTransferArrayBuffer()).then(function (
  268. canTransferArrayBuffer
  269. ) {
  270. let transferableObjects;
  271. const binary = wasmConfig.wasmBinary;
  272. if (defined(binary) && canTransferArrayBuffer) {
  273. transferableObjects = [binary];
  274. }
  275. worker.onmessage = function (event) {
  276. worker.onmessage = function (event) {
  277. completeTask(processor, event.data);
  278. };
  279. deferred.resolve(event.data);
  280. };
  281. worker.postMessage(
  282. { webAssemblyConfig: wasmConfig },
  283. transferableObjects
  284. );
  285. });
  286. });
  287. return deferred.promise;
  288. };
  289. /**
  290. * Returns true if this object was destroyed; otherwise, false.
  291. * <br /><br />
  292. * If this object was destroyed, it should not be used; calling any function other than
  293. * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
  294. *
  295. * @returns {Boolean} True if this object was destroyed; otherwise, false.
  296. *
  297. * @see TaskProcessor#destroy
  298. */
  299. TaskProcessor.prototype.isDestroyed = function () {
  300. return false;
  301. };
  302. /**
  303. * Destroys this object. This will immediately terminate the Worker.
  304. * <br /><br />
  305. * Once an object is destroyed, it should not be used; calling any function other than
  306. * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
  307. */
  308. TaskProcessor.prototype.destroy = function () {
  309. if (defined(this._worker)) {
  310. this._worker.terminate();
  311. }
  312. return destroyObject(this);
  313. };
  314. /**
  315. * An event that's raised when a task is completed successfully. Event handlers are passed
  316. * the error object is a task fails.
  317. *
  318. * @type {Event}
  319. *
  320. * @private
  321. */
  322. TaskProcessor.taskCompletedEvent = taskCompletedEvent;
  323. // exposed for testing purposes
  324. TaskProcessor._defaultWorkerModulePrefix = "Workers/";
  325. TaskProcessor._workerModulePrefix = TaskProcessor._defaultWorkerModulePrefix;
  326. TaskProcessor._canTransferArrayBuffer = undefined;
  327. export default TaskProcessor;