Web Inspector: create special Network waterfall for media events
authordrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 11 Oct 2018 04:13:17 +0000 (04:13 +0000)
committerdrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 11 Oct 2018 04:13:17 +0000 (04:13 +0000)
https://bugs.webkit.org/show_bug.cgi?id=189773
<rdar://problem/44626605>

Reviewed by Joseph Pecoraro.

Source/JavaScriptCore:

* inspector/protocol/DOM.json:
Add `didFireEvent` event that is fired when specific event listeners added by
`InspectorInstrumentation::addEventListenersToNode` are fired.

Source/WebCore:

Test: http/tests/inspector/dom/didFireEvent.html

* html/HTMLMediaElement.cpp:
(WebCore::HTMLMediaElement::HTMLMediaElement):

* inspector/InspectorInstrumentation.h:
(WebCore::InspectorInstrumentation::addEventListenersToNode): Added.
* inspector/InspectorInstrumentation.cpp:
(WebCore::InspectorInstrumentation::addEventListenersToNodeImpl): Added.

* inspector/agents/InspectorDOMAgent.h:
* inspector/agents/InspectorDOMAgent.cpp:
(WebCore::EventFiredCallback): Added.
(WebCore::EventFiredCallback::create): Added.
(WebCore::EventFiredCallback::operator==): Added.
(WebCore::EventFiredCallback::handleEvent): Added.
(WebCore::EventFiredCallback::EventFiredCallback): Added.
(WebCore::InspectorDOMAgent::didCreateFrontendAndBackend):
(WebCore::InspectorDOMAgent::addEventListenersToNode): Added.

Source/WebInspectorUI:

* Localizations/en.lproj/localizedStrings.js:
* UserInterface/Main.html:
* UserInterface/Base/Utilities.js:

* UserInterface/Protocol/DOMObserver.js:
(WI.DOMObserver.prototype.didFireEvent): Added.
* UserInterface/Controllers/DOMManager.js:
(WI.DOMManager.prototype.didFireEvent): Added.
* UserInterface/Models/DOMNode.js:
(WI.DOMNode):
(WI.DOMNode.prototype.get domEvents): Added.
(WI.DOMNode.prototype.didFireEvent): Added.
(WI.DOMNode.prototype._addDOMEvent): Added.

* UserInterface/Views/NetworkTableContentView.js:
(WI.NetworkTableContentView):
(WI.NetworkTableContentView.prototype.shown):
(WI.NetworkTableContentView.prototype.hidden):
(WI.NetworkTableContentView.prototype.closed):
(WI.NetworkTableContentView.prototype.reset):
(WI.NetworkTableContentView.prototype.showRepresentedObject):
(WI.NetworkTableContentView.prototype.networkDetailViewClose): Added.
(WI.NetworkTableContentView.prototype.tableSortChanged):
(WI.NetworkTableContentView.prototype.tableSelectionDidChange):
(WI.NetworkTableContentView.prototype._populateNameCell):
(WI.NetworkTableContentView.prototype._populateWaterfallGraph.positionByStartOffset): Added.
(WI.NetworkTableContentView.prototype._populateWaterfallGraph.setWidthForDuration): Added.
(WI.NetworkTableContentView.prototype._populateWaterfallGraph.createDOMEventLine): Added.
(WI.NetworkTableContentView.prototype._populateWaterfallGraph.appendBlock):
(WI.NetworkTableContentView.prototype._populateWaterfallGraph):
(WI.NetworkTableContentView.prototype._processPendingEntries):
(WI.NetworkTableContentView.prototype._rowIndexForRepresentedObject): Added.
(WI.NetworkTableContentView.prototype._updateEntryForResource):
(WI.NetworkTableContentView.prototype._hideDetailView): Added.
(WI.NetworkTableContentView.prototype._showDetailView): Added.
(WI.NetworkTableContentView.prototype._positionDetailView): Added.
(WI.NetworkTableContentView.prototype._resourceTransferSizeDidChange):
(WI.NetworkTableContentView.prototype._tryLinkResourceToDOMNode):
(WI.NetworkTableContentView.prototype._handleNodeDidFireEvent): Added.
(WI.NetworkTableContentView.prototype._updateFilteredEntries):
(WI.NetworkTableContentView.prototype._typeFilterScopeBarSelectionChanged):
(WI.NetworkTableContentView.prototype._urlFilterDidChange):
(WI.NetworkTableContentView.prototype._restoreSelectedRow):
(WI.NetworkTableContentView.prototype._waterfallPopoverContent): Added.
(WI.NetworkTableContentView.prototype._waterfallPopoverContentForResourceEntry): Added.
(WI.NetworkTableContentView.prototype._waterfallPopoverContentForNodeEntry): Added.
(WI.NetworkTableContentView.prototype._handleResourceEntryMousedownWaterfall): Added.
(WI.NetworkTableContentView.prototype._handleNodeEntryMousedownWaterfall): Added.
(WI.NetworkTableContentView.prototype._handleMousedownWaterfall): Added.
(WI.NetworkTableContentView.prototype.networkResourceDetailViewClose): Deleted.
(WI.NetworkTableContentView.prototype._rowIndexForResource): Deleted.
(WI.NetworkTableContentView.prototype._hideResourceDetailView): Deleted.
(WI.NetworkTableContentView.prototype._showResourceDetailView): Deleted.
(WI.NetworkTableContentView.prototype._waterfallPopoverContentForResource): Deleted.
* UserInterface/Views/NetworkTableContentView.css:
(.content-view.network .network-table): Added.
(.network-table :not(.header) .cell.waterfall .waterfall-container > .dom-event): Added.
(.network-table :not(.header) .cell.waterfall .waterfall-container > .dom-activity): Added.
(.network-table :not(.header) .cell.waterfall .waterfall-container > .dom-activity.playing): Added.

* UserInterface/Views/NetworkDOMNodeDetailView.js: Added.
(WI.NetworkDOMNodeDetailView):
(WI.NetworkDOMNodeDetailView.prototype.initialLayout):
(WI.NetworkDOMNodeDetailView.prototype.showContentViewForIdentifier):

* UserInterface/Views/NetworkResourceDetailView.css:
(.content-view.resource-details):
(.network-resource-detail): Deleted.
(.network-resource-detail .navigation-bar): Deleted.
(.network-resource-detail .item.close > .glyph): Deleted.
(.network-resource-detail .item.close > .glyph:hover): Deleted.
(.network-resource-detail .item.close > .glyph:active): Deleted.
(.network .network-resource-detail .navigation-bar .item.radio.button.text-only): Deleted.
(.network .network-resource-detail .navigation-bar .item.radio.button.text-only.selected): Deleted.
(.network-resource-detail > .content-browser): Deleted.
(@media (prefers-dark-interface)): Deleted.
* UserInterface/Views/NetworkResourceDetailView.js:
(WI.NetworkResourceDetailView):
(WI.NetworkResourceDetailView.prototype.shown):
(WI.NetworkResourceDetailView.prototype.headersContentViewGoToRequestData):
(WI.NetworkResourceDetailView.prototype.sizesContentViewGoToHeaders):
(WI.NetworkResourceDetailView.prototype.sizesContentViewGoToRequestBody):
(WI.NetworkResourceDetailView.prototype.sizesContentViewGoToResponseBody):
(WI.NetworkResourceDetailView.prototype.initialLayout):
(WI.NetworkResourceDetailView.prototype.showContentViewForIdentifier):
(WI.NetworkResourceDetailView.prototype.get resource): Deleted.
(WI.NetworkResourceDetailView.prototype.hidden): Deleted.
(WI.NetworkResourceDetailView.prototype.dispose): Deleted.
(WI.NetworkResourceDetailView.prototype.willShowWithCookie): Deleted.
(WI.NetworkResourceDetailView.prototype.initialLayout): Deleted.
(WI.NetworkResourceDetailView.prototype._showPreferredContentView): Deleted.
(WI.NetworkResourceDetailView.prototype._showContentViewForNavigationItem): Deleted.
(WI.NetworkResourceDetailView.prototype._navigationItemSelected): Deleted.
(WI.NetworkResourceDetailView.prototype._handleCloseButton): Deleted.

* UserInterface/Views/NetworkDetailView.js: Added.
(WI.NetworkDetailView):
(WI.NetworkDetailView.prototype.get representedObject):
(WI.NetworkDetailView.prototype.shown):
(WI.NetworkDetailView.prototype.hidden):
(WI.NetworkDetailView.prototype.dispose):
(WI.NetworkDetailView.prototype.willShowWithCookie):
(WI.NetworkDetailView.prototype.initialLayout):
(WI.NetworkDetailView.prototype.createDetailNavigationItem):
(WI.NetworkDetailView.prototype.detailNavigationItemForIdentifier):
(WI.NetworkDetailView.prototype.showContentViewForIdentifier):
(WI.NetworkDetailView.prototype._showPreferredContentView):
(WI.NetworkDetailView.prototype._navigationItemSelected):
(WI.NetworkDetailView.prototype._handleCloseButton):
* UserInterface/Views/NetworkDetailView.css: Added.
(.network-detail):
(.network-detail .navigation-bar):
(.network-detail .item.close > .glyph):
(.network-detail .item.close > .glyph:hover):
(.network-detail .item.close > .glyph:active):
(.network .network-detail .navigation-bar .item.radio.button.text-only):
(.network .network-detail .navigation-bar .item.radio.button.text-only.selected):
(.network-detail > .content-browser):
(@media (prefers-dark-interface)):
Create base class for detail views shown in the Network tab.

* UserInterface/Views/DOMNodeEventsContentView.js: Added.
(WI.DOMNodeEventsContentView):
(WI.DOMNodeEventsContentView.prototype.initialLayout):
(WI.DOMNodeEventsContentView.prototype.closed):
(WI.DOMNodeEventsContentView.prototype._handleDOMNodeDidFireEvent):
* UserInterface/Views/DOMNodeEventsContentView.css: Added.
(.dom-node-details.dom-events):

* UserInterface/Views/DOMEventsBreakdownView.js: Added.
(WI.DOMEventsBreakdownView):
(WI.DOMEventsBreakdownView.prototype.addEvent):
(WI.DOMEventsBreakdownView.prototype.initialLayout):
(WI.DOMEventsBreakdownView.prototype._populateTable.percentOfTotalTime):
(WI.DOMEventsBreakdownView.prototype._populateTable):
* UserInterface/Views/DOMEventsBreakdownView.css: Added.
(.waterfall-popover-content .dom-events-breakdown):
(.dom-events-breakdown):
(.dom-events-breakdown table):
(.dom-events-breakdown tr > :matches(th, td)):
(.dom-events-breakdown tbody > tr):
(.dom-events-breakdown .graph):
(.dom-events-breakdown .graph > :matches(.point, .area)):
(.dom-events-breakdown .graph > .point):
(.dom-events-breakdown .time):

* UserInterface/Views/ResourceTimingBreakdownView.css:
(.resource-timing-breakdown > table > tr.header:not(.total-row) > td): Added.
(.popover.waterfall-popover): Deleted.

LayoutTests:

* http/tests/inspector/dom/didFireEvent-expected.txt: Added.
* http/tests/inspector/dom/didFireEvent.html: Added.

* inspector/unit-tests/array-utilities-expected.txt:
* inspector/unit-tests/array-utilities.html:

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

