Web Inspector: Network: detail view reverts to "Response" when new requests are added
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / NetworkTableContentView.js
index b0fa7e1..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,25 +85,38 @@ 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._textFilterSearchId = 0;
-        this._textFilterSearchText = null;
-        this._textFilterIsActive = false;
+        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._textFilterNavigationItem = new WI.FilterBarNavigationItem;
-        this._textFilterNavigationItem.filterBar.incremental = false;
-        this._textFilterNavigationItem.filterBar.addEventListener(WI.FilterBar.Event.FilterDidChange, this._textFilterDidChange, this);
-        this._textFilterNavigationItem.filterBar.placeholder = WI.UIString("Filter Full URL and Text");
+        this._urlFilterSearchText = null;
+        this._urlFilterSearchRegex = null;
+        this._urlFilterIsActive = false;
+
+        this._urlFilterNavigationItem = new WI.FilterBarNavigationItem;
+        this._urlFilterNavigationItem.filterBar.addEventListener(WI.FilterBar.Event.FilterDidChange, this._urlFilterDidChange, this);
+        this._urlFilterNavigationItem.filterBar.placeholder = WI.UIString("Filter Full URL");
 
         this._activeTypeFilters = this._generateTypeFilter();
-        this._activeTextFilterResources = new Set;
+        this._activeURLFilterResources = new Set;
 
         this._emptyFilterResultsMessageElement = null;
 
+        this._clearOnLoadNavigationItem = new WI.CheckboxNavigationItem("perserve-log", WI.UIString("Preserve Log"), !WI.settings.clearNetworkOnNavigate.value);
+        this._clearOnLoadNavigationItem.tooltip = WI.UIString("Do not clear network items on new page loads");
+        this._clearOnLoadNavigationItem.addEventListener(WI.CheckboxNavigationItem.Event.CheckedDidChange, () => { WI.settings.clearNetworkOnNavigate.value = !WI.settings.clearNetworkOnNavigate.value; });
+        WI.settings.clearNetworkOnNavigate.addEventListener(WI.Setting.Event.Changed, this._clearNetworkOnNavigateSettingChanged, this);
+
         this._harExportNavigationItem = new WI.ButtonNavigationItem("har-export", WI.UIString("Export"), "Images/Export.svg", 15, 15);
         this._harExportNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText;
-        this._harExportNavigationItem.toolTip = WI.UIString("HAR Export (%s)").format(WI.saveKeyboardShortcut.displayName);
+        this._harExportNavigationItem.tooltip = WI.UIString("HAR Export (%s)").format(WI.saveKeyboardShortcut.displayName);
         this._harExportNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, () => { this._exportHAR(); });
 
+        this._checkboxsNavigationItemGroup = new WI.GroupNavigationItem([this._clearOnLoadNavigationItem, new WI.DividerNavigationItem]);
+        this._checkboxsNavigationItemGroup.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
+
+        this._buttonsNavigationItemGroup = new WI.GroupNavigationItem([this._harExportNavigationItem, new WI.DividerNavigationItem]);
+        this._buttonsNavigationItemGroup.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
+
         // COMPATIBILITY (iOS 10.3): Network.setDisableResourceCaching did not exist.
         if (window.NetworkAgent && NetworkAgent.setResourceCachingDisabled) {
             let toolTipForDisableResourceCache = WI.UIString("Ignore the resource cache when loading resources");
@@ -119,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;
@@ -129,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);
@@ -181,7 +197,7 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
 
     get navigationItems()
     {
-        let items = [this._harExportNavigationItem, new WI.DividerNavigationItem];
+        let items = [this._checkboxsNavigationItemGroup, this._buttonsNavigationItemGroup];
         if (this._disableResourceCacheNavigationItem)
             items.push(this._disableResourceCacheNavigationItem);
         items.push(this._clearNetworkItemsNavigationItem);
@@ -190,11 +206,7 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
 
     get filterNavigationItems()
     {
-        let items = [];
-        if (window.PageAgent)
-            items.push(this._textFilterNavigationItem);
-        items.push(this._typeFilterScopeBar);
-        return items;
+        return [this._urlFilterNavigationItem, this._typeFilterScopeBar, this._groupByDOMNodeNavigationItem];
     }
 
     get supportsSave()
@@ -211,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();
@@ -222,25 +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.frameResourceManager.removeEventListener(WI.FrameResourceManager.Event.MainFrameDidChange, this._mainFrameDidChange, this);
+        WI.settings.clearNetworkOnNavigate.removeEventListener(null, null, this);
+        WI.networkManager.removeEventListener(WI.NetworkManager.Event.MainFrameDidChange, this._mainFrameDidChange, this);
         WI.timelineManager.persistentNetworkTimeline.removeEventListener(WI.Timeline.Event.RecordAdded, this._networkTimelineRecordAdded, this);
 
         super.closed();
@@ -252,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;
@@ -263,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();
         }
     }
 
