1 /* 2 * Copyright (C) 2013 Google Inc. All rights reserved. 3 * Copyright (C) 2012 Intel Inc. All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are 7 * met: 8 * 9 * * Redistributions of source code must retain the above copyright 10 * notice, this list of conditions and the following disclaimer. 11 * * Redistributions in binary form must reproduce the above 12 * copyright notice, this list of conditions and the following disclaimer 13 * in the documentation and/or other materials provided with the 14 * distribution. 15 * * Neither the name of Google Inc. nor the names of its 16 * contributors may be used to endorse or promote products derived from 17 * this software without specific prior written permission. 18 * 19 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 */ 31 32 /** 33 * @constructor 34 * @extends {WebInspector.HBox} 35 * @implements {WebInspector.TimelineModeView} 36 * @param {!WebInspector.TimelineModeViewDelegate} delegate 37 * @param {!WebInspector.TimelineModel} model 38 * @param {!WebInspector.TimelineUIUtils} uiUtils 39 */ 40 WebInspector.TimelineView = function(delegate, model, uiUtils) 41 { 42 WebInspector.HBox.call(this); 43 this.element.classList.add("timeline-view"); 44 45 this._delegate = delegate; 46 this._model = model; 47 this._uiUtils = uiUtils; 48 this._presentationModel = new WebInspector.TimelinePresentationModel(model, uiUtils); 49 this._calculator = new WebInspector.TimelineCalculator(model); 50 this._linkifier = new WebInspector.Linkifier(); 51 this._frameStripByFrame = new Map(); 52 53 this._boundariesAreValid = true; 54 this._scrollTop = 0; 55 56 this._recordsView = this._createRecordsView(); 57 this._recordsView.addEventListener(WebInspector.SplitView.Events.SidebarSizeChanged, this._sidebarResized, this); 58 this._recordsView.show(this.element); 59 this._headerElement = this.element.createChild("div", "fill"); 60 this._headerElement.id = "timeline-graph-records-header"; 61 62 // Create gpu tasks containers. 63 this._cpuBarsElement = this._headerElement.createChild("div", "timeline-utilization-strip"); 64 if (Runtime.experiments.isEnabled("gpuTimeline")) 65 this._gpuBarsElement = this._headerElement.createChild("div", "timeline-utilization-strip gpu"); 66 67 this._popoverHelper = new WebInspector.PopoverHelper(this.element, this._getPopoverAnchor.bind(this), this._showPopover.bind(this)); 68 69 this.element.addEventListener("mousemove", this._mouseMove.bind(this), false); 70 this.element.addEventListener("mouseout", this._mouseOut.bind(this), false); 71 this.element.addEventListener("keydown", this._keyDown.bind(this), false); 72 73 this._expandOffset = 15; 74 } 75 76 WebInspector.TimelineView.prototype = { 77 /** 78 * @param {?WebInspector.TimelineFrameModelBase} frameModel 79 */ 80 setFrameModel: function(frameModel) 81 { 82 this._frameModel = frameModel; 83 }, 84 85 /** 86 * @return {!WebInspector.SplitView} 87 */ 88 _createRecordsView: function() 89 { 90 var recordsView = new WebInspector.SplitView(true, false, "timelinePanelRecorsSplitViewState"); 91 this._containerElement = recordsView.element; 92 this._containerElement.tabIndex = 0; 93 this._containerElement.id = "timeline-container"; 94 this._containerElement.addEventListener("scroll", this._onScroll.bind(this), false); 95 96 // Create records list in the records sidebar. 97 recordsView.sidebarElement().createChild("div", "timeline-records-title").textContent = WebInspector.UIString("RECORDS"); 98 this._sidebarListElement = recordsView.sidebarElement().createChild("div", "timeline-records-list"); 99 100 // Create grid in the records main area. 101 this._gridContainer = new WebInspector.VBoxWithResizeCallback(this._onViewportResize.bind(this)); 102 this._gridContainer.element.id = "resources-container-content"; 103 this._gridContainer.show(recordsView.mainElement()); 104 this._timelineGrid = new WebInspector.TimelineGrid(); 105 this._gridContainer.element.appendChild(this._timelineGrid.element); 106 107 this._itemsGraphsElement = this._gridContainer.element.createChild("div"); 108 this._itemsGraphsElement.id = "timeline-graphs"; 109 110 // Create gap elements 111 this._topGapElement = this._itemsGraphsElement.createChild("div", "timeline-gap"); 112 this._graphRowsElement = this._itemsGraphsElement.createChild("div"); 113 this._bottomGapElement = this._itemsGraphsElement.createChild("div", "timeline-gap"); 114 this._expandElements = this._itemsGraphsElement.createChild("div"); 115 this._expandElements.id = "orphan-expand-elements"; 116 117 return recordsView; 118 }, 119 120 _rootRecord: function() 121 { 122 return this._presentationModel.rootRecord(); 123 }, 124 125 _updateEventDividers: function() 126 { 127 this._timelineGrid.removeEventDividers(); 128 var clientWidth = this._graphRowsElementWidth; 129 var dividers = []; 130 var eventDividerRecords = this._model.eventDividerRecords(); 131 132 for (var i = 0; i < eventDividerRecords.length; ++i) { 133 var record = eventDividerRecords[i]; 134 var position = this._calculator.computePosition(record.startTime()); 135 var dividerPosition = Math.round(position); 136 if (dividerPosition < 0 || dividerPosition >= clientWidth || dividers[dividerPosition]) 137 continue; 138 var title = this._uiUtils.titleForRecord(record); 139 var divider = this._uiUtils.createEventDivider(record.type(), title); 140 divider.style.left = dividerPosition + "px"; 141 dividers[dividerPosition] = divider; 142 } 143 this._timelineGrid.addEventDividers(dividers); 144 }, 145 146 _updateFrameBars: function(frames) 147 { 148 var clientWidth = this._graphRowsElementWidth; 149 if (this._frameContainer) { 150 this._frameContainer.removeChildren(); 151 } else { 152 const frameContainerBorderWidth = 1; 153 this._frameContainer = document.createElementWithClass("div", "fill timeline-frame-container"); 154 this._frameContainer.style.height = WebInspector.TimelinePanel.rowHeight + frameContainerBorderWidth + "px"; 155 this._frameContainer.addEventListener("dblclick", this._onFrameDoubleClicked.bind(this), false); 156 this._frameContainer.addEventListener("click", this._onFrameClicked.bind(this), false); 157 } 158 this._frameStripByFrame.clear(); 159 160 var dividers = []; 161 162 for (var i = 0; i < frames.length; ++i) { 163 var frame = frames[i]; 164 var frameStart = this._calculator.computePosition(frame.startTime); 165 var frameEnd = this._calculator.computePosition(frame.endTime); 166 167 var frameStrip = document.createElementWithClass("div", "timeline-frame-strip"); 168 var actualStart = Math.max(frameStart, 0); 169 var width = frameEnd - actualStart; 170 frameStrip.style.left = actualStart + "px"; 171 frameStrip.style.width = width + "px"; 172 frameStrip._frame = frame; 173 this._frameStripByFrame.set(frame, frameStrip); 174 175 const minWidthForFrameInfo = 60; 176 if (width > minWidthForFrameInfo) 177 frameStrip.textContent = Number.millisToString(frame.endTime - frame.startTime, true); 178 179 this._frameContainer.appendChild(frameStrip); 180 181 if (actualStart > 0) { 182 var frameMarker = this._uiUtils.createBeginFrameDivider(); 183 frameMarker.style.left = frameStart + "px"; 184 dividers.push(frameMarker); 185 } 186 } 187 this._timelineGrid.addEventDividers(dividers); 188 this._headerElement.appendChild(this._frameContainer); 189 }, 190 191 _onFrameDoubleClicked: function(event) 192 { 193 var frameBar = event.target.enclosingNodeOrSelfWithClass("timeline-frame-strip"); 194 if (!frameBar) 195 return; 196 this._delegate.requestWindowTimes(frameBar._frame.startTime, frameBar._frame.endTime); 197 }, 198 199 _onFrameClicked: function(event) 200 { 201 var frameBar = event.target.enclosingNodeOrSelfWithClass("timeline-frame-strip"); 202 if (!frameBar) 203 return; 204 this._delegate.select(WebInspector.TimelineSelection.fromFrame(frameBar._frame)); 205 }, 206 207 /** 208 * @param {!WebInspector.TimelineModel.Record} record 209 */ 210 addRecord: function(record) 211 { 212 this._presentationModel.addRecord(record); 213 this._invalidateAndScheduleRefresh(false, false); 214 }, 215 216 /** 217 * @param {number} width 218 */ 219 setSidebarSize: function(width) 220 { 221 this._recordsView.setSidebarSize(width); 222 }, 223 224 /** 225 * @param {!WebInspector.Event} event 226 */ 227 _sidebarResized: function(event) 228 { 229 this.dispatchEventToListeners(WebInspector.SplitView.Events.SidebarSizeChanged, event.data); 230 }, 231 232 _onViewportResize: function() 233 { 234 this._resize(this._recordsView.sidebarSize()); 235 }, 236 237 /** 238 * @param {number} sidebarWidth 239 */ 240 _resize: function(sidebarWidth) 241 { 242 this._closeRecordDetails(); 243 this._graphRowsElementWidth = this._graphRowsElement.offsetWidth; 244 this._headerElement.style.left = sidebarWidth + "px"; 245 this._headerElement.style.width = this._itemsGraphsElement.offsetWidth + "px"; 246 this._scheduleRefresh(false, true); 247 }, 248 249 _resetView: function() 250 { 251 this._windowStartTime = 0; 252 this._windowEndTime = 0; 253 this._boundariesAreValid = false; 254 this._adjustScrollPosition(0); 255 this._linkifier.reset(); 256 this._closeRecordDetails(); 257 this._automaticallySizeWindow = true; 258 this._presentationModel.reset(); 259 }, 260 261 262 /** 263 * @return {!WebInspector.View} 264 */ 265 view: function() 266 { 267 return this; 268 }, 269 270 dispose: function() 271 { 272 }, 273 274 reset: function() 275 { 276 this._resetView(); 277 this._invalidateAndScheduleRefresh(true, true); 278 }, 279 280 /** 281 * @return {!Array.<!Element>} 282 */ 283 elementsToRestoreScrollPositionsFor: function() 284 { 285 return [this._containerElement]; 286 }, 287 288 /** 289 * @param {?RegExp} textFilter 290 */ 291 refreshRecords: function(textFilter) 292 { 293 this._automaticallySizeWindow = false; 294 this._presentationModel.setTextFilter(textFilter); 295 this._invalidateAndScheduleRefresh(false, true); 296 }, 297 298 willHide: function() 299 { 300 this._closeRecordDetails(); 301 WebInspector.View.prototype.willHide.call(this); 302 }, 303 304 wasShown: function() 305 { 306 this._presentationModel.refreshRecords(); 307 WebInspector.HBox.prototype.wasShown.call(this); 308 }, 309 310 _onScroll: function(event) 311 { 312 this._closeRecordDetails(); 313 this._scrollTop = this._containerElement.scrollTop; 314 var dividersTop = Math.max(0, this._scrollTop); 315 this._timelineGrid.setScrollAndDividerTop(this._scrollTop, dividersTop); 316 this._scheduleRefresh(true, true); 317 }, 318 319 /** 320 * @param {boolean} preserveBoundaries 321 * @param {boolean} userGesture 322 */ 323 _invalidateAndScheduleRefresh: function(preserveBoundaries, userGesture) 324 { 325 this._presentationModel.invalidateFilteredRecords(); 326 this._scheduleRefresh(preserveBoundaries, userGesture); 327 }, 328 329 _clearSelection: function() 330 { 331 this._delegate.select(null); 332 }, 333 334 /** 335 * @param {?WebInspector.TimelinePresentationModel.Record} presentationRecord 336 */ 337 _selectRecord: function(presentationRecord) 338 { 339 if (presentationRecord.coalesced()) { 340 // Presentation record does not have model record to highlight. 341 this._innerSetSelectedRecord(presentationRecord); 342 var aggregatedStats = {}; 343 var presentationChildren = presentationRecord.presentationChildren(); 344 for (var i = 0; i < presentationChildren.length; ++i) 345 this._uiUtils.aggregateTimeForRecord(aggregatedStats, presentationChildren[i].record()); 346 var idle = presentationRecord.endTime() - presentationRecord.startTime(); 347 for (var category in aggregatedStats) 348 idle -= aggregatedStats[category]; 349 aggregatedStats["idle"] = idle; 350 var pieChart = WebInspector.TimelineUIUtils.generatePieChart(aggregatedStats); 351 var title = this._uiUtils.titleForRecord(presentationRecord.record()); 352 this._delegate.showInDetails(title, pieChart); 353 return; 354 } 355 this._delegate.select(WebInspector.TimelineSelection.fromRecord(presentationRecord.record())); 356 }, 357 358 /** 359 * @param {?WebInspector.TimelineSelection} selection 360 */ 361 setSelection: function(selection) 362 { 363 if (!selection) { 364 this._innerSetSelectedRecord(null); 365 this._setSelectedFrame(null); 366 return; 367 } 368 if (selection.type() === WebInspector.TimelineSelection.Type.Record) { 369 var record = /** @type {!WebInspector.TimelineModel.Record} */ (selection.object()); 370 this._innerSetSelectedRecord(this._presentationModel.toPresentationRecord(record)); 371 this._setSelectedFrame(null); 372 } else if (selection.type() === WebInspector.TimelineSelection.Type.Frame) { 373 var frame = /** @type {!WebInspector.TimelineFrame} */ (selection.object()); 374 this._innerSetSelectedRecord(null); 375 this._setSelectedFrame(frame); 376 } 377 }, 378 379 /** 380 * @param {?WebInspector.TimelinePresentationModel.Record} presentationRecord 381 */ 382 _innerSetSelectedRecord: function(presentationRecord) 383 { 384 if (presentationRecord === this._lastSelectedRecord) 385 return; 386 387 // Remove selection rendering.p 388 if (this._lastSelectedRecord) { 389 if (this._lastSelectedRecord.listRow()) 390 this._lastSelectedRecord.listRow().renderAsSelected(false); 391 if (this._lastSelectedRecord.graphRow()) 392 this._lastSelectedRecord.graphRow().renderAsSelected(false); 393 } 394 395 this._lastSelectedRecord = presentationRecord; 396 if (!presentationRecord) 397 return; 398 399 this._innerRevealRecord(presentationRecord); 400 if (presentationRecord.listRow()) 401 presentationRecord.listRow().renderAsSelected(true); 402 if (presentationRecord.graphRow()) 403 presentationRecord.graphRow().renderAsSelected(true); 404 }, 405 406 /** 407 * @param {?WebInspector.TimelineFrame} frame 408 */ 409 _setSelectedFrame: function(frame) 410 { 411 if (this._lastSelectedFrame === frame) 412 return; 413 var oldStripElement = this._lastSelectedFrame && this._frameStripByFrame.get(this._lastSelectedFrame); 414 if (oldStripElement) 415 oldStripElement.classList.remove("selected"); 416 var newStripElement = frame && this._frameStripByFrame.get(frame); 417 if (newStripElement) 418 newStripElement.classList.add("selected"); 419 this._lastSelectedFrame = frame; 420 }, 421 422 /** 423 * @param {number} startTime 424 * @param {number} endTime 425 */ 426 setWindowTimes: function(startTime, endTime) 427 { 428 this._windowStartTime = startTime; 429 this._windowEndTime = endTime; 430 this._presentationModel.setWindowTimes(startTime, endTime); 431 this._automaticallySizeWindow = false; 432 this._invalidateAndScheduleRefresh(false, true); 433 this._clearSelection(); 434 }, 435 436 /** 437 * @param {boolean} preserveBoundaries 438 * @param {boolean} userGesture 439 */ 440 _scheduleRefresh: function(preserveBoundaries, userGesture) 441 { 442 this._closeRecordDetails(); 443 this._boundariesAreValid &= preserveBoundaries; 444 445 if (!this.isShowing()) 446 return; 447 448 if (preserveBoundaries || userGesture) 449 this._refresh(); 450 else { 451 if (!this._refreshTimeout) 452 this._refreshTimeout = setTimeout(this._refresh.bind(this), 300); 453 } 454 }, 455 456 _refresh: function() 457 { 458 if (this._refreshTimeout) { 459 clearTimeout(this._refreshTimeout); 460 delete this._refreshTimeout; 461 } 462 var windowStartTime = this._windowStartTime || this._model.minimumRecordTime(); 463 var windowEndTime = this._windowEndTime || this._model.maximumRecordTime(); 464 this._timelinePaddingLeft = this._expandOffset; 465 this._calculator.setWindow(windowStartTime, windowEndTime); 466 this._calculator.setDisplayWindow(this._timelinePaddingLeft, this._graphRowsElementWidth); 467 468 this._refreshRecords(); 469 if (!this._boundariesAreValid) { 470 this._updateEventDividers(); 471 if (this._frameContainer) 472 this._frameContainer.remove(); 473 if (this._frameModel) { 474 var frames = this._frameModel.filteredFrames(windowStartTime, windowEndTime); 475 const maxFramesForFrameBars = 30; 476 if (frames.length && frames.length < maxFramesForFrameBars) { 477 this._timelineGrid.removeDividers(); 478 this._updateFrameBars(frames); 479 } else { 480 this._timelineGrid.updateDividers(this._calculator); 481 } 482 } else 483 this._timelineGrid.updateDividers(this._calculator); 484 this._refreshAllUtilizationBars(); 485 } 486 this._boundariesAreValid = true; 487 }, 488 489 /** 490 * @param {!WebInspector.TimelinePresentationModel.Record} recordToReveal 491 */ 492 _innerRevealRecord: function(recordToReveal) 493 { 494 var needRefresh = false; 495 // Expand all ancestors. 496 for (var parent = recordToReveal.presentationParent(); parent !== this._rootRecord(); parent = parent.presentationParent()) { 497 if (!parent.collapsed()) 498 continue; 499 this._presentationModel.invalidateFilteredRecords(); 500 parent.setCollapsed(false); 501 needRefresh = true; 502 } 503 var recordsInWindow = this._presentationModel.filteredRecords(); 504 var index = recordsInWindow.indexOf(recordToReveal); 505 506 var itemOffset = index * WebInspector.TimelinePanel.rowHeight; 507 var visibleTop = this._scrollTop - WebInspector.TimelinePanel.headerHeight; 508 var visibleBottom = visibleTop + this._containerElementHeight - WebInspector.TimelinePanel.rowHeight; 509 if (itemOffset < visibleTop) 510 this._containerElement.scrollTop = itemOffset; 511 else if (itemOffset > visibleBottom) 512 this._containerElement.scrollTop = itemOffset - this._containerElementHeight + WebInspector.TimelinePanel.headerHeight + WebInspector.TimelinePanel.rowHeight; 513 else if (needRefresh) 514 this._refreshRecords(); 515 }, 516 517 _refreshRecords: function() 518 { 519 this._containerElementHeight = this._containerElement.clientHeight; 520 var recordsInWindow = this._presentationModel.filteredRecords(); 521 522 // Calculate the visible area. 523 var visibleTop = this._scrollTop; 524 var visibleBottom = visibleTop + this._containerElementHeight; 525 526 var rowHeight = WebInspector.TimelinePanel.rowHeight; 527 var headerHeight = WebInspector.TimelinePanel.headerHeight; 528 529 // Convert visible area to visible indexes. Always include top-level record for a visible nested record. 530 var startIndex = Math.max(0, Math.min(Math.floor((visibleTop - headerHeight) / rowHeight), recordsInWindow.length - 1)); 531 var endIndex = Math.min(recordsInWindow.length, Math.ceil(visibleBottom / rowHeight)); 532 var lastVisibleLine = Math.max(0, Math.floor((visibleBottom - headerHeight) / rowHeight)); 533 if (this._automaticallySizeWindow && recordsInWindow.length > lastVisibleLine) { 534 this._automaticallySizeWindow = false; 535 this._clearSelection(); 536 // If we're at the top, always use real timeline start as a left window bound so that expansion arrow padding logic works. 537 var windowStartTime = startIndex ? recordsInWindow[startIndex].startTime() : this._model.minimumRecordTime(); 538 var windowEndTime = recordsInWindow[Math.max(0, lastVisibleLine - 1)].endTime(); 539 this._delegate.requestWindowTimes(windowStartTime, windowEndTime); 540 recordsInWindow = this._presentationModel.filteredRecords(); 541 endIndex = Math.min(recordsInWindow.length, lastVisibleLine); 542 } 543 544 // Resize gaps first. 545 this._topGapElement.style.height = (startIndex * rowHeight) + "px"; 546 this._recordsView.sidebarElement().firstElementChild.style.flexBasis = (startIndex * rowHeight + headerHeight) + "px"; 547 this._bottomGapElement.style.height = (recordsInWindow.length - endIndex) * rowHeight + "px"; 548 var rowsHeight = headerHeight + recordsInWindow.length * rowHeight; 549 var totalHeight = Math.max(this._containerElementHeight, rowsHeight); 550 551 this._recordsView.mainElement().style.height = totalHeight + "px"; 552 this._recordsView.sidebarElement().style.height = totalHeight + "px"; 553 this._recordsView.resizerElement().style.height = totalHeight + "px"; 554 555 // Update visible rows. 556 var listRowElement = this._sidebarListElement.firstChild; 557 var width = this._graphRowsElementWidth; 558 this._itemsGraphsElement.removeChild(this._graphRowsElement); 559 var graphRowElement = this._graphRowsElement.firstChild; 560 var scheduleRefreshCallback = this._invalidateAndScheduleRefresh.bind(this, true, true); 561 var selectRecordCallback = this._selectRecord.bind(this); 562 this._itemsGraphsElement.removeChild(this._expandElements); 563 this._expandElements.removeChildren(); 564 565 for (var i = 0; i < endIndex; ++i) { 566 var record = recordsInWindow[i]; 567 568 if (i < startIndex) { 569 var lastChildIndex = i + record.visibleChildrenCount(); 570 if (lastChildIndex >= startIndex && lastChildIndex < endIndex) { 571 var expandElement = new WebInspector.TimelineExpandableElement(this._expandElements); 572 var positions = this._calculator.computeBarGraphWindowPosition(record); 573 expandElement._update(record, i, positions.left - this._expandOffset, positions.width); 574 } 575 } else { 576 if (!listRowElement) { 577 listRowElement = new WebInspector.TimelineRecordListRow(this._linkifier, selectRecordCallback, scheduleRefreshCallback).element; 578 this._sidebarListElement.appendChild(listRowElement); 579 } 580 if (!graphRowElement) { 581 graphRowElement = new WebInspector.TimelineRecordGraphRow(this._itemsGraphsElement, selectRecordCallback, scheduleRefreshCallback).element; 582 this._graphRowsElement.appendChild(graphRowElement); 583 } 584 585 listRowElement.row.update(record, visibleTop, this._uiUtils); 586 graphRowElement.row.update(record, this._calculator, this._expandOffset, i, this._uiUtils); 587 if (this._lastSelectedRecord === record) { 588 listRowElement.row.renderAsSelected(true); 589 graphRowElement.row.renderAsSelected(true); 590 } 591 592 listRowElement = listRowElement.nextSibling; 593 graphRowElement = graphRowElement.nextSibling; 594 } 595 } 596 597 // Remove extra rows. 598 while (listRowElement) { 599 var nextElement = listRowElement.nextSibling; 600 listRowElement.row.dispose(); 601 listRowElement = nextElement; 602 } 603 while (graphRowElement) { 604 var nextElement = graphRowElement.nextSibling; 605 graphRowElement.row.dispose(); 606 graphRowElement = nextElement; 607 } 608 609 this._itemsGraphsElement.insertBefore(this._graphRowsElement, this._bottomGapElement); 610 this._itemsGraphsElement.appendChild(this._expandElements); 611 this._adjustScrollPosition(recordsInWindow.length * rowHeight + headerHeight); 612 613 return recordsInWindow.length; 614 }, 615 616 _refreshAllUtilizationBars: function() 617 { 618 this._refreshUtilizationBars(WebInspector.UIString("CPU"), this._model.mainThreadTasks(), this._cpuBarsElement); 619 if (Runtime.experiments.isEnabled("gpuTimeline")) 620 this._refreshUtilizationBars(WebInspector.UIString("GPU"), this._model.gpuThreadTasks(), this._gpuBarsElement); 621 }, 622 623 /** 624 * @param {string} name 625 * @param {!Array.<!WebInspector.TimelineModel.Record>} tasks 626 * @param {?Element} container 627 */ 628 _refreshUtilizationBars: function(name, tasks, container) 629 { 630 if (!container) 631 return; 632 633 const barOffset = 3; 634 const minGap = 3; 635 636 var minWidth = WebInspector.TimelineCalculator._minWidth; 637 var widthAdjustment = minWidth / 2; 638 639 var width = this._graphRowsElementWidth; 640 var boundarySpan = this._windowEndTime - this._windowStartTime; 641 var scale = boundarySpan / (width - minWidth - this._timelinePaddingLeft); 642 var startTime = (this._windowStartTime - this._timelinePaddingLeft * scale); 643 var endTime = startTime + width * scale; 644 645 /** 646 * @param {number} value 647 * @param {!WebInspector.TimelineModel.Record} task 648 * @return {number} 649 */ 650 function compareEndTime(value, task) 651 { 652 return value < task.endTime() ? -1 : 1; 653 } 654 655 var taskIndex = insertionIndexForObjectInListSortedByFunction(startTime, tasks, compareEndTime); 656 657 var foreignStyle = "gpu-task-foreign"; 658 var element = /** @type {?Element} */ (container.firstChild); 659 var lastElement; 660 var lastLeft; 661 var lastRight; 662 663 for (; taskIndex < tasks.length; ++taskIndex) { 664 var task = tasks[taskIndex]; 665 if (task.startTime() > endTime) 666 break; 667 668 var left = Math.max(0, this._calculator.computePosition(task.startTime()) + barOffset - widthAdjustment); 669 var right = Math.min(width, this._calculator.computePosition(task.endTime() || 0) + barOffset + widthAdjustment); 670 671 if (lastElement) { 672 var gap = Math.floor(left) - Math.ceil(lastRight); 673 if (gap < minGap) { 674 if (!task.data["foreign"]) 675 lastElement.classList.remove(foreignStyle); 676 lastRight = right; 677 lastElement._tasksInfo.lastTaskIndex = taskIndex; 678 continue; 679 } 680 lastElement.style.width = (lastRight - lastLeft) + "px"; 681 } 682 683 if (!element) 684 element = container.createChild("div", "timeline-graph-bar"); 685 element.style.left = left + "px"; 686 element._tasksInfo = {name: name, tasks: tasks, firstTaskIndex: taskIndex, lastTaskIndex: taskIndex}; 687 if (task.data["foreign"]) 688 element.classList.add(foreignStyle); 689 lastLeft = left; 690 lastRight = right; 691 lastElement = element; 692 element = /** @type {?Element} */ (element.nextSibling); 693 } 694 695 if (lastElement) 696 lastElement.style.width = (lastRight - lastLeft) + "px"; 697 698 while (element) { 699 var nextElement = element.nextSibling; 700 element._tasksInfo = null; 701 container.removeChild(element); 702 element = nextElement; 703 } 704 }, 705 706 _adjustScrollPosition: function(totalHeight) 707 { 708 // Prevent the container from being scrolled off the end. 709 if ((this._scrollTop + this._containerElementHeight) > totalHeight + 1) 710 this._containerElement.scrollTop = (totalHeight - this._containerElement.offsetHeight); 711 }, 712 713 /** 714 * @param {!Element} element 715 * @param {!Event} event 716 * @return {!Element|!AnchorBox|undefined} 717 */ 718 _getPopoverAnchor: function(element, event) 719 { 720 var anchor = element.enclosingNodeOrSelfWithClass("timeline-graph-bar"); 721 if (anchor && anchor._tasksInfo) 722 return anchor; 723 }, 724 725 _mouseOut: function() 726 { 727 this._hideQuadHighlight(); 728 }, 729 730 /** 731 * @param {!Event} e 732 */ 733 _mouseMove: function(e) 734 { 735 var rowElement = e.target.enclosingNodeOrSelfWithClass("timeline-tree-item"); 736 if (!this._highlightQuad(rowElement)) 737 this._hideQuadHighlight(); 738 739 var taskBarElement = e.target.enclosingNodeOrSelfWithClass("timeline-graph-bar"); 740 if (taskBarElement && taskBarElement._tasksInfo) { 741 var offset = taskBarElement.offsetLeft; 742 this._timelineGrid.showCurtains(offset >= 0 ? offset : 0, taskBarElement.offsetWidth); 743 } else 744 this._timelineGrid.hideCurtains(); 745 }, 746 747 /** 748 * @param {!Event} event 749 */ 750 _keyDown: function(event) 751 { 752 if (!this._lastSelectedRecord || event.shiftKey || event.metaKey || event.ctrlKey) 753 return; 754 755 var record = this._lastSelectedRecord; 756 var recordsInWindow = this._presentationModel.filteredRecords(); 757 var index = recordsInWindow.indexOf(record); 758 var recordsInPage = Math.floor(this._containerElementHeight / WebInspector.TimelinePanel.rowHeight); 759 var rowHeight = WebInspector.TimelinePanel.rowHeight; 760 761 if (index === -1) 762 index = 0; 763 764 switch (event.keyIdentifier) { 765 case "Left": 766 if (record.presentationParent()) { 767 if ((!record.expandable() || record.collapsed()) && record.presentationParent() !== this._presentationModel.rootRecord()) { 768 this._selectRecord(record.presentationParent()); 769 } else { 770 record.setCollapsed(true); 771 this._invalidateAndScheduleRefresh(true, true); 772 } 773 } 774 event.consume(true); 775 break; 776 case "Up": 777 if (--index < 0) 778 break; 779 this._selectRecord(recordsInWindow[index]); 780 event.consume(true); 781 break; 782 case "Right": 783 if (record.expandable() && record.collapsed()) { 784 record.setCollapsed(false); 785 this._invalidateAndScheduleRefresh(true, true); 786 } else { 787 if (++index >= recordsInWindow.length) 788 break; 789 this._selectRecord(recordsInWindow[index]); 790 } 791 event.consume(true); 792 break; 793 case "Down": 794 if (++index >= recordsInWindow.length) 795 break; 796 this._selectRecord(recordsInWindow[index]); 797 event.consume(true); 798 break; 799 case "PageUp": 800 index = Math.max(0, index - recordsInPage); 801 this._scrollTop = Math.max(0, this._scrollTop - recordsInPage * rowHeight); 802 this._containerElement.scrollTop = this._scrollTop; 803 this._selectRecord(recordsInWindow[index]); 804 event.consume(true); 805 break; 806 case "PageDown": 807 index = Math.min(recordsInWindow.length - 1, index + recordsInPage); 808 this._scrollTop = Math.min(this._containerElement.scrollHeight - this._containerElementHeight, this._scrollTop + recordsInPage * rowHeight); 809 this._containerElement.scrollTop = this._scrollTop; 810 this._selectRecord(recordsInWindow[index]); 811 event.consume(true); 812 break; 813 case "Home": 814 index = 0; 815 this._selectRecord(recordsInWindow[index]); 816 event.consume(true); 817 break; 818 case "End": 819 index = recordsInWindow.length - 1; 820 this._selectRecord(recordsInWindow[index]); 821 event.consume(true); 822 break; 823 } 824 }, 825 826 /** 827 * @param {?Element} rowElement 828 * @return {boolean} 829 */ 830 _highlightQuad: function(rowElement) 831 { 832 if (!rowElement || !rowElement.row) 833 return false; 834 var presentationRecord = rowElement.row._record; 835 if (presentationRecord.coalesced()) 836 return false; 837 var record = presentationRecord.record(); 838 if (this._highlightedQuadRecord === record) 839 return true; 840 841 var quad = this._uiUtils.highlightQuadForRecord(record); 842 var target = record.target(); 843 if (!quad || !target) 844 return false; 845 this._highlightedQuadRecord = record; 846 target.domAgent().highlightQuad(quad, WebInspector.Color.PageHighlight.Content.toProtocolRGBA(), WebInspector.Color.PageHighlight.ContentOutline.toProtocolRGBA()); 847 return true; 848 }, 849 850 _hideQuadHighlight: function() 851 { 852 var target = this._highlightedQuadRecord ? this._highlightedQuadRecord.target() : null; 853 if (target) 854 target.domAgent().hideHighlight(); 855 856 if (this._highlightedQuadRecord) 857 delete this._highlightedQuadRecord; 858 }, 859 860 /** 861 * @param {!Element} anchor 862 * @param {!WebInspector.Popover} popover 863 */ 864 _showPopover: function(anchor, popover) 865 { 866 if (!anchor._tasksInfo) 867 return; 868 popover.show(WebInspector.TimelineUIUtils.generateMainThreadBarPopupContent(this._model, anchor._tasksInfo), anchor, null, null, WebInspector.Popover.Orientation.Bottom); 869 }, 870 871 _closeRecordDetails: function() 872 { 873 this._popoverHelper.hidePopover(); 874 }, 875 876 /** 877 * @param {?WebInspector.TimelineModel.Record} record 878 * @param {string=} regex 879 * @param {boolean=} selectRecord 880 */ 881 highlightSearchResult: function(record, regex, selectRecord) 882 { 883 if (this._highlightDomChanges) 884 WebInspector.revertDomChanges(this._highlightDomChanges); 885 this._highlightDomChanges = []; 886 887 var presentationRecord = this._presentationModel.toPresentationRecord(record); 888 if (!presentationRecord) 889 return; 890 891 if (selectRecord) 892 this._selectRecord(presentationRecord); 893 894 for (var element = this._sidebarListElement.firstChild; element; element = element.nextSibling) { 895 if (element.row._record === presentationRecord) { 896 element.row.highlight(regex, this._highlightDomChanges); 897 break; 898 } 899 } 900 }, 901 902 __proto__: WebInspector.HBox.prototype 903 } 904 905 /** 906 * @constructor 907 * @param {!WebInspector.TimelineModel} model 908 * @implements {WebInspector.TimelineGrid.Calculator} 909 */ 910 WebInspector.TimelineCalculator = function(model) 911 { 912 this._model = model; 913 } 914 915 WebInspector.TimelineCalculator._minWidth = 5; 916 917 WebInspector.TimelineCalculator.prototype = { 918 /** 919 * @return {number} 920 */ 921 paddingLeft: function() 922 { 923 return this._paddingLeft; 924 }, 925 926 /** 927 * @param {number} time 928 * @return {number} 929 */ 930 computePosition: function(time) 931 { 932 return (time - this._minimumBoundary) / this.boundarySpan() * this._workingArea + this._paddingLeft; 933 }, 934 935 /** 936 * @param {!WebInspector.TimelinePresentationModel.Record} record 937 * @return {!{start: number, end: number, cpuWidth: number}} 938 */ 939 computeBarGraphPercentages: function(record) 940 { 941 var start = (record.startTime() - this._minimumBoundary) / this.boundarySpan() * 100; 942 var end = (record.startTime() + record.selfTime() - this._minimumBoundary) / this.boundarySpan() * 100; 943 var cpuWidth = (record.endTime() - record.startTime()) / this.boundarySpan() * 100; 944 return {start: start, end: end, cpuWidth: cpuWidth}; 945 }, 946 947 /** 948 * @param {!WebInspector.TimelinePresentationModel.Record} record 949 * @return {!{left: number, width: number, cpuWidth: number}} 950 */ 951 computeBarGraphWindowPosition: function(record) 952 { 953 var percentages = this.computeBarGraphPercentages(record); 954 var widthAdjustment = 0; 955 956 var left = this.computePosition(record.startTime()); 957 var width = (percentages.end - percentages.start) / 100 * this._workingArea; 958 if (width < WebInspector.TimelineCalculator._minWidth) { 959 widthAdjustment = WebInspector.TimelineCalculator._minWidth - width; 960 width = WebInspector.TimelineCalculator._minWidth; 961 } 962 var cpuWidth = percentages.cpuWidth / 100 * this._workingArea + widthAdjustment; 963 return {left: left, width: width, cpuWidth: cpuWidth}; 964 }, 965 966 setWindow: function(minimumBoundary, maximumBoundary) 967 { 968 this._minimumBoundary = minimumBoundary; 969 this._maximumBoundary = maximumBoundary; 970 }, 971 972 /** 973 * @param {number} paddingLeft 974 * @param {number} clientWidth 975 */ 976 setDisplayWindow: function(paddingLeft, clientWidth) 977 { 978 this._workingArea = clientWidth - WebInspector.TimelineCalculator._minWidth - paddingLeft; 979 this._paddingLeft = paddingLeft; 980 }, 981 982 /** 983 * @param {number} value 984 * @param {number=} precision 985 * @return {string} 986 */ 987 formatTime: function(value, precision) 988 { 989 return Number.preciseMillisToString(value - this.zeroTime(), precision); 990 }, 991 992 /** 993 * @return {number} 994 */ 995 maximumBoundary: function() 996 { 997 return this._maximumBoundary; 998 }, 999 1000 /** 1001 * @return {number} 1002 */ 1003 minimumBoundary: function() 1004 { 1005 return this._minimumBoundary; 1006 }, 1007 1008 /** 1009 * @return {number} 1010 */ 1011 zeroTime: function() 1012 { 1013 return this._model.minimumRecordTime(); 1014 }, 1015 1016 /** 1017 * @return {number} 1018 */ 1019 boundarySpan: function() 1020 { 1021 return this._maximumBoundary - this._minimumBoundary; 1022 } 1023 } 1024 1025 /** 1026 * @constructor 1027 * @param {!WebInspector.Linkifier} linkifier 1028 * @param {function(!WebInspector.TimelinePresentationModel.Record)} selectRecord 1029 * @param {function()} scheduleRefresh 1030 */ 1031 WebInspector.TimelineRecordListRow = function(linkifier, selectRecord, scheduleRefresh) 1032 { 1033 this.element = document.createElement("div"); 1034 this.element.row = this; 1035 this.element.style.cursor = "pointer"; 1036 this.element.addEventListener("click", this._onClick.bind(this), false); 1037 this.element.addEventListener("mouseover", this._onMouseOver.bind(this), false); 1038 this.element.addEventListener("mouseout", this._onMouseOut.bind(this), false); 1039 this._linkifier = linkifier; 1040 1041 // Warning is float right block, it goes first. 1042 this._warningElement = this.element.createChild("div", "timeline-tree-item-warning hidden"); 1043 1044 this._expandArrowElement = this.element.createChild("div", "timeline-tree-item-expand-arrow"); 1045 this._expandArrowElement.addEventListener("click", this._onExpandClick.bind(this), false); 1046 var iconElement = this.element.createChild("span", "timeline-tree-icon"); 1047 this._typeElement = this.element.createChild("span", "type"); 1048 1049 this._dataElement = this.element.createChild("span", "data dimmed"); 1050 this._scheduleRefresh = scheduleRefresh; 1051 this._selectRecord = selectRecord; 1052 } 1053 1054 WebInspector.TimelineRecordListRow.prototype = { 1055 /** 1056 * @param {!WebInspector.TimelinePresentationModel.Record} presentationRecord 1057 * @param {number} offset 1058 * @param {!WebInspector.TimelineUIUtils} uiUtils 1059 */ 1060 update: function(presentationRecord, offset, uiUtils) 1061 { 1062 this._record = presentationRecord; 1063 var record = presentationRecord.record(); 1064 this._offset = offset; 1065 1066 this.element.className = "timeline-tree-item timeline-category-" + uiUtils.categoryForRecord(record).name; 1067 var paddingLeft = 5; 1068 var step = -3; 1069 for (var currentRecord = presentationRecord.presentationParent() ? presentationRecord.presentationParent().presentationParent() : null; currentRecord; currentRecord = currentRecord.presentationParent()) 1070 paddingLeft += 12 / (Math.max(1, step++)); 1071 this.element.style.paddingLeft = paddingLeft + "px"; 1072 if (record.thread() !== WebInspector.TimelineModel.MainThreadName) 1073 this.element.classList.add("background"); 1074 1075 this._typeElement.textContent = uiUtils.titleForRecord(record); 1076 1077 if (this._dataElement.firstChild) 1078 this._dataElement.removeChildren(); 1079 1080 this._warningElement.classList.toggle("hidden", !presentationRecord.hasWarnings() && !presentationRecord.childHasWarnings()); 1081 this._warningElement.classList.toggle("timeline-tree-item-child-warning", presentationRecord.childHasWarnings() && !presentationRecord.hasWarnings()); 1082 1083 if (presentationRecord.coalesced()) { 1084 this._dataElement.createTextChild(WebInspector.UIString(" %d", presentationRecord.presentationChildren().length)); 1085 } else { 1086 var detailsNode = uiUtils.buildDetailsNode(record, this._linkifier); 1087 if (detailsNode) { 1088 this._dataElement.createTextChild("("); 1089 this._dataElement.appendChild(detailsNode); 1090 this._dataElement.createTextChild(")"); 1091 } 1092 } 1093 1094 this._expandArrowElement.classList.toggle("parent", presentationRecord.expandable()); 1095 this._expandArrowElement.classList.toggle("expanded", !!presentationRecord.visibleChildrenCount()); 1096 this._record.setListRow(this); 1097 }, 1098 1099 highlight: function(regExp, domChanges) 1100 { 1101 var matchInfo = this.element.textContent.match(regExp); 1102 if (matchInfo) 1103 WebInspector.highlightSearchResult(this.element, matchInfo.index, matchInfo[0].length, domChanges); 1104 }, 1105 1106 dispose: function() 1107 { 1108 this.element.remove(); 1109 }, 1110 1111 /** 1112 * @param {!Event} event 1113 */ 1114 _onExpandClick: function(event) 1115 { 1116 this._record.setCollapsed(!this._record.collapsed()); 1117 this._scheduleRefresh(); 1118 event.consume(true); 1119 }, 1120 1121 /** 1122 * @param {!Event} event 1123 */ 1124 _onClick: function(event) 1125 { 1126 this._selectRecord(this._record); 1127 }, 1128 1129 /** 1130 * @param {boolean} selected 1131 */ 1132 renderAsSelected: function(selected) 1133 { 1134 this.element.classList.toggle("selected", selected); 1135 }, 1136 1137 /** 1138 * @param {!Event} event 1139 */ 1140 _onMouseOver: function(event) 1141 { 1142 this.element.classList.add("hovered"); 1143 if (this._record.graphRow()) 1144 this._record.graphRow().element.classList.add("hovered"); 1145 }, 1146 1147 /** 1148 * @param {!Event} event 1149 */ 1150 _onMouseOut: function(event) 1151 { 1152 this.element.classList.remove("hovered"); 1153 if (this._record.graphRow()) 1154 this._record.graphRow().element.classList.remove("hovered"); 1155 } 1156 } 1157 1158 /** 1159 * @constructor 1160 * @param {!Element} graphContainer 1161 * @param {function(!WebInspector.TimelinePresentationModel.Record)} selectRecord 1162 * @param {function()} scheduleRefresh 1163 */ 1164 WebInspector.TimelineRecordGraphRow = function(graphContainer, selectRecord, scheduleRefresh) 1165 { 1166 this.element = document.createElement("div"); 1167 this.element.row = this; 1168 this.element.addEventListener("mouseover", this._onMouseOver.bind(this), false); 1169 this.element.addEventListener("mouseout", this._onMouseOut.bind(this), false); 1170 this.element.addEventListener("click", this._onClick.bind(this), false); 1171 1172 this._barAreaElement = this.element.createChild("div", "timeline-graph-bar-area"); 1173 1174 this._barCpuElement = this._barAreaElement.createChild("div", "timeline-graph-bar cpu"); 1175 this._barCpuElement.row = this; 1176 1177 this._barElement = this._barAreaElement.createChild("div", "timeline-graph-bar"); 1178 this._barElement.row = this; 1179 1180 this._expandElement = new WebInspector.TimelineExpandableElement(graphContainer); 1181 1182 this._selectRecord = selectRecord; 1183 this._scheduleRefresh = scheduleRefresh; 1184 } 1185 1186 WebInspector.TimelineRecordGraphRow.prototype = { 1187 /** 1188 * @param {!WebInspector.TimelinePresentationModel.Record} presentationRecord 1189 * @param {!WebInspector.TimelineCalculator} calculator 1190 * @param {number} expandOffset 1191 * @param {number} index 1192 * @param {!WebInspector.TimelineUIUtils} uiUtils 1193 */ 1194 update: function(presentationRecord, calculator, expandOffset, index, uiUtils) 1195 { 1196 this._record = presentationRecord; 1197 var record = presentationRecord.record(); 1198 this.element.className = "timeline-graph-side timeline-category-" + uiUtils.categoryForRecord(record).name; 1199 if (record.thread() !== WebInspector.TimelineModel.MainThreadName) 1200 this.element.classList.add("background"); 1201 1202 var barPosition = calculator.computeBarGraphWindowPosition(presentationRecord); 1203 this._barElement.style.left = barPosition.left + "px"; 1204 this._barElement.style.width = barPosition.width + "px"; 1205 this._barCpuElement.style.left = barPosition.left + "px"; 1206 this._barCpuElement.style.width = barPosition.cpuWidth + "px"; 1207 this._expandElement._update(presentationRecord, index, barPosition.left - expandOffset, barPosition.width); 1208 this._record.setGraphRow(this); 1209 }, 1210 1211 /** 1212 * @param {!Event} event 1213 */ 1214 _onClick: function(event) 1215 { 1216 // check if we click arrow and expand if yes. 1217 if (this._expandElement._arrow.containsEventPoint(event)) 1218 this._expand(); 1219 this._selectRecord(this._record); 1220 }, 1221 1222 /** 1223 * @param {boolean} selected 1224 */ 1225 renderAsSelected: function(selected) 1226 { 1227 this.element.classList.toggle("selected", selected); 1228 }, 1229 1230 _expand: function() 1231 { 1232 this._record.setCollapsed(!this._record.collapsed()); 1233 this._scheduleRefresh(); 1234 }, 1235 1236 /** 1237 * @param {!Event} event 1238 */ 1239 _onMouseOver: function(event) 1240 { 1241 this.element.classList.add("hovered"); 1242 if (this._record.listRow()) 1243 this._record.listRow().element.classList.add("hovered"); 1244 }, 1245 1246 /** 1247 * @param {!Event} event 1248 */ 1249 _onMouseOut: function(event) 1250 { 1251 this.element.classList.remove("hovered"); 1252 if (this._record.listRow()) 1253 this._record.listRow().element.classList.remove("hovered"); 1254 }, 1255 1256 dispose: function() 1257 { 1258 this.element.remove(); 1259 this._expandElement._dispose(); 1260 } 1261 } 1262 1263 /** 1264 * @constructor 1265 */ 1266 WebInspector.TimelineExpandableElement = function(container) 1267 { 1268 this._element = container.createChild("div", "timeline-expandable"); 1269 this._element.createChild("div", "timeline-expandable-left"); 1270 this._arrow = this._element.createChild("div", "timeline-expandable-arrow"); 1271 } 1272 1273 WebInspector.TimelineExpandableElement.prototype = { 1274 /** 1275 * @param {!WebInspector.TimelinePresentationModel.Record} record 1276 * @param {number} index 1277 * @param {number} left 1278 * @param {number} width 1279 */ 1280 _update: function(record, index, left, width) 1281 { 1282 const rowHeight = WebInspector.TimelinePanel.rowHeight; 1283 if (record.visibleChildrenCount() || record.expandable()) { 1284 this._element.style.top = index * rowHeight + "px"; 1285 this._element.style.left = left + "px"; 1286 this._element.style.width = Math.max(12, width + 25) + "px"; 1287 if (!record.collapsed()) { 1288 this._element.style.height = (record.visibleChildrenCount() + 1) * rowHeight + "px"; 1289 this._element.classList.add("timeline-expandable-expanded"); 1290 this._element.classList.remove("timeline-expandable-collapsed"); 1291 } else { 1292 this._element.style.height = rowHeight + "px"; 1293 this._element.classList.add("timeline-expandable-collapsed"); 1294 this._element.classList.remove("timeline-expandable-expanded"); 1295 } 1296 this._element.classList.remove("hidden"); 1297 } else { 1298 this._element.classList.add("hidden"); 1299 } 1300 }, 1301 1302 _dispose: function() 1303 { 1304 this._element.remove(); 1305 } 1306 } 1307