Web Inspector: Import / Export Heap Snapshots
authorcommit-queue@webkit.org <commit-queue@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sat, 9 Feb 2019 00:25:46 +0000 (00:25 +0000)
committercommit-queue@webkit.org <commit-queue@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sat, 9 Feb 2019 00:25:46 +0000 (00:25 +0000)
https://bugs.webkit.org/show_bug.cgi?id=194448
<rdar://problem/47928093>

Patch by Joseph Pecoraro <pecoraro@apple.com> on 2019-02-08
Reviewed by Devin Rousso.

Source/WebInspectorUI:

* Localizations/en.lproj/localizedStrings.js:
New strings.

* UserInterface/Proxies/HeapSnapshotProxy.js:
(WI.HeapSnapshotProxy):
(WI.HeapSnapshotProxy.deserialize):
(WI.HeapSnapshotProxy.prototype.get imported):
(WI.HeapSnapshotProxy.prototype.get snapshotStringData):
(WI.HeapSnapshotProxy.prototype.set snapshotStringData):
Include an "imported" state on the HeapSnapshot and allow for
stashing the snapshotStringData on the main thread side.

* UserInterface/Proxies/HeapSnapshotWorkerProxy.js:
(WI.HeapSnapshotWorkerProxy.prototype.createImportedSnapshot):
* UserInterface/Workers/HeapSnapshot/HeapSnapshotWorker.js:
(HeapSnapshotWorker.prototype.clearSnapshots):
(HeapSnapshotWorker.prototype.createSnapshot):
Provide a specialized way to create an imported HeapSnapshot.
Track imported snapshots separately since they won't want to
be searched for live/dead objects due to active recording GCs.

* UserInterface/Workers/HeapSnapshot/HeapSnapshot.js:
(HeapSnapshot):
(HeapSnapshot.updateCategoriesAndMetadata):
(HeapSnapshot.allocationBucketCounts):
(HeapSnapshot.instancesWithClassName):
(HeapSnapshot.prototype.nodeWithIdentifier):
(HeapSnapshot.prototype.dominatedNodes):
(HeapSnapshot.prototype.retainedNodes):
(HeapSnapshot.prototype.retainers):
(HeapSnapshot.prototype.updateDeadNodesAndGatherCollectionData):
(HeapSnapshot.prototype.serialize):
(HeapSnapshot.prototype.serializeNode):
(HeapSnapshot.prototype._buildPostOrderIndexes):
(HeapSnapshot.prototype._buildDominatorIndexes):
(HeapSnapshot.prototype._buildRetainedSizes):
(HeapSnapshot.prototype._gcRootPathes.visitNode):
(HeapSnapshot.prototype._gcRootPathes):
Construct a HeapSnapshot knowinng whether or not it is imported.
Imported snapshots may be the "GCDebugging" snapshot type which
differs from "Inspector" by the number of node fields. So keep
the node field count a member instead of a global constant
in order to work with both snapshot types.

* UserInterface/Models/HeapAllocationsInstrument.js:
(WI.HeapAllocationsInstrument.prototype._takeHeapSnapshot):
* UserInterface/Protocol/ConsoleObserver.js:
(WI.ConsoleObserver.prototype.heapSnapshot):
* UserInterface/Protocol/HeapObserver.js:
(WI.HeapObserver.prototype.trackingStart):
(WI.HeapObserver.prototype.trackingComplete):
Stash the original string JSON data on the main thread side
where we already have the data.

* UserInterface/Views/HeapAllocationsTimelineOverviewGraph.js:
(WI.HeapAllocationsTimelineOverviewGraph.prototype.layout):
Don't show [S] icons for imported snapshots with no timestamp.

* UserInterface/Views/HeapAllocationsTimelineView.js:
(WI.HeapAllocationsTimelineView):
(WI.HeapAllocationsTimelineView.prototype.get navigationItems):
(WI.HeapAllocationsTimelineView.prototype._importButtonNavigationItemClicked):
(WI.HeapAllocationsTimelineView.prototype._takeHeapSnapshotClicked):
Import button that just creates a new snapshot.

* UserInterface/Views/HeapSnapshotContentView.js:
(WI.HeapSnapshotContentView):
(WI.HeapSnapshotContentView.prototype.get navigationItems):
(WI.HeapSnapshotContentView.prototype.get supportsSave):
(WI.HeapSnapshotContentView.prototype.get saveData):
(WI.HeapSnapshotContentView.prototype._exportSnapshot):
Export button that saves the original data.

* UserInterface/Views/TimelineTabContentView.js:
(WI.TimelineTabContentView.displayNameForRecord):
Specialized display string for imported snapshots.

LayoutTests:

* inspector/heap/imported-snapshot-expected.txt: Added.
* inspector/heap/imported-snapshot.html: Added.
* platform/mac/TestExpectations:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@241219 268f45cc-cd09-0410-ab3c-d52691b4dbfc

18 files changed:
LayoutTests/ChangeLog
LayoutTests/inspector/heap/imported-snapshot-expected.txt [new file with mode: 0644]
LayoutTests/inspector/heap/imported-snapshot.html [new file with mode: 0644]
LayoutTests/platform/mac/TestExpectations
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
Source/WebInspectorUI/UserInterface/Models/HeapAllocationsInstrument.js
Source/WebInspectorUI/UserInterface/Protocol/ConsoleObserver.js
Source/WebInspectorUI/UserInterface/Protocol/HeapObserver.js
Source/WebInspectorUI/UserInterface/Proxies/HeapSnapshotProxy.js
Source/WebInspectorUI/UserInterface/Proxies/HeapSnapshotWorkerProxy.js
Source/WebInspectorUI/UserInterface/Views/HeapAllocationsTimelineOverviewGraph.js
Source/WebInspectorUI/UserInterface/Views/HeapAllocationsTimelineView.js
Source/WebInspectorUI/UserInterface/Views/HeapSnapshotContentView.js
Source/WebInspectorUI/UserInterface/Views/RecordingContentView.js
Source/WebInspectorUI/UserInterface/Views/TimelineTabContentView.js
Source/WebInspectorUI/UserInterface/Workers/HeapSnapshot/HeapSnapshot.js
Source/WebInspectorUI/UserInterface/Workers/HeapSnapshot/HeapSnapshotWorker.js

