2008-03-26 Steve Falkenburg <sfalken@apple.com>
[WebKit-https.git] / WebCore / page / inspector / ResourcesPanel.js
1 /*
2  * Copyright (C) 2007 Apple Inc.  All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  *
8  * 1.  Redistributions of source code must retain the above copyright
9  *     notice, this list of conditions and the following disclaimer. 
10  * 2.  Redistributions in binary form must reproduce the above copyright
11  *     notice, this list of conditions and the following disclaimer in the
12  *     documentation and/or other materials provided with the distribution. 
13  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14  *     its contributors may be used to endorse or promote products derived
15  *     from this software without specific prior written permission. 
16  *
17  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  */
28
29 WebInspector.NetworkPanel = function()
30 {
31     WebInspector.Panel.call(this);
32
33     this.timelineEntries = [];
34
35     this.timelineElement = document.createElement("div");
36     this.timelineElement.className = "network-timeline";
37     this.element.appendChild(this.timelineElement);
38
39     this.summaryElement = document.createElement("div");
40     this.summaryElement.className = "network-summary";
41     this.element.appendChild(this.summaryElement);
42
43     this.dividersElement = document.createElement("div");
44     this.dividersElement.className = "network-dividers";
45     this.timelineElement.appendChild(this.dividersElement);
46
47     this.resourcesElement = document.createElement("div");
48     this.resourcesElement.className = "network-resources";
49     this.resourcesElement.addEventListener("click", this.resourcesClicked.bind(this), false);
50     this.timelineElement.appendChild(this.resourcesElement);
51
52     var graphArea = document.createElement("div");
53     graphArea.className = "network-graph-area";
54     this.summaryElement.appendChild(graphArea);
55
56     this.graphLabelElement = document.createElement("div");
57     this.graphLabelElement.className = "network-graph-label";
58     graphArea.appendChild(this.graphLabelElement);
59
60     this.graphModeSelectElement = document.createElement("select");
61     this.graphModeSelectElement.className = "network-graph-mode";
62     this.graphModeSelectElement.addEventListener("change", this.changeGraphMode.bind(this), false);
63     this.graphLabelElement.appendChild(this.graphModeSelectElement);
64     this.graphLabelElement.appendChild(document.createElement("br"));
65
66     var sizeOptionElement = document.createElement("option");
67     sizeOptionElement.calculator = new WebInspector.TransferSizeCalculator();
68     sizeOptionElement.textContent = sizeOptionElement.calculator.title;
69     this.graphModeSelectElement.appendChild(sizeOptionElement);
70
71     var timeOptionElement = document.createElement("option");
72     timeOptionElement.calculator = new WebInspector.TransferTimeCalculator();
73     timeOptionElement.textContent = timeOptionElement.calculator.title;
74     this.graphModeSelectElement.appendChild(timeOptionElement);
75
76     var graphSideElement = document.createElement("div");
77     graphSideElement.className = "network-graph-side";
78     graphArea.appendChild(graphSideElement);
79
80     this.summaryGraphElement = document.createElement("canvas");
81     this.summaryGraphElement.setAttribute("width", "450");
82     this.summaryGraphElement.setAttribute("height", "38");
83     this.summaryGraphElement.className = "network-summary-graph";
84     graphSideElement.appendChild(this.summaryGraphElement);
85
86     this.legendElement = document.createElement("div");
87     this.legendElement.className = "network-graph-legend";
88     graphSideElement.appendChild(this.legendElement);
89
90     this.drawSummaryGraph(); // draws an empty graph
91
92     this.needsRefresh = true; 
93 }
94
95 WebInspector.NetworkPanel.prototype = {
96     show: function()
97     {
98         WebInspector.Panel.prototype.show.call(this);
99         WebInspector.networkListItem.select();
100         this.refreshIfNeeded();
101     },
102
103     hide: function()
104     {
105         WebInspector.Panel.prototype.hide.call(this);
106         WebInspector.networkListItem.deselect();
107     },
108
109     resize: function()
110     {
111         this.updateTimelineDividersIfNeeded();
112     },
113
114     resourcesClicked: function(event)
115     {
116         // If the click wasn't inside a network resource row, ignore it.
117         var resourceElement = event.target.enclosingNodeOrSelfWithClass("network-resource");
118         if (!resourceElement)
119             return;
120
121         // If the click was within the network info element, ignore it.
122         var networkInfo = event.target.enclosingNodeOrSelfWithClass("network-info");
123         if (networkInfo)
124             return;
125
126         // If the click was within the tip balloon element, hide it.
127         var balloon = event.target.enclosingNodeOrSelfWithClass("tip-balloon");
128         if (balloon) {
129             resourceElement.timelineEntry.showingTipBalloon = false;
130             return;
131         }
132
133         resourceElement.timelineEntry.toggleShowingInfo();
134     },
135
136     changeGraphMode: function(event)
137     {
138         this.updateSummaryGraph();
139     },
140
141     get calculator()
142     {
143         return this.graphModeSelectElement.options[this.graphModeSelectElement.selectedIndex].calculator;
144     },
145
146     get totalDuration()
147     {
148         return this.latestEndTime - this.earliestStartTime;
149     },
150
151     get needsRefresh() 
152     { 
153         return this._needsRefresh; 
154     }, 
155
156     set needsRefresh(x) 
157     { 
158         if (this._needsRefresh === x) 
159             return; 
160         this._needsRefresh = x; 
161         if (x && this.visible) 
162             this.refresh(); 
163     },
164
165     refreshIfNeeded: function() 
166     { 
167         if (this.needsRefresh) 
168             this.refresh(); 
169     },
170
171     refresh: function()
172     {
173         this.needsRefresh = false;
174
175         // calling refresh will call updateTimelineBoundriesIfNeeded, which can clear needsRefresh for future entries,
176         // so find all the entries that needs refresh first, then loop back trough them to call refresh
177         var entriesNeedingRefresh = [];
178         var entriesLength = this.timelineEntries.length;
179         for (var i = 0; i < entriesLength; ++i) {
180             var entry = this.timelineEntries[i];
181             if (entry.needsRefresh || entry.infoNeedsRefresh)
182                 entriesNeedingRefresh.push(entry);
183         }
184
185         entriesLength = entriesNeedingRefresh.length;
186         for (var i = 0; i < entriesLength; ++i)
187             entriesNeedingRefresh[i].refresh(false, true, true);
188
189         this.updateTimelineDividersIfNeeded();
190         this.sortTimelineEntriesIfNeeded();
191         this.updateSummaryGraph();
192     },
193
194     makeLegendElement: function(label, value, color)
195     {
196         var legendElement = document.createElement("label");
197         legendElement.className = "network-graph-legend-item";
198
199         if (color) {
200             var swatch = document.createElement("canvas");
201             swatch.className = "network-graph-legend-swatch";
202             swatch.setAttribute("width", "13");
203             swatch.setAttribute("height", "24");
204
205             legendElement.appendChild(swatch);
206
207             this.drawSwatch(swatch, color);
208         }
209
210         var labelElement = document.createElement("div");
211         labelElement.className = "network-graph-legend-label";
212         legendElement.appendChild(labelElement);
213
214         var headerElement = document.createElement("div");
215         var headerElement = document.createElement("div");
216         headerElement.className = "network-graph-legend-header";
217         headerElement.textContent = label;
218         labelElement.appendChild(headerElement);
219
220         var valueElement = document.createElement("div");
221         valueElement.className = "network-graph-legend-value";
222         valueElement.textContent = value;
223         labelElement.appendChild(valueElement);
224
225         return legendElement;
226     },
227
228     sortTimelineEntriesSoonIfNeeded: function()
229     {
230         if ("sortTimelineEntriesTimeout" in this)
231             return;
232         this.sortTimelineEntriesTimeout = setTimeout(this.sortTimelineEntriesIfNeeded.bind(this), 500);
233     },
234
235     sortTimelineEntriesIfNeeded: function()
236     {
237         if ("sortTimelineEntriesTimeout" in this) {
238             clearTimeout(this.sortTimelineEntriesTimeout);
239             delete this.sortTimelineEntriesTimeout;
240         }
241
242         this.timelineEntries.sort(WebInspector.NetworkPanel.timelineEntryCompare);
243
244         var nextSibling = null;
245         for (var i = (this.timelineEntries.length - 1); i >= 0; --i) {
246             var entry = this.timelineEntries[i];
247             if (entry.resourceElement.nextSibling !== nextSibling)
248                 this.resourcesElement.insertBefore(entry.resourceElement, nextSibling);
249             nextSibling = entry.resourceElement;
250         }
251     },
252
253     updateTimelineBoundriesIfNeeded: function(resource, immediate)
254     {
255         var didUpdate = false;
256         if (resource.startTime !== -1 && (this.earliestStartTime === undefined || resource.startTime < this.earliestStartTime)) {
257             this.earliestStartTime = resource.startTime;
258             didUpdate = true;
259         }
260
261         if (resource.endTime !== -1 && (this.latestEndTime === undefined || resource.endTime > this.latestEndTime)) {
262             this.latestEndTime = resource.endTime;
263             didUpdate = true;
264         }
265
266         if (didUpdate) {
267             if (immediate) {
268                 this.refreshAllTimelineEntries(true, true, immediate);
269                 this.updateTimelineDividersIfNeeded();
270             } else {
271                 this.refreshAllTimelineEntriesSoon(true, true, immediate);
272                 this.updateTimelineDividersSoonIfNeeded();
273             }
274         }
275
276         return didUpdate;
277     },
278
279     updateTimelineDividersSoonIfNeeded: function()
280     {
281         if ("updateTimelineDividersTimeout" in this)
282             return;
283         this.updateTimelineDividersTimeout = setTimeout(this.updateTimelineDividersIfNeeded.bind(this), 500);
284     },
285
286     updateTimelineDividersIfNeeded: function()
287     {
288         if ("updateTimelineDividersTimeout" in this) {
289             clearTimeout(this.updateTimelineDividersTimeout);
290             delete this.updateTimelineDividersTimeout;
291         }
292
293         if (!this.visible) {
294             this.needsRefresh = true;
295             return;
296         }
297
298         if (document.body.offsetWidth <= 0) {
299             // The stylesheet hasn't loaded yet, so we need to update later.
300             setTimeout(this.updateTimelineDividersIfNeeded.bind(this), 0);
301             return;
302         }
303
304         var dividerCount = Math.round(this.dividersElement.offsetWidth / 64);
305         var timeSlice = this.totalDuration / dividerCount;
306
307         if (this.lastDividerTimeSlice === timeSlice)
308             return;
309
310         this.lastDividerTimeSlice = timeSlice;
311
312         this.dividersElement.removeChildren();
313
314         for (var i = 1; i <= dividerCount; ++i) {
315             var divider = document.createElement("div");
316             divider.className = "network-divider";
317             if (i === dividerCount)
318                 divider.addStyleClass("last");
319             divider.style.left = ((i / dividerCount) * 100) + "%";
320
321             var label = document.createElement("div");
322             label.className = "network-divider-label";
323             label.textContent = Number.secondsToString(timeSlice * i);
324             divider.appendChild(label);
325
326             this.dividersElement.appendChild(divider);
327         }
328     },
329
330     refreshAllTimelineEntriesSoon: function(skipBoundryUpdate, skipTimelineSort, immediate)
331     {
332         if ("refreshAllTimelineEntriesTimeout" in this)
333             return;
334         this.refreshAllTimelineEntriesTimeout = setTimeout(this.refreshAllTimelineEntries.bind(this), 500, skipBoundryUpdate, skipTimelineSort, immediate);
335     },
336
337     refreshAllTimelineEntries: function(skipBoundryUpdate, skipTimelineSort, immediate)
338     {
339         if ("refreshAllTimelineEntriesTimeout" in this) {
340             clearTimeout(this.refreshAllTimelineEntriesTimeout);
341             delete this.refreshAllTimelineEntriesTimeout;
342         }
343
344         var entriesLength = this.timelineEntries.length;
345         for (var i = 0; i < entriesLength; ++i)
346             this.timelineEntries[i].refresh(skipBoundryUpdate, skipTimelineSort, immediate);
347     },
348
349     fadeOutRect: function(ctx, x, y, w, h, a1, a2)
350     {
351         ctx.save();
352
353         var gradient = ctx.createLinearGradient(x, y, x, y + h);
354         gradient.addColorStop(0.0, "rgba(0, 0, 0, " + (1.0 - a1) + ")");
355         gradient.addColorStop(0.8, "rgba(0, 0, 0, " + (1.0 - a2) + ")");
356         gradient.addColorStop(1.0, "rgba(0, 0, 0, 1.0)");
357
358         ctx.globalCompositeOperation = "destination-out";
359
360         ctx.fillStyle = gradient;
361         ctx.fillRect(x, y, w, h);
362
363         ctx.restore();
364     },
365
366     drawSwatch: function(canvas, color)
367     {
368         var ctx = canvas.getContext("2d");
369
370         function drawSwatchSquare() {
371             ctx.fillStyle = color;
372             ctx.fillRect(0, 0, 13, 13);
373
374             var gradient = ctx.createLinearGradient(0, 0, 13, 13);
375             gradient.addColorStop(0.0, "rgba(255, 255, 255, 0.2)");
376             gradient.addColorStop(1.0, "rgba(255, 255, 255, 0.0)");
377
378             ctx.fillStyle = gradient;
379             ctx.fillRect(0, 0, 13, 13);
380
381             gradient = ctx.createLinearGradient(13, 13, 0, 0);
382             gradient.addColorStop(0.0, "rgba(0, 0, 0, 0.2)");
383             gradient.addColorStop(1.0, "rgba(0, 0, 0, 0.0)");
384
385             ctx.fillStyle = gradient;
386             ctx.fillRect(0, 0, 13, 13);
387
388             ctx.strokeStyle = "rgba(0, 0, 0, 0.6)";
389             ctx.strokeRect(0.5, 0.5, 12, 12);
390         }
391
392         ctx.clearRect(0, 0, 13, 24);
393
394         drawSwatchSquare();
395
396         ctx.save();
397
398         ctx.translate(0, 25);
399         ctx.scale(1, -1);
400
401         drawSwatchSquare();
402
403         ctx.restore();
404
405         this.fadeOutRect(ctx, 0, 13, 13, 13, 0.5, 0.0);
406     },
407
408     drawSummaryGraph: function(segments)
409     {
410         if (!this.summaryGraphElement)
411             return;
412
413         if (!segments || !segments.length)
414             segments = [{color: "white", value: 1}];
415
416         // Calculate the total of all segments.
417         var total = 0;
418         for (var i = 0; i < segments.length; ++i)
419             total += segments[i].value;
420
421         // Calculate the percentage of each segment, rounded to the nearest percent.
422         var percents = segments.map(function(s) { return Math.max(Math.round(100 * s.value / total), 1) });
423
424         // Calculate the total percentage.
425         var percentTotal = 0;
426         for (var i = 0; i < percents.length; ++i)
427             percentTotal += percents[i];
428
429         // Make sure our percentage total is not greater-than 100, it can be greater
430         // if we rounded up for a few segments.
431         while (percentTotal > 100) {
432             for (var i = 0; i < percents.length && percentTotal > 100; ++i) {
433                 if (percents[i] > 1) {
434                     --percents[i];
435                     --percentTotal;
436                 }
437             }
438         }
439
440         // Make sure our percentage total is not less-than 100, it can be less
441         // if we rounded down for a few segments.
442         while (percentTotal < 100) {
443             for (var i = 0; i < percents.length && percentTotal < 100; ++i) {
444                 ++percents[i];
445                 ++percentTotal;
446             }
447         }
448
449         var ctx = this.summaryGraphElement.getContext("2d");
450
451         var x = 0;
452         var y = 0;
453         var w = 450;
454         var h = 19;
455         var r = (h / 2);
456
457         function drawPillShadow()
458         {
459             // This draws a line with a shadow that is offset away from the line. The line is stroked
460             // twice with different X shadow offsets to give more feathered edges. Later we erase the
461             // line with destination-out 100% transparent black, leaving only the shadow. This only
462             // works if nothing has been drawn into the canvas yet.
463
464             ctx.beginPath();
465             ctx.moveTo(x + 4, y + h - 3 - 0.5);
466             ctx.lineTo(x + w - 4, y + h - 3 - 0.5);
467             ctx.closePath();
468
469             ctx.save();
470
471             ctx.shadowBlur = 2;
472             ctx.shadowColor = "rgba(0, 0, 0, 0.5)";
473             ctx.shadowOffsetX = 3;
474             ctx.shadowOffsetY = 5;
475
476             ctx.strokeStyle = "white";
477             ctx.lineWidth = 1;
478
479             ctx.stroke();
480
481             ctx.shadowOffsetX = -3;
482
483             ctx.stroke();
484
485             ctx.restore();
486
487             ctx.save();
488
489             ctx.globalCompositeOperation = "destination-out";
490             ctx.strokeStyle = "rgba(0, 0, 0, 1)";
491             ctx.lineWidth = 1;
492
493             ctx.stroke();
494
495             ctx.restore();
496         }
497
498         function drawPill()
499         {
500             // Make a rounded rect path.
501             ctx.beginPath();
502             ctx.moveTo(x, y + r);
503             ctx.lineTo(x, y + h - r);
504             ctx.quadraticCurveTo(x, y + h, x + r, y + h);
505             ctx.lineTo(x + w - r, y + h);
506             ctx.quadraticCurveTo(x + w, y + h, x + w, y + h - r);
507             ctx.lineTo(x + w, y + r);
508             ctx.quadraticCurveTo(x + w, y, x + w - r, y);
509             ctx.lineTo(x + r, y);
510             ctx.quadraticCurveTo(x, y, x, y + r);
511             ctx.closePath();
512
513             // Clip to the rounded rect path.
514             ctx.save();
515             ctx.clip();
516
517             // Fill the segments with the associated color.
518             var previousSegmentsWidth = 0;
519             for (var i = 0; i < segments.length; ++i) {
520                 var segmentWidth = Math.round(w * percents[i] / 100);
521                 ctx.fillStyle = segments[i].color;
522                 ctx.fillRect(x + previousSegmentsWidth, y, segmentWidth, h);
523                 previousSegmentsWidth += segmentWidth;
524             }
525
526             // Draw the segment divider lines.
527             ctx.lineWidth = 1;
528             for (var i = 1; i < 20; ++i) {
529                 ctx.beginPath();
530                 ctx.moveTo(x + (i * Math.round(w / 20)) + 0.5, y);
531                 ctx.lineTo(x + (i * Math.round(w / 20)) + 0.5, y + h);
532                 ctx.closePath();
533
534                 ctx.strokeStyle = "rgba(0, 0, 0, 0.2)";
535                 ctx.stroke();
536
537                 ctx.beginPath();
538                 ctx.moveTo(x + (i * Math.round(w / 20)) + 1.5, y);
539                 ctx.lineTo(x + (i * Math.round(w / 20)) + 1.5, y + h);
540                 ctx.closePath();
541
542                 ctx.strokeStyle = "rgba(255, 255, 255, 0.2)";
543                 ctx.stroke();
544             }
545
546             // Draw the pill shading.
547             var lightGradient = ctx.createLinearGradient(x, y, x, y + (h / 1.5));
548             lightGradient.addColorStop(0.0, "rgba(220, 220, 220, 0.6)");
549             lightGradient.addColorStop(0.4, "rgba(220, 220, 220, 0.2)");
550             lightGradient.addColorStop(1.0, "rgba(255, 255, 255, 0.0)");
551
552             var darkGradient = ctx.createLinearGradient(x, y + (h / 3), x, y + h);
553             darkGradient.addColorStop(0.0, "rgba(0, 0, 0, 0.0)");
554             darkGradient.addColorStop(0.8, "rgba(0, 0, 0, 0.2)");
555             darkGradient.addColorStop(1.0, "rgba(0, 0, 0, 0.5)");
556
557             ctx.fillStyle = darkGradient;
558             ctx.fillRect(x, y, w, h);
559
560             ctx.fillStyle = lightGradient;
561             ctx.fillRect(x, y, w, h);
562
563             ctx.restore();
564         }
565
566         ctx.clearRect(x, y, w, (h * 2));
567
568         drawPillShadow();
569         drawPill();
570
571         ctx.save();
572
573         ctx.translate(0, (h * 2) + 1);
574         ctx.scale(1, -1);
575
576         drawPill();
577
578         ctx.restore();
579
580         this.fadeOutRect(ctx, x, y + h + 1, w, h, 0.5, 0.0);
581     },
582
583     updateSummaryGraphSoon: function()
584     {
585         if ("updateSummaryGraphTimeout" in this)
586             return;
587         this.updateSummaryGraphTimeout = setTimeout(this.updateSummaryGraph.bind(this), 500);
588     },
589
590     updateSummaryGraph: function()
591     {
592         if ("updateSummaryGraphTimeout" in this) {
593             clearTimeout(this.updateSummaryGraphTimeout);
594             delete this.updateSummaryGraphTimeout;
595         }
596
597         var graphInfo = this.calculator.computeValues(this.timelineEntries);
598
599         var categoryOrder = ["documents", "stylesheets", "images", "scripts", "fonts", "other"];
600         var categoryColors = {documents: {r: 47, g: 102, b: 236}, stylesheets: {r: 157, g: 231, b: 119}, images: {r: 164, g: 60, b: 255}, scripts: {r: 255, g: 121, b: 0}, fonts: {r: 231, g: 231, b: 10}, other: {r: 186, g: 186, b: 186}};
601         var fillSegments = [];
602
603         this.legendElement.removeChildren();
604
605         if (this.totalLegendLabel)
606             this.totalLegendLabel.parentNode.removeChild(this.totalLegendLabel);
607
608         this.totalLegendLabel = this.makeLegendElement(this.calculator.totalTitle, this.calculator.formatValue(graphInfo.total));
609         this.totalLegendLabel.addStyleClass("network-graph-legend-total");
610         this.graphLabelElement.appendChild(this.totalLegendLabel);
611
612         for (var i = 0; i < categoryOrder.length; ++i) {
613             var category = categoryOrder[i];
614             var size = graphInfo.categoryValues[category];
615             if (!size)
616                 continue;
617
618             var color = categoryColors[category];
619             var colorString = "rgb(" + color.r + ", " + color.g + ", " + color.b + ")";
620
621             var fillSegment = {color: colorString, value: size};
622             fillSegments.push(fillSegment);
623
624             var legendLabel = this.makeLegendElement(WebInspector.resourceCategories[category].title, this.calculator.formatValue(size), colorString);
625             this.legendElement.appendChild(legendLabel);
626         }
627
628         this.drawSummaryGraph(fillSegments);
629     },
630
631     clearTimeline: function()
632     {
633         delete this.earliestStartTime;
634         delete this.latestEndTime;
635
636         var entriesLength = this.timelineEntries.length;
637         for (var i = 0; i < entriesLength; ++i)
638             delete this.timelineEntries[i].resource.networkTimelineEntry;
639
640         this.timelineEntries = [];
641         this.resourcesElement.removeChildren();
642
643         this.drawSummaryGraph(); // draws an empty graph
644     },
645
646     addResourceToTimeline: function(resource)
647     {
648         var timelineEntry = new WebInspector.NetworkTimelineEntry(this, resource);
649         this.timelineEntries.push(timelineEntry);
650         this.resourcesElement.appendChild(timelineEntry.resourceElement);
651
652         timelineEntry.refresh();
653         this.updateSummaryGraphSoon();
654     }
655 }
656
657 WebInspector.NetworkPanel.prototype.__proto__ = WebInspector.Panel.prototype;
658
659 WebInspector.NetworkPanel.timelineEntryCompare = function(a, b)
660 {
661     if (a.resource.startTime < b.resource.startTime)
662         return -1;
663     if (a.resource.startTime > b.resource.startTime)
664         return 1;
665     if (a.resource.endTime < b.resource.endTime)
666         return -1;
667     if (a.resource.endTime > b.resource.endTime)
668         return 1;
669     return 0;
670 }
671
672 WebInspector.NetworkTimelineEntry = function(panel, resource)
673 {
674     this.panel = panel;
675     this.resource = resource;
676     resource.networkTimelineEntry = this;
677
678     this.resourceElement = document.createElement("div");
679     this.resourceElement.className = "network-resource";
680     this.resourceElement.timelineEntry = this;
681
682     this.titleElement = document.createElement("div");
683     this.titleElement.className = "network-title";
684     this.resourceElement.appendChild(this.titleElement);
685
686     this.fileElement = document.createElement("div");
687     this.fileElement.className = "network-file";
688     this.fileElement.innerHTML = WebInspector.linkifyURL(resource.url, resource.displayName);
689     this.titleElement.appendChild(this.fileElement);
690
691     this.tipButtonElement = document.createElement("button");
692     this.tipButtonElement.className = "tip-button";
693     this.showingTipButton = this.resource.tips.length;
694     this.fileElement.insertBefore(this.tipButtonElement, this.fileElement.firstChild);
695
696     this.tipButtonElement.addEventListener("click", this.toggleTipBalloon.bind(this), false );
697
698     this.areaElement = document.createElement("div");
699     this.areaElement.className = "network-area";
700     this.titleElement.appendChild(this.areaElement);
701
702     this.barElement = document.createElement("div");
703     this.areaElement.appendChild(this.barElement);
704
705     this.infoElement = document.createElement("div");
706     this.infoElement.className = "network-info hidden";
707     this.resourceElement.appendChild(this.infoElement);
708 }
709
710 WebInspector.NetworkTimelineEntry.prototype = {
711     refresh: function(skipBoundryUpdate, skipTimelineSort, immediate)
712     {
713         if (!this.panel.visible) {
714             this.needsRefresh = true;
715             this.panel.needsRefresh = true;
716             return;
717         }
718
719         delete this.needsRefresh;
720
721         if (!skipBoundryUpdate) {
722             if (this.panel.updateTimelineBoundriesIfNeeded(this.resource, immediate))
723                 return; // updateTimelineBoundriesIfNeeded calls refresh() on all entries, so we can just return
724         }
725
726         if (!skipTimelineSort) {
727             if (immediate)
728                 this.panel.sortTimelineEntriesIfNeeded();
729             else
730                 this.panel.sortTimelineEntriesSoonIfNeeded();
731         }
732
733         if (this.resource.startTime !== -1) {
734             var percentStart = ((this.resource.startTime - this.panel.earliestStartTime) / this.panel.totalDuration) * 100;
735             this.barElement.style.left = percentStart + "%";
736         } else {
737             this.barElement.style.left = null;
738         }
739
740         if (this.resource.endTime !== -1) {
741             var percentEnd = ((this.panel.latestEndTime - this.resource.endTime) / this.panel.totalDuration) * 100;
742             this.barElement.style.right = percentEnd + "%";
743         } else {
744             this.barElement.style.right = "0px";
745         }
746
747         this.barElement.className = "network-bar network-category-" + this.resource.category.name;
748
749         if (this.infoNeedsRefresh)
750             this.refreshInfo();
751     },
752
753     refreshInfo: function()
754     {
755         if (!this.showingInfo) {
756             this.infoNeedsRefresh = true;
757             return;
758         }
759
760         if (!this.panel.visible) {
761             this.panel.needsRefresh = true;
762             this.infoNeedsRefresh = true;
763             return;
764         }
765
766         this.infoNeedsRefresh = false;
767
768         this.infoElement.removeChildren();
769
770         var sections = [
771             {title: WebInspector.UIString("Request"), info: this.resource.sortedRequestHeaders},
772             {title: WebInspector.UIString("Response"), info: this.resource.sortedResponseHeaders}
773         ];
774
775         function createSectionTable(section)
776         {
777             if (!section.info.length)
778                 return;
779
780             var table = document.createElement("table");
781             this.infoElement.appendChild(table);
782
783             var heading = document.createElement("th");
784             heading.textContent = section.title;
785
786             var row = table.createTHead().insertRow(-1).appendChild(heading);
787             var body = document.createElement("tbody");
788             table.appendChild(body);
789
790             section.info.forEach(function(header) {
791                 var row = body.insertRow(-1);
792                 var th = document.createElement("th");
793                 th.textContent = header.header;
794                 row.appendChild(th);
795                 row.insertCell(-1).textContent = header.value;
796             });
797         }
798
799         sections.forEach(createSectionTable, this);
800     },
801
802     refreshInfoIfNeeded: function()
803     {
804         if (this.infoNeedsRefresh === false)
805             return;
806
807         this.refreshInfo();
808     },
809
810     toggleShowingInfo: function()
811     {
812         this.showingInfo = !this.showingInfo;
813     },
814
815     get showingInfo()
816     {
817         return this._showingInfo;
818     },
819
820     set showingInfo(x)
821     {
822         if (this._showingInfo === x)
823             return;
824
825         this._showingInfo = x;
826
827         var element = this.infoElement;
828         if (x) {
829             element.removeStyleClass("hidden");
830             element.style.setProperty("overflow", "hidden");
831             this.refreshInfoIfNeeded();
832             WebInspector.animateStyle([{element: element, start: {height: 0}, end: {height: element.offsetHeight}}], 250, function() { element.style.removeProperty("height"); element.style.removeProperty("overflow") });
833         } else {
834             element.style.setProperty("overflow", "hidden");
835             WebInspector.animateStyle([{element: element, end: {height: 0}}], 250, function() { element.addStyleClass("hidden"); element.style.removeProperty("height") });
836         }
837     },
838
839     get showingTipButton()
840     {
841         return !this.tipButtonElement.hasStyleClass("hidden");
842     },
843
844     set showingTipButton(x)
845     {
846         if (x)
847             this.tipButtonElement.removeStyleClass("hidden");
848         else
849             this.tipButtonElement.addStyleClass("hidden");
850     },
851
852     toggleTipBalloon: function(event)
853     {
854         this.showingTipBalloon = !this.showingTipBalloon;
855         event.stopPropagation();
856     },
857
858     get showingTipBalloon()
859     {
860         return this._showingTipBalloon;
861     },
862
863     set showingTipBalloon(x)
864     {
865         if (this._showingTipBalloon === x)
866             return;
867
868         this._showingTipBalloon = x;
869
870         if (x) {
871             if (!this.tipBalloonElement) {
872                 this.tipBalloonElement = document.createElement("div");
873                 this.tipBalloonElement.className = "tip-balloon";
874                 this.titleElement.appendChild(this.tipBalloonElement);
875
876                 this.tipBalloonContentElement = document.createElement("div");
877                 this.tipBalloonContentElement.className = "tip-balloon-content";
878                 this.tipBalloonElement.appendChild(this.tipBalloonContentElement);
879                 var tipText = "";
880                 for (var id in this.resource.tips)
881                     tipText += this.resource.tips[id].message + "\n";
882                 this.tipBalloonContentElement.textContent = tipText;
883             }
884
885             this.tipBalloonElement.removeStyleClass("hidden");
886             WebInspector.animateStyle([{element: this.tipBalloonElement, start: {left: 160, opacity: 0}, end: {left: 145, opacity: 1}}], 250);
887         } else {
888             var element = this.tipBalloonElement;
889             WebInspector.animateStyle([{element: this.tipBalloonElement, start: {left: 145, opacity: 1}, end: {left: 160, opacity: 0}}], 250, function() { element.addStyleClass("hidden") });
890         }
891     }
892 }
893
894 WebInspector.TimelineValueCalculator = function()
895 {
896 }
897
898 WebInspector.TimelineValueCalculator.prototype = {
899     computeValues: function(entries)
900     {
901         var total = 0;
902         var categoryValues = {};
903
904         function compute(entry)
905         {
906             var value = this._value(entry);
907             if (value === undefined)
908                 return;
909
910             if (!(entry.resource.category.name in categoryValues))
911                 categoryValues[entry.resource.category.name] = 0;
912             categoryValues[entry.resource.category.name] += value;
913             total += value;
914         }
915         entries.forEach(compute, this);
916
917         return {categoryValues: categoryValues, total: total};
918     },
919
920     _value: function(entry)
921     {
922         return 0;
923     },
924
925     get title()
926     {
927         return "";
928     },
929
930     formatValue: function(value)
931     {
932         return value.toString();
933     }
934 }
935
936 WebInspector.TransferTimeCalculator = function()
937 {
938     WebInspector.TimelineValueCalculator.call(this);
939 }
940
941 WebInspector.TransferTimeCalculator.prototype = {
942     computeValues: function(entries)
943     {
944         var entriesByCategory = {};
945         entries.forEach(function(entry) {
946             if (!(entry.resource.category.name in entriesByCategory))
947                 entriesByCategory[entry.resource.category.name] = [];
948             entriesByCategory[entry.resource.category.name].push(entry);
949         });
950
951         var earliestStart;
952         var latestEnd;
953         var categoryValues = {};
954         for (var category in entriesByCategory) {
955             entriesByCategory[category].sort(WebInspector.NetworkPanel.timelineEntryCompare);
956             categoryValues[category] = 0;
957
958             var segment = {start: -1, end: -1};
959             entriesByCategory[category].forEach(function(entry) {
960                 if (entry.resource.startTime == -1 || entry.resource.endTime == -1)
961                     return;
962
963                 if (earliestStart === undefined)
964                     earliestStart = entry.resource.startTime;
965                 else
966                     earliestStart = Math.min(earliestStart, entry.resource.startTime);
967
968                 if (latestEnd === undefined)
969                     latestEnd = entry.resource.endTime;
970                 else
971                     latestEnd = Math.max(latestEnd, entry.resource.endTime);
972
973                 if (entry.resource.startTime <= segment.end) {
974                     segment.end = Math.max(segment.end, entry.resource.endTime);
975                     return;
976                 }
977
978                 categoryValues[category] += segment.end - segment.start;
979
980                 segment.start = entry.resource.startTime;
981                 segment.end = entry.resource.endTime;
982             });
983
984             // Add the last segment
985             categoryValues[category] += segment.end - segment.start;
986         }
987
988         return {categoryValues: categoryValues, total: latestEnd - earliestStart};
989     },
990
991     get title()
992     {
993         return WebInspector.UIString("Transfer Time");
994     },
995
996     get totalTitle()
997     {
998         return WebInspector.UIString("Total Time");
999     },
1000
1001     formatValue: function(value)
1002     {
1003         return Number.secondsToString(value);
1004     }
1005 }
1006
1007 WebInspector.TransferTimeCalculator.prototype.__proto__ = WebInspector.TimelineValueCalculator.prototype;
1008
1009 WebInspector.TransferSizeCalculator = function()
1010 {
1011     WebInspector.TimelineValueCalculator.call(this);
1012 }
1013
1014 WebInspector.TransferSizeCalculator.prototype = {
1015     _value: function(entry)
1016     {
1017         return entry.resource.contentLength;
1018     },
1019
1020     get title()
1021     {
1022         return WebInspector.UIString("Transfer Size");
1023     },
1024
1025     get totalTitle()
1026     {
1027         return WebInspector.UIString("Total Size");
1028     },
1029
1030     formatValue: function(value)
1031     {
1032         return Number.bytesToString(value);
1033     }
1034 }
1035
1036 WebInspector.TransferSizeCalculator.prototype.__proto__ = WebInspector.TimelineValueCalculator.prototype;