1 /* See license.txt for terms of usage */
  2 
  3 
  4 FBL.ns(function() { with (FBL) {
  5 
  6 var toggleProfiling = $("fbToggleProfiling");
  7 
  8 // ************************************************************************************************
  9 
 10 Firebug.Profiler = extend(Firebug.Module,
 11 {
 12     dispatchName: "profiler",
 13     showContext: function(browser, context)
 14     {
 15         this.setEnabled(context);
 16     },
 17 
 18     onPanelEnable: function(panelName)
 19     {
 20         if (FBTrace.DBG_DISPATCH)
 21             FBTrace.sysout("Profiler.onPanelEnable panelName: "+panelName+"\n");
 22 
 23         if (panelName == "net" || panelName == "script")
 24             this.setEnabled();
 25     },
 26 
 27     onPanelDisable: function(panelName)
 28     {
 29         if (FBTrace.DBG_DISPATCH)
 30             FBTrace.sysout("Profiler.onPanelDisable panelName: "+panelName+"\n");
 31 
 32        if (panelName == "net" || panelName == "script")
 33             this.setEnabled();
 34     },
 35 
 36     setEnabled: function()
 37     {
 38         // The profiler is available only if the debugger (script panel) and console are enabled.
 39         var debuggerEnabled = Firebug.Debugger.isAlwaysEnabled();
 40         var consoleEnabled = Firebug.Console.isAlwaysEnabled();
 41         toggleProfiling.disabled = !debuggerEnabled || !consoleEnabled;
 42 
 43         // Update button's tooltip.
 44         var tooltipText = toggleProfiling.disabled ? $STR("ProfileButton.Disabled.Tooltip")
 45             : $STR("ProfileButton.Enabled.Tooltip");
 46         toggleProfiling.setAttribute("tooltiptext", tooltipText);
 47     },
 48 
 49     toggleProfiling: function(context)
 50     {
 51         if (fbs.profiling)
 52             this.stopProfiling(context);
 53         else
 54             this.startProfiling(context);
 55     },
 56 
 57     startProfiling: function(context, title)
 58     {
 59         fbs.startProfiling();
 60 
 61         Firebug.chrome.setGlobalAttribute("cmd_toggleProfiling", "checked", "true");
 62 
 63         var isCustomMessage = !!title;
 64         if (!isCustomMessage)
 65             title = $STR("ProfilerStarted");
 66 
 67         context.profileRow = this.logProfileRow(context, title);
 68         context.profileRow.customMessage = isCustomMessage ;
 69     },
 70 
 71     isProfiling: function()
 72     {
 73         return (Firebug.chrome.getGlobalAttribute("cmd_toggleProfiling", "checked") === "true")
 74     },
 75 
 76     stopProfiling: function(context, cancelReport)
 77     {
 78         var totalTime = fbs.stopProfiling();
 79         if (totalTime == -1)
 80             return;
 81 
 82         Firebug.chrome.setGlobalAttribute("cmd_toggleProfiling", "checked", "false");
 83 
 84         if (cancelReport)
 85             delete context.profileRow;
 86         else
 87             this.logProfileReport(context)
 88     },
 89 
 90     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 91 
 92     logProfileRow: function(context, title)
 93     {
 94         var row = Firebug.Console.openGroup(title, context, "profile",
 95             Firebug.Profiler.ProfileCaption, true, null, true);
 96         setClass(row, "profilerRunning");
 97 
 98         Firebug.Console.closeGroup(context, true);
 99 
100         return row;
101     },
102 
103     logProfileReport: function(context)
104     {
105         var calls = [];
106         var totalCalls = 0;
107         var totalTime = 0;
108 
109         var sourceFileMap = context.sourceFileMap;
110         if (FBTrace.DBG_SOURCEFILES)
111         {
112             for (url in sourceFileMap)
113                 FBTrace.sysout("logProfileReport: "+sourceFileMap[url]+"\n");
114         }
115 
116         jsd.enumerateScripts({enumerateScript: function(script)
117         {
118             if (script.callCount)
119             {
120                 if (!Firebug.filterSystemURLs || !isSystemURL(script.fileName))
121                 {
122                     var sourceLink = FBL.getSourceLinkForScript(script, context);
123                     if (sourceLink && sourceLink.href in sourceFileMap)
124                     {
125                         var call = new ProfileCall(script, context, script.callCount, script.totalExecutionTime,
126                                 script.totalOwnExecutionTime, script.minExecutionTime, script.maxExecutionTime, sourceLink);
127                         calls.push(call);
128 
129                         totalCalls += script.callCount;
130                         totalTime += script.totalOwnExecutionTime;
131                     }
132                 }
133                 script.clearProfileData();
134             }
135         }});
136 
137         for (var i = 0; i < calls.length; ++i)
138             calls[i].percent = Math.round((calls[i].totalOwnTime/totalTime) * 100 * 100) / 100;
139 
140         calls.sort(function(a, b)
141         {
142            return a.totalOwnTime < b.totalOwnTime ? 1 : -1;
143         });
144 
145         totalTime = Math.round(totalTime * 1000) / 1000;
146 
147         var groupRow = context.profileRow && context.profileRow.ownerDocument
148             ? context.profileRow
149             : this.logProfileRow(context, "");
150         delete context.profileRow;
151 
152         removeClass(groupRow, "profilerRunning");
153 
154         if (totalCalls > 0)
155         {
156             var captionBox = groupRow.getElementsByClassName("profileCaption").item(0);
157             if (!groupRow.customMessage)
158                 captionBox.textContent = $STR("Profile");
159             var timeBox = groupRow.getElementsByClassName("profileTime").item(0);
160             timeBox.textContent = $STRP("plural.Profile_Time", [totalTime, totalCalls], 1);
161 
162             var groupBody = groupRow.lastChild;
163             var sizer = Firebug.Profiler.ProfileTable.tag.replace({}, groupBody);
164             var table = sizer.firstChild;
165             var tHeader = table.lastChild;  // no rows inserted.
166 
167             var tag = Firebug.Profiler.ProfileCall.tag;
168             var insert = tag.insertRows;
169 
170             for (var i = 0; i < calls.length; ++i) {
171                 calls[i].index = i;
172                 context.throttle(insert, tag, [{object: calls[i]}, tHeader]);
173             }
174 
175             context.throttle(groupRow.scrollIntoView, groupRow);
176         }
177         else
178         {
179             var captionBox = groupRow.getElementsByClassName("profileCaption").item(0);
180             captionBox.textContent = $STR("NothingToProfile");
181         }
182     }
183 });
184 
185 // ************************************************************************************************
186 
187 Firebug.Profiler.ProfileTable = domplate(
188 {
189     tag:
190       DIV({class: "profileSizer", "tabindex": "-1" },
191         TABLE({class: "profileTable", cellspacing: 0, cellpadding: 0, width: "100%", "role": "grid"},
192             TBODY({class: "profileTbody", "role": "presentation"},
193                 TR({class: "headerRow focusRow profileRow subFocusRow", onclick: "$onClick", "role": "row"},
194                     TH({class: "headerCell alphaValue a11yFocus", "role": "columnheader"},
195                         DIV({class: "headerCellBox"},
196                             $STR("Function")
197                         )
198                     ),
199                     TH({class: "headerCell a11yFocus" , "role": "columnheader"},
200                         DIV({class: "headerCellBox", title: $STR("CallsHeaderTooltip")},
201                             $STR("Calls")
202                         )
203                     ),
204                     TH({class: "headerCell headerSorted a11yFocus", "role": "columnheader", "aria-sort": "descending"},
205                         DIV({class: "headerCellBox", title: $STR("PercentTooltip")},
206                             $STR("Percent")
207                         )
208                     ),
209                     TH({class: "headerCell a11yFocus", "role": "columnheader"},
210                         DIV({class: "headerCellBox", title: $STR("OwnTimeHeaderTooltip")},
211                             $STR("OwnTime")
212                         )
213                     ),
214                     TH({class: "headerCell a11yFocus", "role": "columnheader"},
215                         DIV({class: "headerCellBox", title: $STR("TimeHeaderTooltip")},
216                             $STR("Time")
217                         )
218                     ),
219                     TH({class: "headerCell a11yFocus", "role": "columnheader"},
220                         DIV({class: "headerCellBox", title: $STR("AvgHeaderTooltip")},
221                             $STR("Avg")
222                         )
223                     ),
224                     TH({class: "headerCell a11yFocus", "role": "columnheader"},
225                         DIV({class: "headerCellBox", title: $STR("MinHeaderTooltip")},
226                             $STR("Min")
227                         )
228                     ),
229                     TH({class: "headerCell a11yFocus", "role": "columnheader"},
230                         DIV({class: "headerCellBox", title: $STR("MaxHeaderTooltip")},
231                             $STR("Max")
232                         )
233                     ),
234                     TH({class: "headerCell alphaValue a11yFocus", "role": "columnheader"},
235                         DIV({class: "headerCellBox"},
236                             $STR("File")
237                         )
238                     )
239                 )
240             )
241           )
242         ),
243 
244     onClick: function(event)
245     {
246         var table = getAncestorByClass(event.target, "profileTable");
247         var header = getAncestorByClass(event.target, "headerCell");
248         if (!header)
249             return;
250 
251         var numerical = !hasClass(header, "alphaValue");
252 
253         var colIndex = 0;
254         for (header = header.previousSibling; header; header = header.previousSibling)
255             ++colIndex;
256 
257         this.sort(table, colIndex, numerical);
258     },
259 
260     sort: function(table, colIndex, numerical)
261     {
262         var tbody = getChildByClass(table, "profileTbody");
263 
264         var values = [];
265         for (var row = tbody.childNodes[1]; row; row = row.nextSibling)
266         {
267             var cell = row.childNodes[colIndex];
268             var value = numerical ? parseFloat(cell.textContent) : cell.textContent;
269             values.push({row: row, value: value});
270         }
271 
272         values.sort(function(a, b) { return a.value < b.value ? -1 : 1; });
273 
274         var headerRow = tbody.firstChild;
275         var headerSorted = getChildByClass(headerRow, "headerSorted");
276         removeClass(headerSorted, "headerSorted");
277         if (headerSorted)
278             headerSorted.removeAttribute('aria-sort');
279 
280         var header = headerRow.childNodes[colIndex];
281         setClass(header, "headerSorted");
282 
283         if (!header.sorted || header.sorted == 1)
284         {
285             removeClass(header, "sortedDescending");
286             setClass(header, "sortedAscending");
287             header.setAttribute("aria-sort", "ascending");
288 
289             header.sorted = -1;
290 
291             for (var i = 0; i < values.length; ++i) {
292                 values[i].row.setAttribute("odd", (i % 2));
293                 tbody.appendChild(values[i].row);
294             }
295         }
296         else
297         {
298             removeClass(header, "sortedAscending");
299             setClass(header, "sortedDescending");
300             header.setAttribute("aria-sort", "descending")
301 
302             header.sorted = 1;
303 
304             for (var i = values.length-1; i >= 0; --i) {
305                 values[i].row.setAttribute("odd", (Math.abs(i-values.length-1) % 2));
306                 tbody.appendChild(values[i].row);
307             }
308         }
309     }
310 });
311 
312 // ************************************************************************************************
313 
314 Firebug.Profiler.ProfileCaption = domplate(Firebug.Rep,
315 {
316     tag:
317         SPAN({class: "profileTitle", "role": "status"},
318             SPAN({class: "profileCaption"}, "$objects"),
319             " ",
320             SPAN({class: "profileTime"}, "")
321         )
322 });
323 
324 // ************************************************************************************************
325 
326 Firebug.Profiler.ProfileCall = domplate(Firebug.Rep,
327 {
328     tag:
329         TR({"class": "focusRow profileRow subFocusRow", odd: "$object|isOddRow", "role": "row"},
330             TD({"class": "profileCell", "role": "presentation"},
331                 FirebugReps.OBJECTLINK("$object|getCallName")
332             ),
333             TD({"class": "a11yFocus profileCell", "role": "gridcell"}, "$object.callCount"),
334             TD({"class": "a11yFocus profileCell", "role": "gridcell"}, "$object.percent%"),
335             TD({"class": "a11yFocus profileCell", "role": "gridcell"}, "$object.totalOwnTime|roundTime\\ms"),
336             TD({"class": "a11yFocus profileCell", "role": "gridcell"}, "$object.totalTime|roundTime\\ms"),
337             TD({"class": "a11yFocus profileCell", "role": "gridcell"}, "$object|avgTime|roundTime\\ms"),
338             TD({"class": "a11yFocus profileCell", "role": "gridcell"}, "$object.minTime|roundTime\\ms"),
339             TD({"class": "a11yFocus profileCell", "role": "gridcell"}, "$object.maxTime|roundTime\\ms"),
340             TD({class: "linkCell profileCell", "role": "presentation"},
341                 TAG(FirebugReps.SourceLink.tag, {object: "$object|getSourceLink"})
342             )
343         ),
344 
345     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
346 
347     isOddRow: function(call)
348     {
349         return (call.index % 2) ? 1 : 0;
350     },
351 
352     getCallName: function(call)
353     {
354         return cropString(getFunctionName(call.script, call.context), 60);
355     },
356 
357     avgTime: function(call)
358     {
359         return call.totalTime / call.callCount;
360     },
361 
362     getSourceLink: function(call)
363     {
364         return call.sourceLink;
365     },
366 
367     roundTime: function(ms)
368     {
369         return Math.round(ms * 1000) / 1000;
370     },
371 
372     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
373 
374     className: "profile",
375 
376     supportsObject: function(object)
377     {
378         return object instanceof ProfileCall;
379     },
380 
381     inspectObject: function(call, context)
382     {
383         var sourceLink = this.getSourceLink(call);
384         Firebug.chrome.select(sourceLink);
385     },
386 
387     getTooltip: function(call)
388     {
389         try
390         {
391             var fn = unwrapIValue(call.script.functionObject);
392             return FirebugReps.Func.getTooltip(fn, call.context);
393         }
394         catch (exc)
395         {
396             if (FBTrace.DBG_ERRORS)
397                 FBTrace.sysout("profiler.getTooltip FAILS ", exc);
398         }
399     },
400 
401     getContextMenuItems: function(call, target, context)
402     {
403         var fn = unwrapIValue(call.script.functionObject);
404         return FirebugReps.Func.getContextMenuItems(fn, call.script, context);
405     }
406 });
407 
408 // ************************************************************************************************
409 
410 function ProfileCall(script, context, callCount, totalTime, totalOwnTime, minTime, maxTime, sourceLink)
411 {
412     this.script = script;
413     this.context = context;
414     this.callCount = callCount;
415     this.totalTime = totalTime;
416     this.totalOwnTime = totalOwnTime;
417     this.minTime = minTime;
418     this.maxTime = maxTime;
419     this.sourceLink = sourceLink;
420 }
421 
422 // ************************************************************************************************
423 
424 Firebug.registerModule(Firebug.Profiler);
425 Firebug.registerRep(Firebug.Profiler.ProfileCall);
426 
427 // ************************************************************************************************
428 
429 }});
430