Web Inspector: Provide UIString descriptions to improve localizations
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / NetworkTableContentView.js
1 /*
2  * Copyright (C) 2017 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentView
27 {
28     constructor(representedObject, extraArguments)
29     {
30         super(representedObject);
31
32         // Collections contain the set of values needed to render the table.
33         // The main collection reflects the main target's live activity.
34         // We create other collections for HAR imports.
35         this._collections = [];
36         this._activeCollection = null;
37         this._mainCollection = this._addCollection();
38         this._setActiveCollection(this._mainCollection);
39
40         this._entriesSortComparator = null;
41
42         this._pendingFilter = false;
43         this._showingRepresentedObjectCookie = null;
44
45         this._table = null;
46         this._nameColumnWidthSetting = new WI.Setting("network-table-content-view-name-column-width", 250);
47
48         this._selectedObject = null;
49         this._detailView = null;
50         this._detailViewMap = new Map;
51
52         this._domNodeEntries = new Map;
53
54         this._waterfallTimelineRuler = null;
55         this._waterfallPopover = null;
56
57         // FIXME: Network Timeline.
58         // FIXME: Throttling.
59
60         this._typeFilterScopeBarItemAll = new WI.ScopeBarItem("network-type-filter-all", WI.UIString("All"), {exclusive: true});
61         let typeFilterScopeBarItems = [this._typeFilterScopeBarItemAll];
62
63         let uniqueTypes = [
64             ["Document", (type) => type === WI.Resource.Type.Document],
65             ["Stylesheet", (type) => type === WI.Resource.Type.Stylesheet],
66             ["Image", (type) => type === WI.Resource.Type.Image],
67             ["Font", (type) => type === WI.Resource.Type.Font],
68             ["Script", (type) => type === WI.Resource.Type.Script],
69             ["XHR", (type) => type === WI.Resource.Type.XHR || type === WI.Resource.Type.Fetch],
70             ["Other", (type) => {
71                 return type !== WI.Resource.Type.Document
72                     && type !== WI.Resource.Type.Stylesheet
73                     && type !== WI.Resource.Type.Image
74                     && type !== WI.Resource.Type.Font
75                     && type !== WI.Resource.Type.Script
76                     && type !== WI.Resource.Type.XHR
77                     && type !== WI.Resource.Type.Fetch;
78             }],
79         ];
80         for (let [key, checker] of uniqueTypes) {
81             let type = WI.Resource.Type[key];
82             let scopeBarItem = new WI.ScopeBarItem("network-type-filter-" + key, WI.NetworkTableContentView.shortDisplayNameForResourceType(type));
83             scopeBarItem.__checker = checker;
84             typeFilterScopeBarItems.push(scopeBarItem);
85         }
86
87         this._typeFilterScopeBar = new WI.ScopeBar("network-type-filter-scope-bar", typeFilterScopeBarItems, typeFilterScopeBarItems[0]);
88         this._typeFilterScopeBar.addEventListener(WI.ScopeBar.Event.SelectionChanged, this._typeFilterScopeBarSelectionChanged, this);
89
90         if (WI.MediaInstrument.supported()) {
91             this._groupMediaRequestsByDOMNodeNavigationItem = new WI.CheckboxNavigationItem("group-media-requests", WI.UIString("Group Media Requests"), WI.settings.groupMediaRequestsByDOMNode.value);
92             this._groupMediaRequestsByDOMNodeNavigationItem.addEventListener(WI.CheckboxNavigationItem.Event.CheckedDidChange, this._handleGroupMediaRequestsByDOMNodeCheckedDidChange, this);
93         } else
94             WI.settings.groupMediaRequestsByDOMNode.value = false;
95
96         this._urlFilterSearchText = null;
97         this._urlFilterSearchRegex = null;
98         this._urlFilterIsActive = false;
99
100         this._urlFilterNavigationItem = new WI.FilterBarNavigationItem;
101         this._urlFilterNavigationItem.filterBar.addEventListener(WI.FilterBar.Event.FilterDidChange, this._urlFilterDidChange, this);
102         this._urlFilterNavigationItem.filterBar.placeholder = WI.UIString("Filter Full URL");
103
104         this._activeTypeFilters = this._generateTypeFilter();
105         this._activeURLFilterResources = new Set;
106
107         this._emptyFilterResultsMessageElement = null;
108
109         this._clearOnLoadNavigationItem = new WI.CheckboxNavigationItem("preserve-log", WI.UIString("Preserve Log"), !WI.settings.clearNetworkOnNavigate.value);
110         this._clearOnLoadNavigationItem.tooltip = WI.UIString("Do not clear network items on new page loads");
111         this._clearOnLoadNavigationItem.addEventListener(WI.CheckboxNavigationItem.Event.CheckedDidChange, () => { WI.settings.clearNetworkOnNavigate.value = !WI.settings.clearNetworkOnNavigate.value; });
112         WI.settings.clearNetworkOnNavigate.addEventListener(WI.Setting.Event.Changed, this._clearNetworkOnNavigateSettingChanged, this);
113
114         this._harImportNavigationItem = new WI.ButtonNavigationItem("har-import", WI.UIString("Import"), "Images/Import.svg", 15, 15);
115         this._harImportNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText;
116         this._harImportNavigationItem.tooltip = WI.UIString("HAR Import");
117         this._harImportNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, () => {
118             this._importHAR();
119         });
120
121         this._harExportNavigationItem = new WI.ButtonNavigationItem("har-export", WI.UIString("Export"), "Images/Export.svg", 15, 15);
122         this._harExportNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText;
123         this._harExportNavigationItem.tooltip = WI.UIString("HAR Export (%s)").format(WI.saveKeyboardShortcut.displayName);
124         this._harExportNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, () => {
125             this._exportHAR();
126         });
127
128         this._collectionsPathNavigationItem = new WI.HierarchicalPathNavigationItem;
129         this._collectionsPathNavigationItem.addEventListener(WI.HierarchicalPathNavigationItem.Event.PathComponentWasSelected, this._collectionsHierarchicalPathComponentWasSelected, this);
130
131         this._pathComponentsMap = new Map;
132         this._lastPathComponent = null;
133         let pathComponent = this._addCollectionPathComponent(this._mainCollection, WI.UIString("Live Activity"), "network-overview-icon");
134         this._collectionsPathNavigationItem.components = [pathComponent];
135
136         this._checkboxesNavigationItemGroup = new WI.GroupNavigationItem([this._clearOnLoadNavigationItem, new WI.DividerNavigationItem]);
137         this._checkboxesNavigationItemGroup.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
138
139         this._pathComponentsNavigationItemGroup = new WI.GroupNavigationItem([this._collectionsPathNavigationItem, new WI.DividerNavigationItem]);
140         this._pathComponentsNavigationItemGroup.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
141         this._pathComponentsNavigationItemGroup.hidden = true;
142
143         this._buttonsNavigationItemGroup = new WI.GroupNavigationItem([this._harImportNavigationItem, this._harExportNavigationItem, new WI.DividerNavigationItem]);
144         this._buttonsNavigationItemGroup.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
145
146         // COMPATIBILITY (iOS 10.3): Network.setDisableResourceCaching did not exist.
147         if (window.NetworkAgent && NetworkAgent.setResourceCachingDisabled) {
148             let toolTipForDisableResourceCache = WI.UIString("Ignore the resource cache when loading resources");
149             let activatedToolTipForDisableResourceCache = WI.UIString("Use the resource cache when loading resources");
150             this._disableResourceCacheNavigationItem = new WI.ActivateButtonNavigationItem("disable-resource-cache", toolTipForDisableResourceCache, activatedToolTipForDisableResourceCache, "Images/IgnoreCaches.svg", 16, 16);
151             this._disableResourceCacheNavigationItem.activated = WI.settings.resourceCachingDisabled.value;
152
153             this._disableResourceCacheNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._toggleDisableResourceCache, this);
154             WI.settings.resourceCachingDisabled.addEventListener(WI.Setting.Event.Changed, this._resourceCachingDisabledSettingChanged, this);
155         }
156
157         this._clearNetworkItemsNavigationItem = new WI.ButtonNavigationItem("clear-network-items", WI.UIString("Clear Network Items (%s)").format(WI.clearKeyboardShortcut.displayName), "Images/NavigationItemTrash.svg", 15, 15);
158         this._clearNetworkItemsNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, () => {
159             this.reset();
160         });
161
162         WI.Target.addEventListener(WI.Target.Event.ResourceAdded, this._handleResourceAdded, this);
163         WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
164         WI.Frame.addEventListener(WI.Frame.Event.ResourceWasAdded, this._handleResourceAdded, this);
165         WI.Frame.addEventListener(WI.Frame.Event.ChildFrameWasAdded, this._handleFrameWasAdded, this);
166         WI.Resource.addEventListener(WI.Resource.Event.LoadingDidFinish, this._resourceLoadingDidFinish, this);
167         WI.Resource.addEventListener(WI.Resource.Event.LoadingDidFail, this._resourceLoadingDidFail, this);
168         WI.Resource.addEventListener(WI.Resource.Event.TransferSizeDidChange, this._resourceTransferSizeDidChange, this);
169         WI.networkManager.addEventListener(WI.NetworkManager.Event.MainFrameDidChange, this._mainFrameDidChange, this);
170
171         this._needsInitialPopulate = true;
172
173         // FIXME: This is working around the order of events. Normal page navigation
174         // triggers a MainResource change and then a MainFrame change. Page Transition
175         // triggers a MainFrame change then a MainResource change.
176         this._transitioningPageTarget = false;
177
178         WI.notifications.addEventListener(WI.Notification.TransitionPageTarget, this._transitionPageTarget, this);
179     }
180
181     // Static
182
183     static displayNameForResource(resource)
184     {
185         if (resource.type === WI.Resource.Type.Image || resource.type === WI.Resource.Type.Font || resource.type === WI.Resource.Type.Other) {
186             let fileExtension;
187             if (resource.mimeType)
188                 fileExtension = WI.fileExtensionForMIMEType(resource.mimeType);
189             if (!fileExtension)
190                 fileExtension = WI.fileExtensionForURL(resource.url);
191             if (fileExtension)
192                 return fileExtension;
193         }
194
195         return WI.NetworkTableContentView.shortDisplayNameForResourceType(resource.type).toLowerCase();
196     }
197
198     static shortDisplayNameForResourceType(type)
199     {
200         switch (type) {
201         case WI.Resource.Type.Document:
202             return WI.UIString("Document");
203         case WI.Resource.Type.Stylesheet:
204             return "CSS";
205         case WI.Resource.Type.Image:
206             return WI.UIString("Image");
207         case WI.Resource.Type.Font:
208             return WI.UIString("Font");
209         case WI.Resource.Type.Script:
210             return "JS";
211         case WI.Resource.Type.XHR:
212             return "XHR";
213         case WI.Resource.Type.Fetch:
214             return WI.repeatedUIString.fetch();
215         case WI.Resource.Type.Ping:
216             return WI.UIString("Ping");
217         case WI.Resource.Type.Beacon:
218             return WI.UIString("Beacon");
219         case WI.Resource.Type.WebSocket:
220         case WI.Resource.Type.Other:
221             return WI.UIString("Other");
222         default:
223             console.error("Unknown resource type", type);
224             return null;
225         }
226     }
227
228     static get nodeWaterfallDOMEventSize() { return 8; }
229
230     // Public
231
232     get selectionPathComponents()
233     {
234         return null;
235     }
236
237     get navigationItems()
238     {
239         let items = [this._checkboxesNavigationItemGroup, this._pathComponentsNavigationItemGroup, this._buttonsNavigationItemGroup];
240         if (this._disableResourceCacheNavigationItem)
241             items.push(this._disableResourceCacheNavigationItem);
242         items.push(this._clearNetworkItemsNavigationItem);
243         return items;
244     }
245
246     get filterNavigationItems()
247     {
248         let navigationItems = [this._urlFilterNavigationItem, this._typeFilterScopeBar];
249         if (WI.MediaInstrument.supported())
250             navigationItems.push(this._groupMediaRequestsByDOMNodeNavigationItem);
251         return navigationItems;
252     }
253
254     get supportsSave()
255     {
256         return this._canExportHAR();
257     }
258
259     get saveData()
260     {
261         return {customSaveHandler: () => { this._exportHAR(); }};
262     }
263
264     shown()
265     {
266         super.shown();
267
268         if (this._detailView)
269             this._detailView.shown();
270
271         if (this._table)
272             this._table.restoreScrollPosition();
273     }
274
275     hidden()
276     {
277         this._hidePopover();
278
279         if (this._detailView)
280             this._detailView.hidden();
281
282         super.hidden();
283     }
284
285     closed()
286     {
287         for (let detailView of this._detailViewMap.values())
288             detailView.dispose();
289         this._detailViewMap.clear();
290
291         this._domNodeEntries.clear();
292
293         this._hidePopover();
294         this._hideDetailView();
295
296         WI.Target.removeEventListener(null, null, this);
297         WI.Frame.removeEventListener(null, null, this);
298         WI.Resource.removeEventListener(null, null, this);
299         WI.settings.resourceCachingDisabled.removeEventListener(null, null, this);
300         WI.settings.clearNetworkOnNavigate.removeEventListener(null, null, this);
301         WI.networkManager.removeEventListener(WI.NetworkManager.Event.MainFrameDidChange, this._mainFrameDidChange, this);
302
303         super.closed();
304     }
305
306     reset()
307     {
308         this._runForMainCollection((collection) => {
309             this._resetCollection(collection);
310         });
311
312         for (let detailView of this._detailViewMap.values())
313             detailView.dispose();
314         this._detailViewMap.clear();
315
316         this._domNodeEntries.clear();
317
318         this._updateWaterfallTimelineRuler();
319         this._updateExportButton();
320
321         if (this._table) {
322             this._selectedObject = null;
323             this._table.reloadData();
324             this._hidePopover();
325             this._hideDetailView();
326         }
327     }
328
329     showRepresentedObject(representedObject, cookie)
330     {
331         console.assert(representedObject instanceof WI.Resource);
332
333         let rowIndex = this._rowIndexForRepresentedObject(representedObject);
334         if (rowIndex === -1) {
335             this._selectedObject = null;
336             this._table.deselectAll();
337             this._hideDetailView();
338             return;
339         }
340
341         this._showingRepresentedObjectCookie = cookie;
342         this._table.selectRow(rowIndex);
343         this._showingRepresentedObjectCookie = null;
344     }
345
346     // NetworkDetailView delegate
347
348     networkDetailViewClose(networkDetailView)
349     {
350         this._selectedObject = null;
351         this._table.deselectAll();
352         this._hideDetailView();
353     }
354
355     // Table dataSource
356
357     tableIndexForRepresentedObject(table, object)
358     {
359         return this._activeCollection.filteredEntries.indexOf(object);
360     }
361
362     tableRepresentedObjectForIndex(table, index)
363     {
364         console.assert(index >=0 && index < this._activeCollection.filteredEntries.length);
365         return this._activeCollection.filteredEntries[index];
366     }
367
368     tableNumberOfRows(table)
369     {
370         return this._activeCollection.filteredEntries.length;
371     }
372
373     tableSortChanged(table)
374     {
375         this._generateSortComparator();
376
377         if (!this._entriesSortComparator)
378             return;
379
380         this._hideDetailView();
381
382         for (let nodeEntry of this._domNodeEntries.values())
383             nodeEntry.initiatedResourceEntries.sort(this._entriesSortComparator);
384
385         this._updateSort();
386         this._updateFilteredEntries();
387         this._reloadTable();
388     }
389
390     // Table delegate
391
392     tableCellContextMenuClicked(table, cell, column, rowIndex, event)
393     {
394         if (column !== this._nameColumn)
395             return;
396
397         this._table.selectRow(rowIndex);
398
399         let entry = this._activeCollection.filteredEntries[rowIndex];
400         let contextMenu = WI.ContextMenu.createFromEvent(event);
401         WI.appendContextMenuItemsForSourceCode(contextMenu, entry.resource);
402
403         contextMenu.appendSeparator();
404         contextMenu.appendItem(WI.UIString("Export HAR"), () => { this._exportHAR(); }, !this._canExportHAR());
405     }
406
407     tableShouldSelectRow(table, cell, column, rowIndex)
408     {
409         return column === this._nameColumn;
410     }
411
412     tableSelectionDidChange(table)
413     {
414         let rowIndex = table.selectedRow;
415         if (isNaN(rowIndex)) {
416             this._selectedObject = null;
417             this._hideDetailView();
418             return;
419         }
420
421         let entry = this._activeCollection.filteredEntries[rowIndex];
422         if (entry.resource === this._selectedObject || entry.domNode === this._selectedObject)
423             return;
424
425         this._selectedObject = entry.resource || entry.domNode;
426         if (this._selectedObject)
427             this._showDetailView(this._selectedObject);
428         else
429             this._hideDetailView();
430     }
431
432     tablePopulateCell(table, cell, column, rowIndex)
433     {
434         let entry = this._activeCollection.filteredEntries[rowIndex];
435
436         if (entry.resource)
437             cell.classList.toggle("error", entry.resource.hadLoadingError());
438
439         let setTextContent = (accessor) => {
440             let uniqueValues = this._uniqueValuesForDOMNodeEntry(entry, accessor);
441             if (uniqueValues) {
442                 if (uniqueValues.size > 1) {
443                     cell.classList.add("multiple");
444                     cell.textContent = WI.UIString("(multiple)");
445                     return;
446                 }
447
448                 cell.textContent = uniqueValues.values().next().value || emDash;
449                 return;
450             }
451
452             cell.textContent = accessor(entry) || emDash;
453         };
454
455         switch (column.identifier) {
456         case "name":
457             this._populateNameCell(cell, entry);
458             break;
459         case "domain":
460             this._populateDomainCell(cell, entry);
461             break;
462         case "type":
463             setTextContent((resourceEntry) => resourceEntry.displayType);
464             break;
465         case "mimeType":
466             setTextContent((resourceEntry) => resourceEntry.mimeType);
467             break;
468         case "method":
469             setTextContent((resourceEntry) => resourceEntry.method);
470             break;
471         case "scheme":
472             setTextContent((resourceEntry) => resourceEntry.scheme);
473             break;
474         case "status":
475             setTextContent((resourceEntry) => resourceEntry.status);
476             break;
477         case "protocol":
478             setTextContent((resourceEntry) => resourceEntry.protocol);
479             break;
480         case "initiator":
481             this._populateInitiatorCell(cell, entry);
482             break;
483         case "priority":
484             setTextContent((resourceEntry) => WI.Resource.displayNameForPriority(resourceEntry.priority));
485             break;
486         case "remoteAddress":
487             setTextContent((resourceEntry) => resourceEntry.remoteAddress);
488             break;
489         case "connectionIdentifier":
490             setTextContent((resourceEntry) => resourceEntry.connectionIdentifier);
491             break;
492         case "resourceSize": {
493             let resourceSize = entry.resourceSize;
494             let resourceEntries = entry.initiatedResourceEntries;
495             if (resourceEntries)
496                 resourceSize = resourceEntries.reduce((accumulator, current) => accumulator + (current.resourceSize || 0), 0);
497             cell.textContent = isNaN(resourceSize) ? emDash : Number.bytesToString(resourceSize);
498             break;
499         }
500         case "transferSize":
501             this._populateTransferSizeCell(cell, entry);
502             break;
503         case "time": {
504             // FIXME: <https://webkit.org/b/176748> Web Inspector: Frontend sometimes receives resources with negative duration (responseEnd - requestStart)
505             let time = entry.time;
506             let resourceEntries = entry.initiatedResourceEntries;
507             if (resourceEntries)
508                 time = resourceEntries.reduce((accumulator, current) => accumulator + (current.time || 0), 0);
509             cell.textContent = isNaN(time) ? emDash : Number.secondsToString(Math.max(time, 0));
510             break;
511         }
512         case "waterfall":
513             this._populateWaterfallGraph(cell, entry);
514             break;
515         }
516
517         return cell;
518     }
519
520     // Private
521
522     _addCollection()
523     {
524         let collection = {};
525         this._resetCollection(collection);
526         this._collections.push(collection);
527         return collection;
528     }
529
530     _resetCollection(collection)
531     {
532         collection.entries = [];
533         collection.filteredEntries = [];
534         collection.pendingInsertions = [];
535         collection.pendingUpdates = [];
536         collection.waterfallStartTime = NaN;
537         collection.waterfallEndTime = NaN;
538     }
539
540     _setActiveCollection(collection)
541     {
542         console.assert(this._collections.includes(collection));
543
544         if (this._activeCollection === collection)
545             return;
546
547         this._activeCollection = collection;
548     }
549
550     _addCollectionPathComponent(collection, displayName, iconClassName)
551     {
552         let pathComponent = new WI.HierarchicalPathComponent(displayName, iconClassName, collection);
553         this._pathComponentsMap.set(collection, pathComponent);
554
555         if (this._lastPathComponent) {
556             this._lastPathComponent.nextSibling = pathComponent;
557             pathComponent.previousSibling = this._lastPathComponent;
558         }
559
560         this._lastPathComponent = pathComponent;
561
562         if (this._pathComponentsNavigationItemGroup && this._pathComponentsMap.size > 1)
563             this._pathComponentsNavigationItemGroup.hidden = false;
564
565         return pathComponent;
566     }
567
568     _collectionsHierarchicalPathComponentWasSelected(event)
569     {
570         console.assert(event.data.pathComponent instanceof WI.HierarchicalPathComponent);
571
572         let collection = event.data.pathComponent.representedObject;
573
574         this._changeCollection(collection);
575     }
576
577     _changeCollection(collection)
578     {
579         if (collection === this._activeCollection)
580             return;
581
582         this._setActiveCollection(collection);
583
584         let isMain = collection === this._mainCollection;
585         this._checkboxesNavigationItemGroup.hidden = !isMain;
586         this._groupMediaRequestsByDOMNodeNavigationItem.hidden = !isMain;
587         this._clearNetworkItemsNavigationItem.enabled = isMain;
588         this._collectionsPathNavigationItem.components = [this._pathComponentsMap.get(collection)];
589
590         this._updateSort();
591         this._updateActiveFilterResources();
592         this._updateFilteredEntries();
593         this._updateWaterfallTimelineRuler();
594         this._reloadTable();
595         this._hideDetailView();
596
597         this.needsLayout();
598     }
599
600     _populateNameCell(cell, entry)
601     {
602         console.assert(!cell.firstChild, "We expect the cell to be empty.", cell, cell.firstChild);
603
604         function createIconElement() {
605             let iconElement = cell.appendChild(document.createElement("img"));
606             iconElement.className = "icon";
607         }
608
609         let domNode = entry.domNode;
610         if (domNode) {
611             this._table.element.classList.add("grouped");
612
613             cell.classList.add("parent");
614
615             let disclosureElement = cell.appendChild(document.createElement("img"));
616             disclosureElement.classList.add("disclosure");
617             disclosureElement.classList.toggle("expanded", !!entry.expanded);
618             disclosureElement.addEventListener("click", (event) => {
619                 entry.expanded = !entry.expanded;
620
621                 this._updateFilteredEntries();
622                 this._reloadTable();
623             });
624
625             createIconElement();
626
627             cell.classList.add("dom-node");
628             cell.appendChild(WI.linkifyNodeReference(domNode));
629             return;
630         }
631
632         let resource = entry.resource;
633         if (resource.isLoading()) {
634             let statusElement = cell.appendChild(document.createElement("div"));
635             statusElement.className = "status";
636             let spinner = new WI.IndeterminateProgressSpinner;
637             statusElement.appendChild(spinner.element);
638         }
639
640         createIconElement();
641
642         cell.classList.add(WI.ResourceTreeElement.ResourceIconStyleClassName);
643
644         if (WI.settings.groupMediaRequestsByDOMNode.value && resource.initiatorNode) {
645             let nodeEntry = this._domNodeEntries.get(resource.initiatorNode);
646             if (nodeEntry.initiatedResourceEntries.length > 1 || nodeEntry.domNode.domEvents.length)
647                 cell.classList.add("child");
648         }
649
650         let nameElement = cell.appendChild(document.createElement("span"));
651         nameElement.textContent = entry.name;
652
653         let range = resource.requestedByteRange;
654         if (range) {
655             let rangeElement = nameElement.appendChild(document.createElement("span"));
656             rangeElement.classList.add("range");
657             rangeElement.textContent = WI.UIString("Byte Range %s\u2013%s").format(range.start, range.end);
658         }
659
660         cell.title = resource.url;
661         cell.classList.add(WI.Resource.classNameForResource(resource));
662     }
663
664     _populateDomainCell(cell, entry)
665     {
666         console.assert(!cell.firstChild, "We expect the cell to be empty.", cell, cell.firstChild);
667
668         function createIconAndText(scheme, domain) {
669             let secure = scheme === "https" || scheme === "wss";
670             if (secure) {
671                 let lockIconElement = cell.appendChild(document.createElement("img"));
672                 lockIconElement.className = "lock";
673             }
674
675             cell.append(domain || emDash);
676         }
677
678         let uniqueSchemeValues = this._uniqueValuesForDOMNodeEntry(entry, (resourceEntry) => resourceEntry.scheme);
679         let uniqueDomainValues = this._uniqueValuesForDOMNodeEntry(entry, (resourceEntry) => resourceEntry.domain);
680         if (uniqueSchemeValues && uniqueDomainValues) {
681             if (uniqueSchemeValues.size > 1 || uniqueDomainValues.size > 1) {
682                 cell.classList.add("multiple");
683                 cell.textContent = WI.UIString("(multiple)");
684                 return;
685             }
686
687             createIconAndText(uniqueSchemeValues.values().next().value, uniqueDomainValues.values().next().value);
688             return;
689         }
690
691         createIconAndText(entry.scheme, entry.domain);
692     }
693
694     _populateInitiatorCell(cell, entry)
695     {
696         let domNode = entry.domNode;
697         if (domNode) {
698             cell.textContent = emDash;
699             return;
700         }
701
702         let initiatorLocation = entry.resource.initiatorSourceCodeLocation;
703         if (!initiatorLocation) {
704             cell.textContent = emDash;
705             return;
706         }
707
708         const options = {
709             dontFloat: true,
710             ignoreSearchTab: true,
711         };
712         cell.appendChild(WI.createSourceCodeLocationLink(initiatorLocation, options));
713     }
714
715     _populateTransferSizeCell(cell, entry)
716     {
717         let resourceEntries = entry.initiatedResourceEntries;
718         if (resourceEntries) {
719             if (resourceEntries.every((resourceEntry) => resourceEntry.resource.responseSource === WI.Resource.ResponseSource.MemoryCache)) {
720                 cell.classList.add("cache-type");
721                 cell.textContent = WI.UIString("(memory)");
722                 return;
723             }
724             if (resourceEntries.every((resourceEntry) => resourceEntry.resource.responseSource === WI.Resource.ResponseSource.DiskCache)) {
725                 cell.classList.add("cache-type");
726                 cell.textContent = WI.UIString("(disk)");
727                 return;
728             }
729             if (resourceEntries.every((resourceEntry) => resourceEntry.resource.responseSource === WI.Resource.ResponseSource.ServiceWorker)) {
730                 cell.classList.add("cache-type");
731                 cell.textContent = WI.UIString("(service worker)");
732                 return;
733             }
734             let transferSize = resourceEntries.reduce((accumulator, current) => accumulator + (current.transferSize || 0), 0);
735             if (isNaN(transferSize))
736                 cell.textContent = emDash;
737             else
738                 cell.textContent = Number.bytesToString(transferSize);
739             return;
740         }
741
742         let responseSource = entry.resource.responseSource;
743         if (responseSource === WI.Resource.ResponseSource.MemoryCache) {
744             cell.classList.add("cache-type");
745             cell.textContent = WI.UIString("(memory)");
746             return;
747         }
748         if (responseSource === WI.Resource.ResponseSource.DiskCache) {
749             cell.classList.add("cache-type");
750             cell.textContent = WI.UIString("(disk)");
751             return;
752         }
753         if (responseSource === WI.Resource.ResponseSource.ServiceWorker) {
754             cell.classList.add("cache-type");
755             cell.textContent = WI.UIString("(service worker)");
756             return;
757         }
758
759         let transferSize = entry.transferSize;
760         cell.textContent = isNaN(transferSize) ? emDash : Number.bytesToString(transferSize);
761         console.assert(!cell.classList.contains("cache-type"), "Should not have cache-type class on cell.");
762     }
763
764     _populateWaterfallGraph(cell, entry)
765     {
766         cell.removeChildren();
767
768         let container = cell.appendChild(document.createElement("div"));
769         container.className = "waterfall-container";
770
771         let collection = this._activeCollection;
772         let graphStartTime = this._waterfallTimelineRuler.startTime;
773         let secondsPerPixel = this._waterfallTimelineRuler.secondsPerPixel;
774
775         function positionByStartOffset(element, timestamp) {
776             let styleAttribute = WI.resolvedLayoutDirection() === WI.LayoutDirection.LTR ? "left" : "right";
777             element.style.setProperty(styleAttribute, ((timestamp - graphStartTime) / secondsPerPixel) + "px");
778         }
779
780         function setWidthForDuration(element, startTimestamp, endTimestamp) {
781             element.style.setProperty("width", ((endTimestamp - startTimestamp) / secondsPerPixel) + "px");
782         }
783
784         let domNode = entry.domNode;
785         if (domNode) {
786             let groupedDOMEvents = [];
787             for (let domEvent of domNode.domEvents) {
788                 if (domEvent.originator)
789                     continue;
790
791                 if (!groupedDOMEvents.length || (domEvent.timestamp - groupedDOMEvents.lastValue.endTimestamp) >= (NetworkTableContentView.nodeWaterfallDOMEventSize * secondsPerPixel)) {
792                     groupedDOMEvents.push({
793                         startTimestamp: domEvent.timestamp,
794                         domEvents: [],
795                     });
796                 }
797                 groupedDOMEvents.lastValue.endTimestamp = domEvent.timestamp;
798                 groupedDOMEvents.lastValue.domEvents.push(domEvent);
799             }
800
801             let fullscreenDOMEvents = WI.DOMNode.getFullscreenDOMEvents(domNode.domEvents);
802             if (fullscreenDOMEvents.length) {
803                 if (!fullscreenDOMEvents[0].data.enabled)
804                     fullscreenDOMEvents.unshift({timestamp: graphStartTime});
805
806                 if (fullscreenDOMEvents.lastValue.data.enabled)
807                     fullscreenDOMEvents.push({timestamp: collection.waterfallEndTime});
808
809                 console.assert((fullscreenDOMEvents.length % 2) === 0, "Every enter/exit of fullscreen should have a corresponding exit/enter.");
810
811                 for (let i = 0; i < fullscreenDOMEvents.length; i += 2) {
812                     let fullscreenElement = container.appendChild(document.createElement("div"));
813                     fullscreenElement.classList.add("area", "dom-fullscreen");
814                     positionByStartOffset(fullscreenElement, fullscreenDOMEvents[i].timestamp);
815                     setWidthForDuration(fullscreenElement, fullscreenDOMEvents[i].timestamp, fullscreenDOMEvents[i + 1].timestamp);
816
817                     let originator = fullscreenDOMEvents[i].originator || fullscreenDOMEvents[i + 1].originator;
818                     if (originator)
819                         fullscreenElement.title = WI.UIString("Full-Screen from \u201C%s\u201D").format(originator.displayName);
820                     else
821                         fullscreenElement.title = WI.UIString("Full-Screen");
822                 }
823             }
824
825             for (let powerEfficientPlaybackRange of domNode.powerEfficientPlaybackRanges) {
826                 let startTimestamp = powerEfficientPlaybackRange.startTimestamp || graphStartTime;
827                 let endTimestamp = powerEfficientPlaybackRange.endTimestamp || collection.waterfallEndTime;
828
829                 let powerEfficientPlaybackRangeElement = container.appendChild(document.createElement("div"));
830                 powerEfficientPlaybackRangeElement.classList.add("area", "power-efficient-playback");
831                 powerEfficientPlaybackRangeElement.title = WI.UIString("Power Efficient Playback");
832                 positionByStartOffset(powerEfficientPlaybackRangeElement, startTimestamp);
833                 setWidthForDuration(powerEfficientPlaybackRangeElement, startTimestamp, endTimestamp);
834             }
835
836             let playing = false;
837
838             function createDOMEventLine(domEvents, startTimestamp, endTimestamp) {
839                 if (WI.DOMNode.isStopEvent(domEvents.lastValue.eventName))
840                     return;
841
842                 for (let i = domEvents.length - 1; i >= 0; --i) {
843                     let domEvent = domEvents[i];
844                     if (WI.DOMNode.isPlayEvent(domEvent.eventName)) {
845                         playing = true;
846                         break;
847                     }
848
849                     if (WI.DOMNode.isPauseEvent(domEvent.eventName)) {
850                         playing = false;
851                         break;
852                     }
853                 }
854
855                 let lineElement = container.appendChild(document.createElement("div"));
856                 lineElement.classList.add("dom-activity");
857                 lineElement.classList.toggle("playing", playing);
858                 positionByStartOffset(lineElement, startTimestamp);
859                 setWidthForDuration(lineElement, startTimestamp, endTimestamp);
860             }
861
862             for (let [a, b] of groupedDOMEvents.adjacencies())
863                 createDOMEventLine(a.domEvents, a.endTimestamp, b.startTimestamp);
864
865             if (groupedDOMEvents.length)
866                 createDOMEventLine(groupedDOMEvents.lastValue.domEvents, groupedDOMEvents.lastValue.endTimestamp, collection.waterfallEndTime);
867
868             for (let {startTimestamp, endTimestamp, domEvents} of groupedDOMEvents) {
869                 let paddingForCentering = NetworkTableContentView.nodeWaterfallDOMEventSize * secondsPerPixel / 2;
870
871                 let eventElement = container.appendChild(document.createElement("div"));
872                 eventElement.classList.add("dom-event");
873                 positionByStartOffset(eventElement, startTimestamp - paddingForCentering);
874                 setWidthForDuration(eventElement, startTimestamp, endTimestamp + paddingForCentering);
875                 eventElement.addEventListener("mousedown", (event) => {
876                     if (event.button !== 0 || event.ctrlKey)
877                         return;
878                     this._handleNodeEntryMousedownWaterfall(entry, domEvents);
879                 });
880
881                 for (let domEvent of domEvents)
882                     entry.domEventElements.set(domEvent, eventElement);
883             }
884
885             return;
886         }
887
888         let resource = entry.resource;
889         if (!resource.hasResponse()) {
890             cell.textContent = zeroWidthSpace;
891             return;
892         }
893
894         let {startTime, redirectStart, redirectEnd, fetchStart, domainLookupStart, domainLookupEnd, connectStart, connectEnd, secureConnectionStart, requestStart, responseStart, responseEnd} = resource.timingData;
895         if (isNaN(startTime) || isNaN(responseEnd) || startTime > responseEnd) {
896             cell.textContent = zeroWidthSpace;
897             return;
898         }
899
900         if (responseEnd < graphStartTime) {
901             cell.textContent = zeroWidthSpace;
902             return;
903         }
904
905         let graphEndTime = this._waterfallTimelineRuler.endTime;
906         if (startTime > graphEndTime) {
907             cell.textContent = zeroWidthSpace;
908             return;
909         }
910
911         let lastEndTimestamp = NaN;
912         function appendBlock(startTimestamp, endTimestamp, className) {
913             if (isNaN(startTimestamp) || isNaN(endTimestamp))
914                 return null;
915
916             if (Math.abs(startTimestamp - lastEndTimestamp) < secondsPerPixel * 2)
917                 startTimestamp = lastEndTimestamp;
918             lastEndTimestamp = endTimestamp;
919
920             let block = container.appendChild(document.createElement("div"));
921             block.classList.add("block", className);
922             positionByStartOffset(block, startTimestamp);
923             setWidthForDuration(block, startTimestamp, endTimestamp);
924             return block;
925         }
926
927         // Mouse block sits on top and accepts mouse events on this group.
928         let padSeconds = 10 * secondsPerPixel;
929         let mouseBlock = appendBlock(startTime - padSeconds, responseEnd + padSeconds, "mouse-tracking");
930         mouseBlock.addEventListener("mousedown", (event) => {
931             if (event.button !== 0 || event.ctrlKey)
932                 return;
933             this._handleResourceEntryMousedownWaterfall(entry);
934         });
935
936         // Super small visualization.
937         let totalWidth = (responseEnd - startTime) / secondsPerPixel;
938         if (totalWidth <= 3) {
939             let twoPixels = secondsPerPixel * 2;
940             appendBlock(startTime, startTime + twoPixels, "queue");
941             appendBlock(startTime + twoPixels, startTime + (2 * twoPixels), "response");
942             return;
943         }
944
945         appendBlock(startTime, responseEnd, "filler");
946
947         // FIXME: <https://webkit.org/b/190214> Web Inspector: expose full load metrics for redirect requests
948         appendBlock(redirectStart, redirectEnd, "redirect");
949
950         if (domainLookupStart) {
951             appendBlock(fetchStart, domainLookupStart, "queue");
952             appendBlock(domainLookupStart, domainLookupEnd || connectStart || requestStart, "dns");
953         } else if (connectStart)
954             appendBlock(fetchStart, connectStart, "queue");
955         else if (requestStart)
956             appendBlock(fetchStart, requestStart, "queue");
957         if (connectStart)
958             appendBlock(connectStart, secureConnectionStart || connectEnd, "connect");
959         if (secureConnectionStart)
960             appendBlock(secureConnectionStart, connectEnd, "secure");
961         appendBlock(requestStart, responseStart, "request");
962         appendBlock(responseStart, responseEnd, "response");
963     }
964
965     _generateSortComparator()
966     {
967         let sortColumnIdentifier = this._table.sortColumnIdentifier;
968         if (!sortColumnIdentifier) {
969             this._entriesSortComparator = null;
970             return;
971         }
972
973         let comparator;
974
975         switch (sortColumnIdentifier) {
976         case "name":
977         case "domain":
978         case "mimeType":
979         case "method":
980         case "scheme":
981         case "protocol":
982         case "initiator":
983         case "remoteAddress":
984             // Simple string.
985             comparator = (a, b) => (a[sortColumnIdentifier] || "").extendedLocaleCompare(b[sortColumnIdentifier] || "");
986             break;
987
988         case "status":
989         case "connectionIdentifier":
990         case "resourceSize":
991         case "time":
992             // Simple number.
993             comparator = (a, b) => {
994                 let aValue = a[sortColumnIdentifier];
995                 if (isNaN(aValue))
996                     return 1;
997                 let bValue = b[sortColumnIdentifier];
998                 if (isNaN(bValue))
999                     return -1;
1000                 return aValue - bValue;
1001             };
1002             break;
1003
1004         case "priority":
1005             // Resource.NetworkPriority enum.
1006             comparator = (a, b) => WI.Resource.comparePriority(a.priority, b.priority);
1007             break;
1008
1009         case "type":
1010             // Sort by displayType string.
1011             comparator = (a, b) => (a.displayType || "").extendedLocaleCompare(b.displayType || "");
1012             break;
1013
1014         case "transferSize":
1015             // Handle (memory) and (disk) values.
1016             comparator = (a, b) => {
1017                 let transferSizeA = a.transferSize;
1018                 let transferSizeB = b.transferSize;
1019
1020                 // Treat NaN as the largest value.
1021                 if (isNaN(transferSizeA))
1022                     return 1;
1023                 if (isNaN(transferSizeB))
1024                     return -1;
1025
1026                 // Treat memory cache and disk cache as small values.
1027                 let sourceA = a.resource.responseSource;
1028                 if (sourceA === WI.Resource.ResponseSource.MemoryCache)
1029                     transferSizeA = -20;
1030                 else if (sourceA === WI.Resource.ResponseSource.DiskCache)
1031                     transferSizeA = -10;
1032                 else if (sourceA === WI.Resource.ResponseSource.ServiceWorker)
1033                     transferSizeA = -5;
1034
1035                 let sourceB = b.resource.responseSource;
1036                 if (sourceB === WI.Resource.ResponseSource.MemoryCache)
1037                     transferSizeB = -20;
1038                 else if (sourceB === WI.Resource.ResponseSource.DiskCache)
1039                     transferSizeB = -10;
1040                 else if (sourceB === WI.Resource.ResponseSource.ServiceWorker)
1041                     transferSizeB = -5;
1042
1043                 return transferSizeA - transferSizeB;
1044             };
1045             break;
1046
1047         case "waterfall":
1048             // Sort by startTime number.
1049             comparator = (a, b) => a.startTime - b.startTime;
1050             break;
1051
1052         default:
1053             console.assert("Unexpected sort column", sortColumnIdentifier);
1054             return;
1055         }
1056
1057         let reverseFactor = this._table.sortOrder === WI.Table.SortOrder.Ascending ? 1 : -1;
1058
1059         // If the entry has an `initiatorNode`, use that node's "first" resource as the value of
1060         // `entry`, so long as the entry being compared to doesn't have the same `initiatorNode`.
1061         // This will ensure that all resource entries for a given `initiatorNode` will appear right
1062         // next to each other, as they will all effectively be sorted by the first resource.
1063         let substitute = (entry, other) => {
1064             if (WI.settings.groupMediaRequestsByDOMNode.value && entry.resource.initiatorNode) {
1065                 let nodeEntry = this._domNodeEntries.get(entry.resource.initiatorNode);
1066                 if (!nodeEntry.initiatedResourceEntries.includes(other))
1067                     return nodeEntry.initiatedResourceEntries[0];
1068             }
1069             return entry;
1070         };
1071
1072         this._entriesSortComparator = (a, b) => reverseFactor * comparator(substitute(a, b), substitute(b, a));
1073     }
1074
1075     // Protected
1076
1077     initialLayout()
1078     {
1079         super.initialLayout();
1080
1081         this.element.style.setProperty("--node-waterfall-dom-event-size", NetworkTableContentView.nodeWaterfallDOMEventSize + "px");
1082
1083         this._waterfallTimelineRuler = new WI.TimelineRuler;
1084         this._waterfallTimelineRuler.allowsClippedLabels = true;
1085
1086         this._nameColumn = new WI.TableColumn("name", WI.UIString("Name"), {
1087             minWidth: WI.Sidebar.AbsoluteMinimumWidth,
1088             maxWidth: 500,
1089             initialWidth: this._nameColumnWidthSetting.value,
1090             resizeType: WI.TableColumn.ResizeType.Locked,
1091         });
1092
1093         this._domainColumn = new WI.TableColumn("domain", WI.UIString("Domain"), {
1094             minWidth: 120,
1095             maxWidth: 200,
1096             initialWidth: 150,
1097         });
1098
1099         this._typeColumn = new WI.TableColumn("type", WI.UIString("Type"), {
1100             minWidth: 70,
1101             maxWidth: 120,
1102             initialWidth: 90,
1103         });
1104
1105         this._mimeTypeColumn = new WI.TableColumn("mimeType", WI.UIString("MIME Type"), {
1106             hidden: true,
1107             minWidth: 100,
1108             maxWidth: 150,
1109             initialWidth: 120,
1110         });
1111
1112         this._methodColumn = new WI.TableColumn("method", WI.UIString("Method"), {
1113             hidden: true,
1114             minWidth: 55,
1115             maxWidth: 80,
1116             initialWidth: 65,
1117         });
1118
1119         this._schemeColumn = new WI.TableColumn("scheme", WI.UIString("Scheme"), {
1120             hidden: true,
1121             minWidth: 55,
1122             maxWidth: 80,
1123             initialWidth: 65,
1124         });
1125
1126         this._statusColumn = new WI.TableColumn("status", WI.UIString("Status"), {
1127             hidden: true,
1128             minWidth: 50,
1129             maxWidth: 50,
1130             align: "left",
1131         });
1132
1133         this._protocolColumn = new WI.TableColumn("protocol", WI.UIString("Protocol"), {
1134             hidden: true,
1135             minWidth: 65,
1136             maxWidth: 80,
1137             initialWidth: 75,
1138         });
1139
1140         this._initiatorColumn = new WI.TableColumn("initiator", WI.UIString("Initiator"), {
1141             hidden: true,
1142             minWidth: 75,
1143             maxWidth: 175,
1144             initialWidth: 125,
1145         });
1146
1147         this._priorityColumn = new WI.TableColumn("priority", WI.UIString("Priority"), {
1148             hidden: true,
1149             minWidth: 65,
1150             maxWidth: 80,
1151             initialWidth: 70,
1152         });
1153
1154         this._remoteAddressColumn = new WI.TableColumn("remoteAddress", WI.UIString("IP Address"), {
1155             hidden: true,
1156             minWidth: 150,
1157         });
1158
1159         this._connectionIdentifierColumn = new WI.TableColumn("connectionIdentifier", WI.UIString("Connection ID"), {
1160             hidden: true,
1161             minWidth: 50,
1162             maxWidth: 120,
1163             initialWidth: 80,
1164             align: "right",
1165         });
1166
1167         this._resourceSizeColumn = new WI.TableColumn("resourceSize", WI.UIString("Resource Size"), {
1168             hidden: true,
1169             minWidth: 80,
1170             maxWidth: 100,
1171             initialWidth: 80,
1172             align: "right",
1173         });
1174
1175         this._transferSizeColumn = new WI.TableColumn("transferSize", WI.UIString("Transfer Size", "Amount of data sent over the network for a single resource"), {
1176             minWidth: 100,
1177             maxWidth: 150,
1178             initialWidth: 100,
1179             align: "right",
1180         });
1181
1182         this._timeColumn = new WI.TableColumn("time", WI.UIString("Time"), {
1183             minWidth: 65,
1184             maxWidth: 90,
1185             initialWidth: 65,
1186             align: "right",
1187         });
1188
1189         this._waterfallColumn = new WI.TableColumn("waterfall", WI.UIString("Waterfall"), {
1190             minWidth: 230,
1191             headerView: this._waterfallTimelineRuler,
1192             needsReloadOnResize: true,
1193         });
1194
1195         this._nameColumn.addEventListener(WI.TableColumn.Event.WidthDidChange, this._tableNameColumnDidChangeWidth, this);
1196         this._waterfallColumn.addEventListener(WI.TableColumn.Event.WidthDidChange, this._tableWaterfallColumnDidChangeWidth, this);
1197
1198         this._table = new WI.Table("network-table", this, this, 20);
1199
1200         this._table.addColumn(this._nameColumn);
1201         this._table.addColumn(this._domainColumn);
1202         this._table.addColumn(this._typeColumn);
1203         this._table.addColumn(this._mimeTypeColumn);
1204         this._table.addColumn(this._methodColumn);
1205         this._table.addColumn(this._schemeColumn);
1206         this._table.addColumn(this._statusColumn);
1207         this._table.addColumn(this._protocolColumn);
1208         this._table.addColumn(this._initiatorColumn);
1209         this._table.addColumn(this._priorityColumn);
1210         this._table.addColumn(this._remoteAddressColumn);
1211         this._table.addColumn(this._connectionIdentifierColumn);
1212         this._table.addColumn(this._resourceSizeColumn);
1213         this._table.addColumn(this._transferSizeColumn);
1214         this._table.addColumn(this._timeColumn);
1215         this._table.addColumn(this._waterfallColumn);
1216
1217         if (!this._table.sortColumnIdentifier) {
1218             this._table.sortOrder = WI.Table.SortOrder.Ascending;
1219             this._table.sortColumnIdentifier = "waterfall";
1220         }
1221
1222         this.addSubview(this._table);
1223     }
1224
1225     layout()
1226     {
1227         this._updateWaterfallTimelineRuler();
1228         this._processPendingEntries();
1229         this._positionDetailView();
1230         this._positionEmptyFilterMessage();
1231         this._updateExportButton();
1232     }
1233
1234     didLayoutSubtree()
1235     {
1236         super.didLayoutSubtree();
1237
1238         if (this._waterfallPopover)
1239             this._waterfallPopover.resize();
1240     }
1241
1242     processHAR(result)
1243     {
1244         let resources = WI.networkManager.processHAR(result);
1245         if (!resources)
1246             return;
1247
1248         let importedCollection = this._addCollection();
1249
1250         let displayName = WI.UIString("Imported - %s").format(result.filename);
1251         this._addCollectionPathComponent(importedCollection, displayName, "network-har-icon");
1252
1253         this._changeCollection(importedCollection);
1254
1255         for (let resource of resources)
1256             this._insertResourceAndReloadTable(resource);
1257     }
1258
1259     handleClearShortcut(event)
1260     {
1261         if (!this._isShowingMainCollection())
1262             return;
1263
1264         this.reset();
1265     }
1266
1267     // Private
1268
1269     _updateWaterfallTimeRange(startTimestamp, endTimestamp)
1270     {
1271         let collection = this._activeCollection;
1272
1273         if (isNaN(collection.waterfallStartTime) || startTimestamp < collection.waterfallStartTime)
1274             collection.waterfallStartTime = startTimestamp;
1275
1276         if (isNaN(collection.waterfallEndTime) || endTimestamp > collection.waterfallEndTime)
1277             collection.waterfallEndTime = endTimestamp;
1278     }
1279
1280     _updateWaterfallTimelineRuler()
1281     {
1282         if (!this._waterfallTimelineRuler)
1283             return;
1284
1285         let collection = this._activeCollection;
1286
1287         if (isNaN(collection.waterfallStartTime)) {
1288             this._waterfallTimelineRuler.zeroTime = 0;
1289             this._waterfallTimelineRuler.startTime = 0;
1290             this._waterfallTimelineRuler.endTime = 0.250;
1291         } else {
1292             this._waterfallTimelineRuler.zeroTime = collection.waterfallStartTime;
1293             this._waterfallTimelineRuler.startTime = collection.waterfallStartTime;
1294             this._waterfallTimelineRuler.endTime = collection.waterfallEndTime;
1295
1296             // Add a little bit of padding on the each side.
1297             const paddingPixels = 5;
1298             let padSeconds = paddingPixels * this._waterfallTimelineRuler.secondsPerPixel;
1299             this._waterfallTimelineRuler.zeroTime = collection.waterfallStartTime - padSeconds;
1300             this._waterfallTimelineRuler.startTime = collection.waterfallStartTime - padSeconds;
1301             this._waterfallTimelineRuler.endTime = collection.waterfallEndTime + padSeconds;
1302         }
1303     }
1304
1305     _canExportHAR()
1306     {
1307         if (!this._isShowingMainCollection())
1308             return false;
1309
1310         let mainFrame = WI.networkManager.mainFrame;
1311         if (!mainFrame)
1312             return false;
1313
1314         let mainResource = mainFrame.mainResource;
1315         if (!mainResource)
1316             return false;
1317
1318         if (!mainResource.requestSentDate)
1319             return false;
1320
1321         if (!this._HARResources().length)
1322             return false;
1323
1324         return true;
1325     }
1326
1327     _updateExportButton()
1328     {
1329         this._harExportNavigationItem.enabled = this._canExportHAR();
1330     }
1331
1332     _processPendingEntries()
1333     {
1334         let collection = this._activeCollection;
1335         let needsSort = collection.pendingUpdates.length > 0;
1336         let needsFilter = this._pendingFilter;
1337
1338         // No global sort or filter is needed, so just insert new records into their sorted position.
1339         if (!needsSort && !needsFilter) {
1340             let originalLength = collection.pendingInsertions.length;
1341             for (let resource of collection.pendingInsertions)
1342                 this._insertResourceAndReloadTable(resource);
1343             console.assert(collection.pendingInsertions.length === originalLength);
1344             collection.pendingInsertions = [];
1345             return;
1346         }
1347
1348         for (let resource of collection.pendingInsertions) {
1349             let resourceEntry = this._entryForResource(resource);
1350             this._tryLinkResourceToDOMNode(resourceEntry);
1351             collection.entries.push(resourceEntry);
1352         }
1353         collection.pendingInsertions = [];
1354
1355         for (let updateObject of collection.pendingUpdates) {
1356             if (updateObject instanceof WI.Resource)
1357                 this._updateEntryForResource(updateObject);
1358         }
1359         collection.pendingUpdates = [];
1360
1361         this._pendingFilter = false;
1362
1363         this._updateSort();
1364         this._updateFilteredEntries();
1365         this._reloadTable();
1366     }
1367
1368     _populateWithInitialResourcesIfNeeded(collection)
1369     {
1370         if (!this._needsInitialPopulate)
1371             return;
1372
1373         this._needsInitialPopulate = false;
1374
1375         let populateResourcesForFrame = (frame) => {
1376             if (frame.provisionalMainResource)
1377                 collection.pendingInsertions.push(frame.provisionalMainResource);
1378             else if (frame.mainResource)
1379                 collection.pendingInsertions.push(frame.mainResource);
1380
1381             for (let resource of frame.resourceCollection)
1382                 collection.pendingInsertions.push(resource);
1383
1384             for (let childFrame of frame.childFrameCollection)
1385                 populateResourcesForFrame(childFrame);
1386         };
1387
1388         let populateResourcesForTarget = (target) => {
1389             if (target.mainResource instanceof WI.Resource)
1390                 collection.pendingInsertions.push(target.mainResource);
1391             for (let resource of target.resourceCollection)
1392                 collection.pendingInsertions.push(resource);
1393         };
1394
1395         for (let target of WI.targets) {
1396             if (target === WI.pageTarget)
1397                 populateResourcesForFrame(WI.networkManager.mainFrame);
1398             else
1399                 populateResourcesForTarget(target);
1400         }
1401
1402         this.needsLayout();
1403     }
1404
1405     _checkURLFilterAgainstResource(resource)
1406     {
1407         if (this._urlFilterSearchRegex.test(resource.url)) {
1408             this._activeURLFilterResources.add(resource);
1409             return;
1410         }
1411
1412         for (let redirect of resource.redirects) {
1413             if (this._urlFilterSearchRegex.test(redirect.url)) {
1414                 this._activeURLFilterResources.add(resource);
1415                 return;
1416             }
1417         }
1418     }
1419
1420     _rowIndexForRepresentedObject(object)
1421     {
1422         return this._activeCollection.filteredEntries.findIndex((x) => {
1423             if (x.resource === object)
1424                 return true;
1425             if (x.domNode === object)
1426                 return true;
1427             return false;
1428         });
1429     }
1430
1431     _updateEntryForResource(resource)
1432     {
1433         let collection = this._activeCollection;
1434
1435         let index = collection.entries.findIndex((x) => x.resource === resource);
1436         if (index === -1)
1437             return;
1438
1439         // Don't wipe out the previous entry, as it may be used by a node entry.
1440         function updateExistingEntry(existingEntry, newEntry) {
1441             for (let key in newEntry)
1442                 existingEntry[key] = newEntry[key];
1443         }
1444
1445         let entry = this._entryForResource(resource);
1446         updateExistingEntry(collection.entries[index], entry);
1447
1448         let rowIndex = this._rowIndexForRepresentedObject(resource);
1449         if (rowIndex === -1)
1450             return;
1451
1452         updateExistingEntry(collection.filteredEntries[rowIndex], entry);
1453     }
1454
1455     _hidePopover()
1456     {
1457         if (this._waterfallPopover)
1458             this._waterfallPopover.dismiss();
1459     }
1460
1461     _hideDetailView()
1462     {
1463         if (!this._detailView)
1464             return;
1465
1466         this.element.classList.remove("showing-detail");
1467         this._table.scrollContainer.style.removeProperty("width");
1468
1469         this.removeSubview(this._detailView);
1470
1471         this._detailView.hidden();
1472         this._detailView = null;
1473
1474         this._table.updateLayout(WI.View.LayoutReason.Resize);
1475         this._table.reloadVisibleColumnCells(this._waterfallColumn);
1476     }
1477
1478     _showDetailView(object)
1479     {
1480         let oldDetailView = this._detailView;
1481
1482         this._detailView = this._detailViewMap.get(object);
1483         if (this._detailView === oldDetailView)
1484             return;
1485
1486         if (!this._detailView) {
1487             if (object instanceof WI.Resource)
1488                 this._detailView = new WI.NetworkResourceDetailView(object, this);
1489             else if (object instanceof WI.DOMNode) {
1490                 this._detailView = new WI.NetworkDOMNodeDetailView(object, this);
1491             }
1492
1493             this._detailViewMap.set(object, this._detailView);
1494         }
1495
1496         if (oldDetailView) {
1497             oldDetailView.hidden();
1498             this.replaceSubview(oldDetailView, this._detailView);
1499         } else
1500             this.addSubview(this._detailView);
1501
1502         if (this._showingRepresentedObjectCookie)
1503             this._detailView.willShowWithCookie(this._showingRepresentedObjectCookie);
1504
1505         this._detailView.shown();
1506
1507         this.element.classList.add("showing-detail");
1508         this._table.scrollContainer.style.width = this._nameColumn.width + "px";
1509
1510         // FIXME: It would be nice to avoid this.
1511         // Currently the ResourceDetailView is in the heirarchy but has not yet done a layout so we
1512         // end up seeing the table behind it. This forces us to layout now instead of after a beat.
1513         this.updateLayout();
1514     }
1515
1516     _positionDetailView()
1517     {
1518         if (!this._detailView)
1519             return;
1520
1521         let side = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL ? "right" : "left";
1522         this._detailView.element.style[side] = this._nameColumn.width + "px";
1523         this._table.scrollContainer.style.width = this._nameColumn.width + "px";
1524     }
1525
1526     _updateURLFilterActiveIndicator()
1527     {
1528         this._urlFilterNavigationItem.filterBar.indicatingActive = this._hasURLFilter();
1529     }
1530
1531     _updateEmptyFilterResultsMessage()
1532     {
1533         if (this._hasActiveFilter() && !this._activeCollection.filteredEntries.length)
1534             this._showEmptyFilterResultsMessage();
1535         else
1536             this._hideEmptyFilterResultsMessage();
1537     }
1538
1539     _showEmptyFilterResultsMessage()
1540     {
1541         if (!this._emptyFilterResultsMessageElement) {
1542             let buttonElement = document.createElement("button");
1543             buttonElement.textContent = WI.UIString("Clear Filters");
1544             buttonElement.addEventListener("click", () => { this._resetFilters(); });
1545
1546             this._emptyFilterResultsMessageElement = WI.createMessageTextView(WI.UIString("No Filter Results"));
1547             this._emptyFilterResultsMessageElement.appendChild(buttonElement);
1548         }
1549
1550         this.element.appendChild(this._emptyFilterResultsMessageElement);
1551         this._positionEmptyFilterMessage();
1552     }
1553
1554     _hideEmptyFilterResultsMessage()
1555     {
1556         if (!this._emptyFilterResultsMessageElement)
1557             return;
1558
1559         this._emptyFilterResultsMessageElement.remove();
1560     }
1561
1562     _positionEmptyFilterMessage()
1563     {
1564         if (!this._emptyFilterResultsMessageElement)
1565             return;
1566
1567         let width = this._nameColumn.width - 1; // For the 1px border.
1568         this._emptyFilterResultsMessageElement.style.width = width + "px";
1569     }
1570
1571     _clearNetworkOnNavigateSettingChanged()
1572     {
1573         this._clearOnLoadNavigationItem.checked = !WI.settings.clearNetworkOnNavigate.value;
1574     }
1575
1576     _resourceCachingDisabledSettingChanged()
1577     {
1578         this._disableResourceCacheNavigationItem.activated = WI.settings.resourceCachingDisabled.value;
1579     }
1580
1581     _toggleDisableResourceCache()
1582     {
1583         WI.settings.resourceCachingDisabled.value = !WI.settings.resourceCachingDisabled.value;
1584     }
1585
1586     _mainResourceDidChange(event)
1587     {
1588         this._runForMainCollection((collection) => {
1589             let frame = event.target;
1590             if (frame.isMainFrame() && WI.settings.clearNetworkOnNavigate.value)
1591                 this._resetCollection(collection);
1592
1593             if (this._transitioningPageTarget) {
1594                 this._transitioningPageTarget = false;
1595                 this._needsInitialPopulate = true;
1596                 this._populateWithInitialResourcesIfNeeded(collection);
1597                 return;
1598             }
1599
1600             this._insertResourceAndReloadTable(frame.mainResource);
1601         });
1602     }
1603
1604     _mainFrameDidChange()
1605     {
1606         this._runForMainCollection((collection) => {
1607             this._populateWithInitialResourcesIfNeeded(collection);
1608         });
1609     }
1610
1611     _resourceLoadingDidFinish(event)
1612     {
1613         this._runForMainCollection((collection, wasMain) => {
1614             let resource = event.target;
1615             collection.pendingUpdates.push(resource);
1616
1617             this._updateWaterfallTimeRange(resource.firstTimestamp, resource.timingData.responseEnd);
1618
1619             if (this._hasURLFilter())
1620                 this._checkURLFilterAgainstResource(resource);
1621
1622             if (wasMain)
1623                 this.needsLayout();
1624         });
1625     }
1626
1627     _resourceLoadingDidFail(event)
1628     {
1629         this._runForMainCollection((collection, wasMain) => {
1630             let resource = event.target;
1631             collection.pendingUpdates.push(resource);
1632
1633             this._updateWaterfallTimeRange(resource.firstTimestamp, resource.timingData.responseEnd);
1634
1635             if (this._hasURLFilter())
1636                 this._checkURLFilterAgainstResource(resource);
1637
1638             if (wasMain)
1639                 this.needsLayout();
1640         });
1641     }
1642
1643     _resourceTransferSizeDidChange(event)
1644     {
1645         if (!this._table)
1646             return;
1647
1648         this._runForMainCollection((collection, wasMain) => {
1649             let resource = event.target;
1650
1651             // In the unlikely event that this is the sort column, we may need to resort.
1652             if (this._table.sortColumnIdentifier === "transferSize") {
1653                 collection.pendingUpdates.push(resource);
1654                 this.needsLayout();
1655                 return;
1656             }
1657
1658             let index = collection.entries.findIndex((x) => x.resource === resource);
1659             if (index === -1)
1660                 return;
1661
1662             let entry = collection.entries[index];
1663             entry.transferSize = !isNaN(resource.networkTotalTransferSize) ? resource.networkTotalTransferSize : resource.estimatedTotalTransferSize;
1664
1665             if (!wasMain)
1666                 return;
1667
1668             let rowIndex = this._rowIndexForRepresentedObject(resource);
1669             if (rowIndex === -1)
1670                 return;
1671
1672             this._table.reloadCell(rowIndex, "transferSize");
1673         });
1674     }
1675
1676     _handleResourceAdded(event)
1677     {
1678         this._runForMainCollection((collection) => {
1679             this._insertResourceAndReloadTable(event.data.resource);
1680         });
1681     }
1682
1683     _handleFrameWasAdded(event)
1684     {
1685         if (this._needsInitialPopulate)
1686             return;
1687
1688         this._runForMainCollection((collection) => {
1689             let frame = event.data.childFrame;
1690             let mainResource = frame.provisionalMainResource || frame.mainResource;
1691             console.assert(mainResource, "Frame should have a main resource.");
1692             this._insertResourceAndReloadTable(mainResource);
1693
1694             console.assert(!frame.resourceCollection.size, "New frame should be empty.");
1695             console.assert(!frame.childFrameCollection.size, "New frame should be empty.");
1696         });
1697     }
1698
1699     _runForMainCollection(callback)
1700     {
1701         let currentCollection = this._activeCollection;
1702         let wasMain = currentCollection === this._mainCollection;
1703
1704         if (!wasMain)
1705             this._setActiveCollection(this._mainCollection);
1706
1707         callback(this._activeCollection, wasMain);
1708
1709         if (!wasMain)
1710             this._setActiveCollection(currentCollection);
1711     }
1712
1713     _isShowingMainCollection()
1714     {
1715         return this._activeCollection === this._mainCollection;
1716     }
1717
1718     _isDefaultSort()
1719     {
1720         return this._table.sortColumnIdentifier === "waterfall" && this._table.sortOrder === WI.Table.SortOrder.Ascending;
1721     }
1722
1723     _insertResourceAndReloadTable(resource)
1724     {
1725         if (this._needsInitialPopulate)
1726             return;
1727
1728         let collection = this._activeCollection;
1729
1730         this._updateWaterfallTimeRange(resource.firstTimestamp, resource.timingData.responseEnd);
1731
1732         if (!this._table || !(WI.tabBrowser.selectedTabContentView instanceof WI.NetworkTabContentView)) {
1733             collection.pendingInsertions.push(resource);
1734             this.needsLayout();
1735             return;
1736         }
1737
1738         let resourceEntry = this._entryForResource(resource);
1739
1740         this._tryLinkResourceToDOMNode(resourceEntry);
1741
1742         if (WI.settings.groupMediaRequestsByDOMNode.value && resource.initiatorNode) {
1743             if (!this._entriesSortComparator)
1744                 this._generateSortComparator();
1745         } else if (this._isDefaultSort() || !this._entriesSortComparator) {
1746             // Default sort has fast path.
1747             collection.entries.push(resourceEntry);
1748             if (this._passFilter(resourceEntry)) {
1749                 collection.filteredEntries.push(resourceEntry);
1750                 this._table.reloadDataAddedToEndOnly();
1751             }
1752             return;
1753         }
1754
1755         insertObjectIntoSortedArray(resourceEntry, collection.entries, this._entriesSortComparator);
1756
1757         if (this._passFilter(resourceEntry)) {
1758             if (WI.settings.groupMediaRequestsByDOMNode.value)
1759                 this._updateFilteredEntries();
1760             else
1761                 insertObjectIntoSortedArray(resourceEntry, collection.filteredEntries, this._entriesSortComparator);
1762
1763             // Probably a useless optimization here, but if we only added this row to the end
1764             // we may avoid recreating all visible rows by saying as such.
1765             if (collection.filteredEntries.lastValue === resourceEntry)
1766                 this._table.reloadDataAddedToEndOnly();
1767             else
1768                 this._reloadTable();
1769         }
1770     }
1771
1772     _entryForResource(resource)
1773     {
1774         // FIXME: <https://webkit.org/b/143632> Web Inspector: Resources with the same name in different folders aren't distinguished
1775         // FIXME: <https://webkit.org/b/176765> Web Inspector: Resource names should be less ambiguous
1776
1777         return {
1778             resource,
1779             name: WI.displayNameForURL(resource.url, resource.urlComponents),
1780             domain: WI.displayNameForHost(resource.urlComponents.host),
1781             scheme: resource.urlComponents.scheme ? resource.urlComponents.scheme.toLowerCase() : "",
1782             method: resource.requestMethod,
1783             type: resource.type,
1784             displayType: WI.NetworkTableContentView.displayNameForResource(resource),
1785             mimeType: resource.mimeType,
1786             status: resource.statusCode,
1787             cached: resource.cached,
1788             resourceSize: resource.size,
1789             transferSize: !isNaN(resource.networkTotalTransferSize) ? resource.networkTotalTransferSize : resource.estimatedTotalTransferSize,
1790             time: resource.totalDuration,
1791             protocol: resource.protocol,
1792             initiator: resource.initiatorSourceCodeLocation ? resource.initiatorSourceCodeLocation.displayLocationString() : "",
1793             priority: resource.priority,
1794             remoteAddress: resource.remoteAddress,
1795             connectionIdentifier: resource.connectionIdentifier,
1796             startTime: resource.firstTimestamp,
1797         };
1798     }
1799
1800     _entryForDOMNode(domNode)
1801     {
1802         return {
1803             domNode,
1804             initiatedResourceEntries: [],
1805             domEventElements: new Map,
1806             expanded: true,
1807         };
1808     }
1809
1810     _tryLinkResourceToDOMNode(resourceEntry)
1811     {
1812         let resource = resourceEntry.resource;
1813         if (!resource || !resource.initiatorNode)
1814             return;
1815
1816         let nodeEntry = this._domNodeEntries.get(resource.initiatorNode);
1817         if (!nodeEntry) {
1818             nodeEntry = this._entryForDOMNode(resource.initiatorNode, Object.keys(resourceEntry));
1819             this._domNodeEntries.set(resource.initiatorNode, nodeEntry);
1820
1821             resource.initiatorNode.addEventListener(WI.DOMNode.Event.DidFireEvent, this._handleNodeDidFireEvent, this);
1822             if (resource.initiatorNode.canEnterPowerEfficientPlaybackState())
1823                 resource.initiatorNode.addEventListener(WI.DOMNode.Event.PowerEfficientPlaybackStateChanged, this._handleDOMNodePowerEfficientPlaybackStateChanged, this);
1824         }
1825
1826         if (!this._entriesSortComparator)
1827             this._generateSortComparator();
1828
1829         insertObjectIntoSortedArray(resourceEntry, nodeEntry.initiatedResourceEntries, this._entriesSortComparator);
1830     }
1831
1832     _uniqueValuesForDOMNodeEntry(nodeEntry, accessor)
1833     {
1834         let resourceEntries = nodeEntry.initiatedResourceEntries;
1835         if (!resourceEntries)
1836             return null;
1837
1838         return resourceEntries.reduce((accumulator, current) => {
1839             let value = accessor(current);
1840             if (value || typeof value === "number")
1841                 accumulator.add(value);
1842             return accumulator;
1843         }, new Set);
1844     }
1845
1846     _handleNodeDidFireEvent(event)
1847     {
1848         this._runForMainCollection((collection, wasMain) => {
1849             let domNode = event.target;
1850             let {domEvent} = event.data;
1851
1852             collection.pendingUpdates.push(domNode);
1853
1854             this._updateWaterfallTimeRange(NaN, domEvent.timestamp + (this._waterfallTimelineRuler.secondsPerPixel * 10));
1855
1856             if (wasMain)
1857                 this.needsLayout();
1858         });
1859     }
1860
1861     _handleDOMNodePowerEfficientPlaybackStateChanged(event)
1862     {
1863         this._runForMainCollection((collection, wasMain) => {
1864             let domNode = event.target;
1865             let {timestamp} = event.data;
1866
1867             collection.pendingUpdates.push(domNode);
1868
1869             this._updateWaterfallTimeRange(NaN, timestamp + (this._waterfallTimelineRuler.secondsPerPixel * 10));
1870
1871             if (wasMain)
1872                 this.needsLayout();
1873         });
1874     }
1875
1876     _hasTypeFilter()
1877     {
1878         return !!this._activeTypeFilters;
1879     }
1880
1881     _hasURLFilter()
1882     {
1883         return this._urlFilterIsActive;
1884     }
1885
1886     _hasActiveFilter()
1887     {
1888         return this._hasTypeFilter()
1889             || this._hasURLFilter();
1890     }
1891
1892     _passTypeFilter(entry)
1893     {
1894         if (!this._hasTypeFilter())
1895             return true;
1896         return this._activeTypeFilters.some((checker) => checker(entry.resource.type));
1897     }
1898
1899     _passURLFilter(entry)
1900     {
1901         if (!this._hasURLFilter())
1902             return true;
1903         return this._activeURLFilterResources.has(entry.resource);
1904     }
1905
1906     _passFilter(entry)
1907     {
1908         return this._passTypeFilter(entry)
1909             && this._passURLFilter(entry);
1910     }
1911
1912     _updateSort()
1913     {
1914         if (this._entriesSortComparator) {
1915             let collection = this._activeCollection;
1916             collection.entries = collection.entries.sort(this._entriesSortComparator);
1917         }
1918     }
1919
1920     _updateFilteredEntries()
1921     {
1922         let collection = this._activeCollection;
1923
1924         if (this._hasActiveFilter())
1925             collection.filteredEntries = collection.entries.filter(this._passFilter, this);
1926         else
1927             collection.filteredEntries = collection.entries.slice();
1928
1929         if (WI.settings.groupMediaRequestsByDOMNode.value) {
1930             for (let nodeEntry of this._domNodeEntries.values()) {
1931                 if (nodeEntry.initiatedResourceEntries.length < 2 && !nodeEntry.domNode.domEvents.length)
1932                     continue;
1933
1934                 let firstIndex = Infinity;
1935                 for (let resourceEntry of nodeEntry.initiatedResourceEntries) {
1936                     if (this._hasActiveFilter() && !this._passFilter(resourceEntry))
1937                         continue;
1938
1939                     let index = collection.filteredEntries.indexOf(resourceEntry);
1940                     if (index >= 0 && index < firstIndex)
1941                         firstIndex = index;
1942                 }
1943
1944                 if (!isFinite(firstIndex))
1945                     continue;
1946
1947                 collection.filteredEntries.insertAtIndex(nodeEntry, firstIndex);
1948             }
1949
1950             collection.filteredEntries = collection.filteredEntries.filter((entry) => {
1951                 if (entry.resource && entry.resource.initiatorNode) {
1952                     let nodeEntry = this._domNodeEntries.get(entry.resource.initiatorNode);
1953                     if (!nodeEntry.expanded)
1954                         return false;
1955                 }
1956                 return true;
1957             });
1958         }
1959
1960         this._updateURLFilterActiveIndicator();
1961         this._updateEmptyFilterResultsMessage();
1962     }
1963
1964     _reloadTable()
1965     {
1966         this._table.reloadData();
1967         this._restoreSelectedRow();
1968     }
1969
1970     _generateTypeFilter()
1971     {
1972         let selectedItems = this._typeFilterScopeBar.selectedItems;
1973         if (!selectedItems.length || selectedItems.includes(this._typeFilterScopeBarItemAll))
1974             return null;
1975
1976         return selectedItems.map((item) => item.__checker);
1977     }
1978
1979     _resetFilters()
1980     {
1981         console.assert(this._hasActiveFilter());
1982
1983         // Clear url filter.
1984         this._urlFilterSearchText = null;
1985         this._urlFilterSearchRegex = null;
1986         this._urlFilterIsActive = false;
1987         this._activeURLFilterResources.clear();
1988         this._urlFilterNavigationItem.filterBar.clear();
1989         console.assert(!this._hasURLFilter());
1990
1991         // Clear type filter.
1992         this._typeFilterScopeBar.resetToDefault();
1993         console.assert(!this._hasTypeFilter());
1994
1995         console.assert(!this._hasActiveFilter());
1996
1997         this._updateFilteredEntries();
1998         this._reloadTable();
1999     }
2000
2001     _areFilterListsIdentical(listA, listB)
2002     {
2003         if (listA && listB) {
2004             if (listA.length !== listB.length)
2005                 return false;
2006
2007             for (let i = 0; i < listA.length; ++i) {
2008                 if (listA[i] !== listB[i])
2009                     return false;
2010             }
2011
2012             return true;
2013         }
2014
2015         return false;
2016     }
2017
2018     _typeFilterScopeBarSelectionChanged(event)
2019     {
2020         // FIXME: <https://webkit.org/b/176763> Web Inspector: ScopeBar SelectionChanged event may dispatch multiple times for a single logical change
2021         // We can't use shallow equals here because the contents are functions.
2022         let oldFilter = this._activeTypeFilters;
2023         let newFilter = this._generateTypeFilter();
2024         if (this._areFilterListsIdentical(oldFilter, newFilter))
2025             return;
2026
2027         // Even if the selected resource would still be visible, lets close the detail view if a filter changes.
2028         this._hideDetailView();
2029
2030         this._activeTypeFilters = newFilter;
2031         this._updateFilteredEntries();
2032         this._reloadTable();
2033     }
2034
2035     _handleGroupMediaRequestsByDOMNodeCheckedDidChange(event)
2036     {
2037         WI.settings.groupMediaRequestsByDOMNode.value = this._groupMediaRequestsByDOMNodeNavigationItem.checked;
2038
2039         if (!WI.settings.groupMediaRequestsByDOMNode.value) {
2040             this._table.element.classList.remove("grouped");
2041
2042             if (this._selectedObject && this._selectedObject instanceof WI.DOMNode) {
2043                 this._selectedObject = null;
2044                 this._hideDetailView();
2045             }
2046         }
2047
2048         this._updateSort();
2049         this._updateFilteredEntries();
2050         this._reloadTable();
2051     }
2052
2053     _urlFilterDidChange(event)
2054     {
2055         let searchQuery = this._urlFilterNavigationItem.filterBar.filters.text;
2056         if (searchQuery === this._urlFilterSearchText)
2057             return;
2058
2059         // Even if the selected resource would still be visible, lets close the detail view if a filter changes.
2060         this._hideDetailView();
2061
2062         // Search cleared.
2063         if (!searchQuery) {
2064             this._urlFilterSearchText = null;
2065             this._urlFilterSearchRegex = null;
2066             this._urlFilterIsActive = false;
2067             this._activeURLFilterResources.clear();
2068
2069             this._updateFilteredEntries();
2070             this._reloadTable();
2071             return;
2072         }
2073
2074         this._urlFilterIsActive = true;
2075         this._urlFilterSearchText = searchQuery;
2076         this._urlFilterSearchRegex = WI.SearchUtilities.regExpForString(searchQuery, WI.SearchUtilities.defaultSettings);
2077
2078         this._updateActiveFilterResources();
2079         this._updateFilteredEntries();
2080         this._reloadTable();
2081     }
2082
2083     _updateActiveFilterResources()
2084     {
2085         this._activeURLFilterResources.clear();
2086
2087         if (this._hasURLFilter()) {
2088             for (let entry of this._activeCollection.entries)
2089                 this._checkURLFilterAgainstResource(entry.resource);
2090         }
2091     }
2092
2093     _restoreSelectedRow()
2094     {
2095         if (!this._selectedObject)
2096             return;
2097
2098         let rowIndex = this._rowIndexForRepresentedObject(this._selectedObject);
2099         if (rowIndex === -1) {
2100             this._selectedObject = null;
2101             this._table.deselectAll();
2102             return;
2103         }
2104
2105         this._table.selectRow(rowIndex);
2106         this._showDetailView(this._selectedObject);
2107     }
2108
2109     _HARResources()
2110     {
2111         let resources = this._activeCollection.filteredEntries.map((x) => x.resource);
2112         const supportedHARSchemes = new Set(["http", "https", "ws", "wss"]);
2113         return resources.filter((resource) => {
2114             if (!resource) {
2115                 // DOM node entries are also added to `filteredEntries`.
2116                 return false;
2117             }
2118
2119             if (!resource.finished)
2120                 return false;
2121             if (!resource.requestSentDate)
2122                 return false;
2123             if (!supportedHARSchemes.has(resource.urlComponents.scheme))
2124                 return false;
2125             return true;
2126         });
2127     }
2128
2129     _exportHAR()
2130     {
2131         let resources = this._HARResources();
2132         if (!resources.length) {
2133             InspectorFrontendHost.beep();
2134             return;
2135         }
2136
2137         WI.HARBuilder.buildArchive(resources).then((har) => {
2138             let mainFrame = WI.networkManager.mainFrame;
2139             let archiveName = mainFrame.mainResource.urlComponents.host || mainFrame.mainResource.displayName || "Archive";
2140             WI.FileUtilities.save({
2141                 url: WI.FileUtilities.inspectorURLForFilename(archiveName + ".har"),
2142                 content: JSON.stringify(har, null, 2),
2143                 forceSaveAs: true,
2144             });
2145         });
2146     }
2147
2148     _importHAR()
2149     {
2150         WI.FileUtilities.importJSON((result) => this.processHAR(result));
2151     }
2152
2153     _waterfallPopoverContent()
2154     {
2155         let contentElement = document.createElement("div");
2156         contentElement.classList.add("waterfall-popover-content");
2157         return contentElement;
2158     }
2159
2160     _waterfallPopoverContentForResourceEntry(resourceEntry)
2161     {
2162         let contentElement = this._waterfallPopoverContent();
2163
2164         let resource = resourceEntry.resource;
2165         if (!resource.hasResponse() || !resource.firstTimestamp || !resource.lastTimestamp) {
2166             contentElement.textContent = WI.UIString("Resource has no timing data");
2167             return contentElement;
2168         }
2169
2170         let breakdownView = new WI.ResourceTimingBreakdownView(resource, 300);
2171         contentElement.appendChild(breakdownView.element);
2172         breakdownView.updateLayout();
2173
2174         return contentElement;
2175     }
2176
2177     _waterfallPopoverContentForNodeEntry(nodeEntry, domEvents)
2178     {
2179         let contentElement = this._waterfallPopoverContent();
2180
2181         let breakdownView = new WI.DOMEventsBreakdownView(domEvents);
2182         contentElement.appendChild(breakdownView.element);
2183         breakdownView.updateLayout();
2184
2185         return contentElement;
2186     }
2187
2188     _handleResourceEntryMousedownWaterfall(resourceEntry)
2189     {
2190         let popoverContentElement = this._waterfallPopoverContentForResourceEntry(resourceEntry);
2191         this._handleMousedownWaterfall(resourceEntry, popoverContentElement, (cell) => {
2192             return cell.querySelector(".block.mouse-tracking");
2193         });
2194     }
2195
2196     _handleNodeEntryMousedownWaterfall(nodeEntry, domEvents)
2197     {
2198         let popoverContentElement = this._waterfallPopoverContentForNodeEntry(nodeEntry, domEvents);
2199         this._handleMousedownWaterfall(nodeEntry, popoverContentElement, (cell) => {
2200             let domEventElement = nodeEntry.domEventElements.get(domEvents[0]);
2201
2202             // Show any additional DOM events that have been merged into the range.
2203             if (domEventElement && this._waterfallPopover.visible) {
2204                 let newDOMEvents = Array.from(nodeEntry.domEventElements)
2205                 .filter(([domEvent, element]) => element === domEventElement)
2206                 .map(([domEvent, element]) => domEvent);
2207
2208                 this._waterfallPopover.content = this._waterfallPopoverContentForNodeEntry(nodeEntry, newDOMEvents);
2209             }
2210
2211             return domEventElement;
2212         });
2213     }
2214
2215     _handleMousedownWaterfall(entry, popoverContentElement, updateTargetAndContentFunction)
2216     {
2217         if (!this._waterfallPopover) {
2218             this._waterfallPopover = new WI.Popover;
2219             this._waterfallPopover.element.classList.add("waterfall-popover");
2220         }
2221
2222         if (this._waterfallPopover.visible)
2223             return;
2224
2225         let calculateTargetFrame = () => {
2226             let rowIndex = this._rowIndexForRepresentedObject(entry.resource || entry.domNode);
2227             let cell = this._table.cellForRowAndColumn(rowIndex, this._waterfallColumn);
2228             if (cell) {
2229                 let targetElement = updateTargetAndContentFunction(cell);
2230                 if (targetElement)
2231                     return WI.Rect.rectFromClientRect(targetElement.getBoundingClientRect());
2232             }
2233
2234             this._waterfallPopover.dismiss();
2235             return null;
2236         };
2237
2238         let targetFrame = calculateTargetFrame();
2239         if (!targetFrame)
2240             return;
2241         if (!targetFrame.size.width && !targetFrame.size.height)
2242             return;
2243
2244         let isRTL = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL;
2245         let preferredEdges = isRTL ? [WI.RectEdge.MAX_Y, WI.RectEdge.MIN_Y, WI.RectEdge.MAX_X] : [WI.RectEdge.MAX_Y, WI.RectEdge.MIN_Y, WI.RectEdge.MIN_X];
2246         this._waterfallPopover.windowResizeHandler = () => {
2247             let bounds = calculateTargetFrame();
2248             if (bounds)
2249                 this._waterfallPopover.present(bounds, preferredEdges);
2250         };
2251
2252         this._waterfallPopover.presentNewContentWithFrame(popoverContentElement, targetFrame, preferredEdges);
2253     }
2254
2255     _tableNameColumnDidChangeWidth(event)
2256     {
2257         this._nameColumnWidthSetting.value = event.target.width;
2258
2259         this._positionDetailView();
2260         this._positionEmptyFilterMessage();
2261     }
2262
2263     _tableWaterfallColumnDidChangeWidth(event)
2264     {
2265         this._table.reloadVisibleColumnCells(this._waterfallColumn);
2266     }
2267
2268     _transitionPageTarget(event)
2269     {
2270         this._transitioningPageTarget = true;
2271     }
2272 };