Improve rendering performance in the Computed Styles pane

Currently the Computed Styles pane renders several hundreds of computed
styles in a single cycle, which means creating, appending, layout-out,
rendering, and painting hundreds of heavily styled <li> items that
contains widgets and children. This can create a visual lag ranging
from ~100ms to ~1s. This CL breaks down these operations into smaller
batches, and queues them using browser's requestIdleCallback to provide
an instant rendering feedback.

tracing before: https://chromedevtools.github.io/timeline-viewer/?loadTimelineFromURL=https://gist.githubusercontent.com/hanselfmu/424153ea140df049800e716031882238/raw/c62f1561cf4f56c1c5c869325015f3d99ab65637/computed_tab_perf_before.json
tracing after: https://chromedevtools.github.io/timeline-viewer/?loadTimelineFromURL=https://gist.github.com/hanselfmu/5fdc43d3ae878aed82e79ff3af3c1ed6/raw/7d5095dfde0236eccafe53ec0fb22356e303afb9/computed_tab_perf_after.json

Bug: chromium:1100148, chromium:1106251, chromium:1093263
Change-Id: Id0acbf91542280b026fdb67e28097bdb445606ea
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2308547
Commit-Queue: Changhao Han <changhaohan@chromium.org>
Reviewed-by: Alex Rudenko <alexrudenko@chromium.org>
Reviewed-by: Mathias Bynens <mathias@chromium.org>
This commit is contained in:
Changhao Han
2020-07-22 12:30:40 +00:00
committed by Commit Bot
parent a33f641501
commit 097956ebb5
5 changed files with 85 additions and 41 deletions
+75 -35
View File
@@ -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<string>} */
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
+4 -3
View File
@@ -988,7 +988,7 @@ export class SectionBlock {
}
}
class IdleCallbackManager {
export class IdleCallbackManager {
constructor() {
this._discarded = false;
/** @type {!Array<!Promise<void>>} */
@@ -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});
}));
}
@@ -85,7 +85,7 @@
right: 0;
}
.tree-outline li.odd-row {
.tree-outline li.even-row {
position: relative;
background-color: #f5f5f5;
}
@@ -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));
}
};
@@ -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');