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