Web Inspector: Canvas Tab initial user interface needs some polish
[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
39         this._table = null;
40         this._nameColumnWidthSetting = new WI.Setting("network-table-content-view-name-column-width", 250);
41
42         this._selectedResource = null;
43         this._resourceDetailView = null;
44         this._resourceDetailViewMap = new Map;
45
46         this._waterfallStartTime = NaN;
47         this._waterfallEndTime = NaN;
48         this._waterfallTimelineRuler = null;
49         this._waterfallPopover = null;
50
51         // FIXME: Network Timeline.
52         // FIXME: Throttling.
53
54         const exclusive = true;
55         this._typeFilterScopeBarItemAll = new WI.ScopeBarItem("network-type-filter-all", WI.UIString("All"), exclusive);
56         let typeFilterScopeBarItems = [this._typeFilterScopeBarItemAll];
57
58         let uniqueTypes = [
59             ["Document", (type) => type === WI.Resource.Type.Document],
60             ["Stylesheet", (type) => type === WI.Resource.Type.Stylesheet],
61             ["Image", (type) => type === WI.Resource.Type.Image],
62             ["Font", (type) => type === WI.Resource.Type.Font],
63             ["Script", (type) => type === WI.Resource.Type.Script],
64             ["XHR", (type) => type === WI.Resource.Type.XHR || type === WI.Resource.Type.Fetch],
65             ["Other", (type) => {
66                 return type !== WI.Resource.Type.Document
67                     && type !== WI.Resource.Type.Stylesheet
68                     && type !== WI.Resource.Type.Image
69                     && type !== WI.Resource.Type.Font
70                     && type !== WI.Resource.Type.Script
71                     && type !== WI.Resource.Type.XHR
72                     && type !== WI.Resource.Type.Fetch;
73             }],
74         ];
75         for (let [key, checker] of uniqueTypes) {
76             let type = WI.Resource.Type[key];
77             let scopeBarItem = new WI.ScopeBarItem("network-type-filter-" + key, WI.NetworkTableContentView.shortDisplayNameForResourceType(type));
78             scopeBarItem.__checker = checker;
79             typeFilterScopeBarItems.push(scopeBarItem);
80         }
81
82         this._typeFilterScopeBar = new WI.ScopeBar("network-type-filter-scope-bar", typeFilterScopeBarItems, typeFilterScopeBarItems[0]);
83         this._typeFilterScopeBar.addEventListener(WI.ScopeBar.Event.SelectionChanged, this._typeFilterScopeBarSelectionChanged, this);
84
85         this._textFilterSearchId = 0;
86         this._textFilterSearchText = null;
87         this._textFilterIsActive = false;
88
89         this._textFilterNavigationItem = new WI.FilterBarNavigationItem;
90         this._textFilterNavigationItem.filterBar.incremental = false;
91         this._textFilterNavigationItem.filterBar.addEventListener(WI.FilterBar.Event.FilterDidChange, this._textFilterDidChange, this);
92         this._textFilterNavigationItem.filterBar.placeholder = WI.UIString("Filter Full URL and Text");
93
94         this._activeTypeFilters = this._generateTypeFilter();
95         this._activeTextFilterResources = new Set;
96
97         this._emptyFilterResultsMessageElement = null;
98
99         this._clearOnLoadNavigationItem = new WI.CheckboxNavigationItem("perserve-log", WI.UIString("Preserve Log"), !WI.settings.clearNetworkOnNavigate.value);
100         this._clearOnLoadNavigationItem.tooltip = WI.UIString("Do not clear network items on new page loads");
101         this._clearOnLoadNavigationItem.addEventListener(WI.CheckboxNavigationItem.Event.CheckedDidChange, () => { WI.settings.clearNetworkOnNavigate.value = !WI.settings.clearNetworkOnNavigate.value; });
102         WI.settings.clearNetworkOnNavigate.addEventListener(WI.Setting.Event.Changed, this._clearNetworkOnNavigateSettingChanged, this);
103
104         this._harExportNavigationItem = new WI.ButtonNavigationItem("har-export", WI.UIString("Export"), "Images/Export.svg", 15, 15);
105         this._harExportNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText;
106         this._harExportNavigationItem.tooltip = WI.UIString("HAR Export (%s)").format(WI.saveKeyboardShortcut.displayName);
107         this._harExportNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, () => { this._exportHAR(); });
108
109         this._checkboxsNavigationItemGroup = new WI.GroupNavigationItem([this._clearOnLoadNavigationItem, new WI.DividerNavigationItem]);
110         this._checkboxsNavigationItemGroup.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
111
112         this._buttonsNavigationItemGroup = new WI.GroupNavigationItem([this._harExportNavigationItem, new WI.DividerNavigationItem]);
113         this._buttonsNavigationItemGroup.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
114
115         // COMPATIBILITY (iOS 10.3): Network.setDisableResourceCaching did not exist.
116         if (window.NetworkAgent && NetworkAgent.setResourceCachingDisabled) {
117             let toolTipForDisableResourceCache = WI.UIString("Ignore the resource cache when loading resources");
118             let activatedToolTipForDisableResourceCache = WI.UIString("Use the resource cache when loading resources");
119             this._disableResourceCacheNavigationItem = new WI.ActivateButtonNavigationItem("disable-resource-cache", toolTipForDisableResourceCache, activatedToolTipForDisableResourceCache, "Images/IgnoreCaches.svg", 16, 16);
120             this._disableResourceCacheNavigationItem.activated = WI.resourceCachingDisabledSetting.value;
121
122             this._disableResourceCacheNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._toggleDisableResourceCache, this);
123             WI.resourceCachingDisabledSetting.addEventListener(WI.Setting.Event.Changed, this._resourceCachingDisabledSettingChanged, this);
124         }
125
126         this._clearNetworkItemsNavigationItem = new WI.ButtonNavigationItem("clear-network-items", WI.UIString("Clear Network Items (%s)").format(WI.clearKeyboardShortcut.displayName), "Images/NavigationItemTrash.svg", 15, 15);
127         this._clearNetworkItemsNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, () => { this.reset(); });
128
129         WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
130         WI.Resource.addEventListener(WI.Resource.Event.LoadingDidFinish, this._resourceLoadingDidFinish, this);
131         WI.Resource.addEventListener(WI.Resource.Event.LoadingDidFail, this._resourceLoadingDidFail, this);
132         WI.Resource.addEventListener(WI.Resource.Event.TransferSizeDidChange, this._resourceTransferSizeDidChange, this);
133         WI.frameResourceManager.addEventListener(WI.FrameResourceManager.Event.MainFrameDidChange, this._mainFrameDidChange, this);
134         WI.timelineManager.persistentNetworkTimeline.addEventListener(WI.Timeline.Event.RecordAdded, this._networkTimelineRecordAdded, this);
135
136         this._needsInitialPopulate = true;
137     }
138
139     // Static
140
141     static displayNameForResource(resource)
142     {
143         if (resource.type === WI.Resource.Type.Image || resource.type === WI.Resource.Type.Font) {
144             let fileExtension;
145             if (resource.mimeType)
146                 fileExtension = WI.fileExtensionForMIMEType(resource.mimeType);
147             if (!fileExtension)
148                 fileExtension = WI.fileExtensionForURL(resource.url);
149             if (fileExtension)
150                 return fileExtension;
151         }
152
153         return WI.NetworkTableContentView.shortDisplayNameForResourceType(resource.type).toLowerCase();
154     }
155
156     static shortDisplayNameForResourceType(type)
157     {
158         switch (type) {
159         case WI.Resource.Type.Document:
160             return WI.UIString("Document");
161         case WI.Resource.Type.Stylesheet:
162             return "CSS";
163         case WI.Resource.Type.Image:
164             return WI.UIString("Image");
165         case WI.Resource.Type.Font:
166             return WI.UIString("Font");
167         case WI.Resource.Type.Script:
168             return "JS";
169         case WI.Resource.Type.XHR:
170             return "XHR";
171         case WI.Resource.Type.Fetch:
172             return WI.UIString("Fetch");
173         case WI.Resource.Type.Ping:
174             return WI.UIString("Ping");
175         case WI.Resource.Type.Beacon:
176             return WI.UIString("Beacon");
177         case WI.Resource.Type.WebSocket:
178         case WI.Resource.Type.Other:
179             return WI.UIString("Other");
180         default:
181             console.error("Unknown resource type", type);
182             return null;
183         }
184     }
185
186     // Public
187
188     get selectionPathComponents()
189     {
190         return null;
191     }
192
193     get navigationItems()
194     {
195         let items = [this._checkboxsNavigationItemGroup, this._buttonsNavigationItemGroup];
196         if (this._disableResourceCacheNavigationItem)
197             items.push(this._disableResourceCacheNavigationItem);
198         items.push(this._clearNetworkItemsNavigationItem);
199         return items;
200     }
201
202     get filterNavigationItems()
203     {
204         let items = [];
205         if (window.PageAgent)
206             items.push(this._textFilterNavigationItem);
207         items.push(this._typeFilterScopeBar);
208         return items;
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._resourceDetailView)
226             this._resourceDetailView.shown();
227
228         if (this._table)
229             this._table.restoreScrollPosition();
230     }
231
232     hidden()
233     {
234         this._hidePopover();
235
236         if (this._resourceDetailView)
237             this._resourceDetailView.hidden();
238
239         super.hidden();
240     }
241
242     closed()
243     {
244         for (let detailView of this._resourceDetailViewMap.values())
245             detailView.dispose();
246         this._resourceDetailViewMap.clear();
247
248         this._hidePopover();
249         this._hideResourceDetailView();
250
251         WI.Frame.removeEventListener(null, null, this);
252         WI.Resource.removeEventListener(null, null, this);
253         WI.resourceCachingDisabledSetting.removeEventListener(null, null, this);
254         WI.settings.clearNetworkOnNavigate.removeEventListener(null, null, this);
255         WI.frameResourceManager.removeEventListener(WI.FrameResourceManager.Event.MainFrameDidChange, this._mainFrameDidChange, this);
256         WI.timelineManager.persistentNetworkTimeline.removeEventListener(WI.Timeline.Event.RecordAdded, this._networkTimelineRecordAdded, this);
257
258         super.closed();
259     }
260
261     reset()
262     {
263         this._entries = [];
264         this._filteredEntries = [];
265         this._pendingInsertions = [];
266
267         for (let detailView of this._resourceDetailViewMap.values())
268             detailView.dispose();
269         this._resourceDetailViewMap.clear();
270
271         this._waterfallStartTime = NaN;
272         this._waterfallEndTime = NaN;
273         this._updateWaterfallTimelineRuler();
274         this._updateExportButton();
275
276         if (this._table) {
277             this._selectedResource = null;
278             this._table.clearSelectedRow();
279             this._table.reloadData();
280             this._hidePopover();
281             this._hideResourceDetailView();
282         }
283     }
284
285     showRepresentedObject(representedObject, cookie)
286     {
287         console.assert(representedObject instanceof WI.Resource);
288
289         let rowIndex = this._rowIndexForResource(representedObject);
290         if (rowIndex === -1) {
291             this._selectedResource = null;
292             this._table.clearSelectedRow();
293             this._hideResourceDetailView();
294             return;
295         }
296
297         this._table.selectRow(rowIndex);
298     }
299
300     // NetworkResourceDetailView delegate
301
302     networkResourceDetailViewClose(resourceDetailView)
303     {
304         this._selectedResource = null;
305         this._table.clearSelectedRow();
306         this._hideResourceDetailView();
307     }
308
309     // Table dataSource
310
311     tableNumberOfRows(table)
312     {
313         return this._filteredEntries.length;
314     }
315
316     tableSortChanged(table)
317     {
318         this._generateSortComparator();
319
320         if (!this._entriesSortComparator)
321             return;
322
323         this._hideResourceDetailView();
324
325         this._entries = this._entries.sort(this._entriesSortComparator);
326         this._updateFilteredEntries();
327         this._table.reloadData();
328     }
329
330     // Table delegate
331
332     tableCellMouseDown(table, cell, column, rowIndex, event)
333     {
334         if (column !== this._nameColumn)
335             return;
336
337         this._table.selectRow(rowIndex);
338     }
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     tableSelectedRowChanged(table, rowIndex)
356     {
357         if (isNaN(rowIndex)) {
358             this._selectedResource = null;
359             this._hideResourceDetailView();
360             return;
361         }
362
363         let entry = this._filteredEntries[rowIndex];
364         if (entry.resource === this._selectedResource)
365             return;
366
367         this._selectedResource = entry.resource;
368         this._showResourceDetailView(this._selectedResource);
369     }
370
371     tablePopulateCell(table, cell, column, rowIndex)
372     {
373         let entry = this._filteredEntries[rowIndex];
374
375         cell.classList.toggle("error", entry.resource.hadLoadingError());
376
377         switch (column.identifier) {
378         case "name":
379             this._populateNameCell(cell, entry);
380             break;
381         case "domain":
382             this._populateDomainCell(cell, entry);
383             break;
384         case "type":
385             cell.textContent = entry.displayType || emDash;
386             break;
387         case "mimeType":
388             cell.textContent = entry.mimeType || emDash;
389             break;
390         case "method":
391             cell.textContent = entry.method || emDash;
392             break;
393         case "scheme":
394             cell.textContent = entry.scheme || emDash;
395             break;
396         case "status":
397             cell.textContent = entry.status || emDash;
398             break;
399         case "protocol":
400             cell.textContent = entry.protocol || emDash;
401             break;
402         case "priority":
403             cell.textContent = WI.Resource.displayNameForPriority(entry.priority) || emDash;
404             break;
405         case "remoteAddress":
406             cell.textContent = entry.remoteAddress || emDash;
407             break;
408         case "connectionIdentifier":
409             cell.textContent = entry.connectionIdentifier || emDash;
410             break;
411         case "resourceSize":
412             cell.textContent = isNaN(entry.resourceSize) ? emDash : Number.bytesToString(entry.resourceSize);
413             break;
414         case "transferSize":
415             this._populateTransferSizeCell(cell, entry);
416             break;
417         case "time":
418             // FIXME: <https://webkit.org/b/176748> Web Inspector: Frontend sometimes receives resources with negative duration (responseEnd - requestStart)
419             cell.textContent = isNaN(entry.time) ? emDash : Number.secondsToString(Math.max(entry.time, 0));
420             break;
421         case "waterfall":
422             this._populateWaterfallGraph(cell, entry);
423             break;
424         }
425
426         return cell;
427     }
428
429     // Private
430
431     _populateNameCell(cell, entry)
432     {
433         console.assert(!cell.firstChild, "We expect the cell to be empty.", cell, cell.firstChild);
434
435         let resource = entry.resource;
436         if (resource.isLoading()) {
437             let statusElement = cell.appendChild(document.createElement("div"));
438             statusElement.className = "status";
439             let spinner = new WI.IndeterminateProgressSpinner;
440             statusElement.appendChild(spinner.element);
441         }
442
443         let iconElement = cell.appendChild(document.createElement("img"));
444         iconElement.className = "icon";
445         cell.classList.add(WI.ResourceTreeElement.ResourceIconStyleClassName, entry.resource.type);
446
447         let nameElement = cell.appendChild(document.createElement("span"));
448         nameElement.textContent = entry.name;
449     }
450
451     _populateDomainCell(cell, entry)
452     {
453         console.assert(!cell.firstChild, "We expect the cell to be empty.", cell, cell.firstChild);
454
455         if (!entry.domain) {
456             cell.textContent = emDash;
457             return;
458         }
459
460         let secure = entry.scheme === "https" || entry.scheme === "wss";
461         if (secure) {
462             let lockIconElement = cell.appendChild(document.createElement("img"));
463             lockIconElement.className = "lock";
464         }
465
466         cell.append(entry.domain);
467     }
468
469     _populateTransferSizeCell(cell, entry)
470     {
471         let responseSource = entry.resource.responseSource;
472         if (responseSource === WI.Resource.ResponseSource.MemoryCache) {
473             cell.classList.add("cache-type");
474             cell.textContent = WI.UIString("(memory)");
475             return;
476         }
477         if (responseSource === WI.Resource.ResponseSource.DiskCache) {
478             cell.classList.add("cache-type");
479             cell.textContent = WI.UIString("(disk)");
480             return;
481         }
482         if (responseSource === WI.Resource.ResponseSource.ServiceWorker) {
483             cell.classList.add("cache-type");
484             cell.textContent = WI.UIString("(service worker)");
485             return;
486         }
487
488         let transferSize = entry.transferSize;
489         cell.textContent = isNaN(transferSize) ? emDash : Number.bytesToString(transferSize);
490         console.assert(!cell.classList.contains("cache-type"), "Should not have cache-type class on cell.");
491     }
492
493     _populateWaterfallGraph(cell, entry)
494     {
495         cell.removeChildren();
496
497         let resource = entry.resource;
498         if (!resource.hasResponse()) {
499             cell.textContent = zeroWidthSpace;
500             return;
501         }
502
503         let {startTime, domainLookupStart, domainLookupEnd, connectStart, connectEnd, secureConnectionStart, requestStart, responseStart, responseEnd} = resource.timingData;
504         if (isNaN(startTime)) {
505             cell.textContent = zeroWidthSpace;
506             return;
507         }
508
509         let graphStartTime = this._waterfallTimelineRuler.startTime;
510         if (responseEnd < graphStartTime) {
511             cell.textContent = zeroWidthSpace;
512             return;
513         }
514
515         let graphEndTime = this._waterfallTimelineRuler.endTime;
516         if (startTime > graphEndTime) {
517             cell.textContent = zeroWidthSpace;
518             return;
519         }
520
521         let secondsPerPixel = this._waterfallTimelineRuler.secondsPerPixel;
522
523         let container = cell.appendChild(document.createElement("div"));
524         container.className = "waterfall-container";
525
526         function appendBlock(startTime, endTime, className) {
527             let startOffset = (startTime - graphStartTime) / secondsPerPixel;
528             let width = (endTime - startTime) / secondsPerPixel;
529             let block = container.appendChild(document.createElement("div"));
530             block.classList.add("block", className);
531             let styleAttribute = WI.resolvedLayoutDirection() === WI.LayoutDirection.LTR ? "left" : "right";
532             block.style[styleAttribute] = startOffset + "px";
533             block.style.width = width + "px";
534             return block;
535         }
536
537         // Mouse block sits on top and accepts mouse events on this group.
538         let padSeconds = 10 * secondsPerPixel;
539         let mouseBlock = appendBlock(startTime - padSeconds, responseEnd + padSeconds, "mouse-tracking");
540         mouseBlock.addEventListener("mousedown", (event) => {
541             if (event.button !== 0 || event.ctrlKey)
542                 return;
543             this._handleMousedownWaterfall(mouseBlock, entry, event);
544         });
545
546         // Super small visualization.
547         let totalWidth = (responseEnd - startTime) / secondsPerPixel;
548         if (totalWidth <= 3) {
549             appendBlock(startTime, requestStart, "queue");
550             appendBlock(startTime, responseEnd, "response");
551             return;
552         }
553
554         // Each component.
555         if (domainLookupStart) {
556             appendBlock(startTime, domainLookupStart, "queue");
557             appendBlock(domainLookupStart, connectStart || requestStart, "dns");
558         } else if (connectStart)
559             appendBlock(startTime, connectStart, "queue");
560         else if (requestStart)
561             appendBlock(startTime, requestStart, "queue");
562         if (connectStart)
563             appendBlock(connectStart, connectEnd, "connect");
564         if (secureConnectionStart)
565             appendBlock(secureConnectionStart, connectEnd, "secure");
566         appendBlock(requestStart, responseStart, "request");
567         appendBlock(responseStart, responseEnd, "response");
568     }
569
570     _generateSortComparator()
571     {
572         let sortColumnIdentifier = this._table.sortColumnIdentifier;
573         if (!sortColumnIdentifier) {
574             this._entriesSortComparator = null;
575             return;
576         }
577
578         let comparator;
579
580         switch (sortColumnIdentifier) {
581         case "name":
582         case "domain":
583         case "mimeType":
584         case "method":
585         case "scheme":
586         case "protocol":
587         case "remoteAddress":
588             // Simple string.
589             comparator = (a, b) => (a[sortColumnIdentifier] || "").extendedLocaleCompare(b[sortColumnIdentifier] || "");
590             break;
591
592         case "status":
593         case "connectionIdentifier":
594         case "resourceSize":
595         case "time":
596             // Simple number.
597             comparator = (a, b) => {
598                 let aValue = a[sortColumnIdentifier];
599                 if (isNaN(aValue))
600                     return 1;
601                 let bValue = b[sortColumnIdentifier];
602                 if (isNaN(bValue))
603                     return -1;
604                 return aValue - bValue;
605             };
606             break;
607
608         case "priority":
609             // Resource.NetworkPriority enum.
610             comparator = (a, b) => WI.Resource.comparePriority(a.priority, b.priority);
611             break;
612
613         case "type":
614             // Sort by displayType string.
615             comparator = (a, b) => (a.displayType || "").extendedLocaleCompare(b.displayType || "");
616             break;
617
618         case "transferSize":
619             // Handle (memory) and (disk) values.
620             comparator = (a, b) => {
621                 let transferSizeA = a.transferSize;
622                 let transferSizeB = b.transferSize;
623
624                 // Treat NaN as the largest value.
625                 if (isNaN(transferSizeA))
626                     return 1;
627                 if (isNaN(transferSizeB))
628                     return -1;
629
630                 // Treat memory cache and disk cache as small values.
631                 let sourceA = a.resource.responseSource;
632                 if (sourceA === WI.Resource.ResponseSource.MemoryCache)
633                     transferSizeA = -20;
634                 else if (sourceA === WI.Resource.ResponseSource.DiskCache)
635                     transferSizeA = -10;
636                 else if (sourceA === WI.Resource.ResponseSource.ServiceWorker)
637                     transferSizeA = -5;
638
639                 let sourceB = b.resource.responseSource;
640                 if (sourceB === WI.Resource.ResponseSource.MemoryCache)
641                     transferSizeB = -20;
642                 else if (sourceB === WI.Resource.ResponseSource.DiskCache)
643                     transferSizeB = -10;
644                 else if (sourceB === WI.Resource.ResponseSource.ServiceWorker)
645                     transferSizeB = -5;
646
647                 return transferSizeA - transferSizeB;
648             };
649             break;
650
651         case "waterfall":
652             // Sort by startTime number.
653             comparator = comparator = (a, b) => a.startTime - b.startTime;
654             break;
655
656         default:
657             console.assert("Unexpected sort column", sortColumnIdentifier);
658             return;
659         }
660
661         let reverseFactor = this._table.sortOrder === WI.Table.SortOrder.Ascending ? 1 : -1;
662         this._entriesSortComparator = (a, b) => reverseFactor * comparator(a, b);
663     }
664
665     // Protected
666
667     initialLayout()
668     {
669         this._waterfallTimelineRuler = new WI.TimelineRuler;
670         this._waterfallTimelineRuler.allowsClippedLabels = true;
671
672         this._nameColumn = new WI.TableColumn("name", WI.UIString("Name"), {
673             minWidth: WI.Sidebar.AbsoluteMinimumWidth,
674             maxWidth: 500,
675             initialWidth: this._nameColumnWidthSetting.value,
676             resizeType: WI.TableColumn.ResizeType.Locked,
677         });
678
679         this._domainColumn = new WI.TableColumn("domain", WI.UIString("Domain"), {
680             minWidth: 120,
681             maxWidth: 200,
682             initialWidth: 150,
683         });
684
685         this._typeColumn = new WI.TableColumn("type", WI.UIString("Type"), {
686             minWidth: 70,
687             maxWidth: 120,
688             initialWidth: 90,
689         });
690
691         this._mimeTypeColumn = new WI.TableColumn("mimeType", WI.UIString("MIME Type"), {
692             hidden: true,
693             minWidth: 100,
694             maxWidth: 150,
695             initialWidth: 120,
696         });
697
698         this._methodColumn = new WI.TableColumn("method", WI.UIString("Method"), {
699             hidden: true,
700             minWidth: 55,
701             maxWidth: 80,
702             initialWidth: 65,
703         });
704
705         this._schemeColumn = new WI.TableColumn("scheme", WI.UIString("Scheme"), {
706             hidden: true,
707             minWidth: 55,
708             maxWidth: 80,
709             initialWidth: 65,
710         });
711
712         this._statusColumn = new WI.TableColumn("status", WI.UIString("Status"), {
713             hidden: true,
714             minWidth: 50,
715             maxWidth: 50,
716             align: "left",
717         });
718
719         this._protocolColumn = new WI.TableColumn("protocol", WI.UIString("Protocol"), {
720             hidden: true,
721             minWidth: 65,
722             maxWidth: 80,
723             initialWidth: 75,
724         });
725
726         this._priorityColumn = new WI.TableColumn("priority", WI.UIString("Priority"), {
727             hidden: true,
728             minWidth: 65,
729             maxWidth: 80,
730             initialWidth: 70,
731         });
732
733         this._remoteAddressColumn = new WI.TableColumn("remoteAddress", WI.UIString("IP Address"), {
734             hidden: true,
735             minWidth: 150,
736         });
737
738         this._connectionIdentifierColumn = new WI.TableColumn("connectionIdentifier", WI.UIString("Connection ID"), {
739             hidden: true,
740             minWidth: 50,
741             maxWidth: 120,
742             initialWidth: 80,
743             align: "right",
744         });
745
746         this._resourceSizeColumn = new WI.TableColumn("resourceSize", WI.UIString("Resource Size"), {
747             hidden: true,
748             minWidth: 80,
749             maxWidth: 100,
750             initialWidth: 80,
751             align: "right",
752         });
753
754         this._transferSizeColumn = new WI.TableColumn("transferSize", WI.UIString("Transfer Size"), {
755             minWidth: 100,
756             maxWidth: 150,
757             initialWidth: 100,
758             align: "right",
759         });
760
761         this._timeColumn = new WI.TableColumn("time", WI.UIString("Time"), {
762             minWidth: 65,
763             maxWidth: 90,
764             initialWidth: 65,
765             align: "right",
766         });
767
768         this._waterfallColumn = new WI.TableColumn("waterfall", WI.UIString("Waterfall"), {
769             minWidth: 230,
770             headerView: this._waterfallTimelineRuler,
771         });
772
773         this._nameColumn.addEventListener(WI.TableColumn.Event.WidthDidChange, this._tableNameColumnDidChangeWidth, this);
774         this._waterfallColumn.addEventListener(WI.TableColumn.Event.WidthDidChange, this._tableWaterfallColumnDidChangeWidth, this);
775
776         this._table = new WI.Table("network-table", this, this, 20);
777
778         this._table.addColumn(this._nameColumn);
779         this._table.addColumn(this._domainColumn);
780         this._table.addColumn(this._typeColumn);
781         this._table.addColumn(this._mimeTypeColumn);
782         this._table.addColumn(this._methodColumn);
783         this._table.addColumn(this._schemeColumn);
784         this._table.addColumn(this._statusColumn);
785         this._table.addColumn(this._protocolColumn);
786         this._table.addColumn(this._priorityColumn);
787         this._table.addColumn(this._remoteAddressColumn);
788         this._table.addColumn(this._connectionIdentifierColumn);
789         this._table.addColumn(this._resourceSizeColumn);
790         this._table.addColumn(this._transferSizeColumn);
791         this._table.addColumn(this._timeColumn);
792         this._table.addColumn(this._waterfallColumn);
793
794         if (!this._table.sortColumnIdentifier) {
795             this._table.sortOrder = WI.Table.SortOrder.Ascending;
796             this._table.sortColumnIdentifier = "waterfall";
797         }
798
799         this.addSubview(this._table);
800     }
801
802     layout()
803     {
804         this._updateWaterfallTimelineRuler();
805         this._processPendingEntries();
806         this._positionDetailView();
807         this._positionEmptyFilterMessage();
808         this._updateExportButton();
809     }
810
811     handleClearShortcut(event)
812     {
813         this.reset();
814     }
815
816     // Private
817
818     _updateWaterfallTimelineRuler()
819     {
820         if (!this._waterfallTimelineRuler)
821             return;
822
823         if (isNaN(this._waterfallStartTime)) {
824             this._waterfallTimelineRuler.zeroTime = 0;
825             this._waterfallTimelineRuler.startTime = 0;
826             this._waterfallTimelineRuler.endTime = 0.250;
827         } else {
828             this._waterfallTimelineRuler.zeroTime = this._waterfallStartTime;
829             this._waterfallTimelineRuler.startTime = this._waterfallStartTime;
830             this._waterfallTimelineRuler.endTime = this._waterfallEndTime;
831
832             // Add a little bit of padding on the each side.
833             const paddingPixels = 5;
834             let padSeconds = paddingPixels * this._waterfallTimelineRuler.secondsPerPixel;
835             this._waterfallTimelineRuler.zeroTime = this._waterfallStartTime - padSeconds;
836             this._waterfallTimelineRuler.startTime = this._waterfallStartTime - padSeconds;
837             this._waterfallTimelineRuler.endTime = this._waterfallEndTime + padSeconds;
838         }
839     }
840
841     _updateExportButton()
842     {
843         let enabled = this._filteredEntries.length > 0;
844         this._harExportNavigationItem.enabled = enabled;
845     }
846
847     _processPendingEntries()
848     {
849         let needsSort = this._pendingUpdates.length > 0;
850         let needsFilter = this._pendingFilter;
851
852         // No global sort or filter is needed, so just insert new records into their sorted position.
853         if (!needsSort && !needsFilter) {
854             let originalLength = this._pendingInsertions.length;
855             for (let resource of this._pendingInsertions)
856                 this._insertResourceAndReloadTable(resource);
857             console.assert(this._pendingInsertions.length === originalLength);
858             this._pendingInsertions = [];
859             return;
860         }
861
862         for (let resource of this._pendingInsertions)
863             this._entries.push(this._entryForResource(resource));
864         this._pendingInsertions = [];
865
866         for (let resource of this._pendingUpdates)
867             this._updateEntryForResource(resource);
868         this._pendingUpdates = [];
869
870         this._pendingFilter = false;
871
872         this._updateSortAndFilteredEntries();
873         this._table.reloadData();
874     }
875
876     _populateWithInitialResourcesIfNeeded()
877     {
878         if (!this._needsInitialPopulate)
879             return;
880
881         this._needsInitialPopulate = false;
882
883         let populateResourcesForFrame = (frame) => {
884             if (frame.provisionalMainResource)
885                 this._pendingInsertions.push(frame.provisionalMainResource);
886             else if (frame.mainResource)
887                 this._pendingInsertions.push(frame.mainResource);
888
889             for (let resource of frame.resourceCollection.items)
890                 this._pendingInsertions.push(resource);
891
892             for (let childFrame of frame.childFrameCollection.items)
893                 populateResourcesForFrame(childFrame);
894         };
895
896         let populateResourcesForTarget = (target) => {
897             if (target.mainResource instanceof WI.Resource)
898                 this._pendingInsertions.push(target.mainResource);
899             for (let resource of target.resourceCollection.items)
900                 this._pendingInsertions.push(resource);
901         };
902
903         for (let target of WI.targets) {
904             if (target === WI.pageTarget)
905                 populateResourcesForFrame(WI.frameResourceManager.mainFrame);
906             else
907                 populateResourcesForTarget(target);
908         }
909
910         this.needsLayout();
911     }
912
913     _checkTextFilterAgainstFinishedResource(resource)
914     {
915         let frame = resource.parentFrame;
916         if (!frame)
917             return;
918
919         let searchQuery = this._textFilterSearchText;
920         if (resource.url.includes(searchQuery)) {
921             this._activeTextFilterResources.add(resource);
922             return;
923         }
924
925         let searchId = this._textFilterSearchId;
926
927         const isCaseSensitive = true;
928         const isRegex = false;
929         PageAgent.searchInResource(frame.id, resource.url, searchQuery, isCaseSensitive, isRegex, resource.requestIdentifier, (error, searchResults) => {
930             if (searchId !== this._textFilterSearchId)
931                 return;
932
933             if (error || !searchResults || !searchResults.length)
934                 return;
935
936             this._activeTextFilterResources.add(resource);
937
938             this._pendingFilter = true;
939             this.needsLayout();
940         });
941     }
942
943     _checkTextFilterAgainstFailedResource(resource)
944     {
945         let searchQuery = this._textFilterSearchText;
946         if (resource.url.includes(searchQuery))
947             this._activeTextFilterResources.add(resource);
948     }
949
950     _rowIndexForResource(resource)
951     {
952         return this._filteredEntries.findIndex((x) => x.resource === resource);
953     }
954
955     _updateEntryForResource(resource)
956     {
957         let index = this._entries.findIndex((x) => x.resource === resource);
958         if (index === -1)
959             return;
960
961         let entry = this._entryForResource(resource);
962         this._entries[index] = entry;
963
964         let rowIndex = this._rowIndexForResource(resource);
965         if (rowIndex === -1)
966             return;
967
968         this._filteredEntries[rowIndex] = entry;
969     }
970
971     _hidePopover()
972     {
973         if (this._waterfallPopover)
974             this._waterfallPopover.dismiss();
975     }
976
977     _hideResourceDetailView()
978     {
979         if (!this._resourceDetailView)
980             return;
981
982         this.element.classList.remove("showing-detail");
983         this._table.scrollContainer.style.removeProperty("width");
984
985         this.removeSubview(this._resourceDetailView);
986
987         this._resourceDetailView.hidden();
988         this._resourceDetailView = null;
989
990         this._table.resize();
991     }
992
993     _showResourceDetailView(resource)
994     {
995         let oldResourceDetailView = this._resourceDetailView;
996
997         this._resourceDetailView = this._resourceDetailViewMap.get(resource);
998         if (!this._resourceDetailView) {
999             this._resourceDetailView = new WI.NetworkResourceDetailView(resource, this);
1000             this._resourceDetailViewMap.set(resource, this._resourceDetailView);
1001         }
1002
1003         if (oldResourceDetailView) {
1004             oldResourceDetailView.hidden();
1005             this.replaceSubview(oldResourceDetailView, this._resourceDetailView);
1006         } else
1007             this.addSubview(this._resourceDetailView);
1008         this._resourceDetailView.shown();
1009
1010         this.element.classList.add("showing-detail");
1011         this._table.scrollContainer.style.width = this._nameColumn.width + "px";
1012
1013         // FIXME: It would be nice to avoid this.
1014         // Currently the ResourceDetailView is in the heirarchy but has not yet done a layout so we
1015         // end up seeing the table behind it. This forces us to layout now instead of after a beat.
1016         this.updateLayout();
1017     }
1018
1019     _positionDetailView()
1020     {
1021         if (!this._resourceDetailView)
1022             return;
1023
1024         let side = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL ? "right" : "left";
1025         this._resourceDetailView.element.style[side] = this._nameColumn.width + "px";
1026         this._table.scrollContainer.style.width = this._nameColumn.width + "px";
1027     }
1028
1029     _updateTextFilterActiveIndicator()
1030     {
1031         this._textFilterNavigationItem.filterBar.indicatingActive = this._hasTextFilter();
1032     }
1033
1034     _updateEmptyFilterResultsMessage()
1035     {
1036         if (this._hasActiveFilter() && !this._filteredEntries.length)
1037             this._showEmptyFilterResultsMessage();
1038         else
1039             this._hideEmptyFilterResultsMessage();
1040     }
1041
1042     _showEmptyFilterResultsMessage()
1043     {
1044         if (!this._emptyFilterResultsMessageElement) {
1045             let buttonElement = document.createElement("button");
1046             buttonElement.textContent = WI.UIString("Clear filters");
1047             buttonElement.addEventListener("click", () => { this._resetFilters(); });
1048
1049             this._emptyFilterResultsMessageElement = WI.createMessageTextView(WI.UIString("No Filter Results"));
1050             this._emptyFilterResultsMessageElement.appendChild(buttonElement);
1051         }
1052
1053         this.element.appendChild(this._emptyFilterResultsMessageElement);
1054         this._positionEmptyFilterMessage();
1055     }
1056
1057     _hideEmptyFilterResultsMessage()
1058     {
1059         if (!this._emptyFilterResultsMessageElement)
1060             return;
1061
1062         this._emptyFilterResultsMessageElement.remove();
1063     }
1064
1065     _positionEmptyFilterMessage()
1066     {
1067         if (!this._emptyFilterResultsMessageElement)
1068             return;
1069
1070         let width = this._nameColumn.width - 1; // For the 1px border.
1071         this._emptyFilterResultsMessageElement.style.width = width + "px";
1072     }
1073
1074     _clearNetworkOnNavigateSettingChanged()
1075     {
1076         this._clearOnLoadNavigationItem.checked = !WI.settings.clearNetworkOnNavigate.value;
1077     }
1078
1079     _resourceCachingDisabledSettingChanged()
1080     {
1081         this._disableResourceCacheNavigationItem.activated = WI.resourceCachingDisabledSetting.value;
1082     }
1083
1084     _toggleDisableResourceCache()
1085     {
1086         WI.resourceCachingDisabledSetting.value = !WI.resourceCachingDisabledSetting.value;
1087     }
1088
1089     _mainResourceDidChange(event)
1090     {
1091         let frame = event.target;
1092         if (!frame.isMainFrame() || !WI.settings.clearNetworkOnNavigate.value)
1093             return;
1094
1095         this.reset();
1096
1097         this._insertResourceAndReloadTable(frame.mainResource);
1098     }
1099
1100     _mainFrameDidChange()
1101     {
1102         this._populateWithInitialResourcesIfNeeded();
1103     }
1104
1105     _resourceLoadingDidFinish(event)
1106     {
1107         let resource = event.target;
1108         this._pendingUpdates.push(resource);
1109
1110         if (resource.firstTimestamp < this._waterfallStartTime)
1111             this._waterfallStartTime = resource.firstTimestamp;
1112         if (resource.timingData.responseEnd > this._waterfallEndTime)
1113             this._waterfallEndTime = resource.timingData.responseEnd;
1114
1115         if (this._hasTextFilter())
1116             this._checkTextFilterAgainstFinishedResource(resource);
1117
1118         this.needsLayout();
1119     }
1120
1121     _resourceLoadingDidFail(event)
1122     {
1123         let resource = event.target;
1124         this._pendingUpdates.push(resource);
1125
1126         if (resource.firstTimestamp < this._waterfallStartTime)
1127             this._waterfallStartTime = resource.firstTimestamp;
1128         if (resource.timingData.responseEnd > this._waterfallEndTime)
1129             this._waterfallEndTime = resource.timingData.responseEnd;
1130
1131         if (this._hasTextFilter())
1132             this._checkTextFilterAgainstFailedResource(resource);
1133
1134         this.needsLayout();
1135     }
1136
1137     _resourceTransferSizeDidChange(event)
1138     {
1139         if (!this._table)
1140             return;
1141
1142         let resource = event.target;
1143
1144         // In the unlikely event that this is the sort column, we may need to resort.
1145         if (this._table.sortColumnIdentifier === "transferSize") {
1146             this._pendingUpdates.push(resource);
1147             this.needsLayout();
1148             return;
1149         }
1150
1151         let index = this._entries.findIndex((x) => x.resource === resource);
1152         if (index === -1)
1153             return;
1154
1155         let entry = this._entries[index];
1156         entry.transferSize = !isNaN(resource.networkTotalTransferSize) ? resource.networkTotalTransferSize : resource.estimatedTotalTransferSize;
1157
1158         let rowIndex = this._rowIndexForResource(resource);
1159         if (rowIndex === -1)
1160             return;
1161
1162         this._table.reloadCell(rowIndex, "transferSize");
1163     }
1164
1165     _networkTimelineRecordAdded(event)
1166     {
1167         let resourceTimelineRecord = event.data.record;
1168         console.assert(resourceTimelineRecord instanceof WI.ResourceTimelineRecord);
1169
1170         let resource = resourceTimelineRecord.resource;
1171         if (isNaN(this._waterfallStartTime))
1172             this._waterfallStartTime = this._waterfallEndTime = resource.firstTimestamp;
1173
1174         this._insertResourceAndReloadTable(resource);
1175     }
1176
1177     _isDefaultSort()
1178     {
1179         return this._table.sortColumnIdentifier === "waterfall" && this._table.sortOrder === WI.Table.SortOrder.Ascending;
1180     }
1181
1182     _insertResourceAndReloadTable(resource)
1183     {
1184         if (!this._table || !(WI.tabBrowser.selectedTabContentView instanceof WI.NetworkTabContentView)) {
1185             this._pendingInsertions.push(resource);
1186             this.needsLayout();
1187             return;
1188         }
1189
1190         let entry = this._entryForResource(resource);
1191
1192         // Default sort has fast path.
1193         if (this._isDefaultSort() || !this._entriesSortComparator) {
1194             this._entries.push(entry);
1195             if (this._passFilter(entry)) {
1196                 this._filteredEntries.push(entry);
1197                 this._table.reloadDataAddedToEndOnly();
1198             }
1199             return;
1200         }
1201
1202         insertObjectIntoSortedArray(entry, this._entries, this._entriesSortComparator);
1203
1204         if (this._passFilter(entry)) {
1205             insertObjectIntoSortedArray(entry, this._filteredEntries, this._entriesSortComparator);
1206
1207             // Probably a useless optimization here, but if we only added this row to the end
1208             // we may avoid recreating all visible rows by saying as such.
1209             if (this._filteredEntries.lastValue === entry)
1210                 this._table.reloadDataAddedToEndOnly();
1211             else
1212                 this._table.reloadData();
1213         }
1214     }
1215
1216     _entryForResource(resource)
1217     {
1218         // FIXME: <https://webkit.org/b/143632> Web Inspector: Resources with the same name in different folders aren't distinguished
1219         // FIXME: <https://webkit.org/b/176765> Web Inspector: Resource names should be less ambiguous
1220
1221         return {
1222             resource,
1223             name: WI.displayNameForURL(resource.url, resource.urlComponents),
1224             domain: WI.displayNameForHost(resource.urlComponents.host),
1225             scheme: resource.urlComponents.scheme ? resource.urlComponents.scheme.toLowerCase() : "",
1226             method: resource.requestMethod,
1227             type: resource.type,
1228             displayType: WI.NetworkTableContentView.displayNameForResource(resource),
1229             mimeType: resource.mimeType,
1230             status: resource.statusCode,
1231             cached: resource.cached,
1232             resourceSize: resource.size,
1233             transferSize: !isNaN(resource.networkTotalTransferSize) ? resource.networkTotalTransferSize : resource.estimatedTotalTransferSize,
1234             time: resource.duration,
1235             protocol: resource.protocol,
1236             priority: resource.priority,
1237             remoteAddress: resource.remoteAddress,
1238             connectionIdentifier: resource.connectionIdentifier,
1239             startTime: resource.firstTimestamp,
1240         };
1241     }
1242
1243     _hasTypeFilter()
1244     {
1245         return !!this._activeTypeFilters;
1246     }
1247
1248     _hasTextFilter()
1249     {
1250         return this._textFilterIsActive;
1251     }
1252
1253     _hasActiveFilter()
1254     {
1255         return this._hasTypeFilter()
1256             || this._hasTextFilter();
1257     }
1258
1259     _passTypeFilter(entry)
1260     {
1261         if (!this._hasTypeFilter())
1262             return true;
1263         return this._activeTypeFilters.some((checker) => checker(entry.resource.type));
1264     }
1265
1266     _passTextFilter(entry)
1267     {
1268         if (!this._hasTextFilter())
1269             return true;
1270         return this._activeTextFilterResources.has(entry.resource);
1271     }
1272
1273     _passFilter(entry)
1274     {
1275         return this._passTypeFilter(entry)
1276             && this._passTextFilter(entry);
1277     }
1278
1279     _updateSortAndFilteredEntries()
1280     {
1281         this._entries = this._entries.sort(this._entriesSortComparator);
1282         this._updateFilteredEntries();
1283     }
1284
1285     _updateFilteredEntries()
1286     {
1287         if (this._hasActiveFilter())
1288             this._filteredEntries = this._entries.filter(this._passFilter, this);
1289         else
1290             this._filteredEntries = this._entries.slice();
1291
1292         this._restoreSelectedRow();
1293
1294         this._updateTextFilterActiveIndicator();
1295         this._updateEmptyFilterResultsMessage();
1296     }
1297
1298     _generateTypeFilter()
1299     {
1300         let selectedItems = this._typeFilterScopeBar.selectedItems;
1301         if (!selectedItems.length || selectedItems.includes(this._typeFilterScopeBarItemAll))
1302             return null;
1303
1304         return selectedItems.map((item) => item.__checker);
1305     }
1306
1307     _resetFilters()
1308     {
1309         console.assert(this._hasActiveFilter());
1310
1311         // Clear text filter.
1312         this._textFilterSearchId++;
1313         this._textFilterNavigationItem.filterBar.indicatingProgress = false;
1314         this._textFilterSearchText = null;
1315         this._textFilterIsActive = false;
1316         this._activeTextFilterResources.clear();
1317         this._textFilterNavigationItem.filterBar.clear();
1318         console.assert(!this._hasTextFilter());
1319
1320         // Clear type filter.
1321         this._typeFilterScopeBar.resetToDefault();
1322         console.assert(!this._hasTypeFilter());
1323
1324         console.assert(!this._hasActiveFilter());
1325
1326         this._updateFilteredEntries();
1327         this._table.reloadData();
1328     }
1329
1330     _areFilterListsIdentical(listA, listB)
1331     {
1332         if (listA && listB) {
1333             if (listA.length !== listB.length)
1334                 return false;
1335
1336             for (let i = 0; i < listA.length; ++i) {
1337                 if (listA[i] !== listB[i])
1338                     return false;
1339             }
1340
1341             return true;
1342         }
1343
1344         return false;
1345     }
1346
1347     _typeFilterScopeBarSelectionChanged(event)
1348     {
1349         // FIXME: <https://webkit.org/b/176763> Web Inspector: ScopeBar SelectionChanged event may dispatch multiple times for a single logical change
1350         // We can't use shallow equals here because the contents are functions.
1351         let oldFilter = this._activeTypeFilters;
1352         let newFilter = this._generateTypeFilter();
1353         if (this._areFilterListsIdentical(oldFilter, newFilter))
1354             return;
1355
1356         // Even if the selected resource would still be visible, lets close the detail view if a filter changes.
1357         this._hideResourceDetailView();
1358
1359         this._activeTypeFilters = newFilter;
1360         this._updateFilteredEntries();
1361         this._table.reloadData();
1362     }
1363
1364     _textFilterDidChange(event)
1365     {
1366         let searchQuery = this._textFilterNavigationItem.filterBar.filters.text;
1367         if (searchQuery === this._textFilterSearchText)
1368             return;
1369
1370         // Even if the selected resource would still be visible, lets close the detail view if a filter changes.
1371         this._hideResourceDetailView();
1372
1373         let searchId = ++this._textFilterSearchId;
1374
1375         // Search cleared.
1376         if (!searchQuery) {
1377             this._textFilterNavigationItem.filterBar.indicatingProgress = false;
1378             this._textFilterSearchText = null;
1379             this._textFilterIsActive = false;
1380             this._activeTextFilterResources.clear();
1381
1382             this._updateFilteredEntries();
1383             this._table.reloadData();
1384             return;
1385         }
1386
1387         this._textFilterSearchText = searchQuery;
1388         this._textFilterNavigationItem.filterBar.indicatingProgress = true;
1389
1390         // NetworkTable text filter currently searches:
1391         //   - Resource URL
1392         //   - Resource Text Content
1393         // It does not search all the content in the table (like mimeType, headers, etc).
1394         // For those we should provide more custom filters.
1395
1396         const isCaseSensitive = true;
1397         const isRegex = false;
1398         PageAgent.searchInResources(searchQuery, isCaseSensitive, isRegex, (error, searchResults) => {
1399             if (searchId !== this._textFilterSearchId)
1400                 return;
1401
1402             this._textFilterIsActive = true;
1403             this._activeTextFilterResources.clear();
1404             this._textFilterNavigationItem.filterBar.indicatingProgress = false;
1405
1406             // Add resources based on URL.
1407             for (let entry of this._entries) {
1408                 let resource = entry.resource;
1409                 if (resource.url.includes(searchQuery))
1410                     this._activeTextFilterResources.add(resource);
1411             }
1412
1413             // Add resources based on content.
1414             if (!error) {
1415                 for (let {url, frameId, requestId} of searchResults) {
1416                     if (requestId) {
1417                         let resource = WI.frameResourceManager.resourceForRequestIdentifier(requestId);
1418                         if (resource) {
1419                             this._activeTextFilterResources.add(resource);
1420                             continue;
1421                         }
1422                     }
1423
1424                     if (frameId && url) {
1425                         let frame = WI.frameResourceManager.frameForIdentifier(frameId);
1426                         if (frame) {
1427                             if (frame.mainResource.url === url) {
1428                                 this._activeTextFilterResources.add(frame.mainResource);
1429                                 continue;
1430                             }
1431                             let resource = frame.resourceForURL(url);
1432                             if (resource) {
1433                                 this._activeTextFilterResources.add(resource);
1434                                 continue;
1435                             }
1436                         }
1437                     }
1438                 }
1439             }
1440
1441             // Apply.
1442             this._updateFilteredEntries();
1443             this._table.reloadData();
1444         });
1445     }
1446
1447     _restoreSelectedRow()
1448     {
1449         if (!this._selectedResource)
1450             return;
1451
1452         let rowIndex = this._rowIndexForResource(this._selectedResource);
1453         if (rowIndex === -1) {
1454             this._selectedResource = null;
1455             this._table.clearSelectedRow();
1456             return;
1457         }
1458
1459         this._table.selectRow(rowIndex);
1460     }
1461
1462     _HARResources()
1463     {
1464         let resources = this._filteredEntries.map((x) => x.resource);
1465         const supportedHARSchemes = new Set(["http", "https", "ws", "wss"]);
1466         return resources.filter((resource) => resource.finished && supportedHARSchemes.has(resource.urlComponents.scheme));
1467     }
1468
1469     _exportHAR()
1470     {
1471         let resources = this._HARResources();
1472         if (!resources.length) {
1473             InspectorFrontendHost.beep();
1474             return;
1475         }
1476
1477         WI.HARBuilder.buildArchive(resources).then((har) => {
1478             let mainFrame = WI.frameResourceManager.mainFrame;
1479             let archiveName = mainFrame.mainResource.urlComponents.host || mainFrame.mainResource.displayName || "Archive";
1480             let url = "web-inspector:///" + encodeURI(archiveName) + ".har";
1481             WI.saveDataToFile({
1482                 url,
1483                 content: JSON.stringify(har, null, 2),
1484                 forceSaveAs: true,
1485             });
1486         }).catch(handlePromiseException);
1487     }
1488
1489     _waterfallPopoverContentForResource(resource)
1490     {
1491         let contentElement = document.createElement("div");
1492         contentElement.className = "waterfall-popover";
1493
1494         if (!resource.hasResponse() || !resource.timingData.startTime || !resource.timingData.responseEnd) {
1495             contentElement.textContent = WI.UIString("Resource has no timing data");
1496             return contentElement;
1497         }
1498
1499         let breakdownView = new WI.ResourceTimingBreakdownView(resource);
1500         contentElement.appendChild(breakdownView.element);
1501         breakdownView.updateLayout();
1502
1503         return contentElement;
1504     }
1505
1506     _handleMousedownWaterfall(mouseBlock, entry, event)
1507     {
1508         if (!this._waterfallPopover) {
1509             this._waterfallPopover = new WI.Popover;
1510             this._waterfallPopover.backgroundStyle = WI.Popover.BackgroundStyle.White;
1511         }
1512
1513         if (this._waterfallPopover.visible)
1514             return;
1515
1516         let calculateTargetFrame = () => {
1517             let rowIndex = this._rowIndexForResource(entry.resource);
1518             let cell = this._table.cellForRowAndColumn(rowIndex, this._waterfallColumn);
1519             if (!cell) {
1520                 this._waterfallPopover.dismiss();
1521                 return null;
1522             }
1523
1524             let mouseBlock = cell.querySelector(".block.mouse-tracking");
1525             if (!mouseBlock) {
1526                 this._waterfallPopover.dismiss();
1527                 return null;
1528             }
1529
1530             return WI.Rect.rectFromClientRect(mouseBlock.getBoundingClientRect());
1531         };
1532
1533         let targetFrame = calculateTargetFrame();
1534         if (!targetFrame.size.width && !targetFrame.size.height)
1535             return;
1536
1537         let isRTL = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL;
1538         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];
1539         this._waterfallPopover.windowResizeHandler = () => {
1540             let bounds = calculateTargetFrame();
1541             if (bounds)
1542                 this._waterfallPopover.present(bounds, preferredEdges);
1543         };
1544
1545         let popoverContentElement = this._waterfallPopoverContentForResource(entry.resource);
1546         this._waterfallPopover.presentNewContentWithFrame(popoverContentElement, targetFrame, preferredEdges);
1547     }
1548
1549     _tableNameColumnDidChangeWidth(event)
1550     {
1551         this._nameColumnWidthSetting.value = event.target.width;
1552
1553         this._positionDetailView();
1554         this._positionEmptyFilterMessage();
1555     }
1556
1557     _tableWaterfallColumnDidChangeWidth(event)
1558     {
1559         this._table.reloadVisibleColumnCells(this._waterfallColumn);
1560     }
1561 };