blob: 23c68cae021a6a1ebce7bfc333392126780e7cea [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_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>