1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
7 tvcm.requireStylesheet('cc.picture_ops_chart_view');
8 tvcm.require('tvcm.ui.dom_helpers');
10 tvcm.exportTo('cc', function() {
14 var CHART_PADDING_LEFT = 65;
15 var CHART_PADDING_RIGHT = 30;
16 var CHART_PADDING_BOTTOM = 35;
17 var CHART_PADDING_TOP = 20;
18 var AXIS_PADDING_LEFT = 55;
19 var AXIS_PADDING_RIGHT = 30;
20 var AXIS_PADDING_BOTTOM = 35;
21 var AXIS_PADDING_TOP = 20;
22 var AXIS_TICK_SIZE = 5;
23 var AXIS_LABEL_PADDING = 5;
24 var VERTICAL_TICKS = 5;
25 var HUE_CHAR_CODE_ADJUSTMENT = 5.7;
28 * Provides a chart showing the cumulative time spent in Skia operations
29 * during picture rasterization.
33 var PictureOpsChartView = tvcm.ui.define('picture-ops-chart-view');
35 PictureOpsChartView.prototype = {
36 __proto__: HTMLUnknownElement.prototype,
38 decorate: function() {
39 this.picture_ = undefined;
40 this.pictureOps_ = undefined;
41 this.opCosts_ = undefined;
43 this.chartScale_ = window.devicePixelRatio;
45 this.chart_ = document.createElement('canvas');
46 this.chartCtx_ = this.chart_.getContext('2d');
47 this.appendChild(this.chart_);
49 this.selectedOpIndex_ = undefined;
51 this.chartHeight_ = 0;
52 this.dimensionsHaveChanged_ = true;
54 this.currentBarMouseOverTarget_ = undefined;
56 this.ninetyFifthPercentileCost_ = 0;
57 this.totalOpCost_ = 0;
59 this.chart_.addEventListener('click', this.onClick_.bind(this));
60 this.chart_.addEventListener('mousemove', this.onMouseMove_.bind(this));
62 this.usePercentileScale_ = false;
63 this.usePercentileScaleCheckbox_ = tvcm.ui.createCheckBox(
64 this, 'usePercentileScale',
65 'PictureOpsChartView.usePercentileScale', false,
67 this.usePercentileScaleCheckbox_.classList.add('use-percentile-scale');
68 this.appendChild(this.usePercentileScaleCheckbox_);
71 get dimensionsHaveChanged() {
72 return this.dimensionsHaveChanged_;
75 set dimensionsHaveChanged(dimensionsHaveChanged) {
76 this.dimensionsHaveChanged_ = dimensionsHaveChanged;
79 get usePercentileScale() {
80 return this.usePercentileScale_;
83 set usePercentileScale(usePercentileScale) {
84 this.usePercentileScale_ = usePercentileScale;
85 this.drawChartContents_();
89 return this.opCosts_.length;
92 get selectedOpIndex() {
93 return this.selectedOpIndex_;
96 set selectedOpIndex(selectedOpIndex) {
97 if (selectedOpIndex < 0) throw new Error('Invalid index');
98 if (selectedOpIndex >= this.numOps) throw new Error('Invalid index');
100 this.selectedOpIndex_ = selectedOpIndex;
104 return this.picture_;
107 set picture(picture) {
108 this.picture_ = picture;
109 this.pictureOps_ = picture.tagOpsWithTimings(picture.getOps());
110 this.currentBarMouseOverTarget_ = undefined;
111 this.processPictureData_();
112 this.dimensionsHaveChanged = true;
115 processPictureData_: function() {
116 if (this.pictureOps_ === undefined)
121 // Take a copy of the picture ops data for sorting.
122 this.opCosts_ = this.pictureOps_.map(function(op) {
123 totalOpCost += op.cmd_time;
126 this.opCosts_.sort();
128 var ninetyFifthPercentileCostIndex = Math.floor(
129 this.opCosts_.length * 0.95);
130 this.ninetyFifthPercentileCost_ =
131 this.opCosts_[ninetyFifthPercentileCostIndex];
132 this.maxCost_ = this.opCosts_[this.opCosts_.length - 1];
134 this.totalOpCost_ = totalOpCost;
137 extractBarIndex_: function(e) {
139 var index = undefined;
141 if (this.pictureOps_ === undefined ||
142 this.pictureOps_.length === 0)
148 var totalBarWidth = (BAR_WIDTH + BAR_PADDING) * this.pictureOps_.length;
150 var chartLeft = CHART_PADDING_LEFT;
152 var chartBottom = this.chartHeight_ - CHART_PADDING_BOTTOM;
153 var chartRight = chartLeft + totalBarWidth;
155 if (x < chartLeft || x > chartRight || y < chartTop || y > chartBottom)
158 index = Math.floor((x - chartLeft) / totalBarWidth *
159 this.pictureOps_.length);
161 index = tvcm.clamp(index, 0, this.pictureOps_.length - 1);
166 onClick_: function(e) {
168 var barClicked = this.extractBarIndex_(e);
170 if (barClicked === undefined)
173 // If we click on the already selected item we should deselect.
174 if (barClicked === this.selectedOpIndex)
175 this.selectedOpIndex = undefined;
177 this.selectedOpIndex = barClicked;
181 tvcm.dispatchSimpleEvent(this, 'selection-changed', false);
184 onMouseMove_: function(e) {
186 var lastBarMouseOverTarget = this.currentBarMouseOverTarget_;
187 this.currentBarMouseOverTarget_ = this.extractBarIndex_(e);
189 if (this.currentBarMouseOverTarget_ === lastBarMouseOverTarget)
192 this.drawChartContents_();
195 scrollSelectedItemIntoViewIfNecessary: function() {
197 if (this.selectedOpIndex === undefined)
200 var width = this.offsetWidth;
201 var left = this.scrollLeft;
202 var right = left + width;
203 var targetLeft = CHART_PADDING_LEFT +
204 (BAR_WIDTH + BAR_PADDING) * this.selectedOpIndex;
206 if (targetLeft > left && targetLeft < right)
209 this.scrollLeft = (targetLeft - width * 0.5);
212 updateChartContents: function() {
214 if (this.dimensionsHaveChanged)
215 this.updateChartDimensions_();
217 this.drawChartContents_();
220 updateChartDimensions_: function() {
222 if (!this.pictureOps_)
225 var width = CHART_PADDING_LEFT + CHART_PADDING_RIGHT +
226 ((BAR_WIDTH + BAR_PADDING) * this.pictureOps_.length);
228 if (width < this.offsetWidth)
229 width = this.offsetWidth;
231 // Allow the element to be its natural size as set by flexbox, then lock
232 // the width in before we set the width of the canvas.
233 this.chartWidth_ = width;
234 this.chartHeight_ = this.getBoundingClientRect().height;
236 // Scale up the canvas according to the devicePixelRatio, then reduce it
237 // down again via CSS. Finally we apply a scale to the canvas so that
238 // things are drawn at the correct size.
239 this.chart_.width = this.chartWidth_ * this.chartScale_;
240 this.chart_.height = this.chartHeight_ * this.chartScale_;
242 this.chart_.style.width = this.chartWidth_ + 'px';
243 this.chart_.style.height = this.chartHeight_ + 'px';
245 this.chartCtx_.scale(this.chartScale_, this.chartScale_);
247 this.dimensionsHaveChanged = false;
250 drawChartContents_: function() {
252 this.clearChartContents_();
254 if (this.pictureOps_ === undefined ||
255 this.pictureOps_.length === 0 ||
256 this.pictureOps_[0].cmd_time === undefined) {
258 this.showNoTimingDataMessage_();
262 this.drawSelection_();
264 this.drawChartAxes_();
265 this.drawLinesAtTickMarks_();
266 this.drawLineAtBottomOfChart_();
268 if (this.currentBarMouseOverTarget_ === undefined)
274 drawSelection_: function() {
276 if (this.selectedOpIndex === undefined)
279 var width = (BAR_WIDTH + BAR_PADDING) * this.selectedOpIndex;
280 this.chartCtx_.fillStyle = 'rgb(223, 235, 230)';
281 this.chartCtx_.fillRect(CHART_PADDING_LEFT, CHART_PADDING_TOP,
282 width, this.chartHeight_ - CHART_PADDING_TOP - CHART_PADDING_BOTTOM);
285 drawChartAxes_: function() {
287 var min = this.opCosts_[0];
288 var max = this.opCosts_[this.opCosts_.length - 1];
289 var height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM;
291 var tickYInterval = height / (VERTICAL_TICKS - 1);
292 var tickYPosition = 0;
293 var tickValInterval = (max - min) / (VERTICAL_TICKS - 1);
296 this.chartCtx_.fillStyle = '#333';
297 this.chartCtx_.strokeStyle = '#777';
298 this.chartCtx_.save();
300 // Translate half a pixel to avoid blurry lines.
301 this.chartCtx_.translate(0.5, 0.5);
304 this.chartCtx_.beginPath();
305 this.chartCtx_.moveTo(AXIS_PADDING_LEFT, AXIS_PADDING_TOP);
306 this.chartCtx_.lineTo(AXIS_PADDING_LEFT, this.chartHeight_ -
307 AXIS_PADDING_BOTTOM);
308 this.chartCtx_.lineTo(this.chartWidth_ - AXIS_PADDING_RIGHT,
309 this.chartHeight_ - AXIS_PADDING_BOTTOM);
310 this.chartCtx_.stroke();
311 this.chartCtx_.closePath();
314 this.chartCtx_.translate(AXIS_PADDING_LEFT, AXIS_PADDING_TOP);
316 this.chartCtx_.font = '10px Arial';
317 this.chartCtx_.textAlign = 'right';
318 this.chartCtx_.textBaseline = 'middle';
320 this.chartCtx_.beginPath();
321 for (var t = 0; t < VERTICAL_TICKS; t++) {
323 tickYPosition = Math.round(t * tickYInterval);
324 tickVal = (max - t * tickValInterval).toFixed(4);
326 this.chartCtx_.moveTo(0, tickYPosition);
327 this.chartCtx_.lineTo(-AXIS_TICK_SIZE, tickYPosition);
328 this.chartCtx_.fillText(tickVal,
329 -AXIS_TICK_SIZE - AXIS_LABEL_PADDING, tickYPosition);
333 this.chartCtx_.stroke();
334 this.chartCtx_.closePath();
336 this.chartCtx_.restore();
339 drawLinesAtTickMarks_: function() {
341 var height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM;
342 var width = this.chartWidth_ - AXIS_PADDING_LEFT - AXIS_PADDING_RIGHT;
343 var tickYInterval = height / (VERTICAL_TICKS - 1);
344 var tickYPosition = 0;
346 this.chartCtx_.save();
348 this.chartCtx_.translate(AXIS_PADDING_LEFT + 0.5, AXIS_PADDING_TOP + 0.5);
349 this.chartCtx_.beginPath();
350 this.chartCtx_.strokeStyle = 'rgba(0,0,0,0.05)';
352 for (var t = 0; t < VERTICAL_TICKS; t++) {
353 tickYPosition = Math.round(t * tickYInterval);
355 this.chartCtx_.moveTo(0, tickYPosition);
356 this.chartCtx_.lineTo(width, tickYPosition);
357 this.chartCtx_.stroke();
360 this.chartCtx_.restore();
361 this.chartCtx_.closePath();
364 drawLineAtBottomOfChart_: function() {
365 this.chartCtx_.strokeStyle = '#AAA';
366 this.chartCtx_.beginPath();
367 this.chartCtx_.moveTo(0, this.chartHeight_ - 0.5);
368 this.chartCtx_.lineTo(this.chartWidth_, this.chartHeight_ - 0.5);
369 this.chartCtx_.stroke();
370 this.chartCtx_.closePath();
373 drawTooltip_: function() {
375 var tooltipData = this.pictureOps_[this.currentBarMouseOverTarget_];
376 var tooltipTitle = tooltipData.cmd_string;
377 var tooltipTime = tooltipData.cmd_time.toFixed(4);
378 var toolTipTimePercentage =
379 ((tooltipData.cmd_time / this.totalOpCost_) * 100).toFixed(2);
381 var tooltipWidth = 120;
382 var tooltipHeight = 40;
383 var chartInnerWidth = this.chartWidth_ - CHART_PADDING_RIGHT -
385 var barWidth = BAR_WIDTH + BAR_PADDING;
386 var tooltipOffset = Math.round((tooltipWidth - barWidth) * 0.5);
388 var left = CHART_PADDING_LEFT + this.currentBarMouseOverTarget_ *
389 barWidth - tooltipOffset;
390 var top = Math.round((this.chartHeight_ - tooltipHeight) * 0.5);
392 this.chartCtx_.save();
394 this.chartCtx_.shadowOffsetX = 0;
395 this.chartCtx_.shadowOffsetY = 5;
396 this.chartCtx_.shadowBlur = 4;
397 this.chartCtx_.shadowColor = 'rgba(0,0,0,0.4)';
399 this.chartCtx_.strokeStyle = '#888';
400 this.chartCtx_.fillStyle = '#EEE';
401 this.chartCtx_.fillRect(left, top, tooltipWidth, tooltipHeight);
403 this.chartCtx_.shadowColor = 'transparent';
404 this.chartCtx_.translate(0.5, 0.5);
405 this.chartCtx_.strokeRect(left, top, tooltipWidth, tooltipHeight);
407 this.chartCtx_.restore();
409 this.chartCtx_.fillStyle = '#222';
410 this.chartCtx_.textAlign = 'left';
411 this.chartCtx_.textBaseline = 'top';
412 this.chartCtx_.font = '800 12px Arial';
413 this.chartCtx_.fillText(tooltipTitle, left + 8, top + 8);
415 this.chartCtx_.fillStyle = '#555';
416 this.chartCtx_.font = '400 italic 10px Arial';
417 this.chartCtx_.fillText(tooltipTime + 'ms (' +
418 toolTipTimePercentage + '%)', left + 8, top + 22);
421 drawBars_: function() {
426 var opWidth = BAR_WIDTH + BAR_PADDING;
429 var bottom = this.chartHeight_ - CHART_PADDING_BOTTOM;
430 var maxHeight = this.chartHeight_ - CHART_PADDING_BOTTOM -
434 if (this.usePercentileScale)
435 maxValue = this.ninetyFifthPercentileCost_;
437 maxValue = this.maxCost_;
439 for (var b = 0; b < this.pictureOps_.length; b++) {
441 op = this.pictureOps_[b];
442 opHeight = Math.round(
443 (op.cmd_time / maxValue) * maxHeight);
444 opHeight = Math.max(opHeight, 1);
445 opHover = (b === this.currentBarMouseOverTarget_);
446 opColor = this.getOpColor_(op.cmd_string, opHover);
448 if (b === this.selectedOpIndex)
449 this.chartCtx_.fillStyle = '#FFFF00';
451 this.chartCtx_.fillStyle = opColor;
453 this.chartCtx_.fillRect(CHART_PADDING_LEFT + b * opWidth,
454 bottom - opHeight, BAR_WIDTH, opHeight);
459 getOpColor_: function(opName, hover) {
461 var characters = opName.split('');
463 var hue = characters.reduce(this.reduceNameToHue, 0) % 360;
465 var lightness = hover ? '75%' : '50%';
467 return 'hsl(' + hue + ', ' + saturation + '%, ' + lightness + '%)';
470 reduceNameToHue: function(previousValue, currentValue, index, array) {
471 // Get the char code and apply a magic adjustment value so we get
472 // pretty colors from around the rainbow.
473 return Math.round(previousValue + currentValue.charCodeAt(0) *
474 HUE_CHAR_CODE_ADJUSTMENT);
477 clearChartContents_: function() {
478 this.chartCtx_.clearRect(0, 0, this.chartWidth_, this.chartHeight_);
481 showNoTimingDataMessage_: function() {
482 this.chartCtx_.font = '800 italic 14px Arial';
483 this.chartCtx_.fillStyle = '#333';
484 this.chartCtx_.textAlign = 'center';
485 this.chartCtx_.textBaseline = 'middle';
486 this.chartCtx_.fillText('No timing data available.',
487 this.chartWidth_ * 0.5, this.chartHeight_ * 0.5);
492 PictureOpsChartView: PictureOpsChartView