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 26const UserInitiatedProfileName = "org.webkit.profiles.user-initiated"; 27 28WebInspector.ProfileType = function(id, name) 29{ 30 this._id = id; 31 this._name = name; 32} 33 34WebInspector.ProfileType.URLRegExp = /webkit-profile:\/\/(.+)\/(.+)#([0-9]+)/; 35 36WebInspector.ProfileType.prototype = { 37 get buttonTooltip() 38 { 39 return ""; 40 }, 41 42 get buttonStyle() 43 { 44 return undefined; 45 }, 46 47 get buttonCaption() 48 { 49 return this.name; 50 }, 51 52 get id() 53 { 54 return this._id; 55 }, 56 57 get name() 58 { 59 return this._name; 60 }, 61 62 buttonClicked: function() 63 { 64 }, 65 66 viewForProfile: function(profile) 67 { 68 if (!profile._profileView) 69 profile._profileView = this.createView(profile); 70 return profile._profileView; 71 }, 72 73 get welcomeMessage() 74 { 75 return ""; 76 }, 77 78 // Must be implemented by subclasses. 79 createView: function(profile) 80 { 81 throw new Error("Needs implemented."); 82 }, 83 84 // Must be implemented by subclasses. 85 createSidebarTreeElementForProfile: function(profile) 86 { 87 throw new Error("Needs implemented."); 88 } 89} 90 91WebInspector.ProfilesPanel = function() 92{ 93 WebInspector.Panel.call(this); 94 95 this.createSidebar(); 96 97 this.element.addStyleClass("profiles"); 98 this._profileTypesByIdMap = {}; 99 this._profileTypeButtonsByIdMap = {}; 100 101 var panelEnablerHeading = WebInspector.UIString("You need to enable profiling before you can use the Profiles panel."); 102 var panelEnablerDisclaimer = WebInspector.UIString("Enabling profiling will make scripts run slower."); 103 var panelEnablerButton = WebInspector.UIString("Enable Profiling"); 104 this.panelEnablerView = new WebInspector.PanelEnablerView("profiles", panelEnablerHeading, panelEnablerDisclaimer, panelEnablerButton); 105 this.panelEnablerView.addEventListener("enable clicked", this._enableProfiling, this); 106 107 this.element.appendChild(this.panelEnablerView.element); 108 109 this.profileViews = document.createElement("div"); 110 this.profileViews.id = "profile-views"; 111 this.element.appendChild(this.profileViews); 112 113 this.enableToggleButton = new WebInspector.StatusBarButton("", "enable-toggle-status-bar-item"); 114 this.enableToggleButton.addEventListener("click", this._toggleProfiling.bind(this), false); 115 116 this.profileViewStatusBarItemsContainer = document.createElement("div"); 117 this.profileViewStatusBarItemsContainer.id = "profile-view-status-bar-items"; 118 119 this.welcomeView = new WebInspector.WelcomeView("profiles", WebInspector.UIString("Welcome to the Profiles panel")); 120 this.element.appendChild(this.welcomeView.element); 121 122 this._profiles = []; 123 this.reset(); 124} 125 126WebInspector.ProfilesPanel.prototype = { 127 toolbarItemClass: "profiles", 128 129 get toolbarItemLabel() 130 { 131 return WebInspector.UIString("Profiles"); 132 }, 133 134 get statusBarItems() 135 { 136 function clickHandler(profileType, buttonElement) 137 { 138 profileType.buttonClicked.call(profileType); 139 this.updateProfileTypeButtons(); 140 } 141 142 var items = [this.enableToggleButton.element]; 143 // FIXME: Generate a single "combo-button". 144 for (var typeId in this._profileTypesByIdMap) { 145 var profileType = this.getProfileType(typeId); 146 if (profileType.buttonStyle) { 147 var button = new WebInspector.StatusBarButton(profileType.buttonTooltip, profileType.buttonStyle, profileType.buttonCaption); 148 this._profileTypeButtonsByIdMap[typeId] = button.element; 149 button.element.addEventListener("click", clickHandler.bind(this, profileType, button.element), false); 150 items.push(button.element); 151 } 152 } 153 items.push(this.profileViewStatusBarItemsContainer); 154 return items; 155 }, 156 157 show: function() 158 { 159 WebInspector.Panel.prototype.show.call(this); 160 if (this._shouldPopulateProfiles) 161 this._populateProfiles(); 162 }, 163 164 populateInterface: function() 165 { 166 if (this.visible) 167 this._populateProfiles(); 168 else 169 this._shouldPopulateProfiles = true; 170 }, 171 172 profilerWasEnabled: function() 173 { 174 this.reset(); 175 this.populateInterface(); 176 }, 177 178 profilerWasDisabled: function() 179 { 180 this.reset(); 181 }, 182 183 reset: function() 184 { 185 for (var i = 0; i < this._profiles.length; ++i) 186 delete this._profiles[i]._profileView; 187 delete this.visibleView; 188 189 delete this.currentQuery; 190 this.searchCanceled(); 191 192 this._profiles = []; 193 this._profilesIdMap = {}; 194 this._profileGroups = {}; 195 this._profileGroupsForLinks = {} 196 197 this.sidebarTreeElement.removeStyleClass("some-expandable"); 198 199 for (var typeId in this._profileTypesByIdMap) 200 this.getProfileType(typeId).treeElement.removeChildren(); 201 202 this.profileViews.removeChildren(); 203 204 this.profileViewStatusBarItemsContainer.removeChildren(); 205 206 this._updateInterface(); 207 this.welcomeView.show(); 208 }, 209 210 registerProfileType: function(profileType) 211 { 212 this._profileTypesByIdMap[profileType.id] = profileType; 213 profileType.treeElement = new WebInspector.SidebarSectionTreeElement(profileType.name, null, true); 214 this.sidebarTree.appendChild(profileType.treeElement); 215 profileType.treeElement.expand(); 216 this._addWelcomeMessage(profileType); 217 }, 218 219 _addWelcomeMessage: function(profileType) 220 { 221 var message = profileType.welcomeMessage; 222 // Message text is supposed to have a '%s' substring as a placeholder 223 // for a status bar button. If it is there, we split the message in two 224 // parts, and insert the button between them. 225 var buttonPos = message.indexOf("%s"); 226 if (buttonPos > -1) { 227 var container = document.createDocumentFragment(); 228 var part1 = document.createElement("span"); 229 part1.innerHTML = message.substr(0, buttonPos); 230 container.appendChild(part1); 231 232 var button = new WebInspector.StatusBarButton(profileType.buttonTooltip, profileType.buttonStyle, profileType.buttonCaption); 233 button.element.addEventListener("click", profileType.buttonClicked.bind(profileType), false); 234 container.appendChild(button.element); 235 236 var part2 = document.createElement("span"); 237 part2.innerHTML = message.substr(buttonPos + 2); 238 container.appendChild(part2); 239 this.welcomeView.addMessage(container); 240 } else 241 this.welcomeView.addMessage(message); 242 }, 243 244 _makeKey: function(text, profileTypeId) 245 { 246 return escape(text) + '/' + escape(profileTypeId); 247 }, 248 249 addProfileHeader: function(profile) 250 { 251 var typeId = profile.typeId; 252 var profileType = this.getProfileType(typeId); 253 var sidebarParent = profileType.treeElement; 254 var small = false; 255 var alternateTitle; 256 257 profile.__profilesPanelProfileType = profileType; 258 this._profiles.push(profile); 259 this._profilesIdMap[this._makeKey(profile.uid, typeId)] = profile; 260 261 if (profile.title.indexOf(UserInitiatedProfileName) !== 0) { 262 var profileTitleKey = this._makeKey(profile.title, typeId); 263 if (!(profileTitleKey in this._profileGroups)) 264 this._profileGroups[profileTitleKey] = []; 265 266 var group = this._profileGroups[profileTitleKey]; 267 group.push(profile); 268 269 if (group.length === 2) { 270 // Make a group TreeElement now that there are 2 profiles. 271 group._profilesTreeElement = new WebInspector.ProfileGroupSidebarTreeElement(profile.title); 272 273 // Insert at the same index for the first profile of the group. 274 var index = sidebarParent.children.indexOf(group[0]._profilesTreeElement); 275 sidebarParent.insertChild(group._profilesTreeElement, index); 276 277 // Move the first profile to the group. 278 var selected = group[0]._profilesTreeElement.selected; 279 sidebarParent.removeChild(group[0]._profilesTreeElement); 280 group._profilesTreeElement.appendChild(group[0]._profilesTreeElement); 281 if (selected) { 282 group[0]._profilesTreeElement.select(); 283 group[0]._profilesTreeElement.reveal(); 284 } 285 286 group[0]._profilesTreeElement.small = true; 287 group[0]._profilesTreeElement.mainTitle = WebInspector.UIString("Run %d", 1); 288 289 this.sidebarTreeElement.addStyleClass("some-expandable"); 290 } 291 292 if (group.length >= 2) { 293 sidebarParent = group._profilesTreeElement; 294 alternateTitle = WebInspector.UIString("Run %d", group.length); 295 small = true; 296 } 297 } 298 299 var profileTreeElement = profileType.createSidebarTreeElementForProfile(profile); 300 profileTreeElement.small = small; 301 if (alternateTitle) 302 profileTreeElement.mainTitle = alternateTitle; 303 profile._profilesTreeElement = profileTreeElement; 304 305 sidebarParent.appendChild(profileTreeElement); 306 this.welcomeView.hide(); 307 if (!this.visibleView) 308 this.showProfile(profile); 309 }, 310 311 showProfile: function(profile) 312 { 313 if (!profile) 314 return; 315 316 this.closeVisibleView(); 317 318 var view = profile.__profilesPanelProfileType.viewForProfile(profile); 319 320 view.show(this.profileViews); 321 322 profile._profilesTreeElement.select(true); 323 profile._profilesTreeElement.reveal(); 324 325 this.visibleView = view; 326 327 this.profileViewStatusBarItemsContainer.removeChildren(); 328 329 var statusBarItems = view.statusBarItems; 330 for (var i = 0; i < statusBarItems.length; ++i) 331 this.profileViewStatusBarItemsContainer.appendChild(statusBarItems[i]); 332 }, 333 334 showView: function(view) 335 { 336 this.showProfile(view.profile); 337 }, 338 339 getProfileType: function(typeId) 340 { 341 return this._profileTypesByIdMap[typeId]; 342 }, 343 344 showProfileForURL: function(url) 345 { 346 var match = url.match(WebInspector.ProfileType.URLRegExp); 347 if (!match) 348 return; 349 this.showProfile(this._profilesIdMap[this._makeKey(match[3], match[1])]); 350 }, 351 352 updateProfileTypeButtons: function() 353 { 354 for (var typeId in this._profileTypeButtonsByIdMap) { 355 var buttonElement = this._profileTypeButtonsByIdMap[typeId]; 356 var profileType = this.getProfileType(typeId); 357 buttonElement.className = profileType.buttonStyle; 358 buttonElement.title = profileType.buttonTooltip; 359 // FIXME: Apply profileType.buttonCaption once captions are added to button controls. 360 } 361 }, 362 363 closeVisibleView: function() 364 { 365 if (this.visibleView) 366 this.visibleView.hide(); 367 delete this.visibleView; 368 }, 369 370 displayTitleForProfileLink: function(title, typeId) 371 { 372 title = unescape(title); 373 if (title.indexOf(UserInitiatedProfileName) === 0) { 374 title = WebInspector.UIString("Profile %d", title.substring(UserInitiatedProfileName.length + 1)); 375 } else { 376 var titleKey = this._makeKey(title, typeId); 377 if (!(titleKey in this._profileGroupsForLinks)) 378 this._profileGroupsForLinks[titleKey] = 0; 379 380 groupNumber = ++this._profileGroupsForLinks[titleKey]; 381 382 if (groupNumber > 2) 383 // The title is used in the console message announcing that a profile has started so it gets 384 // incremented twice as often as it's displayed 385 title += " " + WebInspector.UIString("Run %d", groupNumber / 2); 386 } 387 388 return title; 389 }, 390 391 get searchableViews() 392 { 393 var views = []; 394 395 const visibleView = this.visibleView; 396 if (visibleView && visibleView.performSearch) 397 views.push(visibleView); 398 399 var profilesLength = this._profiles.length; 400 for (var i = 0; i < profilesLength; ++i) { 401 var profile = this._profiles[i]; 402 var view = profile.__profilesPanelProfileType.viewForProfile(profile); 403 if (!view.performSearch || view === visibleView) 404 continue; 405 views.push(view); 406 } 407 408 return views; 409 }, 410 411 searchMatchFound: function(view, matches) 412 { 413 view.profile._profilesTreeElement.searchMatches = matches; 414 }, 415 416 searchCanceled: function(startingNewSearch) 417 { 418 WebInspector.Panel.prototype.searchCanceled.call(this, startingNewSearch); 419 420 if (!this._profiles) 421 return; 422 423 for (var i = 0; i < this._profiles.length; ++i) { 424 var profile = this._profiles[i]; 425 profile._profilesTreeElement.searchMatches = 0; 426 } 427 }, 428 429 _updateInterface: function() 430 { 431 // FIXME: Replace ProfileType-specific button visibility changes by a single ProfileType-agnostic "combo-button" visibility change. 432 if (InspectorBackend.profilerEnabled()) { 433 this.enableToggleButton.title = WebInspector.UIString("Profiling enabled. Click to disable."); 434 this.enableToggleButton.toggled = true; 435 for (var typeId in this._profileTypeButtonsByIdMap) 436 this._profileTypeButtonsByIdMap[typeId].removeStyleClass("hidden"); 437 this.profileViewStatusBarItemsContainer.removeStyleClass("hidden"); 438 this.panelEnablerView.visible = false; 439 } else { 440 this.enableToggleButton.title = WebInspector.UIString("Profiling disabled. Click to enable."); 441 this.enableToggleButton.toggled = false; 442 for (var typeId in this._profileTypeButtonsByIdMap) 443 this._profileTypeButtonsByIdMap[typeId].addStyleClass("hidden"); 444 this.profileViewStatusBarItemsContainer.addStyleClass("hidden"); 445 this.panelEnablerView.visible = true; 446 } 447 }, 448 449 _enableProfiling: function() 450 { 451 if (InspectorBackend.profilerEnabled()) 452 return; 453 this._toggleProfiling(this.panelEnablerView.alwaysEnabled); 454 }, 455 456 _toggleProfiling: function(optionalAlways) 457 { 458 if (InspectorBackend.profilerEnabled()) 459 InspectorBackend.disableProfiler(true); 460 else 461 InspectorBackend.enableProfiler(!!optionalAlways); 462 }, 463 464 _populateProfiles: function() 465 { 466 var sidebarTreeChildrenCount = this.sidebarTree.children.length; 467 for (var i = 0; i < sidebarTreeChildrenCount; ++i) { 468 var treeElement = this.sidebarTree.children[i]; 469 if (treeElement.children.length) 470 return; 471 } 472 473 function populateCallback(profileHeaders) { 474 profileHeaders.sort(function(a, b) { return a.uid - b.uid; }); 475 var profileHeadersLength = profileHeaders.length; 476 for (var i = 0; i < profileHeadersLength; ++i) 477 WebInspector.addProfileHeader(profileHeaders[i]); 478 } 479 480 var callId = WebInspector.Callback.wrap(populateCallback); 481 InspectorBackend.getProfileHeaders(callId); 482 483 delete this._shouldPopulateProfiles; 484 }, 485 486 updateMainViewWidth: function(width) 487 { 488 this.welcomeView.element.style.left = width + "px"; 489 this.profileViews.style.left = width + "px"; 490 this.profileViewStatusBarItemsContainer.style.left = width + "px"; 491 this.resize(); 492 } 493} 494 495WebInspector.ProfilesPanel.prototype.__proto__ = WebInspector.Panel.prototype; 496 497WebInspector.ProfileSidebarTreeElement = function(profile) 498{ 499 this.profile = profile; 500 501 if (this.profile.title.indexOf(UserInitiatedProfileName) === 0) 502 this._profileNumber = this.profile.title.substring(UserInitiatedProfileName.length + 1); 503 504 WebInspector.SidebarTreeElement.call(this, "profile-sidebar-tree-item", "", "", profile, false); 505 506 this.refreshTitles(); 507} 508 509WebInspector.ProfileSidebarTreeElement.prototype = { 510 onselect: function() 511 { 512 WebInspector.panels.profiles.showProfile(this.profile); 513 }, 514 515 get mainTitle() 516 { 517 if (this._mainTitle) 518 return this._mainTitle; 519 if (this.profile.title.indexOf(UserInitiatedProfileName) === 0) 520 return WebInspector.UIString("Profile %d", this._profileNumber); 521 return this.profile.title; 522 }, 523 524 set mainTitle(x) 525 { 526 this._mainTitle = x; 527 this.refreshTitles(); 528 }, 529 530 get subtitle() 531 { 532 // There is no subtitle. 533 }, 534 535 set subtitle(x) 536 { 537 // Can't change subtitle. 538 }, 539 540 set searchMatches(matches) 541 { 542 if (!matches) { 543 if (!this.bubbleElement) 544 return; 545 this.bubbleElement.removeStyleClass("search-matches"); 546 this.bubbleText = ""; 547 return; 548 } 549 550 this.bubbleText = matches; 551 this.bubbleElement.addStyleClass("search-matches"); 552 } 553} 554 555WebInspector.ProfileSidebarTreeElement.prototype.__proto__ = WebInspector.SidebarTreeElement.prototype; 556 557WebInspector.ProfileGroupSidebarTreeElement = function(title, subtitle) 558{ 559 WebInspector.SidebarTreeElement.call(this, "profile-group-sidebar-tree-item", title, subtitle, null, true); 560} 561 562WebInspector.ProfileGroupSidebarTreeElement.prototype = { 563 onselect: function() 564 { 565 WebInspector.panels.profiles.showProfile(this.children[this.children.length - 1].profile); 566 } 567} 568 569WebInspector.ProfileGroupSidebarTreeElement.prototype.__proto__ = WebInspector.SidebarTreeElement.prototype; 570 571WebInspector.didGetProfileHeaders = WebInspector.Callback.processCallback; 572WebInspector.didGetProfile = WebInspector.Callback.processCallback; 573