Web Inspector: Network Tab: Metrics Detail View
authorjoepeck@webkit.org <joepeck@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 20 Oct 2017 02:11:31 +0000 (02:11 +0000)
committerjoepeck@webkit.org <joepeck@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 20 Oct 2017 02:11:31 +0000 (02:11 +0000)
https://bugs.webkit.org/show_bug.cgi?id=178323
<rdar://problem/34071929>

Reviewed by Devin Rousso.

Provide a Metrics detail view for resources in the Network tab.
This detail view shows:

  - Transfer Size information (Header + Body bytes)
  - Resource Size information (Compression, MIME)
  - Timing information (ResourceTiming, Waterfall breakdown)

The display of timing information isn't quite final. But this
is a good starting point for all the information.

* Localizations/en.lproj/localizedStrings.js:
* UserInterface/Main.html:
New resources and strings.

* UserInterface/Base/MIMETypeUtilities.js:
(WI.shouldTreatMIMETypeAsText):
Helper for detecting text resources.

* UserInterface/Images/Receiving.svg: Added.
* UserInterface/Images/Sending.svg: Added.
* UserInterface/Images/gtk/Receiving.svg: Added.
* UserInterface/Images/gtk/Sending.svg: Added.
Same images for main and linux ports with different licenses.

* UserInterface/Views/NetworkResourceDetailView.js:
(WI.NetworkResourceDetailView):
(WI.NetworkResourceDetailView.prototype.metricsContentViewGoToHeaders):
(WI.NetworkResourceDetailView.prototype.metricsContentViewGoToRequestBody):
(WI.NetworkResourceDetailView.prototype.metricsContentViewGoToResponseBody):
(WI.NetworkResourceDetailView.prototype.initialLayout):
(WI.NetworkResourceDetailView.prototype._showPreferredContentView):
(WI.NetworkResourceDetailView.prototype._showContentViewForNavigationItem):
Replace "Timing" and "Details" stubs with a single "Metrics" section.
Handle delegate cases from the Metrics content view.

* UserInterface/Views/NetworkTableContentView.js:
(WI.NetworkTableContentView.displayNameForResource):
(WI.NetworkTableContentView.prototype._displayType): Deleted.
(WI.NetworkTableContentView.prototype._entryForResource):
Promote this to a static helper in case anyone else wants it.

(WI.NetworkTableContentView.prototype.closed):
(WI.NetworkTableContentView.prototype.reset):
(WI.NetworkTableContentView.prototype.networkResourceDetailViewClose):
Reorder code a bit to reduce work since hiding the detail view currently
forces a layout.

* UserInterface/Views/ResourceHeadersContentView.js:
(WI.ResourceHeadersContentView):
(WI.ResourceHeadersContentView.prototype._refreshRequestDataSection):
(WI.ResourceHeadersContentView.prototype._resourceResponseReceived):
(WI.ResourceHeadersContentView.prototype._goToRequestDataClicked): Deleted.
Simplify by making the delegate required.

* UserInterface/Views/ResourceMetricsContentView.css: Added.
(.resource-metrics):
(.resource-metrics > .content):
(.resource-metrics > .content .label):
General metrics styles.

(.resource-metrics > .content > section):
(.resource-metrics > .content > section .subtitle):
(.resource-metrics > .content > section:not(:last-of-type)):
(.resource-metrics > .content > section.split):
(.resource-metrics > .content > section.split > .subsection):
(.resource-metrics > .content > section.split > .subsection > table):
(.resource-metrics > .content > section.split > .divider):
(.resource-metrics > .content > section.network > .subsection > .container):
(.resource-metrics > .content > section.network .bytes-group):
(.resource-metrics > .content > section.network .bytes):
(body[dir=ltr] .resource-metrics > .content > section.network table > tr > td.label):
(body[dir=rtl] .resource-metrics > .content > section.network table > tr > td.label):
(.resource-metrics > .content > section.network .suffix):
(.resource-metrics > .content > section.network img):
(.resource-metrics > .content > section.network .go-to-arrow):
(.resource-metrics > .content > section.network .warning):
Styles for Sizes sections.

(.resource-metrics > .content > section.timing):
(.resource-metrics > .content > section.timing .subtitle):
(.resource-metrics > .content > section.timing > ul):
(.resource-metrics > .content > section.timing > ul > li):
(.resource-metrics > .content > section.timing > .waterfall):
(.resource-metrics > .content > section.timing > .waterfall .block):
(.resource-metrics > .content > section.timing > ul > li > .row-label):
(body[dir=ltr] .resource-metrics > .content > section.timing > ul > li > .row-label):
(body[dir=rtl] .resource-metrics > .content > section.timing > ul > li > .row-label):
(.resource-metrics > .content > section.timing > ul > li > .time-label):
(.resource-metrics > .content > section.timing > ul > li.total .block):
(.resource-metrics > .content > section.timing > ul > li.total .time-label):
(.resource-metrics > .content > section.timing .indeterminate-progress-spinner):
(.resource-metrics > .content > section.timing .empty):
Styles for Timing section.

* UserInterface/Views/ResourceMetricsContentView.js: Added.
(WI.ResourceMetricsContentView):
(WI.ResourceMetricsContentView.prototype.initialLayout.createSizeComponents):
(WI.ResourceMetricsContentView.prototype.initialLayout):
(WI.ResourceMetricsContentView.prototype.layout):
(WI.ResourceMetricsContentView.prototype.closed):
(WI.ResourceMetricsContentView.prototype._sizeComponents):
(WI.ResourceMetricsContentView.prototype._refreshTransferSizeSections.appendGoToArrow):
(WI.ResourceMetricsContentView.prototype._refreshTransferSizeSections):
(WI.ResourceMetricsContentView.prototype._refreshResourceSizeSection):
(WI.ResourceMetricsContentView.prototype._refreshTimingSection.createBlock):
(WI.ResourceMetricsContentView.prototype._refreshTimingSection.createTimeLabel):
(WI.ResourceMetricsContentView.prototype._refreshTimingSection.createRow):
(WI.ResourceMetricsContentView.prototype._refreshTimingSection):
(WI.ResourceMetricsContentView.prototype._resourceSizeDidChange):
(WI.ResourceMetricsContentView.prototype._resourceTransferSizeDidChange):
(WI.ResourceMetricsContentView.prototype._resourceMetricsDidChange):
(WI.ResourceMetricsContentView.prototype._resourceTimestampsDidChange):
Metrics content. This just creates all of the elements, and saves a
few to get populated with data later on.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@223735 268f45cc-cd09-0410-ab3c-d52691b4dbfc

