1/* 2 * Copyright (C) 2008 Apple 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 6 * are met: 7 * 1. Redistributions of source code must retain the above copyright 8 * notice, this list of conditions and the following disclaimer. 9 * 2. Redistributions in binary form must reproduce the above copyright 10 * notice, this list of conditions and the following disclaimer in the 11 * documentation and/or other materials provided with the distribution. 12 * 13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY 14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR 17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 */ 25 26// FIXME: Rename the file. 27 28WebInspector.CPUProfileView = function(profile) 29{ 30 WebInspector.View.call(this); 31 32 this.element.addStyleClass("profile-view"); 33 34 this.showSelfTimeAsPercent = true; 35 this.showTotalTimeAsPercent = true; 36 this.showAverageTimeAsPercent = true; 37 38 var columns = { "self": { title: WebInspector.UIString("Self"), width: "72px", sort: "descending", sortable: true }, 39 "total": { title: WebInspector.UIString("Total"), width: "72px", sortable: true }, 40 "average": { title: WebInspector.UIString("Average"), width: "72px", sortable: true }, 41 "calls": { title: WebInspector.UIString("Calls"), width: "54px", sortable: true }, 42 "function": { title: WebInspector.UIString("Function"), disclosure: true, sortable: true } }; 43 44 if (Preferences.samplingCPUProfiler) { 45 delete columns.average; 46 delete columns.calls; 47 } 48 49 this.dataGrid = new WebInspector.DataGrid(columns); 50 this.dataGrid.addEventListener("sorting changed", this._sortData, this); 51 this.dataGrid.element.addEventListener("mousedown", this._mouseDownInDataGrid.bind(this), true); 52 this.element.appendChild(this.dataGrid.element); 53 54 this.viewSelectElement = document.createElement("select"); 55 this.viewSelectElement.className = "status-bar-item"; 56 this.viewSelectElement.addEventListener("change", this._changeView.bind(this), false); 57 this.view = "Heavy"; 58 59 var heavyViewOption = document.createElement("option"); 60 heavyViewOption.label = WebInspector.UIString("Heavy (Bottom Up)"); 61 var treeViewOption = document.createElement("option"); 62 treeViewOption.label = WebInspector.UIString("Tree (Top Down)"); 63 this.viewSelectElement.appendChild(heavyViewOption); 64 this.viewSelectElement.appendChild(treeViewOption); 65 66 this.percentButton = new WebInspector.StatusBarButton("", "percent-time-status-bar-item"); 67 this.percentButton.addEventListener("click", this._percentClicked.bind(this), false); 68 69 this.focusButton = new WebInspector.StatusBarButton(WebInspector.UIString("Focus selected function."), "focus-profile-node-status-bar-item"); 70 this.focusButton.disabled = true; 71 this.focusButton.addEventListener("click", this._focusClicked.bind(this), false); 72 73 this.excludeButton = new WebInspector.StatusBarButton(WebInspector.UIString("Exclude selected function."), "exclude-profile-node-status-bar-item"); 74 this.excludeButton.disabled = true; 75 this.excludeButton.addEventListener("click", this._excludeClicked.bind(this), false); 76 77 this.resetButton = new WebInspector.StatusBarButton(WebInspector.UIString("Restore all functions."), "reset-profile-status-bar-item"); 78 this.resetButton.visible = false; 79 this.resetButton.addEventListener("click", this._resetClicked.bind(this), false); 80 81 this.profile = profile; 82 83 var self = this; 84 function profileCallback(error, profile) 85 { 86 if (error) 87 return; 88 self.profile.head = profile.head; 89 self._assignParentsInProfile(); 90 91 self.profileDataGridTree = self.bottomUpProfileDataGridTree; 92 self.profileDataGridTree.sort(WebInspector.ProfileDataGridTree.propertyComparator("selfTime", false)); 93 94 self.refresh(); 95 96 self._updatePercentButton(); 97 } 98 99 ProfilerAgent.getProfile(this.profile.typeId, this.profile.uid, profileCallback); 100} 101 102WebInspector.CPUProfileView.prototype = { 103 get statusBarItems() 104 { 105 return [this.viewSelectElement, this.percentButton.element, this.focusButton.element, this.excludeButton.element, this.resetButton.element]; 106 }, 107 108 get profile() 109 { 110 return this._profile; 111 }, 112 113 set profile(profile) 114 { 115 this._profile = profile; 116 }, 117 118 get bottomUpProfileDataGridTree() 119 { 120 if (!this._bottomUpProfileDataGridTree) 121 this._bottomUpProfileDataGridTree = new WebInspector.BottomUpProfileDataGridTree(this, this.profile.head); 122 return this._bottomUpProfileDataGridTree; 123 }, 124 125 get topDownProfileDataGridTree() 126 { 127 if (!this._topDownProfileDataGridTree) 128 this._topDownProfileDataGridTree = new WebInspector.TopDownProfileDataGridTree(this, this.profile.head); 129 return this._topDownProfileDataGridTree; 130 }, 131 132 get currentTree() 133 { 134 return this._currentTree; 135 }, 136 137 set currentTree(tree) 138 { 139 this._currentTree = tree; 140 this.refresh(); 141 }, 142 143 get topDownTree() 144 { 145 if (!this._topDownTree) { 146 this._topDownTree = WebInspector.TopDownTreeFactory.create(this.profile.head); 147 this._sortProfile(this._topDownTree); 148 } 149 150 return this._topDownTree; 151 }, 152 153 get bottomUpTree() 154 { 155 if (!this._bottomUpTree) { 156 this._bottomUpTree = WebInspector.BottomUpTreeFactory.create(this.profile.head); 157 this._sortProfile(this._bottomUpTree); 158 } 159 160 return this._bottomUpTree; 161 }, 162 163 show: function(parentElement) 164 { 165 WebInspector.View.prototype.show.call(this, parentElement); 166 this.dataGrid.updateWidths(); 167 }, 168 169 hide: function() 170 { 171 WebInspector.View.prototype.hide.call(this); 172 this._currentSearchResultIndex = -1; 173 }, 174 175 resize: function() 176 { 177 if (this.dataGrid) 178 this.dataGrid.updateWidths(); 179 }, 180 181 refresh: function() 182 { 183 var selectedProfileNode = this.dataGrid.selectedNode ? this.dataGrid.selectedNode.profileNode : null; 184 185 this.dataGrid.removeChildren(); 186 187 var children = this.profileDataGridTree.children; 188 var count = children.length; 189 190 for (var index = 0; index < count; ++index) 191 this.dataGrid.appendChild(children[index]); 192 193 if (selectedProfileNode) 194 selectedProfileNode.selected = true; 195 }, 196 197 refreshVisibleData: function() 198 { 199 var child = this.dataGrid.children[0]; 200 while (child) { 201 child.refresh(); 202 child = child.traverseNextNode(false, null, true); 203 } 204 }, 205 206 refreshShowAsPercents: function() 207 { 208 this._updatePercentButton(); 209 this.refreshVisibleData(); 210 }, 211 212 searchCanceled: function() 213 { 214 if (this._searchResults) { 215 for (var i = 0; i < this._searchResults.length; ++i) { 216 var profileNode = this._searchResults[i].profileNode; 217 218 delete profileNode._searchMatchedSelfColumn; 219 delete profileNode._searchMatchedTotalColumn; 220 delete profileNode._searchMatchedCallsColumn; 221 delete profileNode._searchMatchedFunctionColumn; 222 223 profileNode.refresh(); 224 } 225 } 226 227 delete this._searchFinishedCallback; 228 this._currentSearchResultIndex = -1; 229 this._searchResults = []; 230 }, 231 232 performSearch: function(query, finishedCallback) 233 { 234 // Call searchCanceled since it will reset everything we need before doing a new search. 235 this.searchCanceled(); 236 237 query = query.trim(); 238 239 if (!query.length) 240 return; 241 242 this._searchFinishedCallback = finishedCallback; 243 244 var greaterThan = (query.indexOf(">") === 0); 245 var lessThan = (query.indexOf("<") === 0); 246 var equalTo = (query.indexOf("=") === 0 || ((greaterThan || lessThan) && query.indexOf("=") === 1)); 247 var percentUnits = (query.lastIndexOf("%") === (query.length - 1)); 248 var millisecondsUnits = (query.length > 2 && query.lastIndexOf("ms") === (query.length - 2)); 249 var secondsUnits = (!millisecondsUnits && query.lastIndexOf("s") === (query.length - 1)); 250 251 var queryNumber = parseFloat(query); 252 if (greaterThan || lessThan || equalTo) { 253 if (equalTo && (greaterThan || lessThan)) 254 queryNumber = parseFloat(query.substring(2)); 255 else 256 queryNumber = parseFloat(query.substring(1)); 257 } 258 259 var queryNumberMilliseconds = (secondsUnits ? (queryNumber * 1000) : queryNumber); 260 261 // Make equalTo implicitly true if it wasn't specified there is no other operator. 262 if (!isNaN(queryNumber) && !(greaterThan || lessThan)) 263 equalTo = true; 264 265 function matchesQuery(/*ProfileDataGridNode*/ profileDataGridNode) 266 { 267 delete profileDataGridNode._searchMatchedSelfColumn; 268 delete profileDataGridNode._searchMatchedTotalColumn; 269 delete profileDataGridNode._searchMatchedAverageColumn; 270 delete profileDataGridNode._searchMatchedCallsColumn; 271 delete profileDataGridNode._searchMatchedFunctionColumn; 272 273 if (percentUnits) { 274 if (lessThan) { 275 if (profileDataGridNode.selfPercent < queryNumber) 276 profileDataGridNode._searchMatchedSelfColumn = true; 277 if (profileDataGridNode.totalPercent < queryNumber) 278 profileDataGridNode._searchMatchedTotalColumn = true; 279 if (profileDataGridNode.averagePercent < queryNumberMilliseconds) 280 profileDataGridNode._searchMatchedAverageColumn = true; 281 } else if (greaterThan) { 282 if (profileDataGridNode.selfPercent > queryNumber) 283 profileDataGridNode._searchMatchedSelfColumn = true; 284 if (profileDataGridNode.totalPercent > queryNumber) 285 profileDataGridNode._searchMatchedTotalColumn = true; 286 if (profileDataGridNode.averagePercent < queryNumberMilliseconds) 287 profileDataGridNode._searchMatchedAverageColumn = true; 288 } 289 290 if (equalTo) { 291 if (profileDataGridNode.selfPercent == queryNumber) 292 profileDataGridNode._searchMatchedSelfColumn = true; 293 if (profileDataGridNode.totalPercent == queryNumber) 294 profileDataGridNode._searchMatchedTotalColumn = true; 295 if (profileDataGridNode.averagePercent < queryNumberMilliseconds) 296 profileDataGridNode._searchMatchedAverageColumn = true; 297 } 298 } else if (millisecondsUnits || secondsUnits) { 299 if (lessThan) { 300 if (profileDataGridNode.selfTime < queryNumberMilliseconds) 301 profileDataGridNode._searchMatchedSelfColumn = true; 302 if (profileDataGridNode.totalTime < queryNumberMilliseconds) 303 profileDataGridNode._searchMatchedTotalColumn = true; 304 if (profileDataGridNode.averageTime < queryNumberMilliseconds) 305 profileDataGridNode._searchMatchedAverageColumn = true; 306 } else if (greaterThan) { 307 if (profileDataGridNode.selfTime > queryNumberMilliseconds) 308 profileDataGridNode._searchMatchedSelfColumn = true; 309 if (profileDataGridNode.totalTime > queryNumberMilliseconds) 310 profileDataGridNode._searchMatchedTotalColumn = true; 311 if (profileDataGridNode.averageTime > queryNumberMilliseconds) 312 profileDataGridNode._searchMatchedAverageColumn = true; 313 } 314 315 if (equalTo) { 316 if (profileDataGridNode.selfTime == queryNumberMilliseconds) 317 profileDataGridNode._searchMatchedSelfColumn = true; 318 if (profileDataGridNode.totalTime == queryNumberMilliseconds) 319 profileDataGridNode._searchMatchedTotalColumn = true; 320 if (profileDataGridNode.averageTime == queryNumberMilliseconds) 321 profileDataGridNode._searchMatchedAverageColumn = true; 322 } 323 } else { 324 if (equalTo && profileDataGridNode.numberOfCalls == queryNumber) 325 profileDataGridNode._searchMatchedCallsColumn = true; 326 if (greaterThan && profileDataGridNode.numberOfCalls > queryNumber) 327 profileDataGridNode._searchMatchedCallsColumn = true; 328 if (lessThan && profileDataGridNode.numberOfCalls < queryNumber) 329 profileDataGridNode._searchMatchedCallsColumn = true; 330 } 331 332 if (profileDataGridNode.functionName.hasSubstring(query, true) || profileDataGridNode.url.hasSubstring(query, true)) 333 profileDataGridNode._searchMatchedFunctionColumn = true; 334 335 if (profileDataGridNode._searchMatchedSelfColumn || 336 profileDataGridNode._searchMatchedTotalColumn || 337 profileDataGridNode._searchMatchedAverageColumn || 338 profileDataGridNode._searchMatchedCallsColumn || 339 profileDataGridNode._searchMatchedFunctionColumn) 340 { 341 profileDataGridNode.refresh(); 342 return true; 343 } 344 345 return false; 346 } 347 348 var current = this.profileDataGridTree.children[0]; 349 350 while (current) { 351 if (matchesQuery(current)) { 352 this._searchResults.push({ profileNode: current }); 353 } 354 355 current = current.traverseNextNode(false, null, false); 356 } 357 358 finishedCallback(this, this._searchResults.length); 359 }, 360 361 jumpToFirstSearchResult: function() 362 { 363 if (!this._searchResults || !this._searchResults.length) 364 return; 365 this._currentSearchResultIndex = 0; 366 this._jumpToSearchResult(this._currentSearchResultIndex); 367 }, 368 369 jumpToLastSearchResult: function() 370 { 371 if (!this._searchResults || !this._searchResults.length) 372 return; 373 this._currentSearchResultIndex = (this._searchResults.length - 1); 374 this._jumpToSearchResult(this._currentSearchResultIndex); 375 }, 376 377 jumpToNextSearchResult: function() 378 { 379 if (!this._searchResults || !this._searchResults.length) 380 return; 381 if (++this._currentSearchResultIndex >= this._searchResults.length) 382 this._currentSearchResultIndex = 0; 383 this._jumpToSearchResult(this._currentSearchResultIndex); 384 }, 385 386 jumpToPreviousSearchResult: function() 387 { 388 if (!this._searchResults || !this._searchResults.length) 389 return; 390 if (--this._currentSearchResultIndex < 0) 391 this._currentSearchResultIndex = (this._searchResults.length - 1); 392 this._jumpToSearchResult(this._currentSearchResultIndex); 393 }, 394 395 showingFirstSearchResult: function() 396 { 397 return (this._currentSearchResultIndex === 0); 398 }, 399 400 showingLastSearchResult: function() 401 { 402 return (this._searchResults && this._currentSearchResultIndex === (this._searchResults.length - 1)); 403 }, 404 405 _jumpToSearchResult: function(index) 406 { 407 var searchResult = this._searchResults[index]; 408 if (!searchResult) 409 return; 410 411 var profileNode = searchResult.profileNode; 412 profileNode.reveal(); 413 profileNode.select(); 414 }, 415 416 _changeView: function(event) 417 { 418 if (!event || !this.profile) 419 return; 420 421 if (event.target.selectedIndex == 1 && this.view == "Heavy") { 422 this.profileDataGridTree = this.topDownProfileDataGridTree; 423 this._sortProfile(); 424 this.view = "Tree"; 425 } else if (event.target.selectedIndex == 0 && this.view == "Tree") { 426 this.profileDataGridTree = this.bottomUpProfileDataGridTree; 427 this._sortProfile(); 428 this.view = "Heavy"; 429 } 430 431 if (!this.currentQuery || !this._searchFinishedCallback || !this._searchResults) 432 return; 433 434 // The current search needs to be performed again. First negate out previous match 435 // count by calling the search finished callback with a negative number of matches. 436 // Then perform the search again the with same query and callback. 437 this._searchFinishedCallback(this, -this._searchResults.length); 438 this.performSearch(this.currentQuery, this._searchFinishedCallback); 439 }, 440 441 _percentClicked: function(event) 442 { 443 var currentState = this.showSelfTimeAsPercent && this.showTotalTimeAsPercent && this.showAverageTimeAsPercent; 444 this.showSelfTimeAsPercent = !currentState; 445 this.showTotalTimeAsPercent = !currentState; 446 this.showAverageTimeAsPercent = !currentState; 447 this.refreshShowAsPercents(); 448 }, 449 450 _updatePercentButton: function() 451 { 452 if (this.showSelfTimeAsPercent && this.showTotalTimeAsPercent && this.showAverageTimeAsPercent) { 453 this.percentButton.title = WebInspector.UIString("Show absolute total and self times."); 454 this.percentButton.toggled = true; 455 } else { 456 this.percentButton.title = WebInspector.UIString("Show total and self times as percentages."); 457 this.percentButton.toggled = false; 458 } 459 }, 460 461 _focusClicked: function(event) 462 { 463 if (!this.dataGrid.selectedNode) 464 return; 465 466 this.resetButton.visible = true; 467 this.profileDataGridTree.focus(this.dataGrid.selectedNode); 468 this.refresh(); 469 this.refreshVisibleData(); 470 }, 471 472 _excludeClicked: function(event) 473 { 474 var selectedNode = this.dataGrid.selectedNode 475 476 if (!selectedNode) 477 return; 478 479 selectedNode.deselect(); 480 481 this.resetButton.visible = true; 482 this.profileDataGridTree.exclude(selectedNode); 483 this.refresh(); 484 this.refreshVisibleData(); 485 }, 486 487 _resetClicked: function(event) 488 { 489 this.resetButton.visible = false; 490 this.profileDataGridTree.restore(); 491 this.refresh(); 492 this.refreshVisibleData(); 493 }, 494 495 _dataGridNodeSelected: function(node) 496 { 497 this.focusButton.disabled = false; 498 this.excludeButton.disabled = false; 499 }, 500 501 _dataGridNodeDeselected: function(node) 502 { 503 this.focusButton.disabled = true; 504 this.excludeButton.disabled = true; 505 }, 506 507 _sortData: function(event) 508 { 509 this._sortProfile(this.profile); 510 }, 511 512 _sortProfile: function() 513 { 514 var sortAscending = this.dataGrid.sortOrder === "ascending"; 515 var sortColumnIdentifier = this.dataGrid.sortColumnIdentifier; 516 var sortProperty = { 517 "average": "averageTime", 518 "self": "selfTime", 519 "total": "totalTime", 520 "calls": "numberOfCalls", 521 "function": "functionName" 522 }[sortColumnIdentifier]; 523 524 this.profileDataGridTree.sort(WebInspector.ProfileDataGridTree.propertyComparator(sortProperty, sortAscending)); 525 526 this.refresh(); 527 }, 528 529 _mouseDownInDataGrid: function(event) 530 { 531 if (event.detail < 2) 532 return; 533 534 var cell = event.target.enclosingNodeOrSelfWithNodeName("td"); 535 if (!cell || (!cell.hasStyleClass("total-column") && !cell.hasStyleClass("self-column") && !cell.hasStyleClass("average-column"))) 536 return; 537 538 if (cell.hasStyleClass("total-column")) 539 this.showTotalTimeAsPercent = !this.showTotalTimeAsPercent; 540 else if (cell.hasStyleClass("self-column")) 541 this.showSelfTimeAsPercent = !this.showSelfTimeAsPercent; 542 else if (cell.hasStyleClass("average-column")) 543 this.showAverageTimeAsPercent = !this.showAverageTimeAsPercent; 544 545 this.refreshShowAsPercents(); 546 547 event.preventDefault(); 548 event.stopPropagation(); 549 }, 550 551 _assignParentsInProfile: function() 552 { 553 var head = this.profile.head; 554 head.parent = null; 555 head.head = null; 556 var nodesToTraverse = [ { parent: head, children: head.children } ]; 557 while (nodesToTraverse.length > 0) { 558 var pair = nodesToTraverse.shift(); 559 var parent = pair.parent; 560 var children = pair.children; 561 var length = children.length; 562 for (var i = 0; i < length; ++i) { 563 children[i].head = head; 564 children[i].parent = parent; 565 if (children[i].children.length > 0) 566 nodesToTraverse.push({ parent: children[i], children: children[i].children }); 567 } 568 } 569 } 570} 571 572WebInspector.CPUProfileView.prototype.__proto__ = WebInspector.View.prototype; 573 574WebInspector.CPUProfileType = function() 575{ 576 WebInspector.ProfileType.call(this, WebInspector.CPUProfileType.TypeId, WebInspector.UIString("CPU PROFILES")); 577 this._recording = false; 578} 579 580WebInspector.CPUProfileType.TypeId = "CPU"; 581 582WebInspector.CPUProfileType.prototype = { 583 get buttonTooltip() 584 { 585 return this._recording ? WebInspector.UIString("Stop profiling.") : WebInspector.UIString("Start profiling."); 586 }, 587 588 get buttonStyle() 589 { 590 return this._recording ? "record-profile-status-bar-item status-bar-item toggled-on" : "record-profile-status-bar-item status-bar-item"; 591 }, 592 593 buttonClicked: function() 594 { 595 this._recording = !this._recording; 596 597 if (this._recording) 598 ProfilerAgent.start(); 599 else 600 ProfilerAgent.stop(); 601 }, 602 603 get welcomeMessage() 604 { 605 return WebInspector.UIString("Control CPU profiling by pressing the %s button on the status bar."); 606 }, 607 608 setRecordingProfile: function(isProfiling) 609 { 610 this._recording = isProfiling; 611 }, 612 613 createSidebarTreeElementForProfile: function(profile) 614 { 615 return new WebInspector.ProfileSidebarTreeElement(profile, WebInspector.UIString("Profile %d"), "profile-sidebar-tree-item"); 616 }, 617 618 createView: function(profile) 619 { 620 return new WebInspector.CPUProfileView(profile); 621 } 622} 623 624WebInspector.CPUProfileType.prototype.__proto__ = WebInspector.ProfileType.prototype; 625