diff --git a/front_end/elements/ComputedStyleWidget.js b/front_end/elements/ComputedStyleWidget.js index 5327ed0dcc..9a316f40f2 100644 --- a/front_end/elements/ComputedStyleWidget.js +++ b/front_end/elements/ComputedStyleWidget.js @@ -36,7 +36,7 @@ import * as UI from '../ui/ui.js'; import {ComputedStyle, ComputedStyleModel, Events} from './ComputedStyleModel.js'; // eslint-disable-line no-unused-vars import {ImagePreviewPopover} from './ImagePreviewPopover.js'; import {PlatformFontsWidget} from './PlatformFontsWidget.js'; -import {StylePropertiesSection, StylesSidebarPane, StylesSidebarPropertyRenderer} from './StylesSidebarPane.js'; +import {IdleCallbackManager, StylePropertiesSection, StylesSidebarPane, StylesSidebarPropertyRenderer} from './StylesSidebarPane.js'; /** * @param {!SDK.DOMModel.DOMNode} node @@ -215,6 +215,9 @@ export class ComputedStyleWidget extends UI.ThrottledWidget.ThrottledWidget { const fontsWidget = new PlatformFontsWidget(this._computedStyleModel); fontsWidget.show(this.contentElement); + + /** @type {?IdleCallbackManager} */ + this._idleCallbackManager = null; } /** @@ -229,6 +232,17 @@ export class ComputedStyleWidget extends UI.ThrottledWidget.ThrottledWidget { this.update(); } + /** + * @override + */ + update() { + if (this._idleCallbackManager) { + this._idleCallbackManager.discard(); + } + this._idleCallbackManager = new IdleCallbackManager(); + super.update(); + } + /** * @override * @return {!Promise.} @@ -236,7 +250,7 @@ export class ComputedStyleWidget extends UI.ThrottledWidget.ThrottledWidget { async doUpdate() { const promises = [this._computedStyleModel.fetchComputedStyle(), this._fetchMatchedCascade()]; const [nodeStyles, matchedStyles] = await Promise.all(promises); - this._innerRebuildUpdate(nodeStyles, matchedStyles); + await this._innerRebuildUpdate(nodeStyles, matchedStyles); } /** @@ -264,7 +278,7 @@ export class ComputedStyleWidget extends UI.ThrottledWidget.ThrottledWidget { * @param {?ComputedStyle} nodeStyle * @param {?SDK.CSSMatchedStyles.CSSMatchedStyles} matchedStyles */ - _innerRebuildUpdate(nodeStyle, matchedStyles) { + async _innerRebuildUpdate(nodeStyle, matchedStyles) { /** @type {!Set} */ const expandedProperties = new Set(); for (const treeElement of this._propertiesOutline.rootElement().children()) { @@ -287,11 +301,13 @@ export class ComputedStyleWidget extends UI.ThrottledWidget.ThrottledWidget { const uniqueProperties = [...nodeStyle.computedStyle.keys()]; uniqueProperties.sort(propertySorter); + const node = nodeStyle.node; const propertyTraces = this._computePropertyTraces(matchedStyles); const inheritedProperties = this._computeInheritedProperties(matchedStyles); const showInherited = this._showInheritedComputedStylePropertiesSetting.get(); - for (let i = 0; i < uniqueProperties.length; ++i) { - const propertyName = uniqueProperties[i]; + const computedStyleQueue = []; + // filter and preprocess properties to line up in the computed style queue + for (const propertyName of uniqueProperties) { const propertyValue = nodeStyle.computedStyle.get(propertyName); const canonicalName = SDK.CSSMetadata.cssMetadata().canonicalPropertyName(propertyName); const isInherited = !inheritedProperties.has(canonicalName); @@ -304,38 +320,62 @@ export class ComputedStyleWidget extends UI.ThrottledWidget.ThrottledWidget { if (propertyName !== canonicalName && propertyValue === nodeStyle.computedStyle.get(canonicalName)) { continue; } - - const {propertyElement, propertyValueElement} = - createPropertyElement(nodeStyle.node, propertyName, propertyValue, isInherited); - - const treeElement = new UI.TreeOutline.TreeElement(); - treeElement.title = propertyElement; - treeElement[_propertySymbol] = {name: propertyName, value: propertyValue}; - const isOdd = this._propertiesOutline.rootElement().children().length % 2 === 0; - treeElement.listItemElement.classList.toggle('odd-row', isOdd); - this._propertiesOutline.appendChild(treeElement); - if (!this._propertiesOutline.selectedTreeElement) { - treeElement.select(!hadFocus); - } - - const trace = propertyTraces.get(propertyName); - if (trace) { - const activeProperty = this._renderPropertyTrace(matchedStyles, nodeStyle.node, treeElement, trace); - treeElement.listItemElement.addEventListener('mousedown', e => e.consume(), false); - treeElement.listItemElement.addEventListener('dblclick', e => e.consume(), false); - treeElement.listItemElement.addEventListener('click', handleClick.bind(null, treeElement), false); - treeElement.listItemElement.addEventListener( - 'contextmenu', this._handleContextMenuEvent.bind(this, matchedStyles, activeProperty)); - const gotoSourceElement = UI.Icon.Icon.create('mediumicon-arrow-in-circle', 'goto-source-icon'); - gotoSourceElement.addEventListener('click', navigateToSource.bind(this, activeProperty)); - propertyValueElement.appendChild(gotoSourceElement); - if (expandedProperties.has(propertyName)) { - treeElement.expand(); - } - } + computedStyleQueue.push({propertyName, propertyValue, isInherited}); } - this._updateFilter(this._filterRegex); + // Render computed style properties in batches via idle callbacks to avoid a + // very long task. The batchSize and timeoutInterval should be tweaked in + // pair. Currently, updating, laying-out, rendering, and painting 20 items + // in every 100ms seems to be a good balance between updating too lazy vs. + // updating too much in one cycle. + const batchSize = 20; + const timeoutInterval = 100; + let timeout = 100; + while (computedStyleQueue.length > 0) { + const currentBatch = computedStyleQueue.splice(0, batchSize); + + this._idleCallbackManager.schedule(() => { + for (const {propertyName, propertyValue, isInherited} of currentBatch) { + const {propertyElement, propertyValueElement} = + createPropertyElement(node, propertyName, propertyValue, isInherited); + const treeElement = new UI.TreeOutline.TreeElement(); + treeElement.title = propertyElement; + treeElement[_propertySymbol] = {name: propertyName, value: propertyValue}; + if (!this._propertiesOutline.selectedTreeElement) { + treeElement.select(!hadFocus); + } + + const trace = propertyTraces.get(propertyName); + if (trace) { + const activeProperty = this._renderPropertyTrace( + /** @type {!SDK.CSSMatchedStyles.CSSMatchedStyles} */ (matchedStyles), node, treeElement, trace); + treeElement.listItemElement.addEventListener('mousedown', e => e.consume(), false); + treeElement.listItemElement.addEventListener('dblclick', e => e.consume(), false); + treeElement.listItemElement.addEventListener('click', handleClick.bind(null, treeElement), false); + treeElement.listItemElement.addEventListener( + 'contextmenu', + this._handleContextMenuEvent.bind( + this, /** @type {!SDK.CSSMatchedStyles.CSSMatchedStyles} */ (matchedStyles), activeProperty)); + const gotoSourceElement = UI.Icon.Icon.create('mediumicon-arrow-in-circle', 'goto-source-icon'); + gotoSourceElement.addEventListener('click', navigateToSource.bind(this, activeProperty)); + propertyValueElement.appendChild(gotoSourceElement); + if (expandedProperties.has(propertyName)) { + treeElement.expand(); + } + } + + this._propertiesOutline.appendChild(treeElement); + const isEven = this._propertiesOutline.rootElement().children().length % 2 === 0; + treeElement.listItemElement.classList.toggle('even-row', isEven); + } + + this._updateFilter(this._filterRegex); + }, timeout); + + timeout += timeoutInterval; + } + + await this._idleCallbackManager.awaitDone(); /** * @param {string} a diff --git a/front_end/elements/StylesSidebarPane.js b/front_end/elements/StylesSidebarPane.js index 99e78d4c8e..8e14790558 100644 --- a/front_end/elements/StylesSidebarPane.js +++ b/front_end/elements/StylesSidebarPane.js @@ -988,7 +988,7 @@ export class SectionBlock { } } -class IdleCallbackManager { +export class IdleCallbackManager { constructor() { this._discarded = false; /** @type {!Array>} */ @@ -1001,8 +1001,9 @@ class IdleCallbackManager { /** * @param {function():void} fn + * @param {number} timeout */ - schedule(fn) { + schedule(fn, timeout = 100) { if (this._discarded) { return; } @@ -1020,7 +1021,7 @@ class IdleCallbackManager { return resolve(); } run(); - }, {timeout: 100}); + }, {timeout}); })); } diff --git a/front_end/elements/computedStyleWidgetTree.css b/front_end/elements/computedStyleWidgetTree.css index de27b6fda8..02da10311e 100644 --- a/front_end/elements/computedStyleWidgetTree.css +++ b/front_end/elements/computedStyleWidgetTree.css @@ -85,7 +85,7 @@ right: 0; } -.tree-outline li.odd-row { +.tree-outline li.even-row { position: relative; background-color: #f5f5f5; } diff --git a/front_end/elements_test_runner/ElementsTestRunner.js b/front_end/elements_test_runner/ElementsTestRunner.js index fc2bcc17ce..5142dec9b1 100644 --- a/front_end/elements_test_runner/ElementsTestRunner.js +++ b/front_end/elements_test_runner/ElementsTestRunner.js @@ -364,8 +364,8 @@ ElementsTestRunner.selectNodeAndWaitForStylesWithComputed = function(idValue, ca callback = TestRunner.safeWrap(callback); ElementsTestRunner.selectNodeAndWaitForStyles(idValue, onSidebarRendered); - function onSidebarRendered(node) { - ElementsTestRunner.computedStyleWidget().doUpdate().then(callback.bind(null, node)); + async function onSidebarRendered(node) { + await ElementsTestRunner.computedStyleWidget().doUpdate().then(callback.bind(null, node)); } }; diff --git a/test/e2e/elements/computed-pane-properties_test.ts b/test/e2e/elements/computed-pane-properties_test.ts index 1998f63ade..23bcd144f3 100644 --- a/test/e2e/elements/computed-pane-properties_test.ts +++ b/test/e2e/elements/computed-pane-properties_test.ts @@ -25,6 +25,7 @@ describe('The Computed pane', async () => { let content = await getContentOfComputedPane(); await frontend.keyboard.press('ArrowDown'); await waitForComputedPaneChange(content); + await waitForElementsComputedSection(); const h1Properties = await getAllPropertiesFromComputedPane(); assert.strictEqual(h1Properties.length, 10, 'There should be 10 computed properties on the H1 element'); @@ -40,6 +41,7 @@ describe('The Computed pane', async () => { content = await getContentOfComputedPane(); await frontend.keyboard.press('ArrowDown'); await waitForComputedPaneChange(content); + await waitForElementsComputedSection(); const h2Properties = await getAllPropertiesFromComputedPane(); assert.strictEqual(h2Properties.length, 11, 'There should be 11 computed properties on the H2 element'); @@ -61,6 +63,7 @@ describe('The Computed pane', async () => { await waitForComputedPaneChange(content); await toggleShowAllComputedProperties(); + await waitForElementsComputedSection(); const allH1Properties = await getAllPropertiesFromComputedPane(); const alignContentProperty = allH1Properties.find(property => property && property.name === 'align-content');