| /* |
| * Copyright (C) 2017 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentView |
| { |
| constructor(representedObject, extraArguments) |
| { |
| super(representedObject); |
| |
| // Collections contain the set of values needed to render the table. |
| // The main collection reflects the main target's live activity. |
| // We create other collections for HAR imports. |
| this._collections = []; |
| this._activeCollection = null; |
| this._mainCollection = this._addCollection(); |
| this._setActiveCollection(this._mainCollection); |
| |
| this._entriesSortComparator = null; |
| |
| this._showingRepresentedObjectCookie = null; |
| |
| this._table = null; |
| this._nameColumnWidthSetting = new WI.Setting("network-table-content-view-name-column-width", 250); |
| |
| this._selectedObject = null; |
| this._detailView = null; |
| this._detailViewMap = new Map; |
| |
| this._domNodeEntries = new Map; |
| |
| this._waterfallTimelineRuler = null; |
| this._waterfallPopover = null; |
| |
| // FIXME: Network Timeline. |
| // FIXME: Throttling. |
| |
| this._typeFilterScopeBarItemAll = new WI.ScopeBarItem("network-type-filter-all", WI.UIString("All"), {exclusive: true}); |
| let typeFilterScopeBarItems = [this._typeFilterScopeBarItemAll]; |
| |
| let uniqueTypes = [ |
| ["Document", (type) => type === WI.Resource.Type.Document], |
| ["StyleSheet", (type) => type === WI.Resource.Type.StyleSheet], |
| ["Image", (type) => type === WI.Resource.Type.Image], |
| ["Font", (type) => type === WI.Resource.Type.Font], |
| ["Script", (type) => type === WI.Resource.Type.Script], |
| ["XHR", (type) => type === WI.Resource.Type.XHR || type === WI.Resource.Type.Fetch], |
| ["Other", (type) => { |
| return type !== WI.Resource.Type.Document |
| && type !== WI.Resource.Type.StyleSheet |
| && type !== WI.Resource.Type.Image |
| && type !== WI.Resource.Type.Font |
| && type !== WI.Resource.Type.Script |
| && type !== WI.Resource.Type.XHR |
| && type !== WI.Resource.Type.Fetch; |
| }], |
| ]; |
| for (let [key, checker] of uniqueTypes) { |
| let type = WI.Resource.Type[key]; |
| let scopeBarItem = new WI.ScopeBarItem("network-type-filter-" + key, WI.NetworkTableContentView.shortDisplayNameForResourceType(type)); |
| scopeBarItem.__checker = checker; |
| typeFilterScopeBarItems.push(scopeBarItem); |
| } |
| |
| this._typeFilterScopeBar = new WI.ScopeBar("network-type-filter-scope-bar", typeFilterScopeBarItems, typeFilterScopeBarItems[0]); |
| this._typeFilterScopeBar.addEventListener(WI.ScopeBar.Event.SelectionChanged, this._typeFilterScopeBarSelectionChanged, this); |
| |
| if (WI.MediaInstrument.supported()) { |
| this._groupMediaRequestsByDOMNodeNavigationItem = new WI.CheckboxNavigationItem("group-media-requests", WI.UIString("Group Media Requests"), WI.settings.groupMediaRequestsByDOMNode.value); |
| this._groupMediaRequestsByDOMNodeNavigationItem.addEventListener(WI.CheckboxNavigationItem.Event.CheckedDidChange, this._handleGroupMediaRequestsByDOMNodeCheckedDidChange, this); |
| } else |
| WI.settings.groupMediaRequestsByDOMNode.value = false; |
| |
| this._urlFilterSearchText = null; |
| this._urlFilterSearchRegex = null; |
| this._urlFilterIsActive = false; |
| |
| this._urlFilterNavigationItem = new WI.FilterBarNavigationItem; |
| this._urlFilterNavigationItem.filterBar.addEventListener(WI.FilterBar.Event.FilterDidChange, this._urlFilterDidChange, this); |
| this._urlFilterNavigationItem.filterBar.placeholder = WI.UIString("Filter Full URL"); |
| |
| this._activeTypeFilters = this._generateTypeFilter(); |
| this._activeURLFilterResources = new Set; |
| |
| this._emptyFilterResultsMessageElement = null; |
| |
| this._clearOnLoadNavigationItem = new WI.CheckboxNavigationItem("preserve-log", WI.UIString("Preserve Log"), !WI.settings.clearNetworkOnNavigate.value); |
| this._clearOnLoadNavigationItem.tooltip = WI.UIString("Do not clear network items on new page loads"); |
| this._clearOnLoadNavigationItem.addEventListener(WI.CheckboxNavigationItem.Event.CheckedDidChange, () => { WI.settings.clearNetworkOnNavigate.value = !WI.settings.clearNetworkOnNavigate.value; }); |
| WI.settings.clearNetworkOnNavigate.addEventListener(WI.Setting.Event.Changed, this._clearNetworkOnNavigateSettingChanged, this); |
| |
| this._harImportNavigationItem = new WI.ButtonNavigationItem("har-import", WI.UIString("Import"), "Images/Import.svg", 15, 15); |
| this._harImportNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText; |
| this._harImportNavigationItem.tooltip = WI.UIString("HAR Import"); |
| this._harImportNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, () => { |
| this._importHAR(); |
| }); |
| |
| this._harExportNavigationItem = new WI.ButtonNavigationItem("har-export", WI.UIString("Export"), "Images/Export.svg", 15, 15); |
| this._harExportNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText; |
| this._harExportNavigationItem.tooltip = WI.UIString("HAR Export (%s)").format(WI.saveKeyboardShortcut.displayName); |
| this._harExportNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, () => { |
| this._exportHAR(); |
| }); |
| |
| this._collectionsPathNavigationItem = new WI.HierarchicalPathNavigationItem; |
| this._collectionsPathNavigationItem.addEventListener(WI.HierarchicalPathNavigationItem.Event.PathComponentWasSelected, this._collectionsHierarchicalPathComponentWasSelected, this); |
| |
| this._pathComponentsMap = new Map; |
| this._lastPathComponent = null; |
| let pathComponent = this._addCollectionPathComponent(this._mainCollection, WI.UIString("Live Activity"), "network-overview-icon"); |
| this._collectionsPathNavigationItem.components = [pathComponent]; |
| |
| this._checkboxesNavigationItemGroup = new WI.GroupNavigationItem([this._clearOnLoadNavigationItem, new WI.DividerNavigationItem]); |
| this._checkboxesNavigationItemGroup.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low; |
| |
| this._pathComponentsNavigationItemGroup = new WI.GroupNavigationItem([this._collectionsPathNavigationItem, new WI.DividerNavigationItem]); |
| this._pathComponentsNavigationItemGroup.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low; |
| this._pathComponentsNavigationItemGroup.hidden = true; |
| |
| this._buttonsNavigationItemGroup = new WI.GroupNavigationItem([this._harImportNavigationItem, this._harExportNavigationItem, new WI.DividerNavigationItem]); |
| this._buttonsNavigationItemGroup.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low; |
| |
| // COMPATIBILITY (iOS 10.3): Network.setDisableResourceCaching did not exist. |
| if (window.NetworkAgent && NetworkAgent.setResourceCachingDisabled) { |
| let toolTipForDisableResourceCache = WI.UIString("Ignore the resource cache when loading resources"); |
| let activatedToolTipForDisableResourceCache = WI.UIString("Use the resource cache when loading resources"); |
| this._disableResourceCacheNavigationItem = new WI.ActivateButtonNavigationItem("disable-resource-cache", toolTipForDisableResourceCache, activatedToolTipForDisableResourceCache, "Images/IgnoreCaches.svg", 16, 16); |
| this._disableResourceCacheNavigationItem.activated = WI.settings.resourceCachingDisabled.value; |
| |
| this._disableResourceCacheNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._toggleDisableResourceCache, this); |
| WI.settings.resourceCachingDisabled.addEventListener(WI.Setting.Event.Changed, this._resourceCachingDisabledSettingChanged, this); |
| } |
| |
| this._clearNetworkItemsNavigationItem = new WI.ButtonNavigationItem("clear-network-items", WI.UIString("Clear Network Items (%s)").format(WI.clearKeyboardShortcut.displayName), "Images/NavigationItemTrash.svg", 15, 15); |
| this._clearNetworkItemsNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, () => { |
| this.reset(); |
| }); |
| |
| WI.Target.addEventListener(WI.Target.Event.ResourceAdded, this._handleResourceAdded, this); |
| WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this); |
| WI.Frame.addEventListener(WI.Frame.Event.ResourceWasAdded, this._handleResourceAdded, this); |
| WI.Frame.addEventListener(WI.Frame.Event.ChildFrameWasAdded, this._handleFrameWasAdded, this); |
| WI.Resource.addEventListener(WI.Resource.Event.LoadingDidFinish, this._resourceLoadingDidFinish, this); |
| WI.Resource.addEventListener(WI.Resource.Event.LoadingDidFail, this._resourceLoadingDidFail, this); |
| WI.Resource.addEventListener(WI.Resource.Event.TransferSizeDidChange, this._resourceTransferSizeDidChange, this); |
| WI.networkManager.addEventListener(WI.NetworkManager.Event.MainFrameDidChange, this._mainFrameDidChange, this); |
| |
| this._needsInitialPopulate = true; |
| |
| // FIXME: This is working around the order of events. Normal page navigation |
| // triggers a MainResource change and then a MainFrame change. Page Transition |
| // triggers a MainFrame change then a MainResource change. |
| this._transitioningPageTarget = false; |
| |
| WI.notifications.addEventListener(WI.Notification.TransitionPageTarget, this._transitionPageTarget, this); |
| } |
| |
| // Static |
| |
| static displayNameForResource(resource) |
| { |
| if (resource.type === WI.Resource.Type.Image || resource.type === WI.Resource.Type.Font || resource.type === WI.Resource.Type.Other) { |
| let fileExtension; |
| if (resource.mimeType) |
| fileExtension = WI.fileExtensionForMIMEType(resource.mimeType); |
| if (!fileExtension) |
| fileExtension = WI.fileExtensionForURL(resource.url); |
| if (fileExtension) |
| return fileExtension; |
| } |
| |
| return WI.NetworkTableContentView.shortDisplayNameForResourceType(resource.type).toLowerCase(); |
| } |
| |
| static shortDisplayNameForResourceType(type) |
| { |
| switch (type) { |
| case WI.Resource.Type.Document: |
| return WI.UIString("Document"); |
| case WI.Resource.Type.StyleSheet: |
| return WI.unlocalizedString("CSS"); |
| case WI.Resource.Type.Image: |
| return WI.UIString("Image"); |
| case WI.Resource.Type.Font: |
| return WI.UIString("Font"); |
| case WI.Resource.Type.Script: |
| return WI.unlocalizedString("JS"); |
| case WI.Resource.Type.XHR: |
| return WI.unlocalizedString("XHR"); |
| case WI.Resource.Type.Fetch: |
| return WI.repeatedUIString.fetch(); |
| case WI.Resource.Type.Ping: |
| return WI.UIString("Ping"); |
| case WI.Resource.Type.Beacon: |
| return WI.UIString("Beacon"); |
| case WI.Resource.Type.WebSocket: |
| case WI.Resource.Type.Other: |
| return WI.UIString("Other"); |
| default: |
| console.error("Unknown resource type", type); |
| return null; |
| } |
| } |
| |
| static get nodeWaterfallDOMEventSize() { return 8; } |
| |
| // Public |
| |
| get selectionPathComponents() |
| { |
| return null; |
| } |
| |
| get navigationItems() |
| { |
| let items = [this._checkboxesNavigationItemGroup, this._pathComponentsNavigationItemGroup, this._buttonsNavigationItemGroup]; |
| if (this._disableResourceCacheNavigationItem) |
| items.push(this._disableResourceCacheNavigationItem); |
| items.push(this._clearNetworkItemsNavigationItem); |
| return items; |
| } |
| |
| get filterNavigationItems() |
| { |
| let navigationItems = [this._urlFilterNavigationItem, this._typeFilterScopeBar]; |
| if (WI.MediaInstrument.supported()) |
| navigationItems.push(this._groupMediaRequestsByDOMNodeNavigationItem); |
| return navigationItems; |
| } |
| |
| get supportsSave() |
| { |
| return this._canExportHAR(); |
| } |
| |
| get saveData() |
| { |
| return {customSaveHandler: () => { this._exportHAR(); }}; |
| } |
| |
| shown() |
| { |
| super.shown(); |
| |
| if (this._detailView) |
| this._detailView.shown(); |
| |
| if (this._table) |
| this._table.restoreScrollPosition(); |
| } |
| |
| hidden() |
| { |
| this._hidePopover(); |
| |
| if (this._detailView) |
| this._detailView.hidden(); |
| |
| super.hidden(); |
| } |
| |
| closed() |
| { |
| for (let detailView of this._detailViewMap.values()) |
| detailView.dispose(); |
| this._detailViewMap.clear(); |
| |
| this._domNodeEntries.clear(); |
| |
| this._hidePopover(); |
| this._hideDetailView(); |
| |
| WI.Target.removeEventListener(null, null, this); |
| WI.Frame.removeEventListener(null, null, this); |
| WI.Resource.removeEventListener(null, null, this); |
| WI.settings.resourceCachingDisabled.removeEventListener(null, null, this); |
| WI.settings.clearNetworkOnNavigate.removeEventListener(null, null, this); |
| WI.networkManager.removeEventListener(WI.NetworkManager.Event.MainFrameDidChange, this._mainFrameDidChange, this); |
| |
| super.closed(); |
| } |
| |
| reset() |
| { |
| this._runForMainCollection((collection) => { |
| this._resetCollection(collection); |
| }); |
| |
| for (let detailView of this._detailViewMap.values()) |
| detailView.dispose(); |
| this._detailViewMap.clear(); |
| |
| this._domNodeEntries.clear(); |
| |
| this._updateWaterfallTimelineRuler(); |
| this._updateExportButton(); |
| |
| if (this._table) { |
| this._selectedObject = null; |
| this._table.reloadData(); |
| this._hidePopover(); |
| this._hideDetailView(); |
| } |
| } |
| |
| showRepresentedObject(representedObject, cookie) |
| { |
| console.assert(representedObject instanceof WI.Resource); |
| |
| let rowIndex = this._rowIndexForRepresentedObject(representedObject); |
| if (rowIndex === -1) { |
| this._selectedObject = null; |
| this._table.deselectAll(); |
| this._hideDetailView(); |
| return; |
| } |
| |
| this._showingRepresentedObjectCookie = cookie; |
| this._table.selectRow(rowIndex); |
| this._showingRepresentedObjectCookie = null; |
| } |
| |
| // NetworkDetailView delegate |
| |
| networkDetailViewClose(networkDetailView) |
| { |
| this._selectedObject = null; |
| this._table.deselectAll(); |
| this._hideDetailView(); |
| } |
| |
| // Table dataSource |
| |
| tableIndexForRepresentedObject(table, object) |
| { |
| return this._activeCollection.filteredEntries.indexOf(object); |
| } |
| |
| tableRepresentedObjectForIndex(table, index) |
| { |
| console.assert(index >= 0 && index < this._activeCollection.filteredEntries.length); |
| return this._activeCollection.filteredEntries[index]; |
| } |
| |
| tableNumberOfRows(table) |
| { |
| return this._activeCollection.filteredEntries.length; |
| } |
| |
| tableSortChanged(table) |
| { |
| this._generateSortComparator(); |
| |
| if (!this._entriesSortComparator) |
| return; |
| |
| this._hideDetailView(); |
| |
| for (let nodeEntry of this._domNodeEntries.values()) |
| nodeEntry.initiatedResourceEntries.sort(this._entriesSortComparator); |
| |
| this._updateSort(); |
| this._updateFilteredEntries(); |
| this._reloadTable(); |
| } |
| |
| // Table delegate |
| |
| tableCellContextMenuClicked(table, cell, column, rowIndex, event) |
| { |
| if (column !== this._nameColumn) |
| return; |
| |
| this._table.selectRow(rowIndex); |
| |
| let entry = this._activeCollection.filteredEntries[rowIndex]; |
| let contextMenu = WI.ContextMenu.createFromEvent(event); |
| WI.appendContextMenuItemsForSourceCode(contextMenu, entry.resource); |
| |
| contextMenu.appendSeparator(); |
| contextMenu.appendItem(WI.UIString("Export HAR"), () => { this._exportHAR(); }, !this._canExportHAR()); |
| } |
| |
| tableShouldSelectRow(table, cell, column, rowIndex) |
| { |
| return column === this._nameColumn; |
| } |
| |
| tableSelectionDidChange(table) |
| { |
| let rowIndex = table.selectedRow; |
| if (isNaN(rowIndex)) { |
| this._selectedObject = null; |
| this._hideDetailView(); |
| return; |
| } |
| |
| let entry = this._activeCollection.filteredEntries[rowIndex]; |
| if (entry.resource === this._selectedObject || entry.domNode === this._selectedObject) |
| return; |
| |
| this._selectedObject = entry.resource || entry.domNode; |
| if (this._selectedObject) |
| this._showDetailView(this._selectedObject); |
| else |
| this._hideDetailView(); |
| } |
| |
| tablePopulateCell(table, cell, column, rowIndex) |
| { |
| let entry = this._activeCollection.filteredEntries[rowIndex]; |
| |
| if (entry.resource) |
| cell.classList.toggle("error", entry.resource.hadLoadingError()); |
| |
| let setTextContent = (accessor) => { |
| let uniqueValues = this._uniqueValuesForDOMNodeEntry(entry, accessor); |
| if (uniqueValues) { |
| if (uniqueValues.size > 1) { |
| cell.classList.add("multiple"); |
| cell.textContent = WI.UIString("(multiple)"); |
| return; |
| } |
| |
| cell.textContent = uniqueValues.values().next().value || emDash; |
| return; |
| } |
| |
| cell.textContent = accessor(entry) || emDash; |
| }; |
| |
| switch (column.identifier) { |
| case "name": |
| this._populateNameCell(cell, entry); |
| break; |
| case "domain": |
| this._populateDomainCell(cell, entry); |
| break; |
| case "type": |
| setTextContent((resourceEntry) => resourceEntry.displayType); |
| break; |
| case "mimeType": |
| setTextContent((resourceEntry) => resourceEntry.mimeType); |
| break; |
| case "method": |
| setTextContent((resourceEntry) => resourceEntry.method); |
| break; |
| case "scheme": |
| setTextContent((resourceEntry) => resourceEntry.scheme); |
| break; |
| case "status": |
| setTextContent((resourceEntry) => resourceEntry.status); |
| break; |
| case "protocol": |
| setTextContent((resourceEntry) => resourceEntry.protocol); |
| break; |
| case "initiator": |
| this._populateInitiatorCell(cell, entry); |
| break; |
| case "priority": |
| setTextContent((resourceEntry) => WI.Resource.displayNameForPriority(resourceEntry.priority)); |
| break; |
| case "remoteAddress": |
| setTextContent((resourceEntry) => resourceEntry.remoteAddress); |
| break; |
| case "connectionIdentifier": |
| setTextContent((resourceEntry) => resourceEntry.connectionIdentifier); |
| break; |
| case "resourceSize": { |
| let resourceSize = entry.resourceSize; |
| let resourceEntries = entry.initiatedResourceEntries; |
| if (resourceEntries) |
| resourceSize = resourceEntries.reduce((accumulator, current) => accumulator + (current.resourceSize || 0), 0); |
| cell.textContent = isNaN(resourceSize) ? emDash : Number.bytesToString(resourceSize); |
| break; |
| } |
| case "transferSize": |
| this._populateTransferSizeCell(cell, entry); |
| break; |
| case "time": { |
| // FIXME: <https://webkit.org/b/176748> Web Inspector: Frontend sometimes receives resources with negative duration (responseEnd - requestStart) |
| let time = entry.time; |
| let resourceEntries = entry.initiatedResourceEntries; |
| if (resourceEntries) |
| time = resourceEntries.reduce((accumulator, current) => accumulator + (current.time || 0), 0); |
| cell.textContent = isNaN(time) ? emDash : Number.secondsToString(Math.max(time, 0)); |
| break; |
| } |
| case "waterfall": |
| this._populateWaterfallGraph(cell, entry); |
| break; |
| } |
| |
| return cell; |
| } |
| |
| // Private |
| |
| _addCollection() |
| { |
| let collection = {}; |
| this._resetCollection(collection); |
| this._collections.push(collection); |
| return collection; |
| } |
| |
| _resetCollection(collection) |
| { |
| collection.entries = []; |
| collection.filteredEntries = []; |
| collection.pendingInsertions = []; |
| collection.pendingUpdates = []; |
| collection.waterfallStartTime = NaN; |
| collection.waterfallEndTime = NaN; |
| } |
| |
| _setActiveCollection(collection) |
| { |
| console.assert(this._collections.includes(collection)); |
| |
| if (this._activeCollection === collection) |
| return; |
| |
| this._activeCollection = collection; |
| } |
| |
| _addCollectionPathComponent(collection, displayName, iconClassName) |
| { |
| let pathComponent = new WI.HierarchicalPathComponent(displayName, iconClassName, collection); |
| this._pathComponentsMap.set(collection, pathComponent); |
| |
| if (this._lastPathComponent) { |
| this._lastPathComponent.nextSibling = pathComponent; |
| pathComponent.previousSibling = this._lastPathComponent; |
| } |
| |
| this._lastPathComponent = pathComponent; |
| |
| if (this._pathComponentsNavigationItemGroup && this._pathComponentsMap.size > 1) |
| this._pathComponentsNavigationItemGroup.hidden = false; |
| |
| return pathComponent; |
| } |
| |
| _collectionsHierarchicalPathComponentWasSelected(event) |
| { |
| console.assert(event.data.pathComponent instanceof WI.HierarchicalPathComponent); |
| |
| let collection = event.data.pathComponent.representedObject; |
| |
| this._changeCollection(collection); |
| } |
| |
| _changeCollection(collection) |
| { |
| if (collection === this._activeCollection) |
| return; |
| |
| this._setActiveCollection(collection); |
| |
| let isMain = collection === this._mainCollection; |
| this._checkboxesNavigationItemGroup.hidden = !isMain; |
| this._groupMediaRequestsByDOMNodeNavigationItem.hidden = !isMain; |
| this._clearNetworkItemsNavigationItem.enabled = isMain; |
| this._collectionsPathNavigationItem.components = [this._pathComponentsMap.get(collection)]; |
| |
| this._updateSort(); |
| this._updateActiveFilterResources(); |
| this._updateFilteredEntries(); |
| this._updateWaterfallTimelineRuler(); |
| this._reloadTable(); |
| this._hideDetailView(); |
| |
| this.needsLayout(); |
| } |
| |
| _populateNameCell(cell, entry) |
| { |
| console.assert(!cell.firstChild, "We expect the cell to be empty.", cell, cell.firstChild); |
| |
| function createIconElement() { |
| let iconElement = cell.appendChild(document.createElement("img")); |
| iconElement.className = "icon"; |
| } |
| |
| let domNode = entry.domNode; |
| if (domNode) { |
| this._table.element.classList.add("grouped"); |
| |
| cell.classList.add("parent"); |
| |
| let disclosureElement = cell.appendChild(document.createElement("img")); |
| disclosureElement.classList.add("disclosure"); |
| disclosureElement.classList.toggle("expanded", !!entry.expanded); |
| disclosureElement.addEventListener("click", (event) => { |
| entry.expanded = !entry.expanded; |
| |
| this._updateFilteredEntries(); |
| this._reloadTable(); |
| }); |
| |
| createIconElement(); |
| |
| cell.classList.add("dom-node"); |
| cell.appendChild(WI.linkifyNodeReference(domNode)); |
| return; |
| } |
| |
| let resource = entry.resource; |
| if (resource.isLoading()) { |
| let statusElement = cell.appendChild(document.createElement("div")); |
| statusElement.className = "status"; |
| let spinner = new WI.IndeterminateProgressSpinner; |
| statusElement.appendChild(spinner.element); |
| } |
| |
| createIconElement(); |
| |
| cell.classList.add(WI.ResourceTreeElement.ResourceIconStyleClassName, ...WI.Resource.classNamesForResource(resource)); |
| |
| if (WI.settings.groupMediaRequestsByDOMNode.value && resource.initiatorNode) { |
| let nodeEntry = this._domNodeEntries.get(resource.initiatorNode); |
| if (nodeEntry.initiatedResourceEntries.length > 1 || nodeEntry.domNode.domEvents.length) |
| cell.classList.add("child"); |
| } |
| |
| let nameElement = cell.appendChild(document.createElement("span")); |
| nameElement.textContent = entry.name; |
| |
| let range = resource.requestedByteRange; |
| if (range) { |
| let rangeElement = nameElement.appendChild(document.createElement("span")); |
| rangeElement.classList.add("range"); |
| rangeElement.textContent = WI.UIString("Byte Range %s\u2013%s").format(range.start, range.end); |
| } |
| |
| cell.title = resource.url; |
| cell.classList.add(...WI.Resource.classNamesForResource(resource)); |
| } |
| |
| _populateDomainCell(cell, entry) |
| { |
| console.assert(!cell.firstChild, "We expect the cell to be empty.", cell, cell.firstChild); |
| |
| function createIconAndText(scheme, domain) { |
| let secure = scheme === "https" || scheme === "wss"; |
| if (secure) { |
| let lockIconElement = cell.appendChild(document.createElement("img")); |
| lockIconElement.className = "lock"; |
| } |
| |
| cell.append(domain || emDash); |
| } |
| |
| let uniqueSchemeValues = this._uniqueValuesForDOMNodeEntry(entry, (resourceEntry) => resourceEntry.scheme); |
| let uniqueDomainValues = this._uniqueValuesForDOMNodeEntry(entry, (resourceEntry) => resourceEntry.domain); |
| if (uniqueSchemeValues && uniqueDomainValues) { |
| if (uniqueSchemeValues.size > 1 || uniqueDomainValues.size > 1) { |
| cell.classList.add("multiple"); |
| cell.textContent = WI.UIString("(multiple)"); |
| return; |
| } |
| |
| createIconAndText(uniqueSchemeValues.values().next().value, uniqueDomainValues.values().next().value); |
| return; |
| } |
| |
| createIconAndText(entry.scheme, entry.domain); |
| } |
| |
| _populateInitiatorCell(cell, entry) |
| { |
| let domNode = entry.domNode; |
| if (domNode) { |
| cell.textContent = emDash; |
| return; |
| } |
| |
| let initiatorLocation = entry.resource.initiatorSourceCodeLocation; |
| if (!initiatorLocation) { |
| cell.textContent = emDash; |
| return; |
| } |
| |
| const options = { |
| dontFloat: true, |
| ignoreSearchTab: true, |
| }; |
| cell.appendChild(WI.createSourceCodeLocationLink(initiatorLocation, options)); |
| } |
| |
| _populateTransferSizeCell(cell, entry) |
| { |
| let resourceEntries = entry.initiatedResourceEntries; |
| if (resourceEntries) { |
| if (resourceEntries.every((resourceEntry) => resourceEntry.resource.responseSource === WI.Resource.ResponseSource.MemoryCache)) { |
| cell.classList.add("cache-type"); |
| cell.textContent = WI.UIString("(memory)"); |
| return; |
| } |
| if (resourceEntries.every((resourceEntry) => resourceEntry.resource.responseSource === WI.Resource.ResponseSource.DiskCache)) { |
| cell.classList.add("cache-type"); |
| cell.textContent = WI.UIString("(disk)"); |
| return; |
| } |
| if (resourceEntries.every((resourceEntry) => resourceEntry.resource.responseSource === WI.Resource.ResponseSource.ServiceWorker)) { |
| cell.classList.add("cache-type"); |
| cell.textContent = WI.UIString("(service worker)"); |
| return; |
| } |
| let transferSize = resourceEntries.reduce((accumulator, current) => accumulator + (current.transferSize || 0), 0); |
| if (isNaN(transferSize)) |
| cell.textContent = emDash; |
| else |
| cell.textContent = Number.bytesToString(transferSize); |
| return; |
| } |
| |
| let responseSource = entry.resource.responseSource; |
| if (responseSource === WI.Resource.ResponseSource.MemoryCache) { |
| cell.classList.add("cache-type"); |
| cell.textContent = WI.UIString("(memory)"); |
| return; |
| } |
| if (responseSource === WI.Resource.ResponseSource.DiskCache) { |
| cell.classList.add("cache-type"); |
| cell.textContent = WI.UIString("(disk)"); |
| return; |
| } |
| if (responseSource === WI.Resource.ResponseSource.ServiceWorker) { |
| cell.classList.add("cache-type"); |
| cell.textContent = WI.UIString("(service worker)"); |
| return; |
| } |
| if (responseSource === WI.Resource.ResponseSource.InspectorOverride) { |
| cell.classList.add("cache-type"); |
| cell.textContent = WI.UIString("(inspector override)"); |
| return; |
| } |
| |
| let transferSize = entry.transferSize; |
| cell.textContent = isNaN(transferSize) ? emDash : Number.bytesToString(transferSize); |
| console.assert(!cell.classList.contains("cache-type"), "Should not have cache-type class on cell."); |
| } |
| |
| _populateWaterfallGraph(cell, entry) |
| { |
| cell.removeChildren(); |
| |
| let container = cell.appendChild(document.createElement("div")); |
| container.className = "waterfall-container"; |
| |
| let collection = this._activeCollection; |
| let graphStartTime = this._waterfallTimelineRuler.startTime; |
| let secondsPerPixel = this._waterfallTimelineRuler.secondsPerPixel; |
| |
| function positionByStartOffset(element, timestamp) { |
| let styleAttribute = WI.resolvedLayoutDirection() === WI.LayoutDirection.LTR ? "left" : "right"; |
| element.style.setProperty(styleAttribute, ((timestamp - graphStartTime) / secondsPerPixel) + "px"); |
| } |
| |
| function setWidthForDuration(element, startTimestamp, endTimestamp) { |
| element.style.setProperty("width", ((endTimestamp - startTimestamp) / secondsPerPixel) + "px"); |
| } |
| |
| let domNode = entry.domNode; |
| if (domNode) { |
| let groupedDOMEvents = []; |
| for (let domEvent of domNode.domEvents) { |
| if (domEvent.originator) |
| continue; |
| |
| if (!groupedDOMEvents.length || (domEvent.timestamp - groupedDOMEvents.lastValue.endTimestamp) >= (NetworkTableContentView.nodeWaterfallDOMEventSize * secondsPerPixel)) { |
| groupedDOMEvents.push({ |
| startTimestamp: domEvent.timestamp, |
| domEvents: [], |
| }); |
| } |
| groupedDOMEvents.lastValue.endTimestamp = domEvent.timestamp; |
| groupedDOMEvents.lastValue.domEvents.push(domEvent); |
| } |
| |
| let fullscreenDOMEvents = WI.DOMNode.getFullscreenDOMEvents(domNode.domEvents); |
| if (fullscreenDOMEvents.length) { |
| if (!fullscreenDOMEvents[0].data.enabled) |
| fullscreenDOMEvents.unshift({timestamp: graphStartTime}); |
| |
| if (fullscreenDOMEvents.lastValue.data.enabled) |
| fullscreenDOMEvents.push({timestamp: collection.waterfallEndTime}); |
| |
| console.assert((fullscreenDOMEvents.length % 2) === 0, "Every enter/exit of fullscreen should have a corresponding exit/enter."); |
| |
| for (let i = 0; i < fullscreenDOMEvents.length; i += 2) { |
| let fullscreenElement = container.appendChild(document.createElement("div")); |
| fullscreenElement.classList.add("area", "dom-fullscreen"); |
| positionByStartOffset(fullscreenElement, fullscreenDOMEvents[i].timestamp); |
| setWidthForDuration(fullscreenElement, fullscreenDOMEvents[i].timestamp, fullscreenDOMEvents[i + 1].timestamp); |
| |
| let originator = fullscreenDOMEvents[i].originator || fullscreenDOMEvents[i + 1].originator; |
| if (originator) |
| fullscreenElement.title = WI.UIString("Full-Screen from \u201C%s\u201D").format(originator.displayName); |
| else |
| fullscreenElement.title = WI.UIString("Full-Screen"); |
| } |
| } |
| |
| for (let powerEfficientPlaybackRange of domNode.powerEfficientPlaybackRanges) { |
| let startTimestamp = powerEfficientPlaybackRange.startTimestamp || graphStartTime; |
| let endTimestamp = powerEfficientPlaybackRange.endTimestamp || collection.waterfallEndTime; |
| |
| let powerEfficientPlaybackRangeElement = container.appendChild(document.createElement("div")); |
| powerEfficientPlaybackRangeElement.classList.add("area", "power-efficient-playback"); |
| powerEfficientPlaybackRangeElement.title = WI.UIString("Power Efficient Playback"); |
| positionByStartOffset(powerEfficientPlaybackRangeElement, startTimestamp); |
| setWidthForDuration(powerEfficientPlaybackRangeElement, startTimestamp, endTimestamp); |
| } |
| |
| let playing = false; |
| |
| function createDOMEventLine(domEvents, startTimestamp, endTimestamp) { |
| if (WI.DOMNode.isStopEvent(domEvents.lastValue.eventName)) |
| return; |
| |
| for (let i = domEvents.length - 1; i >= 0; --i) { |
| let domEvent = domEvents[i]; |
| if (WI.DOMNode.isPlayEvent(domEvent.eventName)) { |
| playing = true; |
| break; |
| } |
| |
| if (WI.DOMNode.isPauseEvent(domEvent.eventName)) { |
| playing = false; |
| break; |
| } |
| } |
| |
| let lineElement = container.appendChild(document.createElement("div")); |
| lineElement.classList.add("dom-activity"); |
| lineElement.classList.toggle("playing", playing); |
| positionByStartOffset(lineElement, startTimestamp); |
| setWidthForDuration(lineElement, startTimestamp, endTimestamp); |
| } |
| |
| for (let [a, b] of groupedDOMEvents.adjacencies()) |
| createDOMEventLine(a.domEvents, a.endTimestamp, b.startTimestamp); |
| |
| if (groupedDOMEvents.length) |
| createDOMEventLine(groupedDOMEvents.lastValue.domEvents, groupedDOMEvents.lastValue.endTimestamp, collection.waterfallEndTime); |
| |
| for (let {startTimestamp, endTimestamp, domEvents} of groupedDOMEvents) { |
| let paddingForCentering = NetworkTableContentView.nodeWaterfallDOMEventSize * secondsPerPixel / 2; |
| |
| let eventElement = container.appendChild(document.createElement("div")); |
| eventElement.classList.add("dom-event"); |
| positionByStartOffset(eventElement, startTimestamp - paddingForCentering); |
| setWidthForDuration(eventElement, startTimestamp, endTimestamp + paddingForCentering); |
| eventElement.addEventListener("mousedown", (event) => { |
| if (event.button !== 0 || event.ctrlKey) |
| return; |
| this._handleNodeEntryMousedownWaterfall(entry, domEvents); |
| }); |
| |
| for (let domEvent of domEvents) |
| entry.domEventElements.set(domEvent, eventElement); |
| } |
| |
| return; |
| } |
| |
| let resource = entry.resource; |
| if (!resource.hasResponse()) { |
| cell.textContent = zeroWidthSpace; |
| return; |
| } |
| |
| let {startTime, redirectStart, redirectEnd, fetchStart, domainLookupStart, domainLookupEnd, connectStart, connectEnd, secureConnectionStart, requestStart, responseStart, responseEnd} = resource.timingData; |
| if (isNaN(startTime) || isNaN(responseEnd) || startTime > responseEnd) { |
| cell.textContent = zeroWidthSpace; |
| return; |
| } |
| |
| if (responseEnd < graphStartTime) { |
| cell.textContent = zeroWidthSpace; |
| return; |
| } |
| |
| let graphEndTime = this._waterfallTimelineRuler.endTime; |
| if (startTime > graphEndTime) { |
| cell.textContent = zeroWidthSpace; |
| return; |
| } |
| |
| let lastEndTimestamp = NaN; |
| function appendBlock(startTimestamp, endTimestamp, className) { |
| if (isNaN(startTimestamp) || isNaN(endTimestamp)) |
| return null; |
| |
| if (Math.abs(startTimestamp - lastEndTimestamp) < secondsPerPixel * 2) |
| startTimestamp = lastEndTimestamp; |
| lastEndTimestamp = endTimestamp; |
| |
| let block = container.appendChild(document.createElement("div")); |
| block.classList.add("block", className); |
| positionByStartOffset(block, startTimestamp); |
| setWidthForDuration(block, startTimestamp, endTimestamp); |
| return block; |
| } |
| |
| // Mouse block sits on top and accepts mouse events on this group. |
| let padSeconds = 10 * secondsPerPixel; |
| let mouseBlock = appendBlock(startTime - padSeconds, responseEnd + padSeconds, "mouse-tracking"); |
| mouseBlock.addEventListener("mousedown", (event) => { |
| if (event.button !== 0 || event.ctrlKey) |
| return; |
| this._handleResourceEntryMousedownWaterfall(entry); |
| }); |
| |
| // Super small visualization. |
| let totalWidth = (responseEnd - startTime) / secondsPerPixel; |
| if (totalWidth <= 3) { |
| let twoPixels = secondsPerPixel * 2; |
| appendBlock(startTime, startTime + twoPixels, "queue"); |
| appendBlock(startTime + twoPixels, startTime + (2 * twoPixels), "response"); |
| return; |
| } |
| |
| appendBlock(startTime, responseEnd, "filler"); |
| |
| // FIXME: <https://webkit.org/b/190214> Web Inspector: expose full load metrics for redirect requests |
| appendBlock(redirectStart, redirectEnd, "redirect"); |
| |
| if (domainLookupStart) { |
| appendBlock(fetchStart, domainLookupStart, "queue"); |
| appendBlock(domainLookupStart, domainLookupEnd || connectStart || requestStart, "dns"); |
| } else if (connectStart) |
| appendBlock(fetchStart, connectStart, "queue"); |
| else if (requestStart) |
| appendBlock(fetchStart, requestStart, "queue"); |
| if (connectStart) |
| appendBlock(connectStart, secureConnectionStart || connectEnd, "connect"); |
| if (secureConnectionStart) |
| appendBlock(secureConnectionStart, connectEnd, "secure"); |
| appendBlock(requestStart, responseStart, "request"); |
| appendBlock(responseStart, responseEnd, "response"); |
| } |
| |
| _generateSortComparator() |
| { |
| let sortColumnIdentifier = this._table.sortColumnIdentifier; |
| if (!sortColumnIdentifier) { |
| this._entriesSortComparator = null; |
| return; |
| } |
| |
| let comparator; |
| |
| switch (sortColumnIdentifier) { |
| case "name": |
| case "domain": |
| case "mimeType": |
| case "method": |
| case "scheme": |
| case "protocol": |
| case "initiator": |
| case "remoteAddress": |
| // Simple string. |
| comparator = (a, b) => (a[sortColumnIdentifier] || "").extendedLocaleCompare(b[sortColumnIdentifier] || ""); |
| break; |
| |
| case "status": |
| case "connectionIdentifier": |
| case "resourceSize": |
| case "time": |
| // Simple number. |
| comparator = (a, b) => { |
| let aValue = a[sortColumnIdentifier]; |
| if (isNaN(aValue)) |
| return 1; |
| let bValue = b[sortColumnIdentifier]; |
| if (isNaN(bValue)) |
| return -1; |
| return aValue - bValue; |
| }; |
| break; |
| |
| case "priority": |
| // Resource.NetworkPriority enum. |
| comparator = (a, b) => WI.Resource.comparePriority(a.priority, b.priority); |
| break; |
| |
| case "type": |
| // Sort by displayType string. |
| comparator = (a, b) => (a.displayType || "").extendedLocaleCompare(b.displayType || ""); |
| break; |
| |
| case "transferSize": |
| // Handle (memory) and (disk) values. |
| comparator = (a, b) => { |
| let transferSizeA = a.transferSize; |
| let transferSizeB = b.transferSize; |
| |
| // Treat NaN as the largest value. |
| if (isNaN(transferSizeA)) |
| return 1; |
| if (isNaN(transferSizeB)) |
| return -1; |
| |
| // Treat memory cache and disk cache as small values. |
| let sourceA = a.resource.responseSource; |
| if (sourceA === WI.Resource.ResponseSource.MemoryCache) |
| transferSizeA = -20; |
| else if (sourceA === WI.Resource.ResponseSource.DiskCache) |
| transferSizeA = -10; |
| else if (sourceA === WI.Resource.ResponseSource.ServiceWorker) |
| transferSizeA = -5; |
| else if (sourceA === WI.Resource.ResponseSource.InspectorOverride) |
| transferSizeA = -3; |
| |
| let sourceB = b.resource.responseSource; |
| if (sourceB === WI.Resource.ResponseSource.MemoryCache) |
| transferSizeB = -20; |
| else if (sourceB === WI.Resource.ResponseSource.DiskCache) |
| transferSizeB = -10; |
| else if (sourceB === WI.Resource.ResponseSource.ServiceWorker) |
| transferSizeB = -5; |
| else if (sourceB === WI.Resource.ResponseSource.InspectorOverride) |
| transferSizeB = -3; |
| |
| return transferSizeA - transferSizeB; |
| }; |
| break; |
| |
| case "waterfall": |
| // Sort by startTime number. |
| comparator = (a, b) => a.startTime - b.startTime; |
| break; |
| |
| default: |
| console.assert("Unexpected sort column", sortColumnIdentifier); |
| return; |
| } |
| |
| let reverseFactor = this._table.sortOrder === WI.Table.SortOrder.Ascending ? 1 : -1; |
| |
| // If the entry has an `initiatorNode`, use that node's "first" resource as the value of |
| // `entry`, so long as the entry being compared to doesn't have the same `initiatorNode`. |
| // This will ensure that all resource entries for a given `initiatorNode` will appear right |
| // next to each other, as they will all effectively be sorted by the first resource. |
| let substitute = (entry, other) => { |
| if (WI.settings.groupMediaRequestsByDOMNode.value && entry.resource.initiatorNode) { |
| let nodeEntry = this._domNodeEntries.get(entry.resource.initiatorNode); |
| if (!nodeEntry.initiatedResourceEntries.includes(other)) |
| return nodeEntry.initiatedResourceEntries[0]; |
| } |
| return entry; |
| }; |
| |
| this._entriesSortComparator = (a, b) => reverseFactor * comparator(substitute(a, b), substitute(b, a)); |
| } |
| |
| // Protected |
| |
| initialLayout() |
| { |
| super.initialLayout(); |
| |
| this.element.style.setProperty("--node-waterfall-dom-event-size", NetworkTableContentView.nodeWaterfallDOMEventSize + "px"); |
| |
| this._waterfallTimelineRuler = new WI.TimelineRuler; |
| this._waterfallTimelineRuler.allowsClippedLabels = true; |
| |
| this._nameColumn = new WI.TableColumn("name", WI.UIString("Name"), { |
| minWidth: WI.Sidebar.AbsoluteMinimumWidth, |
| maxWidth: 500, |
| initialWidth: this._nameColumnWidthSetting.value, |
| resizeType: WI.TableColumn.ResizeType.Locked, |
| }); |
| |
| this._domainColumn = new WI.TableColumn("domain", WI.UIString("Domain"), { |
| minWidth: 120, |
| maxWidth: 200, |
| initialWidth: 150, |
| }); |
| |
| this._typeColumn = new WI.TableColumn("type", WI.UIString("Type"), { |
| minWidth: 70, |
| maxWidth: 120, |
| initialWidth: 90, |
| }); |
| |
| this._mimeTypeColumn = new WI.TableColumn("mimeType", WI.UIString("MIME Type"), { |
| hidden: true, |
| minWidth: 100, |
| maxWidth: 150, |
| initialWidth: 120, |
| }); |
| |
| this._methodColumn = new WI.TableColumn("method", WI.UIString("Method"), { |
| hidden: true, |
| minWidth: 55, |
| maxWidth: 80, |
| initialWidth: 65, |
| }); |
| |
| this._schemeColumn = new WI.TableColumn("scheme", WI.UIString("Scheme"), { |
| hidden: true, |
| minWidth: 55, |
| maxWidth: 80, |
| initialWidth: 65, |
| }); |
| |
| this._statusColumn = new WI.TableColumn("status", WI.UIString("Status"), { |
| hidden: true, |
| minWidth: 50, |
| maxWidth: 50, |
| align: "left", |
| }); |
| |
| this._protocolColumn = new WI.TableColumn("protocol", WI.UIString("Protocol"), { |
| hidden: true, |
| minWidth: 65, |
| maxWidth: 80, |
| initialWidth: 75, |
| }); |
| |
| this._initiatorColumn = new WI.TableColumn("initiator", WI.UIString("Initiator"), { |
| hidden: true, |
| minWidth: 75, |
| maxWidth: 175, |
| initialWidth: 125, |
| }); |
| |
| this._priorityColumn = new WI.TableColumn("priority", WI.UIString("Priority"), { |
| hidden: true, |
| minWidth: 65, |
| maxWidth: 80, |
| initialWidth: 70, |
| }); |
| |
| this._remoteAddressColumn = new WI.TableColumn("remoteAddress", WI.UIString("IP Address"), { |
| hidden: true, |
| minWidth: 150, |
| }); |
| |
| this._connectionIdentifierColumn = new WI.TableColumn("connectionIdentifier", WI.UIString("Connection ID"), { |
| hidden: true, |
| minWidth: 50, |
| maxWidth: 120, |
| initialWidth: 80, |
| align: "right", |
| }); |
| |
| this._resourceSizeColumn = new WI.TableColumn("resourceSize", WI.UIString("Resource Size"), { |
| hidden: true, |
| minWidth: 80, |
| maxWidth: 100, |
| initialWidth: 80, |
| align: "right", |
| }); |
| |
| this._transferSizeColumn = new WI.TableColumn("transferSize", WI.UIString("Transfer Size", "Amount of data sent over the network for a single resource"), { |
| minWidth: 100, |
| maxWidth: 150, |
| initialWidth: 100, |
| align: "right", |
| }); |
| |
| this._timeColumn = new WI.TableColumn("time", WI.UIString("Time"), { |
| minWidth: 65, |
| maxWidth: 90, |
| initialWidth: 65, |
| align: "right", |
| }); |
| |
| this._waterfallColumn = new WI.TableColumn("waterfall", WI.UIString("Waterfall"), { |
| minWidth: 230, |
| headerView: this._waterfallTimelineRuler, |
| needsReloadOnResize: true, |
| }); |
| |
| this._nameColumn.addEventListener(WI.TableColumn.Event.WidthDidChange, this._tableNameColumnDidChangeWidth, this); |
| this._waterfallColumn.addEventListener(WI.TableColumn.Event.WidthDidChange, this._tableWaterfallColumnDidChangeWidth, this); |
| |
| this._table = new WI.Table("network-table", this, this, 20); |
| |
| this._table.addColumn(this._nameColumn); |
| this._table.addColumn(this._domainColumn); |
| this._table.addColumn(this._typeColumn); |
| this._table.addColumn(this._mimeTypeColumn); |
| this._table.addColumn(this._methodColumn); |
| this._table.addColumn(this._schemeColumn); |
| this._table.addColumn(this._statusColumn); |
| this._table.addColumn(this._protocolColumn); |
| this._table.addColumn(this._initiatorColumn); |
| this._table.addColumn(this._priorityColumn); |
| this._table.addColumn(this._remoteAddressColumn); |
| this._table.addColumn(this._connectionIdentifierColumn); |
| this._table.addColumn(this._resourceSizeColumn); |
| this._table.addColumn(this._transferSizeColumn); |
| this._table.addColumn(this._timeColumn); |
| this._table.addColumn(this._waterfallColumn); |
| |
| if (!this._table.sortColumnIdentifier) { |
| this._table.sortOrder = WI.Table.SortOrder.Ascending; |
| this._table.sortColumnIdentifier = "waterfall"; |
| } |
| |
| this.addSubview(this._table); |
| } |
| |
| layout() |
| { |
| this._updateWaterfallTimelineRuler(); |
| this._processPendingEntries(); |
| this._positionDetailView(); |
| this._positionEmptyFilterMessage(); |
| this._updateExportButton(); |
| } |
| |
| didLayoutSubtree() |
| { |
| super.didLayoutSubtree(); |
| |
| if (this._waterfallPopover) |
| this._waterfallPopover.resize(); |
| } |
| |
| processHAR(result) |
| { |
| let resources = WI.networkManager.processHAR(result); |
| if (!resources) |
| return; |
| |
| let importedCollection = this._addCollection(); |
| |
| let displayName = WI.UIString("Imported - %s").format(result.filename); |
| this._addCollectionPathComponent(importedCollection, displayName, "network-har-icon"); |
| |
| this._changeCollection(importedCollection); |
| |
| for (let resource of resources) |
| this._insertResourceAndReloadTable(resource); |
| } |
| |
| handleClearShortcut(event) |
| { |
| if (!this._isShowingMainCollection()) |
| return; |
| |
| this.reset(); |
| } |
| |
| // Private |
| |
| _updateWaterfallTimeRange(startTimestamp, endTimestamp) |
| { |
| let collection = this._activeCollection; |
| |
| if (isNaN(collection.waterfallStartTime) || startTimestamp < collection.waterfallStartTime) |
| collection.waterfallStartTime = startTimestamp; |
| |
| if (isNaN(collection.waterfallEndTime) || endTimestamp > collection.waterfallEndTime) |
| collection.waterfallEndTime = endTimestamp; |
| } |
| |
| _updateWaterfallTimelineRuler() |
| { |
| if (!this._waterfallTimelineRuler) |
| return; |
| |
| let collection = this._activeCollection; |
| |
| if (isNaN(collection.waterfallStartTime)) { |
| this._waterfallTimelineRuler.zeroTime = 0; |
| this._waterfallTimelineRuler.startTime = 0; |
| this._waterfallTimelineRuler.endTime = 0.250; |
| } else { |
| this._waterfallTimelineRuler.zeroTime = collection.waterfallStartTime; |
| this._waterfallTimelineRuler.startTime = collection.waterfallStartTime; |
| this._waterfallTimelineRuler.endTime = collection.waterfallEndTime; |
| |
| // Add a little bit of padding on the each side. |
| const paddingPixels = 5; |
| let padSeconds = paddingPixels * this._waterfallTimelineRuler.secondsPerPixel; |
| this._waterfallTimelineRuler.zeroTime = collection.waterfallStartTime - padSeconds; |
| this._waterfallTimelineRuler.startTime = collection.waterfallStartTime - padSeconds; |
| this._waterfallTimelineRuler.endTime = collection.waterfallEndTime + padSeconds; |
| } |
| } |
| |
| _canExportHAR() |
| { |
| if (!this._isShowingMainCollection()) |
| return false; |
| |
| let mainFrame = WI.networkManager.mainFrame; |
| if (!mainFrame) |
| return false; |
| |
| let mainResource = mainFrame.mainResource; |
| if (!mainResource) |
| return false; |
| |
| if (!mainResource.requestSentDate) |
| return false; |
| |
| if (!this._HARResources().length) |
| return false; |
| |
| return true; |
| } |
| |
| _updateExportButton() |
| { |
| this._harExportNavigationItem.enabled = this._canExportHAR(); |
| } |
| |
| _processPendingEntries() |
| { |
| let collection = this._activeCollection; |
| let needsSort = collection.pendingUpdates.length > 0; |
| |
| // No global sort is needed, so just insert new records into their sorted position. |
| if (!needsSort) { |
| let originalLength = collection.pendingInsertions.length; |
| for (let resource of collection.pendingInsertions) |
| this._insertResourceAndReloadTable(resource); |
| console.assert(collection.pendingInsertions.length === originalLength); |
| collection.pendingInsertions = []; |
| return; |
| } |
| |
| for (let resource of collection.pendingInsertions) { |
| let resourceEntry = this._entryForResource(resource); |
| this._tryLinkResourceToDOMNode(resourceEntry); |
| collection.entries.push(resourceEntry); |
| } |
| collection.pendingInsertions = []; |
| |
| for (let updateObject of collection.pendingUpdates) { |
| if (updateObject instanceof WI.Resource) |
| this._updateEntryForResource(updateObject); |
| } |
| collection.pendingUpdates = []; |
| |
| this._updateSort(); |
| this._updateFilteredEntries(); |
| this._reloadTable(); |
| } |
| |
| _populateWithInitialResourcesIfNeeded(collection) |
| { |
| if (!this._needsInitialPopulate) |
| return; |
| |
| this._needsInitialPopulate = false; |
| |
| let populateResourcesForFrame = (frame) => { |
| if (frame.provisionalMainResource) |
| collection.pendingInsertions.push(frame.provisionalMainResource); |
| else if (frame.mainResource) |
| collection.pendingInsertions.push(frame.mainResource); |
| |
| for (let resource of frame.resourceCollection) |
| collection.pendingInsertions.push(resource); |
| |
| for (let childFrame of frame.childFrameCollection) |
| populateResourcesForFrame(childFrame); |
| }; |
| |
| let populateResourcesForTarget = (target) => { |
| if (target.mainResource instanceof WI.Resource) |
| collection.pendingInsertions.push(target.mainResource); |
| for (let resource of target.resourceCollection) |
| collection.pendingInsertions.push(resource); |
| }; |
| |
| for (let target of WI.targets) { |
| if (target === WI.pageTarget) |
| populateResourcesForFrame(WI.networkManager.mainFrame); |
| else |
| populateResourcesForTarget(target); |
| } |
| |
| this.needsLayout(); |
| } |
| |
| _checkURLFilterAgainstResource(resource) |
| { |
| if (this._urlFilterSearchRegex.test(resource.url)) { |
| this._activeURLFilterResources.add(resource); |
| return; |
| } |
| |
| for (let redirect of resource.redirects) { |
| if (this._urlFilterSearchRegex.test(redirect.url)) { |
| this._activeURLFilterResources.add(resource); |
| return; |
| } |
| } |
| } |
| |
| _rowIndexForRepresentedObject(object) |
| { |
| return this._activeCollection.filteredEntries.findIndex((x) => { |
| if (x.resource === object) |
| return true; |
| if (x.domNode === object) |
| return true; |
| return false; |
| }); |
| } |
| |
| _updateEntryForResource(resource) |
| { |
| let collection = this._activeCollection; |
| |
| let index = collection.entries.findIndex((x) => x.resource === resource); |
| if (index === -1) |
| return; |
| |
| // Don't wipe out the previous entry, as it may be used by a node entry. |
| function updateExistingEntry(existingEntry, newEntry) { |
| for (let key in newEntry) |
| existingEntry[key] = newEntry[key]; |
| } |
| |
| let entry = this._entryForResource(resource); |
| updateExistingEntry(collection.entries[index], entry); |
| |
| let rowIndex = this._rowIndexForRepresentedObject(resource); |
| if (rowIndex === -1) |
| return; |
| |
| updateExistingEntry(collection.filteredEntries[rowIndex], entry); |
| } |
| |
| _hidePopover() |
| { |
| if (this._waterfallPopover) |
| this._waterfallPopover.dismiss(); |
| } |
| |
| _hideDetailView() |
| { |
| if (!this._detailView) |
| return; |
| |
| this.element.classList.remove("showing-detail"); |
| this._table.scrollContainer.style.removeProperty("width"); |
| |
| this.removeSubview(this._detailView); |
| |
| this._detailView.hidden(); |
| this._detailView = null; |
| |
| this._table.updateLayout(WI.View.LayoutReason.Resize); |
| this._table.reloadVisibleColumnCells(this._waterfallColumn); |
| } |
| |
| _showDetailView(object) |
| { |
| let oldDetailView = this._detailView; |
| |
| this._detailView = this._detailViewMap.get(object); |
| if (this._detailView === oldDetailView) |
| return; |
| |
| if (!this._detailView) { |
| if (object instanceof WI.Resource) |
| this._detailView = new WI.NetworkResourceDetailView(object, this); |
| else if (object instanceof WI.DOMNode) { |
| this._detailView = new WI.NetworkDOMNodeDetailView(object, this); |
| } |
| |
| this._detailViewMap.set(object, this._detailView); |
| } |
| |
| if (oldDetailView) { |
| oldDetailView.hidden(); |
| this.replaceSubview(oldDetailView, this._detailView); |
| } else |
| this.addSubview(this._detailView); |
| |
| if (this._showingRepresentedObjectCookie) |
| this._detailView.willShowWithCookie(this._showingRepresentedObjectCookie); |
| |
| this._detailView.shown(); |
| |
| this.element.classList.add("showing-detail"); |
| this._table.scrollContainer.style.width = this._nameColumn.width + "px"; |
| |
| // FIXME: It would be nice to avoid this. |
| // Currently the ResourceDetailView is in the heirarchy but has not yet done a layout so we |
| // end up seeing the table behind it. This forces us to layout now instead of after a beat. |
| this.updateLayout(); |
| } |
| |
| _positionDetailView() |
| { |
| if (!this._detailView) |
| return; |
| |
| let side = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL ? "right" : "left"; |
| this._detailView.element.style[side] = this._nameColumn.width + "px"; |
| this._table.scrollContainer.style.width = this._nameColumn.width + "px"; |
| } |
| |
| _updateEmptyFilterResultsMessage() |
| { |
| if (this._hasActiveFilter() && !this._activeCollection.filteredEntries.length) |
| this._showEmptyFilterResultsMessage(); |
| else |
| this._hideEmptyFilterResultsMessage(); |
| } |
| |
| _showEmptyFilterResultsMessage() |
| { |
| if (!this._emptyFilterResultsMessageElement) { |
| let buttonElement = document.createElement("button"); |
| buttonElement.textContent = WI.UIString("Clear Filters"); |
| buttonElement.addEventListener("click", () => { this._resetFilters(); }); |
| |
| this._emptyFilterResultsMessageElement = WI.createMessageTextView(WI.UIString("No Filter Results")); |
| this._emptyFilterResultsMessageElement.appendChild(buttonElement); |
| } |
| |
| this.element.appendChild(this._emptyFilterResultsMessageElement); |
| this._positionEmptyFilterMessage(); |
| } |
| |
| _hideEmptyFilterResultsMessage() |
| { |
| if (!this._emptyFilterResultsMessageElement) |
| return; |
| |
| this._emptyFilterResultsMessageElement.remove(); |
| } |
| |
| _positionEmptyFilterMessage() |
| { |
| if (!this._emptyFilterResultsMessageElement) |
| return; |
| |
| let width = this._nameColumn.width - 1; // For the 1px border. |
| this._emptyFilterResultsMessageElement.style.width = width + "px"; |
| } |
| |
| _clearNetworkOnNavigateSettingChanged() |
| { |
| this._clearOnLoadNavigationItem.checked = !WI.settings.clearNetworkOnNavigate.value; |
| } |
| |
| _resourceCachingDisabledSettingChanged() |
| { |
| this._disableResourceCacheNavigationItem.activated = WI.settings.resourceCachingDisabled.value; |
| } |
| |
| _toggleDisableResourceCache() |
| { |
| WI.settings.resourceCachingDisabled.value = !WI.settings.resourceCachingDisabled.value; |
| } |
| |
| _mainResourceDidChange(event) |
| { |
| this._runForMainCollection((collection, wasMain) => { |
| let frame = event.target; |
| if (frame.isMainFrame() && WI.settings.clearNetworkOnNavigate.value) { |
| this._resetCollection(collection); |
| |
| if (wasMain && !this._needsInitialPopulate) |
| this._hideDetailView(); |
| } |
| |
| if (this._transitioningPageTarget) { |
| this._transitioningPageTarget = false; |
| this._needsInitialPopulate = true; |
| this._populateWithInitialResourcesIfNeeded(collection); |
| return; |
| } |
| |
| this._insertResourceAndReloadTable(frame.mainResource); |
| }); |
| } |
| |
| _mainFrameDidChange() |
| { |
| this._runForMainCollection((collection) => { |
| this._populateWithInitialResourcesIfNeeded(collection); |
| }); |
| } |
| |
| _resourceLoadingDidFinish(event) |
| { |
| this._runForMainCollection((collection, wasMain) => { |
| let resource = event.target; |
| collection.pendingUpdates.push(resource); |
| |
| this._updateWaterfallTimeRange(resource.firstTimestamp, resource.timingData.responseEnd); |
| |
| if (this._hasURLFilter()) |
| this._checkURLFilterAgainstResource(resource); |
| |
| if (wasMain) |
| this.needsLayout(); |
| }); |
| } |
| |
| _resourceLoadingDidFail(event) |
| { |
| this._runForMainCollection((collection, wasMain) => { |
| let resource = event.target; |
| collection.pendingUpdates.push(resource); |
| |
| this._updateWaterfallTimeRange(resource.firstTimestamp, resource.timingData.responseEnd); |
| |
| if (this._hasURLFilter()) |
| this._checkURLFilterAgainstResource(resource); |
| |
| if (wasMain) |
| this.needsLayout(); |
| }); |
| } |
| |
| _resourceTransferSizeDidChange(event) |
| { |
| if (!this._table) |
| return; |
| |
| this._runForMainCollection((collection, wasMain) => { |
| let resource = event.target; |
| |
| // In the unlikely event that this is the sort column, we may need to resort. |
| if (this._table.sortColumnIdentifier === "transferSize") { |
| collection.pendingUpdates.push(resource); |
| this.needsLayout(); |
| return; |
| } |
| |
| let index = collection.entries.findIndex((x) => x.resource === resource); |
| if (index === -1) |
| return; |
| |
| let entry = collection.entries[index]; |
| entry.transferSize = !isNaN(resource.networkTotalTransferSize) ? resource.networkTotalTransferSize : resource.estimatedTotalTransferSize; |
| |
| if (!wasMain) |
| return; |
| |
| let rowIndex = this._rowIndexForRepresentedObject(resource); |
| if (rowIndex === -1) |
| return; |
| |
| this._table.reloadCell(rowIndex, "transferSize"); |
| }); |
| } |
| |
| _handleResourceAdded(event) |
| { |
| this._runForMainCollection((collection) => { |
| this._insertResourceAndReloadTable(event.data.resource); |
| }); |
| } |
| |
| _handleFrameWasAdded(event) |
| { |
| if (this._needsInitialPopulate) |
| return; |
| |
| this._runForMainCollection((collection) => { |
| let frame = event.data.childFrame; |
| let mainResource = frame.provisionalMainResource || frame.mainResource; |
| console.assert(mainResource, "Frame should have a main resource."); |
| this._insertResourceAndReloadTable(mainResource); |
| |
| console.assert(!frame.resourceCollection.size, "New frame should be empty."); |
| console.assert(!frame.childFrameCollection.size, "New frame should be empty."); |
| }); |
| } |
| |
| _runForMainCollection(callback) |
| { |
| let currentCollection = this._activeCollection; |
| let wasMain = currentCollection === this._mainCollection; |
| |
| if (!wasMain) |
| this._setActiveCollection(this._mainCollection); |
| |
| callback(this._activeCollection, wasMain); |
| |
| if (!wasMain) |
| this._setActiveCollection(currentCollection); |
| } |
| |
| _isShowingMainCollection() |
| { |
| return this._activeCollection === this._mainCollection; |
| } |
| |
| _isDefaultSort() |
| { |
| return this._table.sortColumnIdentifier === "waterfall" && this._table.sortOrder === WI.Table.SortOrder.Ascending; |
| } |
| |
| _insertResourceAndReloadTable(resource) |
| { |
| if (this._needsInitialPopulate) |
| return; |
| |
| let collection = this._activeCollection; |
| |
| this._updateWaterfallTimeRange(resource.firstTimestamp, resource.timingData.responseEnd); |
| |
| if (!this._table || !(WI.tabBrowser.selectedTabContentView instanceof WI.NetworkTabContentView)) { |
| collection.pendingInsertions.push(resource); |
| this.needsLayout(); |
| return; |
| } |
| |
| let resourceEntry = this._entryForResource(resource); |
| |
| this._tryLinkResourceToDOMNode(resourceEntry); |
| |
| if (WI.settings.groupMediaRequestsByDOMNode.value && resource.initiatorNode) { |
| if (!this._entriesSortComparator) |
| this._generateSortComparator(); |
| } else if (this._isDefaultSort() || !this._entriesSortComparator) { |
| // Default sort has fast path. |
| collection.entries.push(resourceEntry); |
| if (this._passFilter(resourceEntry)) { |
| collection.filteredEntries.push(resourceEntry); |
| this._table.reloadDataAddedToEndOnly(); |
| } |
| return; |
| } |
| |
| insertObjectIntoSortedArray(resourceEntry, collection.entries, this._entriesSortComparator); |
| |
| if (this._passFilter(resourceEntry)) { |
| if (WI.settings.groupMediaRequestsByDOMNode.value) |
| this._updateFilteredEntries(); |
| else |
| insertObjectIntoSortedArray(resourceEntry, collection.filteredEntries, this._entriesSortComparator); |
| |
| // Probably a useless optimization here, but if we only added this row to the end |
| // we may avoid recreating all visible rows by saying as such. |
| if (collection.filteredEntries.lastValue === resourceEntry) |
| this._table.reloadDataAddedToEndOnly(); |
| else |
| this._reloadTable(); |
| } |
| } |
| |
| _entryForResource(resource) |
| { |
| // FIXME: <https://webkit.org/b/143632> Web Inspector: Resources with the same name in different folders aren't distinguished |
| // FIXME: <https://webkit.org/b/176765> Web Inspector: Resource names should be less ambiguous |
| |
| return { |
| resource, |
| name: WI.displayNameForURL(resource.url, resource.urlComponents), |
| domain: WI.displayNameForHost(resource.urlComponents.host), |
| scheme: resource.urlComponents.scheme ? resource.urlComponents.scheme.toLowerCase() : "", |
| method: resource.requestMethod, |
| type: resource.type, |
| displayType: WI.NetworkTableContentView.displayNameForResource(resource), |
| mimeType: resource.mimeType, |
| status: resource.statusCode, |
| cached: resource.cached, |
| resourceSize: resource.size, |
| transferSize: !isNaN(resource.networkTotalTransferSize) ? resource.networkTotalTransferSize : resource.estimatedTotalTransferSize, |
| time: resource.totalDuration, |
| protocol: resource.protocol, |
| initiator: resource.initiatorSourceCodeLocation ? resource.initiatorSourceCodeLocation.displayLocationString() : "", |
| priority: resource.priority, |
| remoteAddress: resource.remoteAddress, |
| connectionIdentifier: resource.connectionIdentifier, |
| startTime: resource.firstTimestamp, |
| }; |
| } |
| |
| _entryForDOMNode(domNode) |
| { |
| return { |
| domNode, |
| initiatedResourceEntries: [], |
| domEventElements: new Map, |
| expanded: true, |
| }; |
| } |
| |
| _tryLinkResourceToDOMNode(resourceEntry) |
| { |
| let resource = resourceEntry.resource; |
| if (!resource || !resource.initiatorNode) |
| return; |
| |
| let nodeEntry = this._domNodeEntries.get(resource.initiatorNode); |
| if (!nodeEntry) { |
| nodeEntry = this._entryForDOMNode(resource.initiatorNode, Object.keys(resourceEntry)); |
| this._domNodeEntries.set(resource.initiatorNode, nodeEntry); |
| |
| resource.initiatorNode.addEventListener(WI.DOMNode.Event.DidFireEvent, this._handleNodeDidFireEvent, this); |
| if (resource.initiatorNode.canEnterPowerEfficientPlaybackState()) |
| resource.initiatorNode.addEventListener(WI.DOMNode.Event.PowerEfficientPlaybackStateChanged, this._handleDOMNodePowerEfficientPlaybackStateChanged, this); |
| } |
| |
| if (!this._entriesSortComparator) |
| this._generateSortComparator(); |
| |
| insertObjectIntoSortedArray(resourceEntry, nodeEntry.initiatedResourceEntries, this._entriesSortComparator); |
| } |
| |
| _uniqueValuesForDOMNodeEntry(nodeEntry, accessor) |
| { |
| let resourceEntries = nodeEntry.initiatedResourceEntries; |
| if (!resourceEntries) |
| return null; |
| |
| return resourceEntries.reduce((accumulator, current) => { |
| let value = accessor(current); |
| if (value || typeof value === "number") |
| accumulator.add(value); |
| return accumulator; |
| }, new Set); |
| } |
| |
| _handleNodeDidFireEvent(event) |
| { |
| this._runForMainCollection((collection, wasMain) => { |
| let domNode = event.target; |
| let {domEvent} = event.data; |
| |
| collection.pendingUpdates.push(domNode); |
| |
| this._updateWaterfallTimeRange(NaN, domEvent.timestamp + (this._waterfallTimelineRuler.secondsPerPixel * 10)); |
| |
| if (wasMain) |
| this.needsLayout(); |
| }); |
| } |
| |
| _handleDOMNodePowerEfficientPlaybackStateChanged(event) |
| { |
| this._runForMainCollection((collection, wasMain) => { |
| let domNode = event.target; |
| let {timestamp} = event.data; |
| |
| collection.pendingUpdates.push(domNode); |
| |
| this._updateWaterfallTimeRange(NaN, timestamp + (this._waterfallTimelineRuler.secondsPerPixel * 10)); |
| |
| if (wasMain) |
| this.needsLayout(); |
| }); |
| } |
| |
| _hasTypeFilter() |
| { |
| return !!this._activeTypeFilters; |
| } |
| |
| _hasURLFilter() |
| { |
| return this._urlFilterIsActive; |
| } |
| |
| _hasActiveFilter() |
| { |
| return this._hasTypeFilter() |
| || this._hasURLFilter(); |
| } |
| |
| _passTypeFilter(entry) |
| { |
| if (!this._hasTypeFilter()) |
| return true; |
| return this._activeTypeFilters.some((checker) => checker(entry.resource.type)); |
| } |
| |
| _passURLFilter(entry) |
| { |
| if (!this._hasURLFilter()) |
| return true; |
| return this._activeURLFilterResources.has(entry.resource); |
| } |
| |
| _passFilter(entry) |
| { |
| return this._passTypeFilter(entry) |
| && this._passURLFilter(entry); |
| } |
| |
| _updateSort() |
| { |
| if (this._entriesSortComparator) { |
| let collection = this._activeCollection; |
| collection.entries = collection.entries.sort(this._entriesSortComparator); |
| } |
| } |
| |
| _updateFilteredEntries() |
| { |
| let collection = this._activeCollection; |
| |
| if (this._hasActiveFilter()) |
| collection.filteredEntries = collection.entries.filter(this._passFilter, this); |
| else |
| collection.filteredEntries = collection.entries.slice(); |
| |
| if (WI.settings.groupMediaRequestsByDOMNode.value) { |
| for (let nodeEntry of this._domNodeEntries.values()) { |
| if (nodeEntry.initiatedResourceEntries.length < 2 && !nodeEntry.domNode.domEvents.length) |
| continue; |
| |
| let firstIndex = Infinity; |
| for (let resourceEntry of nodeEntry.initiatedResourceEntries) { |
| if (this._hasActiveFilter() && !this._passFilter(resourceEntry)) |
| continue; |
| |
| let index = collection.filteredEntries.indexOf(resourceEntry); |
| if (index >= 0 && index < firstIndex) |
| firstIndex = index; |
| } |
| |
| if (!isFinite(firstIndex)) |
| continue; |
| |
| collection.filteredEntries.insertAtIndex(nodeEntry, firstIndex); |
| } |
| |
| collection.filteredEntries = collection.filteredEntries.filter((entry) => { |
| if (entry.resource && entry.resource.initiatorNode) { |
| let nodeEntry = this._domNodeEntries.get(entry.resource.initiatorNode); |
| if (!nodeEntry.expanded) |
| return false; |
| } |
| return true; |
| }); |
| } |
| |
| this._updateEmptyFilterResultsMessage(); |
| } |
| |
| _reloadTable() |
| { |
| this._table.reloadData(); |
| this._restoreSelectedRow(); |
| } |
| |
| _generateTypeFilter() |
| { |
| let selectedItems = this._typeFilterScopeBar.selectedItems; |
| if (!selectedItems.length || selectedItems.includes(this._typeFilterScopeBarItemAll)) |
| return null; |
| |
| return selectedItems.map((item) => item.__checker); |
| } |
| |
| _resetFilters() |
| { |
| console.assert(this._hasActiveFilter()); |
| |
| // Clear url filter. |
| this._urlFilterSearchText = null; |
| this._urlFilterSearchRegex = null; |
| this._urlFilterIsActive = false; |
| this._activeURLFilterResources.clear(); |
| this._urlFilterNavigationItem.filterBar.clear(); |
| console.assert(!this._hasURLFilter()); |
| |
| // Clear type filter. |
| this._typeFilterScopeBar.resetToDefault(); |
| console.assert(!this._hasTypeFilter()); |
| |
| console.assert(!this._hasActiveFilter()); |
| |
| this._updateFilteredEntries(); |
| this._reloadTable(); |
| } |
| |
| _areFilterListsIdentical(listA, listB) |
| { |
| if (listA && listB) { |
| if (listA.length !== listB.length) |
| return false; |
| |
| for (let i = 0; i < listA.length; ++i) { |
| if (listA[i] !== listB[i]) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| return false; |
| } |
| |
| _typeFilterScopeBarSelectionChanged(event) |
| { |
| // FIXME: <https://webkit.org/b/176763> Web Inspector: ScopeBar SelectionChanged event may dispatch multiple times for a single logical change |
| // We can't use shallow equals here because the contents are functions. |
| let oldFilter = this._activeTypeFilters; |
| let newFilter = this._generateTypeFilter(); |
| if (this._areFilterListsIdentical(oldFilter, newFilter)) |
| return; |
| |
| // Even if the selected resource would still be visible, lets close the detail view if a filter changes. |
| this._hideDetailView(); |
| |
| this._activeTypeFilters = newFilter; |
| this._updateFilteredEntries(); |
| this._reloadTable(); |
| } |
| |
| _handleGroupMediaRequestsByDOMNodeCheckedDidChange(event) |
| { |
| WI.settings.groupMediaRequestsByDOMNode.value = this._groupMediaRequestsByDOMNodeNavigationItem.checked; |
| |
| if (!WI.settings.groupMediaRequestsByDOMNode.value) { |
| this._table.element.classList.remove("grouped"); |
| |
| if (this._selectedObject && this._selectedObject instanceof WI.DOMNode) { |
| this._selectedObject = null; |
| this._hideDetailView(); |
| } |
| } |
| |
| this._updateSort(); |
| this._updateFilteredEntries(); |
| this._reloadTable(); |
| } |
| |
| _urlFilterDidChange(event) |
| { |
| let searchQuery = this._urlFilterNavigationItem.filterBar.filters.text; |
| if (searchQuery === this._urlFilterSearchText) |
| return; |
| |
| // Even if the selected resource would still be visible, lets close the detail view if a filter changes. |
| this._hideDetailView(); |
| |
| // Search cleared. |
| if (!searchQuery) { |
| this._urlFilterSearchText = null; |
| this._urlFilterSearchRegex = null; |
| this._urlFilterIsActive = false; |
| this._activeURLFilterResources.clear(); |
| |
| this._updateFilteredEntries(); |
| this._reloadTable(); |
| return; |
| } |
| |
| this._urlFilterIsActive = true; |
| this._urlFilterSearchText = searchQuery; |
| this._urlFilterSearchRegex = WI.SearchUtilities.regExpForString(searchQuery, WI.SearchUtilities.defaultSettings); |
| |
| this._updateActiveFilterResources(); |
| this._updateFilteredEntries(); |
| this._reloadTable(); |
| } |
| |
| _updateActiveFilterResources() |
| { |
| this._activeURLFilterResources.clear(); |
| |
| if (this._hasURLFilter()) { |
| for (let entry of this._activeCollection.entries) |
| this._checkURLFilterAgainstResource(entry.resource); |
| } |
| } |
| |
| _restoreSelectedRow() |
| { |
| if (!this._selectedObject) |
| return; |
| |
| let rowIndex = this._rowIndexForRepresentedObject(this._selectedObject); |
| if (rowIndex === -1) { |
| this._selectedObject = null; |
| this._table.deselectAll(); |
| return; |
| } |
| |
| this._table.selectRow(rowIndex); |
| this._showDetailView(this._selectedObject); |
| } |
| |
| _HARResources() |
| { |
| let resources = this._activeCollection.filteredEntries.map((x) => x.resource); |
| const supportedHARSchemes = new Set(["http", "https", "ws", "wss"]); |
| return resources.filter((resource) => { |
| if (!resource) { |
| // DOM node entries are also added to `filteredEntries`. |
| return false; |
| } |
| |
| if (!resource.finished) |
| return false; |
| if (!resource.requestSentDate) |
| return false; |
| if (!supportedHARSchemes.has(resource.urlComponents.scheme)) |
| return false; |
| return true; |
| }); |
| } |
| |
| _exportHAR() |
| { |
| let resources = this._HARResources(); |
| if (!resources.length) { |
| InspectorFrontendHost.beep(); |
| return; |
| } |
| |
| WI.HARBuilder.buildArchive(resources).then((har) => { |
| let mainFrame = WI.networkManager.mainFrame; |
| let archiveName = mainFrame.mainResource.urlComponents.host || mainFrame.mainResource.displayName || "Archive"; |
| WI.FileUtilities.save({ |
| url: WI.FileUtilities.inspectorURLForFilename(archiveName + ".har"), |
| content: JSON.stringify(har, null, 2), |
| forceSaveAs: true, |
| }); |
| }); |
| } |
| |
| _importHAR() |
| { |
| WI.FileUtilities.importJSON((result) => this.processHAR(result), {multiple: true}); |
| } |
| |
| _waterfallPopoverContent() |
| { |
| let contentElement = document.createElement("div"); |
| contentElement.classList.add("waterfall-popover-content"); |
| return contentElement; |
| } |
| |
| _waterfallPopoverContentForResourceEntry(resourceEntry) |
| { |
| let contentElement = this._waterfallPopoverContent(); |
| |
| let resource = resourceEntry.resource; |
| if (!resource.hasResponse() || !resource.firstTimestamp || !resource.lastTimestamp) { |
| contentElement.textContent = WI.UIString("Resource has no timing data"); |
| return contentElement; |
| } |
| |
| let breakdownView = new WI.ResourceTimingBreakdownView(resource, 300); |
| contentElement.appendChild(breakdownView.element); |
| breakdownView.updateLayout(); |
| |
| return contentElement; |
| } |
| |
| _waterfallPopoverContentForNodeEntry(nodeEntry, domEvents) |
| { |
| let contentElement = this._waterfallPopoverContent(); |
| |
| let breakdownView = new WI.DOMEventsBreakdownView(domEvents); |
| contentElement.appendChild(breakdownView.element); |
| breakdownView.updateLayout(); |
| |
| return contentElement; |
| } |
| |
| _handleResourceEntryMousedownWaterfall(resourceEntry) |
| { |
| let popoverContentElement = this._waterfallPopoverContentForResourceEntry(resourceEntry); |
| this._handleMousedownWaterfall(resourceEntry, popoverContentElement, (cell) => { |
| return cell.querySelector(".block.mouse-tracking"); |
| }); |
| } |
| |
| _handleNodeEntryMousedownWaterfall(nodeEntry, domEvents) |
| { |
| let popoverContentElement = this._waterfallPopoverContentForNodeEntry(nodeEntry, domEvents); |
| this._handleMousedownWaterfall(nodeEntry, popoverContentElement, (cell) => { |
| let domEventElement = nodeEntry.domEventElements.get(domEvents[0]); |
| |
| // Show any additional DOM events that have been merged into the range. |
| if (domEventElement && this._waterfallPopover.visible) { |
| let newDOMEvents = Array.from(nodeEntry.domEventElements) |
| .filter(([domEvent, element]) => element === domEventElement) |
| .map(([domEvent, element]) => domEvent); |
| |
| this._waterfallPopover.content = this._waterfallPopoverContentForNodeEntry(nodeEntry, newDOMEvents); |
| } |
| |
| return domEventElement; |
| }); |
| } |
| |
| _handleMousedownWaterfall(entry, popoverContentElement, updateTargetAndContentFunction) |
| { |
| if (!this._waterfallPopover) { |
| this._waterfallPopover = new WI.Popover; |
| this._waterfallPopover.element.classList.add("waterfall-popover"); |
| } |
| |
| if (this._waterfallPopover.visible) |
| return; |
| |
| let calculateTargetFrame = () => { |
| let rowIndex = this._rowIndexForRepresentedObject(entry.resource || entry.domNode); |
| let cell = this._table.cellForRowAndColumn(rowIndex, this._waterfallColumn); |
| if (cell) { |
| let targetElement = updateTargetAndContentFunction(cell); |
| if (targetElement) |
| return WI.Rect.rectFromClientRect(targetElement.getBoundingClientRect()); |
| } |
| |
| this._waterfallPopover.dismiss(); |
| return null; |
| }; |
| |
| let targetFrame = calculateTargetFrame(); |
| if (!targetFrame) |
| return; |
| if (!targetFrame.size.width && !targetFrame.size.height) |
| return; |
| |
| let isRTL = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL; |
| 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]; |
| this._waterfallPopover.windowResizeHandler = () => { |
| let bounds = calculateTargetFrame(); |
| if (bounds) |
| this._waterfallPopover.present(bounds, preferredEdges); |
| }; |
| |
| this._waterfallPopover.presentNewContentWithFrame(popoverContentElement, targetFrame, preferredEdges); |
| } |
| |
| _tableNameColumnDidChangeWidth(event) |
| { |
| this._nameColumnWidthSetting.value = event.target.width; |
| |
| this._positionDetailView(); |
| this._positionEmptyFilterMessage(); |
| } |
| |
| _tableWaterfallColumnDidChangeWidth(event) |
| { |
| this._table.reloadVisibleColumnCells(this._waterfallColumn); |
| } |
| |
| _transitionPageTarget(event) |
| { |
| this._transitioningPageTarget = true; |
| } |
| }; |