Upstream version 5.34.104.0
[platform/framework/web/crosswalk.git] / src / third_party / trace-viewer / src / 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.processPictureData_();
111       this.dimensionsHaveChanged = true;
112     },
113
114     processPictureData_: function() {
115       if (this.pictureOps_ === undefined)
116         return;
117
118       var totalOpCost = 0;
119
120       // Take a copy of the picture ops data for sorting.
121       this.opCosts_ = this.pictureOps_.map(function(op) {
122         totalOpCost += op.cmd_time;
123         return op.cmd_time;
124       });
125       this.opCosts_.sort();
126
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];
132
133       this.totalOpCost_ = totalOpCost;
134     },
135
136     extractBarIndex_: function(e) {
137
138       var index = undefined;
139
140       if (this.pictureOps_ === undefined ||
141           this.pictureOps_.length === 0)
142         return index;
143
144       var x = e.offsetX;
145       var y = e.offsetY;
146
147       var totalBarWidth = (BAR_WIDTH + BAR_PADDING) * this.pictureOps_.length;
148
149       var chartLeft = CHART_PADDING_LEFT;
150       var chartTop = 0;
151       var chartBottom = this.chartHeight_ - CHART_PADDING_BOTTOM;
152       var chartRight = chartLeft + totalBarWidth;
153
154       if (x < chartLeft || x > chartRight || y < chartTop || y > chartBottom)
155         return index;
156
157       index = Math.floor((x - chartLeft) / totalBarWidth *
158           this.pictureOps_.length);
159
160       index = tvcm.clamp(index, 0, this.pictureOps_.length - 1);
161
162       return index;
163     },
164
165     onClick_: function(e) {
166
167       var barClicked = this.extractBarIndex_(e);
168
169       if (barClicked === undefined)
170         return;
171
172       // If we click on the already selected item we should deselect.
173       if (barClicked === this.selectedOpIndex)
174         this.selectedOpIndex = undefined;
175       else
176         this.selectedOpIndex = barClicked;
177
178       e.preventDefault();
179
180       tvcm.dispatchSimpleEvent(this, 'selection-changed', false);
181     },
182
183     onMouseMove_: function(e) {
184
185       var lastBarMouseOverTarget = this.currentBarMouseOverTarget_;
186       this.currentBarMouseOverTarget_ = this.extractBarIndex_(e);
187
188       if (this.currentBarMouseOverTarget_ === lastBarMouseOverTarget)
189         return;
190
191       this.drawChartContents_();
192     },
193
194     scrollSelectedItemIntoViewIfNecessary: function() {
195
196       if (this.selectedOpIndex === undefined)
197         return;
198
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;
204
205       if (targetLeft > left && targetLeft < right)
206         return;
207
208       this.scrollLeft = (targetLeft - width * 0.5);
209     },
210
211     updateChartContents: function() {
212
213       if (this.dimensionsHaveChanged)
214         this.updateChartDimensions_();
215
216       this.drawChartContents_();
217     },
218
219     updateChartDimensions_: function() {
220
221       if (!this.pictureOps_)
222         return;
223
224       var width = CHART_PADDING_LEFT + CHART_PADDING_RIGHT +
225           ((BAR_WIDTH + BAR_PADDING) * this.pictureOps_.length);
226
227       if (width < this.offsetWidth)
228         width = this.offsetWidth;
229
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;
234
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_;
240
241       this.chart_.style.width = this.chartWidth_ + 'px';
242       this.chart_.style.height = this.chartHeight_ + 'px';
243
244       this.chartCtx_.scale(this.chartScale_, this.chartScale_);
245
246       this.dimensionsHaveChanged = false;
247     },
248
249     drawChartContents_: function() {
250
251       this.clearChartContents_();
252
253       if (this.pictureOps_ === undefined ||
254           this.pictureOps_.length === 0 ||
255           this.pictureOps_[0].cmd_time === undefined) {
256
257         this.showNoTimingDataMessage_();
258         return;
259       }
260
261       this.drawSelection_();
262       this.drawBars_();
263       this.drawChartAxes_();
264       this.drawLinesAtTickMarks_();
265       this.drawLineAtBottomOfChart_();
266
267       if (this.currentBarMouseOverTarget_ === undefined)
268         return;
269
270       this.drawTooltip_();
271     },
272
273     drawSelection_: function() {
274
275       if (this.selectedOpIndex === undefined)
276         return;
277
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);
282     },
283
284     drawChartAxes_: function() {
285
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;
289
290       var tickYInterval = height / (VERTICAL_TICKS - 1);
291       var tickYPosition = 0;
292       var tickValInterval = (max - min) / (VERTICAL_TICKS - 1);
293       var tickVal = 0;
294
295       this.chartCtx_.fillStyle = '#333';
296       this.chartCtx_.strokeStyle = '#777';
297       this.chartCtx_.save();
298
299       // Translate half a pixel to avoid blurry lines.
300       this.chartCtx_.translate(0.5, 0.5);
301
302       // Sides.
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();
311
312       // Y-axis ticks.
313       this.chartCtx_.translate(AXIS_PADDING_LEFT, AXIS_PADDING_TOP);
314
315       this.chartCtx_.font = '10px Arial';
316       this.chartCtx_.textAlign = 'right';
317       this.chartCtx_.textBaseline = 'middle';
318
319       this.chartCtx_.beginPath();
320       for (var t = 0; t < VERTICAL_TICKS; t++) {
321
322         tickYPosition = Math.round(t * tickYInterval);
323         tickVal = (max - t * tickValInterval).toFixed(4);
324
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);
329
330       }
331
332       this.chartCtx_.stroke();
333       this.chartCtx_.closePath();
334
335       this.chartCtx_.restore();
336     },
337
338     drawLinesAtTickMarks_: function() {
339
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;
344
345       this.chartCtx_.save();
346
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)';
350
351       for (var t = 0; t < VERTICAL_TICKS; t++) {
352         tickYPosition = Math.round(t * tickYInterval);
353
354         this.chartCtx_.moveTo(0, tickYPosition);
355         this.chartCtx_.lineTo(width, tickYPosition);
356         this.chartCtx_.stroke();
357       }
358
359       this.chartCtx_.restore();
360       this.chartCtx_.closePath();
361     },
362
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();
370     },
371
372     drawTooltip_: function() {
373
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);
379
380       var tooltipWidth = 120;
381       var tooltipHeight = 40;
382       var chartInnerWidth = this.chartWidth_ - CHART_PADDING_RIGHT -
383           CHART_PADDING_LEFT;
384       var barWidth = BAR_WIDTH + BAR_PADDING;
385       var tooltipOffset = Math.round((tooltipWidth - barWidth) * 0.5);
386
387       var left = CHART_PADDING_LEFT + this.currentBarMouseOverTarget_ *
388           barWidth - tooltipOffset;
389       var top = Math.round((this.chartHeight_ - tooltipHeight) * 0.5);
390
391       this.chartCtx_.save();
392
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)';
397
398       this.chartCtx_.strokeStyle = '#888';
399       this.chartCtx_.fillStyle = '#EEE';
400       this.chartCtx_.fillRect(left, top, tooltipWidth, tooltipHeight);
401
402       this.chartCtx_.shadowColor = 'transparent';
403       this.chartCtx_.translate(0.5, 0.5);
404       this.chartCtx_.strokeRect(left, top, tooltipWidth, tooltipHeight);
405
406       this.chartCtx_.restore();
407
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);
413
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);
418     },
419
420     drawBars_: function() {
421
422       var op;
423       var opColor = 0;
424       var opHeight = 0;
425       var opWidth = BAR_WIDTH + BAR_PADDING;
426       var opHover = false;
427
428       var bottom = this.chartHeight_ - CHART_PADDING_BOTTOM;
429       var maxHeight = this.chartHeight_ - CHART_PADDING_BOTTOM -
430           CHART_PADDING_TOP;
431
432       var maxValue;
433       if (this.usePercentileScale)
434         maxValue = this.ninetyFifthPercentileCost_;
435       else
436         maxValue = this.maxCost_;
437
438       for (var b = 0; b < this.pictureOps_.length; b++) {
439
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);
446
447         if (b === this.selectedOpIndex)
448           this.chartCtx_.fillStyle = '#FFFF00';
449         else
450           this.chartCtx_.fillStyle = opColor;
451
452         this.chartCtx_.fillRect(CHART_PADDING_LEFT + b * opWidth,
453             bottom - opHeight, BAR_WIDTH, opHeight);
454       }
455
456     },
457
458     getOpColor_: function(opName, hover) {
459
460       var characters = opName.split('');
461
462       var hue = characters.reduce(this.reduceNameToHue, 0) % 360;
463       var saturation = 30;
464       var lightness = hover ? '75%' : '50%';
465
466       return 'hsl(' + hue + ', ' + saturation + '%, ' + lightness + '%)';
467     },
468
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);
474     },
475
476     clearChartContents_: function() {
477       this.chartCtx_.clearRect(0, 0, this.chartWidth_, this.chartHeight_);
478     },
479
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);
487     }
488   };
489
490   return {
491     PictureOpsChartView: PictureOpsChartView
492   };
493 });