Web Inspector: Include more Network information in Resource Details Sidebar
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / ResourceTimelineDataGridNode.js
1 /*
2  * Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
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.
12  *
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.
24  */
25
26 WebInspector.ResourceTimelineDataGridNode = class ResourceTimelineDataGridNode extends WebInspector.TimelineDataGridNode
27 {
28     constructor(resourceTimelineRecord, includesGraph, graphDataSource, shouldShowPopover)
29     {
30         super(includesGraph, graphDataSource);
31
32         this._resource = resourceTimelineRecord.resource;
33         this._record = resourceTimelineRecord;
34         this._shouldShowPopover = shouldShowPopover;
35
36         this._resource.addEventListener(WebInspector.Resource.Event.LoadingDidFinish, this._needsRefresh, this);
37         this._resource.addEventListener(WebInspector.Resource.Event.LoadingDidFail, this._needsRefresh, this);
38         this._resource.addEventListener(WebInspector.Resource.Event.URLDidChange, this._needsRefresh, this);
39
40         if (includesGraph)
41             this._record.addEventListener(WebInspector.TimelineRecord.Event.Updated, this._timelineRecordUpdated, this);
42         else {
43             this._resource.addEventListener(WebInspector.Resource.Event.TypeDidChange, this._needsRefresh, this);
44             this._resource.addEventListener(WebInspector.Resource.Event.SizeDidChange, this._needsRefresh, this);
45             this._resource.addEventListener(WebInspector.Resource.Event.TransferSizeDidChange, this._needsRefresh, this);
46         }
47     }
48
49     // Public
50
51     get records()
52     {
53         return [this._record];
54     }
55
56     get resource()
57     {
58         return this._resource;
59     }
60
61     get data()
62     {
63         if (this._cachedData)
64             return this._cachedData;
65
66         var resource = this._resource;
67         var data = {};
68
69         if (!this._includesGraph) {
70             var zeroTime = this.graphDataSource ? this.graphDataSource.zeroTime : 0;
71
72             data.domain = WebInspector.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 = resource.transferSize;
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;
87         }
88
89         data.graph = this._record.startTime;
90
91         this._cachedData = data;
92         return data;
93     }
94
95     createCellContent(columnIdentifier, cell)
96     {
97         let resource = this._resource;
98
99         if (resource.failed || resource.canceled || resource.statusCode >= 400)
100             cell.classList.add("error");
101
102         let value = this.data[columnIdentifier];
103
104         switch (columnIdentifier) {
105         case "name":
106             cell.classList.add(...this.iconClassNames());
107             cell.title = resource.displayURL;
108             this._updateStatus(cell);
109             return this._createNameCellDocumentFragment();
110
111         case "type":
112             return WebInspector.Resource.displayNameForType(value);
113
114         case "statusCode":
115             cell.title = resource.statusText || "";
116             return value || emDash;
117
118         case "cached":
119             return this._cachedCellContent();
120
121         case "domain":
122             return value || emDash;
123
124         case "size":
125         case "transferSize":
126             return isNaN(value) ? emDash : Number.bytesToString(value, true);
127
128         case "requestSent":
129         case "latency":
130         case "duration":
131             return isNaN(value) ? emDash : Number.secondsToString(value, true);
132
133         case "protocol":
134         case "remoteAddress":
135         case "connectionIdentifier":
136             return value || emDash;
137
138         case "priority":
139             return WebInspector.Resource.displayNameForPriority(value) || emDash;
140         }
141
142         return super.createCellContent(columnIdentifier, cell);
143     }
144
145     refresh()
146     {
147         if (this._scheduledRefreshIdentifier) {
148             cancelAnimationFrame(this._scheduledRefreshIdentifier);
149             this._scheduledRefreshIdentifier = undefined;
150         }
151
152         this._cachedData = null;
153
154         super.refresh();
155     }
156
157     iconClassNames()
158     {
159         return [WebInspector.ResourceTreeElement.ResourceIconStyleClassName, this.resource.type];
160     }
161
162     appendContextMenuItems(contextMenu)
163     {
164         WebInspector.appendContextMenuItemsForSourceCode(contextMenu, this._resource);
165     }
166
167     // Protected
168
169     didAddRecordBar(recordBar)
170     {
171         if (!this._shouldShowPopover)
172             return;
173
174         if (!recordBar.records.length || recordBar.records[0].type !== WebInspector.TimelineRecord.Type.Network)
175             return;
176
177         console.assert(!this._mouseEnterRecordBarListener);
178         this._mouseEnterRecordBarListener = this._mouseoverRecordBar.bind(this);
179         recordBar.element.addEventListener("mouseenter", this._mouseEnterRecordBarListener);
180     }
181
182     didRemoveRecordBar(recordBar)
183     {
184         if (!this._shouldShowPopover)
185             return;
186
187         if (!recordBar.records.length || recordBar.records[0].type !== WebInspector.TimelineRecord.Type.Network)
188             return;
189
190         recordBar.element.removeEventListener("mouseenter", this._mouseEnterRecordBarListener);
191         this._mouseEnterRecordBarListener = null;
192     }
193
194     filterableDataForColumn(columnIdentifier)
195     {
196         if (columnIdentifier === "name")
197             return this._resource.url;
198         return super.filterableDataForColumn(columnIdentifier);
199     }
200
201     // Private
202
203     _createNameCellDocumentFragment()
204     {
205         let fragment = document.createDocumentFragment();
206         let mainTitle = this.displayName();
207         fragment.append(mainTitle);
208
209         // Show the host as the subtitle if it is different from the main resource or if this is the main frame's main resource.
210         let frame = this._resource.parentFrame;
211         let isMainResource = this._resource.isMainResource();
212         let parentResourceHost;
213         if (frame && isMainResource) {
214             // When the resource is a main resource, get the host from the current frame's parent frame instead of the current frame.
215             parentResourceHost = frame.parentFrame ? frame.parentFrame.mainResource.urlComponents.host : null;
216         } else if (frame) {
217             // When the resource is a normal sub-resource, get the host from the current frame's main resource.
218             parentResourceHost = frame.mainResource.urlComponents.host;
219         }
220
221         if (parentResourceHost !== this._resource.urlComponents.host || frame.isMainFrame() && isMainResource) {
222             let subtitle = WebInspector.displayNameForHost(this._resource.urlComponents.host);
223             if (mainTitle !== subtitle) {
224                 let subtitleElement = document.createElement("span");
225                 subtitleElement.classList.add("subtitle");
226                 subtitleElement.textContent = subtitle;
227                 fragment.append(subtitleElement);
228             }
229         }
230
231         return fragment;
232     }
233
234     _cachedCellContent()
235     {
236         if (!this._resource.hasResponse())
237             return emDash;
238
239         let responseSource = this._resource.responseSource;
240         if (responseSource === WebInspector.Resource.ResponseSource.MemoryCache || responseSource === WebInspector.Resource.ResponseSource.DiskCache) {
241             console.assert(this._resource.cached, "This resource has a cache responseSource it should also be marked as cached", this._resource);
242             let span = document.createElement("span");
243             let cacheType = document.createElement("span");
244             cacheType.classList = "cache-type";
245             cacheType.textContent = responseSource === WebInspector.Resource.ResponseSource.MemoryCache ? WebInspector.UIString("(Memory)") : WebInspector.UIString("(Disk)");
246             span.append(WebInspector.UIString("Yes"), " ", cacheType);
247             return span;
248         }
249
250         return this._resource.cached ? WebInspector.UIString("Yes") : WebInspector.UIString("No");
251     }
252
253     _needsRefresh()
254     {
255         if (this.dataGrid instanceof WebInspector.TimelineDataGrid) {
256             this.dataGrid.dataGridNodeNeedsRefresh(this);
257             return;
258         }
259
260         if (this._scheduledRefreshIdentifier)
261             return;
262
263         this._scheduledRefreshIdentifier = requestAnimationFrame(this.refresh.bind(this));
264     }
265
266     _timelineRecordUpdated(event)
267     {
268         if (this.isRecordVisible(this._record))
269             this.needsGraphRefresh();
270     }
271
272     _dataGridNodeGoToArrowClicked()
273     {
274         WebInspector.showSourceCode(this._resource, {ignoreNetworkTab: true});
275     }
276
277     _updateStatus(cell)
278     {
279         if (this._resource.failed)
280             cell.classList.add("error");
281         else {
282             cell.classList.remove("error");
283
284             if (this._resource.finished)
285                 this.createGoToArrowButton(cell, this._dataGridNodeGoToArrowClicked.bind(this));
286         }
287
288         if (this._spinner)
289             this._spinner.element.remove();
290
291         if (this._resource.finished || this._resource.failed)
292             return;
293
294         if (!this._spinner)
295             this._spinner = new WebInspector.IndeterminateProgressSpinner;
296
297         let contentElement = cell.firstChild;
298         contentElement.appendChild(this._spinner.element);
299     }
300
301     _mouseoverRecordBar(event)
302     {
303         let recordBar = WebInspector.TimelineRecordBar.fromElement(event.target);
304         console.assert(recordBar);
305         if (!recordBar)
306             return;
307
308         let calculateTargetFrame = () => {
309             let columnRect = WebInspector.Rect.rectFromClientRect(this.elementWithColumnIdentifier("graph").getBoundingClientRect());
310             let barRect = WebInspector.Rect.rectFromClientRect(event.target.getBoundingClientRect());
311             return columnRect.intersectionWithRect(barRect);
312         };
313
314         let targetFrame = calculateTargetFrame();
315         if (!targetFrame.size.width && !targetFrame.size.height)
316             return;
317
318         console.assert(recordBar.records.length);
319         let resource = recordBar.records[0].resource;
320         if (!resource.timingData)
321             return;
322
323         if (!resource.timingData.responseEnd)
324             return;
325
326         if (this.dataGrid._dismissPopoverTimeout) {
327             clearTimeout(this.dataGrid._dismissPopoverTimeout);
328             this.dataGrid._dismissPopoverTimeout = undefined;
329         }
330
331         let popoverContentElement = document.createElement("div");
332         popoverContentElement.classList.add("resource-timing-popover-content");
333
334         if (resource.failed || resource.urlComponents.scheme === "data" || (resource.cached && resource.statusCode !== 304)) {
335             let descriptionElement = document.createElement("span");
336             descriptionElement.classList.add("description");
337             if (resource.failed)
338                 descriptionElement.textContent = WebInspector.UIString("Resource failed to load.");
339             else if (resource.urlComponents.scheme === "data")
340                 descriptionElement.textContent = WebInspector.UIString("Resource was loaded with the “data“ scheme.");
341             else
342                 descriptionElement.textContent = WebInspector.UIString("Resource was served from the cache.");
343             popoverContentElement.appendChild(descriptionElement);
344         } else {
345             let columns = {
346                 description: {
347                     width: "80px"
348                 },
349                 graph: {
350                     width: `${WebInspector.ResourceTimelineDataGridNode.PopoverGraphColumnWidthPixels}px`
351                 },
352                 duration: {
353                     width: "70px",
354                     aligned: "right"
355                 }
356             };
357
358             let popoverDataGrid = new WebInspector.DataGrid(columns);
359             popoverDataGrid.inline = true;
360             popoverDataGrid.headerVisible = false;
361             popoverContentElement.appendChild(popoverDataGrid.element);
362
363             let graphDataSource = {
364                 get secondsPerPixel() { return resource.duration / WebInspector.ResourceTimelineDataGridNode.PopoverGraphColumnWidthPixels; },
365                 get zeroTime() { return resource.firstTimestamp; },
366                 get startTime() { return resource.firstTimestamp; },
367                 get currentTime() { return this.endTime; },
368
369                 get endTime()
370                 {
371                     let endTimePadding = this.secondsPerPixel * WebInspector.TimelineRecordBar.MinimumWidthPixels;
372                     return resource.lastTimestamp + endTimePadding;
373                 }
374             };
375
376             let secondTimestamp = resource.timingData.domainLookupStart || resource.timingData.connectStart || resource.timingData.requestStart;
377             if (secondTimestamp - resource.timingData.startTime)
378                 popoverDataGrid.appendChild(new WebInspector.ResourceTimingPopoverDataGridNode(WebInspector.UIString("Stalled"), resource.timingData.startTime, secondTimestamp, graphDataSource));
379             if (resource.timingData.domainLookupStart)
380                 popoverDataGrid.appendChild(new WebInspector.ResourceTimingPopoverDataGridNode(WebInspector.UIString("DNS"), resource.timingData.domainLookupStart, resource.timingData.domainLookupEnd, graphDataSource));
381             if (resource.timingData.connectStart)
382                 popoverDataGrid.appendChild(new WebInspector.ResourceTimingPopoverDataGridNode(WebInspector.UIString("Connection"), resource.timingData.connectStart, resource.timingData.connectEnd, graphDataSource));
383             if (resource.timingData.secureConnectionStart)
384                 popoverDataGrid.appendChild(new WebInspector.ResourceTimingPopoverDataGridNode(WebInspector.UIString("Secure"), resource.timingData.secureConnectionStart, resource.timingData.connectEnd, graphDataSource));
385             popoverDataGrid.appendChild(new WebInspector.ResourceTimingPopoverDataGridNode(WebInspector.UIString("Request"), resource.timingData.requestStart, resource.timingData.responseStart, graphDataSource));
386             popoverDataGrid.appendChild(new WebInspector.ResourceTimingPopoverDataGridNode(WebInspector.UIString("Response"), resource.timingData.responseStart, resource.timingData.responseEnd, graphDataSource));
387
388             const higherResolution = true;
389             let totalData = {
390                 description: WebInspector.UIString("Total time"),
391                 duration: Number.secondsToMillisecondsString(resource.timingData.responseEnd - resource.timingData.startTime, higherResolution)
392             };
393             popoverDataGrid.appendChild(new WebInspector.DataGridNode(totalData));
394
395             popoverDataGrid.updateLayout();
396         }
397
398         if (!this.dataGrid._popover)
399             this.dataGrid._popover = new WebInspector.Popover;
400
401         let preferredEdges = [WebInspector.RectEdge.MAX_Y, WebInspector.RectEdge.MIN_Y, WebInspector.RectEdge.MIN_X];
402         this.dataGrid._popover.windowResizeHandler = () => {
403             let bounds = calculateTargetFrame();
404             this.dataGrid._popover.present(bounds.pad(2), preferredEdges);
405         };
406
407         recordBar.element.addEventListener("mouseleave", () => {
408             if (!this.dataGrid)
409                 return;
410
411             this.dataGrid._dismissPopoverTimeout = setTimeout(() => {
412                 if (this.dataGrid)
413                     this.dataGrid._popover.dismiss();
414             }, WebInspector.ResourceTimelineDataGridNode.DelayedPopoverDismissalTimeout);
415         }, {once: true});
416
417         this.dataGrid._popover.presentNewContentWithFrame(popoverContentElement, targetFrame.pad(2), preferredEdges);
418     }
419 };
420
421 WebInspector.ResourceTimelineDataGridNode.PopoverGraphColumnWidthPixels = 110;
422 WebInspector.ResourceTimelineDataGridNode.DelayedPopoverDismissalTimeout = 500;