Web Inspector: Expose Server Timing Response Headers in Network Tab
authorcommit-queue@webkit.org <commit-queue@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 15 Oct 2018 23:10:20 +0000 (23:10 +0000)
committercommit-queue@webkit.org <commit-queue@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 15 Oct 2018 23:10:20 +0000 (23:10 +0000)
https://bugs.webkit.org/show_bug.cgi?id=190440

Patch by Charles Vazac <cvazac@gmail.com> on 2018-10-15
Reviewed by Joseph Pecoraro.

Source/WebInspectorUI:

* Localizations/en.lproj/localizedStrings.js: new key "Server Timing:"
* UserInterface/Main.html: add reference to Models/ServerTimingEntry.js
* UserInterface/Models/Resource.js:
(WI.Resource.prototype.get serverTiming):
(WI.Resource.prototype.updateForResponse):
* UserInterface/Models/ServerTimingEntry.js: Added.
(WI.ServerTimingEntry):
(WI.ServerTimingEntry.parseHeaders): parse raw response headers into an array of ServerTimingEntry objects
(WI.ServerTimingEntry.parseHeaders.consumeDelimiter):
(WI.ServerTimingEntry.parseHeaders.consumeToken):
(WI.ServerTimingEntry.):
* UserInterface/Test.html: add reference to Models/ServerTimingEntry.js
* UserInterface/Views/ResourceTimingBreakdownView.js:
(WI.ResourceTimingBreakdownView.prototype._appendServerTimingRow): render a table row per ServerTimingEntry object
(WI.ResourceTimingBreakdownView.prototype.initialLayout):
(WI.ResourceTimingBreakdownView):

LayoutTests:

* inspector/unit-tests/server-timing-entry-expected.txt:
* inspector/unit-tests/server-timing-entry.html:

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

LayoutTests/ChangeLog
LayoutTests/inspector/unit-tests/server-timing-entry-expected.txt [new file with mode: 0644]
LayoutTests/inspector/unit-tests/server-timing-entry.html [new file with mode: 0644]
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
Source/WebInspectorUI/UserInterface/Main.html
Source/WebInspectorUI/UserInterface/Models/Resource.js
Source/WebInspectorUI/UserInterface/Models/ServerTimingEntry.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Test.html
Source/WebInspectorUI/UserInterface/Views/ResourceTimingBreakdownView.js

index 98c5e0a..113a540 100644 (file)
@@ -1,3 +1,13 @@
+2018-10-15  Charles Vazac  <cvazac@gmail.com>
+
+        Web Inspector: Expose Server Timing Response Headers in Network Tab
+        https://bugs.webkit.org/show_bug.cgi?id=190440
+
+        Reviewed by Joseph Pecoraro.
+
+        * inspector/unit-tests/server-timing-entry-expected.txt:
+        * inspector/unit-tests/server-timing-entry.html:
+
 2018-10-15  Alex Christensen  <achristensen@webkit.org>
 
         Garden WK2 tests after r237104
