summaryrefslogtreecommitdiffstats
path: root/devtools/shared/worker
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/worker')
-rw-r--r--devtools/shared/worker/helper.js133
-rw-r--r--devtools/shared/worker/loader.js517
-rw-r--r--devtools/shared/worker/moz.build13
-rw-r--r--devtools/shared/worker/tests/browser/.eslintrc.js6
-rw-r--r--devtools/shared/worker/tests/browser/browser.ini9
-rw-r--r--devtools/shared/worker/tests/browser/browser_worker-01.js45
-rw-r--r--devtools/shared/worker/tests/browser/browser_worker-02.js46
-rw-r--r--devtools/shared/worker/tests/browser/browser_worker-03.js52
-rw-r--r--devtools/shared/worker/worker.js171
9 files changed, 992 insertions, 0 deletions
diff --git a/devtools/shared/worker/helper.js b/devtools/shared/worker/helper.js
new file mode 100644
index 000000000..69512550b
--- /dev/null
+++ b/devtools/shared/worker/helper.js
@@ -0,0 +1,133 @@
+/* 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/. */
+(function (root, factory) {
+ "use strict";
+
+ if (typeof define === "function" && define.amd) {
+ define(factory);
+ } else if (typeof exports === "object") {
+ module.exports = factory();
+ } else {
+ root.workerHelper = factory();
+ }
+}(this, function () {
+ "use strict";
+
+ /**
+ * This file is to only be included by ChromeWorkers. This exposes
+ * a `createTask` function to workers to register tasks for communication
+ * back to `devtools/shared/worker`.
+ *
+ * Tasks can be send their responses via a return value, either a primitive
+ * or a promise.
+ *
+ * createTask(self, "average", function (data) {
+ * return data.reduce((sum, val) => sum + val, 0) / data.length;
+ * });
+ *
+ * createTask(self, "average", function (data) {
+ * return new Promise((resolve, reject) => {
+ * resolve(data.reduce((sum, val) => sum + val, 0) / data.length);
+ * });
+ * });
+ *
+ *
+ * Errors:
+ *
+ * Returning an Error value, or if the returned promise is rejected, this
+ * propagates to the DevToolsWorker as a rejected promise. If an error is
+ * thrown in a synchronous function, that error is also propagated.
+ */
+
+ /**
+ * Takes a worker's `self` object, a task name, and a function to
+ * be called when that task is called. The task is called with the
+ * passed in data as the first argument
+ *
+ * @param {object} self
+ * @param {string} name
+ * @param {function} fn
+ */
+ function createTask(self, name, fn) {
+ // Store a hash of task name to function on the Worker
+ if (!self._tasks) {
+ self._tasks = {};
+ }
+
+ // Create the onmessage handler if not yet created.
+ if (!self.onmessage) {
+ self.onmessage = createHandler(self);
+ }
+
+ // Store the task on the worker.
+ self._tasks[name] = fn;
+ }
+
+ /**
+ * Creates the `self.onmessage` handler for a Worker.
+ *
+ * @param {object} self
+ * @return {function}
+ */
+ function createHandler(self) {
+ return function (e) {
+ let { id, task, data } = e.data;
+ let taskFn = self._tasks[task];
+
+ if (!taskFn) {
+ self.postMessage({ id, error: `Task "${task}" not found in worker.` });
+ return;
+ }
+
+ try {
+ let results;
+ handleResponse(taskFn(data));
+ } catch (e) {
+ handleError(e);
+ }
+
+ function handleResponse(response) {
+ // If a promise
+ if (response && typeof response.then === "function") {
+ response.then(val => self.postMessage({ id, response: val }), handleError);
+ }
+ // If an error object
+ else if (response instanceof Error) {
+ handleError(response);
+ }
+ // If anything else
+ else {
+ self.postMessage({ id, response });
+ }
+ }
+
+ function handleError(error = "Error") {
+ try {
+ // First, try and structured clone the error across directly.
+ self.postMessage({ id, error });
+ } catch (_) {
+ // We could not clone whatever error value was given. Do our best to
+ // stringify it.
+ let errorString = `Error while performing task "${task}": `;
+
+ try {
+ errorString += error.toString();
+ } catch (_) {
+ errorString += "<could not stringify error>";
+ }
+
+ if ("stack" in error) {
+ try {
+ errorString += "\n" + error.stack;
+ } catch (_) { }
+ }
+
+ self.postMessage({ id, error: errorString });
+ }
+ }
+ };
+ }
+
+ return { createTask: createTask };
+}.bind(this)));
diff --git a/devtools/shared/worker/loader.js b/devtools/shared/worker/loader.js
new file mode 100644
index 000000000..1de72963d
--- /dev/null
+++ b/devtools/shared/worker/loader.js
@@ -0,0 +1,517 @@
+/* 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/. */
+
+"use strict";
+
+// A CommonJS module loader that is designed to run inside a worker debugger.
+// We can't simply use the SDK module loader, because it relies heavily on
+// Components, which isn't available in workers.
+//
+// In principle, the standard instance of the worker loader should provide the
+// same built-in modules as its devtools counterpart, so that both loaders are
+// interchangable on the main thread, making them easier to test.
+//
+// On the worker thread, some of these modules, in particular those that rely on
+// the use of Components, and for which the worker debugger doesn't provide an
+// alternative API, will be replaced by vacuous objects. Consequently, they can
+// still be required, but any attempts to use them will lead to an exception.
+
+this.EXPORTED_SYMBOLS = ["WorkerDebuggerLoader", "worker"];
+
+// Some notes on module ids and URLs:
+//
+// An id is either a relative id or an absolute id. An id is relative if and
+// only if it starts with a dot. An absolute id is a normalized id if and only
+// if it contains no redundant components.
+//
+// Every normalized id is a URL. A URL is either an absolute URL or a relative
+// URL. A URL is absolute if and only if it starts with a scheme name followed
+// by a colon and 2 or 3 slashes.
+
+/**
+ * Convert the given relative id to an absolute id.
+ *
+ * @param String id
+ * The relative id to be resolved.
+ * @param String baseId
+ * The absolute base id to resolve the relative id against.
+ *
+ * @return String
+ * An absolute id
+ */
+function resolveId(id, baseId) {
+ return baseId + "/../" + id;
+}
+
+/**
+ * Convert the given absolute id to a normalized id.
+ *
+ * @param String id
+ * The absolute id to be normalized.
+ *
+ * @return String
+ * A normalized id.
+ */
+function normalizeId(id) {
+ // An id consists of an optional root and a path. A root consists of either
+ // a scheme name followed by 2 or 3 slashes, or a single slash. Slashes in the
+ // root are not used as separators, so only normalize the path.
+ let [_, root, path] = id.match(/^(\w+:\/\/\/?|\/)?(.*)/);
+
+ let stack = [];
+ path.split("/").forEach(function (component) {
+ switch (component) {
+ case "":
+ case ".":
+ break;
+ case "..":
+ if (stack.length === 0) {
+ if (root !== undefined) {
+ throw new Error("Can't normalize absolute id '" + id + "'!");
+ } else {
+ stack.push("..");
+ }
+ } else {
+ if (stack[stack.length - 1] == "..") {
+ stack.push("..");
+ } else {
+ stack.pop();
+ }
+ }
+ break;
+ default:
+ stack.push(component);
+ break;
+ }
+ });
+
+ return (root ? root : "") + stack.join("/");
+}
+
+/**
+ * Create a module object with the given normalized id.
+ *
+ * @param String
+ * The normalized id of the module to be created.
+ *
+ * @return Object
+ * A module with the given id.
+ */
+function createModule(id) {
+ return Object.create(null, {
+ // CommonJS specifies the id property to be non-configurable and
+ // non-writable.
+ id: {
+ configurable: false,
+ enumerable: true,
+ value: id,
+ writable: false
+ },
+
+ // CommonJS does not specify an exports property, so follow the NodeJS
+ // convention, which is to make it non-configurable and writable.
+ exports: {
+ configurable: false,
+ enumerable: true,
+ value: Object.create(null),
+ writable: true
+ }
+ });
+}
+
+/**
+ * Create a CommonJS loader with the following options:
+ * - createSandbox:
+ * A function that will be used to create sandboxes. It should take the name
+ * and prototype of the sandbox to be created, and return the newly created
+ * sandbox as result. This option is required.
+ * - globals:
+ * A map of names to built-in globals that will be exposed to every module.
+ * Defaults to the empty map.
+ * - loadSubScript:
+ * A function that will be used to load scripts in sandboxes. It should take
+ * the URL from and the sandbox in which the script is to be loaded, and not
+ * return a result. This option is required.
+ * - modules:
+ * A map from normalized ids to built-in modules that will be added to the
+ * module cache. Defaults to the empty map.
+ * - paths:
+ * A map of paths to base URLs that will be used to resolve relative URLs to
+ * absolute URLS. Defaults to the empty map.
+ * - resolve:
+ * A function that will be used to resolve relative ids to absolute ids. It
+ * should take the relative id of a module to be required and the absolute
+ * id of the requiring module as arguments, and return the absolute id of
+ * the module to be required as result. Defaults to resolveId above.
+ */
+function WorkerDebuggerLoader(options) {
+ /**
+ * Convert the given relative URL to an absolute URL, using the map of paths
+ * given below.
+ *
+ * @param String url
+ * The relative URL to be resolved.
+ *
+ * @return String
+ * An absolute URL.
+ */
+ function resolveURL(url) {
+ let found = false;
+ for (let [path, baseURL] of paths) {
+ if (url.startsWith(path)) {
+ found = true;
+ url = url.replace(path, baseURL);
+ break;
+ }
+ }
+ if (!found) {
+ throw new Error("Can't resolve relative URL '" + url + "'!");
+ }
+
+ // If the url has no extension, use ".js" by default.
+ return url.endsWith(".js") ? url : url + ".js";
+ }
+
+ /**
+ * Load the given module with the given url.
+ *
+ * @param Object module
+ * The module object to be loaded.
+ * @param String url
+ * The URL to load the module from.
+ */
+ function loadModule(module, url) {
+ // CommonJS specifies 3 free variables: require, exports, and module. These
+ // must be exposed to every module, so define these as properties on the
+ // sandbox prototype. Additional built-in globals are exposed by making
+ // the map of built-in globals the prototype of the sandbox prototype.
+ let prototype = Object.create(globals);
+ prototype.Components = {};
+ prototype.require = createRequire(module);
+ prototype.exports = module.exports;
+ prototype.module = module;
+
+ let sandbox = createSandbox(url, prototype);
+ try {
+ loadSubScript(url, sandbox);
+ } catch (error) {
+ if (/^Error opening input stream/.test(String(error))) {
+ throw new Error("Can't load module '" + module.id + "' with url '" +
+ url + "'!");
+ }
+ throw error;
+ }
+
+ // The value of exports may have been changed by the module script, so
+ // freeze it if and only if it is still an object.
+ if (typeof module.exports === "object" && module.exports !== null) {
+ Object.freeze(module.exports);
+ }
+ }
+
+ /**
+ * Create a require function for the given module. If no module is given,
+ * create a require function for the top-level module instead.
+ *
+ * @param Object requirer
+ * The module for which the require function is to be created.
+ *
+ * @return Function
+ * A require function for the given module.
+ */
+ function createRequire(requirer) {
+ return function require(id) {
+ // Make sure an id was passed.
+ if (id === undefined) {
+ throw new Error("Can't require module without id!");
+ }
+
+ // Built-in modules are cached by id rather than URL, so try to find the
+ // module to be required by id first.
+ let module = modules[id];
+ if (module === undefined) {
+ // Failed to find the module to be required by id, so convert the id to
+ // a URL and try again.
+
+ // If the id is relative, convert it to an absolute id.
+ if (id.startsWith(".")) {
+ if (requirer === undefined) {
+ throw new Error("Can't require top-level module with relative id " +
+ "'" + id + "'!");
+ }
+ id = resolve(id, requirer.id);
+ }
+
+ // Convert the absolute id to a normalized id.
+ id = normalizeId(id);
+
+ // Convert the normalized id to a URL.
+ let url = id;
+
+ // If the URL is relative, resolve it to an absolute URL.
+ if (url.match(/^\w+:\/\//) === null) {
+ url = resolveURL(id);
+ }
+
+ // Try to find the module to be required by URL.
+ module = modules[url];
+ if (module === undefined) {
+ // Failed to find the module to be required in the cache, so create
+ // a new module, load it from the given URL, and add it to the cache.
+
+ // Add modules to the cache early so that any recursive calls to
+ // require for the same module will return the partially-loaded module
+ // from the cache instead of triggering a new load.
+ module = modules[url] = createModule(id);
+
+ try {
+ loadModule(module, url);
+ } catch (error) {
+ // If the module failed to load, remove it from the cache so that
+ // subsequent calls to require for the same module will trigger a
+ // new load, instead of returning a partially-loaded module from
+ // the cache.
+ delete modules[url];
+ throw error;
+ }
+
+ Object.freeze(module);
+ }
+ }
+
+ return module.exports;
+ };
+ }
+
+ let createSandbox = options.createSandbox;
+ let globals = options.globals || Object.create(null);
+ let loadSubScript = options.loadSubScript;
+
+ // Create the module cache, by converting each entry in the map from
+ // normalized ids to built-in modules to a module object, with the exports
+ // property of each module set to a frozen version of the original entry.
+ let modules = options.modules || {};
+ for (let id in modules) {
+ let module = createModule(id);
+ module.exports = Object.freeze(modules[id]);
+ modules[id] = module;
+ }
+
+ // Convert the map of paths to base URLs into an array for use by resolveURL.
+ // The array is sorted from longest to shortest path to ensure that the
+ // longest path is always the first to be found.
+ let paths = options.paths || Object.create(null);
+ paths = Object.keys(paths)
+ .sort((a, b) => b.length - a.length)
+ .map(path => [path, paths[path]]);
+
+ let resolve = options.resolve || resolveId;
+
+ this.require = createRequire();
+}
+
+this.WorkerDebuggerLoader = WorkerDebuggerLoader;
+
+// The following APIs rely on the use of Components, and the worker debugger
+// does not provide alternative definitions for them. Consequently, they are
+// stubbed out both on the main thread and worker threads.
+
+var chrome = {
+ CC: undefined,
+ Cc: undefined,
+ ChromeWorker: undefined,
+ Cm: undefined,
+ Ci: undefined,
+ Cu: undefined,
+ Cr: undefined,
+ components: undefined
+};
+
+var loader = {
+ lazyGetter: function (object, name, lambda) {
+ Object.defineProperty(object, name, {
+ get: function () {
+ delete object[name];
+ return object[name] = lambda.apply(object);
+ },
+ configurable: true,
+ enumerable: true
+ });
+ },
+ lazyImporter: function () {
+ throw new Error("Can't import JSM from worker thread!");
+ },
+ lazyServiceGetter: function () {
+ throw new Error("Can't import XPCOM service from worker thread!");
+ },
+ lazyRequireGetter: function (obj, property, module, destructure) {
+ Object.defineProperty(obj, property, {
+ get: () => destructure ? worker.require(module)[property]
+ : worker.require(module || property)
+ });
+ }
+};
+
+// The following APIs are defined differently depending on whether we are on the
+// main thread or a worker thread. On the main thread, we use the Components
+// object to implement them. On worker threads, we use the APIs provided by
+// the worker debugger.
+
+var {
+ Debugger,
+ URL,
+ createSandbox,
+ dump,
+ rpc,
+ loadSubScript,
+ reportError,
+ setImmediate,
+ xpcInspector
+} = (function () {
+ if (typeof Components === "object") { // Main thread
+ let {
+ Constructor: CC,
+ classes: Cc,
+ manager: Cm,
+ interfaces: Ci,
+ results: Cr,
+ utils: Cu
+ } = Components;
+
+ let principal = CC("@mozilla.org/systemprincipal;1", "nsIPrincipal")();
+
+ // To ensure that the this passed to addDebuggerToGlobal is a global, the
+ // Debugger object needs to be defined in a sandbox.
+ let sandbox = Cu.Sandbox(principal, {});
+ Cu.evalInSandbox(
+ "Components.utils.import('resource://gre/modules/jsdebugger.jsm');" +
+ "addDebuggerToGlobal(this);",
+ sandbox
+ );
+ let Debugger = sandbox.Debugger;
+
+ let createSandbox = function (name, prototype) {
+ return Cu.Sandbox(principal, {
+ invisibleToDebugger: true,
+ sandboxName: name,
+ sandboxPrototype: prototype,
+ wantComponents: false,
+ wantXrays: false
+ });
+ };
+
+ let rpc = undefined;
+
+ let subScriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
+ getService(Ci.mozIJSSubScriptLoader);
+
+ let loadSubScript = function (url, sandbox) {
+ subScriptLoader.loadSubScript(url, sandbox, "UTF-8");
+ };
+
+ let reportError = Cu.reportError;
+
+ let Timer = Cu.import("resource://gre/modules/Timer.jsm", {});
+
+ let setImmediate = function (callback) {
+ Timer.setTimeout(callback, 0);
+ };
+
+ let xpcInspector = Cc["@mozilla.org/jsinspector;1"].
+ getService(Ci.nsIJSInspector);
+
+ return {
+ Debugger,
+ URL: this.URL,
+ createSandbox,
+ dump: this.dump,
+ rpc,
+ loadSubScript,
+ reportError,
+ setImmediate,
+ xpcInspector
+ };
+ } else { // Worker thread
+ let requestors = [];
+
+ let scope = this;
+
+ let xpcInspector = {
+ get eventLoopNestLevel() {
+ return requestors.length;
+ },
+
+ get lastNestRequestor() {
+ return requestors.length === 0 ? null : requestors[requestors.length - 1];
+ },
+
+ enterNestedEventLoop: function (requestor) {
+ requestors.push(requestor);
+ scope.enterEventLoop();
+ return requestors.length;
+ },
+
+ exitNestedEventLoop: function () {
+ requestors.pop();
+ scope.leaveEventLoop();
+ return requestors.length;
+ }
+ };
+
+ return {
+ Debugger: this.Debugger,
+ URL: this.URL,
+ createSandbox: this.createSandbox,
+ dump: this.dump,
+ rpc: this.rpc,
+ loadSubScript: this.loadSubScript,
+ reportError: this.reportError,
+ setImmediate: this.setImmediate,
+ xpcInspector: xpcInspector
+ };
+ }
+}).call(this);
+
+// Create the default instance of the worker loader, using the APIs we defined
+// above.
+
+this.worker = new WorkerDebuggerLoader({
+ createSandbox: createSandbox,
+ globals: {
+ "isWorker": true,
+ "dump": dump,
+ "loader": loader,
+ "reportError": reportError,
+ "rpc": rpc,
+ "setImmediate": setImmediate,
+ "URL": URL,
+ },
+ loadSubScript: loadSubScript,
+ modules: {
+ "Debugger": Debugger,
+ "Services": Object.create(null),
+ "chrome": chrome,
+ "xpcInspector": xpcInspector
+ },
+ paths: {
+ // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
+ "": "resource://gre/modules/commonjs/",
+ // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
+ // Modules here are intended to have one implementation for
+ // chrome, and a separate implementation for content. Here we
+ // map the directory to the chrome subdirectory, but the content
+ // loader will map to the content subdirectory. See the
+ // README.md in devtools/shared/platform.
+ "devtools/shared/platform": "resource://devtools/shared/platform/chrome",
+ // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
+ "devtools": "resource://devtools",
+ // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
+ "promise": "resource://gre/modules/Promise-backend.js",
+ // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
+ "source-map": "resource://devtools/shared/sourcemap/source-map.js",
+ // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
+ "xpcshell-test": "resource://test"
+ // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
+ }
+});
diff --git a/devtools/shared/worker/moz.build b/devtools/shared/worker/moz.build
new file mode 100644
index 000000000..28b26c0a7
--- /dev/null
+++ b/devtools/shared/worker/moz.build
@@ -0,0 +1,13 @@
+# -*- 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/.
+
+BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
+
+DevToolsModules(
+ 'helper.js',
+ 'loader.js',
+ 'worker.js',
+)
diff --git a/devtools/shared/worker/tests/browser/.eslintrc.js b/devtools/shared/worker/tests/browser/.eslintrc.js
new file mode 100644
index 000000000..698ae9181
--- /dev/null
+++ b/devtools/shared/worker/tests/browser/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/shared/worker/tests/browser/browser.ini b/devtools/shared/worker/tests/browser/browser.ini
new file mode 100644
index 000000000..a64916dff
--- /dev/null
+++ b/devtools/shared/worker/tests/browser/browser.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ ../../../../server/tests/browser/head.js
+
+[browser_worker-01.js]
+[browser_worker-02.js]
+[browser_worker-03.js]
diff --git a/devtools/shared/worker/tests/browser/browser_worker-01.js b/devtools/shared/worker/tests/browser/browser_worker-01.js
new file mode 100644
index 000000000..7679e4166
--- /dev/null
+++ b/devtools/shared/worker/tests/browser/browser_worker-01.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the devtools/shared/worker communicates properly
+// as both CommonJS module and as a JSM.
+
+const WORKER_URL =
+ "resource://devtools/client/shared/widgets/GraphsWorker.js";
+
+const count = 100000;
+const WORKER_DATA = (function () {
+ let timestamps = [];
+ for (let i = 0; i < count; i++) {
+ timestamps.push(i);
+ }
+ return timestamps;
+})();
+const INTERVAL = 100;
+const DURATION = 1000;
+
+add_task(function* () {
+ // Test both CJS and JSM versions
+
+ yield testWorker("JSM", () => Cu.import("resource://devtools/shared/worker/worker.js", {}));
+ yield testWorker("CommonJS", () => require("devtools/shared/worker/worker"));
+});
+
+function* testWorker(context, workerFactory) {
+ let { DevToolsWorker, workerify } = workerFactory();
+ let worker = new DevToolsWorker(WORKER_URL);
+ let results = yield worker.performTask("plotTimestampsGraph", {
+ timestamps: WORKER_DATA,
+ interval: INTERVAL,
+ duration: DURATION
+ });
+
+ ok(results.plottedData.length,
+ `worker should have returned an object with array properties in ${context}`);
+
+ let fn = workerify(function (x) { return x * x; });
+ is((yield fn(5)), 25, `workerify works in ${context}`);
+ fn.destroy();
+
+ worker.destroy();
+}
diff --git a/devtools/shared/worker/tests/browser/browser_worker-02.js b/devtools/shared/worker/tests/browser/browser_worker-02.js
new file mode 100644
index 000000000..e6a9a54cf
--- /dev/null
+++ b/devtools/shared/worker/tests/browser/browser_worker-02.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests errors are handled properly by the DevToolsWorker.
+
+const { DevToolsWorker } = require("devtools/shared/worker/worker");
+const WORKER_URL =
+ "resource://devtools/client/shared/widgets/GraphsWorker.js";
+
+add_task(function* () {
+ try {
+ let workerNotFound = new DevToolsWorker("resource://i/dont/exist.js");
+ ok(false, "Creating a DevToolsWorker with an invalid URL throws");
+ } catch (e) {
+ ok(true, "Creating a DevToolsWorker with an invalid URL throws");
+ }
+
+ let worker = new DevToolsWorker(WORKER_URL);
+ try {
+ // plotTimestampsGraph requires timestamp, interval an duration props on the object
+ // passed in so there should be an error thrown in the worker
+ let results = yield worker.performTask("plotTimestampsGraph", {});
+ ok(false, "DevToolsWorker returns a rejected promise when an error occurs in the worker");
+ } catch (e) {
+ ok(true, "DevToolsWorker returns a rejected promise when an error occurs in the worker");
+ }
+
+ try {
+ let results = yield worker.performTask("not a real task");
+ ok(false, "DevToolsWorker returns a rejected promise when task does not exist");
+ } catch (e) {
+ ok(true, "DevToolsWorker returns a rejected promise when task does not exist");
+ }
+
+ worker.destroy();
+ try {
+ let results = yield worker.performTask("plotTimestampsGraph", {
+ timestamps: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ interval: 1,
+ duration: 1
+ });
+ ok(false, "DevToolsWorker rejects when performing a task on a destroyed worker");
+ } catch (e) {
+ ok(true, "DevToolsWorker rejects when performing a task on a destroyed worker");
+ }
+});
diff --git a/devtools/shared/worker/tests/browser/browser_worker-03.js b/devtools/shared/worker/tests/browser/browser_worker-03.js
new file mode 100644
index 000000000..053e381b9
--- /dev/null
+++ b/devtools/shared/worker/tests/browser/browser_worker-03.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the devtools/shared/worker can handle:
+// returned primitives (or promise or Error)
+//
+// And tests `workerify` by doing so.
+
+const { DevToolsWorker, workerify } = require("devtools/shared/worker/worker");
+function square(x) {
+ return x * x;
+}
+
+function squarePromise(x) {
+ return new Promise((resolve) => resolve(x * x));
+}
+
+function squareError(x) {
+ return new Error("Nope");
+}
+
+function squarePromiseReject(x) {
+ return new Promise((_, reject) => reject("Nope"));
+}
+
+add_task(function* () {
+ let fn = workerify(square);
+ is((yield fn(5)), 25, "return primitives successful");
+ fn.destroy();
+
+ fn = workerify(squarePromise);
+ is((yield fn(5)), 25, "promise primitives successful");
+ fn.destroy();
+
+ fn = workerify(squareError);
+ try {
+ yield fn(5);
+ ok(false, "return error should reject");
+ } catch (e) {
+ ok(true, "return error should reject");
+ }
+ fn.destroy();
+
+ fn = workerify(squarePromiseReject);
+ try {
+ yield fn(5);
+ ok(false, "returned rejected promise rejects");
+ } catch (e) {
+ ok(true, "returned rejected promise rejects");
+ }
+ fn.destroy();
+});
diff --git a/devtools/shared/worker/worker.js b/devtools/shared/worker/worker.js
new file mode 100644
index 000000000..9ed9afa27
--- /dev/null
+++ b/devtools/shared/worker/worker.js
@@ -0,0 +1,171 @@
+/* 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/. */
+
+"use strict";
+
+/* global ChromeWorker */
+
+(function (factory) {
+ if (this.module && module.id.indexOf("worker") >= 0) {
+ // require
+ const { Cc, Ci, Cu, ChromeWorker } = require("chrome");
+ const dumpn = require("devtools/shared/DevToolsUtils").dumpn;
+ factory.call(this, require, exports, module, { Cc, Ci, Cu }, ChromeWorker, dumpn);
+ } else {
+ // Cu.import
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+ const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+ this.isWorker = false;
+ this.Promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
+ this.console = Cu.import("resource://gre/modules/Console.jsm", {}).console;
+ factory.call(
+ this, require, this, { exports: this },
+ { Cc, Ci, Cu }, ChromeWorker, null
+ );
+ this.EXPORTED_SYMBOLS = ["DevToolsWorker"];
+ }
+}).call(this, function (require, exports, module, { Ci, Cc }, ChromeWorker, dumpn) {
+ let MESSAGE_COUNTER = 0;
+
+ /**
+ * Creates a wrapper around a ChromeWorker, providing easy
+ * communication to offload demanding tasks. The corresponding URL
+ * must implement the interface provided by `devtools/shared/worker/helper`.
+ *
+ * @see `./devtools/client/shared/widgets/GraphsWorker.js`
+ *
+ * @param {string} url
+ * The URL of the worker.
+ * @param Object opts
+ * An option with the following optional fields:
+ * - name: a name that will be printed with logs
+ * - verbose: log incoming and outgoing messages
+ */
+ function DevToolsWorker(url, opts) {
+ opts = opts || {};
+ this._worker = new ChromeWorker(url);
+ this._verbose = opts.verbose;
+ this._name = opts.name;
+
+ this._worker.addEventListener("error", this.onError, false);
+ }
+ exports.DevToolsWorker = DevToolsWorker;
+
+ /**
+ * Performs the given task in a chrome worker, passing in data.
+ * Returns a promise that resolves when the task is completed, resulting in
+ * the return value of the task.
+ *
+ * @param {string} task
+ * The name of the task to execute in the worker.
+ * @param {any} data
+ * Data to be passed into the task implemented by the worker.
+ * @return {Promise}
+ */
+ DevToolsWorker.prototype.performTask = function (task, data) {
+ if (this._destroyed) {
+ return Promise.reject("Cannot call performTask on a destroyed DevToolsWorker");
+ }
+ let worker = this._worker;
+ let id = ++MESSAGE_COUNTER;
+ let payload = { task, id, data };
+
+ if (this._verbose && dumpn) {
+ dumpn("Sending message to worker" +
+ (this._name ? (" (" + this._name + ")") : "") +
+ ": " +
+ JSON.stringify(payload, null, 2));
+ }
+ worker.postMessage(payload);
+
+ return new Promise((resolve, reject) => {
+ let listener = ({ data: result }) => {
+ if (this._verbose && dumpn) {
+ dumpn("Received message from worker" +
+ (this._name ? (" (" + this._name + ")") : "") +
+ ": " +
+ JSON.stringify(result, null, 2));
+ }
+
+ if (result.id !== id) {
+ return;
+ }
+ worker.removeEventListener("message", listener);
+ if (result.error) {
+ reject(result.error);
+ } else {
+ resolve(result.response);
+ }
+ };
+
+ worker.addEventListener("message", listener);
+ });
+ };
+
+ /**
+ * Terminates the underlying worker. Use when no longer needing the worker.
+ */
+ DevToolsWorker.prototype.destroy = function () {
+ this._worker.terminate();
+ this._worker = null;
+ this._destroyed = true;
+ };
+
+ DevToolsWorker.prototype.onError = function ({ message, filename, lineno }) {
+ dump(new Error(message + " @ " + filename + ":" + lineno) + "\n");
+ };
+
+ /**
+ * Takes a function and returns a Worker-wrapped version of the same function.
+ * Returns a promise upon resolution.
+ * @see `./devtools/shared/shared/tests/browser/browser_devtools-worker-03.js
+ *
+ * ⚠ This should only be used for tests or A/B testing performance ⚠
+ *
+ * The original function must:
+ *
+ * Be a pure function, that is, not use any variables not declared within the
+ * function, or its arguments.
+ *
+ * Return a value or a promise.
+ *
+ * Note any state change in the worker will not affect the callee's context.
+ *
+ * @param {function} fn
+ * @return {function}
+ */
+ function workerify(fn) {
+ console.warn("`workerify` should only be used in tests or measuring performance. " +
+ "This creates an object URL on the browser window, and should not be " +
+ "used in production.");
+ // Fetch via window/utils here as we don't want to include
+ // this module normally.
+ let { getMostRecentBrowserWindow } = require("sdk/window/utils");
+ let { URL, Blob } = getMostRecentBrowserWindow();
+ let stringifiedFn = createWorkerString(fn);
+ let blob = new Blob([stringifiedFn]);
+ let url = URL.createObjectURL(blob);
+ let worker = new DevToolsWorker(url);
+
+ let wrapperFn = data => worker.performTask("workerifiedTask", data);
+
+ wrapperFn.destroy = function () {
+ URL.revokeObjectURL(url);
+ worker.destroy();
+ };
+
+ return wrapperFn;
+ }
+ exports.workerify = workerify;
+
+ /**
+ * Takes a function, and stringifies it, attaching the worker-helper.js
+ * boilerplate hooks.
+ */
+ function createWorkerString(fn) {
+ return `importScripts("resource://gre/modules/workers/require.js");
+ const { createTask } = require("resource://devtools/shared/worker/helper.js");
+ createTask(self, "workerifiedTask", ${fn.toString()});`;
+ }
+});