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/** 27 * @constructor 28 * @extends {WebInspector.View} 29 * @param {!WebInspector.CPUProfileHeader} profileHeader 30 */ 31WebInspector.CPUProfileView = function(profileHeader) 32{ 33 WebInspector.View.call(this); 34 35 this.element.classList.add("profile-view"); 36 37 this.showSelfTimeAsPercent = WebInspector.settings.createSetting("cpuProfilerShowSelfTimeAsPercent", true); 38 this.showTotalTimeAsPercent = WebInspector.settings.createSetting("cpuProfilerShowTotalTimeAsPercent", true); 39 this.showAverageTimeAsPercent = WebInspector.settings.createSetting("cpuProfilerShowAverageTimeAsPercent", true); 40 this._viewType = WebInspector.settings.createSetting("cpuProfilerView", WebInspector.CPUProfileView._TypeHeavy); 41 42 var columns = []; 43 columns.push({id: "self", title: WebInspector.UIString("Self"), width: "72px", sort: WebInspector.DataGrid.Order.Descending, sortable: true}); 44 columns.push({id: "total", title: WebInspector.UIString("Total"), width: "72px", sortable: true}); 45 columns.push({id: "function", title: WebInspector.UIString("Function"), disclosure: true, sortable: true}); 46 47 this.dataGrid = new WebInspector.DataGrid(columns); 48 this.dataGrid.addEventListener(WebInspector.DataGrid.Events.SortingChanged, this._sortProfile, this); 49 this.dataGrid.element.addEventListener("mousedown", this._mouseDownInDataGrid.bind(this), true); 50 this.dataGrid.show(this.element); 51 52 this.viewSelectComboBox = new WebInspector.StatusBarComboBox(this._changeView.bind(this)); 53 54 var options = {}; 55 options[WebInspector.CPUProfileView._TypeFlame] = this.viewSelectComboBox.createOption(WebInspector.UIString("Flame Chart"), "", WebInspector.CPUProfileView._TypeFlame); 56 options[WebInspector.CPUProfileView._TypeHeavy] = this.viewSelectComboBox.createOption(WebInspector.UIString("Heavy (Bottom Up)"), "", WebInspector.CPUProfileView._TypeHeavy); 57 options[WebInspector.CPUProfileView._TypeTree] = this.viewSelectComboBox.createOption(WebInspector.UIString("Tree (Top Down)"), "", WebInspector.CPUProfileView._TypeTree); 58 59 var optionName = this._viewType.get() || WebInspector.CPUProfileView._TypeFlame; 60 var option = options[optionName] || options[WebInspector.CPUProfileView._TypeFlame]; 61 this.viewSelectComboBox.select(option); 62 63 this._statusBarButtonsElement = document.createElement("span"); 64 65 this.percentButton = new WebInspector.StatusBarButton("", "percent-time-status-bar-item"); 66 this.percentButton.addEventListener("click", this._percentClicked, this); 67 this._statusBarButtonsElement.appendChild(this.percentButton.element); 68 69 this.focusButton = new WebInspector.StatusBarButton(WebInspector.UIString("Focus selected function."), "focus-profile-node-status-bar-item"); 70 this.focusButton.setEnabled(false); 71 this.focusButton.addEventListener("click", this._focusClicked, this); 72 this._statusBarButtonsElement.appendChild(this.focusButton.element); 73 74 this.excludeButton = new WebInspector.StatusBarButton(WebInspector.UIString("Exclude selected function."), "exclude-profile-node-status-bar-item"); 75 this.excludeButton.setEnabled(false); 76 this.excludeButton.addEventListener("click", this._excludeClicked, this); 77 this._statusBarButtonsElement.appendChild(this.excludeButton.element); 78 79 this.resetButton = new WebInspector.StatusBarButton(WebInspector.UIString("Restore all functions."), "reset-profile-status-bar-item"); 80 this.resetButton.visible = false; 81 this.resetButton.addEventListener("click", this._resetClicked, this); 82 this._statusBarButtonsElement.appendChild(this.resetButton.element); 83 84 this.profileHead = /** @type {?ProfilerAgent.CPUProfileNode} */ (null); 85 this.profile = profileHeader; 86 87 this._linkifier = new WebInspector.Linkifier(new WebInspector.Linkifier.DefaultFormatter(30)); 88 89 if (this.profile._profile) // If the profile has been loaded from file then use it. 90 this._processProfileData(this.profile._profile); 91 else 92 this._processProfileData(this.profile.protocolProfile()); 93} 94 95WebInspector.CPUProfileView._TypeFlame = "Flame"; 96WebInspector.CPUProfileView._TypeTree = "Tree"; 97WebInspector.CPUProfileView._TypeHeavy = "Heavy"; 98 99WebInspector.CPUProfileView.prototype = { 100 /** 101 * @param {!number} timeLeft 102 * @param {!number} timeRight 103 */ 104 selectRange: function(timeLeft, timeRight) 105 { 106 if (!this._flameChart) 107 return; 108 this._flameChart.selectRange(timeLeft, timeRight); 109 }, 110 111 _revealProfilerNode: function(event) 112 { 113 var current = this.profileDataGridTree.children[0]; 114 115 while (current && current.profileNode !== event.data) 116 current = current.traverseNextNode(false, null, false); 117 118 if (current) 119 current.revealAndSelect(); 120 }, 121 122 /** 123 * @param {?ProfilerAgent.CPUProfile} profile 124 */ 125 _processProfileData: function(profile) 126 { 127 this.profileHead = profile.head; 128 this.samples = profile.samples; 129 130 this._calculateTimes(profile); 131 132 this._assignParentsInProfile(); 133 if (this.samples) 134 this._buildIdToNodeMap(); 135 this._changeView(); 136 this._updatePercentButton(); 137 if (this._flameChart) 138 this._flameChart.update(); 139 }, 140 141 get statusBarItems() 142 { 143 return [this.viewSelectComboBox.element, this._statusBarButtonsElement]; 144 }, 145 146 /** 147 * @return {!WebInspector.ProfileDataGridTree} 148 */ 149 _getBottomUpProfileDataGridTree: function() 150 { 151 if (!this._bottomUpProfileDataGridTree) 152 this._bottomUpProfileDataGridTree = new WebInspector.BottomUpProfileDataGridTree(this, /** @type {!ProfilerAgent.CPUProfileNode} */ (this.profileHead)); 153 return this._bottomUpProfileDataGridTree; 154 }, 155 156 /** 157 * @return {!WebInspector.ProfileDataGridTree} 158 */ 159 _getTopDownProfileDataGridTree: function() 160 { 161 if (!this._topDownProfileDataGridTree) 162 this._topDownProfileDataGridTree = new WebInspector.TopDownProfileDataGridTree(this, /** @type {!ProfilerAgent.CPUProfileNode} */ (this.profileHead)); 163 return this._topDownProfileDataGridTree; 164 }, 165 166 willHide: function() 167 { 168 this._currentSearchResultIndex = -1; 169 }, 170 171 refresh: function() 172 { 173 var selectedProfileNode = this.dataGrid.selectedNode ? this.dataGrid.selectedNode.profileNode : null; 174 175 this.dataGrid.rootNode().removeChildren(); 176 177 var children = this.profileDataGridTree.children; 178 var count = children.length; 179 180 for (var index = 0; index < count; ++index) 181 this.dataGrid.rootNode().appendChild(children[index]); 182 183 if (selectedProfileNode) 184 selectedProfileNode.selected = true; 185 }, 186 187 refreshVisibleData: function() 188 { 189 var child = this.dataGrid.rootNode().children[0]; 190 while (child) { 191 child.refresh(); 192 child = child.traverseNextNode(false, null, true); 193 } 194 }, 195 196 refreshShowAsPercents: function() 197 { 198 this._updatePercentButton(); 199 this.refreshVisibleData(); 200 }, 201 202 searchCanceled: function() 203 { 204 if (this._searchResults) { 205 for (var i = 0; i < this._searchResults.length; ++i) { 206 var profileNode = this._searchResults[i].profileNode; 207 208 delete profileNode._searchMatchedSelfColumn; 209 delete profileNode._searchMatchedTotalColumn; 210 delete profileNode._searchMatchedFunctionColumn; 211 212 profileNode.refresh(); 213 } 214 } 215 216 delete this._searchFinishedCallback; 217 this._currentSearchResultIndex = -1; 218 this._searchResults = []; 219 }, 220 221 performSearch: function(query, finishedCallback) 222 { 223 // Call searchCanceled since it will reset everything we need before doing a new search. 224 this.searchCanceled(); 225 226 query = query.trim(); 227 228 if (!query.length) 229 return; 230 231 this._searchFinishedCallback = finishedCallback; 232 233 var greaterThan = (query.startsWith(">")); 234 var lessThan = (query.startsWith("<")); 235 var equalTo = (query.startsWith("=") || ((greaterThan || lessThan) && query.indexOf("=") === 1)); 236 var percentUnits = (query.lastIndexOf("%") === (query.length - 1)); 237 var millisecondsUnits = (query.length > 2 && query.lastIndexOf("ms") === (query.length - 2)); 238 var secondsUnits = (!millisecondsUnits && query.lastIndexOf("s") === (query.length - 1)); 239 240 var queryNumber = parseFloat(query); 241 if (greaterThan || lessThan || equalTo) { 242 if (equalTo && (greaterThan || lessThan)) 243 queryNumber = parseFloat(query.substring(2)); 244 else 245 queryNumber = parseFloat(query.substring(1)); 246 } 247 248 var queryNumberMilliseconds = (secondsUnits ? (queryNumber * 1000) : queryNumber); 249 250 // Make equalTo implicitly true if it wasn't specified there is no other operator. 251 if (!isNaN(queryNumber) && !(greaterThan || lessThan)) 252 equalTo = true; 253 254 var matcher = createPlainTextSearchRegex(query, "i"); 255 256 function matchesQuery(/*ProfileDataGridNode*/ profileDataGridNode) 257 { 258 delete profileDataGridNode._searchMatchedSelfColumn; 259 delete profileDataGridNode._searchMatchedTotalColumn; 260 delete profileDataGridNode._searchMatchedFunctionColumn; 261 262 if (percentUnits) { 263 if (lessThan) { 264 if (profileDataGridNode.selfPercent < queryNumber) 265 profileDataGridNode._searchMatchedSelfColumn = true; 266 if (profileDataGridNode.totalPercent < queryNumber) 267 profileDataGridNode._searchMatchedTotalColumn = true; 268 } else if (greaterThan) { 269 if (profileDataGridNode.selfPercent > queryNumber) 270 profileDataGridNode._searchMatchedSelfColumn = true; 271 if (profileDataGridNode.totalPercent > queryNumber) 272 profileDataGridNode._searchMatchedTotalColumn = true; 273 } 274 275 if (equalTo) { 276 if (profileDataGridNode.selfPercent == queryNumber) 277 profileDataGridNode._searchMatchedSelfColumn = true; 278 if (profileDataGridNode.totalPercent == queryNumber) 279 profileDataGridNode._searchMatchedTotalColumn = true; 280 } 281 } else if (millisecondsUnits || secondsUnits) { 282 if (lessThan) { 283 if (profileDataGridNode.selfTime < queryNumberMilliseconds) 284 profileDataGridNode._searchMatchedSelfColumn = true; 285 if (profileDataGridNode.totalTime < queryNumberMilliseconds) 286 profileDataGridNode._searchMatchedTotalColumn = true; 287 } else if (greaterThan) { 288 if (profileDataGridNode.selfTime > queryNumberMilliseconds) 289 profileDataGridNode._searchMatchedSelfColumn = true; 290 if (profileDataGridNode.totalTime > queryNumberMilliseconds) 291 profileDataGridNode._searchMatchedTotalColumn = true; 292 } 293 294 if (equalTo) { 295 if (profileDataGridNode.selfTime == queryNumberMilliseconds) 296 profileDataGridNode._searchMatchedSelfColumn = true; 297 if (profileDataGridNode.totalTime == queryNumberMilliseconds) 298 profileDataGridNode._searchMatchedTotalColumn = true; 299 } 300 } 301 302 if (profileDataGridNode.functionName.match(matcher) || (profileDataGridNode.url && profileDataGridNode.url.match(matcher))) 303 profileDataGridNode._searchMatchedFunctionColumn = true; 304 305 if (profileDataGridNode._searchMatchedSelfColumn || 306 profileDataGridNode._searchMatchedTotalColumn || 307 profileDataGridNode._searchMatchedFunctionColumn) 308 { 309 profileDataGridNode.refresh(); 310 return true; 311 } 312 313 return false; 314 } 315 316 var current = this.profileDataGridTree.children[0]; 317 318 while (current) { 319 if (matchesQuery(current)) { 320 this._searchResults.push({ profileNode: current }); 321 } 322 323 current = current.traverseNextNode(false, null, false); 324 } 325 326 finishedCallback(this, this._searchResults.length); 327 }, 328 329 jumpToFirstSearchResult: function() 330 { 331 if (!this._searchResults || !this._searchResults.length) 332 return; 333 this._currentSearchResultIndex = 0; 334 this._jumpToSearchResult(this._currentSearchResultIndex); 335 }, 336 337 jumpToLastSearchResult: function() 338 { 339 if (!this._searchResults || !this._searchResults.length) 340 return; 341 this._currentSearchResultIndex = (this._searchResults.length - 1); 342 this._jumpToSearchResult(this._currentSearchResultIndex); 343 }, 344 345 jumpToNextSearchResult: function() 346 { 347 if (!this._searchResults || !this._searchResults.length) 348 return; 349 if (++this._currentSearchResultIndex >= this._searchResults.length) 350 this._currentSearchResultIndex = 0; 351 this._jumpToSearchResult(this._currentSearchResultIndex); 352 }, 353 354 jumpToPreviousSearchResult: function() 355 { 356 if (!this._searchResults || !this._searchResults.length) 357 return; 358 if (--this._currentSearchResultIndex < 0) 359 this._currentSearchResultIndex = (this._searchResults.length - 1); 360 this._jumpToSearchResult(this._currentSearchResultIndex); 361 }, 362 363 showingFirstSearchResult: function() 364 { 365 return (this._currentSearchResultIndex === 0); 366 }, 367 368 showingLastSearchResult: function() 369 { 370 return (this._searchResults && this._currentSearchResultIndex === (this._searchResults.length - 1)); 371 }, 372 373 currentSearchResultIndex: function() { 374 return this._currentSearchResultIndex; 375 }, 376 377 _jumpToSearchResult: function(index) 378 { 379 var searchResult = this._searchResults[index]; 380 if (!searchResult) 381 return; 382 383 var profileNode = searchResult.profileNode; 384 profileNode.revealAndSelect(); 385 }, 386 387 _ensureFlameChartCreated: function() 388 { 389 if (this._flameChart) 390 return; 391 var dataProvider = new WebInspector.CPUFlameChartDataProvider(this); 392 this._flameChart = new WebInspector.FlameChart(dataProvider); 393 this._flameChart.addEventListener(WebInspector.FlameChart.Events.EntrySelected, this._onEntrySelected.bind(this)); 394 }, 395 396 /** 397 * @param {!WebInspector.Event} event 398 */ 399 _onEntrySelected: function(event) 400 { 401 var node = event.data; 402 if (!node || !node.scriptId) 403 return; 404 var script = WebInspector.debuggerModel.scriptForId(node.scriptId) 405 if (!script) 406 return; 407 var uiLocation = script.rawLocationToUILocation(node.lineNumber); 408 if (!uiLocation) 409 return; 410 WebInspector.panel("sources").showUILocation(uiLocation); 411 }, 412 413 _changeView: function() 414 { 415 if (!this.profile) 416 return; 417 418 switch (this.viewSelectComboBox.selectedOption().value) { 419 case WebInspector.CPUProfileView._TypeFlame: 420 this._ensureFlameChartCreated(); 421 this.dataGrid.detach(); 422 this._flameChart.show(this.element); 423 this._viewType.set(WebInspector.CPUProfileView._TypeFlame); 424 this._statusBarButtonsElement.enableStyleClass("hidden", true); 425 return; 426 case WebInspector.CPUProfileView._TypeTree: 427 this.profileDataGridTree = this._getTopDownProfileDataGridTree(); 428 this._sortProfile(); 429 this._viewType.set(WebInspector.CPUProfileView._TypeTree); 430 break; 431 case WebInspector.CPUProfileView._TypeHeavy: 432 this.profileDataGridTree = this._getBottomUpProfileDataGridTree(); 433 this._sortProfile(); 434 this._viewType.set(WebInspector.CPUProfileView._TypeHeavy); 435 break; 436 } 437 438 this._statusBarButtonsElement.enableStyleClass("hidden", false); 439 440 if (this._flameChart) 441 this._flameChart.detach(); 442 this.dataGrid.show(this.element); 443 444 if (!this.currentQuery || !this._searchFinishedCallback || !this._searchResults) 445 return; 446 447 // The current search needs to be performed again. First negate out previous match 448 // count by calling the search finished callback with a negative number of matches. 449 // Then perform the search again the with same query and callback. 450 this._searchFinishedCallback(this, -this._searchResults.length); 451 this.performSearch(this.currentQuery, this._searchFinishedCallback); 452 }, 453 454 _percentClicked: function(event) 455 { 456 var currentState = this.showSelfTimeAsPercent.get() && this.showTotalTimeAsPercent.get() && this.showAverageTimeAsPercent.get(); 457 this.showSelfTimeAsPercent.set(!currentState); 458 this.showTotalTimeAsPercent.set(!currentState); 459 this.showAverageTimeAsPercent.set(!currentState); 460 this.refreshShowAsPercents(); 461 }, 462 463 _updatePercentButton: function() 464 { 465 if (this.showSelfTimeAsPercent.get() && this.showTotalTimeAsPercent.get() && this.showAverageTimeAsPercent.get()) { 466 this.percentButton.title = WebInspector.UIString("Show absolute total and self times."); 467 this.percentButton.toggled = true; 468 } else { 469 this.percentButton.title = WebInspector.UIString("Show total and self times as percentages."); 470 this.percentButton.toggled = false; 471 } 472 }, 473 474 _focusClicked: function(event) 475 { 476 if (!this.dataGrid.selectedNode) 477 return; 478 479 this.resetButton.visible = true; 480 this.profileDataGridTree.focus(this.dataGrid.selectedNode); 481 this.refresh(); 482 this.refreshVisibleData(); 483 }, 484 485 _excludeClicked: function(event) 486 { 487 var selectedNode = this.dataGrid.selectedNode 488 489 if (!selectedNode) 490 return; 491 492 selectedNode.deselect(); 493 494 this.resetButton.visible = true; 495 this.profileDataGridTree.exclude(selectedNode); 496 this.refresh(); 497 this.refreshVisibleData(); 498 }, 499 500 _resetClicked: function(event) 501 { 502 this.resetButton.visible = false; 503 this.profileDataGridTree.restore(); 504 this._linkifier.reset(); 505 this.refresh(); 506 this.refreshVisibleData(); 507 }, 508 509 _dataGridNodeSelected: function(node) 510 { 511 this.focusButton.setEnabled(true); 512 this.excludeButton.setEnabled(true); 513 }, 514 515 _dataGridNodeDeselected: function(node) 516 { 517 this.focusButton.setEnabled(false); 518 this.excludeButton.setEnabled(false); 519 }, 520 521 _sortProfile: function() 522 { 523 var sortAscending = this.dataGrid.isSortOrderAscending(); 524 var sortColumnIdentifier = this.dataGrid.sortColumnIdentifier(); 525 var sortProperty = { 526 "self": "selfTime", 527 "total": "totalTime", 528 "function": "functionName" 529 }[sortColumnIdentifier]; 530 531 this.profileDataGridTree.sort(WebInspector.ProfileDataGridTree.propertyComparator(sortProperty, sortAscending)); 532 533 this.refresh(); 534 }, 535 536 _mouseDownInDataGrid: function(event) 537 { 538 if (event.detail < 2) 539 return; 540 541 var cell = event.target.enclosingNodeOrSelfWithNodeName("td"); 542 if (!cell || (!cell.classList.contains("total-column") && !cell.classList.contains("self-column") && !cell.classList.contains("average-column"))) 543 return; 544 545 if (cell.classList.contains("total-column")) 546 this.showTotalTimeAsPercent.set(!this.showTotalTimeAsPercent.get()); 547 else if (cell.classList.contains("self-column")) 548 this.showSelfTimeAsPercent.set(!this.showSelfTimeAsPercent.get()); 549 else if (cell.classList.contains("average-column")) 550 this.showAverageTimeAsPercent.set(!this.showAverageTimeAsPercent.get()); 551 552 this.refreshShowAsPercents(); 553 554 event.consume(true); 555 }, 556 557 _calculateTimes: function(profile) 558 { 559 function totalHitCount(node) { 560 var result = node.hitCount; 561 for (var i = 0; i < node.children.length; i++) 562 result += totalHitCount(node.children[i]); 563 return result; 564 } 565 profile.totalHitCount = totalHitCount(profile.head); 566 567 var durationMs = 1000 * (profile.endTime - profile.startTime); 568 var samplingInterval = durationMs / profile.totalHitCount; 569 this.samplingIntervalMs = samplingInterval; 570 571 function calculateTimesForNode(node) { 572 node.selfTime = node.hitCount * samplingInterval; 573 var totalHitCount = node.hitCount; 574 for (var i = 0; i < node.children.length; i++) 575 totalHitCount += calculateTimesForNode(node.children[i]); 576 node.totalTime = totalHitCount * samplingInterval; 577 return totalHitCount; 578 } 579 calculateTimesForNode(profile.head); 580 }, 581 582 _assignParentsInProfile: function() 583 { 584 var head = this.profileHead; 585 head.parent = null; 586 head.head = null; 587 var nodesToTraverse = [ { parent: head, children: head.children } ]; 588 while (nodesToTraverse.length > 0) { 589 var pair = nodesToTraverse.pop(); 590 var parent = pair.parent; 591 var children = pair.children; 592 var length = children.length; 593 for (var i = 0; i < length; ++i) { 594 children[i].head = head; 595 children[i].parent = parent; 596 if (children[i].children.length > 0) 597 nodesToTraverse.push({ parent: children[i], children: children[i].children }); 598 } 599 } 600 }, 601 602 _buildIdToNodeMap: function() 603 { 604 var idToNode = this._idToNode = {}; 605 var stack = [this.profileHead]; 606 while (stack.length) { 607 var node = stack.pop(); 608 idToNode[node.id] = node; 609 for (var i = 0; i < node.children.length; i++) 610 stack.push(node.children[i]); 611 } 612 613 var topLevelNodes = this.profileHead.children; 614 for (var i = 0; i < topLevelNodes.length; i++) { 615 var node = topLevelNodes[i]; 616 if (node.functionName == "(garbage collector)") { 617 this._gcNode = node; 618 break; 619 } 620 } 621 }, 622 623 __proto__: WebInspector.View.prototype 624} 625 626/** 627 * @constructor 628 * @extends {WebInspector.ProfileType} 629 * @implements {WebInspector.CPUProfilerModelDelegate} 630 */ 631WebInspector.CPUProfileType = function() 632{ 633 WebInspector.ProfileType.call(this, WebInspector.CPUProfileType.TypeId, WebInspector.UIString("Collect JavaScript CPU Profile")); 634 this._recording = false; 635 this._nextProfileId = 1; 636 637 this._nextAnonymousConsoleProfileNumber = 1; 638 this._anonymousConsoleProfileIdToTitle = {}; 639 640 WebInspector.CPUProfileType.instance = this; 641 WebInspector.cpuProfilerModel.setDelegate(this); 642} 643 644WebInspector.CPUProfileType.TypeId = "CPU"; 645 646WebInspector.CPUProfileType.prototype = { 647 /** 648 * @override 649 * @return {string} 650 */ 651 fileExtension: function() 652 { 653 return ".cpuprofile"; 654 }, 655 656 get buttonTooltip() 657 { 658 return this._recording ? WebInspector.UIString("Stop CPU profiling.") : WebInspector.UIString("Start CPU profiling."); 659 }, 660 661 /** 662 * @override 663 * @return {boolean} 664 */ 665 buttonClicked: function() 666 { 667 if (this._recording) { 668 this.stopRecordingProfile(); 669 return false; 670 } else { 671 this.startRecordingProfile(); 672 return true; 673 } 674 }, 675 676 get treeItemTitle() 677 { 678 return WebInspector.UIString("CPU PROFILES"); 679 }, 680 681 get description() 682 { 683 return WebInspector.UIString("CPU profiles show where the execution time is spent in your page's JavaScript functions."); 684 }, 685 686 /** 687 * @param {string} id 688 * @param {!DebuggerAgent.Location} scriptLocation 689 * @param {string=} title 690 */ 691 consoleProfile: function(id, scriptLocation, title) 692 { 693 var resolvedTitle = title; 694 if (!resolvedTitle) { 695 resolvedTitle = WebInspector.UIString("Profile %s", this._nextAnonymousConsoleProfileNumber++); 696 this._anonymousConsoleProfileIdToTitle[id] = resolvedTitle; 697 } 698 this._addMessageToConsole(WebInspector.ConsoleMessage.MessageType.Profile, scriptLocation, resolvedTitle); 699 }, 700 701 /** 702 * @param {string} protocolId 703 * @param {!DebuggerAgent.Location} scriptLocation 704 * @param {!ProfilerAgent.CPUProfile} cpuProfile 705 * @param {string=} title 706 */ 707 consoleProfileEnd: function(protocolId, scriptLocation, cpuProfile, title) 708 { 709 // Make sure ProfilesPanel is initialized and CPUProfileType is created. 710 var resolvedTitle = title; 711 if (typeof title === "undefined") { 712 resolvedTitle = this._anonymousConsoleProfileIdToTitle[protocolId]; 713 delete this._anonymousConsoleProfileIdToTitle[protocolId]; 714 } 715 716 var id = this._nextProfileId++; 717 var profile = new WebInspector.CPUProfileHeader(this, resolvedTitle, id); 718 profile.setProtocolProfile(cpuProfile); 719 this.addProfile(profile); 720 721 resolvedTitle += "#" + id; 722 this._addMessageToConsole(WebInspector.ConsoleMessage.MessageType.ProfileEnd, scriptLocation, resolvedTitle); 723 }, 724 725 /** 726 * @param {string} type 727 * @param {!DebuggerAgent.Location} scriptLocation 728 * @param {string} title 729 */ 730 _addMessageToConsole: function(type, scriptLocation, title) 731 { 732 var rawLocation = new WebInspector.DebuggerModel.Location(scriptLocation.scriptId, scriptLocation.lineNumber, scriptLocation.columnNumber || 0); 733 var uiLocation = WebInspector.debuggerModel.rawLocationToUILocation(rawLocation); 734 var url; 735 if (uiLocation) 736 url = uiLocation.url(); 737 var message = WebInspector.ConsoleMessage.create( 738 WebInspector.ConsoleMessage.MessageSource.ConsoleAPI, 739 WebInspector.ConsoleMessage.MessageLevel.Debug, 740 title, 741 type, 742 url || undefined, 743 scriptLocation.lineNumber, 744 scriptLocation.columnNumber); 745 WebInspector.console.addMessage(message); 746 }, 747 748 /** 749 * @param {!ProfilerAgent.CPUProfile} cpuProfile 750 * @param {string} title 751 */ 752 _addProfileHeader: function(cpuProfile, title) 753 { 754 var id = this._nextProfileId++; 755 var profile = new WebInspector.CPUProfileHeader(this, title, id); 756 profile.setProtocolProfile(cpuProfile); 757 this.addProfile(profile); 758 }, 759 760 isRecordingProfile: function() 761 { 762 return this._recording; 763 }, 764 765 startRecordingProfile: function() 766 { 767 if (this._profileBeingRecorded) 768 return; 769 var id = this._nextProfileId++; 770 this._profileBeingRecorded = new WebInspector.CPUProfileHeader(this, WebInspector.UIString("Recording\u2026"), id); 771 this.addProfile(this._profileBeingRecorded); 772 773 this._recording = true; 774 WebInspector.cpuProfilerModel.setRecording(true); 775 WebInspector.userMetrics.ProfilesCPUProfileTaken.record(); 776 ProfilerAgent.start(); 777 }, 778 779 stopRecordingProfile: function() 780 { 781 this._recording = false; 782 WebInspector.cpuProfilerModel.setRecording(false); 783 784 /** 785 * @param {?string} error 786 * @param {?ProfilerAgent.CPUProfile} profile 787 * @this {WebInspector.CPUProfileType} 788 */ 789 function didStopProfiling(error, profile) 790 { 791 if (!this._profileBeingRecorded) 792 return; 793 this._profileBeingRecorded.setProtocolProfile(profile); 794 795 var title = WebInspector.UIString("Profile %d", this._profileBeingRecorded.uid); 796 this._profileBeingRecorded.title = title; 797 this._profileBeingRecorded.sidebarElement.mainTitle = title; 798 var recordedProfile = this._profileBeingRecorded; 799 this._profileBeingRecorded = null; 800 WebInspector.panels.profiles._showProfile(recordedProfile); 801 } 802 ProfilerAgent.stop(didStopProfiling.bind(this)); 803 }, 804 805 /** 806 * @override 807 * @param {string} title 808 * @return {!WebInspector.ProfileHeader} 809 */ 810 createProfileLoadedFromFile: function(title) 811 { 812 return new WebInspector.CPUProfileHeader(this, title); 813 }, 814 815 /** 816 * @override 817 */ 818 removeProfile: function(profile) 819 { 820 if (this._profileBeingRecorded === profile) { 821 this.stopRecordingProfile(); 822 this._profileBeingRecorded = null; 823 } 824 WebInspector.ProfileType.prototype.removeProfile.call(this, profile); 825 }, 826 827 /** 828 * @override 829 */ 830 resetProfiles: function() 831 { 832 this._reset(); 833 }, 834 835 __proto__: WebInspector.ProfileType.prototype 836} 837 838/** 839 * @constructor 840 * @extends {WebInspector.ProfileHeader} 841 * @implements {WebInspector.OutputStream} 842 * @implements {WebInspector.OutputStreamDelegate} 843 * @param {!WebInspector.CPUProfileType} type 844 * @param {string} title 845 * @param {number=} uid 846 */ 847WebInspector.CPUProfileHeader = function(type, title, uid) 848{ 849 WebInspector.ProfileHeader.call(this, type, title, uid); 850 this._tempFile = null; 851} 852 853WebInspector.CPUProfileHeader.prototype = { 854 onTransferStarted: function() 855 { 856 this._jsonifiedProfile = ""; 857 this.sidebarElement.subtitle = WebInspector.UIString("Loading\u2026 %s", Number.bytesToString(this._jsonifiedProfile.length)); 858 }, 859 860 /** 861 * @param {!WebInspector.ChunkedReader} reader 862 */ 863 onChunkTransferred: function(reader) 864 { 865 this.sidebarElement.subtitle = WebInspector.UIString("Loading\u2026 %d\%", Number.bytesToString(this._jsonifiedProfile.length)); 866 }, 867 868 onTransferFinished: function() 869 { 870 this.sidebarElement.subtitle = WebInspector.UIString("Parsing\u2026"); 871 this._profile = JSON.parse(this._jsonifiedProfile); 872 this._jsonifiedProfile = null; 873 this.sidebarElement.subtitle = WebInspector.UIString("Loaded"); 874 875 if (this._profileType._profileBeingRecorded === this) 876 this._profileType._profileBeingRecorded = null; 877 }, 878 879 /** 880 * @param {!WebInspector.ChunkedReader} reader 881 */ 882 onError: function(reader, e) 883 { 884 switch(e.target.error.code) { 885 case e.target.error.NOT_FOUND_ERR: 886 this.sidebarElement.subtitle = WebInspector.UIString("'%s' not found.", reader.fileName()); 887 break; 888 case e.target.error.NOT_READABLE_ERR: 889 this.sidebarElement.subtitle = WebInspector.UIString("'%s' is not readable", reader.fileName()); 890 break; 891 case e.target.error.ABORT_ERR: 892 break; 893 default: 894 this.sidebarElement.subtitle = WebInspector.UIString("'%s' error %d", reader.fileName(), e.target.error.code); 895 } 896 }, 897 898 /** 899 * @param {string} text 900 */ 901 write: function(text) 902 { 903 this._jsonifiedProfile += text; 904 }, 905 906 close: function() { }, 907 908 /** 909 * @override 910 */ 911 createSidebarTreeElement: function() 912 { 913 return new WebInspector.ProfileSidebarTreeElement(this, "profile-sidebar-tree-item"); 914 }, 915 916 /** 917 * @override 918 * @param {!WebInspector.ProfilesPanel} profilesPanel 919 */ 920 createView: function(profilesPanel) 921 { 922 return new WebInspector.CPUProfileView(this); 923 }, 924 925 /** 926 * @override 927 * @return {boolean} 928 */ 929 canSaveToFile: function() 930 { 931 return !!this._tempFile; 932 }, 933 934 saveToFile: function() 935 { 936 var fileOutputStream = new WebInspector.FileOutputStream(); 937 938 /** 939 * @param {boolean} accepted 940 * @this {WebInspector.CPUProfileHeader} 941 */ 942 function onOpenForSave(accepted) 943 { 944 if (!accepted) 945 return; 946 function didRead(data) 947 { 948 if (data) 949 fileOutputStream.write(data, fileOutputStream.close.bind(fileOutputStream)); 950 else 951 fileOutputStream.close(); 952 } 953 this._tempFile.read(didRead.bind(this)); 954 } 955 this._fileName = this._fileName || "CPU-" + new Date().toISO8601Compact() + this._profileType.fileExtension(); 956 fileOutputStream.open(this._fileName, onOpenForSave.bind(this)); 957 }, 958 959 /** 960 * @param {!File} file 961 */ 962 loadFromFile: function(file) 963 { 964 this.sidebarElement.subtitle = WebInspector.UIString("Loading\u2026"); 965 this.sidebarElement.wait = true; 966 967 var fileReader = new WebInspector.ChunkedFileReader(file, 10000000, this); 968 fileReader.start(this); 969 }, 970 971 972 /** 973 * @return {?ProfilerAgent.CPUProfile} 974 */ 975 protocolProfile: function() 976 { 977 return this._protocolProfile; 978 }, 979 980 /** 981 * @param {!ProfilerAgent.CPUProfile} cpuProfile 982 */ 983 setProtocolProfile: function(cpuProfile) 984 { 985 this._protocolProfile = cpuProfile; 986 this._saveProfileDataToTempFile(cpuProfile); 987 }, 988 989 /** 990 * @param {!ProfilerAgent.CPUProfile} data 991 */ 992 _saveProfileDataToTempFile: function(data) 993 { 994 var serializedData = JSON.stringify(data); 995 996 /** 997 * @this {WebInspector.CPUProfileHeader} 998 */ 999 function didCreateTempFile(tempFile) 1000 { 1001 this._writeToTempFile(tempFile, serializedData); 1002 } 1003 new WebInspector.TempFile("cpu-profiler", this.uid, didCreateTempFile.bind(this)); 1004 }, 1005 1006 /** 1007 * @param {?WebInspector.TempFile} tempFile 1008 * @param {string} serializedData 1009 */ 1010 _writeToTempFile: function(tempFile, serializedData) 1011 { 1012 this._tempFile = tempFile; 1013 if (tempFile) 1014 tempFile.write(serializedData); 1015 }, 1016 1017 __proto__: WebInspector.ProfileHeader.prototype 1018} 1019 1020/** 1021 * @constructor 1022 * @implements {WebInspector.FlameChartDataProvider} 1023 */ 1024WebInspector.CPUFlameChartDataProvider = function(cpuProfileView) 1025{ 1026 WebInspector.FlameChartDataProvider.call(this); 1027 this._cpuProfileView = cpuProfileView; 1028} 1029 1030WebInspector.CPUFlameChartDataProvider.prototype = { 1031 /** 1032 * @param {!WebInspector.FlameChart.ColorGenerator} colorGenerator 1033 * @return {!Object} 1034 */ 1035 timelineData: function(colorGenerator) 1036 { 1037 return this._timelineData || this._calculateTimelineData(colorGenerator); 1038 }, 1039 1040 /** 1041 * @param {!WebInspector.FlameChart.ColorGenerator} colorGenerator 1042 * @return {?Object} 1043 */ 1044 _calculateTimelineData: function(colorGenerator) 1045 { 1046 if (!this._cpuProfileView.profileHead) 1047 return null; 1048 1049 var samples = this._cpuProfileView.samples; 1050 var idToNode = this._cpuProfileView._idToNode; 1051 var gcNode = this._cpuProfileView._gcNode; 1052 var samplesCount = samples.length; 1053 var samplingInterval = this._cpuProfileView.samplingIntervalMs; 1054 1055 var index = 0; 1056 1057 var openIntervals = []; 1058 var stackTrace = []; 1059 var colorEntryIndexes = []; 1060 var maxDepth = 5; // minimum stack depth for the case when we see no activity. 1061 var depth = 0; 1062 1063 /** 1064 * @constructor 1065 * @param {!Object} colorPair 1066 * @param {!number} depth 1067 * @param {!number} duration 1068 * @param {!number} startTime 1069 * @param {!Object} node 1070 */ 1071 function ChartEntry(colorPair, depth, duration, startTime, node) 1072 { 1073 this.colorPair = colorPair; 1074 this.depth = depth; 1075 this.duration = duration; 1076 this.startTime = startTime; 1077 this.node = node; 1078 this.selfTime = 0; 1079 } 1080 var entries = /** @type {!Array.<!ChartEntry>} */ ([]); 1081 1082 for (var sampleIndex = 0; sampleIndex < samplesCount; sampleIndex++) { 1083 var node = idToNode[samples[sampleIndex]]; 1084 stackTrace.length = 0; 1085 while (node) { 1086 stackTrace.push(node); 1087 node = node.parent; 1088 } 1089 stackTrace.pop(); // Remove (root) node 1090 1091 maxDepth = Math.max(maxDepth, depth); 1092 depth = 0; 1093 node = stackTrace.pop(); 1094 var intervalIndex; 1095 1096 // GC samples have no stack, so we just put GC node on top of the last recoreded sample. 1097 if (node === gcNode) { 1098 while (depth < openIntervals.length) { 1099 intervalIndex = openIntervals[depth].index; 1100 entries[intervalIndex].duration += samplingInterval; 1101 ++depth; 1102 } 1103 // If previous stack is also GC then just continue. 1104 if (openIntervals.length > 0 && openIntervals.peekLast().node === node) { 1105 entries[intervalIndex].selfTime += samplingInterval; 1106 continue; 1107 } 1108 } 1109 1110 while (node && depth < openIntervals.length && node === openIntervals[depth].node) { 1111 intervalIndex = openIntervals[depth].index; 1112 entries[intervalIndex].duration += samplingInterval; 1113 node = stackTrace.pop(); 1114 ++depth; 1115 } 1116 if (depth < openIntervals.length) 1117 openIntervals.length = depth; 1118 if (!node) { 1119 entries[intervalIndex].selfTime += samplingInterval; 1120 continue; 1121 } 1122 1123 while (node) { 1124 var colorPair = colorGenerator._colorPairForID(node.functionName + ":" + node.url + ":" + node.lineNumber); 1125 var indexesForColor = colorEntryIndexes[colorPair.index]; 1126 if (!indexesForColor) 1127 indexesForColor = colorEntryIndexes[colorPair.index] = []; 1128 1129 var entry = new ChartEntry(colorPair, depth, samplingInterval, sampleIndex * samplingInterval, node); 1130 indexesForColor.push(entries.length); 1131 entries.push(entry); 1132 openIntervals.push({node: node, index: index}); 1133 ++index; 1134 1135 node = stackTrace.pop(); 1136 ++depth; 1137 } 1138 entries[entries.length - 1].selfTime += samplingInterval; 1139 } 1140 1141 var entryNodes = new Array(entries.length); 1142 var entryColorIndexes = new Uint16Array(entries.length); 1143 var entryLevels = new Uint8Array(entries.length); 1144 var entryTotalTimes = new Float32Array(entries.length); 1145 var entrySelfTimes = new Float32Array(entries.length); 1146 var entryOffsets = new Float32Array(entries.length); 1147 var entryTitles = new Array(entries.length); 1148 var entryDeoptFlags = new Uint8Array(entries.length); 1149 1150 for (var i = 0; i < entries.length; ++i) { 1151 var entry = entries[i]; 1152 entryNodes[i] = entry.node; 1153 entryColorIndexes[i] = colorPair.index; 1154 entryLevels[i] = entry.depth; 1155 entryTotalTimes[i] = entry.duration; 1156 entrySelfTimes[i] = entry.selfTime; 1157 entryOffsets[i] = entry.startTime; 1158 entryTitles[i] = entry.node.functionName; 1159 var reason = entry.node.deoptReason; 1160 entryDeoptFlags[i] = (reason && reason !== "no reason"); 1161 } 1162 1163 this._timelineData = { 1164 maxStackDepth: Math.max(maxDepth, depth), 1165 totalTime: this._cpuProfileView.profileHead.totalTime, 1166 entryNodes: entryNodes, 1167 entryColorIndexes: entryColorIndexes, 1168 entryLevels: entryLevels, 1169 entryTotalTimes: entryTotalTimes, 1170 entrySelfTimes: entrySelfTimes, 1171 entryOffsets: entryOffsets, 1172 colorEntryIndexes: colorEntryIndexes, 1173 entryTitles: entryTitles, 1174 entryDeoptFlags: entryDeoptFlags 1175 }; 1176 1177 return this._timelineData; 1178 }, 1179 1180 /** 1181 * @param {number} ms 1182 */ 1183 _millisecondsToString: function(ms) 1184 { 1185 if (ms === 0) 1186 return "0"; 1187 if (ms < 1000) 1188 return WebInspector.UIString("%.1f\u2009ms", ms); 1189 return Number.secondsToString(ms / 1000, true); 1190 }, 1191 1192 /** 1193 * @param {number} entryIndex 1194 */ 1195 prepareHighlightedEntryInfo: function(entryIndex) 1196 { 1197 var timelineData = this._timelineData; 1198 var node = timelineData.entryNodes[entryIndex]; 1199 if (!node) 1200 return null; 1201 1202 var entryInfo = []; 1203 function pushEntryInfoRow(title, text) 1204 { 1205 var row = {}; 1206 row.title = title; 1207 row.text = text; 1208 entryInfo.push(row); 1209 } 1210 1211 pushEntryInfoRow(WebInspector.UIString("Name"), timelineData.entryTitles[entryIndex]); 1212 var selfTime = this._millisecondsToString(timelineData.entrySelfTimes[entryIndex]); 1213 var totalTime = this._millisecondsToString(timelineData.entryTotalTimes[entryIndex]); 1214 pushEntryInfoRow(WebInspector.UIString("Self time"), selfTime); 1215 pushEntryInfoRow(WebInspector.UIString("Total time"), totalTime); 1216 if (node.url) 1217 pushEntryInfoRow(WebInspector.UIString("URL"), node.url + ":" + node.lineNumber); 1218 pushEntryInfoRow(WebInspector.UIString("Aggregated self time"), Number.secondsToString(node.selfTime / 1000, true)); 1219 pushEntryInfoRow(WebInspector.UIString("Aggregated total time"), Number.secondsToString(node.totalTime / 1000, true)); 1220 if (node.deoptReason && node.deoptReason !== "no reason") 1221 pushEntryInfoRow(WebInspector.UIString("Not optimized"), node.deoptReason); 1222 1223 return entryInfo; 1224 }, 1225 1226 /** 1227 * @param {number} entryIndex 1228 * @return {boolean} 1229 */ 1230 canJumpToEntry: function(entryIndex) 1231 { 1232 return this._timelineData.entryNodes[entryIndex].scriptId !== "0"; 1233 }, 1234 1235 /** 1236 * @param {number} entryIndex 1237 * @return {!Object} 1238 */ 1239 entryData: function(entryIndex) 1240 { 1241 return this._timelineData.entryNodes[entryIndex]; 1242 }, 1243 1244 __proto__: WebInspector.FlameChartDataProvider 1245} 1246 1247