32 files changed:
LayoutTests/ChangeLog
LayoutTests/http/tests/inspector/dom/didFireEvent-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/inspector/dom/didFireEvent.html [new file with mode: 0644]
LayoutTests/inspector/unit-tests/array-utilities-expected.txt
LayoutTests/inspector/unit-tests/array-utilities.html
Source/JavaScriptCore/ChangeLog
Source/JavaScriptCore/inspector/protocol/DOM.json
Source/WebCore/ChangeLog
Source/WebCore/html/HTMLMediaElement.cpp
Source/WebCore/inspector/InspectorInstrumentation.cpp
Source/WebCore/inspector/InspectorInstrumentation.h
Source/WebCore/inspector/agents/InspectorDOMAgent.cpp
Source/WebCore/inspector/agents/InspectorDOMAgent.h
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
Source/WebInspectorUI/UserInterface/Base/Utilities.js
Source/WebInspectorUI/UserInterface/Controllers/DOMManager.js
Source/WebInspectorUI/UserInterface/Main.html
Source/WebInspectorUI/UserInterface/Models/DOMNode.js
Source/WebInspectorUI/UserInterface/Protocol/DOMObserver.js
Source/WebInspectorUI/UserInterface/Views/DOMEventsBreakdownView.css [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/DOMEventsBreakdownView.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/DOMNodeEventsContentView.css [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/DOMNodeEventsContentView.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/NetworkDOMNodeDetailView.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/NetworkDetailView.css [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/NetworkDetailView.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/NetworkResourceDetailView.css
Source/WebInspectorUI/UserInterface/Views/NetworkResourceDetailView.js
Source/WebInspectorUI/UserInterface/Views/NetworkTableContentView.css
Source/WebInspectorUI/UserInterface/Views/NetworkTableContentView.js
Source/WebInspectorUI/UserInterface/Views/ResourceTimingBreakdownView.css

index 3a76efb..3f5d4cf 100644 (file)
@@ -1,3 +1,17 @@
+2018-10-10  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: create special Network waterfall for media events
+        https://bugs.webkit.org/show_bug.cgi?id=189773
+        <rdar://problem/44626605>
+
+        Reviewed by Joseph Pecoraro.
+
+        * http/tests/inspector/dom/didFireEvent-expected.txt: Added.
+        * http/tests/inspector/dom/didFireEvent.html: Added.
+
+        * inspector/unit-tests/array-utilities-expected.txt:
+        * inspector/unit-tests/array-utilities.html:
+
 2018-10-10  Brent Fulgham  <bfulgham@apple.com>
 
         Only report the supported WebGL version
diff --git a/LayoutTests/http/tests/inspector/dom/didFireEvent-expected.txt b/LayoutTests/http/tests/inspector/dom/didFireEvent-expected.txt
new file mode 100644 (file)
index 0000000..f279165
--- /dev/null
@@ -0,0 +1,10 @@
+Tests that listeners registered by InspectorDOMAgent::addEventListenersToNode are working.
+
+
+
+== Running test suite: DOM.didFireEvent
+-- Running test case: DOM.didFireEvent
+Adding video source "resources/white.mp4"...
+PASS: Should recieve a "loadstart" event.
+PASS: Event timestamp should be greater than 0.
+
diff --git a/LayoutTests/http/tests/inspector/dom/didFireEvent.html b/LayoutTests/http/tests/inspector/dom/didFireEvent.html
new file mode 100644 (file)
index 0000000..ab6b4ec
--- /dev/null
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="../resources/inspector-test.js"></script>
+<script>
+function loadSource(url, type) {
+    let sourceElement = document.createElement("source");
+    sourceElement.type = type;
+    sourceElement.src = url;
+
+    document.getElementById("video").appendChild(sourceElement);
+}
+
+function test()
+{
+    let suite = InspectorTest.createAsyncSuite("DOM.didFireEvent");
+
+    let videoNode = null;
+
+    suite.addTestCase({
+        name: "DOM.didFireEvent",
+        description: "Check that HTMLMediaElement events work.",
+        test(resolve, reject) {
+            const file = "white.mp4";
+
+            let listener = videoNode.addEventListener(WI.DOMNode.Event.DidFireEvent, (event) => {
+                let {domEvent} = event.data;
+                if (domEvent.eventName !== "loadstart")
+                    return;
+
+                InspectorTest.pass(`Should recieve a "loadstart" event.`)
+                InspectorTest.expectGreaterThan(domEvent.timestamp, 0, "Event timestamp should be greater than 0.");
+
+                videoNode.removeEventListener(WI.DOMNode.Event.DidFireEvent, listener);
+                resolve();
+            });
+
+            InspectorTest.log(`Adding video source "resources/${file}"...`);
+            InspectorTest.evaluateInPage(`loadSource("resources/${file}", "video/mp4")`);
+        }
+    });
+
+    WI.domManager.requestDocument((documentNode) => {
+        WI.domManager.querySelector(documentNode.id, "#video", (videoNodeId) => {
+            videoNode = WI.domManager.nodeForId(videoNodeId);
+            if (videoNode)
+                suite.runTestCasesAndFinish();
+            else {
+                InspectorTest.fail(`DOM node for "#video" not found.`);
+                InspectorTest.completeTest();
+            }
+        });
+    });
+}
+</script>
+</head>
+<body onload="runTest()">
+    <p>Tests that listeners registered by InspectorDOMAgent::addEventListenersToNode are working.</p>
+    <video id="video" muted autoplay></video>
+</body>
+</html>
index 9236063..0b24225 100644 (file)
@@ -74,6 +74,13 @@ PASS: shallowEqual of non-arrays should be false.
 PASS: lastValue of a nonempty array should be the last value.
 PASS: lastValue of an empty array should be undefined.
 
+-- Running test case: Array.prototype.adjacencies
+[] => []
+[1] => []
+[1,2] => [[1,2]]
+[1,2,3] => [[1,2],[2,3]]
+[1,2,3,4] => [[1,2],[2,3],[3,4]]
+
 -- Running test case: Array.prototype.remove
 PASS: remove should only remove the first matching value.
 PASS: remove should only remove values that strictly match.
index fa06b8c..ca9c58f 100644 (file)
@@ -164,6 +164,23 @@ function test()
     });
 
     suite.addTestCase({
+        name: "Array.prototype.adjacencies",
+        test() {
+            function logAdjacencies(array) {
+                InspectorTest.log(JSON.stringify(array) + " => " + JSON.stringify(Array.from(array.adjacencies())));
+            }
+
+            logAdjacencies([]);
+            logAdjacencies([1]);
+            logAdjacencies([1, 2]);
+            logAdjacencies([1, 2, 3]);
+            logAdjacencies([1, 2, 3, 4]);
+
+            return true;
+        }
+    });
+
+    suite.addTestCase({
         name: "Array.prototype.remove",
         test() {
             let arr1 = [1, 2, 3, 1];
index 5a942d1..1af9a33 100644 (file)
@@ -1,3 +1,15 @@
+2018-10-10  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: create special Network waterfall for media events
+        https://bugs.webkit.org/show_bug.cgi?id=189773
+        <rdar://problem/44626605>
+
+        Reviewed by Joseph Pecoraro.
+
+        * inspector/protocol/DOM.json:
+        Add `didFireEvent` event that is fired when specific event listeners added by
+        `InspectorInstrumentation::addEventListenersToNode` are fired.
+
 2018-10-10  Michael Saboff  <msaboff@apple.com>
 
         Increase executable memory pool from 64MB to 128MB for ARM64
index 810b3c6..19203c4 100644 (file)
             "parameters": [
                 { "name": "nodeId", "$ref": "NodeId" }
             ]
+        },
+        {
+            "name": "didFireEvent",
+            "description": "Called when an event is fired on a node.",
+            "parameters": [
+                { "name": "nodeId", "$ref": "NodeId" },
+                { "name": "eventName", "type": "string" },
+                { "name": "timestamp", "$ref": "Network.Timestamp", "description": "Time when the event was fired" }
+            ]
         }
     ]
 }
index 953cbe8..a87f4dd 100644 (file)
@@ -1,3 +1,31 @@
+2018-10-10  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: create special Network waterfall for media events
+        https://bugs.webkit.org/show_bug.cgi?id=189773
+        <rdar://problem/44626605>
+
+        Reviewed by Joseph Pecoraro.
+
+        Test: http/tests/inspector/dom/didFireEvent.html
+
+        * html/HTMLMediaElement.cpp:
+        (WebCore::HTMLMediaElement::HTMLMediaElement):
+
+        * inspector/InspectorInstrumentation.h:
+        (WebCore::InspectorInstrumentation::addEventListenersToNode): Added.
+        * inspector/InspectorInstrumentation.cpp:
+        (WebCore::InspectorInstrumentation::addEventListenersToNodeImpl): Added.
+
+        * inspector/agents/InspectorDOMAgent.h:
+        * inspector/agents/InspectorDOMAgent.cpp:
+        (WebCore::EventFiredCallback): Added.
+        (WebCore::EventFiredCallback::create): Added.
+        (WebCore::EventFiredCallback::operator==): Added.
+        (WebCore::EventFiredCallback::handleEvent): Added.
+        (WebCore::EventFiredCallback::EventFiredCallback): Added.
+        (WebCore::InspectorDOMAgent::didCreateFrontendAndBackend):
+        (WebCore::InspectorDOMAgent::addEventListenersToNode): Added.
+
 2018-10-10  Daniel Bates  <dabates@apple.com>
 
         [iOS] Cleanup -[WAKView _selfHandleEvent:] and -[WAKWindow sendEventSynchronously:]
index e743fa1..703bfa7 100644 (file)
@@ -53,6 +53,7 @@
 #include "HTMLParserIdioms.h"
 #include "HTMLSourceElement.h"
 #include "HTMLVideoElement.h"
+#include "InspectorInstrumentation.h"
 #include "JSDOMException.h"
 #include "JSDOMPromiseDeferred.h"
 #include "JSHTMLMediaElement.h"
@@ -522,6 +523,8 @@ HTMLMediaElement::HTMLMediaElement(const QualifiedName& tagName, Document& docum
     ALWAYS_LOG(LOGIDENTIFIER);
 
     setHasCustomStyleResolveCallbacks();
+
+    InspectorInstrumentation::addEventListenersToNode(*this);
 }
 
 void HTMLMediaElement::finishInitialization()
index 61a7c73..a09c44e 100644 (file)
@@ -135,6 +135,12 @@ int InspectorInstrumentation::identifierForNodeImpl(InstrumentingAgents& instrum
     return 0;
 }
 
+void InspectorInstrumentation::addEventListenersToNodeImpl(InstrumentingAgents& instrumentingAgents, Node& node)
+{
+    if (InspectorDOMAgent* domAgent = instrumentingAgents.inspectorDOMAgent())
+        domAgent->addEventListenersToNode(node);
+}
+
 void InspectorInstrumentation::willInsertDOMNodeImpl(InstrumentingAgents& instrumentingAgents, Node& parent)
 {
     if (InspectorDOMDebuggerAgent* domDebuggerAgent = instrumentingAgents.inspectorDOMDebuggerAgent())
index 926c97d..510d08f 100644 (file)
@@ -107,6 +107,7 @@ public:
     static bool isDebuggerPaused(Frame*);
 
     static int identifierForNode(Node&);
+    static void addEventListenersToNode(Node&);
     static void willInsertDOMNode(Document&, Node& parent);
     static void didInsertDOMNode(Document&, Node&);
     static void willRemoveDOMNode(Document&, Node&);
@@ -292,6 +293,7 @@ private:
     static bool isDebuggerPausedImpl(InstrumentingAgents&);
 
     static int identifierForNodeImpl(InstrumentingAgents&, Node&);
+    static void addEventListenersToNodeImpl(InstrumentingAgents&, Node&);
     static void willInsertDOMNodeImpl(InstrumentingAgents&, Node& parent);
     static void didInsertDOMNodeImpl(InstrumentingAgents&, Node&);
     static void willRemoveDOMNodeImpl(InstrumentingAgents&, Node&);
@@ -480,6 +482,13 @@ inline int InspectorInstrumentation::identifierForNode(Node& node)
     return 0;
 }
 
+inline void InspectorInstrumentation::addEventListenersToNode(Node& node)
+{
+    FAST_RETURN_IF_NO_FRONTENDS(void());
+    if (auto* instrumentingAgents = instrumentingAgentsForDocument(node.document()))
+        addEventListenersToNodeImpl(*instrumentingAgents, node);
+}
+
 inline void InspectorInstrumentation::willInsertDOMNode(Document& document, Node& parent)
 {
     FAST_RETURN_IF_NO_FRONTENDS(void());
index ec1aec8..10a97b8 100644 (file)
@@ -61,6 +61,7 @@
 #include "FrameTree.h"
 #include "HTMLElement.h"
 #include "HTMLFrameOwnerElement.h"
+#include "HTMLMediaElement.h"
 #include "HTMLNames.h"
 #include "HTMLParserIdioms.h"
 #include "HTMLScriptElement.h"
@@ -219,6 +220,42 @@ private:
     RefPtr<Node> m_node;
 };
 
+class EventFiredCallback final : public EventListener {
+public:
+    static Ref<EventFiredCallback> create(InspectorDOMAgent& domAgent)
+    {
+        return adoptRef(*new EventFiredCallback(domAgent));
+    }
+
+    bool operator==(const EventListener& other) const final
+    {
+        return this == &other;
+    }
+
+    void handleEvent(ScriptExecutionContext&, Event& event) final
+    {
+        if (!is<Node>(event.target()))
+            return;
+
+        auto* node = downcast<Node>(event.target());
+        int nodeId = m_domAgent.pushNodePathToFrontend(node);
+        if (!nodeId)
+            return;
+
+        auto timestamp = m_domAgent.m_environment.executionStopwatch()->elapsedTime().seconds();
+        m_domAgent.m_frontendDispatcher->didFireEvent(nodeId, event.type(), timestamp);
+    }
+
+private:
+    EventFiredCallback(InspectorDOMAgent& domAgent)
+        : EventListener(EventListener::CPPEventListenerType)
+        , m_domAgent(domAgent)
+    {
+    }
+
+    InspectorDOMAgent& m_domAgent;
+};
+
 String InspectorDOMAgent::toErrorString(ExceptionCode ec)
 {
     return ec ? String(DOMException::name(ec)) : emptyString();
@@ -253,6 +290,9 @@ void InspectorDOMAgent::didCreateFrontendAndBackend(Inspector::FrontendRouter*,
     m_instrumentingAgents.setInspectorDOMAgent(this);
     m_document = m_pageAgent->mainFrame().document();
 
+    for (auto* mediaElement : HTMLMediaElement::allMediaElements())
+        addEventListenersToNode(*mediaElement);
+
     if (m_nodeToFocus)
         focusNode();
 }
@@ -2090,6 +2130,39 @@ int InspectorDOMAgent::identifierForNode(Node& node)
     return pushNodePathToFrontend(&node);
 }
 
+void InspectorDOMAgent::addEventListenersToNode(Node& node)
+{
+    auto callback = EventFiredCallback::create(*this);
+
+    auto createEventListener = [&] (const AtomicString& eventName) {
+        node.addEventListener(eventName, callback.copyRef(), false);
+    };
+
+    if (is<HTMLMediaElement>(node)) {
+        createEventListener(eventNames().abortEvent);
+        createEventListener(eventNames().canplayEvent);
+        createEventListener(eventNames().canplaythroughEvent);
+        createEventListener(eventNames().durationchangeEvent);
+        createEventListener(eventNames().emptiedEvent);
+        createEventListener(eventNames().endedEvent);
+        createEventListener(eventNames().errorEvent);
+        createEventListener(eventNames().loadeddataEvent);
+        createEventListener(eventNames().loadedmetadataEvent);
+        createEventListener(eventNames().loadstartEvent);
+        createEventListener(eventNames().pauseEvent);
+        createEventListener(eventNames().playEvent);
+        createEventListener(eventNames().playingEvent);
+        createEventListener(eventNames().ratechangeEvent);
+        createEventListener(eventNames().seekedEvent);
+        createEventListener(eventNames().seekingEvent);
+        createEventListener(eventNames().stalledEvent);
+        createEventListener(eventNames().suspendEvent);
+        createEventListener(eventNames().timeupdateEvent);
+        createEventListener(eventNames().volumechangeEvent);
+        createEventListener(eventNames().waitingEvent);
+    }
+}
+
 void InspectorDOMAgent::didInsertDOMNode(Node& node)
 {
     if (containsOnlyHTMLWhitespace(&node))
index cd1f5a1..6d500df 100644 (file)
@@ -158,6 +158,7 @@ public:
 
     // InspectorInstrumentation
     int identifierForNode(Node&);
+    void addEventListenersToNode(Node&);
     void didInsertDOMNode(Node&);
     void didRemoveDOMNode(Node&);
     void willModifyDOMAttr(Element&, const AtomicString& oldValue, const AtomicString& newValue);
@@ -316,6 +317,8 @@ private:
         }
     };
 
