Bug 1312103 - Avoid scrolling latency on highlighters given by APZ; r=pbro draft
authorMatteo Ferretti <mferretti@mozilla.com>
Mon, 02 Jan 2017 11:43:37 +0100
changeset 484558 b207db542f2ab21266e1c58e2827c1327dfca2e5
parent 484557 79cea5a0238246a9dfe4ed8cc57161b09376998d
child 484567 111a7cf9cd1454116e89925a2c5dee631c2d9b50
child 484608 70a48d23058bc30bd158307f593217146122b82b
push id45496
push userbmo:zer0@mozilla.com
push dateWed, 15 Feb 2017 11:02:34 +0000
reviewerspbro
bugs1312103
milestone54.0a1
Bug 1312103 - Avoid scrolling latency on highlighters given by APZ; r=pbro - Used `position: absolute` instead of `position: fixed` whenever was possible. - Updated utility functions and auto-refresh base class to consider the scrolled values in root document and scrollable elements. MozReview-Commit-ID: 6evdOrfj74z
devtools/client/inspector/test/browser_inspector_highlighter-measure_01.js
devtools/client/inspector/test/browser_inspector_highlighter-zoom.js
devtools/client/shared/test/test-actor.js
devtools/server/actors/highlighters.css
devtools/server/actors/highlighters/auto-refresh.js
devtools/server/actors/highlighters/css-grid.js
devtools/server/actors/highlighters/measuring-tool.js
devtools/server/actors/highlighters/utils/markup.js
devtools/shared/layout/utils.js
--- a/devtools/client/inspector/test/browser_inspector_highlighter-measure_01.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-measure_01.js
@@ -32,17 +32,17 @@ add_task(function* () {
   yield areLabelsProperlyDisplayedWhenMouseMoved(helper);
 
   yield finalize();
 });
 
 function* isHiddenByDefault({isElementHidden}) {
   info("Checking the highlighter is hidden by default");
 
-  let hidden = yield isElementHidden("elements");
+  let hidden = yield isElementHidden("root");
   ok(hidden, "highlighter's root is hidden by default");
 
   hidden = yield isElementHidden("label-size");
   ok(hidden, "highlighter's label size is hidden by default");
 
   hidden = yield isElementHidden("label-position");
   ok(hidden, "highlighter's label position is hidden by default");
 }
--- a/devtools/client/inspector/test/browser_inspector_highlighter-zoom.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-zoom.js
@@ -4,56 +4,51 @@
 
 "use strict";
 
 // Test that the highlighter stays correctly positioned and has the right aspect
 // ratio even when the page is zoomed in or out.
 
 const TEST_URL = "data:text/html;charset=utf-8,<div>zoom me</div>";
 
