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