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