| <!DOCTYPE html> |
| <!-- |
| Copyright (c) 2013 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. |
| --> |
| |
| <link rel="stylesheet" href="/ui/extras/chrome/cc/picture_ops_chart_view.css"> |
| |
| <link rel="import" href="/ui/base/dom_helpers.html"> |
| |
| <script> |
| 'use strict'; |
| |
| tr.exportTo('tr.ui.e.chrome.cc', function() { |
| var BAR_PADDING = 1; |
| var BAR_WIDTH = 5; |
| var CHART_PADDING_LEFT = 65; |
| var CHART_PADDING_RIGHT = 30; |
| var CHART_PADDING_BOTTOM = 35; |
| var CHART_PADDING_TOP = 20; |
| var AXIS_PADDING_LEFT = 55; |
| var AXIS_PADDING_RIGHT = 30; |
| var AXIS_PADDING_BOTTOM = 35; |
| var AXIS_PADDING_TOP = 20; |
| var AXIS_TICK_SIZE = 5; |
| var AXIS_LABEL_PADDING = 5; |
| var VERTICAL_TICKS = 5; |
| var HUE_CHAR_CODE_ADJUSTMENT = 5.7; |
| |
| /** |
| * Provides a chart showing the cumulative time spent in Skia operations |
| * during picture rasterization. |
| * |
| * @constructor |
| */ |
| var PictureOpsChartView = |
| tr.ui.b.define('tr-ui-e-chrome-cc-picture-ops-chart-view'); |
| |
| PictureOpsChartView.prototype = { |
| __proto__: HTMLUnknownElement.prototype, |
| |
| decorate: function() { |
| this.picture_ = undefined; |
| this.pictureOps_ = undefined; |
| this.opCosts_ = undefined; |
| |
| this.chartScale_ = window.devicePixelRatio; |
| |
| this.chart_ = document.createElement('canvas'); |
| this.chartCtx_ = this.chart_.getContext('2d'); |
| this.appendChild(this.chart_); |
| |
| this.selectedOpIndex_ = undefined; |
| this.chartWidth_ = 0; |
| this.chartHeight_ = 0; |
| this.dimensionsHaveChanged_ = true; |
| |
| this.currentBarMouseOverTarget_ = undefined; |
| |
| this.ninetyFifthPercentileCost_ = 0; |
| this.totalOpCost_ = 0; |
| |
| this.chart_.addEventListener('click', this.onClick_.bind(this)); |
| this.chart_.addEventListener('mousemove', this.onMouseMove_.bind(this)); |
| |
| this.usePercentileScale_ = false; |
| this.usePercentileScaleCheckbox_ = tr.ui.b.createCheckBox( |
| this, 'usePercentileScale', |
| 'PictureOpsChartView.usePercentileScale', false, |
| 'Limit to 95%-ile'); |
| this.usePercentileScaleCheckbox_.classList.add('use-percentile-scale'); |
| this.appendChild(this.usePercentileScaleCheckbox_); |
| }, |
| |
| get dimensionsHaveChanged() { |
| return this.dimensionsHaveChanged_; |
| }, |
| |
| set dimensionsHaveChanged(dimensionsHaveChanged) { |
| this.dimensionsHaveChanged_ = dimensionsHaveChanged; |
| }, |
| |
| get usePercentileScale() { |
| return this.usePercentileScale_; |
| }, |
| |
| set usePercentileScale(usePercentileScale) { |
| this.usePercentileScale_ = usePercentileScale; |
| this.drawChartContents_(); |
| }, |
| |
| get numOps() { |
| return this.opCosts_.length; |
| }, |
| |
| get selectedOpIndex() { |
| return this.selectedOpIndex_; |
| }, |
| |
| set selectedOpIndex(selectedOpIndex) { |
| if (selectedOpIndex < 0) throw new Error('Invalid index'); |
| if (selectedOpIndex >= this.numOps) throw new Error('Invalid index'); |
| |
| this.selectedOpIndex_ = selectedOpIndex; |
| }, |
| |
| get picture() { |
| return this.picture_; |
| }, |
| |
| set picture(picture) { |
| this.picture_ = picture; |
| this.pictureOps_ = picture.tagOpsWithTimings(picture.getOps()); |
| this.currentBarMouseOverTarget_ = undefined; |
| this.processPictureData_(); |
| this.dimensionsHaveChanged = true; |
| }, |
| |
| processPictureData_: function() { |
| if (this.pictureOps_ === undefined) |
| return; |
| |
| var totalOpCost = 0; |
| |
| // Take a copy of the picture ops data for sorting. |
| this.opCosts_ = this.pictureOps_.map(function(op) { |
| totalOpCost += op.cmd_time; |
| return op.cmd_time; |
| }); |
| this.opCosts_.sort(); |
| |
| var ninetyFifthPercentileCostIndex = Math.floor( |
| this.opCosts_.length * 0.95); |
| this.ninetyFifthPercentileCost_ = |
| this.opCosts_[ninetyFifthPercentileCostIndex]; |
| this.maxCost_ = this.opCosts_[this.opCosts_.length - 1]; |
| |
| this.totalOpCost_ = totalOpCost; |
| }, |
| |
| extractBarIndex_: function(e) { |
| |
| var index = undefined; |
| |
| if (this.pictureOps_ === undefined || |
| this.pictureOps_.length === 0) |
| return index; |
| |
| var x = e.offsetX; |
| var y = e.offsetY; |
| |
| var totalBarWidth = (BAR_WIDTH + BAR_PADDING) * this.pictureOps_.length; |
| |
| var chartLeft = CHART_PADDING_LEFT; |
| var chartTop = 0; |
| var chartBottom = this.chartHeight_ - CHART_PADDING_BOTTOM; |
| var chartRight = chartLeft + totalBarWidth; |
| |
| if (x < chartLeft || x > chartRight || y < chartTop || y > chartBottom) |
| return index; |
| |
| index = Math.floor((x - chartLeft) / totalBarWidth * |
| this.pictureOps_.length); |
| |
| index = tr.b.clamp(index, 0, this.pictureOps_.length - 1); |
| |
| return index; |
| }, |
| |
| onClick_: function(e) { |
| |
| var barClicked = this.extractBarIndex_(e); |
| |
| if (barClicked === undefined) |
| return; |
| |
| // If we click on the already selected item we should deselect. |
| if (barClicked === this.selectedOpIndex) |
| this.selectedOpIndex = undefined; |
| else |
| this.selectedOpIndex = barClicked; |
| |
| e.preventDefault(); |
| |
| tr.b.dispatchSimpleEvent(this, 'selection-changed', false); |
| }, |
| |
| onMouseMove_: function(e) { |
| |
| var lastBarMouseOverTarget = this.currentBarMouseOverTarget_; |
| this.currentBarMouseOverTarget_ = this.extractBarIndex_(e); |
| |
| if (this.currentBarMouseOverTarget_ === lastBarMouseOverTarget) |
| return; |
| |
| this.drawChartContents_(); |
| }, |
| |
| scrollSelectedItemIntoViewIfNecessary: function() { |
| |
| if (this.selectedOpIndex === undefined) |
| return; |
| |
| var width = this.offsetWidth; |
| var left = this.scrollLeft; |
| var right = left + width; |
| var targetLeft = CHART_PADDING_LEFT + |
| (BAR_WIDTH + BAR_PADDING) * this.selectedOpIndex; |
| |
| if (targetLeft > left && targetLeft < right) |
| return; |
| |
| this.scrollLeft = (targetLeft - width * 0.5); |
| }, |
| |
| updateChartContents: function() { |
| |
| if (this.dimensionsHaveChanged) |
| this.updateChartDimensions_(); |
| |
| this.drawChartContents_(); |
| }, |
| |
| updateChartDimensions_: function() { |
| |
| if (!this.pictureOps_) |
| return; |
| |
| var width = CHART_PADDING_LEFT + CHART_PADDING_RIGHT + |
| ((BAR_WIDTH + BAR_PADDING) * this.pictureOps_.length); |
| |
| if (width < this.offsetWidth) |
| width = this.offsetWidth; |
| |
| // Allow the element to be its natural size as set by flexbox, then lock |
| // the width in before we set the width of the canvas. |
| this.chartWidth_ = width; |
| this.chartHeight_ = this.getBoundingClientRect().height; |
| |
| // Scale up the canvas according to the devicePixelRatio, then reduce it |
| // down again via CSS. Finally we apply a scale to the canvas so that |
| // things are drawn at the correct size. |
| this.chart_.width = this.chartWidth_ * this.chartScale_; |
| this.chart_.height = this.chartHeight_ * this.chartScale_; |
| |
| this.chart_.style.width = this.chartWidth_ + 'px'; |
| this.chart_.style.height = this.chartHeight_ + 'px'; |
| |
| this.chartCtx_.scale(this.chartScale_, this.chartScale_); |
| |
| this.dimensionsHaveChanged = false; |
| }, |
| |
| drawChartContents_: function() { |
| |
| this.clearChartContents_(); |
| |
| if (this.pictureOps_ === undefined || |
| this.pictureOps_.length === 0 || |
| this.pictureOps_[0].cmd_time === undefined) { |
| |
| this.showNoTimingDataMessage_(); |
| return; |
| } |
| |
| this.drawSelection_(); |
| this.drawBars_(); |
| this.drawChartAxes_(); |
| this.drawLinesAtTickMarks_(); |
| this.drawLineAtBottomOfChart_(); |
| |
| if (this.currentBarMouseOverTarget_ === undefined) |
| return; |
| |
| this.drawTooltip_(); |
| }, |
| |
| drawSelection_: function() { |
| |
| if (this.selectedOpIndex === undefined) |
| return; |
| |
| var width = (BAR_WIDTH + BAR_PADDING) * this.selectedOpIndex; |
| this.chartCtx_.fillStyle = 'rgb(223, 235, 230)'; |
| this.chartCtx_.fillRect(CHART_PADDING_LEFT, CHART_PADDING_TOP, |
| width, this.chartHeight_ - CHART_PADDING_TOP - CHART_PADDING_BOTTOM); |
| }, |
| |
| drawChartAxes_: function() { |
| |
| var min = this.opCosts_[0]; |
| var max = this.opCosts_[this.opCosts_.length - 1]; |
| var height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM; |
| |
| var tickYInterval = height / (VERTICAL_TICKS - 1); |
| var tickYPosition = 0; |
| var tickValInterval = (max - min) / (VERTICAL_TICKS - 1); |
| var tickVal = 0; |
| |
| this.chartCtx_.fillStyle = '#333'; |
| this.chartCtx_.strokeStyle = '#777'; |
| this.chartCtx_.save(); |
| |
| // Translate half a pixel to avoid blurry lines. |
| this.chartCtx_.translate(0.5, 0.5); |
| |
| // Sides. |
| this.chartCtx_.beginPath(); |
| this.chartCtx_.moveTo(AXIS_PADDING_LEFT, AXIS_PADDING_TOP); |
| this.chartCtx_.lineTo(AXIS_PADDING_LEFT, this.chartHeight_ - |
| AXIS_PADDING_BOTTOM); |
| this.chartCtx_.lineTo(this.chartWidth_ - AXIS_PADDING_RIGHT, |
| this.chartHeight_ - AXIS_PADDING_BOTTOM); |
| this.chartCtx_.stroke(); |
| this.chartCtx_.closePath(); |
| |
| // Y-axis ticks. |
| this.chartCtx_.translate(AXIS_PADDING_LEFT, AXIS_PADDING_TOP); |
| |
| this.chartCtx_.font = '10px Arial'; |
| this.chartCtx_.textAlign = 'right'; |
| this.chartCtx_.textBaseline = 'middle'; |
| |
| this.chartCtx_.beginPath(); |
| for (var t = 0; t < VERTICAL_TICKS; t++) { |
| |
| tickYPosition = Math.round(t * tickYInterval); |
| tickVal = (max - t * tickValInterval).toFixed(4); |
| |
| this.chartCtx_.moveTo(0, tickYPosition); |
| this.chartCtx_.lineTo(-AXIS_TICK_SIZE, tickYPosition); |
| this.chartCtx_.fillText(tickVal, |
| -AXIS_TICK_SIZE - AXIS_LABEL_PADDING, tickYPosition); |
| |
| } |
| |
| this.chartCtx_.stroke(); |
| this.chartCtx_.closePath(); |
| |
| this.chartCtx_.restore(); |
| }, |
| |
| drawLinesAtTickMarks_: function() { |
| |
| var height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM; |
| var width = this.chartWidth_ - AXIS_PADDING_LEFT - AXIS_PADDING_RIGHT; |
| var tickYInterval = height / (VERTICAL_TICKS - 1); |
| var tickYPosition = 0; |
| |
| this.chartCtx_.save(); |
| |
| this.chartCtx_.translate(AXIS_PADDING_LEFT + 0.5, AXIS_PADDING_TOP + 0.5); |
| this.chartCtx_.beginPath(); |
| this.chartCtx_.strokeStyle = 'rgba(0,0,0,0.05)'; |
| |
| for (var t = 0; t < VERTICAL_TICKS; t++) { |
| tickYPosition = Math.round(t * tickYInterval); |
| |
| this.chartCtx_.moveTo(0, tickYPosition); |
| this.chartCtx_.lineTo(width, tickYPosition); |
| this.chartCtx_.stroke(); |
| } |
| |
| this.chartCtx_.restore(); |
| this.chartCtx_.closePath(); |
| }, |
| |
| drawLineAtBottomOfChart_: function() { |
| this.chartCtx_.strokeStyle = '#AAA'; |
| this.chartCtx_.beginPath(); |
| this.chartCtx_.moveTo(0, this.chartHeight_ - 0.5); |
| this.chartCtx_.lineTo(this.chartWidth_, this.chartHeight_ - 0.5); |
| this.chartCtx_.stroke(); |
| this.chartCtx_.closePath(); |
| }, |
| |
| drawTooltip_: function() { |
| |
| var tooltipData = this.pictureOps_[this.currentBarMouseOverTarget_]; |
| var tooltipTitle = tooltipData.cmd_string; |
| var tooltipTime = tooltipData.cmd_time.toFixed(4); |
| var toolTipTimePercentage = |
| ((tooltipData.cmd_time / this.totalOpCost_) * 100).toFixed(2); |
| |
| var tooltipWidth = 120; |
| var tooltipHeight = 40; |
| var chartInnerWidth = this.chartWidth_ - CHART_PADDING_RIGHT - |
| CHART_PADDING_LEFT; |
| var barWidth = BAR_WIDTH + BAR_PADDING; |
| var tooltipOffset = Math.round((tooltipWidth - barWidth) * 0.5); |
| |
| var left = CHART_PADDING_LEFT + this.currentBarMouseOverTarget_ * |
| barWidth - tooltipOffset; |
| var top = Math.round((this.chartHeight_ - tooltipHeight) * 0.5); |
| |
| this.chartCtx_.save(); |
| |
| this.chartCtx_.shadowOffsetX = 0; |
| this.chartCtx_.shadowOffsetY = 5; |
| this.chartCtx_.shadowBlur = 4; |
| this.chartCtx_.shadowColor = 'rgba(0,0,0,0.4)'; |
| |
| this.chartCtx_.strokeStyle = '#888'; |
| this.chartCtx_.fillStyle = '#EEE'; |
| this.chartCtx_.fillRect(left, top, tooltipWidth, tooltipHeight); |
| |
| this.chartCtx_.shadowColor = 'transparent'; |
| this.chartCtx_.translate(0.5, 0.5); |
| this.chartCtx_.strokeRect(left, top, tooltipWidth, tooltipHeight); |
| |
| this.chartCtx_.restore(); |
| |
| this.chartCtx_.fillStyle = '#222'; |
| this.chartCtx_.textAlign = 'left'; |
| this.chartCtx_.textBaseline = 'top'; |
| this.chartCtx_.font = '800 12px Arial'; |
| this.chartCtx_.fillText(tooltipTitle, left + 8, top + 8); |
| |
| this.chartCtx_.fillStyle = '#555'; |
| this.chartCtx_.font = '400 italic 10px Arial'; |
| this.chartCtx_.fillText(tooltipTime + 'ms (' + |
| toolTipTimePercentage + '%)', left + 8, top + 22); |
| }, |
| |
| drawBars_: function() { |
| |
| var op; |
| var opColor = 0; |
| var opHeight = 0; |
| var opWidth = BAR_WIDTH + BAR_PADDING; |
| var opHover = false; |
| |
| var bottom = this.chartHeight_ - CHART_PADDING_BOTTOM; |
| var maxHeight = this.chartHeight_ - CHART_PADDING_BOTTOM - |
| CHART_PADDING_TOP; |
| |
| var maxValue; |
| if (this.usePercentileScale) |
| maxValue = this.ninetyFifthPercentileCost_; |
| else |
| maxValue = this.maxCost_; |
| |
| for (var b = 0; b < this.pictureOps_.length; b++) { |
| |
| op = this.pictureOps_[b]; |
| opHeight = Math.round( |
| (op.cmd_time / maxValue) * maxHeight); |
| opHeight = Math.max(opHeight, 1); |
| opHover = (b === this.currentBarMouseOverTarget_); |
| opColor = this.getOpColor_(op.cmd_string, opHover); |
| |
| if (b === this.selectedOpIndex) |
| this.chartCtx_.fillStyle = '#FFFF00'; |
| else |
| this.chartCtx_.fillStyle = opColor; |
| |
| this.chartCtx_.fillRect(CHART_PADDING_LEFT + b * opWidth, |
| bottom - opHeight, BAR_WIDTH, opHeight); |
| } |
| |
| }, |
| |
| getOpColor_: function(opName, hover) { |
| |
| var characters = opName.split(''); |
| |
| var hue = characters.reduce(this.reduceNameToHue, 0) % 360; |
| var saturation = 30; |
| var lightness = hover ? '75%' : '50%'; |
| |
| return 'hsl(' + hue + ', ' + saturation + '%, ' + lightness + '%)'; |
| }, |
| |
| reduceNameToHue: function(previousValue, currentValue, index, array) { |
| // Get the char code and apply a magic adjustment value so we get |
| // pretty colors from around the rainbow. |
| return Math.round(previousValue + currentValue.charCodeAt(0) * |
| HUE_CHAR_CODE_ADJUSTMENT); |
| }, |
| |
| clearChartContents_: function() { |
| this.chartCtx_.clearRect(0, 0, this.chartWidth_, this.chartHeight_); |
| }, |
| |
| showNoTimingDataMessage_: function() { |
| this.chartCtx_.font = '800 italic 14px Arial'; |
| this.chartCtx_.fillStyle = '#333'; |
| this.chartCtx_.textAlign = 'center'; |
| this.chartCtx_.textBaseline = 'middle'; |
| this.chartCtx_.fillText('No timing data available.', |
| this.chartWidth_ * 0.5, this.chartHeight_ * 0.5); |
| } |
| }; |
| |
| return { |
| PictureOpsChartView: PictureOpsChartView |
| }; |
| }); |
| </script> |