| // Copyright 2015 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| /* eslint-disable @devtools/no-imperative-dom-api */ |
| |
| import type * as Common from '../../core/common/common.js'; |
| import * as SDK from '../../core/sdk/sdk.js'; |
| import * as NetworkTimeCalculator from '../../models/network_time_calculator/network_time_calculator.js'; |
| import * as Trace from '../../models/trace/trace.js'; |
| import * as RenderCoordinator from '../../ui/components/render_coordinator/render_coordinator.js'; |
| import * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js'; |
| import * as ThemeSupport from '../../ui/legacy/theme_support/theme_support.js'; |
| |
| import {NetworkLogView} from './NetworkLogView.js'; |
| |
| export class NetworkOverview extends PerfUI.TimelineOverviewPane.TimelineOverviewBase { |
| private selectedFilmStripTime: number; |
| private numBands: number; |
| private highlightedRequest: SDK.NetworkRequest.NetworkRequest|null; |
| private loadEvents!: number[]; |
| private domContentLoadedEvents!: number[]; |
| private nextBand!: number; |
| private bandMap!: Map<string, number>; |
| private requestsList!: SDK.NetworkRequest.NetworkRequest[]; |
| private requestsSet!: Set<SDK.NetworkRequest.NetworkRequest>; |
| private span!: number; |
| private lastBoundary?: NetworkTimeCalculator.NetworkTimeBoundary|null; |
| |
| constructor() { |
| super(); |
| this.selectedFilmStripTime = -1; |
| this.element.classList.add('network-overview'); |
| |
| this.numBands = 1; |
| this.highlightedRequest = null; |
| |
| SDK.TargetManager.TargetManager.instance().addModelListener( |
| SDK.ResourceTreeModel.ResourceTreeModel, SDK.ResourceTreeModel.Events.Load, this.loadEventFired, this, |
| {scoped: true}); |
| SDK.TargetManager.TargetManager.instance().addModelListener( |
| SDK.ResourceTreeModel.ResourceTreeModel, SDK.ResourceTreeModel.Events.DOMContentLoaded, |
| this.domContentLoadedEventFired, this, {scoped: true}); |
| |
| this.reset(); |
| } |
| |
| setHighlightedRequest(request: SDK.NetworkRequest.NetworkRequest|null): void { |
| this.highlightedRequest = request; |
| this.scheduleUpdate(); |
| } |
| |
| selectFilmStripFrame(time: number): void { |
| this.selectedFilmStripTime = time; |
| this.scheduleUpdate(); |
| } |
| |
| clearFilmStripFrame(): void { |
| this.selectedFilmStripTime = -1; |
| this.scheduleUpdate(); |
| } |
| |
| private loadEventFired( |
| event: Common.EventTarget |
| .EventTargetEvent<{resourceTreeModel: SDK.ResourceTreeModel.ResourceTreeModel, loadTime: number}>): void { |
| const time = event.data.loadTime; |
| if (time) { |
| this.loadEvents.push(time * 1000); |
| } |
| this.scheduleUpdate(); |
| } |
| |
| private domContentLoadedEventFired(event: Common.EventTarget.EventTargetEvent<number>): void { |
| const {data} = event; |
| if (data) { |
| this.domContentLoadedEvents.push(data * 1000); |
| } |
| this.scheduleUpdate(); |
| } |
| |
| private bandId(connectionId: string): number { |
| if (!connectionId || connectionId === '0') { |
| return -1; |
| } |
| if (this.bandMap.has(connectionId)) { |
| return this.bandMap.get(connectionId) as number; |
| } |
| const result = this.nextBand++; |
| this.bandMap.set(connectionId, result); |
| return result; |
| } |
| |
| updateRequest(request: SDK.NetworkRequest.NetworkRequest): void { |
| if (!this.requestsSet.has(request)) { |
| this.requestsSet.add(request); |
| this.requestsList.push(request); |
| } |
| this.scheduleUpdate(); |
| } |
| |
| override wasShown(): void { |
| super.wasShown(); |
| this.onResize(); |
| } |
| |
| override calculator(): PerfUI.TimelineOverviewCalculator.TimelineOverviewCalculator { |
| return super.calculator() as PerfUI.TimelineOverviewCalculator.TimelineOverviewCalculator; |
| } |
| |
| override onResize(): void { |
| const width = this.element.offsetWidth; |
| const height = this.element.offsetHeight; |
| this.calculator().setDisplayWidth(width); |
| this.resetCanvas(); |
| const numBands = (((height - PADDING - 1) / BAND_HEIGHT) - 1) | 0; |
| this.numBands = (numBands > 0) ? numBands : 1; |
| this.scheduleUpdate(); |
| } |
| |
| override reset(): void { |
| this.span = 1; |
| this.lastBoundary = null; |
| this.nextBand = 0; |
| this.bandMap = new Map(); |
| this.requestsList = []; |
| this.requestsSet = new Set(); |
| this.loadEvents = []; |
| this.domContentLoadedEvents = []; |
| |
| // Clear screen. |
| this.resetCanvas(); |
| } |
| |
| scheduleUpdate(): void { |
| if (!this.isShowing()) { |
| return; |
| } |
| void RenderCoordinator.write('NetworkOverview.render', this.update.bind(this)); |
| } |
| |
| override update(): void { |
| const calculator = this.calculator(); |
| |
| const newBoundary = |
| new NetworkTimeCalculator.NetworkTimeBoundary(calculator.minimumBoundary(), calculator.maximumBoundary()); |
| if (!this.lastBoundary || !newBoundary.equals(this.lastBoundary)) { |
| const span = calculator.boundarySpan(); |
| while (this.span < span) { |
| this.span *= 1.25; |
| } |
| |
| calculator.setBounds( |
| calculator.minimumBoundary(), Trace.Types.Timing.Milli(calculator.minimumBoundary() + this.span)); |
| |
| this.lastBoundary = |
| new NetworkTimeCalculator.NetworkTimeBoundary(calculator.minimumBoundary(), calculator.maximumBoundary()); |
| } |
| |
| const context = this.context(); |
| const linesByType = new Map<string, number[]>(); |
| const paddingTop = PADDING; |
| |
| function drawLines(type: string): void { |
| const lines = linesByType.get(type); |
| if (!lines) { |
| return; |
| } |
| const n = lines.length; |
| context.beginPath(); |
| context.strokeStyle = ThemeSupport.ThemeSupport.instance().getComputedValue('--color-background-opacity-80'); |
| context.lineWidth = BORDER_WIDTH; |
| context.fillStyle = ThemeSupport.ThemeSupport.instance().getComputedValue(RequestTimeRangeNameToColor[type]); |
| for (let i = 0; i < n;) { |
| const y = lines[i++] * BAND_HEIGHT + paddingTop; |
| const startTime = lines[i++]; |
| let endTime: number = lines[i++]; |
| if (endTime === Number.MAX_VALUE) { |
| endTime = calculator.maximumBoundary(); |
| } |
| const startX = calculator.computePosition(Trace.Types.Timing.Milli(startTime)); |
| const endX = calculator.computePosition(Trace.Types.Timing.Milli(endTime)) + 1; |
| context.fillRect(startX, y, Math.max(endX - startX, MIN_BAND_WIDTH), BAND_HEIGHT); |
| context.strokeRect(startX, y, Math.max(endX - startX, MIN_BAND_WIDTH), BAND_HEIGHT); |
| } |
| } |
| |
| function addLine(type: string, y: number, start: number, end: number): void { |
| let lines = linesByType.get(type); |
| if (!lines) { |
| lines = []; |
| linesByType.set(type, lines); |
| } |
| lines.push(y, start, end); |
| } |
| |
| const requests = this.requestsList; |
| const n = requests.length; |
| for (let i = 0; i < n; ++i) { |
| const request = requests[i]; |
| const band = this.bandId(request.connectionId); |
| const y = (band === -1) ? 0 : (band % this.numBands + 1); |
| const timeRanges = NetworkTimeCalculator.calculateRequestTimeRanges(request, this.calculator().minimumBoundary()); |
| for (let j = 0; j < timeRanges.length; ++j) { |
| const type = timeRanges[j].name; |
| if (band !== -1 || type === NetworkTimeCalculator.RequestTimeRangeNames.TOTAL) { |
| addLine(type, y, timeRanges[j].start * 1000, timeRanges[j].end * 1000); |
| } |
| } |
| } |
| |
| context.clearRect(0, 0, this.width(), this.height()); |
| context.save(); |
| context.scale(window.devicePixelRatio, window.devicePixelRatio); |
| context.lineWidth = 2; |
| drawLines(NetworkTimeCalculator.RequestTimeRangeNames.TOTAL); |
| drawLines(NetworkTimeCalculator.RequestTimeRangeNames.BLOCKING); |
| drawLines(NetworkTimeCalculator.RequestTimeRangeNames.CONNECTING); |
| drawLines(NetworkTimeCalculator.RequestTimeRangeNames.SERVICE_WORKER); |
| drawLines(NetworkTimeCalculator.RequestTimeRangeNames.SERVICE_WORKER_PREPARATION); |
| drawLines(NetworkTimeCalculator.RequestTimeRangeNames.SERVICE_WORKER_RESPOND_WITH); |
| drawLines(NetworkTimeCalculator.RequestTimeRangeNames.PUSH); |
| drawLines(NetworkTimeCalculator.RequestTimeRangeNames.PROXY); |
| drawLines(NetworkTimeCalculator.RequestTimeRangeNames.DNS); |
| drawLines(NetworkTimeCalculator.RequestTimeRangeNames.SSL); |
| drawLines(NetworkTimeCalculator.RequestTimeRangeNames.SENDING); |
| drawLines(NetworkTimeCalculator.RequestTimeRangeNames.WAITING); |
| drawLines(NetworkTimeCalculator.RequestTimeRangeNames.RECEIVING); |
| |
| if (this.highlightedRequest) { |
| const size = 5; |
| const borderSize = 2; |
| |
| const request = this.highlightedRequest; |
| const band = this.bandId(request.connectionId); |
| const y = ((band === -1) ? 0 : (band % this.numBands + 1)) * BAND_HEIGHT + paddingTop; |
| const timeRanges = NetworkTimeCalculator.calculateRequestTimeRanges(request, this.calculator().minimumBoundary()); |
| |
| context.fillStyle = ThemeSupport.ThemeSupport.instance().getComputedValue('--sys-color-tonal-container'); |
| |
| // The network overview works in seconds, but the calcululator deals in |
| // milliseconds, hence the multiplication by 1000. |
| const start = Trace.Types.Timing.Milli(timeRanges[0].start * 1000); |
| const end = Trace.Types.Timing.Milli(timeRanges[0].end * 1000); |
| context.fillRect( |
| calculator.computePosition(start) - borderSize, y - size / 2 - borderSize, |
| calculator.computePosition(end) - calculator.computePosition(start) + 1 + 2 * borderSize, size * borderSize); |
| |
| for (let j = 0; j < timeRanges.length; ++j) { |
| const type = timeRanges[j].name; |
| if (band !== -1 || type === NetworkTimeCalculator.RequestTimeRangeNames.TOTAL) { |
| context.beginPath(); |
| context.strokeStyle = |
| ThemeSupport.ThemeSupport.instance().getComputedValue(RequestTimeRangeNameToColor[type]); |
| context.lineWidth = size; |
| |
| const start = Trace.Types.Timing.Milli(timeRanges[j].start * 1000); |
| const end = Trace.Types.Timing.Milli(timeRanges[j].end * 1000); |
| context.moveTo(Number(calculator.computePosition(start)) - 0, y); |
| context.lineTo(Number(calculator.computePosition(end)) + 1, y); |
| context.stroke(); |
| } |
| } |
| } |
| |
| const height = this.element.offsetHeight; |
| context.lineWidth = 1; |
| context.beginPath(); |
| context.strokeStyle = ThemeSupport.ThemeSupport.instance().getComputedValue(NetworkLogView.getDCLEventColor()); |
| for (let i = this.domContentLoadedEvents.length - 1; i >= 0; --i) { |
| const position = calculator.computePosition(Trace.Types.Timing.Milli(this.domContentLoadedEvents[i])); |
| const x = Math.round(position) + 0.5; |
| context.moveTo(x, 0); |
| context.lineTo(x, height); |
| } |
| context.stroke(); |
| |
| context.beginPath(); |
| context.strokeStyle = ThemeSupport.ThemeSupport.instance().getComputedValue(NetworkLogView.getLoadEventColor()); |
| for (let i = this.loadEvents.length - 1; i >= 0; --i) { |
| const position = calculator.computePosition(Trace.Types.Timing.Milli(this.loadEvents[i])); |
| const x = Math.round(position) + 0.5; |
| context.moveTo(x, 0); |
| context.lineTo(x, height); |
| } |
| context.stroke(); |
| |
| if (this.selectedFilmStripTime !== -1) { |
| context.lineWidth = 2; |
| context.beginPath(); |
| context.strokeStyle = ThemeSupport.ThemeSupport.instance().getComputedValue('--network-frame-divider-color'); |
| const timeInMilliseconds = Trace.Types.Timing.Milli(this.selectedFilmStripTime); |
| const x = Math.round(calculator.computePosition(timeInMilliseconds)); |
| context.moveTo(x, 0); |
| context.lineTo(x, height); |
| context.stroke(); |
| } |
| context.restore(); |
| } |
| } |
| |
| export const RequestTimeRangeNameToColor = { |
| [NetworkTimeCalculator.RequestTimeRangeNames.TOTAL]: '--network-overview-total', |
| [NetworkTimeCalculator.RequestTimeRangeNames.BLOCKING]: '--network-overview-blocking', |
| [NetworkTimeCalculator.RequestTimeRangeNames.CONNECTING]: '--network-overview-connecting', |
| [NetworkTimeCalculator.RequestTimeRangeNames.SERVICE_WORKER]: '--network-overview-service-worker', |
| [NetworkTimeCalculator.RequestTimeRangeNames.SERVICE_WORKER_PREPARATION]: '--network-overview-service-worker', |
| [NetworkTimeCalculator.RequestTimeRangeNames.SERVICE_WORKER_RESPOND_WITH]: |
| '--network-overview-service-worker-respond-with', |
| [NetworkTimeCalculator.RequestTimeRangeNames.PUSH]: '--network-overview-push', |
| [NetworkTimeCalculator.RequestTimeRangeNames.PROXY]: '--override-network-overview-proxy', |
| [NetworkTimeCalculator.RequestTimeRangeNames.DNS]: '--network-overview-dns', |
| [NetworkTimeCalculator.RequestTimeRangeNames.SSL]: '--network-overview-ssl', |
| [NetworkTimeCalculator.RequestTimeRangeNames.SENDING]: '--override-network-overview-sending', |
| [NetworkTimeCalculator.RequestTimeRangeNames.WAITING]: '--network-overview-waiting', |
| [NetworkTimeCalculator.RequestTimeRangeNames.RECEIVING]: '--network-overview-receiving', |
| [NetworkTimeCalculator.RequestTimeRangeNames.QUEUEING]: '--network-overview-queueing', |
| } as Record<string, string>; |
| |
| const BAND_HEIGHT = 3; |
| const PADDING = 5; |
| |
| // Minimum rectangle width for very short requests. |
| const MIN_BAND_WIDTH = 10; |
| |
| // Border between bars in network overview panel for accessibility. |
| const BORDER_WIDTH = 1; |