+    friend class EventFiredCallback;
+
     HashMap<int, InspectorEventListener> m_eventListenerEntries;
     int m_lastEventListenerId { 1 };
 };
index 2027f42..c6718a7 100644 (file)
@@ -1,5 +1,163 @@
 2018-10-10  Devin Rousso  <drousso@apple.com>
 
+        Web Inspector: create special Network waterfall for media events
+        https://bugs.webkit.org/show_bug.cgi?id=189773
+        <rdar://problem/44626605>
+
+        Reviewed by Joseph Pecoraro.
+
+        * Localizations/en.lproj/localizedStrings.js:
+        * UserInterface/Main.html:
+        * UserInterface/Base/Utilities.js:
+
+        * UserInterface/Protocol/DOMObserver.js:
+        (WI.DOMObserver.prototype.didFireEvent): Added.
+        * UserInterface/Controllers/DOMManager.js:
+        (WI.DOMManager.prototype.didFireEvent): Added.
+        * UserInterface/Models/DOMNode.js:
+        (WI.DOMNode):
+        (WI.DOMNode.prototype.get domEvents): Added.
+        (WI.DOMNode.prototype.didFireEvent): Added.
+        (WI.DOMNode.prototype._addDOMEvent): Added.
+
+        * UserInterface/Views/NetworkTableContentView.js:
+        (WI.NetworkTableContentView):
+        (WI.NetworkTableContentView.prototype.shown):
+        (WI.NetworkTableContentView.prototype.hidden):
+        (WI.NetworkTableContentView.prototype.closed):
+        (WI.NetworkTableContentView.prototype.reset):
+        (WI.NetworkTableContentView.prototype.showRepresentedObject):
+        (WI.NetworkTableContentView.prototype.networkDetailViewClose): Added.
+        (WI.NetworkTableContentView.prototype.tableSortChanged):
+        (WI.NetworkTableContentView.prototype.tableSelectionDidChange):
+        (WI.NetworkTableContentView.prototype._populateNameCell):
+        (WI.NetworkTableContentView.prototype._populateWaterfallGraph.positionByStartOffset): Added.
+        (WI.NetworkTableContentView.prototype._populateWaterfallGraph.setWidthForDuration): Added.
+        (WI.NetworkTableContentView.prototype._populateWaterfallGraph.createDOMEventLine): Added.
+        (WI.NetworkTableContentView.prototype._populateWaterfallGraph.appendBlock):
+        (WI.NetworkTableContentView.prototype._populateWaterfallGraph):
+        (WI.NetworkTableContentView.prototype._processPendingEntries):
+        (WI.NetworkTableContentView.prototype._rowIndexForRepresentedObject): Added.
+        (WI.NetworkTableContentView.prototype._updateEntryForResource):
+        (WI.NetworkTableContentView.prototype._hideDetailView): Added.
+        (WI.NetworkTableContentView.prototype._showDetailView): Added.
+        (WI.NetworkTableContentView.prototype._positionDetailView): Added.
+        (WI.NetworkTableContentView.prototype._resourceTransferSizeDidChange):
+        (WI.NetworkTableContentView.prototype._tryLinkResourceToDOMNode):
+        (WI.NetworkTableContentView.prototype._handleNodeDidFireEvent): Added.
+        (WI.NetworkTableContentView.prototype._updateFilteredEntries):
+        (WI.NetworkTableContentView.prototype._typeFilterScopeBarSelectionChanged):
+        (WI.NetworkTableContentView.prototype._urlFilterDidChange):
+        (WI.NetworkTableContentView.prototype._restoreSelectedRow):
+        (WI.NetworkTableContentView.prototype._waterfallPopoverContent): Added.
+        (WI.NetworkTableContentView.prototype._waterfallPopoverContentForResourceEntry): Added.
+        (WI.NetworkTableContentView.prototype._waterfallPopoverContentForNodeEntry): Added.
+        (WI.NetworkTableContentView.prototype._handleResourceEntryMousedownWaterfall): Added.
+        (WI.NetworkTableContentView.prototype._handleNodeEntryMousedownWaterfall): Added.
+        (WI.NetworkTableContentView.prototype._handleMousedownWaterfall): Added.
+        (WI.NetworkTableContentView.prototype.networkResourceDetailViewClose): Deleted.
+        (WI.NetworkTableContentView.prototype._rowIndexForResource): Deleted.
+        (WI.NetworkTableContentView.prototype._hideResourceDetailView): Deleted.
+        (WI.NetworkTableContentView.prototype._showResourceDetailView): Deleted.
+        (WI.NetworkTableContentView.prototype._waterfallPopoverContentForResource): Deleted.
+        * UserInterface/Views/NetworkTableContentView.css:
+        (.content-view.network .network-table): Added.
+        (.network-table :not(.header) .cell.waterfall .waterfall-container > .dom-event): Added.
+        (.network-table :not(.header) .cell.waterfall .waterfall-container > .dom-activity): Added.
+        (.network-table :not(.header) .cell.waterfall .waterfall-container > .dom-activity.playing): Added.
+
+        * UserInterface/Views/NetworkDOMNodeDetailView.js: Added.
+        (WI.NetworkDOMNodeDetailView):
+        (WI.NetworkDOMNodeDetailView.prototype.initialLayout):
+        (WI.NetworkDOMNodeDetailView.prototype.showContentViewForIdentifier):
+
+        * UserInterface/Views/NetworkResourceDetailView.css:
+        (.content-view.resource-details):
+        (.network-resource-detail): Deleted.
+        (.network-resource-detail .navigation-bar): Deleted.
+        (.network-resource-detail .item.close > .glyph): Deleted.
+        (.network-resource-detail .item.close > .glyph:hover): Deleted.
+        (.network-resource-detail .item.close > .glyph:active): Deleted.
+        (.network .network-resource-detail .navigation-bar .item.radio.button.text-only): Deleted.
+        (.network .network-resource-detail .navigation-bar .item.radio.button.text-only.selected): Deleted.
+        (.network-resource-detail > .content-browser): Deleted.
+        (@media (prefers-dark-interface)): Deleted.
+        * UserInterface/Views/NetworkResourceDetailView.js:
+        (WI.NetworkResourceDetailView):
+        (WI.NetworkResourceDetailView.prototype.shown):
+        (WI.NetworkResourceDetailView.prototype.headersContentViewGoToRequestData):
+        (WI.NetworkResourceDetailView.prototype.sizesContentViewGoToHeaders):
+        (WI.NetworkResourceDetailView.prototype.sizesContentViewGoToRequestBody):
+        (WI.NetworkResourceDetailView.prototype.sizesContentViewGoToResponseBody):
+        (WI.NetworkResourceDetailView.prototype.initialLayout):
+        (WI.NetworkResourceDetailView.prototype.showContentViewForIdentifier):
+        (WI.NetworkResourceDetailView.prototype.get resource): Deleted.
+        (WI.NetworkResourceDetailView.prototype.hidden): Deleted.
+        (WI.NetworkResourceDetailView.prototype.dispose): Deleted.
+        (WI.NetworkResourceDetailView.prototype.willShowWithCookie): Deleted.
+        (WI.NetworkResourceDetailView.prototype.initialLayout): Deleted.
+        (WI.NetworkResourceDetailView.prototype._showPreferredContentView): Deleted.
+        (WI.NetworkResourceDetailView.prototype._showContentViewForNavigationItem): Deleted.
+        (WI.NetworkResourceDetailView.prototype._navigationItemSelected): Deleted.
+        (WI.NetworkResourceDetailView.prototype._handleCloseButton): Deleted.
+
+        * UserInterface/Views/NetworkDetailView.js: Added.
+        (WI.NetworkDetailView):
+        (WI.NetworkDetailView.prototype.get representedObject):
+        (WI.NetworkDetailView.prototype.shown):
+        (WI.NetworkDetailView.prototype.hidden):
+        (WI.NetworkDetailView.prototype.dispose):
+        (WI.NetworkDetailView.prototype.willShowWithCookie):
+        (WI.NetworkDetailView.prototype.initialLayout):
+        (WI.NetworkDetailView.prototype.createDetailNavigationItem):
+        (WI.NetworkDetailView.prototype.detailNavigationItemForIdentifier):
+        (WI.NetworkDetailView.prototype.showContentViewForIdentifier):
+        (WI.NetworkDetailView.prototype._showPreferredContentView):
+        (WI.NetworkDetailView.prototype._navigationItemSelected):
+        (WI.NetworkDetailView.prototype._handleCloseButton):
+        * UserInterface/Views/NetworkDetailView.css: Added.
+        (.network-detail):
+        (.network-detail .navigation-bar):
+        (.network-detail .item.close > .glyph):
+        (.network-detail .item.close > .glyph:hover):
+        (.network-detail .item.close > .glyph:active):
+        (.network .network-detail .navigation-bar .item.radio.button.text-only):
+        (.network .network-detail .navigation-bar .item.radio.button.text-only.selected):
+        (.network-detail > .content-browser):
+        (@media (prefers-dark-interface)):
+        Create base class for detail views shown in the Network tab.
+
+        * UserInterface/Views/DOMNodeEventsContentView.js: Added.
+        (WI.DOMNodeEventsContentView):
+        (WI.DOMNodeEventsContentView.prototype.initialLayout):
+        (WI.DOMNodeEventsContentView.prototype.closed):
+        (WI.DOMNodeEventsContentView.prototype._handleDOMNodeDidFireEvent):
+        * UserInterface/Views/DOMNodeEventsContentView.css: Added.
+        (.dom-node-details.dom-events):
+
+        * UserInterface/Views/DOMEventsBreakdownView.js: Added.
+        (WI.DOMEventsBreakdownView):
+        (WI.DOMEventsBreakdownView.prototype.addEvent):
+        (WI.DOMEventsBreakdownView.prototype.initialLayout):
+        (WI.DOMEventsBreakdownView.prototype._populateTable.percentOfTotalTime):
+        (WI.DOMEventsBreakdownView.prototype._populateTable):
+        * UserInterface/Views/DOMEventsBreakdownView.css: Added.
+        (.waterfall-popover-content .dom-events-breakdown):
+        (.dom-events-breakdown):
+        (.dom-events-breakdown table):
+        (.dom-events-breakdown tr > :matches(th, td)):
+        (.dom-events-breakdown tbody > tr):
+        (.dom-events-breakdown .graph):
+        (.dom-events-breakdown .graph > :matches(.point, .area)):
+        (.dom-events-breakdown .graph > .point):
+        (.dom-events-breakdown .time):
+
+        * UserInterface/Views/ResourceTimingBreakdownView.css:
+        (.resource-timing-breakdown > table > tr.header:not(.total-row) > td): Added.
+        (.popover.waterfall-popover): Deleted.
+
+2018-10-10  Devin Rousso  <drousso@apple.com>
+
         Web Inspector: REGRESSION(r236853): Uncaught Exception: undefined is not an object (evaluating 'entry.resource')
         https://bugs.webkit.org/show_bug.cgi?id=190442
 
