| // Copyright 2016 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 * as Common from '../../core/common/common.js'; |
| import type * as SDK from '../../core/sdk/sdk.js'; |
| import * as NetworkTimeCalculator from '../../models/network_time_calculator/network_time_calculator.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 UI from '../../ui/legacy/legacy.js'; |
| import * as ThemeSupport from '../../ui/legacy/theme_support/theme_support.js'; |
| |
| import type {NetworkNode} from './NetworkDataGridNode.js'; |
| import {RequestTimeRangeNameToColor} from './NetworkOverview.js'; |
| import networkWaterfallColumnStyles from './networkWaterfallColumn.css.js'; |
| import {RequestTimingView} from './RequestTimingView.js'; |
| |
| const BAR_SPACING = 1; |
| |
| export class NetworkWaterfallColumn extends UI.Widget.VBox { |
| private canvas: HTMLCanvasElement; |
| private canvasPosition: DOMRect; |
| private readonly leftPadding: number; |
| private readonly fontSize: number; |
| private rightPadding: number; |
| private scrollTop: number; |
| private headerHeight: number; |
| private calculator: NetworkTimeCalculator.NetworkTimeCalculator; |
| private rowHeight: number; |
| private offsetWidth: number; |
| private offsetHeight: number; |
| private startTime: number; |
| private endTime: number; |
| private readonly popoverHelper: UI.PopoverHelper.PopoverHelper; |
| private nodes: NetworkNode[]; |
| private hoveredNode: NetworkNode|null; |
| private eventDividers: Map<string, number[]>; |
| private readonly styleForTimeRangeName: Map<NetworkTimeCalculator.RequestTimeRangeNames, LayerStyle>; |
| private readonly styleForWaitingResourceType: Map<Common.ResourceType.ResourceType, LayerStyle>; |
| private readonly styleForDownloadingResourceType: Map<Common.ResourceType.ResourceType, LayerStyle>; |
| private readonly wiskerStyle: LayerStyle; |
| private readonly hoverDetailsStyle: LayerStyle; |
| private readonly pathForStyle: Map<LayerStyle, Path2D>; |
| private textLayers: TextLayer[]; |
| |
| constructor(calculator: NetworkTimeCalculator.NetworkTimeCalculator) { |
| // TODO(allada) Make this a shadowDOM when the NetworkWaterfallColumn gets moved into NetworkLogViewColumns. |
| super(); |
| this.registerRequiredCSS(networkWaterfallColumnStyles); |
| |
| this.canvas = this.contentElement.createChild('canvas'); |
| this.canvas.tabIndex = -1; |
| this.setDefaultFocusedElement(this.canvas); |
| this.canvasPosition = this.canvas.getBoundingClientRect(); |
| |
| this.leftPadding = 5; |
| this.fontSize = 10; |
| |
| this.rightPadding = 0; |
| this.scrollTop = 0; |
| |
| this.headerHeight = 0; |
| this.calculator = calculator; |
| |
| this.rowHeight = 0; |
| |
| this.offsetWidth = 0; |
| this.offsetHeight = 0; |
| this.startTime = this.calculator.minimumBoundary(); |
| this.endTime = this.calculator.maximumBoundary(); |
| |
| this.popoverHelper = |
| new UI.PopoverHelper.PopoverHelper(this.element, this.getPopoverRequest.bind(this), 'network.timing'); |
| this.popoverHelper.setTimeout(300, 300); |
| |
| this.nodes = []; |
| |
| this.hoveredNode = null; |
| |
| this.eventDividers = new Map(); |
| |
| this.element.addEventListener('mousemove', this.onMouseMove.bind(this), true); |
| this.element.addEventListener('mouseleave', _event => this.setHoveredNode(null, false), true); |
| this.element.addEventListener('click', this.onClick.bind(this), true); |
| |
| this.styleForTimeRangeName = NetworkWaterfallColumn.buildRequestTimeRangeStyle(); |
| |
| const resourceStyleTuple = NetworkWaterfallColumn.buildResourceTypeStyle(); |
| this.styleForWaitingResourceType = resourceStyleTuple[0]; |
| this.styleForDownloadingResourceType = resourceStyleTuple[1]; |
| |
| const baseLineColor = ThemeSupport.ThemeSupport.instance().getComputedValue('--sys-color-state-disabled'); |
| this.wiskerStyle = {borderColor: baseLineColor, lineWidth: 1, fillStyle: undefined}; |
| this.hoverDetailsStyle = {fillStyle: baseLineColor, lineWidth: 1, borderColor: baseLineColor}; |
| |
| this.pathForStyle = new Map(); |
| this.textLayers = []; |
| } |
| |
| private static buildRequestTimeRangeStyle(): Map<NetworkTimeCalculator.RequestTimeRangeNames, LayerStyle> { |
| const styleMap = new Map<NetworkTimeCalculator.RequestTimeRangeNames, LayerStyle>(); |
| styleMap.set( |
| NetworkTimeCalculator.RequestTimeRangeNames.CONNECTING, |
| {fillStyle: RequestTimeRangeNameToColor[NetworkTimeCalculator.RequestTimeRangeNames.CONNECTING]}); |
| styleMap.set( |
| NetworkTimeCalculator.RequestTimeRangeNames.SSL, |
| {fillStyle: RequestTimeRangeNameToColor[NetworkTimeCalculator.RequestTimeRangeNames.SSL]}); |
| styleMap.set( |
| NetworkTimeCalculator.RequestTimeRangeNames.DNS, |
| {fillStyle: RequestTimeRangeNameToColor[NetworkTimeCalculator.RequestTimeRangeNames.DNS]}); |
| styleMap.set( |
| NetworkTimeCalculator.RequestTimeRangeNames.PROXY, |
| {fillStyle: RequestTimeRangeNameToColor[NetworkTimeCalculator.RequestTimeRangeNames.PROXY]}); |
| styleMap.set( |
| NetworkTimeCalculator.RequestTimeRangeNames.BLOCKING, |
| {fillStyle: RequestTimeRangeNameToColor[NetworkTimeCalculator.RequestTimeRangeNames.BLOCKING]}); |
| styleMap.set( |
| NetworkTimeCalculator.RequestTimeRangeNames.PUSH, |
| {fillStyle: RequestTimeRangeNameToColor[NetworkTimeCalculator.RequestTimeRangeNames.PUSH]}); |
| styleMap.set(NetworkTimeCalculator.RequestTimeRangeNames.QUEUEING, { |
| fillStyle: RequestTimeRangeNameToColor[NetworkTimeCalculator.RequestTimeRangeNames.QUEUEING], |
| lineWidth: 2, |
| borderColor: 'lightgrey', |
| }); |
| // This ensures we always show at least 2 px for a request. |
| styleMap.set(NetworkTimeCalculator.RequestTimeRangeNames.RECEIVING, { |
| fillStyle: RequestTimeRangeNameToColor[NetworkTimeCalculator.RequestTimeRangeNames.RECEIVING], |
| lineWidth: 2, |
| borderColor: '#03A9F4', |
| }); |
| styleMap.set( |
| NetworkTimeCalculator.RequestTimeRangeNames.WAITING, |
| {fillStyle: RequestTimeRangeNameToColor[NetworkTimeCalculator.RequestTimeRangeNames.WAITING]}); |
| styleMap.set( |
| NetworkTimeCalculator.RequestTimeRangeNames.RECEIVING_PUSH, |
| {fillStyle: RequestTimeRangeNameToColor[NetworkTimeCalculator.RequestTimeRangeNames.RECEIVING_PUSH]}); |
| styleMap.set( |
| NetworkTimeCalculator.RequestTimeRangeNames.SERVICE_WORKER, |
| {fillStyle: RequestTimeRangeNameToColor[NetworkTimeCalculator.RequestTimeRangeNames.SERVICE_WORKER]}); |
| styleMap.set(NetworkTimeCalculator.RequestTimeRangeNames.SERVICE_WORKER_PREPARATION, { |
| fillStyle: RequestTimeRangeNameToColor[NetworkTimeCalculator.RequestTimeRangeNames.SERVICE_WORKER_PREPARATION] |
| }); |
| styleMap.set(NetworkTimeCalculator.RequestTimeRangeNames.SERVICE_WORKER_RESPOND_WITH, { |
| fillStyle: RequestTimeRangeNameToColor[NetworkTimeCalculator.RequestTimeRangeNames.SERVICE_WORKER_RESPOND_WITH], |
| }); |
| return styleMap; |
| } |
| |
| private static buildResourceTypeStyle(): Array<Map<Common.ResourceType.ResourceType, LayerStyle>> { |
| const baseResourceTypeColors = new Map([ |
| ['document', 'hsl(215, 100%, 80%)'], |
| ['font', 'hsl(8, 100%, 80%)'], |
| ['media', 'hsl(90, 50%, 80%)'], |
| ['image', 'hsl(90, 50%, 80%)'], |
| ['script', 'hsl(31, 100%, 80%)'], |
| ['stylesheet', 'hsl(272, 64%, 80%)'], |
| ['texttrack', 'hsl(8, 100%, 80%)'], |
| ['websocket', 'hsl(0, 0%, 95%)'], |
| ['xhr', 'hsl(53, 100%, 80%)'], |
| ['fetch', 'hsl(53, 100%, 80%)'], |
| ['other', 'hsl(0, 0%, 95%)'], |
| ]); |
| const waitingStyleMap = new Map<Common.ResourceType.ResourceType, LayerStyle>(); |
| const downloadingStyleMap = new Map<Common.ResourceType.ResourceType, LayerStyle>(); |
| |
| for (const resourceType of Object.values(Common.ResourceType.resourceTypes)) { |
| let color = baseResourceTypeColors.get(resourceType.name()); |
| if (!color) { |
| color = baseResourceTypeColors.get('other'); |
| } |
| const borderColor = toBorderColor((color as string)); |
| |
| waitingStyleMap.set( |
| // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) |
| // @ts-expect-error |
| resourceType, {fillStyle: toWaitingColor((color as string)), lineWidth: 1, borderColor}); |
| // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) |
| // @ts-expect-error |
| downloadingStyleMap.set(resourceType, {fillStyle: color, lineWidth: 1, borderColor}); |
| } |
| return [waitingStyleMap, downloadingStyleMap]; |
| |
| function toBorderColor(color: string): string|null { |
| const parsedColor = Common.Color.parse(color)?.as(Common.Color.Format.HSL); |
| if (!parsedColor) { |
| return ''; |
| } |
| let {s, l} = parsedColor; |
| s /= 2; |
| l -= Math.min(l, 0.2); |
| return new Common.Color.HSL(parsedColor.h, s, l, parsedColor.alpha).asString(); |
| } |
| |
| function toWaitingColor(color: string): string|null { |
| const parsedColor = Common.Color.parse(color)?.as(Common.Color.Format.HSL); |
| if (!parsedColor) { |
| return ''; |
| } |
| let {l} = parsedColor; |
| l *= 1.1; |
| return new Common.Color.HSL(parsedColor.h, parsedColor.s, l, parsedColor.alpha).asString(); |
| } |
| } |
| |
| private resetPaths(): void { |
| this.pathForStyle.clear(); |
| this.pathForStyle.set(this.wiskerStyle, new Path2D()); |
| this.styleForTimeRangeName.forEach(style => this.pathForStyle.set(style, new Path2D())); |
| this.styleForWaitingResourceType.forEach(style => this.pathForStyle.set(style, new Path2D())); |
| this.styleForDownloadingResourceType.forEach(style => this.pathForStyle.set(style, new Path2D())); |
| this.pathForStyle.set(this.hoverDetailsStyle, new Path2D()); |
| } |
| |
| override willHide(): void { |
| this.popoverHelper.hidePopover(); |
| super.willHide(); |
| } |
| |
| override wasShown(): void { |
| super.wasShown(); |
| this.update(); |
| } |
| |
| private onMouseMove(event: MouseEvent): void { |
| this.setHoveredNode(this.getNodeFromPoint(event.offsetY), event.shiftKey); |
| } |
| |
| private onClick(event: MouseEvent): void { |
| const handled = this.setSelectedNode(this.getNodeFromPoint(event.offsetY)); |
| if (handled) { |
| event.consume(true); |
| } |
| } |
| |
| private getPopoverRequest(event: MouseEvent|KeyboardEvent): UI.PopoverHelper.PopoverRequest|null { |
| if (event instanceof KeyboardEvent) { |
| return null; |
| } |
| if (!this.hoveredNode) { |
| return null; |
| } |
| const request = this.hoveredNode.request(); |
| if (!request) { |
| return null; |
| } |
| const useTimingBars = |
| !Common.Settings.Settings.instance().moduleSetting('network-color-code-resource-types').get() && |
| !this.calculator.startAtZero; |
| let range; |
| let start; |
| let end; |
| if (useTimingBars) { |
| range = NetworkTimeCalculator.calculateRequestTimeRanges(request, 0) |
| .find(data => data.name === NetworkTimeCalculator.RequestTimeRangeNames.TOTAL); |
| start = this.timeToPosition((range as NetworkTimeCalculator.RequestTimeRange).start); |
| end = this.timeToPosition((range as NetworkTimeCalculator.RequestTimeRange).end); |
| } else { |
| range = this.getSimplifiedBarRange(request, 0); |
| start = range.start; |
| end = range.end; |
| } |
| |
| if (end - start < 50) { |
| const halfWidth = (end - start) / 2; |
| start = start + halfWidth - 25; |
| end = end - halfWidth + 25; |
| } |
| |
| if (event.clientX < this.canvasPosition.left + start || event.clientX > this.canvasPosition.left + end) { |
| return null; |
| } |
| |
| const rowIndex = this.nodes.findIndex(node => node.hovered()); |
| const barHeight = this.getBarHeight((range as NetworkTimeCalculator.RequestTimeRange).name); |
| const y = this.headerHeight + (this.rowHeight * rowIndex - this.scrollTop) + ((this.rowHeight - barHeight) / 2); |
| |
| if (event.clientY < this.canvasPosition.top + y || event.clientY > this.canvasPosition.top + y + barHeight) { |
| return null; |
| } |
| |
| const anchorBox = this.element.boxInWindow(); |
| anchorBox.x += start; |
| anchorBox.y += y; |
| anchorBox.width = end - start; |
| anchorBox.height = barHeight; |
| |
| return { |
| box: anchorBox, |
| show: async (popover: UI.GlassPane.GlassPane) => { |
| const content = RequestTimingView.create(request, this.calculator); |
| await content.updateComplete; |
| content.show(popover.contentElement); |
| return true; |
| }, |
| hide: undefined, |
| }; |
| } |
| |
| private setHoveredNode(node: NetworkNode|null, highlightInitiatorChain: boolean): void { |
| if (this.hoveredNode) { |
| this.hoveredNode.setHovered(false, false); |
| } |
| this.hoveredNode = node; |
| if (this.hoveredNode) { |
| this.hoveredNode.setHovered(true, highlightInitiatorChain); |
| } |
| } |
| |
| private setSelectedNode(node: NetworkNode|null): boolean { |
| if (node?.dataGrid) { |
| node.select(); |
| node.dataGrid.element.focus(); |
| return true; |
| } |
| return false; |
| } |
| |
| setRowHeight(height: number): void { |
| this.rowHeight = height; |
| } |
| |
| setHeaderHeight(height: number): void { |
| this.headerHeight = height; |
| } |
| |
| setRightPadding(padding: number): void { |
| this.rightPadding = padding; |
| this.calculateCanvasSize(); |
| } |
| |
| setCalculator(calculator: NetworkTimeCalculator.NetworkTimeCalculator): void { |
| this.calculator = calculator; |
| } |
| |
| getNodeFromPoint(y: number): NetworkNode|null { |
| if (y <= this.headerHeight) { |
| return null; |
| } |
| return this.nodes[Math.floor((this.scrollTop + y - this.headerHeight) / this.rowHeight)]; |
| } |
| |
| scheduleDraw(): void { |
| void RenderCoordinator.write('NetworkWaterfallColumn.render', () => this.update()); |
| } |
| |
| update(scrollTop?: number, eventDividers?: Map<string, number[]>, nodes?: NetworkNode[]): void { |
| if (scrollTop !== undefined && this.scrollTop !== scrollTop) { |
| this.popoverHelper.hidePopover(); |
| this.scrollTop = scrollTop; |
| } |
| if (nodes) { |
| this.nodes = nodes; |
| this.calculateCanvasSize(); |
| } |
| if (eventDividers !== undefined) { |
| this.eventDividers = eventDividers; |
| } |
| |
| this.startTime = this.calculator.minimumBoundary(); |
| this.endTime = this.calculator.maximumBoundary(); |
| this.resetCanvas(); |
| this.resetPaths(); |
| this.textLayers = []; |
| this.draw(); |
| } |
| |
| private resetCanvas(): void { |
| const ratio = window.devicePixelRatio; |
| this.canvas.width = this.offsetWidth * ratio; |
| this.canvas.height = this.offsetHeight * ratio; |
| this.canvas.style.width = this.offsetWidth + 'px'; |
| this.canvas.style.height = this.offsetHeight + 'px'; |
| } |
| |
| override onResize(): void { |
| super.onResize(); |
| this.calculateCanvasSize(); |
| this.scheduleDraw(); |
| } |
| |
| private calculateCanvasSize(): void { |
| this.offsetWidth = this.contentElement.offsetWidth - this.rightPadding; |
| this.offsetHeight = this.contentElement.offsetHeight; |
| this.calculator.setDisplayWidth(this.offsetWidth); |
| this.canvasPosition = this.canvas.getBoundingClientRect(); |
| } |
| |
| private timeToPosition(time: number): number { |
| const availableWidth = this.offsetWidth - this.leftPadding; |
| const timeToPixel = availableWidth / (this.endTime - this.startTime); |
| return Math.floor(this.leftPadding + (time - this.startTime) * timeToPixel); |
| } |
| |
| private didDrawForTest(): void { |
| } |
| |
| private draw(): void { |
| const useTimingBars = |
| !Common.Settings.Settings.instance().moduleSetting('network-color-code-resource-types').get() && |
| !this.calculator.startAtZero; |
| const nodes = this.nodes; |
| const context = (this.canvas.getContext('2d')); |
| if (!context) { |
| return; |
| } |
| context.save(); |
| context.scale(window.devicePixelRatio, window.devicePixelRatio); |
| context.translate(0, this.headerHeight); |
| context.rect(0, 0, this.offsetWidth, this.offsetHeight); |
| context.clip(); |
| const firstRequestIndex = Math.floor(this.scrollTop / this.rowHeight); |
| const lastRequestIndex = Math.min(nodes.length, firstRequestIndex + Math.ceil(this.offsetHeight / this.rowHeight)); |
| for (let i = firstRequestIndex; i < lastRequestIndex; i++) { |
| const rowOffset = this.rowHeight * i; |
| const node = nodes[i]; |
| this.decorateRow(context, node, rowOffset - this.scrollTop); |
| let drawNodes: NetworkNode[] = []; |
| if (node.hasChildren() && !node.expanded) { |
| drawNodes = (node.flatChildren() as NetworkNode[]); |
| } |
| drawNodes.push(node); |
| for (const drawNode of drawNodes) { |
| if (useTimingBars) { |
| this.buildTimingBarLayers(drawNode, rowOffset - this.scrollTop); |
| } else { |
| this.buildSimplifiedBarLayers(context, drawNode, rowOffset - this.scrollTop); |
| } |
| } |
| } |
| this.drawLayers(context, useTimingBars); |
| |
| context.save(); |
| context.fillStyle = ThemeSupport.ThemeSupport.instance().getComputedValue('--sys-color-state-disabled'); |
| for (const textData of this.textLayers) { |
| context.fillText(textData.text, textData.x, textData.y); |
| } |
| context.restore(); |
| |
| this.drawEventDividers(context); |
| context.restore(); |
| |
| const freeZoneAtLeft = 75; |
| const freeZoneAtRight = 18; |
| const dividersData = PerfUI.TimelineGrid.TimelineGrid.calculateGridOffsets(this.calculator); |
| PerfUI.TimelineGrid.TimelineGrid.drawCanvasGrid(context, dividersData); |
| PerfUI.TimelineGrid.TimelineGrid.drawCanvasHeaders( |
| context, dividersData, time => this.calculator.formatValue(time, dividersData.precision), this.fontSize, |
| this.headerHeight, freeZoneAtLeft); |
| context.save(); |
| context.scale(window.devicePixelRatio, window.devicePixelRatio); |
| context.clearRect(this.offsetWidth - freeZoneAtRight, 0, freeZoneAtRight, this.headerHeight); |
| context.restore(); |
| this.didDrawForTest(); |
| } |
| |
| private drawLayers(context: CanvasRenderingContext2D, useTimingBars: boolean): void { |
| for (const entry of this.pathForStyle) { |
| const style = (entry[0]); |
| const path = (entry[1]); |
| context.save(); |
| context.beginPath(); |
| if (style.lineWidth) { |
| context.lineWidth = style.lineWidth; |
| if (style.borderColor) { |
| context.strokeStyle = style.borderColor; |
| } |
| context.stroke(path); |
| } |
| if (style.fillStyle) { |
| context.fillStyle = |
| useTimingBars ? ThemeSupport.ThemeSupport.instance().getComputedValue(style.fillStyle) : style.fillStyle; |
| context.fill(path); |
| } |
| context.restore(); |
| } |
| } |
| |
| private drawEventDividers(context: CanvasRenderingContext2D): void { |
| context.save(); |
| context.lineWidth = 1; |
| for (const color of this.eventDividers.keys()) { |
| context.strokeStyle = color; |
| for (const time of this.eventDividers.get(color) || []) { |
| context.beginPath(); |
| const x = this.timeToPosition(time); |
| context.moveTo(x, 0); |
| context.lineTo(x, this.offsetHeight); |
| } |
| context.stroke(); |
| } |
| context.restore(); |
| } |
| |
| private getBarHeight(type?: NetworkTimeCalculator.RequestTimeRangeNames): number { |
| switch (type) { |
| case NetworkTimeCalculator.RequestTimeRangeNames.CONNECTING: |
| case NetworkTimeCalculator.RequestTimeRangeNames.SSL: |
| case NetworkTimeCalculator.RequestTimeRangeNames.DNS: |
| case NetworkTimeCalculator.RequestTimeRangeNames.PROXY: |
| case NetworkTimeCalculator.RequestTimeRangeNames.BLOCKING: |
| case NetworkTimeCalculator.RequestTimeRangeNames.PUSH: |
| case NetworkTimeCalculator.RequestTimeRangeNames.QUEUEING: |
| return 7; |
| default: |
| return 13; |
| } |
| } |
| |
| // Used when `network-color-code-resource-types` is true |
| private getSimplifiedBarRange(request: SDK.NetworkRequest.NetworkRequest, borderOffset: number): { |
| start: number, |
| mid: number, |
| end: number, |
| } { |
| const drawWidth = this.offsetWidth - this.leftPadding; |
| const percentages = this.calculator.computeBarGraphPercentages(request); |
| return { |
| start: this.leftPadding + Math.floor((percentages.start / 100) * drawWidth) + borderOffset, |
| mid: this.leftPadding + Math.floor((percentages.middle / 100) * drawWidth) + borderOffset, |
| end: this.leftPadding + Math.floor((percentages.end / 100) * drawWidth) + borderOffset, |
| }; |
| } |
| |
| // Used when `network-color-code-resource-types` is true |
| private buildSimplifiedBarLayers(context: CanvasRenderingContext2D, node: NetworkNode, y: number): void { |
| const request = node.request(); |
| if (!request) { |
| return; |
| } |
| const borderWidth = 1; |
| const borderOffset = borderWidth % 2 === 0 ? 0 : 0.5; |
| |
| const ranges = this.getSimplifiedBarRange(request, borderOffset); |
| const height = this.getBarHeight(); |
| y += Math.floor(this.rowHeight / 2 - height / 2 + borderWidth) - borderWidth / 2; |
| |
| const waitingStyle = (this.styleForWaitingResourceType.get(request.resourceType()) as LayerStyle); |
| const waitingPath = (this.pathForStyle.get(waitingStyle) as Path2D); |
| waitingPath.rect(ranges.start, y, ranges.mid - ranges.start, height - borderWidth); |
| |
| const barWidth = Math.max(2, ranges.end - ranges.mid); |
| const downloadingStyle = (this.styleForDownloadingResourceType.get(request.resourceType()) as LayerStyle); |
| const downloadingPath = (this.pathForStyle.get(downloadingStyle) as Path2D); |
| downloadingPath.rect(ranges.mid, y, barWidth, height - borderWidth); |
| |
| let labels: NetworkTimeCalculator.Label|null = null; |
| if (node.hovered()) { |
| labels = this.calculator.computeBarGraphLabels(request); |
| const barDotLineLength = 10; |
| const leftLabelWidth = context.measureText(labels.left).width; |
| const rightLabelWidth = context.measureText(labels.right).width; |
| const hoverLinePath = (this.pathForStyle.get(this.hoverDetailsStyle) as Path2D); |
| |
| if (leftLabelWidth < ranges.mid - ranges.start) { |
| const midBarX = ranges.start + (ranges.mid - ranges.start - leftLabelWidth) / 2; |
| this.textLayers.push({text: labels.left, x: midBarX, y: y + this.fontSize}); |
| } else if (barDotLineLength + leftLabelWidth + this.leftPadding < ranges.start) { |
| this.textLayers.push( |
| {text: labels.left, x: ranges.start - leftLabelWidth - barDotLineLength - 1, y: y + this.fontSize}); |
| hoverLinePath.moveTo(ranges.start - barDotLineLength, y + Math.floor(height / 2)); |
| hoverLinePath.arc(ranges.start, y + Math.floor(height / 2), 2, 0, 2 * Math.PI); |
| hoverLinePath.moveTo(ranges.start - barDotLineLength, y + Math.floor(height / 2)); |
| hoverLinePath.lineTo(ranges.start, y + Math.floor(height / 2)); |
| } |
| |
| const endX = ranges.mid + barWidth + borderOffset; |
| if (rightLabelWidth < endX - ranges.mid) { |
| const midBarX = ranges.mid + (endX - ranges.mid - rightLabelWidth) / 2; |
| this.textLayers.push({text: labels.right, x: midBarX, y: y + this.fontSize}); |
| } else if (endX + barDotLineLength + rightLabelWidth < this.offsetWidth - this.leftPadding) { |
| this.textLayers.push({text: labels.right, x: endX + barDotLineLength + 1, y: y + this.fontSize}); |
| hoverLinePath.moveTo(endX, y + Math.floor(height / 2)); |
| hoverLinePath.arc(endX, y + Math.floor(height / 2), 2, 0, 2 * Math.PI); |
| hoverLinePath.moveTo(endX, y + Math.floor(height / 2)); |
| hoverLinePath.lineTo(endX + barDotLineLength, y + Math.floor(height / 2)); |
| } |
| } |
| |
| if (!this.calculator.startAtZero) { |
| const queueingRange = |
| (NetworkTimeCalculator.calculateRequestTimeRanges(request, 0) |
| .find(data => data.name === NetworkTimeCalculator.RequestTimeRangeNames.TOTAL) as |
| NetworkTimeCalculator.RequestTimeRange); |
| const leftLabelWidth = labels ? context.measureText(labels.left).width : 0; |
| const leftTextPlacedInBar = leftLabelWidth < ranges.mid - ranges.start; |
| const wiskerTextPadding = 13; |
| const textOffset = (labels && !leftTextPlacedInBar) ? leftLabelWidth + wiskerTextPadding : 0; |
| const queueingStart = this.timeToPosition(queueingRange.start); |
| if (ranges.start - textOffset > queueingStart) { |
| const wiskerPath = (this.pathForStyle.get(this.wiskerStyle) as Path2D); |
| wiskerPath.moveTo(queueingStart, y + Math.floor(height / 2)); |
| wiskerPath.lineTo(ranges.start - textOffset, y + Math.floor(height / 2)); |
| |
| // TODO(allada) This needs to be floored. |
| const wiskerHeight = height / 2; |
| wiskerPath.moveTo(queueingStart + borderOffset, y + wiskerHeight / 2); |
| wiskerPath.lineTo(queueingStart + borderOffset, y + height - wiskerHeight / 2 - 1); |
| } |
| } |
| } |
| |
| private buildTimingBarLayers(node: NetworkNode, y: number): void { |
| const request = node.request(); |
| if (!request) { |
| return; |
| } |
| const ranges = NetworkTimeCalculator.calculateRequestTimeRanges(request, 0); |
| let index = 0; |
| for (const range of ranges) { |
| if (range.name === NetworkTimeCalculator.RequestTimeRangeNames.TOTAL || |
| range.name === NetworkTimeCalculator.RequestTimeRangeNames.SENDING || range.end - range.start === 0) { |
| continue; |
| } |
| |
| const style = (this.styleForTimeRangeName.get(range.name) as LayerStyle); |
| const path = (this.pathForStyle.get(style) as Path2D); |
| const lineWidth = style.lineWidth || 0; |
| const height = this.getBarHeight(range.name); |
| const middleBarY = y + Math.floor(this.rowHeight / 2 - height / 2) + lineWidth / 2; |
| const start = this.timeToPosition(range.start); |
| const end = this.timeToPosition(range.end); |
| path.rect(start + (index * BAR_SPACING), middleBarY, end - start, height - lineWidth); |
| index++; |
| } |
| } |
| |
| private decorateRow(context: CanvasRenderingContext2D, node: NetworkNode, y: number): void { |
| const nodeBgColorId = node.backgroundColor(); |
| context.save(); |
| context.beginPath(); |
| context.fillStyle = ThemeSupport.ThemeSupport.instance().getComputedValue(nodeBgColorId); |
| context.rect(0, y, this.offsetWidth, this.rowHeight); |
| context.fill(); |
| context.restore(); |
| } |
| } |
| |
| interface TextLayer { |
| x: number; |
| y: number; |
| text: string; |
| } |
| |
| interface LayerStyle { |
| fillStyle?: string; |
| lineWidth?: number; |
| borderColor?: string; |
| } |