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

--HG--
extra : rebase_source : 6b9719379dcbd4479bded26014aa3742bd75208e
This commit is contained in:
Patrick Brosset 2017-06-27 12:02:53 -07:00
Родитель ea1e9c1f12
Коммит c109f6e2ed
28 изменённых файлов: 425 добавлений и 56 удалений

Просмотреть файл

@ -8,6 +8,8 @@ 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,
@ -51,9 +53,9 @@ function GridInspector(inspector, window) {
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);
@ -94,7 +96,7 @@ GridInspector.prototype = {
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();
}),
@ -107,7 +109,7 @@ GridInspector.prototype = {
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);
@ -211,7 +213,7 @@ GridInspector.prototype = {
* 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";
},
@ -278,13 +280,19 @@ GridInspector.prototype = {
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];
@ -303,9 +311,10 @@ GridInspector.prototype = {
}),
/**
* 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();
}
@ -345,15 +354,81 @@ GridInspector.prototype = {
},
/**
* 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.
*

Просмотреть файл

@ -17,3 +17,4 @@ DevToolsModules(
)
BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']

Просмотреть файл

@ -28,5 +28,6 @@ support-files =
[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]

Просмотреть файл

@ -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.");
});

Просмотреть файл

@ -0,0 +1,6 @@
"use strict";
module.exports = {
// Extend from the common devtools xpcshell eslintrc config.
"extends": "../../../../../.eslintrc.xpcshell.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", {});

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -0,0 +1,6 @@
[DEFAULT]
tags = devtools
firefox-appdir = browser
head = head.js
[test_compare_fragments_geometry.js]

Просмотреть файл

@ -6,4 +6,5 @@
DevToolsModules(
'l10n.js',
'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;

Просмотреть файл

@ -29,7 +29,7 @@ const {
} = 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");
@ -107,8 +107,8 @@ function CssRuleView(inspector, document, store, pageStyle) {
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);

Просмотреть файл

@ -120,8 +120,8 @@ function* testCompletion([key, completion, open, selected],
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;

Просмотреть файл

@ -99,8 +99,8 @@ function* testCompletion([key, modifiers, completion, open, selected, change],
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;

Просмотреть файл

@ -107,8 +107,8 @@ function* testCompletion([key, modifiers, completion, open, selected, change],
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;

Просмотреть файл

@ -99,7 +99,7 @@ add_task(function* () {
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;

Просмотреть файл

@ -72,7 +72,7 @@ function* runTestData(view, {value, commitKey, modifiers, expected}) {
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);

Просмотреть файл

@ -41,10 +41,10 @@ function* editAndCheck(view) {
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.");

Просмотреть файл

@ -238,7 +238,7 @@ function* runIncrementTest(propertyEditor, view, tests) {
// 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;
}
@ -272,7 +272,7 @@ function* testIncrement(editor, options, view) {
// Only expect a change if the value actually changed!
if (options.start !== options.end) {
view.throttle.flush();
view.debounce.flush();
yield onRuleViewChanged;
}

Просмотреть файл

@ -69,7 +69,7 @@ function* testEditProperty(inspector, ruleView) {
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");

Просмотреть файл

@ -53,7 +53,7 @@ function* testLivePreviewData(data, ruleView, selector) {
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");

Просмотреть файл

@ -15,7 +15,7 @@ add_task(function* () {
// Turn off throttling, which can cause intermittents. Throttling is used by
// the TextPropertyEditor.
view.throttle = () => {};
view.debounce = () => {};
yield selectNode("div", inspector);

Просмотреть файл

@ -61,7 +61,7 @@ add_task(function* () {
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");

Просмотреть файл

@ -289,7 +289,7 @@ var addProperty = Task.async(function* (view, ruleIndex, name, value,
// 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");
@ -328,7 +328,7 @@ var setProperty = Task.async(function* (view, textProp, value,
} else {
EventUtils.sendString(value, view.styleWindow);
}
view.throttle.flush();
view.debounce.flush();
yield onPreview;
let onValueDone = view.once("ruleview-changed");

Просмотреть файл

@ -75,7 +75,7 @@ function TextPropertyEditor(ruleEditor, property) {
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);
@ -899,7 +899,7 @@ TextPropertyEditor.prototype = {
* 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;

Просмотреть файл

@ -100,17 +100,18 @@ function advanceValidate(keyCode, value, insertionPoint) {
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 () {
@ -126,6 +127,55 @@ function throttle(func, wait, scope) {
};
}
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;
/**

Просмотреть файл

@ -78,9 +78,9 @@ var openInspectorSidebarTab = Task.async(function* (id) {
*/
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,
@ -199,16 +199,16 @@ var selectNode = Task.async(function* (selector, inspector, reason = "test") {
/**
* 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) {
@ -219,12 +219,12 @@ function manualThrottle() {
};
}
throttle.flush = function () {
debounce.flush = function () {
calls.forEach(({func, scope, args}) => func.apply(scope, args));
calls = [];
};
return throttle;
return debounce;
}
/**

Просмотреть файл

@ -58,6 +58,13 @@ var GridActor = ActorClassWithSpec(gridSpec, {
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;
},
});

Просмотреть файл

@ -16,6 +16,18 @@ const GridFront = FrontClassWithSpec(gridSpec, {
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.
*/