index 85f1985..45c6b27 100644 (file)
@@ -243,6 +243,7 @@ localizedStrings["Custom"] = "Custom";
 localizedStrings["DNS"] = "DNS";
 localizedStrings["DOM Content Loaded \u2014 %s"] = "DOM Content Loaded \u2014 %s";
 localizedStrings["DOM Event"] = "DOM Event";
+localizedStrings["DOM Events"] = "DOM Events";
 localizedStrings["Damping"] = "Damping";
 localizedStrings["Data"] = "Data";
 localizedStrings["Data returned from the database is too large."] = "Data returned from the database is too large.";
index 6380181..aa05d37 100644 (file)
@@ -464,6 +464,14 @@ Object.defineProperty(Array.prototype, "lastValue",
     }
 });
 
+Object.defineProperty(Array.prototype, "adjacencies",
+{
+    value: function*() {
+        for (let i = 1; i < this.length; ++i)
+            yield [this[i - 1], this[i]];
+    }
+});
+
 Object.defineProperty(Array.prototype, "remove",
 {
     value(value)
index 0488da6..a3c58e2 100644 (file)
@@ -122,6 +122,17 @@ WI.DOMManager = class DOMManager extends WI.Object
         node.dispatchEventToListeners(WI.DOMNode.Event.EventListenersChanged);
     }
 
+    didFireEvent(nodeId, eventName, timestamp)
+    {
+        // Called from WI.DOMObserver.
+
+        let node = this._idToDOMNode[nodeId];
+        if (!node)
+            return;
+
+        node.didFireEvent(eventName, timestamp);
+    }
+
     // Private
 
     _wrapClientCallback(callback)
index 8e8a89d..d4c8b85 100644 (file)
@@ -66,7 +66,9 @@
     <link rel="stylesheet" href="Views/ContentViewContainer.css">
     <link rel="stylesheet" href="Views/ControlToolbarItem.css">
     <link rel="stylesheet" href="Views/CookieStorageContentView.css">
+    <link rel="stylesheet" href="Views/DOMEventsBreakdownView.css">
     <link rel="stylesheet" href="Views/DOMNodeDetailsSidebarPanel.css">
+    <link rel="stylesheet" href="Views/DOMNodeEventsContentView.css">
     <link rel="stylesheet" href="Views/DOMNodeTreeElement.css">
     <link rel="stylesheet" href="Views/DOMStorageContentView.css">
     <link rel="stylesheet" href="Views/DOMTreeContentView.css">
     <link rel="stylesheet" href="Views/Main.css">
     <link rel="stylesheet" href="Views/NavigationBar.css">
     <link rel="stylesheet" href="Views/NavigationSidebarPanel.css">
+    <link rel="stylesheet" href="Views/NetworkDetailView.css">
     <link rel="stylesheet" href="Views/NetworkResourceDetailView.css">
     <link rel="stylesheet" href="Views/NetworkTabContentView.css">
     <link rel="stylesheet" href="Views/NetworkTableContentView.css">
     <script src="Views/DOMDetailsSidebarPanel.js"></script>
     <script src="Views/FolderTreeElement.js"></script>
     <script src="Views/FolderizedTreeElement.js"></script>
+    <script src="Views/NetworkDetailView.js"></script>
     <script src="Views/NetworkTabContentView.js"></script>
     <script src="Views/NewTabContentView.js"></script>
     <script src="Views/ObjectTreeBaseTreeElement.js"></script>
     <script src="Views/CookieStorageContentView.js"></script>
     <script src="Views/CookieStorageTreeElement.js"></script>
     <script src="Views/DOMBreakpointTreeElement.js"></script>
+    <script src="Views/DOMEventsBreakdownView.js"></script>
     <script src="Views/DOMNodeDetailsSidebarPanel.js"></script>
+    <script src="Views/DOMNodeEventsContentView.js"></script>
     <script src="Views/DOMNodeTreeElement.js"></script>
     <script src="Views/DOMStorageContentView.js"></script>
     <script src="Views/DOMStorageTreeElement.js"></script>
     <script src="Views/MemoryTimelineView.js"></script>
     <script src="Views/MultipleScopeBarItem.js"></script>
     <script src="Views/NavigationBar.js"></script>
+    <script src="Views/NetworkDOMNodeDetailView.js"></script>
     <script src="Views/NetworkResourceDetailView.js"></script>
     <script src="Views/NetworkTableContentView.js"></script>
     <script src="Views/NetworkTimelineOverviewGraph.js"></script>
index 6c9b606..6bbdbac 100644 (file)
@@ -137,10 +137,14 @@ WI.DOMNode = class DOMNode extends WI.Object
             this.name = payload.name;
             this.value = payload.value;
         }
+
+        this._domEvents = [];
     }
 
     // Public
 
+    get domEvents() { return this._domEvents; }
+
     get frameIdentifier()
     {
         return this._frameIdentifier || this.ownerDocument.frameIdentifier;
@@ -697,6 +701,23 @@ WI.DOMNode = class DOMNode extends WI.Object
         return !!this.ownerSVGElement;
     }
 
