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