diff --git a/LayoutTests/inspector/unit-tests/server-timing-entry-expected.txt b/LayoutTests/inspector/unit-tests/server-timing-entry-expected.txt
new file mode 100644 (file)
index 0000000..3b70607
--- /dev/null
@@ -0,0 +1,555 @@
+
+== Running test suite: ServerTimingEntry
+-- Running test case: ServerTimingEntry.parseHeaders
+Testing response header: --><--
+PASS: Parsed ServerTimingEntry count has expected results count of 0.
+
+Testing response header: -->metric<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is undefined
+
+Testing response header: -->metric;dur=123.4<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is 123.4
+PASS: description is undefined
+
+Testing response header: -->metric;dur="123.4"<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is 123.4
+PASS: description is undefined
+
+Testing response header: -->metric;desc=description<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is "description"
+
+Testing response header: -->metric;desc="description"<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is "description"
+
+Testing response header: -->metric;dur=123.4;desc=description<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is 123.4
+PASS: description is "description"
+
+Testing response header: -->metric;desc=description;dur=123.4<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is 123.4
+PASS: description is "description"
+
+Testing response header: -->aB3!#$%&'*+-.^_`|~<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "aB3!#$%&'*+-.^_`|~"
+PASS: duration is undefined
+PASS: description is undefined
+
+Testing response header: -->metric;desc="descr;,=iption";dur=123.4<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is 123.4
+PASS: description is "descr;,=iption"
+
+Testing response header: -->metric ; <--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is undefined
+
+Testing response header: -->metric , <--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is undefined
+
+Testing response header: -->metric ; dur = 123.4 ; desc = description<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is 123.4
+PASS: description is "description"
+
+Testing response header: -->metric ; desc = description ; dur = 123.4<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is 123.4
+PASS: description is "description"
+
+Testing response header: -->metric;desc = "description"<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is "description"
+
+Testing response header: -->metric     ;       <--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is undefined
+
+Testing response header: -->metric     ,       <--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is undefined
+
+Testing response header: -->metric     ;       dur     =       123.4   ;       desc    =       description<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is 123.4
+PASS: description is "description"
+
+Testing response header: -->metric     ;       desc    =       description     ;       dur     =       123.4<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is 123.4
+PASS: description is "description"
+
+Testing response header: -->metric;desc        =       "description"<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is "description"
+
+Testing response header: -->metric1;dur=12.3;desc=description1,metric2;dur=45.6;desc=description2,metric3;dur=78.9;desc=description3<--
+PASS: Parsed ServerTimingEntry count has expected results count of 3.
+PASS: name is "metric1"
+PASS: duration is 12.3
+PASS: description is "description1"
+PASS: name is "metric2"
+PASS: duration is 45.6
+PASS: description is "description2"
+PASS: name is "metric3"
+PASS: duration is 78.9
+PASS: description is "description3"
+
+Testing response header: -->metric1,metric2 ,metric3, metric4 , metric5<--
+PASS: Parsed ServerTimingEntry count has expected results count of 5.
+PASS: name is "metric1"
+PASS: duration is undefined
+PASS: description is undefined
+PASS: name is "metric2"
+PASS: duration is undefined
+PASS: description is undefined
+PASS: name is "metric3"
+PASS: duration is undefined
+PASS: description is undefined
+PASS: name is "metric4"
+PASS: duration is undefined
+PASS: description is undefined
+PASS: name is "metric5"
+PASS: duration is undefined
+PASS: description is undefined
+
+Testing response header: -->metric;desc="description"<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is "description"
+
+Testing response header: -->metric;desc="       description    "<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is "\t description \t"
+
+Testing response header: -->metric;desc="descr\"iption"<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is "descr\"iption"
+
+Testing response header: -->metric;desc=\<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is ""
+
+Testing response header: -->metric;desc="<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is ""
+
+Testing response header: -->metric;desc=\\<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is ""
+
+Testing response header: -->metric;desc=\"<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is ""
+
+Testing response header: -->metric;desc="\<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is ""
+
+Testing response header: -->metric;desc=""<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is ""
+
+Testing response header: -->metric;desc=\\\<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is ""
+
+Testing response header: -->metric;desc=\\"<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is ""
+
+Testing response header: -->metric;desc=\"\<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is ""
+
+Testing response header: -->metric;desc=\""<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is ""
+
+Testing response header: -->metric;desc="\\<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is ""
+
+Testing response header: -->metric;desc="\"<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is ""
+
+Testing response header: -->metric;desc=""\<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is ""
+
+Testing response header: -->metric;desc="""<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is ""
+
+Testing response header: -->metric;desc=\\\\<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is ""
+
+Testing response header: -->metric;desc=\\\"<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is ""
+
+Testing response header: -->metric;desc=\\"\<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is ""
+
+Testing response header: -->metric;desc=\\""<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is ""
+
+Testing response header: -->metric;desc=\"\\<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is ""
+
+Testing response header: -->metric;desc=\"\"<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is ""
+
+Testing response header: -->metric;desc=\""\<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is ""
+
+Testing response header: -->metric;desc=\"""<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is ""
+
+Testing response header: -->metric;desc="\\\<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is ""
+
+Testing response header: -->metric;desc="\\"<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is "\\"
+
+Testing response header: -->metric;desc="\"\<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is ""
+
+Testing response header: -->metric;desc="\""<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is "\""
+
+Testing response header: -->metric;desc=""\\<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is ""
+
+Testing response header: -->metric;desc=""\"<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is ""
+
+Testing response header: -->metric;desc="""\<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is ""
+
+Testing response header: -->metric;desc=""""<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is ""
+
+Testing response header: -->metric;dur=12.3;desc=description1,metric;dur=45.6;desc=description2<--
+PASS: Parsed ServerTimingEntry count has expected results count of 2.
+PASS: name is "metric"
+PASS: duration is 12.3
+PASS: description is "description1"
+PASS: name is "metric"
+PASS: duration is 45.6
+PASS: description is "description2"
+
+Testing response header: -->metric;DuR=123.4;DeSc=description<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is 123.4
+PASS: description is "description"
+
+Testing response header: -->MeTrIc;desc=DeScRiPtIoN<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "MeTrIc"
+PASS: duration is undefined
+PASS: description is "DeScRiPtIoN"
+
+Testing response header: -->metric;dur=foo<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is 0
+PASS: description is undefined
+
+Testing response header: -->metric;dur="foo"<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is 0
+PASS: description is undefined
+
+WARN: Unknown Server-Timing parameter: foo bar
+WARN: Unknown Server-Timing parameter: foo bar
+WARN: Unknown Server-Timing parameter: foo bar
+Testing response header: -->metric1;foo=bar;desc=description;foo=bar;dur=123.4;foo=bar,metric2<--
+PASS: Parsed ServerTimingEntry count has expected results count of 2.
+PASS: name is "metric1"
+PASS: duration is 123.4
+PASS: description is "description"
+PASS: name is "metric2"
+PASS: duration is undefined
+PASS: description is undefined
+
+WARN: Ignoring redundant duration.
+Testing response header: -->metric;dur=123.4;dur=567.8<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is 123.4
+PASS: description is undefined
+
+WARN: Ignoring redundant duration.
+Testing response header: -->metric;dur=foo;dur=567.8<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is 0
+PASS: description is undefined
+
+WARN: Ignoring redundant description.
+Testing response header: -->metric;desc=description1;desc=description2<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is "description1"
+
+WARN: Ignoring redundant duration.
+Testing response header: -->metric;dur;dur=123.4;desc=description<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is 0
+PASS: description is "description"
+
+WARN: Ignoring redundant duration.
+Testing response header: -->metric;dur=;dur=123.4;desc=description<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is 0
+PASS: description is "description"
+
+WARN: Ignoring redundant description.
+Testing response header: -->metric;desc;desc=description;dur=123.4<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is 123.4
+PASS: description is ""
+
+WARN: Ignoring redundant description.
+Testing response header: -->metric;desc=;desc=description;dur=123.4<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is 123.4
+PASS: description is ""
+
+Testing response header: -->metric;desc=d1 d2;dur=123.4<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is 123.4
+PASS: description is "d1"
+
+Testing response header: -->metric1;desc=d1 d2,metric2<--
+PASS: Parsed ServerTimingEntry count has expected results count of 2.
+PASS: name is "metric1"
+PASS: duration is undefined
+PASS: description is "d1"
+PASS: name is "metric2"
+PASS: duration is undefined
+PASS: description is undefined
+
+Testing response header: -->metric;desc="d1" d2;dur=123.4<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is 123.4
+PASS: description is "d1"
+
+Testing response header: -->metric1;desc="d1" d2,metric2<--
+PASS: Parsed ServerTimingEntry count has expected results count of 2.
+PASS: name is "metric1"
+PASS: duration is undefined
+PASS: description is "d1"
+PASS: name is "metric2"
+PASS: duration is undefined
+PASS: description is undefined
+
+Testing response header: -->metric==   ""foo;dur=123.4<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is undefined
+
+Testing response header: -->metric1==   ""foo,metric2<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric1"
+PASS: duration is undefined
+PASS: description is undefined
+
+Testing response header: -->metric;dur foo=12<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is 0
+PASS: description is undefined
+
+WARN: Unknown Server-Timing parameter: foo 
+Testing response header: -->metric;foo dur=12<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is undefined
+PASS: description is undefined
+
+Testing response header: --> <--
+PASS: Parsed ServerTimingEntry count has expected results count of 0.
+
+Testing response header: -->=<--
+PASS: Parsed ServerTimingEntry count has expected results count of 0.
+
+Testing response header: -->[<--
+PASS: Parsed ServerTimingEntry count has expected results count of 0.
+
+Testing response header: -->]<--
+PASS: Parsed ServerTimingEntry count has expected results count of 0.
+
+Testing response header: -->;<--
+PASS: Parsed ServerTimingEntry count has expected results count of 0.
+
+Testing response header: -->,<--
+PASS: Parsed ServerTimingEntry count has expected results count of 0.
+
+Testing response header: -->=;<--
+PASS: Parsed ServerTimingEntry count has expected results count of 0.
+
+Testing response header: -->;=<--
+PASS: Parsed ServerTimingEntry count has expected results count of 0.
+
+Testing response header: -->=,<--
+PASS: Parsed ServerTimingEntry count has expected results count of 0.
+
+Testing response header: -->,=<--
+PASS: Parsed ServerTimingEntry count has expected results count of 0.
+
+Testing response header: -->;,<--
+PASS: Parsed ServerTimingEntry count has expected results count of 0.
+
+Testing response header: -->,;<--
+PASS: Parsed ServerTimingEntry count has expected results count of 0.
+
+Testing response header: -->=;,<--
+PASS: Parsed ServerTimingEntry count has expected results count of 0.
+
+Testing response header: -->metric;    desc=   tabs-should-get-trimmed ;dur=   42      <--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is 42
+PASS: description is "tabs-should-get-trimmed"
+
+Testing response header: -->     metric;dur=123.4;desc=description<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is 123.4
+PASS: description is "description"
+
+Testing response header: -->   metric;dur=123.4;desc=description<--
+PASS: Parsed ServerTimingEntry count has expected results count of 1.
+PASS: name is "metric"
+PASS: duration is 123.4
+PASS: description is "description"
+
+
diff --git a/LayoutTests/inspector/unit-tests/server-timing-entry.html b/LayoutTests/inspector/unit-tests/server-timing-entry.html
new file mode 100644 (file)
index 0000000..1989ca4
--- /dev/null
@@ -0,0 +1,222 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+<script src="../../http/tests/inspector/resources/inspector-test.js"></script>
+<script>
+function test()
+{
+    let suite = InspectorTest.createSyncSuite("ServerTimingEntry");
+
+    suite.addTestCase({
+        name: "ServerTimingEntry.parseHeaders",
+        description: "Parsing of Server-Timing headers.",
+        test() {
+            function testServerTimingHeader(header, expectedResults) {
+                var results = WI.ServerTimingEntry.parseHeaders(header);
+
+                InspectorTest.log(`Testing response header: -->${header}<--`);
+                InspectorTest.expectEqual(results.length, expectedResults.length,
+                    `Parsed ServerTimingEntry count has expected results count of ${expectedResults.length}.`);
+
+                results.forEach((serverTimingEntry, index) => {
+                    var expectedResult = expectedResults[index];
+                    if (expectedResult === undefined) {
+                        // Guard against `results.length > expectedResults.length`.
+                        return;
+                    }
+
+                    InspectorTest.expectEqual(serverTimingEntry.name, expectedResult.name, `name is ${JSON.stringify(expectedResult.name)}`);
+                    InspectorTest.expectEqual(serverTimingEntry.duration, expectedResult.dur, `duration is ${JSON.stringify(expectedResult.dur)}`);
+                    InspectorTest.expectEqual(serverTimingEntry.description, expectedResult.desc, `description is ${JSON.stringify(expectedResult.desc)}`);
+                });
+
+                InspectorTest.log("");
+            }
+
+            // Tests from https://github.com/cvazac/generate-server-timing-tests.
+            // empty string
+            testServerTimingHeader(``, []);
+
+            // name only
+            testServerTimingHeader(`metric`, [{"name":"metric"}]);
+
+            // name and duration
+            testServerTimingHeader(`metric;dur=123.4`, [{"name":"metric","dur":123.4}]);
+            testServerTimingHeader(`metric;dur=\"123.4\"`, [{"name":"metric","dur":123.4}]);
+
+            // name and description
+            testServerTimingHeader(`metric;desc=description`, [{"name":"metric","desc":"description"}]);
+            testServerTimingHeader(`metric;desc=\"description\"`, [{"name":"metric","desc":"description"}]);
+
+            // name, duration, and description
+            testServerTimingHeader(`metric;dur=123.4;desc=description`, [{"name":"metric","dur":123.4,"desc":"description"}]);
+            testServerTimingHeader(`metric;desc=description;dur=123.4`, [{"name":"metric","desc":"description","dur":123.4}]);
+
+            // special chars in name
+            testServerTimingHeader(`aB3!#$%&'*+-.^_\`|~`, [{"name":"aB3!#$%&'*+-.^_`|~"}]);
+
+            // delimiter chars in quoted description
+            testServerTimingHeader(`metric;desc=\"descr;,=iption\";dur=123.4`, [{"name":"metric","desc":"descr;,=iption","dur":123.4}]);
+
+            // spaces
+            testServerTimingHeader(`metric ; `, [{"name":"metric"}]);
+            testServerTimingHeader(`metric , `, [{"name":"metric"}]);
+            testServerTimingHeader(`metric ; dur = 123.4 ; desc = description`, [{"name":"metric","dur":123.4,"desc":"description"}]);
+            testServerTimingHeader(`metric ; desc = description ; dur = 123.4`, [{"name":"metric","desc":"description","dur":123.4}]);
+            testServerTimingHeader(`metric;desc = \"description\"`, [{"name":"metric","desc":"description"}]);
+
+            // tabs
+            testServerTimingHeader(`metric\t;\t`, [{"name":"metric"}]);
+            testServerTimingHeader(`metric\t,\t`, [{"name":"metric"}]);
+            testServerTimingHeader(`metric\t;\tdur\t=\t123.4\t;\tdesc\t=\tdescription`, [{"name":"metric","dur":123.4,"desc":"description"}]);
+            testServerTimingHeader(`metric\t;\tdesc\t=\tdescription\t;\tdur\t=\t123.4`, [{"name":"metric","desc":"description","dur":123.4}]);
+            testServerTimingHeader(`metric;desc\t=\t\"description\"`, [{"name":"metric","desc":"description"}]);
+
+            // multiple entries
+            testServerTimingHeader(`metric1;dur=12.3;desc=description1,metric2;dur=45.6;desc=description2,metric3;dur=78.9;desc=description3`, [{"name":"metric1","dur":12.3,"desc":"description1"},{"name":"metric2","dur":45.6,"desc":"description2"},{"name":"metric3","dur":78.9,"desc":"description3"}]);
+            testServerTimingHeader(`metric1,metric2 ,metric3, metric4 , metric5`, [{"name":"metric1"},{"name":"metric2"},{"name":"metric3"},{"name":"metric4"},{"name":"metric5"}]);
+
+            // quoted-strings - happy path
+            testServerTimingHeader(`metric;desc=\"description\"`, [{"name":"metric","desc":"description"}]);
+            testServerTimingHeader(`metric;desc=\"\t description \t\"`, [{"name":"metric","desc":"\t description \t"}]);
+            testServerTimingHeader(`metric;desc=\"descr\\\"iption\"`, [{"name":"metric","desc":"descr\"iption"}]);
+
+            // quoted-strings - others
+            // metric;desc=\ --> ''
+            testServerTimingHeader(`metric;desc=\\`, [{"name":"metric","desc":""}]);
+            // metric;desc=" --> ''
+            testServerTimingHeader(`metric;desc=\"`, [{"name":"metric","desc":""}]);
+            // metric;desc=\\ --> ''
+            testServerTimingHeader(`metric;desc=\\\\`, [{"name":"metric","desc":""}]);
+            // metric;desc=\" --> ''
+            testServerTimingHeader(`metric;desc=\\\"`, [{"name":"metric","desc":""}]);
+            // metric;desc="\ --> ''
+            testServerTimingHeader(`metric;desc=\"\\`, [{"name":"metric","desc":""}]);
+            // metric;desc="" --> ''
+            testServerTimingHeader(`metric;desc=\"\"`, [{"name":"metric","desc":""}]);
+            // metric;desc=\\\ --> ''
+            testServerTimingHeader(`metric;desc=\\\\\\`, [{"name":"metric","desc":""}]);
+            // metric;desc=\\" --> ''
+            testServerTimingHeader(`metric;desc=\\\\\"`, [{"name":"metric","desc":""}]);
+            // metric;desc=\"\ --> ''
+            testServerTimingHeader(`metric;desc=\\\"\\`, [{"name":"metric","desc":""}]);
+            // metric;desc=\"" --> ''
+            testServerTimingHeader(`metric;desc=\\\"\"`, [{"name":"metric","desc":""}]);
+            // metric;desc="\\ --> ''
+            testServerTimingHeader(`metric;desc=\"\\\\`, [{"name":"metric","desc":""}]);
+            // metric;desc="\" --> ''
+            testServerTimingHeader(`metric;desc=\"\\\"`, [{"name":"metric","desc":""}]);
+            // metric;desc=""\ --> ''
+            testServerTimingHeader(`metric;desc=\"\"\\`, [{"name":"metric","desc":""}]);
+            // metric;desc=""" --> ''
+            testServerTimingHeader(`metric;desc=\"\"\"`, [{"name":"metric","desc":""}]);
+            // metric;desc=\\\\ --> ''
+            testServerTimingHeader(`metric;desc=\\\\\\\\`, [{"name":"metric","desc":""}]);
+            // metric;desc=\\\" --> ''
+            testServerTimingHeader(`metric;desc=\\\\\\\"`, [{"name":"metric","desc":""}]);
+            // metric;desc=\\"\ --> ''
+            testServerTimingHeader(`metric;desc=\\\\\"\\`, [{"name":"metric","desc":""}]);
+            // metric;desc=\\"" --> ''
+            testServerTimingHeader(`metric;desc=\\\\\"\"`, [{"name":"metric","desc":""}]);
+            // metric;desc=\"\\ --> ''
+            testServerTimingHeader(`metric;desc=\\\"\\\\`, [{"name":"metric","desc":""}]);
+            // metric;desc=\"\" --> ''
+            testServerTimingHeader(`metric;desc=\\\"\\\"`, [{"name":"metric","desc":""}]);
+            // metric;desc=\""\ --> ''
+            testServerTimingHeader(`metric;desc=\\\"\"\\`, [{"name":"metric","desc":""}]);
+            // metric;desc=\""" --> ''
+            testServerTimingHeader(`metric;desc=\\\"\"\"`, [{"name":"metric","desc":""}]);
+            // metric;desc="\\\ --> ''
+            testServerTimingHeader(`metric;desc=\"\\\\\\`, [{"name":"metric","desc":""}]);
+            // metric;desc="\\" --> '\'
+            testServerTimingHeader(`metric;desc=\"\\\\\"`, [{"name":"metric","desc":"\\"}]);
+            // metric;desc="\"\ --> ''
+            testServerTimingHeader(`metric;desc=\"\\\"\\`, [{"name":"metric","desc":""}]);
+            // metric;desc="\"" --> '"'
+            testServerTimingHeader(`metric;desc=\"\\\"\"`, [{"name":"metric","desc":"\""}]);
+            // metric;desc=""\\ --> ''
+            testServerTimingHeader(`metric;desc=\"\"\\\\`, [{"name":"metric","desc":""}]);
+            // metric;desc=""\" --> ''
+            testServerTimingHeader(`metric;desc=\"\"\\\"`, [{"name":"metric","desc":""}]);
+            // metric;desc="""\ --> ''
+            testServerTimingHeader(`metric;desc=\"\"\"\\`, [{"name":"metric","desc":""}]);
+            // metric;desc="""" --> ''
+            testServerTimingHeader(`metric;desc=\"\"\"\"`, [{"name":"metric","desc":""}]);
+
+            // duplicate entry names
+            testServerTimingHeader(`metric;dur=12.3;desc=description1,metric;dur=45.6;desc=description2`, [{"name":"metric","dur":12.3,"desc":"description1"},{"name":"metric","dur":45.6,"desc":"description2"}]);
+
+            // param name case sensitivity
+            testServerTimingHeader(`metric;DuR=123.4;DeSc=description`, [{"name":"metric","dur":123.4,"desc":"description"}]);
+
+            // param value case sensitivity
+            testServerTimingHeader(`MeTrIc;desc=DeScRiPtIoN`, [{"name":"MeTrIc","desc":"DeScRiPtIoN"}]);
+
+            // non-numeric durations
+            testServerTimingHeader(`metric;dur=foo`, [{"name":"metric","dur":0}]);
+            testServerTimingHeader(`metric;dur=\"foo\"`, [{"name":"metric","dur":0}]);
+
+            // unrecognized param names
+            testServerTimingHeader(`metric1;foo=bar;desc=description;foo=bar;dur=123.4;foo=bar,metric2`, [{"name":"metric1","desc":"description","dur":123.4},{"name":"metric2"}]);
+
+            // duplicate param names
+            testServerTimingHeader(`metric;dur=123.4;dur=567.8`, [{"name":"metric","dur":123.4}]);
+            testServerTimingHeader(`metric;dur=foo;dur=567.8`, [{"name":"metric","dur":0}]);
+            testServerTimingHeader(`metric;desc=description1;desc=description2`, [{"name":"metric","desc":"description1"}]);
+
+            // incomplete params
+            testServerTimingHeader(`metric;dur;dur=123.4;desc=description`, [{"name":"metric","dur":0,"desc":"description"}]);
+            testServerTimingHeader(`metric;dur=;dur=123.4;desc=description`, [{"name":"metric","dur":0,"desc":"description"}]);
+            testServerTimingHeader(`metric;desc;desc=description;dur=123.4`, [{"name":"metric","desc":"","dur":123.4}]);
+            testServerTimingHeader(`metric;desc=;desc=description;dur=123.4`, [{"name":"metric","desc":"","dur":123.4}]);
+
+            // extraneous characters after param value as token
+            testServerTimingHeader(`metric;desc=d1 d2;dur=123.4`, [{"name":"metric","desc":"d1","dur":123.4}]);
+            testServerTimingHeader(`metric1;desc=d1 d2,metric2`, [{"name":"metric1","desc":"d1"},{"name":"metric2"}]);
+
+            // extraneous characters after param value as quoted-string
+            testServerTimingHeader(`metric;desc=\"d1\" d2;dur=123.4`, [{"name":"metric","desc":"d1","dur":123.4}]);
+            testServerTimingHeader(`metric1;desc=\"d1\" d2,metric2`, [{"name":"metric1","desc":"d1"},{"name":"metric2"}]);
+
+            // nonsense - extraneous characters after entry name token
+            testServerTimingHeader(`metric==   \"\"foo;dur=123.4`, [{"name":"metric"}]);
+            testServerTimingHeader(`metric1==   \"\"foo,metric2`, [{"name":"metric1"}]);
+
+            // nonsense - extraneous characters after param name token
+            testServerTimingHeader(`metric;dur foo=12`, [{"name":"metric","dur":0}]);
+            testServerTimingHeader(`metric;foo dur=12`, [{"name":"metric"}]);
+
+            // nonsense - return zero entries
+            testServerTimingHeader(` `, []);
+            testServerTimingHeader(`=`, []);
+            testServerTimingHeader(`[`, []);
+            testServerTimingHeader(`]`, []);
+            testServerTimingHeader(`;`, []);
+            testServerTimingHeader(`,`, []);
+            testServerTimingHeader(`=;`, []);
+            testServerTimingHeader(`;=`, []);
+            testServerTimingHeader(`=,`, []);
+            testServerTimingHeader(`,=`, []);
+            testServerTimingHeader(`;,`, []);
+            testServerTimingHeader(`,;`, []);
+            testServerTimingHeader(`=;,`, []);
+
+            // tabs
+            testServerTimingHeader(`metric;\tdesc=\ttabs-should-get-trimmed\t;dur=\t42\t`, [{"name":"metric","desc":"tabs-should-get-trimmed","dur":42}]);
+
+            // leading whitespace
+            testServerTimingHeader(`     metric;dur=123.4;desc=description`, [{"name":"metric","dur":123.4,"desc":"description"}]);
+            testServerTimingHeader(`\tmetric;dur=123.4;desc=description`, [{"name":"metric","dur":123.4,"desc":"description"}]);
+
+            return true;
+        }
+    });
+
+    suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body onLoad="runTest()">
+</body>
+</html>
index 8179afc..62b1c23 100644 (file)
@@ -1,3 +1,27 @@
+2018-10-15  Charles Vazac  <cvazac@gmail.com>
+
+        Web Inspector: Expose Server Timing Response Headers in Network Tab
+        https://bugs.webkit.org/show_bug.cgi?id=190440
+
+        Reviewed by Joseph Pecoraro.
+
+        * Localizations/en.lproj/localizedStrings.js: new key "Server Timing:"
+        * UserInterface/Main.html: add reference to Models/ServerTimingEntry.js
+        * UserInterface/Models/Resource.js:
+        (WI.Resource.prototype.get serverTiming):
+        (WI.Resource.prototype.updateForResponse):
+        * UserInterface/Models/ServerTimingEntry.js: Added.
+        (WI.ServerTimingEntry):
+        (WI.ServerTimingEntry.parseHeaders): parse raw response headers into an array of ServerTimingEntry objects
+        (WI.ServerTimingEntry.parseHeaders.consumeDelimiter):
+        (WI.ServerTimingEntry.parseHeaders.consumeToken):
+        (WI.ServerTimingEntry.):
+        * UserInterface/Test.html: add reference to Models/ServerTimingEntry.js
+        * UserInterface/Views/ResourceTimingBreakdownView.js:
+        (WI.ResourceTimingBreakdownView.prototype._appendServerTimingRow): render a table row per ServerTimingEntry object
+        (WI.ResourceTimingBreakdownView.prototype.initialLayout):
+        (WI.ResourceTimingBreakdownView):
+
 2018-10-15  Nikita Vasilyev  <nvasilyev@apple.com>
 
         Web Inspector: Dark Mode: pseudo elements in DOM tree are too dark