+    didFireEvent(eventName, timestamp)
+    {
+        // Called from WI.DOMManager.
+
+        this._addDOMEvent({
+            eventName,
+            timestamp: WI.timelineManager.computeElapsedTime(timestamp),
+        });
+    }
+
+    _addDOMEvent(domEvent)
+    {
+        this._domEvents.push(domEvent);
+
+        this.dispatchEventToListeners(WI.DOMNode.Event.DidFireEvent, {domEvent});
+    }
+
     _setAttributesPayload(attrs)
     {
         this._attributes = [];
@@ -844,6 +865,7 @@ WI.DOMNode.Event = {
     AttributeModified: "dom-node-attribute-modified",
     AttributeRemoved: "dom-node-attribute-removed",
     EventListenersChanged: "dom-node-event-listeners-changed",
+    DidFireEvent: "dom-node-did-fire-event",
 };
 
 WI.DOMNode.PseudoElementType = {
index e99370a..b179231 100644 (file)
@@ -111,4 +111,9 @@ WI.DOMObserver = class DOMObserver
     {
         WI.domManager.willRemoveEventListener(nodeId);
     }
+
+    didFireEvent(nodeId, eventName, timestamp)
+    {
+        WI.domManager.didFireEvent(nodeId, eventName, timestamp);
+    }
 };
diff --git a/Source/WebInspectorUI/UserInterface/Views/DOMEventsBreakdownView.css b/Source/WebInspectorUI/UserInterface/Views/DOMEventsBreakdownView.css
new file mode 100644 (file)
index 0000000..101ef5a
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+
+.waterfall-popover-content .dom-events-breakdown {
+    -webkit-user-select: text;
+    max-height: 210px;
+    overflow-y: scroll;
+}
+
+.dom-events-breakdown {
+    margin: 5px;
+}
+
+.dom-events-breakdown table {
+    width: 100%;
+    border-collapse: collapse;
+}
+
+.dom-events-breakdown tr > :matches(th, td) {
+    padding: 2px 4px;
+    text-align: start;
+}
+
+.dom-events-breakdown tbody > tr {
+    /* FIXME: <https://webkit.org/b/94128> */
+    border-top: 1px solid lightgrey;
+}
+
+.dom-events-breakdown .graph {
+    position: relative;
+    width: 100%;
+    border-right: var(--point-size) solid transparent;
+    border-left: var(--point-size) solid transparent;
+
+    --point-size: 8px;
+}
+
+.dom-events-breakdown .graph > :matches(.point, .area) {
+    position: absolute;
+}
+
+.dom-events-breakdown .graph > .point {
+    top: calc(50% - (var(--point-size) / 2));
+    width: var(--point-size);
+    height: var(--point-size);
+    background-color: var(--selected-background-color);
+    border-radius: 50%;
+}
+
+.dom-events-breakdown .time {
+    text-align: end;
+}
diff --git a/Source/WebInspectorUI/UserInterface/Views/DOMEventsBreakdownView.js b/Source/WebInspectorUI/UserInterface/Views/DOMEventsBreakdownView.js
new file mode 100644 (file)
index 0000000..513a95f
--- /dev/null
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2018 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.DOMEventsBreakdownView = class DOMEventsBreakdownView extends WI.View
+{
+    constructor(domEvents, {includeGraph, startTimestamp} = {})
+    {
+        super();
+
+        this._domEvents = domEvents;
+        this._includeGraph = includeGraph || false;
+        this._startTimestamp = startTimestamp || 0;
+
+        this._tableBodyElement = null;
+
+        this.element.classList.add("dom-events-breakdown");
+    }
+
+    // Public
+
+    addEvent(domEvent)
+    {
+        this._domEvents.push(domEvent);
+
+        this.soon._populateTable();
+    }
+
+    // Protected
+
+    initialLayout()
+    {
+        super.initialLayout();
+
+        let tableElement = this.element.appendChild(document.createElement("table"));
+
+        let headElement = tableElement.appendChild(document.createElement("thead"));
+
+        let headRowElement = headElement.appendChild(document.createElement("tr"));
+
+        let eventHeadCell = headRowElement.appendChild(document.createElement("th"));
+        eventHeadCell.textContent = WI.UIString("Event");
+
+        if (this._includeGraph)
+            headRowElement.appendChild(document.createElement("th"));
+
+        let timeHeadCell = headRowElement.appendChild(document.createElement("th"));
+        timeHeadCell.classList.add("time");
+        timeHeadCell.textContent = WI.UIString("Time");
+
+        this._tableBodyElement = tableElement.appendChild(document.createElement("tbody"));
+
+        this._populateTable();
+    }
+
+    // Private
+
+    _populateTable()
+    {
+        this._tableBodyElement.removeChildren();
+
+        let startTimestamp = this._domEvents[0].timestamp;
+        let endTimestamp = this._domEvents.lastValue.timestamp;
+        let totalTime = endTimestamp - startTimestamp;
+        let styleAttribute = WI.resolvedLayoutDirection() === WI.LayoutDirection.LTR ? "left" : "right";
+
+        function percentOfTotalTime(time) {
+            return time / totalTime * 100;
+        }
+
+        for (let domEvent of this._domEvents) {
+            let rowElement = this._tableBodyElement.appendChild(document.createElement("tr"));
+
+            let nameCell = rowElement.appendChild(document.createElement("td"));
+            nameCell.classList.add("name");
+            nameCell.textContent = domEvent.eventName;
+
+            if (this._includeGraph) {
+                let graphCell = rowElement.appendChild(document.createElement("td"));
+                graphCell.classList.add("graph");
+
+                let graphPoint = graphCell.appendChild(document.createElement("div"));
+                graphPoint.classList.add("point");
+                graphPoint.style.setProperty(styleAttribute, `calc(${percentOfTotalTime(domEvent.timestamp - startTimestamp)}% - (var(--point-size) / 2))`);
+            }
+
+            let timeCell = rowElement.appendChild(document.createElement("td"));
+            timeCell.classList.add("time");
+
+            const higherResolution = true;
+            timeCell.textContent = Number.secondsToString(domEvent.timestamp - this._startTimestamp, higherResolution);
+        }
+    }
+};
diff --git a/Source/WebInspectorUI/UserInterface/Views/DOMNodeEventsContentView.css b/Source/WebInspectorUI/UserInterface/Views/DOMNodeEventsContentView.css
new file mode 100644 (file)
index 0000000..c4fac40
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+
+.dom-node-details.dom-events {
+    overflow-y: scroll;
+}
diff --git a/Source/WebInspectorUI/UserInterface/Views/DOMNodeEventsContentView.js b/Source/WebInspectorUI/UserInterface/Views/DOMNodeEventsContentView.js
new file mode 100644 (file)
index 0000000..336a276
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2018 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.DOMNodeEventsContentView = class DOMNodeEventsContentView extends WI.ContentView
+{
+    constructor(domNode, {startTimestamp} = {})
+    {
+        console.assert(domNode instanceof WI.DOMNode);
+
+        const representedObject = null;
+        super(representedObject);
+
+        this._domNode = domNode;
+        this._startTimestamp = startTimestamp || 0;
+
+        this.element.classList.add("dom-node-details", "dom-events");
+
+        this._breakdownView = null;
+    }
+
+    // Protected
+
+    initialLayout()
+    {
+        super.initialLayout();
+
+        this._breakdownView = new WI.DOMEventsBreakdownView(this._domNode.domEvents.slice(), {
+            includeGraph: true,
+            startTimestamp: this._startTimestamp,
+        });
+        this.addSubview(this._breakdownView);
+
+        this._domNode.addEventListener(WI.DOMNode.Event.DidFireEvent, this._handleDOMNodeDidFireEvent, this);
+    }
+
+    closed()
+    {
+        this._domNode.removeEventListener(null, null, this);
+
+        super.closed();
+    }
+
+    // Private
+
+    _handleDOMNodeDidFireEvent(event)
+    {
+        let {domEvent} = event.data;
+
+        if (this._breakdownView)
+            this._breakdownView.addEvent(domEvent);
+    }
+};
diff --git a/Source/WebInspectorUI/UserInterface/Views/NetworkDOMNodeDetailView.js b/Source/WebInspectorUI/UserInterface/Views/NetworkDOMNodeDetailView.js
new file mode 100644 (file)
index 0000000..0a24bfb
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2018 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.NetworkDOMNodeDetailView = class NetworkDOMNodeDetailView extends WI.NetworkDetailView
+{
+    constructor(domNode, delegate, {startTimestamp} = {})
+    {
+        console.assert(domNode instanceof WI.DOMNode);
+
+        super(domNode, delegate);
+
+        this._startTimestamp = startTimestamp || 0;
+
+        this.element.classList.add("dom-node");
+
+        this._domEventsContentView = null;
+    }
+
+    // Protected
+
+    initialLayout()
+    {
+        this.createDetailNavigationItem("dom-events", WI.UIString("DOM Events"));
+
+        super.initialLayout();
+    }
+
+    // Private
+
+    showContentViewForIdentifier(identifier)
+    {
+        super.showContentViewForIdentifier(identifier);
+
+        switch (identifier) {
+        case "dom-events":
+            if (!this._domEventsContentView) {
+                this._domEventsContentView = new WI.DOMNodeEventsContentView(this.representedObject, {
+                    startTimestamp: this._startTimestamp,
+                });
+            }
+            this._contentBrowser.showContentView(this._domEventsContentView, this._contentViewCookie);
+            break;
+        }
+    }
+};
diff --git a/Source/WebInspectorUI/UserInterface/Views/NetworkDetailView.css b/Source/WebInspectorUI/UserInterface/Views/NetworkDetailView.css
new file mode 100644 (file)
index 0000000..a094c11
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+
+.network-detail {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    /* left or right set by NetworkTableView on display / resize */
+    z-index: 10;
+    background-color: white;
+}
+
+.network-detail .navigation-bar {
+    position: -webkit-sticky;
+    top: 0;
+    z-index: 1;
+}
+
+.network-detail .item.close > .glyph {
+    border-radius: 2px;
+    padding: 2px;
+    background: white;
+}
+
+.network-detail .item.close > .glyph:hover {
+    background-color: var(--button-background-color-hover);
+}
+
+.network-detail .item.close > .glyph:active {
+    background-color: var(--button-background-color-pressed);
+}
+
+.network .network-detail .navigation-bar .item.radio.button.text-only {
+    color: inherit;
+    background-color: inherit;
+}
+
+.network .network-detail .navigation-bar .item.radio.button.text-only.selected {
+    color: var(--selected-background-color);
+    background-color: white;
+}
+
+.network-detail > .content-browser {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+}
+
+@media (prefers-dark-interface) {
+    .network-detail {
+        background-color: var(--background-color);
+    }
+
+    .network-detail .item.close > .glyph {
+        background-color: hsla(0, 0%, 100%, 0.2);
+    }
+
+    .network .network-detail .navigation-bar .item.radio.button.text-only.selected {
+        background-color: unset;
+        color: var(--glyph-color-active);
+    }
+}
diff --git a/Source/WebInspectorUI/UserInterface/Views/NetworkDetailView.js b/Source/WebInspectorUI/UserInterface/Views/NetworkDetailView.js
new file mode 100644 (file)
index 0000000..550f834
--- /dev/null
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2018 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.NetworkDetailView = class NetworkDetailView extends WI.View
+{
+    constructor(representedObject, delegate)
+    {
+        super();
+
+        this._representedObject = representedObject;
+        this._delegate = delegate || null;
+
+        this.element.classList.add("network-detail");
+
+        this._contentBrowser = null;
+
+        this._detailNavigationItemMap = new Map;
+
+        this._contentViewCookie = null;
+    }
+
+    // Public
+
+    get representedObject() { return this._representedObject; }
+
+    shown()
+    {
+        if (!this._contentBrowser)
+            return;
+
+        this._showPreferredContentView();
+
+        if (this._contentViewCookie) {
+            this._contentBrowser.showContentView(this._contentBrowser.currentContentView, this._contentViewCookie);
+            this._contentViewCookie = null;
+        }
+
+        this._contentBrowser.shown();
+    }
+
+    hidden()
+    {
+        this._contentBrowser.hidden();
+    }
+
+    dispose()
+    {
+        this._delegate = null;
+
+        this._contentBrowser.contentViewContainer.closeAllContentViews();
+    }
+
+    willShowWithCookie(cookie)
+    {
+        this._contentViewCookie = cookie;
+    }
+
+    // Protected
+
+    initialLayout()
+    {
+        let closeNavigationItem = new WI.ButtonNavigationItem("close", WI.UIString("Close detail view"), "Images/CloseLarge.svg", 16, 16);
+        closeNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleCloseButton.bind(this));
+        closeNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.High;
+
+        let contentViewNavigationItemsGroup = new WI.GroupNavigationItem;
+        let contentViewNavigationItemsFlexItem = new WI.FlexibleSpaceNavigationItem(contentViewNavigationItemsGroup, WI.FlexibleSpaceNavigationItem.Align.End);
+        contentViewNavigationItemsFlexItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
+
+        const element = null;
+        const disableBackForward = true;
+        const disableFindBanner = false;
+        this._contentBrowser = new WI.ContentBrowser(element, this, disableBackForward, disableFindBanner, contentViewNavigationItemsFlexItem, contentViewNavigationItemsGroup);
+
+        // Insert all of our custom navigation items at the start of the ContentBrowser's NavigationBar.
+        let index = 0;
+        this._contentBrowser.navigationBar.insertNavigationItem(closeNavigationItem, index++);
+        this._contentBrowser.navigationBar.insertNavigationItem(new WI.FlexibleSpaceNavigationItem, index++);
+        for (let detailNavigationItem of this._detailNavigationItemMap.values())
+            this._contentBrowser.navigationBar.insertNavigationItem(detailNavigationItem, index++);
+
+        this._contentBrowser.navigationBar.addEventListener(WI.NavigationBar.Event.NavigationItemSelected, this._navigationItemSelected, this);
+
+        this.addSubview(this._contentBrowser);
+
+        this._showPreferredContentView();
+    }
+
+    createDetailNavigationItem(identifier, toolTip)
+    {
+        this._detailNavigationItemMap.set(identifier, new WI.RadioButtonNavigationItem(identifier, toolTip));
+    }
+
+    detailNavigationItemForIdentifier(identifier)
+    {
+        return this._detailNavigationItemMap.get(identifier);
+    }
+
+    showContentViewForIdentifier(identifier)
+    {
+        // Implemented by subclasses.
+    }
+
+    // Private
+
+    _showPreferredContentView()
+    {
+        let detailNavigationItems = Array.from(this._detailNavigationItemMap.values());
+
+        // Restore the preferred navigation item.
+        let firstNavigationItem = null;
+        let defaultIdentifier = WI.settings.selectedNetworkDetailContentViewIdentifier.value;
+        for (let navigationItem of this._contentBrowser.navigationBar.navigationItems) {
+            if (!(navigationItem instanceof WI.RadioButtonNavigationItem))
+                continue;
+
+            if (!detailNavigationItems.includes(navigationItem))
+                continue;
+
+            if (!firstNavigationItem)
+                firstNavigationItem = navigationItem;
+
+            if (navigationItem.identifier === defaultIdentifier) {
+                this._contentBrowser.navigationBar.selectedNavigationItem = navigationItem;
+                return;
+            }
+        }
+
+        console.assert(firstNavigationItem, "Should have found at least one navigation item above");
+        this._contentBrowser.navigationBar.selectedNavigationItem = firstNavigationItem;
+    }
+
+    _navigationItemSelected(event)
+    {
+        let navigationItem = event.target.selectedNavigationItem;
+        if (!(navigationItem instanceof WI.RadioButtonNavigationItem))
+            return;
+
+        this.showContentViewForIdentifier(navigationItem.identifier);
+
+        console.assert(navigationItem.identifier);
+        WI.settings.selectedNetworkDetailContentViewIdentifier.value = navigationItem.identifier;
+    }
+
+    _handleCloseButton(event)
+    {
+        if (this._delegate && this._delegate.networkDetailViewClose)
+            this._delegate.networkDetailViewClose(this);
+    }
+};
index e55e34d..47c1fc7 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 Apple Inc. All rights reserved.
+ * Copyright (C) 2017-2018 Apple Inc. All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
  * THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-.network-resource-detail {
-    position: absolute;
-    top: 0;
-    left: 0;
-    right: 0;
-    bottom: 0;
-    /* left or right set by NetworkTableView on display / resize */
-    z-index: 10;
-    background-color: var(--background-color);
-}
-
-.network-resource-detail .navigation-bar {
-    position: -webkit-sticky;
-    top: 0;
-    z-index: 1;
-}
-
-.network-resource-detail .item.close > .glyph {
-    border-radius: 2px;
-    padding: 2px;
-    background: white;
-}
-
-.network-resource-detail .item.close > .glyph:hover {
-    background-color: var(--button-background-color-hover);
-}
-
-.network-resource-detail .item.close > .glyph:active {
-    background-color: var(--button-background-color-pressed);
-}
-
-.network .network-resource-detail .navigation-bar .item.radio.button.text-only {
-    color: inherit;
-    background-color: inherit;
-}
-
-.network .network-resource-detail .navigation-bar .item.radio.button.text-only.selected {
-    color: var(--selected-background-color);
-    background-color: var(--background-color);
-}
-
-.network-resource-detail > .content-browser {
-    position: absolute;
-    top: 0;
-    left: 0;
-    right: 0;
-    bottom: 0;
-}
-
 .content-view.resource-details {
     position: absolute;
     top: 0;
     -webkit-user-select: text;
     white-space: nowrap;
 }
