2 * Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
26 WI.ResourceTimelineDataGridNode = class ResourceTimelineDataGridNode extends WI.TimelineDataGridNode
28 constructor(resourceTimelineRecord, includesGraph, graphDataSource, shouldShowPopover)
30 super(includesGraph, graphDataSource);
32 this._resource = resourceTimelineRecord.resource;
33 this._record = resourceTimelineRecord;
34 this._shouldShowPopover = shouldShowPopover;
36 this._resource.addEventListener(WI.Resource.Event.LoadingDidFinish, this._needsRefresh, this);
37 this._resource.addEventListener(WI.Resource.Event.LoadingDidFail, this._needsRefresh, this);
38 this._resource.addEventListener(WI.Resource.Event.URLDidChange, this._needsRefresh, this);
41 this._record.addEventListener(WI.TimelineRecord.Event.Updated, this._timelineRecordUpdated, this);
43 this._resource.addEventListener(WI.Resource.Event.TypeDidChange, this._needsRefresh, this);
44 this._resource.addEventListener(WI.Resource.Event.SizeDidChange, this._needsRefresh, this);
45 this._resource.addEventListener(WI.Resource.Event.TransferSizeDidChange, this._needsRefresh, this);
53 return [this._record];
58 return this._resource;
64 return this._cachedData;
66 var resource = this._resource;
69 if (!this._includesGraph) {
70 var zeroTime = this.graphDataSource ? this.graphDataSource.zeroTime : 0;
72 data.domain = WI.displayNameForHost(resource.urlComponents.host);
73 data.scheme = resource.urlComponents.scheme ? resource.urlComponents.scheme.toUpperCase() : "";
74 data.method = resource.requestMethod;
75 data.type = resource.type;
76 data.statusCode = resource.statusCode;
77 data.cached = resource.cached;
78 data.size = resource.size;
79 data.transferSize = !isNaN(resource.networkTotalTransferSize) ? resource.networkTotalTransferSize : resource.estimatedTotalTransferSize;
80 data.requestSent = resource.requestSentTimestamp - zeroTime;
81 data.duration = resource.receiveDuration;
82 data.latency = resource.latency;
83 data.protocol = resource.protocol;
84 data.priority = resource.priority;
85 data.remoteAddress = resource.remoteAddress;
86 data.connectionIdentifier = resource.connectionIdentifier;
89 data.graph = this._record.startTime;
91 this._cachedData = data;
95 createCellContent(columnIdentifier, cell)
97 let resource = this._resource;
99 if (resource.hadLoadingError())
100 cell.classList.add("error");
102 let value = this.data[columnIdentifier];
104 switch (columnIdentifier) {
106 cell.classList.add(...this.iconClassNames());
107 cell.title = resource.displayURL;
108 this._updateStatus(cell);
109 return this._createNameCellDocumentFragment();
112 var text = WI.Resource.displayNameForType(value);
117 cell.title = resource.statusText || "";
118 return value || emDash;
121 var fragment = this._cachedCellContent();
122 cell.title = fragment.textContent;
129 text = Number.bytesToString(value, true);
139 text = Number.secondsToString(value, true);
148 case "remoteAddress":
149 case "connectionIdentifier":
152 return value || emDash;
155 var title = WI.Resource.displayNameForPriority(value);
158 return title || emDash;
161 return super.createCellContent(columnIdentifier, cell);
166 if (this._scheduledRefreshIdentifier) {
167 cancelAnimationFrame(this._scheduledRefreshIdentifier);
168 this._scheduledRefreshIdentifier = undefined;
171 this._cachedData = null;
178 return [WI.ResourceTreeElement.ResourceIconStyleClassName, this.resource.type];
181 appendContextMenuItems(contextMenu)
183 WI.appendContextMenuItemsForSourceCode(contextMenu, this._resource);
188 didAddRecordBar(recordBar)
190 if (!this._shouldShowPopover)
193 if (!recordBar.records.length || recordBar.records[0].type !== WI.TimelineRecord.Type.Network)
196 console.assert(!this._mouseEnterRecordBarListener);
197 this._mouseEnterRecordBarListener = this._mouseoverRecordBar.bind(this);
198 recordBar.element.addEventListener("mouseenter", this._mouseEnterRecordBarListener);
201 didRemoveRecordBar(recordBar)
203 if (!this._shouldShowPopover)
206 if (!recordBar.records.length || recordBar.records[0].type !== WI.TimelineRecord.Type.Network)
209 recordBar.element.removeEventListener("mouseenter", this._mouseEnterRecordBarListener);
210 this._mouseEnterRecordBarListener = null;
213 filterableDataForColumn(columnIdentifier)
215 if (columnIdentifier === "name")
216 return this._resource.url;
217 return super.filterableDataForColumn(columnIdentifier);
222 _createNameCellDocumentFragment()
224 let fragment = document.createDocumentFragment();
225 let mainTitle = this.displayName();
226 fragment.append(mainTitle);
228 // Show the host as the subtitle if it is different from the main resource or if this is the main frame's main resource.
229 let frame = this._resource.parentFrame;
230 let isMainResource = this._resource.isMainResource();
231 let parentResourceHost;
232 if (frame && isMainResource) {
233 // When the resource is a main resource, get the host from the current frame's parent frame instead of the current frame.
234 parentResourceHost = frame.parentFrame ? frame.parentFrame.mainResource.urlComponents.host : null;
236 // When the resource is a normal sub-resource, get the host from the current frame's main resource.
237 parentResourceHost = frame.mainResource.urlComponents.host;
240 if (parentResourceHost !== this._resource.urlComponents.host || frame.isMainFrame() && isMainResource) {
241 let subtitle = WI.displayNameForHost(this._resource.urlComponents.host);
242 if (mainTitle !== subtitle) {
243 let subtitleElement = document.createElement("span");
244 subtitleElement.classList.add("subtitle");
245 subtitleElement.textContent = subtitle;
246 fragment.append(subtitleElement);
255 if (!this._resource.hasResponse())
258 let responseSource = this._resource.responseSource;
259 if (responseSource === WI.Resource.ResponseSource.MemoryCache || responseSource === WI.Resource.ResponseSource.DiskCache) {
260 console.assert(this._resource.cached, "This resource has a cache responseSource it should also be marked as cached", this._resource);
261 let span = document.createElement("span");
262 let cacheType = document.createElement("span");
263 cacheType.classList = "cache-type";
264 cacheType.textContent = responseSource === WI.Resource.ResponseSource.MemoryCache ? WI.UIString("(Memory)") : WI.UIString("(Disk)");
265 span.append(WI.UIString("Yes"), " ", cacheType);
269 let fragment = document.createDocumentFragment();
270 fragment.append(this._resource.cached ? WI.UIString("Yes") : WI.UIString("No"));
276 if (this.dataGrid instanceof WI.TimelineDataGrid) {
277 this.dataGrid.dataGridNodeNeedsRefresh(this);
281 if (this._scheduledRefreshIdentifier)
284 this._scheduledRefreshIdentifier = requestAnimationFrame(this.refresh.bind(this));
287 _timelineRecordUpdated(event)
289 if (this.isRecordVisible(this._record))
290 this.needsGraphRefresh();
293 _dataGridNodeGoToArrowClicked()
296 ignoreNetworkTab: true,
297 ignoreSearchTab: true,
299 WI.showSourceCode(this._resource, options);
304 if (this._resource.failed)
305 cell.classList.add("error");
307 cell.classList.remove("error");
309 if (this._resource.finished)
310 this.createGoToArrowButton(cell, this._dataGridNodeGoToArrowClicked.bind(this));
313 if (this._resource.isLoading()) {
315 this._spinner = new WI.IndeterminateProgressSpinner;
316 let contentElement = cell.firstChild;
317 contentElement.appendChild(this._spinner.element);
320 this._spinner.element.remove();
324 _mouseoverRecordBar(event)
326 let recordBar = WI.TimelineRecordBar.fromElement(event.target);
327 console.assert(recordBar);
331 let calculateTargetFrame = () => {
332 let columnRect = WI.Rect.rectFromClientRect(this.elementWithColumnIdentifier("graph").getBoundingClientRect());
333 let barRect = WI.Rect.rectFromClientRect(event.target.getBoundingClientRect());
334 return columnRect.intersectionWithRect(barRect);
337 let targetFrame = calculateTargetFrame();
338 if (!targetFrame.size.width && !targetFrame.size.height)
341 console.assert(recordBar.records.length);
342 let resource = recordBar.records[0].resource;
343 if (!resource.timingData)
346 if (!resource.timingData.responseEnd)
349 if (this.dataGrid._dismissPopoverTimeout) {
350 clearTimeout(this.dataGrid._dismissPopoverTimeout);
351 this.dataGrid._dismissPopoverTimeout = undefined;
354 let popoverContentElement = document.createElement("div");
355 popoverContentElement.classList.add("resource-timing-popover-content");
357 if (resource.failed || resource.urlComponents.scheme === "data" || (resource.cached && resource.statusCode !== 304)) {
358 let descriptionElement = document.createElement("span");
359 descriptionElement.classList.add("description");
361 descriptionElement.textContent = WI.UIString("Resource failed to load.");
362 else if (resource.urlComponents.scheme === "data")
363 descriptionElement.textContent = WI.UIString("Resource was loaded with the “data” scheme.");
365 descriptionElement.textContent = WI.UIString("Resource was served from the cache.");
366 popoverContentElement.appendChild(descriptionElement);
373 width: `${WI.ResourceTimelineDataGridNode.PopoverGraphColumnWidthPixels}px`
381 let popoverDataGrid = new WI.DataGrid(columns);
382 popoverDataGrid.inline = true;
383 popoverDataGrid.headerVisible = false;
384 popoverContentElement.appendChild(popoverDataGrid.element);
386 let graphDataSource = {
387 get secondsPerPixel() { return resource.duration / WI.ResourceTimelineDataGridNode.PopoverGraphColumnWidthPixels; },
388 get zeroTime() { return resource.firstTimestamp; },
389 get startTime() { return resource.firstTimestamp; },
390 get currentTime() { return this.endTime; },
394 let endTimePadding = this.secondsPerPixel * WI.TimelineRecordBar.MinimumWidthPixels;
395 return resource.lastTimestamp + endTimePadding;
399 let secondTimestamp = resource.timingData.domainLookupStart || resource.timingData.connectStart || resource.timingData.requestStart;
400 if (secondTimestamp - resource.timingData.startTime)
401 popoverDataGrid.appendChild(new WI.ResourceTimingPopoverDataGridNode(WI.UIString("Stalled"), resource.timingData.startTime, secondTimestamp, graphDataSource));
402 if (resource.timingData.domainLookupStart)
403 popoverDataGrid.appendChild(new WI.ResourceTimingPopoverDataGridNode(WI.UIString("DNS"), resource.timingData.domainLookupStart, resource.timingData.domainLookupEnd, graphDataSource));
404 if (resource.timingData.connectStart)
405 popoverDataGrid.appendChild(new WI.ResourceTimingPopoverDataGridNode(WI.UIString("Connection"), resource.timingData.connectStart, resource.timingData.connectEnd, graphDataSource));
406 if (resource.timingData.secureConnectionStart)
407 popoverDataGrid.appendChild(new WI.ResourceTimingPopoverDataGridNode(WI.UIString("Secure"), resource.timingData.secureConnectionStart, resource.timingData.connectEnd, graphDataSource));
408 popoverDataGrid.appendChild(new WI.ResourceTimingPopoverDataGridNode(WI.UIString("Request"), resource.timingData.requestStart, resource.timingData.responseStart, graphDataSource));
409 popoverDataGrid.appendChild(new WI.ResourceTimingPopoverDataGridNode(WI.UIString("Response"), resource.timingData.responseStart, resource.timingData.responseEnd, graphDataSource));
411 const higherResolution = true;
413 description: WI.UIString("Total time"),
414 duration: Number.secondsToMillisecondsString(resource.timingData.responseEnd - resource.timingData.startTime, higherResolution)
416 popoverDataGrid.appendChild(new WI.DataGridNode(totalData));
418 popoverDataGrid.updateLayout();
421 if (!this.dataGrid._popover)
422 this.dataGrid._popover = new WI.Popover;
424 let preferredEdges = [WI.RectEdge.MAX_Y, WI.RectEdge.MIN_Y, WI.RectEdge.MIN_X];
425 this.dataGrid._popover.windowResizeHandler = () => {
426 let bounds = calculateTargetFrame();
427 this.dataGrid._popover.present(bounds.pad(2), preferredEdges);
430 recordBar.element.addEventListener("mouseleave", () => {
434 this.dataGrid._dismissPopoverTimeout = setTimeout(() => {
436 this.dataGrid._popover.dismiss();
437 }, WI.ResourceTimelineDataGridNode.DelayedPopoverDismissalTimeout);
440 this.dataGrid._popover.presentNewContentWithFrame(popoverContentElement, targetFrame.pad(2), preferredEdges);
444 WI.ResourceTimelineDataGridNode.PopoverGraphColumnWidthPixels = 110;
445 WI.ResourceTimelineDataGridNode.DelayedPopoverDismissalTimeout = 500;