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(profile) 85 { 86 self.profile = profile; 87 self._assignParentsInProfile(); 88 89 self.profileDataGridTree = self.bottomUpProfileDataGridTree; 90 self.profileDataGridTree.sort(WebInspector.ProfileDataGridTree.propertyComparator("selfTime", false)); 91 92 self.refresh(); 93 94 self._updatePercentButton(); 95 } 96 97 var callId = WebInspector.Callback.wrap(profileCallback); 98 InspectorBackend.getProfile(callId, this.profile.uid); 99} 100 101WebInspector.CPUProfileView.prototype = { 102 get statusBarItems() 103 { 104 return [this.viewSelectElement, this.percentButton.element, this.focusButton.element, this.excludeButton.element, this.resetButton.element]; 105 }, 106 107 get profile() 108 { 109 return this._profile; 110 }, 111 112 set profile(profile) 113 { 114 this._profile = profile; 115 }, 116 117 get bottomUpProfileDataGridTree() 118 { 119 if (!this._bottomUpProfileDataGridTree) 120 this._bottomUpProfileDataGridTree = new WebInspector.BottomUpProfileDataGridTree(this, this.profile.head); 121 return this._bottomUpProfileDataGridTree; 122 }, 123 124 get topDownProfileDataGridTree() 125 { 126 if (!this._topDownProfileDataGridTree) 127 this._topDownProfileDataGridTree = new WebInspector.TopDownProfileDataGridTree(this, this.profile.head); 128 return this._topDownProfileDataGridTree; 129 }, 130 131 get currentTree() 132 { 133 return this._currentTree; 134 }, 135 136 set currentTree(tree) 137 { 138 this._currentTree = tree; 139 this.refresh(); 140 }, 141 142 get topDownTree() 143 { 144 if (!this._topDownTree) { 145 this._topDownTree = WebInspector.TopDownTreeFactory.create(this.profile.head); 146 this._sortProfile(this._topDownTree); 147 } 148 149 return this._topDownTree; 150 }, 151 152 get bottomUpTree() 153 { 154 if (!this._bottomUpTree) { 155 this._bottomUpTree = WebInspector.BottomUpTreeFactory.create(this.profile.head); 156 this._sortProfile(this._bottomUpTree); 157 } 158 159 return this._bottomUpTree; 160 }, 161 162 show: function(parentElement) 163 { 164 WebInspector.View.prototype.show.call(this, parentElement); 165 this.dataGrid.updateWidths(); 166 }, 167 168 hide: function() 169 { 170 WebInspector.View.prototype.hide.call(this); 171 this._currentSearchResultIndex = -1; 172 }, 173 174 resize: function() 175 { 176 if (this.dataGrid) 177 this.dataGrid.updateWidths(); 178 }, 179 180 refresh: function() 181 { 182 var selectedProfileNode = this.dataGrid.selectedNode ? this.dataGrid.selectedNode.profileNode : null; 183 184 this.dataGrid.removeChildren(); 185 186 var children = this.profileDataGridTree.children; 187 var count = children.length; 188 189 for (var index = 0; index < count; ++index) 190 this.dataGrid.appendChild(children[index]); 191 192 if (selectedProfileNode) 193 selectedProfileNode.selected = true; 194 }, 195 196 refreshVisibleData: function() 197 { 198 var child = this.dataGrid.children[0]; 199 while (child) { 200 child.refresh(); 201 child = child.traverseNextNode(false, null, true); 202 } 203 }, 204 205 refreshShowAsPercents: function() 206 { 207 this._updatePercentButton(); 208 this.refreshVisibleData(); 209 }, 210 211 searchCanceled: function() 212 { 213 if (this._searchResults) { 214 for (var i = 0; i < this._searchResults.length; ++i) { 215 var profileNode = this._searchResults[i].profileNode; 216 217 delete profileNode._searchMatchedSelfColumn; 218 delete profileNode._searchMatchedTotalColumn; 219 delete profileNode._searchMatchedCallsColumn; 220 delete profileNode._searchMatchedFunctionColumn; 221 222 profileNode.refresh(); 223 } 224 } 225 226 delete this._searchFinishedCallback; 227 this._currentSearchResultIndex = -1; 228 this._searchResults = []; 229 }, 230 231 performSearch: function(query, finishedCallback) 232 { 233 // Call searchCanceled since it will reset everything we need before doing a new search. 234 this.searchCanceled(); 235 236 query = query.trim(); 237 238 if (!query.length) 239 return; 240 241 this._searchFinishedCallback = finishedCallback; 242 243 var greaterThan = (query.indexOf(">") === 0); 244 var lessThan = (query.indexOf("<") === 0); 245 var equalTo = (query.indexOf("=") === 0 || ((greaterThan || lessThan) && query.indexOf("=") === 1)); 246 var percentUnits = (query.lastIndexOf("%") === (query.length - 1)); 247 var millisecondsUnits = (query.length > 2 && query.lastIndexOf("ms") === (query.length - 2)); 248 var secondsUnits = (!millisecondsUnits && query.lastIndexOf("s") === (query.length - 1)); 249 250 var queryNumber = parseFloat(query); 251 if (greaterThan || lessThan || equalTo) { 252 if (equalTo && (greaterThan || lessThan)) 253 queryNumber = parseFloat(query.substring(2)); 254 else 255 queryNumber = parseFloat(query.substring(1)); 256 } 257 258 var queryNumberMilliseconds = (secondsUnits ? (queryNumber * 1000) : queryNumber); 259 260 // Make equalTo implicitly true if it wasn't specified there is no other operator. 261 if (!isNaN(queryNumber) && !(greaterThan || lessThan)) 262 equalTo = true; 263 264 function matchesQuery(/*ProfileDataGridNode*/ profileDataGridNode) 265 { 266 delete profileDataGridNode._searchMatchedSelfColumn; 267 delete profileDataGridNode._searchMatchedTotalColumn; 268 delete profileDataGridNode._searchMatchedAverageColumn; 269 delete profileDataGridNode._searchMatchedCallsColumn; 270 delete profileDataGridNode._searchMatchedFunctionColumn; 271 272 if (percentUnits) { 273 if (lessThan) { 274 if (profileDataGridNode.selfPercent < queryNumber) 275 profileDataGridNode._searchMatchedSelfColumn = true; 276 if (profileDataGridNode.totalPercent < queryNumber) 277 profileDataGridNode._searchMatchedTotalColumn = true; 278 if (profileDataGridNode.averagePercent < queryNumberMilliseconds) 279 profileDataGridNode._searchMatchedAverageColumn = true; 280 } else if (greaterThan) { 281 if (profileDataGridNode.selfPercent > queryNumber) 282 profileDataGridNode._searchMatchedSelfColumn = true; 283 if (profileDataGridNode.totalPercent > queryNumber) 284 profileDataGridNode._searchMatchedTotalColumn = true; 285 if (profileDataGridNode.averagePercent < queryNumberMilliseconds) 286 profileDataGridNode._searchMatchedAverageColumn = true; 287 } 288 289 if (equalTo) { 290 if (profileDataGridNode.selfPercent == queryNumber) 291 profileDataGridNode._searchMatchedSelfColumn = true; 292 if (profileDataGridNode.totalPercent == queryNumber) 293 profileDataGridNode._searchMatchedTotalColumn = true; 294 if (profileDataGridNode.averagePercent < queryNumberMilliseconds) 295 profileDataGridNode._searchMatchedAverageColumn = true; 296 } 297 } else if (millisecondsUnits || secondsUnits) { 298 if (lessThan) { 299 if (profileDataGridNode.selfTime < queryNumberMilliseconds) 300 profileDataGridNode._searchMatchedSelfColumn = true; 301 if (profileDataGridNode.totalTime < queryNumberMilliseconds) 302 profileDataGridNode._searchMatchedTotalColumn = true; 303 if (profileDataGridNode.averageTime < queryNumberMilliseconds) 304 profileDataGridNode._searchMatchedAverageColumn = true; 305 } else if (greaterThan) { 306 if (profileDataGridNode.selfTime > queryNumberMilliseconds) 307 profileDataGridNode._searchMatchedSelfColumn = true; 308 if (profileDataGridNode.totalTime > queryNumberMilliseconds) 309 profileDataGridNode._searchMatchedTotalColumn = true; 310 if (profileDataGridNode.averageTime > queryNumberMilliseconds) 311 profileDataGridNode._searchMatchedAverageColumn = true; 312 } 313 314 if (equalTo) { 315 if (profileDataGridNode.selfTime == queryNumberMilliseconds) 316 profileDataGridNode._searchMatchedSelfColumn = true; 317 if (profileDataGridNode.totalTime == queryNumberMilliseconds) 318 profileDataGridNode._searchMatchedTotalColumn = true; 319 if (profileDataGridNode.averageTime == queryNumberMilliseconds) 320 profileDataGridNode._searchMatchedAverageColumn = true; 321 } 322 } else { 323 if (equalTo && profileDataGridNode.numberOfCalls == queryNumber) 324 profileDataGridNode._searchMatchedCallsColumn = true; 325 if (greaterThan && profileDataGridNode.numberOfCalls > queryNumber) 326 profileDataGridNode._searchMatchedCallsColumn = true; 327 if (lessThan && profileDataGridNode.numberOfCalls < queryNumber) 328 profileDataGridNode._searchMatchedCallsColumn = true; 329 } 330 331 if (profileDataGridNode.functionName.hasSubstring(query, true) || profileDataGridNode.url.hasSubstring(query, true)) 332 profileDataGridNode._searchMatchedFunctionColumn = true; 333 334 if (profileDataGridNode._searchMatchedSelfColumn || 335 profileDataGridNode._searchMatchedTotalColumn || 336 profileDataGridNode._searchMatchedAverageColumn || 337 profileDataGridNode._searchMatchedCallsColumn || 338 profileDataGridNode._searchMatchedFunctionColumn) 339 { 340 profileDataGridNode.refresh(); 341 return true; 342 } 343 344 return false; 345 } 346 347 var current = this.profileDataGridTree.children[0]; 348 349 while (current) { 350 if (matchesQuery(current)) { 351 this._searchResults.push({ profileNode: current }); 352 } 353 354 current = current.traverseNextNode(false, null, false); 355 } 356 357 finishedCallback(this, this._searchResults.length); 358 }, 359 360 jumpToFirstSearchResult: function() 361 { 362 if (!this._searchResults || !this._searchResults.length) 363 return; 364 this._currentSearchResultIndex = 0; 365 this._jumpToSearchResult(this._currentSearchResultIndex); 366 }, 367 368 jumpToLastSearchResult: function() 369 { 370 if (!this._searchResults || !this._searchResults.length) 371 return; 372 this._currentSearchResultIndex = (this._searchResults.length - 1); 373 this._jumpToSearchResult(this._currentSearchResultIndex); 374 }, 375 376 jumpToNextSearchResult: function() 377 { 378 if (!this._searchResults || !this._searchResults.length) 379 return; 380 if (++this._currentSearchResultIndex >= this._searchResults.length) 381 this._currentSearchResultIndex = 0; 382 this._jumpToSearchResult(this._currentSearchResultIndex); 383 }, 384 385 jumpToPreviousSearchResult: function() 386 { 387 if (!this._searchResults || !this._searchResults.length) 388 return; 389 if (--this._currentSearchResultIndex < 0) 390 this._currentSearchResultIndex = (this._searchResults.length - 1); 391 this._jumpToSearchResult(this._currentSearchResultIndex); 392 }, 393 394 showingFirstSearchResult: function() 395 { 396 return (this._currentSearchResultIndex === 0); 397 }, 398 399 showingLastSearchResult: function() 400 { 401 return (this._searchResults && this._currentSearchResultIndex === (this._searchResults.length - 1)); 402 }, 403 404 _jumpToSearchResult: function(index) 405 { 406 var searchResult = this._searchResults[index]; 407 if (!searchResult) 408 return; 409 410 var profileNode = searchResult.profileNode; 411 profileNode.reveal(); 412 profileNode.select(); 413 }, 414 415 _changeView: function(event) 416 { 417 if (!event || !this.profile) 418 return; 419 420 if (event.target.selectedIndex == 1 && this.view == "Heavy") { 421 this.profileDataGridTree = this.topDownProfileDataGridTree; 422 this._sortProfile(); 423 this.view = "Tree"; 424 } else if (event.target.selectedIndex == 0 && this.view == "Tree") { 425 this.profileDataGridTree = this.bottomUpProfileDataGridTree; 426 this._sortProfile(); 427 this.view = "Heavy"; 428 } 429 430 if (!this.currentQuery || !this._searchFinishedCallback || !this._searchResults) 431 return; 432 433 // The current search needs to be performed again. First negate out previous match 434 // count by calling the search finished callback with a negative number of matches. 435 // Then perform the search again the with same query and callback. 436 this._searchFinishedCallback(this, -this._searchResults.length); 437 this.performSearch(this.currentQuery, this._searchFinishedCallback); 438 }, 439 440 _percentClicked: function(event) 441 { 442 var currentState = this.showSelfTimeAsPercent && this.showTotalTimeAsPercent && this.showAverageTimeAsPercent; 443 this.showSelfTimeAsPercent = !currentState; 444 this.showTotalTimeAsPercent = !currentState; 445 this.showAverageTimeAsPercent = !currentState; 446 this.refreshShowAsPercents(); 447 }, 448 449 _updatePercentButton: function() 450 { 451 if (this.showSelfTimeAsPercent && this.showTotalTimeAsPercent && this.showAverageTimeAsPercent) { 452 this.percentButton.title = WebInspector.UIString("Show absolute total and self times."); 453 this.percentButton.toggled = true; 454 } else { 455 this.percentButton.title = WebInspector.UIString("Show total and self times as percentages."); 456 this.percentButton.toggled = false; 457 } 458 }, 459 460 _focusClicked: function(event) 461 { 462 if (!this.dataGrid.selectedNode) 463 return; 464 465 this.resetButton.visible = true; 466 this.profileDataGridTree.focus(this.dataGrid.selectedNode); 467 this.refresh(); 468 this.refreshVisibleData(); 469 }, 470 471 _excludeClicked: function(event) 472 { 473 var selectedNode = this.dataGrid.selectedNode 474 475 if (!selectedNode) 476 return; 477 478 selectedNode.deselect(); 479 480 this.resetButton.visible = true; 481 this.profileDataGridTree.exclude(selectedNode); 482 this.refresh(); 483 this.refreshVisibleData(); 484 }, 485 486 _resetClicked: function(event) 487 { 488 this.resetButton.visible = false; 489 this.profileDataGridTree.restore(); 490 this.refresh(); 491 this.refreshVisibleData(); 492 }, 493 494 _dataGridNodeSelected: function(node) 495 { 496 this.focusButton.disabled = false; 497 this.excludeButton.disabled = false; 498 }, 499 500 _dataGridNodeDeselected: function(node) 501 { 502 this.focusButton.disabled = true; 503 this.excludeButton.disabled = true; 504 }, 505 506 _sortData: function(event) 507 { 508 this._sortProfile(this.profile); 509 }, 510 511 _sortProfile: function() 512 { 513 var sortAscending = this.dataGrid.sortOrder === "ascending"; 514 var sortColumnIdentifier = this.dataGrid.sortColumnIdentifier; 515 var sortProperty = { 516 "average": "averageTime", 517 "self": "selfTime", 518 "total": "totalTime", 519 "calls": "numberOfCalls", 520 "function": "functionName" 521 }[sortColumnIdentifier]; 522 523 this.profileDataGridTree.sort(WebInspector.ProfileDataGridTree.propertyComparator(sortProperty, sortAscending)); 524 525 this.refresh(); 526 }, 527 528 _mouseDownInDataGrid: function(event) 529 { 530 if (event.detail < 2) 531 return; 532 533 var cell = event.target.enclosingNodeOrSelfWithNodeName("td"); 534 if (!cell || (!cell.hasStyleClass("total-column") && !cell.hasStyleClass("self-column") && !cell.hasStyleClass("average-column"))) 535 return; 536 537 if (cell.hasStyleClass("total-column")) 538 this.showTotalTimeAsPercent = !this.showTotalTimeAsPercent; 539 else if (cell.hasStyleClass("self-column")) 540 this.showSelfTimeAsPercent = !this.showSelfTimeAsPercent; 541 else if (cell.hasStyleClass("average-column")) 542 this.showAverageTimeAsPercent = !this.showAverageTimeAsPercent; 543 544 this.refreshShowAsPercents(); 545 546 event.preventDefault(); 547 event.stopPropagation(); 548 }, 549 550 _assignParentsInProfile: function() 551 { 552 var head = this.profile.head; 553 head.parent = null; 554 head.head = null; 555 var nodesToTraverse = [ { parent: head, children: head.children } ]; 556 while (nodesToTraverse.length > 0) { 557 var pair = nodesToTraverse.shift(); 558 var parent = pair.parent; 559 var children = pair.children; 560 var length = children.length; 561 for (var i = 0; i < length; ++i) { 562 children[i].head = head; 563 children[i].parent = parent; 564 if (children[i].children.length > 0) 565 nodesToTraverse.push({ parent: children[i], children: children[i].children }); 566 } 567 } 568 } 569} 570 571WebInspector.CPUProfileView.prototype.__proto__ = WebInspector.View.prototype; 572 573WebInspector.CPUProfileType = function() 574{ 575 WebInspector.ProfileType.call(this, WebInspector.CPUProfileType.TypeId, WebInspector.UIString("CPU PROFILES")); 576 this._recording = false; 577} 578 579WebInspector.CPUProfileType.TypeId = "CPU"; 580 581WebInspector.CPUProfileType.prototype = { 582 get buttonTooltip() 583 { 584 return this._recording ? WebInspector.UIString("Stop profiling.") : WebInspector.UIString("Start profiling."); 585 }, 586 587 get buttonStyle() 588 { 589 return this._recording ? "record-profile-status-bar-item status-bar-item toggled-on" : "record-profile-status-bar-item status-bar-item"; 590 }, 591 592 buttonClicked: function() 593 { 594 this._recording = !this._recording; 595 596 if (this._recording) 597 InspectorBackend.startProfiling(); 598 else 599 InspectorBackend.stopProfiling(); 600 }, 601 602 get welcomeMessage() 603 { 604 return WebInspector.UIString("Start CPU profiling by pressing<br>the %s button on the status bar."); 605 }, 606 607 setRecordingProfile: function(isProfiling) 608 { 609 this._recording = isProfiling; 610 }, 611 612 createSidebarTreeElementForProfile: function(profile) 613 { 614 return new WebInspector.ProfileSidebarTreeElement(profile); 615 }, 616 617 createView: function(profile) 618 { 619 return new WebInspector.CPUProfileView(profile); 620 } 621} 622 623WebInspector.CPUProfileType.prototype.__proto__ = WebInspector.ProfileType.prototype; 624