UNPKG

chrome-devtools-frontend

Version:
262 lines (224 loc) • 9.51 kB
// Copyright 2017 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Common from '../../core/common/common.js'; import * as SDK from '../../core/sdk/sdk.js'; import type * as Bindings from '../../models/bindings/bindings.js'; import * as TimelineModel from '../../models/timeline_model/timeline_model.js'; import {TimelineUIUtils} from './TimelineUIUtils.js'; export class PerformanceModel extends Common.ObjectWrapper.ObjectWrapper<EventTypes> { private mainTargetInternal: SDK.Target.Target|null; private tracingModelInternal: SDK.TracingModel.TracingModel|null; private filtersInternal: TimelineModel.TimelineModelFilter.TimelineModelFilter[]; private readonly timelineModelInternal: TimelineModel.TimelineModel.TimelineModelImpl; private readonly frameModelInternal: TimelineModel.TimelineFrameModel.TimelineFrameModel; private filmStripModelInternal: SDK.FilmStripModel.FilmStripModel|null; private readonly irModel: TimelineModel.TimelineIRModel.TimelineIRModel; private windowInternal: Window; private readonly extensionTracingModels: { title: string, model: SDK.TracingModel.TracingModel, timeOffset: number, }[]; private recordStartTimeInternal?: number; constructor() { super(); this.mainTargetInternal = null; this.tracingModelInternal = null; this.filtersInternal = []; this.timelineModelInternal = new TimelineModel.TimelineModel.TimelineModelImpl(); this.frameModelInternal = new TimelineModel.TimelineFrameModel.TimelineFrameModel( event => TimelineUIUtils.eventStyle(event).category.name); this.filmStripModelInternal = null; this.irModel = new TimelineModel.TimelineIRModel.TimelineIRModel(); this.windowInternal = {left: 0, right: Infinity}; this.extensionTracingModels = []; this.recordStartTimeInternal = undefined; } setMainTarget(target: SDK.Target.Target): void { this.mainTargetInternal = target; } mainTarget(): SDK.Target.Target|null { return this.mainTargetInternal; } setRecordStartTime(time: number): void { this.recordStartTimeInternal = time; } recordStartTime(): number|undefined { return this.recordStartTimeInternal; } setFilters(filters: TimelineModel.TimelineModelFilter.TimelineModelFilter[]): void { this.filtersInternal = filters; } filters(): TimelineModel.TimelineModelFilter.TimelineModelFilter[] { return this.filtersInternal; } isVisible(event: SDK.TracingModel.Event): boolean { return this.filtersInternal.every(f => f.accept(event)); } setTracingModel(model: SDK.TracingModel.TracingModel): void { this.tracingModelInternal = model; this.timelineModelInternal.setEvents(model); let animationEvents: SDK.TracingModel.AsyncEvent[]|null = null; for (const track of this.timelineModelInternal.tracks()) { if (track.type === TimelineModel.TimelineModel.TrackType.Animation) { animationEvents = track.asyncEvents; } } if (animationEvents) { this.irModel.populate([], animationEvents || []); } const mainTracks = this.timelineModelInternal.tracks().filter( track => track.type === TimelineModel.TimelineModel.TrackType.MainThread && track.forMainFrame && track.events.length); const threadData = mainTracks.map(track => { const event = track.events[0]; return {thread: event.thread, time: event.startTime}; }); this.frameModelInternal.addTraceEvents( this.mainTargetInternal, this.timelineModelInternal.inspectedTargetEvents(), threadData); for (const entry of this.extensionTracingModels) { entry.model.adjustTime( this.tracingModelInternal.minimumRecordTime() + (entry.timeOffset / 1000) - (this.recordStartTimeInternal as number)); } this.autoWindowTimes(); } addExtensionEvents(title: string, model: SDK.TracingModel.TracingModel, timeOffset: number): void { this.extensionTracingModels.push({model: model, title: title, timeOffset: timeOffset}); if (!this.tracingModelInternal) { return; } model.adjustTime( this.tracingModelInternal.minimumRecordTime() + (timeOffset / 1000) - (this.recordStartTimeInternal as number)); this.dispatchEventToListeners(Events.ExtensionDataAdded); } tracingModel(): SDK.TracingModel.TracingModel { if (!this.tracingModelInternal) { throw 'call setTracingModel before accessing PerformanceModel'; } return this.tracingModelInternal; } timelineModel(): TimelineModel.TimelineModel.TimelineModelImpl { return this.timelineModelInternal; } filmStripModel(): SDK.FilmStripModel.FilmStripModel { if (this.filmStripModelInternal) { return this.filmStripModelInternal; } if (!this.tracingModelInternal) { throw 'call setTracingModel before accessing PerformanceModel'; } this.filmStripModelInternal = new SDK.FilmStripModel.FilmStripModel(this.tracingModelInternal); return this.filmStripModelInternal; } frames(): TimelineModel.TimelineFrameModel.TimelineFrame[] { return this.frameModelInternal.getFrames(); } frameModel(): TimelineModel.TimelineFrameModel.TimelineFrameModel { return this.frameModelInternal; } interactionRecords(): Common.SegmentedRange.Segment<TimelineModel.TimelineIRModel.Phases>[] { return this.irModel.interactionRecords(); } extensionInfo(): { title: string, model: SDK.TracingModel.TracingModel, }[] { return this.extensionTracingModels; } dispose(): void { if (this.tracingModelInternal) { this.tracingModelInternal.dispose(); } for (const extensionEntry of this.extensionTracingModels) { extensionEntry.model.dispose(); } } filmStripModelFrame(frame: TimelineModel.TimelineFrameModel.TimelineFrame): SDK.FilmStripModel.Frame|null { // For idle frames, look at the state at the beginning of the frame. const screenshotTime = frame.idle ? frame.startTime : frame.endTime; const filmStripModel = (this.filmStripModelInternal as SDK.FilmStripModel.FilmStripModel); const filmStripFrame = filmStripModel.frameByTimestamp(screenshotTime); return filmStripFrame && filmStripFrame.timestamp - frame.endTime < 10 ? filmStripFrame : null; } save(stream: Common.StringOutputStream.OutputStream): Promise<DOMError|null> { if (!this.tracingModelInternal) { throw 'call setTracingModel before accessing PerformanceModel'; } const backingStorage = (this.tracingModelInternal.backingStorage() as Bindings.TempFile.TempFileBackingStorage); return backingStorage.writeToStream(stream); } setWindow(window: Window, animate?: boolean): void { this.windowInternal = window; this.dispatchEventToListeners(Events.WindowChanged, {window, animate}); } window(): Window { return this.windowInternal; } private autoWindowTimes(): void { const timelineModel = this.timelineModelInternal; let tasks: SDK.TracingModel.Event[] = []; for (const track of timelineModel.tracks()) { // Deliberately pick up last main frame's track. if (track.type === TimelineModel.TimelineModel.TrackType.MainThread && track.forMainFrame) { tasks = track.tasks; } } if (!tasks.length) { this.setWindow({left: timelineModel.minimumRecordTime(), right: timelineModel.maximumRecordTime()}); return; } function findLowUtilizationRegion(startIndex: number, stopIndex: number): number { const threshold = 0.1; let cutIndex = startIndex; let cutTime = (tasks[cutIndex].startTime + (tasks[cutIndex].endTime as number)) / 2; let usedTime = 0; const step = Math.sign(stopIndex - startIndex); for (let i = startIndex; i !== stopIndex; i += step) { const task = tasks[i]; const taskTime = (task.startTime + (task.endTime as number)) / 2; const interval = Math.abs(cutTime - taskTime); if (usedTime < threshold * interval) { cutIndex = i; cutTime = taskTime; usedTime = 0; } usedTime += (task.duration as number); } return cutIndex; } const rightIndex = findLowUtilizationRegion(tasks.length - 1, 0); const leftIndex = findLowUtilizationRegion(0, rightIndex); let leftTime: number = tasks[leftIndex].startTime; let rightTime: number = (tasks[rightIndex].endTime as number); const span = rightTime - leftTime; const totalSpan = timelineModel.maximumRecordTime() - timelineModel.minimumRecordTime(); if (span < totalSpan * 0.1) { leftTime = timelineModel.minimumRecordTime(); rightTime = timelineModel.maximumRecordTime(); } else { leftTime = Math.max(leftTime - 0.05 * span, timelineModel.minimumRecordTime()); rightTime = Math.min(rightTime + 0.05 * span, timelineModel.maximumRecordTime()); } this.setWindow({left: leftTime, right: rightTime}); } } // TODO(crbug.com/1167717): Make this a const enum again // eslint-disable-next-line rulesdir/const_enum export enum Events { ExtensionDataAdded = 'ExtensionDataAdded', WindowChanged = 'WindowChanged', } export interface WindowChangedEvent { window: Window; animate: boolean|undefined; } export type EventTypes = { [Events.ExtensionDataAdded]: void, [Events.WindowChanged]: WindowChangedEvent, }; export interface Window { left: number; right: number; }