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