index 45c6b27..1e849d8 100644 (file)
@@ -748,6 +748,7 @@ localizedStrings["Selector Path"] = "Selector Path";
 localizedStrings["Self Size"] = "Self Size";
 localizedStrings["Self Time"] = "Self Time";
 localizedStrings["Semantic Issue"] = "Semantic Issue";
+localizedStrings["Server Timing:"] = "Server Timing:";
 localizedStrings["Service Worker"] = "Service Worker";
 localizedStrings["ServiceWorker"] = "ServiceWorker";
 localizedStrings["Session"] = "Session";
index d4c8b85..fe3521d 100644 (file)
     <script src="Models/ScriptInstrument.js"></script>
     <script src="Models/ScriptSyntaxTree.js"></script>
     <script src="Models/ScriptTimelineRecord.js"></script>
+    <script src="Models/ServerTimingEntry.js"></script>
     <script src="Models/ShaderProgram.js"></script>
     <script src="Models/SourceCodePosition.js"></script>
     <script src="Models/SourceCodeRevision.js"></script>
index 2d3db87..44a78f4 100644 (file)
@@ -50,6 +50,7 @@ WI.Resource = class Resource extends WI.SourceCode
         this._responseHeaders = {};
         this._requestCookies = null;
         this._responseCookies = null;
