1/* 2 * Copyright (C) 2013 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 * @constructor 33 * @extends {WebInspector.View} 34 * @param {WebInspector.CPUProfileView} cpuProfileView 35 */ 36WebInspector.FlameChart = function(cpuProfileView) 37{ 38 WebInspector.View.call(this); 39 this.registerRequiredCSS("flameChart.css"); 40 this.element.className = "fill"; 41 this.element.id = "cpu-flame-chart"; 42 43 this._overviewContainer = this.element.createChild("div", "overview-container"); 44 this._overviewGrid = new WebInspector.OverviewGrid("flame-chart"); 45 this._overviewContainer.appendChild(this._overviewGrid.element); 46 this._overviewCalculator = new WebInspector.FlameChart.OverviewCalculator(); 47 this._overviewGrid.addEventListener(WebInspector.OverviewGrid.Events.WindowChanged, this._onWindowChanged, this); 48 this._overviewCanvas = this._overviewContainer.createChild("canvas"); 49 50 this._chartContainer = this.element.createChild("div", "chart-container"); 51 this._timelineGrid = new WebInspector.TimelineGrid(); 52 this._chartContainer.appendChild(this._timelineGrid.element); 53 this._calculator = new WebInspector.FlameChart.Calculator(); 54 55 this._canvas = this._chartContainer.createChild("canvas"); 56 WebInspector.installDragHandle(this._canvas, this._startCanvasDragging.bind(this), this._canvasDragging.bind(this), this._endCanvasDragging.bind(this), "col-resize"); 57 58 this._cpuProfileView = cpuProfileView; 59 this._windowLeft = 0.0; 60 this._windowRight = 1.0; 61 this._barHeight = 15; 62 this._minWidth = 1; 63 this._paddingLeft = 15; 64 this._canvas.addEventListener("mousewheel", this._onMouseWheel.bind(this), false); 65 this.element.addEventListener("click", this._onClick.bind(this), false); 66 this._popoverHelper = new WebInspector.PopoverHelper(this._chartContainer, this._getPopoverAnchor.bind(this), this._showPopover.bind(this)); 67 this._popoverHelper.setTimeout(250); 68 this._linkifier = new WebInspector.Linkifier(); 69 this._highlightedNodeIndex = -1; 70 71 if (!WebInspector.FlameChart._colorGenerator) 72 WebInspector.FlameChart._colorGenerator = new WebInspector.FlameChart.ColorGenerator(); 73} 74 75/** 76 * @constructor entries.push(new WebInspector.FlameChart.Entry(colorPair, level, node.totalTime, offset, node)); 77/ 78 * @implements {WebInspector.TimelineGrid.Calculator} 79 */ 80WebInspector.FlameChart.Calculator = function() 81{ 82} 83 84WebInspector.FlameChart.Calculator.prototype = { 85 /** 86 * @param {WebInspector.FlameChart} flameChart 87 */ 88 _updateBoundaries: function(flameChart) 89 { 90 this._minimumBoundaries = flameChart._windowLeft * flameChart._timelineData.totalTime; 91 this._maximumBoundaries = flameChart._windowRight * flameChart._timelineData.totalTime; 92 this._paddingLeft = flameChart._paddingLeft; 93 this._width = flameChart._canvas.width - this._paddingLeft; 94 this._timeToPixel = this._width / this.boundarySpan(); 95 }, 96 97 /** 98 * @param {number} time 99 */ 100 computePosition: function(time) 101 { 102 return (time - this._minimumBoundaries) * this._timeToPixel + this._paddingLeft; 103 }, 104 105 formatTime: function(value) 106 { 107 return Number.secondsToString((value + this._minimumBoundaries) / 1000); 108 }, 109 110 maximumBoundary: function() 111 { 112 return this._maximumBoundaries; 113 }, 114 115 minimumBoundary: function() 116 { 117 return this._minimumBoundaries; 118 }, 119 120 zeroTime: function() 121 { 122 return 0; 123 }, 124 125 boundarySpan: function() 126 { 127 return this._maximumBoundaries - this._minimumBoundaries; 128 } 129} 130 131/** 132 * @constructor 133 * @implements {WebInspector.TimelineGrid.Calculator} 134 */ 135WebInspector.FlameChart.OverviewCalculator = function() 136{ 137} 138 139WebInspector.FlameChart.OverviewCalculator.prototype = { 140 /** 141 * @param {WebInspector.FlameChart} flameChart 142 */ 143 _updateBoundaries: function(flameChart) 144 { 145 this._minimumBoundaries = 0; 146 this._maximumBoundaries = flameChart._timelineData.totalTime; 147 this._xScaleFactor = flameChart._canvas.width / flameChart._timelineData.totalTime; 148 }, 149 150 /** 151 * @param {number} time 152 */ 153 computePosition: function(time) 154 { 155 return (time - this._minimumBoundaries) * this._xScaleFactor; 156 }, 157 158 formatTime: function(value) 159 { 160 return Number.secondsToString((value + this._minimumBoundaries) / 1000); 161 }, 162 163 maximumBoundary: function() 164 { 165 return this._maximumBoundaries; 166 }, 167 168 minimumBoundary: function() 169 { 170 return this._minimumBoundaries; 171 }, 172 173 zeroTime: function() 174 { 175 return this._minimumBoundaries; 176 }, 177 178 boundarySpan: function() 179 { 180 return this._maximumBoundaries - this._minimumBoundaries; 181 } 182} 183 184WebInspector.FlameChart.Events = { 185 SelectedNode: "SelectedNode" 186} 187 188/** 189 * @constructor 190 */ 191WebInspector.FlameChart.ColorGenerator = function() 192{ 193 this._colorPairs = {}; 194 this._currentColorIndex = 0; 195} 196 197WebInspector.FlameChart.ColorGenerator.prototype = { 198 /** 199 * @param {!string} id 200 */ 201 _colorPairForID: function(id) 202 { 203 var colorPairs = this._colorPairs; 204 var colorPair = colorPairs[id]; 205 if (!colorPair) { 206 var currentColorIndex = ++this._currentColorIndex; 207 var hue = (currentColorIndex * 5 + 11 * (currentColorIndex % 2)) % 360; 208 colorPairs[id] = colorPair = {highlighted: "hsla(" + hue + ", 100%, 33%, 0.7)", normal: "hsla(" + hue + ", 100%, 66%, 0.7)"}; 209 } 210 return colorPair; 211 } 212} 213 214/** 215 * @constructor 216 * @param {!Object} colorPair 217 * @param {!number} depth 218 * @param {!number} duration 219 * @param {!number} startTime 220 * @param {Object} node 221 */ 222WebInspector.FlameChart.Entry = function(colorPair, depth, duration, startTime, node) 223{ 224 this.colorPair = colorPair; 225 this.depth = depth; 226 this.duration = duration; 227 this.startTime = startTime; 228 this.node = node; 229} 230 231WebInspector.FlameChart.prototype = { 232 _onWindowChanged: function(event) 233 { 234 this._hidePopover(); 235 this._scheduleUpdate(); 236 }, 237 238 _startCanvasDragging: function(event) 239 { 240 if (!this._timelineData) 241 return false; 242 this._isDragging = true; 243 this._dragStartPoint = event.pageX; 244 this._dragStartWindowLeft = this._windowLeft; 245 this._dragStartWindowRight = this._windowRight; 246 this._hidePopover(); 247 return true; 248 }, 249 250 _canvasDragging: function(event) 251 { 252 var pixelShift = this._dragStartPoint - event.pageX; 253 var windowShift = pixelShift / this._totalPixels; 254 255 var windowLeft = Math.max(0, this._dragStartWindowLeft + windowShift); 256 if (windowLeft === this._windowLeft) 257 return; 258 windowShift = windowLeft - this._dragStartWindowLeft; 259 260 var windowRight = Math.min(1, this._dragStartWindowRight + windowShift); 261 if (windowRight === this._windowRight) 262 return; 263 windowShift = windowRight - this._dragStartWindowRight; 264 this._overviewGrid.setWindow(this._dragStartWindowLeft + windowShift, this._dragStartWindowRight + windowShift); 265 }, 266 267 _endCanvasDragging: function() 268 { 269 this._isDragging = false; 270 }, 271 272 _calculateTimelineData: function() 273 { 274 if (this._cpuProfileView.samples) 275 return this._calculateTimelineDataForSamples(); 276 277 if (this._timelineData) 278 return this._timelineData; 279 280 if (!this._cpuProfileView.profileHead) 281 return null; 282 283 var index = 0; 284 var entries = []; 285 286 function appendReversedArray(toArray, fromArray) 287 { 288 for (var i = fromArray.length - 1; i >= 0; --i) 289 toArray.push(fromArray[i]); 290 } 291 292 var stack = []; 293 appendReversedArray(stack, this._cpuProfileView.profileHead.children); 294 295 var levelOffsets = /** @type {Array.<!number>} */ ([0]); 296 var levelExitIndexes = /** @type {Array.<!number>} */ ([0]); 297 var colorGenerator = WebInspector.FlameChart._colorGenerator; 298 299 while (stack.length) { 300 var level = levelOffsets.length - 1; 301 var node = stack.pop(); 302 var offset = levelOffsets[level]; 303 304 var colorPair = colorGenerator._colorPairForID(node.functionName + ":" + node.url + ":" + node.lineNumber); 305 306 entries.push(new WebInspector.FlameChart.Entry(colorPair, level, node.totalTime, offset, node)); 307 308 ++index; 309 310 levelOffsets[level] += node.totalTime; 311 if (node.children.length) { 312 levelExitIndexes.push(stack.length); 313 levelOffsets.push(offset + node.selfTime / 2); 314 appendReversedArray(stack, node.children); 315 } 316 317 while (stack.length === levelExitIndexes[levelExitIndexes.length - 1]) { 318 levelOffsets.pop(); 319 levelExitIndexes.pop(); 320 } 321 } 322 323 this._timelineData = { 324 entries: entries, 325 totalTime: this._cpuProfileView.profileHead.totalTime, 326 } 327 328 return this._timelineData; 329 }, 330 331 _calculateTimelineDataForSamples: function() 332 { 333 if (this._timelineData) 334 return this._timelineData; 335 336 if (!this._cpuProfileView.profileHead) 337 return null; 338 339 var samples = this._cpuProfileView.samples; 340 var idToNode = this._cpuProfileView._idToNode; 341 var samplesCount = samples.length; 342 343 var index = 0; 344 var entries = /** @type {Array.<!WebInspector.FlameChart.Entry>} */ ([]); 345 346 var openIntervals = []; 347 var stackTrace = []; 348 var colorGenerator = WebInspector.FlameChart._colorGenerator; 349 for (var sampleIndex = 0; sampleIndex < samplesCount; sampleIndex++) { 350 var node = idToNode[samples[sampleIndex]]; 351 stackTrace.length = 0; 352 while (node) { 353 stackTrace.push(node); 354 node = node.parent; 355 } 356 stackTrace.pop(); // Remove (root) node 357 358 var depth = 0; 359 node = stackTrace.pop(); 360 while (node && depth < openIntervals.length && node === openIntervals[depth].node) { 361 var intervalIndex = openIntervals[depth].index; 362 entries[intervalIndex].duration += 1; 363 node = stackTrace.pop(); 364 ++depth; 365 } 366 if (depth < openIntervals.length) 367 openIntervals.length = depth; 368 if (!node) 369 continue; 370 371 while (node) { 372 var colorPair = colorGenerator._colorPairForID(node.functionName + ":" + node.url + ":" + node.lineNumber); 373 374 entries.push(new WebInspector.FlameChart.Entry(colorPair, depth, 1, sampleIndex, node)); 375 openIntervals.push({node: node, index: index}); 376 ++index; 377 378 node = stackTrace.pop(); 379 ++depth; 380 } 381 } 382 383 this._timelineData = { 384 entries: entries, 385 totalTime: samplesCount, 386 }; 387 388 return this._timelineData; 389 }, 390 391 _getPopoverAnchor: function(element, event) 392 { 393 if (this._isDragging) 394 return null; 395 396 var nodeIndex = this._coordinatesToNodeIndex(event.offsetX, event.offsetY); 397 398 this._highlightedNodeIndex = nodeIndex; 399 this.update(); 400 401 if (nodeIndex === -1) 402 return null; 403 404 var anchorBox = new AnchorBox(); 405 this._entryToAnchorBox(this._timelineData.entries[nodeIndex], anchorBox); 406 anchorBox.x += event.pageX - event.offsetX; 407 anchorBox.y += event.pageY - event.offsetY; 408 409 return anchorBox; 410 }, 411 412 _showPopover: function(anchor, popover) 413 { 414 if (this._isDragging) 415 return; 416 var node = this._timelineData.entries[this._highlightedNodeIndex].node; 417 if (!node) 418 return; 419 var contentHelper = new WebInspector.PopoverContentHelper(node.functionName); 420 contentHelper.appendTextRow(WebInspector.UIString("Total time"), Number.secondsToString(node.totalTime / 1000, true)); 421 contentHelper.appendTextRow(WebInspector.UIString("Self time"), Number.secondsToString(node.selfTime / 1000, true)); 422 if (node.numberOfCalls) 423 contentHelper.appendTextRow(WebInspector.UIString("Number of calls"), node.numberOfCalls); 424 if (node.url) { 425 var link = this._linkifier.linkifyLocation(node.url, node.lineNumber); 426 contentHelper.appendElementRow("Location", link); 427 } 428 429 popover.show(contentHelper._contentTable, anchor); 430 }, 431 432 _hidePopover: function() 433 { 434 this._popoverHelper.hidePopover(); 435 this._linkifier.reset(); 436 }, 437 438 _onClick: function(e) 439 { 440 if (this._highlightedNodeIndex === -1) 441 return; 442 var node = this._timelineData.entries[this._highlightedNodeIndex].node; 443 this.dispatchEventToListeners(WebInspector.FlameChart.Events.SelectedNode, node); 444 }, 445 446 _onMouseWheel: function(e) 447 { 448 var zoomFactor = (e.wheelDelta > 0) ? 0.9 : 1.1; 449 var windowPoint = (this._pixelWindowLeft + e.offsetX) / this._totalPixels; 450 var overviewReferencePoint = Math.floor(windowPoint * this._pixelWindowWidth); 451 this._overviewGrid.zoom(zoomFactor, overviewReferencePoint); 452 this._hidePopover(); 453 }, 454 455 /** 456 * @param {!number} x 457 * @param {!number} y 458 */ 459 _coordinatesToNodeIndex: function(x, y) 460 { 461 var timelineData = this._timelineData; 462 if (!timelineData) 463 return -1; 464 var timelineEntries = timelineData.entries; 465 var cursorTime = (x + this._pixelWindowLeft - this._paddingLeft) * this._pixelToTime; 466 var cursorLevel = Math.floor((this._canvas.height - y) / this._barHeight); 467 468 for (var i = 0; i < timelineEntries.length; ++i) { 469 if (cursorTime < timelineEntries[i].startTime) 470 return -1; 471 if (cursorTime < (timelineEntries[i].startTime + timelineEntries[i].duration) 472 && cursorLevel === timelineEntries[i].depth) 473 return i; 474 } 475 return -1; 476 }, 477 478 onResize: function() 479 { 480 this._updateOverviewCanvas = true; 481 this._hidePopover(); 482 this._scheduleUpdate(); 483 }, 484 485 _drawOverviewCanvas: function(width, height) 486 { 487 this._overviewCanvas.width = width; 488 this._overviewCanvas.height = height; 489 490 if (!this._timelineData) 491 return; 492 493 var timelineEntries = this._timelineData.entries; 494 495 var drawData = new Uint8Array(width); 496 var scaleFactor = width / this._totalTime; 497 498 for (var nodeIndex = 0; nodeIndex < timelineEntries.length; ++nodeIndex) { 499 var entry = timelineEntries[nodeIndex]; 500 var start = Math.floor(entry.startTime * scaleFactor); 501 var finish = Math.floor((entry.startTime + entry.duration) * scaleFactor); 502 for (var x = start; x < finish; ++x) 503 drawData[x] = Math.max(drawData[x], entry.depth + 1); 504 } 505 506 var context = this._overviewCanvas.getContext("2d"); 507 var yScaleFactor = 2; 508 context.lineWidth = 0.5; 509 context.strokeStyle = "rgba(20,0,0,0.8)"; 510 context.fillStyle="rgba(214,225,254, 0.8)"; 511 context.moveTo(0, height - 1); 512 for (var x = 0; x < width; ++x) 513 context.lineTo(x, height - drawData[x] * yScaleFactor - 1); 514 context.moveTo(width - 1, height - 1); 515 context.moveTo(0, height - 1); 516 context.fill(); 517 context.stroke(); 518 context.closePath(); 519 }, 520 521 /** 522 * @param {WebInspector.FlameChart.Entry} entry 523 * @param {AnchorBox} anchorBox 524 */ 525 _entryToAnchorBox: function(entry, anchorBox) 526 { 527 anchorBox.x = Math.floor(entry.startTime * this._timeToPixel) - this._pixelWindowLeft + this._paddingLeft; 528 anchorBox.y = this._canvas.height - (entry.depth + 1) * this._barHeight; 529 anchorBox.width = Math.floor(entry.duration * this._timeToPixel); 530 anchorBox.height = this._barHeight; 531 if (anchorBox.x < 0) { 532 anchorBox.width += anchorBox.x; 533 anchorBox.x = 0; 534 } 535 anchorBox.width = Number.constrain(anchorBox.width, 0, this._canvas.width - anchorBox.x); 536 }, 537 538 /** 539 * @param {!number} height 540 * @param {!number} width 541 */ 542 draw: function(width, height) 543 { 544 var timelineData = this._calculateTimelineData(); 545 if (!timelineData) 546 return; 547 var timelineEntries = timelineData.entries; 548 this._canvas.height = height; 549 this._canvas.width = width; 550 var barHeight = this._barHeight; 551 552 var context = this._canvas.getContext("2d"); 553 var textPaddingLeft = 2; 554 var paddingLeft = this._paddingLeft; 555 context.font = (barHeight - 3) + "px sans-serif"; 556 context.textBaseline = "top"; 557 this._dotsWidth = context.measureText("\u2026").width; 558 559 var anchorBox = new AnchorBox(); 560 for (var i = 0; i < timelineEntries.length; ++i) { 561 var entry = timelineEntries[i]; 562 var startTime = entry.startTime; 563 if (startTime > this._timeWindowRight) 564 break; 565 if ((startTime + entry.duration) < this._timeWindowLeft) 566 continue; 567 this._entryToAnchorBox(entry, anchorBox); 568 if (anchorBox.width < this._minWidth) 569 continue; 570 571 var colorPair = entry.colorPair; 572 var color; 573 if (this._highlightedNodeIndex === i) 574 color = colorPair.highlighted; 575 else 576 color = colorPair.normal; 577 578 context.beginPath(); 579 context.rect(anchorBox.x, anchorBox.y, anchorBox.width - 1, anchorBox.height - 1); 580 context.fillStyle = color; 581 context.fill(); 582 583 var xText = Math.max(0, anchorBox.x); 584 var widthText = anchorBox.width - textPaddingLeft + anchorBox.x - xText; 585 var title = this._prepareTitle(context, entry.node.functionName, widthText); 586 if (title) { 587 context.fillStyle = "#333"; 588 context.fillText(title, xText + textPaddingLeft, anchorBox.y - 1); 589 } 590 } 591 }, 592 593 _prepareTitle: function(context, title, maxSize) 594 { 595 if (maxSize < this._dotsWidth) 596 return null; 597 var titleWidth = context.measureText(title).width; 598 if (maxSize > titleWidth) 599 return title; 600 maxSize -= this._dotsWidth; 601 var dotRegExp=/[\.\$]/g; 602 var match = dotRegExp.exec(title); 603 if (!match) { 604 var visiblePartSize = maxSize / titleWidth; 605 var newTextLength = Math.floor(title.length * visiblePartSize) + 1; 606 var minTextLength = 4; 607 if (newTextLength < minTextLength) 608 return null; 609 var substring; 610 do { 611 --newTextLength; 612 substring = title.substring(0, newTextLength); 613 } while (context.measureText(substring).width > maxSize); 614 return title.substring(0, newTextLength) + "\u2026"; 615 } 616 while (match) { 617 var substring = title.substring(match.index + 1); 618 var width = context.measureText(substring).width; 619 if (maxSize > width) 620 return "\u2026" + substring; 621 match = dotRegExp.exec(title); 622 } 623 }, 624 625 _scheduleUpdate: function() 626 { 627 if (this._updateTimerId) 628 return; 629 this._updateTimerId = setTimeout(this.update.bind(this), 10); 630 }, 631 632 _updateBoundaries: function() 633 { 634 this._windowLeft = this._overviewGrid.windowLeft(); 635 this._windowRight = this._overviewGrid.windowRight(); 636 this._windowWidth = this._windowRight - this._windowLeft; 637 638 this._totalTime = this._timelineData.totalTime; 639 this._timeWindowLeft = this._windowLeft * this._totalTime; 640 this._timeWindowRight = this._windowRight * this._totalTime; 641 642 this._pixelWindowWidth = this._chartContainer.clientWidth; 643 this._totalPixels = Math.floor(this._pixelWindowWidth / this._windowWidth); 644 this._pixelWindowLeft = Math.floor(this._totalPixels * this._windowLeft); 645 this._pixelWindowRight = Math.floor(this._totalPixels * this._windowRight); 646 647 this._timeToPixel = this._totalPixels / this._totalTime; 648 this._pixelToTime = this._totalTime / this._totalPixels; 649 }, 650 651 update: function() 652 { 653 this._updateTimerId = 0; 654 if (!this._timelineData) 655 this._calculateTimelineData(); 656 if (!this._timelineData) 657 return; 658 this._updateBoundaries(); 659 this.draw(this._chartContainer.clientWidth, this._chartContainer.clientHeight); 660 this._calculator._updateBoundaries(this); 661 this._overviewCalculator._updateBoundaries(this); 662 this._timelineGrid.element.style.width = this.element.clientWidth; 663 this._timelineGrid.updateDividers(this._calculator); 664 this._overviewGrid.updateDividers(this._overviewCalculator); 665 if (this._updateOverviewCanvas) { 666 this._drawOverviewCanvas(this._overviewContainer.clientWidth, this._overviewContainer.clientHeight); 667 this._updateOverviewCanvas = false; 668 } 669 }, 670 671 __proto__: WebInspector.View.prototype 672}; 673