index c7af54c..fe605e3 100644 (file)
@@ -1,3 +1,15 @@
+2019-02-08  Joseph Pecoraro  <pecoraro@apple.com>
+
+        Web Inspector: Import / Export Heap Snapshots
+        https://bugs.webkit.org/show_bug.cgi?id=194448
+        <rdar://problem/47928093>
+
+        Reviewed by Devin Rousso.
+
+        * inspector/heap/imported-snapshot-expected.txt: Added.
+        * inspector/heap/imported-snapshot.html: Added.
+        * platform/mac/TestExpectations:
+
 2019-02-08  Nikita Vasilyev  <nvasilyev@apple.com>
 
         Web Inspector: Styles: close unbalanced quotes and parenthesis when editing values
diff --git a/LayoutTests/inspector/heap/imported-snapshot-expected.txt b/LayoutTests/inspector/heap/imported-snapshot-expected.txt
new file mode 100644 (file)
index 0000000..a797d7e
--- /dev/null
@@ -0,0 +1,11 @@
+Test for an imported HeapSnapshot.
+
+
+== Running test suite: HeapSnapshot.imported
+-- Running test case: HeapSnapshot.imported
+PASS: Should not have an error creating a snapshot.
+PASS: Normal snapshot is not imported.
+PASS: Normal snapshot title should not be set.
+PASS: Imported snapshot is imported.
+PASS: Imported snapshot title should be set.
+
diff --git a/LayoutTests/inspector/heap/imported-snapshot.html b/LayoutTests/inspector/heap/imported-snapshot.html
new file mode 100644 (file)
index 0000000..9e50cb2
--- /dev/null
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="../../http/tests/inspector/resources/inspector-test.js"></script>
+<script>
+function test()
+{
+    let suite = InspectorTest.createAsyncSuite("HeapSnapshot.imported");
+
+    suite.addTestCase({
+        name: "HeapSnapshot.imported",
+        description: "createSnapshot() and createImportedSnapshot() differences.",
+        test(resolve, reject) {
+            HeapAgent.snapshot((error, timestamp, snapshotStringData) => {
+                InspectorTest.expectThat(!error, "Should not have an error creating a snapshot.");
+
+                const importedTitle = "Imported Snapshot";
+                let workerProxy = WI.HeapSnapshotWorkerProxy.singleton();
+                workerProxy.createSnapshot(snapshotStringData, ({objectId, snapshot: serializedSnapshot}) => {
+                    let snapshot = WI.HeapSnapshotProxy.deserialize(objectId, serializedSnapshot);
+                    snapshot.snapshotStringData = snapshotStringData;
+                    workerProxy.createImportedSnapshot(snapshotStringData, importedTitle, ({objectId, snapshot: serializedSnapshot}) => {
+                        let importedSnapshot = WI.HeapSnapshotProxy.deserialize(objectId, serializedSnapshot);
+                        importedSnapshot.snapshotStringData = snapshotStringData;
+
+                        InspectorTest.expectFalse(snapshot.imported, "Normal snapshot is not imported.");
+                        InspectorTest.expectNull(snapshot.title, "Normal snapshot title should not be set.");
+                        InspectorTest.expectTrue(importedSnapshot.imported, "Imported snapshot is imported.");
+                        InspectorTest.expectEqual(importedSnapshot.title, importedTitle, "Imported snapshot title should be set.");
+                        resolve();
+                    });
+                });
+            });
+        }
+    });
+
+    suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body onload="runTest()">
+<p>Test for an imported HeapSnapshot.</p>
+</body>
+</html>
index cad5d12..c3a9484 100644 (file)
@@ -1086,6 +1086,7 @@ webkit.org/b/169228 inspector/worker/console-basic.html [ Pass Timeout ]
 webkit.org/b/164872 inspector/worker/debugger-multiple-targets-pause.html [ Pass Failure Timeout ]
 webkit.org/b/165582 inspector/worker/debugger-scripts.html [ Pass Failure ]
 webkit.org/b/167203 inspector/worker/debugger-shared-breakpoint.html [ Pass Failure Timeout ]
+webkit.org/b/155607 inspector/heap/imported-snapshot.html [ Pass Timeout ]
 webkit.org/b/155607 inspector/heap/snapshot.html [ Pass Timeout ]
 webkit.org/b/143719 inspector/console/console-api.html [ Pass Timeout ]
 webkit.org/b/156078 inspector/console/heapSnapshot.html [ Pass Timeout ]
index d29ddee..df9d637 100644 (file)
@@ -1,5 +1,90 @@
 2019-02-08  Joseph Pecoraro  <pecoraro@apple.com>
 
+        Web Inspector: Import / Export Heap Snapshots
+        https://bugs.webkit.org/show_bug.cgi?id=194448
+        <rdar://problem/47928093>
+
+        Reviewed by Devin Rousso.
+
+        * Localizations/en.lproj/localizedStrings.js:
+        New strings.
+
+        * UserInterface/Proxies/HeapSnapshotProxy.js:
+        (WI.HeapSnapshotProxy):
+        (WI.HeapSnapshotProxy.deserialize):
+        (WI.HeapSnapshotProxy.prototype.get imported):
+        (WI.HeapSnapshotProxy.prototype.get snapshotStringData):
+        (WI.HeapSnapshotProxy.prototype.set snapshotStringData):
+        Include an "imported" state on the HeapSnapshot and allow for
+        stashing the snapshotStringData on the main thread side.
+
+        * UserInterface/Proxies/HeapSnapshotWorkerProxy.js:
+        (WI.HeapSnapshotWorkerProxy.prototype.createImportedSnapshot):
+        * UserInterface/Workers/HeapSnapshot/HeapSnapshotWorker.js:
+        (HeapSnapshotWorker.prototype.clearSnapshots):
+        (HeapSnapshotWorker.prototype.createSnapshot):
+        Provide a specialized way to create an imported HeapSnapshot.
+        Track imported snapshots separately since they won't want to
+        be searched for live/dead objects due to active recording GCs.
+
+        * UserInterface/Workers/HeapSnapshot/HeapSnapshot.js:
+        (HeapSnapshot):
+        (HeapSnapshot.updateCategoriesAndMetadata):
+        (HeapSnapshot.allocationBucketCounts):
+        (HeapSnapshot.instancesWithClassName):
+        (HeapSnapshot.prototype.nodeWithIdentifier):
+        (HeapSnapshot.prototype.dominatedNodes):
+        (HeapSnapshot.prototype.retainedNodes):
+        (HeapSnapshot.prototype.retainers):
+        (HeapSnapshot.prototype.updateDeadNodesAndGatherCollectionData):
+        (HeapSnapshot.prototype.serialize):
+        (HeapSnapshot.prototype.serializeNode):
+        (HeapSnapshot.prototype._buildPostOrderIndexes):
+        (HeapSnapshot.prototype._buildDominatorIndexes):
+        (HeapSnapshot.prototype._buildRetainedSizes):
+        (HeapSnapshot.prototype._gcRootPathes.visitNode):
+        (HeapSnapshot.prototype._gcRootPathes):
+        Construct a HeapSnapshot knowinng whether or not it is imported.
+        Imported snapshots may be the "GCDebugging" snapshot type which
+        differs from "Inspector" by the number of node fields. So keep
+        the node field count a member instead of a global constant
+        in order to work with both snapshot types.
+
+        * UserInterface/Models/HeapAllocationsInstrument.js:
+        (WI.HeapAllocationsInstrument.prototype._takeHeapSnapshot):
+        * UserInterface/Protocol/ConsoleObserver.js:
+        (WI.ConsoleObserver.prototype.heapSnapshot):
+        * UserInterface/Protocol/HeapObserver.js:
+        (WI.HeapObserver.prototype.trackingStart):
+        (WI.HeapObserver.prototype.trackingComplete):
+        Stash the original string JSON data on the main thread side
+        where we already have the data.
+
+        * UserInterface/Views/HeapAllocationsTimelineOverviewGraph.js:
+        (WI.HeapAllocationsTimelineOverviewGraph.prototype.layout):
+        Don't show [S] icons for imported snapshots with no timestamp.
+
+        * UserInterface/Views/HeapAllocationsTimelineView.js:
+        (WI.HeapAllocationsTimelineView):
+        (WI.HeapAllocationsTimelineView.prototype.get navigationItems):
+        (WI.HeapAllocationsTimelineView.prototype._importButtonNavigationItemClicked):
+        (WI.HeapAllocationsTimelineView.prototype._takeHeapSnapshotClicked):
+        Import button that just creates a new snapshot.
+
+        * UserInterface/Views/HeapSnapshotContentView.js:
+        (WI.HeapSnapshotContentView):
+        (WI.HeapSnapshotContentView.prototype.get navigationItems):
+        (WI.HeapSnapshotContentView.prototype.get supportsSave):
+        (WI.HeapSnapshotContentView.prototype.get saveData):
+        (WI.HeapSnapshotContentView.prototype._exportSnapshot):
+        Export button that saves the original data.
+
+        * UserInterface/Views/TimelineTabContentView.js:
+        (WI.TimelineTabContentView.displayNameForRecord):
+        Specialized display string for imported snapshots.
+
+2019-02-08  Joseph Pecoraro  <pecoraro@apple.com>
+
         Web Inspector: Add Debug setting to show Internal Object Classes in Heap Snapshot
         https://bugs.webkit.org/show_bug.cgi?id=194445
 
index d8c7955..ac54d48 100644 (file)
@@ -418,6 +418,7 @@ localizedStrings["Expand columns"] = "Expand columns";
 localizedStrings["Expanded"] = "Expanded";
 localizedStrings["Experimental"] = "Experimental";
 localizedStrings["Export"] = "Export";
+localizedStrings["Export (%s)"] = "Export (%s)";
 localizedStrings["Export HAR"] = "Export HAR";
 localizedStrings["Export Result"] = "Export Result";
 localizedStrings["Export Test"] = "Export Test";
@@ -488,6 +489,7 @@ localizedStrings["HTML Attributes"] = "HTML Attributes";
 localizedStrings["Headers"] = "Headers";
 localizedStrings["Headers:"] = "Headers:";
 localizedStrings["Heading Level"] = "Heading Level";
+localizedStrings["Heap Snapshot %s-%s-%s at %s.%s.%s"] = "Heap Snapshot %s-%s-%s at %s.%s.%s";
 localizedStrings["Heap Snapshot Object (%s)"] = "Heap Snapshot Object (%s)";
 localizedStrings["Height"] = "Height";
 localizedStrings["Hide Console"] = "Hide Console";
@@ -519,6 +521,7 @@ localizedStrings["Immediate Pause Requested"] = "Immediate Pause Requested";
 localizedStrings["Import"] = "Import";
 localizedStrings["Imported"] = "Imported";
 localizedStrings["Imported Recordings"] = "Imported Recordings";
+localizedStrings["Imported \u2014 %s"] = "Imported \u2014 %s";
 localizedStrings["Incomplete"] = "Incomplete";
 localizedStrings["Indent width:"] = "Indent width:";
 localizedStrings["Index"] = "Index";
index 03c33b9..0909065 100644 (file)
@@ -79,6 +79,7 @@ WI.HeapAllocationsInstrument = class HeapAllocationsInstrument extends WI.Instru
             let workerProxy = WI.HeapSnapshotWorkerProxy.singleton();
             workerProxy.createSnapshot(snapshotStringData, ({objectId, snapshot: serializedSnapshot}) => {
                 let snapshot = WI.HeapSnapshotProxy.deserialize(objectId, serializedSnapshot);
+                snapshot.snapshotStringData = snapshotStringData;
                 WI.timelineManager.heapSnapshotAdded(timestamp, snapshot);
             });
         });
