Web Inspector: Network: detail view reverts to "Response" when new requests are added
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / NetworkTableContentView.js
index b7de7b7..c6863c8 100644 (file)
@@ -35,13 +35,16 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         this._pendingInsertions = [];
         this._pendingUpdates = [];
         this._pendingFilter = false;
+        this._showingRepresentedObjectCookie = null;
 
         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;
 
         this._waterfallStartTime = NaN;
         this._waterfallEndTime = NaN;
@@ -82,6 +85,9 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         this._typeFilterScopeBar = new WI.ScopeBar("network-type-filter-scope-bar", typeFilterScopeBarItems, typeFilterScopeBarItems[0]);
         this._typeFilterScopeBar.addEventListener(WI.ScopeBar.Event.SelectionChanged, this._typeFilterScopeBarSelectionChanged, this);
 
+        this._groupByDOMNodeNavigationItem = new WI.CheckboxNavigationItem("group-by-node", WI.UIString("Group by Node"), WI.settings.groupByDOMNode.value);
+        this._groupByDOMNodeNavigationItem.addEventListener(WI.CheckboxNavigationItem.Event.CheckedDidChange, this._handleGroupByDOMNodeCheckedDidChange, this);
+
         this._urlFilterSearchText = null;
         this._urlFilterSearchRegex = null;
         this._urlFilterIsActive = false;
@@ -129,7 +135,7 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         WI.Resource.addEventListener(WI.Resource.Event.LoadingDidFinish, this._resourceLoadingDidFinish, this);
         WI.Resource.addEventListener(WI.Resource.Event.LoadingDidFail, this._resourceLoadingDidFail, this);
         WI.Resource.addEventListener(WI.Resource.Event.TransferSizeDidChange, this._resourceTransferSizeDidChange, this);
-        WI.frameResourceManager.addEventListener(WI.FrameResourceManager.Event.MainFrameDidChange, this._mainFrameDidChange, this);
+        WI.networkManager.addEventListener(WI.NetworkManager.Event.MainFrameDidChange, this._mainFrameDidChange, this);
         WI.timelineManager.persistentNetworkTimeline.addEventListener(WI.Timeline.Event.RecordAdded, this._networkTimelineRecordAdded, this);
 
         this._needsInitialPopulate = true;
@@ -139,7 +145,7 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
 
     static displayNameForResource(resource)
     {
-        if (resource.type === WI.Resource.Type.Image || resource.type === WI.Resource.Type.Font) {
+        if (resource.type === WI.Resource.Type.Image || resource.type === WI.Resource.Type.Font || resource.type === WI.Resource.Type.Other) {
             let fileExtension;
             if (resource.mimeType)
                 fileExtension = WI.fileExtensionForMIMEType(resource.mimeType);
@@ -200,7 +206,7 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
 
     get filterNavigationItems()
     {
-        return [this._urlFilterNavigationItem, this._typeFilterScopeBar];
+        return [this._urlFilterNavigationItem, this._typeFilterScopeBar, this._groupByDOMNodeNavigationItem];
     }
 
     get supportsSave()
@@ -217,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();
@@ -228,26 +234,28 @@ 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);
         WI.resourceCachingDisabledSetting.removeEventListener(null, null, this);
         WI.settings.clearNetworkOnNavigate.removeEventListener(null, null, this);
-        WI.frameResourceManager.removeEventListener(WI.FrameResourceManager.Event.MainFrameDidChange, this._mainFrameDidChange, this);
+        WI.networkManager.removeEventListener(WI.NetworkManager.Event.MainFrameDidChange, this._mainFrameDidChange, this);
         WI.timelineManager.persistentNetworkTimeline.removeEventListener(WI.Timeline.Event.RecordAdded, this._networkTimelineRecordAdded, this);
 
         super.closed();
@@ -259,9 +267,11 @@ 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();
 
         this._waterfallStartTime = NaN;
         this._waterfallEndTime = NaN;
@@ -270,10 +280,9 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
 
         if (this._table) {
             this._selectedResource = null;
-            this._table.clearSelectedRow();
             this._table.reloadData();
             this._hidePopover();
-            this._hideResourceDetailView();
+            this._hideDetailView();
         }
     }
 
@@ -281,24 +290,26 @@ 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.clearSelectedRow();
-            this._hideResourceDetailView();
+            this._table.deselectAll();
+            this._hideDetailView();
             return;
         }
 
