Web Inspector: Improve ES6 Class instances in Heap Snapshot instances view
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / HeapSnapshotInstanceDataGridNode.js
1 /*
2 * Copyright (C) 2016 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.HeapSnapshotInstanceDataGridNode = class HeapSnapshotInstanceDataGridNode extends WebInspector.DataGridNode
27 {
28     constructor(node, tree, edge, base)
29     {
30         // Don't treat strings as having child nodes, even if they have a Structure.
31         let hasChildren = node.hasChildren && node.className !== "string";
32
33         super(node, hasChildren);
34
35         console.assert(node instanceof WebInspector.HeapSnapshotNodeProxy);
36         console.assert(!edge || edge instanceof WebInspector.HeapSnapshotEdgeProxy);
37         console.assert(!base || base instanceof WebInspector.HeapSnapshotInstanceDataGridNode);
38
39         this._node = node;
40         this._tree = tree;
41         this._edge = edge || null;
42         this._base = base || null;
43
44         // FIXME: Make instance grid nodes copyable.
45         this.copyable = false;
46
47         if (hasChildren)
48             this.addEventListener("populate", this._populate, this);
49     }
50
51     // Static
52
53     static logHeapSnapshotNode(node)
54     {
55         let heapObjectIdentifier = node.id;
56         let shouldRevealConsole = true;
57         let text = WebInspector.UIString("Heap Snapshot Object (%s)").format("@" + heapObjectIdentifier);
58
59         node.shortestGCRootPath((gcRootPath) => {
60             if (gcRootPath.length) {
61                 gcRootPath = gcRootPath.slice().reverse();
62                 let windowIndex = gcRootPath.findIndex((x) => {
63                     return x instanceof WebInspector.HeapSnapshotNodeProxy && x.className === "Window";
64                 });
65
66                 let heapSnapshotRootPath = WebInspector.HeapSnapshotRootPath.emptyPath();
67                 for (let i = windowIndex === -1 ? 0 : windowIndex; i < gcRootPath.length; ++i) {
68                     let component = gcRootPath[i];
69                     if (component instanceof WebInspector.HeapSnapshotNodeProxy) {
70                         if (component.className === "Window")
71                             heapSnapshotRootPath = heapSnapshotRootPath.appendGlobalScopeName(component, "window");
72                     } else if (component instanceof WebInspector.HeapSnapshotEdgeProxy)
73                         heapSnapshotRootPath = heapSnapshotRootPath.appendEdge(component);
74                 }
75
76                 if (!heapSnapshotRootPath.isFullPathImpossible())
77                     text = heapSnapshotRootPath.fullPath;
78             }
79
80             if (node.className === "string") {
81                 HeapAgent.getPreview(heapObjectIdentifier, function(error, string, functionDetails, objectPreviewPayload) {
82                     let remoteObject = error ? WebInspector.RemoteObject.fromPrimitiveValue(undefined) : WebInspector.RemoteObject.fromPrimitiveValue(string);
83                     WebInspector.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, shouldRevealConsole);
84                 });
85             } else {
86                 HeapAgent.getRemoteObject(heapObjectIdentifier, WebInspector.RuntimeManager.ConsoleObjectGroup, function(error, remoteObjectPayload) {
87                     let remoteObject = error ? WebInspector.RemoteObject.fromPrimitiveValue(undefined) : WebInspector.RemoteObject.fromPayload(remoteObjectPayload, WebInspector.assumingMainTarget());
88                     WebInspector.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, shouldRevealConsole);
89                 });
90             }
91         });
92     }
93
94     // Protected
95
96     get data() { return this._node; }
97     get node() { return this._node; }
98
99     get propertyName()
100     {
101         if (!this._edge)
102             return "";
103
104         if (!this._propertyName)
105             this._propertyName = WebInspector.HeapSnapshotRootPath.pathComponentForIndividualEdge(this._edge);
106         return this._propertyName;
107     }
108
109     createCells()
110     {
111         super.createCells();
112
113         this.element.classList.add("instance");
114     }
115
116     createCellContent(columnIdentifier)
117     {
118         if (columnIdentifier === "retainedSize") {
119             let subRetainedSize = false;
120             if (this._base && !this._tree.alwaysShowRetainedSize) {
121                 if (this._isDominatedByNonBaseParent())
122                     subRetainedSize = true;
123                 else if (!this._isDominatedByBase())
124                     return emDash;
125             }
126
127             let size = this._node.retainedSize;
128             let fragment = document.createDocumentFragment();
129             let sizeElement = fragment.appendChild(document.createElement("span"));
130             sizeElement.classList.add("size");
131             sizeElement.textContent = Number.bytesToString(size);
132             let fraction = size / this._tree._heapSnapshot.totalSize;
133             let percentElement = fragment.appendChild(document.createElement("span"));
134             percentElement.classList.add("percentage");
135             percentElement.textContent = Number.percentageString(fraction);
136
137             if (subRetainedSize) {
138                 sizeElement.classList.add("sub-retained");
139                 percentElement.classList.add("sub-retained");
140             }
141
142             return fragment;
143         }
144
145         if (columnIdentifier === "size")
146             return Number.bytesToString(this._node.size);
147
148         if (columnIdentifier === "className") {
149             let {className, id, internal, isObjectType} = this._node;
150             let containerElement = document.createElement("span");
151             containerElement.addEventListener("contextmenu", this._contextMenuHandler.bind(this));
152
153             let iconElement = containerElement.appendChild(document.createElement("img"));
154             iconElement.classList.add("icon", WebInspector.HeapSnapshotClusterContentView.iconStyleClassNameForClassName(className, internal, isObjectType));
155
156             if (this._edge) {
157                 let nameElement = containerElement.appendChild(document.createElement("span"));
158                 let propertyName = this.propertyName;
159                 if (propertyName)
160                     nameElement.textContent = propertyName + ": " + this._node.className + " ";
161                 else
162                     nameElement.textContent = this._node.className + " ";
163             }
164
165             let idElement = containerElement.appendChild(document.createElement("span"));
166             idElement.classList.add("object-id");
167             idElement.textContent = "@" + id;
168             idElement.addEventListener("click", WebInspector.HeapSnapshotInstanceDataGridNode.logHeapSnapshotNode.bind(null, this._node));
169             idElement.addEventListener("mouseover", this._mouseoverHandler.bind(this));
170
171             let spacerElement = containerElement.appendChild(document.createElement("span"));
172             spacerElement.textContent = " ";
173
174             if (className === "Window" && this._node.dominatorNodeIdentifier === 0) {
175                 containerElement.append("Window ");
176                 this._populateWindowPreview(containerElement);
177             } else
178                 this._populatePreview(containerElement);
179
180             return containerElement;
181         }
182
183         return super.createCellContent(columnIdentifier);
184     }
185
186     sort()
187     {
188         let children = this.children;
189         children.sort(this._tree._sortComparator);
190
191         for (let i = 0; i < children.length; ++i) {
192             children[i]._recalculateSiblings(i);
193             children[i].sort();
194         }
195     }
196
197     // Private
198
199     _isDominatedByBase()
200     {
201         return this._node.dominatorNodeIdentifier === this._base.node.id;
202     }
203
204     _isDominatedByNonBaseParent()
205     {
206         for (let p = this.parent; p; p = p.parent) {
207             if (p === this._base)
208                 return false;
209             if (this._node.dominatorNodeIdentifier === p.node.id)
210                 return true;
211         }
212
213         return false;
214     }
215
216     _populate()
217     {
218         this.removeEventListener("populate", this._populate, this);
219
220         function propertyName(edge) {
221             return edge ? WebInspector.HeapSnapshotRootPath.pathComponentForIndividualEdge(edge) : "";
222         }
223
224         this._node.retainedNodes((instances, edges) => {
225             // Reference edge from instance so we can get it after sorting.
226             for (let i = 0; i < instances.length; ++i)
227                 instances[i].__edge = edges[i];
228
229             instances.sort((a, b) => {
230                 let fakeDataGridNodeA = {data: a, propertyName: propertyName(a.__edge)};
231                 let fakeDataGridNodeB = {data: b, propertyName: propertyName(b.__edge)};
232                 return this._tree._sortComparator(fakeDataGridNodeA, fakeDataGridNodeB);
233             });
234
235             // FIXME: This should gracefully handle a node that references many objects.
236
237             for (let instance of instances) {
238                 if (instance.__edge && instance.__edge.isPrivateSymbol())
239                     continue;
240
241                 this.appendChild(new WebInspector.HeapSnapshotInstanceDataGridNode(instance, this._tree, instance.__edge, this._base || this));
242             }
243         });
244     }
245
246     _contextMenuHandler(event)
247     {
248         let contextMenu = WebInspector.ContextMenu.createFromEvent(event);
249         contextMenu.appendSeparator();
250         contextMenu.appendItem(WebInspector.UIString("Log Value"), WebInspector.HeapSnapshotInstanceDataGridNode.logHeapSnapshotNode.bind(null, this._node));
251     }
252
253     _populateError(containerElement)
254     {
255         if (this._node.internal)
256             return;
257
258         let previewErrorElement = containerElement.appendChild(document.createElement("span"));
259         previewErrorElement.classList.add("preview-error");
260         previewErrorElement.textContent = WebInspector.UIString("No preview available");
261     }
262
263     _populateWindowPreview(containerElement)
264     {
265         HeapAgent.getRemoteObject(this._node.id, (error, remoteObjectPayload) => {
266             if (error) {
267                 this._populateError(containerElement);
268                 return;
269             }
270
271             function inspectedPage_window_getLocationHref() {
272                 return this.location.href;
273             }
274
275             let remoteObject = WebInspector.RemoteObject.fromPayload(remoteObjectPayload, WebInspector.assumingMainTarget());
276             remoteObject.callFunctionJSON(inspectedPage_window_getLocationHref, undefined, (href) => {
277                 remoteObject.release();
278
279                 if (!href)
280                     this._populateError(containerElement);
281                 else {
282                     let primitiveRemoteObject = WebInspector.RemoteObject.fromPrimitiveValue(href);
283                     containerElement.appendChild(WebInspector.FormattedValue.createElementForRemoteObject(primitiveRemoteObject));
284                 }
285             });
286         });
287     }
288
289     _populatePreview(containerElement)
290     {
291         HeapAgent.getPreview(this._node.id, (error, string, functionDetails, objectPreviewPayload) => {
292             if (error) {
293                 this._populateError(containerElement);
294                 return;
295             }
296
297             if (string) {
298                 let primitiveRemoteObject = WebInspector.RemoteObject.fromPrimitiveValue(string);
299                 containerElement.appendChild(WebInspector.FormattedValue.createElementForRemoteObject(primitiveRemoteObject));
300                 return;
301             }
302
303             if (functionDetails) {
304                 let {location, name, displayName} = functionDetails;
305                 let functionNameElement = containerElement.appendChild(document.createElement("span"));
306                 functionNameElement.classList.add("function-name");
307                 functionNameElement.textContent = name || displayName || WebInspector.UIString("(anonymous function)");
308                 let sourceCode = WebInspector.debuggerManager.scriptForIdentifier(location.scriptId, WebInspector.assumingMainTarget());
309                 if (sourceCode) {
310                     let locationElement = containerElement.appendChild(document.createElement("span"));
311                     locationElement.classList.add("location");
312                     let sourceCodeLocation = sourceCode.createSourceCodeLocation(location.lineNumber, location.columnNumber);
313                     sourceCodeLocation.populateLiveDisplayLocationString(locationElement, "textContent", WebInspector.SourceCodeLocation.ColumnStyle.Hidden, WebInspector.SourceCodeLocation.NameStyle.Short);
314
315                     const options = {
316                         dontFloat: true,
317                         useGoToArrowButton: true,
318                         ignoreNetworkTab: true,
319                         ignoreSearchTab: true,
320                     };
321                     let goToArrowButtonLink = WebInspector.createSourceCodeLocationLink(sourceCodeLocation, options);
322                     containerElement.appendChild(goToArrowButtonLink);
323                 }
324                 return;
325             }
326
327             if (objectPreviewPayload) {
328                 let objectPreview = WebInspector.ObjectPreview.fromPayload(objectPreviewPayload);
329                 let previewElement = WebInspector.FormattedValue.createObjectPreviewOrFormattedValueForObjectPreview(objectPreview);
330                 containerElement.appendChild(previewElement);
331                 return;
332             }
333         });
334     }
335
336     _mouseoverHandler(event)
337     {
338         let targetFrame = WebInspector.Rect.rectFromClientRect(event.target.getBoundingClientRect());
339         if (!targetFrame.size.width && !targetFrame.size.height)
340             return;
341
342         if (this._tree.popoverGridNode === this._node)
343             return;
344
345         this._tree.popoverGridNode = this._node;
346         this._tree.popoverTargetElement = event.target;
347
348         let popoverContentElement = document.createElement("div");
349         popoverContentElement.classList.add("heap-snapshot", "heap-snapshot-instance-popover-content");
350
351         function appendTitle(node) {
352             let idElement = document.createElement("span");
353             idElement.classList.add("object-id");
354             idElement.textContent = "@" + node.id;
355             idElement.addEventListener("click", WebInspector.HeapSnapshotInstanceDataGridNode.logHeapSnapshotNode.bind(null, node));
356
357             let title = popoverContentElement.appendChild(document.createElement("div"));
358             title.classList.add("title");
359             let localizedString = WebInspector.UIString("Shortest property path to %s").format("@@@");
360             let [before, after] = localizedString.split(/\s*@@@\s*/);
361             title.append(before + " ", idElement, " " + after);
362         }
363
364         function appendPath(path) {
365             let tableContainer = popoverContentElement.appendChild(document.createElement("div"));
366             tableContainer.classList.add("table-container");
367
368             let tableElement = tableContainer.appendChild(document.createElement("table"));
369
370             path = path.slice().reverse();
371             let windowIndex = path.findIndex((x) => {
372                 return x instanceof WebInspector.HeapSnapshotNodeProxy && x.className === "Window";
373             });
374
375             let edge = null;
376             for (let i = windowIndex === -1 ? 0 : windowIndex; i < path.length; ++i) {
377                 let component = path[i];
378                 if (component instanceof WebInspector.HeapSnapshotEdgeProxy) {
379                     edge = component;
380                     continue;
381                 }
382                 appendPathRow(tableElement, edge, component);
383                 edge = null;
384             }
385         }
386
387         function appendPathRow(tableElement, edge, node) {
388             let tableRow = tableElement.appendChild(document.createElement("tr"));
389
390             // Edge name.
391             let pathDataElement = tableRow.appendChild(document.createElement("td"));
392             pathDataElement.classList.add("edge-name");
393
394             if (node.className === "Window")
395                 pathDataElement.textContent = "window";
396             else if (edge) {
397                 let edgeString = stringifyEdge(edge);
398                 pathDataElement.textContent = typeof edgeString === "string" ? edgeString : emDash;
399             } else
400                 pathDataElement.textContent = emDash;
401
402             if (pathDataElement.textContent.length > 10)
403                 pathDataElement.title = pathDataElement.textContent;
404
405             // Object.
406             let objectDataElement = tableRow.appendChild(document.createElement("td"));
407             objectDataElement.classList.add("object-data");
408
409             let containerElement = objectDataElement.appendChild(document.createElement("div"));
410             containerElement.classList.add("node");
411
412             let iconElement = containerElement.appendChild(document.createElement("img"));
413             iconElement.classList.add("icon", WebInspector.HeapSnapshotClusterContentView.iconStyleClassNameForClassName(node.className, node.internal, node.isObjectType));
414
415             let classNameElement = containerElement.appendChild(document.createElement("span"));
416             classNameElement.textContent = sanitizeClassName(node.className) + " ";
417
418             let idElement = containerElement.appendChild(document.createElement("span"));
419             idElement.classList.add("object-id");
420             idElement.textContent = "@" + node.id;
421             idElement.addEventListener("click", WebInspector.HeapSnapshotInstanceDataGridNode.logHeapSnapshotNode.bind(null, node));
422
423             // Extra.
424             if (node.className === "Function") {
425                 let goToArrowPlaceHolderElement = containerElement.appendChild(document.createElement("span"));
426                 goToArrowPlaceHolderElement.style.display = "inline-block";
427                 goToArrowPlaceHolderElement.style.width = "10px";
428                 HeapAgent.getPreview(node.id, function(error, string, functionDetails, objectPreviewPayload) {
429                     if (functionDetails) {
430                         let location = functionDetails.location;
431                         let sourceCode = WebInspector.debuggerManager.scriptForIdentifier(location.scriptId, WebInspector.assumingMainTarget());
432                         if (sourceCode) {
433                             let sourceCodeLocation = sourceCode.createSourceCodeLocation(location.lineNumber, location.columnNumber);
434
435                             const options = {
436                                 dontFloat: true,
437                                 useGoToArrowButton: true,
438                                 ignoreNetworkTab: true,
439                                 ignoreSearchTab: true,
440                             };
441                             let goToArrowButtonLink = WebInspector.createSourceCodeLocationLink(sourceCodeLocation, options);
442                             containerElement.replaceChild(goToArrowButtonLink, goToArrowPlaceHolderElement);
443                         }
444                     }
445                 });
446             }
447         }
448
449         function sanitizeClassName(className) {
450             if (className.endsWith("LexicalEnvironment"))
451                 return WebInspector.UIString("Scope");
452             return className;
453         }
454
455         function stringifyEdge(edge) {
456             switch (edge.type) {
457             case WebInspector.HeapSnapshotEdgeProxy.EdgeType.Property:
458             case WebInspector.HeapSnapshotEdgeProxy.EdgeType.Variable:
459                 if (/^(?![0-9])\w+$/.test(edge.data))
460                     return edge.data;
461                 return "[" + doubleQuotedString(edge.data) + "]";
462             case WebInspector.HeapSnapshotEdgeProxy.EdgeType.Index:
463                 return "[" + edge.data + "]";
464             case WebInspector.HeapSnapshotEdgeProxy.EdgeType.Internal:
465             default:
466                 return null;
467             }
468         }
469
470         this._node.shortestGCRootPath((path) => {
471             if (!this._tree.visible)
472                 return;
473
474             if (path.length) {
475                 appendTitle(this._node);
476                 appendPath(path);
477             } else if (this._node.gcRoot) {
478                 let textElement = popoverContentElement.appendChild(document.createElement("div"));
479                 textElement.textContent = WebInspector.UIString("This object is a root");
480             } else {
481                 let emptyElement = popoverContentElement.appendChild(document.createElement("div"));
482                 emptyElement.textContent = WebInspector.UIString("This object is referenced by internal objects");
483             }
484
485             this._tree.popover.presentNewContentWithFrame(popoverContentElement, targetFrame.pad(2), [WebInspector.RectEdge.MAX_Y, WebInspector.RectEdge.MIN_Y, WebInspector.RectEdge.MAX_X]);
486         });
487     }
488 };