| /* |
| * Copyright (C) 2007 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of |
| * its contributors may be used to endorse or promote products derived |
| * from this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY |
| * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
| * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
| * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
| * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| WebInspector.NetworkPanel = function() |
| { |
| WebInspector.Panel.call(this); |
| |
| this.timelineEntries = []; |
| |
| this.timelineElement = document.createElement("div"); |
| this.timelineElement.className = "network-timeline"; |
| this.element.appendChild(this.timelineElement); |
| |
| this.summaryElement = document.createElement("div"); |
| this.summaryElement.className = "network-summary"; |
| this.element.appendChild(this.summaryElement); |
| |
| this.dividersElement = document.createElement("div"); |
| this.dividersElement.className = "network-dividers"; |
| this.timelineElement.appendChild(this.dividersElement); |
| |
| this.resourcesElement = document.createElement("div"); |
| this.resourcesElement.className = "network-resources"; |
| this.resourcesElement.addEventListener("click", this.resourcesClicked.bind(this), false); |
| this.timelineElement.appendChild(this.resourcesElement); |
| |
| var graphArea = document.createElement("div"); |
| graphArea.className = "network-graph-area"; |
| this.summaryElement.appendChild(graphArea); |
| |
| this.graphLabelElement = document.createElement("div"); |
| this.graphLabelElement.className = "network-graph-label"; |
| graphArea.appendChild(this.graphLabelElement); |
| |
| this.graphModeSelectElement = document.createElement("select"); |
| this.graphModeSelectElement.className = "network-graph-mode"; |
| this.graphModeSelectElement.addEventListener("change", this.changeGraphMode.bind(this), false); |
| this.graphLabelElement.appendChild(this.graphModeSelectElement); |
| this.graphLabelElement.appendChild(document.createElement("br")); |
| |
| var sizeOptionElement = document.createElement("option"); |
| sizeOptionElement.calculator = new WebInspector.TransferSizeCalculator(); |
| sizeOptionElement.textContent = sizeOptionElement.calculator.title; |
| this.graphModeSelectElement.appendChild(sizeOptionElement); |
| |
| var timeOptionElement = document.createElement("option"); |
| timeOptionElement.calculator = new WebInspector.TransferTimeCalculator(); |
| timeOptionElement.textContent = timeOptionElement.calculator.title; |
| this.graphModeSelectElement.appendChild(timeOptionElement); |
| |
| var graphSideElement = document.createElement("div"); |
| graphSideElement.className = "network-graph-side"; |
| graphArea.appendChild(graphSideElement); |
| |
| this.summaryGraphElement = document.createElement("canvas"); |
| this.summaryGraphElement.setAttribute("width", "450"); |
| this.summaryGraphElement.setAttribute("height", "38"); |
| this.summaryGraphElement.className = "network-summary-graph"; |
| graphSideElement.appendChild(this.summaryGraphElement); |
| |
| this.legendElement = document.createElement("div"); |
| this.legendElement.className = "network-graph-legend"; |
| graphSideElement.appendChild(this.legendElement); |
| |
| this.drawSummaryGraph(); // draws an empty graph |
| |
| this.needsRefresh = true; |
| } |
| |
| WebInspector.NetworkPanel.prototype = { |
| show: function() |
| { |
| WebInspector.Panel.prototype.show.call(this); |
| WebInspector.networkListItem.select(); |
| this.refreshIfNeeded(); |
| }, |
| |
| hide: function() |
| { |
| WebInspector.Panel.prototype.hide.call(this); |
| WebInspector.networkListItem.deselect(); |
| }, |
| |
| resize: function() |
| { |
| this.updateTimelineDividersIfNeeded(); |
| }, |
| |
| resourcesClicked: function(event) |
| { |
| // If the click wasn't inside a network resource row, ignore it. |
| var resourceElement = event.target.firstParentOrSelfWithClass("network-resource"); |
| if (!resourceElement) |
| return; |
| |
| // If the click was within the network info element, ignore it. |
| var networkInfo = event.target.firstParentOrSelfWithClass("network-info"); |
| if (networkInfo) |
| return; |
| |
| // If the click was within the tip balloon element, hide it. |
| var balloon = event.target.firstParentOrSelfWithClass("tip-balloon"); |
| if (balloon) { |
| resourceElement.timelineEntry.showingTipBalloon = false; |
| return; |
| } |
| |
| resourceElement.timelineEntry.toggleShowingInfo(); |
| }, |
| |
| changeGraphMode: function(event) |
| { |
| this.updateSummaryGraph(); |
| }, |
| |
| get calculator() |
| { |
| return this.graphModeSelectElement.options[this.graphModeSelectElement.selectedIndex].calculator; |
| }, |
| |
| get totalDuration() |
| { |
| return this.latestEndTime - this.earliestStartTime; |
| }, |
| |
| get needsRefresh() |
| { |
| return this._needsRefresh; |
| }, |
| |
| set needsRefresh(x) |
| { |
| if (this._needsRefresh === x) |
| return; |
| this._needsRefresh = x; |
| if (x && this.visible) |
| this.refresh(); |
| }, |
| |
| refreshIfNeeded: function() |
| { |
| if (this.needsRefresh) |
| this.refresh(); |
| }, |
| |
| refresh: function() |
| { |
| this.needsRefresh = false; |
| |
| // calling refresh will call updateTimelineBoundriesIfNeeded, which can clear needsRefresh for future entries, |
| // so find all the entries that needs refresh first, then loop back trough them to call refresh |
| var entriesNeedingRefresh = []; |
| var entriesLength = this.timelineEntries.length; |
| for (var i = 0; i < entriesLength; ++i) { |
| var entry = this.timelineEntries[i]; |
| if (entry.needsRefresh || entry.infoNeedsRefresh) |
| entriesNeedingRefresh.push(entry); |
| } |
| |
| entriesLength = entriesNeedingRefresh.length; |
| for (var i = 0; i < entriesLength; ++i) |
| entriesNeedingRefresh[i].refresh(false, true, true); |
| |
| this.updateTimelineDividersIfNeeded(); |
| this.sortTimelineEntriesIfNeeded(); |
| this.updateSummaryGraph(); |
| }, |
| |
| makeLegendElement: function(label, value, color) |
| { |
| var legendElement = document.createElement("label"); |
| legendElement.className = "network-graph-legend-item"; |
| |
| if (color) { |
| var swatch = document.createElement("canvas"); |
| swatch.className = "network-graph-legend-swatch"; |
| swatch.setAttribute("width", "13"); |
| swatch.setAttribute("height", "24"); |
| |
| legendElement.appendChild(swatch); |
| |
| this.drawSwatch(swatch, color); |
| } |
| |
| var labelElement = document.createElement("div"); |
| labelElement.className = "network-graph-legend-label"; |
| legendElement.appendChild(labelElement); |
| |
| var headerElement = document.createElement("div"); |
| var headerElement = document.createElement("div"); |
| headerElement.className = "network-graph-legend-header"; |
| headerElement.textContent = label; |
| labelElement.appendChild(headerElement); |
| |
| var valueElement = document.createElement("div"); |
| valueElement.className = "network-graph-legend-value"; |
| valueElement.textContent = value; |
| labelElement.appendChild(valueElement); |
| |
| return legendElement; |
| }, |
| |
| sortTimelineEntriesSoonIfNeeded: function() |
| { |
| if ("sortTimelineEntriesTimeout" in this) |
| return; |
| this.sortTimelineEntriesTimeout = setTimeout(this.sortTimelineEntriesIfNeeded.bind(this), 500); |
| }, |
| |
| sortTimelineEntriesIfNeeded: function() |
| { |
| if ("sortTimelineEntriesTimeout" in this) { |
| clearTimeout(this.sortTimelineEntriesTimeout); |
| delete this.sortTimelineEntriesTimeout; |
| } |
| |
| this.timelineEntries.sort(WebInspector.NetworkPanel.timelineEntryCompare); |
| |
| var nextSibling = null; |
| for (var i = (this.timelineEntries.length - 1); i >= 0; --i) { |
| var entry = this.timelineEntries[i]; |
| if (entry.resourceElement.nextSibling !== nextSibling) |
| this.resourcesElement.insertBefore(entry.resourceElement, nextSibling); |
| nextSibling = entry.resourceElement; |
| } |
| }, |
| |
| updateTimelineBoundriesIfNeeded: function(resource, immediate) |
| { |
| var didUpdate = false; |
| if (resource.startTime !== -1 && (this.earliestStartTime === undefined || resource.startTime < this.earliestStartTime)) { |
| this.earliestStartTime = resource.startTime; |
| didUpdate = true; |
| } |
| |
| if (resource.endTime !== -1 && (this.latestEndTime === undefined || resource.endTime > this.latestEndTime)) { |
| this.latestEndTime = resource.endTime; |
| didUpdate = true; |
| } |
| |
| if (didUpdate) { |
| if (immediate) { |
| this.refreshAllTimelineEntries(true, true, immediate); |
| this.updateTimelineDividersIfNeeded(); |
| } else { |
| this.refreshAllTimelineEntriesSoon(true, true, immediate); |
| this.updateTimelineDividersSoonIfNeeded(); |
| } |
| } |
| |
| return didUpdate; |
| }, |
| |
| updateTimelineDividersSoonIfNeeded: function() |
| { |
| if ("updateTimelineDividersTimeout" in this) |
| return; |
| this.updateTimelineDividersTimeout = setTimeout(this.updateTimelineDividersIfNeeded.bind(this), 500); |
| }, |
| |
| updateTimelineDividersIfNeeded: function() |
| { |
| if ("updateTimelineDividersTimeout" in this) { |
| clearTimeout(this.updateTimelineDividersTimeout); |
| delete this.updateTimelineDividersTimeout; |
| } |
| |
| if (!this.visible) { |
| this.needsRefresh = true; |
| return; |
| } |
| |
| if (document.body.offsetWidth <= 0) { |
| // The stylesheet hasn't loaded yet, so we need to update later. |
| setTimeout(this.updateTimelineDividersIfNeeded.bind(this), 0); |
| return; |
| } |
| |
| var dividerCount = Math.round(this.dividersElement.offsetWidth / 64); |
| var timeSlice = this.totalDuration / dividerCount; |
| |
| if (this.lastDividerTimeSlice === timeSlice) |
| return; |
| |
| this.lastDividerTimeSlice = timeSlice; |
| |
| this.dividersElement.removeChildren(); |
| |
| for (var i = 1; i <= dividerCount; ++i) { |
| var divider = document.createElement("div"); |
| divider.className = "network-divider"; |
| if (i === dividerCount) |
| divider.addStyleClass("last"); |
| divider.style.left = ((i / dividerCount) * 100) + "%"; |
| |
| var label = document.createElement("div"); |
| label.className = "network-divider-label"; |
| label.textContent = Number.secondsToString(timeSlice * i); |
| divider.appendChild(label); |
| |
| this.dividersElement.appendChild(divider); |
| } |
| }, |
| |
| refreshAllTimelineEntriesSoon: function(skipBoundryUpdate, skipTimelineSort, immediate) |
| { |
| if ("refreshAllTimelineEntriesTimeout" in this) |
| return; |
| this.refreshAllTimelineEntriesTimeout = setTimeout(this.refreshAllTimelineEntries.bind(this), 500, skipBoundryUpdate, skipTimelineSort, immediate); |
| }, |
| |
| refreshAllTimelineEntries: function(skipBoundryUpdate, skipTimelineSort, immediate) |
| { |
| if ("refreshAllTimelineEntriesTimeout" in this) { |
| clearTimeout(this.refreshAllTimelineEntriesTimeout); |
| delete this.refreshAllTimelineEntriesTimeout; |
| } |
| |
| var entriesLength = this.timelineEntries.length; |
| for (var i = 0; i < entriesLength; ++i) |
| this.timelineEntries[i].refresh(skipBoundryUpdate, skipTimelineSort, immediate); |
| }, |
| |
| fadeOutRect: function(ctx, x, y, w, h, a1, a2) |
| { |
| ctx.save(); |
| |
| var gradient = ctx.createLinearGradient(x, y, x, y + h); |
| gradient.addColorStop(0.0, "rgba(0, 0, 0, " + (1.0 - a1) + ")"); |
| gradient.addColorStop(0.8, "rgba(0, 0, 0, " + (1.0 - a2) + ")"); |
| gradient.addColorStop(1.0, "rgba(0, 0, 0, 1.0)"); |
| |
| ctx.globalCompositeOperation = "destination-out"; |
| |
| ctx.fillStyle = gradient; |
| ctx.fillRect(x, y, w, h); |
| |
| ctx.restore(); |
| }, |
| |
| drawSwatch: function(canvas, color) |
| { |
| var ctx = canvas.getContext("2d"); |
| |
| function drawSwatchSquare() { |
| ctx.fillStyle = color; |
| ctx.fillRect(0, 0, 13, 13); |
| |
| var gradient = ctx.createLinearGradient(0, 0, 13, 13); |
| gradient.addColorStop(0.0, "rgba(255, 255, 255, 0.2)"); |
| gradient.addColorStop(1.0, "rgba(255, 255, 255, 0.0)"); |
| |
| ctx.fillStyle = gradient; |
| ctx.fillRect(0, 0, 13, 13); |
| |
| gradient = ctx.createLinearGradient(13, 13, 0, 0); |
| gradient.addColorStop(0.0, "rgba(0, 0, 0, 0.2)"); |
| gradient.addColorStop(1.0, "rgba(0, 0, 0, 0.0)"); |
| |
| ctx.fillStyle = gradient; |
| ctx.fillRect(0, 0, 13, 13); |
| |
| ctx.strokeStyle = "rgba(0, 0, 0, 0.6)"; |
| ctx.strokeRect(0.5, 0.5, 12, 12); |
| } |
| |
| ctx.clearRect(0, 0, 13, 24); |
| |
| drawSwatchSquare(); |
| |
| ctx.save(); |
| |
| ctx.translate(0, 25); |
| ctx.scale(1, -1); |
| |
| drawSwatchSquare(); |
| |
| ctx.restore(); |
| |
| this.fadeOutRect(ctx, 0, 13, 13, 13, 0.5, 0.0); |
| }, |
| |
| drawSummaryGraph: function(segments) |
| { |
| if (!this.summaryGraphElement) |
| return; |
| |
| if (!segments || !segments.length) |
| segments = [{color: "white", value: 1}]; |
| |
| // Calculate the total of all segments. |
| var total = 0; |
| for (var i = 0; i < segments.length; ++i) |
| total += segments[i].value; |
| |
| // Calculate the percentage of each segment, rounded to the nearest percent. |
| var percents = segments.map(function(s) { return Math.max(Math.round(100 * s.value / total), 1) }); |
| |
| // Calculate the total percentage. |
| var percentTotal = 0; |
| for (var i = 0; i < percents.length; ++i) |
| percentTotal += percents[i]; |
| |
| // Make sure our percentage total is not greater-than 100, it can be greater |
| // if we rounded up for a few segments. |
| while (percentTotal > 100) { |
| for (var i = 0; i < percents.length && percentTotal > 100; ++i) { |
| if (percents[i] > 1) { |
| --percents[i]; |
| --percentTotal; |
| } |
| } |
| } |
| |
| // Make sure our percentage total is not less-than 100, it can be less |
| // if we rounded down for a few segments. |
| while (percentTotal < 100) { |
| for (var i = 0; i < percents.length && percentTotal < 100; ++i) { |
| ++percents[i]; |
| ++percentTotal; |
| } |
| } |
| |
| var ctx = this.summaryGraphElement.getContext("2d"); |
| |
| var x = 0; |
| var y = 0; |
| var w = 450; |
| var h = 19; |
| var r = (h / 2); |
| |
| function drawPillShadow() |
| { |
| // This draws a line with a shadow that is offset away from the line. The line is stroked |
| // twice with different X shadow offsets to give more feathered edges. Later we erase the |
| // line with destination-out 100% transparent black, leaving only the shadow. This only |
| // works if nothing has been drawn into the canvas yet. |
| |
| ctx.beginPath(); |
| ctx.moveTo(x + 4, y + h - 3 - 0.5); |
| ctx.lineTo(x + w - 4, y + h - 3 - 0.5); |
| ctx.closePath(); |
| |
| ctx.save(); |
| |
| ctx.shadowBlur = 2; |
| ctx.shadowColor = "rgba(0, 0, 0, 0.5)"; |
| ctx.shadowOffsetX = 3; |
| ctx.shadowOffsetY = 5; |
| |
| ctx.strokeStyle = "white"; |
| ctx.lineWidth = 1; |
| |
| ctx.stroke(); |
| |
| ctx.shadowOffsetX = -3; |
| |
| ctx.stroke(); |
| |
| ctx.restore(); |
| |
| ctx.save(); |
| |
| ctx.globalCompositeOperation = "destination-out"; |
| ctx.strokeStyle = "rgba(0, 0, 0, 1)"; |
| ctx.lineWidth = 1; |
| |
| ctx.stroke(); |
| |
| ctx.restore(); |
| } |
| |
| function drawPill() |
| { |
| // Make a rounded rect path. |
| ctx.beginPath(); |
| ctx.moveTo(x, y + r); |
| ctx.lineTo(x, y + h - r); |
| ctx.quadraticCurveTo(x, y + h, x + r, y + h); |
| ctx.lineTo(x + w - r, y + h); |
| ctx.quadraticCurveTo(x + w, y + h, x + w, y + h - r); |
| ctx.lineTo(x + w, y + r); |
| ctx.quadraticCurveTo(x + w, y, x + w - r, y); |
| ctx.lineTo(x + r, y); |
| ctx.quadraticCurveTo(x, y, x, y + r); |
| ctx.closePath(); |
| |
| // Clip to the rounded rect path. |
| ctx.save(); |
| ctx.clip(); |
| |
| // Fill the segments with the associated color. |
| var previousSegmentsWidth = 0; |
| for (var i = 0; i < segments.length; ++i) { |
| var segmentWidth = Math.round(w * percents[i] / 100); |
| ctx.fillStyle = segments[i].color; |
| ctx.fillRect(x + previousSegmentsWidth, y, segmentWidth, h); |
| previousSegmentsWidth += segmentWidth; |
| } |
| |
| // Draw the segment divider lines. |
| ctx.lineWidth = 1; |
| for (var i = 1; i < 20; ++i) { |
| ctx.beginPath(); |
| ctx.moveTo(x + (i * Math.round(w / 20)) + 0.5, y); |
| ctx.lineTo(x + (i * Math.round(w / 20)) + 0.5, y + h); |
| ctx.closePath(); |
| |
| ctx.strokeStyle = "rgba(0, 0, 0, 0.2)"; |
| ctx.stroke(); |
| |
| ctx.beginPath(); |
| ctx.moveTo(x + (i * Math.round(w / 20)) + 1.5, y); |
| ctx.lineTo(x + (i * Math.round(w / 20)) + 1.5, y + h); |
| ctx.closePath(); |
| |
| ctx.strokeStyle = "rgba(255, 255, 255, 0.2)"; |
| ctx.stroke(); |
| } |
| |
| // Draw the pill shading. |
| var lightGradient = ctx.createLinearGradient(x, y, x, y + (h / 1.5)); |
| lightGradient.addColorStop(0.0, "rgba(220, 220, 220, 0.6)"); |
| lightGradient.addColorStop(0.4, "rgba(220, 220, 220, 0.2)"); |
| lightGradient.addColorStop(1.0, "rgba(255, 255, 255, 0.0)"); |
| |
| var darkGradient = ctx.createLinearGradient(x, y + (h / 3), x, y + h); |
| darkGradient.addColorStop(0.0, "rgba(0, 0, 0, 0.0)"); |
| darkGradient.addColorStop(0.8, "rgba(0, 0, 0, 0.2)"); |
| darkGradient.addColorStop(1.0, "rgba(0, 0, 0, 0.5)"); |
| |
| ctx.fillStyle = darkGradient; |
| ctx.fillRect(x, y, w, h); |
| |
| ctx.fillStyle = lightGradient; |
| ctx.fillRect(x, y, w, h); |
| |
| ctx.restore(); |
| } |
| |
| ctx.clearRect(x, y, w, (h * 2)); |
| |
| drawPillShadow(); |
| drawPill(); |
| |
| ctx.save(); |
| |
| ctx.translate(0, (h * 2) + 1); |
| ctx.scale(1, -1); |
| |
| drawPill(); |
| |
| ctx.restore(); |
| |
| this.fadeOutRect(ctx, x, y + h + 1, w, h, 0.5, 0.0); |
| }, |
| |
| updateSummaryGraphSoon: function() |
| { |
| if ("updateSummaryGraphTimeout" in this) |
| return; |
| this.updateSummaryGraphTimeout = setTimeout(this.updateSummaryGraph.bind(this), 500); |
| }, |
| |
| updateSummaryGraph: function() |
| { |
| if ("updateSummaryGraphTimeout" in this) { |
| clearTimeout(this.updateSummaryGraphTimeout); |
| delete this.updateSummaryGraphTimeout; |
| } |
| |
| var graphInfo = this.calculator.computeValues(this.timelineEntries); |
| |
| var categoryOrder = ["documents", "stylesheets", "images", "scripts", "fonts", "other"]; |
| 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}}; |
| var fillSegments = []; |
| |
| this.legendElement.removeChildren(); |
| |
| if (this.totalLegendLabel) |
| this.totalLegendLabel.parentNode.removeChild(this.totalLegendLabel); |
| |
| this.totalLegendLabel = this.makeLegendElement(this.calculator.totalTitle, this.calculator.formatValue(graphInfo.total)); |
| this.totalLegendLabel.addStyleClass("network-graph-legend-total"); |
| this.graphLabelElement.appendChild(this.totalLegendLabel); |
| |
| for (var i = 0; i < categoryOrder.length; ++i) { |
| var category = categoryOrder[i]; |
| var size = graphInfo.categoryValues[category]; |
| if (!size) |
| continue; |
| |
| var color = categoryColors[category]; |
| var colorString = "rgb(" + color.r + ", " + color.g + ", " + color.b + ")"; |
| |
| var fillSegment = {color: colorString, value: size}; |
| fillSegments.push(fillSegment); |
| |
| var legendLabel = this.makeLegendElement(WebInspector.resourceCategories[category].title, this.calculator.formatValue(size), colorString); |
| this.legendElement.appendChild(legendLabel); |
| } |
| |
| this.drawSummaryGraph(fillSegments); |
| }, |
| |
| clearTimeline: function() |
| { |
| delete this.earliestStartTime; |
| delete this.latestEndTime; |
| |
| var entriesLength = this.timelineEntries.length; |
| for (var i = 0; i < entriesLength; ++i) |
| delete this.timelineEntries[i].resource.networkTimelineEntry; |
| |
| this.timelineEntries = []; |
| this.resourcesElement.removeChildren(); |
| |
| this.drawSummaryGraph(); // draws an empty graph |
| }, |
| |
| addResourceToTimeline: function(resource) |
| { |
| var timelineEntry = new WebInspector.NetworkTimelineEntry(this, resource); |
| this.timelineEntries.push(timelineEntry); |
| this.resourcesElement.appendChild(timelineEntry.resourceElement); |
| |
| timelineEntry.refresh(); |
| this.updateSummaryGraphSoon(); |
| } |
| } |
| |
| WebInspector.NetworkPanel.prototype.__proto__ = WebInspector.Panel.prototype; |
| |
| WebInspector.NetworkPanel.timelineEntryCompare = function(a, b) |
| { |
| if (a.resource.startTime < b.resource.startTime) |
| return -1; |
| if (a.resource.startTime > b.resource.startTime) |
| return 1; |
| if (a.resource.endTime < b.resource.endTime) |
| return -1; |
| if (a.resource.endTime > b.resource.endTime) |
| return 1; |
| return 0; |
| } |
| |
| WebInspector.NetworkTimelineEntry = function(panel, resource) |
| { |
| this.panel = panel; |
| this.resource = resource; |
| resource.networkTimelineEntry = this; |
| |
| this.resourceElement = document.createElement("div"); |
| this.resourceElement.className = "network-resource"; |
| this.resourceElement.timelineEntry = this; |
| |
| this.titleElement = document.createElement("div"); |
| this.titleElement.className = "network-title"; |
| this.resourceElement.appendChild(this.titleElement); |
| |
| this.fileElement = document.createElement("div"); |
| this.fileElement.className = "network-file"; |
| this.fileElement.innerHTML = WebInspector.linkifyURL(resource.url, resource.displayName); |
| this.titleElement.appendChild(this.fileElement); |
| |
| this.tipButtonElement = document.createElement("button"); |
| this.tipButtonElement.className = "tip-button"; |
| this.showingTipButton = this.resource.tips.length; |
| this.fileElement.insertBefore(this.tipButtonElement, this.fileElement.firstChild); |
| |
| this.tipButtonElement.addEventListener("click", this.toggleTipBalloon.bind(this), false ); |
| |
| this.areaElement = document.createElement("div"); |
| this.areaElement.className = "network-area"; |
| this.titleElement.appendChild(this.areaElement); |
| |
| this.barElement = document.createElement("div"); |
| this.areaElement.appendChild(this.barElement); |
| |
| this.infoElement = document.createElement("div"); |
| this.infoElement.className = "network-info hidden"; |
| this.resourceElement.appendChild(this.infoElement); |
| } |
| |
| WebInspector.NetworkTimelineEntry.prototype = { |
| refresh: function(skipBoundryUpdate, skipTimelineSort, immediate) |
| { |
| if (!this.panel.visible) { |
| this.needsRefresh = true; |
| this.panel.needsRefresh = true; |
| return; |
| } |
| |
| delete this.needsRefresh; |
| |
| if (!skipBoundryUpdate) { |
| if (this.panel.updateTimelineBoundriesIfNeeded(this.resource, immediate)) |
| return; // updateTimelineBoundriesIfNeeded calls refresh() on all entries, so we can just return |
| } |
| |
| if (!skipTimelineSort) { |
| if (immediate) |
| this.panel.sortTimelineEntriesIfNeeded(); |
| else |
| this.panel.sortTimelineEntriesSoonIfNeeded(); |
| } |
| |
| if (this.resource.startTime !== -1) { |
| var percentStart = ((this.resource.startTime - this.panel.earliestStartTime) / this.panel.totalDuration) * 100; |
| this.barElement.style.left = percentStart + "%"; |
| } else { |
| this.barElement.style.left = null; |
| } |
| |
| if (this.resource.endTime !== -1) { |
| var percentEnd = ((this.panel.latestEndTime - this.resource.endTime) / this.panel.totalDuration) * 100; |
| this.barElement.style.right = percentEnd + "%"; |
| } else { |
| this.barElement.style.right = "0px"; |
| } |
| |
| this.barElement.className = "network-bar network-category-" + this.resource.category.name; |
| |
| if (this.infoNeedsRefresh) |
| this.refreshInfo(); |
| }, |
| |
| refreshInfo: function() |
| { |
| if (!this.showingInfo) { |
| this.infoNeedsRefresh = true; |
| return; |
| } |
| |
| if (!this.panel.visible) { |
| this.panel.needsRefresh = true; |
| this.infoNeedsRefresh = true; |
| return; |
| } |
| |
| this.infoNeedsRefresh = false; |
| |
| this.infoElement.removeChildren(); |
| |
| var sections = [ |
| {title: WebInspector.UIString("Request"), info: this.resource.sortedRequestHeaders}, |
| {title: WebInspector.UIString("Response"), info: this.resource.sortedResponseHeaders} |
| ]; |
| |
| function createSectionTable(section) |
| { |
| if (!section.info.length) |
| return; |
| |
| var table = document.createElement("table"); |
| this.infoElement.appendChild(table); |
| |
| var heading = document.createElement("th"); |
| heading.textContent = section.title; |
| |
| var row = table.createTHead().insertRow(-1).appendChild(heading); |
| var body = document.createElement("tbody"); |
| table.appendChild(body); |
| |
| section.info.forEach(function(header) { |
| var row = body.insertRow(-1); |
| var th = document.createElement("th"); |
| th.textContent = header.header; |
| row.appendChild(th); |
| row.insertCell(-1).textContent = header.value; |
| }); |
| } |
| |
| sections.forEach(createSectionTable, this); |
| }, |
| |
| refreshInfoIfNeeded: function() |
| { |
| if (this.infoNeedsRefresh === false) |
| return; |
| |
| this.refreshInfo(); |
| }, |
| |
| toggleShowingInfo: function() |
| { |
| this.showingInfo = !this.showingInfo; |
| }, |
| |
| get showingInfo() |
| { |
| return this._showingInfo; |
| }, |
| |
| set showingInfo(x) |
| { |
| if (this._showingInfo === x) |
| return; |
| |
| this._showingInfo = x; |
| |
| var element = this.infoElement; |
| if (x) { |
| element.removeStyleClass("hidden"); |
| element.style.setProperty("overflow", "hidden"); |
| this.refreshInfoIfNeeded(); |
| WebInspector.animateStyle([{element: element, start: {height: 0}, end: {height: element.offsetHeight}}], 250, function() { element.style.removeProperty("height"); element.style.removeProperty("overflow") }); |
| } else { |
| element.style.setProperty("overflow", "hidden"); |
| WebInspector.animateStyle([{element: element, end: {height: 0}}], 250, function() { element.addStyleClass("hidden"); element.style.removeProperty("height") }); |
| } |
| }, |
| |
| get showingTipButton() |
| { |
| return !this.tipButtonElement.hasStyleClass("hidden"); |
| }, |
| |
| set showingTipButton(x) |
| { |
| if (x) |
| this.tipButtonElement.removeStyleClass("hidden"); |
| else |
| this.tipButtonElement.addStyleClass("hidden"); |
| }, |
| |
| toggleTipBalloon: function(event) |
| { |
| this.showingTipBalloon = !this.showingTipBalloon; |
| event.stopPropagation(); |
| }, |
| |
| get showingTipBalloon() |
| { |
| return this._showingTipBalloon; |
| }, |
| |
| set showingTipBalloon(x) |
| { |
| if (this._showingTipBalloon === x) |
| return; |
| |
| this._showingTipBalloon = x; |
| |
| if (x) { |
| if (!this.tipBalloonElement) { |
| this.tipBalloonElement = document.createElement("div"); |
| this.tipBalloonElement.className = "tip-balloon"; |
| this.titleElement.appendChild(this.tipBalloonElement); |
| |
| this.tipBalloonContentElement = document.createElement("div"); |
| this.tipBalloonContentElement.className = "tip-balloon-content"; |
| this.tipBalloonElement.appendChild(this.tipBalloonContentElement); |
| var tipText = ""; |
| for (var id in this.resource.tips) |
| tipText += this.resource.tips[id].message + "\n"; |
| this.tipBalloonContentElement.textContent = tipText; |
| } |
| |
| this.tipBalloonElement.removeStyleClass("hidden"); |
| WebInspector.animateStyle([{element: this.tipBalloonElement, start: {left: 160, opacity: 0}, end: {left: 145, opacity: 1}}], 250); |
| } else { |
| var element = this.tipBalloonElement; |
| WebInspector.animateStyle([{element: this.tipBalloonElement, start: {left: 145, opacity: 1}, end: {left: 160, opacity: 0}}], 250, function() { element.addStyleClass("hidden") }); |
| } |
| } |
| } |
| |
| WebInspector.TimelineValueCalculator = function() |
| { |
| } |
| |
| WebInspector.TimelineValueCalculator.prototype = { |
| computeValues: function(entries) |
| { |
| var total = 0; |
| var categoryValues = {}; |
| |
| function compute(entry) |
| { |
| var value = this._value(entry); |
| if (value === undefined) |
| return; |
| |
| if (!(entry.resource.category.name in categoryValues)) |
| categoryValues[entry.resource.category.name] = 0; |
| categoryValues[entry.resource.category.name] += value; |
| total += value; |
| } |
| entries.forEach(compute, this); |
| |
| return {categoryValues: categoryValues, total: total}; |
| }, |
| |
| _value: function(entry) |
| { |
| return 0; |
| }, |
| |
| get title() |
| { |
| return ""; |
| }, |
| |
| formatValue: function(value) |
| { |
| return value.toString(); |
| } |
| } |
| |
| WebInspector.TransferTimeCalculator = function() |
| { |
| WebInspector.TimelineValueCalculator.call(this); |
| } |
| |
| WebInspector.TransferTimeCalculator.prototype = { |
| computeValues: function(entries) |
| { |
| var entriesByCategory = {}; |
| entries.forEach(function(entry) { |
| if (!(entry.resource.category.name in entriesByCategory)) |
| entriesByCategory[entry.resource.category.name] = []; |
| entriesByCategory[entry.resource.category.name].push(entry); |
| }); |
| |
| var earliestStart; |
| var latestEnd; |
| var categoryValues = {}; |
| for (var category in entriesByCategory) { |
| entriesByCategory[category].sort(WebInspector.NetworkPanel.timelineEntryCompare); |
| categoryValues[category] = 0; |
| |
| var segment = {start: -1, end: -1}; |
| entriesByCategory[category].forEach(function(entry) { |
| if (entry.resource.startTime == -1 || entry.resource.endTime == -1) |
| return; |
| |
| if (earliestStart === undefined) |
| earliestStart = entry.resource.startTime; |
| else |
| earliestStart = Math.min(earliestStart, entry.resource.startTime); |
| |
| if (latestEnd === undefined) |
| latestEnd = entry.resource.endTime; |
| else |
| latestEnd = Math.max(latestEnd, entry.resource.endTime); |
| |
| if (entry.resource.startTime <= segment.end) { |
| segment.end = Math.max(segment.end, entry.resource.endTime); |
| return; |
| } |
| |
| categoryValues[category] += segment.end - segment.start; |
| |
| segment.start = entry.resource.startTime; |
| segment.end = entry.resource.endTime; |
| }); |
| |
| // Add the last segment |
| categoryValues[category] += segment.end - segment.start; |
| } |
| |
| return {categoryValues: categoryValues, total: latestEnd - earliestStart}; |
| }, |
| |
| get title() |
| { |
| return WebInspector.UIString("Transfer Time"); |
| }, |
| |
| get totalTitle() |
| { |
| return WebInspector.UIString("Total Time"); |
| }, |
| |
| formatValue: function(value) |
| { |
| return Number.secondsToString(value); |
| } |
| } |
| |
| WebInspector.TransferTimeCalculator.prototype.__proto__ = WebInspector.TimelineValueCalculator.prototype; |
| |
| WebInspector.TransferSizeCalculator = function() |
| { |
| WebInspector.TimelineValueCalculator.call(this); |
| } |
| |
| WebInspector.TransferSizeCalculator.prototype = { |
| _value: function(entry) |
| { |
| return entry.resource.contentLength; |
| }, |
| |
| get title() |
| { |
| return WebInspector.UIString("Transfer Size"); |
| }, |
| |
| get totalTitle() |
| { |
| return WebInspector.UIString("Total Size"); |
| }, |
| |
| formatValue: function(value) |
| { |
| return Number.bytesToString(value); |
| } |
| } |
| |
| WebInspector.TransferSizeCalculator.prototype.__proto__ = WebInspector.TimelineValueCalculator.prototype; |