Bug 1374587 - Avoid getting NodeActors for grids when we already know them and filter reflows; r=gl draft
authorPatrick Brosset <pbrosset@mozilla.com>
Tue, 27 Jun 2017 12:02:53 -0700
changeset 601469 6b9719379dcbd4479bded26014aa3742bd75208e
parent 600295 d50abca6521baeae8ac6b07ddf843d63a1aa5f84
child 635263 3b1ba73b49cdc29009a037ea832596ddfcfeb066
push id66067
push userbmo:pbrosset@mozilla.com
push dateWed, 28 Jun 2017 20:03:27 +0000
reviewersgl
bugs1374587
milestone56.0a1
Bug 1374587 - Avoid getting NodeActors for grids when we already know them and filter reflows; r=gl Here we make updates on reflow slower (with a throttle) cause we don't really need to update live. We also filter all reflows that do not cause either the list of grids or the current outline to change. Finally, we also attach NodeActors to GridActor forms (when they are know) in order to avoid one more round-trip to the server. Globally, this makes performance of the grid panel far better on pages that cause many reflows. Note that this commit also fixes a debounce vs. throttle confusion that existed in an inspector utils file. The throttle function there was actually a debounce function, so it was renamed, and an actual throttle function was added. MozReview-Commit-ID: GeqgZR0o0E6
devtools/client/inspector/grids/grid-inspector.js
devtools/client/inspector/grids/moz.build
devtools/client/inspector/grids/test/browser.ini
devtools/client/inspector/grids/test/browser_grids_grid-outline-updates-on-grid-change.js
devtools/client/inspector/grids/test/unit/.eslintrc.js
devtools/client/inspector/grids/test/unit/head.js
devtools/client/inspector/grids/test/unit/test_compare_fragments_geometry.js
devtools/client/inspector/grids/test/unit/xpcshell.ini
devtools/client/inspector/grids/utils/moz.build
devtools/client/inspector/grids/utils/utils.js
devtools/client/inspector/rules/rules.js
devtools/client/inspector/rules/test/browser_rules_completion-existing-property_01.js
devtools/client/inspector/rules/test/browser_rules_completion-existing-property_02.js
devtools/client/inspector/rules/test/browser_rules_completion-new-property_02.js
devtools/client/inspector/rules/test/browser_rules_completion-new-property_multiline.js
devtools/client/inspector/rules/test/browser_rules_edit-property-commit.js
devtools/client/inspector/rules/test/browser_rules_edit-property-computed.js
devtools/client/inspector/rules/test/browser_rules_edit-property-increments.js
devtools/client/inspector/rules/test/browser_rules_edit-property_02.js
devtools/client/inspector/rules/test/browser_rules_livepreview.js
devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_02.js
devtools/client/inspector/rules/test/browser_rules_search-filter_09.js
devtools/client/inspector/rules/test/head.js
devtools/client/inspector/rules/views/text-property-editor.js
devtools/client/inspector/shared/utils.js
devtools/client/inspector/test/shared-head.js
devtools/server/actors/layout.js
devtools/shared/fronts/layout.js
--- a/devtools/client/inspector/grids/grid-inspector.js
+++ b/devtools/client/inspector/grids/grid-inspector.js
@@ -3,16 +3,18 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const Services = require("Services");
 const { Task } = require("devtools/shared/task");
 
 const SwatchColorPickerTooltip = require("devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip");
