Upstream version 9.37.197.0
[platform/framework/web/crosswalk.git] / src / third_party / trace-viewer / trace_viewer / cc / picture_ops_chart_view.js
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.
4
5 'use strict';
6
7 tvcm.requireStylesheet('cc.picture_ops_chart_view');
8 tvcm.require('tvcm.ui.dom_helpers');
9
10 tvcm.exportTo('cc', function() {
11
12   var BAR_PADDING = 1;
13   var BAR_WIDTH = 5;
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;
26
27   /**
28    * Provides a chart showing the cumulative time spent in Skia operations
29    * during picture rasterization.
30    *
31    * @constructor
32    */
33   var PictureOpsChartView = tvcm.ui.define('picture-ops-chart-view');
34
35   PictureOpsChartView.prototype = {
36     __proto__: HTMLUnknownElement.prototype,
37
38     decorate: function() {
39       this.picture_ = undefined;
40       this.pictureOps_ = undefined;
41       this.opCosts_ = undefined;
42
43       this.chartScale_ = window.devicePixelRatio;
44
45       this.chart_ = document.createElement('canvas');
46       this.chartCtx_ = this.chart_.getContext('2d');
47       this.appendChild(this.chart_);
48
49       this.selectedOpIndex_ = undefined;
50       this.chartWidth_ = 0;
51       this.chartHeight_ = 0;
52       this.dimensionsHaveChanged_ = true;
53
54       this.currentBarMouseOverTarget_ = undefined;
55
56       this.ninetyFifthPercentileCost_ = 0;
57       this.totalOpCost_ = 0;
58
59       this.chart_.addEventListener('click', this.onClick_.bind(this));
60       this.chart_.addEventListener('mousemove', this.onMouseMove_.bind(this));
61
62       this.usePercentileScale_ = false;
63       this.usePercentileScaleCheckbox_ = tvcm.ui.createCheckBox(
64           this, 'usePercentileScale',
65           'PictureOpsChartView.usePercentileScale', false,
66           'Limit to 95%-ile');
67       this.usePercentileScaleCheckbox_.classList.add('use-percentile-scale');
68       this.appendChild(this.usePercentileScaleCheckbox_);
69     },
70
71     get dimensionsHaveChanged() {
72       return this.dimensionsHaveChanged_;
73     },
74
75     set dimensionsHaveChanged(dimensionsHaveChanged) {
76       this.dimensionsHaveChanged_ = dimensionsHaveChanged;
77     },
78
79     get usePercentileScale() {
80       return this.usePercentileScale_;
81     },
82
83     set usePercentileScale(usePercentileScale) {
84       this.usePercentileScale_ = usePercentileScale;
85       this.drawChartContents_();
86     },
87
88     get numOps() {
89       return this.opCosts_.length;
90     },
91
92     get selectedOpIndex() {
93       return this.selectedOpIndex_;
94     },
95
96     set selectedOpIndex(selectedOpIndex) {
97       if (selectedOpIndex < 0) throw new Error('Invalid index');
98       if (selectedOpIndex >= this.numOps) throw new Error('Invalid index');
99
100       this.selectedOpIndex_ = selectedOpIndex;
101     },
102
103     get picture() {
104       return this.picture_;
105     },
106
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;
113     },
114
115     processPictureData_: function() {
116       if (this.pictureOps_ === undefined)
117         return;
118
119       var totalOpCost = 0;
120
121       // Take a copy of the picture ops data for sorting.
122       this.opCosts_ = this.pictureOps_.map(function(op) {
123         totalOpCost += op.cmd_time;
124         return op.cmd_time;
125       });
126       this.opCosts_.sort();
127
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];
133
134       this.totalOpCost_ = totalOpCost;
135     },
136
137     extractBarIndex_: function(e) {
138
139       var index = undefined;
140
141       if (this.pictureOps_ === undefined ||
142           this.pictureOps_.length === 0)
143         return index;
144
145       var x = e.offsetX;
146       var y = e.offsetY;
147
148       var totalBarWidth = (BAR_WIDTH + BAR_PADDING) * this.pictureOps_.length;
149
150       var chartLeft = CHART_PADDING_LEFT;
151       var chartTop = 0;
152       var chartBottom = this.chartHeight_ - CHART_PADDING_BOTTOM;
153       var chartRight = chartLeft + totalBarWidth;
154
155       if (x < chartLeft || x > chartRight || y < chartTop || y > chartBottom)
156         return index;
157
158       index = Math.floor((x - chartLeft) / totalBarWidth *
159           this.pictureOps_.length);
160
161       index = tvcm.clamp(index, 0, this.pictureOps_.length - 1);
162
163       return index;
164     },
165
166     onClick_: function(e) {
167
168       var barClicked = this.extractBarIndex_(e);
169
170       if (barClicked === undefined)
171         return;
172
173       // If we click on the already selected item we should deselect.
174       if (barClicked === this.selectedOpIndex)
175         this.selectedOpIndex = undefined;
176       else
177         this.selectedOpIndex = barClicked;
178
179       e.preventDefault();
180
181       tvcm.dispatchSimpleEvent(this, 'selection-changed', false);
182     },
183
184     onMouseMove_: function(e) {
185
186       var lastBarMouseOverTarget = this.currentBarMouseOverTarget_;
187       this.currentBarMouseOverTarget_ = this.extractBarIndex_(e);
188
189       if (this.currentBarMouseOverTarget_ === lastBarMouseOverTarget)
190         return;
191
192       this.drawChartContents_();
193     },
194
195     scrollSelectedItemIntoViewIfNecessary: function() {
196
197       if (this.selectedOpIndex === undefined)
198         return;
199
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;
205
206       if (targetLeft > left && targetLeft < right)
207         return;
208
209       this.scrollLeft = (targetLeft - width * 0.5);
210     },
211
212     updateChartContents: function() {
213
214       if (this.dimensionsHaveChanged)
215         this.updateChartDimensions_();
216
217       this.drawChartContents_();
218     },
219
220     updateChartDimensions_: function() {
221
222       if (!this.pictureOps_)
223         return;
224
225       var width = CHART_PADDING_LEFT + CHART_PADDING_RIGHT +
226           ((BAR_WIDTH + BAR_PADDING) * this.pictureOps_.length);
227
228       if (width < this.offsetWidth)
229         width = this.offsetWidth;
230
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;
235
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_;
241
242       this.chart_.style.width = this.chartWidth_ + 'px';
243       this.chart_.style.height = this.chartHeight_ + 'px';
244
245       this.chartCtx_.scale(this.chartScale_, this.chartScale_);
246
247       this.dimensionsHaveChanged = false;
248     },
249
250     drawChartContents_: function() {
251
252       this.clearChartContents_();
253
254       if (this.pictureOps_ === undefined ||
255           this.pictureOps_.length === 0 ||
256           this.pictureOps_[0].cmd_time === undefined) {
257
258         this.showNoTimingDataMessage_();
259         return;
260       }
261
262       this.drawSelection_();
263       this.drawBars_();
264       this.drawChartAxes_();
265       this.drawLinesAtTickMarks_();
266       this.drawLineAtBottomOfChart_();
267
268       if (this.currentBarMouseOverTarget_ === undefined)
269         return;
270
271       this.drawTooltip_();
272     },
273
274     drawSelection_: function() {
275
276       if (this.selectedOpIndex === undefined)
277         return;
278
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);
283     },
284
285     drawChartAxes_: function() {
286
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;
290
291       var tickYInterval = height / (VERTICAL_TICKS - 1);
292       var tickYPosition = 0;
293       var tickValInterval = (max - min) / (VERTICAL_TICKS - 1);
294       var tickVal = 0;
295
296       this.chartCtx_.fillStyle = '#333';
297       this.chartCtx_.strokeStyle = '#777';
298       this.chartCtx_.save();
299
300       // Translate half a pixel to avoid blurry lines.
301       this.chartCtx_.translate(0.5, 0.5);
302
303       // Sides.
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();
312
313       // Y-axis ticks.
314       this.chartCtx_.translate(AXIS_PADDING_LEFT, AXIS_PADDING_TOP);
315
316       this.chartCtx_.font = '10px Arial';
317       this.chartCtx_.textAlign = 'right';
318       this.chartCtx_.textBaseline = 'middle';
319
320       this.chartCtx_.beginPath();
321       for (var t = 0; t < VERTICAL_TICKS; t++) {
322
323         tickYPosition = Math.round(t * tickYInterval);
324         tickVal = (max - t * tickValInterval).toFixed(4);
325
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);
330
331       }
332
333       this.chartCtx_.stroke();
334       this.chartCtx_.closePath();
335
336       this.chartCtx_.restore();
337     },
338
339     drawLinesAtTickMarks_: function() {
340
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;
345
346       this.chartCtx_.save();
347
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)';
351
352       for (var t = 0; t < VERTICAL_TICKS; t++) {
353         tickYPosition = Math.round(t * tickYInterval);
354
355         this.chartCtx_.moveTo(0, tickYPosition);
356         this.chartCtx_.lineTo(width, tickYPosition);
357         this.chartCtx_.stroke();
358       }
359
360       this.chartCtx_.restore();
361       this.chartCtx_.closePath();
362     },
363
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();
371     },
372
373     drawTooltip_: function() {
374
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);
380
381       var tooltipWidth = 120;
382       var tooltipHeight = 40;
383       var chartInnerWidth = this.chartWidth_ - CHART_PADDING_RIGHT -
384           CHART_PADDING_LEFT;
385       var barWidth = BAR_WIDTH + BAR_PADDING;
386       var tooltipOffset = Math.round((tooltipWidth - barWidth) * 0.5);
387
388       var left = CHART_PADDING_LEFT + this.currentBarMouseOverTarget_ *
389           barWidth - tooltipOffset;
390       var top = Math.round((this.chartHeight_ - tooltipHeight) * 0.5);
391
392       this.chartCtx_.save();
393
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)';
398
399       this.chartCtx_.strokeStyle = '#888';
400       this.chartCtx_.fillStyle = '#EEE';
401       this.chartCtx_.fillRect(left, top, tooltipWidth, tooltipHeight);
402
403       this.chartCtx_.shadowColor = 'transparent';
404       this.chartCtx_.translate(0.5, 0.5);
405       this.chartCtx_.strokeRect(left, top, tooltipWidth, tooltipHeight);
406
407       this.chartCtx_.restore();
408
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);
414
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);
419     },
420
421     drawBars_: function() {
422
423       var op;
424       var opColor = 0;
425       var opHeight = 0;
426       var opWidth = BAR_WIDTH + BAR_PADDING;
427       var opHover = false;
428
429       var bottom = this.chartHeight_ - CHART_PADDING_BOTTOM;
430       var maxHeight = this.chartHeight_ - CHART_PADDING_BOTTOM -
431           CHART_PADDING_TOP;
432
433       var maxValue;
434       if (this.usePercentileScale)
435         maxValue = this.ninetyFifthPercentileCost_;
436       else
437         maxValue = this.maxCost_;
438
439       for (var b = 0; b < this.pictureOps_.length; b++) {
440
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);
447
448         if (b === this.selectedOpIndex)
449           this.chartCtx_.fillStyle = '#FFFF00';
450         else
451           this.chartCtx_.fillStyle = opColor;
452
453         this.chartCtx_.fillRect(CHART_PADDING_LEFT + b * opWidth,
454             bottom - opHeight, BAR_WIDTH, opHeight);
455       }
456
457     },
458
459     getOpColor_: function(opName, hover) {
460
461       var characters = opName.split('');
462
463       var hue = characters.reduce(this.reduceNameToHue, 0) % 360;
464       var saturation = 30;
465       var lightness = hover ? '75%' : '50%';
466
467       return 'hsl(' + hue + ', ' + saturation + '%, ' + lightness + '%)';
468     },
469
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);
475     },
476
477     clearChartContents_: function() {
478       this.chartCtx_.clearRect(0, 0, this.chartWidth_, this.chartHeight_);
479     },
480
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);
488     }
489   };
490
491   return {
492     PictureOpsChartView: PictureOpsChartView
493   };
494 });