13 files changed:
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
Source/WebInspectorUI/UserInterface/Base/MIMETypeUtilities.js
Source/WebInspectorUI/UserInterface/Images/Receiving.svg [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Images/Sending.svg [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Images/gtk/Receiving.svg [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Images/gtk/Sending.svg [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Main.html
Source/WebInspectorUI/UserInterface/Views/NetworkResourceDetailView.js
Source/WebInspectorUI/UserInterface/Views/NetworkTableContentView.js
Source/WebInspectorUI/UserInterface/Views/ResourceHeadersContentView.js
Source/WebInspectorUI/UserInterface/Views/ResourceMetricsContentView.css [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/ResourceMetricsContentView.js [new file with mode: 0644]

index 3122685..9b0c73f 100644 (file)
@@ -1,5 +1,128 @@
 2017-10-19  Joseph Pecoraro  <pecoraro@apple.com>
 
+        Web Inspector: Network Tab: Metrics Detail View
+        https://bugs.webkit.org/show_bug.cgi?id=178323
+        <rdar://problem/34071929>
+
+        Reviewed by Devin Rousso.
+
+        Provide a Metrics detail view for resources in the Network tab.
+        This detail view shows:
+        
+          - Transfer Size information (Header + Body bytes)
+          - Resource Size information (Compression, MIME)
+          - Timing information (ResourceTiming, Waterfall breakdown)
+
+        The display of timing information isn't quite final. But this
+        is a good starting point for all the information.
+
+        * Localizations/en.lproj/localizedStrings.js:
+        * UserInterface/Main.html:
+        New resources and strings.
+
+        * UserInterface/Base/MIMETypeUtilities.js:
+        (WI.shouldTreatMIMETypeAsText):
+        Helper for detecting text resources.
+
+        * UserInterface/Images/Receiving.svg: Added.
+        * UserInterface/Images/Sending.svg: Added.
+        * UserInterface/Images/gtk/Receiving.svg: Added.
+        * UserInterface/Images/gtk/Sending.svg: Added.
+        Same images for main and linux ports with different licenses.
+
+        * UserInterface/Views/NetworkResourceDetailView.js:
+        (WI.NetworkResourceDetailView):
+        (WI.NetworkResourceDetailView.prototype.metricsContentViewGoToHeaders):
+        (WI.NetworkResourceDetailView.prototype.metricsContentViewGoToRequestBody):
+        (WI.NetworkResourceDetailView.prototype.metricsContentViewGoToResponseBody):
+        (WI.NetworkResourceDetailView.prototype.initialLayout):
+        (WI.NetworkResourceDetailView.prototype._showPreferredContentView):
+        (WI.NetworkResourceDetailView.prototype._showContentViewForNavigationItem):
+        Replace "Timing" and "Details" stubs with a single "Metrics" section.
+        Handle delegate cases from the Metrics content view.
+
+        * UserInterface/Views/NetworkTableContentView.js:
+        (WI.NetworkTableContentView.displayNameForResource):
+        (WI.NetworkTableContentView.prototype._displayType): Deleted.
+        (WI.NetworkTableContentView.prototype._entryForResource):
+        Promote this to a static helper in case anyone else wants it.
+
+        (WI.NetworkTableContentView.prototype.closed):
+        (WI.NetworkTableContentView.prototype.reset):
+        (WI.NetworkTableContentView.prototype.networkResourceDetailViewClose):
+        Reorder code a bit to reduce work since hiding the detail view currently
+        forces a layout.
+
+        * UserInterface/Views/ResourceHeadersContentView.js:
+        (WI.ResourceHeadersContentView):
+        (WI.ResourceHeadersContentView.prototype._refreshRequestDataSection):
+        (WI.ResourceHeadersContentView.prototype._resourceResponseReceived):
+        (WI.ResourceHeadersContentView.prototype._goToRequestDataClicked): Deleted.
+        Simplify by making the delegate required.
+
+        * UserInterface/Views/ResourceMetricsContentView.css: Added.
+        (.resource-metrics):
+        (.resource-metrics > .content):
+        (.resource-metrics > .content .label):
+        General metrics styles.
+
+        (.resource-metrics > .content > section):
+        (.resource-metrics > .content > section .subtitle):
+        (.resource-metrics > .content > section:not(:last-of-type)):
+        (.resource-metrics > .content > section.split):
+        (.resource-metrics > .content > section.split > .subsection):
+        (.resource-metrics > .content > section.split > .subsection > table):
+        (.resource-metrics > .content > section.split > .divider):
+        (.resource-metrics > .content > section.network > .subsection > .container):
+        (.resource-metrics > .content > section.network .bytes-group):
+        (.resource-metrics > .content > section.network .bytes):
+        (body[dir=ltr] .resource-metrics > .content > section.network table > tr > td.label):
+        (body[dir=rtl] .resource-metrics > .content > section.network table > tr > td.label):
+        (.resource-metrics > .content > section.network .suffix):
+        (.resource-metrics > .content > section.network img):
+        (.resource-metrics > .content > section.network .go-to-arrow):
+        (.resource-metrics > .content > section.network .warning):
+        Styles for Sizes sections.
+
+        (.resource-metrics > .content > section.timing):
+        (.resource-metrics > .content > section.timing .subtitle):
+        (.resource-metrics > .content > section.timing > ul):
+        (.resource-metrics > .content > section.timing > ul > li):
+        (.resource-metrics > .content > section.timing > .waterfall):
+        (.resource-metrics > .content > section.timing > .waterfall .block):
+        (.resource-metrics > .content > section.timing > ul > li > .row-label):
+        (body[dir=ltr] .resource-metrics > .content > section.timing > ul > li > .row-label):
+        (body[dir=rtl] .resource-metrics > .content > section.timing > ul > li > .row-label):
+        (.resource-metrics > .content > section.timing > ul > li > .time-label):
+        (.resource-metrics > .content > section.timing > ul > li.total .block):
+        (.resource-metrics > .content > section.timing > ul > li.total .time-label):
+        (.resource-metrics > .content > section.timing .indeterminate-progress-spinner):
+        (.resource-metrics > .content > section.timing .empty):
+        Styles for Timing section.
+
+        * UserInterface/Views/ResourceMetricsContentView.js: Added.
+        (WI.ResourceMetricsContentView):
+        (WI.ResourceMetricsContentView.prototype.initialLayout.createSizeComponents):
+        (WI.ResourceMetricsContentView.prototype.initialLayout):
+        (WI.ResourceMetricsContentView.prototype.layout):
+        (WI.ResourceMetricsContentView.prototype.closed):
+        (WI.ResourceMetricsContentView.prototype._sizeComponents):
+        (WI.ResourceMetricsContentView.prototype._refreshTransferSizeSections.appendGoToArrow):
+        (WI.ResourceMetricsContentView.prototype._refreshTransferSizeSections):
+        (WI.ResourceMetricsContentView.prototype._refreshResourceSizeSection):
+        (WI.ResourceMetricsContentView.prototype._refreshTimingSection.createBlock):
+        (WI.ResourceMetricsContentView.prototype._refreshTimingSection.createTimeLabel):
+        (WI.ResourceMetricsContentView.prototype._refreshTimingSection.createRow):
+        (WI.ResourceMetricsContentView.prototype._refreshTimingSection):
+        (WI.ResourceMetricsContentView.prototype._resourceSizeDidChange):
+        (WI.ResourceMetricsContentView.prototype._resourceTransferSizeDidChange):
+        (WI.ResourceMetricsContentView.prototype._resourceMetricsDidChange):
+        (WI.ResourceMetricsContentView.prototype._resourceTimestampsDidChange):
+        Metrics content. This just creates all of the elements, and saves a
+        few to get populated with data later on.
+
+2017-10-19  Joseph Pecoraro  <pecoraro@apple.com>
+
         Web Inspector: Network Tab - Improve graphical representation of network waterfall
         https://bugs.webkit.org/show_bug.cgi?id=147897
         <rdar://problem/27482198>
index 9841567..d335be4 100644 (file)
@@ -132,6 +132,7 @@ localizedStrings["Binary Frame"] = "Binary Frame";
 localizedStrings["Blend"] = "Blend";
 localizedStrings["Block Variables"] = "Block Variables";
 localizedStrings["Blur"] = "Blur";
+localizedStrings["Body:"] = "Body:";
 localizedStrings["Border"] = "Border";
 localizedStrings["Bottom"] = "Bottom";
 localizedStrings["Boundary"] = "Boundary";
@@ -145,6 +146,8 @@ localizedStrings["Breakpoints"] = "Breakpoints";
 localizedStrings["Breakpoints disabled"] = "Breakpoints disabled";
 localizedStrings["Bubbling"] = "Bubbling";
 localizedStrings["Busy"] = "Busy";
+localizedStrings["Bytes Received"] = "Bytes Received";
+localizedStrings["Bytes Sent"] = "Bytes Sent";
 localizedStrings["CSP Hash"] = "CSP Hash";
 localizedStrings["CSS"] = "CSS";
 localizedStrings["CSS Canvas"] = "CSS Canvas";
@@ -220,6 +223,7 @@ localizedStrings["Composite"] = "Composite";
 localizedStrings["Composited"] = "Composited";
 localizedStrings["Compressed"] = "Compressed";
 localizedStrings["Compression"] = "Compression";
+localizedStrings["Compression:"] = "Compression:";
 localizedStrings["Condition"] = "Condition";
 localizedStrings["Conditional expression"] = "Conditional expression";
 localizedStrings["Connecting"] = "Connecting";
@@ -453,6 +457,7 @@ localizedStrings["Grow"] = "Grow";
 localizedStrings["HTML Attributes"] = "HTML Attributes";
 localizedStrings["HTTP"] = "HTTP";
 localizedStrings["Headers"] = "Headers";
+localizedStrings["Headers:"] = "Headers:";
 localizedStrings["Heading Level"] = "Heading Level";
 localizedStrings["Heap Snapshot Object (%s)"] = "Heap Snapshot Object (%s)";
 localizedStrings["Height"] = "Height";
@@ -552,6 +557,7 @@ localizedStrings["Logs"] = "Logs";
 localizedStrings["Low"] = "Low";
 localizedStrings["Lowest: %s"] = "Lowest: %s";
 localizedStrings["MIME Type"] = "MIME Type";
+localizedStrings["MIME Type:"] = "MIME Type:";
 localizedStrings["Main"] = "Main";
 localizedStrings["Main Frame"] = "Main Frame";
 localizedStrings["Manifest URL"] = "Manifest URL";
@@ -571,6 +577,7 @@ localizedStrings["Memory usage of this canvas"] = "Memory usage of this canvas";
 localizedStrings["Memory: %s"] = "Memory: %s";
 localizedStrings["Message"] = "Message";
 localizedStrings["Method"] = "Method";
+localizedStrings["Metrics"] = "Metrics";
 localizedStrings["Microtask Dispatched"] = "Microtask Dispatched";
 localizedStrings["Min"] = "Min";
 localizedStrings["Missing Dependencies:%s"] = "Missing Dependencies:%s";
@@ -621,6 +628,7 @@ localizedStrings["No response headers"] = "No response headers";
 localizedStrings["Node"] = "Node";
 localizedStrings["Node Removed"] = "Node Removed";
 localizedStrings["Nodes"] = "Nodes";
+localizedStrings["None"] = "None";
 localizedStrings["Not found"] = "Not found";
 localizedStrings["Number"] = "Number";
 localizedStrings["Numeric"] = "Numeric";
@@ -920,6 +928,7 @@ localizedStrings["This action causes no visual change"] = "This action causes no
 localizedStrings["This object is a root"] = "This object is a root";
 localizedStrings["This object is referenced by internal objects"] = "This object is referenced by internal objects";
 localizedStrings["This property needs a value.\nClick to open autocomplete."] = "This property needs a value.\nClick to open autocomplete.";
+localizedStrings["This text resource could benefit from compression"] = "This text resource could benefit from compression";
 localizedStrings["Time"] = "Time";
 localizedStrings["Timeline"] = "Timeline";
 localizedStrings["Timeline Recording %d"] = "Timeline Recording %d";
index 1184eff..0e1bb9d 100644 (file)
@@ -131,3 +131,18 @@ WI.fileExtensionForMIMEType = function(mimeType)
 
     return null;
 };
+
+WI.shouldTreatMIMETypeAsText = function(mimeType)
+{
+    if (mimeType.startsWith("text/"))
+        return true;
+
+    if (mimeType.endsWith("+json") || mimeType.endsWith("+xml"))
+        return true;
+
+    // Various script and JSON mime types.
+    if (mimeType.startsWith("application/"))
+        return mimeType.endsWith("script") || mimeType.endsWith("json");
+
+    return false;
+};
diff --git a/Source/WebInspectorUI/UserInterface/Images/Receiving.svg b/Source/WebInspectorUI/UserInterface/Images/Receiving.svg
new file mode 100644 (file)
index 0000000..a211803
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright © 2017 Apple Inc. All rights reserved. -->
+<svg xmlns="http://www.w3.org/2000/svg" id="root" version="1.1" viewBox="0 0 50 50">
+    <line stroke="hsl(0, 0%, 75%)" stroke-linecap="round" stroke-width="5" x1="25" y1="44" x2="25" y2="6"/>
+    <line stroke="hsl(0, 0%, 75%)" stroke-linecap="round" stroke-width="5" x1="25" y1="44" x2="10" y2="28"/>
+    <line stroke="hsl(0, 0%, 75%)" stroke-linecap="round" stroke-width="5" x1="25" y1="44" x2="40" y2="28"/>
+</svg>
diff --git a/Source/WebInspectorUI/UserInterface/Images/Sending.svg b/Source/WebInspectorUI/UserInterface/Images/Sending.svg
new file mode 100644 (file)
index 0000000..7a58858
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright © 2017 Apple Inc. All rights reserved. -->
+<svg xmlns="http://www.w3.org/2000/svg" id="root" version="1.1" viewBox="0 0 50 50">
+    <line stroke="hsl(0, 0%, 75%)" stroke-linecap="round" stroke-width="5" x1="25" y1="6" x2="25" y2="44"/>
+    <line stroke="hsl(0, 0%, 75%)" stroke-linecap="round" stroke-width="5" x1="25" y1="6" x2="10" y2="22"/>
+    <line stroke="hsl(0, 0%, 75%)" stroke-linecap="round" stroke-width="5" x1="25" y1="6" x2="40" y2="22"/>
+</svg>
diff --git a/Source/WebInspectorUI/UserInterface/Images/gtk/Receiving.svg b/Source/WebInspectorUI/UserInterface/Images/gtk/Receiving.svg
new file mode 100644 (file)
index 0000000..e2ff863
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Licensed under the Creative Commons Attribution-Share Alike 3.0 United States License (http://creativecommons.org/licenses/by-sa/3.0/) -->
+<svg xmlns="http://www.w3.org/2000/svg" id="root" version="1.1" viewBox="0 0 50 50">
+    <line stroke="hsl(0, 0%, 75%)" stroke-linecap="round" stroke-width="5" x1="25" y1="44" x2="25" y2="6"/>
+    <line stroke="hsl(0, 0%, 75%)" stroke-linecap="round" stroke-width="5" x1="25" y1="44" x2="10" y2="28"/>
+    <line stroke="hsl(0, 0%, 75%)" stroke-linecap="round" stroke-width="5" x1="25" y1="44" x2="40" y2="28"/>
+</svg>
diff --git a/Source/WebInspectorUI/UserInterface/Images/gtk/Sending.svg b/Source/WebInspectorUI/UserInterface/Images/gtk/Sending.svg
new file mode 100644 (file)
index 0000000..ff419af
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Licensed under the Creative Commons Attribution-Share Alike 3.0 United States License (http://creativecommons.org/licenses/by-sa/3.0/) -->
+<svg xmlns="http://www.w3.org/2000/svg" id="root" version="1.1" viewBox="0 0 50 50">
+    <line stroke="hsl(0, 0%, 75%)" stroke-linecap="round" stroke-width="5" x1="25" y1="6" x2="25" y2="44"/>
+    <line stroke="hsl(0, 0%, 75%)" stroke-linecap="round" stroke-width="5" x1="25" y1="6" x2="10" y2="22"/>
+    <line stroke="hsl(0, 0%, 75%)" stroke-linecap="round" stroke-width="5" x1="25" y1="6" x2="40" y2="22"/>
+</svg>
index fa97561..c0f17b4 100644 (file)
     <link rel="stylesheet" href="Views/RenderingFrameTimelineView.css">
     <link rel="stylesheet" href="Views/Resizer.css">
     <link rel="stylesheet" href="Views/ResourceCookiesContentView.css">
+    <link rel="stylesheet" href="Views/ResourceMetricsContentView.css">
     <link rel="stylesheet" href="Views/ResourceDetailsSection.css">
     <link rel="stylesheet" href="Views/ResourceDetailsSidebarPanel.css">
     <link rel="stylesheet" href="Views/ResourceHeadersContentView.css">
     <script src="Views/Resizer.js"></script>
     <script src="Views/ResourceClusterContentView.js"></script>
     <script src="Views/ResourceCollectionContentView.js"></script>
+    <script src="Views/ResourceCookiesContentView.js"></script>
+    <script src="Views/ResourceMetricsContentView.js"></script>
     <script src="Views/ResourceDetailsSection.js"></script>
     <script src="Views/ResourceDetailsSidebarPanel.js"></script>
     <script src="Views/ResourceHeadersContentView.js"></script>
-    <script src="Views/ResourceCookiesContentView.js"></script>
     <script src="Views/ResourceSidebarPanel.js"></script>
     <script src="Views/ResourceTimelineDataGridNode.js"></script>
     <script src="Views/ResourceTimingBreakdownView.js"></script>
index 74e8275..3871c95 100644 (file)
@@ -40,8 +40,7 @@ WI.NetworkResourceDetailView = class NetworkResourceDetailView extends WI.View
         this._resourceContentView = null;
         this._headersContentView = null;
         this._cookiesContentView = null;
-        this._timingContentView = null;
-        this._detailsContentView = null;
+        this._metricsContentView = null;
     }
 
     // Public
@@ -78,6 +77,27 @@ WI.NetworkResourceDetailView = class NetworkResourceDetailView extends WI.View
         this._resourceContentView.showRequest();
     }
 
+    // ResourceMetricsContentView delegate
+
+    metricsContentViewGoToHeaders(metricsContentView)
+    {
+        this._contentBrowser.navigationBar.selectedNavigationItem = this._headersNavigationItem;
+    }
+
+    metricsContentViewGoToRequestBody(metricsContentView)
+    {
+        this._contentBrowser.navigationBar.selectedNavigationItem = this._previewNavigationItem;
+
+        this._resourceContentView.showRequest();
+    }
+
+    metricsContentViewGoToResponseBody(metricsContentView)
+    {
+        this._contentBrowser.navigationBar.selectedNavigationItem = this._previewNavigationItem;
+
+        this._resourceContentView.showResponse();
+    }
+
     // Protected
 
     initialLayout()
@@ -97,8 +117,7 @@ WI.NetworkResourceDetailView = class NetworkResourceDetailView extends WI.View
         this._previewNavigationItem = new WI.RadioButtonNavigationItem("preview", WI.UIString("Preview"));
         this._headersNavigationItem = new WI.RadioButtonNavigationItem("headers", WI.UIString("Headers"));
         this._cookiesNavigationItem = new WI.RadioButtonNavigationItem("cookies", WI.UIString("Cookies"));
-        this._timingNavigationItem = new WI.RadioButtonNavigationItem("timing", WI.UIString("Timing"));
-        this._detailsNavigationItem = new WI.RadioButtonNavigationItem("details", WI.UIString("Details"));
+        this._metricsNavigationItem = new WI.RadioButtonNavigationItem("metrics", WI.UIString("Metrics"));
 
         // Insert all of our custom navigation items at the start of the ContentBrowser's NavigationBar.
         let index = 0;
@@ -107,8 +126,7 @@ WI.NetworkResourceDetailView = class NetworkResourceDetailView extends WI.View
         this._contentBrowser.navigationBar.insertNavigationItem(this._previewNavigationItem, index++);
         this._contentBrowser.navigationBar.insertNavigationItem(this._headersNavigationItem, index++);
         this._contentBrowser.navigationBar.insertNavigationItem(this._cookiesNavigationItem, index++);
-        this._contentBrowser.navigationBar.insertNavigationItem(this._timingNavigationItem, index++);
-        this._contentBrowser.navigationBar.insertNavigationItem(this._detailsNavigationItem, index++);
+        this._contentBrowser.navigationBar.insertNavigationItem(this._metricsNavigationItem, index++);
         this._contentBrowser.navigationBar.addEventListener(WI.NavigationBar.Event.NavigationItemSelected, this._navigationItemSelected, this);
 
         this.addSubview(this._contentBrowser);
@@ -130,8 +148,7 @@ WI.NetworkResourceDetailView = class NetworkResourceDetailView extends WI.View
             if (navigationItem !== this._previewNavigationItem
                 && navigationItem !== this._headersNavigationItem
                 && navigationItem !== this._cookiesNavigationItem
-                && navigationItem !== this._timingNavigationItem
-                && navigationItem !== this._detailsNavigationItem)
+                && navigationItem !== this._metricsNavigationItem)
                 continue;
 
             if (!firstNavigationItem)
@@ -165,17 +182,10 @@ WI.NetworkResourceDetailView = class NetworkResourceDetailView extends WI.View
                 this._cookiesContentView = new WI.ResourceCookiesContentView(this._resource);
             this._contentBrowser.showContentView(this._cookiesContentView);
             break;
-        case "timing":
-            // FIXME: Provide a Resource Timing View.
-            if (!this._timingContentView)
-                this._timingContentView = new WI.DebugContentView("Timing");
-            this._contentBrowser.showContentView(this._timingContentView);
-            break;
-        case "details":
-            // FIXME: Provide a Resource Details View.
-            if (!this._detailsContentView)
-                this._detailsContentView = new WI.DebugContentView("Details");
-            this._contentBrowser.showContentView(this._detailsContentView);
+        case "metrics":
+            if (!this._metricsContentView)
+                this._metricsContentView = new WI.ResourceMetricsContentView(this._resource, this);
+            this._contentBrowser.showContentView(this._metricsContentView);
             break;
         }
     }
index 3499105..ab39682 100644 (file)
@@ -115,6 +115,21 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
 
     // Static
 
+    static displayNameForResource(resource)
+    {
+        if (resource.type === WI.Resource.Type.Image || resource.type === WI.Resource.Type.Font) {
+            let fileExtension;
+            if (resource.mimeType)
+                fileExtension = WI.fileExtensionForMIMEType(resource.mimeType);
+            if (!fileExtension)
+                fileExtension = WI.fileExtensionForURL(resource.url);
+            if (fileExtension)
+                return fileExtension;
+        }
+
+        return WI.NetworkTableContentView.shortDisplayNameForResourceType(resource.type).toLowerCase();
+    }
+
     static shortDisplayNameForResourceType(type)
     {
         switch (type) {
@@ -192,13 +207,13 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
 
     closed()
     {
-        this._hidePopover();
-        this._hideResourceDetailView();
-
         for (let detailView of this._resourceDetailViewMap.values())
             detailView.dispose();
         this._resourceDetailViewMap.clear();
 
+        this._hidePopover();
+        this._hideResourceDetailView();
+
         WI.Frame.removeEventListener(null, null, this);
         WI.Resource.removeEventListener(null, null, this);
         WI.frameResourceManager.removeEventListener(WI.FrameResourceManager.Event.MainFrameDidChange, this._mainFrameDidChange, this);
@@ -222,11 +237,11 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         this._updateWaterfallTimelineRuler();
 
         if (this._table) {
-            this._hidePopover();
-            this._hideResourceDetailView();
             this._selectedResource = null;
             this._table.clearSelectedRow();
             this._table.reloadData();
+            this._hidePopover();
+            this._hideResourceDetailView();
         }
     }
 
@@ -234,9 +249,9 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
 
     networkResourceDetailViewClose(resourceDetailView)
     {
-        this._hideResourceDetailView();
         this._selectedResource = null;
         this._table.clearSelectedRow();
+        this._hideResourceDetailView();
     }
 
     // Table dataSource
@@ -1099,21 +1114,6 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         }
     }
 
-    _displayType(resource)
-    {
-        if (resource.type === WI.Resource.Type.Image || resource.type === WI.Resource.Type.Font) {
-            let fileExtension;
-            if (resource.mimeType)
-                fileExtension = WI.fileExtensionForMIMEType(resource.mimeType);
-            if (!fileExtension)
-                fileExtension = WI.fileExtensionForURL(resource.url);
-            if (fileExtension)
-                return fileExtension;
-        }
-
-        return WI.NetworkTableContentView.shortDisplayNameForResourceType(resource.type).toLowerCase();
-    }
-
     _entryForResource(resource)
     {
         // FIXME: <https://webkit.org/b/143632> Web Inspector: Resources with the same name in different folders aren't distinguished
@@ -1126,7 +1126,7 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
             scheme: resource.urlComponents.scheme ? resource.urlComponents.scheme.toLowerCase() : "",
             method: resource.requestMethod,
             type: resource.type,
-            displayType: this._displayType(resource),
+            displayType: WI.NetworkTableContentView.displayNameForResource(resource),
             mimeType: resource.mimeType,
             status: resource.statusCode,
             cached: resource.cached,
index c0a0f20..d3f2f9e 100644 (file)
@@ -30,13 +30,14 @@ WI.ResourceHeadersContentView = class ResourceHeadersContentView extends WI.Cont
         super(null);
 
         console.assert(resource instanceof WI.Resource);
+        console.assert(delegate);
 
         this._resource = resource;
         this._resource.addEventListener(WI.Resource.Event.MetricsDidChange, this._resourceMetricsDidChange, this);
         this._resource.addEventListener(WI.Resource.Event.RequestHeadersDidChange, this._resourceRequestHeadersDidChange, this);
         this._resource.addEventListener(WI.Resource.Event.ResponseReceived, this._resourceResponseReceived, this);
 
-        this._delegate = delegate || null;
+        this._delegate = delegate;
 
         this._searchQuery = null;
         this._searchResults = null;
@@ -406,7 +407,7 @@ WI.ResourceHeadersContentView = class ResourceHeadersContentView extends WI.Cont
             this._appendKeyValuePair(detailsElement, WI.UIString("Encoding"), encoding);
 
         let goToButton = detailsElement.appendChild(WI.createGoToArrowButton());
-        goToButton.addEventListener("click", this._goToRequestDataClicked.bind(this));
+        goToButton.addEventListener("click", () => { this._delegate.headersContentViewGoToRequestData(this); });
         this._appendKeyValuePair(detailsElement, WI.UIString("Request Data"), goToButton);
     }
 
@@ -481,10 +482,4 @@ WI.ResourceHeadersContentView = class ResourceHeadersContentView extends WI.Cont
         this._needsResponseHeadersRefresh = true;
         this.needsLayout();
     }