+        this._serverTimingEntries = null;
         this._parentFrame = null;
         this._initiatorSourceCodeLocation = initiatorSourceCodeLocation || null;
         this._initiatorNode = initiatorNode || null;
@@ -606,6 +607,13 @@ WI.Resource = class Resource extends WI.SourceCode
         return this._scripts || [];
     }
 
+    get serverTiming()
+    {
+        if (!this._serverTimingEntries)
+            this._serverTimingEntries = WI.ServerTimingEntry.parseHeaders(this._responseHeaders.valueForCaseInsensitiveKey("Server-Timing"));
+        return this._serverTimingEntries;
+    }
+
     scriptForLocation(sourceCodeLocation)
     {
         console.assert(!(this instanceof WI.SourceMapResource));
@@ -689,6 +697,7 @@ WI.Resource = class Resource extends WI.SourceCode
         this._statusText = statusText;
         this._responseHeaders = responseHeaders || {};
         this._responseCookies = null;
+        this._serverTimingEntries = null;
         this._responseReceivedTimestamp = elapsedTime || NaN;
         this._timingData = WI.ResourceTimingData.fromPayload(timingData, this);
 
diff --git a/Source/WebInspectorUI/UserInterface/Models/ServerTimingEntry.js b/Source/WebInspectorUI/UserInterface/Models/ServerTimingEntry.js
new file mode 100644 (file)
index 0000000..f5ffbe5
--- /dev/null
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2018 Apple Inc. All rights reserved.
+ * Copyright 2017 The Chromium Authors. 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.ServerTimingEntry = class ServerTimingEntry
+{
+    constructor(name)
+    {
+        this._name = name;
+        this._duration = undefined;
+        this._description = undefined;
+    }
+
+    // Static
+
+    static parseHeaders(valueString = "")
+    {
+        // https://w3c.github.io/server-timing/#the-server-timing-header-field
+        function trimLeadingWhiteSpace() {
+            valueString = valueString.trimStart();
+        }
+
+        function consumeDelimiter(char) {
+            console.assert(char.length === 1);
+            trimLeadingWhiteSpace();
+            if (valueString.charAt(0) !== char)
+                return false;
+
+            valueString = valueString.substring(1);
+            return true;
+        }
+
+        function consumeToken() {
+            // https://tools.ietf.org/html/rfc7230#appendix-B
+            let result = /^(?:\s*)([\w!#$%&'*+\-.^`|~]+)(?:\s*)(.*)/.exec(valueString);
+            if (!result)
+                return null;
+
+            valueString = result[2];
+            return result[1];
+        }
+
+        function consumeTokenOrQuotedString() {
+            trimLeadingWhiteSpace();
+            if (valueString.charAt(0) === "\"")
+                return consumeQuotedString();
+
+            return consumeToken();
+        }
+
+        function consumeQuotedString() {
+            // https://tools.ietf.org/html/rfc7230#section-3.2.6
+            console.assert(valueString.charAt(0) === "\"");
+
+            // Consume the leading DQUOTE.
+            valueString = valueString.substring(1);
+
+            let unescapedValueString = "";
+            for (let i = 0; i < valueString.length; ++i) {
+                let char = valueString.charAt(i);
+                switch (char) {
+                  case "\\":
+                    // Backslash character found, ignore it.
+                    ++i;
+                    if (i < valueString.length) {
+                        // Take the character after the backslash.
+                        unescapedValueString += valueString.charAt(i);
+                    }
+                    break;
+                case "\"":
+                    // Trailing DQUOTE.
+                    valueString = valueString.substring(i + 1);
+                    return unescapedValueString;
+                default:
+                    unescapedValueString += char;
+                    break;
+                }
+            }
+
+            // No trailing DQUOTE found, this was not a valid quoted-string. Consume the entire string to complete parsing.
+            valueString = "";
+            return null;
+        }
+
+        function consumeExtraneous() {
+            let result = /([,;].*)/.exec(valueString);
+            if (result)
+                valueString = result[1];
+        }
+
+        function getParserForParameter(paramName) {
+            switch (paramName) {
+            case "dur":
+                return function(paramValue, entry) {
+                    if (paramValue !== null) {
+                        let duration = parseFloat(paramValue);
+                        if (!isNaN(duration)) {
+                            entry.duration = duration;
+                            return;
+                        }
+                    }
+                    entry.duration = 0;
+                };
+
+            case "desc":
+                return function(paramValue, entry) {
+                    entry.description = paramValue || "";
+                };
+
+            default:
+                return null;
+            }
+        }
+
+        let entries = [];
+        let name;
+        while ((name = consumeToken()) !== null) {
+            let entry = new WI.ServerTimingEntry(name);
+
+            while (consumeDelimiter(";")) {
+                let paramName;
+                if ((paramName = consumeToken()) === null)
+                    continue;
+
+                paramName = paramName.toLowerCase();
+                let parseParameter = getParserForParameter(paramName);
+                let paramValue = null;
+                if (consumeDelimiter("=")) {
+                    // Always parse the value, even if we don't recognize the parameter name.
+                    paramValue = consumeTokenOrQuotedString();
+                    consumeExtraneous();
+                }
+
+                if (parseParameter)
+                    parseParameter(paramValue, entry);
+                else
+                    console.warn("Unknown Server-Timing parameter:", paramName, paramValue)
+            }
+
+            entries.push(entry);
+            if (!consumeDelimiter(","))
+                break;
+        }
+
+        return entries;
+    }
+
+    // Public
+
+    get name() { return this._name; }
+    get duration() { return this._duration; }
+    get description() { return this._description; }
+
+    set duration(duration)
+    {
+        if (this._duration !== undefined) {
+            console.warn("Ignoring redundant duration.");
+            return;
+        }
+
+        this._duration = duration;
+    }
+
+    set description(description)
+    {
+        if (this._description !== undefined) {
+            console.warn("Ignoring redundant description.");
+            return;
+        }
+
+        this._description = description;
+    }
+};
index fd3902e..9a97567 100644 (file)
     <script src="Models/ScriptInstrument.js"></script>
     <script src="Models/ScriptSyntaxTree.js"></script>
     <script src="Models/ScriptTimelineRecord.js"></script>
+    <script src="Models/ServerTimingEntry.js"></script>
     <script src="Models/ShaderProgram.js"></script>
     <script src="Models/SourceCodeRevision.js"></script>
     <script src="Models/SourceCodeTimeline.js"></script>
index b41d963..a1d3221 100644 (file)
@@ -97,11 +97,48 @@ WI.ResourceTimingBreakdownView = class ResourceTimingBreakdownView extends WI.Vi
         return row;
     }
 
+    _appendServerTimingRow(label, duration, maxDuration)
+    {
+        let row = this._tableElement.appendChild(document.createElement("tr"));
+
+        let labelCell = row.appendChild(document.createElement("td"));
+        labelCell.className = "label";
+        labelCell.textContent = label;
+
+        // We need to allow duration to be zero.
+        if (duration !== undefined) {
+            let graphWidth = (duration / maxDuration) * 100;
+            let graphCell = row.appendChild(document.createElement("td"));
+            graphCell.className = "graph";
+
+            let block = graphCell.appendChild(document.createElement("div"));
+            // FIXME: Provide unique colors for the different ServerTiming rows based on the label/order.
+            block.classList.add("block", "response");
+            block.style.width = graphWidth + "%";
+            block.style.right = 0;
+
+            let timeCell = row.appendChild(document.createElement("td"));
+            timeCell.className = "time";
+            // Convert duration from milliseconds to seconds.
+            timeCell.textContent = Number.secondsToMillisecondsString(duration / 1000);
+        }
+
+        return row;
+    }
+
+    _appendDividerRow()
+    {
+        let emptyCell = this._appendEmptyRow().appendChild(document.createElement("td"));
+        emptyCell.colSpan = 3;
+        emptyCell.appendChild(document.createElement("hr"));
+    }
+
     initialLayout()
     {
         super.initialLayout();
 
         let {startTime, redirectStart, redirectEnd, fetchStart, domainLookupStart, domainLookupEnd, connectStart, connectEnd, secureConnectionStart, requestStart, responseStart, responseEnd} = this._resource.timingData;
+        let serverTiming = this._resource.serverTiming;
 
         this._tableElement = this.element.appendChild(document.createElement("table"));
         this._tableElement.className = "waterfall";
@@ -139,5 +176,19 @@ WI.ResourceTimingBreakdownView = class ResourceTimingBreakdownView extends WI.Vi
         this._appendHeaderRow(WI.UIString("Totals:"));
         this._appendHeaderRow(WI.UIString("Time to First Byte"), Number.secondsToMillisecondsString(responseStart - startTime), "total-row");
         this._appendHeaderRow(WI.UIString("Start to Finish"), Number.secondsToMillisecondsString(responseEnd - startTime), "total-row");
+
+        if (serverTiming.length > 0) {
+            this._appendDividerRow()
+            this._appendHeaderRow(WI.UIString("Server Timing:"));
+
+            let maxDuration = serverTiming.reduce((max, {duration = 0}) => Math.max(max, duration), 0);
+
+            for (let entry of serverTiming) {
+                let {name, duration, description} = entry;
+
+                // For the label, prefer description over name.
+                this._appendServerTimingRow(description || name, duration, maxDuration);
+            }
+        }
     }
 };