-// TEST_LEVELS entries should contain the following properties:
-// - level: the zoom level to test
-// - expected: the style attribute value to check for on the root highlighter
-//   element.
-const TEST_LEVELS = [{
-  level: 2,
-  expected: "position:absolute;transform-origin:top left;" +
-            "transform:scale(0.5);width:200%;height:200%;"
-}, {
-  level: 1,
-  expected: "position:absolute;width:100%;height:100%;"
-}, {
-  level: .5,
-  expected: "position:absolute;transform-origin:top left;" +
-            "transform:scale(2);width:50%;height:50%;"
-}];
+// TEST_LEVELS entries should contain the zoom level to test.
+const TEST_LEVELS = [2, 1, .5];
+
+// Returns the expected style attribute value to check for on the root highlighter
+// element, for the values given.
+const expectedStyle = (w, h, z) =>
+        (z !== 1 ? `transform-origin:top left; transform:scale(${1 / z}); ` : "") +
+        `position:absolute; width:${w * z}px;height:${h * z}px; ` +
+        "overflow:hidden";
 
 add_task(function* () {
   let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
 
   info("Highlighting the test node");
 
   yield hoverElement("div", inspector);
   let isVisible = yield testActor.isHighlighting();
   ok(isVisible, "The highlighter is visible");
 
-  for (let {level, expected} of TEST_LEVELS) {
+  for (let level of TEST_LEVELS) {
     info("Zoom to level " + level +
          " and check that the highlighter is correct");
 
     yield testActor.zoomPageTo(level);
     isVisible = yield testActor.isHighlighting();
     ok(isVisible, "The highlighter is still visible at zoom level " + level);
 
     yield testActor.isNodeCorrectlyHighlighted("div", is);
 
     info("Check that the highlighter root wrapper node was scaled down");
 
     let style = yield getRootNodeStyle(testActor);
-    is(style, expected, "The style attribute of the root element is correct");
+    let { width, height } = yield testActor.getWindowDimensions();
+    is(style, expectedStyle(width, height, level),
+      "The style attribute of the root element is correct");
   }
 });
 
 function* hoverElement(selector, inspector) {
   info("Hovering node " + selector + " in the markup view");
   let container = yield getContainerForSelector(selector, inspector);
   yield hoverContainer(container, inspector);
 }
--- a/devtools/client/shared/test/test-actor.js
+++ b/devtools/client/shared/test/test-actor.js
@@ -4,17 +4,19 @@
 
 /* exported TestActor, TestActorFront */
 
 "use strict";
 
 // A helper actor for inspector and markupview tests.
 
 const { Cc, Ci, Cu } = require("chrome");
-const {getRect, getElementFromPoint, getAdjustedQuads} = require("devtools/shared/layout/utils");
+const {
+  getRect, getElementFromPoint, getAdjustedQuads, getWindowDimensions
+} = require("devtools/shared/layout/utils");
 const defer = require("devtools/shared/defer");
 const {Task} = require("devtools/shared/task");
 const {isContentStylesheet} = require("devtools/shared/inspector/css-logic");
 const DOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
 const loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
                  .getService(Ci.mozIJSSubScriptLoader);
 
 // Set up a dummy environment so that EventUtils works. We need to be careful to
@@ -284,16 +286,22 @@ var testSpec = protocol.generateActorSpe
     },
     getStyleSheetsInfoForNode: {
       request: {
         selector: Arg(0, "string")
       },
       response: {
         value: RetVal("json")
       }
+    },
+    getWindowDimensions: {
+      request: {},
+      response: {
+        value: RetVal("json")
+      }
     }
   }
 });
 
 var TestActor = exports.TestActor = protocol.ActorClassWithSpec(testSpec, {
   initialize: function (conn, tabActor, options) {
     this.conn = conn;
     this.tabActor = tabActor;
@@ -777,16 +785,26 @@ var TestActor = exports.TestActor = prot
       let sheet = domRules.GetElementAt(i).parentStyleSheet;
       sheets.push({
         href: sheet.href,
         isContentSheet: isContentStylesheet(sheet)
       });
     }
 
     return sheets;
+  },
+
+  /**
+   * Returns the window's dimensions for the `window` given.
+   *
+   * @return {Object} An object with `width` and `height` properties, representing the
+   * number of pixels for the document's size.
+   */
+  getWindowDimensions: function () {
+    return getWindowDimensions(this.content);
   }
 });
 
 var TestActorFront = exports.TestActorFront = protocol.FrontClassWithSpec(testSpec, {
   initialize: function (client, { testActor }, toolbox) {
     protocol.Front.prototype.initialize.call(this, client, { actor: testActor });
     this.manage(this);
     this.toolbox = toolbox;
--- a/devtools/server/actors/highlighters.css
+++ b/devtools/server/actors/highlighters.css
@@ -30,18 +30,26 @@
 :-moz-native-anonymous .highlighter-container {
   --highlighter-guide-color: #08c;
   --highlighter-content-color: #87ceeb;
   --highlighter-bubble-text-color: hsl(216, 33%, 97%);
   --highlighter-bubble-background-color: hsl(214, 13%, 24%);
   --highlighter-bubble-border-color: rgba(255, 255, 255, 0.2);
 }
 
+/**
+ * Highlighters are asbolute positioned in the page by default.
+ * A single highlighter can have fixed position in its css class if needed (see below the
+ * eye dropper or rulers highlighter, for example); but if it has to handle the
+ * document's scrolling (as rulers does), it would lag a bit behind due the APZ (Async
+ * Pan/Zoom module), that performs asynchronously panning and zooming on the compositor
+ * thread rather than the main thread.
+ */
 :-moz-native-anonymous .highlighter-container {
-  position: fixed;
+  position: absolute;
   width: 100%;
   height: 100%;
   /* The container for all highlighters doesn't react to pointer-events by
      default. This is because most highlighters cover the whole viewport but
      don't contain UIs that need to be accessed.
      If your highlighter has UI that needs to be interacted with, add
      'pointer-events:auto;' on its container element. */
   pointer-events: none;
@@ -378,16 +386,20 @@
 :-moz-native-anonymous .measuring-tool-highlighter-root {
   position: absolute;
   top: 0;
   left: 0;
   pointer-events: auto;
   cursor: crosshair;
 }
 
+:-moz-native-anonymous .measuring-tool-highlighter-elements {
+  position: absolute;
+}
+
 :-moz-native-anonymous .measuring-tool-highlighter-root path {
   shape-rendering: crispEdges;
   fill: rgba(135, 206, 235, 0.6);
   stroke: var(--highlighter-guide-color);
   pointer-events: none;
 }
 
 :-moz-native-anonymous .dragging path {
@@ -438,17 +450,17 @@
 :-moz-native-anonymous .eye-dropper-root {
   --magnifier-width: 96px;
   --magnifier-height: 96px;
   /* Width accounts for all color formats (hsl being the longest) */
   --label-width: 160px;
   --label-height: 23px;
   --color: #e0e0e0;
 
-  position: absolute;
+  position: fixed;
   /* Tool start position. This should match the X/Y defines in JS */
   top: 100px;
   left: 100px;
 
   /* Prevent interacting with the page when hovering and clicking */
   pointer-events: auto;
 
   /* Offset the UI so it is centered around the pointer */
--- a/devtools/server/actors/highlighters/auto-refresh.js
+++ b/devtools/server/actors/highlighters/auto-refresh.js
@@ -2,21 +2,51 @@
  * 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";
 
 const { Cu } = require("chrome");
 const EventEmitter = require("devtools/shared/event-emitter");
 const { isNodeValid } = require("./utils/markup");
-const { getAdjustedQuads } = require("devtools/shared/layout/utils");
+const { getAdjustedQuads, getCurrentZoom,
+        getWindowDimensions } = require("devtools/shared/layout/utils");
 
 // Note that the order of items in this array is important because it is used
 // for drawing the BoxModelHighlighter's path elements correctly.
 const BOX_MODEL_REGIONS = ["margin", "border", "padding", "content"];
+const QUADS_PROPS = ["p1", "p2", "p3", "p4", "bounds"];
+
+function areValuesDifferent(oldValue, newValue, zoom) {
+  let delta = Math.abs(oldValue.toFixed(4) - newValue.toFixed(4));
+  return delta / zoom > 1 / zoom;
+}
+
+function areQuadsDifferent(oldQuads, newQuads, zoom) {
+  for (let region of BOX_MODEL_REGIONS) {
+    if (oldQuads[region].length !== newQuads[region].length) {
+      return true;
+    }
+
+    for (let i = 0; i < oldQuads[region].length; i++) {
+      for (let prop of QUADS_PROPS) {
+        let oldProp = oldQuads[region][i][prop];
+        let newProp = newQuads[region][i][prop];
+
+        for (let key of Object.keys(oldProp)) {
+          if (areValuesDifferent(oldProp[key], newProp[key], zoom)) {
+            return true;
+          }
+        }
+      }
+    }
+  }
+
+  return false;
+}
 
 /**
  * Base class for auto-refresh-on-change highlighters. Sub classes will have a
  * chance to update whenever the current node's geometry changes.
  *
  * Sub classes must implement the following methods:
  * _show: called when the highlighter should be shown,
  * _hide: called when the highlighter should be hidden,
@@ -36,16 +66,18 @@ const BOX_MODEL_REGIONS = ["margin", "bo
 function AutoRefreshHighlighter(highlighterEnv) {
   EventEmitter.decorate(this);
 
   this.highlighterEnv = highlighterEnv;
 
   this.currentNode = null;
   this.currentQuads = {};
 
+  this._winDimensions = getWindowDimensions(this.win);
+
   this.update = this.update.bind(this);
 }
 
 AutoRefreshHighlighter.prototype = {
   /**
    * Window corresponding to the current highlighterEnv
    */
   get win() {
@@ -134,40 +166,57 @@ AutoRefreshHighlighter.prototype = {
 
     return true;
   },
 
   /**
    * Update the stored box quads by reading the current node's box quads.
    */
   _updateAdjustedQuads: function () {
+    this.currentQuads = {};
+
     for (let region of BOX_MODEL_REGIONS) {
       this.currentQuads[region] = getAdjustedQuads(
         this.win,
         this.currentNode, region);
     }
   },
 
   /**
    * Update the knowledge we have of the current node's boxquads and return true
    * if any of the points x/y or bounds have change since.
    * @return {Boolean}
    */
   _hasMoved: function () {
-    let oldQuads = JSON.stringify(this.currentQuads);
+    let oldQuads = this.currentQuads;
     this._updateAdjustedQuads();
-    let newQuads = JSON.stringify(this.currentQuads);
-    return oldQuads !== newQuads;
+
+    return areQuadsDifferent(oldQuads, this.currentQuads, getCurrentZoom(this.win));
+  },
+
+  /**
+   * Update the knowledge we have of the current window's dimensions and return `true`
+   * if they have changed since.
+   * @return {Boolean}
+   */
+  _haveWindowDimensionsChanged: function () {
+    let { width, height } = getWindowDimensions(this.win);
+    let haveChanged = (this._winDimensions.width !== width ||
+                      this._winDimensions.height !== height);
+
+    this._winDimensions = { width, height };
+    return haveChanged;
   },
 
   /**
    * Update the highlighter if the node has moved since the last update.
    */
   update: function () {
-    if (!this._isNodeValid(this.currentNode) || !this._hasMoved()) {
+    if (!this._isNodeValid(this.currentNode) ||
+       (!this._hasMoved() && !this._haveWindowDimensionsChanged())) {
       return;
     }
 
     this._update();
     this.emit("updated");
   },
 
   _show: function () {
--- a/devtools/server/actors/highlighters/css-grid.js
+++ b/devtools/server/actors/highlighters/css-grid.js
@@ -10,17 +10,18 @@ const { AutoRefreshHighlighter } = requi
 const {
   CanvasFrameAnonymousContentHelper,
   createNode,
   createSVGNode,
   moveInfobar,
 } = require("./utils/markup");
 const {
   getCurrentZoom,
-  setIgnoreLayoutChanges
+  setIgnoreLayoutChanges,
+  getWindowDimensions
 } = require("devtools/shared/layout/utils");
 const { stringifyGridFragments } = require("devtools/server/actors/utils/css-grid-utils");
 
 const CSS_GRID_ENABLED_PREF = "layout.css.grid.enabled";
 const ROWS = "rows";
 const COLUMNS = "cols";
 const GRID_LINES_PROPERTIES = {
   "edge": {
@@ -113,34 +114,43 @@ CssGridHighlighter.prototype = extend(Au
 
   _buildMarkup() {
     let container = createNode(this.win, {
       attributes: {
         "class": "highlighter-container"
       }
     });
 
+    let root = createNode(this.win, {
+      parent: container,
+      attributes: {
+        "id": "root",
+        "class": "root"
+      },
+      prefix: this.ID_CLASS_PREFIX
+    });
+
     // We use a <canvas> element so that we can draw an arbitrary number of lines
     // which wouldn't be possible with HTML or SVG without having to insert and remove
     // the whole markup on every update.
     createNode(this.win, {
-      parent: container,
+      parent: root,
       nodeType: "canvas",
       attributes: {
         "id": "canvas",
         "class": "canvas",
         "hidden": "true"
       },
       prefix: this.ID_CLASS_PREFIX
     });
 
     // Build the SVG element
     let svg = createSVGNode(this.win, {
       nodeType: "svg",
-      parent: container,
+      parent: root,
       attributes: {
         "id": "elements",
         "width": "100%",
         "height": "100%",
         "hidden": "true"
       },
       prefix: this.ID_CLASS_PREFIX
     });
@@ -350,18 +360,26 @@ CssGridHighlighter.prototype = extend(Au
   /**
    * Update the highlighter on the current highlighted node (the one that was
    * passed as an argument to show(node)).
    * Should be called whenever node's geometry or grid changes.
    */
   _update() {
     setIgnoreLayoutChanges(true);
 
+    let root = this.getElement("root");
+    // Hide the root element and force the reflow in order to get the proper window's
+    // dimensions without increasing them.
+    root.setAttribute("style", "display: none");
+    this.currentNode.offsetWidth;
+
+    let { width, height } = getWindowDimensions(this.win);
+
     // Clear the canvas the grid area highlights.
-    this.clearCanvas();
+    this.clearCanvas(width, height);
     this.clearGridAreas();
 
     // Start drawing the grid fragments.
     for (let i = 0; i < this.gridData.length; i++) {
       let fragment = this.gridData[i];
       let quad = this.currentQuads.content[i];
       this.renderFragment(fragment, quad);
     }
@@ -370,16 +388,19 @@ CssGridHighlighter.prototype = extend(Au
     if (this.options.showAllGridAreas) {
       this.showAllGridAreas();
     } else if (this.options.showGridArea) {
       this.showGridArea(this.options.showGridArea);
     }
 
     this._showGrid();
 
+    root.setAttribute("style",
+      `position:absolute; width:${width}px;height:${height}px; overflow:hidden`);
+
     setIgnoreLayoutChanges(false, this.highlighterEnv.window.document.documentElement);
     return true;
   },
 
   /**
    * Update the grid information displayed in the grid info bar.
    *
    * @param  {GridArea} area
@@ -429,25 +450,23 @@ CssGridHighlighter.prototype = extend(Au
       x: x1,
       y: y1,
     };
     let container = this.getElement("infobar-container");
 
     moveInfobar(container, bounds, this.win);
   },
 
-  clearCanvas() {
+  clearCanvas(width, height) {
     let ratio = parseFloat((this.win.devicePixelRatio || 1).toFixed(2));
-    let width = this.win.innerWidth;
-    let height = this.win.innerHeight;
 
     // Resize the canvas taking the dpr into account so as to have crisp lines.
     this.canvas.setAttribute("width", width * ratio);
     this.canvas.setAttribute("height", height * ratio);
-    this.canvas.setAttribute("style", `width:${width}px;height:${height}px`);
+    this.canvas.setAttribute("style", `width:${width}px;height:${height}px;`);
     this.ctx.scale(ratio, ratio);
 
     this.ctx.clearRect(0, 0, width, height);
   },
 
   getFirstRowLinePos(fragment) {
     return fragment.rows.lines[0].start;
   },
--- a/devtools/server/actors/highlighters/measuring-tool.js
+++ b/devtools/server/actors/highlighters/measuring-tool.js
@@ -1,16 +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";
 
 const events = require("sdk/event/core");
-const { getCurrentZoom,
+const { getCurrentZoom, getWindowDimensions,
   setIgnoreLayoutChanges } = require("devtools/shared/layout/utils");
 const {
   CanvasFrameAnonymousContentHelper,
   createSVGNode, createNode } = require("./utils/markup");
 
 // Hard coded value about the size of measuring tool label, in order to
 // position and flip it when is needed.
 const LABEL_SIZE_MARGIN = 8;
@@ -60,29 +60,29 @@ MeasuringToolHighlighter.prototype = {
       attributes: {"class": "highlighter-container"}
     });
 
     let root = createNode(window, {
       parent: container,
       attributes: {
         "id": "root",
         "class": "root",
+        "hidden": "true",
       },
       prefix
     });
 
     let svg = createSVGNode(window, {
       nodeType: "svg",
       parent: root,
       attributes: {
         id: "elements",
         "class": "elements",
         width: "100%",
         height: "100%",
-        hidden: "true"
       },
       prefix
     });
 
     createNode(window, {
       nodeType: "label",
       attributes: {
         id: "label-size",
@@ -152,33 +152,17 @@ MeasuringToolHighlighter.prototype = {
 
   _update() {
     let { window } = this.env;
 
     setIgnoreLayoutChanges(true);
 
     let zoom = getCurrentZoom(window);
 
-    let { documentElement } = window.document;
-
-    let width = Math.max(documentElement.clientWidth,
-                         documentElement.scrollWidth,
-                         documentElement.offsetWidth);
-
-    let height = Math.max(documentElement.clientHeight,
-                          documentElement.scrollHeight,
-                          documentElement.offsetHeight);
-
-    let { body } = window.document;
-
-    // get the size of the content document despite the compatMode
-    if (body) {
-      width = Math.max(width, body.scrollWidth, body.offsetWidth);
-      height = Math.max(height, body.scrollHeight, body.offsetHeight);
-    }
+    let { width, height } = getWindowDimensions(window);
 
     let { coords } = this;
 
     let isZoomChanged = zoom !== coords.zoom;
 
     if (isZoomChanged) {
       coords.zoom = zoom;
       this.updateLabel();
@@ -193,17 +177,17 @@ MeasuringToolHighlighter.prototype = {
     }
 
     // If either the document's size or the zoom is changed since the last
     // repaint, we update the tool's size as well.
     if (isZoomChanged || isDocumentSizeChanged) {
       this.updateViewport();
     }
 
-    setIgnoreLayoutChanges(false, documentElement);
+    setIgnoreLayoutChanges(false, window.document.documentElement);
 
     this._rafID = window.requestAnimationFrame(() => this._update());
   },
 
   _cancelUpdate() {
     if (this._rafID) {
       this.env.window.cancelAnimationFrame(this._rafID);
       this._rafID = 0;
@@ -227,30 +211,30 @@ MeasuringToolHighlighter.prototype = {
     this.markup.destroy();
 
     events.emit(this, "destroy");
   },
 
   show() {
     setIgnoreLayoutChanges(true);
 
-    this.getElement("elements").removeAttribute("hidden");
+    this.getElement("root").removeAttribute("hidden");
 
     this._update();
 
     setIgnoreLayoutChanges(false, this.env.window.document.documentElement);
   },
 
   hide() {
     setIgnoreLayoutChanges(true);
 
     this.hideLabel("size");
     this.hideLabel("position");
 
-    this.getElement("elements").setAttribute("hidden", "true");
+    this.getElement("root").setAttribute("hidden", "true");
 
     this._cancelUpdate();
 
     setIgnoreLayoutChanges(false, this.env.window.document.documentElement);
   },
 
   getElement(id) {
     return this.markup.getElement(this.ID_CLASS_PREFIX + id);
@@ -385,34 +369,33 @@ MeasuringToolHighlighter.prototype = {
       if (style) {
         labelSize.setAttribute("style",
           style.replace(/scale[^)]+\)/, `scale(${scale})`));
       }
     }
   },
 
   updateViewport() {
-    let { scrollX, scrollY, devicePixelRatio } = this.env.window;
+    let { devicePixelRatio } = this.env.window;
     let { documentWidth, documentHeight, zoom } = this.coords;
 
     // Because `devicePixelRatio` is affected by zoom (see bug 809788),
     // in order to get the "real" device pixel ratio, we need divide by `zoom`
     let pixelRatio = devicePixelRatio / zoom;
 
     // The "real" device pixel ratio is used to calculate the max stroke
     // width we can actually assign: on retina, for instance, it would be 0.5,
     // where on non high dpi monitor would be 1.
     let minWidth = 1 / pixelRatio;
-    let strokeWidth = Math.min(minWidth, minWidth / zoom);
+    let strokeWidth = minWidth / zoom;
 
     this.getElement("root").setAttribute("style",
       `stroke-width:${strokeWidth};
        width:${documentWidth}px;
-       height:${documentHeight}px;
-       transform: translate(${-scrollX}px,${-scrollY}px)`);
+       height:${documentHeight}px;`);
   },
 
   updateGuides() {
     let { x, y, w, h } = this.coords;
 
     let guide = this.getElement("guide-top");
 
     guide.setAttribute("x1", "0");
@@ -544,20 +527,17 @@ MeasuringToolHighlighter.prototype = {
         this.showLabel(type);
         break;
       case "mouseleave":
         if (!this._isDragging) {
           this.hideLabel("position");
         }
         break;
       case "scroll":
-        setIgnoreLayoutChanges(true);
-        this.updateViewport();
-        setIgnoreLayoutChanges(false, this.env.window.document.documentElement);
-
+        this.hideLabel("position");
         break;
       case "pagehide":
         this.destroy();
         break;
     }
   }
 };
 exports.MeasuringToolHighlighter = MeasuringToolHighlighter;
--- a/devtools/server/actors/highlighters/utils/markup.js
+++ b/devtools/server/actors/highlighters/utils/markup.js
@@ -1,16 +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";
 
 const { Cc, Ci, Cu } = require("chrome");
-const { getCurrentZoom,
+const { getCurrentZoom, getWindowDimensions,
   getRootBindingParent } = require("devtools/shared/layout/utils");
 const { on, emit } = require("sdk/event/core");
 
 const lazyContainer = {};
 
 loader.lazyRequireGetter(lazyContainer, "CssLogic",
   "devtools/server/css-logic", true);
 exports.getComputedStyle = (node) =>
@@ -524,25 +524,34 @@ CanvasFrameAnonymousContentHelper.protot
    * Note that if the matching element already has an inline style attribute, it
    * *won't* be preserved.
    *
    * @param {DOMNode} node This node is used to determine which container window
    * should be used to read the current zoom value.
    * @param {String} id The ID of the root element inserted with this API.
    */
   scaleRootElement: function (node, id) {
+    let boundaryWindow = this.highlighterEnv.window;
     let zoom = getCurrentZoom(node);
-    let value = "position:absolute;width:100%;height:100%;";
+    // Hide the root element and force the reflow in order to get the proper window's
+    // dimensions without increasing them.
+    this.setAttributeForElement(id, "style", "display: none");
+    node.offsetWidth;
+
+    let { width, height } = getWindowDimensions(boundaryWindow);
+    let value = "";
 
     if (zoom !== 1) {
-      value = "position:absolute;";
-      value += "transform-origin:top left;transform:scale(" + (1 / zoom) + ");";
-      value += "width:" + (100 * zoom) + "%;height:" + (100 * zoom) + "%;";
+      value = `transform-origin:top left; transform:scale(${1 / zoom}); `;
+      width *= zoom;
+      height *= zoom;
     }
 
+    value += `position:absolute; width:${width}px;height:${height}px; overflow:hidden`;
+
     this.setAttributeForElement(id, "style", value);
   }
 };
 exports.CanvasFrameAnonymousContentHelper = CanvasFrameAnonymousContentHelper;
 
 /**
  * Move the infobar to the right place in the highlighter. This helper method is utilized
  * in both css-grid.js and box-model.js to help position the infobar in an appropriate
--- a/devtools/shared/layout/utils.js
+++ b/devtools/shared/layout/utils.js
@@ -198,16 +198,20 @@ function getAdjustedQuads(boundaryWindow
   });
 
   if (!quads.length) {
     return [];
   }
 
   let [xOffset, yOffset] = getFrameOffsets(boundaryWindow, node);
   let scale = getCurrentZoom(node);
+  let { scrollX, scrollY } = boundaryWindow;
+
+  xOffset += scrollX * scale;
+  yOffset += scrollY * scale;
 
   let adjustedQuads = [];
   for (let quad of quads) {
     adjustedQuads.push({
       p1: {
         w: quad.p1.w * scale,
         x: quad.p1.x * scale + xOffset,
         y: quad.p1.y * scale + yOffset,
@@ -315,17 +319,17 @@ exports.getRect = getRect;
  * @param {DOMNode} node
  * @return {Object}
  *         An object with p1,p2,p3,p4 properties being {x,y} objects
  */
 function getNodeBounds(boundaryWindow, node) {
   if (!node) {
     return null;
   }
-
+  let { scrollX, scrollY } = boundaryWindow;
   let scale = getCurrentZoom(node);
 
   // Find out the offset of the node in its current frame
   let offsetLeft = 0;
   let offsetTop = 0;
   let el = node;
   while (el && el.parentNode) {
     offsetLeft += el.offsetLeft;
@@ -342,18 +346,18 @@ function getNodeBounds(boundaryWindow, n
     if (el.scrollLeft) {
       offsetLeft -= el.scrollLeft;
     }
     el = el.parentNode;
   }
 
   // And add the potential frame offset if the node is nested
   let [xOffset, yOffset] = getFrameOffsets(boundaryWindow, node);
-  xOffset += offsetLeft;
-  yOffset += offsetTop;
+  xOffset += offsetLeft + scrollX;
+  yOffset += offsetTop + scrollY;
 
   xOffset *= scale;
   yOffset *= scale;
 
   // Get the width and height
   let width = node.offsetWidth * scale;
   let height = node.offsetHeight * scale;
 
@@ -624,16 +628,43 @@ function getCurrentZoom(node) {
     throw new Error("Unable to get the zoom from the given argument.");
   }
 
   return utilsFor(win).fullZoom;
 }
 exports.getCurrentZoom = getCurrentZoom;
 
 /**
+ * Returns the window's dimensions for the `window` given.
+ *
+ * @return {Object} An object with `width` and `height` properties, representing the
+ * number of pixels for the document's size.
+ */
+function getWindowDimensions(window) {
+  // First we'll try without flushing layout, because it's way faster.
+  let windowUtils = utilsFor(window);
+  let { width, height } = windowUtils.getRootBounds();
+
+  if (!width || !height) {
+    // We need a flush after all :'(
+    width = window.innerWidth + window.scrollMaxX - window.scrollMinX;
+    height = window.innerHeight + window.scrollMaxY - window.scrollMinY;
+
+    let scrollbarHeight = {};
+    let scrollbarWidth = {};
+    windowUtils.getScrollbarSize(false, scrollbarWidth, scrollbarHeight);
+    width -= scrollbarWidth.value;
+    height -= scrollbarHeight.value;
+  }
+
+  return { width, height };
+}
+exports.getWindowDimensions = getWindowDimensions;
+
+/**
  * Return the default view for a given node, where node can be:
  * - a DOM node
  * - the document node
  * - the window itself
  * @param {DOMNode|DOMWindow|DOMDocument} node The node to get the window for.
  * @return {DOMWindow}
  */
 function getWindowFor(node) {