1/* 2 * Copyright (C) 2009 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 31WebInspector.TimelinePanel = function() 32{ 33 WebInspector.Panel.call(this); 34 this.element.addStyleClass("timeline"); 35 36 this._overviewPane = new WebInspector.TimelineOverviewPane(this.categories); 37 this._overviewPane.addEventListener("window changed", this._windowChanged, this); 38 this._overviewPane.addEventListener("filter changed", this._refresh, this); 39 this.element.appendChild(this._overviewPane.element); 40 41 this._sidebarBackgroundElement = document.createElement("div"); 42 this._sidebarBackgroundElement.className = "sidebar timeline-sidebar-background"; 43 this.element.appendChild(this._sidebarBackgroundElement); 44 45 this._containerElement = document.createElement("div"); 46 this._containerElement.id = "timeline-container"; 47 this._containerElement.addEventListener("scroll", this._onScroll.bind(this), false); 48 this.element.appendChild(this._containerElement); 49 50 this.createSidebar(this._containerElement, this._containerElement); 51 var itemsTreeElement = new WebInspector.SidebarSectionTreeElement(WebInspector.UIString("RECORDS"), {}, true); 52 itemsTreeElement.expanded = true; 53 this.sidebarTree.appendChild(itemsTreeElement); 54 55 this._sidebarListElement = document.createElement("div"); 56 this.sidebarElement.appendChild(this._sidebarListElement); 57 58 this._containerContentElement = document.createElement("div"); 59 this._containerContentElement.id = "resources-container-content"; 60 this._containerElement.appendChild(this._containerContentElement); 61 62 this._timelineGrid = new WebInspector.TimelineGrid(); 63 this._itemsGraphsElement = this._timelineGrid.itemsGraphsElement; 64 this._itemsGraphsElement.id = "timeline-graphs"; 65 this._containerContentElement.appendChild(this._timelineGrid.element); 66 67 this._topGapElement = document.createElement("div"); 68 this._topGapElement.className = "timeline-gap"; 69 this._itemsGraphsElement.appendChild(this._topGapElement); 70 71 this._graphRowsElement = document.createElement("div"); 72 this._itemsGraphsElement.appendChild(this._graphRowsElement); 73 74 this._bottomGapElement = document.createElement("div"); 75 this._bottomGapElement.className = "timeline-gap"; 76 this._itemsGraphsElement.appendChild(this._bottomGapElement); 77 78 this._createStatusbarButtons(); 79 80 this._records = []; 81 this._sendRequestRecords = {}; 82 this._calculator = new WebInspector.TimelineCalculator(); 83 this._boundariesAreValid = true; 84} 85 86WebInspector.TimelinePanel.prototype = { 87 toolbarItemClass: "timeline", 88 89 get toolbarItemLabel() 90 { 91 return WebInspector.UIString("Timeline"); 92 }, 93 94 get statusBarItems() 95 { 96 return [this.toggleTimelineButton.element, this.clearButton.element]; 97 }, 98 99 get categories() 100 { 101 if (!this._categories) { 102 this._categories = { 103 loading: new WebInspector.TimelineCategory("loading", WebInspector.UIString("Loading"), "rgb(47,102,236)"), 104 scripting: new WebInspector.TimelineCategory("scripting", WebInspector.UIString("Scripting"), "rgb(157,231,119)"), 105 rendering: new WebInspector.TimelineCategory("rendering", WebInspector.UIString("Rendering"), "rgb(164,60,255)") 106 }; 107 } 108 return this._categories; 109 }, 110 111 _createStatusbarButtons: function() 112 { 113 this.toggleTimelineButton = new WebInspector.StatusBarButton("", "record-profile-status-bar-item"); 114 this.toggleTimelineButton.addEventListener("click", this._toggleTimelineButtonClicked.bind(this), false); 115 116 this.clearButton = new WebInspector.StatusBarButton("", "timeline-clear-status-bar-item"); 117 this.clearButton.addEventListener("click", this.reset.bind(this), false); 118 }, 119 120 _toggleTimelineButtonClicked: function() 121 { 122 if (this.toggleTimelineButton.toggled) 123 InspectorBackend.stopTimelineProfiler(); 124 else 125 InspectorBackend.startTimelineProfiler(); 126 }, 127 128 timelineWasStarted: function() 129 { 130 this.toggleTimelineButton.toggled = true; 131 }, 132 133 timelineWasStopped: function() 134 { 135 this.toggleTimelineButton.toggled = false; 136 }, 137 138 addRecordToTimeline: function(record) 139 { 140 this._innerAddRecordToTimeline(record, this._records); 141 this._scheduleRefresh(); 142 }, 143 144 _innerAddRecordToTimeline: function(record, collection) 145 { 146 var formattedRecord = this._formatRecord(record); 147 148 // Glue subsequent records with same category and title together if they are closer than 100ms to each other. 149 if (this._lastRecord && (!record.children || !record.children.length) && 150 this._lastRecord.category == formattedRecord.category && 151 this._lastRecord.title == formattedRecord.title && 152 this._lastRecord.details == formattedRecord.details && 153 formattedRecord.startTime - this._lastRecord.endTime < 0.1) { 154 this._lastRecord.endTime = formattedRecord.endTime; 155 this._lastRecord.count++; 156 } else { 157 collection.push(formattedRecord); 158 for (var i = 0; record.children && i < record.children.length; ++i) { 159 if (!formattedRecord.children) 160 formattedRecord.children = []; 161 var formattedChild = this._innerAddRecordToTimeline(record.children[i], formattedRecord.children); 162 formattedChild.parent = formattedRecord; 163 } 164 this._lastRecord = record.children && record.children.length ? null : formattedRecord; 165 } 166 return formattedRecord; 167 }, 168 169 _formatRecord: function(record) 170 { 171 var recordTypes = WebInspector.TimelineAgent.RecordType; 172 if (!this._recordStyles) { 173 this._recordStyles = {}; 174 this._recordStyles[recordTypes.EventDispatch] = { title: WebInspector.UIString("Event"), category: this.categories.scripting }; 175 this._recordStyles[recordTypes.Layout] = { title: WebInspector.UIString("Layout"), category: this.categories.rendering }; 176 this._recordStyles[recordTypes.RecalculateStyles] = { title: WebInspector.UIString("Recalculate Style"), category: this.categories.rendering }; 177 this._recordStyles[recordTypes.Paint] = { title: WebInspector.UIString("Paint"), category: this.categories.rendering }; 178 this._recordStyles[recordTypes.ParseHTML] = { title: WebInspector.UIString("Parse"), category: this.categories.loading }; 179 this._recordStyles[recordTypes.TimerInstall] = { title: WebInspector.UIString("Install Timer"), category: this.categories.scripting }; 180 this._recordStyles[recordTypes.TimerRemove] = { title: WebInspector.UIString("Remove Timer"), category: this.categories.scripting }; 181 this._recordStyles[recordTypes.TimerFire] = { title: WebInspector.UIString("Timer Fired"), category: this.categories.scripting }; 182 this._recordStyles[recordTypes.XHRReadyStateChange] = { title: WebInspector.UIString("XHR Ready State Change"), category: this.categories.scripting }; 183 this._recordStyles[recordTypes.XHRLoad] = { title: WebInspector.UIString("XHR Load"), category: this.categories.scripting }; 184 this._recordStyles[recordTypes.EvaluateScript] = { title: WebInspector.UIString("Evaluate Script"), category: this.categories.scripting }; 185 this._recordStyles[recordTypes.MarkTimeline] = { title: WebInspector.UIString("Mark"), category: this.categories.scripting }; 186 this._recordStyles[recordTypes.ResourceSendRequest] = { title: WebInspector.UIString("Send Request"), category: this.categories.loading }; 187 this._recordStyles[recordTypes.ResourceReceiveResponse] = { title: WebInspector.UIString("Receive Response"), category: this.categories.loading }; 188 this._recordStyles[recordTypes.ResourceFinish] = { title: WebInspector.UIString("Finish Loading"), category: this.categories.loading }; 189 } 190 191 var style = this._recordStyles[record.type]; 192 if (!style) 193 style = this._recordStyles[recordTypes.EventDispatch]; 194 195 var formattedRecord = {}; 196 formattedRecord.category = style.category; 197 formattedRecord.title = style.title; 198 formattedRecord.startTime = record.startTime / 1000; 199 formattedRecord.data = record.data; 200 formattedRecord.count = 1; 201 formattedRecord.type = record.type; 202 formattedRecord.endTime = (typeof record.endTime !== "undefined") ? record.endTime / 1000 : formattedRecord.startTime; 203 formattedRecord.record = record; 204 205 // Make resource receive record last since request was sent; make finish record last since response received. 206 if (record.type === WebInspector.TimelineAgent.RecordType.ResourceSendRequest) { 207 this._sendRequestRecords[record.data.identifier] = formattedRecord; 208 } else if (record.type === WebInspector.TimelineAgent.RecordType.ResourceReceiveResponse) { 209 var sendRequestRecord = this._sendRequestRecords[record.data.identifier]; 210 if (sendRequestRecord) { // False if we started instrumentation in the middle of request. 211 sendRequestRecord._responseReceivedFormattedTime = formattedRecord.startTime; 212 formattedRecord.startTime = sendRequestRecord.startTime; 213 sendRequestRecord.details = this._getRecordDetails(record); 214 } 215 } else if (record.type === WebInspector.TimelineAgent.RecordType.ResourceFinish) { 216 var sendRequestRecord = this._sendRequestRecords[record.data.identifier]; 217 if (sendRequestRecord) // False for main resource. 218 formattedRecord.startTime = sendRequestRecord._responseReceivedFormattedTime; 219 } 220 formattedRecord.details = this._getRecordDetails(record); 221 222 return formattedRecord; 223 }, 224 225 _getRecordDetails: function(record) 226 { 227 switch (record.type) { 228 case WebInspector.TimelineAgent.RecordType.EventDispatch: 229 return record.data ? record.data.type : ""; 230 case WebInspector.TimelineAgent.RecordType.Paint: 231 return record.data.width + "\u2009\u00d7\u2009" + record.data.height; 232 case WebInspector.TimelineAgent.RecordType.TimerInstall: 233 case WebInspector.TimelineAgent.RecordType.TimerRemove: 234 case WebInspector.TimelineAgent.RecordType.TimerFire: 235 return record.data.timerId; 236 case WebInspector.TimelineAgent.RecordType.XHRReadyStateChange: 237 case WebInspector.TimelineAgent.RecordType.XHRLoad: 238 case WebInspector.TimelineAgent.RecordType.EvaluateScript: 239 case WebInspector.TimelineAgent.RecordType.ResourceSendRequest: 240 return WebInspector.displayNameForURL(record.data.url); 241 case WebInspector.TimelineAgent.RecordType.ResourceReceiveResponse: 242 case WebInspector.TimelineAgent.RecordType.ResourceFinish: 243 var sendRequestRecord = this._sendRequestRecords[record.data.identifier]; 244 return sendRequestRecord ? WebInspector.displayNameForURL(sendRequestRecord.data.url) : ""; 245 case WebInspector.TimelineAgent.RecordType.MarkTimeline: 246 return record.data.message; 247 default: 248 return ""; 249 } 250 }, 251 252 setSidebarWidth: function(width) 253 { 254 WebInspector.Panel.prototype.setSidebarWidth.call(this, width); 255 this._sidebarBackgroundElement.style.width = width + "px"; 256 this._overviewPane.setSidebarWidth(width); 257 }, 258 259 updateMainViewWidth: function(width) 260 { 261 this._containerContentElement.style.left = width + "px"; 262 this._scheduleRefresh(); 263 this._overviewPane.updateMainViewWidth(width); 264 }, 265 266 resize: function() { 267 this._scheduleRefresh(); 268 }, 269 270 reset: function() 271 { 272 this._lastRecord = null; 273 this._sendRequestRecords = {}; 274 this._records = []; 275 this._boundariesAreValid = false; 276 this._overviewPane.reset(); 277 this._adjustScrollPosition(0); 278 this._refresh(); 279 }, 280 281 show: function() 282 { 283 WebInspector.Panel.prototype.show.call(this); 284 285 if (this._needsRefresh) 286 this._refresh(); 287 }, 288 289 _onScroll: function(event) 290 { 291 var scrollTop = this._containerElement.scrollTop; 292 var dividersTop = Math.max(0, scrollTop); 293 this._timelineGrid.setScrollAndDividerTop(scrollTop, dividersTop); 294 this._scheduleRefresh(true); 295 }, 296 297 _windowChanged: function() 298 { 299 this._scheduleRefresh(); 300 }, 301 302 _scheduleRefresh: function(preserveBoundaries) 303 { 304 this._boundariesAreValid &= preserveBoundaries; 305 if (this._needsRefresh) 306 return; 307 this._needsRefresh = true; 308 309 if (this.visible && !("_refreshTimeout" in this)) { 310 if (preserveBoundaries) 311 this._refresh(); 312 else 313 this._refreshTimeout = setTimeout(this._refresh.bind(this), 100); 314 } 315 }, 316 317 _refresh: function() 318 { 319 this._needsRefresh = false; 320 if ("_refreshTimeout" in this) { 321 clearTimeout(this._refreshTimeout); 322 delete this._refreshTimeout; 323 } 324 325 if (!this._boundariesAreValid) 326 this._overviewPane.update(this._records); 327 this._refreshRecords(!this._boundariesAreValid); 328 this._boundariesAreValid = true; 329 }, 330 331 _refreshRecords: function(updateBoundaries) 332 { 333 if (updateBoundaries) { 334 this._calculator.reset(); 335 this._calculator.windowLeft = this._overviewPane.windowLeft; 336 this._calculator.windowRight = this._overviewPane.windowRight; 337 338 for (var i = 0; i < this._records.length; ++i) 339 this._calculator.updateBoundaries(this._records[i]); 340 341 this._calculator.calculateWindow(); 342 } 343 344 var recordsInWindow = []; 345 for (var i = 0; i < this._records.length; ++i) { 346 var record = this._records[i]; 347 var percentages = this._calculator.computeBarGraphPercentages(record); 348 if (percentages.start < 100 && percentages.end >= 0 && !record.category.hidden) 349 this._addToRecordsWindow(record, recordsInWindow); 350 } 351 352 // Calculate the visible area. 353 var visibleTop = this._containerElement.scrollTop; 354 var visibleBottom = visibleTop + this._containerElement.clientHeight; 355 356 // Define row height, should be in sync with styles for timeline graphs. 357 const rowHeight = 18; 358 const expandOffset = 15; 359 360 // Convert visible area to visible indexes. Always include top-level record for a visible nested record. 361 var startIndex = Math.max(0, Math.min(Math.floor(visibleTop / rowHeight) - 1, recordsInWindow.length - 1)); 362 while (startIndex > 0 && recordsInWindow[startIndex].parent) 363 startIndex--; 364 var endIndex = Math.min(recordsInWindow.length, Math.ceil(visibleBottom / rowHeight)); 365 while (endIndex < recordsInWindow.length - 1 && recordsInWindow[endIndex].parent) 366 endIndex++; 367 368 // Resize gaps first. 369 const top = (startIndex * rowHeight) + "px"; 370 this._topGapElement.style.height = top; 371 this.sidebarElement.style.top = top; 372 this.sidebarResizeElement.style.top = top; 373 this._bottomGapElement.style.height = (recordsInWindow.length - endIndex) * rowHeight + "px"; 374 375 // Update visible rows. 376 var listRowElement = this._sidebarListElement.firstChild; 377 var width = this._graphRowsElement.offsetWidth; 378 this._itemsGraphsElement.removeChild(this._graphRowsElement); 379 var graphRowElement = this._graphRowsElement.firstChild; 380 var scheduleRefreshCallback = this._scheduleRefresh.bind(this, true); 381 for (var i = startIndex; i < endIndex; ++i) { 382 var record = recordsInWindow[i]; 383 var isEven = !(i % 2); 384 385 if (!listRowElement) { 386 listRowElement = new WebInspector.TimelineRecordListRow().element; 387 this._sidebarListElement.appendChild(listRowElement); 388 } 389 if (!graphRowElement) { 390 graphRowElement = new WebInspector.TimelineRecordGraphRow(this._itemsGraphsElement, scheduleRefreshCallback, rowHeight).element; 391 this._graphRowsElement.appendChild(graphRowElement); 392 } 393 394 listRowElement.listRow.update(record, isEven); 395 graphRowElement.graphRow.update(record, isEven, this._calculator, width, expandOffset, i); 396 397 listRowElement = listRowElement.nextSibling; 398 graphRowElement = graphRowElement.nextSibling; 399 } 400 401 // Remove extra rows. 402 while (listRowElement) { 403 var nextElement = listRowElement.nextSibling; 404 listRowElement.listRow.dispose(); 405 listRowElement = nextElement; 406 } 407 while (graphRowElement) { 408 var nextElement = graphRowElement.nextSibling; 409 graphRowElement.graphRow.dispose(); 410 graphRowElement = nextElement; 411 } 412 413 this._itemsGraphsElement.insertBefore(this._graphRowsElement, this._bottomGapElement); 414 // Reserve some room for expand / collapse controls to the left for records that start at 0ms. 415 var timelinePaddingLeft = this._calculator.windowLeft === 0 ? expandOffset : 0; 416 if (updateBoundaries) 417 this._timelineGrid.updateDividers(true, this._calculator, timelinePaddingLeft); 418 this._adjustScrollPosition((recordsInWindow.length + 1) * rowHeight); 419 }, 420 421 _addToRecordsWindow: function(record, recordsWindow) 422 { 423 recordsWindow.push(record); 424 if (!record.collapsed) { 425 var index = recordsWindow.length; 426 for (var i = 0; record.children && i < record.children.length; ++i) 427 this._addToRecordsWindow(record.children[i], recordsWindow); 428 record.visibleChildrenCount = recordsWindow.length - index; 429 } 430 }, 431 432 _adjustScrollPosition: function(totalHeight) 433 { 434 // Prevent the container from being scrolled off the end. 435 if ((this._containerElement.scrollTop + this._containerElement.offsetHeight) > totalHeight + 1) 436 this._containerElement.scrollTop = (totalHeight - this._containerElement.offsetHeight); 437 } 438} 439 440WebInspector.TimelinePanel.prototype.__proto__ = WebInspector.Panel.prototype; 441 442 443WebInspector.TimelineCategory = function(name, title, color) 444{ 445 this.name = name; 446 this.title = title; 447 this.color = color; 448} 449 450 451WebInspector.TimelineCalculator = function() 452{ 453 this.reset(); 454 this.windowLeft = 0.0; 455 this.windowRight = 1.0; 456 this._uiString = WebInspector.UIString.bind(WebInspector); 457} 458 459WebInspector.TimelineCalculator.prototype = { 460 computeBarGraphPercentages: function(record) 461 { 462 var start = (record.startTime - this.minimumBoundary) / this.boundarySpan * 100; 463 var end = (record.endTime - this.minimumBoundary) / this.boundarySpan * 100; 464 return {start: start, end: end}; 465 }, 466 467 calculateWindow: function() 468 { 469 this.minimumBoundary = this._absoluteMinimumBoundary + this.windowLeft * (this._absoluteMaximumBoundary - this._absoluteMinimumBoundary); 470 this.maximumBoundary = this._absoluteMinimumBoundary + this.windowRight * (this._absoluteMaximumBoundary - this._absoluteMinimumBoundary); 471 this.boundarySpan = this.maximumBoundary - this.minimumBoundary; 472 }, 473 474 reset: function() 475 { 476 this._absoluteMinimumBoundary = -1; 477 this._absoluteMaximumBoundary = -1; 478 }, 479 480 updateBoundaries: function(record) 481 { 482 var lowerBound = record.startTime; 483 if (this._absoluteMinimumBoundary === -1 || lowerBound < this._absoluteMinimumBoundary) 484 this._absoluteMinimumBoundary = lowerBound; 485 486 var upperBound = record.endTime; 487 if (this._absoluteMaximumBoundary === -1 || upperBound > this._absoluteMaximumBoundary) 488 this._absoluteMaximumBoundary = upperBound; 489 }, 490 491 formatValue: function(value) 492 { 493 return Number.secondsToString(value + this.minimumBoundary - this._absoluteMinimumBoundary, this._uiString); 494 } 495} 496 497 498WebInspector.TimelineRecordListRow = function() 499{ 500 this.element = document.createElement("div"); 501 this.element.listRow = this; 502 var iconElement = document.createElement("span"); 503 iconElement.className = "timeline-tree-icon"; 504 this.element.appendChild(iconElement); 505 506 this._typeElement = document.createElement("span"); 507 this._typeElement.className = "type"; 508 this.element.appendChild(this._typeElement); 509 510 var separatorElement = document.createElement("span"); 511 separatorElement.className = "separator"; 512 separatorElement.textContent = " "; 513 514 this._dataElement = document.createElement("span"); 515 this._dataElement.className = "data dimmed"; 516 517 this._repeatCountElement = document.createElement("span"); 518 this._repeatCountElement.className = "count"; 519 520 this.element.appendChild(separatorElement); 521 this.element.appendChild(this._dataElement); 522 this.element.appendChild(this._repeatCountElement); 523} 524 525WebInspector.TimelineRecordListRow.prototype = { 526 update: function(record, isEven) 527 { 528 this.element.className = "timeline-tree-item timeline-category-" + record.category.name + (isEven ? " even" : ""); 529 this._typeElement.textContent = record.title; 530 531 if (record.details) { 532 this._dataElement.textContent = "(" + record.details + ")"; 533 this._dataElement.title = record.details; 534 } else { 535 this._dataElement.textContent = ""; 536 this._dataElement.title = ""; 537 } 538 539 if (record.count > 1) 540 this._repeatCountElement.textContent = "\u2009\u00d7\u2009" + record.count; 541 else 542 this._repeatCountElement.textContent = ""; 543 }, 544 545 dispose: function() 546 { 547 this.element.parentElement.removeChild(this.element); 548 } 549} 550 551 552WebInspector.TimelineRecordGraphRow = function(graphContainer, refreshCallback, rowHeight) 553{ 554 this.element = document.createElement("div"); 555 this.element.graphRow = this; 556 557 this._barAreaElement = document.createElement("div"); 558 this._barAreaElement.className = "timeline-graph-bar-area"; 559 this.element.appendChild(this._barAreaElement); 560 561 this._barElement = document.createElement("div"); 562 this._barElement.className = "timeline-graph-bar"; 563 this._barAreaElement.appendChild(this._barElement); 564 565 this._expandElement = document.createElement("div"); 566 this._expandElement.className = "timeline-expandable"; 567 graphContainer.appendChild(this._expandElement); 568 569 var leftBorder = document.createElement("div"); 570 leftBorder.className = "timeline-expandable-left"; 571 this._expandElement.appendChild(leftBorder); 572 573 this._expandElement.addEventListener("click", this._onClick.bind(this)); 574 this._refreshCallback = refreshCallback; 575 this._rowHeight = rowHeight; 576} 577 578WebInspector.TimelineRecordGraphRow.prototype = { 579 update: function(record, isEven, calculator, clientWidth, expandOffset, index) 580 { 581 this._record = record; 582 this.element.className = "timeline-graph-side timeline-category-" + record.category.name + (isEven ? " even" : ""); 583 var percentages = calculator.computeBarGraphPercentages(record); 584 var left = percentages.start / 100 * clientWidth; 585 var width = (percentages.end - percentages.start) / 100 * clientWidth; 586 this._barElement.style.left = (left + expandOffset) + "px"; 587 this._barElement.style.width = width + "px"; 588 589 if (record.visibleChildrenCount) { 590 this._expandElement.style.top = index * this._rowHeight + "px"; 591 this._expandElement.style.left = left + "px"; 592 this._expandElement.style.width = Math.max(12, width + 25) + "px"; 593 if (!record.collapsed) { 594 this._expandElement.style.height = (record.visibleChildrenCount + 1) * this._rowHeight + "px"; 595 this._expandElement.addStyleClass("timeline-expandable-expanded"); 596 this._expandElement.removeStyleClass("timeline-expandable-collapsed"); 597 } else { 598 this._expandElement.style.height = this._rowHeight + "px"; 599 this._expandElement.addStyleClass("timeline-expandable-collapsed"); 600 this._expandElement.removeStyleClass("timeline-expandable-expanded"); 601 } 602 this._expandElement.removeStyleClass("hidden"); 603 } else { 604 this._expandElement.addStyleClass("hidden"); 605 } 606 }, 607 608 _onClick: function(event) 609 { 610 this._record.collapsed = !this._record.collapsed; 611 this._refreshCallback(); 612 }, 613 614 dispose: function() 615 { 616 this.element.parentElement.removeChild(this.element); 617 this._expandElement.parentElement.removeChild(this._expandElement); 618 } 619} 620