index 047ee50..136c214 100644 (file)
@@ -53,6 +53,7 @@ WI.ConsoleObserver = class ConsoleObserver
         let workerProxy = WI.HeapSnapshotWorkerProxy.singleton();
         workerProxy.createSnapshot(snapshotStringData, title || null, ({objectId, snapshot: serializedSnapshot}) => {
             let snapshot = WI.HeapSnapshotProxy.deserialize(objectId, serializedSnapshot);
+            snapshot.snapshotStringData = snapshotStringData;
             WI.timelineManager.heapSnapshotAdded(timestamp, snapshot);
         });
     }
index 828bf53..1b9d429 100644 (file)
@@ -37,6 +37,7 @@ WI.HeapObserver = class HeapObserver
         let workerProxy = WI.HeapSnapshotWorkerProxy.singleton();
         workerProxy.createSnapshot(snapshotStringData, ({objectId, snapshot: serializedSnapshot}) => {
             let snapshot = WI.HeapSnapshotProxy.deserialize(objectId, serializedSnapshot);
+            snapshot.snapshotStringData = snapshotStringData;
             WI.timelineManager.heapTrackingStarted(timestamp, snapshot);
         });
     }
@@ -46,6 +47,7 @@ WI.HeapObserver = class HeapObserver
         let workerProxy = WI.HeapSnapshotWorkerProxy.singleton();
         workerProxy.createSnapshot(snapshotStringData, ({objectId, snapshot: serializedSnapshot}) => {
             let snapshot = WI.HeapSnapshotProxy.deserialize(objectId, serializedSnapshot);
+            snapshot.snapshotStringData = snapshotStringData;
             WI.timelineManager.heapTrackingCompleted(timestamp, snapshot);
         });
     }
