| /* | |
| * 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. | |
| */ | |
| /** | |
| * @unrestricted | |
| */ | |
| export class TimelineFrameModel { | |
| /** | |
| * @param {function(!SDK.TracingModel.Event):string} categoryMapper | |
| */ | |
| constructor(categoryMapper) { | |
| this._categoryMapper = categoryMapper; | |
| this.reset(); | |
| } | |
| /** | |
| * @param {number=} startTime | |
| * @param {number=} endTime | |
| * @return {!Array<!TimelineFrame>} | |
| */ | |
| frames(startTime, endTime) { | |
| if (!startTime && !endTime) { | |
| return this._frames; | |
| } | |
| const firstFrame = this._frames.lowerBound(startTime || 0, (time, frame) => time - frame.endTime); | |
| const lastFrame = this._frames.lowerBound(endTime || Infinity, (time, frame) => time - frame.startTime); | |
| return this._frames.slice(firstFrame, lastFrame); | |
| } | |
| /** | |
| * @param {!SDK.TracingModel.Event} rasterTask | |
| * @return {boolean} | |
| */ | |
| hasRasterTile(rasterTask) { | |
| const data = rasterTask.args['tileData']; | |
| if (!data) { | |
| return false; | |
| } | |
| const frameId = data['sourceFrameNumber']; | |
| const frame = frameId && this._frameById[frameId]; | |
| if (!frame || !frame.layerTree) { | |
| return false; | |
| } | |
| return true; | |
| } | |
| /** | |
| * @param {!SDK.TracingModel.Event} rasterTask | |
| * @return Promise<?{rect: !Protocol.DOM.Rect, snapshot: !SDK.PaintProfilerSnapshot}>} | |
| */ | |
| rasterTilePromise(rasterTask) { | |
| if (!this._target) { | |
| return Promise.resolve(null); | |
| } | |
| const data = rasterTask.args['tileData']; | |
| const frameId = data['sourceFrameNumber']; | |
| const tileId = data['tileId'] && data['tileId']['id_ref']; | |
| const frame = frameId && this._frameById[frameId]; | |
| if (!frame || !frame.layerTree || !tileId) { | |
| return Promise.resolve(null); | |
| } | |
| return frame.layerTree.layerTreePromise().then(layerTree => layerTree && layerTree.pictureForRasterTile(tileId)); | |
| } | |
| reset() { | |
| this._minimumRecordTime = Infinity; | |
| this._frames = []; | |
| this._frameById = {}; | |
| this._lastFrame = null; | |
| this._lastLayerTree = null; | |
| this._mainFrameCommitted = false; | |
| this._mainFrameRequested = false; | |
| this._framePendingCommit = null; | |
| this._lastBeginFrame = null; | |
| this._lastNeedsBeginFrame = null; | |
| this._framePendingActivation = null; | |
| this._lastTaskBeginTime = null; | |
| this._target = null; | |
| this._layerTreeId = null; | |
| this._currentTaskTimeByCategory = {}; | |
| } | |
| /** | |
| * @param {number} startTime | |
| */ | |
| handleBeginFrame(startTime) { | |
| if (!this._lastFrame) { | |
| this._startFrame(startTime); | |
| } | |
| this._lastBeginFrame = startTime; | |
| } | |
| /** | |
| * @param {number} startTime | |
| */ | |
| handleDrawFrame(startTime) { | |
| if (!this._lastFrame) { | |
| this._startFrame(startTime); | |
| return; | |
| } | |
| // - if it wasn't drawn, it didn't happen! | |
| // - only show frames that either did not wait for the main thread frame or had one committed. | |
| if (this._mainFrameCommitted || !this._mainFrameRequested) { | |
| if (this._lastNeedsBeginFrame) { | |
| const idleTimeEnd = this._framePendingActivation ? this._framePendingActivation.triggerTime : | |
| (this._lastBeginFrame || this._lastNeedsBeginFrame); | |
| if (idleTimeEnd > this._lastFrame.startTime) { | |
| this._lastFrame.idle = true; | |
| this._startFrame(idleTimeEnd); | |
| if (this._framePendingActivation) { | |
| this._commitPendingFrame(); | |
| } | |
| this._lastBeginFrame = null; | |
| } | |
| this._lastNeedsBeginFrame = null; | |
| } | |
| this._startFrame(startTime); | |
| } | |
| this._mainFrameCommitted = false; | |
| } | |
| handleActivateLayerTree() { | |
| if (!this._lastFrame) { | |
| return; | |
| } | |
| if (this._framePendingActivation && !this._lastNeedsBeginFrame) { | |
| this._commitPendingFrame(); | |
| } | |
| } | |
| handleRequestMainThreadFrame() { | |
| if (!this._lastFrame) { | |
| return; | |
| } | |
| this._mainFrameRequested = true; | |
| } | |
| handleCompositeLayers() { | |
| if (!this._framePendingCommit) { | |
| return; | |
| } | |
| this._framePendingActivation = this._framePendingCommit; | |
| this._framePendingCommit = null; | |
| this._mainFrameRequested = false; | |
| this._mainFrameCommitted = true; | |
| } | |
| /** | |
| * @param {!TracingFrameLayerTree} layerTree | |
| */ | |
| handleLayerTreeSnapshot(layerTree) { | |
| this._lastLayerTree = layerTree; | |
| } | |
| /** | |
| * @param {number} startTime | |
| * @param {boolean} needsBeginFrame | |
| */ | |
| handleNeedFrameChanged(startTime, needsBeginFrame) { | |
| if (needsBeginFrame) { | |
| this._lastNeedsBeginFrame = startTime; | |
| } | |
| } | |
| /** | |
| * @param {number} startTime | |
| */ | |
| _startFrame(startTime) { | |
| if (this._lastFrame) { | |
| this._flushFrame(this._lastFrame, startTime); | |
| } | |
| this._lastFrame = new TimelineFrame(startTime, startTime - this._minimumRecordTime); | |
| } | |
| /** | |
| * @param {!TimelineFrame} frame | |
| * @param {number} endTime | |
| */ | |
| _flushFrame(frame, endTime) { | |
| frame._setLayerTree(this._lastLayerTree); | |
| frame._setEndTime(endTime); | |
| if (this._lastLayerTree) { | |
| this._lastLayerTree._setPaints(frame._paints); | |
| } | |
| if (this._frames.length && | |
| (frame.startTime !== this._frames.peekLast().endTime || frame.startTime > frame.endTime)) { | |
| console.assert( | |
| false, `Inconsistent frame time for frame ${this._frames.length} (${frame.startTime} - ${frame.endTime})`); | |
| } | |
| this._frames.push(frame); | |
| if (typeof frame._mainFrameId === 'number') { | |
| this._frameById[frame._mainFrameId] = frame; | |
| } | |
| } | |
| _commitPendingFrame() { | |
| this._lastFrame._addTimeForCategories(this._framePendingActivation.timeByCategory); | |
| this._lastFrame._paints = this._framePendingActivation.paints; | |
| this._lastFrame._mainFrameId = this._framePendingActivation.mainFrameId; | |
| this._framePendingActivation = null; | |
| } | |
| /** | |
| * @param {?SDK.Target} target | |
| * @param {!Array.<!SDK.TracingModel.Event>} events | |
| * @param {!Array<!{thread: !SDK.TracingModel.Thread, time: number}>} threadData | |
| */ | |
| addTraceEvents(target, events, threadData) { | |
| this._target = target; | |
| let j = 0; | |
| this._currentProcessMainThread = threadData.length && threadData[0].thread || null; | |
| for (let i = 0; i < events.length; ++i) { | |
| while (j + 1 < threadData.length && threadData[j + 1].time <= events[i].startTime) { | |
| this._currentProcessMainThread = threadData[++j].thread; | |
| } | |
| this._addTraceEvent(events[i]); | |
| } | |
| this._currentProcessMainThread = null; | |
| } | |
| /** | |
| * @param {!SDK.TracingModel.Event} event | |
| */ | |
| _addTraceEvent(event) { | |
| const eventNames = TimelineModel.TimelineModel.RecordType; | |
| if (event.startTime && event.startTime < this._minimumRecordTime) { | |
| this._minimumRecordTime = event.startTime; | |
| } | |
| if (event.name === eventNames.SetLayerTreeId) { | |
| this._layerTreeId = event.args['layerTreeId'] || event.args['data']['layerTreeId']; | |
| } else if ( | |
| event.phase === SDK.TracingModel.Phase.SnapshotObject && event.name === eventNames.LayerTreeHostImplSnapshot && | |
| parseInt(event.id, 0) === this._layerTreeId) { | |
| const snapshot = /** @type {!SDK.TracingModel.ObjectSnapshot} */ (event); | |
| this.handleLayerTreeSnapshot(new TracingFrameLayerTree(this._target, snapshot)); | |
| } else { | |
| this._processCompositorEvents(event); | |
| if (event.thread === this._currentProcessMainThread) { | |
| this._addMainThreadTraceEvent(event); | |
| } else if (this._lastFrame && event.selfTime && !SDK.TracingModel.isTopLevelEvent(event)) { | |
| this._lastFrame._addTimeForCategory(this._categoryMapper(event), event.selfTime); | |
| } | |
| } | |
| } | |
| /** | |
| * @param {!SDK.TracingModel.Event} event | |
| */ | |
| _processCompositorEvents(event) { | |
| const eventNames = TimelineModel.TimelineModel.RecordType; | |
| if (event.args['layerTreeId'] !== this._layerTreeId) { | |
| return; | |
| } | |
| const timestamp = event.startTime; | |
| if (event.name === eventNames.BeginFrame) { | |
| this.handleBeginFrame(timestamp); | |
| } else if (event.name === eventNames.DrawFrame) { | |
| this.handleDrawFrame(timestamp); | |
| } else if (event.name === eventNames.ActivateLayerTree) { | |
| this.handleActivateLayerTree(); | |
| } else if (event.name === eventNames.RequestMainThreadFrame) { | |
| this.handleRequestMainThreadFrame(); | |
| } else if (event.name === eventNames.NeedsBeginFrameChanged) { | |
| this.handleNeedFrameChanged(timestamp, event.args['data'] && event.args['data']['needsBeginFrame']); | |
| } | |
| } | |
| /** | |
| * @param {!SDK.TracingModel.Event} event | |
| */ | |
| _addMainThreadTraceEvent(event) { | |
| const eventNames = TimelineModel.TimelineModel.RecordType; | |
| if (SDK.TracingModel.isTopLevelEvent(event)) { | |
| this._currentTaskTimeByCategory = {}; | |
| this._lastTaskBeginTime = event.startTime; | |
| } | |
| if (!this._framePendingCommit && TimelineFrameModel._mainFrameMarkers.indexOf(event.name) >= 0) { | |
| this._framePendingCommit = | |
| new TimelineModel.PendingFrame(this._lastTaskBeginTime || event.startTime, this._currentTaskTimeByCategory); | |
| } | |
| if (!this._framePendingCommit) { | |
| this._addTimeForCategory(this._currentTaskTimeByCategory, event); | |
| return; | |
| } | |
| this._addTimeForCategory(this._framePendingCommit.timeByCategory, event); | |
| if (event.name === eventNames.BeginMainThreadFrame && event.args['data'] && event.args['data']['frameId']) { | |
| this._framePendingCommit.mainFrameId = event.args['data']['frameId']; | |
| } | |
| if (event.name === eventNames.Paint && event.args['data']['layerId'] && | |
| TimelineModel.TimelineData.forEvent(event).picture && this._target) { | |
| this._framePendingCommit.paints.push(new LayerPaintEvent(event, this._target)); | |
| } | |
| if (event.name === eventNames.CompositeLayers && event.args['layerTreeId'] === this._layerTreeId) { | |
| this.handleCompositeLayers(); | |
| } | |
| } | |
| /** | |
| * @param {!Object.<string, number>} timeByCategory | |
| * @param {!SDK.TracingModel.Event} event | |
| */ | |
| _addTimeForCategory(timeByCategory, event) { | |
| if (!event.selfTime) { | |
| return; | |
| } | |
| const categoryName = this._categoryMapper(event); | |
| timeByCategory[categoryName] = (timeByCategory[categoryName] || 0) + event.selfTime; | |
| } | |
| } | |
| TimelineFrameModel._mainFrameMarkers = [ | |
| TimelineModel.TimelineModel.RecordType.ScheduleStyleRecalculation, | |
| TimelineModel.TimelineModel.RecordType.InvalidateLayout, TimelineModel.TimelineModel.RecordType.BeginMainThreadFrame, | |
| TimelineModel.TimelineModel.RecordType.ScrollLayer | |
| ]; | |
| /** | |
| * @unrestricted | |
| */ | |
| export class TracingFrameLayerTree { | |
| /** | |
| * @param {!SDK.Target} target | |
| * @param {!SDK.TracingModel.ObjectSnapshot} snapshot | |
| */ | |
| constructor(target, snapshot) { | |
| this._target = target; | |
| this._snapshot = snapshot; | |
| /** @type {!Array<!LayerPaintEvent>|undefined} */ | |
| this._paints; | |
| } | |
| /** | |
| * @return {!Promise<?TimelineModel.TracingLayerTree>} | |
| */ | |
| async layerTreePromise() { | |
| const result = await this._snapshot.objectPromise(); | |
| if (!result) { | |
| return null; | |
| } | |
| const viewport = result['device_viewport_size']; | |
| const tiles = result['active_tiles']; | |
| const rootLayer = result['active_tree']['root_layer']; | |
| const layers = result['active_tree']['layers']; | |
| const layerTree = new TimelineModel.TracingLayerTree(this._target); | |
| layerTree.setViewportSize(viewport); | |
| layerTree.setTiles(tiles); | |
| await layerTree.setLayers(rootLayer, layers, this._paints || []); | |
| return layerTree; | |
| } | |
| /** | |
| * @return {!Array<!LayerPaintEvent>} | |
| */ | |
| paints() { | |
| return this._paints || []; | |
| } | |
| /** | |
| * @param {!Array<!LayerPaintEvent>} paints | |
| */ | |
| _setPaints(paints) { | |
| this._paints = paints; | |
| } | |
| } | |
| /** | |
| * @unrestricted | |
| */ | |
| export class TimelineFrame { | |
| /** | |
| * @param {number} startTime | |
| * @param {number} startTimeOffset | |
| */ | |
| constructor(startTime, startTimeOffset) { | |
| this.startTime = startTime; | |
| this.startTimeOffset = startTimeOffset; | |
| this.endTime = this.startTime; | |
| this.duration = 0; | |
| this.timeByCategory = {}; | |
| this.cpuTime = 0; | |
| this.idle = false; | |
| /** @type {?TracingFrameLayerTree} */ | |
| this.layerTree = null; | |
| /** @type {!Array.<!LayerPaintEvent>} */ | |
| this._paints = []; | |
| /** @type {number|undefined} */ | |
| this._mainFrameId = undefined; | |
| } | |
| /** | |
| * @return {boolean} | |
| */ | |
| hasWarnings() { | |
| return false; | |
| } | |
| /** | |
| * @param {number} endTime | |
| */ | |
| _setEndTime(endTime) { | |
| this.endTime = endTime; | |
| this.duration = this.endTime - this.startTime; | |
| } | |
| /** | |
| * @param {?TracingFrameLayerTree} layerTree | |
| */ | |
| _setLayerTree(layerTree) { | |
| this.layerTree = layerTree; | |
| } | |
| /** | |
| * @param {!Object} timeByCategory | |
| */ | |
| _addTimeForCategories(timeByCategory) { | |
| for (const category in timeByCategory) { | |
| this._addTimeForCategory(category, timeByCategory[category]); | |
| } | |
| } | |
| /** | |
| * @param {string} category | |
| * @param {number} time | |
| */ | |
| _addTimeForCategory(category, time) { | |
| this.timeByCategory[category] = (this.timeByCategory[category] || 0) + time; | |
| this.cpuTime += time; | |
| } | |
| } | |
| /** | |
| * @unrestricted | |
| */ | |
| export class LayerPaintEvent { | |
| /** | |
| * @param {!SDK.TracingModel.Event} event | |
| * @param {?SDK.Target} target | |
| */ | |
| constructor(event, target) { | |
| this._event = event; | |
| this._target = target; | |
| } | |
| /** | |
| * @return {string} | |
| */ | |
| layerId() { | |
| return this._event.args['data']['layerId']; | |
| } | |
| /** | |
| * @return {!SDK.TracingModel.Event} | |
| */ | |
| event() { | |
| return this._event; | |
| } | |
| /** | |
| * @return {!Promise<?{rect: !Array<number>, serializedPicture: string}>} | |
| */ | |
| picturePromise() { | |
| const picture = TimelineModel.TimelineData.forEvent(this._event).picture; | |
| return picture.objectPromise().then(result => { | |
| if (!result) { | |
| return null; | |
| } | |
| const rect = result['params'] && result['params']['layer_rect']; | |
| const picture = result['skp64']; | |
| return rect && picture ? {rect: rect, serializedPicture: picture} : null; | |
| }); | |
| } | |
| /** | |
| * @return !Promise<?{rect: !Array<number>, snapshot: !SDK.PaintProfilerSnapshot}>} | |
| */ | |
| snapshotPromise() { | |
| const paintProfilerModel = this._target && this._target.model(SDK.PaintProfilerModel); | |
| return this.picturePromise().then(picture => { | |
| if (!picture || !paintProfilerModel) { | |
| return null; | |
| } | |
| return paintProfilerModel.loadSnapshot(picture.serializedPicture) | |
| .then(snapshot => snapshot ? {rect: picture.rect, snapshot: snapshot} : null); | |
| }); | |
| } | |
| } | |
| /** | |
| * @unrestricted | |
| */ | |
| export class PendingFrame { | |
| /** | |
| * @param {number} triggerTime | |
| * @param {!Object.<string, number>} timeByCategory | |
| */ | |
| constructor(triggerTime, timeByCategory) { | |
| /** @type {!Object.<string, number>} */ | |
| this.timeByCategory = timeByCategory; | |
| /** @type {!Array.<!LayerPaintEvent>} */ | |
| this.paints = []; | |
| /** @type {number|undefined} */ | |
| this.mainFrameId = undefined; | |
| this.triggerTime = triggerTime; | |
| } | |
| } | |
| /* Legacy exported object */ | |
| self.TimelineModel = self.TimelineModel || {}; | |
| /* Legacy exported object */ | |
| TimelineModel = TimelineModel || {}; | |
| /** @constructor */ | |
| TimelineModel.TimelineFrameModel = TimelineFrameModel; | |
| /** @constructor */ | |
| TimelineModel.TracingFrameLayerTree = TracingFrameLayerTree; | |
| /** @constructor */ | |
| TimelineModel.TimelineFrame = TimelineFrame; | |
| /** @constructor */ | |
| TimelineModel.LayerPaintEvent = LayerPaintEvent; | |
| /** @constructor */ | |
| TimelineModel.PendingFrame = PendingFrame; |