blob: 695d4bedbb5caec84bddc0e75d6e3f5cb0def7af [file] [log] [blame]
/*
* Copyright (C) 2013, 2015 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.ResourceTimelineDataGridNode = class ResourceTimelineDataGridNode extends WI.TimelineDataGridNode
{
constructor(record, options = {})
{
console.assert(record instanceof WI.ResourceTimelineRecord);
super([record], options);
this._shouldShowPopover = options.shouldShowPopover;
this.resource.addEventListener(WI.Resource.Event.LoadingDidFinish, this._needsRefresh, this);
this.resource.addEventListener(WI.Resource.Event.LoadingDidFail, this._needsRefresh, this);
this.resource.addEventListener(WI.Resource.Event.URLDidChange, this._needsRefresh, this);
if (options.includesGraph)
this.record.addEventListener(WI.TimelineRecord.Event.Updated, this._timelineRecordUpdated, this);
else {
this.resource.addEventListener(WI.Resource.Event.TypeDidChange, this._needsRefresh, this);
this.resource.addEventListener(WI.Resource.Event.SizeDidChange, this._needsRefresh, this);
this.resource.addEventListener(WI.Resource.Event.TransferSizeDidChange, this._needsRefresh, this);
}
}
// Public
get resource()
{
return this.record.resource;
}
get data()
{
if (this._cachedData)
return this._cachedData;
this._cachedData = super.data;
this._cachedData.domain = WI.displayNameForHost(this.resource.urlComponents.host);
this._cachedData.scheme = this.resource.urlComponents.scheme ? this.resource.urlComponents.scheme.toUpperCase() : "";
this._cachedData.method = this.resource.requestMethod;
this._cachedData.type = this.resource.type;
this._cachedData.statusCode = this.resource.statusCode;
this._cachedData.cached = this.resource.cached;
this._cachedData.size = this.resource.size;
this._cachedData.transferSize = !isNaN(this.resource.networkTotalTransferSize) ? this.resource.networkTotalTransferSize : this.resource.estimatedTotalTransferSize;
this._cachedData.requestSent = this.resource.requestSentTimestamp - (this.graphDataSource ? this.graphDataSource.zeroTime : 0);
this._cachedData.duration = this.resource.receiveDuration;
this._cachedData.latency = this.resource.latency;
this._cachedData.protocol = this.resource.protocol;
this._cachedData.priority = this.resource.priority;
this._cachedData.remoteAddress = this.resource.remoteAddress;
this._cachedData.connectionIdentifier = this.resource.connectionIdentifier;
this._cachedData.initiator = this.resource.initiatorSourceCodeLocation;
return this._cachedData;
}
createCellContent(columnIdentifier, cell)
{
if (this.resource.hadLoadingError())
cell.classList.add("error");
let value = this.data[columnIdentifier];
switch (columnIdentifier) {
case "name":
cell.classList.add(...this.iconClassNames());
cell.title = this.resource.displayURL;
this._updateStatus(cell);
return this._createNameCellDocumentFragment();
case "type":
var text = WI.Resource.displayNameForType(value);
cell.title = text;
return text;
case "statusCode":
cell.title = this.resource.statusText || "";
return value || emDash;
case "cached":
var fragment = this._cachedCellContent();
cell.title = fragment.textContent;
return fragment;
case "size":
case "transferSize":
var text = emDash;
if (!isNaN(value)) {
text = Number.bytesToString(value, true);
cell.title = text;
}
return text;
case "requestSent":
case "latency":
case "duration":
var text = emDash;
if (!isNaN(value)) {
text = Number.secondsToString(value, true);
cell.title = text;
}
return text;
case "domain":
case "method":
case "scheme":
case "protocol":
case "remoteAddress":
case "connectionIdentifier":
if (value)
cell.title = value;
return value || emDash;
case "priority":
var title = WI.Resource.displayNameForPriority(value);
if (title)
cell.title = title;
return title || emDash;
case "source": // Timeline Overview
return super.createCellContent("initiator", cell);
}
return super.createCellContent(columnIdentifier, cell);
}
refresh()
{
if (this._scheduledRefreshIdentifier) {
cancelAnimationFrame(this._scheduledRefreshIdentifier);
this._scheduledRefreshIdentifier = undefined;
}
this._cachedData = null;
super.refresh();
}
iconClassNames()
{
return [WI.ResourceTreeElement.ResourceIconStyleClassName, ...WI.Resource.classNamesForResource(this.resource)];
}
appendContextMenuItems(contextMenu)
{
WI.appendContextMenuItemsForSourceCode(contextMenu, this.resource);
}
// Protected
didAddRecordBar(recordBar)
{
if (!this._shouldShowPopover)
return;
if (!recordBar.records.length || recordBar.records[0].type !== WI.TimelineRecord.Type.Network)
return;
console.assert(!this._mouseEnterRecordBarListener);
this._mouseEnterRecordBarListener = this._mouseoverRecordBar.bind(this);
recordBar.element.addEventListener("mouseenter", this._mouseEnterRecordBarListener);
}
didRemoveRecordBar(recordBar)
{
if (!this._shouldShowPopover)
return;
if (!recordBar.records.length || recordBar.records[0].type !== WI.TimelineRecord.Type.Network)
return;
recordBar.element.removeEventListener("mouseenter", this._mouseEnterRecordBarListener);
this._mouseEnterRecordBarListener = null;
}
filterableDataForColumn(columnIdentifier)
{
if (columnIdentifier === "name")
return this.resource.url;
return super.filterableDataForColumn(columnIdentifier);
}
// Private
_createNameCellDocumentFragment()
{
let fragment = document.createDocumentFragment();
let mainTitle = this.displayName();
fragment.append(mainTitle);
// Show the host as the subtitle if it is different from the main resource or if this is the main frame's main resource.
let frame = this.resource.parentFrame;
let isMainResource = this.resource.isMainResource();
let parentResourceHost;
if (frame && isMainResource) {
// When the resource is a main resource, get the host from the current frame's parent frame instead of the current frame.
parentResourceHost = frame.parentFrame ? frame.parentFrame.mainResource.urlComponents.host : null;
} else if (frame) {
// When the resource is a normal sub-resource, get the host from the current frame's main resource.
parentResourceHost = frame.mainResource.urlComponents.host;
}
if (parentResourceHost !== this.resource.urlComponents.host || frame.isMainFrame() && isMainResource) {
let subtitle = WI.displayNameForHost(this.resource.urlComponents.host);
if (mainTitle !== subtitle) {
let subtitleElement = document.createElement("span");
subtitleElement.classList.add("subtitle");
subtitleElement.textContent = subtitle;
fragment.append(subtitleElement);
}
}
return fragment;
}
_cachedCellContent()
{
if (!this.resource.hasResponse())
return emDash;
let responseSource = this.resource.responseSource;
if (responseSource === WI.Resource.ResponseSource.MemoryCache || responseSource === WI.Resource.ResponseSource.DiskCache) {
console.assert(this.resource.cached, "This resource has a cache responseSource it should also be marked as cached", this.resource);
let span = document.createElement("span");
let cacheType = document.createElement("span");
cacheType.classList = "cache-type";
cacheType.textContent = responseSource === WI.Resource.ResponseSource.MemoryCache ? WI.UIString("(Memory)") : WI.UIString("(Disk)");
span.append(WI.UIString("Yes"), " ", cacheType);
return span;
}
let fragment = document.createDocumentFragment();
fragment.append(this.resource.cached ? WI.UIString("Yes") : WI.UIString("No"));
return fragment;
}
_needsRefresh()
{
if (this.dataGrid instanceof WI.TimelineDataGrid) {
this.dataGrid.dataGridNodeNeedsRefresh(this);
return;
}
if (this._scheduledRefreshIdentifier)
return;
this._scheduledRefreshIdentifier = requestAnimationFrame(this.refresh.bind(this));
}
_timelineRecordUpdated(event)
{
if (this.isRecordVisible(this.record))
this.needsGraphRefresh();
}
_dataGridNodeGoToArrowClicked()
{
const options = {
ignoreNetworkTab: true,
ignoreSearchTab: true,
};
WI.showSourceCode(this.resource, options);
}
_updateStatus(cell)
{
if (this.resource.failed)
cell.classList.add("error");
else {
cell.classList.remove("error");
if (this.resource.finished)
this.createGoToArrowButton(cell, this._dataGridNodeGoToArrowClicked.bind(this));
}
if (this.resource.isLoading()) {
if (!this._spinner)
this._spinner = new WI.IndeterminateProgressSpinner;
let contentElement = cell.firstChild;
contentElement.appendChild(this._spinner.element);
} else {
if (this._spinner)
this._spinner.element.remove();
}
}
_mouseoverRecordBar(event)
{
let recordBar = WI.TimelineRecordBar.fromElement(event.target);
console.assert(recordBar);
if (!recordBar)
return;
let calculateTargetFrame = () => {
let columnRect = WI.Rect.rectFromClientRect(this.elementWithColumnIdentifier("graph").getBoundingClientRect());
let barRect = WI.Rect.rectFromClientRect(event.target.getBoundingClientRect());
return columnRect.intersectionWithRect(barRect);
};
let targetFrame = calculateTargetFrame();
if (!targetFrame.size.width && !targetFrame.size.height)
return;
console.assert(recordBar.records.length);
let resource = recordBar.records[0].resource;
if (!resource.timingData)
return;
if (!resource.timingData.responseEnd)
return;
if (this.dataGrid._dismissPopoverTimeout) {
clearTimeout(this.dataGrid._dismissPopoverTimeout);
this.dataGrid._dismissPopoverTimeout = undefined;
}
let popoverContentElement = document.createElement("div");
popoverContentElement.classList.add("resource-timing-popover-content");
if (resource.failed || resource.urlComponents.scheme === "data" || (resource.cached && resource.statusCode !== 304)) {
let descriptionElement = document.createElement("span");
descriptionElement.classList.add("description");
if (resource.failed)
descriptionElement.textContent = WI.UIString("Resource failed to load.");
else if (resource.urlComponents.scheme === "data")
descriptionElement.textContent = WI.UIString("Resource was loaded with the \u201Cdata\u201D scheme.");
else
descriptionElement.textContent = WI.UIString("Resource was served from the cache.");
popoverContentElement.appendChild(descriptionElement);
} else {
let columns = {
description: {
width: "80px"
},
graph: {
width: `${WI.ResourceTimelineDataGridNode.PopoverGraphColumnWidthPixels}px`
},
duration: {
width: "70px",
aligned: "right"
}
};
let popoverDataGrid = new WI.DataGrid(columns);
popoverDataGrid.inline = true;
popoverDataGrid.headerVisible = false;
popoverContentElement.appendChild(popoverDataGrid.element);
let graphDataSource = {
get secondsPerPixel() { return resource.totalDuration / WI.ResourceTimelineDataGridNode.PopoverGraphColumnWidthPixels; },
get zeroTime() { return resource.firstTimestamp; },
get startTime() { return this.zeroTime; },
get currentTime() { return resource.lastTimestamp + this._extraTimePadding; },
get endTime() { return this.currentTime; },
get _extraTimePadding() { return this.secondsPerPixel * WI.TimelineRecordBar.MinimumWidthPixels; },
};
if (resource.timingData.redirectEnd - resource.timingData.redirectStart) {
// FIXME: <https://webkit.org/b/190214> Web Inspector: expose full load metrics for redirect requests
popoverDataGrid.appendChild(new WI.ResourceTimingPopoverDataGridNode(WI.UIString("Redirects"), resource.timingData.redirectStart, resource.timingData.redirectEnd, graphDataSource));
}
let secondTimestamp = resource.timingData.domainLookupStart || resource.timingData.connectStart || resource.timingData.requestStart;
if (secondTimestamp - resource.timingData.fetchStart)
popoverDataGrid.appendChild(new WI.ResourceTimingPopoverDataGridNode(WI.UIString("Stalled"), resource.timingData.fetchStart, secondTimestamp, graphDataSource));
if (resource.timingData.domainLookupStart)
popoverDataGrid.appendChild(new WI.ResourceTimingPopoverDataGridNode(WI.UIString("DNS"), resource.timingData.domainLookupStart, resource.timingData.domainLookupEnd, graphDataSource));
if (resource.timingData.connectStart)
popoverDataGrid.appendChild(new WI.ResourceTimingPopoverDataGridNode(WI.UIString("Connection"), resource.timingData.connectStart, resource.timingData.connectEnd, graphDataSource));
if (resource.timingData.secureConnectionStart)
popoverDataGrid.appendChild(new WI.ResourceTimingPopoverDataGridNode(WI.UIString("Secure"), resource.timingData.secureConnectionStart, resource.timingData.connectEnd, graphDataSource));
popoverDataGrid.appendChild(new WI.ResourceTimingPopoverDataGridNode(WI.UIString("Request"), resource.timingData.requestStart, resource.timingData.responseStart, graphDataSource));
popoverDataGrid.appendChild(new WI.ResourceTimingPopoverDataGridNode(WI.UIString("Response"), resource.timingData.responseStart, resource.timingData.responseEnd, graphDataSource));
const higherResolution = true;
let totalData = {
description: WI.UIString("Total time"),
duration: Number.secondsToMillisecondsString(resource.timingData.responseEnd - resource.timingData.startTime, higherResolution)
};
popoverDataGrid.appendChild(new WI.DataGridNode(totalData));
popoverDataGrid.updateLayout();
}
if (!this.dataGrid._popover)
this.dataGrid._popover = new WI.Popover;
let preferredEdges = [WI.RectEdge.MAX_Y, WI.RectEdge.MIN_Y, WI.RectEdge.MIN_X];
this.dataGrid._popover.windowResizeHandler = () => {
let bounds = calculateTargetFrame();
this.dataGrid._popover.present(bounds.pad(2), preferredEdges);
};
recordBar.element.addEventListener("mouseleave", () => {
if (!this.dataGrid)
return;
this.dataGrid._dismissPopoverTimeout = setTimeout(() => {
if (this.dataGrid)
this.dataGrid._popover.dismiss();
}, WI.ResourceTimelineDataGridNode.DelayedPopoverDismissalTimeout);
}, {once: true});
this.dataGrid._popover.presentNewContentWithFrame(popoverContentElement, targetFrame.pad(2), preferredEdges);
}
};
WI.ResourceTimelineDataGridNode.PopoverGraphColumnWidthPixels = 110;
WI.ResourceTimelineDataGridNode.DelayedPopoverDismissalTimeout = 500;