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 * @return {?WebInspector.FlameChart.TimelineData} 115 */ 116 _calculateTimelineData: function() 117 { 118 /** 119 * @constructor 120 * @param {number} depth 121 * @param {number} duration 122 * @param {number} startTime 123 * @param {number} selfTime 124 * @param {!ProfilerAgent.CPUProfileNode} node 125 */ 126 function ChartEntry(depth, duration, startTime, selfTime, node) 127 { 128 this.depth = depth; 129 this.duration = duration; 130 this.startTime = startTime; 131 this.selfTime = selfTime; 132 this.node = node; 133 } 134 135 /** @type {!Array.<?ChartEntry>} */ 136 var entries = []; 137 /** @type {!Array.<number>} */ 138 var stack = []; 139 var maxDepth = 5; 140 141 function onOpenFrame() 142 { 143 stack.push(entries.length); 144 // Reserve space for the entry, as they have to be ordered by startTime. 145 // The entry itself will be put there in onCloseFrame. 146 entries.push(null); 147 } 148 function onCloseFrame(depth, node, startTime, totalTime, selfTime) 149 { 150 var index = stack.pop(); 151 entries[index] = new ChartEntry(depth, totalTime, startTime, selfTime, node); 152 maxDepth = Math.max(maxDepth, depth); 153 } 154 this._cpuProfile.forEachFrame(onOpenFrame, onCloseFrame); 155 156 /** @type {!Array.<!ProfilerAgent.CPUProfileNode>} */ 157 var entryNodes = new Array(entries.length); 158 var entryLevels = new Uint8Array(entries.length); 159 var entryTotalTimes = new Float32Array(entries.length); 160 var entrySelfTimes = new Float32Array(entries.length); 161 var entryStartTimes = new Float64Array(entries.length); 162 var minimumBoundary = this.minimumBoundary(); 163 164 for (var i = 0; i < entries.length; ++i) { 165 var entry = entries[i]; 166 entryNodes[i] = entry.node; 167 entryLevels[i] = entry.depth; 168 entryTotalTimes[i] = entry.duration; 169 entryStartTimes[i] = entry.startTime; 170 entrySelfTimes[i] = entry.selfTime; 171 } 172 173 this._maxStackDepth = maxDepth; 174 175 /** @type {!WebInspector.FlameChart.TimelineData} */ 176 this._timelineData = { 177 entryLevels: entryLevels, 178 entryTotalTimes: entryTotalTimes, 179 entryStartTimes: entryStartTimes, 180 }; 181 182 /** @type {!Array.<!ProfilerAgent.CPUProfileNode>} */ 183 this._entryNodes = entryNodes; 184 this._entrySelfTimes = entrySelfTimes; 185 186 return this._timelineData; 187 }, 188 189 /** 190 * @param {number} ms 191 * @return {string} 192 */ 193 _millisecondsToString: function(ms) 194 { 195 if (ms === 0) 196 return "0"; 197 if (ms < 1000) 198 return WebInspector.UIString("%.1f\u2009ms", ms); 199 return Number.secondsToString(ms / 1000, true); 200 }, 201 202 /** 203 * @param {number} entryIndex 204 * @return {?Array.<!{title: string, text: string}>} 205 */ 206 prepareHighlightedEntryInfo: function(entryIndex) 207 { 208 var timelineData = this._timelineData; 209 var node = this._entryNodes[entryIndex]; 210 if (!node) 211 return null; 212 213 var entryInfo = []; 214 function pushEntryInfoRow(title, text) 215 { 216 var row = {}; 217 row.title = title; 218 row.text = text; 219 entryInfo.push(row); 220 } 221 222 pushEntryInfoRow(WebInspector.UIString("Name"), node.functionName); 223 var selfTime = this._millisecondsToString(this._entrySelfTimes[entryIndex]); 224 var totalTime = this._millisecondsToString(timelineData.entryTotalTimes[entryIndex]); 225 pushEntryInfoRow(WebInspector.UIString("Self time"), selfTime); 226 pushEntryInfoRow(WebInspector.UIString("Total time"), totalTime); 227 var target = this._target; 228 var text = WebInspector.Linkifier.liveLocationText(target, node.scriptId, node.lineNumber, node.columnNumber); 229 pushEntryInfoRow(WebInspector.UIString("URL"), text); 230 pushEntryInfoRow(WebInspector.UIString("Aggregated self time"), Number.secondsToString(node.selfTime / 1000, true)); 231 pushEntryInfoRow(WebInspector.UIString("Aggregated total time"), Number.secondsToString(node.totalTime / 1000, true)); 232 if (node.deoptReason && node.deoptReason !== "no reason") 233 pushEntryInfoRow(WebInspector.UIString("Not optimized"), node.deoptReason); 234 235 return entryInfo; 236 }, 237 238 /** 239 * @param {number} entryIndex 240 * @return {boolean} 241 */ 242 canJumpToEntry: function(entryIndex) 243 { 244 return this._entryNodes[entryIndex].scriptId !== "0"; 245 }, 246 247 /** 248 * @param {number} entryIndex 249 * @return {?string} 250 */ 251 entryTitle: function(entryIndex) 252 { 253 var node = this._entryNodes[entryIndex]; 254 return node.functionName; 255 }, 256 257 /** 258 * @param {number} entryIndex 259 * @return {?string} 260 */ 261 entryFont: function(entryIndex) 262 { 263 if (!this._font) { 264 this._font = (this.barHeight() - 4) + "px " + WebInspector.fontFamily(); 265 this._boldFont = "bold " + this._font; 266 } 267 var node = this._entryNodes[entryIndex]; 268 var reason = node.deoptReason; 269 return (reason && reason !== "no reason") ? this._boldFont : this._font; 270 }, 271 272 /** 273 * @param {number} entryIndex 274 * @return {string} 275 */ 276 entryColor: function(entryIndex) 277 { 278 var node = this._entryNodes[entryIndex]; 279 return this._colorGenerator.colorForID(node.functionName + ":" + node.url); 280 }, 281 282 /** 283 * @param {number} entryIndex 284 * @param {!CanvasRenderingContext2D} context 285 * @param {?string} text 286 * @param {number} barX 287 * @param {number} barY 288 * @param {number} barWidth 289 * @param {number} barHeight 290 * @param {function(number):number} timeToPosition 291 * @return {boolean} 292 */ 293 decorateEntry: function(entryIndex, context, text, barX, barY, barWidth, barHeight, timeToPosition) 294 { 295 return false; 296 }, 297 298 /** 299 * @param {number} entryIndex 300 * @return {boolean} 301 */ 302 forceDecoration: function(entryIndex) 303 { 304 return false; 305 }, 306 307 /** 308 * @param {number} entryIndex 309 * @return {!{startTime: number, endTime: number}} 310 */ 311 highlightTimeRange: function(entryIndex) 312 { 313 var startTime = this._timelineData.entryStartTimes[entryIndex]; 314 return { 315 startTime: startTime, 316 endTime: startTime + this._timelineData.entryTotalTimes[entryIndex] 317 }; 318 }, 319 320 /** 321 * @return {number} 322 */ 323 paddingLeft: function() 324 { 325 return 15; 326 }, 327 328 /** 329 * @param {number} entryIndex 330 * @return {string} 331 */ 332 textColor: function(entryIndex) 333 { 334 return "#333"; 335 } 336 } 337 338 339 /** 340 * @return {!WebInspector.FlameChart.ColorGenerator} 341 */ 342 WebInspector.CPUFlameChartDataProvider.colorGenerator = function() 343 { 344 if (!WebInspector.CPUFlameChartDataProvider._colorGenerator) { 345 var colorGenerator = new WebInspector.FlameChart.ColorGenerator( 346 { min: 180, max: 310, count: 7 }, 347 { min: 50, max: 80, count: 5 }, 348 { min: 80, max: 90, count: 3 }); 349 colorGenerator.setColorForID("(idle):", "hsl(0, 0%, 94%)"); 350 colorGenerator.setColorForID("(program):", "hsl(0, 0%, 80%)"); 351 colorGenerator.setColorForID("(garbage collector):", "hsl(0, 0%, 80%)"); 352 WebInspector.CPUFlameChartDataProvider._colorGenerator = colorGenerator; 353 } 354 return WebInspector.CPUFlameChartDataProvider._colorGenerator; 355 } 356 357 358 /** 359 * @constructor 360 * @extends {WebInspector.VBox} 361 * @param {!WebInspector.FlameChartDataProvider} dataProvider 362 */ 363 WebInspector.CPUProfileFlameChart = function(dataProvider) 364 { 365 WebInspector.VBox.call(this); 366 this.registerRequiredCSS("flameChart.css"); 367 this.element.id = "cpu-flame-chart"; 368 369 this._overviewPane = new WebInspector.CPUProfileFlameChart.OverviewPane(dataProvider); 370 this._overviewPane.show(this.element); 371 372 this._mainPane = new WebInspector.FlameChart(dataProvider, this._overviewPane, true); 373 this._mainPane.show(this.element); 374 this._mainPane.addEventListener(WebInspector.FlameChart.Events.EntrySelected, this._onEntrySelected, this); 375 this._overviewPane.addEventListener(WebInspector.OverviewGrid.Events.WindowChanged, this._onWindowChanged, this); 376 } 377 378 WebInspector.CPUProfileFlameChart.prototype = { 379 /** 380 * @param {!WebInspector.Event} event 381 */ 382 _onWindowChanged: function(event) 383 { 384 var windowLeft = event.data.windowTimeLeft; 385 var windowRight = event.data.windowTimeRight; 386 this._mainPane.setWindowTimes(windowLeft, windowRight); 387 }, 388 389 /** 390 * @param {!number} timeLeft 391 * @param {!number} timeRight 392 */ 393 selectRange: function(timeLeft, timeRight) 394 { 395 this._overviewPane._selectRange(timeLeft, timeRight); 396 }, 397 398 /** 399 * @param {!WebInspector.Event} event 400 */ 401 _onEntrySelected: function(event) 402 { 403 this.dispatchEventToListeners(WebInspector.FlameChart.Events.EntrySelected, event.data); 404 }, 405 406 update: function() 407 { 408 this._overviewPane.update(); 409 this._mainPane.update(); 410 }, 411 412 __proto__: WebInspector.VBox.prototype 413 }; 414 415 /** 416 * @constructor 417 * @implements {WebInspector.TimelineGrid.Calculator} 418 */ 419 WebInspector.CPUProfileFlameChart.OverviewCalculator = function() 420 { 421 } 422 423 WebInspector.CPUProfileFlameChart.OverviewCalculator.prototype = { 424 /** 425 * @return {number} 426 */ 427 paddingLeft: function() 428 { 429 return 0; 430 }, 431 432 /** 433 * @param {!WebInspector.CPUProfileFlameChart.OverviewPane} overviewPane 434 */ 435 _updateBoundaries: function(overviewPane) 436 { 437 this._minimumBoundaries = overviewPane._dataProvider.minimumBoundary(); 438 var totalTime = overviewPane._dataProvider.totalTime(); 439 this._maximumBoundaries = this._minimumBoundaries + totalTime; 440 this._xScaleFactor = overviewPane._overviewContainer.clientWidth / totalTime; 441 }, 442 443 /** 444 * @param {number} time 445 * @return {number} 446 */ 447 computePosition: function(time) 448 { 449 return (time - this._minimumBoundaries) * this._xScaleFactor; 450 }, 451 452 /** 453 * @param {number} value 454 * @param {number=} precision 455 * @return {string} 456 */ 457 formatTime: function(value, precision) 458 { 459 return Number.secondsToString((value - this._minimumBoundaries) / 1000); 460 }, 461 462 /** 463 * @return {number} 464 */ 465 maximumBoundary: function() 466 { 467 return this._maximumBoundaries; 468 }, 469 470 /** 471 * @return {number} 472 */ 473 minimumBoundary: function() 474 { 475 return this._minimumBoundaries; 476 }, 477 478 /** 479 * @return {number} 480 */ 481 zeroTime: function() 482 { 483 return this._minimumBoundaries; 484 }, 485 486 /** 487 * @return {number} 488 */ 489 boundarySpan: function() 490 { 491 return this._maximumBoundaries - this._minimumBoundaries; 492 } 493 } 494 495 /** 496 * @constructor 497 * @extends {WebInspector.VBox} 498 * @implements {WebInspector.FlameChartDelegate} 499 * @param {!WebInspector.FlameChartDataProvider} dataProvider 500 */ 501 WebInspector.CPUProfileFlameChart.OverviewPane = function(dataProvider) 502 { 503 WebInspector.VBox.call(this); 504 this.element.classList.add("flame-chart-overview-pane"); 505 this._overviewContainer = this.element.createChild("div", "overview-container"); 506 this._overviewGrid = new WebInspector.OverviewGrid("flame-chart"); 507 this._overviewGrid.element.classList.add("fill"); 508 this._overviewCanvas = this._overviewContainer.createChild("canvas", "flame-chart-overview-canvas"); 509 this._overviewContainer.appendChild(this._overviewGrid.element); 510 this._overviewCalculator = new WebInspector.CPUProfileFlameChart.OverviewCalculator(); 511 this._dataProvider = dataProvider; 512 this._overviewGrid.addEventListener(WebInspector.OverviewGrid.Events.WindowChanged, this._onWindowChanged, this); 513 } 514 515 WebInspector.CPUProfileFlameChart.OverviewPane.prototype = { 516 /** 517 * @param {number} windowStartTime 518 * @param {number} windowEndTime 519 */ 520 requestWindowTimes: function(windowStartTime, windowEndTime) 521 { 522 this._selectRange(windowStartTime, windowEndTime); 523 }, 524 525 /** 526 * @param {!number} timeLeft 527 * @param {!number} timeRight 528 */ 529 _selectRange: function(timeLeft, timeRight) 530 { 531 var startTime = this._dataProvider.minimumBoundary(); 532 var totalTime = this._dataProvider.totalTime(); 533 this._overviewGrid.setWindow((timeLeft - startTime) / totalTime, (timeRight - startTime) / totalTime); 534 }, 535 536 /** 537 * @param {!WebInspector.Event} event 538 */ 539 _onWindowChanged: function(event) 540 { 541 var startTime = this._dataProvider.minimumBoundary(); 542 var totalTime = this._dataProvider.totalTime(); 543 var data = { 544 windowTimeLeft: startTime + this._overviewGrid.windowLeft() * totalTime, 545 windowTimeRight: startTime + this._overviewGrid.windowRight() * totalTime 546 }; 547 this.dispatchEventToListeners(WebInspector.OverviewGrid.Events.WindowChanged, data); 548 }, 549 550 /** 551 * @return {?WebInspector.FlameChart.TimelineData} 552 */ 553 _timelineData: function() 554 { 555 return this._dataProvider.timelineData(); 556 }, 557 558 onResize: function() 559 { 560 this._scheduleUpdate(); 561 }, 562 563 _scheduleUpdate: function() 564 { 565 if (this._updateTimerId) 566 return; 567 this._updateTimerId = requestAnimationFrame(this.update.bind(this)); 568 }, 569 570 update: function() 571 { 572 this._updateTimerId = 0; 573 var timelineData = this._timelineData(); 574 if (!timelineData) 575 return; 576 this._resetCanvas(this._overviewContainer.clientWidth, this._overviewContainer.clientHeight - WebInspector.FlameChart.DividersBarHeight); 577 this._overviewCalculator._updateBoundaries(this); 578 this._overviewGrid.updateDividers(this._overviewCalculator); 579 this._drawOverviewCanvas(); 580 }, 581 582 _drawOverviewCanvas: function() 583 { 584 var canvasWidth = this._overviewCanvas.width; 585 var canvasHeight = this._overviewCanvas.height; 586 var drawData = this._calculateDrawData(canvasWidth); 587 var context = this._overviewCanvas.getContext("2d"); 588 var ratio = window.devicePixelRatio; 589 var offsetFromBottom = ratio; 590 var lineWidth = 1; 591 var yScaleFactor = canvasHeight / (this._dataProvider.maxStackDepth() * 1.1); 592 context.lineWidth = lineWidth; 593 context.translate(0.5, 0.5); 594 context.strokeStyle = "rgba(20,0,0,0.4)"; 595 context.fillStyle = "rgba(214,225,254,0.8)"; 596 context.moveTo(-lineWidth, canvasHeight + lineWidth); 597 context.lineTo(-lineWidth, Math.round(canvasHeight - drawData[0] * yScaleFactor - offsetFromBottom)); 598 var value; 599 for (var x = 0; x < canvasWidth; ++x) { 600 value = Math.round(canvasHeight - drawData[x] * yScaleFactor - offsetFromBottom); 601 context.lineTo(x, value); 602 } 603 context.lineTo(canvasWidth + lineWidth, value); 604 context.lineTo(canvasWidth + lineWidth, canvasHeight + lineWidth); 605 context.fill(); 606 context.stroke(); 607 context.closePath(); 608 }, 609 610 /** 611 * @param {number} width 612 * @return {!Uint8Array} 613 */ 614 _calculateDrawData: function(width) 615 { 616 var dataProvider = this._dataProvider; 617 var timelineData = this._timelineData(); 618 var entryStartTimes = timelineData.entryStartTimes; 619 var entryTotalTimes = timelineData.entryTotalTimes; 620 var entryLevels = timelineData.entryLevels; 621 var length = entryStartTimes.length; 622 var minimumBoundary = this._dataProvider.minimumBoundary(); 623 624 var drawData = new Uint8Array(width); 625 var scaleFactor = width / dataProvider.totalTime(); 626 627 for (var entryIndex = 0; entryIndex < length; ++entryIndex) { 628 var start = Math.floor((entryStartTimes[entryIndex] - minimumBoundary) * scaleFactor); 629 var finish = Math.floor((entryStartTimes[entryIndex] - minimumBoundary + entryTotalTimes[entryIndex]) * scaleFactor); 630 for (var x = start; x <= finish; ++x) 631 drawData[x] = Math.max(drawData[x], entryLevels[entryIndex] + 1); 632 } 633 return drawData; 634 }, 635 636 /** 637 * @param {!number} width 638 * @param {!number} height 639 */ 640 _resetCanvas: function(width, height) 641 { 642 var ratio = window.devicePixelRatio; 643 this._overviewCanvas.width = width * ratio; 644 this._overviewCanvas.height = height * ratio; 645 this._overviewCanvas.style.width = width + "px"; 646 this._overviewCanvas.style.height = height + "px"; 647 }, 648 649 __proto__: WebInspector.VBox.prototype 650 } 651