c6863c86f7ffef4d122373f2ef7d2008f299e8ee
[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         this._entries = [];
33         this._entriesSortComparator = null;
34         this._filteredEntries = [];
35         this._pendingInsertions = [];
36         this._pendingUpdates = [];
37         this._pendingFilter = false;
38         this._showingRepresentedObjectCookie = null;
39
40         this._table = null;
41         this._nameColumnWidthSetting = new WI.Setting("network-table-content-view-name-column-width", 250);
42
43         this._selectedObject = null;
44         this._detailView = null;
45         this._detailViewMap = new Map;
46
47         this._domNodeEntries = new Map;
48
49         this._waterfallStartTime = NaN;
50         this._waterfallEndTime = NaN;
51         this._waterfallTimelineRuler = null;
52         this._waterfallPopover = null;
53
54         // FIXME: Network Timeline.
55         // FIXME: Throttling.
56
57         const exclusive = true;
58         this._typeFilterScopeBarItemAll = new WI.ScopeBarItem("network-type-filter-all", WI.UIString("All"), exclusive);
59         let typeFilterScopeBarItems = [this._typeFilterScopeBarItemAll];
60
61         let uniqueTypes = [
62             ["Document", (type) => type === WI.Resource.Type.Document],
63             ["Stylesheet", (type) => type === WI.Resource.Type.Stylesheet],
64             ["Image", (type) => type === WI.Resource.Type.Image],
65             ["Font", (type) => type === WI.Resource.Type.Font],
66             ["Script", (type) => type === WI.Resource.Type.Script],
67             ["XHR", (type) => type === WI.Resource.Type.XHR || type === WI.Resource.Type.Fetch],
68             ["Other", (type) => {
69                 return type !== WI.Resource.Type.Document
70                     && type !== WI.Resource.Type.Stylesheet
71                     && type !== WI.Resource.Type.Image
72                     && type !== WI.Resource.Type.Font
73                     && type !== WI.Resource.Type.Script
74                     && type !== WI.Resource.Type.XHR
75                     && type !== WI.Resource.Type.Fetch;
76             }],
77         ];
78         for (let [key, checker] of uniqueTypes) {
79             let type = WI.Resource.Type[key];
80             let scopeBarItem = new WI.ScopeBarItem("network-type-filter-" + key, WI.NetworkTableContentView.shortDisplayNameForResourceType(type));
81             scopeBarItem.__checker = checker;
82             typeFilterScopeBarItems.push(scopeBarItem);
83         }
84
85         this._typeFilterScopeBar = new WI.ScopeBar("network-type-filter-scope-bar", typeFilterScopeBarItems, typeFilterScopeBarItems[0]);
86         this._typeFilterScopeBar.addEventListener(WI.ScopeBar.Event.SelectionChanged, this._typeFilterScopeBarSelectionChanged, this);
87
88         this._groupByDOMNodeNavigationItem = new WI.CheckboxNavigationItem("group-by-node", WI.UIString("Group by Node"), WI.settings.groupByDOMNode.value);
89         this._groupByDOMNodeNavigationItem.addEventListener(WI.CheckboxNavigationItem.Event.CheckedDidChange, this._handleGroupByDOMNodeCheckedDidChange, this);
90
91         this._urlFilterSearchText = null;
92         this._urlFilterSearchRegex = null;
93         this._urlFilterIsActive = false;
94
95         this._urlFilterNavigationItem = new WI.FilterBarNavigationItem;
96         this._urlFilterNavigationItem.filterBar.addEventListener(WI.FilterBar.Event.FilterDidChange, this._urlFilterDidChange, this);
97         this._urlFilterNavigationItem.filterBar.placeholder = WI.UIString("Filter Full URL");
98
99         this._activeTypeFilters = this._generateTypeFilter();
100         this._activeURLFilterResources = new Set;
101
102         this._emptyFilterResultsMessageElement = null;
103
104         this._clearOnLoadNavigationItem = new WI.CheckboxNavigationItem("perserve-log", WI.UIString("Preserve Log"), !WI.settings.clearNetworkOnNavigate.value);
105         this._clearOnLoadNavigationItem.tooltip = WI.UIString("Do not clear network items on new page loads");
106         this._clearOnLoadNavigationItem.addEventListener(WI.CheckboxNavigationItem.Event.CheckedDidChange, () => { WI.settings.clearNetworkOnNavigate.value = !WI.settings.clearNetworkOnNavigate.value; });
107         WI.settings.clearNetworkOnNavigate.addEventListener(WI.Setting.Event.Changed, this._clearNetworkOnNavigateSettingChanged, this);
108
109         this._harExportNavigationItem = new WI.ButtonNavigationItem("har-export", WI.UIString("Export"), "Images/Export.svg", 15, 15);
110         this._harExportNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText;
111         this._harExportNavigationItem.tooltip = WI.UIString("HAR Export (%s)").format(WI.saveKeyboardShortcut.displayName);
112         this._harExportNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, () => { this._exportHAR(); });
113
114         this._checkboxsNavigationItemGroup = new WI.GroupNavigationItem([this._clearOnLoadNavigationItem, new WI.DividerNavigationItem]);
115         this._checkboxsNavigationItemGroup.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
116
117         this._buttonsNavigationItemGroup = new WI.GroupNavigationItem([this._harExportNavigationItem, new WI.DividerNavigationItem]);
118         this._buttonsNavigationItemGroup.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
119
120         // COMPATIBILITY (iOS 10.3): Network.setDisableResourceCaching did not exist.
121         if (window.NetworkAgent && NetworkAgent.setResourceCachingDisabled) {
122             let toolTipForDisableResourceCache = WI.UIString("Ignore the resource cache when loading resources");
123             let activatedToolTipForDisableResourceCache = WI.UIString("Use the resource cache when loading resources");
124             this._disableResourceCacheNavigationItem = new WI.ActivateButtonNavigationItem("disable-resource-cache", toolTipForDisableResourceCache, activatedToolTipForDisableResourceCache, "Images/IgnoreCaches.svg", 16, 16);
125             this._disableResourceCacheNavigationItem.activated = WI.resourceCachingDisabledSetting.value;
126
127             this._disableResourceCacheNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._toggleDisableResourceCache, this);
128             WI.resourceCachingDisabledSetting.addEventListener(WI.Setting.Event.Changed, this._resourceCachingDisabledSettingChanged, this);
129         }
130
131         this._clearNetworkItemsNavigationItem = new WI.ButtonNavigationItem("clear-network-items", WI.UIString("Clear Network Items (%s)").format(WI.clearKeyboardShortcut.displayName), "Images/NavigationItemTrash.svg", 15, 15);
132         this._clearNetworkItemsNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, () => { this.reset(); });
133
134         WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
135         WI.Resource.addEventListener(WI.Resource.Event.LoadingDidFinish, this._resourceLoadingDidFinish, this);
136         WI.Resource.addEventListener(WI.Resource.Event.LoadingDidFail, this._resourceLoadingDidFail, this);
137         WI.Resource.addEventListener(WI.Resource.Event.TransferSizeDidChange, this._resourceTransferSizeDidChange, this);
138         WI.networkManager.addEventListener(WI.NetworkManager.Event.MainFrameDidChange, this._mainFrameDidChange, this);
139         WI.timelineManager.persistentNetworkTimeline.addEventListener(WI.Timeline.Event.RecordAdded, this._networkTimelineRecordAdded, this);
140
141         this._needsInitialPopulate = true;
142     }
143
144     // Static
145
146     static displayNameForResource(resource)
147     {
148         if (resource.type === WI.Resource.Type.Image || resource.type === WI.Resource.Type.Font || resource.type === WI.Resource.Type.Other) {
149             let fileExtension;
150             if (resource.mimeType)
151                 fileExtension = WI.fileExtensionForMIMEType(resource.mimeType);
152             if (!fileExtension)
153                 fileExtension = WI.fileExtensionForURL(resource.url);
154             if (fileExtension)
155                 return fileExtension;
156         }
157
158         return WI.NetworkTableContentView.shortDisplayNameForResourceType(resource.type).toLowerCase();
159     }
160
161     static shortDisplayNameForResourceType(type)
162     {
163         switch (type) {
164         case WI.Resource.Type.Document:
165             return WI.UIString("Document");
166         case WI.Resource.Type.Stylesheet:
167             return "CSS";
168         case WI.Resource.Type.Image:
169             return WI.UIString("Image");
170         case WI.Resource.Type.Font:
171             return WI.UIString("Font");
172         case WI.Resource.Type.Script:
173             return "JS";
174         case WI.Resource.Type.XHR:
175             return "XHR";
176         case WI.Resource.Type.Fetch:
177             return WI.UIString("Fetch");
178         case WI.Resource.Type.Ping:
179             return WI.UIString("Ping");
180         case WI.Resource.Type.Beacon:
181             return WI.UIString("Beacon");
182         case WI.Resource.Type.WebSocket:
183         case WI.Resource.Type.Other:
184             return WI.UIString("Other");
185         default:
186             console.error("Unknown resource type", type);
187             return null;
188         }
189     }
190
191     // Public
192
193     get selectionPathComponents()
194     {
195         return null;
196     }
197
198     get navigationItems()
199     {
200         let items = [this._checkboxsNavigationItemGroup, this._buttonsNavigationItemGroup];
201         if (this._disableResourceCacheNavigationItem)
202             items.push(this._disableResourceCacheNavigationItem);
203         items.push(this._clearNetworkItemsNavigationItem);
204         return items;
205     }
206
207     get filterNavigationItems()
208     {
209         return [this._urlFilterNavigationItem, this._typeFilterScopeBar, this._groupByDOMNodeNavigationItem];
210     }
211
212     get supportsSave()
213     {
214         return this._filteredEntries.some((entry) => entry.resource.finished);
215     }
216
217     get saveData()
218     {
219         return {customSaveHandler: () => { this._exportHAR(); }};
220     }
221
222     shown()
223     {
224         super.shown();
225
226         if (this._detailView)
227             this._detailView.shown();
228
229         if (this._table)
230             this._table.restoreScrollPosition();
231     }
232
233     hidden()
234     {
235         this._hidePopover();
236
237         if (this._detailView)
238             this._detailView.hidden();
239
240         super.hidden();
241     }
242
243     closed()
244     {
245         for (let detailView of this._detailViewMap.values())
246             detailView.dispose();
247         this._detailViewMap.clear();
248
249         this._domNodeEntries.clear();
250
251         this._hidePopover();
252         this._hideDetailView();
253
254         WI.Frame.removeEventListener(null, null, this);
255         WI.Resource.removeEventListener(null, null, this);
256         WI.resourceCachingDisabledSetting.removeEventListener(null, null, this);
257         WI.settings.clearNetworkOnNavigate.removeEventListener(null, null, this);
258         WI.networkManager.removeEventListener(WI.NetworkManager.Event.MainFrameDidChange, this._mainFrameDidChange, this);
259         WI.timelineManager.persistentNetworkTimeline.removeEventListener(WI.Timeline.Event.RecordAdded, this._networkTimelineRecordAdded, this);
260
261         super.closed();
262     }
263
264     reset()
265     {
266         this._entries = [];
267         this._filteredEntries = [];
268         this._pendingInsertions = [];
269
270         for (let detailView of this._detailViewMap.values())
271             detailView.dispose();
272         this._detailViewMap.clear();
273
274         this._domNodeEntries.clear();
275
276         this._waterfallStartTime = NaN;
277         this._waterfallEndTime = NaN;
278         this._updateWaterfallTimelineRuler();
279         this._updateExportButton();
280
281         if (this._table) {
282             this._selectedResource = null;
283             this._table.reloadData();
284             this._hidePopover();
285             this._hideDetailView();
286         }
287     }
288
289     showRepresentedObject(representedObject, cookie)
290     {
291         console.assert(representedObject instanceof WI.Resource);
292
293         let rowIndex = this._rowIndexForRepresentedObject(representedObject);
294         if (rowIndex === -1) {
295             this._selectedResource = null;
296             this._table.deselectAll();
297             this._hideDetailView();
298             return;
299         }
300
301         this._showingRepresentedObjectCookie = cookie;
302         this._table.selectRow(rowIndex);
303         this._showingRepresentedObjectCookie = null;
304     }
305
306     // NetworkDetailView delegate
307
308     networkDetailViewClose(networkDetailView)
309     {
310         this._selectedResource = null;
311         this._table.deselectAll();
312         this._hideDetailView();
313     }
314
315     // Table dataSource
316
317     tableNumberOfRows(table)
318     {
319         return this._filteredEntries.length;
320     }
321
322     tableSortChanged(table)
323     {
324         this._generateSortComparator();
325
326         if (!this._entriesSortComparator)
327             return;
328
329         this._hideDetailView();
330
331         for (let nodeEntry of this._domNodeEntries.values())
332             nodeEntry.initiatedResourceEntries.sort(this._entriesSortComparator);
333
334         this._updateSort();
335         this._updateFilteredEntries();
336         this._reloadTable();
337     }
338
339     // Table delegate
340
341     tableCellContextMenuClicked(table, cell, column, rowIndex, event)
342     {
343         if (column !== this._nameColumn)
344             return;
345
346         this._table.selectRow(rowIndex);
347
348         let entry = this._filteredEntries[rowIndex];
349         let contextMenu = WI.ContextMenu.createFromEvent(event);
350         WI.appendContextMenuItemsForSourceCode(contextMenu, entry.resource);
351
352         contextMenu.appendSeparator();
353         contextMenu.appendItem(WI.UIString("Export HAR"), () => { this._exportHAR(); });
354     }
355
356     tableShouldSelectRow(table, cell, column, rowIndex)
357     {
358         return column === this._nameColumn;
359     }
360
361     tableSelectionDidChange(table)
362     {
363         let rowIndex = table.selectedRow;
364         if (isNaN(rowIndex)) {
365             this._selectedObject = null;
366             this._hideDetailView();
367             return;
368         }
369
370         let entry = this._filteredEntries[rowIndex];
371         if (entry.resource === this._selectedObject || entry.domNode === this._selectedObject)
372             return;
373
374         this._selectedObject = entry.resource || entry.domNode;
375         if (this._selectedObject)
376             this._showDetailView(this._selectedObject);
377         else
378             this._hideDetailView();
379     }
380
381     tablePopulateCell(table, cell, column, rowIndex)
382     {
383         let entry = this._filteredEntries[rowIndex];
384
385         if (entry.resource)
386             cell.classList.toggle("error", entry.resource.hadLoadingError());
387
388         let setTextContent = (accessor) => {
389             let uniqueValues = this._uniqueValuesForDOMNodeEntry(entry, accessor);
390             if (uniqueValues) {
391                 if (uniqueValues.size > 1) {
392                     cell.classList.add("multiple");
393                     cell.textContent = WI.UIString("(multiple)");
394                     return;
395                 }
396
397                 cell.textContent = uniqueValues.values().next().value || emDash;
398                 return;
399             }
400
401             cell.textContent = accessor(entry) || emDash;
402         };
403
404         switch (column.identifier) {
405         case "name":
406             this._populateNameCell(cell, entry);
407             break;
408         case "domain":
409             this._populateDomainCell(cell, entry);
410             break;
411         case "type":
412             setTextContent((resourceEntry) => resourceEntry.displayType);
413             break;
414         case "mimeType":
415             setTextContent((resourceEntry) => resourceEntry.mimeType);
416             break;
417         case "method":
418             setTextContent((resourceEntry) => resourceEntry.method);
419             break;
420         case "scheme":
421             setTextContent((resourceEntry) => resourceEntry.scheme);
422             break;
423         case "status":
424             setTextContent((resourceEntry) => resourceEntry.status);
425             break;
426         case "protocol":
427             setTextContent((resourceEntry) => resourceEntry.protocol);
428             break;
429         case "initiator":
430             this._populateInitiatorCell(cell, entry);
431             break;
432         case "priority":
433             setTextContent((resourceEntry) => WI.Resource.displayNameForPriority(resourceEntry.priority));
434             break;
435         case "remoteAddress":
436             setTextContent((resourceEntry) => resourceEntry.remoteAddress);
437             break;
438         case "connectionIdentifier":
439             setTextContent((resourceEntry) => resourceEntry.connectionIdentifier);
440             break;
441         case "resourceSize": {
442             let resourceSize = entry.resourceSize;
443             let resourceEntries = entry.initiatedResourceEntries;
444             if (resourceEntries)
445                 resourceSize = resourceEntries.reduce((accumulator, current) => accumulator + (current.resourceSize || 0), 0);
446             cell.textContent = isNaN(resourceSize) ? emDash : Number.bytesToString(resourceSize);
447             break;
448         }
449         case "transferSize":
450             this._populateTransferSizeCell(cell, entry);
451             break;
452         case "time": {
453             // FIXME: <https://webkit.org/b/176748> Web Inspector: Frontend sometimes receives resources with negative duration (responseEnd - requestStart)
454             let time = entry.time;
455             let resourceEntries = entry.initiatedResourceEntries;
456             if (resourceEntries)
457                 time = resourceEntries.reduce((accumulator, current) => accumulator + (current.time || 0), 0);
458             cell.textContent = isNaN(time) ? emDash : Number.secondsToString(Math.max(time, 0));
459             break;
460         }
461         case "waterfall":
462             this._populateWaterfallGraph(cell, entry);
463             break;
464         }
465
466         return cell;
467     }
468
469     // Private
470
471     _populateNameCell(cell, entry)
472     {
473         console.assert(!cell.firstChild, "We expect the cell to be empty.", cell, cell.firstChild);
474
475         function createIconElement() {
476             let iconElement = cell.appendChild(document.createElement("img"));
477             iconElement.className = "icon";
478         }
479
480         let domNode = entry.domNode;
481         if (domNode) {
482             this._table.element.classList.add("grouped");
483
484             cell.classList.add("parent");
485
486             let disclosureElement = cell.appendChild(document.createElement("img"));
487             disclosureElement.classList.add("disclosure");
488             disclosureElement.classList.toggle("expanded", !!entry.expanded);
489             disclosureElement.addEventListener("click", (event) => {
490                 entry.expanded = !entry.expanded;
491
492                 this._updateFilteredEntries();
493                 this._reloadTable();
494             });
495
496             createIconElement();
497
498             cell.classList.add("dom-node");
499             cell.appendChild(WI.linkifyNodeReference(domNode));
500             return;
501         }
502
503         let resource = entry.resource;
504         if (resource.isLoading()) {
505             let statusElement = cell.appendChild(document.createElement("div"));
506             statusElement.className = "status";
507             let spinner = new WI.IndeterminateProgressSpinner;
508             statusElement.appendChild(spinner.element);
509         }
510
511         createIconElement();
512
513         cell.classList.add(WI.ResourceTreeElement.ResourceIconStyleClassName);
514
515         let nameElement = cell.appendChild(document.createElement("span"));
516
517         if (WI.settings.groupByDOMNode.value && resource.initiatorNode) {
518             let nodeEntry = this._domNodeEntries.get(resource.initiatorNode);
519             if (nodeEntry.initiatedResourceEntries.length > 1 || nodeEntry.domNode.domEvents.length) {
520                 cell.classList.add("child");
521
522                 let range = resource.requestedByteRange;
523                 if (range)
524                     nameElement.textContent = WI.UIString("Byte Range %s\u2013%s").format(range.start, range.end);
525             }
526         }
527
528         if (!nameElement.textContent)
529             nameElement.textContent = entry.name;
530
531         cell.title = resource.url;
532         cell.classList.add(WI.Resource.classNameForResource(resource));
533     }
534
535     _populateDomainCell(cell, entry)
536     {
537         console.assert(!cell.firstChild, "We expect the cell to be empty.", cell, cell.firstChild);
538
539         function createIconAndText(scheme, domain) {
540             let secure = scheme === "https" || scheme === "wss";
541             if (secure) {
542                 let lockIconElement = cell.appendChild(document.createElement("img"));
543                 lockIconElement.className = "lock";
544             }
545
546             cell.append(domain);
547         }
548
549         let uniqueSchemeValues = this._uniqueValuesForDOMNodeEntry(entry, (resourceEntry) => resourceEntry.scheme);
550         let uniqueDomainValues = this._uniqueValuesForDOMNodeEntry(entry, (resourceEntry) => resourceEntry.domain);
551         if (uniqueSchemeValues && uniqueDomainValues) {
552             if (uniqueSchemeValues.size > 1 || uniqueDomainValues.size > 1) {
553                 cell.classList.add("multiple");
554                 cell.textContent = WI.UIString("(multiple)");
555                 return;
556             }
557
558             createIconAndText(uniqueSchemeValues.values().next().value, uniqueDomainValues.values().next().value);
559             return;
560         }
561
562         if (!entry.domain) {
563             cell.textContent = emDash;
564             return;
565         }
566
567         createIconAndText(entry.scheme, entry.domain);
568     }
569
570     _populateInitiatorCell(cell, entry)
571     {
572         let domNode = entry.domNode;
573         if (domNode) {
574             cell.textContent = emDash;
575             return;
576         }
577
578         let initiatorLocation = entry.resource.initiatorSourceCodeLocation;
579         if (!initiatorLocation) {
580             cell.textContent = emDash;
581             return;
582         }
583
584         const options = {
585             dontFloat: true,
586             ignoreSearchTab: true,
587         };
588         cell.appendChild(WI.createSourceCodeLocationLink(initiatorLocation, options));
589     }
590
591     _populateTransferSizeCell(cell, entry)
592     {
593         let resourceEntries = entry.initiatedResourceEntries;
594         if (resourceEntries) {
595             if (resourceEntries.every((resourceEntry) => resourceEntry.resource.responseSource === WI.Resource.ResponseSource.MemoryCache)) {
596                 cell.classList.add("cache-type");
597                 cell.textContent = WI.UIString("(memory)");
598                 return;
599             }
600             if (resourceEntries.every((resourceEntry) => resourceEntry.resource.responseSource === WI.Resource.ResponseSource.DiskCache)) {
601                 cell.classList.add("cache-type");
602                 cell.textContent = WI.UIString("(disk)");
603                 return;
604             }
605             if (resourceEntries.every((resourceEntry) => resourceEntry.resource.responseSource === WI.Resource.ResponseSource.ServiceWorker)) {
606                 cell.classList.add("cache-type");
607                 cell.textContent = WI.UIString("(service worker)");
608                 return;
609             }
610             let transferSize = resourceEntries.reduce((accumulator, current) => accumulator + (current.transferSize || 0), 0);
611             if (isNaN(transferSize))
612                 cell.textContent = emDash;
613             else
614                 cell.textContent = Number.bytesToString(transferSize);
615             return;
616         }
617
618         let responseSource = entry.resource.responseSource;
619         if (responseSource === WI.Resource.ResponseSource.MemoryCache) {
620             cell.classList.add("cache-type");
621             cell.textContent = WI.UIString("(memory)");
622             return;
623         }
624         if (responseSource === WI.Resource.ResponseSource.DiskCache) {
625             cell.classList.add("cache-type");
626             cell.textContent = WI.UIString("(disk)");
627             return;
628         }
629         if (responseSource === WI.Resource.ResponseSource.ServiceWorker) {
630             cell.classList.add("cache-type");
631             cell.textContent = WI.UIString("(service worker)");
632             return;
633         }
634
635         let transferSize = entry.transferSize;
636         cell.textContent = isNaN(transferSize) ? emDash : Number.bytesToString(transferSize);
637         console.assert(!cell.classList.contains("cache-type"), "Should not have cache-type class on cell.");
638     }
639
640     _populateWaterfallGraph(cell, entry)
641     {
642         cell.removeChildren();
643
644         let container = cell.appendChild(document.createElement("div"));
645         container.className = "waterfall-container";
646
647         let graphStartTime = this._waterfallTimelineRuler.startTime;
648         let secondsPerPixel = this._waterfallTimelineRuler.secondsPerPixel;
649
650         function positionByStartOffset(element, timestamp) {
651             let styleAttribute = WI.resolvedLayoutDirection() === WI.LayoutDirection.LTR ? "left" : "right";
652             element.style.setProperty(styleAttribute, ((timestamp - graphStartTime) / secondsPerPixel) + "px");
653         }
654
655         function setWidthForDuration(element, startTimestamp, endTimestamp) {
656             element.style.setProperty("width", ((endTimestamp - startTimestamp) / secondsPerPixel) + "px");
657         }
658
659         let domNode = entry.domNode;
660         if (domNode) {
661             const domEventElementSize = 8; // Keep this in sync with `--node-waterfall-dom-event-size`.
662
663             let groupedDOMEvents = domNode.domEvents.reduce((accumulator, current) => {
664                 if (!accumulator.length || (current.timestamp - accumulator.lastValue.endTimestamp) >= (domEventElementSize * secondsPerPixel)) {
665                     accumulator.push({
666                         startTimestamp: current.timestamp,
667                         domEvents: [],
668                     });
669                 }
670                 accumulator.lastValue.endTimestamp = current.timestamp;
671                 accumulator.lastValue.domEvents.push(current);
672                 return accumulator;
673             }, []);
674
675             let playing = false;
676
677             function createDOMEventLine(domEvents, startTimestamp, endTimestamp) {
678                 if (domEvents.lastValue.eventName === "ended")
679                     return;
680
681                 for (let i = domEvents.length - 1; i >= 0; --i) {
682                     let domEvent = domEvents[i];
683                     if (domEvent.eventName === "play" || domEvent.eventName === "playing" || domEvent.eventName === "timeupdate") {
684                         playing = true;
685                         break;
686                     }
687
688                     if (domEvent.eventName === "pause" || domEvent.eventName === "stall") {
689                         playing = false;
690                         break;
691                     }
692                 }
693
694                 let lineElement = container.appendChild(document.createElement("div"));
695                 lineElement.classList.add("dom-activity");
696                 lineElement.classList.toggle("playing", playing);
697                 positionByStartOffset(lineElement, startTimestamp);
698                 setWidthForDuration(lineElement, startTimestamp, endTimestamp);
699             }
700
701             for (let [a, b] of groupedDOMEvents.adjacencies())
702                 createDOMEventLine(a.domEvents, a.endTimestamp, b.startTimestamp);
703
704             if (groupedDOMEvents.length)
705                 createDOMEventLine(groupedDOMEvents.lastValue.domEvents, groupedDOMEvents.lastValue.endTimestamp, this._waterfallEndTime);
706
707             for (let {startTimestamp, endTimestamp, domEvents} of groupedDOMEvents) {
708                 let paddingForCentering = domEventElementSize * secondsPerPixel / 2;
709
710                 let eventElement = container.appendChild(document.createElement("div"));
711                 eventElement.classList.add("dom-event");
712                 positionByStartOffset(eventElement, startTimestamp - paddingForCentering);
713                 setWidthForDuration(eventElement, startTimestamp, endTimestamp + paddingForCentering);
714                 eventElement.addEventListener("mousedown", (event) => {
715                     if (event.button !== 0 || event.ctrlKey)
716                         return;
717                     this._handleNodeEntryMousedownWaterfall(eventElement, entry, domEvents);
718                 });
719             }
720
721             return;
722         }
723
724         let resource = entry.resource;
725         if (!resource.hasResponse()) {
726             cell.textContent = zeroWidthSpace;
727             return;
728         }
729
730         let {startTime, redirectStart, redirectEnd, fetchStart, domainLookupStart, domainLookupEnd, connectStart, connectEnd, secureConnectionStart, requestStart, responseStart, responseEnd} = resource.timingData;
731         if (isNaN(startTime) || isNaN(responseEnd)) {
732             cell.textContent = zeroWidthSpace;
733             return;
734         }
735
736         if (responseEnd < graphStartTime) {
737             cell.textContent = zeroWidthSpace;
738             return;
739         }
740
741         let graphEndTime = this._waterfallTimelineRuler.endTime;
742         if (startTime > graphEndTime) {
743             cell.textContent = zeroWidthSpace;
744             return;
745         }
746
747         function appendBlock(startTimestamp, endTimestamp, className) {
748             if (isNaN(startTimestamp) || isNaN(endTimestamp) || endTimestamp - startTimestamp <= 0)
749                 return null;
750
751             let block = container.appendChild(document.createElement("div"));
752             block.classList.add("block", className);
753             positionByStartOffset(block, startTimestamp);
754             setWidthForDuration(block, startTimestamp, endTimestamp);
755             return block;
756         }
757
758         // Mouse block sits on top and accepts mouse events on this group.
759         let padSeconds = 10 * secondsPerPixel;
760         let mouseBlock = appendBlock(startTime - padSeconds, responseEnd + padSeconds, "mouse-tracking");
761         mouseBlock.addEventListener("mousedown", (event) => {
762             if (event.button !== 0 || event.ctrlKey)
763                 return;
764             this._handleResourceEntryMousedownWaterfall(mouseBlock, entry);
765         });
766
767         // Super small visualization.
768         let totalWidth = (responseEnd - startTime) / secondsPerPixel;
769         if (totalWidth <= 3) {
770             appendBlock(startTime, requestStart, "queue");
771             appendBlock(startTime, responseEnd, "response");
772             return;
773         }
774
775         appendBlock(startTime, responseEnd, "filler");
776
777         // FIXME: <https://webkit.org/b/190214> Web Inspector: expose full load metrics for redirect requests
778         appendBlock(redirectStart, redirectEnd, "redirect");
779
780         if (domainLookupStart) {
781             appendBlock(fetchStart, domainLookupStart, "queue");
782             appendBlock(domainLookupStart, domainLookupEnd || connectStart || requestStart, "dns");
783         } else if (connectStart)
784             appendBlock(fetchStart, connectStart, "queue");
785         else if (requestStart)
786             appendBlock(fetchStart, requestStart, "queue");
787         if (connectStart)
788             appendBlock(connectStart, secureConnectionStart || connectEnd, "connect");
789         if (secureConnectionStart)
790             appendBlock(secureConnectionStart, connectEnd, "secure");
791         appendBlock(requestStart, responseStart, "request");
792         appendBlock(responseStart, responseEnd, "response");
793     }
794
795     _generateSortComparator()
796     {
797         let sortColumnIdentifier = this._table.sortColumnIdentifier;
798         if (!sortColumnIdentifier) {
799             this._entriesSortComparator = null;
800             return;
801         }
802
803         let comparator;
804
805         switch (sortColumnIdentifier) {
806         case "name":
807         case "domain":
808         case "mimeType":
809         case "method":
810         case "scheme":
811         case "protocol":
812         case "initiator":
813         case "remoteAddress":
814             // Simple string.
815             comparator = (a, b) => (a[sortColumnIdentifier] || "").extendedLocaleCompare(b[sortColumnIdentifier] || "");
816             break;
817
818         case "status":
819         case "connectionIdentifier":
820         case "resourceSize":
821         case "time":
822             // Simple number.
823             comparator = (a, b) => {
824                 let aValue = a[sortColumnIdentifier];
825                 if (isNaN(aValue))
826                     return 1;
827                 let bValue = b[sortColumnIdentifier];
828                 if (isNaN(bValue))
829                     return -1;
830                 return aValue - bValue;
831             };
832             break;
833
834         case "priority":
835             // Resource.NetworkPriority enum.
836             comparator = (a, b) => WI.Resource.comparePriority(a.priority, b.priority);
837             break;
838
839         case "type":
840             // Sort by displayType string.
841             comparator = (a, b) => (a.displayType || "").extendedLocaleCompare(b.displayType || "");
842             break;
843
844         case "transferSize":
845             // Handle (memory) and (disk) values.
846             comparator = (a, b) => {
847                 let transferSizeA = a.transferSize;
848                 let transferSizeB = b.transferSize;
849
850                 // Treat NaN as the largest value.
851                 if (isNaN(transferSizeA))
852                     return 1;
853                 if (isNaN(transferSizeB))
854                     return -1;
855
856                 // Treat memory cache and disk cache as small values.
857                 let sourceA = a.resource.responseSource;
858                 if (sourceA === WI.Resource.ResponseSource.MemoryCache)
859                     transferSizeA = -20;
860                 else if (sourceA === WI.Resource.ResponseSource.DiskCache)
861                     transferSizeA = -10;
862                 else if (sourceA === WI.Resource.ResponseSource.ServiceWorker)
863                     transferSizeA = -5;
864
865                 let sourceB = b.resource.responseSource;
866                 if (sourceB === WI.Resource.ResponseSource.MemoryCache)
867                     transferSizeB = -20;
868                 else if (sourceB === WI.Resource.ResponseSource.DiskCache)
869                     transferSizeB = -10;
870                 else if (sourceB === WI.Resource.ResponseSource.ServiceWorker)
871                     transferSizeB = -5;
872
873                 return transferSizeA - transferSizeB;
874             };
875             break;
876
877         case "waterfall":
878             // Sort by startTime number.
879             comparator = (a, b) => a.startTime - b.startTime;
880             break;
881
882         default:
883             console.assert("Unexpected sort column", sortColumnIdentifier);
884             return;
885         }
886
887         let reverseFactor = this._table.sortOrder === WI.Table.SortOrder.Ascending ? 1 : -1;
888
889         // If the entry has an `initiatorNode`, use that node's "first" resource as the value of
890         // `entry`, so long as the entry being compared to doesn't have the same `initiatorNode`.
891         // This will ensure that all resource entries for a given `initiatorNode` will appear right
892         // next to each other, as they will all effectively be sorted by the first resource.
893         let substitute = (entry, other) => {
894             if (WI.settings.groupByDOMNode.value && entry.resource.initiatorNode) {
895                 let nodeEntry = this._domNodeEntries.get(entry.resource.initiatorNode);
896                 if (!nodeEntry.initiatedResourceEntries.includes(other))
897                     return nodeEntry.initiatedResourceEntries[0];
898             }
899             return entry;
900         };
901
902         this._entriesSortComparator = (a, b) => reverseFactor * comparator(substitute(a, b), substitute(b, a));
903     }
904
905     // Protected
906
907     initialLayout()
908     {
909         this._waterfallTimelineRuler = new WI.TimelineRuler;
910         this._waterfallTimelineRuler.allowsClippedLabels = true;
911
912         this._nameColumn = new WI.TableColumn("name", WI.UIString("Name"), {
913             minWidth: WI.Sidebar.AbsoluteMinimumWidth,
914             maxWidth: 500,
915             initialWidth: this._nameColumnWidthSetting.value,
916             resizeType: WI.TableColumn.ResizeType.Locked,
917         });
918
919         this._domainColumn = new WI.TableColumn("domain", WI.UIString("Domain"), {
920             minWidth: 120,
921             maxWidth: 200,
922             initialWidth: 150,
923         });
924
925         this._typeColumn = new WI.TableColumn("type", WI.UIString("Type"), {
926             minWidth: 70,
927             maxWidth: 120,
928             initialWidth: 90,
929         });
930
931         this._mimeTypeColumn = new WI.TableColumn("mimeType", WI.UIString("MIME Type"), {
932             hidden: true,
933             minWidth: 100,
934             maxWidth: 150,
935             initialWidth: 120,
936         });
937
938         this._methodColumn = new WI.TableColumn("method", WI.UIString("Method"), {
939             hidden: true,
940             minWidth: 55,
941             maxWidth: 80,
942             initialWidth: 65,
943         });
944
945         this._schemeColumn = new WI.TableColumn("scheme", WI.UIString("Scheme"), {
946             hidden: true,
947             minWidth: 55,
948             maxWidth: 80,
949             initialWidth: 65,
950         });
951
952         this._statusColumn = new WI.TableColumn("status", WI.UIString("Status"), {
953             hidden: true,
954             minWidth: 50,
955             maxWidth: 50,
956             align: "left",
957         });
958
959         this._protocolColumn = new WI.TableColumn("protocol", WI.UIString("Protocol"), {
960             hidden: true,
961             minWidth: 65,
962             maxWidth: 80,
963             initialWidth: 75,
964         });
965
966         this._initiatorColumn = new WI.TableColumn("initiator", WI.UIString("Initiator"), {
967             hidden: true,
968             minWidth: 75,
969             maxWidth: 175,
970             initialWidth: 125,
971         });
972
973         this._priorityColumn = new WI.TableColumn("priority", WI.UIString("Priority"), {
974             hidden: true,
975             minWidth: 65,
976             maxWidth: 80,
977             initialWidth: 70,
978         });
979
980         this._remoteAddressColumn = new WI.TableColumn("remoteAddress", WI.UIString("IP Address"), {
981             hidden: true,
982             minWidth: 150,
983         });
984
985         this._connectionIdentifierColumn = new WI.TableColumn("connectionIdentifier", WI.UIString("Connection ID"), {
986             hidden: true,
987             minWidth: 50,
988             maxWidth: 120,
989             initialWidth: 80,
990             align: "right",
991         });
992
993         this._resourceSizeColumn = new WI.TableColumn("resourceSize", WI.UIString("Resource Size"), {
994             hidden: true,
995             minWidth: 80,
996             maxWidth: 100,
997             initialWidth: 80,
998             align: "right",
999         });
1000
1001         this._transferSizeColumn = new WI.TableColumn("transferSize", WI.UIString("Transfer Size"), {
1002             minWidth: 100,
1003             maxWidth: 150,
1004             initialWidth: 100,
1005             align: "right",
1006         });
1007
1008         this._timeColumn = new WI.TableColumn("time", WI.UIString("Time"), {
1009             minWidth: 65,
1010             maxWidth: 90,
1011             initialWidth: 65,
1012             align: "right",
1013         });
1014
1015         this._waterfallColumn = new WI.TableColumn("waterfall", WI.UIString("Waterfall"), {
1016             minWidth: 230,
1017             headerView: this._waterfallTimelineRuler,
1018         });
1019
1020         this._nameColumn.addEventListener(WI.TableColumn.Event.WidthDidChange, this._tableNameColumnDidChangeWidth, this);
1021         this._waterfallColumn.addEventListener(WI.TableColumn.Event.WidthDidChange, this._tableWaterfallColumnDidChangeWidth, this);
1022
1023         this._table = new WI.Table("network-table", this, this, 20);
1024
1025         this._table.addColumn(this._nameColumn);
1026         this._table.addColumn(this._domainColumn);
1027         this._table.addColumn(this._typeColumn);
1028         this._table.addColumn(this._mimeTypeColumn);
1029         this._table.addColumn(this._methodColumn);
1030         this._table.addColumn(this._schemeColumn);
1031         this._table.addColumn(this._statusColumn);
1032         this._table.addColumn(this._protocolColumn);
1033         this._table.addColumn(this._initiatorColumn);
1034         this._table.addColumn(this._priorityColumn);
1035         this._table.addColumn(this._remoteAddressColumn);
1036         this._table.addColumn(this._connectionIdentifierColumn);
1037         this._table.addColumn(this._resourceSizeColumn);
1038         this._table.addColumn(this._transferSizeColumn);
1039         this._table.addColumn(this._timeColumn);
1040         this._table.addColumn(this._waterfallColumn);
1041
1042         if (!this._table.sortColumnIdentifier) {
1043             this._table.sortOrder = WI.Table.SortOrder.Ascending;
1044             this._table.sortColumnIdentifier = "waterfall";
1045         }
1046
1047         this.addSubview(this._table);
1048     }
1049
1050     layout()
1051     {
1052         this._updateWaterfallTimelineRuler();
1053         this._processPendingEntries();
1054         this._positionDetailView();
1055         this._positionEmptyFilterMessage();
1056         this._updateExportButton();
1057     }
1058
1059     handleClearShortcut(event)
1060     {
1061         this.reset();
1062     }
1063
1064     // Private
1065
1066     _updateWaterfallTimelineRuler()
1067     {
1068         if (!this._waterfallTimelineRuler)
1069             return;
1070
1071         if (isNaN(this._waterfallStartTime)) {
1072             this._waterfallTimelineRuler.zeroTime = 0;
1073             this._waterfallTimelineRuler.startTime = 0;
1074             this._waterfallTimelineRuler.endTime = 0.250;
1075         } else {
1076             this._waterfallTimelineRuler.zeroTime = this._waterfallStartTime;
1077             this._waterfallTimelineRuler.startTime = this._waterfallStartTime;
1078             this._waterfallTimelineRuler.endTime = this._waterfallEndTime;
1079
1080             // Add a little bit of padding on the each side.
1081             const paddingPixels = 5;
1082             let padSeconds = paddingPixels * this._waterfallTimelineRuler.secondsPerPixel;
1083             this._waterfallTimelineRuler.zeroTime = this._waterfallStartTime - padSeconds;
1084             this._waterfallTimelineRuler.startTime = this._waterfallStartTime - padSeconds;
1085             this._waterfallTimelineRuler.endTime = this._waterfallEndTime + padSeconds;
1086         }
1087     }
1088
1089     _updateExportButton()
1090     {
1091         let enabled = this._filteredEntries.length > 0;
1092         this._harExportNavigationItem.enabled = enabled;
1093     }
1094
1095     _processPendingEntries()
1096     {
1097         let needsSort = this._pendingUpdates.length > 0;
1098         let needsFilter = this._pendingFilter;
1099
1100         // No global sort or filter is needed, so just insert new records into their sorted position.
1101         if (!needsSort && !needsFilter) {
1102             let originalLength = this._pendingInsertions.length;
1103             for (let resource of this._pendingInsertions)
1104                 this._insertResourceAndReloadTable(resource);
1105             console.assert(this._pendingInsertions.length === originalLength);
1106             this._pendingInsertions = [];
1107             return;
1108         }
1109
1110         for (let resource of this._pendingInsertions) {
1111             let resourceEntry = this._entryForResource(resource);
1112             this._tryLinkResourceToDOMNode(resourceEntry);
1113             this._entries.push(resourceEntry);
1114         }
1115         this._pendingInsertions = [];
1116
1117         for (let updateObject of this._pendingUpdates) {
1118             if (updateObject instanceof WI.Resource)
1119                 this._updateEntryForResource(updateObject);
1120         }
1121         this._pendingUpdates = [];
1122
1123         this._pendingFilter = false;
1124
1125         this._updateSort();
1126         this._updateFilteredEntries();
1127         this._reloadTable();
1128     }
1129
1130     _populateWithInitialResourcesIfNeeded()
1131     {
1132         if (!this._needsInitialPopulate)
1133             return;
1134
1135         this._needsInitialPopulate = false;
1136
1137         let populateResourcesForFrame = (frame) => {
1138             if (frame.provisionalMainResource)
1139                 this._pendingInsertions.push(frame.provisionalMainResource);
1140             else if (frame.mainResource)
1141                 this._pendingInsertions.push(frame.mainResource);
1142
1143             for (let resource of frame.resourceCollection)
1144                 this._pendingInsertions.push(resource);
1145
1146             for (let childFrame of frame.childFrameCollection)
1147                 populateResourcesForFrame(childFrame);
1148         };
1149
1150         let populateResourcesForTarget = (target) => {
1151             if (target.mainResource instanceof WI.Resource)
1152                 this._pendingInsertions.push(target.mainResource);
1153             for (let resource of target.resourceCollection)
1154                 this._pendingInsertions.push(resource);
1155         };
1156
1157         for (let target of WI.targets) {
1158             if (target === WI.pageTarget)
1159                 populateResourcesForFrame(WI.networkManager.mainFrame);
1160             else
1161                 populateResourcesForTarget(target);
1162         }
1163
1164         this.needsLayout();
1165     }
1166
1167     _checkURLFilterAgainstResource(resource)
1168     {
1169         if (this._urlFilterSearchRegex.test(resource.url)) {
1170             this._activeURLFilterResources.add(resource);
1171             return;
1172         }
1173
1174         for (let redirect of resource.redirects) {
1175             if (this._urlFilterSearchRegex.test(redirect.url)) {
1176                 this._activeURLFilterResources.add(resource);
1177                 return;
1178             }
1179         }
1180     }
1181
1182     _rowIndexForRepresentedObject(object)
1183     {
1184         return this._filteredEntries.findIndex((x) => {
1185             if (x.resource === object)
1186                 return true;
1187             if (x.domNode === object)
1188                 return true;
1189             return false;
1190         });
1191     }
1192
1193     _updateEntryForResource(resource)
1194     {
1195         let index = this._entries.findIndex((x) => x.resource === resource);
1196         if (index === -1)
1197             return;
1198
1199         // Don't wipe out the previous entry, as it may be used by a node entry.
1200         function updateExistingEntry(existingEntry, newEntry) {
1201             for (let key in newEntry)
1202                 existingEntry[key] = newEntry[key];
1203         }
1204
1205         let entry = this._entryForResource(resource);
1206         updateExistingEntry(this._entries[index], entry);
1207
1208         let rowIndex = this._rowIndexForRepresentedObject(resource);
1209         if (rowIndex === -1)
1210             return;
1211
1212         updateExistingEntry(this._filteredEntries[rowIndex], entry);
1213     }
1214
1215     _hidePopover()
1216     {
1217         if (this._waterfallPopover)
1218             this._waterfallPopover.dismiss();
1219     }
1220
1221     _hideDetailView()
1222     {
1223         if (!this._detailView)
1224             return;
1225
1226         this.element.classList.remove("showing-detail");
1227         this._table.scrollContainer.style.removeProperty("width");
1228
1229         this.removeSubview(this._detailView);
1230
1231         this._detailView.hidden();
1232         this._detailView = null;
1233
1234         this._table.resize();
1235         this._table.reloadVisibleColumnCells(this._waterfallColumn);
1236     }
1237
1238     _showDetailView(object)
1239     {
1240         let oldDetailView = this._detailView;
1241
1242         this._detailView = this._detailViewMap.get(object);
1243         if (this._detailView === oldDetailView)
1244             return;
1245
1246         if (!this._detailView) {
1247             if (object instanceof WI.Resource)
1248                 this._detailView = new WI.NetworkResourceDetailView(object, this);
1249             else if (object instanceof WI.DOMNode) {
1250                 this._detailView = new WI.NetworkDOMNodeDetailView(object, this, {
1251                     startTimestamp: this._waterfallStartTime,
1252                 });
1253             }
1254
1255             this._detailViewMap.set(object, this._detailView);
1256         }
1257
1258         if (oldDetailView) {
1259             oldDetailView.hidden();
1260             this.replaceSubview(oldDetailView, this._detailView);
1261         } else
1262             this.addSubview(this._detailView);
1263
1264         if (this._showingRepresentedObjectCookie)
1265             this._detailView.willShowWithCookie(this._showingRepresentedObjectCookie);
1266
1267         this._detailView.shown();
1268
1269         this.element.classList.add("showing-detail");
1270         this._table.scrollContainer.style.width = this._nameColumn.width + "px";
1271
1272         // FIXME: It would be nice to avoid this.
1273         // Currently the ResourceDetailView is in the heirarchy but has not yet done a layout so we
1274         // end up seeing the table behind it. This forces us to layout now instead of after a beat.
1275         this.updateLayout();
1276     }
1277
1278     _positionDetailView()
1279     {
1280         if (!this._detailView)
1281             return;
1282
1283         let side = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL ? "right" : "left";
1284         this._detailView.element.style[side] = this._nameColumn.width + "px";
1285         this._table.scrollContainer.style.width = this._nameColumn.width + "px";
1286     }
1287
1288     _updateURLFilterActiveIndicator()
1289     {
1290         this._urlFilterNavigationItem.filterBar.indicatingActive = this._hasURLFilter();
1291     }
1292
1293     _updateEmptyFilterResultsMessage()
1294     {
1295         if (this._hasActiveFilter() && !this._filteredEntries.length)
1296             this._showEmptyFilterResultsMessage();
1297         else
1298             this._hideEmptyFilterResultsMessage();
1299     }
1300
1301     _showEmptyFilterResultsMessage()
1302     {
1303         if (!this._emptyFilterResultsMessageElement) {
1304             let buttonElement = document.createElement("button");
1305             buttonElement.textContent = WI.UIString("Clear filters");
1306             buttonElement.addEventListener("click", () => { this._resetFilters(); });
1307
1308             this._emptyFilterResultsMessageElement = WI.createMessageTextView(WI.UIString("No Filter Results"));
1309             this._emptyFilterResultsMessageElement.appendChild(buttonElement);
1310         }
1311
1312         this.element.appendChild(this._emptyFilterResultsMessageElement);
1313         this._positionEmptyFilterMessage();
1314     }
1315
1316     _hideEmptyFilterResultsMessage()
1317     {
1318         if (!this._emptyFilterResultsMessageElement)
1319             return;
1320
1321         this._emptyFilterResultsMessageElement.remove();
1322     }
1323
1324     _positionEmptyFilterMessage()
1325     {
1326         if (!this._emptyFilterResultsMessageElement)
1327             return;
1328
1329         let width = this._nameColumn.width - 1; // For the 1px border.
1330         this._emptyFilterResultsMessageElement.style.width = width + "px";
1331     }
1332
1333     _clearNetworkOnNavigateSettingChanged()
1334     {
1335         this._clearOnLoadNavigationItem.checked = !WI.settings.clearNetworkOnNavigate.value;
1336     }
1337
1338     _resourceCachingDisabledSettingChanged()
1339     {
1340         this._disableResourceCacheNavigationItem.activated = WI.resourceCachingDisabledSetting.value;
1341     }
1342
1343     _toggleDisableResourceCache()
1344     {
1345         WI.resourceCachingDisabledSetting.value = !WI.resourceCachingDisabledSetting.value;
1346     }
1347
1348     _mainResourceDidChange(event)
1349     {
1350         let frame = event.target;
1351         if (!frame.isMainFrame() || !WI.settings.clearNetworkOnNavigate.value)
1352             return;
1353
1354         this.reset();
1355
1356         this._insertResourceAndReloadTable(frame.mainResource);
1357     }
1358
1359     _mainFrameDidChange()
1360     {
1361         this._populateWithInitialResourcesIfNeeded();
1362     }
1363
1364     _resourceLoadingDidFinish(event)
1365     {
1366         let resource = event.target;
1367         this._pendingUpdates.push(resource);
1368
1369         if (resource.firstTimestamp < this._waterfallStartTime)
1370             this._waterfallStartTime = resource.firstTimestamp;
1371         if (resource.timingData.responseEnd > this._waterfallEndTime)
1372             this._waterfallEndTime = resource.timingData.responseEnd;
1373
1374         if (this._hasURLFilter())
1375             this._checkURLFilterAgainstResource(resource);
1376
1377         this.needsLayout();
1378     }
1379
1380     _resourceLoadingDidFail(event)
1381     {
1382         let resource = event.target;
1383         this._pendingUpdates.push(resource);
1384
1385         if (resource.firstTimestamp < this._waterfallStartTime)
1386             this._waterfallStartTime = resource.firstTimestamp;
1387         if (resource.timingData.responseEnd > this._waterfallEndTime)
1388             this._waterfallEndTime = resource.timingData.responseEnd;
1389
1390         if (this._hasURLFilter())
1391             this._checkURLFilterAgainstResource(resource);
1392
1393         this.needsLayout();
1394     }
1395
1396     _resourceTransferSizeDidChange(event)
1397     {
1398         if (!this._table)
1399             return;
1400
1401         let resource = event.target;
1402
1403         // In the unlikely event that this is the sort column, we may need to resort.
1404         if (this._table.sortColumnIdentifier === "transferSize") {
1405             this._pendingUpdates.push(resource);
1406             this.needsLayout();
1407             return;
1408         }
1409
1410         let index = this._entries.findIndex((x) => x.resource === resource);
1411         if (index === -1)
1412             return;
1413
1414         let entry = this._entries[index];
1415         entry.transferSize = !isNaN(resource.networkTotalTransferSize) ? resource.networkTotalTransferSize : resource.estimatedTotalTransferSize;
1416
1417         let rowIndex = this._rowIndexForRepresentedObject(resource);
1418         if (rowIndex === -1)
1419             return;
1420
1421         this._table.reloadCell(rowIndex, "transferSize");
1422     }
1423
1424     _networkTimelineRecordAdded(event)
1425     {
1426         let resourceTimelineRecord = event.data.record;
1427         console.assert(resourceTimelineRecord instanceof WI.ResourceTimelineRecord);
1428
1429         let resource = resourceTimelineRecord.resource;
1430         if (isNaN(this._waterfallStartTime))
1431             this._waterfallStartTime = this._waterfallEndTime = resource.firstTimestamp;
1432
1433         this._insertResourceAndReloadTable(resource);
1434     }
1435
1436     _isDefaultSort()
1437     {
1438         return this._table.sortColumnIdentifier === "waterfall" && this._table.sortOrder === WI.Table.SortOrder.Ascending;
1439     }
1440
1441     _insertResourceAndReloadTable(resource)
1442     {
1443         if (!this._table || !(WI.tabBrowser.selectedTabContentView instanceof WI.NetworkTabContentView)) {
1444             this._pendingInsertions.push(resource);
1445             this.needsLayout();
1446             return;
1447         }
1448
1449         let resourceEntry = this._entryForResource(resource);
1450
1451         this._tryLinkResourceToDOMNode(resourceEntry);
1452
1453         if (WI.settings.groupByDOMNode.value && resource.initiatorNode) {
1454             if (!this._entriesSortComparator)
1455                 this._generateSortComparator();
1456         } else if (this._isDefaultSort() || !this._entriesSortComparator) {
1457             // Default sort has fast path.
1458             this._entries.push(resourceEntry);
1459             if (this._passFilter(resourceEntry)) {
1460                 this._filteredEntries.push(resourceEntry);
1461                 this._table.reloadDataAddedToEndOnly();
1462             }
1463             return;
1464         }
1465
1466         insertObjectIntoSortedArray(resourceEntry, this._entries, this._entriesSortComparator);
1467
1468         if (this._passFilter(resourceEntry)) {
1469             if (WI.settings.groupByDOMNode.value)
1470                 this._updateFilteredEntries();
1471             else
1472                 insertObjectIntoSortedArray(resourceEntry, this._filteredEntries, this._entriesSortComparator);
1473
1474             // Probably a useless optimization here, but if we only added this row to the end
1475             // we may avoid recreating all visible rows by saying as such.
1476             if (this._filteredEntries.lastValue === resourceEntry)
1477                 this._table.reloadDataAddedToEndOnly();
1478             else
1479                 this._reloadTable();
1480         }
1481     }
1482
1483     _entryForResource(resource)
1484     {
1485         // FIXME: <https://webkit.org/b/143632> Web Inspector: Resources with the same name in different folders aren't distinguished
1486         // FIXME: <https://webkit.org/b/176765> Web Inspector: Resource names should be less ambiguous
1487
1488         return {
1489             resource,
1490             name: WI.displayNameForURL(resource.url, resource.urlComponents),
1491             domain: WI.displayNameForHost(resource.urlComponents.host),
1492             scheme: resource.urlComponents.scheme ? resource.urlComponents.scheme.toLowerCase() : "",
1493             method: resource.requestMethod,
1494             type: resource.type,
1495             displayType: WI.NetworkTableContentView.displayNameForResource(resource),
1496             mimeType: resource.mimeType,
1497             status: resource.statusCode,
1498             cached: resource.cached,
1499             resourceSize: resource.size,
1500             transferSize: !isNaN(resource.networkTotalTransferSize) ? resource.networkTotalTransferSize : resource.estimatedTotalTransferSize,
1501             time: resource.totalDuration,
1502             protocol: resource.protocol,
1503             initiator: resource.initiatorSourceCodeLocation ? resource.initiatorSourceCodeLocation.displayLocationString() : "",
1504             priority: resource.priority,
1505             remoteAddress: resource.remoteAddress,
1506             connectionIdentifier: resource.connectionIdentifier,
1507             startTime: resource.firstTimestamp,
1508         };
1509     }
1510
1511     _entryForDOMNode(domNode)
1512     {
1513         return {
1514             domNode,
1515             initiatedResourceEntries: [],
1516             expanded: true,
1517         };
1518     }
1519
1520     _tryLinkResourceToDOMNode(resourceEntry)
1521     {
1522         let resource = resourceEntry.resource;
1523         if (!resource || !resource.initiatorNode)
1524             return;
1525
1526         let nodeEntry = this._domNodeEntries.get(resource.initiatorNode);
1527         if (!nodeEntry) {
1528             nodeEntry = this._entryForDOMNode(resource.initiatorNode, Object.keys(resourceEntry));
1529             this._domNodeEntries.set(resource.initiatorNode, nodeEntry);
1530
1531             resource.initiatorNode.addEventListener(WI.DOMNode.Event.DidFireEvent, this._handleNodeDidFireEvent, this);
1532         }
1533
1534         if (!this._entriesSortComparator)
1535             this._generateSortComparator();
1536
1537         insertObjectIntoSortedArray(resourceEntry, nodeEntry.initiatedResourceEntries, this._entriesSortComparator);
1538     }
1539
1540     _uniqueValuesForDOMNodeEntry(nodeEntry, accessor)
1541     {
1542         let resourceEntries = nodeEntry.initiatedResourceEntries;
1543         if (!resourceEntries)
1544             return null;
1545
1546         return resourceEntries.reduce((accumulator, current) => {
1547             let value = accessor(current);
1548             if (value || typeof value === "number")
1549                 accumulator.add(value);
1550             return accumulator;
1551         }, new Set);
1552     }
1553
1554     _handleNodeDidFireEvent(event)
1555     {
1556         let domNode = event.target;
1557         let {domEvent} = event.data;
1558
1559         this._pendingUpdates.push(domNode);
1560
1561         if (domEvent.timestamp > this._waterfallEndTime)
1562             this._waterfallEndTime = domEvent.timestamp + (this._waterfallTimelineRuler.secondsPerPixel * 10);
1563
1564         this.needsLayout();
1565     }
1566
1567     _hasTypeFilter()
1568     {
1569         return !!this._activeTypeFilters;
1570     }
1571
1572     _hasURLFilter()
1573     {
1574         return this._urlFilterIsActive;
1575     }
1576
1577     _hasActiveFilter()
1578     {
1579         return this._hasTypeFilter()
1580             || this._hasURLFilter();
1581     }
1582
1583     _passTypeFilter(entry)
1584     {
1585         if (!this._hasTypeFilter())
1586             return true;
1587         return this._activeTypeFilters.some((checker) => checker(entry.resource.type));
1588     }
1589
1590     _passURLFilter(entry)
1591     {
1592         if (!this._hasURLFilter())
1593             return true;
1594         return this._activeURLFilterResources.has(entry.resource);
1595     }
1596
1597     _passFilter(entry)
1598     {
1599         return this._passTypeFilter(entry)
1600             && this._passURLFilter(entry);
1601     }
1602
1603     _updateSort()
1604     {
1605         if (this._entriesSortComparator)
1606             this._entries = this._entries.sort(this._entriesSortComparator);
1607     }
1608
1609     _updateFilteredEntries()
1610     {
1611         if (this._hasActiveFilter())
1612             this._filteredEntries = this._entries.filter(this._passFilter, this);
1613         else
1614             this._filteredEntries = this._entries.slice();
1615
1616         if (WI.settings.groupByDOMNode.value) {
1617             for (let nodeEntry of this._domNodeEntries.values()) {
1618                 if (nodeEntry.initiatedResourceEntries.length < 2 && !nodeEntry.domNode.domEvents.length)
1619                     continue;
1620
1621                 let firstIndex = Infinity;
1622                 for (let resourceEntry of nodeEntry.initiatedResourceEntries) {
1623                     if (this._hasActiveFilter() && !this._passFilter(resourceEntry))
1624                         continue;
1625
1626                     let index = this._filteredEntries.indexOf(resourceEntry);
1627                     if (index >= 0 && index < firstIndex)
1628                         firstIndex = index;
1629                 }
1630
1631                 if (!isFinite(firstIndex))
1632                     continue;
1633
1634                 this._filteredEntries.insertAtIndex(nodeEntry, firstIndex);
1635             }
1636
1637             this._filteredEntries = this._filteredEntries.filter((entry) => {
1638                 if (entry.resource && entry.resource.initiatorNode) {
1639                     let nodeEntry = this._domNodeEntries.get(entry.resource.initiatorNode);
1640                     if (!nodeEntry.expanded)
1641                         return false;
1642                 }
1643                 return true;
1644             });
1645         }
1646
1647         this._updateURLFilterActiveIndicator();
1648         this._updateEmptyFilterResultsMessage();
1649     }
1650
1651     _reloadTable()
1652     {
1653         this._table.reloadData();
1654         this._restoreSelectedRow();
1655     }
1656
1657     _generateTypeFilter()
1658     {
1659         let selectedItems = this._typeFilterScopeBar.selectedItems;
1660         if (!selectedItems.length || selectedItems.includes(this._typeFilterScopeBarItemAll))
1661             return null;
1662
1663         return selectedItems.map((item) => item.__checker);
1664     }
1665
1666     _resetFilters()
1667     {
1668         console.assert(this._hasActiveFilter());
1669
1670         // Clear url filter.
1671         this._urlFilterSearchText = null;
1672         this._urlFilterSearchRegex = null;
1673         this._urlFilterIsActive = false;
1674         this._activeURLFilterResources.clear();
1675         this._urlFilterNavigationItem.filterBar.clear();
1676         console.assert(!this._hasURLFilter());
1677
1678         // Clear type filter.
1679         this._typeFilterScopeBar.resetToDefault();
1680         console.assert(!this._hasTypeFilter());
1681
1682         console.assert(!this._hasActiveFilter());
1683
1684         this._updateFilteredEntries();
1685         this._reloadTable();
1686     }
1687
1688     _areFilterListsIdentical(listA, listB)
1689     {
1690         if (listA && listB) {
1691             if (listA.length !== listB.length)
1692                 return false;
1693
1694             for (let i = 0; i < listA.length; ++i) {
1695                 if (listA[i] !== listB[i])
1696                     return false;
1697             }
1698
1699             return true;
1700         }
1701
1702         return false;
1703     }
1704
1705     _typeFilterScopeBarSelectionChanged(event)
1706     {
1707         // FIXME: <https://webkit.org/b/176763> Web Inspector: ScopeBar SelectionChanged event may dispatch multiple times for a single logical change
1708         // We can't use shallow equals here because the contents are functions.
1709         let oldFilter = this._activeTypeFilters;
1710         let newFilter = this._generateTypeFilter();
1711         if (this._areFilterListsIdentical(oldFilter, newFilter))
1712             return;
1713
1714         // Even if the selected resource would still be visible, lets close the detail view if a filter changes.
1715         this._hideDetailView();
1716
1717         this._activeTypeFilters = newFilter;
1718         this._updateFilteredEntries();
1719         this._reloadTable();
1720     }
1721
1722     _handleGroupByDOMNodeCheckedDidChange(event)
1723     {
1724         WI.settings.groupByDOMNode.value = this._groupByDOMNodeNavigationItem.checked;
1725
1726         if (!WI.settings.groupByDOMNode.value)
1727             this._table.element.classList.remove("grouped");
1728
1729         this._updateSort();
1730         this._updateFilteredEntries();
1731         this._reloadTable();
1732     }
1733
1734     _urlFilterDidChange(event)
1735     {
1736         let searchQuery = this._urlFilterNavigationItem.filterBar.filters.text;
1737         if (searchQuery === this._urlFilterSearchText)
1738             return;
1739
1740         // Even if the selected resource would still be visible, lets close the detail view if a filter changes.
1741         this._hideDetailView();
1742
1743         // Search cleared.
1744         if (!searchQuery) {
1745             this._urlFilterSearchText = null;
1746             this._urlFilterSearchRegex = null;
1747             this._urlFilterIsActive = false;
1748             this._activeURLFilterResources.clear();
1749
1750             this._updateFilteredEntries();
1751             this._reloadTable();
1752             return;
1753         }
1754
1755         this._urlFilterIsActive = true;
1756         this._urlFilterSearchText = searchQuery;
1757         this._urlFilterSearchRegex = new RegExp(searchQuery.escapeForRegExp(), "i");
1758
1759         this._activeURLFilterResources.clear();
1760
1761         for (let entry of this._entries)
1762             this._checkURLFilterAgainstResource(entry.resource);
1763
1764         this._updateFilteredEntries();
1765         this._reloadTable();
1766     }
1767
1768     _restoreSelectedRow()
1769     {
1770         if (!this._selectedObject)
1771             return;
1772
1773         let rowIndex = this._rowIndexForRepresentedObject(this._selectedObject);
1774         if (rowIndex === -1) {
1775             this._selectedResource = null;
1776             this._table.deselectAll();
1777             return;
1778         }
1779
1780         this._table.selectRow(rowIndex);
1781         this._showDetailView(this._selectedObject);
1782     }
1783
1784     _HARResources()
1785     {
1786         let resources = this._filteredEntries.map((x) => x.resource);
1787         const supportedHARSchemes = new Set(["http", "https", "ws", "wss"]);
1788         return resources.filter((resource) => resource.finished && supportedHARSchemes.has(resource.urlComponents.scheme));
1789     }
1790
1791     _exportHAR()
1792     {
1793         let resources = this._HARResources();
1794         if (!resources.length) {
1795             InspectorFrontendHost.beep();
1796             return;
1797         }
1798
1799         WI.HARBuilder.buildArchive(resources).then((har) => {
1800             let mainFrame = WI.networkManager.mainFrame;
1801             let archiveName = mainFrame.mainResource.urlComponents.host || mainFrame.mainResource.displayName || "Archive";
1802             let url = "web-inspector:///" + encodeURI(archiveName) + ".har";
1803             WI.saveDataToFile({
1804                 url,
1805                 content: JSON.stringify(har, null, 2),
1806                 forceSaveAs: true,
1807             });
1808         }).catch(handlePromiseException);
1809     }
1810
1811     _waterfallPopoverContent()
1812     {
1813         let contentElement = document.createElement("div");
1814         contentElement.classList.add("waterfall-popover-content");
1815         return contentElement;
1816     }
1817
1818     _waterfallPopoverContentForResourceEntry(resourceEntry)
1819     {
1820         let contentElement = this._waterfallPopoverContent();
1821
1822         let resource = resourceEntry.resource;
1823         if (!resource.hasResponse() || !resource.firstTimestamp || !resource.lastTimestamp) {
1824             contentElement.textContent = WI.UIString("Resource has no timing data");
1825             return contentElement;
1826         }
1827
1828         let breakdownView = new WI.ResourceTimingBreakdownView(resource, 300);
1829         contentElement.appendChild(breakdownView.element);
1830         breakdownView.updateLayout();
1831
1832         return contentElement;
1833     }
1834
1835     _waterfallPopoverContentForNodeEntry(nodeEntry, domEvents)
1836     {
1837         let contentElement = this._waterfallPopoverContent();
1838
1839         let breakdownView = new WI.DOMEventsBreakdownView(domEvents, {
1840             startTimestamp: this._waterfallStartTime,
1841         });
1842         contentElement.appendChild(breakdownView.element);
1843         breakdownView.updateLayout();
1844
1845         return contentElement;
1846     }
1847
1848     _handleResourceEntryMousedownWaterfall(targetElement, resourceEntry)
1849     {
1850         let popoverContentElement = this._waterfallPopoverContentForResourceEntry(resourceEntry);
1851         this._handleMousedownWaterfall(resourceEntry, targetElement, popoverContentElement);
1852     }
1853
1854     _handleNodeEntryMousedownWaterfall(targetElement, nodeEntry, domEvents)
1855     {
1856         let popoverContentElement = this._waterfallPopoverContentForNodeEntry(nodeEntry, domEvents);
1857         this._handleMousedownWaterfall(nodeEntry, targetElement, popoverContentElement);
1858     }
1859
1860     _handleMousedownWaterfall(entry, targetElement, popoverContentElement)
1861     {
1862         if (!this._waterfallPopover) {
1863             this._waterfallPopover = new WI.Popover;
1864             this._waterfallPopover.element.classList.add("waterfall-popover");
1865         }
1866
1867         if (this._waterfallPopover.visible)
1868             return;
1869
1870         let calculateTargetFrame = () => {
1871             if (!entry.resource)
1872                 return WI.Rect.rectFromClientRect(targetElement.getBoundingClientRect());
1873
1874             let rowIndex = this._rowIndexForRepresentedObject(entry.resource);
1875             let cell = this._table.cellForRowAndColumn(rowIndex, this._waterfallColumn);
1876             if (!cell) {
1877                 this._waterfallPopover.dismiss();
1878                 return null;
1879             }
1880
1881             let mouseBlock = cell.querySelector(".block.mouse-tracking");
1882             if (!mouseBlock) {
1883                 this._waterfallPopover.dismiss();
1884                 return null;
1885             }
1886
1887             return WI.Rect.rectFromClientRect(mouseBlock.getBoundingClientRect());
1888         };
1889
1890         let targetFrame = calculateTargetFrame();
1891         if (!targetFrame)
1892             return;
1893         if (!targetFrame.size.width && !targetFrame.size.height)
1894             return;
1895
1896         let isRTL = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL;
1897         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];
1898         this._waterfallPopover.windowResizeHandler = () => {
1899             let bounds = calculateTargetFrame();
1900             if (bounds)
1901                 this._waterfallPopover.present(bounds, preferredEdges);
1902         };
1903
1904         this._waterfallPopover.presentNewContentWithFrame(popoverContentElement, targetFrame, preferredEdges);
1905     }
1906
1907     _tableNameColumnDidChangeWidth(event)
1908     {
1909         this._nameColumnWidthSetting.value = event.target.width;
1910
1911         this._positionDetailView();
1912         this._positionEmptyFilterMessage();
1913     }
1914
1915     _tableWaterfallColumnDidChangeWidth(event)
1916     {
1917         this._table.reloadVisibleColumnCells(this._waterfallColumn);
1918     }
1919 };