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.addEventListener("mousemove", this._onMouseMove.bind(this), false); 62 this._canvas.addEventListener("mousewheel", this._onMouseWheel.bind(this), false); 63 this._canvas.addEventListener("click", this._onClick.bind(this), false); 64 WebInspector.installDragHandle(this._canvas, this._startCanvasDragging.bind(this), this._canvasDragging.bind(this), this._endCanvasDragging.bind(this), "move", null); 65 66 this._vScrollElement = this.element.createChild("div", "flame-chart-v-scroll"); 67 this._vScrollContent = this._vScrollElement.createChild("div"); 68 this._vScrollElement.addEventListener("scroll", this._scheduleUpdate.bind(this), false); 69 70 this._entryInfo = this.element.createChild("div", "profile-entry-info"); 71 this._highlightElement = this.element.createChild("div", "flame-chart-highlight-element"); 72 this._selectedElement = this.element.createChild("div", "flame-chart-selected-element"); 73 74 this._dataProvider = dataProvider; 75 76 this._windowLeft = 0.0; 77 this._windowRight = 1.0; 78 this._windowWidth = 1.0; 79 this._timeWindowLeft = 0; 80 this._timeWindowRight = Infinity; 81 this._barHeight = dataProvider.barHeight(); 82 this._barHeightDelta = this._isTopDown ? -this._barHeight : this._barHeight; 83 this._minWidth = 1; 84 this._paddingLeft = this._dataProvider.paddingLeft(); 85 this._markerPadding = 2; 86 this._markerRadius = this._barHeight / 2 - this._markerPadding; 87 this._highlightedEntryIndex = -1; 88 this._selectedEntryIndex = -1; 89 this._textWidth = {}; 90 } 91 92 WebInspector.FlameChart.DividersBarHeight = 20; 93 94 /** 95 * @interface 96 */ 97 WebInspector.FlameChartDataProvider = function() 98 { 99 } 100 101 /** @typedef {!{ 102 entryLevels: (!Array.<number>|!Uint8Array), 103 entryTotalTimes: (!Array.<number>|!Float32Array), 104 entryStartTimes: (!Array.<number>|!Float64Array) 105 }} 106 */ 107 WebInspector.FlameChart.TimelineData; 108 109 WebInspector.FlameChartDataProvider.prototype = { 110 /** 111 * @return {number} 112 */ 113 barHeight: function() { }, 114 115 /** 116 * @param {number} startTime 117 * @param {number} endTime 118 * @return {?Array.<number>} 119 */ 120 dividerOffsets: function(startTime, endTime) { }, 121 122 /** 123 * @return {number} 124 */ 125 minimumBoundary: function() { }, 126 127 /** 128 * @return {number} 129 */ 130 totalTime: function() { }, 131 132 /** 133 * @return {number} 134 */ 135 maxStackDepth: function() { }, 136 137 /** 138 * @return {?WebInspector.FlameChart.TimelineData} 139 */ 140 timelineData: function() { }, 141 142 /** 143 * @param {number} entryIndex 144 * @return {?Array.<!{title: string, text: string}>} 145 */ 146 prepareHighlightedEntryInfo: function(entryIndex) { }, 147 148 /** 149 * @param {number} entryIndex 150 * @return {boolean} 151 */ 152 canJumpToEntry: function(entryIndex) { }, 153 154 /** 155 * @param {number} entryIndex 156 * @return {?string} 157 */ 158 entryTitle: function(entryIndex) { }, 159 160 /** 161 * @param {number} entryIndex 162 * @return {?string} 163 */ 164 entryFont: function(entryIndex) { }, 165 166 /** 167 * @param {number} entryIndex 168 * @return {string} 169 */ 170 entryColor: function(entryIndex) { }, 171 172 /** 173 * @param {number} entryIndex 174 * @param {!CanvasRenderingContext2D} context 175 * @param {?string} text 176 * @param {number} barX 177 * @param {number} barY 178 * @param {number} barWidth 179 * @param {number} barHeight 180 * @param {function(number):number} timeToPosition 181 * @return {boolean} 182 */ 183 decorateEntry: function(entryIndex, context, text, barX, barY, barWidth, barHeight, timeToPosition) { }, 184 185 /** 186 * @param {number} entryIndex 187 * @return {boolean} 188 */ 189 forceDecoration: function(entryIndex) { }, 190 191 /** 192 * @param {number} entryIndex 193 * @return {string} 194 */ 195 textColor: function(entryIndex) { }, 196 197 /** 198 * @return {number} 199 */ 200 textBaseline: function() { }, 201 202 /** 203 * @return {number} 204 */ 205 textPadding: function() { }, 206 207 /** 208 * @return {?{startTime: number, endTime: number}} 209 */ 210 highlightTimeRange: function(entryIndex) { }, 211 212 /** 213 * @return {number} 214 */ 215 paddingLeft: function() { }, 216 } 217 218 WebInspector.FlameChart.Events = { 219 EntrySelected: "EntrySelected" 220 } 221 222 223 /** 224 * @constructor 225 * @param {!{min: number, max: number, count: number}|number=} hueSpace 226 * @param {!{min: number, max: number, count: number}|number=} satSpace 227 * @param {!{min: number, max: number, count: number}|number=} lightnessSpace 228 */ 229 WebInspector.FlameChart.ColorGenerator = function(hueSpace, satSpace, lightnessSpace) 230 { 231 this._hueSpace = hueSpace || { min: 0, max: 360, count: 20 }; 232 this._satSpace = satSpace || 67; 233 this._lightnessSpace = lightnessSpace || 80; 234 this._colors = {}; 235 } 236 237 WebInspector.FlameChart.ColorGenerator.prototype = { 238 /** 239 * @param {string} id 240 * @param {string|!CanvasGradient} color 241 */ 242 setColorForID: function(id, color) 243 { 244 this._colors[id] = color; 245 }, 246 247 /** 248 * @param {string} id 249 * @return {string} 250 */ 251 colorForID: function(id) 252 { 253 var color = this._colors[id]; 254 if (!color) { 255 color = this._generateColorForID(id); 256 this._colors[id] = color; 257 } 258 return color; 259 }, 260 261 /** 262 * @param {string} id 263 * @return {string} 264 */ 265 _generateColorForID: function(id) 266 { 267 var hash = id.hashCode(); 268 var h = this._indexToValueInSpace(hash, this._hueSpace); 269 var s = this._indexToValueInSpace(hash, this._satSpace); 270 var l = this._indexToValueInSpace(hash, this._lightnessSpace); 271 return "hsl(" + h + ", " + s + "%, " + l + "%)"; 272 }, 273 274 /** 275 * @param {number} index 276 * @param {!{min: number, max: number, count: number}|number} space 277 * @return {number} 278 */ 279 _indexToValueInSpace: function(index, space) 280 { 281 if (typeof space === "number") 282 return space; 283 index %= space.count; 284 return space.min + Math.floor(index / space.count * (space.max - space.min)); 285 } 286 } 287 288 289 /** 290 * @constructor 291 * @implements {WebInspector.TimelineGrid.Calculator} 292 */ 293 WebInspector.FlameChart.Calculator = function() 294 { 295 this._paddingLeft = 0; 296 } 297 298 WebInspector.FlameChart.Calculator.prototype = { 299 /** 300 * @return {number} 301 */ 302 paddingLeft: function() 303 { 304 return this._paddingLeft; 305 }, 306 307 /** 308 * @param {!WebInspector.FlameChart} mainPane 309 */ 310 _updateBoundaries: function(mainPane) 311 { 312 this._totalTime = mainPane._dataProvider.totalTime(); 313 this._zeroTime = mainPane._dataProvider.minimumBoundary(); 314 this._minimumBoundaries = this._zeroTime + mainPane._windowLeft * this._totalTime; 315 this._maximumBoundaries = this._zeroTime + mainPane._windowRight * this._totalTime; 316 this._paddingLeft = mainPane._paddingLeft; 317 this._width = mainPane._canvas.width / window.devicePixelRatio - this._paddingLeft; 318 this._timeToPixel = this._width / this.boundarySpan(); 319 }, 320 321 /** 322 * @param {number} time 323 * @return {number} 324 */ 325 computePosition: function(time) 326 { 327 return Math.round((time - this._minimumBoundaries) * this._timeToPixel + this._paddingLeft); 328 }, 329 330 /** 331 * @param {number} value 332 * @param {number=} precision 333 * @return {string} 334 */ 335 formatTime: function(value, precision) 336 { 337 return Number.preciseMillisToString(value - this._zeroTime, precision); 338 }, 339 340 /** 341 * @return {number} 342 */ 343 maximumBoundary: function() 344 { 345 return this._maximumBoundaries; 346 }, 347 348 /** 349 * @return {number} 350 */ 351 minimumBoundary: function() 352 { 353 return this._minimumBoundaries; 354 }, 355 356 /** 357 * @return {number} 358 */ 359 zeroTime: function() 360 { 361 return this._zeroTime; 362 }, 363 364 /** 365 * @return {number} 366 */ 367 boundarySpan: function() 368 { 369 return this._maximumBoundaries - this._minimumBoundaries; 370 } 371 } 372 373 WebInspector.FlameChart.prototype = { 374 _resetCanvas: function() 375 { 376 var ratio = window.devicePixelRatio; 377 this._canvas.width = this._offsetWidth * ratio; 378 this._canvas.height = this._offsetHeight * ratio; 379 this._canvas.style.width = this._offsetWidth + "px"; 380 this._canvas.style.height = this._offsetHeight + "px"; 381 }, 382 383 /** 384 * @return {?WebInspector.FlameChart.TimelineData} 385 */ 386 _timelineData: function() 387 { 388 var timelineData = this._dataProvider.timelineData(); 389 if (timelineData !== this._rawTimelineData || timelineData.entryStartTimes.length !== this._rawTimelineDataLength) 390 this._processTimelineData(timelineData); 391 return this._rawTimelineData; 392 }, 393 394 /** 395 * @param {number} startTime 396 * @param {number} endTime 397 */ 398 setWindowTimes: function(startTime, endTime) 399 { 400 this._timeWindowLeft = startTime; 401 this._timeWindowRight = endTime; 402 this._scheduleUpdate(); 403 }, 404 405 /** 406 * @param {!MouseEvent} event 407 */ 408 _startCanvasDragging: function(event) 409 { 410 if (!this._timelineData() || this._timeWindowRight === Infinity) 411 return false; 412 this._isDragging = true; 413 this._maxDragOffset = 0; 414 this._dragStartPointX = event.pageX; 415 this._dragStartPointY = event.pageY; 416 this._dragStartScrollTop = this._vScrollElement.scrollTop; 417 this._dragStartWindowLeft = this._timeWindowLeft; 418 this._dragStartWindowRight = this._timeWindowRight; 419 this._canvas.style.cursor = ""; 420 421 return true; 422 }, 423 424 /** 425 * @param {!MouseEvent} event 426 */ 427 _canvasDragging: function(event) 428 { 429 var pixelShift = this._dragStartPointX - event.pageX; 430 var pixelScroll = this._dragStartPointY - event.pageY; 431 this._vScrollElement.scrollTop = this._dragStartScrollTop + pixelScroll; 432 var windowShift = pixelShift / this._totalPixels; 433 var windowTime = this._windowWidth * this._totalTime; 434 var timeShift = windowTime * pixelShift / this._pixelWindowWidth; 435 timeShift = Number.constrain( 436 timeShift, 437 this._minimumBoundary - this._dragStartWindowLeft, 438 this._minimumBoundary + this._totalTime - this._dragStartWindowRight 439 ); 440 var windowLeft = this._dragStartWindowLeft + timeShift; 441 var windowRight = this._dragStartWindowRight + timeShift; 442 this._flameChartDelegate.requestWindowTimes(windowLeft, windowRight); 443 this._maxDragOffset = Math.max(this._maxDragOffset, Math.abs(pixelShift)); 444 }, 445 446 _endCanvasDragging: function() 447 { 448 this._isDragging = false; 449 }, 450 451 /** 452 * @param {?Event} event 453 */ 454 _onMouseMove: function(event) 455 { 456 if (this._isDragging) 457 return; 458 var entryIndex = this._coordinatesToEntryIndex(event.offsetX, event.offsetY); 459 460 if (this._highlightedEntryIndex === entryIndex) 461 return; 462 463 if (entryIndex === -1 || !this._dataProvider.canJumpToEntry(entryIndex)) 464 this._canvas.style.cursor = "default"; 465 else 466 this._canvas.style.cursor = "pointer"; 467 468 this._highlightedEntryIndex = entryIndex; 469 470 this._updateElementPosition(this._highlightElement, this._highlightedEntryIndex); 471 this._entryInfo.removeChildren(); 472 473 if (this._highlightedEntryIndex === -1) 474 return; 475 476 if (!this._isDragging) { 477 var entryInfo = this._dataProvider.prepareHighlightedEntryInfo(this._highlightedEntryIndex); 478 if (entryInfo) 479 this._entryInfo.appendChild(this._buildEntryInfo(entryInfo)); 480 } 481 }, 482 483 _onClick: function() 484 { 485 // onClick comes after dragStart and dragEnd events. 486 // So if there was drag (mouse move) in the middle of that events 487 // we skip the click. Otherwise we jump to the sources. 488 const clickThreshold = 5; 489 if (this._maxDragOffset > clickThreshold) 490 return; 491 if (this._highlightedEntryIndex === -1) 492 return; 493 this.dispatchEventToListeners(WebInspector.FlameChart.Events.EntrySelected, this._highlightedEntryIndex); 494 }, 495 496 /** 497 * @param {?Event} e 498 */ 499 _onMouseWheel: function(e) 500 { 501 var scrollIsThere = this._totalHeight > this._offsetHeight; 502 var windowLeft = this._timeWindowLeft ? this._timeWindowLeft : this._dataProvider.minimumBoundary(); 503 var windowRight = this._timeWindowRight !== Infinity ? this._timeWindowRight : this._dataProvider.minimumBoundary() + this._dataProvider.totalTime(); 504 505 var panHorizontally = Math.abs(e.wheelDeltaX) > Math.abs(e.wheelDeltaY) && !e.shiftKey; 506 var panVertically = scrollIsThere && ((e.wheelDeltaY && !e.shiftKey) || (Math.abs(e.wheelDeltaX) === 120 && !e.shiftKey)); 507 if (panVertically) { 508 this._vScrollElement.scrollTop -= e.wheelDeltaY / 120 * this._offsetHeight / 8; 509 } else if (panHorizontally) { 510 var shift = -e.wheelDeltaX * this._pixelToTime; 511 shift = Number.constrain(shift, this._minimumBoundary - windowLeft, this._totalTime + this._minimumBoundary - windowRight); 512 windowLeft += shift; 513 windowRight += shift; 514 } else { // Zoom. 515 const mouseWheelZoomSpeed = 1 / 120; 516 var zoom = Math.pow(1.2, -(e.wheelDeltaY || e.wheelDeltaX) * mouseWheelZoomSpeed) - 1; 517 var cursorTime = this._cursorTime(e.offsetX); 518 windowLeft += (windowLeft - cursorTime) * zoom; 519 windowRight += (windowRight - cursorTime) * zoom; 520 } 521 windowLeft = Number.constrain(windowLeft, this._minimumBoundary, this._totalTime + this._minimumBoundary); 522 windowRight = Number.constrain(windowRight, this._minimumBoundary, this._totalTime + this._minimumBoundary); 523 this._flameChartDelegate.requestWindowTimes(windowLeft, windowRight); 524 525 // Block swipe gesture. 526 e.consume(true); 527 }, 528 529 /** 530 * @param {number} x 531 * @return {number} 532 */ 533 _cursorTime: function(x) 534 { 535 return (x + this._pixelWindowLeft - this._paddingLeft) * this._pixelToTime + this._minimumBoundary; 536 }, 537 538 /** 539 * @param {number} x 540 * @param {number} y 541 * @return {number} 542 */ 543 _coordinatesToEntryIndex: function(x, y) 544 { 545 y += this._scrollTop; 546 var timelineData = this._timelineData(); 547 if (!timelineData) 548 return -1; 549 var cursorTime = this._cursorTime(x); 550 var cursorLevel; 551 var offsetFromLevel; 552 if (this._isTopDown) { 553 cursorLevel = Math.floor((y - WebInspector.FlameChart.DividersBarHeight) / this._barHeight); 554 offsetFromLevel = y - WebInspector.FlameChart.DividersBarHeight - cursorLevel * this._barHeight; 555 } else { 556 cursorLevel = Math.floor((this._canvas.height / window.devicePixelRatio - y) / this._barHeight); 557 offsetFromLevel = this._canvas.height / window.devicePixelRatio - cursorLevel * this._barHeight; 558 } 559 var entryStartTimes = timelineData.entryStartTimes; 560 var entryTotalTimes = timelineData.entryTotalTimes; 561 var entryIndexes = this._timelineLevels[cursorLevel]; 562 if (!entryIndexes || !entryIndexes.length) 563 return -1; 564 565 function comparator(time, entryIndex) 566 { 567 return time - entryStartTimes[entryIndex]; 568 } 569 var indexOnLevel = Math.max(entryIndexes.upperBound(cursorTime, comparator) - 1, 0); 570 571 /** 572 * @this {WebInspector.FlameChart} 573 * @param {number} entryIndex 574 * @return {boolean} 575 */ 576 function checkEntryHit(entryIndex) 577 { 578 if (entryIndex === undefined) 579 return false; 580 var startTime = entryStartTimes[entryIndex]; 581 var duration = entryTotalTimes[entryIndex]; 582 if (isNaN(duration)) { 583 var dx = (startTime - cursorTime) / this._pixelToTime; 584 var dy = this._barHeight / 2 - offsetFromLevel; 585 return dx * dx + dy * dy < this._markerRadius * this._markerRadius; 586 } 587 var endTime = startTime + duration; 588 var barThreshold = 3 * this._pixelToTime; 589 return startTime - barThreshold < cursorTime && cursorTime < endTime + barThreshold; 590 } 591 592 var entryIndex = entryIndexes[indexOnLevel]; 593 if (checkEntryHit.call(this, entryIndex)) 594 return entryIndex; 595 entryIndex = entryIndexes[indexOnLevel + 1]; 596 if (checkEntryHit.call(this, entryIndex)) 597 return entryIndex; 598 return -1; 599 }, 600 601 /** 602 * @param {number} height 603 * @param {number} width 604 */ 605 _draw: function(width, height) 606 { 607 var timelineData = this._timelineData(); 608 if (!timelineData) 609 return; 610 611 var context = this._canvas.getContext("2d"); 612 context.save(); 613 var ratio = window.devicePixelRatio; 614 context.scale(ratio, ratio); 615 616 var timeWindowRight = this._timeWindowRight; 617 var timeWindowLeft = this._timeWindowLeft; 618 var timeToPixel = this._timeToPixel; 619 var pixelWindowLeft = this._pixelWindowLeft; 620 var paddingLeft = this._paddingLeft; 621 var minWidth = this._minWidth; 622 var entryTotalTimes = timelineData.entryTotalTimes; 623 var entryStartTimes = timelineData.entryStartTimes; 624 var entryLevels = timelineData.entryLevels; 625 626 var titleIndices = new Uint32Array(timelineData.entryTotalTimes); 627 var nextTitleIndex = 0; 628 var markerIndices = new Uint32Array(timelineData.entryTotalTimes); 629 var nextMarkerIndex = 0; 630 var textPadding = this._dataProvider.textPadding(); 631 this._minTextWidth = 2 * textPadding + this._measureWidth(context, "\u2026"); 632 var minTextWidth = this._minTextWidth; 633 634 var barHeight = this._barHeight; 635 636 var timeToPosition = this._timeToPosition.bind(this); 637 var textBaseHeight = this._baseHeight + barHeight - this._dataProvider.textBaseline(); 638 var colorBuckets = {}; 639 var minVisibleBarLevel = Math.max(Math.floor((this._scrollTop - this._baseHeight) / barHeight), 0); 640 var maxVisibleBarLevel = Math.min(Math.floor((this._scrollTop - this._baseHeight + height) / barHeight), this._dataProvider.maxStackDepth()); 641 642 context.translate(0, -this._scrollTop); 643 644 function comparator(time, entryIndex) 645 { 646 return time - entryStartTimes[entryIndex]; 647 } 648 649 for (var level = minVisibleBarLevel; level <= maxVisibleBarLevel; ++level) { 650 // Entries are ordered by start time within a level, so find the last visible entry. 651 var levelIndexes = this._timelineLevels[level]; 652 var rightIndexOnLevel = levelIndexes.lowerBound(timeWindowRight, comparator) - 1; 653 var lastDrawOffset = Infinity; 654 for (var entryIndexOnLevel = rightIndexOnLevel; entryIndexOnLevel >= 0; --entryIndexOnLevel) { 655 var entryIndex = levelIndexes[entryIndexOnLevel]; 656 var entryStartTime = entryStartTimes[entryIndex]; 657 var entryOffsetRight = entryStartTime + (isNaN(entryTotalTimes[entryIndex]) ? 0 : entryTotalTimes[entryIndex]); 658 if (entryOffsetRight <= timeWindowLeft) 659 break; 660 661 var barX = this._timeToPosition(entryStartTime); 662 if (barX >= lastDrawOffset) 663 continue; 664 var barRight = Math.min(this._timeToPosition(entryOffsetRight), lastDrawOffset); 665 lastDrawOffset = barX; 666 667 var color = this._dataProvider.entryColor(entryIndex); 668 var bucket = colorBuckets[color]; 669 if (!bucket) { 670 bucket = []; 671 colorBuckets[color] = bucket; 672 } 673 bucket.push(entryIndex); 674 } 675 } 676 677 var colors = Object.keys(colorBuckets); 678 // We don't use for-in here because it couldn't be optimized. 679 for (var c = 0; c < colors.length; ++c) { 680 var color = colors[c]; 681 context.fillStyle = color; 682 context.strokeStyle = color; 683 var indexes = colorBuckets[color]; 684 685 // First fill the boxes. 686 context.beginPath(); 687 for (var i = 0; i < indexes.length; ++i) { 688 var entryIndex = indexes[i]; 689 var entryStartTime = entryStartTimes[entryIndex]; 690 var barX = this._timeToPosition(entryStartTime); 691 var barRight = this._timeToPosition(entryStartTime + entryTotalTimes[entryIndex]); 692 var barWidth = Math.max(barRight - barX, minWidth); 693 var barLevel = entryLevels[entryIndex]; 694 var barY = this._levelToHeight(barLevel); 695 if (isNaN(entryTotalTimes[entryIndex])) { 696 context.moveTo(barX + this._markerRadius, barY + barHeight / 2); 697 context.arc(barX, barY + barHeight / 2, this._markerRadius, 0, Math.PI * 2); 698 markerIndices[nextMarkerIndex++] = entryIndex; 699 } else { 700 context.rect(barX, barY, barWidth, barHeight); 701 if (barWidth > minTextWidth || this._dataProvider.forceDecoration(entryIndex)) 702 titleIndices[nextTitleIndex++] = entryIndex; 703 } 704 } 705 context.fill(); 706 } 707 708 context.strokeStyle = "rgb(0, 0, 0)"; 709 context.beginPath(); 710 for (var m = 0; m < nextMarkerIndex; ++m) { 711 var entryIndex = markerIndices[m]; 712 var entryStartTime = entryStartTimes[entryIndex]; 713 var barX = this._timeToPosition(entryStartTime); 714 var barLevel = entryLevels[entryIndex]; 715 var barY = this._levelToHeight(barLevel); 716 context.moveTo(barX + this._markerRadius, barY + barHeight / 2); 717 context.arc(barX, barY + barHeight / 2, this._markerRadius, 0, Math.PI * 2); 718 } 719 context.stroke(); 720 721 context.textBaseline = "alphabetic"; 722 723 for (var i = 0; i < nextTitleIndex; ++i) { 724 var entryIndex = titleIndices[i]; 725 var entryStartTime = entryStartTimes[entryIndex]; 726 var barX = this._timeToPosition(entryStartTime); 727 var barRight = this._timeToPosition(entryStartTime + entryTotalTimes[entryIndex]); 728 var barWidth = Math.max(barRight - barX, minWidth); 729 var barLevel = entryLevels[entryIndex]; 730 var barY = this._levelToHeight(barLevel); 731 var text = this._dataProvider.entryTitle(entryIndex); 732 if (text && text.length) { 733 context.font = this._dataProvider.entryFont(entryIndex); 734 text = this._prepareText(context, text, barWidth - 2 * textPadding); 735 } 736 737 if (this._dataProvider.decorateEntry(entryIndex, context, text, barX, barY, barWidth, barHeight, timeToPosition)) 738 continue; 739 if (!text || !text.length) 740 continue; 741 742 context.fillStyle = this._dataProvider.textColor(entryIndex); 743 context.fillText(text, barX + textPadding, textBaseHeight - barLevel * this._barHeightDelta); 744 } 745 context.restore(); 746 747 var offsets = this._dataProvider.dividerOffsets(this._calculator.minimumBoundary(), this._calculator.maximumBoundary()); 748 WebInspector.TimelineGrid.drawCanvasGrid(this._canvas, this._calculator, offsets); 749 750 this._updateElementPosition(this._highlightElement, this._highlightedEntryIndex); 751 this._updateElementPosition(this._selectedElement, this._selectedEntryIndex); 752 }, 753 754 /** 755 * @param {?WebInspector.FlameChart.TimelineData} timelineData 756 */ 757 _processTimelineData: function(timelineData) 758 { 759 if (!timelineData) { 760 this._timelineLevels = null; 761 this._rawTimelineData = null; 762 this._rawTimelineDataLength = 0; 763 return; 764 } 765 766 var entryCounters = new Uint32Array(this._dataProvider.maxStackDepth() + 1); 767 for (var i = 0; i < timelineData.entryLevels.length; ++i) 768 ++entryCounters[timelineData.entryLevels[i]]; 769 var levelIndexes = new Array(entryCounters.length); 770 for (var i = 0; i < levelIndexes.length; ++i) { 771 levelIndexes[i] = new Uint32Array(entryCounters[i]); 772 entryCounters[i] = 0; 773 } 774 for (var i = 0; i < timelineData.entryLevels.length; ++i) { 775 var level = timelineData.entryLevels[i]; 776 levelIndexes[level][entryCounters[level]++] = i; 777 } 778 this._timelineLevels = levelIndexes; 779 this._rawTimelineData = timelineData; 780 this._rawTimelineDataLength = timelineData.entryStartTimes.length; 781 }, 782 783 /** 784 * @param {number} entryIndex 785 */ 786 setSelectedEntry: function(entryIndex) 787 { 788 this._selectedEntryIndex = entryIndex; 789 this._updateElementPosition(this._selectedElement, this._selectedEntryIndex); 790 }, 791 792 _updateElementPosition: function(element, entryIndex) 793 { 794 if (element.parentElement) 795 element.remove(); 796 if (entryIndex === -1) 797 return; 798 var timeRange = this._dataProvider.highlightTimeRange(entryIndex); 799 if (!timeRange) 800 return; 801 var timelineData = this._timelineData(); 802 var barX = this._timeToPosition(timeRange.startTime); 803 var barRight = this._timeToPosition(timeRange.endTime); 804 if (barRight === 0 || barX === this._canvas.width) 805 return; 806 var barWidth = Math.max(barRight - barX, this._minWidth); 807 var barY = this._levelToHeight(timelineData.entryLevels[entryIndex]) - this._scrollTop; 808 var style = element.style; 809 style.left = barX + "px"; 810 style.top = barY + "px"; 811 style.width = barWidth + "px"; 812 style.height = this._barHeight + "px"; 813 this.element.appendChild(element); 814 }, 815 816 /** 817 * @param {number} time 818 */ 819 _timeToPosition: function(time) 820 { 821 var value = Math.floor((time - this._minimumBoundary) * this._timeToPixel) - this._pixelWindowLeft + this._paddingLeft; 822 return Math.min(this._canvas.width, Math.max(0, value)); 823 }, 824 825 _levelToHeight: function(level) 826 { 827 return this._baseHeight - level * this._barHeightDelta; 828 }, 829 830 _buildEntryInfo: function(entryInfo) 831 { 832 var infoTable = document.createElement("table"); 833 infoTable.className = "info-table"; 834 for (var i = 0; i < entryInfo.length; ++i) { 835 var row = infoTable.createChild("tr"); 836 var titleCell = row.createChild("td"); 837 titleCell.textContent = entryInfo[i].title; 838 titleCell.className = "title"; 839 var textCell = row.createChild("td"); 840 textCell.textContent = entryInfo[i].text; 841 } 842 return infoTable; 843 }, 844 845 /** 846 * @param {!CanvasRenderingContext2D} context 847 * @param {string} title 848 * @param {number} maxSize 849 * @return {string} 850 */ 851 _prepareText: function(context, title, maxSize) 852 { 853 var titleWidth = this._measureWidth(context, title); 854 if (maxSize >= titleWidth) 855 return title; 856 857 var l = 2; 858 var r = title.length; 859 while (l < r) { 860 var m = (l + r) >> 1; 861 if (this._measureWidth(context, title.trimMiddle(m)) <= maxSize) 862 l = m + 1; 863 else 864 r = m; 865 } 866 title = title.trimMiddle(r - 1); 867 return title !== "\u2026" ? title : ""; 868 }, 869 870 /** 871 * @param {!CanvasRenderingContext2D} context 872 * @param {string} text 873 * @return {number} 874 */ 875 _measureWidth: function(context, text) 876 { 877 if (text.length > 20) 878 return context.measureText(text).width; 879 880 var font = context.font; 881 var textWidths = this._textWidth[font]; 882 if (!textWidths) { 883 textWidths = {}; 884 this._textWidth[font] = textWidths; 885 } 886 var width = textWidths[text]; 887 if (!width) { 888 width = context.measureText(text).width; 889 textWidths[text] = width; 890 } 891 return width; 892 }, 893 894 _updateBoundaries: function() 895 { 896 this._totalTime = this._dataProvider.totalTime(); 897 this._minimumBoundary = this._dataProvider.minimumBoundary(); 898 899 if (this._timeWindowRight !== Infinity) { 900 this._windowLeft = (this._timeWindowLeft - this._minimumBoundary) / this._totalTime; 901 this._windowRight = (this._timeWindowRight - this._minimumBoundary) / this._totalTime; 902 this._windowWidth = this._windowRight - this._windowLeft; 903 } else { 904 this._windowLeft = 0; 905 this._windowRight = 1; 906 this._windowWidth = 1; 907 } 908 909 this._pixelWindowWidth = this._offsetWidth - this._paddingLeft; 910 this._totalPixels = Math.floor(this._pixelWindowWidth / this._windowWidth); 911 this._pixelWindowLeft = Math.floor(this._totalPixels * this._windowLeft); 912 this._pixelWindowRight = Math.floor(this._totalPixels * this._windowRight); 913 914 this._timeToPixel = this._totalPixels / this._totalTime; 915 this._pixelToTime = this._totalTime / this._totalPixels; 916 this._paddingLeftTime = this._paddingLeft / this._timeToPixel; 917 918 this._baseHeight = this._isTopDown ? WebInspector.FlameChart.DividersBarHeight : this._offsetHeight - this._barHeight; 919 920 this._totalHeight = this._levelToHeight(this._dataProvider.maxStackDepth() + 1); 921 this._vScrollContent.style.height = this._totalHeight + "px"; 922 this._scrollTop = this._vScrollElement.scrollTop; 923 this._updateScrollBar(); 924 }, 925 926 onResize: function() 927 { 928 this._updateScrollBar(); 929 this._scheduleUpdate(); 930 }, 931 932 _updateScrollBar: function() 933 { 934 var showScroll = this._totalHeight > this._offsetHeight; 935 this._vScrollElement.classList.toggle("hidden", !showScroll); 936 this._offsetWidth = this.element.offsetWidth - (WebInspector.isMac() ? 0 : this._vScrollElement.offsetWidth); 937 this._offsetHeight = this.element.offsetHeight; 938 }, 939 940 _scheduleUpdate: function() 941 { 942 if (this._updateTimerId) 943 return; 944 this._updateTimerId = requestAnimationFrame(this.update.bind(this)); 945 }, 946 947 update: function() 948 { 949 this._updateTimerId = 0; 950 if (!this._timelineData()) 951 return; 952 this._resetCanvas(); 953 this._updateBoundaries(); 954 this._calculator._updateBoundaries(this); 955 this._draw(this._offsetWidth, this._offsetHeight); 956 }, 957 958 reset: function() 959 { 960 this._highlightedEntryIndex = -1; 961 this._selectedEntryIndex = -1; 962 this._textWidth = {}; 963 this.update(); 964 }, 965 966 __proto__: WebInspector.HBox.prototype 967 } 968