1 /* See license.txt for terms of usage */
  2 
  3 define([
  4     "firebug/firebug",
  5     "firebug/lib/trace",
  6     "firebug/lib/domplate",
  7     "firebug/lib/object",
  8     "firebug/lib/locale",
  9     "firebug/lib/dom",
 10     "firebug/lib/events",
 11     "firebug/lib/string",
 12     "firebug/lib/wrapper",
 13     "firebug/lib/css",
 14 ],
 15 function(Firebug, FBTrace, Domplate, Obj, Locale, Dom, Events, Str, Wrapper, Css) {
 16 with (Domplate) {
 17 
 18 // ********************************************************************************************* //
 19 // Docs
 20 
 21 // See http://www.w3.org/TR/navigation-timing/
 22 
 23 // ********************************************************************************************* //
 24 // Constants
 25 
 26 const Cc = Components.classes;
 27 const Ci = Components.interfaces;
 28 
 29 // List of timing properties in performance.timing structure.
 30 var timingProps = [
 31     "connectEnd",
 32     "connectStart",
 33     "domComplete",
 34     "domContentLoadedEventEnd",
 35     "domContentLoadedEventStart",
 36     "domInteractive",
 37     "domLoading",
 38     "domainLookupEnd",
 39     "domainLookupStart",
 40     "fetchStart",
 41     "loadEventEnd",
 42     "loadEventStart",
 43     "navigationStart",
 44     "redirectCount",
 45     "redirectEnd",
 46     "redirectStart",
 47     "requestStart",
 48     "responseEnd",
 49     "responseStart",
 50     "unloadEventEnd",
 51     "unloadEventStart",
 52 ];
 53 
 54 // ********************************************************************************************* //
 55 // Module
 56 
 57 var PerformanceTimingModule = Obj.extend(Firebug.Module,
 58 {
 59     initialize: function(prefDomain, prefNames)
 60     {
 61         Firebug.Module.initialize.apply(this, arguments);
 62         Firebug.Console.addListener(ConsoleListener);
 63     },
 64 
 65     shutdown: function()
 66     {
 67         Firebug.Module.shutdown.apply(this, arguments);
 68         Firebug.Console.removeListener(ConsoleListener);
 69     },
 70 });
 71 
 72 // ********************************************************************************************* //
 73 // Domplate
 74 
 75 /**
 76  * This template is used to render the timing waterfall graph.
 77  */
 78 var PerformanceTimingRep = domplate(Firebug.Rep,
 79 /** @lends PerformanceTimingRep */
 80 {
 81     className: "perfTiming",
 82 
 83     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
 84 
 85     tag:
 86         TABLE({"class": "perfTimingTable", cellspacing: 0, cellpadding: 0, width: "100%",
 87             "role": "grid", _repObject: "$object"},
 88             TBODY({"class": "perfTimingTbody", "role": "presentation"},
 89                 FOR("bar", "$object.bars",
 90                     TR(
 91                         TD(
 92                             DIV({"class": "perfTimingBox"},
 93                                 DIV({"class": "perfTimingBar $bar.className",
 94                                     style: "left: $bar.left%; width: $bar.width%"},
 95                                     SPAN({"class": "perfTimingBarLabel"}, "$bar.label")
 96                                 ),
 97                                 DIV({"class": "perfTimingEvent domLoading",
 98                                     style: "left: $bar.domLoading%;"}
 99                                 ),
100                                 DIV({"class": "perfTimingEvent domInteractive",
101                                     style: "left: $bar.domInteractive%;"}
102                                 ),
103                                 DIV({"class": "perfTimingEvent domContentLoaded",
104                                     style: "left: $bar.domContentLoaded%;"}
105                                 ),
106                                 DIV({"class": "perfTimingEvent onLoad",
107                                     style: "left: $bar.onLoad%;"}
108                                 ),
109                                 DIV({"class": "perfTimingEvent cursor"})
110                             )
111                         )
112                     )
113                 )
114             )
115         ),
116 
117     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
118 
119     getRealObject: function(object)
120     {
121         return Wrapper.unwrapObject(object.timing);
122     },
123 
124     supportsObject: function(object, type)
125     {
126         return (object instanceof PerfTimingObj);
127     },
128 
129     getContextMenuItems: function(object, target, context)
130     {
131         return [];
132     },
133 
134     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
135 
136     showInfoTip: function(infoTip, target, x, y)
137     {
138         var table = Dom.getAncestorByClass(target, "perfTimingTable");
139         if (!table)
140             return false;
141 
142         var timingObj = table.repObject;
143         return PerfInfoTip.render(timingObj.timing, infoTip);
144     }
145 });
146 
147 // ********************************************************************************************* //
148 // InfoTip
149 
150 /**
151  * Hovering mouse over the waterfall graph shows an infotip. This template is responsible
152  * for rendering its content.
153  */
154 var PerfInfoTip = domplate(Firebug.Rep,
155 /** @lends PerfInfoTip */
156 {
157     tableTag:
158         TABLE({"class": "timeInfoTip", "id": "fbPerfTimingInfoTip"},
159             TBODY()
160         ),
161 
162     timingsTag:
163         FOR("bar", "$bars",
164             TR({"class": "timeInfoTipRow", $collapsed: "$bar|hideBar"},
165                 TD({"class": "timeInfoTipBar $bar|getClassName"}),
166                 TD({"class": "timeInfoTipCell startTime"},
167                     "$bar.start|formatStartTime"
168                 ),
169                 TD({"class": "timeInfoTipCell elapsedTime"},
170                     "$bar.elapsed|formatTime"
171                 ),
172                 TD("$bar|getLabel")
173             )
174         ),
175 
176     separatorTag:
177         TR(
178             TD({"class": "timeInfoTipSeparator", "colspan": 4, "height": "10px"},
179                 SPAN("$label")
180             )
181         ),
182 
183     eventsTag:
184         FOR("event", "$events",
185             TR({"class": "timeInfoTipEventRow"},
186                 TD({"class": "timeInfoTipBar", align: "center"},
187                     DIV({"class": "$event|getClassName timeInfoTipEventBar"})
188                 ),
189                 TD("$event.start|formatStartTime"),
190                 TD({"class": "timeInfotTipEventName", "colspan": 2},
191                     "$event|getTimeStampLabel"
192                 )
193             )
194         ),
195 
196     hideBar: function(obj)
197     {
198         return !obj.elapsed && obj.className == "redirect";
199     },
200 
201     getClassName: function(obj)
202     {
203         return obj.className;
204     },
205 
206     formatTime: function(time)
207     {
208         return Str.formatTime(time);
209     },
210 
211     formatStartTime: function(time)
212     {
213         var label = Str.formatTime(time);
214         if (!time)
215             return label;
216 
217         return (time > 0 ? "+" : "") + label;
218     },
219 
220     getLabel: function(obj)
221     {
222         return Locale.$STR("perftiming." + obj.label);
223     },
224 
225     getTimeStampLabel: function(obj)
226     {
227         return obj.name;
228     },
229 
230     render: function(timing, parentNode)
231     {
232         var infoTip = Firebug.NetMonitor.TimeInfoTip.tableTag.replace({}, parentNode);
233 
234         // Insert top description.
235         this.separatorTag.insertRows({label: Locale.$STR("perftiming.bars.label")},
236             infoTip.firstChild);
237 
238         // Insert request timing info.
239         var bars = calculateBars(timing);
240         this.timingsTag.insertRows({bars: bars}, infoTip.firstChild);
241 
242         var t = timing;
243 
244         var events = [];
245         events.push({
246             name: "DOM Loading",
247             className: "domLoading",
248             start: t.domLoading - t.navigationStart
249         });
250 
251         events.push({
252             name: "DOM Interactive",
253             className: "domInteractive",
254             start: t.domInteractive - t.navigationStart,
255         });
256 
257         events.push({
258             name: "DOMContentLoaded",
259             className: "domContentLoaded",
260             start: t.domContentLoadedEventStart - t.navigationStart,
261         });
262 
263         events.push({
264             name: "load",
265             className: "onLoad",
266             start: t.loadEventStart - t.navigationStart,
267         })
268 
269         // Insert separator.
270         this.separatorTag.insertRows({label: Locale.$STR("requestinfo.timings.label")},
271             infoTip.firstChild);
272 
273         this.eventsTag.insertRows({events: events}, infoTip.firstChild);
274 
275         return true;
276     }
277 });
278 
279 // ********************************************************************************************* //
280 // Rep Object
281 
282 function PerfTimingObj(bars, timing)
283 {
284     this.bars = bars;
285     this.timing = timing;
286 }
287 
288 // ********************************************************************************************* //
289 
290 /**
291  * Console listener is responsible for rendering the Performance visualization every time
292  * the user logs 'performance.timing' on the command line.
293  */
294 var ConsoleListener =
295 /** @lends ConsoleListener */
296 {
297     tag:
298         DIV({_repObject: "$object"},
299             DIV({"class": "documentCookieBody"})
300         ),
301 
302     log: function(context, object, className, sourceLink)
303     {
304         if (!context || !object)
305             return;
306 
307         var type = Object.prototype.toString.call(object);
308         if (type === "[object PerformanceTiming]")
309             performanceTiming(context, object);
310     },
311 
312     logFormatted: function(context, objects, className, sourceLink)
313     {
314     }
315 };
316 
317 // ********************************************************************************************* //
318 // Console Logging
319 
320 /**
321  * This function is responsible for inserting the waterfall graph into the Console panel.
322  */
323 function performanceTiming(context, timing)
324 {
325     var t = timing;
326     var elapsed = t.loadEventEnd - t.navigationStart;
327 
328     var objects = [];
329     var rep = PerformanceTimingRep;
330     var bars = calculateBars(t);
331 
332     var result = []
333     for (var i=0; i<bars.length; i++)
334     {
335         var bar = bars[i];
336 
337         // Filter our empty bars.
338         if (!bar.elapsed)
339             continue;
340 
341         bar.left = calculatePos(bar.start, elapsed);
342         bar.width = calculatePos(bar.elapsed, elapsed);
343         bar.label = bar.label + " " + Str.formatTime(bar.elapsed);
344 
345         result.push(bar);
346     }
347 
348     // Events
349     var domLoading = calculatePos(t.domLoading - t.navigationStart, elapsed);
350     var domInteractive = calculatePos(t.domInteractive - t.navigationStart, elapsed);
351     var domContentLoaded = calculatePos(t.domContentLoadedEventStart - t.navigationStart, elapsed);
352     var onLoad = calculatePos(t.loadEventStart - t.navigationStart, elapsed);
353 
354     for (var i=0; i<result.length; i++)
355     {
356         var bar = result[i];
357         bar.domLoading = domLoading;
358         bar.domInteractive = domInteractive;
359         bar.domContentLoaded = domContentLoaded;
360         bar.onLoad = onLoad;
361     }
362 
363     var input = new PerfTimingObj(result, t);
364     Firebug.Console.log(input, context, "perfTiming", rep, true);
365 
366     // Details
367     var row = Firebug.Console.openCollapsedGroup("perfTimingDetails", context, "perfTimingDetails",
368         DetailsCaption, true, null, true);
369     Firebug.Console.closeGroup(context, true);
370 
371     var logGroupBody = row.lastChild;
372     var table = DetailsTable.tag.replace({object: t}, logGroupBody);
373     var tBody = table.lastChild;
374 
375     // Iterate only known properties (these are also localized).
376     var timings = [];
377     for (var i=0; i<timingProps.length; i++)
378     {
379         var name = timingProps[i];
380         var value = t[name];
381         var startTime = value ? (value - t.navigationStart) : 0;
382         var timing = {
383             name: name,
384             timeLabel: startTime ? "+" + Str.formatTime(startTime) : 0,
385             desc: Locale.$STR("perftiming." + name),
386             time: startTime,
387         }
388         timings.push(timing);
389     }
390 
391     timings.sort(function(a, b) {
392         return a.time > b.time ? 1 : -1;
393     })
394 
395     DetailsEntry.tag.insertRows({timings: timings}, tBody);
396 
397     return Firebug.Console.getDefaultReturnValue(context.window);
398 }
399 
400 // ********************************************************************************************* //
401 // Detailed Log
402 
403 /**
404  * A capation for detailed performance timing info.
405  */
406 var DetailsCaption = domplate(
407 /** @lends DetailsCaption */
408 {
409     tag:
410         SPAN({"class": "timingTitle"},
411             SPAN({"class": "timingCaption"},
412                 Locale.$STR("perftiming.details_title")
413             ),
414             SPAN({"class": "timingCaptionDesc"},
415                 Locale.$STR("perftiming.details_title_desc")
416             )
417         )
418 });
419 
420 // ********************************************************************************************* //
421 
422 /**
423  * This template represents a table with detailed timing info.
424  */
425 var DetailsTable = domplate(
426 /** @lends DetailsTable */
427 {
428     tag:
429         TABLE({"class": "timingTable", cellspacing: 0, cellpadding: 0, width: "100%",
430             "role": "grid", _repObject: "$object"},
431             THEAD({"class": "timingThead", "role": "presentation"},
432                 TR({"class": "headerRow focusRow timingRow subFocusRow", "role": "row"},
433                     TH({"class": "headerCell a11yFocus", "role": "columnheader", width: "10%"},
434                         DIV({"class": "headerCellBox"},
435                             Locale.$STR("Name")
436                         )
437                     ),
438                     TH({"class": "headerCell a11yFocus", "role": "columnheader", width: "10%"},
439                         DIV({"class": "headerCellBox"},
440                             Locale.$STR("Time")
441                         )
442                     ),
443                     TH({"class": "headerCell a11yFocus", "role": "columnheader", width: "70%"},
444                         DIV({"class": "headerCellBox"},
445                             Locale.$STR("Description")
446                         )
447                     )
448                 )
449             ),
450             TBODY({"class": "perfTimingTbody", "role": "presentation"}
451             )
452         ),
453 });
454 
455 // ********************************************************************************************* //
456 
457 /**
458  * A row within detailed performance timing info.
459  */
460 var DetailsEntry = domplate(
461 /** @lends DetailsEntry */
462 {
463     tag:
464         FOR("timing", "$timings",
465             TR({"class": "focusRow timingRow subFocusRow", "role": "row", _repObject: "$timing",
466                 onmousemove: "$onMouseMove", onmouseout: "$onMouseOut"},
467                 TD({"class": "a11yFocus timingCell timingName", "role": "gridcell"},
468                     "$timing.name"
469                 ),
470                 TD({"class": "a11yFocus timingCell timingTime", "role": "gridcell"},
471                     "$timing.timeLabel"
472                 ),
473                 TD({"class": "a11yFocus timingCell timingDesc", "role": "gridcell"},
474                     "$timing.desc"
475                 )
476             )
477         ),
478 
479     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
480 
481     onMouseMove: function(event)
482     {
483         var row = Dom.getAncestorByClass(event.target, "timingRow");
484         if (!row)
485             return;
486 
487         var log = Dom.getAncestorByClass(row, "logRow-perfTimingDetails");
488         var graph = log.previousSibling;
489         if (!Css.hasClass(graph, "logRow-perfTiming"))
490             return;
491 
492         var table = Dom.getAncestorByClass(row, "timingTable");
493         var timing = table.repObject;
494 
495         var elapsed = timing.loadEventEnd - timing.navigationStart;
496         var startTime = row.repObject.time;
497 
498         var tBody = graph.getElementsByClassName("perfTimingTbody")[0];
499         var rows = tBody.getElementsByTagName("tr");
500         for (var i=0; i<rows.length; i++)
501         {
502             var row = rows[i];
503             var cursor = row.getElementsByClassName("cursor")[0];
504 
505             Dom.hide(cursor, false);
506             cursor.style.left = calculatePos(startTime, elapsed) + "%";
507         }
508     },
509 
510     onMouseOut: function(event)
511     {
512         var row = Dom.getAncestorByClass(event.target, "timingRow");
513         if (!row)
514             return;
515 
516         var log = Dom.getAncestorByClass(row, "logRow-perfTimingDetails");
517         var graph = log.previousSibling;
518         if (!Css.hasClass(graph, "logRow-perfTiming"))
519             return;
520 
521         var tBody = graph.getElementsByClassName("perfTimingTbody")[0];
522         var rows = tBody.getElementsByTagName("tr");
523         for (var i=0; i<rows.length; i++)
524         {
525             var row = rows[i];
526             var cursor = row.getElementsByClassName("cursor")[0];
527             Dom.hide(cursor, true);
528         }
529     }
530 });
531 
532 // ********************************************************************************************* //
533 // Helpers
534 
535 function calculatePos(time, elapsed)
536 {
537     return Math.round((time / elapsed) * 100);
538 }
539 
540 function calculateBars(timing)
541 {
542     var result = [];
543     var t = timing;
544 
545     // Page Load bar
546     result.push({
547         className: "pageLoad",
548         start: 0,
549         elapsed: t.loadEventEnd - t.navigationStart,
550         label: Locale.$STR("Page Load"),
551     });
552 
553     // Redirect
554     result.push({
555         className: "redirect",
556         start: t.redirectStart ? t.redirectStart - t.navigationStart : 0,
557         elapsed: t.redirectStart ? t.redirectEnd - t.redirectStart : 0,
558         label: Locale.$STR("Redirect"),
559     });
560 
561     // DNS
562     var dns = t.domainLookupEnd - t.domainLookupStart;
563     result.push({
564         className: "dns",
565         start: t.domainLookupStart - t.navigationStart,
566         elapsed: t.domainLookupEnd - t.domainLookupStart,
567         label: Locale.$STR("DNS"),
568     });
569 
570     // Connect bar
571     result.push({
572         className: "connecting",
573         start: t.connectStart - t.navigationStart,
574         elapsed: t.connectEnd - t.connectStart,
575         label: Locale.$STR("Connecting"),
576     });
577 
578     // Waiting bar
579     result.push({
580         className: "waiting",
581         start: t.requestStart - t.navigationStart,
582         elapsed: t.responseStart - t.requestStart,
583         label: Locale.$STR("Waiting"),
584     });
585 
586     // Response bar
587     result.push({
588         className: "response",
589         start: t.responseStart - t.navigationStart,
590         elapsed: t.responseEnd - t.responseStart,
591         label: Locale.$STR("Receiving"),
592     });
593 
594     // Processing bar
595     result.push({
596         className: "processing",
597         start: t.responseEnd - t.navigationStart,
598         elapsed: t.loadEventStart - t.responseEnd,
599         label: Locale.$STR("DOM Processing"),
600     });
601 
602     // DOMContentLoaded
603     result.push({
604         className: "DOMContentLoaded",
605         start: t.domContentLoadedEventStart - t.navigationStart,
606         elapsed: t.domContentLoadedEventEnd - t.domContentLoadedEventStart,
607         label: Locale.$STR("DOMContentLoaded"),
608     });
609 
610     // onLoad
611     result.push({
612         className: "onLoad",
613         start: t.loadEventStart - t.navigationStart,
614         elapsed: t.loadEventEnd - t.loadEventStart,
615         label: Locale.$STR("onLoad"),
616     });
617 
618     return result;
619 }
620 
621 // ********************************************************************************************* //
622 // Registration
623 
624 Firebug.registerRep(PerformanceTimingRep);
625 Firebug.registerModule(PerformanceTimingModule);
626 
627 return PerformanceTimingModule;
628 
629 // ********************************************************************************************* //
630 }});
631