mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-10 05:35:05 +08:00
R=szuend@chromium.org Bug: 1050549 Change-Id: Ic068b0178efcd5bafd46e8e054a77315ff9083f5 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2644904 Commit-Queue: Tim van der Lippe <tvanderlippe@chromium.org> Reviewed-by: Simon Zünd <szuend@chromium.org>
588 lines
18 KiB
JavaScript
588 lines
18 KiB
JavaScript
/*
|
|
* 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 Platform from '../platform/platform.js';
|
|
import * as SDK from '../sdk/sdk.js';
|
|
|
|
import {RecordType, TimelineData} from './TimelineModel.js';
|
|
import {TracingLayerTile, TracingLayerTree} from './TracingLayerTree.js'; // eslint-disable-line no-unused-vars
|
|
|
|
export class TimelineFrameModel {
|
|
/**
|
|
* @param {function(!SDK.TracingModel.Event):string} categoryMapper
|
|
*/
|
|
constructor(categoryMapper) {
|
|
this._categoryMapper = categoryMapper;
|
|
/** @type {!Array<!TimelineFrame>} */
|
|
this._frames;
|
|
/** @type {!Object<number, !TimelineFrame>} */
|
|
this._frameById;
|
|
/** @type {number} */
|
|
this._minimumRecordTime;
|
|
/** @type {?TimelineFrame} */
|
|
this._lastFrame;
|
|
/** @type {boolean} */
|
|
this._mainFrameCommitted;
|
|
/** @type {boolean} */
|
|
this._mainFrameRequested;
|
|
/** @type {number} */
|
|
this._minimumRecordTime;
|
|
/** @type {?TracingFrameLayerTree} */
|
|
this._lastLayerTree;
|
|
/** @type {?PendingFrame} */
|
|
this._framePendingActivation;
|
|
/** @type {!Object<string, number>} */
|
|
this._currentTaskTimeByCategory;
|
|
/** @type {?SDK.SDKModel.Target} */
|
|
this._target;
|
|
|
|
this.reset();
|
|
}
|
|
|
|
/**
|
|
* @param {number=} startTime
|
|
* @param {number=} endTime
|
|
* @return {!Array<!TimelineFrame>}
|
|
*/
|
|
frames(startTime, endTime) {
|
|
if (!startTime && !endTime) {
|
|
return this._frames;
|
|
}
|
|
const firstFrame =
|
|
Platform.ArrayUtilities.lowerBound(this._frames, startTime || 0, (time, frame) => time - frame.endTime);
|
|
const lastFrame =
|
|
Platform.ArrayUtilities.lowerBound(this._frames, 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.PaintProfiler.PaintProfilerSnapshot}>}
|
|
*/
|
|
rasterTilePromise(rasterTask) {
|
|
if (!this._target) {
|
|
return Promise.resolve(null);
|
|
}
|
|
const data = rasterTask.args['tileData'];
|
|
const frameId = /** @type {number} */ (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._lastDroppedFrame = 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
|
|
*/
|
|
handleDroppedFrame(startTime) {
|
|
if (!this._lastFrame) {
|
|
this._startFrame(startTime);
|
|
}
|
|
this._lastDroppedFrame = 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;
|
|
}
|
|
if (this._lastDroppedFrame) {
|
|
this._lastFrame.dropped = true;
|
|
this._startFrame(this._lastDroppedFrame);
|
|
this._lastDroppedFrame = 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);
|
|
}
|
|
const lastFrame = this._frames[this._frames.length - 1];
|
|
if (this._frames.length && lastFrame &&
|
|
(frame.startTime !== lastFrame.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() {
|
|
if (!this._framePendingActivation || !this._lastFrame) {
|
|
return;
|
|
}
|
|
|
|
this._lastFrame._addTimeForCategories(this._framePendingActivation.timeByCategory);
|
|
this._lastFrame._paints = this._framePendingActivation.paints;
|
|
this._lastFrame._mainFrameId = this._framePendingActivation.mainFrameId;
|
|
this._framePendingActivation = null;
|
|
}
|
|
|
|
/**
|
|
* @param {?SDK.SDKModel.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 = 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.id && event.phase === SDK.TracingModel.Phase.SnapshotObject &&
|
|
event.name === eventNames.LayerTreeHostImplSnapshot && Number(event.id) === this._layerTreeId && this._target) {
|
|
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.TracingModel.isTopLevelEvent(event)) {
|
|
this._lastFrame._addTimeForCategory(this._categoryMapper(event), event.selfTime);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!SDK.TracingModel.Event} event
|
|
*/
|
|
_processCompositorEvents(event) {
|
|
const eventNames = 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']);
|
|
} else if (event.name === eventNames.DroppedFrame) {
|
|
this.handleDroppedFrame(timestamp);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!SDK.TracingModel.Event} event
|
|
*/
|
|
_addMainThreadTraceEvent(event) {
|
|
const eventNames = RecordType;
|
|
|
|
if (SDK.TracingModel.TracingModel.isTopLevelEvent(event)) {
|
|
this._currentTaskTimeByCategory = {};
|
|
this._lastTaskBeginTime = event.startTime;
|
|
}
|
|
if (!this._framePendingCommit && TimelineFrameModel._mainFrameMarkers.indexOf(event.name) >= 0) {
|
|
this._framePendingCommit =
|
|
new 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'] && 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 = [
|
|
RecordType.ScheduleStyleRecalculation, RecordType.InvalidateLayout, RecordType.BeginMainThreadFrame,
|
|
RecordType.ScrollLayer
|
|
];
|
|
|
|
export class TracingFrameLayerTree {
|
|
/**
|
|
* @param {!SDK.SDKModel.Target} target
|
|
* @param {!SDK.TracingModel.ObjectSnapshot} snapshot
|
|
*/
|
|
constructor(target, snapshot) {
|
|
this._target = target;
|
|
this._snapshot = snapshot;
|
|
/** @type {!Array<!LayerPaintEvent>|undefined} */
|
|
this._paints;
|
|
}
|
|
|
|
/**
|
|
* @return {!Promise<?TracingLayerTree>}
|
|
*/
|
|
async layerTreePromise() {
|
|
const result = /** @type {!Object<string, *>} */ (await this._snapshot.objectPromise());
|
|
if (!result) {
|
|
return null;
|
|
}
|
|
const viewport = /** @type {!{width: number, height: number}} */ (result['device_viewport_size']);
|
|
const tiles = /** @type {!Array<!TracingLayerTile>} */ (result['active_tiles']);
|
|
const rootLayer = result['active_tree']['root_layer'];
|
|
const layers = result['active_tree']['layers'];
|
|
const layerTree = new 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;
|
|
}
|
|
}
|
|
|
|
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;
|
|
/** @type {!Object<string, number>} */
|
|
this.timeByCategory = {};
|
|
this.cpuTime = 0;
|
|
this.idle = false;
|
|
this.dropped = 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<string, number>} 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;
|
|
}
|
|
}
|
|
|
|
export class LayerPaintEvent {
|
|
/**
|
|
* @param {!SDK.TracingModel.Event} event
|
|
* @param {?SDK.SDKModel.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 = TimelineData.forEvent(this._event).picture;
|
|
if (!picture) {
|
|
return Promise.resolve(null);
|
|
}
|
|
|
|
return picture.objectPromise().then(
|
|
/**
|
|
* @param {*} result
|
|
*/
|
|
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.PaintProfiler.PaintProfilerSnapshot}>}
|
|
*/
|
|
async snapshotPromise() {
|
|
const paintProfilerModel = this._target && this._target.model(SDK.PaintProfiler.PaintProfilerModel);
|
|
const picture = await this.picturePromise();
|
|
if (!picture || !paintProfilerModel) {
|
|
return null;
|
|
}
|
|
const snapshot = await paintProfilerModel.loadSnapshot(picture.serializedPicture);
|
|
return snapshot ? {rect: picture.rect, snapshot: snapshot} : null;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|