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 base.requireStylesheet('cc.picture_ops_chart_view');
8 base.require('ui.dom_helpers');
10 base.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 = 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_ = 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.processPictureData_();
111 this.dimensionsHaveChanged = true;
114 processPictureData_: function() {
115 if (this.pictureOps_ === undefined)
120 // Take a copy of the picture ops data for sorting.
121 this.opCosts_ = this.pictureOps_.map(function(op) {
122 totalOpCost += op.cmd_time;
125 this.opCosts_.sort();
127 var ninetyFifthPercentileCostIndex = Math.floor(
128 this.opCosts_.length * 0.95);
129 this.ninetyFifthPercentileCost_ =
130 this.opCosts_[ninetyFifthPercentileCostIndex];
131 this.maxCost_ = this.opCosts_[this.opCosts_.length - 1];
133 this.totalOpCost_ = totalOpCost;
136 extractBarIndex_: function(e) {
138 var index = undefined;
140 if (this.pictureOps_ === undefined ||
141 this.pictureOps_.length === 0)
147 var totalBarWidth = (BAR_WIDTH + BAR_PADDING) * this.pictureOps_.length;
149 var chartLeft = CHART_PADDING_LEFT;
151 var chartBottom = this.chartHeight_ - CHART_PADDING_BOTTOM;
152 var chartRight = chartLeft + totalBarWidth;
154 if (x < chartLeft || x > chartRight || y < chartTop || y > chartBottom)
157 index = Math.floor((x - chartLeft) / totalBarWidth *
158 this.pictureOps_.length);
160 index = base.clamp(index, 0, this.pictureOps_.length - 1);
165 onClick_: function(e) {
167 var barClicked = this.extractBarIndex_(e);
169 if (barClicked === undefined)
172 // If we click on the already selected item we should deselect.
173 if (barClicked === this.selectedOpIndex)
174 this.selectedOpIndex = undefined;
176 this.selectedOpIndex = barClicked;
180 base.dispatchSimpleEvent(this, 'selection-changed', false);
183 onMouseMove_: function(e) {
185 var lastBarMouseOverTarget = this.currentBarMouseOverTarget_;
186 this.currentBarMouseOverTarget_ = this.extractBarIndex_(e);
188 if (this.currentBarMouseOverTarget_ === lastBarMouseOverTarget)
191 this.drawChartContents_();
194 scrollSelectedItemIntoViewIfNecessary: function() {
196 if (this.selectedOpIndex === undefined)
199 var width = this.offsetWidth;
200 var left = this.scrollLeft;
201 var right = left + width;
202 var targetLeft = CHART_PADDING_LEFT +
203 (BAR_WIDTH + BAR_PADDING) * this.selectedOpIndex;
205 if (targetLeft > left && targetLeft < right)
208 this.scrollLeft = (targetLeft - width * 0.5);
211 updateChartContents: function() {
213 if (this.dimensionsHaveChanged)
214 this.updateChartDimensions_();
216 this.drawChartContents_();
219 updateChartDimensions_: function() {
221 if (!this.pictureOps_)
224 var width = CHART_PADDING_LEFT + CHART_PADDING_RIGHT +
225 ((BAR_WIDTH + BAR_PADDING) * this.pictureOps_.length);
227 if (width < this.offsetWidth)
228 width = this.offsetWidth;
230 // Allow the element to be its natural size as set by flexbox, then lock
231 // the width in before we set the width of the canvas.
232 this.chartWidth_ = width;
233 this.chartHeight_ = this.getBoundingClientRect().height;
235 // Scale up the canvas according to the devicePixelRatio, then reduce it
236 // down again via CSS. Finally we apply a scale to the canvas so that
237 // things are drawn at the correct size.
238 this.chart_.width = this.chartWidth_ * this.chartScale_;
239 this.chart_.height = this.chartHeight_ * this.chartScale_;
241 this.chart_.style.width = this.chartWidth_ + 'px';
242 this.chart_.style.height = this.chartHeight_ + 'px';
244 this.chartCtx_.scale(this.chartScale_, this.chartScale_);
246 this.dimensionsHaveChanged = false;
249 drawChartContents_: function() {
251 this.clearChartContents_();
253 if (this.pictureOps_ === undefined ||
254 this.pictureOps_.length === 0 ||
255 this.pictureOps_[0].cmd_time === undefined) {
257 this.showNoTimingDataMessage_();
261 this.drawSelection_();
263 this.drawChartAxes_();
264 this.drawLinesAtTickMarks_();
265 this.drawLineAtBottomOfChart_();
267 if (this.currentBarMouseOverTarget_ === undefined)
273 drawSelection_: function() {
275 if (this.selectedOpIndex === undefined)
278 var width = (BAR_WIDTH + BAR_PADDING) * this.selectedOpIndex;
279 this.chartCtx_.fillStyle = 'rgb(223, 235, 230)';
280 this.chartCtx_.fillRect(CHART_PADDING_LEFT, CHART_PADDING_TOP,
281 width, this.chartHeight_ - CHART_PADDING_TOP - CHART_PADDING_BOTTOM);
284 drawChartAxes_: function() {
286 var min = this.opCosts_[0];
287 var max = this.opCosts_[this.opCosts_.length - 1];
288 var height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM;
290 var tickYInterval = height / (VERTICAL_TICKS - 1);
291 var tickYPosition = 0;
292 var tickValInterval = (max - min) / (VERTICAL_TICKS - 1);
295 this.chartCtx_.fillStyle = '#333';
296 this.chartCtx_.strokeStyle = '#777';
297 this.chartCtx_.save();
299 // Translate half a pixel to avoid blurry lines.
300 this.chartCtx_.translate(0.5, 0.5);
303 this.chartCtx_.beginPath();
304 this.chartCtx_.moveTo(AXIS_PADDING_LEFT, AXIS_PADDING_TOP);
305 this.chartCtx_.lineTo(AXIS_PADDING_LEFT, this.chartHeight_ -
306 AXIS_PADDING_BOTTOM);
307 this.chartCtx_.lineTo(this.chartWidth_ - AXIS_PADDING_RIGHT,
308 this.chartHeight_ - AXIS_PADDING_BOTTOM);
309 this.chartCtx_.stroke();
310 this.chartCtx_.closePath();
313 this.chartCtx_.translate(AXIS_PADDING_LEFT, AXIS_PADDING_TOP);
315 this.chartCtx_.font = '10px Arial';
316 this.chartCtx_.textAlign = 'right';
317 this.chartCtx_.textBaseline = 'middle';
319 this.chartCtx_.beginPath();
320 for (var t = 0; t < VERTICAL_TICKS; t++) {
322 tickYPosition = Math.round(t * tickYInterval);
323 tickVal = (max - t * tickValInterval).toFixed(4);
325 this.chartCtx_.moveTo(0, tickYPosition);
326 this.chartCtx_.lineTo(-AXIS_TICK_SIZE, tickYPosition);
327 this.chartCtx_.fillText(tickVal,
328 -AXIS_TICK_SIZE - AXIS_LABEL_PADDING, tickYPosition);
332 this.chartCtx_.stroke();
333 this.chartCtx_.closePath();
335 this.chartCtx_.restore();
338 drawLinesAtTickMarks_: function() {
340 var height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM;
341 var width = this.chartWidth_ - AXIS_PADDING_LEFT - AXIS_PADDING_RIGHT;
342 var tickYInterval = height / (VERTICAL_TICKS - 1);
343 var tickYPosition = 0;
345 this.chartCtx_.save();
347 this.chartCtx_.translate(AXIS_PADDING_LEFT + 0.5, AXIS_PADDING_TOP + 0.5);
348 this.chartCtx_.beginPath();
349 this.chartCtx_.strokeStyle = 'rgba(0,0,0,0.05)';
351 for (var t = 0; t < VERTICAL_TICKS; t++) {
352 tickYPosition = Math.round(t * tickYInterval);
354 this.chartCtx_.moveTo(0, tickYPosition);
355 this.chartCtx_.lineTo(width, tickYPosition);
356 this.chartCtx_.stroke();
359 this.chartCtx_.restore();
360 this.chartCtx_.closePath();
363 drawLineAtBottomOfChart_: function() {
364 this.chartCtx_.strokeStyle = '#AAA';
365 this.chartCtx_.beginPath();
366 this.chartCtx_.moveTo(0, this.chartHeight_ - 0.5);
367 this.chartCtx_.lineTo(this.chartWidth_, this.chartHeight_ - 0.5);
368 this.chartCtx_.stroke();
369 this.chartCtx_.closePath();
372 drawTooltip_: function() {
374 var tooltipData = this.pictureOps_[this.currentBarMouseOverTarget_];
375 var tooltipTitle = tooltipData.cmd_string;
376 var tooltipTime = tooltipData.cmd_time.toFixed(4);
377 var toolTipTimePercentage =
378 ((tooltipData.cmd_time / this.totalOpCost_) * 100).toFixed(2);
380 var tooltipWidth = 120;
381 var tooltipHeight = 40;
382 var chartInnerWidth = this.chartWidth_ - CHART_PADDING_RIGHT -
384 var barWidth = BAR_WIDTH + BAR_PADDING;
385 var tooltipOffset = Math.round((tooltipWidth - barWidth) * 0.5);
387 var left = CHART_PADDING_LEFT + this.currentBarMouseOverTarget_ *
388 barWidth - tooltipOffset;
389 var top = Math.round((this.chartHeight_ - tooltipHeight) * 0.5);
391 this.chartCtx_.save();
393 this.chartCtx_.shadowOffsetX = 0;
394 this.chartCtx_.shadowOffsetY = 5;
395 this.chartCtx_.shadowBlur = 4;
396 this.chartCtx_.shadowColor = 'rgba(0,0,0,0.4)';
398 this.chartCtx_.strokeStyle = '#888';
399 this.chartCtx_.fillStyle = '#EEE';
400 this.chartCtx_.fillRect(left, top, tooltipWidth, tooltipHeight);
402 this.chartCtx_.shadowColor = 'transparent';
403 this.chartCtx_.translate(0.5, 0.5);
404 this.chartCtx_.strokeRect(left, top, tooltipWidth, tooltipHeight);
406 this.chartCtx_.restore();
408 this.chartCtx_.fillStyle = '#222';
409 this.chartCtx_.textAlign = 'left';
410 this.chartCtx_.textBaseline = 'top';
411 this.chartCtx_.font = '800 12px Arial';
412 this.chartCtx_.fillText(tooltipTitle, left + 8, top + 8);
414 this.chartCtx_.fillStyle = '#555';
415 this.chartCtx_.font = '400 italic 10px Arial';
416 this.chartCtx_.fillText(tooltipTime + 'ms (' +
417 toolTipTimePercentage + '%)', left + 8, top + 22);
420 drawBars_: function() {
425 var opWidth = BAR_WIDTH + BAR_PADDING;
428 var bottom = this.chartHeight_ - CHART_PADDING_BOTTOM;
429 var maxHeight = this.chartHeight_ - CHART_PADDING_BOTTOM -
433 if (this.usePercentileScale)
434 maxValue = this.ninetyFifthPercentileCost_;
436 maxValue = this.maxCost_;
438 for (var b = 0; b < this.pictureOps_.length; b++) {
440 op = this.pictureOps_[b];
441 opHeight = Math.round(
442 (op.cmd_time / maxValue) * maxHeight);
443 opHeight = Math.max(opHeight, 1);
444 opHover = (b === this.currentBarMouseOverTarget_);
445 opColor = this.getOpColor_(op.cmd_string, opHover);
447 if (b === this.selectedOpIndex)
448 this.chartCtx_.fillStyle = '#FFFF00';
450 this.chartCtx_.fillStyle = opColor;
452 this.chartCtx_.fillRect(CHART_PADDING_LEFT + b * opWidth,
453 bottom - opHeight, BAR_WIDTH, opHeight);
458 getOpColor_: function(opName, hover) {
460 var characters = opName.split('');
462 var hue = characters.reduce(this.reduceNameToHue, 0) % 360;
464 var lightness = hover ? '75%' : '50%';
466 return 'hsl(' + hue + ', ' + saturation + '%, ' + lightness + '%)';
469 reduceNameToHue: function(previousValue, currentValue, index, array) {
470 // Get the char code and apply a magic adjustment value so we get
471 // pretty colors from around the rainbow.
472 return Math.round(previousValue + currentValue.charCodeAt(0) *
473 HUE_CHAR_CODE_ADJUSTMENT);
476 clearChartContents_: function() {
477 this.chartCtx_.clearRect(0, 0, this.chartWidth_, this.chartHeight_);
480 showNoTimingDataMessage_: function() {
481 this.chartCtx_.font = '800 italic 14px Arial';
482 this.chartCtx_.fillStyle = '#333';
483 this.chartCtx_.textAlign = 'center';
484 this.chartCtx_.textBaseline = 'middle';
485 this.chartCtx_.fillText('No timing data available.',
486 this.chartWidth_ * 0.5, this.chartHeight_ * 0.5);
491 PictureOpsChartView: PictureOpsChartView