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