1 /** 2 * Copyright (C) 2014 Google Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * * Neither the name of Google Inc. nor the names of its 15 * contributors may be used to endorse or promote products derived from 16 * this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 31 32 /** 33 * @constructor 34 * @implements {WebInspector.FlameChartDataProvider} 35 * @param {!WebInspector.CPUProfileDataModel} cpuProfile 36 * @param {?WebInspector.Target} target 37 */ 38 WebInspector.CPUFlameChartDataProvider = function(cpuProfile, target) 39 { 40 WebInspector.FlameChartDataProvider.call(this); 41 this._cpuProfile = cpuProfile; 42 this._target = target; 43 this._colorGenerator = WebInspector.CPUFlameChartDataProvider.colorGenerator(); 44 } 45 46 WebInspector.CPUFlameChartDataProvider.prototype = { 47 /** 48 * @return {number} 49 */ 50 barHeight: function() 51 { 52 return 15; 53 }, 54 55 /** 56 * @return {number} 57 */ 58 textBaseline: function() 59 { 60 return 4; 61 }, 62 63 /** 64 * @return {number} 65 */ 66 textPadding: function() 67 { 68 return 2; 69 }, 70 71 /** 72 * @param {number} startTime 73 * @param {number} endTime 74 * @return {?Array.<number>} 75 */ 76 dividerOffsets: function(startTime, endTime) 77 { 78 return null; 79 }, 80 81 /** 82 * @return {number} 83 */ 84 minimumBoundary: function() 85 { 86 return this._cpuProfile.profileStartTime; 87 }, 88 89 /** 90 * @return {number} 91 */ 92 totalTime: function() 93 { 94 return this._cpuProfile.profileHead.totalTime; 95 }, 96 97 /** 98 * @return {number} 99 */ 100 maxStackDepth: function() 101 { 102 return this._maxStackDepth; 103 }, 104 105 /** 106 * @return {?WebInspector.FlameChart.TimelineData} 107 */ 108 timelineData: function() 109 { 110 return this._timelineData || this._calculateTimelineData(); 111 }, 112 113 /** 114 * @param {number} index 115 * @return {string} 116 */ 117 markerColor: function(index) 118 { 119 throw new Error("Unreachable."); 120 }, 121 122 /** 123 * @param {number} index 124 * @return {string} 125 */ 126 markerTitle: function(index) 127 { 128 throw new Error("Unreachable."); 129 }, 130 131 /** 132 * @return {?WebInspector.FlameChart.TimelineData} 133 */ 134 _calculateTimelineData: function() 135 { 136 /** 137 * @constructor 138 * @param {number} depth 139 * @param {number} duration 140 * @param {number} startTime 141 * @param {number} selfTime 142 * @param {!ProfilerAgent.CPUProfileNode} node 143 */ 144 function ChartEntry(depth, duration, startTime, selfTime, node) 145 { 146 this.depth = depth; 147 this.duration = duration; 148 this.startTime = startTime; 149 this.selfTime = selfTime; 150 this.node = node; 151 } 152 153 /** @type {!Array.<?ChartEntry>} */ 154 var entries = []; 155 /** @type {!Array.<number>} */ 156 var stack = []; 157 var maxDepth = 5; 158 159 function onOpenFrame() 160 { 161 stack.push(entries.length); 162 // Reserve space for the entry, as they have to be ordered by startTime. 163 // The entry itself will be put there in onCloseFrame. 164 entries.push(null); 165 } 166 function onCloseFrame(depth, node, startTime, totalTime, selfTime) 167 { 168 var index = stack.pop(); 169 entries[index] = new ChartEntry(depth, totalTime, startTime, selfTime, node); 170 maxDepth = Math.max(maxDepth, depth); 171 } 172 this._cpuProfile.forEachFrame(onOpenFrame, onCloseFrame); 173 174 /** @type {!Array.<!ProfilerAgent.CPUProfileNode>} */ 175 var entryNodes = new Array(entries.length); 176 var entryLevels = new Uint8Array(entries.length); 177 var entryTotalTimes = new Float32Array(entries.length); 178 var entrySelfTimes = new Float32Array(entries.length); 179 var entryStartTimes = new Float64Array(entries.length); 180 var minimumBoundary = this.minimumBoundary(); 181 182 for (var i = 0; i < entries.length; ++i) { 183 var entry = entries[i]; 184 entryNodes[i] = entry.node; 185 entryLevels[i] = entry.depth; 186 entryTotalTimes[i] = entry.duration; 187 entryStartTimes[i] = entry.startTime; 188 entrySelfTimes[i] = entry.selfTime; 189 } 190 191 this._maxStackDepth = maxDepth; 192 193 this._timelineData = new WebInspector.FlameChart.TimelineData(entryLevels, entryTotalTimes, entryStartTimes); 194 195 /** @type {!Array.<!ProfilerAgent.CPUProfileNode>} */ 196 this._entryNodes = entryNodes; 197 this._entrySelfTimes = entrySelfTimes; 198 199 return this._timelineData; 200 }, 201 202 /** 203 * @param {number} ms 204 * @return {string} 205 */ 206 _millisecondsToString: function(ms) 207 { 208 if (ms === 0) 209 return "0"; 210 if (ms < 1000) 211 return WebInspector.UIString("%.1f\u2009ms", ms); 212 return Number.secondsToString(ms / 1000, true); 213 }, 214 215 /** 216 * @param {number} entryIndex 217 * @return {?Array.<!{title: string, text: string}>} 218 */ 219 prepareHighlightedEntryInfo: function(entryIndex) 220 { 221 var timelineData = this._timelineData; 222 var node = this._entryNodes[entryIndex]; 223 if (!node) 224 return null; 225 226 var entryInfo = []; 227 function pushEntryInfoRow(title, text) 228 { 229 var row = {}; 230 row.title = title; 231 row.text = text; 232 entryInfo.push(row); 233 } 234 235 var name = WebInspector.CPUProfileDataModel.beautifyFunctionName(node.functionName); 236 pushEntryInfoRow(WebInspector.UIString("Name"), name); 237 var selfTime = this._millisecondsToString(this._entrySelfTimes[entryIndex]); 238 var totalTime = this._millisecondsToString(timelineData.entryTotalTimes[entryIndex]); 239 pushEntryInfoRow(WebInspector.UIString("Self time"), selfTime); 240 pushEntryInfoRow(WebInspector.UIString("Total time"), totalTime); 241 var text = this._target ? WebInspector.Linkifier.liveLocationText(this._target, node.scriptId, node.lineNumber, node.columnNumber) : node.url; 242 pushEntryInfoRow(WebInspector.UIString("URL"), text); 243 pushEntryInfoRow(WebInspector.UIString("Aggregated self time"), Number.secondsToString(node.selfTime / 1000, true)); 244 pushEntryInfoRow(WebInspector.UIString("Aggregated total time"), Number.secondsToString(node.totalTime / 1000, true)); 245 if (node.deoptReason && node.deoptReason !== "no reason") 246 pushEntryInfoRow(WebInspector.UIString("Not optimized"), node.deoptReason); 247 248 return entryInfo; 249 }, 250 251 /** 252 * @param {number} entryIndex 253 * @return {boolean} 254 */ 255 canJumpToEntry: function(entryIndex) 256 { 257 return this._entryNodes[entryIndex].scriptId !== "0"; 258 }, 259 260 /** 261 * @param {number} entryIndex 262 * @return {?string} 263 */ 264 entryTitle: function(entryIndex) 265 { 266 var node = this._entryNodes[entryIndex]; 267 return WebInspector.CPUProfileDataModel.beautifyFunctionName(node.functionName); 268 }, 269 270 /** 271 * @param {number} entryIndex 272 * @return {?string} 273 */ 274 entryFont: function(entryIndex) 275 { 276 if (!this._font) { 277 this._font = (this.barHeight() - 4) + "px " + WebInspector.fontFamily(); 278 this._boldFont = "bold " + this._font; 279 } 280 var node = this._entryNodes[entryIndex]; 281 var reason = node.deoptReason; 282 return (reason && reason !== "no reason") ? this._boldFont : this._font; 283 }, 284 285 /** 286 * @param {number} entryIndex 287 * @return {string} 288 */ 289 entryColor: function(entryIndex) 290 { 291 var node = this._entryNodes[entryIndex]; 292 return this._colorGenerator.colorForID(node.functionName + ":" + node.url); 293 }, 294 295 /** 296 * @param {number} entryIndex 297 * @param {!CanvasRenderingContext2D} context 298 * @param {?string} text 299 * @param {number} barX 300 * @param {number} barY 301 * @param {number} barWidth 302 * @param {number} barHeight 303 * @param {function(number):number} timeToPosition 304 * @return {boolean} 305 */ 306 decorateEntry: function(entryIndex, context, text, barX, barY, barWidth, barHeight, timeToPosition) 307 { 308 return false; 309 }, 310 311 /** 312 * @param {number} entryIndex 313 * @return {boolean} 314 */ 315 forceDecoration: function(entryIndex) 316 { 317 return false; 318 }, 319 320 /** 321 * @param {number} entryIndex 322 * @return {!{startTime: number, endTime: number}} 323 */ 324 highlightTimeRange: function(entryIndex) 325 { 326 var startTime = this._timelineData.entryStartTimes[entryIndex]; 327 return { 328 startTime: startTime, 329 endTime: startTime + this._timelineData.entryTotalTimes[entryIndex] 330 }; 331 }, 332 333 /** 334 * @return {number} 335 */ 336 paddingLeft: function() 337 { 338 return 15; 339 }, 340 341 /** 342 * @param {number} entryIndex 343 * @return {string} 344 */ 345 textColor: function(entryIndex) 346 { 347 return "#333"; 348 } 349 } 350 351 352 /** 353 * @return {!WebInspector.FlameChart.ColorGenerator} 354 */ 355 WebInspector.CPUFlameChartDataProvider.colorGenerator = function() 356 { 357 if (!WebInspector.CPUFlameChartDataProvider._colorGenerator) { 358 var colorGenerator = new WebInspector.FlameChart.ColorGenerator( 359 { min: 180, max: 310, count: 7 }, 360 { min: 50, max: 80, count: 5 }, 361 { min: 80, max: 90, count: 3 }); 362 colorGenerator.setColorForID("(idle):", "hsl(0, 0%, 94%)"); 363 colorGenerator.setColorForID("(program):", "hsl(0, 0%, 80%)"); 364 colorGenerator.setColorForID("(garbage collector):", "hsl(0, 0%, 80%)"); 365 WebInspector.CPUFlameChartDataProvider._colorGenerator = colorGenerator; 366 } 367 return WebInspector.CPUFlameChartDataProvider._colorGenerator; 368 } 369 370 371 /** 372 * @constructor 373 * @extends {WebInspector.VBox} 374 * @param {!WebInspector.FlameChartDataProvider} dataProvider 375 */ 376 WebInspector.CPUProfileFlameChart = function(dataProvider) 377 { 378 WebInspector.VBox.call(this); 379 this.registerRequiredCSS("flameChart.css"); 380 this.element.id = "cpu-flame-chart"; 381 382 this._overviewPane = new WebInspector.CPUProfileFlameChart.OverviewPane(dataProvider); 383 this._overviewPane.show(this.element); 384 385 this._mainPane = new WebInspector.FlameChart(dataProvider, this._overviewPane, true); 386 this._mainPane.show(this.element); 387 this._mainPane.addEventListener(WebInspector.FlameChart.Events.EntrySelected, this._onEntrySelected, this); 388 this._overviewPane.addEventListener(WebInspector.OverviewGrid.Events.WindowChanged, this._onWindowChanged, this); 389 } 390 391 WebInspector.CPUProfileFlameChart.prototype = { 392 /** 393 * @param {!WebInspector.Event} event 394 */ 395 _onWindowChanged: function(event) 396 { 397 var windowLeft = event.data.windowTimeLeft; 398 var windowRight = event.data.windowTimeRight; 399 this._mainPane.setWindowTimes(windowLeft, windowRight); 400 }, 401 402 /** 403 * @param {!number} timeLeft 404 * @param {!number} timeRight 405 */ 406 selectRange: function(timeLeft, timeRight) 407 { 408 this._overviewPane._selectRange(timeLeft, timeRight); 409 }, 410 411 /** 412 * @param {!WebInspector.Event} event 413 */ 414 _onEntrySelected: function(event) 415 { 416 this.dispatchEventToListeners(WebInspector.FlameChart.Events.EntrySelected, event.data); 417 }, 418 419 update: function() 420 { 421 this._overviewPane.update(); 422 this._mainPane.update(); 423 }, 424 425 __proto__: WebInspector.VBox.prototype 426 }; 427 428 /** 429 * @constructor 430 * @implements {WebInspector.TimelineGrid.Calculator} 431 */ 432 WebInspector.CPUProfileFlameChart.OverviewCalculator = function() 433 { 434 } 435 436 WebInspector.CPUProfileFlameChart.OverviewCalculator.prototype = { 437 /** 438 * @return {number} 439 */ 440 paddingLeft: function() 441 { 442 return 0; 443 }, 444 445 /** 446 * @param {!WebInspector.CPUProfileFlameChart.OverviewPane} overviewPane 447 */ 448 _updateBoundaries: function(overviewPane) 449 { 450 this._minimumBoundaries = overviewPane._dataProvider.minimumBoundary(); 451 var totalTime = overviewPane._dataProvider.totalTime(); 452 this._maximumBoundaries = this._minimumBoundaries + totalTime; 453 this._xScaleFactor = overviewPane._overviewContainer.clientWidth / totalTime; 454 }, 455 456 /** 457 * @param {number} time 458 * @return {number} 459 */ 460 computePosition: function(time) 461 { 462 return (time - this._minimumBoundaries) * this._xScaleFactor; 463 }, 464 465 /** 466 * @param {number} value 467 * @param {number=} precision 468 * @return {string} 469 */ 470 formatTime: function(value, precision) 471 { 472 return Number.secondsToString((value - this._minimumBoundaries) / 1000); 473 }, 474 475 /** 476 * @return {number} 477 */ 478 maximumBoundary: function() 479 { 480 return this._maximumBoundaries; 481 }, 482 483 /** 484 * @return {number} 485 */ 486 minimumBoundary: function() 487 { 488 return this._minimumBoundaries; 489 }, 490 491 /** 492 * @return {number} 493 */ 494 zeroTime: function() 495 { 496 return this._minimumBoundaries; 497 }, 498 499 /** 500 * @return {number} 501 */ 502 boundarySpan: function() 503 { 504 return this._maximumBoundaries - this._minimumBoundaries; 505 } 506 } 507 508 /** 509 * @constructor 510 * @extends {WebInspector.VBox} 511 * @implements {WebInspector.FlameChartDelegate} 512 * @param {!WebInspector.FlameChartDataProvider} dataProvider 513 */ 514 WebInspector.CPUProfileFlameChart.OverviewPane = function(dataProvider) 515 { 516 WebInspector.VBox.call(this); 517 this.element.classList.add("flame-chart-overview-pane"); 518 this._overviewContainer = this.element.createChild("div", "overview-container"); 519 this._overviewGrid = new WebInspector.OverviewGrid("flame-chart"); 520 this._overviewGrid.element.classList.add("fill"); 521 this._overviewCanvas = this._overviewContainer.createChild("canvas", "flame-chart-overview-canvas"); 522 this._overviewContainer.appendChild(this._overviewGrid.element); 523 this._overviewCalculator = new WebInspector.CPUProfileFlameChart.OverviewCalculator(); 524 this._dataProvider = dataProvider; 525 this._overviewGrid.addEventListener(WebInspector.OverviewGrid.Events.WindowChanged, this._onWindowChanged, this); 526 } 527 528 WebInspector.CPUProfileFlameChart.OverviewPane.prototype = { 529 /** 530 * @param {number} windowStartTime 531 * @param {number} windowEndTime 532 */ 533 requestWindowTimes: function(windowStartTime, windowEndTime) 534 { 535 this._selectRange(windowStartTime, windowEndTime); 536 }, 537 538 /** 539 * @param {!number} timeLeft 540 * @param {!number} timeRight 541 */ 542 _selectRange: function(timeLeft, timeRight) 543 { 544 var startTime = this._dataProvider.minimumBoundary(); 545 var totalTime = this._dataProvider.totalTime(); 546 this._overviewGrid.setWindow((timeLeft - startTime) / totalTime, (timeRight - startTime) / totalTime); 547 }, 548 549 /** 550 * @param {!WebInspector.Event} event 551 */ 552 _onWindowChanged: function(event) 553 { 554 var startTime = this._dataProvider.minimumBoundary(); 555 var totalTime = this._dataProvider.totalTime(); 556 var data = { 557 windowTimeLeft: startTime + this._overviewGrid.windowLeft() * totalTime, 558 windowTimeRight: startTime + this._overviewGrid.windowRight() * totalTime 559 }; 560 this.dispatchEventToListeners(WebInspector.OverviewGrid.Events.WindowChanged, data); 561 }, 562 563 /** 564 * @return {?WebInspector.FlameChart.TimelineData} 565 */ 566 _timelineData: function() 567 { 568 return this._dataProvider.timelineData(); 569 }, 570 571 onResize: function() 572 { 573 this._scheduleUpdate(); 574 }, 575 576 _scheduleUpdate: function() 577 { 578 if (this._updateTimerId) 579 return; 580 this._updateTimerId = requestAnimationFrame(this.update.bind(this)); 581 }, 582 583 update: function() 584 { 585 this._updateTimerId = 0; 586 var timelineData = this._timelineData(); 587 if (!timelineData) 588 return; 589 this._resetCanvas(this._overviewContainer.clientWidth, this._overviewContainer.clientHeight - WebInspector.FlameChart.DividersBarHeight); 590 this._overviewCalculator._updateBoundaries(this); 591 this._overviewGrid.updateDividers(this._overviewCalculator); 592 this._drawOverviewCanvas(); 593 }, 594 595 _drawOverviewCanvas: function() 596 { 597 var canvasWidth = this._overviewCanvas.width; 598 var canvasHeight = this._overviewCanvas.height; 599 var drawData = this._calculateDrawData(canvasWidth); 600 var context = this._overviewCanvas.getContext("2d"); 601 var ratio = window.devicePixelRatio; 602 var offsetFromBottom = ratio; 603 var lineWidth = 1; 604 var yScaleFactor = canvasHeight / (this._dataProvider.maxStackDepth() * 1.1); 605 context.lineWidth = lineWidth; 606 context.translate(0.5, 0.5); 607 context.strokeStyle = "rgba(20,0,0,0.4)"; 608 context.fillStyle = "rgba(214,225,254,0.8)"; 609 context.moveTo(-lineWidth, canvasHeight + lineWidth); 610 context.lineTo(-lineWidth, Math.round(canvasHeight - drawData[0] * yScaleFactor - offsetFromBottom)); 611 var value; 612 for (var x = 0; x < canvasWidth; ++x) { 613 value = Math.round(canvasHeight - drawData[x] * yScaleFactor - offsetFromBottom); 614 context.lineTo(x, value); 615 } 616 context.lineTo(canvasWidth + lineWidth, value); 617 context.lineTo(canvasWidth + lineWidth, canvasHeight + lineWidth); 618 context.fill(); 619 context.stroke(); 620 context.closePath(); 621 }, 622 623 /** 624 * @param {number} width 625 * @return {!Uint8Array} 626 */ 627 _calculateDrawData: function(width) 628 { 629 var dataProvider = this._dataProvider; 630 var timelineData = this._timelineData(); 631 var entryStartTimes = timelineData.entryStartTimes; 632 var entryTotalTimes = timelineData.entryTotalTimes; 633 var entryLevels = timelineData.entryLevels; 634 var length = entryStartTimes.length; 635 var minimumBoundary = this._dataProvider.minimumBoundary(); 636 637 var drawData = new Uint8Array(width); 638 var scaleFactor = width / dataProvider.totalTime(); 639 640 for (var entryIndex = 0; entryIndex < length; ++entryIndex) { 641 var start = Math.floor((entryStartTimes[entryIndex] - minimumBoundary) * scaleFactor); 642 var finish = Math.floor((entryStartTimes[entryIndex] - minimumBoundary + entryTotalTimes[entryIndex]) * scaleFactor); 643 for (var x = start; x <= finish; ++x) 644 drawData[x] = Math.max(drawData[x], entryLevels[entryIndex] + 1); 645 } 646 return drawData; 647 }, 648 649 /** 650 * @param {!number} width 651 * @param {!number} height 652 */ 653 _resetCanvas: function(width, height) 654 { 655 var ratio = window.devicePixelRatio; 656 this._overviewCanvas.width = width * ratio; 657 this._overviewCanvas.height = height * ratio; 658 this._overviewCanvas.style.width = width + "px"; 659 this._overviewCanvas.style.height = height + "px"; 660 }, 661 662 __proto__: WebInspector.VBox.prototype 663 } 664