| <!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_summary_view.css"> |
| |
| <link rel="import" href="/ui/base/ui.html"> |
| |
| <script> |
| 'use strict'; |
| |
| tr.exportTo('tr.ui.e.chrome.cc', function() { |
| var OPS_TIMING_ITERATIONS = 3; |
| var CHART_PADDING_LEFT = 65; |
| var CHART_PADDING_RIGHT = 40; |
| var AXIS_PADDING_LEFT = 60; |
| var AXIS_PADDING_RIGHT = 35; |
| var AXIS_PADDING_TOP = 25; |
| var AXIS_PADDING_BOTTOM = 45; |
| var AXIS_LABEL_PADDING = 5; |
| var AXIS_TICK_SIZE = 10; |
| var LABEL_PADDING = 5; |
| var LABEL_INTERLEAVE_OFFSET = 15; |
| var BAR_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 PictureOpsChartSummaryView = tr.ui.b.define( |
| 'tr-ui-e-chrome-cc-picture-ops-chart-summary-view'); |
| |
| PictureOpsChartSummaryView.prototype = { |
| __proto__: HTMLUnknownElement.prototype, |
| |
| decorate: function() { |
| this.picture_ = undefined; |
| this.pictureDataProcessed_ = false; |
| |
| this.chartScale_ = window.devicePixelRatio; |
| |
| this.chart_ = document.createElement('canvas'); |
| this.chartCtx_ = this.chart_.getContext('2d'); |
| this.appendChild(this.chart_); |
| |
| this.opsTimingData_ = []; |
| |
| this.chartWidth_ = 0; |
| this.chartHeight_ = 0; |
| this.requiresRedraw_ = true; |
| |
| this.currentBarMouseOverTarget_ = null; |
| |
| this.chart_.addEventListener('mousemove', this.onMouseMove_.bind(this)); |
| }, |
| |
| get requiresRedraw() { |
| return this.requiresRedraw_; |
| }, |
| |
| set requiresRedraw(requiresRedraw) { |
| this.requiresRedraw_ = requiresRedraw; |
| }, |
| |
| get picture() { |
| return this.picture_; |
| }, |
| |
| set picture(picture) { |
| this.picture_ = picture; |
| this.pictureDataProcessed_ = false; |
| |
| if (this.classList.contains('hidden')) |
| return; |
| |
| this.processPictureData_(); |
| this.requiresRedraw = true; |
| this.updateChartContents(); |
| }, |
| |
| hide: function() { |
| this.classList.add('hidden'); |
| }, |
| |
| show: function() { |
| |
| this.classList.remove('hidden'); |
| |
| if (this.pictureDataProcessed_) |
| return; |
| |
| this.processPictureData_(); |
| this.requiresRedraw = true; |
| this.updateChartContents(); |
| |
| }, |
| |
| onMouseMove_: function(e) { |
| |
| var lastBarMouseOverTarget = this.currentBarMouseOverTarget_; |
| this.currentBarMouseOverTarget_ = null; |
| |
| var x = e.offsetX; |
| var y = e.offsetY; |
| |
| var chartLeft = CHART_PADDING_LEFT; |
| var chartRight = this.chartWidth_ - CHART_PADDING_RIGHT; |
| var chartTop = AXIS_PADDING_TOP; |
| var chartBottom = this.chartHeight_ - AXIS_PADDING_BOTTOM; |
| var chartInnerWidth = chartRight - chartLeft; |
| |
| if (x > chartLeft && x < chartRight && y > chartTop && y < chartBottom) { |
| |
| this.currentBarMouseOverTarget_ = Math.floor( |
| (x - chartLeft) / chartInnerWidth * this.opsTimingData_.length); |
| |
| this.currentBarMouseOverTarget_ = tr.b.clamp( |
| this.currentBarMouseOverTarget_, 0, this.opsTimingData_.length - 1); |
| |
| } |
| |
| if (this.currentBarMouseOverTarget_ === lastBarMouseOverTarget) |
| return; |
| |
| this.drawChartContents_(); |
| }, |
| |
| updateChartContents: function() { |
| |
| if (this.requiresRedraw) |
| this.updateChartDimensions_(); |
| |
| this.drawChartContents_(); |
| }, |
| |
| updateChartDimensions_: function() { |
| this.chartWidth_ = this.offsetWidth; |
| this.chartHeight_ = this.offsetHeight; |
| |
| // 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_); |
| }, |
| |
| processPictureData_: function() { |
| |
| this.resetOpsTimingData_(); |
| this.pictureDataProcessed_ = true; |
| |
| if (!this.picture_) |
| return; |
| |
| var ops = this.picture_.getOps(); |
| if (!ops) |
| return; |
| |
| ops = this.picture_.tagOpsWithTimings(ops); |
| |
| // Check that there are valid times. |
| if (ops[0].cmd_time === undefined) |
| return; |
| |
| this.collapseOpsToTimingBuckets_(ops); |
| }, |
| |
| drawChartContents_: function() { |
| |
| this.clearChartContents_(); |
| |
| if (this.opsTimingData_.length === 0) { |
| this.showNoTimingDataMessage_(); |
| return; |
| } |
| |
| this.drawChartAxes_(); |
| this.drawBars_(); |
| this.drawLineAtBottomOfChart_(); |
| |
| if (this.currentBarMouseOverTarget_ === null) |
| return; |
| |
| this.drawTooltip_(); |
| }, |
| |
| drawLineAtBottomOfChart_: function() { |
| this.chartCtx_.strokeStyle = '#AAA'; |
| this.chartCtx_.moveTo(0, this.chartHeight_ - 0.5); |
| this.chartCtx_.lineTo(this.chartWidth_, this.chartHeight_ - 0.5); |
| this.chartCtx_.stroke(); |
| }, |
| |
| drawTooltip_: function() { |
| |
| var tooltipData = this.opsTimingData_[this.currentBarMouseOverTarget_]; |
| var tooltipTitle = tooltipData.cmd_string; |
| var tooltipTime = tooltipData.cmd_time.toFixed(4); |
| |
| var tooltipWidth = 110; |
| var tooltipHeight = 40; |
| var chartInnerWidth = this.chartWidth_ - CHART_PADDING_RIGHT - |
| CHART_PADDING_LEFT; |
| var barWidth = chartInnerWidth / this.opsTimingData_.length; |
| 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_.textBaseline = 'top'; |
| this.chartCtx_.font = '800 12px Arial'; |
| this.chartCtx_.fillText(tooltipTitle, left + 8, top + 8); |
| |
| this.chartCtx_.fillStyle = '#555'; |
| this.chartCtx_.textBaseline = 'top'; |
| this.chartCtx_.font = '400 italic 10px Arial'; |
| this.chartCtx_.fillText('Total: ' + tooltipTime + 'ms', |
| left + 8, top + 22); |
| }, |
| |
| drawBars_: function() { |
| |
| var len = this.opsTimingData_.length; |
| var max = this.opsTimingData_[0].cmd_time; |
| var min = this.opsTimingData_[len - 1].cmd_time; |
| |
| var width = this.chartWidth_ - CHART_PADDING_LEFT - CHART_PADDING_RIGHT; |
| var height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM; |
| var barWidth = Math.floor(width / len); |
| |
| var opData; |
| var opTiming; |
| var opHeight; |
| var opLabel; |
| var barLeft; |
| |
| for (var b = 0; b < len; b++) { |
| |
| opData = this.opsTimingData_[b]; |
| opTiming = opData.cmd_time / max; |
| |
| opHeight = Math.round(Math.max(1, opTiming * height)); |
| opLabel = opData.cmd_string; |
| barLeft = CHART_PADDING_LEFT + b * barWidth; |
| |
| this.chartCtx_.fillStyle = this.getOpColor_(opLabel); |
| |
| this.chartCtx_.fillRect(barLeft + BAR_PADDING, AXIS_PADDING_TOP + |
| height - opHeight, barWidth - 2 * BAR_PADDING, opHeight); |
| } |
| |
| }, |
| |
| getOpColor_: function(opName) { |
| |
| var characters = opName.split(''); |
| var hue = characters.reduce(this.reduceNameToHue, 0) % 360; |
| |
| return 'hsl(' + hue + ', 30%, 50%)'; |
| }, |
| |
| 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); |
| }, |
| |
| drawChartAxes_: function() { |
| |
| var len = this.opsTimingData_.length; |
| var max = this.opsTimingData_[0].cmd_time; |
| var min = this.opsTimingData_[len - 1].cmd_time; |
| |
| var width = this.chartWidth_ - AXIS_PADDING_LEFT - AXIS_PADDING_RIGHT; |
| var height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM; |
| |
| var totalBarWidth = this.chartWidth_ - CHART_PADDING_LEFT - |
| CHART_PADDING_RIGHT; |
| var barWidth = Math.floor(totalBarWidth / len); |
| 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_.save(); |
| |
| this.chartCtx_.translate(AXIS_PADDING_LEFT, AXIS_PADDING_TOP); |
| this.chartCtx_.moveTo(0, 0); |
| this.chartCtx_.lineTo(0, height); |
| this.chartCtx_.lineTo(width, height); |
| |
| // Y-axis ticks. |
| this.chartCtx_.font = '10px Arial'; |
| this.chartCtx_.textAlign = 'right'; |
| this.chartCtx_.textBaseline = 'middle'; |
| |
| 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_.restore(); |
| |
| |
| // Labels. |
| |
| this.chartCtx_.save(); |
| |
| this.chartCtx_.translate(CHART_PADDING_LEFT + Math.round(barWidth * 0.5), |
| AXIS_PADDING_TOP + height + LABEL_PADDING); |
| |
| this.chartCtx_.font = '10px Arial'; |
| this.chartCtx_.textAlign = 'center'; |
| this.chartCtx_.textBaseline = 'top'; |
| |
| var labelTickLeft; |
| var labelTickBottom; |
| for (var l = 0; l < len; l++) { |
| |
| labelTickLeft = Math.round(l * barWidth); |
| labelTickBottom = l % 2 * LABEL_INTERLEAVE_OFFSET; |
| |
| this.chartCtx_.save(); |
| this.chartCtx_.moveTo(labelTickLeft, -LABEL_PADDING); |
| this.chartCtx_.lineTo(labelTickLeft, labelTickBottom); |
| this.chartCtx_.stroke(); |
| this.chartCtx_.restore(); |
| |
| this.chartCtx_.fillText(this.opsTimingData_[l].cmd_string, |
| labelTickLeft, labelTickBottom); |
| } |
| |
| this.chartCtx_.restore(); |
| |
| this.chartCtx_.restore(); |
| }, |
| |
| 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); |
| }, |
| |
| collapseOpsToTimingBuckets_: function(ops) { |
| |
| var opsTimingDataIndexHash_ = {}; |
| var timingData = this.opsTimingData_; |
| var op; |
| var opIndex; |
| |
| for (var i = 0; i < ops.length; i++) { |
| |
| op = ops[i]; |
| |
| if (op.cmd_time === undefined) |
| continue; |
| |
| // Try to locate the entry for the current operation |
| // based on its name. If that fails, then create one for it. |
| opIndex = opsTimingDataIndexHash_[op.cmd_string] || null; |
| |
| if (opIndex === null) { |
| timingData.push({ |
| cmd_time: 0, |
| cmd_string: op.cmd_string |
| }); |
| |
| opIndex = timingData.length - 1; |
| opsTimingDataIndexHash_[op.cmd_string] = opIndex; |
| } |
| |
| timingData[opIndex].cmd_time += op.cmd_time; |
| |
| } |
| |
| timingData.sort(this.sortTimingBucketsByOpTimeDescending_); |
| |
| this.collapseTimingBucketsToOther_(4); |
| }, |
| |
| collapseTimingBucketsToOther_: function(count) { |
| |
| var timingData = this.opsTimingData_; |
| var otherSource = timingData.splice(count, timingData.length - count); |
| var otherDestination = null; |
| |
| if (!otherSource.length) |
| return; |
| |
| timingData.push({ |
| cmd_time: 0, |
| cmd_string: 'Other' |
| }); |
| |
| otherDestination = timingData[timingData.length - 1]; |
| for (var i = 0; i < otherSource.length; i++) { |
| otherDestination.cmd_time += otherSource[i].cmd_time; |
| } |
| }, |
| |
| sortTimingBucketsByOpTimeDescending_: function(a, b) { |
| return b.cmd_time - a.cmd_time; |
| }, |
| |
| resetOpsTimingData_: function() { |
| this.opsTimingData_.length = 0; |
| } |
| }; |
| |
| return { |
| PictureOpsChartSummaryView: PictureOpsChartSummaryView |
| }; |
| }); |
| </script> |