@@ -274,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
@@ -308,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)
@@ -337,30 +350,56 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         WI.appendContextMenuItemsForSourceCode(contextMenu, entry.resource);
 
         contextMenu.appendSeparator();
-        contextMenu.appendItem(WI.UIString("Export HAR"), this._exportHAR);
+        contextMenu.appendItem(WI.UIString("Export HAR"), () => { this._exportHAR(); });
+    }
+
+    tableShouldSelectRow(table, cell, column, rowIndex)
+    {
+        return column === this._nameColumn;
     }
 
-    tableSelectedRowChanged(table, rowIndex)
+    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":
@@ -370,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;
@@ -420,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"));
@@ -428,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");
@@ -482,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;
@@ -506,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;
         }
 
@@ -528,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.
@@ -539,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");
@@ -572,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] || "");
@@ -638,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:
@@ -647,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
@@ -711,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,
@@ -771,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);
@@ -847,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()
@@ -874,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);
         }
@@ -898,46 +1164,30 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         this.needsLayout();
     }
 
-    _checkTextFilterAgainstFinishedResource(resource)
+    _checkURLFilterAgainstResource(resource)
     {
-        let frame = resource.parentFrame;
-        if (!frame)
-            return;
-
-        let searchQuery = this._textFilterSearchText;
-        if (resource.url.includes(searchQuery)) {
-            this._activeTextFilterResources.add(resource);
+        if (this._urlFilterSearchRegex.test(resource.url)) {
+            this._activeURLFilterResources.add(resource);
             return;
         }
 
-        let searchId = this._textFilterSearchId;
-
-        const isCaseSensitive = true;
-        const isRegex = false;
-        PageAgent.searchInResource(frame.id, resource.url, searchQuery, isCaseSensitive, isRegex, resource.requestIdentifier, (error, searchResults) => {
-            if (searchId !== this._textFilterSearchId)
-                return;
-
-            if (error || !searchResults || !searchResults.length)
+        for (let redirect of resource.redirects) {
+            if (this._urlFilterSearchRegex.test(redirect.url)) {
+                this._activeURLFilterResources.add(resource);
                 return;
-
-            this._activeTextFilterResources.add(resource);
-
-            this._pendingFilter = true;
-            this.needsLayout();
-        });
-    }
-
-    _checkTextFilterAgainstFailedResource(resource)
-    {
-        let searchQuery = this._textFilterSearchText;
-        if (resource.url.includes(searchQuery))
-            this._activeTextFilterResources.add(resource);
+            }
+        }
     }
 
-    _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)
@@ -946,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()
@@ -962,38 +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";
@@ -1006,17 +1277,17 @@ 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";
     }
 
-    _updateTextFilterActiveIndicator()
+    _updateURLFilterActiveIndicator()
     {
-        this._textFilterNavigationItem.filterBar.indicatingActive = this._hasTextFilter();
+        this._urlFilterNavigationItem.filterBar.indicatingActive = this._hasURLFilter();
     }
 
     _updateEmptyFilterResultsMessage()
@@ -1030,17 +1301,12 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
     _showEmptyFilterResultsMessage()
     {
         if (!this._emptyFilterResultsMessageElement) {
-            let message = WI.UIString("No Filter Results");
             let buttonElement = document.createElement("button");
             buttonElement.textContent = WI.UIString("Clear filters");
             buttonElement.addEventListener("click", () => { this._resetFilters(); });
 
-            this._emptyFilterResultsMessageElement = document.createElement("div");
-            this._emptyFilterResultsMessageElement.className = "empty-content-placeholder";
-
-            let messageElement = this._emptyFilterResultsMessageElement.appendChild(document.createElement("div"));
-            messageElement.className = "message";
-            messageElement.append(message, document.createElement("br"), buttonElement);
+            this._emptyFilterResultsMessageElement = WI.createMessageTextView(WI.UIString("No Filter Results"));
+            this._emptyFilterResultsMessageElement.appendChild(buttonElement);
         }
 
         this.element.appendChild(this._emptyFilterResultsMessageElement);
@@ -1064,6 +1330,11 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         this._emptyFilterResultsMessageElement.style.width = width + "px";
     }
 