index dce225e..60f45c9 100644 (file)
@@ -25,7 +25,7 @@
 
 WI.HeapSnapshotProxy = class HeapSnapshotProxy extends WI.Object
 {
-    constructor(snapshotObjectId, identifier, title, totalSize, totalObjectCount, liveSize, categories)
+    constructor(snapshotObjectId, identifier, title, totalSize, totalObjectCount, liveSize, categories, imported)
     {
         super();
 
@@ -37,6 +37,8 @@ WI.HeapSnapshotProxy = class HeapSnapshotProxy extends WI.Object
         this._totalObjectCount = totalObjectCount;
         this._liveSize = liveSize;
         this._categories = Map.fromObject(categories);
+        this._imported = imported;
+        this._snapshotStringData = null;
 
         console.assert(!this.invalid);
 
@@ -49,8 +51,8 @@ WI.HeapSnapshotProxy = class HeapSnapshotProxy extends WI.Object
 
     static deserialize(objectId, serializedSnapshot)
     {
-        let {identifier, title, totalSize, totalObjectCount, liveSize, categories} = serializedSnapshot;
-        return new WI.HeapSnapshotProxy(objectId, identifier, title, totalSize, totalObjectCount, liveSize, categories);
+        let {identifier, title, totalSize, totalObjectCount, liveSize, categories, imported} = serializedSnapshot;
+        return new WI.HeapSnapshotProxy(objectId, identifier, title, totalSize, totalObjectCount, liveSize, categories, imported);
     }
 
     static invalidateSnapshotProxies()
@@ -73,8 +75,19 @@ WI.HeapSnapshotProxy = class HeapSnapshotProxy extends WI.Object
     get totalObjectCount() { return this._totalObjectCount; }
     get liveSize() { return this._liveSize; }
     get categories() { return this._categories; }
+    get imported() { return this._imported; }
     get invalid() { return this._proxyObjectId === 0; }
 
+    get snapshotStringData()
+    {
+        return this._snapshotStringData;
+    }
+
+    set snapshotStringData(data)
+    {
+        this._snapshotStringData = data;
+    }
+
     updateForCollectionEvent(event)
     {
         console.assert(!this.invalid);
index 4c51b4a..d4c0a88 100644 (file)
@@ -66,6 +66,12 @@ WI.HeapSnapshotWorkerProxy = class HeapSnapshotWorkerProxy extends WI.Object
         this.performAction("createSnapshotDiff", ...arguments);
     }
 
+    createImportedSnapshot(snapshotStringData, title, callback)
+    {
+        const imported = true;
+        this.performAction("createSnapshot", snapshotStringData, title, imported, callback);
+    }
+
     // Public
 
     performAction(actionName)
index eea29c8..6d3e2fc 100644 (file)
@@ -73,6 +73,9 @@ WI.HeapAllocationsTimelineOverviewGraph = class HeapAllocationsTimelineOverviewG
         }
 
         for (let record of visibleRecords) {
+            if (isNaN(record.timestamp))
+                continue;
+
             const halfImageWidth = 8;
             let x = xScale(record.timestamp) - halfImageWidth;
             if (x <= 1)
index 73a604c..cde7b4e 100644 (file)
@@ -59,6 +59,11 @@ WI.HeapAllocationsTimelineView = class HeapAllocationsTimelineView extends WI.Ti
             },
         };
 