-
-@media (prefers-dark-interface) {
-    .network-resource-detail .item.close > .glyph {
-        background-color: hsla(0, 0%, 100%, 0.2);
-    }
-
-    .network .network-resource-detail .navigation-bar .item.radio.button.text-only.selected {
-        background-color: unset;
-        color: var(--glyph-color-active);
-    }
-}
index 31f2aec..2d0230f 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 Apple Inc. All rights reserved.
+ * Copyright (C) 2017-2018 Apple Inc. All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
  * THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-WI.NetworkResourceDetailView = class NetworkResourceDetailView extends WI.View
+WI.NetworkResourceDetailView = class NetworkResourceDetailView extends WI.NetworkDetailView
 {
     constructor(resource, delegate)
     {
-        super();
-
         console.assert(resource instanceof WI.Resource);
 
-        this._resource = resource;
-        this._delegate = delegate || null;
+        super(resource, delegate);
 
-        this.element.classList.add("network-resource-detail");
+        this.element.classList.add("resource");
 
-        this._contentBrowser = null;
         this._resourceContentView = null;
         this._headersContentView = null;
         this._cookiesContentView = null;
         this._sizesContentView = null;
         this._timingContentView = null;
-
-        this._contentViewCookie = null;
     }
 
     // Public
 
-    get resource() { return this._resource; }
-
     shown()
     {
-        if (!this._contentBrowser)
-            return;
-
-        this._showPreferredContentView();
-
-        if (this._contentViewCookie) {
-            if ("lineNumber" in this._contentViewCookie && "columnNumber" in this._contentViewCookie)
-                this._contentBrowser.navigationBar.selectedNavigationItem = this._previewNavigationItem;
-
-            this._contentBrowser.showContentView(this._contentBrowser.currentContentView, this._contentViewCookie);
-            this._contentViewCookie = null;
-        }
-
-        this._contentBrowser.shown();
-    }
-
-    hidden()
-    {
-        this._contentBrowser.hidden();
-    }
-
-    dispose()
-    {
-        this._delegate = null;
+        if (this._contentBrowser && this._contentViewCookie && "lineNumber" in this._contentViewCookie && "columnNumber" in this._contentViewCookie)
+            this._contentBrowser.navigationBar.selectedNavigationItem = this.detailNavigationItemForIdentifier("preview");
 
-        this._contentBrowser.contentViewContainer.closeAllContentViews();
-    }
-
-    willShowWithCookie(cookie)
-    {
-        this._contentViewCookie = cookie;
+        super.shown();
     }
 
     // ResourceHeadersContentView delegate
 
     headersContentViewGoToRequestData(headersContentView)
     {
-        this._contentBrowser.navigationBar.selectedNavigationItem = this._previewNavigationItem;
+        this._contentBrowser.navigationBar.selectedNavigationItem = this.detailNavigationItemForIdentifier("preview");
 
         this._resourceContentView.showRequest();
     }
@@ -98,19 +63,19 @@ WI.NetworkResourceDetailView = class NetworkResourceDetailView extends WI.View
 
     sizesContentViewGoToHeaders(metricsContentView)
     {
-        this._contentBrowser.navigationBar.selectedNavigationItem = this._headersNavigationItem;
+        this._contentBrowser.navigationBar.selectedNavigationItem = this.detailNavigationItemForIdentifier("headers");
     }
 
     sizesContentViewGoToRequestBody(metricsContentView)
     {
-        this._contentBrowser.navigationBar.selectedNavigationItem = this._previewNavigationItem;
+        this._contentBrowser.navigationBar.selectedNavigationItem = this.detailNavigationItemForIdentifier("preview");
 
         this._resourceContentView.showRequest();
     }
 
     sizesContentViewGoToResponseBody(metricsContentView)
     {
-        this._contentBrowser.navigationBar.selectedNavigationItem = this._previewNavigationItem;
+        this._contentBrowser.navigationBar.selectedNavigationItem = this.detailNavigationItemForIdentifier("preview");
 
         this._resourceContentView.showResponse();
     }
@@ -119,123 +84,52 @@ WI.NetworkResourceDetailView = class NetworkResourceDetailView extends WI.View
 
     initialLayout()
     {
-        let closeNavigationItem = new WI.ButtonNavigationItem("close", WI.UIString("Close detail view"), "Images/CloseLarge.svg", 16, 16);
-        closeNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleCloseButton.bind(this));
-        closeNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.High;
-
-        let contentViewNavigationItemsGroup = new WI.GroupNavigationItem;
-        let contentViewNavigationItemsFlexItem = new WI.FlexibleSpaceNavigationItem(contentViewNavigationItemsGroup, WI.FlexibleSpaceNavigationItem.Align.End);
-        contentViewNavigationItemsFlexItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
-
-        const disableBackForward = true;
-        const disableFindBanner = false;
-        this._contentBrowser = new WI.ContentBrowser(null, this, disableBackForward, disableFindBanner, contentViewNavigationItemsFlexItem, contentViewNavigationItemsGroup);
-
-        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._sizesNavigationItem = new WI.RadioButtonNavigationItem("sizes", WI.UIString("Sizes"));
-        this._timingNavigationItem = new WI.RadioButtonNavigationItem("timing", WI.UIString("Timing"));
-
-        // Insert all of our custom navigation items at the start of the ContentBrowser's NavigationBar.
-        let index = 0;
-        this._contentBrowser.navigationBar.insertNavigationItem(closeNavigationItem, index++);
-        this._contentBrowser.navigationBar.insertNavigationItem(new WI.FlexibleSpaceNavigationItem, index++);
-        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._sizesNavigationItem, index++);
-        this._contentBrowser.navigationBar.insertNavigationItem(this._timingNavigationItem, index++);
-        this._contentBrowser.navigationBar.addEventListener(WI.NavigationBar.Event.NavigationItemSelected, this._navigationItemSelected, this);
-
-        this.addSubview(this._contentBrowser);
-
-        this._showPreferredContentView();
+        this.createDetailNavigationItem("preview", WI.UIString("Preview"));
+        this.createDetailNavigationItem("headers", WI.UIString("Headers"));
+        this.createDetailNavigationItem("cookies", WI.UIString("Cookies"));
+        this.createDetailNavigationItem("sizes", WI.UIString("Sizes"));
+        this.createDetailNavigationItem("timing", WI.UIString("Timing"));
+
+        super.initialLayout();
     }
 
     // Private
 
-    _showPreferredContentView()
+    showContentViewForIdentifier(identifier)
     {
-        // Restore the preferred navigation item.
-        let firstNavigationItem = null;
-        let defaultIdentifier = WI.settings.selectedNetworkDetailContentViewIdentifier.value;
-        for (let navigationItem of this._contentBrowser.navigationBar.navigationItems) {
-            if (!(navigationItem instanceof WI.RadioButtonNavigationItem))
-                continue;
-
-            if (navigationItem !== this._previewNavigationItem
-                && navigationItem !== this._headersNavigationItem
-                && navigationItem !== this._cookiesNavigationItem
-                && navigationItem !== this._sizesNavigationItem
-                && navigationItem !== this._timingNavigationItem)
-                continue;
-
-            if (!firstNavigationItem)
-                firstNavigationItem = navigationItem;
-
-            if (navigationItem.identifier === defaultIdentifier) {
-                this._contentBrowser.navigationBar.selectedNavigationItem = navigationItem;
-                return;
-            }
-        }
+        super.showContentViewForIdentifier(identifier);
 
-        console.assert(firstNavigationItem, "Should have found at least one navigation item above");
-        this._contentBrowser.navigationBar.selectedNavigationItem = firstNavigationItem;
-    }
-
-    _showContentViewForNavigationItem(navigationItem)
-    {
-        let identifier = navigationItem.identifier;
         if (this._contentViewCookie && "lineNumber" in this._contentViewCookie && "columnNumber" in this._contentViewCookie)
-            identifier = this._previewNavigationItem.identifier;
+            identifier = "preview";
 
         switch (identifier) {
         case "preview":
             if (!this._resourceContentView)
-                this._resourceContentView = this._contentBrowser.showContentViewForRepresentedObject(this._resource);
+                this._resourceContentView = this._contentBrowser.showContentViewForRepresentedObject(this.representedObject);
             this._contentBrowser.showContentView(this._resourceContentView, this._contentViewCookie);
             break;
         case "headers":
             if (!this._headersContentView)
-                this._headersContentView = new WI.ResourceHeadersContentView(this._resource, this);
+                this._headersContentView = new WI.ResourceHeadersContentView(this.representedObject, this);
             this._contentBrowser.showContentView(this._headersContentView, this._contentViewCookie);
             break;
         case "cookies":
             if (!this._cookiesContentView)
-                this._cookiesContentView = new WI.ResourceCookiesContentView(this._resource);
+                this._cookiesContentView = new WI.ResourceCookiesContentView(this.representedObject);
             this._contentBrowser.showContentView(this._cookiesContentView, this._contentViewCookie);
             break;
         case "sizes":
             if (!this._sizesContentView)
-                this._sizesContentView = new WI.ResourceSizesContentView(this._resource, this);
+                this._sizesContentView = new WI.ResourceSizesContentView(this.representedObject, this);
             this._contentBrowser.showContentView(this._sizesContentView, this._contentViewCookie);
             break;
         case "timing":
             if (!this._timingContentView)
-                this._timingContentView = new WI.ResourceTimingContentView(this._resource);
+                this._timingContentView = new WI.ResourceTimingContentView(this.representedObject);
             this._contentBrowser.showContentView(this._timingContentView, this._contentViewCookie);
             break;
         }
 
         this._contentViewCookie = null;
     }
