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