+        this._importButtonNavigationItem = new WI.ButtonNavigationItem("import", WI.UIString("Import"), "Images/Import.svg", 15, 15);
+        this._importButtonNavigationItem.toolTip = WI.UIString("Import");
+        this._importButtonNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText;
+        this._importButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._importButtonNavigationItemClicked, this);
+
         let snapshotTooltip = WI.UIString("Take snapshot");
         this._takeHeapSnapshotButtonItem = new WI.ButtonNavigationItem("take-snapshot", snapshotTooltip, "Images/Camera.svg", 16, 16);
         this._takeHeapSnapshotButtonItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._takeHeapSnapshotClicked, this);
@@ -184,7 +189,7 @@ WI.HeapAllocationsTimelineView = class HeapAllocationsTimelineView extends WI.Ti
     get navigationItems()
     {
         if (this._showingSnapshotList) {
-            let items = [this._takeHeapSnapshotButtonItem, this._compareHeapSnapshotsButtonItem];
+            let items = [this._importButtonNavigationItem, this._takeHeapSnapshotButtonItem, this._compareHeapSnapshotsButtonItem];
             if (this._selectingComparisonHeapSnapshots)
                 items.push(this._compareHeapSnapshotHelpTextItem);
             return items;
@@ -214,6 +219,22 @@ WI.HeapAllocationsTimelineView = class HeapAllocationsTimelineView extends WI.Ti
         return components.concat(this._contentViewContainer.currentContentView.selectionPathComponents);
     }
 
+    get supportsSave()
+    {
+        if (this._showingSnapshotList)
+            return false;
+
+        if (!this._contentViewContainer.currentContentView)
+            return false;
+
+        return this._contentViewContainer.currentContentView.supportsSave;
+    }
+
+    get saveData()
+    {
+        return this._contentViewContainer.currentContentView.saveData;
+    }
+
     selectRecord(record)
     {
         if (record)
@@ -382,12 +403,27 @@ WI.HeapAllocationsTimelineView = class HeapAllocationsTimelineView extends WI.Ti
         this._compareHeapSnapshotsButtonItem.enabled = hasAtLeastTwoValidSnapshots;
     }
 
+    _importButtonNavigationItemClicked()
+    {
+        WI.FileUtilities.importText(function(result) {
+            let snapshotStringData = result.text;
+            let workerProxy = WI.HeapSnapshotWorkerProxy.singleton();
+            workerProxy.createImportedSnapshot(snapshotStringData, result.filename, ({objectId, snapshot: serializedSnapshot}) => {
+                let snapshot = WI.HeapSnapshotProxy.deserialize(objectId, serializedSnapshot);
+                snapshot.snapshotStringData = snapshotStringData;
+                const timestamp = NaN;
+                WI.timelineManager.heapSnapshotAdded(timestamp, snapshot);
+            });
+        });
+    }
+
     _takeHeapSnapshotClicked()
     {
         HeapAgent.snapshot(function(error, timestamp, snapshotStringData) {
             let workerProxy = WI.HeapSnapshotWorkerProxy.singleton();
             workerProxy.createSnapshot(snapshotStringData, ({objectId, snapshot: serializedSnapshot}) => {
                 let snapshot = WI.HeapSnapshotProxy.deserialize(objectId, serializedSnapshot);
+                snapshot.snapshotStringData = snapshotStringData;
                 WI.timelineManager.heapSnapshotAdded(timestamp, snapshot);
             });
         });
index 62cddbf..e5b5dc5 100644 (file)
@@ -33,6 +33,12 @@ WI.HeapSnapshotContentView = class HeapSnapshotContentView extends WI.ContentVie
 
         this.element.classList.add("heap-snapshot");
 
+        this._exportButtonNavigationItem = new WI.ButtonNavigationItem("export", WI.UIString("Export"), "Images/Export.svg", 15, 15);
+        this._exportButtonNavigationItem.toolTip = WI.UIString("Export (%s)").format(WI.saveKeyboardShortcut.displayName);
+        this._exportButtonNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText;
+        this._exportButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.High;
+        this._exportButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, () => { this._exportSnapshot(); });
+
         this._dataGrid = new WI.DataGrid(columns);
         this._dataGrid.sortColumnIdentifier = "retainedSize";
         this._dataGrid.sortOrder = WI.DataGrid.SortOrder.Descending;
@@ -52,6 +58,23 @@ WI.HeapSnapshotContentView = class HeapSnapshotContentView extends WI.ContentVie
 
     // Protected
 
+    get navigationItems()
+    {
+        if (this.representedObject instanceof WI.HeapSnapshotProxy)
+            return [this._exportButtonNavigationItem];
+        return [];
+    }
+
+    get supportsSave()
+    {
+        return this.representedObject instanceof WI.HeapSnapshotProxy;
+    }
+
+    get saveData()
+    {
+        return {customSaveHandler: () => { this._exportSnapshot(); }};
+    }
+
     shown()
     {
         super.shown();
@@ -73,6 +96,31 @@ WI.HeapSnapshotContentView = class HeapSnapshotContentView extends WI.ContentVie
 
     // Private
 
+    _exportSnapshot()
+    {
+        if (!this.representedObject.snapshotStringData) {
+            InspectorFrontendHost.beep();
+            return;
+        }
+
+        let date = new Date;
+        let values = [
+            date.getFullYear(),
+            Number.zeroPad(date.getMonth() + 1, 2),
+            Number.zeroPad(date.getDate(), 2),
+            Number.zeroPad(date.getHours(), 2),
+            Number.zeroPad(date.getMinutes(), 2),
+            Number.zeroPad(date.getSeconds(), 2),
+        ];
+        let filename = WI.UIString("Heap Snapshot %s-%s-%s at %s.%s.%s").format(...values);
+        let url = "web-inspector:///" + encodeURI(filename) + ".json";
+        WI.FileUtilities.save({
+            url,
+            content: this.representedObject.snapshotStringData,
+            forceSaveAs: true,
+        });
+    }
+
     _sortDataGrid()
     {
         if (!this._heapSnapshotDataGridTree)
index 596242d..e8f7db1 100644 (file)
@@ -147,8 +147,6 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
         return {customSaveHandler: () => { this._exportRecording(); }};
     }
 
-    // Protected
-
     initialLayout()
     {
         let previewHeader = this.element.appendChild(document.createElement("header"));
index 1e91a77..e9d70ce 100644 (file)
@@ -277,6 +277,8 @@ WI.TimelineTabContentView = class TimelineTabContentView extends WI.ContentBrows
         case WI.TimelineRecord.Type.RenderingFrame:
             return WI.UIString("Frame %d").format(timelineRecord.frameNumber);
         case WI.TimelineRecord.Type.HeapAllocations:
+            if (timelineRecord.heapSnapshot.imported)
+                return WI.UIString("Imported \u2014 %s").format(timelineRecord.heapSnapshot.title);
             if (timelineRecord.heapSnapshot.title)
                 return WI.UIString("Snapshot %d \u2014 %s").format(timelineRecord.heapSnapshot.identifier, timelineRecord.heapSnapshot.title);
             return WI.UIString("Snapshot %d").format(timelineRecord.heapSnapshot.identifier);
index df45415..af6fdff 100644 (file)
@@ -36,6 +36,7 @@ const nodeIdOffset = 0;
 const nodeSizeOffset = 1;
 const nodeClassNameOffset = 2;
 const nodeInternalOffset = 3;
+const gcDebuggingNodeFieldCount = 7;
 
 // edges
 // [<0:fromId>, <1:toId>, <2:typeTableIndex>, <3:edgeDataIndexOrEdgeNameIndex>]
@@ -73,21 +74,24 @@ let nextSnapshotIdentifier = 1;
 
 HeapSnapshot = class HeapSnapshot
 {
-    constructor(objectId, snapshotDataString, title = null)
+    constructor(objectId, snapshotDataString, title = null, imported = false)
     {
         this._identifier = nextSnapshotIdentifier++;
         this._objectId = objectId;
         this._title = title;
+        this._imported = imported;
 
         let json = JSON.parse(snapshotDataString);
         snapshotDataString = null;
 
         let {version, type, nodes, nodeClassNames, edges, edgeTypes, edgeNames} = json;
         console.assert(version === 1, "Expect JavaScriptCore Heap Snapshot version 1");
-        console.assert(!type || type === "Inspector", "Expect an Inspector Heap Snapshot");
+        console.assert(!type || (type === "Inspector" || type === "GCDebugging"), "Expect an Inspector / GCDebugging Heap Snapshot");
+
+        this._nodeFieldCount = type === "GCDebugging" ? gcDebuggingNodeFieldCount : nodeFieldCount;
 
         this._nodes = nodes;
-        this._nodeCount = nodes.length / nodeFieldCount;
+        this._nodeCount = nodes.length / this._nodeFieldCount;
 
         this._edges = edges;
         this._edgeCount = edges.length / edgeFieldCount;
@@ -99,8 +103,8 @@ HeapSnapshot = class HeapSnapshot
         this._totalSize = 0;
         this._nodeIdentifierToOrdinal = new Map; // <node identifier> => nodeOrdinal
         this._lastNodeIdentifier = 0;
-        for (let nodeIndex = 0; nodeIndex < nodes.length; nodeIndex += nodeFieldCount) {
-            let nodeOrdinal = nodeIndex / nodeFieldCount;
+        for (let nodeIndex = 0; nodeIndex < nodes.length; nodeIndex += this._nodeFieldCount) {
+            let nodeOrdinal = nodeIndex / this._nodeFieldCount;
             let nodeIdentifier = nodes[nodeIndex + nodeIdOffset];
             this._nodeIdentifierToOrdinal.set(nodeIdentifier, nodeOrdinal);
             this._totalSize += nodes[nodeIndex + nodeSizeOffset];
@@ -151,9 +155,9 @@ HeapSnapshot = class HeapSnapshot
         let nodeOrdinalIsDead = snapshot._nodeOrdinalIsDead;
 
         // Skip the <root> node.
-        let firstNodeIndex = nodeFieldCount;
+        let firstNodeIndex = snapshot._nodeFieldCount;
         let firstNodeOrdinal = 1;
-        for (let nodeIndex = firstNodeIndex, nodeOrdinal = firstNodeOrdinal; nodeIndex < nodes.length; nodeIndex += nodeFieldCount, nodeOrdinal++) {
+        for (let nodeIndex = firstNodeIndex, nodeOrdinal = firstNodeOrdinal; nodeIndex < nodes.length; nodeIndex += snapshot._nodeFieldCount, nodeOrdinal++) {
             if (allowNodeIdentifierCallback && !allowNodeIdentifierCallback(nodes[nodeIndex + nodeIdOffset]))
                 continue;
 
@@ -191,10 +195,10 @@ HeapSnapshot = class HeapSnapshot
         let nodes = snapshot._nodes;
 
         // Skip the <root> node.
-        let firstNodeIndex = nodeFieldCount;
+        let firstNodeIndex = snapshot._nodeFieldCount;
 
     outer:
-        for (let nodeIndex = firstNodeIndex; nodeIndex < nodes.length; nodeIndex += nodeFieldCount) {
+        for (let nodeIndex = firstNodeIndex; nodeIndex < nodes.length; nodeIndex += snapshot._nodeFieldCount) {
             if (allowNodeIdentifierCallback && !allowNodeIdentifierCallback(nodes[nodeIndex + nodeIdOffset]))
                 continue;
 
@@ -219,9 +223,9 @@ HeapSnapshot = class HeapSnapshot
         let nodeClassNamesTable = snapshot._nodeClassNamesTable;
 
         // Skip the <root> node.
-        let firstNodeIndex = nodeFieldCount;
+        let firstNodeIndex = snapshot._nodeFieldCount;
         let firstNodeOrdinal = 1;
-        for (let nodeIndex = firstNodeIndex, nodeOrdinal = firstNodeOrdinal; nodeIndex < nodes.length; nodeIndex += nodeFieldCount, nodeOrdinal++) {
+        for (let nodeIndex = firstNodeIndex, nodeOrdinal = firstNodeOrdinal; nodeIndex < nodes.length; nodeIndex += snapshot._nodeFieldCount, nodeOrdinal++) {
             if (allowNodeIdentifierCallback && !allowNodeIdentifierCallback(nodes[nodeIndex + nodeIdOffset]))
                 continue;
 
@@ -253,7 +257,7 @@ HeapSnapshot = class HeapSnapshot
     nodeWithIdentifier(nodeIdentifier)
     {
         let nodeOrdinal = this._nodeIdentifierToOrdinal.get(nodeIdentifier);
-        let nodeIndex = nodeOrdinal * nodeFieldCount;
+        let nodeIndex = nodeOrdinal * this._nodeFieldCount;
         return this.serializeNode(nodeIndex);
     }
 
@@ -297,7 +301,7 @@ HeapSnapshot = class HeapSnapshot
         let targetNodeOrdinal = this._nodeIdentifierToOrdinal.get(nodeIdentifier);
         for (let nodeOrdinal = 0; nodeOrdinal < this._nodeCount; ++nodeOrdinal) {
             if (this._nodeOrdinalToDominatorNodeOrdinal[nodeOrdinal] === targetNodeOrdinal)
-                dominatedNodes.push(nodeOrdinal * nodeFieldCount);
+                dominatedNodes.push(nodeOrdinal * this._nodeFieldCount);
         }
 
         return dominatedNodes.map(this.serializeNode, this);
@@ -313,7 +317,7 @@ HeapSnapshot = class HeapSnapshot
         for (; this._edges[edgeIndex + edgeFromIdOffset] === nodeIdentifier; edgeIndex += edgeFieldCount) {
             let toNodeIdentifier = this._edges[edgeIndex + edgeToIdOffset];
             let toNodeOrdinal = this._nodeIdentifierToOrdinal.get(toNodeIdentifier);
-            let toNodeIndex = toNodeOrdinal * nodeFieldCount;
+            let toNodeIndex = toNodeOrdinal * this._nodeFieldCount;
             retainedNodes.push(toNodeIndex);
             edges.push(edgeIndex);
         }
@@ -334,7 +338,7 @@ HeapSnapshot = class HeapSnapshot
         let incomingEdgeIndexEnd = this._nodeOrdinalToFirstIncomingEdge[nodeOrdinal + 1];
         for (let edgeIndex = incomingEdgeIndex; edgeIndex < incomingEdgeIndexEnd; ++edgeIndex) {
             let fromNodeOrdinal = this._incomingNodes[edgeIndex];
-            let fromNodeIndex = fromNodeOrdinal * nodeFieldCount;
+            let fromNodeIndex = fromNodeOrdinal * this._nodeFieldCount;
             retainers.push(fromNodeIndex);
             edges.push(this._incomingEdges[edgeIndex]);
         }
@@ -347,6 +351,9 @@ HeapSnapshot = class HeapSnapshot
 
     updateDeadNodesAndGatherCollectionData(snapshots)
     {
+        console.assert(!this._imported, "Should never use an imported snapshot to modify snapshots");
+        console.assert(snapshots.every((x) => !x._imported), "Should never modify nodes of imported snapshots");
+
         let previousSnapshotIndex = snapshots.indexOf(this) - 1;
         let previousSnapshot = snapshots[previousSnapshotIndex];
         if (!previousSnapshot)
@@ -356,7 +363,7 @@ HeapSnapshot = class HeapSnapshot
 
         // All of the node identifiers that could have existed prior to this snapshot.
         let known = new Map;
-        for (let nodeIndex = 0; nodeIndex < this._nodes.length; nodeIndex += nodeFieldCount) {
+        for (let nodeIndex = 0; nodeIndex < this._nodes.length; nodeIndex += this._nodeFieldCount) {
             let nodeIdentifier = this._nodes[nodeIndex + nodeIdOffset];
             if (nodeIdentifier > lastNodeIdentifier)
                 continue;
@@ -365,7 +372,7 @@ HeapSnapshot = class HeapSnapshot
 
         // Determine which node identifiers have since been deleted.
         let collectedNodesList = [];
-        for (let nodeIndex = 0; nodeIndex < previousSnapshot._nodes.length; nodeIndex += nodeFieldCount) {
+        for (let nodeIndex = 0; nodeIndex < previousSnapshot._nodes.length; nodeIndex += this._nodeFieldCount) {
             let nodeIdentifier = previousSnapshot._nodes[nodeIndex + nodeIdOffset];
             let wasDeleted = !known.has(nodeIdentifier);
             if (wasDeleted)
@@ -403,20 +410,21 @@ HeapSnapshot = class HeapSnapshot
             totalObjectCount: this._nodeCount - 1, // <root>.
             liveSize: this._liveSize,
             categories: this._categories,
+            imported: this._imported,
         };
     }
 
     serializeNode(nodeIndex)
     {
-        console.assert((nodeIndex % nodeFieldCount) === 0, "Invalid nodeIndex to serialize");
+        console.assert((nodeIndex % this._nodeFieldCount) === 0, "Invalid nodeIndex to serialize");
 
         let nodeIdentifier = this._nodes[nodeIndex + nodeIdOffset];
-        let nodeOrdinal = nodeIndex / nodeFieldCount;
+        let nodeOrdinal = nodeIndex / this._nodeFieldCount;
         let edgeIndex = this._nodeOrdinalToFirstOutgoingEdge[nodeOrdinal];
         let hasChildren = this._edges[edgeIndex + edgeFromIdOffset] === nodeIdentifier;
 
         let dominatorNodeOrdinal = this._nodeOrdinalToDominatorNodeOrdinal[nodeOrdinal];
-        let dominatorNodeIndex = dominatorNodeOrdinal * nodeFieldCount;
+        let dominatorNodeIndex = dominatorNodeOrdinal * this._nodeFieldCount;
         let dominatorNodeIdentifier = this._nodes[dominatorNodeIndex + nodeIdOffset];
 
         return {
@@ -533,7 +541,7 @@ HeapSnapshot = class HeapSnapshot
 
         while (stackTop >= 0) {
             let nodeOrdinal = stackNodes[stackTop];
-            let nodeIdentifier = this._nodes[(nodeOrdinal * nodeFieldCount) + nodeIdOffset];
+            let nodeIdentifier = this._nodes[(nodeOrdinal * this._nodeFieldCount) + nodeIdOffset];
             let edgeIndex = stackEdges[stackTop];
 
             if (this._edges[edgeIndex + edgeFromIdOffset] === nodeIdentifier) {
@@ -657,7 +665,7 @@ HeapSnapshot = class HeapSnapshot
                     changed = true;
 
                     let outgoingEdgeIndex = this._nodeOrdinalToFirstOutgoingEdge[nodeOrdinal];
-                    let nodeIdentifier = this._nodes[(nodeOrdinal * nodeFieldCount) + nodeIdOffset];
+                    let nodeIdentifier = this._nodes[(nodeOrdinal * this._nodeFieldCount) + nodeIdOffset];
                     for (let edgeIndex = outgoingEdgeIndex; this._edges[edgeIndex + edgeFromIdOffset] === nodeIdentifier; edgeIndex += edgeFieldCount) {
                         let toNodeIdentifier = this._edges[edgeIndex + edgeToIdOffset];
                         let toNodeOrdinal = this._nodeIdentifierToOrdinal.get(toNodeIdentifier);
@@ -678,7 +686,7 @@ HeapSnapshot = class HeapSnapshot
     _buildRetainedSizes(postOrderIndexToNodeOrdinal)
     {
         // Self size.
-        for (let nodeIndex = 0, nodeOrdinal = 0; nodeOrdinal < this._nodeCount; nodeIndex += nodeFieldCount, nodeOrdinal++)
+        for (let nodeIndex = 0, nodeOrdinal = 0; nodeOrdinal < this._nodeCount; nodeIndex += this._nodeFieldCount, nodeOrdinal++)
             this._nodeOrdinalToRetainedSizes[nodeOrdinal] = this._nodes[nodeIndex + nodeSizeOffset];
 
         // Attribute size to dominator.
@@ -731,7 +739,7 @@ HeapSnapshot = class HeapSnapshot
         {
             if (this._nodeOrdinalIsGCRoot[nodeOrdinal]) {
                 let fullPath = currentPath.slice();
-                let nodeIndex = nodeOrdinal * nodeFieldCount;
+                let nodeIndex = nodeOrdinal * this._nodeFieldCount;
                 fullPath.push({node: nodeIndex});
                 paths.push(fullPath);
                 return;
@@ -741,7 +749,7 @@ HeapSnapshot = class HeapSnapshot
                 return;
             visited[nodeOrdinal] = 1;
 
-            let nodeIndex = nodeOrdinal * nodeFieldCount;
+            let nodeIndex = nodeOrdinal * this._nodeFieldCount;
             currentPath.push({node: nodeIndex});
 
             // Loop in reverse order because edges were added in reverse order.
@@ -750,7 +758,7 @@ HeapSnapshot = class HeapSnapshot
             let incomingEdgeIndexEnd = this._nodeOrdinalToFirstIncomingEdge[nodeOrdinal + 1];
             for (let incomingEdgeIndex = incomingEdgeIndexEnd - 1; incomingEdgeIndex >= incomingEdgeIndexStart; --incomingEdgeIndex) {
                 let fromNodeOrdinal = this._incomingNodes[incomingEdgeIndex];
-                let fromNodeIndex = fromNodeOrdinal * nodeFieldCount;
+                let fromNodeIndex = fromNodeOrdinal * this._nodeFieldCount;
                 let fromNodeIsInternal = this._nodes[fromNodeIndex + nodeInternalOffset];
                 if (fromNodeIsInternal)
                     continue;
index 13f6849..f268aaf 100644 (file)
@@ -47,20 +47,23 @@ HeapSnapshotWorker = class HeapSnapshotWorker
         this._snapshots = [];
     }
 
-    createSnapshot(snapshotString, title)
+    createSnapshot(snapshotString, title, imported)
     {
         let objectId = this._nextObjectId++;
-        let snapshot = new HeapSnapshot(objectId, snapshotString, title);
-        this._snapshots.push(snapshot);
+        let snapshot = new HeapSnapshot(objectId, snapshotString, title, imported);
         this._objects.set(objectId, snapshot);
 
-        if (this._snapshots.length > 1) {
-            setTimeout(() => {
-                let collectionData = snapshot.updateDeadNodesAndGatherCollectionData(this._snapshots);
-                if (!collectionData || !collectionData.affectedSnapshots.length)
-                    return;
-                this.sendEvent("HeapSnapshot.CollectionEvent", collectionData);
-            }, 0);
+        if (!imported) {
+            this._snapshots.push(snapshot);
+
+            if (this._snapshots.length > 1) {
+                setTimeout(() => {
+                    let collectionData = snapshot.updateDeadNodesAndGatherCollectionData(this._snapshots);
+                    if (!collectionData || !collectionData.affectedSnapshots.length)
+                        return;
+                    this.sendEvent("HeapSnapshot.CollectionEvent", collectionData);
+                }, 0);
+            }            
         }
 
         return {objectId, snapshot: snapshot.serialize()};