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