diff options
Diffstat (limited to 'toolkit/components/promiseworker/worker')
-rw-r--r-- | toolkit/components/promiseworker/worker/PromiseWorker.js | 206 | ||||
-rw-r--r-- | toolkit/components/promiseworker/worker/moz.build | 9 |
2 files changed, 215 insertions, 0 deletions
diff --git a/toolkit/components/promiseworker/worker/PromiseWorker.js b/toolkit/components/promiseworker/worker/PromiseWorker.js new file mode 100644 index 000000000..ba4408c1a --- /dev/null +++ b/toolkit/components/promiseworker/worker/PromiseWorker.js @@ -0,0 +1,206 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * A wrapper around `self` with extended capabilities designed + * to simplify main thread-to-worker thread asynchronous function calls. + * + * This wrapper: + * - groups requests and responses as a method `post` that returns a `Promise`; + * - ensures that exceptions thrown on the worker thread are correctly serialized; + * - provides some utilities for benchmarking various operations. + * + * Generally, you should use PromiseWorker.js along with its main thread-side + * counterpart PromiseWorker.jsm. + */ + +"use strict"; + +if (typeof Components != "undefined") { + throw new Error("This module is meant to be used from the worker thread"); +} +if (typeof require == "undefined" || typeof module == "undefined") { + throw new Error("this module is meant to be imported using the implementation of require() at resource://gre/modules/workers/require.js"); +} + +importScripts("resource://gre/modules/workers/require.js"); + +/** + * Built-in JavaScript exceptions that may be serialized without + * loss of information. + */ +const EXCEPTION_NAMES = { + EvalError: "EvalError", + InternalError: "InternalError", + RangeError: "RangeError", + ReferenceError: "ReferenceError", + SyntaxError: "SyntaxError", + TypeError: "TypeError", + URIError: "URIError", +}; + +/** + * A constructor used to return data to the caller thread while + * also executing some specific treatment (e.g. shutting down + * the current thread, transmitting data instead of copying it). + * + * @param {object=} data The data to return to the caller thread. + * @param {object=} meta Additional instructions, as an object + * that may contain the following fields: + * - {bool} shutdown If |true|, shut down the current thread after + * having sent the result. + * - {Array} transfers An array of objects that should be transferred + * instead of being copied. + * + * @constructor + */ +function Meta(data, meta) { + this.data = data; + this.meta = meta; +} +exports.Meta = Meta; + +/** + * Base class for a worker. + * + * Derived classes are expected to provide the following methods: + * { + * dispatch: function(method, args) { + * // Dispatch a call to method `method` with args `args` + * }, + * log: function(...msg) { + * // Log (or discard) messages (optional) + * }, + * postMessage: function(message, ...transfers) { + * // Post a message to the main thread + * }, + * close: function() { + * // Close the worker + * } + * } + * + * By default, the AbstractWorker is not connected to a message port, + * hence will not receive anything. + * + * To connect it, use `onmessage`, as follows: + * self.addEventListener("message", msg => myWorkerInstance.handleMessage(msg)); + */ +function AbstractWorker(agent) { + this._agent = agent; +} +AbstractWorker.prototype = { + // Default logger: discard all messages + log: function() { + }, + + /** + * Handle a message. + */ + handleMessage: function(msg) { + let data = msg.data; + this.log("Received message", data); + let id = data.id; + + let start; + let options; + if (data.args) { + options = data.args[data.args.length - 1]; + } + // If |outExecutionDuration| option was supplied, start measuring the + // duration of the operation. + if (options && typeof options === "object" && "outExecutionDuration" in options) { + start = Date.now(); + } + + let result; + let exn; + let durationMs; + let method = data.fun; + try { + this.log("Calling method", method); + result = this.dispatch(method, data.args); + this.log("Method", method, "succeeded"); + } catch (ex) { + exn = ex; + this.log("Error while calling agent method", method, exn, exn.moduleStack || exn.stack || ""); + } + + if (start) { + // Record duration + durationMs = Date.now() - start; + this.log("Method took", durationMs, "ms"); + } + + // Now, post a reply, possibly as an uncaught error. + // We post this message from outside the |try ... catch| block + // to avoid capturing errors that take place during |postMessage| and + // built-in serialization. + if (!exn) { + this.log("Sending positive reply", result, "id is", id); + if (result instanceof Meta) { + if ("transfers" in result.meta) { + // Take advantage of zero-copy transfers + this.postMessage({ok: result.data, id: id, durationMs: durationMs}, + result.meta.transfers); + } else { + this.postMessage({ok: result.data, id:id, durationMs: durationMs}); + } + if (result.meta.shutdown || false) { + // Time to close the worker + this.close(); + } + } else { + this.postMessage({ok: result, id:id, durationMs: durationMs}); + } + } else if (exn.constructor.name in EXCEPTION_NAMES) { + // Rather than letting the DOM mechanism [de]serialize built-in + // JS errors, which loses lots of information (in particular, + // the constructor name, the moduleName and the moduleStack), + // we [de]serialize them manually with a little more care. + this.log("Sending back exception", exn.constructor.name, "id is", id); + let error = { + exn: exn.constructor.name, + message: exn.message, + fileName: exn.moduleName || exn.fileName, + lineNumber: exn.lineNumber, + stack: exn.moduleStack + }; + this.postMessage({fail: error, id: id, durationMs: durationMs}); + } else if (exn == StopIteration) { + // StopIteration is a well-known singleton, and requires a + // slightly different treatment. + this.log("Sending back StopIteration, id is", id); + let error = { + exn: "StopIteration" + }; + this.postMessage({fail: error, id: id, durationMs: durationMs}); + } else if ("toMsg" in exn) { + // Extension mechanism for exception [de]serialization. We + // assume that any exception with a method `toMsg()` knows how + // to serialize itself. The other side is expected to have + // registered a deserializer using the `ExceptionHandlers` + // object. + this.log("Sending back an error that knows how to serialize itself", exn, "id is", id); + let msg = exn.toMsg(); + this.postMessage({fail: msg, id:id, durationMs: durationMs}); + } else { + // If we encounter an exception for which we have no + // serialization mechanism in place, we have no choice but to + // let the DOM handle said [de]serialization. We can just + // attempt to mitigate the data loss by injecting `moduleName` and + // `moduleStack`. + this.log("Sending back regular error", exn, exn.moduleStack || exn.stack, "id is", id); + + try { + // Attempt to introduce human-readable filename and stack + exn.filename = exn.moduleName; + exn.stack = exn.moduleStack; + } catch (_) { + // Nothing we can do + } + throw exn; + } + } +}; +exports.AbstractWorker = AbstractWorker; diff --git a/toolkit/components/promiseworker/worker/moz.build b/toolkit/components/promiseworker/worker/moz.build new file mode 100644 index 000000000..305d49838 --- /dev/null +++ b/toolkit/components/promiseworker/worker/moz.build @@ -0,0 +1,9 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +EXTRA_JS_MODULES.workers = [ + 'PromiseWorker.js', +] |