blob: ec52b28ea60a9730af4774f5c05f76f91e833592 [file] [log] [blame]
/*
* 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);
this._entries = [];
this._entriesSortComparator = null;
this._filteredEntries = [];
this._pendingInsertions = [];
this._pendingUpdates = [];
this._pendingFilter = false;
this._table = null;
this._nameColumnWidthSetting = new WI.Setting("network-table-content-view-name-column-width", 250);
this._selectedResource = null;
this._resourceDetailView = null;
this._resourceDetailViewMap = new Map;
this._waterfallStartTime = NaN;
this._waterfallEndTime = NaN;
this._waterfallTimelineRuler = null;
this._waterfallPopover = null;
// FIXME: Network Timeline.
// FIXME: Throttling.
const exclusive = true;
this._typeFilterScopeBarItemAll = new WI.ScopeBarItem("network-type-filter-all", WI.UIString("All"), exclusive);
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);
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("perserve-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._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._checkboxsNavigationItemGroup = new WI.GroupNavigationItem([this._clearOnLoadNavigationItem, new WI.DividerNavigationItem]);
this._checkboxsNavigationItemGroup.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
this._buttonsNavigationItemGroup = new WI.GroupNavigationItem([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.resourceCachingDisabledSetting.value;
this._disableResourceCacheNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._toggleDisableResourceCache, this);
WI.resourceCachingDisabledSetting.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.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, 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.frameResourceManager.addEventListener(WI.FrameResourceManager.Event.MainFrameDidChange, this._mainFrameDidChange, this);
WI.timelineManager.persistentNetworkTimeline.addEventListener(WI.Timeline.Event.RecordAdded, this._networkTimelineRecordAdded, this);
this._needsInitialPopulate = true;
}
// Static
static displayNameForResource(resource)
{
if (resource.type === WI.Resource.Type.Image || resource.type === WI.Resource.Type.Font) {
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 "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 "JS";
case WI.Resource.Type.XHR:
return "XHR";
case WI.Resource.Type.Fetch:
return WI.UIString("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;
}
}
// Public
get selectionPathComponents()
{
return null;
}
get navigationItems()
{
let items = [this._checkboxsNavigationItemGroup, this._buttonsNavigationItemGroup];
if (this._disableResourceCacheNavigationItem)
items.push(this._disableResourceCacheNavigationItem);
items.push(this._clearNetworkItemsNavigationItem);
return items;
}
get filterNavigationItems()
{
return [this._urlFilterNavigationItem, this._typeFilterScopeBar];
}
get supportsSave()
{
return this._filteredEntries.some((entry) => entry.resource.finished);
}
get saveData()
{
return {customSaveHandler: () => { this._exportHAR(); }};
}
shown()
{
super.shown();
if (this._resourceDetailView)
this._resourceDetailView.shown();
if (this._table)
this._table.restoreScrollPosition();
}
hidden()
{
this._hidePopover();
if (this._resourceDetailView)
this._resourceDetailView.hidden();
super.hidden();
}
closed()
{
for (let detailView of this._resourceDetailViewMap.values())
detailView.dispose();
this._resourceDetailViewMap.clear();
this._hidePopover();
this._hideResourceDetailView();
WI.Frame.removeEventListener(null, null, this);
WI.Resource.removeEventListener(null, null, this);
WI.resourceCachingDisabledSetting.removeEventListener(null, null, this);
WI.settings.clearNetworkOnNavigate.removeEventListener(null, null, this);
WI.frameResourceManager.removeEventListener(WI.FrameResourceManager.Event.MainFrameDidChange, this._mainFrameDidChange, this);
WI.timelineManager.persistentNetworkTimeline.removeEventListener(WI.Timeline.Event.RecordAdded, this._networkTimelineRecordAdded, this);
super.closed();
}
reset()
{
this._entries = [];
this._filteredEntries = [];
this._pendingInsertions = [];
for (let detailView of this._resourceDetailViewMap.values())
detailView.dispose();
this._resourceDetailViewMap.clear();
this._waterfallStartTime = NaN;
this._waterfallEndTime = NaN;
this._updateWaterfallTimelineRuler();
this._updateExportButton();
if (this._table) {
this._selectedResource = null;
this._table.clearSelectedRow();
this._table.reloadData();
this._hidePopover();
this._hideResourceDetailView();
}
}
showRepresentedObject(representedObject, cookie)
{
console.assert(representedObject instanceof WI.Resource);
let rowIndex = this._rowIndexForResource(representedObject);
if (rowIndex === -1) {
this._selectedResource = null;
this._table.clearSelectedRow();
this._hideResourceDetailView();
return;
}
this._table.selectRow(rowIndex);
}
// NetworkResourceDetailView delegate
networkResourceDetailViewClose(resourceDetailView)
{
this._selectedResource = null;
this._table.clearSelectedRow();
this._hideResourceDetailView();
}
// Table dataSource
tableNumberOfRows(table)
{
return this._filteredEntries.length;
}
tableSortChanged(table)
{
this._generateSortComparator();
if (!this._entriesSortComparator)
return;
this._hideResourceDetailView();
this._entries = this._entries.sort(this._entriesSortComparator);
this._updateFilteredEntries();
this._table.reloadData();
}
// Table delegate
tableCellMouseDown(table, cell, column, rowIndex, event)
{
if (column !== this._nameColumn)
return;
this._table.selectRow(rowIndex);
}
tableCellContextMenuClicked(table, cell, column, rowIndex, event)
{
if (column !== this._nameColumn)
return;
this._table.selectRow(rowIndex);
let entry = this._filteredEntries[rowIndex];
let contextMenu = WI.ContextMenu.createFromEvent(event);
WI.appendContextMenuItemsForSourceCode(contextMenu, entry.resource);
contextMenu.appendSeparator();
contextMenu.appendItem(WI.UIString("Export HAR"), () => { this._exportHAR(); });
}
tableSelectedRowChanged(table, rowIndex)
{
if (isNaN(rowIndex)) {
this._selectedResource = null;
this._hideResourceDetailView();
return;
}
let entry = this._filteredEntries[rowIndex];
if (entry.resource === this._selectedResource)
return;
this._selectedResource = entry.resource;
this._showResourceDetailView(this._selectedResource);
}
tablePopulateCell(table, cell, column, rowIndex)
{
let entry = this._filteredEntries[rowIndex];
cell.classList.toggle("error", entry.resource.hadLoadingError());
switch (column.identifier) {
case "name":
this._populateNameCell(cell, entry);
break;
case "domain":
this._populateDomainCell(cell, entry);
break;
case "type":
cell.textContent = entry.displayType || emDash;
break;
case "mimeType":
cell.textContent = entry.mimeType || emDash;
break;
case "method":
cell.textContent = entry.method || emDash;
break;
case "scheme":
cell.textContent = entry.scheme || emDash;
break;
case "status":
cell.textContent = entry.status || emDash;
break;
case "protocol":
cell.textContent = entry.protocol || emDash;
break;
case "priority":
cell.textContent = WI.Resource.displayNameForPriority(entry.priority) || emDash;
break;
case "remoteAddress":
cell.textContent = entry.remoteAddress || emDash;
break;
case "connectionIdentifier":
cell.textContent = entry.connectionIdentifier || emDash;
break;
case "resourceSize":
cell.textContent = isNaN(entry.resourceSize) ? emDash : Number.bytesToString(entry.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)
cell.textContent = isNaN(entry.time) ? emDash : Number.secondsToString(Math.max(entry.time, 0));
break;
case "waterfall":
this._populateWaterfallGraph(cell, entry);
break;
}
return cell;
}
// Private
_populateNameCell(cell, entry)
{
console.assert(!cell.firstChild, "We expect the cell to be empty.", cell, cell.firstChild);
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);
}
let iconElement = cell.appendChild(document.createElement("img"));
iconElement.className = "icon";
cell.classList.add(WI.ResourceTreeElement.ResourceIconStyleClassName, entry.resource.type);
let nameElement = cell.appendChild(document.createElement("span"));
nameElement.textContent = entry.name;
}
_populateDomainCell(cell, entry)
{
console.assert(!cell.firstChild, "We expect the cell to be empty.", cell, cell.firstChild);
if (!entry.domain) {
cell.textContent = emDash;
return;
}
let secure = entry.scheme === "https" || entry.scheme === "wss";
if (secure) {
let lockIconElement = cell.appendChild(document.createElement("img"));
lockIconElement.className = "lock";
}
cell.append(entry.domain);
}
_populateTransferSizeCell(cell, entry)
{
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;
}
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 resource = entry.resource;
if (!resource.hasResponse()) {
cell.textContent = zeroWidthSpace;
return;
}
let {startTime, domainLookupStart, domainLookupEnd, connectStart, connectEnd, secureConnectionStart, requestStart, responseStart, responseEnd} = resource.timingData;
if (isNaN(startTime)) {
cell.textContent = zeroWidthSpace;
return;
}
let graphStartTime = this._waterfallTimelineRuler.startTime;
if (responseEnd < graphStartTime) {
cell.textContent = zeroWidthSpace;
return;
}
let graphEndTime = this._waterfallTimelineRuler.endTime;
if (startTime > graphEndTime) {
cell.textContent = zeroWidthSpace;
return;
}
let secondsPerPixel = this._waterfallTimelineRuler.secondsPerPixel;
let container = cell.appendChild(document.createElement("div"));
container.className = "waterfall-container";
function appendBlock(startTime, endTime, className) {
let startOffset = (startTime - graphStartTime) / secondsPerPixel;
let width = (endTime - startTime) / secondsPerPixel;
let block = container.appendChild(document.createElement("div"));
block.classList.add("block", className);
let styleAttribute = WI.resolvedLayoutDirection() === WI.LayoutDirection.LTR ? "left" : "right";
block.style[styleAttribute] = startOffset + "px";
block.style.width = width + "px";
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._handleMousedownWaterfall(mouseBlock, entry, event);
});
// Super small visualization.
let totalWidth = (responseEnd - startTime) / secondsPerPixel;
if (totalWidth <= 3) {
appendBlock(startTime, requestStart, "queue");
appendBlock(startTime, responseEnd, "response");
return;
}
// Each component.
if (domainLookupStart) {
appendBlock(startTime, domainLookupStart, "queue");
appendBlock(domainLookupStart, connectStart || requestStart, "dns");
} else if (connectStart)
appendBlock(startTime, connectStart, "queue");
else if (requestStart)
appendBlock(startTime, requestStart, "queue");
if (connectStart)
appendBlock(connectStart, 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 "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;
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;
return transferSizeA - transferSizeB;
};
break;
case "waterfall":
// Sort by startTime number.
comparator = 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;
this._entriesSortComparator = (a, b) => reverseFactor * comparator(a, b);
}
// Protected
initialLayout()
{
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._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"), {
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,
});
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._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();
}
handleClearShortcut(event)
{
this.reset();
}
// Private
_updateWaterfallTimelineRuler()
{
if (!this._waterfallTimelineRuler)
return;
if (isNaN(this._waterfallStartTime)) {
this._waterfallTimelineRuler.zeroTime = 0;
this._waterfallTimelineRuler.startTime = 0;
this._waterfallTimelineRuler.endTime = 0.250;
} else {
this._waterfallTimelineRuler.zeroTime = this._waterfallStartTime;
this._waterfallTimelineRuler.startTime = this._waterfallStartTime;
this._waterfallTimelineRuler.endTime = this._waterfallEndTime;
// Add a little bit of padding on the each side.
const paddingPixels = 5;
let padSeconds = paddingPixels * this._waterfallTimelineRuler.secondsPerPixel;
this._waterfallTimelineRuler.zeroTime = this._waterfallStartTime - padSeconds;
this._waterfallTimelineRuler.startTime = this._waterfallStartTime - padSeconds;
this._waterfallTimelineRuler.endTime = this._waterfallEndTime + padSeconds;
}
}
_updateExportButton()
{
let enabled = this._filteredEntries.length > 0;
this._harExportNavigationItem.enabled = enabled;
}
_processPendingEntries()
{
let needsSort = this._pendingUpdates.length > 0;
let needsFilter = this._pendingFilter;
// No global sort or filter is needed, so just insert new records into their sorted position.
if (!needsSort && !needsFilter) {
let originalLength = this._pendingInsertions.length;
for (let resource of this._pendingInsertions)
this._insertResourceAndReloadTable(resource);
console.assert(this._pendingInsertions.length === originalLength);
this._pendingInsertions = [];
return;
}
for (let resource of this._pendingInsertions)
this._entries.push(this._entryForResource(resource));
this._pendingInsertions = [];
for (let resource of this._pendingUpdates)
this._updateEntryForResource(resource);
this._pendingUpdates = [];
this._pendingFilter = false;
this._updateSortAndFilteredEntries();
this._table.reloadData();
}
_populateWithInitialResourcesIfNeeded()
{
if (!this._needsInitialPopulate)
return;
this._needsInitialPopulate = false;
let populateResourcesForFrame = (frame) => {
if (frame.provisionalMainResource)
this._pendingInsertions.push(frame.provisionalMainResource);
else if (frame.mainResource)
this._pendingInsertions.push(frame.mainResource);
for (let resource of frame.resourceCollection.items)
this._pendingInsertions.push(resource);
for (let childFrame of frame.childFrameCollection.items)
populateResourcesForFrame(childFrame);
};
let populateResourcesForTarget = (target) => {
if (target.mainResource instanceof WI.Resource)
this._pendingInsertions.push(target.mainResource);
for (let resource of target.resourceCollection.items)
this._pendingInsertions.push(resource);
};
for (let target of WI.targets) {
if (target === WI.pageTarget)
populateResourcesForFrame(WI.frameResourceManager.mainFrame);
else
populateResourcesForTarget(target);
}
this.needsLayout();
}
_checkURLFilterAgainstResource(resource)
{
if (this._urlFilterSearchRegex.test(resource.url))
this._activeURLFilterResources.add(resource);
}
_rowIndexForResource(resource)
{
return this._filteredEntries.findIndex((x) => x.resource === resource);
}
_updateEntryForResource(resource)
{
let index = this._entries.findIndex((x) => x.resource === resource);
if (index === -1)
return;
let entry = this._entryForResource(resource);
this._entries[index] = entry;
let rowIndex = this._rowIndexForResource(resource);
if (rowIndex === -1)
return;
this._filteredEntries[rowIndex] = entry;
}
_hidePopover()
{
if (this._waterfallPopover)
this._waterfallPopover.dismiss();
}
_hideResourceDetailView()
{
if (!this._resourceDetailView)
return;
this.element.classList.remove("showing-detail");
this._table.scrollContainer.style.removeProperty("width");
this.removeSubview(this._resourceDetailView);
this._resourceDetailView.hidden();
this._resourceDetailView = null;
this._table.resize();
this._table.reloadVisibleColumnCells(this._waterfallColumn);
}
_showResourceDetailView(resource)
{
let oldResourceDetailView = this._resourceDetailView;
this._resourceDetailView = this._resourceDetailViewMap.get(resource);
if (!this._resourceDetailView) {
this._resourceDetailView = new WI.NetworkResourceDetailView(resource, this);
this._resourceDetailViewMap.set(resource, this._resourceDetailView);
}
if (oldResourceDetailView) {
oldResourceDetailView.hidden();
this.replaceSubview(oldResourceDetailView, this._resourceDetailView);
} else
this.addSubview(this._resourceDetailView);
this._resourceDetailView.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._resourceDetailView)
return;
let side = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL ? "right" : "left";
this._resourceDetailView.element.style[side] = this._nameColumn.width + "px";
this._table.scrollContainer.style.width = this._nameColumn.width + "px";
}
_updateURLFilterActiveIndicator()
{
this._urlFilterNavigationItem.filterBar.indicatingActive = this._hasURLFilter();
}
_updateEmptyFilterResultsMessage()
{
if (this._hasActiveFilter() && !this._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.resourceCachingDisabledSetting.value;
}
_toggleDisableResourceCache()
{
WI.resourceCachingDisabledSetting.value = !WI.resourceCachingDisabledSetting.value;
}
_mainResourceDidChange(event)
{
let frame = event.target;
if (!frame.isMainFrame() || !WI.settings.clearNetworkOnNavigate.value)
return;
this.reset();
this._insertResourceAndReloadTable(frame.mainResource);
}
_mainFrameDidChange()
{
this._populateWithInitialResourcesIfNeeded();
}
_resourceLoadingDidFinish(event)
{
let resource = event.target;
this._pendingUpdates.push(resource);
if (resource.firstTimestamp < this._waterfallStartTime)
this._waterfallStartTime = resource.firstTimestamp;
if (resource.timingData.responseEnd > this._waterfallEndTime)
this._waterfallEndTime = resource.timingData.responseEnd;
if (this._hasURLFilter())
this._checkURLFilterAgainstResource(resource);
this.needsLayout();
}
_resourceLoadingDidFail(event)
{
let resource = event.target;
this._pendingUpdates.push(resource);
if (resource.firstTimestamp < this._waterfallStartTime)
this._waterfallStartTime = resource.firstTimestamp;
if (resource.timingData.responseEnd > this._waterfallEndTime)
this._waterfallEndTime = resource.timingData.responseEnd;
if (this._hasURLFilter())
this._checkURLFilterAgainstResource(resource);
this.needsLayout();
}
_resourceTransferSizeDidChange(event)
{
if (!this._table)
return;
let resource = event.target;
// In the unlikely event that this is the sort column, we may need to resort.
if (this._table.sortColumnIdentifier === "transferSize") {
this._pendingUpdates.push(resource);
this.needsLayout();
return;
}
let index = this._entries.findIndex((x) => x.resource === resource);
if (index === -1)
return;
let entry = this._entries[index];
entry.transferSize = !isNaN(resource.networkTotalTransferSize) ? resource.networkTotalTransferSize : resource.estimatedTotalTransferSize;
let rowIndex = this._rowIndexForResource(resource);
if (rowIndex === -1)
return;
this._table.reloadCell(rowIndex, "transferSize");
}
_networkTimelineRecordAdded(event)
{
let resourceTimelineRecord = event.data.record;
console.assert(resourceTimelineRecord instanceof WI.ResourceTimelineRecord);
let resource = resourceTimelineRecord.resource;
if (isNaN(this._waterfallStartTime))
this._waterfallStartTime = this._waterfallEndTime = resource.firstTimestamp;
this._insertResourceAndReloadTable(resource);
}
_isDefaultSort()
{
return this._table.sortColumnIdentifier === "waterfall" && this._table.sortOrder === WI.Table.SortOrder.Ascending;
}
_insertResourceAndReloadTable(resource)
{
if (!this._table || !(WI.tabBrowser.selectedTabContentView instanceof WI.NetworkTabContentView)) {
this._pendingInsertions.push(resource);
this.needsLayout();
return;
}
let entry = this._entryForResource(resource);
// Default sort has fast path.
if (this._isDefaultSort() || !this._entriesSortComparator) {
this._entries.push(entry);
if (this._passFilter(entry)) {
this._filteredEntries.push(entry);
this._table.reloadDataAddedToEndOnly();
}
return;
}
insertObjectIntoSortedArray(entry, this._entries, this._entriesSortComparator);
if (this._passFilter(entry)) {
insertObjectIntoSortedArray(entry, this._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 (this._filteredEntries.lastValue === entry)
this._table.reloadDataAddedToEndOnly();
else
this._table.reloadData();
}
}
_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,
priority: resource.priority,
remoteAddress: resource.remoteAddress,
connectionIdentifier: resource.connectionIdentifier,
startTime: resource.firstTimestamp,
};
}
_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);
}
_updateSortAndFilteredEntries()
{
this._entries = this._entries.sort(this._entriesSortComparator);
this._updateFilteredEntries();
}
_updateFilteredEntries()
{
if (this._hasActiveFilter())
this._filteredEntries = this._entries.filter(this._passFilter, this);
else
this._filteredEntries = this._entries.slice();
this._restoreSelectedRow();
this._updateURLFilterActiveIndicator();
this._updateEmptyFilterResultsMessage();
}
_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._table.reloadData();
}
_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._hideResourceDetailView();
this._activeTypeFilters = newFilter;
this._updateFilteredEntries();
this._table.reloadData();
}
_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._hideResourceDetailView();
// Search cleared.
if (!searchQuery) {
this._urlFilterSearchText = null;
this._urlFilterSearchRegex = null;
this._urlFilterIsActive = false;
this._activeURLFilterResources.clear();
this._updateFilteredEntries();
this._table.reloadData();
return;
}
this._urlFilterIsActive = true;
this._urlFilterSearchText = searchQuery;
this._urlFilterSearchRegex = new RegExp(searchQuery.escapeForRegExp(), "i");
this._activeURLFilterResources.clear();
for (let entry of this._entries)
this._checkURLFilterAgainstResource(entry.resource);
this._updateFilteredEntries();
this._table.reloadData();
}
_restoreSelectedRow()
{
if (!this._selectedResource)
return;
let rowIndex = this._rowIndexForResource(this._selectedResource);
if (rowIndex === -1) {
this._selectedResource = null;
this._table.clearSelectedRow();
return;
}
this._table.selectRow(rowIndex);
}
_HARResources()
{
let resources = this._filteredEntries.map((x) => x.resource);
const supportedHARSchemes = new Set(["http", "https", "ws", "wss"]);
return resources.filter((resource) => resource.finished && supportedHARSchemes.has(resource.urlComponents.scheme));
}
_exportHAR()
{
let resources = this._HARResources();
if (!resources.length) {
InspectorFrontendHost.beep();
return;
}
WI.HARBuilder.buildArchive(resources).then((har) => {
let mainFrame = WI.frameResourceManager.mainFrame;
let archiveName = mainFrame.mainResource.urlComponents.host || mainFrame.mainResource.displayName || "Archive";
let url = "web-inspector:///" + encodeURI(archiveName) + ".har";
WI.saveDataToFile({
url,
content: JSON.stringify(har, null, 2),
forceSaveAs: true,
});
}).catch(handlePromiseException);
}
_waterfallPopoverContentForResource(resource)
{
let contentElement = document.createElement("div");
contentElement.className = "waterfall-popover-content";
if (!resource.hasResponse() || !resource.timingData.startTime || !resource.timingData.responseEnd) {
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;
}
_handleMousedownWaterfall(mouseBlock, entry, event)
{
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._rowIndexForResource(entry.resource);
let cell = this._table.cellForRowAndColumn(rowIndex, this._waterfallColumn);
if (!cell) {
this._waterfallPopover.dismiss();
return null;
}
let mouseBlock = cell.querySelector(".block.mouse-tracking");
if (!mouseBlock) {
this._waterfallPopover.dismiss();
return null;
}
return WI.Rect.rectFromClientRect(mouseBlock.getBoundingClientRect());
};
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);
};
let popoverContentElement = this._waterfallPopoverContentForResource(entry.resource);
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);
}
};