UNPKG

chrome-devtools-frontend

Version:
459 lines (393 loc) • 15.1 kB
/* * Copyright (C) 2013 Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ import * as Common from '../../core/common/common.js'; import * as SDK from '../../core/sdk/sdk.js'; import * as UI from '../../ui/legacy/legacy.js'; import type * as ProtocolProxyApi from '../../generated/protocol-proxy-api.js'; import type * as Protocol from '../../generated/protocol.js'; export class LayerTreeModel extends SDK.SDKModel.SDKModel<EventTypes> { readonly layerTreeAgent: ProtocolProxyApi.LayerTreeApi; readonly paintProfilerModel: SDK.PaintProfiler.PaintProfilerModel; private layerTreeInternal: SDK.LayerTreeBase.LayerTreeBase|null; private readonly throttler: Common.Throttler.Throttler; private enabled?: boolean; private lastPaintRectByLayerId?: Map<string, Protocol.DOM.Rect>; constructor(target: SDK.Target.Target) { super(target); this.layerTreeAgent = target.layerTreeAgent(); target.registerLayerTreeDispatcher(new LayerTreeDispatcher(this)); this.paintProfilerModel = target.model(SDK.PaintProfiler.PaintProfilerModel) as SDK.PaintProfiler.PaintProfilerModel; const resourceTreeModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel); if (resourceTreeModel) { resourceTreeModel.addEventListener( SDK.ResourceTreeModel.Events.PrimaryPageChanged, this.onPrimaryPageChanged, this); } this.layerTreeInternal = null; this.throttler = new Common.Throttler.Throttler(20); } async disable(): Promise<void> { if (!this.enabled) { return; } this.enabled = false; await this.layerTreeAgent.invoke_disable(); } enable(): void { if (this.enabled) { return; } this.enabled = true; void this.forceEnable(); } private async forceEnable(): Promise<void> { this.lastPaintRectByLayerId = new Map(); if (!this.layerTreeInternal) { this.layerTreeInternal = new AgentLayerTree(this); } await this.layerTreeAgent.invoke_enable(); } layerTree(): SDK.LayerTreeBase.LayerTreeBase|null { return this.layerTreeInternal; } async layerTreeChanged(layers: Protocol.LayerTree.Layer[]|null): Promise<void> { if (!this.enabled) { return; } void this.throttler.schedule(this.innerSetLayers.bind(this, layers)); } private async innerSetLayers(layers: Protocol.LayerTree.Layer[]|null): Promise<void> { const layerTree = this.layerTreeInternal as AgentLayerTree; await layerTree.setLayers(layers); if (!this.lastPaintRectByLayerId) { this.lastPaintRectByLayerId = new Map(); } for (const layerId of this.lastPaintRectByLayerId.keys()) { const lastPaintRect = this.lastPaintRectByLayerId.get(layerId); const layer = layerTree.layerById(layerId); if (layer) { (layer as AgentLayer).setLastPaintRect(lastPaintRect); } } this.lastPaintRectByLayerId = new Map(); this.dispatchEventToListeners(Events.LayerTreeChanged); } layerPainted(layerId: string, clipRect: Protocol.DOM.Rect): void { if (!this.enabled) { return; } const layerTree = this.layerTreeInternal as AgentLayerTree; const layer = layerTree.layerById(layerId) as AgentLayer; if (!layer) { if (!this.lastPaintRectByLayerId) { this.lastPaintRectByLayerId = new Map(); } this.lastPaintRectByLayerId.set(layerId, clipRect); return; } layer.didPaint(clipRect); this.dispatchEventToListeners(Events.LayerPainted, layer); } private onPrimaryPageChanged(): void { this.layerTreeInternal = null; if (this.enabled) { void this.forceEnable(); } } } SDK.SDKModel.SDKModel.register(LayerTreeModel, {capabilities: SDK.Target.Capability.DOM, autostart: false}); // TODO(crbug.com/1167717): Make this a const enum again // eslint-disable-next-line rulesdir/const_enum export enum Events { LayerTreeChanged = 'LayerTreeChanged', LayerPainted = 'LayerPainted', } export type EventTypes = { [Events.LayerTreeChanged]: void, [Events.LayerPainted]: AgentLayer, }; export class AgentLayerTree extends SDK.LayerTreeBase.LayerTreeBase { private layerTreeModel: LayerTreeModel; constructor(layerTreeModel: LayerTreeModel) { super(layerTreeModel.target()); this.layerTreeModel = layerTreeModel; } async setLayers(payload: Protocol.LayerTree.Layer[]|null): Promise<void> { if (!payload) { this.innerSetLayers(payload); return; } const idsToResolve = new Set<Protocol.DOM.BackendNodeId>(); for (let i = 0; i < payload.length; ++i) { const backendNodeId = payload[i].backendNodeId; if (!backendNodeId || this.backendNodeIdToNode().has(backendNodeId)) { continue; } idsToResolve.add(backendNodeId); } await this.resolveBackendNodeIds(idsToResolve); this.innerSetLayers(payload); } private innerSetLayers(layers: Protocol.LayerTree.Layer[]|null): void { this.setRoot(null); this.setContentRoot(null); // Payload will be null when not in the composited mode. if (!layers) { return; } let root; const oldLayersById = this.layersById; this.layersById = new Map(); for (let i = 0; i < layers.length; ++i) { const layerId = layers[i].layerId; let layer: AgentLayer|(AgentLayer | null) = oldLayersById.get(layerId) as AgentLayer | null; if (layer) { layer.reset(layers[i]); } else { layer = new AgentLayer(this.layerTreeModel, layers[i]); } this.layersById.set(layerId, layer); const backendNodeId = layers[i].backendNodeId; if (backendNodeId) { layer.setNode(this.backendNodeIdToNode().get(backendNodeId) || null); } if (!this.contentRoot() && layer.drawsContent()) { this.setContentRoot(layer); } const parentId = layer.parentId(); if (parentId) { const parent = this.layersById.get(parentId); if (!parent) { throw new Error(`Missing parent ${parentId} for layer ${layerId}`); } parent.addChild(layer); } else { if (root) { console.assert(false, 'Multiple root layers'); } root = layer; } } if (root) { this.setRoot(root); root.calculateQuad(new WebKitCSSMatrix()); } } } export class AgentLayer implements SDK.LayerTreeBase.Layer { private scrollRectsInternal!: Protocol.LayerTree.ScrollRect[]; private quadInternal!: number[]; private childrenInternal!: AgentLayer[]; private image!: HTMLImageElement|null; private parentInternal!: AgentLayer|null; private layerPayload!: Protocol.LayerTree.Layer; private layerTreeModel: LayerTreeModel; private nodeInternal?: SDK.DOMModel.DOMNode|null; lastPaintRectInternal?: Protocol.DOM.Rect; private paintCountInternal?: number; private stickyPositionConstraintInternal?: SDK.LayerTreeBase.StickyPositionConstraint|null; constructor(layerTreeModel: LayerTreeModel, layerPayload: Protocol.LayerTree.Layer) { this.layerTreeModel = layerTreeModel; this.reset(layerPayload); } id(): Protocol.LayerTree.LayerId { return this.layerPayload.layerId; } parentId(): Protocol.LayerTree.LayerId|null { return this.layerPayload.parentLayerId || null; } parent(): SDK.LayerTreeBase.Layer|null { return this.parentInternal; } isRoot(): boolean { return !this.parentId(); } children(): SDK.LayerTreeBase.Layer[] { return this.childrenInternal; } addChild(childParam: SDK.LayerTreeBase.Layer): void { const child = childParam as AgentLayer; if (child.parentInternal) { console.assert(false, 'Child already has a parent'); } this.childrenInternal.push(child); child.parentInternal = this; } setNode(node: SDK.DOMModel.DOMNode|null): void { this.nodeInternal = node; } node(): SDK.DOMModel.DOMNode|null { return this.nodeInternal || null; } nodeForSelfOrAncestor(): SDK.DOMModel.DOMNode|null { let layer: (AgentLayer|null)|this = this; for (; layer; layer = layer.parentInternal) { if (layer.nodeInternal) { return layer.nodeInternal; } } return null; } offsetX(): number { return this.layerPayload.offsetX; } offsetY(): number { return this.layerPayload.offsetY; } width(): number { return this.layerPayload.width; } height(): number { return this.layerPayload.height; } transform(): number[]|null { return this.layerPayload.transform || null; } quad(): number[] { return this.quadInternal; } anchorPoint(): number[] { return [ this.layerPayload.anchorX || 0, this.layerPayload.anchorY || 0, this.layerPayload.anchorZ || 0, ]; } invisible(): boolean { return this.layerPayload.invisible || false; } paintCount(): number { return this.paintCountInternal || this.layerPayload.paintCount; } lastPaintRect(): Protocol.DOM.Rect|null { return this.lastPaintRectInternal || null; } setLastPaintRect(lastPaintRect?: Protocol.DOM.Rect): void { this.lastPaintRectInternal = lastPaintRect; } scrollRects(): Protocol.LayerTree.ScrollRect[] { return this.scrollRectsInternal; } stickyPositionConstraint(): SDK.LayerTreeBase.StickyPositionConstraint|null { return this.stickyPositionConstraintInternal || null; } async requestCompositingReasons(): Promise<string[]> { const reasons = await this.layerTreeModel.layerTreeAgent.invoke_compositingReasons({layerId: this.id()}); return reasons.compositingReasons || []; } async requestCompositingReasonIds(): Promise<string[]> { const reasons = await this.layerTreeModel.layerTreeAgent.invoke_compositingReasons({layerId: this.id()}); return reasons.compositingReasonIds || []; } drawsContent(): boolean { return this.layerPayload.drawsContent; } gpuMemoryUsage(): number { const bytesPerPixel = 4; return this.drawsContent() ? this.width() * this.height() * bytesPerPixel : 0; } snapshots(): Promise<SDK.PaintProfiler.SnapshotWithRect|null>[] { const promise = this.layerTreeModel.paintProfilerModel.makeSnapshot(this.id()).then(snapshot => { if (!snapshot) { return null; } return {rect: {x: 0, y: 0, width: this.width(), height: this.height()}, snapshot: snapshot}; }); return [promise]; } didPaint(rect: Protocol.DOM.Rect): void { this.lastPaintRectInternal = rect; this.paintCountInternal = this.paintCount() + 1; this.image = null; } reset(layerPayload: Protocol.LayerTree.Layer): void { this.nodeInternal = null; this.childrenInternal = []; this.parentInternal = null; this.paintCountInternal = 0; this.layerPayload = layerPayload; this.image = null; this.scrollRectsInternal = this.layerPayload.scrollRects || []; this.stickyPositionConstraintInternal = this.layerPayload.stickyPositionConstraint ? new SDK.LayerTreeBase.StickyPositionConstraint( this.layerTreeModel.layerTree(), this.layerPayload.stickyPositionConstraint) : null; } private matrixFromArray(a: number[]): DOMMatrix { function toFixed9(x: number): string { return x.toFixed(9); } return new WebKitCSSMatrix('matrix3d(' + a.map(toFixed9).join(',') + ')'); } private calculateTransformToViewport(parentTransform: DOMMatrix): DOMMatrix { const offsetMatrix = new WebKitCSSMatrix().translate(this.layerPayload.offsetX, this.layerPayload.offsetY); let matrix: DOMMatrix = offsetMatrix; if (this.layerPayload.transform) { const transformMatrix = this.matrixFromArray(this.layerPayload.transform); const anchorVector = new UI.Geometry.Vector( this.layerPayload.width * this.anchorPoint()[0], this.layerPayload.height * this.anchorPoint()[1], this.anchorPoint()[2]); const anchorPoint = UI.Geometry.multiplyVectorByMatrixAndNormalize(anchorVector, matrix); const anchorMatrix = new WebKitCSSMatrix().translate(-anchorPoint.x, -anchorPoint.y, -anchorPoint.z); matrix = anchorMatrix.inverse().multiply(transformMatrix.multiply(anchorMatrix.multiply(matrix))); } matrix = parentTransform.multiply(matrix); return matrix; } private createVertexArrayForRect(width: number, height: number): number[] { return [0, 0, 0, width, 0, 0, width, height, 0, 0, height, 0]; } calculateQuad(parentTransform: DOMMatrix): void { const matrix = this.calculateTransformToViewport(parentTransform); this.quadInternal = []; const vertices = this.createVertexArrayForRect(this.layerPayload.width, this.layerPayload.height); for (let i = 0; i < 4; ++i) { const point = UI.Geometry.multiplyVectorByMatrixAndNormalize( new UI.Geometry.Vector(vertices[i * 3], vertices[i * 3 + 1], vertices[i * 3 + 2]), matrix); this.quadInternal.push(point.x, point.y); } function calculateQuadForLayer(layer: AgentLayer): void { layer.calculateQuad(matrix); } this.childrenInternal.forEach(calculateQuadForLayer); } } class LayerTreeDispatcher implements ProtocolProxyApi.LayerTreeDispatcher { private readonly layerTreeModel: LayerTreeModel; constructor(layerTreeModel: LayerTreeModel) { this.layerTreeModel = layerTreeModel; } layerTreeDidChange({layers}: Protocol.LayerTree.LayerTreeDidChangeEvent): void { void this.layerTreeModel.layerTreeChanged(layers || null); } layerPainted({layerId, clip}: Protocol.LayerTree.LayerPaintedEvent): void { this.layerTreeModel.layerPainted(layerId, clip); } }