1 /* See license.txt for terms of usage */ 2 3 define([ 4 "firebug/lib/object", 5 "firebug/firebug", 6 "firebug/lib/domplate", 7 "firebug/chrome/reps", 8 "firebug/lib/locale", 9 "firebug/lib/wrapper", 10 "firebug/lib/url", 11 "firebug/js/stackFrame", 12 "firebug/lib/events", 13 "firebug/lib/css", 14 "firebug/lib/dom", 15 "firebug/lib/string", 16 "firebug/js/fbs", 17 ], 18 function(Obj, Firebug, Domplate, FirebugReps, Locale, Wrapper, Url, StackFrame, Events, 19 Css, Dom, Str, FBS) { 20 21 // ********************************************************************************************* // 22 // Constants 23 24 const Cc = Components.classes; 25 const Ci = Components.interfaces; 26 27 // ********************************************************************************************* // 28 // Profiler 29 30 Firebug.Profiler = Obj.extend(Firebug.Module, 31 { 32 dispatchName: "profiler", 33 34 showContext: function(browser, context) 35 { 36 this.setEnabled(context); 37 }, 38 39 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 40 // Activation 41 42 onPanelEnable: function(panelName) 43 { 44 if (FBTrace.DBG_PROFILER) 45 FBTrace.sysout("Profiler.onPanelEnable panelName: "+panelName+"\n"); 46 47 if (panelName == "console" || panelName == "script") 48 this.setEnabled(); 49 }, 50 51 onPanelDisable: function(panelName) 52 { 53 if (FBTrace.DBG_PROFILER) 54 FBTrace.sysout("Profiler.onPanelDisable panelName: "+panelName+"\n"); 55 56 if (panelName == "console" || panelName == "script") 57 this.setEnabled(); 58 }, 59 60 setEnabled: function() 61 { 62 if (!Firebug.currentContext) 63 return false; 64 65 // TODO this should be a panel listener operation. 66 67 // The profiler is available only if the Script panel and Console are enabled 68 var scriptPanel = Firebug.currentContext.getPanel("script", true); 69 var consolePanel = Firebug.currentContext.getPanel("console", true); 70 var disabled = (scriptPanel && !scriptPanel.isEnabled()) || 71 (consolePanel && !consolePanel.isEnabled()); 72 73 if (!disabled) 74 { 75 // The profiler is available only if the Debugger and Console are activated 76 var debuggerTool = Firebug.connection.getTool("script"); 77 var consoleTool = Firebug.connection.getTool("console"); 78 disabled = (debuggerTool && !debuggerTool.getActive()) || 79 (consoleTool && !consoleTool.getActive()); 80 } 81 82 // Attributes must be modified on the <command> element. All toolbar buttons 83 // and menuitems are hooked up to the command. 84 Firebug.chrome.setGlobalAttribute("cmd_firebug_toggleProfiling", "disabled", 85 disabled ? "true" : "false"); 86 87 // Update button's tooltip. 88 var tooltipText = disabled ? Locale.$STR("ProfileButton.Disabled.Tooltip") 89 : Locale.$STR("ProfileButton.Enabled.Tooltip"); 90 Firebug.chrome.setGlobalAttribute("cmd_firebug_toggleProfiling", "tooltiptext", tooltipText); 91 }, 92 93 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 94 95 onConsoleCleared: function(context) 96 { 97 if (this.isProfiling()) 98 this.stopProfiling(context, true); 99 }, 100 101 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 102 103 toggleProfiling: function(context) 104 { 105 if (FBS.profiling) 106 this.stopProfiling(context); 107 else 108 this.startProfiling(context); 109 }, 110 111 startProfiling: function(context, title) 112 { 113 FBS.startProfiling(); 114 115 Firebug.chrome.setGlobalAttribute("cmd_firebug_toggleProfiling", "checked", "true"); 116 117 var originalTitle = title; 118 var isCustomMessage = !!title; 119 if (!isCustomMessage) 120 title = Locale.$STR("ProfilerStarted"); 121 122 context.profileRow = this.logProfileRow(context, title); 123 context.profileRow.customMessage = isCustomMessage; 124 context.profileRow.originalTitle = originalTitle; 125 126 Events.dispatch(this.fbListeners, "startProfiling", [context, originalTitle]); 127 Firebug.Console.addListener(this); 128 }, 129 130 stopProfiling: function(context, cancelReport) 131 { 132 var totalTime = FBS.stopProfiling(); 133 if (totalTime == -1) 134 return; 135 136 Firebug.chrome.setGlobalAttribute("cmd_firebug_toggleProfiling", "checked", "false"); 137 138 if (cancelReport) 139 delete context.profileRow; 140 else 141 this.logProfileReport(context, cancelReport); 142 143 Firebug.Console.removeListener(this); 144 145 // stopProfiling event fired within logProfileReport 146 }, 147 148 isProfiling: function() 149 { 150 return (Firebug.chrome.getGlobalAttribute("cmd_firebug_toggleProfiling", "checked") === "true") 151 }, 152 153 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 154 155 logProfileRow: function(context, title) 156 { 157 var row = Firebug.Console.openGroup(title, context, "profile", 158 Firebug.Profiler.ProfileCaption, true, null, true); 159 Css.setClass(row, "profilerRunning"); 160 161 Firebug.Console.closeGroup(context, true); 162 163 return row; 164 }, 165 166 logProfileReport: function(context, cancelReport) 167 { 168 var calls = []; 169 var totalCalls = 0; 170 var totalTime = 0; 171 172 var sourceFileMap = context.sourceFileMap; 173 if (FBTrace.DBG_PROFILER) 174 { 175 for (var url in sourceFileMap) 176 FBTrace.sysout("logProfileReport: "+sourceFileMap[url]+"\n"); 177 } 178 179 var jsd = Cc["@mozilla.org/js/jsd/debugger-service;1"].getService(Ci.jsdIDebuggerService); 180 jsd.enumerateScripts({enumerateScript: function(script) 181 { 182 if (script.callCount) 183 { 184 if (!Firebug.filterSystemURLs || !Url.isSystemURL(script.fileName)) 185 { 186 var sourceLink = Firebug.SourceFile.getSourceLinkForScript(script, context); 187 if (sourceLink && sourceLink.href in sourceFileMap) 188 { 189 var call = new ProfileCall(script, context, script.callCount, 190 script.totalExecutionTime, script.totalOwnExecutionTime, 191 script.minExecutionTime, script.maxExecutionTime, sourceLink); 192 193 calls.push(call); 194 195 totalCalls += script.callCount; 196 totalTime += script.totalOwnExecutionTime; 197 } 198 } 199 script.clearProfileData(); 200 } 201 }}); 202 203 for (var i = 0; i < calls.length; ++i) 204 calls[i].percent = Math.round((calls[i].totalOwnTime/totalTime) * 100 * 100) / 100; 205 206 calls.sort(function(a, b) 207 { 208 return a.totalOwnTime < b.totalOwnTime ? 1 : -1; 209 }); 210 211 totalTime = Math.round(totalTime * 1000) / 1000; 212 213 var groupRow = context.profileRow && context.profileRow.ownerDocument 214 ? context.profileRow 215 : this.logProfileRow(context, ""); 216 delete context.profileRow; 217 218 Css.removeClass(groupRow, "profilerRunning"); 219 220 if (totalCalls > 0) 221 { 222 var captionBox = groupRow.getElementsByClassName("profileCaption").item(0); 223 if (!groupRow.customMessage) 224 captionBox.textContent = Locale.$STR("Profile"); 225 226 var timeBox = groupRow.getElementsByClassName("profileTime").item(0); 227 timeBox.textContent = Locale.$STRP("plural.Profile_Time2", [totalTime, totalCalls], 1); 228 229 var groupBody = groupRow.lastChild; 230 var sizer = Firebug.Profiler.ProfileTable.tag.replace({}, groupBody); 231 var table = sizer.firstChild; 232 var tHeader = table.lastChild; // no rows inserted. 233 234 var tag = Firebug.Profiler.ProfileCall.tag; 235 var insert = tag.insertRows; 236 237 for (var i = 0; i < calls.length; ++i) { 238 calls[i].index = i; 239 context.throttle(insert, tag, [{object: calls[i]}, tHeader]); 240 } 241 242 context.throttle(groupRow.scrollIntoView, groupRow, []); 243 } 244 else 245 { 246 var captionBox = groupRow.getElementsByClassName("profileCaption").item(0); 247 captionBox.textContent = Locale.$STR("NothingToProfile"); 248 } 249 250 Events.dispatch(this.fbListeners, "stopProfiling", [context, 251 groupRow.originalTitle, calls, cancelReport]); 252 } 253 }); 254 255 // ********************************************************************************************* // 256 257 with (Domplate) { 258 Firebug.Profiler.ProfileTable = domplate( 259 { 260 tag: 261 DIV({"class": "profileSizer", "tabindex": "-1" }, 262 TABLE({"class": "profileTable", cellspacing: 0, cellpadding: 0, width: "100%", 263 "role": "grid"}, 264 THEAD({"class": "profileThead", "role": "presentation"}, 265 TR({"class": "headerRow focusRow profileRow subFocusRow", onclick: "$onClick", 266 "role": "row"}, 267 TH({"class": "headerCell alphaValue a11yFocus", "role": "columnheader"}, 268 DIV({"class": "headerCellBox"}, 269 Locale.$STR("Function") 270 ) 271 ), 272 TH({"class": "headerCell a11yFocus", "role": "columnheader"}, 273 DIV({"class": "headerCellBox", title: Locale.$STR("CallsHeaderTooltip")}, 274 Locale.$STR("Calls") 275 ) 276 ), 277 TH({"class": "headerCell headerSorted a11yFocus", "role": "columnheader", 278 "aria-sort": "descending"}, 279 DIV({"class": "headerCellBox", title: Locale.$STR("PercentTooltip")}, 280 Locale.$STR("Percent") 281 ) 282 ), 283 TH({"class": "headerCell a11yFocus", "role": "columnheader"}, 284 DIV({"class": "headerCellBox", title: Locale.$STR("OwnTimeHeaderTooltip")}, 285 Locale.$STR("OwnTime") 286 ) 287 ), 288 TH({"class": "headerCell a11yFocus", "role": "columnheader"}, 289 DIV({"class": "headerCellBox", title: Locale.$STR("TimeHeaderTooltip")}, 290 Locale.$STR("Time") 291 ) 292 ), 293 TH({"class": "headerCell a11yFocus", "role": "columnheader"}, 294 DIV({"class": "headerCellBox", title: Locale.$STR("AvgHeaderTooltip")}, 295 Locale.$STR("Avg") 296 ) 297 ), 298 TH({"class": "headerCell a11yFocus", "role": "columnheader"}, 299 DIV({"class": "headerCellBox", title: Locale.$STR("MinHeaderTooltip")}, 300 Locale.$STR("Min") 301 ) 302 ), 303 TH({"class": "headerCell a11yFocus", "role": "columnheader"}, 304 DIV({"class": "headerCellBox", title: Locale.$STR("MaxHeaderTooltip")}, 305 Locale.$STR("Max") 306 ) 307 ), 308 TH({"class": "headerCell alphaValue a11yFocus", "role": "columnheader"}, 309 DIV({"class": "headerCellBox"}, 310 Locale.$STR("File") 311 ) 312 ) 313 ) 314 ), 315 TBODY({"class": "profileTbody", "role": "presentation"}) 316 ) 317 ), 318 319 onClick: function(event) 320 { 321 var table = Dom.getAncestorByClass(event.target, "profileTable"); 322 var header = Dom.getAncestorByClass(event.target, "headerCell"); 323 if (!header) 324 return; 325 326 var numerical = !Css.hasClass(header, "alphaValue"); 327 328 var colIndex = 0; 329 for (header = header.previousSibling; header; header = header.previousSibling) 330 ++colIndex; 331 332 this.sort(table, colIndex, numerical); 333 }, 334 335 sort: function(table, colIndex, numerical) 336 { 337 sortAscending = function() 338 { 339 Css.removeClass(header, "sortedDescending"); 340 Css.setClass(header, "sortedAscending"); 341 header.setAttribute("aria-sort", "ascending"); 342 343 header.sorted = -1; 344 345 for (var i = 0; i < values.length; ++i) 346 tbody.appendChild(values[i].row); 347 }, 348 349 sortDescending = function() 350 { 351 Css.removeClass(header, "sortedAscending"); 352 Css.setClass(header, "sortedDescending"); 353 header.setAttribute("aria-sort", "descending") 354 355 header.sorted = 1; 356 357 for (var i = values.length-1; i >= 0; --i) 358 tbody.appendChild(values[i].row); 359 } 360 361 var tbody = Dom.getChildByClass(table, "profileTbody"); 362 var thead = Dom.getChildByClass(table, "profileThead"); 363 364 var values = []; 365 for (var row = tbody.childNodes[0]; row; row = row.nextSibling) 366 { 367 var cell = row.childNodes[colIndex]; 368 var value = numerical ? parseFloat(cell.textContent) : cell.textContent; 369 values.push({row: row, value: value}); 370 } 371 372 values.sort(function(a, b) { return a.value < b.value ? -1 : 1; }); 373 374 var headerRow = thead.firstChild; 375 var headerSorted = Dom.getChildByClass(headerRow, "headerSorted"); 376 Css.removeClass(headerSorted, "headerSorted"); 377 if (headerSorted) 378 headerSorted.removeAttribute('aria-sort'); 379 380 var header = headerRow.childNodes[colIndex]; 381 Css.setClass(header, "headerSorted"); 382 383 if (numerical) 384 { 385 if (!header.sorted || header.sorted == -1) 386 { 387 sortDescending(); 388 } 389 else 390 { 391 sortAscending(); 392 } 393 } 394 else 395 { 396 if (!header.sorted || header.sorted == -1) 397 { 398 sortAscending(); 399 } 400 else 401 { 402 sortDescending(); 403 } 404 } 405 } 406 }); 407 408 // ********************************************************************************************* // 409 410 Firebug.Profiler.ProfileCaption = domplate(Firebug.Rep, 411 { 412 tag: 413 SPAN({"class": "profileTitle", "role": "status"}, 414 SPAN({"class": "profileCaption"}, "$object"), 415 " ", 416 SPAN({"class": "profileTime"}, "") 417 ) 418 }); 419 420 // ********************************************************************************************* // 421 422 Firebug.Profiler.ProfileCall = domplate(Firebug.Rep, 423 { 424 tag: 425 TR({"class": "focusRow profileRow subFocusRow", "role": "row"}, 426 TD({"class": "profileCell", "role": "presentation"}, 427 FirebugReps.OBJECTLINK("$object|getCallName") 428 ), 429 TD({"class": "a11yFocus profileCell", "role": "gridcell"}, "$object.callCount"), 430 TD({"class": "a11yFocus profileCell", "role": "gridcell"}, "$object.percent%"), 431 TD({"class": "a11yFocus profileCell", "role": "gridcell"}, "$object.totalOwnTime|roundTime\\ms"), 432 TD({"class": "a11yFocus profileCell", "role": "gridcell"}, "$object.totalTime|roundTime\\ms"), 433 TD({"class": "a11yFocus profileCell", "role": "gridcell"}, "$object|avgTime|roundTime\\ms"), 434 TD({"class": "a11yFocus profileCell", "role": "gridcell"}, "$object.minTime|roundTime\\ms"), 435 TD({"class": "a11yFocus profileCell", "role": "gridcell"}, "$object.maxTime|roundTime\\ms"), 436 TD({"class": "linkCell profileCell", "role": "presentation"}, 437 TAG(FirebugReps.SourceLink.tag, {object: "$object|getSourceLink"}) 438 ) 439 ), 440 441 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 442 443 getCallName: function(call) 444 { 445 return Str.cropString(StackFrame.getFunctionName(call.script, call.context), 60); 446 }, 447 448 avgTime: function(call) 449 { 450 return call.totalTime / call.callCount; 451 }, 452 453 getSourceLink: function(call) 454 { 455 return call.sourceLink; 456 }, 457 458 roundTime: function(ms) 459 { 460 return Math.round(ms * 1000) / 1000; 461 }, 462 463 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 464 465 className: "profile", 466 467 supportsObject: function(object, type) 468 { 469 return object instanceof ProfileCall; 470 }, 471 472 inspectObject: function(call, context) 473 { 474 var sourceLink = this.getSourceLink(call); 475 Firebug.chrome.select(sourceLink); 476 }, 477 478 getTooltip: function(call) 479 { 480 try 481 { 482 var fn = StackFrame.getFunctionName(call.script, call.context); 483 return FirebugReps.Func.getTooltip(fn, call.context); 484 } 485 catch (exc) 486 { 487 if (FBTrace.DBG_ERRORS) 488 FBTrace.sysout("profiler.getTooltip FAILS ", exc); 489 } 490 }, 491 492 getContextMenuItems: function(call, target, context) 493 { 494 var fn = Wrapper.unwrapIValue(call.script.functionObject); 495 return FirebugReps.Func.getContextMenuItems(fn, call.script, context); 496 } 497 }); 498 499 } // END Domplate 500 501 // ********************************************************************************************* // 502 503 function ProfileCall(script, context, callCount, totalTime, totalOwnTime, minTime, maxTime, sourceLink) 504 { 505 this.script = script; 506 this.context = context; 507 this.callCount = callCount; 508 this.totalTime = totalTime; 509 this.totalOwnTime = totalOwnTime; 510 this.minTime = minTime; 511 this.maxTime = maxTime; 512 this.sourceLink = sourceLink; 513 } 514 515 // ********************************************************************************************* // 516 // CommandLine Support 517 518 function profile(context, args) 519 { 520 var title = args[0]; 521 Firebug.Profiler.startProfiling(context, title); 522 return Firebug.Console.getDefaultReturnValue(context.window); 523 }; 524 525 function profileEnd(context) 526 { 527 Firebug.Profiler.stopProfiling(context); 528 return Firebug.Console.getDefaultReturnValue(context.window); 529 }; 530 531 // ********************************************************************************************* // 532 // Registration 533 534 Firebug.registerModule(Firebug.Profiler); 535 Firebug.registerRep(Firebug.Profiler.ProfileCall); 536 537 Firebug.registerCommand("profile", { 538 handler: profile.bind(this), 539 helpUrl: "http://getfirebug.com/wiki/index.php/profile", 540 description: Locale.$STR("console.cmd.help.profile") 541 }) 542 543 Firebug.registerCommand("profileEnd", { 544 handler: profileEnd.bind(this), 545 helpUrl: "http://getfirebug.com/wiki/index.php/profileEnd", 546 description: Locale.$STR("console.cmd.help.profileEnd") 547 }) 548 549 return Firebug.Profiler; 550 551 // ********************************************************************************************* // 552 }); 553