UNPKG

chrome-devtools-frontend

Version:
272 lines (232 loc) • 10.1 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 * as SourceMapScopes from '../../models/source_map_scopes/source_map_scopes.js'; import * as TimelineModel from '../../models/timeline_model/timeline_model.js'; import {TimelineUIUtils} from './TimelineUIUtils.js'; const resolveNamesTimeout = 500; 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 windowInternal: Window; private willResolveNames = false; 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.windowInternal = {left: 0, right: Infinity}; 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)); } async setTracingModel(model: SDK.TracingModel.TracingModel, isFreshRecording = false): Promise<void> { this.tracingModelInternal = model; this.timelineModelInternal.setEvents(model, isFreshRecording); await this.addSourceMapListeners(); 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); this.autoWindowTimes(); } #cpuProfileNodes(): SDK.CPUProfileDataModel.CPUProfileNode[] { return this.timelineModel().cpuProfiles().flatMap(p => p.nodes() || []); } async addSourceMapListeners(): Promise<void> { const debuggerModelsToListen = new Set<SDK.DebuggerModel.DebuggerModel>(); for (const node of this.#cpuProfileNodes()) { if (!node) { continue; } const debuggerModelToListen = this.#maybeGetDebuggerModelForNode(node); if (!debuggerModelToListen) { continue; } debuggerModelsToListen.add(debuggerModelToListen); } for (const debuggerModel of debuggerModelsToListen) { debuggerModel.sourceMapManager().addEventListener( SDK.SourceMapManager.Events.SourceMapAttached, this.#onAttachedSourceMap, this); } await this.#resolveNamesFromCPUProfile(); } // If a node corresponds to a script that has not been parsed or a script // that has a source map, we should listen to SourceMapAttached events to // attempt a function name resolving. #maybeGetDebuggerModelForNode(node: SDK.CPUProfileDataModel.CPUProfileNode): SDK.DebuggerModel.DebuggerModel|null { const target = node.target(); const debuggerModel = target?.model(SDK.DebuggerModel.DebuggerModel); if (!debuggerModel) { return null; } const script = debuggerModel.scriptForId(String(node.callFrame.scriptId)); const shouldListenToSourceMap = !script || script.sourceMapURL; if (shouldListenToSourceMap) { return debuggerModel; } return null; } async #resolveNamesFromCPUProfile(): Promise<void> { for (const node of this.#cpuProfileNodes()) { const resolvedFunctionName = await SourceMapScopes.NamesResolver.resolveProfileFrameFunctionName(node.callFrame, node.target()); node.setFunctionName(resolvedFunctionName); } } async #onAttachedSourceMap(): Promise<void> { if (!this.willResolveNames) { this.willResolveNames = true; // Resolving names triggers a repaint of the flame chart. Instead of attempting to resolve // names every time a source map is attached, wait for some time once the first source map is // attached. This way we allow for other source maps to be parsed before attempting a name // resolving using the available source maps. Otherwise the UI is blocked when the number // of source maps is particularly large. setTimeout(this.resolveNamesAndUpdate.bind(this), resolveNamesTimeout); } } async resolveNamesAndUpdate(): Promise<void> { this.willResolveNames = false; await this.#resolveNamesFromCPUProfile(); this.dispatchEventToListeners(Events.NamesResolved); } 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; } 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; } 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 { WindowChanged = 'WindowChanged', NamesResolved = 'NamesResolved', } export interface WindowChangedEvent { window: Window; animate: boolean|undefined; } export type EventTypes = { [Events.WindowChanged]: WindowChangedEvent, [Events.NamesResolved]: void, }; export interface Window { left: number; right: number; }