3 Copyright (c) 2013 The Chromium Authors. All rights reserved.
4 Use of this source code is governed by a BSD-style license that can be
5 found in the LICENSE file.
8 <link rel="import" href="/base/ui.html">
9 <link rel="stylesheet" href="/cc/picture_ops_chart_summary_view.css">
14 tv.exportTo('cc', function() {
15 var OPS_TIMING_ITERATIONS = 3;
16 var CHART_PADDING_LEFT = 65;
17 var CHART_PADDING_RIGHT = 40;
18 var AXIS_PADDING_LEFT = 60;
19 var AXIS_PADDING_RIGHT = 35;
20 var AXIS_PADDING_TOP = 25;
21 var AXIS_PADDING_BOTTOM = 45;
22 var AXIS_LABEL_PADDING = 5;
23 var AXIS_TICK_SIZE = 10;
24 var LABEL_PADDING = 5;
25 var LABEL_INTERLEAVE_OFFSET = 15;
27 var VERTICAL_TICKS = 5;
28 var HUE_CHAR_CODE_ADJUSTMENT = 5.7;
31 * Provides a chart showing the cumulative time spent in Skia operations
32 * during picture rasterization.
36 var PictureOpsChartSummaryView = tv.ui.define(
37 'picture-ops-chart-summary-view');
39 PictureOpsChartSummaryView.prototype = {
40 __proto__: HTMLUnknownElement.prototype,
42 decorate: function() {
43 this.picture_ = undefined;
44 this.pictureDataProcessed_ = false;
46 this.chartScale_ = window.devicePixelRatio;
48 this.chart_ = document.createElement('canvas');
49 this.chartCtx_ = this.chart_.getContext('2d');
50 this.appendChild(this.chart_);
52 this.opsTimingData_ = [];
55 this.chartHeight_ = 0;
56 this.requiresRedraw_ = true;
58 this.currentBarMouseOverTarget_ = null;
60 this.chart_.addEventListener('mousemove', this.onMouseMove_.bind(this));
63 get requiresRedraw() {
64 return this.requiresRedraw_;
67 set requiresRedraw(requiresRedraw) {
68 this.requiresRedraw_ = requiresRedraw;
75 set picture(picture) {
76 this.picture_ = picture;
77 this.pictureDataProcessed_ = false;
79 if (this.classList.contains('hidden'))
82 this.processPictureData_();
83 this.requiresRedraw = true;
84 this.updateChartContents();
88 this.classList.add('hidden');
93 this.classList.remove('hidden');
95 if (this.pictureDataProcessed_)
98 this.processPictureData_();
99 this.requiresRedraw = true;
100 this.updateChartContents();
104 onMouseMove_: function(e) {
106 var lastBarMouseOverTarget = this.currentBarMouseOverTarget_;
107 this.currentBarMouseOverTarget_ = null;
112 var chartLeft = CHART_PADDING_LEFT;
113 var chartRight = this.chartWidth_ - CHART_PADDING_RIGHT;
114 var chartTop = AXIS_PADDING_TOP;
115 var chartBottom = this.chartHeight_ - AXIS_PADDING_BOTTOM;
116 var chartInnerWidth = chartRight - chartLeft;
118 if (x > chartLeft && x < chartRight && y > chartTop && y < chartBottom) {
120 this.currentBarMouseOverTarget_ = Math.floor(
121 (x - chartLeft) / chartInnerWidth * this.opsTimingData_.length);
123 this.currentBarMouseOverTarget_ = tv.clamp(
124 this.currentBarMouseOverTarget_, 0, this.opsTimingData_.length - 1);
128 if (this.currentBarMouseOverTarget_ === lastBarMouseOverTarget)
131 this.drawChartContents_();
134 updateChartContents: function() {
136 if (this.requiresRedraw)
137 this.updateChartDimensions_();
139 this.drawChartContents_();
142 updateChartDimensions_: function() {
143 this.chartWidth_ = this.offsetWidth;
144 this.chartHeight_ = this.offsetHeight;
146 // Scale up the canvas according to the devicePixelRatio, then reduce it
147 // down again via CSS. Finally we apply a scale to the canvas so that
148 // things are drawn at the correct size.
149 this.chart_.width = this.chartWidth_ * this.chartScale_;
150 this.chart_.height = this.chartHeight_ * this.chartScale_;
152 this.chart_.style.width = this.chartWidth_ + 'px';
153 this.chart_.style.height = this.chartHeight_ + 'px';
155 this.chartCtx_.scale(this.chartScale_, this.chartScale_);
158 processPictureData_: function() {
160 this.resetOpsTimingData_();
161 this.pictureDataProcessed_ = true;
166 var ops = this.picture_.getOps();
170 ops = this.picture_.tagOpsWithTimings(ops);
172 // Check that there are valid times.
173 if (ops[0].cmd_time === undefined)
176 this.collapseOpsToTimingBuckets_(ops);
179 drawChartContents_: function() {
181 this.clearChartContents_();
183 if (this.opsTimingData_.length === 0) {
184 this.showNoTimingDataMessage_();
188 this.drawChartAxes_();
190 this.drawLineAtBottomOfChart_();
192 if (this.currentBarMouseOverTarget_ === null)
198 drawLineAtBottomOfChart_: function() {
199 this.chartCtx_.strokeStyle = '#AAA';
200 this.chartCtx_.moveTo(0, this.chartHeight_ - 0.5);
201 this.chartCtx_.lineTo(this.chartWidth_, this.chartHeight_ - 0.5);
202 this.chartCtx_.stroke();
205 drawTooltip_: function() {
207 var tooltipData = this.opsTimingData_[this.currentBarMouseOverTarget_];
208 var tooltipTitle = tooltipData.cmd_string;
209 var tooltipTime = tooltipData.cmd_time.toFixed(4);
211 var tooltipWidth = 110;
212 var tooltipHeight = 40;
213 var chartInnerWidth = this.chartWidth_ - CHART_PADDING_RIGHT -
215 var barWidth = chartInnerWidth / this.opsTimingData_.length;
216 var tooltipOffset = Math.round((tooltipWidth - barWidth) * 0.5);
218 var left = CHART_PADDING_LEFT + this.currentBarMouseOverTarget_ *
219 barWidth - tooltipOffset;
220 var top = Math.round((this.chartHeight_ - tooltipHeight) * 0.5);
222 this.chartCtx_.save();
224 this.chartCtx_.shadowOffsetX = 0;
225 this.chartCtx_.shadowOffsetY = 5;
226 this.chartCtx_.shadowBlur = 4;
227 this.chartCtx_.shadowColor = 'rgba(0,0,0,0.4)';
229 this.chartCtx_.strokeStyle = '#888';
230 this.chartCtx_.fillStyle = '#EEE';
231 this.chartCtx_.fillRect(left, top, tooltipWidth, tooltipHeight);
233 this.chartCtx_.shadowColor = 'transparent';
234 this.chartCtx_.translate(0.5, 0.5);
235 this.chartCtx_.strokeRect(left, top, tooltipWidth, tooltipHeight);
237 this.chartCtx_.restore();
239 this.chartCtx_.fillStyle = '#222';
240 this.chartCtx_.textBaseline = 'top';
241 this.chartCtx_.font = '800 12px Arial';
242 this.chartCtx_.fillText(tooltipTitle, left + 8, top + 8);
244 this.chartCtx_.fillStyle = '#555';
245 this.chartCtx_.textBaseline = 'top';
246 this.chartCtx_.font = '400 italic 10px Arial';
247 this.chartCtx_.fillText('Total: ' + tooltipTime + 'ms',
251 drawBars_: function() {
253 var len = this.opsTimingData_.length;
254 var max = this.opsTimingData_[0].cmd_time;
255 var min = this.opsTimingData_[len - 1].cmd_time;
257 var width = this.chartWidth_ - CHART_PADDING_LEFT - CHART_PADDING_RIGHT;
258 var height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM;
259 var barWidth = Math.floor(width / len);
267 for (var b = 0; b < len; b++) {
269 opData = this.opsTimingData_[b];
270 opTiming = opData.cmd_time / max;
272 opHeight = Math.round(Math.max(1, opTiming * height));
273 opLabel = opData.cmd_string;
274 barLeft = CHART_PADDING_LEFT + b * barWidth;
276 this.chartCtx_.fillStyle = this.getOpColor_(opLabel);
278 this.chartCtx_.fillRect(barLeft + BAR_PADDING, AXIS_PADDING_TOP +
279 height - opHeight, barWidth - 2 * BAR_PADDING, opHeight);
284 getOpColor_: function(opName) {
286 var characters = opName.split('');
287 var hue = characters.reduce(this.reduceNameToHue, 0) % 360;
289 return 'hsl(' + hue + ', 30%, 50%)';
292 reduceNameToHue: function(previousValue, currentValue, index, array) {
293 // Get the char code and apply a magic adjustment value so we get
294 // pretty colors from around the rainbow.
295 return Math.round(previousValue + currentValue.charCodeAt(0) *
296 HUE_CHAR_CODE_ADJUSTMENT);
299 drawChartAxes_: function() {
301 var len = this.opsTimingData_.length;
302 var max = this.opsTimingData_[0].cmd_time;
303 var min = this.opsTimingData_[len - 1].cmd_time;
305 var width = this.chartWidth_ - AXIS_PADDING_LEFT - AXIS_PADDING_RIGHT;
306 var height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM;
308 var totalBarWidth = this.chartWidth_ - CHART_PADDING_LEFT -
310 var barWidth = Math.floor(totalBarWidth / len);
311 var tickYInterval = height / (VERTICAL_TICKS - 1);
312 var tickYPosition = 0;
313 var tickValInterval = (max - min) / (VERTICAL_TICKS - 1);
316 this.chartCtx_.fillStyle = '#333';
317 this.chartCtx_.strokeStyle = '#777';
318 this.chartCtx_.save();
320 // Translate half a pixel to avoid blurry lines.
321 this.chartCtx_.translate(0.5, 0.5);
325 this.chartCtx_.save();
327 this.chartCtx_.translate(AXIS_PADDING_LEFT, AXIS_PADDING_TOP);
328 this.chartCtx_.moveTo(0, 0);
329 this.chartCtx_.lineTo(0, height);
330 this.chartCtx_.lineTo(width, height);
333 this.chartCtx_.font = '10px Arial';
334 this.chartCtx_.textAlign = 'right';
335 this.chartCtx_.textBaseline = 'middle';
337 for (var t = 0; t < VERTICAL_TICKS; t++) {
339 tickYPosition = Math.round(t * tickYInterval);
340 tickVal = (max - t * tickValInterval).toFixed(4);
342 this.chartCtx_.moveTo(0, tickYPosition);
343 this.chartCtx_.lineTo(-AXIS_TICK_SIZE, tickYPosition);
344 this.chartCtx_.fillText(tickVal,
345 -AXIS_TICK_SIZE - AXIS_LABEL_PADDING, tickYPosition);
349 this.chartCtx_.stroke();
351 this.chartCtx_.restore();
356 this.chartCtx_.save();
358 this.chartCtx_.translate(CHART_PADDING_LEFT + Math.round(barWidth * 0.5),
359 AXIS_PADDING_TOP + height + LABEL_PADDING);
361 this.chartCtx_.font = '10px Arial';
362 this.chartCtx_.textAlign = 'center';
363 this.chartCtx_.textBaseline = 'top';
367 for (var l = 0; l < len; l++) {
369 labelTickLeft = Math.round(l * barWidth);
370 labelTickBottom = l % 2 * LABEL_INTERLEAVE_OFFSET;
372 this.chartCtx_.save();
373 this.chartCtx_.moveTo(labelTickLeft, -LABEL_PADDING);
374 this.chartCtx_.lineTo(labelTickLeft, labelTickBottom);
375 this.chartCtx_.stroke();
376 this.chartCtx_.restore();
378 this.chartCtx_.fillText(this.opsTimingData_[l].cmd_string,
379 labelTickLeft, labelTickBottom);
382 this.chartCtx_.restore();
384 this.chartCtx_.restore();
387 clearChartContents_: function() {
388 this.chartCtx_.clearRect(0, 0, this.chartWidth_, this.chartHeight_);
391 showNoTimingDataMessage_: function() {
392 this.chartCtx_.font = '800 italic 14px Arial';
393 this.chartCtx_.fillStyle = '#333';
394 this.chartCtx_.textAlign = 'center';
395 this.chartCtx_.textBaseline = 'middle';
396 this.chartCtx_.fillText('No timing data available.',
397 this.chartWidth_ * 0.5, this.chartHeight_ * 0.5);
400 collapseOpsToTimingBuckets_: function(ops) {
402 var opsTimingDataIndexHash_ = {};
403 var timingData = this.opsTimingData_;
407 for (var i = 0; i < ops.length; i++) {
411 if (op.cmd_time === undefined)
414 // Try to locate the entry for the current operation
415 // based on its name. If that fails, then create one for it.
416 opIndex = opsTimingDataIndexHash_[op.cmd_string] || null;
418 if (opIndex === null) {
421 cmd_string: op.cmd_string
424 opIndex = timingData.length - 1;
425 opsTimingDataIndexHash_[op.cmd_string] = opIndex;
428 timingData[opIndex].cmd_time += op.cmd_time;
432 timingData.sort(this.sortTimingBucketsByOpTimeDescending_);
434 this.collapseTimingBucketsToOther_(4);
437 collapseTimingBucketsToOther_: function(count) {
439 var timingData = this.opsTimingData_;
440 var otherSource = timingData.splice(count, timingData.length - count);
441 var otherDestination = null;
443 if (!otherSource.length)
451 otherDestination = timingData[timingData.length - 1];
452 for (var i = 0; i < otherSource.length; i++) {
453 otherDestination.cmd_time += otherSource[i].cmd_time;
457 sortTimingBucketsByOpTimeDescending_: function(a, b) {
458 return b.cmd_time - a.cmd_time;
461 resetOpsTimingData_: function() {
462 this.opsTimingData_.length = 0;
467 PictureOpsChartSummaryView: PictureOpsChartSummaryView