1<!DOCTYPE html>
2<!--
3Copyright (c) 2013 The Chromium Authors. All rights reserved.
4Use of this source code is governed by a BSD-style license that can be
5found in the LICENSE file.
6-->
7
8<link rel="stylesheet" href="/tracing/ui/extras/chrome/cc/picture_ops_chart_view.css">
9
10<link rel="import" href="/tracing/ui/base/dom_helpers.html">
11
12<script>
13'use strict';
14
15tr.exportTo('tr.ui.e.chrome.cc', function() {
16  var BAR_PADDING = 1;
17  var BAR_WIDTH = 5;
18  var CHART_PADDING_LEFT = 65;
19  var CHART_PADDING_RIGHT = 30;
20  var CHART_PADDING_BOTTOM = 35;
21  var CHART_PADDING_TOP = 20;
22  var AXIS_PADDING_LEFT = 55;
23  var AXIS_PADDING_RIGHT = 30;
24  var AXIS_PADDING_BOTTOM = 35;
25  var AXIS_PADDING_TOP = 20;
26  var AXIS_TICK_SIZE = 5;
27  var AXIS_LABEL_PADDING = 5;
28  var VERTICAL_TICKS = 5;
29  var HUE_CHAR_CODE_ADJUSTMENT = 5.7;
30
31  /**
32   * Provides a chart showing the cumulative time spent in Skia operations
33   * during picture rasterization.
34   *
35   * @constructor
36   */
37  var PictureOpsChartView =
38      tr.ui.b.define('tr-ui-e-chrome-cc-picture-ops-chart-view');
39
40  PictureOpsChartView.prototype = {
41    __proto__: HTMLUnknownElement.prototype,
42
43    decorate: function() {
44      this.picture_ = undefined;
45      this.pictureOps_ = undefined;
46      this.opCosts_ = undefined;
47
48      this.chartScale_ = window.devicePixelRatio;
49
50      this.chart_ = document.createElement('canvas');
51      this.chartCtx_ = this.chart_.getContext('2d');
52      this.appendChild(this.chart_);
53
54      this.selectedOpIndex_ = undefined;
55      this.chartWidth_ = 0;
56      this.chartHeight_ = 0;
57      this.dimensionsHaveChanged_ = true;
58
59      this.currentBarMouseOverTarget_ = undefined;
60
61      this.ninetyFifthPercentileCost_ = 0;
62      this.totalOpCost_ = 0;
63
64      this.chart_.addEventListener('click', this.onClick_.bind(this));
65      this.chart_.addEventListener('mousemove', this.onMouseMove_.bind(this));
66
67      this.usePercentileScale_ = false;
68      this.usePercentileScaleCheckbox_ = tr.ui.b.createCheckBox(
69          this, 'usePercentileScale',
70          'PictureOpsChartView.usePercentileScale', false,
71          'Limit to 95%-ile');
72      this.usePercentileScaleCheckbox_.classList.add('use-percentile-scale');
73      this.appendChild(this.usePercentileScaleCheckbox_);
74    },
75
76    get dimensionsHaveChanged() {
77      return this.dimensionsHaveChanged_;
78    },
79
80    set dimensionsHaveChanged(dimensionsHaveChanged) {
81      this.dimensionsHaveChanged_ = dimensionsHaveChanged;
82    },
83
84    get usePercentileScale() {
85      return this.usePercentileScale_;
86    },
87
88    set usePercentileScale(usePercentileScale) {
89      this.usePercentileScale_ = usePercentileScale;
90      this.drawChartContents_();
91    },
92
93    get numOps() {
94      return this.opCosts_.length;
95    },
96
97    get selectedOpIndex() {
98      return this.selectedOpIndex_;
99    },
100
101    set selectedOpIndex(selectedOpIndex) {
102      if (selectedOpIndex < 0) throw new Error('Invalid index');
103      if (selectedOpIndex >= this.numOps) throw new Error('Invalid index');
104
105      this.selectedOpIndex_ = selectedOpIndex;
106    },
107
108    get picture() {
109      return this.picture_;
110    },
111
112    set picture(picture) {
113      this.picture_ = picture;
114      this.pictureOps_ = picture.tagOpsWithTimings(picture.getOps());
115      this.currentBarMouseOverTarget_ = undefined;
116      this.processPictureData_();
117      this.dimensionsHaveChanged = true;
118    },
119
120    processPictureData_: function() {
121      if (this.pictureOps_ === undefined)
122        return;
123
124      var totalOpCost = 0;
125
126      // Take a copy of the picture ops data for sorting.
127      this.opCosts_ = this.pictureOps_.map(function(op) {
128        totalOpCost += op.cmd_time;
129        return op.cmd_time;
130      });
131      this.opCosts_.sort();
132
133      var ninetyFifthPercentileCostIndex = Math.floor(
134          this.opCosts_.length * 0.95);
135      this.ninetyFifthPercentileCost_ =
136          this.opCosts_[ninetyFifthPercentileCostIndex];
137      this.maxCost_ = this.opCosts_[this.opCosts_.length - 1];
138
139      this.totalOpCost_ = totalOpCost;
140    },
141
142    extractBarIndex_: function(e) {
143
144      var index = undefined;
145
146      if (this.pictureOps_ === undefined ||
147          this.pictureOps_.length === 0)
148        return index;
149
150      var x = e.offsetX;
151      var y = e.offsetY;
152
153      var totalBarWidth = (BAR_WIDTH + BAR_PADDING) * this.pictureOps_.length;
154
155      var chartLeft = CHART_PADDING_LEFT;
156      var chartTop = 0;
157      var chartBottom = this.chartHeight_ - CHART_PADDING_BOTTOM;
158      var chartRight = chartLeft + totalBarWidth;
159
160      if (x < chartLeft || x > chartRight || y < chartTop || y > chartBottom)
161        return index;
162
163      index = Math.floor((x - chartLeft) / totalBarWidth *
164          this.pictureOps_.length);
165
166      index = tr.b.clamp(index, 0, this.pictureOps_.length - 1);
167
168      return index;
169    },
170
171    onClick_: function(e) {
172
173      var barClicked = this.extractBarIndex_(e);
174
175      if (barClicked === undefined)
176        return;
177
178      // If we click on the already selected item we should deselect.
179      if (barClicked === this.selectedOpIndex)
180        this.selectedOpIndex = undefined;
181      else
182        this.selectedOpIndex = barClicked;
183
184      e.preventDefault();
185
186      tr.b.dispatchSimpleEvent(this, 'selection-changed', false);
187    },
188
189    onMouseMove_: function(e) {
190
191      var lastBarMouseOverTarget = this.currentBarMouseOverTarget_;
192      this.currentBarMouseOverTarget_ = this.extractBarIndex_(e);
193
194      if (this.currentBarMouseOverTarget_ === lastBarMouseOverTarget)
195        return;
196
197      this.drawChartContents_();
198    },
199
200    scrollSelectedItemIntoViewIfNecessary: function() {
201
202      if (this.selectedOpIndex === undefined)
203        return;
204
205      var width = this.offsetWidth;
206      var left = this.scrollLeft;
207      var right = left + width;
208      var targetLeft = CHART_PADDING_LEFT +
209          (BAR_WIDTH + BAR_PADDING) * this.selectedOpIndex;
210
211      if (targetLeft > left && targetLeft < right)
212        return;
213
214      this.scrollLeft = (targetLeft - width * 0.5);
215    },
216
217    updateChartContents: function() {
218
219      if (this.dimensionsHaveChanged)
220        this.updateChartDimensions_();
221
222      this.drawChartContents_();
223    },
224
225    updateChartDimensions_: function() {
226
227      if (!this.pictureOps_)
228        return;
229
230      var width = CHART_PADDING_LEFT + CHART_PADDING_RIGHT +
231          ((BAR_WIDTH + BAR_PADDING) * this.pictureOps_.length);
232
233      if (width < this.offsetWidth)
234        width = this.offsetWidth;
235
236      // Allow the element to be its natural size as set by flexbox, then lock
237      // the width in before we set the width of the canvas.
238      this.chartWidth_ = width;
239      this.chartHeight_ = this.getBoundingClientRect().height;
240
241      // Scale up the canvas according to the devicePixelRatio, then reduce it
242      // down again via CSS. Finally we apply a scale to the canvas so that
243      // things are drawn at the correct size.
244      this.chart_.width = this.chartWidth_ * this.chartScale_;
245      this.chart_.height = this.chartHeight_ * this.chartScale_;
246
247      this.chart_.style.width = this.chartWidth_ + 'px';
248      this.chart_.style.height = this.chartHeight_ + 'px';
249
250      this.chartCtx_.scale(this.chartScale_, this.chartScale_);
251
252      this.dimensionsHaveChanged = false;
253    },
254
255    drawChartContents_: function() {
256
257      this.clearChartContents_();
258
259      if (this.pictureOps_ === undefined ||
260          this.pictureOps_.length === 0 ||
261          this.pictureOps_[0].cmd_time === undefined) {
262
263        this.showNoTimingDataMessage_();
264        return;
265      }
266
267      this.drawSelection_();
268      this.drawBars_();
269      this.drawChartAxes_();
270      this.drawLinesAtTickMarks_();
271      this.drawLineAtBottomOfChart_();
272
273      if (this.currentBarMouseOverTarget_ === undefined)
274        return;
275
276      this.drawTooltip_();
277    },
278
279    drawSelection_: function() {
280
281      if (this.selectedOpIndex === undefined)
282        return;
283
284      var width = (BAR_WIDTH + BAR_PADDING) * this.selectedOpIndex;
285      this.chartCtx_.fillStyle = 'rgb(223, 235, 230)';
286      this.chartCtx_.fillRect(CHART_PADDING_LEFT, CHART_PADDING_TOP,
287          width, this.chartHeight_ - CHART_PADDING_TOP - CHART_PADDING_BOTTOM);
288    },
289
290    drawChartAxes_: function() {
291
292      var min = this.opCosts_[0];
293      var max = this.opCosts_[this.opCosts_.length - 1];
294      var height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM;
295
296      var tickYInterval = height / (VERTICAL_TICKS - 1);
297      var tickYPosition = 0;
298      var tickValInterval = (max - min) / (VERTICAL_TICKS - 1);
299      var tickVal = 0;
300
301      this.chartCtx_.fillStyle = '#333';
302      this.chartCtx_.strokeStyle = '#777';
303      this.chartCtx_.save();
304
305      // Translate half a pixel to avoid blurry lines.
306      this.chartCtx_.translate(0.5, 0.5);
307
308      // Sides.
309      this.chartCtx_.beginPath();
310      this.chartCtx_.moveTo(AXIS_PADDING_LEFT, AXIS_PADDING_TOP);
311      this.chartCtx_.lineTo(AXIS_PADDING_LEFT, this.chartHeight_ -
312          AXIS_PADDING_BOTTOM);
313      this.chartCtx_.lineTo(this.chartWidth_ - AXIS_PADDING_RIGHT,
314          this.chartHeight_ - AXIS_PADDING_BOTTOM);
315      this.chartCtx_.stroke();
316      this.chartCtx_.closePath();
317
318      // Y-axis ticks.
319      this.chartCtx_.translate(AXIS_PADDING_LEFT, AXIS_PADDING_TOP);
320
321      this.chartCtx_.font = '10px Arial';
322      this.chartCtx_.textAlign = 'right';
323      this.chartCtx_.textBaseline = 'middle';
324
325      this.chartCtx_.beginPath();
326      for (var t = 0; t < VERTICAL_TICKS; t++) {
327
328        tickYPosition = Math.round(t * tickYInterval);
329        tickVal = (max - t * tickValInterval).toFixed(4);
330
331        this.chartCtx_.moveTo(0, tickYPosition);
332        this.chartCtx_.lineTo(-AXIS_TICK_SIZE, tickYPosition);
333        this.chartCtx_.fillText(tickVal,
334            -AXIS_TICK_SIZE - AXIS_LABEL_PADDING, tickYPosition);
335
336      }
337
338      this.chartCtx_.stroke();
339      this.chartCtx_.closePath();
340
341      this.chartCtx_.restore();
342    },
343
344    drawLinesAtTickMarks_: function() {
345
346      var height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM;
347      var width = this.chartWidth_ - AXIS_PADDING_LEFT - AXIS_PADDING_RIGHT;
348      var tickYInterval = height / (VERTICAL_TICKS - 1);
349      var tickYPosition = 0;
350
351      this.chartCtx_.save();
352
353      this.chartCtx_.translate(AXIS_PADDING_LEFT + 0.5, AXIS_PADDING_TOP + 0.5);
354      this.chartCtx_.beginPath();
355      this.chartCtx_.strokeStyle = 'rgba(0,0,0,0.05)';
356
357      for (var t = 0; t < VERTICAL_TICKS; t++) {
358        tickYPosition = Math.round(t * tickYInterval);
359
360        this.chartCtx_.moveTo(0, tickYPosition);
361        this.chartCtx_.lineTo(width, tickYPosition);
362        this.chartCtx_.stroke();
363      }
364
365      this.chartCtx_.restore();
366      this.chartCtx_.closePath();
367    },
368
369    drawLineAtBottomOfChart_: function() {
370      this.chartCtx_.strokeStyle = '#AAA';
371      this.chartCtx_.beginPath();
372      this.chartCtx_.moveTo(0, this.chartHeight_ - 0.5);
373      this.chartCtx_.lineTo(this.chartWidth_, this.chartHeight_ - 0.5);
374      this.chartCtx_.stroke();
375      this.chartCtx_.closePath();
376    },
377
378    drawTooltip_: function() {
379
380      var tooltipData = this.pictureOps_[this.currentBarMouseOverTarget_];
381      var tooltipTitle = tooltipData.cmd_string;
382      var tooltipTime = tooltipData.cmd_time.toFixed(4);
383      var toolTipTimePercentage =
384          ((tooltipData.cmd_time / this.totalOpCost_) * 100).toFixed(2);
385
386      var tooltipWidth = 120;
387      var tooltipHeight = 40;
388      var chartInnerWidth = this.chartWidth_ - CHART_PADDING_RIGHT -
389          CHART_PADDING_LEFT;
390      var barWidth = BAR_WIDTH + BAR_PADDING;
391      var tooltipOffset = Math.round((tooltipWidth - barWidth) * 0.5);
392
393      var left = CHART_PADDING_LEFT + this.currentBarMouseOverTarget_ *
394          barWidth - tooltipOffset;
395      var top = Math.round((this.chartHeight_ - tooltipHeight) * 0.5);
396
397      this.chartCtx_.save();
398
399      this.chartCtx_.shadowOffsetX = 0;
400      this.chartCtx_.shadowOffsetY = 5;
401      this.chartCtx_.shadowBlur = 4;
402      this.chartCtx_.shadowColor = 'rgba(0,0,0,0.4)';
403
404      this.chartCtx_.strokeStyle = '#888';
405      this.chartCtx_.fillStyle = '#EEE';
406      this.chartCtx_.fillRect(left, top, tooltipWidth, tooltipHeight);
407
408      this.chartCtx_.shadowColor = 'transparent';
409      this.chartCtx_.translate(0.5, 0.5);
410      this.chartCtx_.strokeRect(left, top, tooltipWidth, tooltipHeight);
411
412      this.chartCtx_.restore();
413
414      this.chartCtx_.fillStyle = '#222';
415      this.chartCtx_.textAlign = 'left';
416      this.chartCtx_.textBaseline = 'top';
417      this.chartCtx_.font = '800 12px Arial';
418      this.chartCtx_.fillText(tooltipTitle, left + 8, top + 8);
419
420      this.chartCtx_.fillStyle = '#555';
421      this.chartCtx_.font = '400 italic 10px Arial';
422      this.chartCtx_.fillText(tooltipTime + 'ms (' +
423          toolTipTimePercentage + '%)', left + 8, top + 22);
424    },
425
426    drawBars_: function() {
427
428      var op;
429      var opColor = 0;
430      var opHeight = 0;
431      var opWidth = BAR_WIDTH + BAR_PADDING;
432      var opHover = false;
433
434      var bottom = this.chartHeight_ - CHART_PADDING_BOTTOM;
435      var maxHeight = this.chartHeight_ - CHART_PADDING_BOTTOM -
436          CHART_PADDING_TOP;
437
438      var maxValue;
439      if (this.usePercentileScale)
440        maxValue = this.ninetyFifthPercentileCost_;
441      else
442        maxValue = this.maxCost_;
443
444      for (var b = 0; b < this.pictureOps_.length; b++) {
445
446        op = this.pictureOps_[b];
447        opHeight = Math.round(
448            (op.cmd_time / maxValue) * maxHeight);
449        opHeight = Math.max(opHeight, 1);
450        opHover = (b === this.currentBarMouseOverTarget_);
451        opColor = this.getOpColor_(op.cmd_string, opHover);
452
453        if (b === this.selectedOpIndex)
454          this.chartCtx_.fillStyle = '#FFFF00';
455        else
456          this.chartCtx_.fillStyle = opColor;
457
458        this.chartCtx_.fillRect(CHART_PADDING_LEFT + b * opWidth,
459            bottom - opHeight, BAR_WIDTH, opHeight);
460      }
461
462    },
463
464    getOpColor_: function(opName, hover) {
465
466      var characters = opName.split('');
467
468      var hue = characters.reduce(this.reduceNameToHue, 0) % 360;
469      var saturation = 30;
470      var lightness = hover ? '75%' : '50%';
471
472      return 'hsl(' + hue + ', ' + saturation + '%, ' + lightness + '%)';
473    },
474
475    reduceNameToHue: function(previousValue, currentValue, index, array) {
476      // Get the char code and apply a magic adjustment value so we get
477      // pretty colors from around the rainbow.
478      return Math.round(previousValue + currentValue.charCodeAt(0) *
479          HUE_CHAR_CODE_ADJUSTMENT);
480    },
481
482    clearChartContents_: function() {
483      this.chartCtx_.clearRect(0, 0, this.chartWidth_, this.chartHeight_);
484    },
485
486    showNoTimingDataMessage_: function() {
487      this.chartCtx_.font = '800 italic 14px Arial';
488      this.chartCtx_.fillStyle = '#333';
489      this.chartCtx_.textAlign = 'center';
490      this.chartCtx_.textBaseline = 'middle';
491      this.chartCtx_.fillText('No timing data available.',
492          this.chartWidth_ * 0.5, this.chartHeight_ * 0.5);
493    }
494  };
495
496  return {
497    PictureOpsChartView: PictureOpsChartView
498  };
499});
500</script>
501