-
-    _goToRequestDataClicked(event)
-    {
-        if (this._delegate)
-            this._delegate.headersContentViewGoToRequestData(this);
-    }
 };
diff --git a/Source/WebInspectorUI/UserInterface/Views/ResourceMetricsContentView.css b/Source/WebInspectorUI/UserInterface/Views/ResourceMetricsContentView.css
new file mode 100644 (file)
index 0000000..d1d58fd
--- /dev/null
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2017 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. 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 INC. 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.
+ */
+
+.resource-metrics {
+    font-family: '-webkit-system-font';
+}
+
+.resource-metrics > .content {
+    width: 550px;
+    margin: 0 auto;
+}
+
+.resource-metrics > .content .label {
+    color: gray;
+}
+
+.resource-metrics > .content > section {
+    position: relative;
+    padding: 10px 0;
+}
+
+.resource-metrics > .content > section .subtitle {
+    margin-bottom: 10px;
+    font-size: 14px;
+    text-align: center;
+    letter-spacing: 0.04em;
+}
+
+.resource-metrics > .content > section:not(:last-of-type) {
+    border-bottom: 1px solid var(--border-color);
+}
+
+.resource-metrics > .content > section.split {
+    display: flex;
+    justify-content: center;
+}
+
+.resource-metrics > .content > section.split > .subsection {
+    width: 160px;
+    margin: 10px 0px;
+}
+
+.resource-metrics > .content > section.split > .subsection.large .icon {
+    width: 32px;
+    height: 32px;
+}
+
+.resource-metrics > .content > section.split > .subsection > table {
+    margin: 0 auto;
+}
+
+.resource-metrics > .content > section.split > .divider {
+    margin: 0 10px;
+    border-right: 1px solid var(--border-color);
+}
+
+.resource-metrics > .content > section.network > .subsection > .container {
+    display: flex;
+    justify-content: center;
+    margin-bottom: 8px;
+}
+
+.resource-metrics > .content > section.network .bytes-group {
+    text-align: start;
+}
+
+.resource-metrics > .content > section.network .bytes {
+    margin-top: 2px;
+    font-size: 28px;
+    display: inline-block;
+}
+
+.resource-metrics > .content > section.network table > tr > td.label {
+    text-align: end;
+}
+
+.resource-metrics > .content > section.network .suffix {
+    display: inline-block;
+    margin: 13px 1px 0 1px;
+    font-size: 15px;
+}
+
+.resource-metrics > .content > section.network img {
+    width: 32px;
+    height: 32px;
+    margin: 3px;
+}
+
+.resource-metrics > .content > section.network .go-to-arrow {
+    bottom: -1px;
+    height: 12px;
+    vertical-align: top;
+}
+
+.resource-metrics > .content > section.network .warning {
+    display: inline-block;
+    width: 10px;
+    height: 10px;
+    -webkit-margin-start: 3px;
+}
+
+.resource-metrics > .content > section.timing {
+    padding: 20px 0;
+}
+
+.resource-metrics > .content > section.timing .subtitle {
+    letter-spacing: 0.06em;
+}
+
+.resource-metrics > .content > section.timing > ul {
+    margin: 0;
+    padding: 0;
+    list-style-type: none;
+}
+
+.resource-metrics > .content > section.timing > ul > li {
+    position: relative;
+    height: 20px;
+    padding: 1px 0;
+    line-height: 18px;
+}
+
+.resource-metrics > .content > section.timing > .waterfall {
+    width: 500px;
+    margin: auto;
+}
+
+.resource-metrics > .content > section.timing > .waterfall .block {
+    top: 4px;
+    min-width: 1px;
+    height: 12px;
+}
+
+.resource-metrics > .content > section.timing > ul > li > .row-label {
+    display: inline-block;
+    min-width: 70px;
+    color: black;
+}
+
+.resource-metrics > .content > section.timing > ul > li > .row-label {
+    text-align: end;
+}
+
+.resource-metrics > .content > section.timing > ul > li > .time-label {
+    position: absolute;
+    top: 1px;
+    color: gray;
+}
+
+.resource-metrics > .content > section.timing > ul > li.total .block {
+    display: none;
+}
+
+.resource-metrics > .content > section.timing > ul > li.total .time-label {
+    color: black;
+}
+
+.resource-metrics > .content > section.timing .indeterminate-progress-spinner {
+    margin: 0 auto;
+}
+
+.resource-metrics > .content > section.timing .empty {
+    text-align: center;
+    color: gray;
+}
diff --git a/Source/WebInspectorUI/UserInterface/Views/ResourceMetricsContentView.js b/Source/WebInspectorUI/UserInterface/Views/ResourceMetricsContentView.js
new file mode 100644 (file)
index 0000000..fb684fc
--- /dev/null
@@ -0,0 +1,379 @@
+/*
+ * Copyright (C) 2017 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. 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 INC. 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.
+ */
+
+WI.ResourceMetricsContentView = class ResourceMetricsContentView extends WI.ContentView
+{
+    constructor(resource, delegate)
+    {
+        super(null);
+
+        console.assert(resource instanceof WI.Resource);
+        console.assert(delegate);
+
+        this._resource = resource;
+        this._resource.addEventListener(WI.Resource.Event.SizeDidChange, this._resourceSizeDidChange, this);
+        this._resource.addEventListener(WI.Resource.Event.TransferSizeDidChange, this._resourceTransferSizeDidChange, this);
+        this._resource.addEventListener(WI.Resource.Event.MetricsDidChange, this._resourceMetricsDidChange, this);
+        this._resource.addEventListener(WI.Resource.Event.TimestampsDidChange, this._resourceTimestampsDidChange, this);
+
+        this._delegate = delegate;
+
+        this.element.classList.add("resource-details", "resource-metrics");
+
+        this._needsTransferSizesRefresh = false;
+        this._needsResourceSizeRefresh = false;
+        this._needsTimingRefresh = false;
+    }
+
+    // Protected
+
+    initialLayout()
+    {
+        super.initialLayout();
+
+        let contentElement = this.element.appendChild(document.createElement("div"));
+        contentElement.className = "content";
+
+        // Network.
+
+        let networkSection = contentElement.appendChild(document.createElement("section"));
+        networkSection.className = "network split";
+
+        function createSizeComponents(parentElement, subtitle, imageSource, label1, label2) {
+            let subtitleElement = parentElement.appendChild(document.createElement("div"));
+            subtitleElement.className = "subtitle";
+            subtitleElement.textContent = subtitle;
+
+            let container = parentElement.appendChild(document.createElement("div"));
+            container.className = "container";
+
+            let imageElement = container.appendChild(document.createElement("img"));
+            if (imageSource)
+                imageElement.src = imageSource;
+
+            let groupElement = container.appendChild(document.createElement("div"));
+            groupElement.className = "bytes-group";
+
+            let bytesElement = groupElement.appendChild(document.createElement("div"));
+            bytesElement.className = "bytes";
+
+            let suffixElement = groupElement.appendChild(document.createElement("div"));
+            suffixElement.className = "suffix";
+
+            let table = parentElement.appendChild(document.createElement("table"));
+            let headerRow = table.appendChild(document.createElement("tr"));
+            let label1Element = headerRow.appendChild(document.createElement("td"));
+            let value1Element = headerRow.appendChild(document.createElement("td"));
+            let bodyRow = table.appendChild(document.createElement("tr"));
+            let label2Element = bodyRow.appendChild(document.createElement("td"));
+            let value2Element = bodyRow.appendChild(document.createElement("td"));
+
+            label1Element.textContent = label1;
+            label1Element.className = "label";
+            label2Element.textContent = label2;
+            label2Element.className = "label";
+
+            return {
+                container,
+                bytesElement,
+                suffixElement,
+                imageElement,
+                value1Element,
+                value2Element,
+            };
+        }
+
+        let sendingSection = networkSection.appendChild(document.createElement("div"));
+        sendingSection.className = "subsection";
+
+        let sendingComponents = createSizeComponents(sendingSection, WI.UIString("Bytes Sent"), "Images/Sending.svg", WI.UIString("Headers:"), WI.UIString("Body:"));
+        this._sendingBytesElement = sendingComponents.bytesElement;
+        this._sendingBytesSuffixElement = sendingComponents.suffixElement;
+        this._sendingHeaderBytesElement = sendingComponents.value1Element;
+        this._sendingBodyBytesElement = sendingComponents.value2Element;
+
+        let bytesDivider = networkSection.appendChild(document.createElement("div"));
+        bytesDivider.className = "divider";
+
+        let receivingSection = networkSection.appendChild(document.createElement("div"));
+        receivingSection.className = "subsection";
+
+        let receivingComponents = createSizeComponents(receivingSection, WI.UIString("Bytes Received"), "Images/Receiving.svg", WI.UIString("Headers:"), WI.UIString("Body:"));
+        this._receivingBytesElement = receivingComponents.bytesElement;
+        this._receivingBytesSuffixElement = receivingComponents.suffixElement;
+        this._receivingHeaderBytesElement = receivingComponents.value1Element;
+        this._receivingBodyBytesElement = receivingComponents.value2Element;
+
+        let resourceDivider = networkSection.appendChild(document.createElement("div"));
+        resourceDivider.className = "divider";
+
+        let resourceSection = networkSection.appendChild(document.createElement("div"));
+        resourceSection.className = "subsection large";
+
+        let resourceComponents = createSizeComponents(resourceSection, WI.UIString("Resource Size"), null, WI.UIString("Compression:"), WI.UIString("MIME Type:"));
+        resourceComponents.container.classList.add(WI.ResourceTreeElement.ResourceIconStyleClassName, this._resource.type);
+        resourceComponents.imageElement.classList.add("icon");
+        this._resourceBytesElement = resourceComponents.bytesElement;
+        this._resourceBytesSuffixElement = resourceComponents.suffixElement;
+        this._compressionElement = resourceComponents.value1Element;
+        this._contentTypeElement = resourceComponents.value2Element;
+
+        // Timing.
+
+        this._timingSection = contentElement.appendChild(document.createElement("section"));
+        this._timingSection.className = "timing";
+
+        this._timingSubtitle = document.createElement("div");
+        this._timingSubtitle.className = "subtitle";
+        this._timingSubtitle.textContent = WI.UIString("Timing");
+
+        // Populate.
+
+        this._refreshTransferSizeSections();
+        this._refreshResourceSizeSection();
+        this._refreshTimingSection();
+
+        this._needsTransferSizesRefresh = false;
+        this._needsResourceSizeRefresh = false;
+        this._needsTimingRefresh = false;
+    }
+
+    layout()
+    {
+        super.layout();
+
+        if (this._needsTransferSizesRefresh) {
+            this._refreshTransferSizeSections();
+            this._needsTransferSizesRefresh = false;
+        }
+
+        if (this._needsResourceSizeRefresh) {
+            this._refreshResourceSizeSection();
+            this._needsResourceSizeRefresh = false;
+        }
+
+        if (this._needsTimingRefresh) {
+            this._refreshTimingSection();
+            this._needsTimingRefresh = false;
+        }
+    }
+
+    closed()
+    {
+        this._resource.removeEventListener(null, null, this);
+
+        super.closed();
+    }
+
+    // Private
+
+    _sizeComponents(bytes)
+    {
+        console.assert(bytes >= 0);
+
+        // Prefer KB over B. And prefer 1 decimal point to keep sizes simple
+        // but we will still need B if bytes is less than 0.1 KB.
+        if (bytes < 103)
+            return [bytes.toFixed(0), "B"];
+
+        let kilobytes = bytes / 1024;
+        if (kilobytes < 1024)
+            return [kilobytes.toFixed(1), "KB"];
+
+        let megabytes = kilobytes / 1024;
+        if (megabytes < 1024)
+            return [megabytes.toFixed(1), "MB"];
+
+        let gigabytes = megabytes / 1024;
+        return [gigabytes.toFixed(1), "GB"];
+    }
+
+    _refreshTransferSizeSections()
+    {
+        let bytesSentHeader = this._resource.requestHeadersTransferSize;
+        let bytesSentBody = this._resource.requestBodyTransferSize;
+        let bytesSent = bytesSentHeader + bytesSentBody;
+
+        let bytesReceivedHeader = this._resource.responseHeadersTransferSize;
+        let bytesReceivedBody = this._resource.responseBodyTransferSize;
+        let bytesReceived = bytesReceivedHeader + bytesReceivedBody;
+
+        let [sentValue, sentSuffix] = this._sizeComponents(bytesSent || 0);
+        this._sendingBytesElement.textContent = sentValue;
+        this._sendingBytesSuffixElement.textContent = sentSuffix;
+
+        this._sendingHeaderBytesElement.textContent = bytesSentHeader ? Number.bytesToString(bytesSentHeader) : emDash;
+        this._sendingBodyBytesElement.textContent = bytesSentBody ? Number.bytesToString(bytesSentBody) : emDash;
+
+        let [receivedValue, receivedSuffix] = this._sizeComponents(bytesReceived || 0);
+        this._receivingBytesElement.textContent = receivedValue;
+        this._receivingBytesSuffixElement.textContent = receivedSuffix;
+
+        this._receivingHeaderBytesElement.textContent = bytesReceivedHeader ? Number.bytesToString(bytesReceivedHeader) : emDash;
+        this._receivingBodyBytesElement.textContent = bytesReceivedBody ? Number.bytesToString(bytesReceivedBody) : emDash;
+
+        function appendGoToArrow(parentElement, handler) {
+            let goToButton = parentElement.appendChild(WI.createGoToArrowButton());
+            goToButton.addEventListener("click", handler);
+        }
+
+        if (bytesSentHeader)
+            appendGoToArrow(this._sendingHeaderBytesElement, () => { this._delegate.metricsContentViewGoToHeaders(this); });
+        if (bytesSentBody)
+            appendGoToArrow(this._sendingBodyBytesElement, () => { this._delegate.metricsContentViewGoToRequestBody(this); });
+        if (bytesReceivedHeader)
+            appendGoToArrow(this._receivingHeaderBytesElement, () => { this._delegate.metricsContentViewGoToHeaders(this); });
+        if (bytesReceivedBody)
+            appendGoToArrow(this._receivingBodyBytesElement, () => { this._delegate.metricsContentViewGoToResponseBody(this); });
+    }
+
+    _refreshResourceSizeSection()
+    {
+        let encodedSize = !isNaN(this._resource.networkEncodedSize) ? this._resource.networkEncodedSize : this._resource.estimatedNetworkEncodedSize;
+        let decodedSize = !isNaN(this._resource.networkDecodedSize) ? this._resource.networkDecodedSize : this._resource.size;
+        let compressionRate = decodedSize / encodedSize;
+        let compressionString = compressionRate > 0 && isFinite(compressionRate) ? WI.UIString("%.2f\u00d7").format(compressionRate) : WI.UIString("None");
+
+        let [resourceSizeValue, resourceSizeSuffix] = this._sizeComponents(decodedSize || 0);
+        this._resourceBytesElement.textContent = resourceSizeValue;
+        this._resourceBytesSuffixElement.textContent = resourceSizeSuffix;
+
+        let contentEncoding = this._resource.responseHeaders.valueForCaseInsensitiveKey("Content-Encoding");
+        if (contentEncoding)
+            compressionString += ` (${contentEncoding.toLowerCase()})`;
+
+        this._compressionElement.textContent = compressionString;
+        this._contentTypeElement.textContent = this._resource.mimeType || emDash;
+
+        const minimumSizeBeforeWarning = 1024;
+        if (compressionRate <= 1 && encodedSize >= minimumSizeBeforeWarning && WI.shouldTreatMIMETypeAsText(this._resource.mimeType))
+            this._compressionElement.appendChild(useSVGSymbol("Images/Warning.svg", "warning", WI.UIString("This text resource could benefit from compression")));
+    }
+
+    _refreshTimingSection()
+    {
+        this._timingSection.removeChildren();
+
+        this._timingSection.appendChild(this._timingSubtitle);
+
+        if (!this._resource.hasResponse()) {
+            let spinner = new WI.IndeterminateProgressSpinner;
+            this._timingSection.appendChild(spinner.element);
+            return;
+        }
+
+        if (!this._resource.timingData.startTime || !this._resource.timingData.responseEnd) {
+            let p = this._timingSection.appendChild(document.createElement("p"));
+            p.className = "empty";
+            p.textContent = WI.UIString("Resource does not have timing data");
+            return;
+        }
+
+        // FIXME: Converge on using WI.ResourceTimingBreakdownView when a design is finalized.
+
+        let listElement = this._timingSection.appendChild(document.createElement("ul"));
+        listElement.className = "waterfall"; // Include waterfall block styles.
+
+        const graphWidth = 380;
+        const graphStartOffset = 80;
+
+        let {startTime, domainLookupStart, domainLookupEnd, connectStart, connectEnd, secureConnectionStart, requestStart, responseStart, responseEnd} = this._resource.timingData;
+        let graphStartTime = startTime;
+        let graphEndTime = responseEnd;
+        let secondsPerPixel = (responseEnd - startTime) / graphWidth;
+
+        function createBlock(startTime, endTime, className, makeEmpty) {
+            let startOffset = graphStartOffset + ((startTime - graphStartTime) / secondsPerPixel);
+            let width = makeEmpty ? 1 : (endTime - startTime) / secondsPerPixel;
+            let block = document.createElement("div");
+            block.classList.add("block", className);
+            let property = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL ? "right" : "left";
+            block.style[property] = startOffset + "px";
+            block.style.width = width + "px";
+            return block;
+        }
+
+        function createTimeLabel(endTime, label) {
+            let positionOffset = graphStartOffset + ((endTime - graphStartTime) / secondsPerPixel);
+            positionOffset += 3;
+            let timeLabel = document.createElement("div");
+            timeLabel.className = "time-label";
+            timeLabel.textContent = label;
+            let property = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL ? "right" : "left";
+            timeLabel.style[property] = positionOffset + "px";
+            return timeLabel;
+        }
+
+        function createRow(label, startTime, endTime, className) {
+            let row = document.createElement("li");
+            let labelElement = row.appendChild(document.createElement("span"));
+            labelElement.className = "row-label";
+            labelElement.textContent = label;
+            row.appendChild(createBlock(startTime, endTime, className));
+            row.appendChild(createTimeLabel(endTime, Number.secondsToMillisecondsString(endTime - startTime)));
+            return row;
+        }
+
+        listElement.appendChild(createRow(WI.UIString("Scheduled"), startTime, domainLookupStart || connectStart || requestStart, "queue"));
+        if (domainLookupStart)
+            listElement.appendChild(createRow(WI.UIString("DNS"), domainLookupStart, domainLookupEnd || connectStart || requestStart, "dns"));
+        if (connectStart)
+            listElement.appendChild(createRow(WI.UIString("TCP"), connectStart, connectEnd || requestStart, "connect"));
+        if (secureConnectionStart)
+            listElement.appendChild(createRow(WI.UIString("Secure"), secureConnectionStart, connectEnd || requestStart, "secure"));
+        listElement.appendChild(createRow(WI.UIString("Request"), requestStart, responseStart, "request"));
+        listElement.appendChild(createRow(WI.UIString("Response"), responseStart, responseEnd, "response"));
+
+        let totalRow = createRow(WI.UIString("Total"), startTime, responseEnd, "total");
+        listElement.appendChild(totalRow);
+        totalRow.classList.add("total");
+    }
+
+    _resourceSizeDidChange(event)
+    {
+        this._needsTransferSizesRefresh = true;
+        this.needsLayout();
+    }
+
+    _resourceTransferSizeDidChange(event)
+    {
+        this._needsTransferSizesRefresh = true;
+        this.needsLayout();
+    }
+
+    _resourceMetricsDidChange(event)
+    {
+        this._needsTransferSizesRefresh = true;
+        this._needsResourceSizeRefresh = true;
+        this._needsTimingRefresh = true;
+        this.needsLayout();
+    }
+
+    _resourceTimestampsDidChange(event)
+    {
+        this._needsTimingRefresh = true;
+        this.needsLayout();
+    }
+};