+        this._showingRepresentedObjectCookie = cookie;
         this._table.selectRow(rowIndex);
+        this._showingRepresentedObjectCookie = null;
     }
 
-    // NetworkResourceDetailView delegate
+    // NetworkDetailView delegate
 
-    networkResourceDetailViewClose(resourceDetailView)
+    networkDetailViewClose(networkDetailView)
     {
         this._selectedResource = null;
-        this._table.clearSelectedRow();
-        this._hideResourceDetailView();
+        this._table.deselectAll();
+        this._hideDetailView();
     }
 
     // Table dataSource
@@ -315,23 +326,18 @@ 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);
 
-        this._entries = this._entries.sort(this._entriesSortComparator);
+        this._updateSort();
         this._updateFilteredEntries();
-        this._table.reloadData();
+        this._reloadTable();
     }
 
     // Table delegate
 
-    tableCellMouseDown(table, cell, column, rowIndex, event)
-    {
-        if (column !== this._nameColumn)
-            return;
-
-        this._table.selectRow(rowIndex);
-    }
-
     tableCellContextMenuClicked(table, cell, column, rowIndex, event)
     {
         if (column !== this._nameColumn)
@@ -347,27 +353,53 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         contextMenu.appendItem(WI.UIString("Export HAR"), () => { this._exportHAR(); });
     }
 
