blob: 487365bd7ed9d054346f8599a358706632e3c78d [file] [log] [blame]
<!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>