| OLD | NEW |
| (Empty) |
| 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 UI.FlameChartDelegate = function() {}; | |
| 35 | |
| 36 UI.FlameChartDelegate.prototype = { | |
| 37 /** | |
| 38 * @param {number} startTime | |
| 39 * @param {number} endTime | |
| 40 */ | |
| 41 requestWindowTimes(startTime, endTime) {}, | |
| 42 | |
| 43 /** | |
| 44 * @param {number} startTime | |
| 45 * @param {number} endTime | |
| 46 */ | |
| 47 updateRangeSelection(startTime, endTime) {}, | |
| 48 }; | |
| 49 | |
| 50 /** | |
| 51 * @unrestricted | |
| 52 */ | |
| 53 UI.FlameChart = class extends UI.ChartViewport { | |
| 54 /** | |
| 55 * @param {!UI.FlameChartDataProvider} dataProvider | |
| 56 * @param {!UI.FlameChartDelegate} flameChartDelegate | |
| 57 * @param {!Common.Setting=} groupExpansionSetting | |
| 58 */ | |
| 59 constructor(dataProvider, flameChartDelegate, groupExpansionSetting) { | |
| 60 super(); | |
| 61 this.registerRequiredCSS('ui_lazy/flameChart.css'); | |
| 62 this.contentElement.classList.add('flame-chart-main-pane'); | |
| 63 this._flameChartDelegate = flameChartDelegate; | |
| 64 this._groupExpansionSetting = groupExpansionSetting; | |
| 65 this._groupExpansionState = groupExpansionSetting && groupExpansionSetting.g
et() || {}; | |
| 66 | |
| 67 this._dataProvider = dataProvider; | |
| 68 this._calculator = new UI.FlameChart.Calculator(dataProvider); | |
| 69 | |
| 70 this._canvas = /** @type {!HTMLCanvasElement} */ (this.viewportElement.creat
eChild('canvas')); | |
| 71 this._canvas.tabIndex = 1; | |
| 72 this.setDefaultFocusedElement(this._canvas); | |
| 73 this._canvas.addEventListener('mousemove', this._onMouseMove.bind(this), fal
se); | |
| 74 this._canvas.addEventListener('mouseout', this._onMouseOut.bind(this), false
); | |
| 75 this._canvas.addEventListener('click', this._onClick.bind(this), false); | |
| 76 this._canvas.addEventListener('keydown', this._onKeyDown.bind(this), false); | |
| 77 | |
| 78 this._entryInfo = this.viewportElement.createChild('div', 'flame-chart-entry
-info'); | |
| 79 this._markerHighlighElement = this.viewportElement.createChild('div', 'flame
-chart-marker-highlight-element'); | |
| 80 this._highlightElement = this.viewportElement.createChild('div', 'flame-char
t-highlight-element'); | |
| 81 this._selectedElement = this.viewportElement.createChild('div', 'flame-chart
-selected-element'); | |
| 82 | |
| 83 this._rulerEnabled = true; | |
| 84 this._windowLeft = 0.0; | |
| 85 this._windowRight = 1.0; | |
| 86 this._timeWindowLeft = 0; | |
| 87 this._timeWindowRight = Infinity; | |
| 88 this._rangeSelectionStart = 0; | |
| 89 this._rangeSelectionEnd = 0; | |
| 90 this._barHeight = 17; | |
| 91 this._textBaseline = 5; | |
| 92 this._textPadding = 5; | |
| 93 this._paddingLeft = 0; | |
| 94 var markerPadding = 2; | |
| 95 this._markerRadius = this._barHeight / 2 - markerPadding; | |
| 96 | |
| 97 /** @const */ | |
| 98 this._headerLeftPadding = 6; | |
| 99 /** @const */ | |
| 100 this._arrowSide = 8; | |
| 101 /** @const */ | |
| 102 this._expansionArrowIndent = this._headerLeftPadding + this._arrowSide / 2; | |
| 103 /** @const */ | |
| 104 this._headerLabelXPadding = 3; | |
| 105 /** @const */ | |
| 106 this._headerLabelYPadding = 2; | |
| 107 | |
| 108 this._highlightedMarkerIndex = -1; | |
| 109 this._highlightedEntryIndex = -1; | |
| 110 this._selectedEntryIndex = -1; | |
| 111 this._rawTimelineDataLength = 0; | |
| 112 /** @type {!Map<string,!Map<string,number>>} */ | |
| 113 this._textWidth = new Map(); | |
| 114 | |
| 115 this._lastMouseOffsetX = 0; | |
| 116 } | |
| 117 | |
| 118 /** | |
| 119 * @override | |
| 120 */ | |
| 121 willHide() { | |
| 122 this.hideHighlight(); | |
| 123 } | |
| 124 | |
| 125 /** | |
| 126 * @param {number} value | |
| 127 */ | |
| 128 setBarHeight(value) { | |
| 129 this._barHeight = value; | |
| 130 } | |
| 131 | |
| 132 /** | |
| 133 * @param {number} value | |
| 134 */ | |
| 135 setTextBaseline(value) { | |
| 136 this._textBaseline = value; | |
| 137 } | |
| 138 | |
| 139 /** | |
| 140 * @param {number} value | |
| 141 */ | |
| 142 setTextPadding(value) { | |
| 143 this._textPadding = value; | |
| 144 } | |
| 145 | |
| 146 /** | |
| 147 * @param {number} value | |
| 148 */ | |
| 149 setPaddingLeft(value) { | |
| 150 this._paddingLeft = value; | |
| 151 } | |
| 152 | |
| 153 /** | |
| 154 * @param {boolean} enable | |
| 155 */ | |
| 156 enableRuler(enable) { | |
| 157 this._rulerEnabled = enable; | |
| 158 } | |
| 159 | |
| 160 /** | |
| 161 * @param {number} entryIndex | |
| 162 */ | |
| 163 highlightEntry(entryIndex) { | |
| 164 if (this._highlightedEntryIndex === entryIndex) | |
| 165 return; | |
| 166 this._highlightedEntryIndex = entryIndex; | |
| 167 this._updateElementPosition(this._highlightElement, this._highlightedEntryIn
dex); | |
| 168 } | |
| 169 | |
| 170 hideHighlight() { | |
| 171 this._entryInfo.removeChildren(); | |
| 172 this._highlightedEntryIndex = -1; | |
| 173 this._updateElementPosition(this._highlightElement, this._highlightedEntryIn
dex); | |
| 174 } | |
| 175 | |
| 176 _resetCanvas() { | |
| 177 var ratio = window.devicePixelRatio; | |
| 178 this._canvas.width = this._offsetWidth * ratio; | |
| 179 this._canvas.height = this._offsetHeight * ratio; | |
| 180 this._canvas.style.width = this._offsetWidth + 'px'; | |
| 181 this._canvas.style.height = this._offsetHeight + 'px'; | |
| 182 } | |
| 183 | |
| 184 /** | |
| 185 * @return {?UI.FlameChart.TimelineData} | |
| 186 */ | |
| 187 _timelineData() { | |
| 188 if (!this._dataProvider) | |
| 189 return null; | |
| 190 var timelineData = this._dataProvider.timelineData(); | |
| 191 if (timelineData !== this._rawTimelineData || timelineData.entryStartTimes.l
ength !== this._rawTimelineDataLength) | |
| 192 this._processTimelineData(timelineData); | |
| 193 return this._rawTimelineData; | |
| 194 } | |
| 195 | |
| 196 /** | |
| 197 * @param {number} entryIndex | |
| 198 */ | |
| 199 _revealEntry(entryIndex) { | |
| 200 var timelineData = this._timelineData(); | |
| 201 if (!timelineData) | |
| 202 return; | |
| 203 // Think in terms of not where we are, but where we'll be after animation (i
f present) | |
| 204 var timeLeft = this._cancelWindowTimesAnimation ? this._pendingAnimationTime
Left : this._timeWindowLeft; | |
| 205 var timeRight = this._cancelWindowTimesAnimation ? this._pendingAnimationTim
eRight : this._timeWindowRight; | |
| 206 var entryStartTime = timelineData.entryStartTimes[entryIndex]; | |
| 207 var entryTotalTime = timelineData.entryTotalTimes[entryIndex]; | |
| 208 var entryEndTime = entryStartTime + entryTotalTime; | |
| 209 var minEntryTimeWindow = Math.min(entryTotalTime, timeRight - timeLeft); | |
| 210 | |
| 211 var y = this._levelToHeight(timelineData.entryLevels[entryIndex]); | |
| 212 this.setScrollOffset(y, this._barHeight); | |
| 213 | |
| 214 if (timeLeft > entryEndTime) { | |
| 215 var delta = timeLeft - entryEndTime + minEntryTimeWindow; | |
| 216 this._flameChartDelegate.requestWindowTimes(timeLeft - delta, timeRight -
delta); | |
| 217 } else if (timeRight < entryStartTime) { | |
| 218 var delta = entryStartTime - timeRight + minEntryTimeWindow; | |
| 219 this._flameChartDelegate.requestWindowTimes(timeLeft + delta, timeRight +
delta); | |
| 220 } | |
| 221 } | |
| 222 | |
| 223 /** | |
| 224 * @override | |
| 225 * @param {number} startTime | |
| 226 * @param {number} endTime | |
| 227 */ | |
| 228 setWindowTimes(startTime, endTime) { | |
| 229 super.setWindowTimes(startTime, endTime); | |
| 230 this._updateHighlight(); | |
| 231 } | |
| 232 | |
| 233 /** | |
| 234 * @param {!Event} event | |
| 235 */ | |
| 236 _onMouseMove(event) { | |
| 237 this._lastMouseOffsetX = event.offsetX; | |
| 238 this._lastMouseOffsetY = event.offsetY; | |
| 239 if (!this._enabled()) | |
| 240 return; | |
| 241 if (this.isDragging()) | |
| 242 return; | |
| 243 if (this._coordinatesToGroupIndex(event.offsetX, event.offsetY) >= 0) { | |
| 244 this.hideHighlight(); | |
| 245 this.viewportElement.style.cursor = 'pointer'; | |
| 246 return; | |
| 247 } | |
| 248 this._updateHighlight(); | |
| 249 } | |
| 250 | |
| 251 _updateHighlight() { | |
| 252 var inDividersBar = this._lastMouseOffsetY < UI.FlameChart.HeaderHeight; | |
| 253 this._highlightedMarkerIndex = inDividersBar ? this._markerIndexAtPosition(t
his._lastMouseOffsetX) : -1; | |
| 254 this._updateMarkerHighlight(); | |
| 255 | |
| 256 var entryIndex = this._coordinatesToEntryIndex(this._lastMouseOffsetX, this.
_lastMouseOffsetY); | |
| 257 if (entryIndex === -1) { | |
| 258 this.hideHighlight(); | |
| 259 return; | |
| 260 } | |
| 261 if (this.isDragging()) | |
| 262 return; | |
| 263 this._updatePopover(entryIndex); | |
| 264 this.viewportElement.style.cursor = this._dataProvider.canJumpToEntry(entryI
ndex) ? 'pointer' : 'default'; | |
| 265 this.highlightEntry(entryIndex); | |
| 266 } | |
| 267 | |
| 268 _onMouseOut() { | |
| 269 this._lastMouseOffsetX = -1; | |
| 270 this._lastMouseOffsetY = -1; | |
| 271 this.hideHighlight(); | |
| 272 } | |
| 273 | |
| 274 /** | |
| 275 * @param {number} entryIndex | |
| 276 */ | |
| 277 _updatePopover(entryIndex) { | |
| 278 if (entryIndex === this._highlightedEntryIndex) { | |
| 279 this._updatePopoverOffset(); | |
| 280 return; | |
| 281 } | |
| 282 this._entryInfo.removeChildren(); | |
| 283 var popoverElement = this._dataProvider.prepareHighlightedEntryInfo(entryInd
ex); | |
| 284 if (popoverElement) { | |
| 285 this._entryInfo.appendChild(popoverElement); | |
| 286 this._updatePopoverOffset(); | |
| 287 } | |
| 288 } | |
| 289 | |
| 290 _updatePopoverOffset() { | |
| 291 var mouseX = this._lastMouseOffsetX; | |
| 292 var mouseY = this._lastMouseOffsetY; | |
| 293 var parentWidth = this._entryInfo.parentElement.clientWidth; | |
| 294 var parentHeight = this._entryInfo.parentElement.clientHeight; | |
| 295 var infoWidth = this._entryInfo.clientWidth; | |
| 296 var infoHeight = this._entryInfo.clientHeight; | |
| 297 var /** @const */ offsetX = 10; | |
| 298 var /** @const */ offsetY = 6; | |
| 299 var x; | |
| 300 var y; | |
| 301 for (var quadrant = 0; quadrant < 4; ++quadrant) { | |
| 302 var dx = quadrant & 2 ? -offsetX - infoWidth : offsetX; | |
| 303 var dy = quadrant & 1 ? -offsetY - infoHeight : offsetY; | |
| 304 x = Number.constrain(mouseX + dx, 0, parentWidth - infoWidth); | |
| 305 y = Number.constrain(mouseY + dy, 0, parentHeight - infoHeight); | |
| 306 if (x >= mouseX || mouseX >= x + infoWidth || y >= mouseY || mouseY >= y +
infoHeight) | |
| 307 break; | |
| 308 } | |
| 309 this._entryInfo.style.left = x + 'px'; | |
| 310 this._entryInfo.style.top = y + 'px'; | |
| 311 } | |
| 312 | |
| 313 /** | |
| 314 * @param {!Event} event | |
| 315 */ | |
| 316 _onClick(event) { | |
| 317 this.focus(); | |
| 318 // onClick comes after dragStart and dragEnd events. | |
| 319 // So if there was drag (mouse move) in the middle of that events | |
| 320 // we skip the click. Otherwise we jump to the sources. | |
| 321 const clickThreshold = 5; | |
| 322 if (this.maxDragOffset() > clickThreshold) | |
| 323 return; | |
| 324 var groupIndex = this._coordinatesToGroupIndex(event.offsetX, event.offsetY)
; | |
| 325 if (groupIndex >= 0) { | |
| 326 this._toggleGroupVisibility(groupIndex); | |
| 327 return; | |
| 328 } | |
| 329 this.hideRangeSelection(); | |
| 330 this.dispatchEventToListeners(UI.FlameChart.Events.EntrySelected, this._high
lightedEntryIndex); | |
| 331 } | |
| 332 | |
| 333 /** | |
| 334 * @param {number} groupIndex | |
| 335 */ | |
| 336 _toggleGroupVisibility(groupIndex) { | |
| 337 if (!this._isGroupCollapsible(groupIndex)) | |
| 338 return; | |
| 339 var groups = this._rawTimelineData.groups; | |
| 340 var group = groups[groupIndex]; | |
| 341 group.expanded = !group.expanded; | |
| 342 this._groupExpansionState[group.name] = group.expanded; | |
| 343 if (this._groupExpansionSetting) | |
| 344 this._groupExpansionSetting.set(this._groupExpansionState); | |
| 345 this._updateLevelPositions(); | |
| 346 | |
| 347 this._updateHighlight(); | |
| 348 if (!group.expanded) { | |
| 349 var timelineData = this._timelineData(); | |
| 350 var level = timelineData.entryLevels[this._selectedEntryIndex]; | |
| 351 if (this._selectedEntryIndex >= 0 && level >= group.startLevel && | |
| 352 (groupIndex === groups.length || groups[groupIndex + 1].startLevel > l
evel)) | |
| 353 this._selectedEntryIndex = -1; | |
| 354 } | |
| 355 | |
| 356 this._updateHeight(); | |
| 357 this._resetCanvas(); | |
| 358 this._draw(this._offsetWidth, this._offsetHeight); | |
| 359 } | |
| 360 | |
| 361 /** | |
| 362 * @param {!Event} e | |
| 363 */ | |
| 364 _onKeyDown(e) { | |
| 365 this._handleSelectionNavigation(e); | |
| 366 } | |
| 367 | |
| 368 /** | |
| 369 * @param {!Event} e | |
| 370 */ | |
| 371 _handleSelectionNavigation(e) { | |
| 372 if (!UI.KeyboardShortcut.hasNoModifiers(e)) | |
| 373 return; | |
| 374 if (this._selectedEntryIndex === -1) | |
| 375 return; | |
| 376 var timelineData = this._timelineData(); | |
| 377 if (!timelineData) | |
| 378 return; | |
| 379 | |
| 380 /** | |
| 381 * @param {number} time | |
| 382 * @param {number} entryIndex | |
| 383 * @return {number} | |
| 384 */ | |
| 385 function timeComparator(time, entryIndex) { | |
| 386 return time - timelineData.entryStartTimes[entryIndex]; | |
| 387 } | |
| 388 | |
| 389 /** | |
| 390 * @param {number} entry1 | |
| 391 * @param {number} entry2 | |
| 392 * @return {boolean} | |
| 393 */ | |
| 394 function entriesIntersect(entry1, entry2) { | |
| 395 var start1 = timelineData.entryStartTimes[entry1]; | |
| 396 var start2 = timelineData.entryStartTimes[entry2]; | |
| 397 var end1 = start1 + timelineData.entryTotalTimes[entry1]; | |
| 398 var end2 = start2 + timelineData.entryTotalTimes[entry2]; | |
| 399 return start1 < end2 && start2 < end1; | |
| 400 } | |
| 401 | |
| 402 var keys = UI.KeyboardShortcut.Keys; | |
| 403 if (e.keyCode === keys.Left.code || e.keyCode === keys.Right.code) { | |
| 404 var level = timelineData.entryLevels[this._selectedEntryIndex]; | |
| 405 var levelIndexes = this._timelineLevels[level]; | |
| 406 var indexOnLevel = levelIndexes.lowerBound(this._selectedEntryIndex); | |
| 407 indexOnLevel += e.keyCode === keys.Left.code ? -1 : 1; | |
| 408 e.consume(true); | |
| 409 if (indexOnLevel >= 0 && indexOnLevel < levelIndexes.length) | |
| 410 this.dispatchEventToListeners(UI.FlameChart.Events.EntrySelected, levelI
ndexes[indexOnLevel]); | |
| 411 return; | |
| 412 } | |
| 413 if (e.keyCode === keys.Up.code || e.keyCode === keys.Down.code) { | |
| 414 e.consume(true); | |
| 415 var level = timelineData.entryLevels[this._selectedEntryIndex]; | |
| 416 level += e.keyCode === keys.Up.code ? -1 : 1; | |
| 417 if (level < 0 || level >= this._timelineLevels.length) | |
| 418 return; | |
| 419 var entryTime = timelineData.entryStartTimes[this._selectedEntryIndex] + | |
| 420 timelineData.entryTotalTimes[this._selectedEntryIndex] / 2; | |
| 421 var levelIndexes = this._timelineLevels[level]; | |
| 422 var indexOnLevel = levelIndexes.upperBound(entryTime, timeComparator) - 1; | |
| 423 if (!entriesIntersect(this._selectedEntryIndex, levelIndexes[indexOnLevel]
)) { | |
| 424 ++indexOnLevel; | |
| 425 if (indexOnLevel >= levelIndexes.length || | |
| 426 !entriesIntersect(this._selectedEntryIndex, levelIndexes[indexOnLeve
l])) | |
| 427 return; | |
| 428 } | |
| 429 this.dispatchEventToListeners(UI.FlameChart.Events.EntrySelected, levelInd
exes[indexOnLevel]); | |
| 430 } | |
| 431 } | |
| 432 | |
| 433 /** | |
| 434 * @param {number} x | |
| 435 * @return {number} | |
| 436 */ | |
| 437 _cursorTime(x) { | |
| 438 return (x + this._pixelWindowLeft - this._paddingLeft) * this._pixelToTime +
this._minimumBoundary; | |
| 439 } | |
| 440 | |
| 441 /** | |
| 442 * @param {number} x | |
| 443 * @param {number} y | |
| 444 * @return {number} | |
| 445 */ | |
| 446 _coordinatesToEntryIndex(x, y) { | |
| 447 if (x < 0 || y < 0) | |
| 448 return -1; | |
| 449 y += this.getScrollOffset(); | |
| 450 var timelineData = this._timelineData(); | |
| 451 if (!timelineData) | |
| 452 return -1; | |
| 453 var cursorTime = this._cursorTime(x); | |
| 454 var cursorLevel = this._visibleLevelOffsets.upperBound(y) - 1; | |
| 455 if (cursorLevel < 0 || !this._visibleLevels[cursorLevel]) | |
| 456 return -1; | |
| 457 var offsetFromLevel = y - this._visibleLevelOffsets[cursorLevel]; | |
| 458 if (offsetFromLevel > this._barHeight) | |
| 459 return -1; | |
| 460 var entryStartTimes = timelineData.entryStartTimes; | |
| 461 var entryTotalTimes = timelineData.entryTotalTimes; | |
| 462 var entryIndexes = this._timelineLevels[cursorLevel]; | |
| 463 if (!entryIndexes || !entryIndexes.length) | |
| 464 return -1; | |
| 465 | |
| 466 /** | |
| 467 * @param {number} time | |
| 468 * @param {number} entryIndex | |
| 469 * @return {number} | |
| 470 */ | |
| 471 function comparator(time, entryIndex) { | |
| 472 return time - entryStartTimes[entryIndex]; | |
| 473 } | |
| 474 var indexOnLevel = Math.max(entryIndexes.upperBound(cursorTime, comparator)
- 1, 0); | |
| 475 | |
| 476 /** | |
| 477 * @this {UI.FlameChart} | |
| 478 * @param {number} entryIndex | |
| 479 * @return {boolean} | |
| 480 */ | |
| 481 function checkEntryHit(entryIndex) { | |
| 482 if (entryIndex === undefined) | |
| 483 return false; | |
| 484 var startTime = entryStartTimes[entryIndex]; | |
| 485 var duration = entryTotalTimes[entryIndex]; | |
| 486 if (isNaN(duration)) { | |
| 487 var dx = (startTime - cursorTime) / this._pixelToTime; | |
| 488 var dy = this._barHeight / 2 - offsetFromLevel; | |
| 489 return dx * dx + dy * dy < this._markerRadius * this._markerRadius; | |
| 490 } | |
| 491 var endTime = startTime + duration; | |
| 492 var barThreshold = 3 * this._pixelToTime; | |
| 493 return startTime - barThreshold < cursorTime && cursorTime < endTime + bar
Threshold; | |
| 494 } | |
| 495 | |
| 496 var entryIndex = entryIndexes[indexOnLevel]; | |
| 497 if (checkEntryHit.call(this, entryIndex)) | |
| 498 return entryIndex; | |
| 499 entryIndex = entryIndexes[indexOnLevel + 1]; | |
| 500 if (checkEntryHit.call(this, entryIndex)) | |
| 501 return entryIndex; | |
| 502 return -1; | |
| 503 } | |
| 504 | |
| 505 /** | |
| 506 * @param {number} x | |
| 507 * @param {number} y | |
| 508 * @return {number} | |
| 509 */ | |
| 510 _coordinatesToGroupIndex(x, y) { | |
| 511 if (x < 0 || y < 0) | |
| 512 return -1; | |
| 513 y += this.getScrollOffset(); | |
| 514 var groups = this._rawTimelineData.groups || []; | |
| 515 var group = this._groupOffsets.upperBound(y) - 1; | |
| 516 | |
| 517 if (group < 0 || group >= groups.length || y - this._groupOffsets[group] >=
groups[group].style.height) | |
| 518 return -1; | |
| 519 var context = /** @type {!CanvasRenderingContext2D} */ (this._canvas.getCont
ext('2d')); | |
| 520 context.save(); | |
| 521 context.font = groups[group].style.font; | |
| 522 var right = this._headerLeftPadding + this._labelWidthForGroup(context, grou
ps[group]); | |
| 523 context.restore(); | |
| 524 if (x > right) | |
| 525 return -1; | |
| 526 | |
| 527 return group; | |
| 528 } | |
| 529 | |
| 530 /** | |
| 531 * @param {number} x | |
| 532 * @return {number} | |
| 533 */ | |
| 534 _markerIndexAtPosition(x) { | |
| 535 var markers = this._timelineData().markers; | |
| 536 if (!markers) | |
| 537 return -1; | |
| 538 var accurracyOffsetPx = 1; | |
| 539 var time = this._cursorTime(x); | |
| 540 var leftTime = this._cursorTime(x - accurracyOffsetPx); | |
| 541 var rightTime = this._cursorTime(x + accurracyOffsetPx); | |
| 542 | |
| 543 var left = this._markerIndexBeforeTime(leftTime); | |
| 544 var markerIndex = -1; | |
| 545 var distance = Infinity; | |
| 546 for (var i = left; i < markers.length && markers[i].startTime() < rightTime;
i++) { | |
| 547 var nextDistance = Math.abs(markers[i].startTime() - time); | |
| 548 if (nextDistance < distance) { | |
| 549 markerIndex = i; | |
| 550 distance = nextDistance; | |
| 551 } | |
| 552 } | |
| 553 return markerIndex; | |
| 554 } | |
| 555 | |
| 556 /** | |
| 557 * @param {number} time | |
| 558 * @return {number} | |
| 559 */ | |
| 560 _markerIndexBeforeTime(time) { | |
| 561 return this._timelineData().markers.lowerBound( | |
| 562 time, (markerTimestamp, marker) => markerTimestamp - marker.startTime())
; | |
| 563 } | |
| 564 | |
| 565 /** | |
| 566 * @param {number} height | |
| 567 * @param {number} width | |
| 568 */ | |
| 569 _draw(width, height) { | |
| 570 var timelineData = this._timelineData(); | |
| 571 if (!timelineData) | |
| 572 return; | |
| 573 | |
| 574 var context = /** @type {!CanvasRenderingContext2D} */ (this._canvas.getCont
ext('2d')); | |
| 575 context.save(); | |
| 576 var ratio = window.devicePixelRatio; | |
| 577 var top = this.getScrollOffset(); | |
| 578 context.scale(ratio, ratio); | |
| 579 context.translate(0, -top); | |
| 580 var defaultFont = '11px ' + Host.fontFamily(); | |
| 581 context.font = defaultFont; | |
| 582 | |
| 583 var timeWindowRight = this._timeWindowRight; | |
| 584 var timeWindowLeft = this._timeWindowLeft - this._paddingLeft / this._timeTo
Pixel; | |
| 585 var entryTotalTimes = timelineData.entryTotalTimes; | |
| 586 var entryStartTimes = timelineData.entryStartTimes; | |
| 587 var entryLevels = timelineData.entryLevels; | |
| 588 | |
| 589 var titleIndices = []; | |
| 590 var markerIndices = []; | |
| 591 var textPadding = this._textPadding; | |
| 592 var minTextWidth = 2 * textPadding + UI.measureTextWidth(context, '\u2026'); | |
| 593 var barHeight = this._barHeight; | |
| 594 var minVisibleBarLevel = Math.max(this._visibleLevelOffsets.upperBound(top)
- 1, 0); | |
| 595 | |
| 596 /** @type {!Map<string, !Array<number>>} */ | |
| 597 var colorBuckets = new Map(); | |
| 598 for (var level = minVisibleBarLevel; level < this._dataProvider.maxStackDept
h(); ++level) { | |
| 599 if (this._levelToHeight(level) > top + height) | |
| 600 break; | |
| 601 if (!this._visibleLevels[level]) | |
| 602 continue; | |
| 603 | |
| 604 // Entries are ordered by start time within a level, so find the last visi
ble entry. | |
| 605 var levelIndexes = this._timelineLevels[level]; | |
| 606 var rightIndexOnLevel = | |
| 607 levelIndexes.lowerBound(timeWindowRight, (time, entryIndex) => time -
entryStartTimes[entryIndex]) - 1; | |
| 608 var lastDrawOffset = Infinity; | |
| 609 for (var entryIndexOnLevel = rightIndexOnLevel; entryIndexOnLevel >= 0; --
entryIndexOnLevel) { | |
| 610 var entryIndex = levelIndexes[entryIndexOnLevel]; | |
| 611 var entryStartTime = entryStartTimes[entryIndex]; | |
| 612 var entryOffsetRight = entryStartTime + (entryTotalTimes[entryIndex] ||
0); | |
| 613 if (entryOffsetRight <= timeWindowLeft) | |
| 614 break; | |
| 615 | |
| 616 var barX = this._timeToPositionClipped(entryStartTime); | |
| 617 // Check if the entry entirely fits into an already drawn pixel, we can
just skip drawing it. | |
| 618 if (barX >= lastDrawOffset) | |
| 619 continue; | |
| 620 lastDrawOffset = barX; | |
| 621 | |
| 622 var color = this._dataProvider.entryColor(entryIndex); | |
| 623 var bucket = colorBuckets.get(color); | |
| 624 if (!bucket) { | |
| 625 bucket = []; | |
| 626 colorBuckets.set(color, bucket); | |
| 627 } | |
| 628 bucket.push(entryIndex); | |
| 629 } | |
| 630 } | |
| 631 | |
| 632 var colors = colorBuckets.keysArray(); | |
| 633 // We don't use for-of here because it's slow. | |
| 634 for (var c = 0; c < colors.length; ++c) { | |
| 635 var color = colors[c]; | |
| 636 var indexes = colorBuckets.get(color); | |
| 637 context.beginPath(); | |
| 638 context.fillStyle = color; | |
| 639 for (var i = 0; i < indexes.length; ++i) { | |
| 640 var entryIndex = indexes[i]; | |
| 641 var entryStartTime = entryStartTimes[entryIndex]; | |
| 642 var barX = this._timeToPositionClipped(entryStartTime); | |
| 643 var duration = entryTotalTimes[entryIndex]; | |
| 644 var barLevel = entryLevels[entryIndex]; | |
| 645 var barY = this._levelToHeight(barLevel); | |
| 646 if (isNaN(duration)) { | |
| 647 context.moveTo(barX + this._markerRadius, barY + barHeight / 2); | |
| 648 context.arc(barX, barY + barHeight / 2, this._markerRadius, 0, Math.PI
* 2); | |
| 649 markerIndices.push(entryIndex); | |
| 650 continue; | |
| 651 } | |
| 652 var barRight = this._timeToPositionClipped(entryStartTime + duration); | |
| 653 var barWidth = Math.max(barRight - barX, 1); | |
| 654 context.rect(barX, barY, barWidth - 0.4, barHeight - 1); | |
| 655 if (barWidth > minTextWidth || this._dataProvider.forceDecoration(entryI
ndex)) | |
| 656 titleIndices.push(entryIndex); | |
| 657 } | |
| 658 context.fill(); | |
| 659 } | |
| 660 | |
| 661 context.strokeStyle = 'rgba(0, 0, 0, 0.2)'; | |
| 662 context.beginPath(); | |
| 663 for (var m = 0; m < markerIndices.length; ++m) { | |
| 664 var entryIndex = markerIndices[m]; | |
| 665 var entryStartTime = entryStartTimes[entryIndex]; | |
| 666 var barX = this._timeToPositionClipped(entryStartTime); | |
| 667 var barLevel = entryLevels[entryIndex]; | |
| 668 var barY = this._levelToHeight(barLevel); | |
| 669 context.moveTo(barX + this._markerRadius, barY + barHeight / 2); | |
| 670 context.arc(barX, barY + barHeight / 2, this._markerRadius, 0, Math.PI * 2
); | |
| 671 } | |
| 672 context.stroke(); | |
| 673 | |
| 674 context.textBaseline = 'alphabetic'; | |
| 675 var textBaseHeight = this._barHeight - this._textBaseline; | |
| 676 | |
| 677 for (var i = 0; i < titleIndices.length; ++i) { | |
| 678 var entryIndex = titleIndices[i]; | |
| 679 var entryStartTime = entryStartTimes[entryIndex]; | |
| 680 var barX = this._timeToPositionClipped(entryStartTime); | |
| 681 var barRight = Math.min(this._timeToPositionClipped(entryStartTime + entry
TotalTimes[entryIndex]), width) + 1; | |
| 682 var barWidth = barRight - barX; | |
| 683 var barLevel = entryLevels[entryIndex]; | |
| 684 var barY = this._levelToHeight(barLevel); | |
| 685 var text = this._dataProvider.entryTitle(entryIndex); | |
| 686 if (text && text.length) { | |
| 687 context.font = this._dataProvider.entryFont(entryIndex) || defaultFont; | |
| 688 text = UI.trimTextMiddle(context, text, barWidth - 2 * textPadding); | |
| 689 } | |
| 690 var unclippedBarX = this._timeToPosition(entryStartTime); | |
| 691 if (this._dataProvider.decorateEntry( | |
| 692 entryIndex, context, text, barX, barY, barWidth, barHeight, unclip
pedBarX, this._timeToPixel)) | |
| 693 continue; | |
| 694 if (!text || !text.length) | |
| 695 continue; | |
| 696 context.fillStyle = this._dataProvider.textColor(entryIndex); | |
| 697 context.fillText(text, barX + textPadding, barY + textBaseHeight); | |
| 698 } | |
| 699 | |
| 700 context.restore(); | |
| 701 | |
| 702 this._drawGroupHeaders(width, height); | |
| 703 this._drawMarkers(); | |
| 704 const headerHeight = this._rulerEnabled ? UI.FlameChart.HeaderHeight : 0; | |
| 705 UI.TimelineGrid.drawCanvasGrid(context, this._calculator, 3, headerHeight); | |
| 706 | |
| 707 this._updateElementPosition(this._highlightElement, this._highlightedEntryIn
dex); | |
| 708 this._updateElementPosition(this._selectedElement, this._selectedEntryIndex)
; | |
| 709 this._updateMarkerHighlight(); | |
| 710 } | |
| 711 | |
| 712 /** | |
| 713 * @param {number} width | |
| 714 * @param {number} height | |
| 715 */ | |
| 716 _drawGroupHeaders(width, height) { | |
| 717 var context = /** @type {!CanvasRenderingContext2D} */ (this._canvas.getCont
ext('2d')); | |
| 718 var top = this.getScrollOffset(); | |
| 719 var ratio = window.devicePixelRatio; | |
| 720 var barHeight = this._barHeight; | |
| 721 var textBaseHeight = barHeight - this._textBaseline; | |
| 722 var groups = this._rawTimelineData.groups || []; | |
| 723 if (!groups.length) | |
| 724 return; | |
| 725 | |
| 726 var groupOffsets = this._groupOffsets; | |
| 727 var lastGroupOffset = Array.prototype.peekLast.call(groupOffsets); | |
| 728 var colorUsage = UI.ThemeSupport.ColorUsage; | |
| 729 | |
| 730 context.save(); | |
| 731 context.scale(ratio, ratio); | |
| 732 context.translate(0, -top); | |
| 733 | |
| 734 context.fillStyle = UI.themeSupport.patchColor('#fff', colorUsage.Background
); | |
| 735 forEachGroup.call(this, (offset, index, group) => { | |
| 736 var paddingHeight = group.style.padding; | |
| 737 if (paddingHeight < 5) | |
| 738 return; | |
| 739 context.fillRect(0, offset - paddingHeight + 2, width, paddingHeight - 4); | |
| 740 }); | |
| 741 if (groups.length && lastGroupOffset < top + height) | |
| 742 context.fillRect(0, lastGroupOffset + 2, width, top + height - lastGroupOf
fset); | |
| 743 | |
| 744 context.strokeStyle = UI.themeSupport.patchColor('#eee', colorUsage.Backgrou
nd); | |
| 745 context.beginPath(); | |
| 746 forEachGroup.call(this, (offset, index, group, isFirst) => { | |
| 747 if (isFirst || group.style.padding < 4) | |
| 748 return; | |
| 749 hLine(offset - 2.5); | |
| 750 }); | |
| 751 hLine(lastGroupOffset + 1.5); | |
| 752 context.stroke(); | |
| 753 | |
| 754 forEachGroup.call(this, (offset, index, group) => { | |
| 755 if (group.style.useFirstLineForOverview) | |
| 756 return; | |
| 757 if (!this._isGroupCollapsible(index) || group.expanded) { | |
| 758 if (!group.style.shareHeaderLine) { | |
| 759 context.fillStyle = group.style.backgroundColor; | |
| 760 context.fillRect(0, offset, width, group.style.height); | |
| 761 } | |
| 762 return; | |
| 763 } | |
| 764 var nextGroup = index + 1; | |
| 765 while (nextGroup < groups.length && groups[nextGroup].style.nestingLevel >
group.style.nestingLevel) | |
| 766 nextGroup++; | |
| 767 var endLevel = nextGroup < groups.length ? groups[nextGroup].startLevel :
this._dataProvider.maxStackDepth(); | |
| 768 this._drawCollapsedOverviewForGroup(offset + 1, group.startLevel, endLevel
); | |
| 769 }); | |
| 770 | |
| 771 context.save(); | |
| 772 forEachGroup.call(this, (offset, index, group) => { | |
| 773 context.font = group.style.font; | |
| 774 if (this._isGroupCollapsible(index) && !group.expanded || group.style.shar
eHeaderLine) { | |
| 775 const width = this._labelWidthForGroup(context, group) + 2; | |
| 776 context.fillStyle = Common.Color.parse(group.style.backgroundColor).setA
lpha(0.8).asString(null); | |
| 777 context.fillRect( | |
| 778 this._headerLeftPadding - this._headerLabelXPadding, offset + this._
headerLabelYPadding, width, | |
| 779 barHeight - 2 * this._headerLabelYPadding); | |
| 780 } | |
| 781 context.fillStyle = group.style.color; | |
| 782 context.fillText( | |
| 783 group.name, Math.floor(this._expansionArrowIndent * (group.style.nesti
ngLevel + 1) + this._arrowSide), | |
| 784 offset + textBaseHeight); | |
| 785 }); | |
| 786 context.restore(); | |
| 787 | |
| 788 context.fillStyle = UI.themeSupport.patchColor('#6e6e6e', colorUsage.Foregro
und); | |
| 789 context.beginPath(); | |
| 790 forEachGroup.call(this, (offset, index, group) => { | |
| 791 if (this._isGroupCollapsible(index)) { | |
| 792 drawExpansionArrow.call( | |
| 793 this, this._expansionArrowIndent * (group.style.nestingLevel + 1), | |
| 794 offset + textBaseHeight - this._arrowSide / 2, !!group.expanded); | |
| 795 } | |
| 796 }); | |
| 797 context.fill(); | |
| 798 | |
| 799 context.strokeStyle = UI.themeSupport.patchColor('#ddd', colorUsage.Backgrou
nd); | |
| 800 context.beginPath(); | |
| 801 context.stroke(); | |
| 802 | |
| 803 context.restore(); | |
| 804 | |
| 805 /** | |
| 806 * @param {number} y | |
| 807 */ | |
| 808 function hLine(y) { | |
| 809 context.moveTo(0, y); | |
| 810 context.lineTo(width, y); | |
| 811 } | |
| 812 | |
| 813 /** | |
| 814 * @param {number} x | |
| 815 * @param {number} y | |
| 816 * @param {boolean} expanded | |
| 817 * @this {UI.FlameChart} | |
| 818 */ | |
| 819 function drawExpansionArrow(x, y, expanded) { | |
| 820 var arrowHeight = this._arrowSide * Math.sqrt(3) / 2; | |
| 821 var arrowCenterOffset = Math.round(arrowHeight / 2); | |
| 822 context.save(); | |
| 823 context.translate(x, y); | |
| 824 context.rotate(expanded ? Math.PI / 2 : 0); | |
| 825 context.moveTo(-arrowCenterOffset, -this._arrowSide / 2); | |
| 826 context.lineTo(-arrowCenterOffset, this._arrowSide / 2); | |
| 827 context.lineTo(arrowHeight - arrowCenterOffset, 0); | |
| 828 context.restore(); | |
| 829 } | |
| 830 | |
| 831 /** | |
| 832 * @param {function(number, number, !UI.FlameChart.Group, boolean)} callback | |
| 833 * @this {UI.FlameChart} | |
| 834 */ | |
| 835 function forEachGroup(callback) { | |
| 836 /** @type !Array<{nestingLevel: number, visible: boolean}> */ | |
| 837 var groupStack = [{nestingLevel: -1, visible: true}]; | |
| 838 for (var i = 0; i < groups.length; ++i) { | |
| 839 var groupTop = groupOffsets[i]; | |
| 840 var group = groups[i]; | |
| 841 if (groupTop - group.style.padding > top + height) | |
| 842 break; | |
| 843 var firstGroup = true; | |
| 844 while (groupStack.peekLast().nestingLevel >= group.style.nestingLevel) { | |
| 845 groupStack.pop(); | |
| 846 firstGroup = false; | |
| 847 } | |
| 848 var parentGroupVisible = groupStack.peekLast().visible; | |
| 849 var thisGroupVisible = parentGroupVisible && (!this._isGroupCollapsible(
i) || group.expanded); | |
| 850 groupStack.push({nestingLevel: group.style.nestingLevel, visible: thisGr
oupVisible}); | |
| 851 if (!parentGroupVisible || groupTop + group.style.height < top) | |
| 852 continue; | |
| 853 callback(groupTop, i, group, firstGroup); | |
| 854 } | |
| 855 } | |
| 856 } | |
| 857 | |
| 858 /** | |
| 859 * @param {!CanvasRenderingContext2D} context | |
| 860 * @param {!UI.FlameChart.Group} group | |
| 861 * @return {number} | |
| 862 */ | |
| 863 _labelWidthForGroup(context, group) { | |
| 864 return UI.measureTextWidth(context, group.name) + this._expansionArrowIndent
* (group.style.nestingLevel + 1) + | |
| 865 2 * this._headerLabelXPadding; | |
| 866 } | |
| 867 | |
| 868 /** | |
| 869 * @param {number} y | |
| 870 * @param {number} startLevel | |
| 871 * @param {number} endLevel | |
| 872 */ | |
| 873 _drawCollapsedOverviewForGroup(y, startLevel, endLevel) { | |
| 874 var range = new Common.SegmentedRange(mergeCallback); | |
| 875 var timeWindowRight = this._timeWindowRight; | |
| 876 var timeWindowLeft = this._timeWindowLeft - this._paddingLeft / this._timeTo
Pixel; | |
| 877 var context = /** @type {!CanvasRenderingContext2D} */ (this._canvas.getCont
ext('2d')); | |
| 878 var barHeight = this._barHeight - 2; | |
| 879 var entryStartTimes = this._rawTimelineData.entryStartTimes; | |
| 880 var entryTotalTimes = this._rawTimelineData.entryTotalTimes; | |
| 881 | |
| 882 for (var level = startLevel; level < endLevel; ++level) { | |
| 883 var levelIndexes = this._timelineLevels[level]; | |
| 884 var rightIndexOnLevel = | |
| 885 levelIndexes.lowerBound(timeWindowRight, (time, entryIndex) => time -
entryStartTimes[entryIndex]) - 1; | |
| 886 var lastDrawOffset = Infinity; | |
| 887 | |
| 888 for (var entryIndexOnLevel = rightIndexOnLevel; entryIndexOnLevel >= 0; --
entryIndexOnLevel) { | |
| 889 var entryIndex = levelIndexes[entryIndexOnLevel]; | |
| 890 var entryStartTime = entryStartTimes[entryIndex]; | |
| 891 var startPosition = this._timeToPositionClipped(entryStartTime); | |
| 892 var entryEndTime = entryStartTime + entryTotalTimes[entryIndex]; | |
| 893 if (isNaN(entryEndTime) || startPosition >= lastDrawOffset) | |
| 894 continue; | |
| 895 if (entryEndTime <= timeWindowLeft) | |
| 896 break; | |
| 897 lastDrawOffset = startPosition; | |
| 898 var color = this._dataProvider.entryColor(entryIndex); | |
| 899 range.append(new Common.Segment(startPosition, this._timeToPositionClipp
ed(entryEndTime), color)); | |
| 900 } | |
| 901 } | |
| 902 | |
| 903 var segments = range.segments().slice().sort((a, b) => a.data.localeCompare(
b.data)); | |
| 904 var lastColor; | |
| 905 context.beginPath(); | |
| 906 for (var i = 0; i < segments.length; ++i) { | |
| 907 var segment = segments[i]; | |
| 908 if (lastColor !== segments[i].data) { | |
| 909 context.fill(); | |
| 910 context.beginPath(); | |
| 911 lastColor = segments[i].data; | |
| 912 context.fillStyle = lastColor; | |
| 913 } | |
| 914 context.rect(segment.begin, y, segment.end - segment.begin, barHeight); | |
| 915 } | |
| 916 context.fill(); | |
| 917 | |
| 918 /** | |
| 919 * @param {!Common.Segment} a | |
| 920 * @param {!Common.Segment} b | |
| 921 * @return {?Common.Segment} | |
| 922 */ | |
| 923 function mergeCallback(a, b) { | |
| 924 return a.data === b.data && a.end + 0.4 > b.end ? a : null; | |
| 925 } | |
| 926 } | |
| 927 | |
| 928 _drawMarkers() { | |
| 929 var markers = this._timelineData().markers; | |
| 930 var left = this._markerIndexBeforeTime(this._calculator.minimumBoundary()); | |
| 931 var rightBoundary = this._calculator.maximumBoundary(); | |
| 932 | |
| 933 var context = /** @type {!CanvasRenderingContext2D} */ (this._canvas.getCont
ext('2d')); | |
| 934 context.save(); | |
| 935 var ratio = window.devicePixelRatio; | |
| 936 context.scale(ratio, ratio); | |
| 937 context.translate(0, 3); | |
| 938 var height = UI.FlameChart.HeaderHeight - 1; | |
| 939 for (var i = left; i < markers.length; i++) { | |
| 940 var timestamp = markers[i].startTime(); | |
| 941 if (timestamp > rightBoundary) | |
| 942 break; | |
| 943 markers[i].draw(context, this._calculator.computePosition(timestamp), heig
ht, this._timeToPixel); | |
| 944 } | |
| 945 context.restore(); | |
| 946 } | |
| 947 | |
| 948 _updateMarkerHighlight() { | |
| 949 var element = this._markerHighlighElement; | |
| 950 if (element.parentElement) | |
| 951 element.remove(); | |
| 952 var markerIndex = this._highlightedMarkerIndex; | |
| 953 if (markerIndex === -1) | |
| 954 return; | |
| 955 var marker = this._timelineData().markers[markerIndex]; | |
| 956 var barX = this._timeToPositionClipped(marker.startTime()); | |
| 957 element.title = marker.title(); | |
| 958 var style = element.style; | |
| 959 style.left = barX + 'px'; | |
| 960 style.backgroundColor = marker.color(); | |
| 961 this.viewportElement.appendChild(element); | |
| 962 } | |
| 963 | |
| 964 /** | |
| 965 * @param {?UI.FlameChart.TimelineData} timelineData | |
| 966 */ | |
| 967 _processTimelineData(timelineData) { | |
| 968 if (!timelineData) { | |
| 969 this._timelineLevels = null; | |
| 970 this._visibleLevelOffsets = null; | |
| 971 this._visibleLevels = null; | |
| 972 this._groupOffsets = null; | |
| 973 this._rawTimelineData = null; | |
| 974 this._rawTimelineDataLength = 0; | |
| 975 return; | |
| 976 } | |
| 977 | |
| 978 this._rawTimelineData = timelineData; | |
| 979 this._rawTimelineDataLength = timelineData.entryStartTimes.length; | |
| 980 | |
| 981 var entryCounters = new Uint32Array(this._dataProvider.maxStackDepth() + 1); | |
| 982 for (var i = 0; i < timelineData.entryLevels.length; ++i) | |
| 983 ++entryCounters[timelineData.entryLevels[i]]; | |
| 984 var levelIndexes = new Array(entryCounters.length); | |
| 985 for (var i = 0; i < levelIndexes.length; ++i) { | |
| 986 levelIndexes[i] = new Uint32Array(entryCounters[i]); | |
| 987 entryCounters[i] = 0; | |
| 988 } | |
| 989 for (var i = 0; i < timelineData.entryLevels.length; ++i) { | |
| 990 var level = timelineData.entryLevels[i]; | |
| 991 levelIndexes[level][entryCounters[level]++] = i; | |
| 992 } | |
| 993 this._timelineLevels = levelIndexes; | |
| 994 var groups = this._rawTimelineData.groups || []; | |
| 995 for (var i = 0; i < groups.length; ++i) { | |
| 996 var expanded = this._groupExpansionState[groups[i].name]; | |
| 997 if (expanded !== undefined) | |
| 998 groups[i].expanded = expanded; | |
| 999 } | |
| 1000 this._updateLevelPositions(); | |
| 1001 this._updateHeight(); | |
| 1002 } | |
| 1003 | |
| 1004 _updateLevelPositions() { | |
| 1005 var levelCount = this._dataProvider.maxStackDepth(); | |
| 1006 var groups = this._rawTimelineData.groups || []; | |
| 1007 this._visibleLevelOffsets = new Uint32Array(levelCount + 1); | |
| 1008 this._visibleLevels = new Uint16Array(levelCount); | |
| 1009 this._groupOffsets = new Uint32Array(groups.length + 1); | |
| 1010 | |
| 1011 var groupIndex = -1; | |
| 1012 var currentOffset = this._rulerEnabled ? UI.FlameChart.HeaderHeight : 2; | |
| 1013 var visible = true; | |
| 1014 /** @type !Array<{nestingLevel: number, visible: boolean}> */ | |
| 1015 var groupStack = [{nestingLevel: -1, visible: true}]; | |
| 1016 var lastGroupLevel = Math.max(levelCount, groups.peekLast().startLevel + 1); | |
| 1017 for (var level = 0; level < lastGroupLevel; ++level) { | |
| 1018 while (groupIndex < groups.length - 1 && level === groups[groupIndex + 1].
startLevel) { | |
| 1019 ++groupIndex; | |
| 1020 var style = groups[groupIndex].style; | |
| 1021 var nextLevel = true; | |
| 1022 while (groupStack.peekLast().nestingLevel >= style.nestingLevel) { | |
| 1023 groupStack.pop(); | |
| 1024 nextLevel = false; | |
| 1025 } | |
| 1026 var thisGroupIsVisible = | |
| 1027 groupIndex >= 0 && this._isGroupCollapsible(groupIndex) ? groups[gro
upIndex].expanded : true; | |
| 1028 var parentGroupIsVisible = groupStack.peekLast().visible; | |
| 1029 visible = thisGroupIsVisible && parentGroupIsVisible; | |
| 1030 groupStack.push({nestingLevel: style.nestingLevel, visible: visible}); | |
| 1031 if (parentGroupIsVisible) | |
| 1032 currentOffset += nextLevel ? 0 : style.padding; | |
| 1033 this._groupOffsets[groupIndex] = currentOffset; | |
| 1034 if (parentGroupIsVisible && !style.shareHeaderLine) | |
| 1035 currentOffset += style.height; | |
| 1036 } | |
| 1037 var isFirstOnLevel = groupIndex >= 0 && level === groups[groupIndex].start
Level; | |
| 1038 var thisLevelIsVisible = visible || isFirstOnLevel && groups[groupIndex].s
tyle.useFirstLineForOverview; | |
| 1039 if (level < levelCount) { | |
| 1040 this._visibleLevels[level] = thisLevelIsVisible; | |
| 1041 this._visibleLevelOffsets[level] = currentOffset; | |
| 1042 } | |
| 1043 if (thisLevelIsVisible || (parentGroupIsVisible && style.shareHeaderLine &
& isFirstOnLevel)) | |
| 1044 currentOffset += this._barHeight; | |
| 1045 } | |
| 1046 if (groupIndex >= 0) | |
| 1047 this._groupOffsets[groupIndex + 1] = currentOffset; | |
| 1048 this._visibleLevelOffsets[level] = currentOffset; | |
| 1049 } | |
| 1050 | |
| 1051 /** | |
| 1052 * @param {number} index | |
| 1053 */ | |
| 1054 _isGroupCollapsible(index) { | |
| 1055 var groups = this._rawTimelineData.groups || []; | |
| 1056 var style = groups[index].style; | |
| 1057 if (!style.shareHeaderLine || !style.collapsible) | |
| 1058 return !!style.collapsible; | |
| 1059 var isLastGroup = index + 1 >= groups.length; | |
| 1060 if (!isLastGroup && groups[index + 1].style.nestingLevel > style.nestingLeve
l) | |
| 1061 return true; | |
| 1062 var nextGroupLevel = isLastGroup ? this._dataProvider.maxStackDepth() : grou
ps[index + 1].startLevel; | |
| 1063 // For groups that only have one line and share header line, pretend these a
re not collapsible. | |
| 1064 return nextGroupLevel !== groups[index].startLevel + 1; | |
| 1065 } | |
| 1066 | |
| 1067 /** | |
| 1068 * @param {number} entryIndex | |
| 1069 */ | |
| 1070 setSelectedEntry(entryIndex) { | |
| 1071 if (entryIndex === -1 && !this.isDragging()) | |
| 1072 this.hideRangeSelection(); | |
| 1073 if (this._selectedEntryIndex === entryIndex) | |
| 1074 return; | |
| 1075 this._selectedEntryIndex = entryIndex; | |
| 1076 this._revealEntry(entryIndex); | |
| 1077 this._updateElementPosition(this._selectedElement, this._selectedEntryIndex)
; | |
| 1078 } | |
| 1079 | |
| 1080 /** | |
| 1081 * @param {!Element} element | |
| 1082 * @param {number} entryIndex | |
| 1083 */ | |
| 1084 _updateElementPosition(element, entryIndex) { | |
| 1085 const elementMinWidthPx = 2; | |
| 1086 if (element.parentElement) | |
| 1087 element.remove(); | |
| 1088 if (entryIndex === -1) | |
| 1089 return; | |
| 1090 var timelineData = this._timelineData(); | |
| 1091 var startTime = timelineData.entryStartTimes[entryIndex]; | |
| 1092 var endTime = startTime + (timelineData.entryTotalTimes[entryIndex] || 0); | |
| 1093 var barX = this._timeToPositionClipped(startTime); | |
| 1094 var barRight = this._timeToPositionClipped(endTime); | |
| 1095 if (barRight === 0 || barX === this._offsetWidth) | |
| 1096 return; | |
| 1097 var barWidth = barRight - barX; | |
| 1098 var barCenter = barX + barWidth / 2; | |
| 1099 barWidth = Math.max(barWidth, elementMinWidthPx); | |
| 1100 barX = barCenter - barWidth / 2; | |
| 1101 var barY = this._levelToHeight(timelineData.entryLevels[entryIndex]) - this.
getScrollOffset(); | |
| 1102 var style = element.style; | |
| 1103 style.left = barX + 'px'; | |
| 1104 style.top = barY + 'px'; | |
| 1105 style.width = barWidth + 'px'; | |
| 1106 style.height = this._barHeight - 1 + 'px'; | |
| 1107 this.viewportElement.appendChild(element); | |
| 1108 } | |
| 1109 | |
| 1110 /** | |
| 1111 * @param {number} time | |
| 1112 * @return {number} | |
| 1113 */ | |
| 1114 _timeToPositionClipped(time) { | |
| 1115 return Number.constrain(this._timeToPosition(time), 0, this._offsetWidth); | |
| 1116 } | |
| 1117 | |
| 1118 /** | |
| 1119 * @param {number} time | |
| 1120 * @return {number} | |
| 1121 */ | |
| 1122 _timeToPosition(time) { | |
| 1123 return Math.floor((time - this._minimumBoundary) * this._timeToPixel) - this
._pixelWindowLeft + this._paddingLeft; | |
| 1124 } | |
| 1125 | |
| 1126 /** | |
| 1127 * @param {number} level | |
| 1128 * @return {number} | |
| 1129 */ | |
| 1130 _levelToHeight(level) { | |
| 1131 return this._visibleLevelOffsets[level]; | |
| 1132 } | |
| 1133 | |
| 1134 _updateBoundaries() { | |
| 1135 this._totalTime = this._dataProvider.totalTime(); | |
| 1136 this._minimumBoundary = this._dataProvider.minimumBoundary(); | |
| 1137 | |
| 1138 var windowWidth = 1; | |
| 1139 if (this._timeWindowRight !== Infinity) { | |
| 1140 this._windowLeft = (this._timeWindowLeft - this._minimumBoundary) / this._
totalTime; | |
| 1141 this._windowRight = (this._timeWindowRight - this._minimumBoundary) / this
._totalTime; | |
| 1142 windowWidth = this._windowRight - this._windowLeft; | |
| 1143 } else if (this._timeWindowLeft === Infinity) { | |
| 1144 this._windowLeft = Infinity; | |
| 1145 this._windowRight = Infinity; | |
| 1146 } else { | |
| 1147 this._windowLeft = 0; | |
| 1148 this._windowRight = 1; | |
| 1149 } | |
| 1150 | |
| 1151 var totalPixels = Math.floor((this._offsetWidth - this._paddingLeft) / windo
wWidth); | |
| 1152 this._pixelWindowLeft = Math.floor(totalPixels * this._windowLeft); | |
| 1153 | |
| 1154 this._timeToPixel = totalPixels / this._totalTime; | |
| 1155 this._pixelToTime = this._totalTime / totalPixels; | |
| 1156 } | |
| 1157 | |
| 1158 _updateHeight() { | |
| 1159 var height = this._levelToHeight(this._dataProvider.maxStackDepth()); | |
| 1160 this.setContentHeight(height); | |
| 1161 } | |
| 1162 | |
| 1163 /** | |
| 1164 * @override | |
| 1165 */ | |
| 1166 onResize() { | |
| 1167 super.onResize(); | |
| 1168 this.scheduleUpdate(); | |
| 1169 } | |
| 1170 | |
| 1171 /** | |
| 1172 * @override | |
| 1173 */ | |
| 1174 update() { | |
| 1175 if (!this._timelineData()) | |
| 1176 return; | |
| 1177 this._resetCanvas(); | |
| 1178 this._updateHeight(); | |
| 1179 this._updateBoundaries(); | |
| 1180 this._calculator._updateBoundaries(this); | |
| 1181 this._draw(this._offsetWidth, this._offsetHeight); | |
| 1182 if (!this.isDragging()) | |
| 1183 this._updateHighlight(); | |
| 1184 } | |
| 1185 | |
| 1186 /** | |
| 1187 * @override | |
| 1188 */ | |
| 1189 reset() { | |
| 1190 super.reset(); | |
| 1191 this._highlightedMarkerIndex = -1; | |
| 1192 this._highlightedEntryIndex = -1; | |
| 1193 this._selectedEntryIndex = -1; | |
| 1194 /** @type {!Map<string,!Map<string,number>>} */ | |
| 1195 this._textWidth = new Map(); | |
| 1196 this.update(); | |
| 1197 } | |
| 1198 | |
| 1199 _enabled() { | |
| 1200 return this._rawTimelineDataLength !== 0; | |
| 1201 } | |
| 1202 }; | |
| 1203 | |
| 1204 UI.FlameChart.HeaderHeight = 15; | |
| 1205 | |
| 1206 UI.FlameChart.MinimalTimeWindowMs = 0.5; | |
| 1207 | |
| 1208 /** | |
| 1209 * @interface | |
| 1210 */ | |
| 1211 UI.FlameChartDataProvider = function() {}; | |
| 1212 | |
| 1213 /** | |
| 1214 * @typedef {!{name: string, startLevel: number, expanded: (boolean|undefined),
style: !UI.FlameChart.GroupStyle}} | |
| 1215 */ | |
| 1216 UI.FlameChart.Group; | |
| 1217 | |
| 1218 /** | |
| 1219 * @typedef {!{ | |
| 1220 * height: number, | |
| 1221 * padding: number, | |
| 1222 * collapsible: boolean, | |
| 1223 * font: string, | |
| 1224 * color: string, | |
| 1225 * backgroundColor: string, | |
| 1226 * nestingLevel: number, | |
| 1227 * shareHeaderLine: (boolean|undefined), | |
| 1228 * useFirstLineForOverview: (boolean|undefined) | |
| 1229 * }} | |
| 1230 */ | |
| 1231 UI.FlameChart.GroupStyle; | |
| 1232 | |
| 1233 /** | |
| 1234 * @unrestricted | |
| 1235 */ | |
| 1236 UI.FlameChart.TimelineData = class { | |
| 1237 /** | |
| 1238 * @param {!Array<number>|!Uint16Array} entryLevels | |
| 1239 * @param {!Array<number>|!Float32Array} entryTotalTimes | |
| 1240 * @param {!Array<number>|!Float64Array} entryStartTimes | |
| 1241 * @param {?Array<!UI.FlameChart.Group>} groups | |
| 1242 */ | |
| 1243 constructor(entryLevels, entryTotalTimes, entryStartTimes, groups) { | |
| 1244 this.entryLevels = entryLevels; | |
| 1245 this.entryTotalTimes = entryTotalTimes; | |
| 1246 this.entryStartTimes = entryStartTimes; | |
| 1247 this.groups = groups; | |
| 1248 /** @type {!Array.<!UI.FlameChartMarker>} */ | |
| 1249 this.markers = []; | |
| 1250 } | |
| 1251 }; | |
| 1252 | |
| 1253 UI.FlameChartDataProvider.prototype = { | |
| 1254 /** | |
| 1255 * @return {number} | |
| 1256 */ | |
| 1257 minimumBoundary() {}, | |
| 1258 | |
| 1259 /** | |
| 1260 * @return {number} | |
| 1261 */ | |
| 1262 totalTime() {}, | |
| 1263 | |
| 1264 /** | |
| 1265 * @param {number} value | |
| 1266 * @param {number=} precision | |
| 1267 * @return {string} | |
| 1268 */ | |
| 1269 formatValue(value, precision) {}, | |
| 1270 | |
| 1271 /** | |
| 1272 * @return {number} | |
| 1273 */ | |
| 1274 maxStackDepth() {}, | |
| 1275 | |
| 1276 /** | |
| 1277 * @return {?UI.FlameChart.TimelineData} | |
| 1278 */ | |
| 1279 timelineData() {}, | |
| 1280 | |
| 1281 /** | |
| 1282 * @param {number} entryIndex | |
| 1283 * @return {?Element} | |
| 1284 */ | |
| 1285 prepareHighlightedEntryInfo(entryIndex) {}, | |
| 1286 | |
| 1287 /** | |
| 1288 * @param {number} entryIndex | |
| 1289 * @return {boolean} | |
| 1290 */ | |
| 1291 canJumpToEntry(entryIndex) {}, | |
| 1292 | |
| 1293 /** | |
| 1294 * @param {number} entryIndex | |
| 1295 * @return {?string} | |
| 1296 */ | |
| 1297 entryTitle(entryIndex) {}, | |
| 1298 | |
| 1299 /** | |
| 1300 * @param {number} entryIndex | |
| 1301 * @return {?string} | |
| 1302 */ | |
| 1303 entryFont(entryIndex) {}, | |
| 1304 | |
| 1305 /** | |
| 1306 * @param {number} entryIndex | |
| 1307 * @return {string} | |
| 1308 */ | |
| 1309 entryColor(entryIndex) {}, | |
| 1310 | |
| 1311 /** | |
| 1312 * @param {number} entryIndex | |
| 1313 * @param {!CanvasRenderingContext2D} context | |
| 1314 * @param {?string} text | |
| 1315 * @param {number} barX | |
| 1316 * @param {number} barY | |
| 1317 * @param {number} barWidth | |
| 1318 * @param {number} barHeight | |
| 1319 * @param {number} unclippedBarX | |
| 1320 * @param {number} timeToPixels | |
| 1321 * @return {boolean} | |
| 1322 */ | |
| 1323 decorateEntry(entryIndex, context, text, barX, barY, barWidth, barHeight, uncl
ippedBarX, timeToPixels) {}, | |
| 1324 | |
| 1325 /** | |
| 1326 * @param {number} entryIndex | |
| 1327 * @return {boolean} | |
| 1328 */ | |
| 1329 forceDecoration(entryIndex) {}, | |
| 1330 | |
| 1331 /** | |
| 1332 * @param {number} entryIndex | |
| 1333 * @return {string} | |
| 1334 */ | |
| 1335 textColor(entryIndex) {}, | |
| 1336 }; | |
| 1337 | |
| 1338 /** | |
| 1339 * @interface | |
| 1340 */ | |
| 1341 UI.FlameChartMarker = function() {}; | |
| 1342 | |
| 1343 UI.FlameChartMarker.prototype = { | |
| 1344 /** | |
| 1345 * @return {number} | |
| 1346 */ | |
| 1347 startTime() {}, | |
| 1348 | |
| 1349 /** | |
| 1350 * @return {string} | |
| 1351 */ | |
| 1352 color() {}, | |
| 1353 | |
| 1354 /** | |
| 1355 * @return {string} | |
| 1356 */ | |
| 1357 title() {}, | |
| 1358 | |
| 1359 /** | |
| 1360 * @param {!CanvasRenderingContext2D} context | |
| 1361 * @param {number} x | |
| 1362 * @param {number} height | |
| 1363 * @param {number} pixelsPerMillisecond | |
| 1364 */ | |
| 1365 draw(context, x, height, pixelsPerMillisecond) {}, | |
| 1366 }; | |
| 1367 | |
| 1368 /** @enum {symbol} */ | |
| 1369 UI.FlameChart.Events = { | |
| 1370 EntrySelected: Symbol('EntrySelected') | |
| 1371 }; | |
| 1372 | |
| 1373 /** | |
| 1374 * @unrestricted | |
| 1375 */ | |
| 1376 UI.FlameChart.ColorGenerator = class { | |
| 1377 /** | |
| 1378 * @param {!{min: number, max: number}|number=} hueSpace | |
| 1379 * @param {!{min: number, max: number, count: (number|undefined)}|number=} sat
Space | |
| 1380 * @param {!{min: number, max: number, count: (number|undefined)}|number=} lig
htnessSpace | |
| 1381 * @param {!{min: number, max: number, count: (number|undefined)}|number=} alp
haSpace | |
| 1382 */ | |
| 1383 constructor(hueSpace, satSpace, lightnessSpace, alphaSpace) { | |
| 1384 this._hueSpace = hueSpace || {min: 0, max: 360}; | |
| 1385 this._satSpace = satSpace || 67; | |
| 1386 this._lightnessSpace = lightnessSpace || 80; | |
| 1387 this._alphaSpace = alphaSpace || 1; | |
| 1388 /** @type {!Map<string, string>} */ | |
| 1389 this._colors = new Map(); | |
| 1390 } | |
| 1391 | |
| 1392 /** | |
| 1393 * @param {string} id | |
| 1394 * @param {string} color | |
| 1395 */ | |
| 1396 setColorForID(id, color) { | |
| 1397 this._colors.set(id, color); | |
| 1398 } | |
| 1399 | |
| 1400 /** | |
| 1401 * @param {string} id | |
| 1402 * @return {string} | |
| 1403 */ | |
| 1404 colorForID(id) { | |
| 1405 var color = this._colors.get(id); | |
| 1406 if (!color) { | |
| 1407 color = this._generateColorForID(id); | |
| 1408 this._colors.set(id, color); | |
| 1409 } | |
| 1410 return color; | |
| 1411 } | |
| 1412 | |
| 1413 /** | |
| 1414 * @param {string} id | |
| 1415 * @return {string} | |
| 1416 */ | |
| 1417 _generateColorForID(id) { | |
| 1418 var hash = String.hashCode(id); | |
| 1419 var h = this._indexToValueInSpace(hash, this._hueSpace); | |
| 1420 var s = this._indexToValueInSpace(hash >> 8, this._satSpace); | |
| 1421 var l = this._indexToValueInSpace(hash >> 16, this._lightnessSpace); | |
| 1422 var a = this._indexToValueInSpace(hash >> 24, this._alphaSpace); | |
| 1423 return 'hsla(' + h + ', ' + s + '%, ' + l + '%, ' + a + ')'; | |
| 1424 } | |
| 1425 | |
| 1426 /** | |
| 1427 * @param {number} index | |
| 1428 * @param {!{min: number, max: number, count: (number|undefined)}|number} spac
e | |
| 1429 * @return {number} | |
| 1430 */ | |
| 1431 _indexToValueInSpace(index, space) { | |
| 1432 if (typeof space === 'number') | |
| 1433 return space; | |
| 1434 var count = space.count || space.max - space.min; | |
| 1435 index %= count; | |
| 1436 return space.min + Math.floor(index / (count - 1) * (space.max - space.min))
; | |
| 1437 } | |
| 1438 }; | |
| 1439 | |
| 1440 /** | |
| 1441 * @implements {UI.TimelineGrid.Calculator} | |
| 1442 * @unrestricted | |
| 1443 */ | |
| 1444 UI.FlameChart.Calculator = class { | |
| 1445 /** | |
| 1446 * @param {!UI.FlameChartDataProvider} dataProvider | |
| 1447 */ | |
| 1448 constructor(dataProvider) { | |
| 1449 this._dataProvider = dataProvider; | |
| 1450 this._paddingLeft = 0; | |
| 1451 } | |
| 1452 | |
| 1453 /** | |
| 1454 * @override | |
| 1455 * @return {number} | |
| 1456 */ | |
| 1457 paddingLeft() { | |
| 1458 return this._paddingLeft; | |
| 1459 } | |
| 1460 | |
| 1461 /** | |
| 1462 * @param {!UI.FlameChart} mainPane | |
| 1463 */ | |
| 1464 _updateBoundaries(mainPane) { | |
| 1465 this._totalTime = mainPane._dataProvider.totalTime(); | |
| 1466 this._zeroTime = mainPane._dataProvider.minimumBoundary(); | |
| 1467 this._minimumBoundaries = this._zeroTime + mainPane._windowLeft * this._tota
lTime; | |
| 1468 this._maximumBoundaries = this._zeroTime + mainPane._windowRight * this._tot
alTime; | |
| 1469 this._paddingLeft = mainPane._paddingLeft; | |
| 1470 this._width = mainPane._offsetWidth - this._paddingLeft; | |
| 1471 this._timeToPixel = this._width / this.boundarySpan(); | |
| 1472 } | |
| 1473 | |
| 1474 /** | |
| 1475 * @override | |
| 1476 * @param {number} time | |
| 1477 * @return {number} | |
| 1478 */ | |
| 1479 computePosition(time) { | |
| 1480 return Math.round((time - this._minimumBoundaries) * this._timeToPixel + thi
s._paddingLeft); | |
| 1481 } | |
| 1482 | |
| 1483 /** | |
| 1484 * @override | |
| 1485 * @param {number} value | |
| 1486 * @param {number=} precision | |
| 1487 * @return {string} | |
| 1488 */ | |
| 1489 formatValue(value, precision) { | |
| 1490 return this._dataProvider.formatValue(value - this._zeroTime, precision); | |
| 1491 } | |
| 1492 | |
| 1493 /** | |
| 1494 * @override | |
| 1495 * @return {number} | |
| 1496 */ | |
| 1497 maximumBoundary() { | |
| 1498 return this._maximumBoundaries; | |
| 1499 } | |
| 1500 | |
| 1501 /** | |
| 1502 * @override | |
| 1503 * @return {number} | |
| 1504 */ | |
| 1505 minimumBoundary() { | |
| 1506 return this._minimumBoundaries; | |
| 1507 } | |
| 1508 | |
| 1509 /** | |
| 1510 * @override | |
| 1511 * @return {number} | |
| 1512 */ | |
| 1513 zeroTime() { | |
| 1514 return this._zeroTime; | |
| 1515 } | |
| 1516 | |
| 1517 /** | |
| 1518 * @override | |
| 1519 * @return {number} | |
| 1520 */ | |
| 1521 boundarySpan() { | |
| 1522 return this._maximumBoundaries - this._minimumBoundaries; | |
| 1523 } | |
| 1524 }; | |
| OLD | NEW |