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 (WebInspector.experimentsSettings.gpuTimeline.isEnabled()) 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.createElement("div"); 154 this._frameContainer.classList.add("fill"); 155 this._frameContainer.classList.add("timeline-frame-container"); 156 this._frameContainer.style.height = WebInspector.TimelinePanel.rowHeight + frameContainerBorderWidth + "px"; 157 this._frameContainer.addEventListener("dblclick", this._onFrameDoubleClicked.bind(this), false); 158 this._frameContainer.addEventListener("click", this._onFrameClicked.bind(this), false); 159 } 160 this._frameStripByFrame.clear(); 161 162 var dividers = []; 163 164 for (var i = 0; i < frames.length; ++i) { 165 var frame = frames[i]; 166 var frameStart = this._calculator.computePosition(frame.startTime); 167 var frameEnd = this._calculator.computePosition(frame.endTime); 168 169 var frameStrip = document.createElement("div"); 170 frameStrip.className = "timeline-frame-strip"; 171 var actualStart = Math.max(frameStart, 0); 172 var width = frameEnd - actualStart; 173 frameStrip.style.left = actualStart + "px"; 174 frameStrip.style.width = width + "px"; 175 frameStrip._frame = frame; 176 this._frameStripByFrame.put(frame, frameStrip); 177 178 const minWidthForFrameInfo = 60; 179 if (width > minWidthForFrameInfo) 180 frameStrip.textContent = Number.millisToString(frame.endTime - frame.startTime, true); 181 182 this._frameContainer.appendChild(frameStrip); 183 184 if (actualStart > 0) { 185 var frameMarker = this._uiUtils.createBeginFrameDivider(); 186 frameMarker.style.left = frameStart + "px"; 187 dividers.push(frameMarker); 188 } 189 } 190 this._timelineGrid.addEventDividers(dividers); 191 this._headerElement.appendChild(this._frameContainer); 192 }, 193 194 _onFrameDoubleClicked: function(event) 195 { 196 var frameBar = event.target.enclosingNodeOrSelfWithClass("timeline-frame-strip"); 197 if (!frameBar) 198 return; 199 this._delegate.requestWindowTimes(frameBar._frame.startTime, frameBar._frame.endTime); 200 }, 201 202 _onFrameClicked: function(event) 203 { 204 var frameBar = event.target.enclosingNodeOrSelfWithClass("timeline-frame-strip"); 205 if (!frameBar) 206 return; 207 this._delegate.select(WebInspector.TimelineSelection.fromFrame(frameBar._frame)); 208 }, 209 210 /** 211 * @param {!WebInspector.TimelineModel.Record} record 212 */ 213 addRecord: function(record) 214 { 215 this._presentationModel.addRecord(record); 216 this._invalidateAndScheduleRefresh(false, false); 217 }, 218 219 /** 220 * @param {number} width 221 */ 222 setSidebarSize: function(width) 223 { 224 this._recordsView.setSidebarSize(width); 225 }, 226 227 /** 228 * @param {!WebInspector.Event} event 229 */ 230 _sidebarResized: function(event) 231 { 232 this.dispatchEventToListeners(WebInspector.SplitView.Events.SidebarSizeChanged, event.data); 233 }, 234 235 _onViewportResize: function() 236 { 237 this._resize(this._recordsView.sidebarSize()); 238 }, 239 240 /** 241 * @param {number} sidebarWidth 242 */ 243 _resize: function(sidebarWidth) 244 { 245 this._closeRecordDetails(); 246 this._graphRowsElementWidth = this._graphRowsElement.offsetWidth; 247 this._headerElement.style.left = sidebarWidth + "px"; 248 this._headerElement.style.width = this._itemsGraphsElement.offsetWidth + "px"; 249 this._scheduleRefresh(false, true); 250 }, 251 252 _resetView: function() 253 { 254 this._windowStartTime = 0; 255 this._windowEndTime = 0; 256 this._boundariesAreValid = false; 257 this._adjustScrollPosition(0); 258 this._linkifier.reset(); 259 this._closeRecordDetails(); 260 this._automaticallySizeWindow = true; 261 this._presentationModel.reset(); 262 }, 263 264 265 /** 266 * @return {!WebInspector.View} 267 */ 268 view: function() 269 { 270 return this; 271 }, 272 273 dispose: function() 274 { 275 }, 276 277 reset: function() 278 { 279 this._resetView(); 280 this._invalidateAndScheduleRefresh(true, true); 281 }, 282 283 /** 284 * @return {!Array.<!Element>} 285 */ 286 elementsToRestoreScrollPositionsFor: function() 287 { 288 return [this._containerElement]; 289 }, 290 291 /** 292 * @param {?RegExp} textFilter 293 */ 294 refreshRecords: function(textFilter) 295 { 296 this._presentationModel.reset(); 297 var records = this._model.records(); 298 for (var i = 0; i < records.length; ++i) 299 this.addRecord(records[i]); 300 this._automaticallySizeWindow = false; 301 this._presentationModel.setTextFilter(textFilter); 302 this._invalidateAndScheduleRefresh(false, true); 303 }, 304 305 willHide: function() 306 { 307 this._closeRecordDetails(); 308 WebInspector.View.prototype.willHide.call(this); 309 }, 310 311 _onScroll: function(event) 312 { 313 this._closeRecordDetails(); 314 this._scrollTop = this._containerElement.scrollTop; 315 var dividersTop = Math.max(0, this._scrollTop); 316 this._timelineGrid.setScrollAndDividerTop(this._scrollTop, dividersTop); 317 this._scheduleRefresh(true, true); 318 }, 319 320 /** 321 * @param {boolean} preserveBoundaries 322 * @param {boolean} userGesture 323 */ 324 _invalidateAndScheduleRefresh: function(preserveBoundaries, userGesture) 325 { 326 this._presentationModel.invalidateFilteredRecords(); 327 this._scheduleRefresh(preserveBoundaries, userGesture); 328 }, 329 330 _clearSelection: function() 331 { 332 this._delegate.select(null); 333 }, 334 335 /** 336 * @param {?WebInspector.TimelinePresentationModel.Record} presentationRecord 337 */ 338 _selectRecord: function(presentationRecord) 339 { 340 if (presentationRecord.coalesced()) { 341 // Presentation record does not have model record to highlight. 342 this._innerSetSelectedRecord(presentationRecord); 343 var aggregatedStats = {}; 344 var presentationChildren = presentationRecord.presentationChildren(); 345 for (var i = 0; i < presentationChildren.length; ++i) 346 WebInspector.TimelineUIUtils.aggregateTimeByCategory(aggregatedStats, presentationChildren[i].record().aggregatedStats()); 347 var idle = presentationRecord.record().endTime() - presentationRecord.record().startTime(); 348 for (var category in aggregatedStats) 349 idle -= aggregatedStats[category]; 350 aggregatedStats["idle"] = idle; 351 var pieChart = WebInspector.TimelineUIUtils.generatePieChart(aggregatedStats); 352 this._delegate.showInDetails(WebInspector.TimelineUIUtils.recordStyle(presentationRecord.record()).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._model.loadedFromFile(), this._uiUtils); 586 graphRowElement.row.update(record, this._calculator, this._expandOffset, i); 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 (WebInspector.experimentsSettings.gpuTimeline.isEnabled()) 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 = 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 _getPopoverAnchor: function(element) 714 { 715 var anchor = element.enclosingNodeOrSelfWithClass("timeline-graph-bar"); 716 if (anchor && anchor._tasksInfo) 717 return anchor; 718 return null; 719 }, 720 721 _mouseOut: function() 722 { 723 this._hideQuadHighlight(); 724 }, 725 726 /** 727 * @param {?Event} e 728 */ 729 _mouseMove: function(e) 730 { 731 var rowElement = e.target.enclosingNodeOrSelfWithClass("timeline-tree-item"); 732 if (!this._highlightQuad(rowElement)) 733 this._hideQuadHighlight(); 734 735 var taskBarElement = e.target.enclosingNodeOrSelfWithClass("timeline-graph-bar"); 736 if (taskBarElement && taskBarElement._tasksInfo) { 737 var offset = taskBarElement.offsetLeft; 738 this._timelineGrid.showCurtains(offset >= 0 ? offset : 0, taskBarElement.offsetWidth); 739 } else 740 this._timelineGrid.hideCurtains(); 741 }, 742 743 /** 744 * @param {?Event} event 745 */ 746 _keyDown: function(event) 747 { 748 if (!this._lastSelectedRecord || event.shiftKey || event.metaKey || event.ctrlKey) 749 return; 750 751 var record = this._lastSelectedRecord; 752 var recordsInWindow = this._presentationModel.filteredRecords(); 753 var index = recordsInWindow.indexOf(record); 754 var recordsInPage = Math.floor(this._containerElementHeight / WebInspector.TimelinePanel.rowHeight); 755 var rowHeight = WebInspector.TimelinePanel.rowHeight; 756 757 if (index === -1) 758 index = 0; 759 760 switch (event.keyIdentifier) { 761 case "Left": 762 if (record.presentationParent()) { 763 if ((!record.expandable() || record.collapsed()) && record.presentationParent() !== this._presentationModel.rootRecord()) { 764 this._selectRecord(record.presentationParent()); 765 } else { 766 record.setCollapsed(true); 767 this._invalidateAndScheduleRefresh(true, true); 768 } 769 } 770 event.consume(true); 771 break; 772 case "Up": 773 if (--index < 0) 774 break; 775 this._selectRecord(recordsInWindow[index]); 776 event.consume(true); 777 break; 778 case "Right": 779 if (record.expandable() && record.collapsed()) { 780 record.setCollapsed(false); 781 this._invalidateAndScheduleRefresh(true, true); 782 } else { 783 if (++index >= recordsInWindow.length) 784 break; 785 this._selectRecord(recordsInWindow[index]); 786 } 787 event.consume(true); 788 break; 789 case "Down": 790 if (++index >= recordsInWindow.length) 791 break; 792 this._selectRecord(recordsInWindow[index]); 793 event.consume(true); 794 break; 795 case "PageUp": 796 index = Math.max(0, index - recordsInPage); 797 this._scrollTop = Math.max(0, this._scrollTop - recordsInPage * rowHeight); 798 this._containerElement.scrollTop = this._scrollTop; 799 this._selectRecord(recordsInWindow[index]); 800 event.consume(true); 801 break; 802 case "PageDown": 803 index = Math.min(recordsInWindow.length - 1, index + recordsInPage); 804 this._scrollTop = Math.min(this._containerElement.scrollHeight - this._containerElementHeight, this._scrollTop + recordsInPage * rowHeight); 805 this._containerElement.scrollTop = this._scrollTop; 806 this._selectRecord(recordsInWindow[index]); 807 event.consume(true); 808 break; 809 case "Home": 810 index = 0; 811 this._selectRecord(recordsInWindow[index]); 812 event.consume(true); 813 break; 814 case "End": 815 index = recordsInWindow.length - 1; 816 this._selectRecord(recordsInWindow[index]); 817 event.consume(true); 818 break; 819 } 820 }, 821 822 /** 823 * @param {?Element} rowElement 824 * @return {boolean} 825 */ 826 _highlightQuad: function(rowElement) 827 { 828 if (!rowElement || !rowElement.row) 829 return false; 830 var presentationRecord = rowElement.row._record; 831 if (presentationRecord.coalesced()) 832 return false; 833 var record = presentationRecord.record(); 834 if (this._highlightedQuadRecord === record) 835 return true; 836 this._highlightedQuadRecord = record; 837 838 var quad = this._uiUtils.highlightQuadForRecord(record); 839 if (!quad) 840 return false; 841 record.target().domAgent().highlightQuad(quad, WebInspector.Color.PageHighlight.Content.toProtocolRGBA(), WebInspector.Color.PageHighlight.ContentOutline.toProtocolRGBA()); 842 return true; 843 }, 844 845 _hideQuadHighlight: function() 846 { 847 if (this._highlightedQuadRecord) { 848 this._highlightedQuadRecord.target().domAgent().hideHighlight(); 849 delete this._highlightedQuadRecord; 850 } 851 }, 852 853 /** 854 * @param {!Element} anchor 855 * @param {!WebInspector.Popover} popover 856 */ 857 _showPopover: function(anchor, popover) 858 { 859 if (!anchor._tasksInfo) 860 return; 861 popover.show(WebInspector.TimelineUIUtils.generateMainThreadBarPopupContent(this._model, anchor._tasksInfo), anchor, null, null, WebInspector.Popover.Orientation.Bottom); 862 }, 863 864 _closeRecordDetails: function() 865 { 866 this._popoverHelper.hidePopover(); 867 }, 868 869 /** 870 * @param {?WebInspector.TimelineModel.Record} record 871 * @param {string=} regex 872 * @param {boolean=} selectRecord 873 */ 874 highlightSearchResult: function(record, regex, selectRecord) 875 { 876 if (this._highlightDomChanges) 877 WebInspector.revertDomChanges(this._highlightDomChanges); 878 this._highlightDomChanges = []; 879 880 var presentationRecord = this._presentationModel.toPresentationRecord(record); 881 if (!presentationRecord) 882 return; 883 884 if (selectRecord) 885 this._selectRecord(presentationRecord); 886 887 for (var element = this._sidebarListElement.firstChild; element; element = element.nextSibling) { 888 if (element.row._record === presentationRecord) { 889 element.row.highlight(regex, this._highlightDomChanges); 890 break; 891 } 892 } 893 }, 894 895 __proto__: WebInspector.HBox.prototype 896 } 897 898 /** 899 * @constructor 900 * @param {!WebInspector.TimelineModel} model 901 * @implements {WebInspector.TimelineGrid.Calculator} 902 */ 903 WebInspector.TimelineCalculator = function(model) 904 { 905 this._model = model; 906 } 907 908 WebInspector.TimelineCalculator._minWidth = 5; 909 910 WebInspector.TimelineCalculator.prototype = { 911 /** 912 * @return {number} 913 */ 914 paddingLeft: function() 915 { 916 return this._paddingLeft; 917 }, 918 919 /** 920 * @param {number} time 921 * @return {number} 922 */ 923 computePosition: function(time) 924 { 925 return (time - this._minimumBoundary) / this.boundarySpan() * this._workingArea + this._paddingLeft; 926 }, 927 928 /** 929 * @param {!WebInspector.TimelinePresentationModel.Record} record 930 * @return {!{start: number, end: number, cpuWidth: number}} 931 */ 932 computeBarGraphPercentages: function(record) 933 { 934 var start = (record.startTime() - this._minimumBoundary) / this.boundarySpan() * 100; 935 var end = (record.startTime() + record.selfTime() - this._minimumBoundary) / this.boundarySpan() * 100; 936 var cpuWidth = (record.endTime() - record.startTime()) / this.boundarySpan() * 100; 937 return {start: start, end: end, cpuWidth: cpuWidth}; 938 }, 939 940 /** 941 * @param {!WebInspector.TimelinePresentationModel.Record} record 942 * @return {!{left: number, width: number, cpuWidth: number}} 943 */ 944 computeBarGraphWindowPosition: function(record) 945 { 946 var percentages = this.computeBarGraphPercentages(record); 947 var widthAdjustment = 0; 948 949 var left = this.computePosition(record.startTime()); 950 var width = (percentages.end - percentages.start) / 100 * this._workingArea; 951 if (width < WebInspector.TimelineCalculator._minWidth) { 952 widthAdjustment = WebInspector.TimelineCalculator._minWidth - width; 953 width = WebInspector.TimelineCalculator._minWidth; 954 } 955 var cpuWidth = percentages.cpuWidth / 100 * this._workingArea + widthAdjustment; 956 return {left: left, width: width, cpuWidth: cpuWidth}; 957 }, 958 959 setWindow: function(minimumBoundary, maximumBoundary) 960 { 961 this._minimumBoundary = minimumBoundary; 962 this._maximumBoundary = maximumBoundary; 963 }, 964 965 /** 966 * @param {number} paddingLeft 967 * @param {number} clientWidth 968 */ 969 setDisplayWindow: function(paddingLeft, clientWidth) 970 { 971 this._workingArea = clientWidth - WebInspector.TimelineCalculator._minWidth - paddingLeft; 972 this._paddingLeft = paddingLeft; 973 }, 974 975 /** 976 * @param {number} value 977 * @param {number=} precision 978 * @return {string} 979 */ 980 formatTime: function(value, precision) 981 { 982 return Number.preciseMillisToString(value - this.zeroTime(), precision); 983 }, 984 985 /** 986 * @return {number} 987 */ 988 maximumBoundary: function() 989 { 990 return this._maximumBoundary; 991 }, 992 993 /** 994 * @return {number} 995 */ 996 minimumBoundary: function() 997 { 998 return this._minimumBoundary; 999 }, 1000 1001 /** 1002 * @return {number} 1003 */ 1004 zeroTime: function() 1005 { 1006 return this._model.minimumRecordTime(); 1007 }, 1008 1009 /** 1010 * @return {number} 1011 */ 1012 boundarySpan: function() 1013 { 1014 return this._maximumBoundary - this._minimumBoundary; 1015 } 1016 } 1017 1018 /** 1019 * @constructor 1020 * @param {!WebInspector.Linkifier} linkifier 1021 * @param {function(!WebInspector.TimelinePresentationModel.Record)} selectRecord 1022 * @param {function()} scheduleRefresh 1023 */ 1024 WebInspector.TimelineRecordListRow = function(linkifier, selectRecord, scheduleRefresh) 1025 { 1026 this.element = document.createElement("div"); 1027 this.element.row = this; 1028 this.element.style.cursor = "pointer"; 1029 this.element.addEventListener("click", this._onClick.bind(this), false); 1030 this.element.addEventListener("mouseover", this._onMouseOver.bind(this), false); 1031 this.element.addEventListener("mouseout", this._onMouseOut.bind(this), false); 1032 this._linkifier = linkifier; 1033 1034 // Warning is float right block, it goes first. 1035 this._warningElement = this.element.createChild("div", "timeline-tree-item-warning hidden"); 1036 1037 this._expandArrowElement = this.element.createChild("div", "timeline-tree-item-expand-arrow"); 1038 this._expandArrowElement.addEventListener("click", this._onExpandClick.bind(this), false); 1039 var iconElement = this.element.createChild("span", "timeline-tree-icon"); 1040 this._typeElement = this.element.createChild("span", "type"); 1041 1042 this._dataElement = this.element.createChild("span", "data dimmed"); 1043 this._scheduleRefresh = scheduleRefresh; 1044 this._selectRecord = selectRecord; 1045 } 1046 1047 WebInspector.TimelineRecordListRow.prototype = { 1048 /** 1049 * @param {!WebInspector.TimelinePresentationModel.Record} presentationRecord 1050 * @param {number} offset 1051 * @param {boolean} loadedFromFile 1052 * @param {!WebInspector.TimelineUIUtils} uiUtils 1053 */ 1054 update: function(presentationRecord, offset, loadedFromFile, uiUtils) 1055 { 1056 this._record = presentationRecord; 1057 var record = presentationRecord.record(); 1058 this._offset = offset; 1059 1060 this.element.className = "timeline-tree-item timeline-category-" + record.category().name; 1061 var paddingLeft = 5; 1062 var step = -3; 1063 for (var currentRecord = presentationRecord.presentationParent() ? presentationRecord.presentationParent().presentationParent() : null; currentRecord; currentRecord = currentRecord.presentationParent()) 1064 paddingLeft += 12 / (Math.max(1, step++)); 1065 this.element.style.paddingLeft = paddingLeft + "px"; 1066 if (record.thread()) 1067 this.element.classList.add("background"); 1068 1069 this._typeElement.textContent = uiUtils.titleForRecord(record); 1070 1071 if (this._dataElement.firstChild) 1072 this._dataElement.removeChildren(); 1073 1074 this._warningElement.classList.toggle("hidden", !presentationRecord.hasWarnings() && !presentationRecord.childHasWarnings()); 1075 this._warningElement.classList.toggle("timeline-tree-item-child-warning", presentationRecord.childHasWarnings() && !presentationRecord.hasWarnings()); 1076 1077 if (presentationRecord.coalesced()) { 1078 this._dataElement.createTextChild(WebInspector.UIString(" %d", presentationRecord.presentationChildren().length)); 1079 } else { 1080 var detailsNode = uiUtils.buildDetailsNode(record, this._linkifier, loadedFromFile); 1081 if (detailsNode) { 1082 this._dataElement.appendChild(document.createTextNode("(")); 1083 this._dataElement.appendChild(detailsNode); 1084 this._dataElement.appendChild(document.createTextNode(")")); 1085 } 1086 } 1087 1088 this._expandArrowElement.classList.toggle("parent", presentationRecord.expandable()); 1089 this._expandArrowElement.classList.toggle("expanded", !!presentationRecord.visibleChildrenCount()); 1090 this._record.setListRow(this); 1091 }, 1092 1093 highlight: function(regExp, domChanges) 1094 { 1095 var matchInfo = this.element.textContent.match(regExp); 1096 if (matchInfo) 1097 WebInspector.highlightSearchResult(this.element, matchInfo.index, matchInfo[0].length, domChanges); 1098 }, 1099 1100 dispose: function() 1101 { 1102 this.element.remove(); 1103 }, 1104 1105 /** 1106 * @param {?Event} event 1107 */ 1108 _onExpandClick: function(event) 1109 { 1110 this._record.setCollapsed(!this._record.collapsed()); 1111 this._scheduleRefresh(); 1112 event.consume(true); 1113 }, 1114 1115 /** 1116 * @param {?Event} event 1117 */ 1118 _onClick: function(event) 1119 { 1120 this._selectRecord(this._record); 1121 }, 1122 1123 /** 1124 * @param {boolean} selected 1125 */ 1126 renderAsSelected: function(selected) 1127 { 1128 this.element.classList.toggle("selected", selected); 1129 }, 1130 1131 /** 1132 * @param {?Event} event 1133 */ 1134 _onMouseOver: function(event) 1135 { 1136 this.element.classList.add("hovered"); 1137 if (this._record.graphRow()) 1138 this._record.graphRow().element.classList.add("hovered"); 1139 }, 1140 1141 /** 1142 * @param {?Event} event 1143 */ 1144 _onMouseOut: function(event) 1145 { 1146 this.element.classList.remove("hovered"); 1147 if (this._record.graphRow()) 1148 this._record.graphRow().element.classList.remove("hovered"); 1149 } 1150 } 1151 1152 /** 1153 * @constructor 1154 * @param {!Element} graphContainer 1155 * @param {function(!WebInspector.TimelinePresentationModel.Record)} selectRecord 1156 * @param {function()} scheduleRefresh 1157 */ 1158 WebInspector.TimelineRecordGraphRow = function(graphContainer, selectRecord, scheduleRefresh) 1159 { 1160 this.element = document.createElement("div"); 1161 this.element.row = this; 1162 this.element.addEventListener("mouseover", this._onMouseOver.bind(this), false); 1163 this.element.addEventListener("mouseout", this._onMouseOut.bind(this), false); 1164 this.element.addEventListener("click", this._onClick.bind(this), false); 1165 1166 this._barAreaElement = document.createElement("div"); 1167 this._barAreaElement.className = "timeline-graph-bar-area"; 1168 this.element.appendChild(this._barAreaElement); 1169 1170 this._barCpuElement = document.createElement("div"); 1171 this._barCpuElement.className = "timeline-graph-bar cpu" 1172 this._barCpuElement.row = this; 1173 this._barAreaElement.appendChild(this._barCpuElement); 1174 1175 this._barElement = document.createElement("div"); 1176 this._barElement.className = "timeline-graph-bar"; 1177 this._barElement.row = this; 1178 this._barAreaElement.appendChild(this._barElement); 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 */ 1193 update: function(presentationRecord, calculator, expandOffset, index) 1194 { 1195 this._record = presentationRecord; 1196 var record = presentationRecord.record(); 1197 this.element.className = "timeline-graph-side timeline-category-" + record.category().name; 1198 if (record.thread()) 1199 this.element.classList.add("background"); 1200 1201 var barPosition = calculator.computeBarGraphWindowPosition(presentationRecord); 1202 this._barElement.style.left = barPosition.left + "px"; 1203 this._barElement.style.width = barPosition.width + "px"; 1204 this._barCpuElement.style.left = barPosition.left + "px"; 1205 this._barCpuElement.style.width = barPosition.cpuWidth + "px"; 1206 this._expandElement._update(presentationRecord, index, barPosition.left - expandOffset, barPosition.width); 1207 this._record.setGraphRow(this); 1208 }, 1209 1210 /** 1211 * @param {?Event} event 1212 */ 1213 _onClick: function(event) 1214 { 1215 // check if we click arrow and expand if yes. 1216 if (this._expandElement._arrow.containsEventPoint(event)) 1217 this._expand(); 1218 this._selectRecord(this._record); 1219 }, 1220 1221 /** 1222 * @param {boolean} selected 1223 */ 1224 renderAsSelected: function(selected) 1225 { 1226 this.element.classList.toggle("selected", selected); 1227 }, 1228 1229 _expand: function() 1230 { 1231 this._record.setCollapsed(!this._record.collapsed()); 1232 this._scheduleRefresh(); 1233 }, 1234 1235 /** 1236 * @param {?Event} event 1237 */ 1238 _onMouseOver: function(event) 1239 { 1240 this.element.classList.add("hovered"); 1241 if (this._record.listRow()) 1242 this._record.listRow().element.classList.add("hovered"); 1243 }, 1244 1245 /** 1246 * @param {?Event} event 1247 */ 1248 _onMouseOut: function(event) 1249 { 1250 this.element.classList.remove("hovered"); 1251 if (this._record.listRow()) 1252 this._record.listRow().element.classList.remove("hovered"); 1253 }, 1254 1255 dispose: function() 1256 { 1257 this.element.remove(); 1258 this._expandElement._dispose(); 1259 } 1260 } 1261 1262 /** 1263 * @constructor 1264 */ 1265 WebInspector.TimelineExpandableElement = function(container) 1266 { 1267 this._element = container.createChild("div", "timeline-expandable"); 1268 this._element.createChild("div", "timeline-expandable-left"); 1269 this._arrow = this._element.createChild("div", "timeline-expandable-arrow"); 1270 } 1271 1272 WebInspector.TimelineExpandableElement.prototype = { 1273 /** 1274 * @param {!WebInspector.TimelinePresentationModel.Record} record 1275 * @param {number} index 1276 * @param {number} left 1277 * @param {number} width 1278 */ 1279 _update: function(record, index, left, width) 1280 { 1281 const rowHeight = WebInspector.TimelinePanel.rowHeight; 1282 if (record.visibleChildrenCount() || record.expandable()) { 1283 this._element.style.top = index * rowHeight + "px"; 1284 this._element.style.left = left + "px"; 1285 this._element.style.width = Math.max(12, width + 25) + "px"; 1286 if (!record.collapsed()) { 1287 this._element.style.height = (record.visibleChildrenCount() + 1) * rowHeight + "px"; 1288 this._element.classList.add("timeline-expandable-expanded"); 1289 this._element.classList.remove("timeline-expandable-collapsed"); 1290 } else { 1291 this._element.style.height = rowHeight + "px"; 1292 this._element.classList.add("timeline-expandable-collapsed"); 1293 this._element.classList.remove("timeline-expandable-expanded"); 1294 } 1295 this._element.classList.remove("hidden"); 1296 } else 1297 this._element.classList.add("hidden"); 1298 }, 1299 1300 _dispose: function() 1301 { 1302 this._element.remove(); 1303 } 1304 } 1305