UNPKG

chrome-devtools-frontend

Version:
700 lines (624 loc) • 24.7 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 '../common/common.js'; import * as PerfUI from '../perf_ui/perf_ui.js'; import * as SDK from '../sdk/sdk.js'; // eslint-disable-line no-unused-vars import * as ThemeSupport from '../theme_support/theme_support.js'; import * as UI from '../ui/ui.js'; import {NetworkNode} from './NetworkDataGridNode.js'; // eslint-disable-line no-unused-vars import {NetworkTimeCalculator} from './NetworkTimeCalculator.js'; // eslint-disable-line no-unused-vars import {RequestTimeRange, RequestTimeRangeNames, RequestTimingView} from './RequestTimingView.js'; // eslint-disable-line no-unused-vars export class NetworkWaterfallColumn extends UI.Widget.VBox { /** * @param {!NetworkTimeCalculator} calculator */ constructor(calculator) { // TODO(allada) Make this a shadowDOM when the NetworkWaterfallColumn gets moved into NetworkLogViewColumns. super(false); this.registerRequiredCSS('network/networkWaterfallColumn.css', {enableLegacyPatching: false}); /** @type {!HTMLCanvasElement} */ this._canvas = /** @type {!HTMLCanvasElement} */ (this.contentElement.createChild('canvas')); this._canvas.tabIndex = -1; this.setDefaultFocusedElement(this._canvas); this._canvasPosition = this._canvas.getBoundingClientRect(); /** @const */ this._leftPadding = 5; /** @const */ this._fontSize = 10; this._rightPadding = 0; this._scrollTop = 0; this._headerHeight = 0; this._calculator = calculator; // this._rawRowHeight captures model height (41 or 21px), // this._rowHeight is computed height of the row in CSS pixels, can be 20.8 for zoomed-in content. this._rawRowHeight = 0; 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)); this._popoverHelper.setHasPadding(true); this._popoverHelper.setTimeout(300, 300); /** @type {!Array<!NetworkNode>} */ this._nodes = []; /** @type {?NetworkNode} */ this._hoveredNode = null; /** @type {!Map<string, !Array<number>>} */ this._eventDividers = new Map(); /** @type {(number|undefined)} */ this._updateRequestID; 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(); /** @type {!Map<!Common.ResourceType.ResourceType, !_LayerStyle>} */ this._styleForWaitingResourceType = resourceStyleTuple[0]; /** @type {!Map<!Common.ResourceType.ResourceType, !_LayerStyle>} */ this._styleForDownloadingResourceType = resourceStyleTuple[1]; const baseLineColor = ThemeSupport.ThemeSupport.instance().patchColorText('#a5a5a5', ThemeSupport.ThemeSupport.ColorUsage.Foreground); /** @type {!_LayerStyle} */ this._wiskerStyle = {borderColor: baseLineColor, lineWidth: 1, fillStyle: undefined}; /** @type {!_LayerStyle} */ this._hoverDetailsStyle = {fillStyle: baseLineColor, lineWidth: 1, borderColor: baseLineColor}; /** @type {!Map<!_LayerStyle, !Path2D>} */ this._pathForStyle = new Map(); /** @type {!Array<!_TextLayer>} */ this._textLayers = []; } /** * @return {!Map<!RequestTimeRangeNames, !_LayerStyle>} */ static _buildRequestTimeRangeStyle() { const types = RequestTimeRangeNames; const styleMap = new Map(); styleMap.set(types.Connecting, {fillStyle: '#FF9800'}); styleMap.set(types.SSL, {fillStyle: '#9C27B0'}); styleMap.set(types.DNS, {fillStyle: '#009688'}); styleMap.set(types.Proxy, {fillStyle: '#A1887F'}); styleMap.set(types.Blocking, {fillStyle: '#AAAAAA'}); styleMap.set(types.Push, {fillStyle: '#8CDBff'}); styleMap.set(types.Queueing, {fillStyle: 'white', lineWidth: 2, borderColor: 'lightgrey'}); // This ensures we always show at least 2 px for a request. styleMap.set(types.Receiving, {fillStyle: '#03A9F4', lineWidth: 2, borderColor: '#03A9F4'}); styleMap.set(types.Waiting, {fillStyle: '#00C853'}); styleMap.set(types.ReceivingPush, {fillStyle: '#03A9F4'}); styleMap.set(types.ServiceWorker, {fillStyle: 'orange'}); styleMap.set(types.ServiceWorkerPreparation, {fillStyle: 'orange'}); styleMap.set(types.ServiceWorkerRespondWith, {fillStyle: '#A8A3FF'}); return styleMap; } /** * @return {!Array<!Map<!Common.ResourceType.ResourceType, !_LayerStyle>>} */ static _buildResourceTypeStyle() { 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(); const downloadingStyleMap = new Map(); for (const resourceType of Object.values(Common.ResourceType.resourceTypes)) { let color = baseResourceTypeColors.get(resourceType.name()); if (!color) { color = baseResourceTypeColors.get('other'); } const borderColor = toBorderColor(/** @type {string} */ (color)); waitingStyleMap.set( resourceType, {fillStyle: toWaitingColor(/** @type {string} */ (color)), lineWidth: 1, borderColor: borderColor}); downloadingStyleMap.set(resourceType, {fillStyle: color, lineWidth: 1, borderColor: borderColor}); } return [waitingStyleMap, downloadingStyleMap]; /** * @param {string} color */ function toBorderColor(color) { const parsedColor = Common.Color.Color.parse(color); if (!parsedColor) { return ''; } const hsla = parsedColor.hsla(); hsla[1] /= 2; hsla[2] -= Math.min(hsla[2], 0.2); return parsedColor.asString(null); } /** * @param {string} color */ function toWaitingColor(color) { const parsedColor = Common.Color.Color.parse(color); if (!parsedColor) { return ''; } const hsla = parsedColor.hsla(); hsla[2] *= 1.1; return parsedColor.asString(null); } } _resetPaths() { 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() { this._popoverHelper.hidePopover(); } /** * @override */ wasShown() { this.update(); } /** * @param {!Event} event */ _onMouseMove(/** @type {!MouseEvent} */ event) { this._setHoveredNode(this.getNodeFromPoint(event.offsetX, event.offsetY), event.shiftKey); } /** * @param {!Event} event */ _onClick(/** @type {!MouseEvent} */ event) { const handled = this._setSelectedNode(this.getNodeFromPoint(event.offsetX, event.offsetY)); if (handled) { event.consume(true); } } /** * @param {!MouseEvent} event * @return {?UI.PopoverHelper.PopoverRequest} */ _getPopoverRequest(event) { if (!this._hoveredNode) { return null; } const request = this._hoveredNode.request(); if (!request) { return null; } const useTimingBars = !Common.Settings.Settings.instance().moduleSetting('networkColorCodeResourceTypes').get() && !this._calculator.startAtZero; let range; let start; let end; if (useTimingBars) { range = RequestTimingView.calculateRequestTimeRanges(request, 0) .find(data => data.name === RequestTimeRangeNames.Total); start = this._timeToPosition(/** @type {!RequestTimeRange} */ (range).start); end = this._timeToPosition(/** @type {!RequestTimeRange} */ (range).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(/** @type {!RequestTimeRange} */ (range).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, /** @param {!UI.GlassPane.GlassPane} popover */ show: popover => { const content = RequestTimingView.createTimingTable( /** @type {!SDK.NetworkRequest.NetworkRequest} */ (request), this._calculator); popover.contentElement.appendChild(content); return Promise.resolve(true); }, hide: undefined }; } /** * @param {?NetworkNode} node * @param {boolean} highlightInitiatorChain */ _setHoveredNode(node, highlightInitiatorChain) { if (this._hoveredNode) { this._hoveredNode.setHovered(false, false); } this._hoveredNode = node; if (this._hoveredNode) { this._hoveredNode.setHovered(true, highlightInitiatorChain); } } /** * @param {?NetworkNode} node * @returns {boolean} */ _setSelectedNode(node) { if (node && node.dataGrid) { node.select(); node.dataGrid.element.focus(); return true; } return false; } /** * @param {number} height */ setRowHeight(height) { this._rawRowHeight = height; this._updateRowHeight(); } _updateRowHeight() { this._rowHeight = Math.round(this._rawRowHeight * window.devicePixelRatio) / window.devicePixelRatio; } /** * @param {number} height */ setHeaderHeight(height) { this._headerHeight = height; } /** * @param {number} padding */ setRightPadding(padding) { this._rightPadding = padding; this._calculateCanvasSize(); } /** * @param {!NetworkTimeCalculator} calculator */ setCalculator(calculator) { this._calculator = calculator; } /** * @param {number} x * @param {number} y * @return {?NetworkNode} */ getNodeFromPoint(x, y) { if (y <= this._headerHeight) { return null; } return this._nodes[Math.floor((this._scrollTop + y - this._headerHeight) / this._rowHeight)]; } scheduleDraw() { if (this._updateRequestID) { return; } this._updateRequestID = this.element.window().requestAnimationFrame(() => this.update()); } /** * @param {number=} scrollTop * @param {!Map<string, !Array<number>>=} eventDividers * @param {!Array<!NetworkNode>=} nodes */ update(scrollTop, eventDividers, nodes) { 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; } if (this._updateRequestID) { this.element.window().cancelAnimationFrame(this._updateRequestID); delete this._updateRequestID; } this._startTime = this._calculator.minimumBoundary(); this._endTime = this._calculator.maximumBoundary(); this._resetCanvas(); this._resetPaths(); this._textLayers = []; this._draw(); } _resetCanvas() { 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() { super.onResize(); this._updateRowHeight(); this._calculateCanvasSize(); this.scheduleDraw(); } _calculateCanvasSize() { this._offsetWidth = this.contentElement.offsetWidth - this._rightPadding; this._offsetHeight = this.contentElement.offsetHeight; this._calculator.setDisplayWidth(this._offsetWidth); this._canvasPosition = this._canvas.getBoundingClientRect(); } /** * @param {number} time * @return {number} */ _timeToPosition(time) { const availableWidth = this._offsetWidth - this._leftPadding; const timeToPixel = availableWidth / (this._endTime - this._startTime); return Math.floor(this._leftPadding + (time - this._startTime) * timeToPixel); } _didDrawForTest() { } _draw() { const useTimingBars = !Common.Settings.Settings.instance().moduleSetting('networkColorCodeResourceTypes').get() && !this._calculator.startAtZero; const nodes = this._nodes; /** @type {?CanvasRenderingContext2D} */ const context = /** @type {?CanvasRenderingContext2D} */ (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); /** @type {!Array<!NetworkNode>} */ let drawNodes = []; if (node.hasChildren() && !node.expanded) { drawNodes = /** @type {!Array<!NetworkNode>} */ (node.flatChildren()); } 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); context.save(); context.fillStyle = ThemeSupport.ThemeSupport.instance().patchColorText('#888', ThemeSupport.ThemeSupport.ColorUsage.Foreground); 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.clearRect(this._offsetWidth - freeZoneAtRight, 0, freeZoneAtRight, this._headerHeight); this._didDrawForTest(); } /** * @param {!CanvasRenderingContext2D} context */ _drawLayers(context) { for (const entry of this._pathForStyle) { const style = /** @type {!_LayerStyle} */ (entry[0]); const path = /** @type {!Path2D} */ (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 = style.fillStyle; context.fill(path); } context.restore(); } } /** * @param {!CanvasRenderingContext2D} context */ _drawEventDividers(context) { 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(); } /** * @param {!RequestTimeRangeNames=} type * @return {number} */ _getBarHeight(type) { const types = RequestTimeRangeNames; switch (type) { case types.Connecting: case types.SSL: case types.DNS: case types.Proxy: case types.Blocking: case types.Push: case types.Queueing: return 7; default: return 13; } } /** * @param {!SDK.NetworkRequest.NetworkRequest} request * @param {number} borderOffset * @return {!{start: number, mid: number, end: number}} */ _getSimplifiedBarRange(request, borderOffset) { 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 }; } /** * @param {!CanvasRenderingContext2D} context * @param {!NetworkNode} node * @param {number} y */ _buildSimplifiedBarLayers(context, node, y) { 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 = /** @type {!_LayerStyle} */ (this._styleForWaitingResourceType.get(request.resourceType())); const waitingPath = /** @type {!Path2D} */ (this._pathForStyle.get(waitingStyle)); waitingPath.rect(ranges.start, y, ranges.mid - ranges.start, height - borderWidth); const barWidth = Math.max(2, ranges.end - ranges.mid); const downloadingStyle = /** @type {!_LayerStyle} */ (this._styleForDownloadingResourceType.get(request.resourceType())); const downloadingPath = /** @type {!Path2D} */ (this._pathForStyle.get(downloadingStyle)); downloadingPath.rect(ranges.mid, y, barWidth, height - borderWidth); /** @type {?{left: string, right: string, tooltip: (string|undefined)}} */ let labels = 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 = /** @type {!Path2D} */ (this._pathForStyle.get(this._hoverDetailsStyle)); 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 = /** @type {!RequestTimeRange} */ (RequestTimingView.calculateRequestTimeRanges(request, 0) .find(data => data.name === RequestTimeRangeNames.Total)); 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 = /** @type {!Path2D} */ (this._pathForStyle.get(this._wiskerStyle)); 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); } } } /** * @param {!NetworkNode} node * @param {number} y */ _buildTimingBarLayers(node, y) { const request = node.request(); if (!request) { return; } const ranges = RequestTimingView.calculateRequestTimeRanges(request, 0); for (const range of ranges) { if (range.name === RequestTimeRangeNames.Total || range.name === RequestTimeRangeNames.Sending || range.end - range.start === 0) { continue; } const style = /** @type {!_LayerStyle} */ (this._styleForTimeRangeName.get(range.name)); const path = /** @type {!Path2D} */ (this._pathForStyle.get(style)); 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, middleBarY, end - start, height - lineWidth); } } /** * @param {!CanvasRenderingContext2D} context * @param {!NetworkNode} node * @param {number} y */ _decorateRow(context, node, y) { 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(); } } /** @typedef {!{x: number, y: number, text: string}} */ // @ts-ignore typedef export let _TextLayer; /** @typedef {!{fillStyle: (string|undefined), lineWidth: (number|undefined), borderColor: (string|undefined)}} */ // @ts-ignore typedef export let _LayerStyle;