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 base.requireStylesheet('cc.picture_ops_chart_summary_view');
9 base.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 = ui.define('picture-ops-chart-summary-view');
34 PictureOpsChartSummaryView.prototype = {
35 __proto__: HTMLUnknownElement.prototype,
37 decorate: function() {
38 this.picture_ = undefined;
39 this.pictureDataProcessed_ = false;
41 this.chartScale_ = window.devicePixelRatio;
43 this.chart_ = document.createElement('canvas');
44 this.chartCtx_ = this.chart_.getContext('2d');
45 this.appendChild(this.chart_);
47 this.opsTimingData_ = [];
50 this.chartHeight_ = 0;
51 this.requiresRedraw_ = true;
53 this.currentBarMouseOverTarget_ = null;
55 this.chart_.addEventListener('mousemove', this.onMouseMove_.bind(this));
58 get requiresRedraw() {
59 return this.requiresRedraw_;
62 set requiresRedraw(requiresRedraw) {
63 this.requiresRedraw_ = requiresRedraw;
70 set picture(picture) {
71 this.picture_ = picture;
72 this.pictureDataProcessed_ = false;
74 if (this.classList.contains('hidden'))
77 this.processPictureData_();
78 this.requiresRedraw = true;
79 this.updateChartContents();
83 this.classList.add('hidden');
88 this.classList.remove('hidden');
90 if (this.pictureDataProcessed_)
93 this.processPictureData_();
94 this.requiresRedraw = true;
95 this.updateChartContents();
99 onMouseMove_: function(e) {
101 var lastBarMouseOverTarget = this.currentBarMouseOverTarget_;
102 this.currentBarMouseOverTarget_ = null;
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;
113 if (x > chartLeft && x < chartRight && y > chartTop && y < chartBottom) {
115 this.currentBarMouseOverTarget_ = Math.floor(
116 (x - chartLeft) / chartInnerWidth * this.opsTimingData_.length);
118 this.currentBarMouseOverTarget_ = base.clamp(
119 this.currentBarMouseOverTarget_, 0, this.opsTimingData_.length - 1);
123 if (this.currentBarMouseOverTarget_ === lastBarMouseOverTarget)
126 this.drawChartContents_();
129 updateChartContents: function() {
131 if (this.requiresRedraw)
132 this.updateChartDimensions_();
134 this.drawChartContents_();
137 updateChartDimensions_: function() {
138 this.chartWidth_ = this.offsetWidth;
139 this.chartHeight_ = this.offsetHeight;
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_;
147 this.chart_.style.width = this.chartWidth_ + 'px';
148 this.chart_.style.height = this.chartHeight_ + 'px';
150 this.chartCtx_.scale(this.chartScale_, this.chartScale_);
153 processPictureData_: function() {
155 this.resetOpsTimingData_();
156 this.pictureDataProcessed_ = true;
161 var ops = this.picture_.getOps();
165 ops = this.picture_.tagOpsWithTimings(ops);
167 // Check that there are valid times.
168 if (ops[0].cmd_time === undefined)
171 this.collapseOpsToTimingBuckets_(ops);
174 drawChartContents_: function() {
176 this.clearChartContents_();
178 if (this.opsTimingData_.length === 0) {
179 this.showNoTimingDataMessage_();
183 this.drawChartAxes_();
185 this.drawLineAtBottomOfChart_();
187 if (this.currentBarMouseOverTarget_ === null)
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();
200 drawTooltip_: function() {
202 var tooltipData = this.opsTimingData_[this.currentBarMouseOverTarget_];
203 var tooltipTitle = tooltipData.cmd_string;
204 var tooltipTime = tooltipData.cmd_time.toFixed(4);
206 var tooltipWidth = 110;
207 var tooltipHeight = 40;
208 var chartInnerWidth = this.chartWidth_ - CHART_PADDING_RIGHT -
210 var barWidth = chartInnerWidth / this.opsTimingData_.length;
211 var tooltipOffset = Math.round((tooltipWidth - barWidth) * 0.5);
213 var left = CHART_PADDING_LEFT + this.currentBarMouseOverTarget_ *
214 barWidth - tooltipOffset;
215 var top = Math.round((this.chartHeight_ - tooltipHeight) * 0.5);
217 this.chartCtx_.save();
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)';
224 this.chartCtx_.strokeStyle = '#888';
225 this.chartCtx_.fillStyle = '#EEE';
226 this.chartCtx_.fillRect(left, top, tooltipWidth, tooltipHeight);
228 this.chartCtx_.shadowColor = 'transparent';
229 this.chartCtx_.translate(0.5, 0.5);
230 this.chartCtx_.strokeRect(left, top, tooltipWidth, tooltipHeight);
232 this.chartCtx_.restore();
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);
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',
246 drawBars_: function() {
248 var len = this.opsTimingData_.length;
249 var max = this.opsTimingData_[0].cmd_time;
250 var min = this.opsTimingData_[len - 1].cmd_time;
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);
262 for (var b = 0; b < len; b++) {
264 opData = this.opsTimingData_[b];
265 opTiming = opData.cmd_time / max;
267 opHeight = Math.round(Math.max(1, opTiming * height));
268 opLabel = opData.cmd_string;
269 barLeft = CHART_PADDING_LEFT + b * barWidth;
271 this.chartCtx_.fillStyle = this.getOpColor_(opLabel);
273 this.chartCtx_.fillRect(barLeft + BAR_PADDING, AXIS_PADDING_TOP +
274 height - opHeight, barWidth - 2 * BAR_PADDING, opHeight);
279 getOpColor_: function(opName) {
281 var characters = opName.split('');
282 var hue = characters.reduce(this.reduceNameToHue, 0) % 360;
284 return 'hsl(' + hue + ', 30%, 50%)';
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);
294 drawChartAxes_: function() {
296 var len = this.opsTimingData_.length;
297 var max = this.opsTimingData_[0].cmd_time;
298 var min = this.opsTimingData_[len - 1].cmd_time;
300 var width = this.chartWidth_ - AXIS_PADDING_LEFT - AXIS_PADDING_RIGHT;
301 var height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM;
303 var totalBarWidth = this.chartWidth_ - CHART_PADDING_LEFT -
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);
311 this.chartCtx_.fillStyle = '#333';
312 this.chartCtx_.strokeStyle = '#777';
313 this.chartCtx_.save();
315 // Translate half a pixel to avoid blurry lines.
316 this.chartCtx_.translate(0.5, 0.5);
320 this.chartCtx_.save();
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);
328 this.chartCtx_.font = '10px Arial';
329 this.chartCtx_.textAlign = 'right';
330 this.chartCtx_.textBaseline = 'middle';
332 for (var t = 0; t < VERTICAL_TICKS; t++) {
334 tickYPosition = Math.round(t * tickYInterval);
335 tickVal = (max - t * tickValInterval).toFixed(4);
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);
344 this.chartCtx_.stroke();
346 this.chartCtx_.restore();
351 this.chartCtx_.save();
353 this.chartCtx_.translate(CHART_PADDING_LEFT + Math.round(barWidth * 0.5),
354 AXIS_PADDING_TOP + height + LABEL_PADDING);
356 this.chartCtx_.font = '10px Arial';
357 this.chartCtx_.textAlign = 'center';
358 this.chartCtx_.textBaseline = 'top';
362 for (var l = 0; l < len; l++) {
364 labelTickLeft = Math.round(l * barWidth);
365 labelTickBottom = l % 2 * LABEL_INTERLEAVE_OFFSET;
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();
373 this.chartCtx_.fillText(this.opsTimingData_[l].cmd_string,
374 labelTickLeft, labelTickBottom);
377 this.chartCtx_.restore();
379 this.chartCtx_.restore();
382 clearChartContents_: function() {
383 this.chartCtx_.clearRect(0, 0, this.chartWidth_, this.chartHeight_);
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);
395 collapseOpsToTimingBuckets_: function(ops) {
397 var opsTimingDataIndexHash_ = {};
398 var timingData = this.opsTimingData_;
402 for (var i = 0; i < ops.length; i++) {
406 if (op.cmd_time === undefined)
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;
413 if (opIndex === null) {
416 cmd_string: op.cmd_string
419 opIndex = timingData.length - 1;
420 opsTimingDataIndexHash_[op.cmd_string] = opIndex;
423 timingData[opIndex].cmd_time += op.cmd_time;
427 timingData.sort(this.sortTimingBucketsByOpTimeDescending_);
429 this.collapseTimingBucketsToOther_(4);
432 collapseTimingBucketsToOther_: function(count) {
434 var timingData = this.opsTimingData_;
435 var otherSource = timingData.splice(count, timingData.length - count);
436 var otherDestination = null;
438 if (!otherSource.length)
446 otherDestination = timingData[timingData.length - 1];
447 for (var i = 0; i < otherSource.length; i++) {
448 otherDestination.cmd_time += otherSource[i].cmd_time;
452 sortTimingBucketsByOpTimeDescending_: function(a, b) {
453 return b.cmd_time - a.cmd_time;
456 resetOpsTimingData_: function() {
457 this.opsTimingData_.length = 0;
462 PictureOpsChartSummaryView: PictureOpsChartSummaryView