-    tableSelectedRowChanged(table, rowIndex)
+    tableShouldSelectRow(table, cell, column, rowIndex)
     {
+        return column === this._nameColumn;
+    }
+
+    tableSelectionDidChange(table)
+    {
+        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;
-        this._showResourceDetailView(this._selectedResource);
+        this._selectedObject = entry.resource || entry.domNode;
+        if (this._selectedObject)
+            this._showDetailView(this._selectedObject);
+        else
+            this._hideDetailView();
     }
 
     tablePopulateCell(table, cell, column, rowIndex)
     {
         let entry = this._filteredEntries[rowIndex];
 
-        cell.classList.toggle("error", entry.resource.hadLoadingError());
+        if (entry.resource)
+            cell.classList.toggle("error", entry.resource.hadLoadingError());
+
+        let setTextContent = (accessor) => {
+            let uniqueValues = this._uniqueValuesForDOMNodeEntry(entry, accessor);
+            if (uniqueValues) {
+                if (uniqueValues.size > 1) {
+                    cell.classList.add("multiple");
+                    cell.textContent = WI.UIString("(multiple)");
+                    return;
+                }
+
+                cell.textContent = uniqueValues.values().next().value || emDash;
+                return;
+            }
+
+            cell.textContent = accessor(entry) || emDash;
+        };
 
         switch (column.identifier) {
         case "name":
@@ -377,42 +409,55 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
             this._populateDomainCell(cell, entry);
             break;
         case "type":
-            cell.textContent = entry.displayType || emDash;
+            setTextContent((resourceEntry) => resourceEntry.displayType);
             break;
         case "mimeType":
-            cell.textContent = entry.mimeType || emDash;
+            setTextContent((resourceEntry) => resourceEntry.mimeType);
             break;
         case "method":
-            cell.textContent = entry.method || emDash;
+            setTextContent((resourceEntry) => resourceEntry.method);
             break;
         case "scheme":
-            cell.textContent = entry.scheme || emDash;
+            setTextContent((resourceEntry) => resourceEntry.scheme);
             break;
         case "status":
-            cell.textContent = entry.status || emDash;
+            setTextContent((resourceEntry) => resourceEntry.status);
             break;
         case "protocol":
-            cell.textContent = entry.protocol || emDash;
+            setTextContent((resourceEntry) => resourceEntry.protocol);
+            break;
+        case "initiator":
+            this._populateInitiatorCell(cell, entry);
             break;
         case "priority":
-            cell.textContent = WI.Resource.displayNameForPriority(entry.priority) || emDash;
+            setTextContent((resourceEntry) => WI.Resource.displayNameForPriority(resourceEntry.priority));
             break;
         case "remoteAddress":
-            cell.textContent = entry.remoteAddress || emDash;
+            setTextContent((resourceEntry) => resourceEntry.remoteAddress);
             break;
         case "connectionIdentifier":
-            cell.textContent = entry.connectionIdentifier || emDash;
+            setTextContent((resourceEntry) => resourceEntry.connectionIdentifier);
             break;
-        case "resourceSize":
-            cell.textContent = isNaN(entry.resourceSize) ? emDash : Number.bytesToString(entry.resourceSize);
+        case "resourceSize": {
+            let resourceSize = entry.resourceSize;
+            let resourceEntries = entry.initiatedResourceEntries;
+            if (resourceEntries)
+                resourceSize = resourceEntries.reduce((accumulator, current) => accumulator + (current.resourceSize || 0), 0);
+            cell.textContent = isNaN(resourceSize) ? emDash : Number.bytesToString(resourceSize);
             break;
+        }
         case "transferSize":
             this._populateTransferSizeCell(cell, entry);
             break;
-        case "time":
+        case "time": {
             // FIXME: <https://webkit.org/b/176748> Web Inspector: Frontend sometimes receives resources with negative duration (responseEnd - requestStart)
-            cell.textContent = isNaN(entry.time) ? emDash : Number.secondsToString(Math.max(entry.time, 0));
+            let time = entry.time;
+            let resourceEntries = entry.initiatedResourceEntries;
+            if (resourceEntries)
+                time = resourceEntries.reduce((accumulator, current) => accumulator + (current.time || 0), 0);
+            cell.textContent = isNaN(time) ? emDash : Number.secondsToString(Math.max(time, 0));
             break;
+        }
         case "waterfall":
             this._populateWaterfallGraph(cell, entry);
             break;
@@ -427,6 +472,34 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
     {
         console.assert(!cell.firstChild, "We expect the cell to be empty.", cell, cell.firstChild);
 
+        function createIconElement() {
+            let iconElement = cell.appendChild(document.createElement("img"));
+            iconElement.className = "icon";
+        }
+
+        let domNode = entry.domNode;
+        if (domNode) {
+            this._table.element.classList.add("grouped");
+
+            cell.classList.add("parent");
+
+            let disclosureElement = cell.appendChild(document.createElement("img"));
+            disclosureElement.classList.add("disclosure");
+            disclosureElement.classList.toggle("expanded", !!entry.expanded);
+            disclosureElement.addEventListener("click", (event) => {
+                entry.expanded = !entry.expanded;
+
+                this._updateFilteredEntries();
+                this._reloadTable();
+            });
+
+            createIconElement();
+
+            cell.classList.add("dom-node");
+            cell.appendChild(WI.linkifyNodeReference(domNode));
+            return;
+        }
+
         let resource = entry.resource;
         if (resource.isLoading()) {
             let statusElement = cell.appendChild(document.createElement("div"));
@@ -435,34 +508,113 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
             statusElement.appendChild(spinner.element);
         }
 
-        let iconElement = cell.appendChild(document.createElement("img"));
-        iconElement.className = "icon";
-        cell.classList.add(WI.ResourceTreeElement.ResourceIconStyleClassName, entry.resource.type);
+        createIconElement();
+
+        cell.classList.add(WI.ResourceTreeElement.ResourceIconStyleClassName);
 
         let nameElement = cell.appendChild(document.createElement("span"));
-        nameElement.textContent = entry.name;
+
+        if (WI.settings.groupByDOMNode.value && resource.initiatorNode) {
+            let nodeEntry = this._domNodeEntries.get(resource.initiatorNode);
+            if (nodeEntry.initiatedResourceEntries.length > 1 || nodeEntry.domNode.domEvents.length) {
+                cell.classList.add("child");
+
+                let range = resource.requestedByteRange;
+                if (range)
+                    nameElement.textContent = WI.UIString("Byte Range %s\u2013%s").format(range.start, range.end);
+            }
+        }
+
+        if (!nameElement.textContent)
+            nameElement.textContent = entry.name;
+
+        cell.title = resource.url;
+        cell.classList.add(WI.Resource.classNameForResource(resource));
     }
 
     _populateDomainCell(cell, entry)
     {
         console.assert(!cell.firstChild, "We expect the cell to be empty.", cell, cell.firstChild);
 
+        function createIconAndText(scheme, domain) {
+            let secure = scheme === "https" || scheme === "wss";
+            if (secure) {
+                let lockIconElement = cell.appendChild(document.createElement("img"));
+                lockIconElement.className = "lock";
+            }
+
+            cell.append(domain);
+        }
+
+        let uniqueSchemeValues = this._uniqueValuesForDOMNodeEntry(entry, (resourceEntry) => resourceEntry.scheme);
+        let uniqueDomainValues = this._uniqueValuesForDOMNodeEntry(entry, (resourceEntry) => resourceEntry.domain);
+        if (uniqueSchemeValues && uniqueDomainValues) {
+            if (uniqueSchemeValues.size > 1 || uniqueDomainValues.size > 1) {
+                cell.classList.add("multiple");
+                cell.textContent = WI.UIString("(multiple)");
+                return;
+            }
+
+            createIconAndText(uniqueSchemeValues.values().next().value, uniqueDomainValues.values().next().value);
+            return;
+        }
+
         if (!entry.domain) {
             cell.textContent = emDash;
             return;
         }
 
-        let secure = entry.scheme === "https" || entry.scheme === "wss";
-        if (secure) {
-            let lockIconElement = cell.appendChild(document.createElement("img"));
-            lockIconElement.className = "lock";
+        createIconAndText(entry.scheme, entry.domain);
+    }
+
+    _populateInitiatorCell(cell, entry)
+    {
+        let domNode = entry.domNode;
+        if (domNode) {
+            cell.textContent = emDash;
+            return;
+        }
+
+        let initiatorLocation = entry.resource.initiatorSourceCodeLocation;
+        if (!initiatorLocation) {
+            cell.textContent = emDash;
+            return;
         }
 
-        cell.append(entry.domain);
+        const options = {
+            dontFloat: true,
+            ignoreSearchTab: true,
+        };
+        cell.appendChild(WI.createSourceCodeLocationLink(initiatorLocation, options));
     }
 
     _populateTransferSizeCell(cell, entry)
     {
+        let resourceEntries = entry.initiatedResourceEntries;
+        if (resourceEntries) {
+            if (resourceEntries.every((resourceEntry) => resourceEntry.resource.responseSource === WI.Resource.ResponseSource.MemoryCache)) {
+                cell.classList.add("cache-type");
+                cell.textContent = WI.UIString("(memory)");
+                return;
+            }
+            if (resourceEntries.every((resourceEntry) => resourceEntry.resource.responseSource === WI.Resource.ResponseSource.DiskCache)) {
+                cell.classList.add("cache-type");
+                cell.textContent = WI.UIString("(disk)");
+                return;
+            }
+            if (resourceEntries.every((resourceEntry) => resourceEntry.resource.responseSource === WI.Resource.ResponseSource.ServiceWorker)) {
+                cell.classList.add("cache-type");
+                cell.textContent = WI.UIString("(service worker)");
+                return;
+            }
+            let transferSize = resourceEntries.reduce((accumulator, current) => accumulator + (current.transferSize || 0), 0);
+            if (isNaN(transferSize))
+                cell.textContent = emDash;
+            else
+                cell.textContent = Number.bytesToString(transferSize);
+            return;
+        }
+
         let responseSource = entry.resource.responseSource;
         if (responseSource === WI.Resource.ResponseSource.MemoryCache) {
             cell.classList.add("cache-type");
@@ -489,19 +641,98 @@ 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) {
+            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;
+        }
+
         let resource = entry.resource;
         if (!resource.hasResponse()) {
             cell.textContent = zeroWidthSpace;
             return;
         }
 
-        let {startTime, domainLookupStart, domainLookupEnd, connectStart, connectEnd, secureConnectionStart, requestStart, responseStart, responseEnd} = resource.timingData;
-        if (isNaN(startTime)) {
+        let {startTime, redirectStart, redirectEnd, fetchStart, domainLookupStart, domainLookupEnd, connectStart, connectEnd, secureConnectionStart, requestStart, responseStart, responseEnd} = resource.timingData;
+        if (isNaN(startTime) || isNaN(responseEnd)) {
             cell.textContent = zeroWidthSpace;
             return;
         }
 
-        let graphStartTime = this._waterfallTimelineRuler.startTime;
         if (responseEnd < graphStartTime) {
             cell.textContent = zeroWidthSpace;
             return;
@@ -513,19 +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;
 
-        function appendBlock(startTime, endTime, className) {
-            let startOffset = (startTime - graphStartTime) / secondsPerPixel;
-            let width = (endTime - startTime) / 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;
         }
 
@@ -535,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.
@@ -546,16 +772,20 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
             return;
         }
 
-        // Each component.
+        appendBlock(startTime, responseEnd, "filler");
+
+        // FIXME: <https://webkit.org/b/190214> Web Inspector: expose full load metrics for redirect requests
+        appendBlock(redirectStart, redirectEnd, "redirect");
+
         if (domainLookupStart) {
-            appendBlock(startTime, domainLookupStart, "queue");
-            appendBlock(domainLookupStart, connectStart || requestStart, "dns");
+            appendBlock(fetchStart, domainLookupStart, "queue");
+            appendBlock(domainLookupStart, domainLookupEnd || connectStart || requestStart, "dns");
         } else if (connectStart)
-            appendBlock(startTime, connectStart, "queue");
+            appendBlock(fetchStart, connectStart, "queue");
         else if (requestStart)
-            appendBlock(startTime, requestStart, "queue");
+            appendBlock(fetchStart, requestStart, "queue");
         if (connectStart)
-            appendBlock(connectStart, connectEnd, "connect");
+            appendBlock(connectStart, secureConnectionStart || connectEnd, "connect");
         if (secureConnectionStart)
             appendBlock(secureConnectionStart, connectEnd, "secure");
         appendBlock(requestStart, responseStart, "request");
@@ -579,6 +809,7 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         case "method":
         case "scheme":
         case "protocol":
+        case "initiator":
         case "remoteAddress":
             // Simple string.
             comparator = (a, b) => (a[sortColumnIdentifier] || "").extendedLocaleCompare(b[sortColumnIdentifier] || "");
@@ -645,7 +876,7 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
 
         case "waterfall":
             // Sort by startTime number.
-            comparator = comparator = (a, b) => a.startTime - b.startTime;
+            comparator = (a, b) => a.startTime - b.startTime;
             break;
 
         default:
@@ -654,7 +885,21 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         }
 
         let reverseFactor = this._table.sortOrder === WI.Table.SortOrder.Ascending ? 1 : -1;
-        this._entriesSortComparator = (a, b) => reverseFactor * comparator(a, b);
+
+        // If the entry has an `initiatorNode`, use that node's "first" resource as the value of
+        // `entry`, so long as the entry being compared to doesn't have the same `initiatorNode`.
+        // This will ensure that all resource entries for a given `initiatorNode` will appear right
+        // next to each other, as they will all effectively be sorted by the first resource.
+        let substitute = (entry, other) => {
+            if (WI.settings.groupByDOMNode.value && entry.resource.initiatorNode) {
+                let nodeEntry = this._domNodeEntries.get(entry.resource.initiatorNode);
+                if (!nodeEntry.initiatedResourceEntries.includes(other))
+                    return nodeEntry.initiatedResourceEntries[0];
+            }
+            return entry;
+        };
+
+        this._entriesSortComparator = (a, b) => reverseFactor * comparator(substitute(a, b), substitute(b, a));
     }
 
     // Protected
@@ -718,6 +963,13 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
             initialWidth: 75,
         });
 
+        this._initiatorColumn = new WI.TableColumn("initiator", WI.UIString("Initiator"), {
+            hidden: true,
+            minWidth: 75,
+            maxWidth: 175,
+            initialWidth: 125,
+        });
+
         this._priorityColumn = new WI.TableColumn("priority", WI.UIString("Priority"), {
             hidden: true,
             minWidth: 65,
@@ -778,6 +1030,7 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         this._table.addColumn(this._schemeColumn);
         this._table.addColumn(this._statusColumn);
         this._table.addColumn(this._protocolColumn);
+        this._table.addColumn(this._initiatorColumn);
         this._table.addColumn(this._priorityColumn);
         this._table.addColumn(this._remoteAddressColumn);
         this._table.addColumn(this._connectionIdentifierColumn);
@@ -854,18 +1107,24 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
             return;
         }
 
-        for (let resource of this._pendingInsertions)
-            this._entries.push(this._entryForResource(resource));
+        for (let resource of this._pendingInsertions) {
+            let resourceEntry = this._entryForResource(resource);
+            this._tryLinkResourceToDOMNode(resourceEntry);
+            this._entries.push(resourceEntry);
+        }
         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;
 
-        this._updateSortAndFilteredEntries();
-        this._table.reloadData();
+        this._updateSort();
+        this._updateFilteredEntries();
+        this._reloadTable();
     }
 
     _populateWithInitialResourcesIfNeeded()
@@ -881,23 +1140,23 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
             else if (frame.mainResource)
                 this._pendingInsertions.push(frame.mainResource);
 
-            for (let resource of frame.resourceCollection.items)
+            for (let resource of frame.resourceCollection)
                 this._pendingInsertions.push(resource);
 
-            for (let childFrame of frame.childFrameCollection.items)
+            for (let childFrame of frame.childFrameCollection)
                 populateResourcesForFrame(childFrame);
         };
 
         let populateResourcesForTarget = (target) => {
             if (target.mainResource instanceof WI.Resource)
                 this._pendingInsertions.push(target.mainResource);
-            for (let resource of target.resourceCollection.items)
+            for (let resource of target.resourceCollection)
                 this._pendingInsertions.push(resource);
         };
 
         for (let target of WI.targets) {
             if (target === WI.pageTarget)
-                populateResourcesForFrame(WI.frameResourceManager.mainFrame);
+                populateResourcesForFrame(WI.networkManager.mainFrame);
             else
                 populateResourcesForTarget(target);
         }