-
-    _navigationItemSelected(event)
-    {
-        let navigationItem = event.target.selectedNavigationItem;
-        if (!(navigationItem instanceof WI.RadioButtonNavigationItem))
-            return;
-
-        this._showContentViewForNavigationItem(navigationItem);
-
-        console.assert(navigationItem.identifier);
-        WI.settings.selectedNetworkDetailContentViewIdentifier.value = navigationItem.identifier;
-    }
-
-    _handleCloseButton(event)
-    {
-        if (this._delegate && this._delegate.networkResourceDetailViewClose)
-            this._delegate.networkResourceDetailViewClose(this);
-    }
 };
index 03cc066..9f5279c 100644 (file)
  * THE POSSIBILITY OF SUCH DAMAGE.
  */
 
+.content-view.network .network-table {
+    --node-waterfall-dom-event-size: 8px; /* Keep this in sync with `domEventElementSize`. */
+}
+
 .content-view.network .navigation-bar .filter-bar {
     background: none;
 }
@@ -152,6 +156,25 @@ body[dir=rtl] .network-table .cell.name > .status {
     bottom: 0;
 }
 
+.network-table :not(.header) .cell.waterfall .waterfall-container > .dom-event {
+    position: absolute;
+    top: calc(50% - (var(--node-waterfall-dom-event-size) / 2));
+    min-width: var(--node-waterfall-dom-event-size);
+    height: var(--node-waterfall-dom-event-size);
+    background-color: var(--selected-background-color);
+    border-radius: calc(var(--node-waterfall-dom-event-size) / 2);
+}
+
+.network-table :not(.header) .cell.waterfall .waterfall-container > .dom-activity {
+    position: absolute;
+    top: calc(50% - 0.5px);
+    border-top: 1px dashed var(--selected-background-color);
+}
+
+.network-table :not(.header) .cell.waterfall .waterfall-container > .dom-activity.playing {
+    border-top-style: solid;
+}
+
 .network-table .timeline-ruler {
     position: absolute;
     top: 0;
index 3ae0aa4..08703ca 100644 (file)
@@ -40,9 +40,9 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         this._table = null;
         this._nameColumnWidthSetting = new WI.Setting("network-table-content-view-name-column-width", 250);
 
-        this._selectedResource = null;
-        this._resourceDetailView = null;
-        this._resourceDetailViewMap = new Map;
+        this._selectedObject = null;
+        this._detailView = null;
+        this._detailViewMap = new Map;
 
         this._domNodeEntries = new Map;
 
@@ -223,8 +223,8 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
     {
         super.shown();
 
-        if (this._resourceDetailView)
-            this._resourceDetailView.shown();
+        if (this._detailView)
+            this._detailView.shown();
 
         if (this._table)
             this._table.restoreScrollPosition();
@@ -234,22 +234,22 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
     {
         this._hidePopover();
 
-        if (this._resourceDetailView)
-            this._resourceDetailView.hidden();
+        if (this._detailView)
+            this._detailView.hidden();
 
         super.hidden();
     }
 
     closed()
     {
-        for (let detailView of this._resourceDetailViewMap.values())
+        for (let detailView of this._detailViewMap.values())
             detailView.dispose();
-        this._resourceDetailViewMap.clear();
+        this._detailViewMap.clear();
 
         this._domNodeEntries.clear();
 
         this._hidePopover();
-        this._hideResourceDetailView();
+        this._hideDetailView();
 
         WI.Frame.removeEventListener(null, null, this);
         WI.Resource.removeEventListener(null, null, this);
@@ -267,9 +267,9 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         this._filteredEntries = [];
         this._pendingInsertions = [];
 
-        for (let detailView of this._resourceDetailViewMap.values())
+        for (let detailView of this._detailViewMap.values())
             detailView.dispose();
-        this._resourceDetailViewMap.clear();
+        this._detailViewMap.clear();
 
         this._domNodeEntries.clear();
 
@@ -282,7 +282,7 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
             this._selectedResource = null;
             this._table.reloadData();
             this._hidePopover();
-            this._hideResourceDetailView();
+            this._hideDetailView();
         }
     }
 
@@ -290,11 +290,11 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
     {
         console.assert(representedObject instanceof WI.Resource);
 
-        let rowIndex = this._rowIndexForResource(representedObject);
+        let rowIndex = this._rowIndexForRepresentedObject(representedObject);
         if (rowIndex === -1) {
             this._selectedResource = null;
             this._table.deselectAll();
-            this._hideResourceDetailView();
+            this._hideDetailView();
             return;
         }
 
@@ -303,13 +303,13 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         this._showingRepresentedObjectCookie = null;
     }
 
-    // NetworkResourceDetailView delegate
+    // NetworkDetailView delegate
 
-    networkResourceDetailViewClose(resourceDetailView)
+    networkDetailViewClose(networkDetailView)
     {
         this._selectedResource = null;
         this._table.deselectAll();
-        this._hideResourceDetailView();
+        this._hideDetailView();
     }
 
     // Table dataSource
@@ -326,7 +326,7 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         if (!this._entriesSortComparator)
             return;
 
-        this._hideResourceDetailView();
+        this._hideDetailView();
 
         for (let nodeEntry of this._domNodeEntries.values())
             nodeEntry.initiatedResourceEntries.sort(this._entriesSortComparator);
@@ -362,20 +362,20 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
     {
         let rowIndex = table.selectedRow;
         if (isNaN(rowIndex)) {
-            this._selectedResource = null;
-            this._hideResourceDetailView();
+            this._selectedObject = null;
+            this._hideDetailView();
             return;
         }
 
         let entry = this._filteredEntries[rowIndex];
-        if (entry.resource === this._selectedResource)
+        if (entry.resource === this._selectedObject || entry.domNode === this._selectedObject)
             return;
 
-        this._selectedResource = entry.resource;
-        if (this._selectedResource)
-            this._showResourceDetailView(this._selectedResource);
+        this._selectedObject = entry.resource || entry.domNode;
+        if (this._selectedObject)
+            this._showDetailView(this._selectedObject);
         else
-            this._hideResourceDetailView();
+            this._hideDetailView();
     }
 
     tablePopulateCell(table, cell, column, rowIndex)
@@ -516,7 +516,7 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
 
         if (WI.settings.groupByDOMNode.value && resource.initiatorNode) {
             let nodeEntry = this._domNodeEntries.get(resource.initiatorNode);
-            if (nodeEntry.initiatedResourceEntries.length > 1) {
+            if (nodeEntry.initiatedResourceEntries.length > 1 || nodeEntry.domNode.domEvents.length) {
                 cell.classList.add("child");
 
                 let range = resource.requestedByteRange;
@@ -641,9 +641,83 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
     {
         cell.removeChildren();
 
+        let container = cell.appendChild(document.createElement("div"));
+        container.className = "waterfall-container";
+
+        let graphStartTime = this._waterfallTimelineRuler.startTime;
+        let secondsPerPixel = this._waterfallTimelineRuler.secondsPerPixel;
+
+        function positionByStartOffset(element, timestamp) {
+            let styleAttribute = WI.resolvedLayoutDirection() === WI.LayoutDirection.LTR ? "left" : "right";
+            element.style.setProperty(styleAttribute, ((timestamp - graphStartTime) / secondsPerPixel) + "px");
+        }
+
+        function setWidthForDuration(element, startTimestamp, endTimestamp) {
+            element.style.setProperty("width", ((endTimestamp - startTimestamp) / secondsPerPixel) + "px");
+        }
+
         let domNode = entry.domNode;
         if (domNode) {
-            // FIXME: <https://webkit.org/b/189773> Web Inspector: create special Network waterfall for media events
+            const domEventElementSize = 8; // Keep this in sync with `--node-waterfall-dom-event-size`.
+
+            let groupedDOMEvents = domNode.domEvents.reduce((accumulator, current) => {
+                if (!accumulator.length || (current.timestamp - accumulator.lastValue.endTimestamp) >= (domEventElementSize * secondsPerPixel)) {
+                    accumulator.push({
+                        startTimestamp: current.timestamp,
+                        domEvents: [],
+                    });
+                }
+                accumulator.lastValue.endTimestamp = current.timestamp;
+                accumulator.lastValue.domEvents.push(current);
+                return accumulator;
+            }, []);
+
+            let playing = false;
+
+            function createDOMEventLine(domEvents, startTimestamp, endTimestamp) {
+                if (domEvents.lastValue.eventName === "ended")
+                    return;
+
+                for (let i = domEvents.length - 1; i >= 0; --i) {
+                    let domEvent = domEvents[i];
+                    if (domEvent.eventName === "play" || domEvent.eventName === "playing" || domEvent.eventName === "timeupdate") {
+                        playing = true;
+                        break;
+                    }
+
+                    if (domEvent.eventName === "pause" || domEvent.eventName === "stall") {
+                        playing = false;
+                        break;
+                    }
+                }
+
+                let lineElement = container.appendChild(document.createElement("div"));
+                lineElement.classList.add("dom-activity");
+                lineElement.classList.toggle("playing", playing);
+                positionByStartOffset(lineElement, startTimestamp);
+                setWidthForDuration(lineElement, startTimestamp, endTimestamp);
+            }
+
+            for (let [a, b] of groupedDOMEvents.adjacencies())
+                createDOMEventLine(a.domEvents, a.endTimestamp, b.startTimestamp);
+
+            if (groupedDOMEvents.length)
+                createDOMEventLine(groupedDOMEvents.lastValue.domEvents, groupedDOMEvents.lastValue.endTimestamp, this._waterfallEndTime);
+
+            for (let {startTimestamp, endTimestamp, domEvents} of groupedDOMEvents) {
+                let paddingForCentering = domEventElementSize * secondsPerPixel / 2;
+
+                let eventElement = container.appendChild(document.createElement("div"));
+                eventElement.classList.add("dom-event");
+                positionByStartOffset(eventElement, startTimestamp - paddingForCentering);
+                setWidthForDuration(eventElement, startTimestamp, endTimestamp + paddingForCentering);
+                eventElement.addEventListener("mousedown", (event) => {
+                    if (event.button !== 0 || event.ctrlKey)
+                        return;
+                    this._handleNodeEntryMousedownWaterfall(eventElement, entry, domEvents);
+                });
+            }
+
             return;
         }
 
@@ -659,7 +733,6 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
             return;
         }
 
-        let graphStartTime = this._waterfallTimelineRuler.startTime;
         if (responseEnd < graphStartTime) {
             cell.textContent = zeroWidthSpace;
             return;
@@ -671,22 +744,14 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
             return;
         }
 
-        let secondsPerPixel = this._waterfallTimelineRuler.secondsPerPixel;
-
-        let container = cell.appendChild(document.createElement("div"));
-        container.className = "waterfall-container";
-
         function appendBlock(startTimestamp, endTimestamp, className) {
             if (isNaN(startTimestamp) || isNaN(endTimestamp) || endTimestamp - startTimestamp <= 0)
                 return null;
 
-            let startOffset = (startTimestamp - graphStartTime) / secondsPerPixel;
-            let width = (endTimestamp - startTimestamp) / secondsPerPixel;
             let block = container.appendChild(document.createElement("div"));
             block.classList.add("block", className);
-            let styleAttribute = WI.resolvedLayoutDirection() === WI.LayoutDirection.LTR ? "left" : "right";
-            block.style[styleAttribute] = startOffset + "px";
-            block.style.width = width + "px";
+            positionByStartOffset(block, startTimestamp);
+            setWidthForDuration(block, startTimestamp, endTimestamp);
             return block;
         }
 
@@ -696,7 +761,7 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         mouseBlock.addEventListener("mousedown", (event) => {
             if (event.button !== 0 || event.ctrlKey)
                 return;
-            this._handleMousedownWaterfall(mouseBlock, entry, event);
+            this._handleResourceEntryMousedownWaterfall(mouseBlock, entry);
         });
 
         // Super small visualization.
@@ -1049,8 +1114,10 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         }
         this._pendingInsertions = [];
 
-        for (let resource of this._pendingUpdates)
-            this._updateEntryForResource(resource);
+        for (let updateObject of this._pendingUpdates) {
+            if (updateObject instanceof WI.Resource)
+                this._updateEntryForResource(updateObject);
+        }
         this._pendingUpdates = [];
 
         this._pendingFilter = false;
@@ -1112,9 +1179,15 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         }
     }
 
-    _rowIndexForResource(resource)
+    _rowIndexForRepresentedObject(object)
     {
-        return this._filteredEntries.findIndex((x) => x.resource === resource);
+        return this._filteredEntries.findIndex((x) => {
+            if (x.resource === object)
+                return true;
+            if (x.domNode === object)
+                return true;
+            return false;
+        });
     }
 
     _updateEntryForResource(resource)
@@ -1132,7 +1205,7 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         let entry = this._entryForResource(resource);
         updateExistingEntry(this._entries[index], entry);
 
-        let rowIndex = this._rowIndexForResource(resource);
+        let rowIndex = this._rowIndexForRepresentedObject(resource);
         if (rowIndex === -1)
             return;
 
@@ -1145,43 +1218,50 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
             this._waterfallPopover.dismiss();
     }
 
