Bug 1514856 - move image data array traversal to worker thread when calculating contrast ratio for text nodes. r=jdescottes,ochameau
authorYura Zenevich <yura.zenevich@gmail.com>
Wed, 16 Jan 2019 19:00:45 +0000
changeset 514124 4fe5d0b7257233694b6c069d9b3c6c8a690943b3
parent 514123 ee36ea46016d1bbe51bbc1e5d6c398f1dc3f1a83
child 514125 50582093318ac50411fc4ddc0a4a730fb0b748d4
push id1953
push userffxbld-merge
push dateMon, 11 Mar 2019 12:10:20 +0000
treeherdermozilla-release@9c35dcbaa899 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjdescottes, ochameau
bugs1514856
milestone66.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1514856 - move image data array traversal to worker thread when calculating contrast ratio for text nodes. r=jdescottes,ochameau MozReview-Commit-ID: K3twiMih7e9 Differential Revision: https://phabricator.services.mozilla.com/D15113
devtools/server/actors/accessibility/accessible.js
devtools/server/actors/accessibility/contrast.js
devtools/server/actors/accessibility/moz.build
devtools/server/actors/accessibility/worker.js
devtools/server/actors/utils/accessibility.js
devtools/shared/worker/tests/browser/browser_worker-01.js
devtools/shared/worker/worker.js
--- a/devtools/server/actors/accessibility/accessible.js
+++ b/devtools/server/actors/accessibility/accessible.js
@@ -3,17 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { Ci, Cu } = require("chrome");
 const { Actor, ActorClassWithSpec } = require("devtools/shared/protocol");
 const { accessibleSpec } = require("devtools/shared/specs/accessibility");
 
