UNPKG

chrome-devtools-frontend

Version:
319 lines (274 loc) 13.2 kB
// Copyright 2023 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. /* eslint-disable rulesdir/no-imperative-dom-api */ import * as Common from '../../core/common/common.js'; import * as Trace from '../../models/trace/trace.js'; import * as TraceBounds from '../../services/trace_bounds/trace_bounds.js'; import * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js'; import * as UI from '../../ui/legacy/legacy.js'; import * as TimelineComponents from './components/components.js'; import {ModificationsManager} from './ModificationsManager.js'; import { type TimelineEventOverview, TimelineEventOverviewCPUActivity, TimelineEventOverviewMemory, TimelineEventOverviewNetwork, TimelineEventOverviewResponsiveness, TimelineFilmStripOverview, } from './TimelineEventOverview.js'; import miniMapStyles from './timelineMiniMap.css.js'; import {TimelineUIUtils} from './TimelineUIUtils.js'; export interface OverviewData { parsedTrace: Trace.Handlers.Types.ParsedTrace; isCpuProfile?: boolean; settings: { showScreenshots: boolean, showMemory: boolean, }; } /** * This component wraps the generic PerfUI Overview component and configures it * specifically for the Performance Panel, including injecting the CSS we use * to customise how the components render within the Performance Panel. * * It is also responsible for listening to events from the OverviewPane to * update the visible trace window, and when this happens it will update the * TraceBounds service with the new values. */ export class TimelineMiniMap extends Common.ObjectWrapper.eventMixin<PerfUI.TimelineOverviewPane.EventTypes, typeof UI.Widget.VBox>(UI.Widget.VBox) { #overviewComponent = new PerfUI.TimelineOverviewPane.TimelineOverviewPane('timeline'); #controls: TimelineEventOverview[] = []; breadcrumbs: TimelineComponents.Breadcrumbs.Breadcrumbs|null = null; #breadcrumbsUI: TimelineComponents.BreadcrumbsUI.BreadcrumbsUI; #data: OverviewData|null = null; #onTraceBoundsChangeBound = this.#onTraceBoundsChange.bind(this); constructor() { super(); this.registerRequiredCSS(miniMapStyles); this.element.classList.add('timeline-minimap'); this.#breadcrumbsUI = new TimelineComponents.BreadcrumbsUI.BreadcrumbsUI(); this.element.prepend(this.#breadcrumbsUI); this.#overviewComponent.show(this.element); this.#overviewComponent.addEventListener(PerfUI.TimelineOverviewPane.Events.OVERVIEW_PANE_WINDOW_CHANGED, event => { this.#onOverviewPanelWindowChanged(event); }); this.#overviewComponent.addEventListener( PerfUI.TimelineOverviewPane.Events.OVERVIEW_PANE_BREADCRUMB_ADDED, event => { this.addBreadcrumb(event.data); }); // We want to add/remove an overlay for these two events, and the overlay system is controlled by // `TimelineFlameChartView`, so we need to dispatch them up to the `TimelinePanel` level to call // `TimelineFlameChartView` -> `addOverlay()/removeOverlay()`. this.#overviewComponent.addEventListener(PerfUI.TimelineOverviewPane.Events.OVERVIEW_PANE_MOUSE_MOVE, event => { this.dispatchEventToListeners(PerfUI.TimelineOverviewPane.Events.OVERVIEW_PANE_MOUSE_MOVE, event.data); }); this.#overviewComponent.addEventListener(PerfUI.TimelineOverviewPane.Events.OVERVIEW_PANE_MOUSE_LEAVE, () => { this.dispatchEventToListeners(PerfUI.TimelineOverviewPane.Events.OVERVIEW_PANE_MOUSE_LEAVE); }); this.#breadcrumbsUI.addEventListener(TimelineComponents.BreadcrumbsUI.BreadcrumbActivatedEvent.eventName, event => { const {breadcrumb, childBreadcrumbsRemoved} = (event as TimelineComponents.BreadcrumbsUI.BreadcrumbActivatedEvent); this.#activateBreadcrumb( breadcrumb, {removeChildBreadcrumbs: Boolean(childBreadcrumbsRemoved), updateVisibleWindow: true}); }); this.#overviewComponent.enableCreateBreadcrumbsButton(); TraceBounds.TraceBounds.onChange(this.#onTraceBoundsChangeBound); // Set the initial bounds for the overview. Otherwise if we mount & there // is not an immediate RESET event, then we don't render correctly. const state = TraceBounds.TraceBounds.BoundsManager.instance().state(); if (state) { const {timelineTraceWindow, minimapTraceBounds} = state.milli; this.#overviewComponent.setWindowTimes(timelineTraceWindow.min, timelineTraceWindow.max); this.#overviewComponent.setBounds(minimapTraceBounds.min, minimapTraceBounds.max); } } #onOverviewPanelWindowChanged( event: Common.EventTarget.EventTargetEvent<PerfUI.TimelineOverviewPane.OverviewPaneWindowChangedEvent>): void { const parsedTrace = this.#data?.parsedTrace; if (!parsedTrace) { return; } const traceBoundsState = TraceBounds.TraceBounds.BoundsManager.instance().state(); if (!traceBoundsState) { return; } const left = (event.data.startTime > 0) ? event.data.startTime : traceBoundsState.milli.entireTraceBounds.min; const right = Number.isFinite(event.data.endTime) ? event.data.endTime : traceBoundsState.milli.entireTraceBounds.max; TraceBounds.TraceBounds.BoundsManager.instance().setTimelineVisibleWindow( Trace.Helpers.Timing.traceWindowFromMilliSeconds( Trace.Types.Timing.Milli(left), Trace.Types.Timing.Milli(right), ), { shouldAnimate: true, }, ); } #onTraceBoundsChange(event: TraceBounds.TraceBounds.StateChangedEvent): void { if (event.updateType === 'RESET' || event.updateType === 'VISIBLE_WINDOW') { this.#overviewComponent.setWindowTimes( event.state.milli.timelineTraceWindow.min, event.state.milli.timelineTraceWindow.max); // If the visible window has changed because we are revealing a certain // time period to the user, we need to ensure that this new time // period fits within the current minimap bounds. If it doesn't, we // do some work to update the minimap bounds. Note that this only // applies if the user has created breadcrumbs, which scope the // minimap. If they have not, the entire trace is the minimap, and // therefore there is no work to be done. const newWindowFitsBounds = Trace.Helpers.Timing.windowFitsInsideBounds({ window: event.state.micro.timelineTraceWindow, bounds: event.state.micro.minimapTraceBounds, }); if (!newWindowFitsBounds) { this.#updateMiniMapBoundsToFitNewWindow(event.state.micro.timelineTraceWindow); } } if (event.updateType === 'RESET' || event.updateType === 'MINIMAP_BOUNDS') { this.#overviewComponent.setBounds( event.state.milli.minimapTraceBounds.min, event.state.milli.minimapTraceBounds.max); } } #updateMiniMapBoundsToFitNewWindow(newWindow: Trace.Types.Timing.TraceWindowMicro): void { if (!this.breadcrumbs) { return; } // Find the smallest breadcrumb that fits this window. // Breadcrumbs are a linked list from largest to smallest so we have to // walk through until we find one that does not fit, and pick the last // before that. let currentBreadcrumb: Trace.Types.File.Breadcrumb|null = this.breadcrumbs.initialBreadcrumb; let lastBreadcrumbThatFits: Trace.Types.File.Breadcrumb = this.breadcrumbs.initialBreadcrumb; while (currentBreadcrumb) { const fits = Trace.Helpers.Timing.windowFitsInsideBounds({ window: newWindow, bounds: currentBreadcrumb.window, }); if (fits) { lastBreadcrumbThatFits = currentBreadcrumb; } else { // If this breadcrumb does not fit, none of its children (which are all // smaller by definition) will, so we can exit the loop early. break; } currentBreadcrumb = currentBreadcrumb.child; } // Activate the breadcrumb that fits the visible window. We do not update // the visible window here as we are doing this work as a reaction to // something else triggering a change in the window visibility. this.#activateBreadcrumb(lastBreadcrumbThatFits, {removeChildBreadcrumbs: false, updateVisibleWindow: false}); } addBreadcrumb({startTime, endTime}: PerfUI.TimelineOverviewPane.OverviewPaneBreadcrumbAddedEvent): void { if (!this.breadcrumbs) { console.warn('ModificationsManager has not been created, therefore Breadcrumbs can not be added'); return; } const traceBoundsState = TraceBounds.TraceBounds.BoundsManager.instance().state(); if (!traceBoundsState) { return; } const bounds = traceBoundsState.milli.minimapTraceBounds; // The OverviewPane can emit 0 and Infinity as numbers for the range; in // this case we change them to be the min and max values of the minimap // bounds. const breadcrumbTimes = { startTime: Trace.Types.Timing.Milli(Math.max(startTime, bounds.min)), endTime: Trace.Types.Timing.Milli(Math.min(endTime, bounds.max)), }; const newVisibleTraceWindow = Trace.Helpers.Timing.traceWindowFromMilliSeconds(breadcrumbTimes.startTime, breadcrumbTimes.endTime); const addedBreadcrumb = this.breadcrumbs.add(newVisibleTraceWindow); this.#breadcrumbsUI.data = { initialBreadcrumb: this.breadcrumbs.initialBreadcrumb, activeBreadcrumb: addedBreadcrumb, }; } highlightBounds(bounds: Trace.Types.Timing.TraceWindowMicro, withBracket = false): void { this.#overviewComponent.highlightBounds(bounds, withBracket); } clearBoundsHighlight(): void { this.#overviewComponent.clearBoundsHighlight(); } /** * Activates a given breadcrumb. * @param options.removeChildBreadcrumbs - if true, any child breadcrumbs will be removed. * @param options.updateVisibleWindow - if true, the visible window will be updated to match the bounds of the breadcrumb */ #activateBreadcrumb( breadcrumb: Trace.Types.File.Breadcrumb, options: TimelineComponents.Breadcrumbs.SetActiveBreadcrumbOptions): void { if (!this.breadcrumbs) { return; } this.breadcrumbs.setActiveBreadcrumb(breadcrumb, options); // Only the initial breadcrumb is passed in because breadcrumbs are stored in a linked list and breadcrumbsUI component iterates through them this.#breadcrumbsUI.data = { initialBreadcrumb: this.breadcrumbs.initialBreadcrumb, activeBreadcrumb: breadcrumb, }; } reset(): void { this.#data = null; this.#overviewComponent.reset(); } #setMarkers(parsedTrace: Trace.Handlers.Types.ParsedTrace): void { const markers = new Map<number, HTMLDivElement>(); const {Meta} = parsedTrace; // Only add markers for navigation start times. const navStartEvents = Meta.mainFrameNavigations; const minTimeInMilliseconds = Trace.Helpers.Timing.microToMilli(Meta.traceBounds.min); for (const event of navStartEvents) { const {startTime} = Trace.Helpers.Timing.eventTimingsMilliSeconds(event); markers.set(startTime, TimelineUIUtils.createEventDivider(event, minTimeInMilliseconds)); } this.#overviewComponent.setMarkers(markers); } #setNavigationStartEvents(parsedTrace: Trace.Handlers.Types.ParsedTrace): void { this.#overviewComponent.setNavStartTimes(parsedTrace.Meta.mainFrameNavigations); } getControls(): TimelineEventOverview[] { return this.#controls; } setData(data: OverviewData): void { if (this.#data?.parsedTrace === data.parsedTrace) { return; } this.#data = data; this.#controls = []; this.#setMarkers(data.parsedTrace); this.#setNavigationStartEvents(data.parsedTrace); this.#controls.push(new TimelineEventOverviewResponsiveness(data.parsedTrace)); this.#controls.push(new TimelineEventOverviewCPUActivity(data.parsedTrace)); this.#controls.push(new TimelineEventOverviewNetwork(data.parsedTrace)); if (data.settings.showScreenshots) { const filmStrip = Trace.Extras.FilmStrip.fromParsedTrace(data.parsedTrace); if (filmStrip.frames.length) { this.#controls.push(new TimelineFilmStripOverview(filmStrip)); } } if (data.settings.showMemory) { this.#controls.push(new TimelineEventOverviewMemory(data.parsedTrace)); } this.#overviewComponent.setOverviewControls(this.#controls); this.#overviewComponent.showingScreenshots = data.settings.showScreenshots; this.#setInitialBreadcrumb(); } #setInitialBreadcrumb(): void { // Set the initial breadcrumb that ModificationsManager created from the initial full window // or loaded from the file. this.breadcrumbs = ModificationsManager.activeManager()?.getTimelineBreadcrumbs() ?? null; if (!this.breadcrumbs) { return; } let lastBreadcrumb = this.breadcrumbs.initialBreadcrumb; while (lastBreadcrumb.child !== null) { lastBreadcrumb = lastBreadcrumb.child; } this.#breadcrumbsUI.data = { initialBreadcrumb: this.breadcrumbs.initialBreadcrumb, activeBreadcrumb: lastBreadcrumb, }; } }