UNPKG

chrome-devtools-frontend

Version:
394 lines (349 loc) 14.9 kB
// Copyright 2016 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 i18n from '../../core/i18n/i18n.js'; import * as Platform from '../../core/platform/platform.js'; import timelineFlamechartPopoverStyles from './timelineFlamechartPopover.css.js'; import type * as TimelineModel from '../../models/timeline_model/timeline_model.js'; import * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js'; import type * as TraceEngine from '../../models/trace/trace.js'; import * as UI from '../../ui/legacy/legacy.js'; import * as ThemeSupport from '../../ui/legacy/theme_support/theme_support.js'; import * as Protocol from '../../generated/protocol.js'; import {type PerformanceModel} from './PerformanceModel.js'; import {FlameChartStyle, Selection} from './TimelineFlameChartView.js'; import {TimelineSelection} from './TimelineSelection.js'; import {TimelineUIUtils} from './TimelineUIUtils.js'; const UIStrings = { /** *@description Title of the Network tool */ network: 'Network', }; const str_ = i18n.i18n.registerUIStrings('panels/timeline/TimelineFlameChartNetworkDataProvider.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export class TimelineFlameChartNetworkDataProvider implements PerfUI.FlameChart.FlameChartDataProvider { readonly #font: string; readonly #style: { padding: number, height: number, collapsible: boolean, color: string, font: string, backgroundColor: string, nestingLevel: number, useFirstLineForOverview: boolean, useDecoratorsForOverview: boolean, shareHeaderLine: boolean, }; #group: PerfUI.FlameChart.Group; #minimumBoundaryInternal: number; #maximumBoundary: number; #timeSpan: number; #requests: TimelineModel.TimelineModel.NetworkRequest[]; #maxLevel: number; #legacyTimelineModel?: TimelineModel.TimelineModel.TimelineModelImpl|null; // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration // eslint-disable-next-line @typescript-eslint/no-explicit-any #timelineDataInternal?: any; #startTime?: number; #endTime?: number; #lastSelection?: Selection; #priorityToValue?: Map<string, number>; // Ignored during the migration to new trace data engine. /* eslint-disable-next-line no-unused-private-class-members */ #traceEngineData: TraceEngine.TraceModel.PartialTraceParseDataDuringMigration|null; constructor() { this.#font = `${PerfUI.Font.DEFAULT_FONT_SIZE} ${PerfUI.Font.getFontFamilyForCanvas()}`; this.#style = { padding: 4, height: 17, collapsible: true, color: ThemeSupport.ThemeSupport.instance().getComputedValue('--color-text-primary'), font: this.#font, backgroundColor: ThemeSupport.ThemeSupport.instance().getComputedValue('--color-background'), nestingLevel: 0, useFirstLineForOverview: false, useDecoratorsForOverview: true, shareHeaderLine: false, }; this.#group = ({startLevel: 0, name: i18nString(UIStrings.network), expanded: false, style: this.#style} as PerfUI.FlameChart.Group); this.#minimumBoundaryInternal = 0; this.#maximumBoundary = 0; this.#timeSpan = 0; this.#requests = []; this.#maxLevel = 0; this.#legacyTimelineModel = null; this.#traceEngineData = null; // In the event of a theme change, these colors must be recalculated. ThemeSupport.ThemeSupport.instance().addEventListener(ThemeSupport.ThemeChangeEvent.eventName, () => { this.#style.color = ThemeSupport.ThemeSupport.instance().getComputedValue('--color-text-primary'); this.#style.backgroundColor = ThemeSupport.ThemeSupport.instance().getComputedValue('--color-background'); }); } setModel( performanceModel: PerformanceModel|null, traceEngineData: TraceEngine.TraceModel.PartialTraceParseDataDuringMigration|null): void { this.#timelineDataInternal = null; this.#legacyTimelineModel = performanceModel && performanceModel.timelineModel(); this.#traceEngineData = traceEngineData; } isEmpty(): boolean { this.timelineData(); return !this.#requests.length; } maxStackDepth(): number { return this.#maxLevel; } timelineData(): PerfUI.FlameChart.FlameChartTimelineData { if (this.#timelineDataInternal) { return this.#timelineDataInternal; } this.#requests = []; this.#timelineDataInternal = PerfUI.FlameChart.FlameChartTimelineData.createEmpty(); if (this.#legacyTimelineModel) { this.#appendTimelineData(); } return this.#timelineDataInternal; } minimumBoundary(): number { return this.#minimumBoundaryInternal; } totalTime(): number { return this.#timeSpan; } setWindowTimes(startTime: number, endTime: number): void { this.#startTime = startTime; this.#endTime = endTime; this.#updateTimelineData(); } createSelection(index: number): TimelineSelection|null { if (index === -1) { return null; } const request = this.#requests[index]; this.#lastSelection = new Selection(TimelineSelection.fromNetworkRequest(request), index); return this.#lastSelection.timelineSelection; } entryIndexForSelection(selection: TimelineSelection|null): number { if (!selection) { return -1; } if (this.#lastSelection && this.#lastSelection.timelineSelection.object === selection.object) { return this.#lastSelection.entryIndex; } if (!TimelineSelection.isNetworkRequestSelection(selection.object)) { return -1; } const index = this.#requests.indexOf(selection.object); if (index !== -1) { this.#lastSelection = new Selection(TimelineSelection.fromNetworkRequest(selection.object), index); } return index; } entryColor(index: number): string { const request = (this.#requests[index] as TimelineModel.TimelineModel.NetworkRequest); const category = TimelineUIUtils.networkRequestCategory(request); return TimelineUIUtils.networkCategoryColor(category); } textColor(_index: number): string { return FlameChartStyle.textColor; } entryTitle(index: number): string|null { const request = (this.#requests[index] as TimelineModel.TimelineModel.NetworkRequest); const parsedURL = new Common.ParsedURL.ParsedURL(request.url || ''); return parsedURL.isValid ? `${parsedURL.displayName} (${parsedURL.host})` : request.url || null; } entryFont(_index: number): string|null { return this.#font; } decorateEntry( index: number, context: CanvasRenderingContext2D, text: string|null, barX: number, barY: number, barWidth: number, barHeight: number, unclippedBarX: number, timeToPixelRatio: number): boolean { const request = (this.#requests[index] as TimelineModel.TimelineModel.NetworkRequest); if (!request.timing) { return false; } const beginTime = request.beginTime(); const timeToPixel = (time: number): number => Math.floor(unclippedBarX + (time - beginTime) * timeToPixelRatio); const minBarWidthPx = 2; const startTime = request.getStartTime(); const endTime = request.endTime; const {sendStartTime, headersEndTime} = request.getSendReceiveTiming(); const sendStart = Math.max(timeToPixel(sendStartTime), unclippedBarX); const headersEnd = Math.max(timeToPixel(headersEndTime), sendStart); const finish = Math.max(timeToPixel(request.finishTime || endTime), headersEnd + minBarWidthPx); const start = timeToPixel(startTime); const end = Math.max(timeToPixel(endTime), finish); // Draw waiting time. context.fillStyle = 'hsla(0, 100%, 100%, 0.8)'; context.fillRect(sendStart + 0.5, barY + 0.5, headersEnd - sendStart - 0.5, barHeight - 2); // Clear portions of initial rect to prepare for the ticks. context.fillStyle = ThemeSupport.ThemeSupport.instance().getComputedValue('--color-background'); context.fillRect(barX, barY - 0.5, sendStart - barX, barHeight); context.fillRect(finish, barY - 0.5, barX + barWidth - finish, barHeight); // Draws left and right whiskers function drawTick(begin: number, end: number, y: number): void { const /** @const */ tickHeightPx = 6; context.moveTo(begin, y - tickHeightPx / 2); context.lineTo(begin, y + tickHeightPx / 2); context.moveTo(begin, y); context.lineTo(end, y); } context.beginPath(); context.lineWidth = 1; context.strokeStyle = '#ccc'; const lineY = Math.floor(barY + barHeight / 2) + 0.5; const leftTick = start + 0.5; const rightTick = end - 0.5; drawTick(leftTick, sendStart, lineY); drawTick(rightTick, finish, lineY); context.stroke(); if (typeof request.priority === 'string') { const color = this.#colorForPriority(request.priority); if (color) { context.fillStyle = color; context.fillRect(sendStart + 0.5, barY + 0.5, 3.5, 3.5); } } // Draw request URL as text const textStart = Math.max(sendStart, 0); const textWidth = finish - textStart; const /** @const */ minTextWidthPx = 20; if (textWidth >= minTextWidthPx) { text = this.entryTitle(index) || ''; if (request.fromServiceWorker) { text = '⚙ ' + text; } if (text) { const /** @const */ textPadding = 4; const /** @const */ textBaseline = 5; const textBaseHeight = barHeight - textBaseline; const trimmedText = UI.UIUtils.trimTextEnd(context, text, textWidth - 2 * textPadding); context.fillStyle = '#333'; context.fillText(trimmedText, textStart + textPadding, barY + textBaseHeight); } } return true; } forceDecoration(_index: number): boolean { return true; } prepareHighlightedEntryInfo(index: number): Element|null { const /** @const */ maxURLChars = 80; const request = (this.#requests[index] as TimelineModel.TimelineModel.NetworkRequest); if (!request.url) { return null; } const element = document.createElement('div'); const root = UI.Utils.createShadowRootWithCoreStyles(element, { cssFile: [timelineFlamechartPopoverStyles], delegatesFocus: undefined, }); const contents = root.createChild('div', 'timeline-flamechart-popover'); const startTime = request.getStartTime(); const duration = request.endTime - startTime; if (startTime && isFinite(duration)) { contents.createChild('span', 'timeline-info-network-time').textContent = i18n.TimeUtilities.millisToString(duration, true); } if (typeof request.priority === 'string') { const div = (contents.createChild('span') as HTMLElement); div.textContent = PerfUI.NetworkPriorities.uiLabelForNetworkPriority((request.priority as Protocol.Network.ResourcePriority)); div.style.color = this.#colorForPriority(request.priority) || 'black'; } contents.createChild('span').textContent = Platform.StringUtilities.trimMiddle(request.url, maxURLChars); return element; } #colorForPriority(priority: string): string|null { if (!this.#priorityToValue) { this.#priorityToValue = new Map([ [Protocol.Network.ResourcePriority.VeryLow, 1], [Protocol.Network.ResourcePriority.Low, 2], [Protocol.Network.ResourcePriority.Medium, 3], [Protocol.Network.ResourcePriority.High, 4], [Protocol.Network.ResourcePriority.VeryHigh, 5], ]); } const value = this.#priorityToValue.get(priority); return value ? `hsla(214, 80%, 50%, ${value / 5})` : null; } #appendTimelineData(): void { if (this.#legacyTimelineModel) { this.#minimumBoundaryInternal = this.#legacyTimelineModel.minimumRecordTime(); this.#maximumBoundary = this.#legacyTimelineModel.maximumRecordTime(); this.#timeSpan = this.#legacyTimelineModel.isEmpty() ? 1000 : this.#maximumBoundary - this.#minimumBoundaryInternal; this.#legacyTimelineModel.networkRequests().forEach(this.#appendEntry.bind(this)); this.#updateTimelineData(); } } #updateTimelineData(): void { if (!this.#timelineDataInternal) { return; } const lastTimeByLevel = []; let maxLevel = 0; for (let i = 0; i < this.#requests.length; ++i) { const r = this.#requests[i]; const beginTime = r.beginTime(); const startTime = (this.#startTime as number); const endTime = (this.#endTime as number); const visible = beginTime < endTime && r.endTime > startTime; if (!visible) { this.#timelineDataInternal.entryLevels[i] = -1; continue; } while (lastTimeByLevel.length && lastTimeByLevel[lastTimeByLevel.length - 1] <= beginTime) { lastTimeByLevel.pop(); } this.#timelineDataInternal.entryLevels[i] = lastTimeByLevel.length; lastTimeByLevel.push(r.endTime); maxLevel = Math.max(maxLevel, lastTimeByLevel.length); } for (let i = 0; i < this.#requests.length; ++i) { if (this.#timelineDataInternal.entryLevels[i] === -1) { this.#timelineDataInternal.entryLevels[i] = maxLevel; } } this.#timelineDataInternal = PerfUI.FlameChart.FlameChartTimelineData.create({ entryLevels: this.#timelineDataInternal.entryLevels, entryTotalTimes: this.#timelineDataInternal.entryTotalTimes, entryStartTimes: this.#timelineDataInternal.entryStartTimes, groups: [this.#group], }); this.#maxLevel = maxLevel; } #appendEntry(request: TimelineModel.TimelineModel.NetworkRequest): void { this.#requests.push(request); this.#timelineDataInternal.entryStartTimes.push(request.beginTime()); this.#timelineDataInternal.entryTotalTimes.push(request.endTime - request.beginTime()); this.#timelineDataInternal.entryLevels.push(this.#requests.length - 1); } preferredHeight(): number { return this.#style.height * (this.#group.expanded ? Platform.NumberUtilities.clamp(this.#maxLevel + 1, 4, 8.5) : 1); } isExpanded(): boolean { return this.#group && Boolean(this.#group.expanded); } formatValue(value: number, precision?: number): string { return i18n.TimeUtilities.preciseMillisToString(value, precision); } canJumpToEntry(_entryIndex: number): boolean { return false; } // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration // eslint-disable-next-line @typescript-eslint/no-explicit-any // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration // eslint-disable-next-line @typescript-eslint/no-explicit-any navStartTimes(): Map<any, any> { if (!this.#legacyTimelineModel) { return new Map(); } return this.#legacyTimelineModel.navStartTimes(); } }