-    _hideResourceDetailView()
+    _hideDetailView()
     {
-        if (!this._resourceDetailView)
+        if (!this._detailView)
             return;
 
         this.element.classList.remove("showing-detail");
         this._table.scrollContainer.style.removeProperty("width");
 
-        this.removeSubview(this._resourceDetailView);
+        this.removeSubview(this._detailView);
 
-        this._resourceDetailView.hidden();
-        this._resourceDetailView = null;
+        this._detailView.hidden();
+        this._detailView = null;
 
         this._table.resize();
         this._table.reloadVisibleColumnCells(this._waterfallColumn);
     }
 
-    _showResourceDetailView(resource)
+    _showDetailView(object)
     {
-        let oldResourceDetailView = this._resourceDetailView;
+        let oldDetailView = this._detailView;
+
+        this._detailView = this._detailViewMap.get(object);
+        if (!this._detailView) {
+            if (object instanceof WI.Resource)
+                this._detailView = new WI.NetworkResourceDetailView(object, this);
+            else if (object instanceof WI.DOMNode) {
+                this._detailView = new WI.NetworkDOMNodeDetailView(object, this, {
+                    startTimestamp: this._waterfallStartTime,
+                });
+            }
 
-        this._resourceDetailView = this._resourceDetailViewMap.get(resource);
-        if (!this._resourceDetailView) {
-            this._resourceDetailView = new WI.NetworkResourceDetailView(resource, this);
-            this._resourceDetailViewMap.set(resource, this._resourceDetailView);
+            this._detailViewMap.set(object, this._detailView);
         }
 
-        if (oldResourceDetailView) {
-            oldResourceDetailView.hidden();
-            this.replaceSubview(oldResourceDetailView, this._resourceDetailView);
+        if (oldDetailView) {
+            oldDetailView.hidden();
+            this.replaceSubview(oldDetailView, this._detailView);
         } else
-            this.addSubview(this._resourceDetailView);
+            this.addSubview(this._detailView);
 
         if (this._showingRepresentedObjectCookie)
-            this._resourceDetailView.willShowWithCookie(this._showingRepresentedObjectCookie);
+            this._detailView.willShowWithCookie(this._showingRepresentedObjectCookie);
 
-        this._resourceDetailView.shown();
+        this._detailView.shown();
 
         this.element.classList.add("showing-detail");
         this._table.scrollContainer.style.width = this._nameColumn.width + "px";
@@ -1194,11 +1274,11 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
 
     _positionDetailView()
     {
-        if (!this._resourceDetailView)
+        if (!this._detailView)
             return;
 
         let side = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL ? "right" : "left";
-        this._resourceDetailView.element.style[side] = this._nameColumn.width + "px";
+        this._detailView.element.style[side] = this._nameColumn.width + "px";
         this._table.scrollContainer.style.width = this._nameColumn.width + "px";
     }
 
@@ -1331,7 +1411,7 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         let entry = this._entries[index];
         entry.transferSize = !isNaN(resource.networkTotalTransferSize) ? resource.networkTotalTransferSize : resource.estimatedTotalTransferSize;
 
-        let rowIndex = this._rowIndexForResource(resource);
+        let rowIndex = this._rowIndexForRepresentedObject(resource);
         if (rowIndex === -1)
             return;
 
@@ -1444,6 +1524,8 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         if (!nodeEntry) {
             nodeEntry = this._entryForDOMNode(resource.initiatorNode, Object.keys(resourceEntry));
             this._domNodeEntries.set(resource.initiatorNode, nodeEntry);
+
+            resource.initiatorNode.addEventListener(WI.DOMNode.Event.DidFireEvent, this._handleNodeDidFireEvent, this);
         }
 
         if (!this._entriesSortComparator)
@@ -1466,6 +1548,19 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         }, new Set);
     }
 
+    _handleNodeDidFireEvent(event)
+    {
+        let domNode = event.target;
+        let {domEvent} = event.data;
+
+        this._pendingUpdates.push(domNode);
+
+        if (domEvent.timestamp > this._waterfallEndTime)
+            this._waterfallEndTime = domEvent.timestamp + (this._waterfallTimelineRuler.secondsPerPixel * 10);
+
+        this.needsLayout();
+    }
+
     _hasTypeFilter()
     {
         return !!this._activeTypeFilters;
@@ -1517,7 +1612,7 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
 
         if (WI.settings.groupByDOMNode.value) {
             for (let nodeEntry of this._domNodeEntries.values()) {
-                if (nodeEntry.initiatedResourceEntries.length < 2)
+                if (nodeEntry.initiatedResourceEntries.length < 2 && !nodeEntry.domNode.domEvents.length)
                     continue;
 
                 let firstIndex = Infinity;
@@ -1614,7 +1709,7 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
             return;
 
         // Even if the selected resource would still be visible, lets close the detail view if a filter changes.
-        this._hideResourceDetailView();
+        this._hideDetailView();
 
         this._activeTypeFilters = newFilter;
         this._updateFilteredEntries();
@@ -1640,7 +1735,7 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
             return;
 
         // Even if the selected resource would still be visible, lets close the detail view if a filter changes.
-        this._hideResourceDetailView();
+        this._hideDetailView();
 
         // Search cleared.
         if (!searchQuery) {
@@ -1669,10 +1764,10 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
 
     _restoreSelectedRow()
     {
-        if (!this._selectedResource)
+        if (!this._selectedObject)
             return;
 
-        let rowIndex = this._rowIndexForResource(this._selectedResource);
+        let rowIndex = this._rowIndexForRepresentedObject(this._selectedObject);
         if (rowIndex === -1) {
             this._selectedResource = null;
             this._table.deselectAll();
@@ -1709,11 +1804,18 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         }).catch(handlePromiseException);
     }
 
-    _waterfallPopoverContentForResource(resource)
+    _waterfallPopoverContent()
     {
         let contentElement = document.createElement("div");
-        contentElement.className = "waterfall-popover-content";
+        contentElement.classList.add("waterfall-popover-content");
+        return contentElement;
+    }
+
+    _waterfallPopoverContentForResourceEntry(resourceEntry)
+    {
+        let contentElement = this._waterfallPopoverContent();
 
+        let resource = resourceEntry.resource;
         if (!resource.hasResponse() || !resource.firstTimestamp || !resource.lastTimestamp) {
             contentElement.textContent = WI.UIString("Resource has no timing data");
             return contentElement;
@@ -1726,7 +1828,32 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         return contentElement;
     }
 
-    _handleMousedownWaterfall(mouseBlock, entry, event)
+    _waterfallPopoverContentForNodeEntry(nodeEntry, domEvents)
+    {
+        let contentElement = this._waterfallPopoverContent();
+
+        let breakdownView = new WI.DOMEventsBreakdownView(domEvents, {
+            startTimestamp: this._waterfallStartTime,
+        });
+        contentElement.appendChild(breakdownView.element);
+        breakdownView.updateLayout();
+
+        return contentElement;
+    }
+
+    _handleResourceEntryMousedownWaterfall(targetElement, resourceEntry)
+    {
+        let popoverContentElement = this._waterfallPopoverContentForResourceEntry(resourceEntry);
+        this._handleMousedownWaterfall(resourceEntry, targetElement, popoverContentElement);
+    }
+
+    _handleNodeEntryMousedownWaterfall(targetElement, nodeEntry, domEvents)
+    {
+        let popoverContentElement = this._waterfallPopoverContentForNodeEntry(nodeEntry, domEvents);
+        this._handleMousedownWaterfall(nodeEntry, targetElement, popoverContentElement);
+    }
+
+    _handleMousedownWaterfall(entry, targetElement, popoverContentElement)
     {
         if (!this._waterfallPopover) {
             this._waterfallPopover = new WI.Popover;
@@ -1737,7 +1864,10 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
             return;
 
         let calculateTargetFrame = () => {
-            let rowIndex = this._rowIndexForResource(entry.resource);
+            if (!entry.resource)
+                return WI.Rect.rectFromClientRect(targetElement.getBoundingClientRect());
+
+            let rowIndex = this._rowIndexForRepresentedObject(entry.resource);
             let cell = this._table.cellForRowAndColumn(rowIndex, this._waterfallColumn);
             if (!cell) {
                 this._waterfallPopover.dismiss();
@@ -1767,7 +1897,6 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
                 this._waterfallPopover.present(bounds, preferredEdges);
         };
 
-        let popoverContentElement = this._waterfallPopoverContentForResource(entry.resource);
         this._waterfallPopover.presentNewContentWithFrame(popoverContentElement, targetFrame, preferredEdges);
     }
 
index 0e98964..7ffd41f 100644 (file)
  * THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-.popover.waterfall-popover {
-    --popover-background-color: white;
-}
-
 .waterfall-popover-content .resource-timing-breakdown {
     margin: 5px;
     -webkit-user-select: text;
@@ -73,8 +69,8 @@
 }
 
 @media (prefers-dark-interface) {
-    .popover.waterfall-popover {
-        --popover-background-color: var(--panel-background-color);
+    .resource-timing-breakdown > table > tr.header:not(.total-row) > td {
+        color: var(--text-color);
     }
 
     .resource-timing-breakdown > table > tr > td.label,