+    _clearNetworkOnNavigateSettingChanged()
+    {
+        this._clearOnLoadNavigationItem.checked = !WI.settings.clearNetworkOnNavigate.value;
+    }
+
     _resourceCachingDisabledSettingChanged()
     {
         this._disableResourceCacheNavigationItem.activated = WI.resourceCachingDisabledSetting.value;
@@ -1100,8 +1371,8 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         if (resource.timingData.responseEnd > this._waterfallEndTime)
             this._waterfallEndTime = resource.timingData.responseEnd;
 
-        if (this._hasTextFilter())
-            this._checkTextFilterAgainstFinishedResource(resource);
+        if (this._hasURLFilter())
+            this._checkURLFilterAgainstResource(resource);
 
         this.needsLayout();
     }
@@ -1116,8 +1387,8 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         if (resource.timingData.responseEnd > this._waterfallEndTime)
             this._waterfallEndTime = resource.timingData.responseEnd;
 
-        if (this._hasTextFilter())
-            this._checkTextFilterAgainstFailedResource(resource);
+        if (this._hasURLFilter())
+            this._checkURLFilterAgainstResource(resource);
 
         this.needsLayout();
     }
@@ -1143,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;
 
@@ -1175,29 +1446,37 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
             return;
         }
 
-        let entry = this._entryForResource(resource);
+        let resourceEntry = this._entryForResource(resource);
+
+        this._tryLinkResourceToDOMNode(resourceEntry);
 
-        // Default sort has fast path.
-        if (this._isDefaultSort() || !this._entriesSortComparator) {
-            this._entries.push(entry);
-            if (this._passFilter(entry)) {
-                this._filteredEntries.push(entry);
+        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();
         }
     }
 
@@ -1219,8 +1498,9 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
             cached: resource.cached,
             resourceSize: resource.size,
             transferSize: !isNaN(resource.networkTotalTransferSize) ? resource.networkTotalTransferSize : resource.estimatedTotalTransferSize,
-            time: resource.duration,
+            time: resource.totalDuration,
             protocol: resource.protocol,
+            initiator: resource.initiatorSourceCodeLocation ? resource.initiatorSourceCodeLocation.displayLocationString() : "",
             priority: resource.priority,
             remoteAddress: resource.remoteAddress,
             connectionIdentifier: resource.connectionIdentifier,
@@ -1228,20 +1508,76 @@ 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;
     }
 
-    _hasTextFilter()
+    _hasURLFilter()
     {
-        return this._textFilterIsActive;
+        return this._urlFilterIsActive;
     }
 
     _hasActiveFilter()
     {
         return this._hasTypeFilter()
-            || this._hasTextFilter();
+            || this._hasURLFilter();
     }
 
     _passTypeFilter(entry)
@@ -1251,23 +1587,23 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         return this._activeTypeFilters.some((checker) => checker(entry.resource.type));
     }
 
-    _passTextFilter(entry)
+    _passURLFilter(entry)
     {
-        if (!this._hasTextFilter())
+        if (!this._hasURLFilter())
             return true;
-        return this._activeTextFilterResources.has(entry.resource);
+        return this._activeURLFilterResources.has(entry.resource);
     }
 
     _passFilter(entry)
     {
         return this._passTypeFilter(entry)
-            && this._passTextFilter(entry);
+            && 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()
@@ -1277,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._updateTextFilterActiveIndicator();
+        this._updateURLFilterActiveIndicator();
         this._updateEmptyFilterResultsMessage();
     }
 
+    _reloadTable()
+    {
+        this._table.reloadData();
+        this._restoreSelectedRow();
+    }
+
     _generateTypeFilter()
     {
         let selectedItems = this._typeFilterScopeBar.selectedItems;
@@ -1296,14 +1667,13 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
     {
         console.assert(this._hasActiveFilter());
 
-        // Clear text filter.
-        this._textFilterSearchId++;
-        this._textFilterNavigationItem.filterBar.indicatingProgress = false;
-        this._textFilterSearchText = null;
-        this._textFilterIsActive = false;
-        this._activeTextFilterResources.clear();
-        this._textFilterNavigationItem.filterBar.clear();
-        console.assert(!this._hasTextFilter());
+        // Clear url filter.
+        this._urlFilterSearchText = null;
+        this._urlFilterSearchRegex = null;
+        this._urlFilterIsActive = false;
+        this._activeURLFilterResources.clear();
+        this._urlFilterNavigationItem.filterBar.clear();
+        console.assert(!this._hasURLFilter());
 
         // Clear type filter.
         this._typeFilterScopeBar.resetToDefault();
@@ -1312,7 +1682,7 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         console.assert(!this._hasActiveFilter());
 
         this._updateFilteredEntries();
-        this._table.reloadData();
+        this._reloadTable();
     }
 
     _areFilterListsIdentical(listA, listB)
@@ -1342,109 +1712,73 @@ 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();
     }
 