@@ -907,13 +1166,28 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
 
     _checkURLFilterAgainstResource(resource)
     {
-        if (this._urlFilterSearchRegex.test(resource.url))
+        if (this._urlFilterSearchRegex.test(resource.url)) {
             this._activeURLFilterResources.add(resource);
+            return;
+        }
+
+        for (let redirect of resource.redirects) {
+            if (this._urlFilterSearchRegex.test(redirect.url)) {
+                this._activeURLFilterResources.add(resource);
+                return;
+            }
+        }
     }
 
-    _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)
@@ -922,14 +1196,20 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         if (index === -1)
             return;
 
+        // Don't wipe out the previous entry, as it may be used by a node entry.
+        function updateExistingEntry(existingEntry, newEntry) {
+            for (let key in newEntry)
+                existingEntry[key] = newEntry[key];
+        }
+
         let entry = this._entryForResource(resource);
-        this._entries[index] = entry;
+        updateExistingEntry(this._entries[index], entry);
 
-        let rowIndex = this._rowIndexForResource(resource);
+        let rowIndex = this._rowIndexForRepresentedObject(resource);
         if (rowIndex === -1)
             return;
 
-        this._filteredEntries[rowIndex] = entry;
+        updateExistingEntry(this._filteredEntries[rowIndex], entry);
     }
 
     _hidePopover()
