Web Inspector: Associate Worker Resources with the Worker and not the Page
[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         }
84
85         data.graph = this._record.startTime;
86
87         this._cachedData = data;
88         return data;
89     }
90
91     createCellContent(columnIdentifier, cell)
92     {
93         var resource = this._resource;
94
95         if (resource.failed || resource.canceled || resource.statusCode >= 400)
96             cell.classList.add("error");
97
98         var value = this.data[columnIdentifier];
99
100         switch (columnIdentifier) {
101         case "name":
102             cell.classList.add(...this.iconClassNames());
103             cell.title = resource.displayURL;
104             this._updateStatus(cell);
105             return this._createNameCellDocumentFragment();
106
107         case "type":
108             return WebInspector.Resource.displayNameForType(value);
109
110         case "statusCode":
111             cell.title = resource.statusText || "";
112             return value || emDash;
113
114         case "cached":
115             return value ? WebInspector.UIString("Yes") : WebInspector.UIString("No");
116
117         case "domain":
118             return value || emDash;
119
120         case "size":
121         case "transferSize":
122             return isNaN(value) ? emDash : Number.bytesToString(value, true);
123
124         case "requestSent":
125         case "latency":
126         case "duration":
127             return isNaN(value) ? emDash : Number.secondsToString(value, true);
128         }
129
130         return super.createCellContent(columnIdentifier, cell);
131     }
132
133     refresh()
134     {
135         if (this._scheduledRefreshIdentifier) {
136             cancelAnimationFrame(this._scheduledRefreshIdentifier);
137             this._scheduledRefreshIdentifier = undefined;
138         }
139
140         this._cachedData = null;
141
142         super.refresh();
143     }
144
145     iconClassNames()
146     {
147         return [WebInspector.ResourceTreeElement.ResourceIconStyleClassName, this.resource.type];
148     }
149
150     appendContextMenuItems(contextMenu)
151     {
152         WebInspector.appendContextMenuItemsForSourceCode(contextMenu, this._resource);
153     }
154
155     // Protected
156
157     didAddRecordBar(recordBar)
158     {
159         if (!this._shouldShowPopover)
160             return;
161
162         if (!recordBar.records.length || recordBar.records[0].type !== WebInspector.TimelineRecord.Type.Network)
163             return;
164
165         console.assert(!this._mouseEnterRecordBarListener);
166         this._mouseEnterRecordBarListener = this._mouseoverRecordBar.bind(this);
167         recordBar.element.addEventListener("mouseenter", this._mouseEnterRecordBarListener);
168     }
169
170     didRemoveRecordBar(recordBar)
171     {
172         if (!this._shouldShowPopover)
173             return;
174
175         if (!recordBar.records.length || recordBar.records[0].type !== WebInspector.TimelineRecord.Type.Network)
176             return;
177
178         recordBar.element.removeEventListener("mouseenter", this._mouseEnterRecordBarListener);
179         this._mouseEnterRecordBarListener = null;
180     }
181
182     filterableDataForColumn(columnIdentifier)
183     {
184         if (columnIdentifier === "name")
185             return this._resource.url;
186         return super.filterableDataForColumn(columnIdentifier);
187     }
188
189     // Private
190
191     _createNameCellDocumentFragment()
192     {
193         let fragment = document.createDocumentFragment();
194         let mainTitle = this.displayName();
195         fragment.append(mainTitle);
196
197         // Show the host as the subtitle if it is different from the main resource or if this is the main frame's main resource.
198         let frame = this._resource.parentFrame;
199         let isMainResource = this._resource.isMainResource();
200         let parentResourceHost;
201         if (frame && isMainResource) {
202             // When the resource is a main resource, get the host from the current frame's parent frame instead of the current frame.
203             parentResourceHost = frame.parentFrame ? frame.parentFrame.mainResource.urlComponents.host : null;
204         } else if (frame) {
205             // When the resource is a normal sub-resource, get the host from the current frame's main resource.
206             parentResourceHost = frame.mainResource.urlComponents.host;
207         }
208
209         if (parentResourceHost !== this._resource.urlComponents.host || frame.isMainFrame() && isMainResource) {
210             let subtitle = WebInspector.displayNameForHost(this._resource.urlComponents.host);
211             if (mainTitle !== subtitle) {
212                 let subtitleElement = document.createElement("span");
213                 subtitleElement.classList.add("subtitle");
214                 subtitleElement.textContent = subtitle;
215                 fragment.append(subtitleElement);
216             }
217         }
218
219         return fragment;
220     }
221
222     _needsRefresh()
223     {
224         if (this.dataGrid instanceof WebInspector.TimelineDataGrid) {
225             this.dataGrid.dataGridNodeNeedsRefresh(this);
226             return;
227         }
228
229         if (this._scheduledRefreshIdentifier)
230             return;
231
232         this._scheduledRefreshIdentifier = requestAnimationFrame(this.refresh.bind(this));
233     }
234
235     _timelineRecordUpdated(event)
236     {
237         if (this.isRecordVisible(this._record))
238             this.needsGraphRefresh();
239     }
240
241     _dataGridNodeGoToArrowClicked()
242     {
243         WebInspector.showSourceCode(this._resource);
244     }
245
246     _updateStatus(cell)
247     {
248         if (this._resource.failed)
249             cell.classList.add("error");
250         else {
251             cell.classList.remove("error");
252
253             if (this._resource.finished)
254                 this.createGoToArrowButton(cell, this._dataGridNodeGoToArrowClicked.bind(this));
255         }
256
257         if (this._spinner)
258             this._spinner.element.remove();
259
260         if (this._resource.finished || this._resource.failed)
261             return;
262
263         if (!this._spinner)
264             this._spinner = new WebInspector.IndeterminateProgressSpinner;
265
266         let contentElement = cell.firstChild;
267         contentElement.appendChild(this._spinner.element);
268     }
269
270     _mouseoverRecordBar(event)
271     {
272         let recordBar = WebInspector.TimelineRecordBar.fromElement(event.target);
273         console.assert(recordBar);
274         if (!recordBar)
275             return;
276
277         let calculateTargetFrame = () => {
278             let columnRect = WebInspector.Rect.rectFromClientRect(this.elementWithColumnIdentifier("graph").getBoundingClientRect());
279             let barRect = WebInspector.Rect.rectFromClientRect(event.target.getBoundingClientRect());
280             return columnRect.intersectionWithRect(barRect);
281         };
282
283         let targetFrame = calculateTargetFrame();
284         if (!targetFrame.size.width && !targetFrame.size.height)
285             return;
286
287         console.assert(recordBar.records.length);
288         let resource = recordBar.records[0].resource;
289         if (!resource.timingData)
290             return;
291
292         if (!resource.timingData.responseEnd)
293             return;
294
295         if (this.dataGrid._dismissPopoverTimeout) {
296             clearTimeout(this.dataGrid._dismissPopoverTimeout);
297             this.dataGrid._dismissPopoverTimeout = undefined;
298         }
299
300         let popoverContentElement = document.createElement("div");
301         popoverContentElement.classList.add("resource-timing-popover-content");
302
303         if (resource.failed || resource.urlComponents.scheme === "data" || (resource.cached && resource.statusCode !== 304)) {
304             let descriptionElement = document.createElement("span");
305             descriptionElement.classList.add("description");
306             if (resource.failed)
307                 descriptionElement.textContent = WebInspector.UIString("Resource failed to load.");
308             else if (resource.urlComponents.scheme === "data")
309                 descriptionElement.textContent = WebInspector.UIString("Resource was loaded with the 'data' scheme.");
310             else
311                 descriptionElement.textContent = WebInspector.UIString("Resource was served from the cache.");
312             popoverContentElement.appendChild(descriptionElement);
313         } else {
314             let columns = {
315                 description: {
316                     width: "80px"
317                 },
318                 graph: {
319                     width: `${WebInspector.ResourceTimelineDataGridNode.PopoverGraphColumnWidthPixels}px`
320                 },
321                 duration: {
322                     width: "70px",
323                     aligned: "right"
324                 }
325             };
326
327             let popoverDataGrid = new WebInspector.DataGrid(columns);
328             popoverDataGrid.inline = true;
329             popoverDataGrid.headerVisible = false;
330             popoverContentElement.appendChild(popoverDataGrid.element);
331
332             let graphDataSource = {
333                 get secondsPerPixel() { return resource.duration / WebInspector.ResourceTimelineDataGridNode.PopoverGraphColumnWidthPixels; },
334                 get zeroTime() { return resource.firstTimestamp; },
335                 get startTime() { return resource.firstTimestamp; },
336                 get currentTime() { return this.endTime; },
337
338                 get endTime()
339                 {
340                     let endTimePadding = this.secondsPerPixel * WebInspector.TimelineRecordBar.MinimumWidthPixels;
341                     return resource.lastTimestamp + endTimePadding;
342                 }
343             };
344
345             let secondTimestamp = resource.timingData.domainLookupStart || resource.timingData.connectStart || resource.timingData.requestStart;
346             if (secondTimestamp - resource.timingData.startTime)
347                 popoverDataGrid.appendChild(new WebInspector.ResourceTimingPopoverDataGridNode(WebInspector.UIString("Stalled"), resource.timingData.startTime, secondTimestamp, graphDataSource));
348             if (resource.timingData.domainLookupStart)
349                 popoverDataGrid.appendChild(new WebInspector.ResourceTimingPopoverDataGridNode(WebInspector.UIString("DNS"), resource.timingData.domainLookupStart, resource.timingData.domainLookupEnd, graphDataSource));
350             if (resource.timingData.connectStart)
351                 popoverDataGrid.appendChild(new WebInspector.ResourceTimingPopoverDataGridNode(WebInspector.UIString("Connection"), resource.timingData.connectStart, resource.timingData.connectEnd, graphDataSource));
352             if (resource.timingData.secureConnectionStart)
353                 popoverDataGrid.appendChild(new WebInspector.ResourceTimingPopoverDataGridNode(WebInspector.UIString("Secure"), resource.timingData.secureConnectionStart, resource.timingData.connectEnd, graphDataSource));
354             popoverDataGrid.appendChild(new WebInspector.ResourceTimingPopoverDataGridNode(WebInspector.UIString("Request"), resource.timingData.requestStart, resource.timingData.responseStart, graphDataSource));
355             popoverDataGrid.appendChild(new WebInspector.ResourceTimingPopoverDataGridNode(WebInspector.UIString("Response"), resource.timingData.responseStart, resource.timingData.responseEnd, graphDataSource));
356
357             const higherResolution = true;
358             let totalData = {
359                 description: WebInspector.UIString("Total time"),
360                 duration: Number.secondsToMillisecondsString(resource.timingData.responseEnd - resource.timingData.startTime, higherResolution)
361             };
362             popoverDataGrid.appendChild(new WebInspector.DataGridNode(totalData));
363
364             popoverDataGrid.updateLayout();
365         }
366
367         if (!this.dataGrid._popover)
368             this.dataGrid._popover = new WebInspector.Popover;
369
370         let preferredEdges = [WebInspector.RectEdge.MAX_Y, WebInspector.RectEdge.MIN_Y, WebInspector.RectEdge.MIN_X];
371         this.dataGrid._popover.windowResizeHandler = () => {
372             let bounds = calculateTargetFrame();
373             this.dataGrid._popover.present(bounds.pad(2), preferredEdges);
374         };
375
376         recordBar.element.addEventListener("mouseleave", () => {
377             if (!this.dataGrid)
378                 return;
379             this.dataGrid._dismissPopoverTimeout = setTimeout(() => this.dataGrid._popover.dismiss(), WebInspector.ResourceTimelineDataGridNode.DelayedPopoverDismissalTimeout);
380         }, {once: true});
381
382         this.dataGrid._popover.presentNewContentWithFrame(popoverContentElement, targetFrame.pad(2), preferredEdges);
383     }
384 };
385
386 WebInspector.ResourceTimelineDataGridNode.PopoverGraphColumnWidthPixels = 110;
387 WebInspector.ResourceTimelineDataGridNode.DelayedPopoverDismissalTimeout = 500;