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.
7 tvcm.requireStylesheet('cc.picture_ops_chart_summary_view');
9 tvcm.exportTo('cc', function() {
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;
23 var VERTICAL_TICKS = 5;
24 var HUE_CHAR_CODE_ADJUSTMENT = 5.7;
27 * Provides a chart showing the cumulative time spent in Skia operations
28 * during picture rasterization.
32 var PictureOpsChartSummaryView = tvcm.ui.define(
33 'picture-ops-chart-summary-view');
35 PictureOpsChartSummaryView.prototype = {
36 __proto__: HTMLUnknownElement.prototype,
38 decorate: function() {
39 this.picture_ = undefined;
40 this.pictureDataProcessed_ = false;
42 this.chartScale_ = window.devicePixelRatio;
44 this.chart_ = document.createElement('canvas');
45 this.chartCtx_ = this.chart_.getContext('2d');
46 this.appendChild(this.chart_);
48 this.opsTimingData_ = [];
51 this.chartHeight_ = 0;
52 this.requiresRedraw_ = true;
54 this.currentBarMouseOverTarget_ = null;
56 this.chart_.addEventListener('mousemove', this.onMouseMove_.bind(this));
59 get requiresRedraw() {
60 return this.requiresRedraw_;
63 set requiresRedraw(requiresRedraw) {
64 this.requiresRedraw_ = requiresRedraw;
71 set picture(picture) {
72 this.picture_ = picture;
73 this.pictureDataProcessed_ = false;
75 if (this.classList.contains('hidden'))
78 this.processPictureData_();
79 this.requiresRedraw = true;
80 this.updateChartContents();
84 this.classList.add('hidden');
89 this.classList.remove('hidden');
91 if (this.pictureDataProcessed_)
94 this.processPictureData_();
95 this.requiresRedraw = true;
96 this.updateChartContents();
100 onMouseMove_: function(e) {
102 var lastBarMouseOverTarget = this.currentBarMouseOverTarget_;
103 this.currentBarMouseOverTarget_ = null;
108 var chartLeft = CHART_PADDING_LEFT;
109 var chartRight = this.chartWidth_ - CHART_PADDING_RIGHT;
110 var chartTop = AXIS_PADDING_TOP;
111 var chartBottom = this.chartHeight_ - AXIS_PADDING_BOTTOM;
112 var chartInnerWidth = chartRight - chartLeft;
114 if (x > chartLeft && x < chartRight && y > chartTop && y < chartBottom) {
116 this.currentBarMouseOverTarget_ = Math.floor(
117 (x - chartLeft) / chartInnerWidth * this.opsTimingData_.length);
119 this.currentBarMouseOverTarget_ = tvcm.clamp(
120 this.currentBarMouseOverTarget_, 0, this.opsTimingData_.length - 1);
124 if (this.currentBarMouseOverTarget_ === lastBarMouseOverTarget)
127 this.drawChartContents_();
130 updateChartContents: function() {
132 if (this.requiresRedraw)
133 this.updateChartDimensions_();
135 this.drawChartContents_();
138 updateChartDimensions_: function() {
139 this.chartWidth_ = this.offsetWidth;
140 this.chartHeight_ = this.offsetHeight;
142 // Scale up the canvas according to the devicePixelRatio, then reduce it
143 // down again via CSS. Finally we apply a scale to the canvas so that
144 // things are drawn at the correct size.
145 this.chart_.width = this.chartWidth_ * this.chartScale_;
146 this.chart_.height = this.chartHeight_ * this.chartScale_;
148 this.chart_.style.width = this.chartWidth_ + 'px';
149 this.chart_.style.height = this.chartHeight_ + 'px';
151 this.chartCtx_.scale(this.chartScale_, this.chartScale_);
154 processPictureData_: function() {
156 this.resetOpsTimingData_();
157 this.pictureDataProcessed_ = true;
162 var ops = this.picture_.getOps();
166 ops = this.picture_.tagOpsWithTimings(ops);
168 // Check that there are valid times.
169 if (ops[0].cmd_time === undefined)
172 this.collapseOpsToTimingBuckets_(ops);
175 drawChartContents_: function() {
177 this.clearChartContents_();
179 if (this.opsTimingData_.length === 0) {
180 this.showNoTimingDataMessage_();
184 this.drawChartAxes_();
186 this.drawLineAtBottomOfChart_();
188 if (this.currentBarMouseOverTarget_ === null)
194 drawLineAtBottomOfChart_: function() {
195 this.chartCtx_.strokeStyle = '#AAA';
196 this.chartCtx_.moveTo(0, this.chartHeight_ - 0.5);
197 this.chartCtx_.lineTo(this.chartWidth_, this.chartHeight_ - 0.5);
198 this.chartCtx_.stroke();
201 drawTooltip_: function() {
203 var tooltipData = this.opsTimingData_[this.currentBarMouseOverTarget_];
204 var tooltipTitle = tooltipData.cmd_string;
205 var tooltipTime = tooltipData.cmd_time.toFixed(4);
207 var tooltipWidth = 110;
208 var tooltipHeight = 40;
209 var chartInnerWidth = this.chartWidth_ - CHART_PADDING_RIGHT -
211 var barWidth = chartInnerWidth / this.opsTimingData_.length;
212 var tooltipOffset = Math.round((tooltipWidth - barWidth) * 0.5);
214 var left = CHART_PADDING_LEFT + this.currentBarMouseOverTarget_ *
215 barWidth - tooltipOffset;
216 var top = Math.round((this.chartHeight_ - tooltipHeight) * 0.5);
218 this.chartCtx_.save();
220 this.chartCtx_.shadowOffsetX = 0;
221 this.chartCtx_.shadowOffsetY = 5;
222 this.chartCtx_.shadowBlur = 4;
223 this.chartCtx_.shadowColor = 'rgba(0,0,0,0.4)';
225 this.chartCtx_.strokeStyle = '#888';
226 this.chartCtx_.fillStyle = '#EEE';
227 this.chartCtx_.fillRect(left, top, tooltipWidth, tooltipHeight);
229 this.chartCtx_.shadowColor = 'transparent';
230 this.chartCtx_.translate(0.5, 0.5);
231 this.chartCtx_.strokeRect(left, top, tooltipWidth, tooltipHeight);
233 this.chartCtx_.restore();
235 this.chartCtx_.fillStyle = '#222';
236 this.chartCtx_.textBaseline = 'top';
237 this.chartCtx_.font = '800 12px Arial';
238 this.chartCtx_.fillText(tooltipTitle, left + 8, top + 8);
240 this.chartCtx_.fillStyle = '#555';
241 this.chartCtx_.textBaseline = 'top';
242 this.chartCtx_.font = '400 italic 10px Arial';
243 this.chartCtx_.fillText('Total: ' + tooltipTime + 'ms',
247 drawBars_: function() {
249 var len = this.opsTimingData_.length;
250 var max = this.opsTimingData_[0].cmd_time;
251 var min = this.opsTimingData_[len - 1].cmd_time;
253 var width = this.chartWidth_ - CHART_PADDING_LEFT - CHART_PADDING_RIGHT;
254 var height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM;
255 var barWidth = Math.floor(width / len);
263 for (var b = 0; b < len; b++) {
265 opData = this.opsTimingData_[b];
266 opTiming = opData.cmd_time / max;
268 opHeight = Math.round(Math.max(1, opTiming * height));
269 opLabel = opData.cmd_string;
270 barLeft = CHART_PADDING_LEFT + b * barWidth;
272 this.chartCtx_.fillStyle = this.getOpColor_(opLabel);
274 this.chartCtx_.fillRect(barLeft + BAR_PADDING, AXIS_PADDING_TOP +
275 height - opHeight, barWidth - 2 * BAR_PADDING, opHeight);
280 getOpColor_: function(opName) {
282 var characters = opName.split('');
283 var hue = characters.reduce(this.reduceNameToHue, 0) % 360;
285 return 'hsl(' + hue + ', 30%, 50%)';
288 reduceNameToHue: function(previousValue, currentValue, index, array) {
289 // Get the char code and apply a magic adjustment value so we get
290 // pretty colors from around the rainbow.
291 return Math.round(previousValue + currentValue.charCodeAt(0) *
292 HUE_CHAR_CODE_ADJUSTMENT);
295 drawChartAxes_: function() {
297 var len = this.opsTimingData_.length;
298 var max = this.opsTimingData_[0].cmd_time;
299 var min = this.opsTimingData_[len - 1].cmd_time;
301 var width = this.chartWidth_ - AXIS_PADDING_LEFT - AXIS_PADDING_RIGHT;
302 var height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM;
304 var totalBarWidth = this.chartWidth_ - CHART_PADDING_LEFT -
306 var barWidth = Math.floor(totalBarWidth / len);
307 var tickYInterval = height / (VERTICAL_TICKS - 1);
308 var tickYPosition = 0;
309 var tickValInterval = (max - min) / (VERTICAL_TICKS - 1);
312 this.chartCtx_.fillStyle = '#333';
313 this.chartCtx_.strokeStyle = '#777';
314 this.chartCtx_.save();
316 // Translate half a pixel to avoid blurry lines.
317 this.chartCtx_.translate(0.5, 0.5);
321 this.chartCtx_.save();
323 this.chartCtx_.translate(AXIS_PADDING_LEFT, AXIS_PADDING_TOP);
324 this.chartCtx_.moveTo(0, 0);
325 this.chartCtx_.lineTo(0, height);
326 this.chartCtx_.lineTo(width, height);
329 this.chartCtx_.font = '10px Arial';
330 this.chartCtx_.textAlign = 'right';
331 this.chartCtx_.textBaseline = 'middle';
333 for (var t = 0; t < VERTICAL_TICKS; t++) {
335 tickYPosition = Math.round(t * tickYInterval);
336 tickVal = (max - t * tickValInterval).toFixed(4);
338 this.chartCtx_.moveTo(0, tickYPosition);
339 this.chartCtx_.lineTo(-AXIS_TICK_SIZE, tickYPosition);
340 this.chartCtx_.fillText(tickVal,
341 -AXIS_TICK_SIZE - AXIS_LABEL_PADDING, tickYPosition);
345 this.chartCtx_.stroke();
347 this.chartCtx_.restore();
352 this.chartCtx_.save();
354 this.chartCtx_.translate(CHART_PADDING_LEFT + Math.round(barWidth * 0.5),
355 AXIS_PADDING_TOP + height + LABEL_PADDING);
357 this.chartCtx_.font = '10px Arial';
358 this.chartCtx_.textAlign = 'center';
359 this.chartCtx_.textBaseline = 'top';
363 for (var l = 0; l < len; l++) {
365 labelTickLeft = Math.round(l * barWidth);
366 labelTickBottom = l % 2 * LABEL_INTERLEAVE_OFFSET;
368 this.chartCtx_.save();
369 this.chartCtx_.moveTo(labelTickLeft, -LABEL_PADDING);
370 this.chartCtx_.lineTo(labelTickLeft, labelTickBottom);
371 this.chartCtx_.stroke();
372 this.chartCtx_.restore();
374 this.chartCtx_.fillText(this.opsTimingData_[l].cmd_string,
375 labelTickLeft, labelTickBottom);
378 this.chartCtx_.restore();
380 this.chartCtx_.restore();
383 clearChartContents_: function() {
384 this.chartCtx_.clearRect(0, 0, this.chartWidth_, this.chartHeight_);
387 showNoTimingDataMessage_: function() {
388 this.chartCtx_.font = '800 italic 14px Arial';
389 this.chartCtx_.fillStyle = '#333';
390 this.chartCtx_.textAlign = 'center';
391 this.chartCtx_.textBaseline = 'middle';
392 this.chartCtx_.fillText('No timing data available.',
393 this.chartWidth_ * 0.5, this.chartHeight_ * 0.5);
396 collapseOpsToTimingBuckets_: function(ops) {
398 var opsTimingDataIndexHash_ = {};
399 var timingData = this.opsTimingData_;
403 for (var i = 0; i < ops.length; i++) {
407 if (op.cmd_time === undefined)
410 // Try to locate the entry for the current operation
411 // based on its name. If that fails, then create one for it.
412 opIndex = opsTimingDataIndexHash_[op.cmd_string] || null;
414 if (opIndex === null) {
417 cmd_string: op.cmd_string
420 opIndex = timingData.length - 1;
421 opsTimingDataIndexHash_[op.cmd_string] = opIndex;
424 timingData[opIndex].cmd_time += op.cmd_time;
428 timingData.sort(this.sortTimingBucketsByOpTimeDescending_);
430 this.collapseTimingBucketsToOther_(4);
433 collapseTimingBucketsToOther_: function(count) {
435 var timingData = this.opsTimingData_;
436 var otherSource = timingData.splice(count, timingData.length - count);
437 var otherDestination = null;
439 if (!otherSource.length)
447 otherDestination = timingData[timingData.length - 1];
448 for (var i = 0; i < otherSource.length; i++) {
449 otherDestination.cmd_time += otherSource[i].cmd_time;
453 sortTimingBucketsByOpTimeDescending_: function(a, b) {
454 return b.cmd_time - a.cmd_time;
457 resetOpsTimingData_: function() {
458 this.opsTimingData_.length = 0;
463 PictureOpsChartSummaryView: PictureOpsChartSummaryView