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