-loader.lazyRequireGetter(this, "getContrastRatioFor", "devtools/server/actors/utils/accessibility", true);
+loader.lazyRequireGetter(this, "getContrastRatioFor", "devtools/server/actors/accessibility/contrast", true);
 loader.lazyRequireGetter(this, "isDefunct", "devtools/server/actors/utils/accessibility", true);
 loader.lazyRequireGetter(this, "findCssSelector", "devtools/shared/inspector/css-logic", true);
 
 const nsIAccessibleRelation = Ci.nsIAccessibleRelation;
 const RELATIONS_TO_IGNORE = new Set([
   nsIAccessibleRelation.RELATION_CONTAINING_APPLICATION,
   nsIAccessibleRelation.RELATION_CONTAINING_TAB_PANE,
   nsIAccessibleRelation.RELATION_CONTAINING_WINDOW,
@@ -375,37 +375,46 @@ const AccessibleActor = ActorClassWithSp
 
   get _nonEmptyTextLeafs() {
     return this.children().filter(child => this._isValidTextLeaf(child.rawAccessible));
   },
 
   /**
    * Calculate the contrast ratio of the given accessible.
    */
-  _getContrastRatio() {
+  async _getContrastRatio() {
     if (!this._isValidTextLeaf(this.rawAccessible)) {
       return null;
     }
 
     const { DOMNode: rawNode } = this.rawAccessible;
-    return getContrastRatioFor(rawNode.parentNode, {
+    const contrastRatio = await getContrastRatioFor(rawNode.parentNode, {
       bounds: this.bounds,
       win: rawNode.ownerGlobal,
     });
+
+    return contrastRatio;
   },
 
   /**
    * Audit the state of the accessible object.
    *
    * @return {Object|null}
    *         Audit results for the accessible object.
   */
   async audit() {
+    // More audit steps will be added here in the near future. In addition to colour
+    // contrast ratio we will add autits for to the missing names, invalid states, etc.
+    // (For example see bug 1518808).
+    const [ contrastRatio ] = await Promise.all([
+      this._getContrastRatio(),
+    ]);
+
     return this.isDefunct ? null : {
-      contrastRatio: this._getContrastRatio(),
+      contrastRatio,
     };
   },
 
   snapshot() {
     return getSnapshot(this.rawAccessible, this.walker.a11yService);
   },
 });
 
copy from devtools/server/actors/utils/accessibility.js
copy to devtools/server/actors/accessibility/contrast.js
--- a/devtools/server/actors/utils/accessibility.js
+++ b/devtools/server/actors/accessibility/contrast.js
@@ -1,25 +1,27 @@
 /* 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";
 
-loader.lazyRequireGetter(this, "Ci", "chrome", true);
 loader.lazyRequireGetter(this, "colorUtils", "devtools/shared/css/color", true);
 loader.lazyRequireGetter(this, "CssLogic", "devtools/server/actors/inspector/css-logic", true);
 loader.lazyRequireGetter(this, "getBounds", "devtools/server/actors/highlighters/utils/accessibility", true);
 loader.lazyRequireGetter(this, "getCurrentZoom", "devtools/shared/layout/utils", true);
-loader.lazyRequireGetter(this, "Services");
 loader.lazyRequireGetter(this, "addPseudoClassLock", "devtools/server/actors/highlighters/utils/markup", true);
 loader.lazyRequireGetter(this, "removePseudoClassLock", "devtools/server/actors/highlighters/utils/markup", true);
+loader.lazyRequireGetter(this, "DevToolsWorker", "devtools/shared/worker/worker", true);
 
+const WORKER_URL = "resource://devtools/server/actors/accessibility/worker.js";
 const HIGHLIGHTED_PSEUDO_CLASS = ":-moz-devtools-highlighted";
 
+loader.lazyGetter(this, "worker", () => new DevToolsWorker(WORKER_URL));
+
 /**
  * Get text style properties for a given node, if possible.
  * @param  {DOMNode} node
  *         DOM node for which text styling information is to be calculated.
  * @return {Object}
  *         Color and text size information for a given DOM node.
  */
 function getTextProperties(node) {
@@ -88,109 +90,51 @@ function getImageCtx(win, bounds, node) 
   if (node) {
     removePseudoClassLock(node, HIGHLIGHTED_PSEUDO_CLASS);
   }
 
   return ctx;
 }
 
 /**
- * Get RGBA or a range of RGBAs for the background pixels under the text. If luminance is
- * uniform, only return one value of RGBA, otherwise return values that correspond to the
- * min and max luminances.
- * @param  {ImageData} dataText
- *         pixel data for the accessible object with text visible.
- * @param  {ImageData} dataBackground
- *         pixel data for the accessible object with transparent text.
- * @return {Object}
- *         RGBA or a range of RGBAs with min and max values.
- */
-function getBgRGBA(dataText, dataBackground) {
-  let min = [0, 0, 0, 1];
-  let max = [255, 255, 255, 1];
-  let minLuminance = 1;
-  let maxLuminance = 0;
-  const luminances = {};
-
-  let foundDistinctColor = false;
-  for (let i = 0; i < dataText.length; i = i + 4) {
-    const tR = dataText[i];
-    const bgR = dataBackground[i];
-    const tG = dataText[i + 1];
-    const bgG = dataBackground[i + 1];
-    const tB = dataText[i + 2];
-    const bgB = dataBackground[i + 2];
-
-    // Ignore pixels that are the same where pixels that are different between the two
-    // images are assumed to belong to the text within the node.
-    if (tR === bgR && tG === bgG && tB === bgB) {
-      continue;
-    }
-
-    foundDistinctColor = true;
-
-    const bgColor = `rgb(${bgR}, ${bgG}, ${bgB})`;
-    let luminance = luminances[bgColor];
-
-    if (!luminance) {
-      // Calculate luminance for the RGB value and store it to only measure once.
-      luminance = colorUtils.calculateLuminance([bgR, bgG, bgB]);
-      luminances[bgColor] = luminance;
-    }
-
-    if (minLuminance >= luminance) {
-      minLuminance = luminance;
-      min = [bgR, bgG, bgB, 1];
-    }
-
-    if (maxLuminance <= luminance) {
-      maxLuminance = luminance;
-      max = [bgR, bgG, bgB, 1];
-    }
-  }
-
-  if (!foundDistinctColor) {
-    return null;
-  }
-
-  return minLuminance === maxLuminance ? { value: max } : { min, max };
-}
-
-/**
  * Calculates the contrast ratio of the referenced DOM node.
  *
  * @param  {DOMNode} node
  *         The node for which we want to calculate the contrast ratio.
  * @param  {Object}  options
  *         - bounds   {Object}
  *                    Bounds for the accessible object.
  *         - win      {Object}
  *                    Target window.
  *
  * @return {Object}
  *         An object that may contain one or more of the following fields: error,
  *         isLargeText, value, min, max values for contrast.
 */
-function getContrastRatioFor(node, options = {}) {
+async function getContrastRatioFor(node, options = {}) {
   const props = getTextProperties(node);
   if (!props) {
     return {
       error: true,
     };
   }
 
   const bounds = getBounds(options.win, options.bounds);
   const textContext = getImageCtx(options.win, bounds);
   const backgroundContext = getImageCtx(options.win, bounds, node);
 
   const { data: dataText } = textContext.getImageData(0, 0, bounds.width, bounds.height);
   const { data: dataBackground } = backgroundContext.getImageData(
     0, 0, bounds.width, bounds.height);
 
-  const rgba = getBgRGBA(dataText, dataBackground);
+  const rgba = await worker.performTask("getBgRGBA", {
+    dataTextBuf: dataText.buffer,
+    dataBackgroundBuf: dataBackground.buffer,
+  }, [ dataText.buffer, dataBackground.buffer ]);
+
   if (!rgba) {
     return {
       error: true,
     };
   }
 
   const { color, isLargeText } = props;
   if (rgba.value) {
@@ -216,40 +160,9 @@ function getContrastRatioFor(node, optio
     max,
     color,
     backgroundColorMin: rgba.min,
     backgroundColorMax: rgba.max,
     isLargeText,
   };
 }
 
-/**
- * Helper function that determines if nsIAccessible object is in defunct state.
- *
- * @param  {nsIAccessible}  accessible
- *         object to be tested.
- * @return {Boolean}
- *         True if accessible object is defunct, false otherwise.
- */
-function isDefunct(accessible) {
-  // If accessibility is disabled, safely assume that the accessible object is
-  // now dead.
-  if (!Services.appinfo.accessibilityEnabled) {
-    return true;
-  }
-
-  let defunct = false;
-
-  try {
-    const extraState = {};
-    accessible.getState({}, extraState);
-    // extraState.value is a bitmask. We are applying bitwise AND to mask out
-    // irrelevant states.
-    defunct = !!(extraState.value & Ci.nsIAccessibleStates.EXT_STATE_DEFUNCT);
-  } catch (e) {
-    defunct = true;
-  }
-
-  return defunct;
-}
-
 exports.getContrastRatioFor = getContrastRatioFor;
-exports.isDefunct = isDefunct;
--- a/devtools/server/actors/accessibility/moz.build
+++ b/devtools/server/actors/accessibility/moz.build
@@ -1,13 +1,15 @@
 # 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/.
 
 DevToolsModules(
   'accessibility-parent.js',
   'accessibility.js',
   'accessible.js',
+  'contrast.js',
   'walker.js',
+  'worker.js',
 )
 
 with Files('**'):
     BUG_COMPONENT = ('DevTools', 'Accessibility Tools')
new file mode 100644
--- /dev/null
+++ b/devtools/server/actors/accessibility/worker.js
@@ -0,0 +1,102 @@
+/* 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";
+
+/* eslint-env worker */
+
+/**
+ * Import `createTask` to communicate with `devtools/shared/worker`.
+ */
+importScripts("resource://gre/modules/workers/require.js");
+const { createTask } = require("resource://devtools/shared/worker/helper.js");
+
+/**
+ * @see LineGraphWidget.prototype.setDataFromTimestamps in Graphs.js
+ * @param number id
+ * @param array timestamps
+ * @param number interval
+ * @param number duration
+ */
+createTask(self, "getBgRGBA", ({ dataTextBuf, dataBackgroundBuf }) =>
+  getBgRGBA(dataTextBuf, dataBackgroundBuf));
+
+/**
+ * Calculates the luminance of a rgba tuple based on the formula given in
+ * https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
+ *
+ * @param {Array} rgba An array with [r,g,b,a] values.
+ * @return {Number} The calculated luminance.
+ */
+function calculateLuminance(rgba) {
+  for (let i = 0; i < 3; i++) {
+    rgba[i] /= 255;
+    rgba[i] = (rgba[i] < 0.03928) ? (rgba[i] / 12.92) :
+                                    Math.pow(((rgba[i] + 0.055) / 1.055), 2.4);
+  }
+  return 0.2126 * rgba[0] + 0.7152 * rgba[1] + 0.0722 * rgba[2];
+}
+
+/**
+ * Get RGBA or a range of RGBAs for the background pixels under the text. If luminance is
+ * uniform, only return one value of RGBA, otherwise return values that correspond to the
+ * min and max luminances.
+ * @param  {ImageData} dataTextBuf
+ *         pixel data for the accessible object with text visible.
+ * @param  {ImageData} dataBackgroundBuf
+ *         pixel data for the accessible object with transparent text.
+ * @return {Object}
+ *         RGBA or a range of RGBAs with min and max values.
+ */
+function getBgRGBA(dataTextBuf, dataBackgroundBuf) {
+  let min = [0, 0, 0, 1];
+  let max = [255, 255, 255, 1];
+  let minLuminance = 1;
+  let maxLuminance = 0;
+  const luminances = {};
+  const dataText = new Uint8ClampedArray(dataTextBuf);
+  const dataBackground = new Uint8ClampedArray(dataBackgroundBuf);
+
+  let foundDistinctColor = false;
+  for (let i = 0; i < dataText.length; i = i + 4) {
+    const tR = dataText[i];
+    const bgR = dataBackground[i];
+    const tG = dataText[i + 1];
+    const bgG = dataBackground[i + 1];
+    const tB = dataText[i + 2];
+    const bgB = dataBackground[i + 2];
+
+    // Ignore pixels that are the same where pixels that are different between the two
+    // images are assumed to belong to the text within the node.
+    if (tR === bgR && tG === bgG && tB === bgB) {
+      continue;
+    }
+
+    foundDistinctColor = true;
+
+    const bgColor = `rgb(${bgR}, ${bgG}, ${bgB})`;
+    let luminance = luminances[bgColor];
+
+    if (!luminance) {
+      // Calculate luminance for the RGB value and store it to only measure once.
+      luminance = calculateLuminance([bgR, bgG, bgB]);
+      luminances[bgColor] = luminance;
+    }
+
+    if (minLuminance >= luminance) {
+      minLuminance = luminance;
+      min = [bgR, bgG, bgB, 1];
+    }
+
+    if (maxLuminance <= luminance) {
+      maxLuminance = luminance;
+      max = [bgR, bgG, bgB, 1];
+    }
+  }
+
+  if (!foundDistinctColor) {
+    return null;
+  }
+
+  return minLuminance === maxLuminance ? { value: max } : { min, max };
+}
--- a/devtools/server/actors/utils/accessibility.js
+++ b/devtools/server/actors/utils/accessibility.js
@@ -1,230 +1,16 @@
 /* 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";
 
 loader.lazyRequireGetter(this, "Ci", "chrome", true);
-loader.lazyRequireGetter(this, "colorUtils", "devtools/shared/css/color", true);
-loader.lazyRequireGetter(this, "CssLogic", "devtools/server/actors/inspector/css-logic", true);
-loader.lazyRequireGetter(this, "getBounds", "devtools/server/actors/highlighters/utils/accessibility", true);
-loader.lazyRequireGetter(this, "getCurrentZoom", "devtools/shared/layout/utils", true);
 loader.lazyRequireGetter(this, "Services");
-loader.lazyRequireGetter(this, "addPseudoClassLock", "devtools/server/actors/highlighters/utils/markup", true);
-loader.lazyRequireGetter(this, "removePseudoClassLock", "devtools/server/actors/highlighters/utils/markup", true);
-
-const HIGHLIGHTED_PSEUDO_CLASS = ":-moz-devtools-highlighted";
-
-/**
- * Get text style properties for a given node, if possible.
- * @param  {DOMNode} node
- *         DOM node for which text styling information is to be calculated.
- * @return {Object}
- *         Color and text size information for a given DOM node.
- */
-function getTextProperties(node) {
-  const computedStyles = CssLogic.getComputedStyle(node);
-  if (!computedStyles) {
-    return null;
-  }
-
-  const { color, "font-size": fontSize, "font-weight": fontWeight } = computedStyles;
-  const opacity = parseFloat(computedStyles.opacity);
-
-  let { r, g, b, a } = colorUtils.colorToRGBA(color, true);
-  a = opacity * a;
-  const textRgbaColor = new colorUtils.CssColor(`rgba(${r}, ${g}, ${b}, ${a})`, true);
-  // TODO: For cases where text color is transparent, it likely comes from the color of
-  // the background that is underneath it (commonly from background-clip: text
-  // property). With some additional investigation it might be possible to calculate the
-  // color contrast where the color of the background is used as text color and the
-  // color of the ancestor's background is used as its background.
-  if (textRgbaColor.isTransparent()) {
-    return null;
-  }
-
-  const isBoldText = parseInt(fontWeight, 10) >= 600;
-  const isLargeText = Math.ceil(parseFloat(fontSize) * 72) / 96 >= (isBoldText ? 14 : 18);
-
-  return {
-    // Blend text color taking its alpha into account asuming white background.
-    color: colorUtils.blendColors([r, g, b, a]),
-    isLargeText,
-  };
-}
-
-/**
- * Get canvas rendering context for the current target window bound by the bounds of the
- * accessible objects.
- * @param  {Object}  win
- *         Current target window.
- * @param  {Object}  bounds
- *         Bounds for the accessible object.
- * @param  {null|DOMNode} node
- *         If not null, a node that corresponds to the accessible object to be used to
- *         make its text color transparent.
- * @return {CanvasRenderingContext2D}
- *         Canvas rendering context for the current window.
- */
-function getImageCtx(win, bounds, node) {
-  const doc = win.document;
-  const canvas = doc.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
-  const scale = getCurrentZoom(win);
-
-  const { left, top, width, height } = bounds;
-  canvas.width = width / scale;
-  canvas.height = height / scale;
-  const ctx = canvas.getContext("2d", { alpha: false });
-
-  // If node is passed, make its color related text properties invisible.
-  if (node) {
-    addPseudoClassLock(node, HIGHLIGHTED_PSEUDO_CLASS);
-  }
-
-  ctx.drawWindow(win, left / scale, top / scale, width / scale, height / scale, "#fff",
-                 ctx.DRAWWINDOW_USE_WIDGET_LAYERS);
-
-  // Restore all inline styling.
-  if (node) {
-    removePseudoClassLock(node, HIGHLIGHTED_PSEUDO_CLASS);
-  }
-
-  return ctx;
-}
-
-/**
- * Get RGBA or a range of RGBAs for the background pixels under the text. If luminance is
- * uniform, only return one value of RGBA, otherwise return values that correspond to the
- * min and max luminances.
- * @param  {ImageData} dataText
- *         pixel data for the accessible object with text visible.
- * @param  {ImageData} dataBackground
- *         pixel data for the accessible object with transparent text.
- * @return {Object}
- *         RGBA or a range of RGBAs with min and max values.
- */
-function getBgRGBA(dataText, dataBackground) {
-  let min = [0, 0, 0, 1];
-  let max = [255, 255, 255, 1];
-  let minLuminance = 1;
-  let maxLuminance = 0;
-  const luminances = {};
-
-  let foundDistinctColor = false;
-  for (let i = 0; i < dataText.length; i = i + 4) {
-    const tR = dataText[i];
-    const bgR = dataBackground[i];
-    const tG = dataText[i + 1];
-    const bgG = dataBackground[i + 1];
-    const tB = dataText[i + 2];
-    const bgB = dataBackground[i + 2];
-
-    // Ignore pixels that are the same where pixels that are different between the two
-    // images are assumed to belong to the text within the node.
-    if (tR === bgR && tG === bgG && tB === bgB) {
-      continue;
-    }
-
-    foundDistinctColor = true;
-
-    const bgColor = `rgb(${bgR}, ${bgG}, ${bgB})`;
-    let luminance = luminances[bgColor];
-
-    if (!luminance) {
-      // Calculate luminance for the RGB value and store it to only measure once.
-      luminance = colorUtils.calculateLuminance([bgR, bgG, bgB]);
-      luminances[bgColor] = luminance;
-    }
-
-    if (minLuminance >= luminance) {
-      minLuminance = luminance;
-      min = [bgR, bgG, bgB, 1];
-    }
-
-    if (maxLuminance <= luminance) {
-      maxLuminance = luminance;
-      max = [bgR, bgG, bgB, 1];
-    }
-  }
-
-  if (!foundDistinctColor) {
-    return null;
-  }
-
-  return minLuminance === maxLuminance ? { value: max } : { min, max };
-}
-
-/**
- * Calculates the contrast ratio of the referenced DOM node.
- *
- * @param  {DOMNode} node
- *         The node for which we want to calculate the contrast ratio.
- * @param  {Object}  options
- *         - bounds   {Object}
- *                    Bounds for the accessible object.
- *         - win      {Object}
- *                    Target window.
- *
- * @return {Object}
- *         An object that may contain one or more of the following fields: error,
- *         isLargeText, value, min, max values for contrast.
-*/
-function getContrastRatioFor(node, options = {}) {
-  const props = getTextProperties(node);
-  if (!props) {
-    return {
-      error: true,
-    };
-  }
-
-  const bounds = getBounds(options.win, options.bounds);
-  const textContext = getImageCtx(options.win, bounds);
-  const backgroundContext = getImageCtx(options.win, bounds, node);
-
-  const { data: dataText } = textContext.getImageData(0, 0, bounds.width, bounds.height);
-  const { data: dataBackground } = backgroundContext.getImageData(
-    0, 0, bounds.width, bounds.height);
-
-  const rgba = getBgRGBA(dataText, dataBackground);
-  if (!rgba) {
-    return {
-      error: true,
-    };
-  }
-
-  const { color, isLargeText } = props;
-  if (rgba.value) {
-    return {
-      value: colorUtils.calculateContrastRatio(rgba.value, color),
-      color,
-      backgroundColor: rgba.value,
-      isLargeText,
-    };
-  }
-
-  let min = colorUtils.calculateContrastRatio(rgba.min, color);
-  let max = colorUtils.calculateContrastRatio(rgba.max, color);
-
-  // Flip minimum and maximum contrast ratios if necessary.
-  if (min > max) {
-    [min, max] = [max, min];
-    [rgba.min, rgba.max] = [rgba.max, rgba.min];
-  }
-
-  return {
-    min,
-    max,
-    color,
-    backgroundColorMin: rgba.min,
-    backgroundColorMax: rgba.max,
-    isLargeText,
-  };
-}
 
 /**
  * Helper function that determines if nsIAccessible object is in defunct state.
  *
  * @param  {nsIAccessible}  accessible
  *         object to be tested.
  * @return {Boolean}
  *         True if accessible object is defunct, false otherwise.
@@ -246,10 +32,9 @@ function isDefunct(accessible) {
     defunct = !!(extraState.value & Ci.nsIAccessibleStates.EXT_STATE_DEFUNCT);
   } catch (e) {
     defunct = true;
   }
 
   return defunct;
 }
 
-exports.getContrastRatioFor = getContrastRatioFor;
 exports.isDefunct = isDefunct;
--- a/devtools/shared/worker/tests/browser/browser_worker-01.js
+++ b/devtools/shared/worker/tests/browser/browser_worker-01.js
@@ -4,32 +4,34 @@
 "use strict";
 
 // 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 BUFFER_SIZE = 8;
 const count = 100000;
 const WORKER_DATA = (function() {
   const timestamps = [];
   for (let i = 0; i < count; i++) {
     timestamps.push(i);
   }
   return timestamps;
 })();
 const INTERVAL = 100;
 const DURATION = 1000;
 
 add_task(async function() {
   // Test both CJS and JSM versions
 
   await testWorker("JSM", () => ChromeUtils.import("resource://devtools/shared/worker/worker.js", {}));
   await testWorker("CommonJS", () => require("devtools/shared/worker/worker"));
+  await testTransfer();
 });
 
 async function testWorker(context, workerFactory) {
   const { DevToolsWorker, workerify } = workerFactory();
   const worker = new DevToolsWorker(WORKER_URL);
   const results = await worker.performTask("plotTimestampsGraph", {
     timestamps: WORKER_DATA,
     interval: INTERVAL,
@@ -40,8 +42,25 @@ async function testWorker(context, worke
     `worker should have returned an object with array properties in ${context}`);
 
   const fn = workerify(x => x * x);
   is((await fn(5)), 25, `workerify works in ${context}`);
   fn.destroy();
 
   worker.destroy();
 }
+
+async function testTransfer() {
+  const { workerify } =
+    ChromeUtils.import("resource://devtools/shared/worker/worker.js", {});
+  const workerFn = workerify(({ buf }) => buf.byteLength);
+  const buf = new ArrayBuffer(BUFFER_SIZE);
+
+  is(buf.byteLength, BUFFER_SIZE, "Size of the buffer before transfer is correct.");
+
+  is((await workerFn({ buf })), 8, "Sent array buffer to worker");
+  is(buf.byteLength, 8, "Array buffer was copied, not transferred.");
+
+  is((await workerFn({ buf }, [ buf ])), 8, "Sent array buffer to worker");
+  is(buf.byteLength, 0, "Array buffer was transferred, not copied.");
+
+  workerFn.destroy();
+}
--- a/devtools/shared/worker/worker.js
+++ b/devtools/shared/worker/worker.js
@@ -55,33 +55,35 @@
    * 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) {
+  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);
+    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));
@@ -141,17 +143,18 @@
     // Fetch modules here as we don't want to include it normally.
     const Services = require("Services");
     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 => worker.performTask("workerifiedTask", data);
+    const wrapperFn = (data, transfer) =>
+      worker.performTask("workerifiedTask", data, transfer);
 
     wrapperFn.destroy = function() {
       URL.revokeObjectURL(url);
       worker.destroy();
     };
 
     return wrapperFn;
   }