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 26WebInspector.ProfileView = function(profile) 27{ 28 WebInspector.View.call(this); 29 30 this.element.addStyleClass("profile-view"); 31 32 this.showSelfTimeAsPercent = true; 33 this.showTotalTimeAsPercent = true; 34 this.showAverageTimeAsPercent = true; 35 36 var columns = { "self": { title: WebInspector.UIString("Self"), width: "72px", sort: "descending", sortable: true }, 37 "total": { title: WebInspector.UIString("Total"), width: "72px", sortable: true }, 38 "average": { title: WebInspector.UIString("Average"), width: "72px", sortable: true }, 39 "calls": { title: WebInspector.UIString("Calls"), width: "54px", sortable: true }, 40 "function": { title: WebInspector.UIString("Function"), disclosure: true, sortable: true } }; 41 42 this.dataGrid = new WebInspector.DataGrid(columns); 43 this.dataGrid.addEventListener("sorting changed", this._sortData, this); 44 this.dataGrid.element.addEventListener("mousedown", this._mouseDownInDataGrid.bind(this), true); 45 this.element.appendChild(this.dataGrid.element); 46 47 this.viewSelectElement = document.createElement("select"); 48 this.viewSelectElement.className = "status-bar-item"; 49 this.viewSelectElement.addEventListener("change", this._changeView.bind(this), false); 50 this.view = "Heavy"; 51 52 var heavyViewOption = document.createElement("option"); 53 heavyViewOption.label = WebInspector.UIString("Heavy (Bottom Up)"); 54 var treeViewOption = document.createElement("option"); 55 treeViewOption.label = WebInspector.UIString("Tree (Top Down)"); 56 this.viewSelectElement.appendChild(heavyViewOption); 57 this.viewSelectElement.appendChild(treeViewOption); 58 59 this.percentButton = document.createElement("button"); 60 this.percentButton.className = "percent-time-status-bar-item status-bar-item"; 61 this.percentButton.addEventListener("click", this._percentClicked.bind(this), false); 62 63 this.focusButton = document.createElement("button"); 64 this.focusButton.title = WebInspector.UIString("Focus selected function."); 65 this.focusButton.className = "focus-profile-node-status-bar-item status-bar-item"; 66 this.focusButton.disabled = true; 67 this.focusButton.addEventListener("click", this._focusClicked.bind(this), false); 68 69 this.excludeButton = document.createElement("button"); 70 this.excludeButton.title = WebInspector.UIString("Exclude selected function."); 71 this.excludeButton.className = "exclude-profile-node-status-bar-item status-bar-item"; 72 this.excludeButton.disabled = true; 73 this.excludeButton.addEventListener("click", this._excludeClicked.bind(this), false); 74 75 this.resetButton = document.createElement("button"); 76 this.resetButton.title = WebInspector.UIString("Restore all functions."); 77 this.resetButton.className = "reset-profile-status-bar-item status-bar-item hidden"; 78 this.resetButton.addEventListener("click", this._resetClicked.bind(this), false); 79 80 this.profile = profile; 81 82 this.profileDataGridTree = this.bottomUpProfileDataGridTree; 83 this.profileDataGridTree.sort(WebInspector.ProfileDataGridTree.propertyComparator("selfTime", false)); 84 85 this.refresh(); 86 87 this._updatePercentButton(); 88} 89 90WebInspector.ProfileView.prototype = { 91 get statusBarItems() 92 { 93 return [this.viewSelectElement, this.percentButton, this.focusButton, this.excludeButton, this.resetButton]; 94 }, 95 96 get profile() 97 { 98 return this._profile; 99 }, 100 101 set profile(profile) 102 { 103 this._profile = profile; 104 }, 105 106 get bottomUpProfileDataGridTree() 107 { 108 if (!this._bottomUpProfileDataGridTree) 109 this._bottomUpProfileDataGridTree = new WebInspector.BottomUpProfileDataGridTree(this, this.profile.head); 110 return this._bottomUpProfileDataGridTree; 111 }, 112 113 get topDownProfileDataGridTree() 114 { 115 if (!this._topDownProfileDataGridTree) 116 this._topDownProfileDataGridTree = new WebInspector.TopDownProfileDataGridTree(this, this.profile.head); 117 return this._topDownProfileDataGridTree; 118 }, 119 120 get currentTree() 121 { 122 return this._currentTree; 123 }, 124 125 set currentTree(tree) 126 { 127 this._currentTree = tree; 128 this.refresh(); 129 }, 130 131 get topDownTree() 132 { 133 if (!this._topDownTree) { 134 this._topDownTree = WebInspector.TopDownTreeFactory.create(this.profile.head); 135 this._sortProfile(this._topDownTree); 136 } 137 138 return this._topDownTree; 139 }, 140 141 get bottomUpTree() 142 { 143 if (!this._bottomUpTree) { 144 this._bottomUpTree = WebInspector.BottomUpTreeFactory.create(this.profile.head); 145 this._sortProfile(this._bottomUpTree); 146 } 147 148 return this._bottomUpTree; 149 }, 150 151 show: function(parentElement) 152 { 153 WebInspector.View.prototype.show.call(this, parentElement); 154 this.dataGrid.updateWidths(); 155 }, 156 157 hide: function() 158 { 159 WebInspector.View.prototype.hide.call(this); 160 this._currentSearchResultIndex = -1; 161 }, 162 163 resize: function() 164 { 165 if (this.dataGrid) 166 this.dataGrid.updateWidths(); 167 }, 168 169 refresh: function() 170 { 171 var selectedProfileNode = this.dataGrid.selectedNode ? this.dataGrid.selectedNode.profileNode : null; 172 173 this.dataGrid.removeChildren(); 174 175 var children = this.profileDataGridTree.children; 176 var count = children.length; 177 178 for (var index = 0; index < count; ++index) 179 this.dataGrid.appendChild(children[index]); 180 181 if (selectedProfileNode) 182 selectedProfileNode.selected = true; 183 }, 184 185 refreshVisibleData: function() 186 { 187 var child = this.dataGrid.children[0]; 188 while (child) { 189 child.refresh(); 190 child = child.traverseNextNode(false, null, true); 191 } 192 }, 193 194 refreshShowAsPercents: function() 195 { 196 this._updatePercentButton(); 197 this.refreshVisibleData(); 198 }, 199 200 searchCanceled: function() 201 { 202 if (this._searchResults) { 203 for (var i = 0; i < this._searchResults.length; ++i) { 204 var profileNode = this._searchResults[i].profileNode; 205 206 delete profileNode._searchMatchedSelfColumn; 207 delete profileNode._searchMatchedTotalColumn; 208 delete profileNode._searchMatchedCallsColumn; 209 delete profileNode._searchMatchedFunctionColumn; 210 211 profileNode.refresh(); 212 } 213 } 214 215 delete this._searchFinishedCallback; 216 this._currentSearchResultIndex = -1; 217 this._searchResults = []; 218 }, 219 220 performSearch: function(query, finishedCallback) 221 { 222 // Call searchCanceled since it will reset everything we need before doing a new search. 223 this.searchCanceled(); 224 225 query = query.trimWhitespace(); 226 227 if (!query.length) 228 return; 229 230 this._searchFinishedCallback = finishedCallback; 231 232 var greaterThan = (query.indexOf(">") === 0); 233 var lessThan = (query.indexOf("<") === 0); 234 var equalTo = (query.indexOf("=") === 0 || ((greaterThan || lessThan) && query.indexOf("=") === 1)); 235 var percentUnits = (query.lastIndexOf("%") === (query.length - 1)); 236 var millisecondsUnits = (query.length > 2 && query.lastIndexOf("ms") === (query.length - 2)); 237 var secondsUnits = (!millisecondsUnits && query.lastIndexOf("s") === (query.length - 1)); 238 239 var queryNumber = parseFloat(query); 240 if (greaterThan || lessThan || equalTo) { 241 if (equalTo && (greaterThan || lessThan)) 242 queryNumber = parseFloat(query.substring(2)); 243 else 244 queryNumber = parseFloat(query.substring(1)); 245 } 246 247 var queryNumberMilliseconds = (secondsUnits ? (queryNumber * 1000) : queryNumber); 248 249 // Make equalTo implicitly true if it wasn't specified there is no other operator. 250 if (!isNaN(queryNumber) && !(greaterThan || lessThan)) 251 equalTo = true; 252 253 function matchesQuery(/*ProfileDataGridNode*/ profileDataGridNode) 254 { 255 delete profileDataGridNode._searchMatchedSelfColumn; 256 delete profileDataGridNode._searchMatchedTotalColumn; 257 delete profileDataGridNode._searchMatchedAverageColumn; 258 delete profileDataGridNode._searchMatchedCallsColumn; 259 delete profileDataGridNode._searchMatchedFunctionColumn; 260 261 if (percentUnits) { 262 if (lessThan) { 263 if (profileDataGridNode.selfPercent < queryNumber) 264 profileDataGridNode._searchMatchedSelfColumn = true; 265 if (profileDataGridNode.totalPercent < queryNumber) 266 profileDataGridNode._searchMatchedTotalColumn = true; 267 if (profileDataGridNode.averagePercent < queryNumberMilliseconds) 268 profileDataGridNode._searchMatchedAverageColumn = true; 269 } else if (greaterThan) { 270 if (profileDataGridNode.selfPercent > queryNumber) 271 profileDataGridNode._searchMatchedSelfColumn = true; 272 if (profileDataGridNode.totalPercent > queryNumber) 273 profileDataGridNode._searchMatchedTotalColumn = true; 274 if (profileDataGridNode.averagePercent < queryNumberMilliseconds) 275 profileDataGridNode._searchMatchedAverageColumn = true; 276 } 277 278 if (equalTo) { 279 if (profileDataGridNode.selfPercent == queryNumber) 280 profileDataGridNode._searchMatchedSelfColumn = true; 281 if (profileDataGridNode.totalPercent == queryNumber) 282 profileDataGridNode._searchMatchedTotalColumn = true; 283 if (profileDataGridNode.averagePercent < queryNumberMilliseconds) 284 profileDataGridNode._searchMatchedAverageColumn = true; 285 } 286 } else if (millisecondsUnits || secondsUnits) { 287 if (lessThan) { 288 if (profileDataGridNode.selfTime < queryNumberMilliseconds) 289 profileDataGridNode._searchMatchedSelfColumn = true; 290 if (profileDataGridNode.totalTime < queryNumberMilliseconds) 291 profileDataGridNode._searchMatchedTotalColumn = true; 292 if (profileDataGridNode.averageTime < queryNumberMilliseconds) 293 profileDataGridNode._searchMatchedAverageColumn = true; 294 } else if (greaterThan) { 295 if (profileDataGridNode.selfTime > queryNumberMilliseconds) 296 profileDataGridNode._searchMatchedSelfColumn = true; 297 if (profileDataGridNode.totalTime > queryNumberMilliseconds) 298 profileDataGridNode._searchMatchedTotalColumn = true; 299 if (profileDataGridNode.averageTime > queryNumberMilliseconds) 300 profileDataGridNode._searchMatchedAverageColumn = true; 301 } 302 303 if (equalTo) { 304 if (profileDataGridNode.selfTime == queryNumberMilliseconds) 305 profileDataGridNode._searchMatchedSelfColumn = true; 306 if (profileDataGridNode.totalTime == queryNumberMilliseconds) 307 profileDataGridNode._searchMatchedTotalColumn = true; 308 if (profileDataGridNode.averageTime == queryNumberMilliseconds) 309 profileDataGridNode._searchMatchedAverageColumn = true; 310 } 311 } else { 312 if (equalTo && profileDataGridNode.numberOfCalls == queryNumber) 313 profileDataGridNode._searchMatchedCallsColumn = true; 314 if (greaterThan && profileDataGridNode.numberOfCalls > queryNumber) 315 profileDataGridNode._searchMatchedCallsColumn = true; 316 if (lessThan && profileDataGridNode.numberOfCalls < queryNumber) 317 profileDataGridNode._searchMatchedCallsColumn = true; 318 } 319 320 if (profileDataGridNode.functionName.hasSubstring(query, true) || profileDataGridNode.url.hasSubstring(query, true)) 321 profileDataGridNode._searchMatchedFunctionColumn = true; 322 323 if (profileDataGridNode._searchMatchedSelfColumn || 324 profileDataGridNode._searchMatchedTotalColumn || 325 profileDataGridNode._searchMatchedAverageColumn || 326 profileDataGridNode._searchMatchedCallsColumn || 327 profileDataGridNode._searchMatchedFunctionColumn) 328 { 329 profileDataGridNode.refresh(); 330 return true; 331 } 332 333 return false; 334 } 335 336 var current = this.profileDataGridTree.children[0]; 337 338 while (current) { 339 if (matchesQuery(current)) { 340 this._searchResults.push({ profileNode: current }); 341 } 342 343 current = current.traverseNextNode(false, null, false); 344 } 345 346 finishedCallback(this, this._searchResults.length); 347 }, 348 349 jumpToFirstSearchResult: function() 350 { 351 if (!this._searchResults || !this._searchResults.length) 352 return; 353 this._currentSearchResultIndex = 0; 354 this._jumpToSearchResult(this._currentSearchResultIndex); 355 }, 356 357 jumpToLastSearchResult: function() 358 { 359 if (!this._searchResults || !this._searchResults.length) 360 return; 361 this._currentSearchResultIndex = (this._searchResults.length - 1); 362 this._jumpToSearchResult(this._currentSearchResultIndex); 363 }, 364 365 jumpToNextSearchResult: function() 366 { 367 if (!this._searchResults || !this._searchResults.length) 368 return; 369 if (++this._currentSearchResultIndex >= this._searchResults.length) 370 this._currentSearchResultIndex = 0; 371 this._jumpToSearchResult(this._currentSearchResultIndex); 372 }, 373 374 jumpToPreviousSearchResult: function() 375 { 376 if (!this._searchResults || !this._searchResults.length) 377 return; 378 if (--this._currentSearchResultIndex < 0) 379 this._currentSearchResultIndex = (this._searchResults.length - 1); 380 this._jumpToSearchResult(this._currentSearchResultIndex); 381 }, 382 383 showingFirstSearchResult: function() 384 { 385 return (this._currentSearchResultIndex === 0); 386 }, 387 388 showingLastSearchResult: function() 389 { 390 return (this._searchResults && this._currentSearchResultIndex === (this._searchResults.length - 1)); 391 }, 392 393 _jumpToSearchResult: function(index) 394 { 395 var searchResult = this._searchResults[index]; 396 if (!searchResult) 397 return; 398 399 var profileNode = searchResult.profileNode; 400 profileNode.reveal(); 401 profileNode.select(); 402 }, 403 404 _changeView: function(event) 405 { 406 if (!event || !this.profile) 407 return; 408 409 if (event.target.selectedIndex == 1 && this.view == "Heavy") { 410 this.profileDataGridTree = this.topDownProfileDataGridTree; 411 this._sortProfile(); 412 this.view = "Tree"; 413 } else if (event.target.selectedIndex == 0 && this.view == "Tree") { 414 this.profileDataGridTree = this.bottomUpProfileDataGridTree; 415 this._sortProfile(); 416 this.view = "Heavy"; 417 } 418 419 if (!this.currentQuery || !this._searchFinishedCallback || !this._searchResults) 420 return; 421 422 // The current search needs to be performed again. First negate out previous match 423 // count by calling the search finished callback with a negative number of matches. 424 // Then perform the search again the with same query and callback. 425 this._searchFinishedCallback(this, -this._searchResults.length); 426 this.performSearch(this.currentQuery, this._searchFinishedCallback); 427 }, 428 429 _percentClicked: function(event) 430 { 431 var currentState = this.showSelfTimeAsPercent && this.showTotalTimeAsPercent && this.showAverageTimeAsPercent; 432 this.showSelfTimeAsPercent = !currentState; 433 this.showTotalTimeAsPercent = !currentState; 434 this.showAverageTimeAsPercent = !currentState; 435 this.refreshShowAsPercents(); 436 }, 437 438 _updatePercentButton: function() 439 { 440 if (this.showSelfTimeAsPercent && this.showTotalTimeAsPercent && this.showAverageTimeAsPercent) { 441 this.percentButton.title = WebInspector.UIString("Show absolute total and self times."); 442 this.percentButton.addStyleClass("toggled-on"); 443 } else { 444 this.percentButton.title = WebInspector.UIString("Show total and self times as percentages."); 445 this.percentButton.removeStyleClass("toggled-on"); 446 } 447 }, 448 449 _focusClicked: function(event) 450 { 451 if (!this.dataGrid.selectedNode) 452 return; 453 454 this.resetButton.removeStyleClass("hidden"); 455 this.profileDataGridTree.focus(this.dataGrid.selectedNode); 456 this.refresh(); 457 this.refreshVisibleData(); 458 }, 459 460 _excludeClicked: function(event) 461 { 462 var selectedNode = this.dataGrid.selectedNode 463 464 if (!selectedNode) 465 return; 466 467 selectedNode.deselect(); 468 469 this.resetButton.removeStyleClass("hidden"); 470 this.profileDataGridTree.exclude(selectedNode); 471 this.refresh(); 472 this.refreshVisibleData(); 473 }, 474 475 _resetClicked: function(event) 476 { 477 this.resetButton.addStyleClass("hidden"); 478 this.profileDataGridTree.restore(); 479 this.refresh(); 480 this.refreshVisibleData(); 481 }, 482 483 _dataGridNodeSelected: function(node) 484 { 485 this.focusButton.disabled = false; 486 this.excludeButton.disabled = false; 487 }, 488 489 _dataGridNodeDeselected: function(node) 490 { 491 this.focusButton.disabled = true; 492 this.excludeButton.disabled = true; 493 }, 494 495 _sortData: function(event) 496 { 497 this._sortProfile(this.profile); 498 }, 499 500 _sortProfile: function() 501 { 502 var sortAscending = this.dataGrid.sortOrder === "ascending"; 503 var sortColumnIdentifier = this.dataGrid.sortColumnIdentifier; 504 var sortProperty = { 505 "average": "averageTime", 506 "self": "selfTime", 507 "total": "totalTime", 508 "calls": "numberOfCalls", 509 "function": "functionName" 510 }[sortColumnIdentifier]; 511 512 this.profileDataGridTree.sort(WebInspector.ProfileDataGridTree.propertyComparator(sortProperty, sortAscending)); 513 514 this.refresh(); 515 }, 516 517 _mouseDownInDataGrid: function(event) 518 { 519 if (event.detail < 2) 520 return; 521 522 var cell = event.target.enclosingNodeOrSelfWithNodeName("td"); 523 if (!cell || (!cell.hasStyleClass("total-column") && !cell.hasStyleClass("self-column") && !cell.hasStyleClass("average-column"))) 524 return; 525 526 if (cell.hasStyleClass("total-column")) 527 this.showTotalTimeAsPercent = !this.showTotalTimeAsPercent; 528 else if (cell.hasStyleClass("self-column")) 529 this.showSelfTimeAsPercent = !this.showSelfTimeAsPercent; 530 else if (cell.hasStyleClass("average-column")) 531 this.showAverageTimeAsPercent = !this.showAverageTimeAsPercent; 532 533 this.refreshShowAsPercents(); 534 535 event.preventDefault(); 536 event.stopPropagation(); 537 } 538} 539 540WebInspector.ProfileView.prototype.__proto__ = WebInspector.View.prototype; 541