@@ -938,39 +1218,53 @@ 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._resourceDetailView = this._resourceDetailViewMap.get(resource);
-        if (!this._resourceDetailView) {
-            this._resourceDetailView = new WI.NetworkResourceDetailView(resource, this);
-            this._resourceDetailViewMap.set(resource, this._resourceDetailView);
+        this._detailView = this._detailViewMap.get(object);
+        if (this._detailView === oldDetailView)
+            return;
+
+        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._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._resourceDetailView.shown();
+            this.addSubview(this._detailView);
+
+        if (this._showingRepresentedObjectCookie)
+            this._detailView.willShowWithCookie(this._showingRepresentedObjectCookie);
+
+        this._detailView.shown();
 
         this.element.classList.add("showing-detail");
         this._table.scrollContainer.style.width = this._nameColumn.width + "px";
@@ -983,11 +1277,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";
     }
 
@@ -1120,7 +1414,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;
 
@@ -1152,29 +1446,37 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
             return;
         }
 
-        let entry = this._entryForResource(resource);
+        let resourceEntry = this._entryForResource(resource);
 
-        // Default sort has fast path.
-        if (this._isDefaultSort() || !this._entriesSortComparator) {
-            this._entries.push(entry);
-            if (this._passFilter(entry)) {
-                this._filteredEntries.push(entry);
+        this._tryLinkResourceToDOMNode(resourceEntry);
+
+        if (WI.settings.groupByDOMNode.value && resource.initiatorNode) {
+            if (!this._entriesSortComparator)
+                this._generateSortComparator();
+        } else if (this._isDefaultSort() || !this._entriesSortComparator) {
+            // Default sort has fast path.
+            this._entries.push(resourceEntry);
+            if (this._passFilter(resourceEntry)) {
+                this._filteredEntries.push(resourceEntry);
                 this._table.reloadDataAddedToEndOnly();
             }
             return;
         }
 
-        insertObjectIntoSortedArray(entry, this._entries, this._entriesSortComparator);
+        insertObjectIntoSortedArray(resourceEntry, this._entries, this._entriesSortComparator);
 
-        if (this._passFilter(entry)) {
-            insertObjectIntoSortedArray(entry, this._filteredEntries, this._entriesSortComparator);
+        if (this._passFilter(resourceEntry)) {
+            if (WI.settings.groupByDOMNode.value)
+                this._updateFilteredEntries();
+            else
+                insertObjectIntoSortedArray(resourceEntry, this._filteredEntries, this._entriesSortComparator);
 
             // Probably a useless optimization here, but if we only added this row to the end
             // we may avoid recreating all visible rows by saying as such.
-            if (this._filteredEntries.lastValue === entry)
+            if (this._filteredEntries.lastValue === resourceEntry)
                 this._table.reloadDataAddedToEndOnly();
             else
-                this._table.reloadData();
+                this._reloadTable();
         }
     }
 
@@ -1198,6 +1500,7 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
             transferSize: !isNaN(resource.networkTotalTransferSize) ? resource.networkTotalTransferSize : resource.estimatedTotalTransferSize,
             time: resource.totalDuration,
             protocol: resource.protocol,
+            initiator: resource.initiatorSourceCodeLocation ? resource.initiatorSourceCodeLocation.displayLocationString() : "",
             priority: resource.priority,
             remoteAddress: resource.remoteAddress,
             connectionIdentifier: resource.connectionIdentifier,
@@ -1205,6 +1508,62 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         };
     }
 
