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"
9    href="/tracing/ui/extras/chrome/cc/picture_ops_chart_summary_view.css">
10
11<link rel="import" href="/tracing/ui/base/ui.html">
12
13<script>
14'use strict';
15
16tr.exportTo('tr.ui.e.chrome.cc', function() {
17  var OPS_TIMING_ITERATIONS = 3;
18  var CHART_PADDING_LEFT = 65;
19  var CHART_PADDING_RIGHT = 40;
20  var AXIS_PADDING_LEFT = 60;
21  var AXIS_PADDING_RIGHT = 35;
22  var AXIS_PADDING_TOP = 25;
23  var AXIS_PADDING_BOTTOM = 45;
24  var AXIS_LABEL_PADDING = 5;
25  var AXIS_TICK_SIZE = 10;
26  var LABEL_PADDING = 5;
27  var LABEL_INTERLEAVE_OFFSET = 15;
28  var BAR_PADDING = 5;
29  var VERTICAL_TICKS = 5;
30  var HUE_CHAR_CODE_ADJUSTMENT = 5.7;
31
32  /**
33   * Provides a chart showing the cumulative time spent in Skia operations
34   * during picture rasterization.
35   *
36   * @constructor
37   */
38  var PictureOpsChartSummaryView = tr.ui.b.define(
39      'tr-ui-e-chrome-cc-picture-ops-chart-summary-view');
40
41  PictureOpsChartSummaryView.prototype = {
42    __proto__: HTMLUnknownElement.prototype,
43
44    decorate: function() {
45      this.picture_ = undefined;
46      this.pictureDataProcessed_ = false;
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.opsTimingData_ = [];
55
56      this.chartWidth_ = 0;
57      this.chartHeight_ = 0;
58      this.requiresRedraw_ = true;
59
60      this.currentBarMouseOverTarget_ = null;
61
62      this.chart_.addEventListener('mousemove', this.onMouseMove_.bind(this));
63    },
64
65    get requiresRedraw() {
66      return this.requiresRedraw_;
67    },
68
69    set requiresRedraw(requiresRedraw) {
70      this.requiresRedraw_ = requiresRedraw;
71    },
72
73    get picture() {
74      return this.picture_;
75    },
76
77    set picture(picture) {
78      this.picture_ = picture;
79      this.pictureDataProcessed_ = false;
80
81      if (this.classList.contains('hidden'))
82        return;
83
84      this.processPictureData_();
85      this.requiresRedraw = true;
86      this.updateChartContents();
87    },
88
89    hide: function() {
90      this.classList.add('hidden');
91    },
92
93    show: function() {
94
95      this.classList.remove('hidden');
96
97      if (this.pictureDataProcessed_)
98        return;
99
100      this.processPictureData_();
101      this.requiresRedraw = true;
102      this.updateChartContents();
103
104    },
105
106    onMouseMove_: function(e) {
107
108      var lastBarMouseOverTarget = this.currentBarMouseOverTarget_;
109      this.currentBarMouseOverTarget_ = null;
110
111      var x = e.offsetX;
112      var y = e.offsetY;
113
114      var chartLeft = CHART_PADDING_LEFT;
115      var chartRight = this.chartWidth_ - CHART_PADDING_RIGHT;
116      var chartTop = AXIS_PADDING_TOP;
117      var chartBottom = this.chartHeight_ - AXIS_PADDING_BOTTOM;
118      var chartInnerWidth = chartRight - chartLeft;
119
120      if (x > chartLeft && x < chartRight && y > chartTop && y < chartBottom) {
121
122        this.currentBarMouseOverTarget_ = Math.floor(
123            (x - chartLeft) / chartInnerWidth * this.opsTimingData_.length);
124
125        this.currentBarMouseOverTarget_ = tr.b.clamp(
126            this.currentBarMouseOverTarget_, 0, this.opsTimingData_.length - 1);
127
128      }
129
130      if (this.currentBarMouseOverTarget_ === lastBarMouseOverTarget)
131        return;
132
133      this.drawChartContents_();
134    },
135
136    updateChartContents: function() {
137
138      if (this.requiresRedraw)
139        this.updateChartDimensions_();
140
141      this.drawChartContents_();
142    },
143
144    updateChartDimensions_: function() {
145      this.chartWidth_ = this.offsetWidth;
146      this.chartHeight_ = this.offsetHeight;
147
148      // Scale up the canvas according to the devicePixelRatio, then reduce it
149      // down again via CSS. Finally we apply a scale to the canvas so that
150      // things are drawn at the correct size.
151      this.chart_.width = this.chartWidth_ * this.chartScale_;
152      this.chart_.height = this.chartHeight_ * this.chartScale_;
153
154      this.chart_.style.width = this.chartWidth_ + 'px';
155      this.chart_.style.height = this.chartHeight_ + 'px';
156
157      this.chartCtx_.scale(this.chartScale_, this.chartScale_);
158    },
159
160    processPictureData_: function() {
161
162      this.resetOpsTimingData_();
163      this.pictureDataProcessed_ = true;
164
165      if (!this.picture_)
166        return;
167
168      var ops = this.picture_.getOps();
169      if (!ops)
170        return;
171
172      ops = this.picture_.tagOpsWithTimings(ops);
173
174      // Check that there are valid times.
175      if (ops[0].cmd_time === undefined)
176        return;
177
178      this.collapseOpsToTimingBuckets_(ops);
179    },
180
181    drawChartContents_: function() {
182
183      this.clearChartContents_();
184
185      if (this.opsTimingData_.length === 0) {
186        this.showNoTimingDataMessage_();
187        return;
188      }
189
190      this.drawChartAxes_();
191      this.drawBars_();
192      this.drawLineAtBottomOfChart_();
193
194      if (this.currentBarMouseOverTarget_ === null)
195        return;
196
197      this.drawTooltip_();
198    },
199
200    drawLineAtBottomOfChart_: function() {
201      this.chartCtx_.strokeStyle = '#AAA';
202      this.chartCtx_.moveTo(0, this.chartHeight_ - 0.5);
203      this.chartCtx_.lineTo(this.chartWidth_, this.chartHeight_ - 0.5);
204      this.chartCtx_.stroke();
205    },
206
207    drawTooltip_: function() {
208
209      var tooltipData = this.opsTimingData_[this.currentBarMouseOverTarget_];
210      var tooltipTitle = tooltipData.cmd_string;
211      var tooltipTime = tooltipData.cmd_time.toFixed(4);
212
213      var tooltipWidth = 110;
214      var tooltipHeight = 40;
215      var chartInnerWidth = this.chartWidth_ - CHART_PADDING_RIGHT -
216          CHART_PADDING_LEFT;
217      var barWidth = chartInnerWidth / this.opsTimingData_.length;
218      var tooltipOffset = Math.round((tooltipWidth - barWidth) * 0.5);
219
220      var left = CHART_PADDING_LEFT + this.currentBarMouseOverTarget_ *
221          barWidth - tooltipOffset;
222      var top = Math.round((this.chartHeight_ - tooltipHeight) * 0.5);
223
224      this.chartCtx_.save();
225
226      this.chartCtx_.shadowOffsetX = 0;
227      this.chartCtx_.shadowOffsetY = 5;
228      this.chartCtx_.shadowBlur = 4;
229      this.chartCtx_.shadowColor = 'rgba(0,0,0,0.4)';
230
231      this.chartCtx_.strokeStyle = '#888';
232      this.chartCtx_.fillStyle = '#EEE';
233      this.chartCtx_.fillRect(left, top, tooltipWidth, tooltipHeight);
234
235      this.chartCtx_.shadowColor = 'transparent';
236      this.chartCtx_.translate(0.5, 0.5);
237      this.chartCtx_.strokeRect(left, top, tooltipWidth, tooltipHeight);
238
239      this.chartCtx_.restore();
240
241      this.chartCtx_.fillStyle = '#222';
242      this.chartCtx_.textBaseline = 'top';
243      this.chartCtx_.font = '800 12px Arial';
244      this.chartCtx_.fillText(tooltipTitle, left + 8, top + 8);
245
246      this.chartCtx_.fillStyle = '#555';
247      this.chartCtx_.textBaseline = 'top';
248      this.chartCtx_.font = '400 italic 10px Arial';
249      this.chartCtx_.fillText('Total: ' + tooltipTime + 'ms',
250          left + 8, top + 22);
251    },
252
253    drawBars_: function() {
254
255      var len = this.opsTimingData_.length;
256      var max = this.opsTimingData_[0].cmd_time;
257      var min = this.opsTimingData_[len - 1].cmd_time;
258
259      var width = this.chartWidth_ - CHART_PADDING_LEFT - CHART_PADDING_RIGHT;
260      var height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM;
261      var barWidth = Math.floor(width / len);
262
263      var opData;
264      var opTiming;
265      var opHeight;
266      var opLabel;
267      var barLeft;
268
269      for (var b = 0; b < len; b++) {
270
271        opData = this.opsTimingData_[b];
272        opTiming = opData.cmd_time / max;
273
274        opHeight = Math.round(Math.max(1, opTiming * height));
275        opLabel = opData.cmd_string;
276        barLeft = CHART_PADDING_LEFT + b * barWidth;
277
278        this.chartCtx_.fillStyle = this.getOpColor_(opLabel);
279
280        this.chartCtx_.fillRect(barLeft + BAR_PADDING, AXIS_PADDING_TOP +
281            height - opHeight, barWidth - 2 * BAR_PADDING, opHeight);
282      }
283
284    },
285
286    getOpColor_: function(opName) {
287
288      var characters = opName.split('');
289      var hue = characters.reduce(this.reduceNameToHue, 0) % 360;
290
291      return 'hsl(' + hue + ', 30%, 50%)';
292    },
293
294    reduceNameToHue: function(previousValue, currentValue, index, array) {
295      // Get the char code and apply a magic adjustment value so we get
296      // pretty colors from around the rainbow.
297      return Math.round(previousValue + currentValue.charCodeAt(0) *
298          HUE_CHAR_CODE_ADJUSTMENT);
299    },
300
301    drawChartAxes_: function() {
302
303      var len = this.opsTimingData_.length;
304      var max = this.opsTimingData_[0].cmd_time;
305      var min = this.opsTimingData_[len - 1].cmd_time;
306
307      var width = this.chartWidth_ - AXIS_PADDING_LEFT - AXIS_PADDING_RIGHT;
308      var height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM;
309
310      var totalBarWidth = this.chartWidth_ - CHART_PADDING_LEFT -
311          CHART_PADDING_RIGHT;
312      var barWidth = Math.floor(totalBarWidth / len);
313      var tickYInterval = height / (VERTICAL_TICKS - 1);
314      var tickYPosition = 0;
315      var tickValInterval = (max - min) / (VERTICAL_TICKS - 1);
316      var tickVal = 0;
317
318      this.chartCtx_.fillStyle = '#333';
319      this.chartCtx_.strokeStyle = '#777';
320      this.chartCtx_.save();
321
322      // Translate half a pixel to avoid blurry lines.
323      this.chartCtx_.translate(0.5, 0.5);
324
325      // Sides.
326
327      this.chartCtx_.save();
328
329      this.chartCtx_.translate(AXIS_PADDING_LEFT, AXIS_PADDING_TOP);
330      this.chartCtx_.moveTo(0, 0);
331      this.chartCtx_.lineTo(0, height);
332      this.chartCtx_.lineTo(width, height);
333
334      // Y-axis ticks.
335      this.chartCtx_.font = '10px Arial';
336      this.chartCtx_.textAlign = 'right';
337      this.chartCtx_.textBaseline = 'middle';
338
339      for (var t = 0; t < VERTICAL_TICKS; t++) {
340
341        tickYPosition = Math.round(t * tickYInterval);
342        tickVal = (max - t * tickValInterval).toFixed(4);
343
344        this.chartCtx_.moveTo(0, tickYPosition);
345        this.chartCtx_.lineTo(-AXIS_TICK_SIZE, tickYPosition);
346        this.chartCtx_.fillText(tickVal,
347            -AXIS_TICK_SIZE - AXIS_LABEL_PADDING, tickYPosition);
348
349      }
350
351      this.chartCtx_.stroke();
352
353      this.chartCtx_.restore();
354
355
356      // Labels.
357
358      this.chartCtx_.save();
359
360      this.chartCtx_.translate(CHART_PADDING_LEFT + Math.round(barWidth * 0.5),
361          AXIS_PADDING_TOP + height + LABEL_PADDING);
362
363      this.chartCtx_.font = '10px Arial';
364      this.chartCtx_.textAlign = 'center';
365      this.chartCtx_.textBaseline = 'top';
366
367      var labelTickLeft;
368      var labelTickBottom;
369      for (var l = 0; l < len; l++) {
370
371        labelTickLeft = Math.round(l * barWidth);
372        labelTickBottom = l % 2 * LABEL_INTERLEAVE_OFFSET;
373
374        this.chartCtx_.save();
375        this.chartCtx_.moveTo(labelTickLeft, -LABEL_PADDING);
376        this.chartCtx_.lineTo(labelTickLeft, labelTickBottom);
377        this.chartCtx_.stroke();
378        this.chartCtx_.restore();
379
380        this.chartCtx_.fillText(this.opsTimingData_[l].cmd_string,
381            labelTickLeft, labelTickBottom);
382      }
383
384      this.chartCtx_.restore();
385
386      this.chartCtx_.restore();
387    },
388
389    clearChartContents_: function() {
390      this.chartCtx_.clearRect(0, 0, this.chartWidth_, this.chartHeight_);
391    },
392
393    showNoTimingDataMessage_: function() {
394      this.chartCtx_.font = '800 italic 14px Arial';
395      this.chartCtx_.fillStyle = '#333';
396      this.chartCtx_.textAlign = 'center';
397      this.chartCtx_.textBaseline = 'middle';
398      this.chartCtx_.fillText('No timing data available.',
399          this.chartWidth_ * 0.5, this.chartHeight_ * 0.5);
400    },
401
402    collapseOpsToTimingBuckets_: function(ops) {
403
404      var opsTimingDataIndexHash_ = {};
405      var timingData = this.opsTimingData_;
406      var op;
407      var opIndex;
408
409      for (var i = 0; i < ops.length; i++) {
410
411        op = ops[i];
412
413        if (op.cmd_time === undefined)
414          continue;
415
416        // Try to locate the entry for the current operation
417        // based on its name. If that fails, then create one for it.
418        opIndex = opsTimingDataIndexHash_[op.cmd_string] || null;
419
420        if (opIndex === null) {
421          timingData.push({
422            cmd_time: 0,
423            cmd_string: op.cmd_string
424          });
425
426          opIndex = timingData.length - 1;
427          opsTimingDataIndexHash_[op.cmd_string] = opIndex;
428        }
429
430        timingData[opIndex].cmd_time += op.cmd_time;
431
432      }
433
434      timingData.sort(this.sortTimingBucketsByOpTimeDescending_);
435
436      this.collapseTimingBucketsToOther_(4);
437    },
438
439    collapseTimingBucketsToOther_: function(count) {
440
441      var timingData = this.opsTimingData_;
442      var otherSource = timingData.splice(count, timingData.length - count);
443      var otherDestination = null;
444
445      if (!otherSource.length)
446        return;
447
448      timingData.push({
449        cmd_time: 0,
450        cmd_string: 'Other'
451      });
452
453      otherDestination = timingData[timingData.length - 1];
454      for (var i = 0; i < otherSource.length; i++) {
455        otherDestination.cmd_time += otherSource[i].cmd_time;
456      }
457    },
458
459    sortTimingBucketsByOpTimeDescending_: function(a, b) {
460      return b.cmd_time - a.cmd_time;
461    },
462
463    resetOpsTimingData_: function() {
464      this.opsTimingData_.length = 0;
465    }
466  };
467
468  return {
469    PictureOpsChartSummaryView: PictureOpsChartSummaryView
470  };
471});
472</script>
473