+const { throttle } = require("devtools/client/inspector/shared/utils");
+const { compareFragmentsGeometry } = require("devtools/client/inspector/grids/utils/utils");
 
 const {
   updateGridColor,
   updateGridHighlighted,
   updateGrids,
 } = require("./actions/grids");
 const {
   updateShowGridAreas,
@@ -46,19 +48,19 @@ function GridInspector(inspector, window
   this.inspector = inspector;
   this.store = inspector.store;
   this.telemetry = inspector.telemetry;
   this.walker = this.inspector.walker;
 
   this.getSwatchColorPickerTooltip = this.getSwatchColorPickerTooltip.bind(this);
   this.updateGridPanel = this.updateGridPanel.bind(this);
 
-  this.onGridLayoutChange = this.onGridLayoutChange.bind(this);
+  this.onNavigate = this.onNavigate.bind(this);
   this.onHighlighterChange = this.onHighlighterChange.bind(this);
-  this.onReflow = this.onReflow.bind(this);
+  this.onReflow = throttle(this.onReflow, 500, this);
   this.onSetGridOverlayColor = this.onSetGridOverlayColor.bind(this);
   this.onShowGridAreaHighlight = this.onShowGridAreaHighlight.bind(this);
   this.onShowGridCellHighlight = this.onShowGridCellHighlight.bind(this);
   this.onShowGridLineNamesHighlight = this.onShowGridLineNamesHighlight.bind(this);
   this.onSidebarSelect = this.onSidebarSelect.bind(this);
   this.onToggleGridHighlighter = this.onToggleGridHighlighter.bind(this);
   this.onToggleShowGridAreas = this.onToggleShowGridAreas.bind(this);
   this.onToggleShowGridLineNumbers = this.onToggleShowGridLineNumbers.bind(this);
@@ -89,30 +91,30 @@ GridInspector.prototype = {
       {
         supportsCssColor4ColorFunction: () => false
       }
     );
 
     this.highlighters.on("grid-highlighter-hidden", this.onHighlighterChange);
     this.highlighters.on("grid-highlighter-shown", this.onHighlighterChange);
     this.inspector.sidebar.on("select", this.onSidebarSelect);
-    this.inspector.target.on("navigate", this.onGridLayoutChange);
+    this.inspector.on("new-root", this.onNavigate);
 
     this.onSidebarSelect();
   }),
 
   /**
    * Destruction function called when the inspector is destroyed. Removes event listeners
    * and cleans up references.
    */
   destroy() {
     this.highlighters.off("grid-highlighter-hidden", this.onHighlighterChange);
     this.highlighters.off("grid-highlighter-shown", this.onHighlighterChange);
     this.inspector.sidebar.off("select", this.onSidebarSelect);
-    this.inspector.target.off("navigate", this.onGridLayoutChange);
+    this.inspector.off("new-root", this.onNavigate);
 
     this.inspector.reflowTracker.untrackReflows(this, this.onReflow);
 
     this.swatchColorPickerTooltip.destroy();
 
     this.document = null;
     this.highlighters = null;
     this.inspector = null;
@@ -206,17 +208,17 @@ GridInspector.prototype = {
   getSwatchColorPickerTooltip() {
     return this.swatchColorPickerTooltip;
   },
 
   /**
    * Returns true if the layout panel is visible, and false otherwise.
    */
   isPanelVisible() {
-    return this.inspector.toolbox && this.inspector.sidebar &&
+    return this.inspector && this.inspector.toolbox && this.inspector.sidebar &&
            this.inspector.toolbox.currentToolId === "inspector" &&
            this.inspector.sidebar.getCurrentTabID() === "layoutview";
   },
 
   /**
    * Load the grid highligher display settings into the store from the stored preferences.
    */
   loadHighlighterSettings() {
@@ -273,23 +275,29 @@ GridInspector.prototype = {
       this.telemetry.log(CSS_GRID_COUNT_HISTOGRAM_ID, gridFronts.length);
       this.inspector.previousURL = this.inspector.target.url;
     }
 
     let grids = [];
     for (let i = 0; i < gridFronts.length; i++) {
       let grid = gridFronts[i];
 
-      let nodeFront;
-      try {
-        nodeFront = yield this.walker.getNodeFromActor(grid.actorID, ["containerEl"]);
-      } catch (e) {
-        // This call might fail if called asynchrously after the toolbox is finished
-        // closing.
-        return;
+      let nodeFront = grid.containerNodeFront;
+
+      // If the GridFront didn't yet have access to the NodeFront for its container, then
+      // get it from the walker. This happens when the walker hasn't yet seen this
+      // particular DOM Node in the tree yet, or when we are connected to an older server.
+      if (!nodeFront) {
+        try {
+          nodeFront = yield this.walker.getNodeFromActor(grid.actorID, ["containerEl"]);
+        } catch (e) {
+          // This call might fail if called asynchrously after the toolbox is finished
+          // closing.
+          return;
+        }
       }
 
       let fallbackColor = GRID_COLORS[i % GRID_COLORS.length];
       let color = this.getInitialGridColor(nodeFront, fallbackColor);
 
       grids.push({
         id: i,
         color,
@@ -298,19 +306,20 @@ GridInspector.prototype = {
         nodeFront,
       });
     }
 
     this.store.dispatch(updateGrids(grids));
   }),
 
   /**
-   * Handler for "navigate" event fired by the tab target. Updates grid panel contents.
+   * Handler for "new-root" event fired by the inspector, which indicates a page
+   * navigation. Updates grid panel contents.
    */
-  onGridLayoutChange() {
+  onNavigate() {
     if (this.isPanelVisible()) {
       this.updateGridPanel();
     }
   },
 
   /**
    * Handler for "grid-highlighter-shown" and "grid-highlighter-hidden" events emitted
    * from the HighlightersOverlay. Updates the NodeFront's grid highlighted state.
@@ -340,26 +349,92 @@ GridInspector.prototype = {
     }
 
     this.lastHighlighterColor = null;
     this.lastHighlighterNode = null;
     this.lastHighlighterState = null;
   },
 
   /**
-   * Handler for the "reflow" event fired by the inspector's reflow tracker. On reflows,
-   * update the grid panel content.
+   * Given a list of new grid fronts, and if we have a currently highlighted grid, check
+   * if its fragments have changed.
+   *
+   * @param  {Array} newGridFronts
+   *         A list of GridFront objects.
+   * @return {Boolean}
    */
-  onReflow() {
-    if (this.isPanelVisible()) {
-      this.updateGridPanel();
+  haveCurrentFragmentsChanged(newGridFronts) {
+    const currentNode = this.highlighters.gridHighlighterShown;
+    if (!currentNode) {
+      return false;
+    }
+
+    const newGridFront = newGridFronts.find(g => g.containerNodeFront === currentNode);
+    if (!newGridFront) {
+      return false;
     }
+
+    const { grids } = this.store.getState();
+    const oldFragments = grids.find(g => g.nodeFront === currentNode).gridFragments;
+    const newFragments = newGridFront.gridFragments;
+
+    return !compareFragmentsGeometry(oldFragments, newFragments);
   },
 
   /**
+   * Handler for the "reflow" event fired by the inspector's reflow tracker. On reflows,
+   * update the grid panel content, because the shape or number of grids on the page may
+   * have changed.
+   *
+   * Note that there may be frequent reflows on the page and that not all of them actually
+   * cause the grids to change. So, we want to limit how many times we update the grid
+   * panel to only reflows that actually either change the list of grids, or those that
+   * change the current outlined grid.
+   * To achieve this, this function compares the list of grid containers from before and
+   * after the reflow, as well as the grid fragment data on the currently highlighted
+   * grid.
+   */
+  onReflow: Task.async(function* () {
+    if (!this.isPanelVisible()) {
+      return;
+    }
+
+    // The list of grids currently displayed.
+    const { grids } = this.store.getState();
+
+    // The new list of grids from the server.
+    let newGridFronts;
+    try {
+      newGridFronts = yield this.layoutInspector.getAllGrids(this.walker.rootNode);
+    } catch (e) {
+      // This call might fail if called asynchrously after the toolbox is finished
+      // closing.
+      return;
+    }
+
+    // Compare the list of DOM nodes which define these grids.
+    const oldNodeFronts = grids.map(grid => grid.nodeFront.actorID);
+    const newNodeFronts = newGridFronts.filter(grid => grid.containerNodeFront)
+                                       .map(grid => grid.containerNodeFront.actorID);
+    if (grids.length === newGridFronts.length &&
+        oldNodeFronts.sort().join(",") == newNodeFronts.sort().join(",")) {
+      // Same list of containers, but let's check if the geometry of the current grid has
+      // changed, if it hasn't we can safely abort.
+      if (!this.highlighters.gridHighlighterShown ||
+          (this.highlighters.gridHighlighterShown &&
+           !this.haveCurrentFragmentsChanged(newGridFronts))) {
+        return;
+      }
+    }
+
+    // Either the list of containers or the current fragments have changed, do update.
+    this.updateGridPanel(newGridFronts);
+  }),
+
+  /**
    * Handler for a change in the grid overlay color picker for a grid container.
    *
    * @param  {NodeFront} node
    *         The NodeFront of the grid container element for which the grid color is
    *         being updated.
    * @param  {String} color
    *         A hex string representing the color to use.
    */
--- a/devtools/client/inspector/grids/moz.build
+++ b/devtools/client/inspector/grids/moz.build
@@ -12,8 +12,9 @@ DIRS += [
 ]
 
 DevToolsModules(
     'grid-inspector.js',
     'types.js',
 )
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
--- a/devtools/client/inspector/grids/test/browser.ini
+++ b/devtools/client/inspector/grids/test/browser.ini
@@ -23,10 +23,11 @@ support-files =
 [browser_grids_grid-list-on-mutation-element-added.js]
 [browser_grids_grid-list-on-mutation-element-removed.js]
 [browser_grids_grid-list-toggle-multiple-grids.js]
 [browser_grids_grid-list-toggle-single-grid.js]
 [browser_grids_grid-outline-cannot-show-outline.js]
 [browser_grids_grid-outline-highlight-area.js]
 [browser_grids_grid-outline-highlight-cell.js]
 [browser_grids_grid-outline-selected-grid.js]
+[browser_grids_grid-outline-updates-on-grid-change.js]
 [browser_grids_highlighter-setting-rules-grid-toggle.js]
 [browser_grids_number-of-css-grids-telemetry.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-outline-updates-on-grid-change.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the grid outline does reflect the grid in the page even after the grid has
+// changed.
+
+const TEST_URI = `
+  <style>
+  .container {
+    display: grid;
+    grid-template-columns: repeat(2, 20vw);
+    grid-auto-rows: 20px;
+  }
+  </style>
+  <div class="container">
+    <div>item 1</div>
+    <div>item 2</div>
+  </div>
+`;
+
+add_task(function* () {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+
+  let { inspector, gridInspector, testActor } = yield openLayoutView();
+  let { document: doc } = gridInspector;
+  let { highlighters, store } = inspector;
+
+  info("Clicking on the first checkbox to highlight the grid");
+  let checkbox = doc.querySelector("#grid-list input");
+
+  let onHighlighterShown = highlighters.once("grid-highlighter-shown");
+  let onCheckboxChange = waitUntilState(store, state =>
+    state.grids.length == 1 && state.grids[0].highlighted);
+  let onGridOutlineRendered = waitForDOM(doc, ".grid-outline-cell", 2);
+
+  checkbox.click();
+
+  yield onHighlighterShown;
+  yield onCheckboxChange;
+  let elements = yield onGridOutlineRendered;
+
+  info("Checking the grid outline is shown.");
+  is(elements.length, 2, "Grid outline is shown.");
+
+  info("Changing the grid in the page");
+  let onReflow = new Promise(resolve => {
+    let listener = {
+      callback: () => {
+        inspector.reflowTracker.untrackReflows(listener, listener.callback);
+        resolve();
+      }
+    };
+    inspector.reflowTracker.trackReflows(listener, listener.callback);
+  });
+  let onGridOutlineChanged = waitForDOM(doc, ".grid-outline-cell", 4);
+
+  testActor.eval(`
+    const div = content.document.createElement("div");
+    div.textContent = "item 3";
+    content.document.querySelector(".container").appendChild(div);
+  `);
+
+  yield onReflow;
+  elements = yield onGridOutlineChanged;
+
+  info("Checking the grid outline is correct.");
+  is(elements.length, 4, "Grid outline was changed.");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/grids/test/unit/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+  // Extend from the common devtools xpcshell eslintrc config.
+  "extends": "../../../../../.eslintrc.xpcshell.js"
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/grids/test/unit/head.js
@@ -0,0 +1,9 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+const { utils: Cu } = Components;
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/grids/test/unit/test_compare_fragments_geometry.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { compareFragmentsGeometry } = require("devtools/client/inspector/grids/utils/utils");
+
+const TESTS = [{
+  desc: "No fragments",
+  grids: [[], []],
+  expected: true
+}, {
+  desc: "Different number of fragments",
+  grids: [
+    [{}, {}, {}],
+    [{}, {}]
+  ],
+  expected: false
+}, {
+  desc: "Different number of columns",
+  grids: [
+    [{cols: {lines: [{}, {}]}, rows: {lines: []}}],
+    [{cols: {lines: [{}]}, rows: {lines: []}}]
+  ],
+  expected: false
+}, {
+  desc: "Different number of rows",
+  grids: [
+    [{cols: {lines: [{}, {}]}, rows: {lines: [{}]}}],
+    [{cols: {lines: [{}, {}]}, rows: {lines: [{}, {}]}}]
+  ],
+  expected: false
+}, {
+  desc: "Different number of rows and columns",
+  grids: [
+    [{cols: {lines: [{}]}, rows: {lines: [{}]}}],
+    [{cols: {lines: [{}, {}]}, rows: {lines: [{}, {}]}}]
+  ],
+  expected: false
+}, {
+  desc: "Different column sizes",
+  grids: [
+    [{cols: {lines: [{start: 0}, {start: 500}]}, rows: {lines: []}}],
+    [{cols: {lines: [{start: 0}, {start: 1000}]}, rows: {lines: []}}]
+  ],
+  expected: false
+}, {
+  desc: "Different row sizes",
+  grids: [
+    [{cols: {lines: [{start: 0}, {start: 500}]}, rows: {lines: [{start: -100}]}}],
+    [{cols: {lines: [{start: 0}, {start: 500}]}, rows: {lines: [{start: 0}]}}]
+  ],
+  expected: false
+}, {
+  desc: "Different row and column sizes",
+  grids: [
+    [{cols: {lines: [{start: 0}, {start: 500}]}, rows: {lines: [{start: -100}]}}],
+    [{cols: {lines: [{start: 0}, {start: 505}]}, rows: {lines: [{start: 0}]}}]
+  ],
+  expected: false
+}, {
+  desc: "Complete structure, same fragments",
+  grids: [
+    [{cols: {lines: [{start: 0}, {start: 100.3}, {start: 200.6}]},
+      rows: {lines: [{start: 0}, {start: 1000}, {start: 2000}]}}],
+    [{cols: {lines: [{start: 0}, {start: 100.3}, {start: 200.6}]},
+      rows: {lines: [{start: 0}, {start: 1000}, {start: 2000}]}}]
+  ],
+  expected: true
+}];
+
+function run_test() {
+  for (let { desc, grids, expected } of TESTS) {
+    if (desc) {
+      do_print(desc);
+    }
+    equal(compareFragmentsGeometry(grids[0], grids[1]), expected);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/grids/test/unit/xpcshell.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+tags = devtools
+firefox-appdir = browser
+head = head.js
+
+[test_compare_fragments_geometry.js]
--- a/devtools/client/inspector/grids/utils/moz.build
+++ b/devtools/client/inspector/grids/utils/moz.build
@@ -1,9 +1,10 @@
 # -*- 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/.
 
 DevToolsModules(
     'l10n.js',
+    'utils.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/grids/utils/utils.js
@@ -0,0 +1,52 @@
+/* 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";
+
+/**
+ * Compares 2 sets of grid fragments to each other and checks if they have the same
+ * general geometry.
+ * This means that things like areas, area names or line names are ignored.
+ * This only checks if the 2 sets of fragments have as many fragments, as many lines, and
+ * that those lines are at the same distance.
+ *
+ * @param  {Array} fragments1
+ *         A list of gridFragment objects.
+ * @param  {Array} fragments2
+ *         Another list of gridFragment objects to compare to the first list.
+ * @return {Boolean}
+ *         True if the fragments are the same, false otherwise.
+ */
+function compareFragmentsGeometry(fragments1, fragments2) {
+  // Compare the number of fragments.
+  if (fragments1.length !== fragments2.length) {
+    return false;
+  }
+
+  // Compare the number of areas, rows and columns.
+  for (let i = 0; i < fragments1.length; i++) {
+    if (fragments1[i].cols.lines.length !== fragments2[i].cols.lines.length ||
+        fragments1[i].rows.lines.length !== fragments2[i].rows.lines.length) {
+      return false;
+    }
+  }
+
+  // Compare the offset of lines.
+  for (let i = 0; i < fragments1.length; i++) {
+    for (let j = 0; j < fragments1[i].cols.lines.length; j++) {
+      if (fragments1[i].cols.lines[j].start !== fragments2[i].cols.lines[j].start) {
+        return false;
+      }
+    }
+    for (let j = 0; j < fragments1[i].rows.lines.length; j++) {
+      if (fragments1[i].rows.lines[j].start !== fragments2[i].rows.lines[j].start) {
+        return false;
+      }
+    }
+  }
+
+  return true;
+}
+
+module.exports.compareFragmentsGeometry = compareFragmentsGeometry;
--- a/devtools/client/inspector/rules/rules.js
+++ b/devtools/client/inspector/rules/rules.js
@@ -24,17 +24,17 @@ const {
   VIEW_NODE_SELECTOR_TYPE,
   VIEW_NODE_PROPERTY_TYPE,
   VIEW_NODE_VALUE_TYPE,
   VIEW_NODE_IMAGE_URL_TYPE,
   VIEW_NODE_LOCATION_TYPE,
 } = require("devtools/client/inspector/shared/node-types");
 const StyleInspectorMenu = require("devtools/client/inspector/shared/style-inspector-menu");
 const TooltipsOverlay = require("devtools/client/inspector/shared/tooltips-overlay");
-const {createChild, promiseWarn, throttle} = require("devtools/client/inspector/shared/utils");
+const {createChild, promiseWarn, debounce} = require("devtools/client/inspector/shared/utils");
 const EventEmitter = require("devtools/shared/event-emitter");
 const KeyShortcuts = require("devtools/client/shared/key-shortcuts");
 const clipboardHelper = require("devtools/shared/platform/clipboard");
 const AutocompletePopup = require("devtools/client/shared/autocomplete-popup");
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles";
 const PREF_DEFAULT_COLOR_UNIT = "devtools.defaultColorUnit";
@@ -102,18 +102,18 @@ const FILTER_STRICT_RE = /\s*`(.*?)`\s*$
 function CssRuleView(inspector, document, store, pageStyle) {
   this.inspector = inspector;
   this.highlighters = inspector.highlighters;
   this.styleDocument = document;
   this.styleWindow = this.styleDocument.defaultView;
   this.store = store || {};
   this.pageStyle = pageStyle;
 
-  // Allow tests to override throttling behavior, as this can cause intermittents.
-  this.throttle = throttle;
+  // Allow tests to override debouncing behavior, as this can cause intermittents.
+  this.debounce = debounce;
 
   this.cssProperties = getCssProperties(inspector.toolbox);
 
   this._outputParser = new OutputParser(document, this.cssProperties);
 
   this._onAddRule = this._onAddRule.bind(this);
   this._onContextMenu = this._onContextMenu.bind(this);
   this._onCopy = this._onCopy.bind(this);
--- a/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_01.js
+++ b/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_01.js
@@ -115,18 +115,18 @@ function* testCompletion([key, completio
 
   // Also listening for popup opened/closed events if needed.
   let popupEvent = open ? "popup-opened" : "popup-closed";
   let onPopupEvent = editor.popup.isOpen !== open ? once(editor.popup, popupEvent) : null;
 
   info("Synthesizing key " + key);
   EventUtils.synthesizeKey(key, {}, view.styleWindow);
 
-  // Flush the throttle for the preview text.
-  view.throttle.flush();
+  // Flush the debounce for the preview text.
+  view.debounce.flush();
 
   yield onSuggest;
   yield onPopupEvent;
 
   info("Checking the state");
   if (completion !== null) {
     is(editor.input.value, completion, "Correct value is autocompleted");
   }
--- a/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_02.js
+++ b/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_02.js
@@ -94,18 +94,18 @@ function* testCompletion([key, modifiers
   info("Synthesizing key " + key + ", modifiers: " + Object.keys(modifiers));
 
   // Also listening for popup opened/closed events if needed.
   let popupEvent = open ? "popup-opened" : "popup-closed";
   let onPopupEvent = editor.popup.isOpen !== open ? once(editor.popup, popupEvent) : null;
 
   EventUtils.synthesizeKey(key, modifiers, view.styleWindow);
 
-  // Flush the throttle for the preview text.
-  view.throttle.flush();
+  // Flush the debounce for the preview text.
+  view.debounce.flush();
 
   yield onDone;
   yield onPopupEvent;
 
   // The key might have been a TAB or shift-TAB, in which case the editor will
   // be a new one
   editor = inplaceEditor(view.styleDocument.activeElement);
 
--- a/devtools/client/inspector/rules/test/browser_rules_completion-new-property_02.js
+++ b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_02.js
@@ -102,18 +102,18 @@ function* testCompletion([key, modifiers
 
   // Also listening for popup opened/closed events if needed.
   let popupEvent = open ? "popup-opened" : "popup-closed";
   let onPopupEvent = editor.popup.isOpen !== open ? once(editor.popup, popupEvent) : null;
 
   info("Synthesizing key " + key + ", modifiers: " + Object.keys(modifiers));
   EventUtils.synthesizeKey(key, modifiers, view.styleWindow);
 
-  // Flush the throttle for the preview text.
-  view.throttle.flush();
+  // Flush the debounce for the preview text.
+  view.debounce.flush();
 
   yield onDone;
   yield onPopupEvent;
 
   info("Checking the state");
   if (completion !== null) {
     // The key might have been a TAB or shift-TAB, in which case the editor will
     // be a new one
--- a/devtools/client/inspector/rules/test/browser_rules_completion-new-property_multiline.js
+++ b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_multiline.js
@@ -94,17 +94,17 @@ add_task(function* () {
 
   info("Select the background-color suggestion with a mouse click.");
   let onRuleviewChanged = view.once("ruleview-changed");
   let onSuggest = editor.once("after-suggest");
 
   let node = editor.popup._list.childNodes[editor.popup.selectedIndex];
   EventUtils.synthesizeMouseAtCenter(node, {}, editor.popup._window);
 
-  view.throttle.flush();
+  view.debounce.flush();
   yield onSuggest;
   yield onRuleviewChanged;
 
   is(editor.input.value, EXPECTED_CSS_VALUE,
     "Input value correctly autocompleted");
 
   info("Press ESCAPE to leave the input.");
   onRuleviewChanged = view.once("ruleview-changed");
--- a/devtools/client/inspector/rules/test/browser_rules_edit-property-commit.js
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-commit.js
@@ -67,17 +67,17 @@ function* runTestData(view, {value, comm
 
   let editor = yield focusEditableField(view, propEditor.valueSpan);
   is(inplaceEditor(propEditor.valueSpan), editor,
     "Focused editor should be the value span.");
 
   info("Entering test data " + value);
   let onRuleViewChanged = view.once("ruleview-changed");
   EventUtils.sendString(value, view.styleWindow);
-  view.throttle.flush();
+  view.debounce.flush();
   yield onRuleViewChanged;
 
   info("Entering the commit key " + commitKey + " " + modifiers);
   onRuleViewChanged = view.once("ruleview-changed");
   let onBlur = once(editor.input, "blur");
   EventUtils.synthesizeKey(commitKey, modifiers);
   yield onBlur;
   yield onRuleViewChanged;
--- a/devtools/client/inspector/rules/test/browser_rules_edit-property-computed.js
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-computed.js
@@ -36,20 +36,20 @@ function* editAndCheck(view) {
 
   let onPropertyChange = waitForComputedStyleProperty("#testid", null,
     "padding-top", newPaddingValue);
   let onRefreshAfterPreview = once(view, "ruleview-changed");
 
   info("Entering a new value");
   EventUtils.sendString(newPaddingValue, view.styleWindow);
 
-  info("Waiting for the throttled previewValue to apply the " +
+  info("Waiting for the debounced previewValue to apply the " +
     "changes to document");
 
-  view.throttle.flush();
+  view.debounce.flush();
   yield onPropertyChange;
 
   info("Waiting for ruleview-refreshed after previewValue was applied.");
   yield onRefreshAfterPreview;
 
   let onBlur = once(editor.input, "blur");
 
   info("Entering the commit key and finishing edit");
--- a/devtools/client/inspector/rules/test/browser_rules_edit-property-increments.js
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-increments.js
@@ -233,17 +233,17 @@ function* runIncrementTest(propertyEdito
   for (let test in tests) {
     yield testIncrement(editor, tests[test], view, propertyEditor);
   }
 
   // Blur the field to put back the UI in its initial state (and avoid pending
   // requests when the test ends).
   let onRuleViewChanged = view.once("ruleview-changed");
   EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow);
-  view.throttle.flush();
+  view.debounce.flush();
   yield onRuleViewChanged;
 }
 
 function* testIncrement(editor, options, view) {
   editor.input.value = options.start;
   let input = editor.input;
 
   if (options.selectAll) {
@@ -267,14 +267,14 @@ function* testIncrement(editor, options,
 
   EventUtils.synthesizeKey(key, {altKey: options.alt, shiftKey: options.shift},
     view.styleWindow);
 
   yield onKeyUp;
 
   // Only expect a change if the value actually changed!
   if (options.start !== options.end) {
-    view.throttle.flush();
+    view.debounce.flush();
     yield onRuleViewChanged;
   }
 
   is(input.value, options.end, "Value changed to " + options.end);
 }
--- a/devtools/client/inspector/rules/test/browser_rules_edit-property_02.js
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_02.js
@@ -64,17 +64,17 @@ function* testEditProperty(inspector, ru
 
   info("Entering a value following by a semi-colon to commit it");
   let onBlur = once(editor.input, "blur");
   // Use sendChar() to pass each character as a string so that we can test
   // prop.editor.warning.hidden after each character.
   for (let ch of "red;") {
     let onPreviewDone = ruleView.once("ruleview-changed");
     EventUtils.sendChar(ch, ruleView.styleWindow);
-    ruleView.throttle.flush();
+    ruleView.debounce.flush();
     yield onPreviewDone;
     is(prop.editor.warning.hidden, true,
       "warning triangle is hidden or shown as appropriate");
   }
   yield onBlur;
 
   let newValue = yield executeInContent("Test:GetRulePropertyValue", {
     styleSheetIndex: 0,
--- a/devtools/client/inspector/rules/test/browser_rules_livepreview.js
+++ b/devtools/client/inspector/rules/test/browser_rules_livepreview.js
@@ -48,17 +48,17 @@ function* testLivePreviewData(data, rule
   info("Focusing the property value inplace-editor");
   let editor = yield focusEditableField(ruleView, propEditor.valueSpan);
   is(inplaceEditor(propEditor.valueSpan), editor,
     "The focused editor is the value");
 
   info("Entering value in the editor: " + data.value);
   let onPreviewDone = ruleView.once("ruleview-changed");
   EventUtils.sendString(data.value, ruleView.styleWindow);
-  ruleView.throttle.flush();
+  ruleView.debounce.flush();
   yield onPreviewDone;
 
   let onValueDone = ruleView.once("ruleview-changed");
   if (data.escape) {
     EventUtils.synthesizeKey("VK_ESCAPE", {});
   } else {
     EventUtils.synthesizeKey("VK_RETURN", {});
   }
--- a/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_02.js
+++ b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_02.js
@@ -10,17 +10,17 @@
 const TEST_URI = "<div>Test Element</div>";
 
 add_task(function* () {
   yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view} = yield openRuleView();
 
   // Turn off throttling, which can cause intermittents. Throttling is used by
   // the TextPropertyEditor.
-  view.throttle = () => {};
+  view.debounce = () => {};
 
   yield selectNode("div", inspector);
 
   let ruleEditor = getRuleViewRuleEditor(view, 0);
   // Note that we wait for a markup mutation here because this new rule will end
   // up creating a style attribute on the node shown in the markup-view.
   // (we also wait for the rule-view to refresh).
   let onMutation = inspector.once("markupmutation");
--- a/devtools/client/inspector/rules/test/browser_rules_search-filter_09.js
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_09.js
@@ -56,17 +56,17 @@ add_task(function* () {
 
   // Getting the new value editor after focus
   editor = inplaceEditor(view.styleDocument.activeElement);
   let propEditor = ruleEditor.rule.textProps[2].editor;
 
   info("Entering a value and bluring the field to expect a rule change");
   onRuleViewChanged = view.once("ruleview-changed");
   editor.input.value = "100%";
-  view.throttle.flush();
+  view.debounce.flush();
   yield onRuleViewChanged;
 
   onRuleViewChanged = view.once("ruleview-changed");
   editor.input.blur();
   yield onRuleViewChanged;
 
   ok(propEditor.container.classList.contains("ruleview-highlight"),
     "margin-left text property is correctly highlighted.");
--- a/devtools/client/inspector/rules/test/head.js
+++ b/devtools/client/inspector/rules/test/head.js
@@ -284,17 +284,17 @@ var addProperty = Task.async(function* (
   is(editor, inplaceEditor(textProp.editor.valueSpan),
      "The inplace editor appeared for the value");
 
   info("Adding value " + value);
   // Setting the input value schedules a preview to be shown in 10ms which
   // triggers a ruleview-changed event (see bug 1209295).
   let onPreview = view.once("ruleview-changed");
   editor.input.value = value;
-  view.throttle.flush();
+  view.debounce.flush();
   yield onPreview;
 
   let onValueAdded = view.once("ruleview-changed");
   EventUtils.synthesizeKey(commitValueWith, {}, view.styleWindow);
   yield onValueAdded;
 
   if (blurNewProperty) {
     view.styleDocument.activeElement.blur();
@@ -323,17 +323,17 @@ var setProperty = Task.async(function* (
   yield focusEditableField(view, textProp.editor.valueSpan);
 
   let onPreview = view.once("ruleview-changed");
   if (value === null) {
     EventUtils.synthesizeKey("VK_DELETE", {}, view.styleWindow);
   } else {
     EventUtils.sendString(value, view.styleWindow);
   }
-  view.throttle.flush();
+  view.debounce.flush();
   yield onPreview;
 
   let onValueDone = view.once("ruleview-changed");
   EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
   yield onValueDone;
 
   if (blurNewProperty) {
     view.styleDocument.activeElement.blur();
--- a/devtools/client/inspector/rules/views/text-property-editor.js
+++ b/devtools/client/inspector/rules/views/text-property-editor.js
@@ -70,17 +70,17 @@ function TextPropertyEditor(ruleEditor, 
   this._onEnableClicked = this._onEnableClicked.bind(this);
   this._onExpandClicked = this._onExpandClicked.bind(this);
   this._onStartEditing = this._onStartEditing.bind(this);
   this._onNameDone = this._onNameDone.bind(this);
   this._onValueDone = this._onValueDone.bind(this);
   this._onSwatchCommit = this._onSwatchCommit.bind(this);
   this._onSwatchPreview = this._onSwatchPreview.bind(this);
   this._onSwatchRevert = this._onSwatchRevert.bind(this);
-  this._onValidate = this.ruleView.throttle(this._previewValue, 10, this);
+  this._onValidate = this.ruleView.debounce(this._previewValue, 10, this);
   this.update = this.update.bind(this);
   this.updatePropertyState = this.updatePropertyState.bind(this);
 
   this._create();
   this.update();
 }
 
 TextPropertyEditor.prototype = {
@@ -894,17 +894,17 @@ TextPropertyEditor.prototype = {
    * Live preview this property, without committing changes.
    *
    * @param {String} value
    *        The value to set the current property to.
    * @param {Boolean} reverting
    *        True if we're reverting the previously previewed value
    */
   _previewValue: function (value, reverting = false) {
-    // Since function call is throttled, we need to make sure we are still
+    // Since function call is debounced, we need to make sure we are still
     // editing, and any selector modifications have been completed
     if (!reverting && (!this.editing || this.ruleEditor.isEditing)) {
       return;
     }
 
     let val = parseSingleValue(this.cssProperties.isKnown, value);
     this.ruleEditor.rule.previewPropertyValue(this.prop, val.value,
                                               val.priority);
--- a/devtools/client/inspector/shared/utils.js
+++ b/devtools/client/inspector/shared/utils.js
@@ -95,42 +95,92 @@ function advanceValidate(keyCode, value,
     }
   }
   return false;
 }
 
 exports.advanceValidate = advanceValidate;
 
 /**
- * Create a throttling function wrapper to regulate its frequency.
+ * Create a debouncing function wrapper to only call the target function after a certain
+ * amount of time has passed without it being called.
  *
  * @param {Function} func
- *         The function to throttle
+ *         The function to debounce
  * @param {number} wait
- *         The throttling period
+ *         The wait period
  * @param {Object} scope
  *         The scope to use for func
- * @return {Function} The throttled function
+ * @return {Function} The debounced function
  */
-function throttle(func, wait, scope) {
+function debounce(func, wait, scope) {
   let timer = null;
 
   return function () {
     if (timer) {
       clearTimeout(timer);
     }
 
     let args = arguments;
     timer = setTimeout(function () {
       timer = null;
       func.apply(scope, args);
     }, wait);
   };
 }
 
+exports.debounce = debounce;
+
+/**
+ * From underscore's `_.throttle`
+ * http://underscorejs.org
+ * (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+ * Underscore may be freely distributed under the MIT license.
+ *
+ * Returns a function, that, when invoked, will only be triggered at most once during a
+ * given window of time. The throttled function will run as much as it can, without ever
+ * going more than once per wait duration.
+ *
+ * @param  {Function} func
+ *         The function to throttle
+ * @param  {number} wait
+ *         The wait period
+ * @param  {Object} scope
+ *         The scope to use for func
+ * @return {Function} The throttled function
+ */
+function throttle(func, wait, scope) {
+  let args, result;
+  let timeout = null;
+  let previous = 0;
+
+  let later = function () {
+    previous = Date.now();
+    timeout = null;
+    result = func.apply(scope, args);
+    args = null;
+  };
+
+  return function () {
+    let now = Date.now();
+    let remaining = wait - (now - previous);
+    args = arguments;
+    if (remaining <= 0) {
+      clearTimeout(timeout);
+      timeout = null;
+      previous = now;
+      result = func.apply(scope, args);
+      args = null;
+    } else if (!timeout) {
+      timeout = setTimeout(later, remaining);
+    }
+    return result;
+  };
+}
+
 exports.throttle = throttle;
 
 /**
  * Event handler that causes a blur on the target if the input has
  * multiple CSS properties as the value.
  */
 function blurOnMultipleProperties(cssProperties) {
   return (e) => {
--- a/devtools/client/inspector/test/shared-head.js
+++ b/devtools/client/inspector/test/shared-head.js
@@ -73,19 +73,19 @@ var openInspectorSidebarTab = Task.async
  * Open the toolbox, with the inspector tool visible, and the rule-view
  * sidebar tab selected.
  *
  * @return a promise that resolves when the inspector is ready and the rule view
  * is visible and ready
  */
 function openRuleView() {
   return openInspectorSidebarTab("ruleview").then(data => {
-    // Replace the view to use a custom throttle function that can be triggered manually
+    // Replace the view to use a custom debounce function that can be triggered manually
     // through an additional ".flush()" property.
-    data.inspector.getPanel("ruleview").view.throttle = manualThrottle();
+    data.inspector.getPanel("ruleview").view.debounce = manualDebounce();
 
     return {
       toolbox: data.toolbox,
       inspector: data.inspector,
       testActor: data.testActor,
       view: data.inspector.getPanel("ruleview").view
     };
   });
@@ -194,42 +194,42 @@ var selectNode = Task.async(function* (s
   let nodeFront = yield getNodeFront(selector, inspector);
   let updated = inspector.once("inspector-updated");
   inspector.selection.setNodeFront(nodeFront, reason);
   yield updated;
 });
 
 /**
  * Create a throttling function that can be manually "flushed". This is to replace the
- * use of the `throttle` function from `devtools/client/inspector/shared/utils.js`, which
+ * use of the `debounce` function from `devtools/client/inspector/shared/utils.js`, which
  * has a setTimeout that can cause intermittents.
- * @return {Function} This function has the same function signature as throttle, but
+ * @return {Function} This function has the same function signature as debounce, but
  *                    the property `.flush()` has been added for flushing out any
- *                    throttled calls.
+ *                    debounced calls.
  */
-function manualThrottle() {
+function manualDebounce() {
   let calls = [];
 
-  function throttle(func, wait, scope) {
+  function debounce(func, wait, scope) {
     return function () {
       let existingCall = calls.find(call => call.func === func);
       if (existingCall) {
         existingCall.args = arguments;
       } else {
         calls.push({ func, wait, scope, args: arguments });
       }
     };
   }
 
-  throttle.flush = function () {
+  debounce.flush = function () {
     calls.forEach(({func, scope, args}) => func.apply(scope, args));
     calls = [];
   };
 
-  return throttle;
+  return debounce;
 }
 
 /**
  * Wait for a content -> chrome message on the message manager (the window
  * messagemanager is used).
  *
  * @param {String} name
  *        The message name
--- a/devtools/server/actors/layout.js
+++ b/devtools/server/actors/layout.js
@@ -53,16 +53,23 @@ var GridActor = ActorClassWithSpec(gridS
     let gridFragments = this.containerEl.getGridFragments();
     this.gridFragments = getStringifiableFragments(gridFragments);
 
     let form = {
       actor: this.actorID,
       gridFragments: this.gridFragments
     };
 
+    // If the WalkerActor already knows the container element, then also return its
+    // ActorID so we avoid the client from doing another round trip to get it in many
+    // cases.
+    if (this.walker.hasNode(this.containerEl)) {
+      form.containerNodeActorID = this.walker.getNode(this.containerEl).actorID;
+    }
+
     return form;
   },
 });
 
 /**
  * The CSS layout actor provides layout information for the given document.
  */
 var LayoutActor = ActorClassWithSpec(layoutSpec, {
--- a/devtools/shared/fronts/layout.js
+++ b/devtools/shared/fronts/layout.js
@@ -12,16 +12,28 @@ const GridFront = FrontClassWithSpec(gri
     if (detail === "actorid") {
       this.actorID = form;
       return;
     }
     this._form = form;
   },
 
   /**
+   * In some cases, the GridActor already knows the NodeActor ID of the node where the
+   * grid is located. In such cases, this getter returns the NodeFront for it.
+   */
+  get containerNodeFront() {
+    if (!this._form.containerNodeActorID) {
+      return null;
+    }
+
+    return this.conn.getActor(this._form.containerNodeActorID);
+  },
+
+  /**
    * Getter for the grid fragments data.
    */
   get gridFragments() {
     return this._form.gridFragments;
   }
 });
 
 const LayoutFront = FrontClassWithSpec(layoutSpec, {});