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 * @interface 33 */ 34 WebInspector.FlameChartDelegate = function() { } 35 36 WebInspector.FlameChartDelegate.prototype = { 37 /** 38 * @param {number} startTime 39 * @param {number} endTime 40 */ 41 requestWindowTimes: function(startTime, endTime) { } 42 } 43 44 /** 45 * @constructor 46 * @extends {WebInspector.HBox} 47 * @param {!WebInspector.FlameChartDataProvider} dataProvider 48 * @param {!WebInspector.FlameChartDelegate} flameChartDelegate 49 * @param {boolean} isTopDown 50 */ 51 WebInspector.FlameChart = function(dataProvider, flameChartDelegate, isTopDown) 52 { 53 WebInspector.HBox.call(this); 54 this.element.classList.add("flame-chart-main-pane"); 55 this._flameChartDelegate = flameChartDelegate; 56 this._isTopDown = isTopDown; 57 58 this._calculator = new WebInspector.FlameChart.Calculator(); 59 60 this._canvas = this.element.createChild("canvas"); 61 this._canvas.tabIndex = 1; 62 this.setDefaultFocusedElement(this._canvas); 63 this._canvas.addEventListener("mousemove", this._onMouseMove.bind(this), false); 64 this._canvas.addEventListener("mousewheel", this._onMouseWheel.bind(this), false); 65 this._canvas.addEventListener("click", this._onClick.bind(this), false); 66 this._canvas.addEventListener("keydown", this._onKeyDown.bind(this), false); 67 WebInspector.installDragHandle(this._canvas, this._startCanvasDragging.bind(this), this._canvasDragging.bind(this), this._endCanvasDragging.bind(this), "move", null); 68 69 this._vScrollElement = this.element.createChild("div", "flame-chart-v-scroll"); 70 this._vScrollContent = this._vScrollElement.createChild("div"); 71 this._vScrollElement.addEventListener("scroll", this.scheduleUpdate.bind(this), false); 72 73 this._entryInfo = this.element.createChild("div", "profile-entry-info"); 74 this._markerHighlighElement = this.element.createChild("div", "flame-chart-marker-highlight-element"); 75 this._highlightElement = this.element.createChild("div", "flame-chart-highlight-element"); 76 this._selectedElement = this.element.createChild("div", "flame-chart-selected-element"); 77 78 this._dataProvider = dataProvider; 79 80 this._windowLeft = 0.0; 81 this._windowRight = 1.0; 82 this._windowWidth = 1.0; 83 this._timeWindowLeft = 0; 84 this._timeWindowRight = Infinity; 85 this._barHeight = dataProvider.barHeight(); 86 this._barHeightDelta = this._isTopDown ? -this._barHeight : this._barHeight; 87 this._minWidth = 1; 88 this._paddingLeft = this._dataProvider.paddingLeft(); 89 this._markerPadding = 2; 90 this._markerRadius = this._barHeight / 2 - this._markerPadding; 91 this._highlightedMarkerIndex = -1; 92 this._highlightedEntryIndex = -1; 93 this._selectedEntryIndex = -1; 94 this._textWidth = {}; 95 } 96 97 WebInspector.FlameChart.DividersBarHeight = 20; 98 99 /** 100 * @interface 101 */ 102 WebInspector.FlameChartDataProvider = function() 103 { 104 } 105 106 /** 107 * @constructor 108 * @param {!Array.<number>|!Uint8Array} entryLevels 109 * @param {!Array.<number>|!Float32Array} entryTotalTimes 110 * @param {!Array.<number>|!Float64Array} entryStartTimes 111 */ 112 WebInspector.FlameChart.TimelineData = function(entryLevels, entryTotalTimes, entryStartTimes) 113 { 114 this.entryLevels = entryLevels; 115 this.entryTotalTimes = entryTotalTimes; 116 this.entryStartTimes = entryStartTimes; 117 /** @type {!Array.<number>} */ 118 this.markerTimestamps = []; 119 } 120 121 WebInspector.FlameChartDataProvider.prototype = { 122 /** 123 * @return {number} 124 */ 125 barHeight: function() { }, 126 127 /** 128 * @param {number} startTime 129 * @param {number} endTime 130 * @return {?Array.<number>} 131 */ 132 dividerOffsets: function(startTime, endTime) { }, 133 134 /** 135 * @param {number} index 136 * @return {string} 137 */ 138 markerColor: function(index) { }, 139 140 /** 141 * @param {number} index 142 * @return {string} 143 */ 144 markerTitle: function(index) { }, 145 146 /** 147 * @return {number} 148 */ 149 minimumBoundary: function() { }, 150 151 /** 152 * @return {number} 153 */ 154 totalTime: function() { }, 155 156 /** 157 * @return {number} 158 */ 159 maxStackDepth: function() { }, 160 161 /** 162 * @return {?WebInspector.FlameChart.TimelineData} 163 */ 164 timelineData: function() { }, 165 166 /** 167 * @param {number} entryIndex 168 * @return {?Array.<!{title: string, text: string}>} 169 */ 170 prepareHighlightedEntryInfo: function(entryIndex) { }, 171 172 /** 173 * @param {number} entryIndex 174 * @return {boolean} 175 */ 176 canJumpToEntry: function(entryIndex) { }, 177 178 /** 179 * @param {number} entryIndex 180 * @return {?string} 181 */ 182 entryTitle: function(entryIndex) { }, 183 184 /** 185 * @param {number} entryIndex 186 * @return {?string} 187 */ 188 entryFont: function(entryIndex) { }, 189 190 /** 191 * @param {number} entryIndex 192 * @return {string} 193 */ 194 entryColor: function(entryIndex) { }, 195 196 /** 197 * @param {number} entryIndex 198 * @param {!CanvasRenderingContext2D} context 199 * @param {?string} text 200 * @param {number} barX 201 * @param {number} barY 202 * @param {number} barWidth 203 * @param {number} barHeight 204 * @param {function(number):number} timeToPosition 205 * @return {boolean} 206 */ 207 decorateEntry: function(entryIndex, context, text, barX, barY, barWidth, barHeight, timeToPosition) { }, 208 209 /** 210 * @param {number} entryIndex 211 * @return {boolean} 212 */ 213 forceDecoration: function(entryIndex) { }, 214 215 /** 216 * @param {number} entryIndex 217 * @return {string} 218 */ 219 textColor: function(entryIndex) { }, 220 221 /** 222 * @return {number} 223 */ 224 textBaseline: function() { }, 225 226 /** 227 * @return {number} 228 */ 229 textPadding: function() { }, 230 231 /** 232 * @return {?{startTime: number, endTime: number}} 233 */ 234 highlightTimeRange: function(entryIndex) { }, 235 236 /** 237 * @return {number} 238 */ 239 paddingLeft: function() { }, 240 } 241 242 WebInspector.FlameChart.Events = { 243 EntrySelected: "EntrySelected" 244 } 245 246 247 /** 248 * @constructor 249 * @param {!{min: number, max: number, count: number}|number=} hueSpace 250 * @param {!{min: number, max: number, count: number}|number=} satSpace 251 * @param {!{min: number, max: number, count: number}|number=} lightnessSpace 252 * @param {!{min: number, max: number, count: number}|number=} alphaSpace 253 */ 254 WebInspector.FlameChart.ColorGenerator = function(hueSpace, satSpace, lightnessSpace, alphaSpace) 255 { 256 this._hueSpace = hueSpace || { min: 0, max: 360, count: 20 }; 257 this._satSpace = satSpace || 67; 258 this._lightnessSpace = lightnessSpace || 80; 259 this._alphaSpace = alphaSpace || 1; 260 this._colors = {}; 261 } 262 263 WebInspector.FlameChart.ColorGenerator.prototype = { 264 /** 265 * @param {string} id 266 * @param {string|!CanvasGradient} color 267 */ 268 setColorForID: function(id, color) 269 { 270 this._colors[id] = color; 271 }, 272 273 /** 274 * @param {string} id 275 * @return {string} 276 */ 277 colorForID: function(id) 278 { 279 var color = this._colors[id]; 280 if (!color) { 281 color = this._generateColorForID(id); 282 this._colors[id] = color; 283 } 284 return color; 285 }, 286 287 /** 288 * @param {string} id 289 * @return {string} 290 */ 291 _generateColorForID: function(id) 292 { 293 var hash = id.hashCode(); 294 var h = this._indexToValueInSpace(hash, this._hueSpace); 295 var s = this._indexToValueInSpace(hash, this._satSpace); 296 var l = this._indexToValueInSpace(hash, this._lightnessSpace); 297 var a = this._indexToValueInSpace(hash, this._alphaSpace); 298 return "hsla(" + h + ", " + s + "%, " + l + "%, " + a + ")"; 299 }, 300 301 /** 302 * @param {number} index 303 * @param {!{min: number, max: number, count: number}|number} space 304 * @return {number} 305 */ 306 _indexToValueInSpace: function(index, space) 307 { 308 if (typeof space === "number") 309 return space; 310 index %= space.count; 311 return space.min + Math.floor(index / space.count * (space.max - space.min)); 312 } 313 } 314 315 316 /** 317 * @constructor 318 * @implements {WebInspector.TimelineGrid.Calculator} 319 */ 320 WebInspector.FlameChart.Calculator = function() 321 { 322 this._paddingLeft = 0; 323 } 324 325 WebInspector.FlameChart.Calculator.prototype = { 326 /** 327 * @return {number} 328 */ 329 paddingLeft: function() 330 { 331 return this._paddingLeft; 332 }, 333 334 /** 335 * @param {!WebInspector.FlameChart} mainPane 336 */ 337 _updateBoundaries: function(mainPane) 338 { 339 this._totalTime = mainPane._dataProvider.totalTime(); 340 this._zeroTime = mainPane._dataProvider.minimumBoundary(); 341 this._minimumBoundaries = this._zeroTime + mainPane._windowLeft * this._totalTime; 342 this._maximumBoundaries = this._zeroTime + mainPane._windowRight * this._totalTime; 343 this._paddingLeft = mainPane._paddingLeft; 344 this._width = mainPane._canvas.width / window.devicePixelRatio - this._paddingLeft; 345 this._timeToPixel = this._width / this.boundarySpan(); 346 }, 347 348 /** 349 * @param {number} time 350 * @return {number} 351 */ 352 computePosition: function(time) 353 { 354 return Math.round((time - this._minimumBoundaries) * this._timeToPixel + this._paddingLeft); 355 }, 356 357 /** 358 * @param {number} value 359 * @param {number=} precision 360 * @return {string} 361 */ 362 formatTime: function(value, precision) 363 { 364 return Number.preciseMillisToString(value - this._zeroTime, precision); 365 }, 366 367 /** 368 * @return {number} 369 */ 370 maximumBoundary: function() 371 { 372 return this._maximumBoundaries; 373 }, 374 375 /** 376 * @return {number} 377 */ 378 minimumBoundary: function() 379 { 380 return this._minimumBoundaries; 381 }, 382 383 /** 384 * @return {number} 385 */ 386 zeroTime: function() 387 { 388 return this._zeroTime; 389 }, 390 391 /** 392 * @return {number} 393 */ 394 boundarySpan: function() 395 { 396 return this._maximumBoundaries - this._minimumBoundaries; 397 } 398 } 399 400 WebInspector.FlameChart.prototype = { 401 _resetCanvas: function() 402 { 403 var ratio = window.devicePixelRatio; 404 this._canvas.width = this._offsetWidth * ratio; 405 this._canvas.height = this._offsetHeight * ratio; 406 this._canvas.style.width = this._offsetWidth + "px"; 407 this._canvas.style.height = this._offsetHeight + "px"; 408 }, 409 410 /** 411 * @return {?WebInspector.FlameChart.TimelineData} 412 */ 413 _timelineData: function() 414 { 415 var timelineData = this._dataProvider.timelineData(); 416 if (timelineData !== this._rawTimelineData || timelineData.entryStartTimes.length !== this._rawTimelineDataLength) 417 this._processTimelineData(timelineData); 418 return this._rawTimelineData; 419 }, 420 421 _cancelAnimation: function() 422 { 423 if (this._cancelWindowTimesAnimation) { 424 this._timeWindowLeft = this._pendingAnimationTimeLeft; 425 this._timeWindowRight = this._pendingAnimationTimeRight; 426 this._cancelWindowTimesAnimation(); 427 delete this._cancelWindowTimesAnimation; 428 } 429 }, 430 431 /** 432 * @param {number} startTime 433 * @param {number} endTime 434 */ 435 setWindowTimes: function(startTime, endTime) 436 { 437 if (this._muteAnimation || this._timeWindowLeft === 0 || this._timeWindowRight === Infinity) { 438 // Initial setup. 439 this._timeWindowLeft = startTime; 440 this._timeWindowRight = endTime; 441 this.scheduleUpdate(); 442 return; 443 } 444 445 this._cancelAnimation(); 446 this._cancelWindowTimesAnimation = WebInspector.animateFunction(this._animateWindowTimes.bind(this), 447 [{from: this._timeWindowLeft, to: startTime}, {from: this._timeWindowRight, to: endTime}], 5, 448 this._animationCompleted.bind(this)); 449 this._pendingAnimationTimeLeft = startTime; 450 this._pendingAnimationTimeRight = endTime; 451 }, 452 453 /** 454 * @param {number} startTime 455 * @param {number} endTime 456 */ 457 _animateWindowTimes: function(startTime, endTime) 458 { 459 this._timeWindowLeft = startTime; 460 this._timeWindowRight = endTime; 461 this.update(); 462 }, 463 464 _animationCompleted: function() 465 { 466 delete this._cancelWindowTimesAnimation; 467 }, 468 469 /** 470 * @param {!MouseEvent} event 471 */ 472 _startCanvasDragging: function(event) 473 { 474 if (!this._timelineData() || this._timeWindowRight === Infinity) 475 return false; 476 this._isDragging = true; 477 this._maxDragOffset = 0; 478 this._dragStartPointX = event.pageX; 479 this._dragStartPointY = event.pageY; 480 this._dragStartScrollTop = this._vScrollElement.scrollTop; 481 this._dragStartWindowLeft = this._timeWindowLeft; 482 this._dragStartWindowRight = this._timeWindowRight; 483 this._canvas.style.cursor = ""; 484 485 return true; 486 }, 487 488 /** 489 * @param {!MouseEvent} event 490 */ 491 _canvasDragging: function(event) 492 { 493 var pixelShift = this._dragStartPointX - event.pageX; 494 this._dragStartPointX = event.pageX; 495 this._muteAnimation = true; 496 this._handlePanGesture(pixelShift * this._pixelToTime); 497 this._muteAnimation = false; 498 499 var pixelScroll = this._dragStartPointY - event.pageY; 500 this._vScrollElement.scrollTop = this._dragStartScrollTop + pixelScroll; 501 this._maxDragOffset = Math.max(this._maxDragOffset, Math.abs(pixelShift)); 502 }, 503 504 _endCanvasDragging: function() 505 { 506 this._isDragging = false; 507 }, 508 509 /** 510 * @param {!Event} event 511 */ 512 _onMouseMove: function(event) 513 { 514 this._lastMouseOffsetX = event.offsetX; 515 516 if (this._isDragging) 517 return; 518 519 var inDividersBar = event.offsetY < WebInspector.FlameChart.DividersBarHeight; 520 this._highlightedMarkerIndex = inDividersBar ? this._markerIndexAtPosition(event.offsetX) : -1; 521 this._updateMarkerHighlight(); 522 if (inDividersBar) 523 return; 524 525 var entryIndex = this._coordinatesToEntryIndex(event.offsetX, event.offsetY); 526 527 if (this._highlightedEntryIndex === entryIndex) 528 return; 529 530 if (entryIndex === -1 || !this._dataProvider.canJumpToEntry(entryIndex)) 531 this._canvas.style.cursor = "default"; 532 else 533 this._canvas.style.cursor = "pointer"; 534 535 this._highlightedEntryIndex = entryIndex; 536 537 this._updateElementPosition(this._highlightElement, this._highlightedEntryIndex); 538 this._entryInfo.removeChildren(); 539 540 if (this._highlightedEntryIndex === -1) 541 return; 542 543 if (!this._isDragging) { 544 var entryInfo = this._dataProvider.prepareHighlightedEntryInfo(this._highlightedEntryIndex); 545 if (entryInfo) 546 this._entryInfo.appendChild(this._buildEntryInfo(entryInfo)); 547 } 548 }, 549 550 _onClick: function() 551 { 552 this.focus(); 553 // onClick comes after dragStart and dragEnd events. 554 // So if there was drag (mouse move) in the middle of that events 555 // we skip the click. Otherwise we jump to the sources. 556 const clickThreshold = 5; 557 if (this._maxDragOffset > clickThreshold) 558 return; 559 if (this._highlightedEntryIndex === -1) 560 return; 561 this.dispatchEventToListeners(WebInspector.FlameChart.Events.EntrySelected, this._highlightedEntryIndex); 562 }, 563 564 /** 565 * @param {!Event} e 566 */ 567 _onMouseWheel: function(e) 568 { 569 // Pan vertically when shift down only. 570 var panVertically = e.shiftKey && (e.wheelDeltaY || Math.abs(e.wheelDeltaX) === 120); 571 var panHorizontally = Math.abs(e.wheelDeltaX) > Math.abs(e.wheelDeltaY) && !e.shiftKey; 572 if (panVertically) { 573 this._vScrollElement.scrollTop -= (e.wheelDeltaY || e.wheelDeltaX) / 120 * this._offsetHeight / 8; 574 } else if (panHorizontally) { 575 var shift = -e.wheelDeltaX * this._pixelToTime; 576 this._muteAnimation = true; 577 this._handlePanGesture(shift); 578 this._muteAnimation = false; 579 } else { // Zoom. 580 const mouseWheelZoomSpeed = 1 / 120; 581 this._handleZoomGesture(Math.pow(1.2, -(e.wheelDeltaY || e.wheelDeltaX) * mouseWheelZoomSpeed) - 1); 582 } 583 584 // Block swipe gesture. 585 e.consume(true); 586 }, 587 588 /** 589 * @param {!Event} e 590 */ 591 _onKeyDown: function(e) 592 { 593 if (e.altKey || e.ctrlKey || e.metaKey) 594 return; 595 var zoomMultiplier = e.shiftKey ? 0.8 : 0.3; 596 var panMultiplier = e.shiftKey ? 320 : 80; 597 if (e.keyCode === "A".charCodeAt(0)) { 598 this._handlePanGesture(-panMultiplier * this._pixelToTime); 599 e.consume(true); 600 } else if (e.keyCode === "D".charCodeAt(0)) { 601 this._handlePanGesture(panMultiplier * this._pixelToTime); 602 e.consume(true); 603 } else if (e.keyCode === "W".charCodeAt(0)) { 604 this._handleZoomGesture(-zoomMultiplier); 605 e.consume(true); 606 } else if (e.keyCode === "S".charCodeAt(0)) { 607 this._handleZoomGesture(zoomMultiplier); 608 e.consume(true); 609 } 610 }, 611 612 /** 613 * @param {number} zoom 614 */ 615 _handleZoomGesture: function(zoom) 616 { 617 this._cancelAnimation(); 618 var bounds = this._windowForGesture(); 619 var cursorTime = this._cursorTime(this._lastMouseOffsetX); 620 bounds.left += (bounds.left - cursorTime) * zoom; 621 bounds.right += (bounds.right - cursorTime) * zoom; 622 this._requestWindowTimes(bounds); 623 }, 624 625 /** 626 * @param {number} shift 627 */ 628 _handlePanGesture: function(shift) 629 { 630 this._cancelAnimation(); 631 var bounds = this._windowForGesture(); 632 shift = Number.constrain(shift, this._minimumBoundary - bounds.left, this._totalTime + this._minimumBoundary - bounds.right); 633 bounds.left += shift; 634 bounds.right += shift; 635 this._requestWindowTimes(bounds); 636 }, 637 638 /** 639 * @return {{left: number, right: number}} 640 */ 641 _windowForGesture: function() 642 { 643 var windowLeft = this._timeWindowLeft ? this._timeWindowLeft : this._dataProvider.minimumBoundary(); 644 var windowRight = this._timeWindowRight !== Infinity ? this._timeWindowRight : this._dataProvider.minimumBoundary() + this._dataProvider.totalTime(); 645 return {left: windowLeft, right: windowRight}; 646 }, 647 648 /** 649 * @param {{left: number, right: number}} bounds 650 */ 651 _requestWindowTimes: function(bounds) 652 { 653 bounds.left = Number.constrain(bounds.left, this._minimumBoundary, this._totalTime + this._minimumBoundary); 654 bounds.right = Number.constrain(bounds.right, this._minimumBoundary, this._totalTime + this._minimumBoundary); 655 this._flameChartDelegate.requestWindowTimes(bounds.left, bounds.right); 656 }, 657 658 /** 659 * @param {number} x 660 * @return {number} 661 */ 662 _cursorTime: function(x) 663 { 664 return (x + this._pixelWindowLeft - this._paddingLeft) * this._pixelToTime + this._minimumBoundary; 665 }, 666 667 /** 668 * @param {number} x 669 * @param {number} y 670 * @return {number} 671 */ 672 _coordinatesToEntryIndex: function(x, y) 673 { 674 y += this._scrollTop; 675 var timelineData = this._timelineData(); 676 if (!timelineData) 677 return -1; 678 var cursorTime = this._cursorTime(x); 679 var cursorLevel; 680 var offsetFromLevel; 681 if (this._isTopDown) { 682 cursorLevel = Math.floor((y - WebInspector.FlameChart.DividersBarHeight) / this._barHeight); 683 offsetFromLevel = y - WebInspector.FlameChart.DividersBarHeight - cursorLevel * this._barHeight; 684 } else { 685 cursorLevel = Math.floor((this._canvas.height / window.devicePixelRatio - y) / this._barHeight); 686 offsetFromLevel = this._canvas.height / window.devicePixelRatio - cursorLevel * this._barHeight; 687 } 688 var entryStartTimes = timelineData.entryStartTimes; 689 var entryTotalTimes = timelineData.entryTotalTimes; 690 var entryIndexes = this._timelineLevels[cursorLevel]; 691 if (!entryIndexes || !entryIndexes.length) 692 return -1; 693 694 /** 695 * @param {number} time 696 * @param {number} entryIndex 697 * @return {number} 698 */ 699 function comparator(time, entryIndex) 700 { 701 return time - entryStartTimes[entryIndex]; 702 } 703 var indexOnLevel = Math.max(entryIndexes.upperBound(cursorTime, comparator) - 1, 0); 704 705 /** 706 * @this {WebInspector.FlameChart} 707 * @param {number} entryIndex 708 * @return {boolean} 709 */ 710 function checkEntryHit(entryIndex) 711 { 712 if (entryIndex === undefined) 713 return false; 714 var startTime = entryStartTimes[entryIndex]; 715 var duration = entryTotalTimes[entryIndex]; 716 if (isNaN(duration)) { 717 var dx = (startTime - cursorTime) / this._pixelToTime; 718 var dy = this._barHeight / 2 - offsetFromLevel; 719 return dx * dx + dy * dy < this._markerRadius * this._markerRadius; 720 } 721 var endTime = startTime + duration; 722 var barThreshold = 3 * this._pixelToTime; 723 return startTime - barThreshold < cursorTime && cursorTime < endTime + barThreshold; 724 } 725 726 var entryIndex = entryIndexes[indexOnLevel]; 727 if (checkEntryHit.call(this, entryIndex)) 728 return entryIndex; 729 entryIndex = entryIndexes[indexOnLevel + 1]; 730 if (checkEntryHit.call(this, entryIndex)) 731 return entryIndex; 732 return -1; 733 }, 734 735 /** 736 * @param {number} x 737 * @return {number} 738 */ 739 _markerIndexAtPosition: function(x) 740 { 741 var markers = this._timelineData().markerTimestamps; 742 if (!markers) 743 return -1; 744 var accurracyOffsetPx = 1; 745 var time = this._cursorTime(x); 746 var leftTime = this._cursorTime(x - accurracyOffsetPx); 747 var rightTime = this._cursorTime(x + accurracyOffsetPx); 748 749 /** 750 * @param {number} time 751 * @param {number} markerTimestamp 752 * @return {number} 753 */ 754 function comparator(time, markerTimestamp) 755 { 756 return time - markerTimestamp; 757 } 758 var left = markers.lowerBound(leftTime, comparator); 759 var markerIndex = -1; 760 var distance = Infinity; 761 for (var i = left; i < markers.length && markers[i] < rightTime; i++) { 762 var nextDistance = Math.abs(markers[i] - time); 763 if (nextDistance < distance) { 764 markerIndex = i; 765 distance = nextDistance; 766 } 767 } 768 return markerIndex; 769 }, 770 771 /** 772 * @param {number} height 773 * @param {number} width 774 */ 775 _draw: function(width, height) 776 { 777 var timelineData = this._timelineData(); 778 if (!timelineData) 779 return; 780 781 var context = this._canvas.getContext("2d"); 782 context.save(); 783 var ratio = window.devicePixelRatio; 784 context.scale(ratio, ratio); 785 786 var timeWindowRight = this._timeWindowRight; 787 var timeWindowLeft = this._timeWindowLeft; 788 var timeToPixel = this._timeToPixel; 789 var pixelWindowLeft = this._pixelWindowLeft; 790 var paddingLeft = this._paddingLeft; 791 var minWidth = this._minWidth; 792 var entryTotalTimes = timelineData.entryTotalTimes; 793 var entryStartTimes = timelineData.entryStartTimes; 794 var entryLevels = timelineData.entryLevels; 795 796 var titleIndices = new Uint32Array(timelineData.entryTotalTimes); 797 var nextTitleIndex = 0; 798 var markerIndices = new Uint32Array(timelineData.entryTotalTimes); 799 var nextMarkerIndex = 0; 800 var textPadding = this._dataProvider.textPadding(); 801 this._minTextWidth = 2 * textPadding + this._measureWidth(context, "\u2026"); 802 var minTextWidth = this._minTextWidth; 803 804 var barHeight = this._barHeight; 805 806 var timeToPosition = this._timeToPosition.bind(this); 807 var textBaseHeight = this._baseHeight + barHeight - this._dataProvider.textBaseline(); 808 var colorBuckets = {}; 809 var minVisibleBarLevel = Math.max(Math.floor((this._scrollTop - this._baseHeight) / barHeight), 0); 810 var maxVisibleBarLevel = Math.min(Math.floor((this._scrollTop - this._baseHeight + height) / barHeight), this._dataProvider.maxStackDepth()); 811 812 context.translate(0, -this._scrollTop); 813 814 function comparator(time, entryIndex) 815 { 816 return time - entryStartTimes[entryIndex]; 817 } 818 819 for (var level = minVisibleBarLevel; level <= maxVisibleBarLevel; ++level) { 820 // Entries are ordered by start time within a level, so find the last visible entry. 821 var levelIndexes = this._timelineLevels[level]; 822 var rightIndexOnLevel = levelIndexes.lowerBound(timeWindowRight, comparator) - 1; 823 var lastDrawOffset = Infinity; 824 for (var entryIndexOnLevel = rightIndexOnLevel; entryIndexOnLevel >= 0; --entryIndexOnLevel) { 825 var entryIndex = levelIndexes[entryIndexOnLevel]; 826 var entryStartTime = entryStartTimes[entryIndex]; 827 var entryOffsetRight = entryStartTime + (isNaN(entryTotalTimes[entryIndex]) ? 0 : entryTotalTimes[entryIndex]); 828 if (entryOffsetRight <= timeWindowLeft) 829 break; 830 831 var barX = this._timeToPosition(entryStartTime); 832 if (barX >= lastDrawOffset) 833 continue; 834 var barRight = Math.min(this._timeToPosition(entryOffsetRight), lastDrawOffset); 835 lastDrawOffset = barX; 836 837 var color = this._dataProvider.entryColor(entryIndex); 838 var bucket = colorBuckets[color]; 839 if (!bucket) { 840 bucket = []; 841 colorBuckets[color] = bucket; 842 } 843 bucket.push(entryIndex); 844 } 845 } 846 847 var colors = Object.keys(colorBuckets); 848 // We don't use for-in here because it couldn't be optimized. 849 for (var c = 0; c < colors.length; ++c) { 850 var color = colors[c]; 851 context.fillStyle = color; 852 context.strokeStyle = color; 853 var indexes = colorBuckets[color]; 854 855 // First fill the boxes. 856 context.beginPath(); 857 for (var i = 0; i < indexes.length; ++i) { 858 var entryIndex = indexes[i]; 859 var entryStartTime = entryStartTimes[entryIndex]; 860 var barX = this._timeToPosition(entryStartTime); 861 var barRight = this._timeToPosition(entryStartTime + entryTotalTimes[entryIndex]); 862 var barWidth = Math.max(barRight - barX, minWidth); 863 var barLevel = entryLevels[entryIndex]; 864 var barY = this._levelToHeight(barLevel); 865 if (isNaN(entryTotalTimes[entryIndex])) { 866 context.moveTo(barX + this._markerRadius, barY + barHeight / 2); 867 context.arc(barX, barY + barHeight / 2, this._markerRadius, 0, Math.PI * 2); 868 markerIndices[nextMarkerIndex++] = entryIndex; 869 } else { 870 context.rect(barX, barY, barWidth, barHeight); 871 if (barWidth > minTextWidth || this._dataProvider.forceDecoration(entryIndex)) 872 titleIndices[nextTitleIndex++] = entryIndex; 873 } 874 } 875 context.fill(); 876 } 877 878 context.strokeStyle = "rgb(0, 0, 0)"; 879 context.beginPath(); 880 for (var m = 0; m < nextMarkerIndex; ++m) { 881 var entryIndex = markerIndices[m]; 882 var entryStartTime = entryStartTimes[entryIndex]; 883 var barX = this._timeToPosition(entryStartTime); 884 var barLevel = entryLevels[entryIndex]; 885 var barY = this._levelToHeight(barLevel); 886 context.moveTo(barX + this._markerRadius, barY + barHeight / 2); 887 context.arc(barX, barY + barHeight / 2, this._markerRadius, 0, Math.PI * 2); 888 } 889 context.stroke(); 890 891 context.textBaseline = "alphabetic"; 892 893 for (var i = 0; i < nextTitleIndex; ++i) { 894 var entryIndex = titleIndices[i]; 895 var entryStartTime = entryStartTimes[entryIndex]; 896 var barX = this._timeToPosition(entryStartTime); 897 var barRight = this._timeToPosition(entryStartTime + entryTotalTimes[entryIndex]); 898 var barWidth = Math.max(barRight - barX, minWidth); 899 var barLevel = entryLevels[entryIndex]; 900 var barY = this._levelToHeight(barLevel); 901 var text = this._dataProvider.entryTitle(entryIndex); 902 if (text && text.length) { 903 context.font = this._dataProvider.entryFont(entryIndex); 904 text = this._prepareText(context, text, barWidth - 2 * textPadding); 905 } 906 907 if (this._dataProvider.decorateEntry(entryIndex, context, text, barX, barY, barWidth, barHeight, timeToPosition)) 908 continue; 909 if (!text || !text.length) 910 continue; 911 912 context.fillStyle = this._dataProvider.textColor(entryIndex); 913 context.fillText(text, barX + textPadding, textBaseHeight - barLevel * this._barHeightDelta); 914 } 915 context.restore(); 916 917 var offsets = this._dataProvider.dividerOffsets(this._calculator.minimumBoundary(), this._calculator.maximumBoundary()); 918 WebInspector.TimelineGrid.drawCanvasGrid(this._canvas, this._calculator, offsets); 919 this._drawMarkers(); 920 921 this._updateElementPosition(this._highlightElement, this._highlightedEntryIndex); 922 this._updateElementPosition(this._selectedElement, this._selectedEntryIndex); 923 this._updateMarkerHighlight(); 924 }, 925 926 _drawMarkers: function() 927 { 928 var markerTimestamps = this._timelineData().markerTimestamps; 929 /** 930 * @param {number} time 931 * @param {number} markerTimestamp 932 * @return {number} 933 */ 934 function compare(time, markerTimestamp) 935 { 936 return time - markerTimestamp; 937 } 938 var left = markerTimestamps.lowerBound(this._calculator.minimumBoundary(), compare); 939 var rightBoundary = this._calculator.maximumBoundary(); 940 941 var context = this._canvas.getContext("2d"); 942 context.save(); 943 var ratio = window.devicePixelRatio; 944 context.scale(ratio, ratio); 945 var height = WebInspector.FlameChart.DividersBarHeight - 1; 946 context.lineWidth = 2; 947 for (var i = left; i < markerTimestamps.length; i++) { 948 var timestamp = markerTimestamps[i]; 949 if (timestamp > rightBoundary) 950 break; 951 var position = this._calculator.computePosition(timestamp); 952 context.strokeStyle = this._dataProvider.markerColor(i); 953 context.beginPath(); 954 context.moveTo(position, 0); 955 context.lineTo(position, height); 956 context.stroke(); 957 } 958 context.restore(); 959 }, 960 961 _updateMarkerHighlight: function() 962 { 963 var element = this._markerHighlighElement; 964 if (element.parentElement) 965 element.remove(); 966 var markerIndex = this._highlightedMarkerIndex; 967 if (markerIndex === -1) 968 return; 969 var barX = this._timeToPosition(this._timelineData().markerTimestamps[markerIndex]); 970 element.title = this._dataProvider.markerTitle(markerIndex); 971 var style = element.style; 972 style.left = barX + "px"; 973 style.backgroundColor = this._dataProvider.markerColor(markerIndex); 974 this.element.appendChild(element); 975 }, 976 977 /** 978 * @param {?WebInspector.FlameChart.TimelineData} timelineData 979 */ 980 _processTimelineData: function(timelineData) 981 { 982 if (!timelineData) { 983 this._timelineLevels = null; 984 this._rawTimelineData = null; 985 this._rawTimelineDataLength = 0; 986 return; 987 } 988 989 var entryCounters = new Uint32Array(this._dataProvider.maxStackDepth() + 1); 990 for (var i = 0; i < timelineData.entryLevels.length; ++i) 991 ++entryCounters[timelineData.entryLevels[i]]; 992 var levelIndexes = new Array(entryCounters.length); 993 for (var i = 0; i < levelIndexes.length; ++i) { 994 levelIndexes[i] = new Uint32Array(entryCounters[i]); 995 entryCounters[i] = 0; 996 } 997 for (var i = 0; i < timelineData.entryLevels.length; ++i) { 998 var level = timelineData.entryLevels[i]; 999 levelIndexes[level][entryCounters[level]++] = i; 1000 } 1001 this._timelineLevels = levelIndexes; 1002 this._rawTimelineData = timelineData; 1003 this._rawTimelineDataLength = timelineData.entryStartTimes.length; 1004 }, 1005 1006 /** 1007 * @param {number} entryIndex 1008 */ 1009 setSelectedEntry: function(entryIndex) 1010 { 1011 this._selectedEntryIndex = entryIndex; 1012 this._updateElementPosition(this._selectedElement, this._selectedEntryIndex); 1013 }, 1014 1015 _updateElementPosition: function(element, entryIndex) 1016 { 1017 if (element.parentElement) 1018 element.remove(); 1019 if (entryIndex === -1) 1020 return; 1021 var timeRange = this._dataProvider.highlightTimeRange(entryIndex); 1022 if (!timeRange) 1023 return; 1024 var timelineData = this._timelineData(); 1025 var barX = this._timeToPosition(timeRange.startTime); 1026 var barRight = this._timeToPosition(timeRange.endTime); 1027 if (barRight === 0 || barX === this._canvas.width) 1028 return; 1029 var barWidth = Math.max(barRight - barX, this._minWidth); 1030 var barY = this._levelToHeight(timelineData.entryLevels[entryIndex]) - this._scrollTop; 1031 var style = element.style; 1032 style.left = barX + "px"; 1033 style.top = barY + "px"; 1034 style.width = barWidth + "px"; 1035 style.height = this._barHeight + "px"; 1036 this.element.appendChild(element); 1037 }, 1038 1039 /** 1040 * @param {number} time 1041 */ 1042 _timeToPosition: function(time) 1043 { 1044 var value = Math.floor((time - this._minimumBoundary) * this._timeToPixel) - this._pixelWindowLeft + this._paddingLeft; 1045 return Math.min(this._canvas.width, Math.max(0, value)); 1046 }, 1047 1048 _levelToHeight: function(level) 1049 { 1050 return this._baseHeight - level * this._barHeightDelta; 1051 }, 1052 1053 _buildEntryInfo: function(entryInfo) 1054 { 1055 var infoTable = document.createElementWithClass("table", "info-table"); 1056 for (var i = 0; i < entryInfo.length; ++i) { 1057 var row = infoTable.createChild("tr"); 1058 row.createChild("td", "title").textContent = entryInfo[i].title; 1059 row.createChild("td").textContent = entryInfo[i].text; 1060 } 1061 return infoTable; 1062 }, 1063 1064 /** 1065 * @param {!CanvasRenderingContext2D} context 1066 * @param {string} title 1067 * @param {number} maxSize 1068 * @return {string} 1069 */ 1070 _prepareText: function(context, title, maxSize) 1071 { 1072 var titleWidth = this._measureWidth(context, title); 1073 if (maxSize >= titleWidth) 1074 return title; 1075 1076 var l = 2; 1077 var r = title.length; 1078 while (l < r) { 1079 var m = (l + r) >> 1; 1080 if (this._measureWidth(context, title.trimMiddle(m)) <= maxSize) 1081 l = m + 1; 1082 else 1083 r = m; 1084 } 1085 title = title.trimMiddle(r - 1); 1086 return title !== "\u2026" ? title : ""; 1087 }, 1088 1089 /** 1090 * @param {!CanvasRenderingContext2D} context 1091 * @param {string} text 1092 * @return {number} 1093 */ 1094 _measureWidth: function(context, text) 1095 { 1096 if (text.length > 20) 1097 return context.measureText(text).width; 1098 1099 var font = context.font; 1100 var textWidths = this._textWidth[font]; 1101 if (!textWidths) { 1102 textWidths = {}; 1103 this._textWidth[font] = textWidths; 1104 } 1105 var width = textWidths[text]; 1106 if (!width) { 1107 width = context.measureText(text).width; 1108 textWidths[text] = width; 1109 } 1110 return width; 1111 }, 1112 1113 _updateBoundaries: function() 1114 { 1115 this._totalTime = this._dataProvider.totalTime(); 1116 this._minimumBoundary = this._dataProvider.minimumBoundary(); 1117 1118 if (this._timeWindowRight !== Infinity) { 1119 this._windowLeft = (this._timeWindowLeft - this._minimumBoundary) / this._totalTime; 1120 this._windowRight = (this._timeWindowRight - this._minimumBoundary) / this._totalTime; 1121 this._windowWidth = this._windowRight - this._windowLeft; 1122 } else { 1123 this._windowLeft = 0; 1124 this._windowRight = 1; 1125 this._windowWidth = 1; 1126 } 1127 1128 this._pixelWindowWidth = this._offsetWidth - this._paddingLeft; 1129 this._totalPixels = Math.floor(this._pixelWindowWidth / this._windowWidth); 1130 this._pixelWindowLeft = Math.floor(this._totalPixels * this._windowLeft); 1131 this._pixelWindowRight = Math.floor(this._totalPixels * this._windowRight); 1132 1133 this._timeToPixel = this._totalPixels / this._totalTime; 1134 this._pixelToTime = this._totalTime / this._totalPixels; 1135 this._paddingLeftTime = this._paddingLeft / this._timeToPixel; 1136 1137 this._baseHeight = this._isTopDown ? WebInspector.FlameChart.DividersBarHeight : this._offsetHeight - this._barHeight; 1138 1139 this._totalHeight = this._levelToHeight(this._dataProvider.maxStackDepth() + 1); 1140 this._vScrollContent.style.height = this._totalHeight + "px"; 1141 this._scrollTop = this._vScrollElement.scrollTop; 1142 this._updateScrollBar(); 1143 }, 1144 1145 onResize: function() 1146 { 1147 this._updateScrollBar(); 1148 this.scheduleUpdate(); 1149 }, 1150 1151 _updateScrollBar: function() 1152 { 1153 var showScroll = this._totalHeight > this._offsetHeight; 1154 this._vScrollElement.classList.toggle("hidden", !showScroll); 1155 this._offsetWidth = this.element.offsetWidth - (WebInspector.isMac() ? 0 : this._vScrollElement.offsetWidth); 1156 this._offsetHeight = this.element.offsetHeight; 1157 }, 1158 1159 scheduleUpdate: function() 1160 { 1161 if (this._updateTimerId || this._cancelWindowTimesAnimation) 1162 return; 1163 this._updateTimerId = requestAnimationFrame(this.update.bind(this)); 1164 }, 1165 1166 update: function() 1167 { 1168 this._updateTimerId = 0; 1169 if (!this._timelineData()) 1170 return; 1171 this._resetCanvas(); 1172 this._updateBoundaries(); 1173 this._calculator._updateBoundaries(this); 1174 this._draw(this._offsetWidth, this._offsetHeight); 1175 }, 1176 1177 reset: function() 1178 { 1179 this._highlightedMarkerIndex = -1; 1180 this._highlightedEntryIndex = -1; 1181 this._selectedEntryIndex = -1; 1182 this._textWidth = {}; 1183 this.update(); 1184 }, 1185 1186 __proto__: WebInspector.HBox.prototype 1187 } 1188