3 Copyright (c) 2013 The Chromium Authors. All rights reserved.
4 Use of this source code is governed by a BSD-style license that can be
5 found in the LICENSE file.
8 <link rel="stylesheet" href="/cc/picture_ops_chart_view.css">
9 <link rel="import" href="/tvcm/ui/dom_helpers.html">
14 tvcm.exportTo('cc', function() {
17 var CHART_PADDING_LEFT = 65;
18 var CHART_PADDING_RIGHT = 30;
19 var CHART_PADDING_BOTTOM = 35;
20 var CHART_PADDING_TOP = 20;
21 var AXIS_PADDING_LEFT = 55;
22 var AXIS_PADDING_RIGHT = 30;
23 var AXIS_PADDING_BOTTOM = 35;
24 var AXIS_PADDING_TOP = 20;
25 var AXIS_TICK_SIZE = 5;
26 var AXIS_LABEL_PADDING = 5;
27 var VERTICAL_TICKS = 5;
28 var HUE_CHAR_CODE_ADJUSTMENT = 5.7;
31 * Provides a chart showing the cumulative time spent in Skia operations
32 * during picture rasterization.
36 var PictureOpsChartView = tvcm.ui.define('picture-ops-chart-view');
38 PictureOpsChartView.prototype = {
39 __proto__: HTMLUnknownElement.prototype,
41 decorate: function() {
42 this.picture_ = undefined;
43 this.pictureOps_ = undefined;
44 this.opCosts_ = undefined;
46 this.chartScale_ = window.devicePixelRatio;
48 this.chart_ = document.createElement('canvas');
49 this.chartCtx_ = this.chart_.getContext('2d');
50 this.appendChild(this.chart_);
52 this.selectedOpIndex_ = undefined;
54 this.chartHeight_ = 0;
55 this.dimensionsHaveChanged_ = true;
57 this.currentBarMouseOverTarget_ = undefined;
59 this.ninetyFifthPercentileCost_ = 0;
60 this.totalOpCost_ = 0;
62 this.chart_.addEventListener('click', this.onClick_.bind(this));
63 this.chart_.addEventListener('mousemove', this.onMouseMove_.bind(this));
65 this.usePercentileScale_ = false;
66 this.usePercentileScaleCheckbox_ = tvcm.ui.createCheckBox(
67 this, 'usePercentileScale',
68 'PictureOpsChartView.usePercentileScale', false,
70 this.usePercentileScaleCheckbox_.classList.add('use-percentile-scale');
71 this.appendChild(this.usePercentileScaleCheckbox_);
74 get dimensionsHaveChanged() {
75 return this.dimensionsHaveChanged_;
78 set dimensionsHaveChanged(dimensionsHaveChanged) {
79 this.dimensionsHaveChanged_ = dimensionsHaveChanged;
82 get usePercentileScale() {
83 return this.usePercentileScale_;
86 set usePercentileScale(usePercentileScale) {
87 this.usePercentileScale_ = usePercentileScale;
88 this.drawChartContents_();
92 return this.opCosts_.length;
95 get selectedOpIndex() {
96 return this.selectedOpIndex_;
99 set selectedOpIndex(selectedOpIndex) {
100 if (selectedOpIndex < 0) throw new Error('Invalid index');
101 if (selectedOpIndex >= this.numOps) throw new Error('Invalid index');
103 this.selectedOpIndex_ = selectedOpIndex;
107 return this.picture_;
110 set picture(picture) {
111 this.picture_ = picture;
112 this.pictureOps_ = picture.tagOpsWithTimings(picture.getOps());
113 this.currentBarMouseOverTarget_ = undefined;
114 this.processPictureData_();
115 this.dimensionsHaveChanged = true;
118 processPictureData_: function() {
119 if (this.pictureOps_ === undefined)
124 // Take a copy of the picture ops data for sorting.
125 this.opCosts_ = this.pictureOps_.map(function(op) {
126 totalOpCost += op.cmd_time;
129 this.opCosts_.sort();
131 var ninetyFifthPercentileCostIndex = Math.floor(
132 this.opCosts_.length * 0.95);
133 this.ninetyFifthPercentileCost_ =
134 this.opCosts_[ninetyFifthPercentileCostIndex];
135 this.maxCost_ = this.opCosts_[this.opCosts_.length - 1];
137 this.totalOpCost_ = totalOpCost;
140 extractBarIndex_: function(e) {
142 var index = undefined;
144 if (this.pictureOps_ === undefined ||
145 this.pictureOps_.length === 0)
151 var totalBarWidth = (BAR_WIDTH + BAR_PADDING) * this.pictureOps_.length;
153 var chartLeft = CHART_PADDING_LEFT;
155 var chartBottom = this.chartHeight_ - CHART_PADDING_BOTTOM;
156 var chartRight = chartLeft + totalBarWidth;
158 if (x < chartLeft || x > chartRight || y < chartTop || y > chartBottom)
161 index = Math.floor((x - chartLeft) / totalBarWidth *
162 this.pictureOps_.length);
164 index = tvcm.clamp(index, 0, this.pictureOps_.length - 1);
169 onClick_: function(e) {
171 var barClicked = this.extractBarIndex_(e);
173 if (barClicked === undefined)
176 // If we click on the already selected item we should deselect.
177 if (barClicked === this.selectedOpIndex)
178 this.selectedOpIndex = undefined;
180 this.selectedOpIndex = barClicked;
184 tvcm.dispatchSimpleEvent(this, 'selection-changed', false);
187 onMouseMove_: function(e) {
189 var lastBarMouseOverTarget = this.currentBarMouseOverTarget_;
190 this.currentBarMouseOverTarget_ = this.extractBarIndex_(e);
192 if (this.currentBarMouseOverTarget_ === lastBarMouseOverTarget)
195 this.drawChartContents_();
198 scrollSelectedItemIntoViewIfNecessary: function() {
200 if (this.selectedOpIndex === undefined)
203 var width = this.offsetWidth;
204 var left = this.scrollLeft;
205 var right = left + width;
206 var targetLeft = CHART_PADDING_LEFT +
207 (BAR_WIDTH + BAR_PADDING) * this.selectedOpIndex;
209 if (targetLeft > left && targetLeft < right)
212 this.scrollLeft = (targetLeft - width * 0.5);
215 updateChartContents: function() {
217 if (this.dimensionsHaveChanged)
218 this.updateChartDimensions_();
220 this.drawChartContents_();
223 updateChartDimensions_: function() {
225 if (!this.pictureOps_)
228 var width = CHART_PADDING_LEFT + CHART_PADDING_RIGHT +
229 ((BAR_WIDTH + BAR_PADDING) * this.pictureOps_.length);
231 if (width < this.offsetWidth)
232 width = this.offsetWidth;
234 // Allow the element to be its natural size as set by flexbox, then lock
235 // the width in before we set the width of the canvas.
236 this.chartWidth_ = width;
237 this.chartHeight_ = this.getBoundingClientRect().height;
239 // Scale up the canvas according to the devicePixelRatio, then reduce it
240 // down again via CSS. Finally we apply a scale to the canvas so that
241 // things are drawn at the correct size.
242 this.chart_.width = this.chartWidth_ * this.chartScale_;
243 this.chart_.height = this.chartHeight_ * this.chartScale_;
245 this.chart_.style.width = this.chartWidth_ + 'px';
246 this.chart_.style.height = this.chartHeight_ + 'px';
248 this.chartCtx_.scale(this.chartScale_, this.chartScale_);
250 this.dimensionsHaveChanged = false;
253 drawChartContents_: function() {
255 this.clearChartContents_();
257 if (this.pictureOps_ === undefined ||
258 this.pictureOps_.length === 0 ||
259 this.pictureOps_[0].cmd_time === undefined) {
261 this.showNoTimingDataMessage_();
265 this.drawSelection_();
267 this.drawChartAxes_();
268 this.drawLinesAtTickMarks_();
269 this.drawLineAtBottomOfChart_();
271 if (this.currentBarMouseOverTarget_ === undefined)
277 drawSelection_: function() {
279 if (this.selectedOpIndex === undefined)
282 var width = (BAR_WIDTH + BAR_PADDING) * this.selectedOpIndex;
283 this.chartCtx_.fillStyle = 'rgb(223, 235, 230)';
284 this.chartCtx_.fillRect(CHART_PADDING_LEFT, CHART_PADDING_TOP,
285 width, this.chartHeight_ - CHART_PADDING_TOP - CHART_PADDING_BOTTOM);
288 drawChartAxes_: function() {
290 var min = this.opCosts_[0];
291 var max = this.opCosts_[this.opCosts_.length - 1];
292 var height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM;
294 var tickYInterval = height / (VERTICAL_TICKS - 1);
295 var tickYPosition = 0;
296 var tickValInterval = (max - min) / (VERTICAL_TICKS - 1);
299 this.chartCtx_.fillStyle = '#333';
300 this.chartCtx_.strokeStyle = '#777';
301 this.chartCtx_.save();
303 // Translate half a pixel to avoid blurry lines.
304 this.chartCtx_.translate(0.5, 0.5);
307 this.chartCtx_.beginPath();
308 this.chartCtx_.moveTo(AXIS_PADDING_LEFT, AXIS_PADDING_TOP);
309 this.chartCtx_.lineTo(AXIS_PADDING_LEFT, this.chartHeight_ -
310 AXIS_PADDING_BOTTOM);
311 this.chartCtx_.lineTo(this.chartWidth_ - AXIS_PADDING_RIGHT,
312 this.chartHeight_ - AXIS_PADDING_BOTTOM);
313 this.chartCtx_.stroke();
314 this.chartCtx_.closePath();
317 this.chartCtx_.translate(AXIS_PADDING_LEFT, AXIS_PADDING_TOP);
319 this.chartCtx_.font = '10px Arial';
320 this.chartCtx_.textAlign = 'right';
321 this.chartCtx_.textBaseline = 'middle';
323 this.chartCtx_.beginPath();
324 for (var t = 0; t < VERTICAL_TICKS; t++) {
326 tickYPosition = Math.round(t * tickYInterval);
327 tickVal = (max - t * tickValInterval).toFixed(4);
329 this.chartCtx_.moveTo(0, tickYPosition);
330 this.chartCtx_.lineTo(-AXIS_TICK_SIZE, tickYPosition);
331 this.chartCtx_.fillText(tickVal,
332 -AXIS_TICK_SIZE - AXIS_LABEL_PADDING, tickYPosition);
336 this.chartCtx_.stroke();
337 this.chartCtx_.closePath();
339 this.chartCtx_.restore();
342 drawLinesAtTickMarks_: function() {
344 var height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM;
345 var width = this.chartWidth_ - AXIS_PADDING_LEFT - AXIS_PADDING_RIGHT;
346 var tickYInterval = height / (VERTICAL_TICKS - 1);
347 var tickYPosition = 0;
349 this.chartCtx_.save();
351 this.chartCtx_.translate(AXIS_PADDING_LEFT + 0.5, AXIS_PADDING_TOP + 0.5);
352 this.chartCtx_.beginPath();
353 this.chartCtx_.strokeStyle = 'rgba(0,0,0,0.05)';
355 for (var t = 0; t < VERTICAL_TICKS; t++) {
356 tickYPosition = Math.round(t * tickYInterval);
358 this.chartCtx_.moveTo(0, tickYPosition);
359 this.chartCtx_.lineTo(width, tickYPosition);
360 this.chartCtx_.stroke();
363 this.chartCtx_.restore();
364 this.chartCtx_.closePath();
367 drawLineAtBottomOfChart_: function() {
368 this.chartCtx_.strokeStyle = '#AAA';
369 this.chartCtx_.beginPath();
370 this.chartCtx_.moveTo(0, this.chartHeight_ - 0.5);
371 this.chartCtx_.lineTo(this.chartWidth_, this.chartHeight_ - 0.5);
372 this.chartCtx_.stroke();
373 this.chartCtx_.closePath();
376 drawTooltip_: function() {
378 var tooltipData = this.pictureOps_[this.currentBarMouseOverTarget_];
379 var tooltipTitle = tooltipData.cmd_string;
380 var tooltipTime = tooltipData.cmd_time.toFixed(4);
381 var toolTipTimePercentage =
382 ((tooltipData.cmd_time / this.totalOpCost_) * 100).toFixed(2);
384 var tooltipWidth = 120;
385 var tooltipHeight = 40;
386 var chartInnerWidth = this.chartWidth_ - CHART_PADDING_RIGHT -
388 var barWidth = BAR_WIDTH + BAR_PADDING;
389 var tooltipOffset = Math.round((tooltipWidth - barWidth) * 0.5);
391 var left = CHART_PADDING_LEFT + this.currentBarMouseOverTarget_ *
392 barWidth - tooltipOffset;
393 var top = Math.round((this.chartHeight_ - tooltipHeight) * 0.5);
395 this.chartCtx_.save();
397 this.chartCtx_.shadowOffsetX = 0;
398 this.chartCtx_.shadowOffsetY = 5;
399 this.chartCtx_.shadowBlur = 4;
400 this.chartCtx_.shadowColor = 'rgba(0,0,0,0.4)';
402 this.chartCtx_.strokeStyle = '#888';
403 this.chartCtx_.fillStyle = '#EEE';
404 this.chartCtx_.fillRect(left, top, tooltipWidth, tooltipHeight);
406 this.chartCtx_.shadowColor = 'transparent';
407 this.chartCtx_.translate(0.5, 0.5);
408 this.chartCtx_.strokeRect(left, top, tooltipWidth, tooltipHeight);
410 this.chartCtx_.restore();
412 this.chartCtx_.fillStyle = '#222';
413 this.chartCtx_.textAlign = 'left';
414 this.chartCtx_.textBaseline = 'top';
415 this.chartCtx_.font = '800 12px Arial';
416 this.chartCtx_.fillText(tooltipTitle, left + 8, top + 8);
418 this.chartCtx_.fillStyle = '#555';
419 this.chartCtx_.font = '400 italic 10px Arial';
420 this.chartCtx_.fillText(tooltipTime + 'ms (' +
421 toolTipTimePercentage + '%)', left + 8, top + 22);
424 drawBars_: function() {
429 var opWidth = BAR_WIDTH + BAR_PADDING;
432 var bottom = this.chartHeight_ - CHART_PADDING_BOTTOM;
433 var maxHeight = this.chartHeight_ - CHART_PADDING_BOTTOM -
437 if (this.usePercentileScale)
438 maxValue = this.ninetyFifthPercentileCost_;
440 maxValue = this.maxCost_;
442 for (var b = 0; b < this.pictureOps_.length; b++) {
444 op = this.pictureOps_[b];
445 opHeight = Math.round(
446 (op.cmd_time / maxValue) * maxHeight);
447 opHeight = Math.max(opHeight, 1);
448 opHover = (b === this.currentBarMouseOverTarget_);
449 opColor = this.getOpColor_(op.cmd_string, opHover);
451 if (b === this.selectedOpIndex)
452 this.chartCtx_.fillStyle = '#FFFF00';
454 this.chartCtx_.fillStyle = opColor;
456 this.chartCtx_.fillRect(CHART_PADDING_LEFT + b * opWidth,
457 bottom - opHeight, BAR_WIDTH, opHeight);
462 getOpColor_: function(opName, hover) {
464 var characters = opName.split('');
466 var hue = characters.reduce(this.reduceNameToHue, 0) % 360;
468 var lightness = hover ? '75%' : '50%';
470 return 'hsl(' + hue + ', ' + saturation + '%, ' + lightness + '%)';
473 reduceNameToHue: function(previousValue, currentValue, index, array) {
474 // Get the char code and apply a magic adjustment value so we get
475 // pretty colors from around the rainbow.
476 return Math.round(previousValue + currentValue.charCodeAt(0) *
477 HUE_CHAR_CODE_ADJUSTMENT);
480 clearChartContents_: function() {
481 this.chartCtx_.clearRect(0, 0, this.chartWidth_, this.chartHeight_);
484 showNoTimingDataMessage_: function() {
485 this.chartCtx_.font = '800 italic 14px Arial';
486 this.chartCtx_.fillStyle = '#333';
487 this.chartCtx_.textAlign = 'center';
488 this.chartCtx_.textBaseline = 'middle';
489 this.chartCtx_.fillText('No timing data available.',
490 this.chartWidth_ * 0.5, this.chartHeight_ * 0.5);
495 PictureOpsChartView: PictureOpsChartView