+    _entryForDOMNode(domNode)
+    {
+        return {
+            domNode,
+            initiatedResourceEntries: [],
+            expanded: true,
+        };
+    }
+
+    _tryLinkResourceToDOMNode(resourceEntry)
+    {
+        let resource = resourceEntry.resource;
+        if (!resource || !resource.initiatorNode)
+            return;
+
+        let nodeEntry = this._domNodeEntries.get(resource.initiatorNode);
+        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)
+            this._generateSortComparator();
+
+        insertObjectIntoSortedArray(resourceEntry, nodeEntry.initiatedResourceEntries, this._entriesSortComparator);
+    }
+
+    _uniqueValuesForDOMNodeEntry(nodeEntry, accessor)
+    {
+        let resourceEntries = nodeEntry.initiatedResourceEntries;
+        if (!resourceEntries)
+            return null;
+
+        return resourceEntries.reduce((accumulator, current) => {
+            let value = accessor(current);
+            if (value || typeof value === "number")
+                accumulator.add(value);
+            return accumulator;
+        }, 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;
@@ -1241,10 +1600,10 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
             && this._passURLFilter(entry);
     }
 
-    _updateSortAndFilteredEntries()
+    _updateSort()
     {
-        this._entries = this._entries.sort(this._entriesSortComparator);
-        this._updateFilteredEntries();
+        if (this._entriesSortComparator)
+            this._entries = this._entries.sort(this._entriesSortComparator);
     }
 
     _updateFilteredEntries()
@@ -1254,12 +1613,47 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         else
             this._filteredEntries = this._entries.slice();
 
-        this._restoreSelectedRow();
+        if (WI.settings.groupByDOMNode.value) {
+            for (let nodeEntry of this._domNodeEntries.values()) {
+                if (nodeEntry.initiatedResourceEntries.length < 2 && !nodeEntry.domNode.domEvents.length)
+                    continue;
+
+                let firstIndex = Infinity;
+                for (let resourceEntry of nodeEntry.initiatedResourceEntries) {
+                    if (this._hasActiveFilter() && !this._passFilter(resourceEntry))
+                        continue;
+
+                    let index = this._filteredEntries.indexOf(resourceEntry);
+                    if (index >= 0 && index < firstIndex)
+                        firstIndex = index;
+                }
+
+                if (!isFinite(firstIndex))
+                    continue;
+
+                this._filteredEntries.insertAtIndex(nodeEntry, firstIndex);
+            }
+
+            this._filteredEntries = this._filteredEntries.filter((entry) => {
+                if (entry.resource && entry.resource.initiatorNode) {
+                    let nodeEntry = this._domNodeEntries.get(entry.resource.initiatorNode);
+                    if (!nodeEntry.expanded)
+                        return false;
+                }
+                return true;
+            });
+        }
 
         this._updateURLFilterActiveIndicator();
         this._updateEmptyFilterResultsMessage();
     }
 
