/* 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.includes("worker")) { // require const dumpn = require("devtools/shared/DevToolsUtils").dumpn; factory.call(this, require, exports, module, ChromeWorker, dumpn); } else { // Cu.import const { require } = ChromeUtils.importESModule( "resource://devtools/shared/loader/Loader.sys.mjs" ); this.isWorker = false; this.console = console; factory.call(this, require, this, { exports: this }, ChromeWorker, null); this.EXPORTED_SYMBOLS = ["DevToolsWorker", "workerify"]; } }).call(this, function (require, exports, module, 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`. * * @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); } 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. * @param {undefined|Array} transfer * Optional array of transferable objects to transfer ownership of. * @return {Promise} */ DevToolsWorker.prototype.performTask = function (task, data, transfer) { if (this._destroyed) { return Promise.reject( "Cannot call performTask on a destroyed DevToolsWorker" ); } const worker = this._worker; const id = ++MESSAGE_COUNTER; const 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, transfer); return new Promise((resolve, reject) => { const 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 modules here as we don't want to include it normally. const { URL, Blob } = Services.wm.getMostRecentWindow("navigator:browser"); const stringifiedFn = createWorkerString(fn); const blob = new Blob([stringifiedFn]); const url = URL.createObjectURL(blob); const worker = new DevToolsWorker(url); const wrapperFn = (data, transfer) => worker.performTask("workerifiedTask", data, transfer); 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()});`; } });