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" href="/tracing/ui/extras/chrome/cc/picture_ops_chart_view.css"> 9 10<link rel="import" href="/tracing/ui/base/dom_helpers.html"> 11 12<script> 13'use strict'; 14 15tr.exportTo('tr.ui.e.chrome.cc', function() { 16 var BAR_PADDING = 1; 17 var BAR_WIDTH = 5; 18 var CHART_PADDING_LEFT = 65; 19 var CHART_PADDING_RIGHT = 30; 20 var CHART_PADDING_BOTTOM = 35; 21 var CHART_PADDING_TOP = 20; 22 var AXIS_PADDING_LEFT = 55; 23 var AXIS_PADDING_RIGHT = 30; 24 var AXIS_PADDING_BOTTOM = 35; 25 var AXIS_PADDING_TOP = 20; 26 var AXIS_TICK_SIZE = 5; 27 var AXIS_LABEL_PADDING = 5; 28 var VERTICAL_TICKS = 5; 29 var HUE_CHAR_CODE_ADJUSTMENT = 5.7; 30 31 /** 32 * Provides a chart showing the cumulative time spent in Skia operations 33 * during picture rasterization. 34 * 35 * @constructor 36 */ 37 var PictureOpsChartView = 38 tr.ui.b.define('tr-ui-e-chrome-cc-picture-ops-chart-view'); 39 40 PictureOpsChartView.prototype = { 41 __proto__: HTMLUnknownElement.prototype, 42 43 decorate: function() { 44 this.picture_ = undefined; 45 this.pictureOps_ = undefined; 46 this.opCosts_ = undefined; 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.selectedOpIndex_ = undefined; 55 this.chartWidth_ = 0; 56 this.chartHeight_ = 0; 57 this.dimensionsHaveChanged_ = true; 58 59 this.currentBarMouseOverTarget_ = undefined; 60 61 this.ninetyFifthPercentileCost_ = 0; 62 this.totalOpCost_ = 0; 63 64 this.chart_.addEventListener('click', this.onClick_.bind(this)); 65 this.chart_.addEventListener('mousemove', this.onMouseMove_.bind(this)); 66 67 this.usePercentileScale_ = false; 68 this.usePercentileScaleCheckbox_ = tr.ui.b.createCheckBox( 69 this, 'usePercentileScale', 70 'PictureOpsChartView.usePercentileScale', false, 71 'Limit to 95%-ile'); 72 this.usePercentileScaleCheckbox_.classList.add('use-percentile-scale'); 73 this.appendChild(this.usePercentileScaleCheckbox_); 74 }, 75 76 get dimensionsHaveChanged() { 77 return this.dimensionsHaveChanged_; 78 }, 79 80 set dimensionsHaveChanged(dimensionsHaveChanged) { 81 this.dimensionsHaveChanged_ = dimensionsHaveChanged; 82 }, 83 84 get usePercentileScale() { 85 return this.usePercentileScale_; 86 }, 87 88 set usePercentileScale(usePercentileScale) { 89 this.usePercentileScale_ = usePercentileScale; 90 this.drawChartContents_(); 91 }, 92 93 get numOps() { 94 return this.opCosts_.length; 95 }, 96 97 get selectedOpIndex() { 98 return this.selectedOpIndex_; 99 }, 100 101 set selectedOpIndex(selectedOpIndex) { 102 if (selectedOpIndex < 0) throw new Error('Invalid index'); 103 if (selectedOpIndex >= this.numOps) throw new Error('Invalid index'); 104 105 this.selectedOpIndex_ = selectedOpIndex; 106 }, 107 108 get picture() { 109 return this.picture_; 110 }, 111 112 set picture(picture) { 113 this.picture_ = picture; 114 this.pictureOps_ = picture.tagOpsWithTimings(picture.getOps()); 115 this.currentBarMouseOverTarget_ = undefined; 116 this.processPictureData_(); 117 this.dimensionsHaveChanged = true; 118 }, 119 120 processPictureData_: function() { 121 if (this.pictureOps_ === undefined) 122 return; 123 124 var totalOpCost = 0; 125 126 // Take a copy of the picture ops data for sorting. 127 this.opCosts_ = this.pictureOps_.map(function(op) { 128 totalOpCost += op.cmd_time; 129 return op.cmd_time; 130 }); 131 this.opCosts_.sort(); 132 133 var ninetyFifthPercentileCostIndex = Math.floor( 134 this.opCosts_.length * 0.95); 135 this.ninetyFifthPercentileCost_ = 136 this.opCosts_[ninetyFifthPercentileCostIndex]; 137 this.maxCost_ = this.opCosts_[this.opCosts_.length - 1]; 138 139 this.totalOpCost_ = totalOpCost; 140 }, 141 142 extractBarIndex_: function(e) { 143 144 var index = undefined; 145 146 if (this.pictureOps_ === undefined || 147 this.pictureOps_.length === 0) 148 return index; 149 150 var x = e.offsetX; 151 var y = e.offsetY; 152 153 var totalBarWidth = (BAR_WIDTH + BAR_PADDING) * this.pictureOps_.length; 154 155 var chartLeft = CHART_PADDING_LEFT; 156 var chartTop = 0; 157 var chartBottom = this.chartHeight_ - CHART_PADDING_BOTTOM; 158 var chartRight = chartLeft + totalBarWidth; 159 160 if (x < chartLeft || x > chartRight || y < chartTop || y > chartBottom) 161 return index; 162 163 index = Math.floor((x - chartLeft) / totalBarWidth * 164 this.pictureOps_.length); 165 166 index = tr.b.clamp(index, 0, this.pictureOps_.length - 1); 167 168 return index; 169 }, 170 171 onClick_: function(e) { 172 173 var barClicked = this.extractBarIndex_(e); 174 175 if (barClicked === undefined) 176 return; 177 178 // If we click on the already selected item we should deselect. 179 if (barClicked === this.selectedOpIndex) 180 this.selectedOpIndex = undefined; 181 else 182 this.selectedOpIndex = barClicked; 183 184 e.preventDefault(); 185 186 tr.b.dispatchSimpleEvent(this, 'selection-changed', false); 187 }, 188 189 onMouseMove_: function(e) { 190 191 var lastBarMouseOverTarget = this.currentBarMouseOverTarget_; 192 this.currentBarMouseOverTarget_ = this.extractBarIndex_(e); 193 194 if (this.currentBarMouseOverTarget_ === lastBarMouseOverTarget) 195 return; 196 197 this.drawChartContents_(); 198 }, 199 200 scrollSelectedItemIntoViewIfNecessary: function() { 201 202 if (this.selectedOpIndex === undefined) 203 return; 204 205 var width = this.offsetWidth; 206 var left = this.scrollLeft; 207 var right = left + width; 208 var targetLeft = CHART_PADDING_LEFT + 209 (BAR_WIDTH + BAR_PADDING) * this.selectedOpIndex; 210 211 if (targetLeft > left && targetLeft < right) 212 return; 213 214 this.scrollLeft = (targetLeft - width * 0.5); 215 }, 216 217 updateChartContents: function() { 218 219 if (this.dimensionsHaveChanged) 220 this.updateChartDimensions_(); 221 222 this.drawChartContents_(); 223 }, 224 225 updateChartDimensions_: function() { 226 227 if (!this.pictureOps_) 228 return; 229 230 var width = CHART_PADDING_LEFT + CHART_PADDING_RIGHT + 231 ((BAR_WIDTH + BAR_PADDING) * this.pictureOps_.length); 232 233 if (width < this.offsetWidth) 234 width = this.offsetWidth; 235 236 // Allow the element to be its natural size as set by flexbox, then lock 237 // the width in before we set the width of the canvas. 238 this.chartWidth_ = width; 239 this.chartHeight_ = this.getBoundingClientRect().height; 240 241 // Scale up the canvas according to the devicePixelRatio, then reduce it 242 // down again via CSS. Finally we apply a scale to the canvas so that 243 // things are drawn at the correct size. 244 this.chart_.width = this.chartWidth_ * this.chartScale_; 245 this.chart_.height = this.chartHeight_ * this.chartScale_; 246 247 this.chart_.style.width = this.chartWidth_ + 'px'; 248 this.chart_.style.height = this.chartHeight_ + 'px'; 249 250 this.chartCtx_.scale(this.chartScale_, this.chartScale_); 251 252 this.dimensionsHaveChanged = false; 253 }, 254 255 drawChartContents_: function() { 256 257 this.clearChartContents_(); 258 259 if (this.pictureOps_ === undefined || 260 this.pictureOps_.length === 0 || 261 this.pictureOps_[0].cmd_time === undefined) { 262 263 this.showNoTimingDataMessage_(); 264 return; 265 } 266 267 this.drawSelection_(); 268 this.drawBars_(); 269 this.drawChartAxes_(); 270 this.drawLinesAtTickMarks_(); 271 this.drawLineAtBottomOfChart_(); 272 273 if (this.currentBarMouseOverTarget_ === undefined) 274 return; 275 276 this.drawTooltip_(); 277 }, 278 279 drawSelection_: function() { 280 281 if (this.selectedOpIndex === undefined) 282 return; 283 284 var width = (BAR_WIDTH + BAR_PADDING) * this.selectedOpIndex; 285 this.chartCtx_.fillStyle = 'rgb(223, 235, 230)'; 286 this.chartCtx_.fillRect(CHART_PADDING_LEFT, CHART_PADDING_TOP, 287 width, this.chartHeight_ - CHART_PADDING_TOP - CHART_PADDING_BOTTOM); 288 }, 289 290 drawChartAxes_: function() { 291 292 var min = this.opCosts_[0]; 293 var max = this.opCosts_[this.opCosts_.length - 1]; 294 var height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM; 295 296 var tickYInterval = height / (VERTICAL_TICKS - 1); 297 var tickYPosition = 0; 298 var tickValInterval = (max - min) / (VERTICAL_TICKS - 1); 299 var tickVal = 0; 300 301 this.chartCtx_.fillStyle = '#333'; 302 this.chartCtx_.strokeStyle = '#777'; 303 this.chartCtx_.save(); 304 305 // Translate half a pixel to avoid blurry lines. 306 this.chartCtx_.translate(0.5, 0.5); 307 308 // Sides. 309 this.chartCtx_.beginPath(); 310 this.chartCtx_.moveTo(AXIS_PADDING_LEFT, AXIS_PADDING_TOP); 311 this.chartCtx_.lineTo(AXIS_PADDING_LEFT, this.chartHeight_ - 312 AXIS_PADDING_BOTTOM); 313 this.chartCtx_.lineTo(this.chartWidth_ - AXIS_PADDING_RIGHT, 314 this.chartHeight_ - AXIS_PADDING_BOTTOM); 315 this.chartCtx_.stroke(); 316 this.chartCtx_.closePath(); 317 318 // Y-axis ticks. 319 this.chartCtx_.translate(AXIS_PADDING_LEFT, AXIS_PADDING_TOP); 320 321 this.chartCtx_.font = '10px Arial'; 322 this.chartCtx_.textAlign = 'right'; 323 this.chartCtx_.textBaseline = 'middle'; 324 325 this.chartCtx_.beginPath(); 326 for (var t = 0; t < VERTICAL_TICKS; t++) { 327 328 tickYPosition = Math.round(t * tickYInterval); 329 tickVal = (max - t * tickValInterval).toFixed(4); 330 331 this.chartCtx_.moveTo(0, tickYPosition); 332 this.chartCtx_.lineTo(-AXIS_TICK_SIZE, tickYPosition); 333 this.chartCtx_.fillText(tickVal, 334 -AXIS_TICK_SIZE - AXIS_LABEL_PADDING, tickYPosition); 335 336 } 337 338 this.chartCtx_.stroke(); 339 this.chartCtx_.closePath(); 340 341 this.chartCtx_.restore(); 342 }, 343 344 drawLinesAtTickMarks_: function() { 345 346 var height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM; 347 var width = this.chartWidth_ - AXIS_PADDING_LEFT - AXIS_PADDING_RIGHT; 348 var tickYInterval = height / (VERTICAL_TICKS - 1); 349 var tickYPosition = 0; 350 351 this.chartCtx_.save(); 352 353 this.chartCtx_.translate(AXIS_PADDING_LEFT + 0.5, AXIS_PADDING_TOP + 0.5); 354 this.chartCtx_.beginPath(); 355 this.chartCtx_.strokeStyle = 'rgba(0,0,0,0.05)'; 356 357 for (var t = 0; t < VERTICAL_TICKS; t++) { 358 tickYPosition = Math.round(t * tickYInterval); 359 360 this.chartCtx_.moveTo(0, tickYPosition); 361 this.chartCtx_.lineTo(width, tickYPosition); 362 this.chartCtx_.stroke(); 363 } 364 365 this.chartCtx_.restore(); 366 this.chartCtx_.closePath(); 367 }, 368 369 drawLineAtBottomOfChart_: function() { 370 this.chartCtx_.strokeStyle = '#AAA'; 371 this.chartCtx_.beginPath(); 372 this.chartCtx_.moveTo(0, this.chartHeight_ - 0.5); 373 this.chartCtx_.lineTo(this.chartWidth_, this.chartHeight_ - 0.5); 374 this.chartCtx_.stroke(); 375 this.chartCtx_.closePath(); 376 }, 377 378 drawTooltip_: function() { 379 380 var tooltipData = this.pictureOps_[this.currentBarMouseOverTarget_]; 381 var tooltipTitle = tooltipData.cmd_string; 382 var tooltipTime = tooltipData.cmd_time.toFixed(4); 383 var toolTipTimePercentage = 384 ((tooltipData.cmd_time / this.totalOpCost_) * 100).toFixed(2); 385 386 var tooltipWidth = 120; 387 var tooltipHeight = 40; 388 var chartInnerWidth = this.chartWidth_ - CHART_PADDING_RIGHT - 389 CHART_PADDING_LEFT; 390 var barWidth = BAR_WIDTH + BAR_PADDING; 391 var tooltipOffset = Math.round((tooltipWidth - barWidth) * 0.5); 392 393 var left = CHART_PADDING_LEFT + this.currentBarMouseOverTarget_ * 394 barWidth - tooltipOffset; 395 var top = Math.round((this.chartHeight_ - tooltipHeight) * 0.5); 396 397 this.chartCtx_.save(); 398 399 this.chartCtx_.shadowOffsetX = 0; 400 this.chartCtx_.shadowOffsetY = 5; 401 this.chartCtx_.shadowBlur = 4; 402 this.chartCtx_.shadowColor = 'rgba(0,0,0,0.4)'; 403 404 this.chartCtx_.strokeStyle = '#888'; 405 this.chartCtx_.fillStyle = '#EEE'; 406 this.chartCtx_.fillRect(left, top, tooltipWidth, tooltipHeight); 407 408 this.chartCtx_.shadowColor = 'transparent'; 409 this.chartCtx_.translate(0.5, 0.5); 410 this.chartCtx_.strokeRect(left, top, tooltipWidth, tooltipHeight); 411 412 this.chartCtx_.restore(); 413 414 this.chartCtx_.fillStyle = '#222'; 415 this.chartCtx_.textAlign = 'left'; 416 this.chartCtx_.textBaseline = 'top'; 417 this.chartCtx_.font = '800 12px Arial'; 418 this.chartCtx_.fillText(tooltipTitle, left + 8, top + 8); 419 420 this.chartCtx_.fillStyle = '#555'; 421 this.chartCtx_.font = '400 italic 10px Arial'; 422 this.chartCtx_.fillText(tooltipTime + 'ms (' + 423 toolTipTimePercentage + '%)', left + 8, top + 22); 424 }, 425 426 drawBars_: function() { 427 428 var op; 429 var opColor = 0; 430 var opHeight = 0; 431 var opWidth = BAR_WIDTH + BAR_PADDING; 432 var opHover = false; 433 434 var bottom = this.chartHeight_ - CHART_PADDING_BOTTOM; 435 var maxHeight = this.chartHeight_ - CHART_PADDING_BOTTOM - 436 CHART_PADDING_TOP; 437 438 var maxValue; 439 if (this.usePercentileScale) 440 maxValue = this.ninetyFifthPercentileCost_; 441 else 442 maxValue = this.maxCost_; 443 444 for (var b = 0; b < this.pictureOps_.length; b++) { 445 446 op = this.pictureOps_[b]; 447 opHeight = Math.round( 448 (op.cmd_time / maxValue) * maxHeight); 449 opHeight = Math.max(opHeight, 1); 450 opHover = (b === this.currentBarMouseOverTarget_); 451 opColor = this.getOpColor_(op.cmd_string, opHover); 452 453 if (b === this.selectedOpIndex) 454 this.chartCtx_.fillStyle = '#FFFF00'; 455 else 456 this.chartCtx_.fillStyle = opColor; 457 458 this.chartCtx_.fillRect(CHART_PADDING_LEFT + b * opWidth, 459 bottom - opHeight, BAR_WIDTH, opHeight); 460 } 461 462 }, 463 464 getOpColor_: function(opName, hover) { 465 466 var characters = opName.split(''); 467 468 var hue = characters.reduce(this.reduceNameToHue, 0) % 360; 469 var saturation = 30; 470 var lightness = hover ? '75%' : '50%'; 471 472 return 'hsl(' + hue + ', ' + saturation + '%, ' + lightness + '%)'; 473 }, 474 475 reduceNameToHue: function(previousValue, currentValue, index, array) { 476 // Get the char code and apply a magic adjustment value so we get 477 // pretty colors from around the rainbow. 478 return Math.round(previousValue + currentValue.charCodeAt(0) * 479 HUE_CHAR_CODE_ADJUSTMENT); 480 }, 481 482 clearChartContents_: function() { 483 this.chartCtx_.clearRect(0, 0, this.chartWidth_, this.chartHeight_); 484 }, 485 486 showNoTimingDataMessage_: function() { 487 this.chartCtx_.font = '800 italic 14px Arial'; 488 this.chartCtx_.fillStyle = '#333'; 489 this.chartCtx_.textAlign = 'center'; 490 this.chartCtx_.textBaseline = 'middle'; 491 this.chartCtx_.fillText('No timing data available.', 492 this.chartWidth_ * 0.5, this.chartHeight_ * 0.5); 493 } 494 }; 495 496 return { 497 PictureOpsChartView: PictureOpsChartView 498 }; 499}); 500</script> 501