+    _reloadTable()
+    {
+        this._table.reloadData();
+        this._restoreSelectedRow();
+    }
+
     _generateTypeFilter()
     {
         let selectedItems = this._typeFilterScopeBar.selectedItems;
@@ -1288,7 +1682,7 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         console.assert(!this._hasActiveFilter());
 
         this._updateFilteredEntries();
-        this._table.reloadData();
+        this._reloadTable();
     }
 
     _areFilterListsIdentical(listA, listB)
@@ -1318,11 +1712,23 @@ 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();
-        this._table.reloadData();
+        this._reloadTable();
+    }
+
+    _handleGroupByDOMNodeCheckedDidChange(event)
+    {
+        WI.settings.groupByDOMNode.value = this._groupByDOMNodeNavigationItem.checked;
+
+        if (!WI.settings.groupByDOMNode.value)
+            this._table.element.classList.remove("grouped");
+
+        this._updateSort();
+        this._updateFilteredEntries();
+        this._reloadTable();
     }
 
     _urlFilterDidChange(event)
@@ -1332,7 +1738,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) {
@@ -1342,7 +1748,7 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
             this._activeURLFilterResources.clear();
 
             this._updateFilteredEntries();
-            this._table.reloadData();
+            this._reloadTable();
             return;
         }
 
