| /* |
| * 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; |