-    _textFilterDidChange(event)
+    _urlFilterDidChange(event)
     {
-        let searchQuery = this._textFilterNavigationItem.filterBar.filters.text;
-        if (searchQuery === this._textFilterSearchText)
+        let searchQuery = this._urlFilterNavigationItem.filterBar.filters.text;
+        if (searchQuery === this._urlFilterSearchText)
             return;
 
         // Even if the selected resource would still be visible, lets close the detail view if a filter changes.
-        this._hideResourceDetailView();
-
-        let searchId = ++this._textFilterSearchId;
+        this._hideDetailView();
 
         // Search cleared.
         if (!searchQuery) {
-            this._textFilterNavigationItem.filterBar.indicatingProgress = false;
-            this._textFilterSearchText = null;
-            this._textFilterIsActive = false;
-            this._activeTextFilterResources.clear();
+            this._urlFilterSearchText = null;
+            this._urlFilterSearchRegex = null;
+            this._urlFilterIsActive = false;
+            this._activeURLFilterResources.clear();
 
             this._updateFilteredEntries();
-            this._table.reloadData();
+            this._reloadTable();
             return;
         }
 
-        this._textFilterSearchText = searchQuery;
-        this._textFilterNavigationItem.filterBar.indicatingProgress = true;
+        this._urlFilterIsActive = true;
+        this._urlFilterSearchText = searchQuery;
+        this._urlFilterSearchRegex = new RegExp(searchQuery.escapeForRegExp(), "i");
 
-        // NetworkTable text filter currently searches:
-        //   - Resource URL
-        //   - Resource Text Content
-        // It does not search all the content in the table (like mimeType, headers, etc).
-        // For those we should provide more custom filters.
+        this._activeURLFilterResources.clear();
 
-        const isCaseSensitive = true;
-        const isRegex = false;
-        PageAgent.searchInResources(searchQuery, isCaseSensitive, isRegex, (error, searchResults) => {
-            if (searchId !== this._textFilterSearchId)
-                return;
-
-            this._textFilterIsActive = true;
-            this._activeTextFilterResources.clear();
-            this._textFilterNavigationItem.filterBar.indicatingProgress = false;
-
-            // Add resources based on URL.
-            for (let entry of this._entries) {
-                let resource = entry.resource;
-                if (resource.url.includes(searchQuery))
-                    this._activeTextFilterResources.add(resource);
-            }
+        for (let entry of this._entries)
+            this._checkURLFilterAgainstResource(entry.resource);
 
-            // Add resources based on content.
-            if (!error) {
-                for (let {url, frameId, requestId} of searchResults) {
-                    if (requestId) {
-                        let resource = WI.frameResourceManager.resourceForRequestIdentifier(requestId);
-                        if (resource) {
-                            this._activeTextFilterResources.add(resource);
-                            continue;
-                        }
-                    }
-
-                    if (frameId && url) {
-                        let frame = WI.frameResourceManager.frameForIdentifier(frameId);
-                        if (frame) {
-                            if (frame.mainResource.url === url) {
-                                this._activeTextFilterResources.add(frame.mainResource);
-                                continue;
-                            }
-                            let resource = frame.resourceForURL(url);
-                            if (resource) {
-                                this._activeTextFilterResources.add(resource);
-                                continue;
-                            }
-                        }
-                    }
-                }
-            }
-
-            // Apply.
-            this._updateFilteredEntries();
-            this._table.reloadData();
-        });
+        this._updateFilteredEntries();
+        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()
@@ -1463,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({
@@ -1474,35 +1808,70 @@ 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;
+    }
 
-        if (!resource.hasResponse() || !resource.timingData.startTime || !resource.timingData.responseEnd) {
+    _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;
         }
 
-        let breakdownView = new WI.ResourceTimingBreakdownView(resource);
+        let breakdownView = new WI.ResourceTimingBreakdownView(resource, 300);
         contentElement.appendChild(breakdownView.element);
         breakdownView.updateLayout();
 
         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();
@@ -1519,6 +1888,8 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         };
 
         let targetFrame = calculateTargetFrame();
+        if (!targetFrame)
+            return;
         if (!targetFrame.size.width && !targetFrame.size.height)
             return;
 
@@ -1530,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);
     }