@@ -1356,22 +1762,23 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
             this._checkURLFilterAgainstResource(entry.resource);
 
         this._updateFilteredEntries();
-        this._table.reloadData();
+        this._reloadTable();
     }
 
     _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.clearSelectedRow();
+            this._table.deselectAll();
             return;
         }
 
         this._table.selectRow(rowIndex);
+        this._showDetailView(this._selectedObject);
     }
 
     _HARResources()
@@ -1390,7 +1797,7 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         }
 
         WI.HARBuilder.buildArchive(resources).then((har) => {
-            let mainFrame = WI.frameResourceManager.mainFrame;
+            let mainFrame = WI.networkManager.mainFrame;
             let archiveName = mainFrame.mainResource.urlComponents.host || mainFrame.mainResource.displayName || "Archive";
             let url = "web-inspector:///" + encodeURI(archiveName) + ".har";
             WI.saveDataToFile({
@@ -1401,12 +1808,19 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         }).catch(handlePromiseException);
     }
 
-    _waterfallPopoverContentForResource(resource)
+    _waterfallPopoverContent()
     {
         let contentElement = document.createElement("div");
-        contentElement.className = "waterfall-popover";
+        contentElement.classList.add("waterfall-popover-content");
+        return contentElement;
+    }
+
+    _waterfallPopoverContentForResourceEntry(resourceEntry)
+    {
+        let contentElement = this._waterfallPopoverContent();
 
-        if (!resource.hasResponse() || !resource.timingData.startTime || !resource.timingData.responseEnd) {
+        let resource = resourceEntry.resource;
+        if (!resource.hasResponse() || !resource.firstTimestamp || !resource.lastTimestamp) {
             contentElement.textContent = WI.UIString("Resource has no timing data");
             return contentElement;
         }
@@ -1418,18 +1832,46 @@ 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;
-            this._waterfallPopover.backgroundStyle = WI.Popover.BackgroundStyle.White;
+            this._waterfallPopover.element.classList.add("waterfall-popover");
         }
 
         if (this._waterfallPopover.visible)
             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();
@@ -1459,7 +1901,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);
     }