4956a10b704388f377c707bf5277be86b52906a3
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / TimelineDataGridNode.js
1 /*
2  * Copyright (C) 2014, 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.TimelineDataGridNode = class TimelineDataGridNode extends WebInspector.DataGridNode
27 {
28     constructor(includesGraph, graphDataSource, hasChildren)
29     {
30         super({}, hasChildren);
31
32         this.copyable = false;
33
34         this._includesGraph = includesGraph || false;
35         this._graphDataSource = graphDataSource || null;
36
37         if (graphDataSource) {
38             this._graphContainerElement = document.createElement("div");
39             this._timelineRecordBars = [];
40         }
41     }
42
43     // Public
44
45     get records()
46     {
47         // Implemented by subclasses.
48         return [];
49     }
50
51     get graphDataSource()
52     {
53         return this._graphDataSource;
54     }
55
56     get data()
57     {
58         if (!this._graphDataSource)
59             return {};
60
61         var records = this.records || [];
62         return {graph: records.length ? records[0].startTime : 0};
63     }
64
65     collapse()
66     {
67         super.collapse();
68
69         if (!this._graphDataSource || !this.revealed)
70             return;
71
72         // Refresh to show child bars in our graph now that we collapsed.
73         this.refreshGraph();
74     }
75
76     expand()
77     {
78         super.expand();
79
80         if (!this._graphDataSource || !this.revealed)
81             return;
82
83         // Refresh to remove child bars from our graph now that we expanded.
84         this.refreshGraph();
85
86         // Refresh child graphs since they haven't been updating while we were collapsed.
87         var childNode = this.children[0];
88         while (childNode) {
89             if (childNode instanceof WebInspector.TimelineDataGridNode)
90                 childNode.refreshGraph();
91             childNode = childNode.traverseNextNode(true, this);
92         }
93     }
94
95     createCellContent(columnIdentifier, cell)
96     {
97         if (columnIdentifier === "graph" && this._graphDataSource) {
98             this.needsGraphRefresh();
99             return this._graphContainerElement;
100         }
101
102         var value = this.data[columnIdentifier];
103         if (!value)
104             return emDash;
105
106         if (value instanceof WebInspector.SourceCodeLocation) {
107             if (value.sourceCode instanceof WebInspector.Resource) {
108                 cell.classList.add(WebInspector.ResourceTreeElement.ResourceIconStyleClassName);
109                 cell.classList.add(value.sourceCode.type);
110             } else if (value.sourceCode instanceof WebInspector.Script) {
111                 if (value.sourceCode.url) {
112                     cell.classList.add(WebInspector.ResourceTreeElement.ResourceIconStyleClassName);
113                     cell.classList.add(WebInspector.Resource.Type.Script);
114                 } else
115                     cell.classList.add(WebInspector.ScriptTreeElement.AnonymousScriptIconStyleClassName);
116             } else
117                 console.error("Unknown SourceCode subclass.");
118
119             // Give the whole cell a tooltip and keep it up to date.
120             value.populateLiveDisplayLocationTooltip(cell);
121
122             var fragment = document.createDocumentFragment();
123
124             var goToArrowButtonLink = WebInspector.createSourceCodeLocationLink(value, false, true);
125             fragment.appendChild(goToArrowButtonLink);
126
127             var titleElement = document.createElement("span");
128             value.populateLiveDisplayLocationString(titleElement, "textContent");
129             fragment.appendChild(titleElement);
130
131             return fragment;
132         }
133
134         if (value instanceof WebInspector.CallFrame) {
135             var callFrame = value;
136
137             var isAnonymousFunction = false;
138             var functionName = callFrame.functionName;
139             if (!functionName) {
140                 functionName = WebInspector.UIString("(anonymous function)");
141                 isAnonymousFunction = true;
142             }
143
144             cell.classList.add(WebInspector.CallFrameView.FunctionIconStyleClassName);
145
146             var fragment = document.createDocumentFragment();
147
148             if (callFrame.sourceCodeLocation && callFrame.sourceCodeLocation.sourceCode) {
149                 // Give the whole cell a tooltip and keep it up to date.
150                 callFrame.sourceCodeLocation.populateLiveDisplayLocationTooltip(cell);
151
152                 var goToArrowButtonLink = WebInspector.createSourceCodeLocationLink(callFrame.sourceCodeLocation, false, true);
153                 fragment.appendChild(goToArrowButtonLink);
154
155                 if (isAnonymousFunction) {
156                     // For anonymous functions we show the resource or script icon and name.
157                     if (callFrame.sourceCodeLocation.sourceCode instanceof WebInspector.Resource) {
158                         cell.classList.add(WebInspector.ResourceTreeElement.ResourceIconStyleClassName);
159                         cell.classList.add(callFrame.sourceCodeLocation.sourceCode.type);
160                     } else if (callFrame.sourceCodeLocation.sourceCode instanceof WebInspector.Script) {
161                         if (callFrame.sourceCodeLocation.sourceCode.url) {
162                             cell.classList.add(WebInspector.ResourceTreeElement.ResourceIconStyleClassName);
163                             cell.classList.add(WebInspector.Resource.Type.Script);
164                         } else
165                             cell.classList.add(WebInspector.ScriptTreeElement.AnonymousScriptIconStyleClassName);
166                     } else
167                         console.error("Unknown SourceCode subclass.");
168
169                     var titleElement = document.createElement("span");
170                     callFrame.sourceCodeLocation.populateLiveDisplayLocationString(titleElement, "textContent");
171
172                     fragment.appendChild(titleElement);
173                 } else {
174                     // Show the function name and icon.
175                     cell.classList.add(WebInspector.CallFrameView.FunctionIconStyleClassName);
176
177                     fragment.append(functionName);
178
179                     var subtitleElement = document.createElement("span");
180                     subtitleElement.classList.add("subtitle");
181                     callFrame.sourceCodeLocation.populateLiveDisplayLocationString(subtitleElement, "textContent");
182
183                     fragment.appendChild(subtitleElement);
184                 }
185
186                 return fragment;
187             }
188
189             var icon = document.createElement("div");
190             icon.classList.add("icon");
191
192             fragment.append(icon, functionName);
193
194             return fragment;
195         }
196
197         return super.createCellContent(columnIdentifier, cell);
198     }
199
200     refresh()
201     {
202         if (this._graphDataSource && this._includesGraph)
203             this.needsGraphRefresh();
204
205         super.refresh();
206     }
207
208     refreshGraph()
209     {
210         if (!this._graphDataSource)
211             return;
212
213         if (this._scheduledGraphRefreshIdentifier) {
214             cancelAnimationFrame(this._scheduledGraphRefreshIdentifier);
215             this._scheduledGraphRefreshIdentifier = undefined;
216         }
217
218         // We are not visible, but an ancestor will draw our graph.
219         // They need notified by using our needsGraphRefresh.
220         console.assert(this.revealed);
221         if (!this.revealed)
222             return;
223
224         var secondsPerPixel = this._graphDataSource.secondsPerPixel;
225         console.assert(isFinite(secondsPerPixel) && secondsPerPixel > 0);
226
227         var recordBarIndex = 0;
228
229         function createBar(records, renderMode)
230         {
231             var timelineRecordBar = this._timelineRecordBars[recordBarIndex];
232             if (!timelineRecordBar)
233                 timelineRecordBar = this._timelineRecordBars[recordBarIndex] = new WebInspector.TimelineRecordBar(records, renderMode);
234             else {
235                 timelineRecordBar.renderMode = renderMode;
236                 timelineRecordBar.records = records;
237             }
238             timelineRecordBar.refresh(this._graphDataSource);
239             if (!timelineRecordBar.element.parentNode)
240                 this._graphContainerElement.appendChild(timelineRecordBar.element);
241             ++recordBarIndex;
242         }
243
244         function collectRecordsByType(records, recordsByTypeMap)
245         {
246             for (var record of records) {
247                 var typedRecords = recordsByTypeMap.get(record.type);
248                 if (!typedRecords) {
249                     typedRecords = [];
250                     recordsByTypeMap.set(record.type, typedRecords);
251                 }
252
253                 typedRecords.push(record);
254             }
255         }
256
257         var boundCreateBar = createBar.bind(this);
258
259         if (this.expanded) {
260             // When expanded just use the records for this node.
261             WebInspector.TimelineRecordBar.createCombinedBars(this.records, secondsPerPixel, this._graphDataSource, boundCreateBar);
262         } else {
263             // When collapsed use the records for this node and its descendants.
264             // To share bars better, group records by type.
265
266             var recordTypeMap = new Map;
267             collectRecordsByType(this.records, recordTypeMap);
268
269             var childNode = this.children[0];
270             while (childNode) {
271                 if (childNode instanceof WebInspector.TimelineDataGridNode)
272                     collectRecordsByType(childNode.records, recordTypeMap);
273                 childNode = childNode.traverseNextNode(false, this);
274             }
275
276             for (var records of recordTypeMap.values())
277                 WebInspector.TimelineRecordBar.createCombinedBars(records, secondsPerPixel, this._graphDataSource, boundCreateBar);
278         }
279
280         // Remove the remaining unused TimelineRecordBars.
281         for (; recordBarIndex < this._timelineRecordBars.length; ++recordBarIndex) {
282             this._timelineRecordBars[recordBarIndex].records = null;
283             this._timelineRecordBars[recordBarIndex].element.remove();
284         }
285     }
286
287     needsGraphRefresh()
288     {
289         if (!this.revealed) {
290             // We are not visible, but an ancestor will be drawing our graph.
291             // Notify the next visible ancestor that their graph needs to refresh.
292             var ancestor = this;
293             while (ancestor && !ancestor.root) {
294                 if (ancestor.revealed && ancestor instanceof WebInspector.TimelineDataGridNode) {
295                     ancestor.needsGraphRefresh();
296                     return;
297                 }
298
299                 ancestor = ancestor.parent;
300             }
301
302             return;
303         }
304
305         if (!this._graphDataSource || this._scheduledGraphRefreshIdentifier)
306             return;
307
308         this._scheduledGraphRefreshIdentifier = requestAnimationFrame(this.refreshGraph.bind(this));
309     }
310
311     // Protected
312
313     createGoToArrowButton(cellElement, callback)
314     {
315         function buttonClicked(event)
316         {
317             if (this.hidden || !this.revealed)
318                 return;
319
320             event.stopPropagation();
321
322             callback(this, cellElement.__columnIdentifier);
323         }
324
325         let button = WebInspector.createGoToArrowButton();
326         button.addEventListener("click", buttonClicked.bind(this));
327
328         let contentElement = cellElement.firstChild;
329         contentElement.appendChild(button);
330     }
331
332     isRecordVisible(record)
333     {
334         if (!this._graphDataSource)
335             return false;
336
337         if (isNaN(record.startTime))
338             return false;
339
340         // If this bar is completely before the bounds of the graph, not visible.
341         if (record.endTime < this.graphDataSource.startTime)
342             return false;
343
344         // If this record is completely after the current time or end time, not visible.
345         if (record.startTime > this.graphDataSource.currentTime || record.startTime > this.graphDataSource.endTime)
346             return false;
347
348         return true;
349     }
350 };