Web Inspector: Timelines - Import / Export Timeline Recordings
authorjoepeck@webkit.org <joepeck@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 15 Mar 2019 23:45:51 +0000 (23:45 +0000)
committerjoepeck@webkit.org <joepeck@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 15 Mar 2019 23:45:51 +0000 (23:45 +0000)
https://bugs.webkit.org/show_bug.cgi?id=195709
<rdar://problem/23188921>

Reviewed by Devin Rousso.

Source/WebInspectorUI:

Timeline exporting saves TimelineRecording and TimelineOverview state.
The TimelineRecording includes all kinds of model objects, such as
records, markers, memory pressure events, etc. It also includes raw
protocol data, such as script profiler samples. TimelineOverview
includes some of the view state to restore, such as the selected
time range, zoom level, and selected timeline.

Timeline importing constructs a new TimelineRecording by replaying
the records, markers, and other events, as well as re-initializing
more state. To finally display the imported recording, the content
view will immediately initialize start/current/end times and the
overview will restore the view state.

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

* UserInterface/Controllers/TimelineManager.js:
(WI.TimelineManager.synthesizeImportError):
(WI.TimelineManager.prototype.importRecording):
Import API.

(WI.TimelineManager.prototype.scriptProfilerTrackingCompleted):
Initialize the samples on the recording via a different path
so that the data can be saved for exporting.

* UserInterface/Models/TimelineRecording.js:
(WI.TimelineRecording):
(WI.TimelineRecording.import):
(WI.TimelineRecording.prototype.exportData):
(WI.TimelineRecording.prototype.get capturing):
(WI.TimelineRecording.prototype.get imported):
(WI.TimelineRecording.prototype.unloaded):
(WI.TimelineRecording.prototype.reset):
(WI.TimelineRecording.prototype.addEventMarker):
(WI.TimelineRecording.prototype.addRecord):
(WI.TimelineRecording.prototype.addMemoryPressureEvent):
(WI.TimelineRecording.prototype.initializeCallingContextTrees):
(WI.TimelineRecording.prototype.canExport):
Save data at the TimelineRecording level that can be used for export.
We only allow exporting a TimelineRecording that has started/stopped
at least once and is not currently capturing.

* UserInterface/Views/TimelineRecordingContentView.js:
(WI.TimelineRecordingContentView):
(WI.TimelineRecordingContentView.prototype.get navigationItems):
(WI.TimelineRecordingContentView.prototype.get supportsSave):
(WI.TimelineRecordingContentView.prototype.get saveData):
(WI.TimelineRecordingContentView.prototype.shown):
(WI.TimelineRecordingContentView.prototype._capturingStarted):
(WI.TimelineRecordingContentView.prototype._capturingStopped):
(WI.TimelineRecordingContentView.prototype._initializeImportedRecording):
(WI.TimelineRecordingContentView.prototype._exportTimelineRecording):
(WI.TimelineRecordingContentView.prototype._importButtonNavigationItemClicked):
(WI.TimelineRecordingContentView.prototype._recordingReset):
Add Import and Export buttons in the Timeline navigation bar.

* UserInterface/Views/TimelineOverview.js:
(WI.TimelineOverview):
(WI.TimelineOverview.prototype.exportData):
(WI.TimelineOverview.prototype._instrumentAdded):
(WI.TimelineOverview.prototype._recordingImported):
When importing a recording update the TimelineOverview state
soon afterwards.

* UserInterface/Models/CPUTimelineRecord.js:
(WI.CPUTimelineRecord.fromJSON):
(WI.CPUTimelineRecord.prototype.toJSON):
* UserInterface/Models/GarbageCollection.js:
(WI.GarbageCollection.fromJSON):
(WI.GarbageCollection.prototype.toJSON):
* UserInterface/Models/Geometry.js:
(WI.Quad.fromJSON):
(WI.Quad.prototype.toJSON):
* UserInterface/Models/HeapAllocationsTimelineRecord.js:
(WI.HeapAllocationsTimelineRecord.fromJSON):
(WI.HeapAllocationsTimelineRecord.prototype.toJSON):
* UserInterface/Models/LayoutTimelineRecord.js:
(WI.LayoutTimelineRecord.fromJSON):
(WI.LayoutTimelineRecord.prototype.toJSON):
* UserInterface/Models/MediaTimelineRecord.js:
(WI.MediaTimelineRecord.fromJSON):
(WI.MediaTimelineRecord.prototype.toJSON):
* UserInterface/Models/MemoryPressureEvent.js:
(WI.MemoryPressureEvent.fromJSON):
(WI.MemoryPressureEvent.prototype.toJSON):
* UserInterface/Models/MemoryTimelineRecord.js:
(WI.MemoryTimelineRecord):
(WI.MemoryTimelineRecord.fromJSON):
(WI.MemoryTimelineRecord.prototype.toJSON):
* UserInterface/Models/RenderingFrameTimelineRecord.js:
(WI.RenderingFrameTimelineRecord.fromJSON):
(WI.RenderingFrameTimelineRecord.prototype.toJSON):
* UserInterface/Models/ResourceTimelineRecord.js:
(WI.ResourceTimelineRecord.fromJSON):
(WI.ResourceTimelineRecord.prototype.toJSON):
* UserInterface/Models/ScriptTimelineRecord.js:
(WI.ScriptTimelineRecord.fromJSON):
(WI.ScriptTimelineRecord.prototype.toJSON):
* UserInterface/Models/TimelineMarker.js:
(WI.TimelineMarker.fromJSON):
(WI.TimelineMarker.prototype.toJSON):
(WI.TimelineMarker.prototype.get type):
(WI.TimelineMarker.prototype.get details):
(WI.TimelineMarker.prototype.set time):
(WI.TimelineMarker):
* UserInterface/Models/TimelineRecord.js:
(WI.TimelineRecord.fromJSON):
(WI.TimelineRecord.prototype.toJSON):
Import / Export toJSON / fromJSON implementations.

* UserInterface/Views/CPUTimelineOverviewGraph.js:
(WI.CPUTimelineOverviewGraph):
(WI.CPUTimelineOverviewGraph.prototype._cpuTimelineRecordAdded):
(WI.CPUTimelineOverviewGraph.prototype._processRecord):
* UserInterface/Views/LayoutTimelineOverviewGraph.js:
(WI.LayoutTimelineOverviewGraph):
(WI.LayoutTimelineOverviewGraph.prototype._layoutTimelineRecordAdded):
(WI.LayoutTimelineOverviewGraph.prototype._processRecord):
* UserInterface/Views/LayoutTimelineView.js:
(WI.LayoutTimelineView):
(WI.LayoutTimelineView.prototype._layoutTimelineRecordAdded):
(WI.LayoutTimelineView.prototype._processRecord):
* UserInterface/Views/MediaTimelineView.js:
(WI.MediaTimelineView):
(WI.MediaTimelineView.prototype._handleRecordAdded):
(WI.MediaTimelineView.prototype._processRecord):
* UserInterface/Views/MemoryTimelineOverviewGraph.js:
(WI.MemoryTimelineOverviewGraph):
(WI.MemoryTimelineOverviewGraph.prototype._memoryTimelineRecordAdded):
(WI.MemoryTimelineOverviewGraph.prototype._processRecord):
* UserInterface/Views/MemoryTimelineView.js:
(WI.MemoryTimelineView):
(WI.MemoryTimelineView.prototype._memoryTimelineRecordAdded):
(WI.MemoryTimelineView.prototype._processRecord):
* UserInterface/Views/NetworkTimelineOverviewGraph.js:
(WI.NetworkTimelineOverviewGraph):
(WI.NetworkTimelineOverviewGraph.prototype.reset):
(WI.NetworkTimelineOverviewGraph.prototype._networkTimelineRecordAdded):
(WI.NetworkTimelineOverviewGraph.prototype._processRecord):
(WI.NetworkTimelineOverviewGraph.prototype._networkTimelineRecordAdded.compareByStartTime): Deleted.
* UserInterface/Views/NetworkTimelineView.js:
(WI.NetworkTimelineView):
(WI.NetworkTimelineView.prototype._networkTimelineRecordAdded):
(WI.NetworkTimelineView.prototype._processRecord):
* UserInterface/Views/RenderingFrameTimelineView.js:
(WI.RenderingFrameTimelineView):
(WI.RenderingFrameTimelineView.prototype._renderingFrameTimelineRecordAdded):
(WI.RenderingFrameTimelineView.prototype._processRecord):
* UserInterface/Views/ScriptDetailsTimelineView.js:
(WI.ScriptDetailsTimelineView):
(WI.ScriptDetailsTimelineView.prototype._scriptTimelineRecordAdded):
(WI.ScriptDetailsTimelineView.prototype._processRecord):
Add common _processRecord path to each timeline OverviewGraph and TimelineView.
By calling this in construction we populate graphs with TimelineRecords that
may have already existed. This is necessary for imports, but this also fixes
the case where you enable a timeline that had data and it didn't show data.

* UserInterface/Views/LayoutTimelineOverviewGraph.css:
(.timeline-overview-graph.layout-overview > .graph-row):
(.timeline-overview-graph.layout-overview > .graph-row > .timeline-record-bar):
(.timeline-overview-graph.layout-overview > .graph-row > .timeline-record-bar > .segment):
(.timeline-overview-graph.layout > .graph-row): Deleted.
(.timeline-overview-graph.layout > .graph-row > .timeline-record-bar): Deleted.
(.timeline-overview-graph.layout > .graph-row > .timeline-record-bar > .segment): Deleted.
* UserInterface/Views/TimelineRecordBar.css:
(.timeline-record-bar.timeline-record-type-layout.paint > .segment,):
(.timeline-record-bar.timeline-record-type-layout.layout-timeline-record-paint > .segment,): Deleted.
We simplified some of the sub-record type enum strings. To do this we needed to change
"layout" to "layout-overview" to avoid a conflict.

LayoutTests:

* inspector/timeline/timeline-recording-expected.txt: Added.
* inspector/timeline/timeline-recording.html: Added.

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

46 files changed:
LayoutTests/ChangeLog
LayoutTests/inspector/timeline/timeline-recording-expected.txt [new file with mode: 0644]
LayoutTests/inspector/timeline/timeline-recording.html [new file with mode: 0644]
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
Source/WebInspectorUI/UserInterface/Base/Main.js
Source/WebInspectorUI/UserInterface/Controllers/TimelineManager.js
Source/WebInspectorUI/UserInterface/Main.html
Source/WebInspectorUI/UserInterface/Models/CPUTimelineRecord.js
Source/WebInspectorUI/UserInterface/Models/GarbageCollection.js
Source/WebInspectorUI/UserInterface/Models/Geometry.js
Source/WebInspectorUI/UserInterface/Models/HeapAllocationsTimelineRecord.js
Source/WebInspectorUI/UserInterface/Models/LayoutTimelineRecord.js
Source/WebInspectorUI/UserInterface/Models/MediaTimelineRecord.js
Source/WebInspectorUI/UserInterface/Models/MemoryPressureEvent.js
Source/WebInspectorUI/UserInterface/Models/MemoryTimelineRecord.js
Source/WebInspectorUI/UserInterface/Models/RenderingFrameTimelineRecord.js
Source/WebInspectorUI/UserInterface/Models/Resource.js
Source/WebInspectorUI/UserInterface/Models/ResourceTimelineRecord.js
Source/WebInspectorUI/UserInterface/Models/ScriptTimelineRecord.js
Source/WebInspectorUI/UserInterface/Models/SourceMapResource.js
Source/WebInspectorUI/UserInterface/Models/TimelineMarker.js
Source/WebInspectorUI/UserInterface/Models/TimelineRecord.js
Source/WebInspectorUI/UserInterface/Models/TimelineRecording.js
Source/WebInspectorUI/UserInterface/Views/CPUTimelineOverviewGraph.js
Source/WebInspectorUI/UserInterface/Views/HeapAllocationsTimelineView.js
Source/WebInspectorUI/UserInterface/Views/HeapSnapshotContentView.js
Source/WebInspectorUI/UserInterface/Views/LayoutTimelineOverviewGraph.css
Source/WebInspectorUI/UserInterface/Views/LayoutTimelineOverviewGraph.js
Source/WebInspectorUI/UserInterface/Views/LayoutTimelineView.js
Source/WebInspectorUI/UserInterface/Views/MediaTimelineView.js
Source/WebInspectorUI/UserInterface/Views/MemoryTimelineOverviewGraph.js
Source/WebInspectorUI/UserInterface/Views/MemoryTimelineView.js
Source/WebInspectorUI/UserInterface/Views/NetworkTimelineOverviewGraph.js
Source/WebInspectorUI/UserInterface/Views/NetworkTimelineView.js
Source/WebInspectorUI/UserInterface/Views/OverviewTimelineView.js
Source/WebInspectorUI/UserInterface/Views/RenderingFrameTimelineView.js
Source/WebInspectorUI/UserInterface/Views/ScriptDetailsTimelineView.js
Source/WebInspectorUI/UserInterface/Views/TimelineOverview.js
Source/WebInspectorUI/UserInterface/Views/TimelineRecordBar.css
Source/WebInspectorUI/UserInterface/Views/TimelineRecordingContentView.css
Source/WebInspectorUI/UserInterface/Views/TimelineRecordingContentView.js
Source/WebInspectorUI/UserInterface/Views/TimelineRecordingImportedView.css [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/TimelineRecordingImportedView.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/TimelineRecordingProgressView.js
Source/WebInspectorUI/UserInterface/Views/TimelineView.js

index 7fb72af..ccd81b4 100644 (file)
@@ -1,3 +1,14 @@
+2019-03-15  Joseph Pecoraro  <pecoraro@apple.com>
+
+        Web Inspector: Timelines - Import / Export Timeline Recordings
+        https://bugs.webkit.org/show_bug.cgi?id=195709
+        <rdar://problem/23188921>
+
+        Reviewed by Devin Rousso.
+
+        * inspector/timeline/timeline-recording-expected.txt: Added.
+        * inspector/timeline/timeline-recording.html: Added.
+
 2019-03-15  Zalan Bujtas  <zalan@apple.com>
 
         [ContentChangeObserver] HTMLImageElement::willRespondToMouseClickEvents returns quirk value.
diff --git a/LayoutTests/inspector/timeline/timeline-recording-expected.txt b/LayoutTests/inspector/timeline/timeline-recording-expected.txt
new file mode 100644 (file)
index 0000000..1c79afa
--- /dev/null
@@ -0,0 +1,70 @@
+Tests for timeline recording.
+
+
+== Running test suite: TimelineRecording
+-- Running test case: TimelineRecording.Basic
+Loaded
+PASS: TimelineManager should not be capturing.
+PASS: TimelineRecording should not be capturing.
+PASS: TimelineRecording should not be readonly.
+PASS: TimelineRecording should not be imported.
+PASS: TimelineRecording should not have a startTime.
+PASS: TimelineRecording should not have a endTime.
+Start
+PASS: TimelineManager should be capturing.
+PASS: TimelineManager active recording should not have changed.
+PASS: TimelineRecording should be capturing.
+PASS: TimelineRecording should not be readonly.
+Reload
+Stop
+PASS: TimelineManager should not be capturing.
+PASS: TimelineRecording should not be capturing.
+PASS: TimelineRecording should not be readonly.
+PASS: TimelineRecording should not be imported.
+PASS: TimelineRecording should have a startTime.
+PASS: TimelineRecording should have a endTime.
+
+-- Running test case: TimelineRecording.prototype.exportData
+PASS: TimelineRecording should be able to export.
+PASS: TimelineRecording should be able to produce export data.
+PASS: TimelineRecording should have at least 10 Timeline Records.
+Export Data:
+{
+  "displayName": "<filtered>",
+  "startTime": "<filtered>",
+  "endTime": "<filtered>",
+  "discontinuities": [],
+  "instrumentTypes": [
+    "timeline-record-type-network",
+    "timeline-record-type-layout",
+    "timeline-record-type-script",
+    "timeline-record-type-cpu",
+    "timeline-record-type-rendering-frame"
+  ],
+  "records": "<filtered>",
+  "markers": [
+    {
+      "time": "<filtered>",
+      "type": "dom-content-event"
+    },
+    {
+      "time": "<filtered>",
+      "type": "load-event"
+    }
+  ],
+  "memoryPressureEvents": [],
+  "sampleStackTraces": [],
+  "sampleDurations": []
+}
+
+-- Running test case: TimelineRecording.import
+PASS: TimelineManager active recording is not this imported recording.
+PASS: TimelineRecording should not be capturing.
+PASS: TimelineRecording should be readonly.
+PASS: TimelineRecording should be imported.
+PASS: TimelineRecording should have a startTime.
+PASS: TimelineRecording should have a endTime.
+PASS: TimelineRecording identifier should be 999.
+Display Name:
+Imported - TEST
+
diff --git a/LayoutTests/inspector/timeline/timeline-recording.html b/LayoutTests/inspector/timeline/timeline-recording.html
new file mode 100644 (file)
index 0000000..04701f9
--- /dev/null
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="../../http/tests/inspector/resources/inspector-test.js"></script>
+<script>
+function test()
+{
+    let suite = InspectorTest.createAsyncSuite("TimelineRecording");
+
+    let exportData = null;
+
+    suite.addTestCase({
+        name: "TimelineRecording.Basic",
+        description: "Make a basic Timeline recording.",
+        async test() {
+            let recording = WI.timelineManager.activeRecording;
+
+            InspectorTest.log("Loaded");
+            InspectorTest.expectFalse(WI.timelineManager.isCapturing(), "TimelineManager should not be capturing.");
+            InspectorTest.expectFalse(recording.capturing, "TimelineRecording should not be capturing.");
+            InspectorTest.expectFalse(recording.readonly, "TimelineRecording should not be readonly.");
+            InspectorTest.expectFalse(recording.imported, "TimelineRecording should not be imported.");
+            InspectorTest.expectThat(isNaN(recording.startTime), "TimelineRecording should not have a startTime.");
+            InspectorTest.expectThat(isNaN(recording.endTime), "TimelineRecording should not have a endTime.");
+
+            InspectorTest.log("Start");
+            WI.timelineManager.startCapturing();
+            await WI.timelineManager.awaitEvent(WI.TimelineManager.Event.CapturingStarted);
+            InspectorTest.expectTrue(WI.timelineManager.isCapturing(), "TimelineManager should be capturing.");
+            InspectorTest.expectEqual(WI.timelineManager.activeRecording, recording, "TimelineManager active recording should not have changed.");
+            InspectorTest.expectTrue(recording.capturing, "TimelineRecording should be capturing.");
+            InspectorTest.expectFalse(recording.readonly, "TimelineRecording should not be readonly.");
+
+            InspectorTest.log("Reload");
+            await Promise.all([
+                InspectorTest.awaitEvent(FrontendTestHarness.Event.TestPageDidLoad),
+                InspectorTest.reloadPage(),
+            ]);
+
+            InspectorTest.log("Stop");
+            WI.timelineManager.stopCapturing();
+            InspectorTest.expectFalse(WI.timelineManager.isCapturing(), "TimelineManager should not be capturing.");
+            InspectorTest.expectFalse(recording.capturing, "TimelineRecording should not be capturing.");
+            InspectorTest.expectFalse(recording.readonly, "TimelineRecording should not be readonly.");
+            InspectorTest.expectFalse(recording.imported, "TimelineRecording should not be imported.");
+            InspectorTest.expectThat(!isNaN(recording.startTime), "TimelineRecording should have a startTime.");
+            InspectorTest.expectThat(!isNaN(recording.endTime), "TimelineRecording should have a endTime.");
+        }
+    });
+
+    suite.addTestCase({
+        name: "TimelineRecording.prototype.exportData",
+        description: "Test for a recording export.",
+        async test() {
+            let recording = WI.timelineManager.activeRecording;
+            InspectorTest.assert(!isNaN(recording.startTime), "FAIL: Previous test loading a recording failed.");
+            InspectorTest.assert(!isNaN(recording.endTime), "FAIL: Previous test loading a recording failed.");
+
+            InspectorTest.expectTrue(recording.canExport(), "TimelineRecording should be able to export.");
+
+            exportData = recording.exportData();
+            InspectorTest.expectThat(exportData, "TimelineRecording should be able to produce export data.");
+            InspectorTest.expectThat(exportData.records.length > 10, "TimelineRecording should have at least 10 Timeline Records.");
+
+            InspectorTest.log("Export Data:");
+            let filterKeys = new Set(["startTime", "endTime", "time", "records", "displayName"]);
+            InspectorTest.json(exportData, (key, value) => {
+                if (filterKeys.has(key))
+                    return "<filtered>";
+                return value;
+            });
+        }
+    });
+
+    suite.addTestCase({
+        name: "TimelineRecording.import",
+        description: "Test for a recording import.",
+        async test() {
+            InspectorTest.assert(exportData, "FAIL: Previous test exporting a recording failed.");
+
+            // NOTE: This runs the toJSON handlers on the timeline records and other model objects,
+            // which is important because importing expects the serialized form of the objects, not
+            // actual model objects.
+            let jsonData = JSON.parse(JSON.stringify(exportData));
+
+            const identifier = 999;
+            let recording = WI.TimelineRecording.import(identifier, jsonData, "TEST");
+            InspectorTest.expectNotEqual(WI.timelineManager.activeRecording, recording, "TimelineManager active recording is not this imported recording.");
+            InspectorTest.expectFalse(recording.capturing, "TimelineRecording should not be capturing.");
+            InspectorTest.expectTrue(recording.readonly, "TimelineRecording should be readonly.");
+            InspectorTest.expectTrue(recording.imported, "TimelineRecording should be imported.");
+            InspectorTest.expectThat(!isNaN(recording.startTime), "TimelineRecording should have a startTime.");
+            InspectorTest.expectThat(!isNaN(recording.endTime), "TimelineRecording should have a endTime.");
+            InspectorTest.expectEqual(recording.identifier, identifier, `TimelineRecording identifier should be ${identifier}.`);
+
+            InspectorTest.log("Display Name:");
+            InspectorTest.log(recording.displayName);
+        }
+    });
+
+    suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body onload="runTest()">
+<p>Tests for timeline recording.</p>
+</body>
+</html>
index c4b7b07..a891215 100644 (file)
@@ -1,5 +1,183 @@
 2019-03-15  Joseph Pecoraro  <pecoraro@apple.com>
 
+        Web Inspector: Timelines - Import / Export Timeline Recordings
+        https://bugs.webkit.org/show_bug.cgi?id=195709
+        <rdar://problem/23188921>
+
+        Reviewed by Devin Rousso.
+
+        Timeline exporting saves TimelineRecording and TimelineOverview state.
+        The TimelineRecording includes all kinds of model objects, such as
+        records, markers, memory pressure events, etc. It also includes raw
+        protocol data, such as script profiler samples. TimelineOverview
+        includes some of the view state to restore, such as the selected
+        time range, zoom level, and selected timeline.
+
+        Timeline importing constructs a new TimelineRecording by replaying
+        the records, markers, and other events, as well as re-initializing
+        more state. To finally display the imported recording, the content
+        view will immediately initialize start/current/end times and the
+        overview will restore the view state.
+
+        * Localizations/en.lproj/localizedStrings.js:
+        New strings.
+
+        * UserInterface/Controllers/TimelineManager.js:
+        (WI.TimelineManager.synthesizeImportError):
+        (WI.TimelineManager.prototype.importRecording):
+        Import API.
+
+        (WI.TimelineManager.prototype.scriptProfilerTrackingCompleted):
+        Initialize the samples on the recording via a different path
+        so that the data can be saved for exporting.
+
+        * UserInterface/Models/TimelineRecording.js:
+        (WI.TimelineRecording):
+        (WI.TimelineRecording.import):
+        (WI.TimelineRecording.prototype.exportData):
+        (WI.TimelineRecording.prototype.get capturing):
+        (WI.TimelineRecording.prototype.get imported):
+        (WI.TimelineRecording.prototype.unloaded):
+        (WI.TimelineRecording.prototype.reset):
+        (WI.TimelineRecording.prototype.addEventMarker):
+        (WI.TimelineRecording.prototype.addRecord):
+        (WI.TimelineRecording.prototype.addMemoryPressureEvent):
+        (WI.TimelineRecording.prototype.initializeCallingContextTrees):
+        (WI.TimelineRecording.prototype.canExport):
+        Save data at the TimelineRecording level that can be used for export.
+        We only allow exporting a TimelineRecording that has started/stopped
+        at least once and is not currently capturing.
+
+        * UserInterface/Views/TimelineRecordingContentView.js:
+        (WI.TimelineRecordingContentView):
+        (WI.TimelineRecordingContentView.prototype.get navigationItems):
+        (WI.TimelineRecordingContentView.prototype.get supportsSave):
+        (WI.TimelineRecordingContentView.prototype.get saveData):
+        (WI.TimelineRecordingContentView.prototype.shown):
+        (WI.TimelineRecordingContentView.prototype._capturingStarted):
+        (WI.TimelineRecordingContentView.prototype._capturingStopped):
+        (WI.TimelineRecordingContentView.prototype._initializeImportedRecording):
+        (WI.TimelineRecordingContentView.prototype._exportTimelineRecording):
+        (WI.TimelineRecordingContentView.prototype._importButtonNavigationItemClicked):
+        (WI.TimelineRecordingContentView.prototype._recordingReset):
+        Add Import and Export buttons in the Timeline navigation bar.
+
+        * UserInterface/Views/TimelineOverview.js:
+        (WI.TimelineOverview):
+        (WI.TimelineOverview.prototype.exportData):
+        (WI.TimelineOverview.prototype._instrumentAdded):
+        (WI.TimelineOverview.prototype._recordingImported):
+        When importing a recording update the TimelineOverview state
+        soon afterwards.
+
+        * UserInterface/Models/CPUTimelineRecord.js:
+        (WI.CPUTimelineRecord.fromJSON):
+        (WI.CPUTimelineRecord.prototype.toJSON):
+        * UserInterface/Models/GarbageCollection.js:
+        (WI.GarbageCollection.fromJSON):
+        (WI.GarbageCollection.prototype.toJSON):
+        * UserInterface/Models/Geometry.js:
+        (WI.Quad.fromJSON):
+        (WI.Quad.prototype.toJSON):
+        * UserInterface/Models/HeapAllocationsTimelineRecord.js:
+        (WI.HeapAllocationsTimelineRecord.fromJSON):
+        (WI.HeapAllocationsTimelineRecord.prototype.toJSON):
+        * UserInterface/Models/LayoutTimelineRecord.js:
+        (WI.LayoutTimelineRecord.fromJSON):
+        (WI.LayoutTimelineRecord.prototype.toJSON):
+        * UserInterface/Models/MediaTimelineRecord.js:
+        (WI.MediaTimelineRecord.fromJSON):
+        (WI.MediaTimelineRecord.prototype.toJSON):
+        * UserInterface/Models/MemoryPressureEvent.js:
+        (WI.MemoryPressureEvent.fromJSON):
+        (WI.MemoryPressureEvent.prototype.toJSON):
+        * UserInterface/Models/MemoryTimelineRecord.js:
+        (WI.MemoryTimelineRecord):
+        (WI.MemoryTimelineRecord.fromJSON):
+        (WI.MemoryTimelineRecord.prototype.toJSON):
+        * UserInterface/Models/RenderingFrameTimelineRecord.js:
+        (WI.RenderingFrameTimelineRecord.fromJSON):
+        (WI.RenderingFrameTimelineRecord.prototype.toJSON):
+        * UserInterface/Models/ResourceTimelineRecord.js:
+        (WI.ResourceTimelineRecord.fromJSON):
+        (WI.ResourceTimelineRecord.prototype.toJSON):
+        * UserInterface/Models/ScriptTimelineRecord.js:
+        (WI.ScriptTimelineRecord.fromJSON):
+        (WI.ScriptTimelineRecord.prototype.toJSON):
+        * UserInterface/Models/TimelineMarker.js:
+        (WI.TimelineMarker.fromJSON):
+        (WI.TimelineMarker.prototype.toJSON):
+        (WI.TimelineMarker.prototype.get type):
+        (WI.TimelineMarker.prototype.get details):
+        (WI.TimelineMarker.prototype.set time):
+        (WI.TimelineMarker):
+        * UserInterface/Models/TimelineRecord.js:
+        (WI.TimelineRecord.fromJSON):
+        (WI.TimelineRecord.prototype.toJSON):
+        Import / Export toJSON / fromJSON implementations.
+        
+        * UserInterface/Views/CPUTimelineOverviewGraph.js:
+        (WI.CPUTimelineOverviewGraph):
+        (WI.CPUTimelineOverviewGraph.prototype._cpuTimelineRecordAdded):
+        (WI.CPUTimelineOverviewGraph.prototype._processRecord):
+        * UserInterface/Views/LayoutTimelineOverviewGraph.js:
+        (WI.LayoutTimelineOverviewGraph):
+        (WI.LayoutTimelineOverviewGraph.prototype._layoutTimelineRecordAdded):
+        (WI.LayoutTimelineOverviewGraph.prototype._processRecord):
+        * UserInterface/Views/LayoutTimelineView.js:
+        (WI.LayoutTimelineView):
+        (WI.LayoutTimelineView.prototype._layoutTimelineRecordAdded):
+        (WI.LayoutTimelineView.prototype._processRecord):
+        * UserInterface/Views/MediaTimelineView.js:
+        (WI.MediaTimelineView):
+        (WI.MediaTimelineView.prototype._handleRecordAdded):
+        (WI.MediaTimelineView.prototype._processRecord):
+        * UserInterface/Views/MemoryTimelineOverviewGraph.js:
+        (WI.MemoryTimelineOverviewGraph):
+        (WI.MemoryTimelineOverviewGraph.prototype._memoryTimelineRecordAdded):
+        (WI.MemoryTimelineOverviewGraph.prototype._processRecord):
+        * UserInterface/Views/MemoryTimelineView.js:
+        (WI.MemoryTimelineView):
+        (WI.MemoryTimelineView.prototype._memoryTimelineRecordAdded):
+        (WI.MemoryTimelineView.prototype._processRecord):
+        * UserInterface/Views/NetworkTimelineOverviewGraph.js:
+        (WI.NetworkTimelineOverviewGraph):
+        (WI.NetworkTimelineOverviewGraph.prototype.reset):
+        (WI.NetworkTimelineOverviewGraph.prototype._networkTimelineRecordAdded):
+        (WI.NetworkTimelineOverviewGraph.prototype._processRecord):
+        (WI.NetworkTimelineOverviewGraph.prototype._networkTimelineRecordAdded.compareByStartTime): Deleted.
+        * UserInterface/Views/NetworkTimelineView.js:
+        (WI.NetworkTimelineView):
+        (WI.NetworkTimelineView.prototype._networkTimelineRecordAdded):
+        (WI.NetworkTimelineView.prototype._processRecord):
+        * UserInterface/Views/RenderingFrameTimelineView.js:
+        (WI.RenderingFrameTimelineView):
+        (WI.RenderingFrameTimelineView.prototype._renderingFrameTimelineRecordAdded):
+        (WI.RenderingFrameTimelineView.prototype._processRecord):
+        * UserInterface/Views/ScriptDetailsTimelineView.js:
+        (WI.ScriptDetailsTimelineView):
+        (WI.ScriptDetailsTimelineView.prototype._scriptTimelineRecordAdded):
+        (WI.ScriptDetailsTimelineView.prototype._processRecord):
+        Add common _processRecord path to each timeline OverviewGraph and TimelineView.
+        By calling this in construction we populate graphs with TimelineRecords that
+        may have already existed. This is necessary for imports, but this also fixes
+        the case where you enable a timeline that had data and it didn't show data.
+
+        * UserInterface/Views/LayoutTimelineOverviewGraph.css:
+        (.timeline-overview-graph.layout-overview > .graph-row):
+        (.timeline-overview-graph.layout-overview > .graph-row > .timeline-record-bar):
+        (.timeline-overview-graph.layout-overview > .graph-row > .timeline-record-bar > .segment):
+        (.timeline-overview-graph.layout > .graph-row): Deleted.
+        (.timeline-overview-graph.layout > .graph-row > .timeline-record-bar): Deleted.
+        (.timeline-overview-graph.layout > .graph-row > .timeline-record-bar > .segment): Deleted.
+        * UserInterface/Views/TimelineRecordBar.css:
+        (.timeline-record-bar.timeline-record-type-layout.paint > .segment,):
+        (.timeline-record-bar.timeline-record-type-layout.layout-timeline-record-paint > .segment,): Deleted.
+        We simplified some of the sub-record type enum strings. To do this we needed to change
+        "layout" to "layout-overview" to avoid a conflict.
+
+2019-03-15  Joseph Pecoraro  <pecoraro@apple.com>
+
         Web Inspector: Network - Toggle Between Live Activity and Imported HAR resource collections
         https://bugs.webkit.org/show_bug.cgi?id=195734
 
index 290a535..2d8b2d1 100644 (file)
@@ -554,6 +554,7 @@ localizedStrings["Import"] = "Import";
 localizedStrings["Imported"] = "Imported";
 localizedStrings["Imported - %s"] = "Imported - %s";
 localizedStrings["Imported Recordings"] = "Imported Recordings";
+localizedStrings["Imported Timeline Recording"] = "Imported Timeline Recording";
 localizedStrings["Imported \u2014 %s"] = "Imported \u2014 %s";
 localizedStrings["Incomplete"] = "Incomplete";
 localizedStrings["Indent width:"] = "Indent width:";
@@ -1055,6 +1056,7 @@ localizedStrings["Time spent on the main thread"] = "Time spent on the main thre
 localizedStrings["Time to First Byte"] = "Time to First Byte";
 localizedStrings["Timeline"] = "Timeline";
 localizedStrings["Timeline Recording %d"] = "Timeline Recording %d";
+localizedStrings["Timeline Recording Import Error: %s"] = "Timeline Recording Import Error: %s";
 localizedStrings["Timelines"] = "Timelines";
 localizedStrings["Timer %d Fired"] = "Timer %d Fired";
 localizedStrings["Timer %d Installed"] = "Timer %d Installed";
@@ -1189,4 +1191,5 @@ localizedStrings["toggle"] = "toggle";
 localizedStrings["unknown %s \u0022%s\u0022"] = "unknown %s \u0022%s\u0022";
 localizedStrings["unsupported %s"] = "unsupported %s";
 localizedStrings["unsupported HAR version"] = "unsupported HAR version";
+localizedStrings["unsupported version"] = "unsupported version";
 localizedStrings["value"] = "value";
index 4d743ab..fcf9a2e 100644 (file)
@@ -2736,7 +2736,7 @@ WI.elementDragEnd = function(event)
 
 WI.createMessageTextView = function(message, isError)
 {
-    var messageElement = document.createElement("div");
+    let messageElement = document.createElement("div");
     messageElement.className = "message-text-view";
     if (isError)
         messageElement.classList.add("error");
index 9f3ed1b..d8a5389 100644 (file)
@@ -136,6 +136,21 @@ WI.TimelineManager = class TimelineManager extends WI.Object
         return types;
     }
 
+    static synthesizeImportError(message)
+    {
+        message = WI.UIString("Timeline Recording Import Error: %s").format(message);
+
+        if (window.InspectorTest) {
+            console.error(message);
+            return;
+        }
+
+        let consoleMessage = new WI.ConsoleMessage(WI.mainTarget, WI.ConsoleMessage.MessageSource.Other, WI.ConsoleMessage.MessageLevel.Error, message);
+        consoleMessage.shouldRevealConsole = true;
+
+        WI.consoleLogViewController.appendConsoleMessage(consoleMessage);
+    }
+
     // Public
 
     reset()
@@ -259,6 +274,52 @@ WI.TimelineManager = class TimelineManager extends WI.Object
         this._activeRecording = null;
     }
 
+    processJSON({filename, json, error})
+    {
+        if (error) {
+            WI.TimelineManager.synthesizeImportError(error);
+            return;
+        }
+
+        if (typeof json !== "object" || json === null) {
+            WI.TimelineManager.synthesizeImportError(WI.UIString("invalid JSON"));
+            return;
+        }
+
+        if (!json.recording  || typeof json.recording !== "object" || !json.overview || typeof json.overview !== "object" || typeof json.version !== "number") {
+            WI.TimelineManager.synthesizeImportError(WI.UIString("invalid JSON"));
+            return;
+        }
+
+        if (json.version !== WI.TimelineRecording.SerializationVersion) {
+            WI.NetworkManager.synthesizeImportError(WI.UIString("unsupported version"));
+            return;
+        }
+
+        let recordingData = json.recording;
+        let overviewData = json.overview;
+
+        let identifier = this._nextRecordingIdentifier++;
+        let newRecording = WI.TimelineRecording.import(identifier, recordingData, filename);
+        this._recordings.push(newRecording);
+
+        this.dispatchEventToListeners(WI.TimelineManager.Event.RecordingCreated, {recording: newRecording});
+
+        if (this._isCapturing)
+            this.stopCapturing();
+
+        let oldRecording = this._activeRecording;
+        if (oldRecording) {
+            const importing = true;
+            oldRecording.unloaded(importing);
+        }
+
+        this._activeRecording = newRecording;
+
+        this.dispatchEventToListeners(WI.TimelineManager.Event.RecordingLoaded, {oldRecording});
+        this.dispatchEventToListeners(WI.TimelineManager.Event.RecordingImported, {overviewData});
+    }
+
     computeElapsedTime(timestamp)
     {
         if (!this._activeRecording)
@@ -1026,9 +1087,6 @@ WI.TimelineManager = class TimelineManager extends WI.Object
         if (samples) {
             let {stackTraces} = samples;
             let topDownCallingContextTree = this.activeRecording.topDownCallingContextTree;
-            let bottomUpCallingContextTree = this.activeRecording.bottomUpCallingContextTree;
-            let topFunctionsTopDownCallingContextTree = this.activeRecording.topFunctionsTopDownCallingContextTree;
-            let topFunctionsBottomUpCallingContextTree = this.activeRecording.topFunctionsBottomUpCallingContextTree;
 
             // Calculate a per-sample duration.
             let timestampIndex = 0;
@@ -1062,12 +1120,7 @@ WI.TimelineManager = class TimelineManager extends WI.Object
             if (timestampIndex < timestampCount)
                 sampleDurations.fill(defaultDuration, sampleDurationIndex);
 
-            for (let i = 0; i < stackTraces.length; i++) {
-                topDownCallingContextTree.updateTreeWithStackTrace(stackTraces[i], sampleDurations[i]);
-                bottomUpCallingContextTree.updateTreeWithStackTrace(stackTraces[i], sampleDurations[i]);
-                topFunctionsTopDownCallingContextTree.updateTreeWithStackTrace(stackTraces[i], sampleDurations[i]);
-                topFunctionsBottomUpCallingContextTree.updateTreeWithStackTrace(stackTraces[i], sampleDurations[i]);
-            }
+            this.activeRecording.initializeCallingContextTrees(stackTraces, sampleDurations);
 
             // FIXME: This transformation should not be needed after introducing ProfileView.
             // Once we eliminate ProfileNodeTreeElements and ProfileNodeDataGridNodes.
@@ -1213,6 +1266,7 @@ WI.TimelineManager = class TimelineManager extends WI.Object
 WI.TimelineManager.Event = {
     RecordingCreated: "timeline-manager-recording-created",
     RecordingLoaded: "timeline-manager-recording-loaded",
+    RecordingImported: "timeline-manager-recording-imported",
     CapturingWillStart: "timeline-manager-capturing-will-start",
     CapturingStarted: "timeline-manager-capturing-started",
     CapturingStopped: "timeline-manager-capturing-stopped"
index d1c2605..41db7de 100644 (file)
     <link rel="stylesheet" href="Views/TimelineRecordBar.css">
     <link rel="stylesheet" href="Views/TimelineRecordFrame.css">
     <link rel="stylesheet" href="Views/TimelineRecordingContentView.css">
+    <link rel="stylesheet" href="Views/TimelineRecordingImportedView.css">
     <link rel="stylesheet" href="Views/TimelineRuler.css">
     <link rel="stylesheet" href="Views/TimelineTabContentView.css">
     <link rel="stylesheet" href="Views/TimelineView.css">
     <script src="Views/TimelineRecordFrame.js"></script>
     <script src="Views/TimelineRecordingContentView.js"></script>
     <script src="Views/TimelineRecordingProgressView.js"></script>
+    <script src="Views/TimelineRecordingImportedView.js"></script>
     <script src="Views/TimelineRuler.js"></script>
     <script src="Views/TitleView.js"></script>
     <script src="Views/ToggleButtonNavigationItem.js"></script>
index ea07bfc..2166891 100644 (file)
@@ -36,8 +36,7 @@ WI.CPUTimelineRecord = class CPUTimelineRecord extends WI.TimelineRecord
 
         this._timestamp = timestamp;
         this._usage = usage;
-
-        threads = threads || [];
+        this._threads = threads || [];
 
         this._mainThreadUsage = 0;
         this._webkitThreadUsage = 0;
@@ -45,7 +44,7 @@ WI.CPUTimelineRecord = class CPUTimelineRecord extends WI.TimelineRecord
         this._unknownThreadUsage = 0;
         this._workersData = null;
 
-        for (let thread of threads) {
+        for (let thread of this._threads) {
             if (thread.type === InspectorBackend.domains.CPUProfiler.ThreadInfoType.Main) {
                 console.assert(!this._mainThreadUsage, "There should only be one main thread.");
                 this._mainThreadUsage += thread.usage;
@@ -69,6 +68,23 @@ WI.CPUTimelineRecord = class CPUTimelineRecord extends WI.TimelineRecord
         }
     }
 
+    // Import / Export
+
+    static fromJSON(json)
+    {
+        return new WI.CPUTimelineRecord(json);
+    }
+
+    toJSON()
+    {
+        return {
+            type: this.type,
+            timestamp: this._timestamp,
+            usage: this._usage,
+            threads: this._threads,
+        };
+    }
+
     // Public
 
     get timestamp() { return this._timestamp; }
index 91e6723..d9ffbc5 100644 (file)
@@ -45,6 +45,24 @@ WI.GarbageCollection = class GarbageCollection
         return new WI.GarbageCollection(type, payload.startTime, payload.endTime);
     }
 
+    // Import / Export
+
+    static fromJSON(json)
+    {
+        let {type, startTime, endTime} = json;
+        return new WI.GarbageCollection(type, startTime, endTime);
+    }
+
+    toJSON()
+    {
+        return {
+            __type: "GarbageCollection",
+            type: this.type,
+            startTime: this.startTime,
+            endTime: this.endTime,
+        };
+    }
+
     // Public
 
     get type() { return this._type; }
@@ -58,6 +76,6 @@ WI.GarbageCollection = class GarbageCollection
 };
 
 WI.GarbageCollection.Type = {
-    Partial: Symbol("Partial"),
-    Full: Symbol("Full")
+    Partial: "partial",
+    Full: "full",
 };
index 204cf3f..9f798b3 100644 (file)
@@ -284,6 +284,18 @@ WI.Quad = class Quad
         this.height = Math.round(Math.sqrt(Math.pow(quad[0] - quad[6], 2) + Math.pow(quad[1] - quad[7], 2)));
     }
 
+    // Import / Export
+
+    static fromJSON(json)
+    {
+        return new WI.Quad(json);
+    }
+
+    toJSON()
+    {
+        return this.toProtocol();
+    }
+
     // Public
 
     toProtocol()
index d10c0b9..111417f 100644 (file)
@@ -36,6 +36,36 @@ WI.HeapAllocationsTimelineRecord = class HeapAllocationsTimelineRecord extends W
         this._heapSnapshot = heapSnapshot;
     }
 
+    // Import / Export
+
+    static fromJSON(json)
+    {
+        // NOTE: This just goes through and generates a new heap snapshot,
+        // it is not perfect but does what we want. It asynchronously creates
+        // a heap snapshot at the right time, and insert it into the active
+        // recording, which on an import should be the newly imported recording.
+        let {timestamp, title, snapshotStringData} = json;
+
+        let workerProxy = WI.HeapSnapshotWorkerProxy.singleton();
+        workerProxy.createImportedSnapshot(snapshotStringData, title, ({objectId, snapshot: serializedSnapshot}) => {
+            let snapshot = WI.HeapSnapshotProxy.deserialize(objectId, serializedSnapshot);
+            snapshot.snapshotStringData = snapshotStringData;
+            WI.timelineManager.heapSnapshotAdded(timestamp, snapshot);
+        });
+
+        return null;
+    }
+
+    toJSON()
+    {
+        return {
+            type: this.type,
+            timestamp: this._timestamp,
+            title: WI.TimelineTabContentView.displayNameForRecord(this),
+            snapshotStringData: this._heapSnapshot.snapshotStringData,
+        };
+    }
+
     // Public
 
     get timestamp() { return this._timestamp; }
index 95f8af5..faa4b6c 100644 (file)
@@ -61,6 +61,29 @@ WI.LayoutTimelineRecord = class LayoutTimelineRecord extends WI.TimelineRecord
         }
     }
 
+    // Import / Export
+
+    static fromJSON(json)
+    {
+        let {eventType, startTime, endTime, callFrames, sourceCodeLocation, quad} = json;
+        quad = quad ? WI.Quad.fromJSON(quad) : null;
+        return new WI.LayoutTimelineRecord(eventType, startTime, endTime, callFrames, sourceCodeLocation, quad);
+    }
+
+    toJSON()
+    {
+        // FIXME: CallFrames
+        // FIXME: SourceCodeLocation
+
+        return {
+            type: this.type,
+            eventType: this._eventType,
+            startTime: this.startTime,
+            endTime: this.endTime,
+            quad: this._quad || undefined,
+        }
+    }
+
     // Public
 
     get eventType()
@@ -97,13 +120,13 @@ WI.LayoutTimelineRecord = class LayoutTimelineRecord extends WI.TimelineRecord
 };
 
 WI.LayoutTimelineRecord.EventType = {
-    InvalidateStyles: "layout-timeline-record-invalidate-styles",
-    RecalculateStyles: "layout-timeline-record-recalculate-styles",
-    InvalidateLayout: "layout-timeline-record-invalidate-layout",
-    ForcedLayout: "layout-timeline-record-forced-layout",
-    Layout: "layout-timeline-record-layout",
-    Paint: "layout-timeline-record-paint",
-    Composite: "layout-timeline-record-composite"
+    InvalidateStyles: "invalidate-styles",
+    RecalculateStyles: "recalculate-styles",
+    InvalidateLayout: "invalidate-layout",
+    ForcedLayout: "forced-layout",
+    Layout: "layout",
+    Paint: "paint",
+    Composite: "composite"
 };
 
 WI.LayoutTimelineRecord.TypeIdentifier = "layout-timeline-record";
index 8149c24..c1b9953 100644 (file)
@@ -37,6 +37,27 @@ WI.MediaTimelineRecord = class MediaTimelineRecord extends WI.TimelineRecord
         this._isLowPower = isLowPower || false;
     }
 
+    // Import / Export
+
+    static fromJSON(json)
+    {
+        let {eventType, timestamp} = json;
+        return new WI.MediaTimelineRecord(eventType, timestamp, json);
+    }
+
+    toJSON()
+    {
+        // FIXME: DOMNode
+
+        return {
+            type: this.type,
+            eventType: this._eventType,
+            timestamp: this.startTime,
+            domEvent: this._domEvent,
+            isLowPower: this._isLowPower,
+        };
+    }
+
     // Public
 
     get eventType() { return this._eventType; }
index 0cfeca7..940c369 100644 (file)
@@ -52,6 +52,23 @@ WI.MemoryPressureEvent = class MemoryPressureEvent
         return new WI.MemoryPressureEvent(timestamp, severity);
     }
 
+    // Import / Export
+
+
+    static fromJSON(json)
+    {
+        let {timestamp, severity} = json;
+        return new WI.MemoryPressureEvent(timestamp, severity);
+    }
+
+    toJSON()
+    {
+        return {
+            timestamp: this._timestamp,
+            severity: this._severity,
+        };
+    }
+
     // Public
 
     get timestamp() { return this._timestamp; }
@@ -59,6 +76,6 @@ WI.MemoryPressureEvent = class MemoryPressureEvent
 };
 
 WI.MemoryPressureEvent.Severity = {
-    Critical: Symbol("Critical"),
-    NonCritical: Symbol("NonCritical"),
+    Critical: "critical",
+    NonCritical: "non-critical",
 };
index 6395817..1e37b9a 100644 (file)
@@ -34,6 +34,7 @@ WI.MemoryTimelineRecord = class MemoryTimelineRecord extends WI.TimelineRecord
 
         this._timestamp = timestamp;
         this._categories = WI.MemoryTimelineRecord.memoryCategoriesFromProtocol(categories);
+        this._exportCategories = categories;
 
         this._totalSize = 0;
         for (let {size} of categories)
@@ -79,6 +80,23 @@ WI.MemoryTimelineRecord = class MemoryTimelineRecord extends WI.TimelineRecord
         ];
     }
 
+    // Import / Export
+
+    static fromJSON(json)
+    {
+        let {timestamp, categories} = json;
+        return new WI.MemoryTimelineRecord(timestamp, categories);
+    }
+
+    toJSON()
+    {
+        return {
+            type: this.type,
+            timestamp: this.startTime,
+            categories: this._exportCategories,
+        };
+    }
+
     // Public
 
     get timestamp() { return this._timestamp; }
index 10dca42..5fd8108 100644 (file)
@@ -69,6 +69,27 @@ WI.RenderingFrameTimelineRecord = class RenderingFrameTimelineRecord extends WI.
         }
     }
 
+    // Import / Export
+
+    static fromJSON(json)
+    {
+        let {startTime, endTime} = json;
+        let record = new WI.RenderingFrameTimelineRecord(startTime, endTime);
+        record.setupFrameIndex();
+        return record;
+    }
+
+    toJSON()
+    {
+        // FIXME: durationByTaskType data cannot be calculated if this does not have children.
+
+        return {
+            type: this.type,
+            startTime: this.startTime,
+            endTime: this.endTime,
+        };
+    }
+
     // Public
 
     get frameIndex()
index 38ae1d9..237c053 100644 (file)
@@ -66,6 +66,7 @@ WI.Resource = class Resource extends WI.SourceCode
         this._statusText = null;
         this._cached = false;
         this._canceled = false;
+        this._finished = false;
         this._failed = false;
         this._failureReasonText = null;
         this._receivedNetworkLoadMetrics = false;
index 1b54a70..098721e 100644 (file)
@@ -33,6 +33,26 @@ WI.ResourceTimelineRecord = class ResourceTimelineRecord extends WI.TimelineReco
         this._resource.addEventListener(WI.Resource.Event.TimestampsDidChange, this._dispatchUpdatedEvent, this);
     }
 
+    // Import / Export
+
+    static fromJSON(json)
+    {
+        let {entry, archiveStartTime} = json;
+        let localResource = WI.LocalResource.fromHAREntry(entry, archiveStartTime);
+        return new WI.ResourceTimelineRecord(localResource);
+    }
+
+    toJSON()
+    {
+        const content = "";
+
+        return {
+            type: this.type,
+            archiveStartTime: this._resource.requestSentWalltime - this.startTime,
+            entry: WI.HARBuilder.entry(this._resource, content),
+        };
+    }
+
     // Public
 
     get resource()
index 5f59b0d..77bca00 100644 (file)
@@ -48,6 +48,32 @@ WI.ScriptTimelineRecord = class ScriptTimelineRecord extends WI.TimelineRecord
         }
     }
 
+    // Import / Export
+
+    static fromJSON(json)
+    {
+        let {eventType, startTime, endTime, callFrames, sourceCodeLocation, details, profilePayload} = json;
+
+        if (typeof details === "object" && details.__type === "GarbageCollection")
+            details = WI.GarbageCollection.fromJSON(details);
+
+        return new WI.ScriptTimelineRecord(eventType, startTime, endTime, callFrames, sourceCodeLocation, details, profilePayload);
+    }
+
+    toJSON()
+    {
+        // FIXME: CallFrames
+        // FIXME: SourceCodeLocation
+
+        return {
+            type: this.type,
+            eventType: this._eventType,
+            startTime: this.startTime,
+            endTime: this.endTime,
+            details: this._details,
+        };
+    }
+
     // Public
 
     get eventType()
@@ -183,20 +209,20 @@ WI.ScriptTimelineRecord = class ScriptTimelineRecord extends WI.TimelineRecord
 };
 
 WI.ScriptTimelineRecord.EventType = {
-    ScriptEvaluated: "script-timeline-record-script-evaluated",
-    APIScriptEvaluated: "script-timeline-record-api-script-evaluated",
-    MicrotaskDispatched: "script-timeline-record-microtask-dispatched",
-    EventDispatched: "script-timeline-record-event-dispatched",
-    ProbeSampleRecorded: "script-timeline-record-probe-sample-recorded",
-    TimerFired: "script-timeline-record-timer-fired",
-    TimerInstalled: "script-timeline-record-timer-installed",
-    TimerRemoved: "script-timeline-record-timer-removed",
-    AnimationFrameFired: "script-timeline-record-animation-frame-fired",
-    AnimationFrameRequested: "script-timeline-record-animation-frame-requested",
-    AnimationFrameCanceled: "script-timeline-record-animation-frame-canceled",
-    ObserverCallback: "script-timeline-record-observer-callback",
-    ConsoleProfileRecorded: "script-timeline-record-console-profile-recorded",
-    GarbageCollected: "script-timeline-record-garbage-collected",
+    ScriptEvaluated: "script-evaluated",
+    APIScriptEvaluated: "api-script-evaluated",
+    MicrotaskDispatched: "microtask-dispatched",
+    EventDispatched: "event-dispatched",
+    ProbeSampleRecorded: "probe-sample-recorded",
+    TimerFired: "timer-fired",
+    TimerInstalled: "timer-installed",
+    TimerRemoved: "timer-removed",
+    AnimationFrameFired: "animation-frame-fired",
+    AnimationFrameRequested: "animation-frame-requested",
+    AnimationFrameCanceled: "animation-frame-canceled",
+    ObserverCallback: "observer-callback",
+    ConsoleProfileRecorded: "console-profile-recorded",
+    GarbageCollected: "garbage-collected",
 };
 
 WI.ScriptTimelineRecord.EventType.displayName = function(eventType, details, includeDetailsInMainTitle)
index 09a81d5..cc08de5 100644 (file)
@@ -74,7 +74,7 @@ WI.SourceMapResource = class SourceMapResource extends WI.Resource
         return resourceURLComponents.path.substring(sourceMappingBasePathURLComponents.path.length, resourceURLComponents.length);
     }
 
-    requestContentFromBackend(callback)
+    requestContentFromBackend()
     {
         // Revert the markAsFinished that was done in the constructor.
         this.revertMarkAsFinished();
index de069a7..dd47a36 100644 (file)
@@ -36,8 +36,28 @@ WI.TimelineMarker = class TimelineMarker extends WI.Object
         this._details = details || null;
     }
 
+    // Import / Export
+
+    static fromJSON(json)
+    {
+        let {time, type, details} = json;
+        return new WI.TimelineMarker(time, type, details);
+    }
+
+    toJSON()
+    {
+        return {
+            time: this._time,
+            type: this._type,
+            details: this._details || undefined,
+        };
+    }
+
     // Public
 
+    get type() { return this._type; }
+    get details() { return this._details; }
+
     get time()
     {
         return this._time;
@@ -56,16 +76,6 @@ WI.TimelineMarker = class TimelineMarker extends WI.Object
 
         this.dispatchEventToListeners(WI.TimelineMarker.Event.TimeChanged);
     }
-
-    get type()
-    {
-        return this._type;
-    }
-
-    get details()
-    {
-        return this._details;
-    }
 };
 
 WI.TimelineMarker.Event = {
index 2019a8d..c21e3fc 100644 (file)
@@ -42,6 +42,38 @@ WI.TimelineRecord = class TimelineRecord extends WI.Object
         this._children = [];
     }
 
+    // Import / Export
+
+    static fromJSON(json)
+    {
+        switch (json.type) {
+        case WI.TimelineRecord.Type.Network:
+            return WI.ResourceTimelineRecord.fromJSON(json);
+        case WI.TimelineRecord.Type.Layout:
+            return WI.LayoutTimelineRecord.fromJSON(json);
+        case WI.TimelineRecord.Type.Script:
+            return WI.ScriptTimelineRecord.fromJSON(json);
+        case WI.TimelineRecord.Type.RenderingFrame:
+            return WI.RenderingFrameTimelineRecord.fromJSON(json);
+        case WI.TimelineRecord.Type.CPU:
+            return WI.CPUTimelineRecord.fromJSON(json);
+        case WI.TimelineRecord.Type.Memory:
+            return WI.MemoryTimelineRecord.fromJSON(json);
+        case WI.TimelineRecord.Type.HeapAllocations:
+            return WI.HeapAllocationsTimelineRecord.fromJSON(json);
+        case WI.TimelineRecord.Type.Media:
+            return WI.MediaTimelineRecord.fromJSON(json);
+        default:
+            console.error("Unknown TimelineRecord.Type: " + json.type, json);
+            return null;
+        }
+    }
+
+    toJSON()
+    {
+        throw WI.NotImplementedError.subclassMustOverride();
+    }
+
     // Public
 
     get type()
index 9ae7638..d33a12d 100644 (file)
@@ -34,7 +34,19 @@ WI.TimelineRecording = class TimelineRecording extends WI.Object
         this._displayName = displayName;
         this._capturing = false;
         this._readonly = false;
+        this._imported = false;
         this._instruments = instruments || [];
+
+        this._startTime = NaN;
+        this._endTime = NaN;
+        this._discontinuities = null;
+
+        this._exportDataRecords = null;
+        this._exportDataMarkers = null;
+        this._exportDataMemoryPressureEvents = null;
+        this._exportDataSampleStackTraces = null;
+        this._exportDataSampleDurations = null;
+
         this._topDownCallingContextTree = new WI.CallingContextTree(WI.CallingContextTree.Type.TopDown);
         this._bottomUpCallingContextTree = new WI.CallingContextTree(WI.CallingContextTree.Type.BottomUp);
         this._topFunctionsTopDownCallingContextTree = new WI.CallingContextTree(WI.CallingContextTree.Type.TopFunctionsTopDown);
@@ -60,13 +72,85 @@ WI.TimelineRecording = class TimelineRecording extends WI.Object
         return WI.sharedApp.debuggableType === WI.DebuggableType.Web;
     }
 
+    // Import / Export
+
+    static import(identifier, json, displayName)
+    {
+        let {startTime, endTime, discontinuities, instrumentTypes, records, markers, memoryPressureEvents, sampleStackTraces, sampleDurations} = json;
+        let importedDisplayName = WI.UIString("Imported - %s").format(displayName);
+        let instruments = instrumentTypes.map((type) => WI.Instrument.createForTimelineType(type));
+        let recording = new WI.TimelineRecording(identifier, importedDisplayName, instruments);
+
+        recording._readonly = true;
+        recording._imported = true;
+        recording._startTime = startTime;
+        recording._endTime = endTime;
+        recording._discontinuities = discontinuities;
+
+        recording.initializeCallingContextTrees(sampleStackTraces, sampleDurations);
+
+        for (let recordJSON of records) {
+            let record = WI.TimelineRecord.fromJSON(recordJSON);
+            if (record) {
+                recording.addRecord(record);
+
+                if (record instanceof WI.ScriptTimelineRecord)
+                    record.profilePayload = recording._topDownCallingContextTree.toCPUProfilePayload(record.startTime, record.endTime);
+            }
+        }
+
+        for (let memoryPressureJSON of memoryPressureEvents) {
+            let memoryPressureEvent = WI.MemoryPressureEvent.fromJSON(memoryPressureJSON);
+            if (memoryPressureEvent)
+                recording.addMemoryPressureEvent(memoryPressureEvent);
+        }
+
+        // Add markers once we've transitioned the active recording.
+        setTimeout(() => {
+            recording.__importing = true;
+
+            for (let markerJSON of markers) {
+                let marker = WI.TimelineMarker.fromJSON(markerJSON);
+                if (marker)
+                    recording.addEventMarker(marker);
+            }
+
+            recording.__importing = false;
+        });
+
+        return recording;
+    }
+
+    exportData()
+    {
+        console.assert(this.canExport(), "Attempted to export a recording which should not be exportable.");
+
+        // FIXME: Overview data (sourceCodeTimelinesMap).
+        // FIXME: Record hierarchy (parent / child relationship) is lost.
+
+        return {
+            displayName: this._displayName,
+            startTime: this._startTime,
+            endTime: this._endTime,
+            discontinuities: this._discontinuities,
+            instrumentTypes: this._instruments.map((instrument) => instrument.timelineRecordType),
+            records: this._exportDataRecords,
+            markers: this._exportDataMarkers,
+            memoryPressureEvents: this._exportDataMemoryPressureEvents,
+            sampleStackTraces: this._exportDataSampleStackTraces,
+            sampleDurations: this._exportDataSampleDurations,
+        };
+    }
+
     // Public
 
     get displayName() { return this._displayName; }
     get identifier() { return this._identifier; }
     get timelines() { return this._timelines; }
     get instruments() { return this._instruments; }
+    get capturing() { return this._capturing; }
     get readonly() { return this._readonly; }
+    get imported() { return this._imported; }
     get startTime() { return this._startTime; }
     get endTime() { return this._endTime; }
 
@@ -113,9 +197,9 @@ WI.TimelineRecording = class TimelineRecording extends WI.Object
         return true;
     }
 
-    unloaded()
+    unloaded(importing)
     {
-        console.assert(!this.isEmpty(), "Shouldn't unload an empty recording; it should be reused instead.");
+        console.assert(importing || !this.isEmpty(), "Shouldn't unload an empty recording; it should be reused instead.");
 
         this._readonly = true;
 
@@ -127,10 +211,17 @@ WI.TimelineRecording = class TimelineRecording extends WI.Object
         console.assert(!this._readonly, "Can't reset a read-only recording.");
 
         this._sourceCodeTimelinesMap = new Map;
+
         this._startTime = NaN;
         this._endTime = NaN;
         this._discontinuities = [];
 
+        this._exportDataRecords = [];
+        this._exportDataMarkers = []
+        this._exportDataMemoryPressureEvents = [];
+        this._exportDataSampleStackTraces = [];
+        this._exportDataSampleDurations = [];
+
         this._topDownCallingContextTree.reset();
         this._bottomUpCallingContextTree.reset();
         this._topFunctionsTopDownCallingContextTree.reset();
@@ -192,7 +283,9 @@ WI.TimelineRecording = class TimelineRecording extends WI.Object
 
     addEventMarker(marker)
     {
-        if (!this._capturing)
+        this._exportDataMarkers.push(marker);
+
+        if (!this._capturing && !this.__importing)
             return;
 
         this.dispatchEventToListeners(WI.TimelineRecording.Event.MarkerAdded, {marker});
@@ -200,7 +293,9 @@ WI.TimelineRecording = class TimelineRecording extends WI.Object
 
     addRecord(record)
     {
-        var timeline = this._timelines.get(record.type);
+        this._exportDataRecords.push(record);
+
+        let timeline = this._timelines.get(record.type);
         console.assert(timeline, record, this._timelines);
         if (!timeline)
             return;
@@ -247,6 +342,8 @@ WI.TimelineRecording = class TimelineRecording extends WI.Object
 
     addMemoryPressureEvent(memoryPressureEvent)
     {
+        this._exportDataMemoryPressureEvents.push(memoryPressureEvent);
+
         let memoryTimeline = this._timelines.get(WI.TimelineRecord.Type.Memory);
         console.assert(memoryTimeline, this._timelines);
         if (!memoryTimeline)
@@ -326,6 +423,30 @@ WI.TimelineRecording = class TimelineRecording extends WI.Object
         }
     }
 
+    initializeCallingContextTrees(stackTraces, sampleDurations)
+    {
+        this._exportDataSampleStackTraces.concat(stackTraces);
+        this._exportDataSampleDurations.concat(sampleDurations);
+
+        for (let i = 0; i < stackTraces.length; i++) {
+            this._topDownCallingContextTree.updateTreeWithStackTrace(stackTraces[i], sampleDurations[i]);
+            this._bottomUpCallingContextTree.updateTreeWithStackTrace(stackTraces[i], sampleDurations[i]);
+            this._topFunctionsTopDownCallingContextTree.updateTreeWithStackTrace(stackTraces[i], sampleDurations[i]);
+            this._topFunctionsBottomUpCallingContextTree.updateTreeWithStackTrace(stackTraces[i], sampleDurations[i]);
+        }
+    }
+
+    canExport()
+    {
+        if (this._capturing)
+            return false;
+
+        if (isNaN(this._startTime))
+            return false;
+
+        return true;
+    }
+
     // Private
 
     _keyForRecord(record)
@@ -373,3 +494,5 @@ WI.TimelineRecording.Event = {
 WI.TimelineRecording.isLegacy = undefined;
 WI.TimelineRecording.TimestampThresholdForLegacyRecordConversion = 10000000; // Some value not near zero.
 WI.TimelineRecording.TimestampThresholdForLegacyAssumedMilliseconds = 1420099200000; // Date.parse("Jan 1, 2015"). Milliseconds since epoch.
+
+WI.TimelineRecording.SerializationVersion = 1;
index a52af5d..ef1337f 100644 (file)
@@ -51,6 +51,9 @@ WI.CPUTimelineOverviewGraph = class CPUTimelineOverviewGraph extends WI.Timeline
         this._lastSelectedRecordInLayout = null;
 
         this.reset();
+
+        for (let record of this._cpuTimeline.records)
+            this._processRecord(record);
     }
 
     // Static
@@ -212,8 +215,13 @@ WI.CPUTimelineOverviewGraph = class CPUTimelineOverviewGraph extends WI.Timeline
     {
         let cpuTimelineRecord = event.data.record;
 
-        this._maxUsage = Math.max(this._maxUsage, cpuTimelineRecord.usage);
+        this._processRecord(cpuTimelineRecord);
 
         this.needsLayout();
     }
+
+    _processRecord(cpuTimelineRecord)
+    {
+        this._maxUsage = Math.max(this._maxUsage, cpuTimelineRecord.usage);
+    }
 };
index 0e92078..4c96245 100644 (file)
@@ -219,22 +219,6 @@ 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)
index 5f07971..dda3ac8 100644 (file)
@@ -65,16 +65,6 @@ WI.HeapSnapshotContentView = class HeapSnapshotContentView extends WI.ContentVie
         return [];
     }
 
-    get supportsSave()
-    {
-        return this.representedObject instanceof WI.HeapSnapshotProxy;
-    }
-
-    get saveData()
-    {
-        return {customSaveHandler: () => { this._exportSnapshot(); }};
-    }
-
     shown()
     {
         super.shown();
index 8935487..8de4d2b 100644 (file)
  * THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-.timeline-overview-graph.layout > .graph-row {
+.timeline-overview-graph.layout-overview > .graph-row {
     height: 16px;
 }
 
-.timeline-overview-graph.layout > .graph-row > .timeline-record-bar {
+.timeline-overview-graph.layout-overview > .graph-row > .timeline-record-bar {
     height: 12px;
     margin-top: 4px;
 }
 
-.timeline-overview-graph.layout > .graph-row > .timeline-record-bar > .segment {
+.timeline-overview-graph.layout-overview > .graph-row > .timeline-record-bar > .segment {
     border-radius: 2px;
 }
index e98030a..642d00b 100644 (file)
@@ -29,12 +29,15 @@ WI.LayoutTimelineOverviewGraph = class LayoutTimelineOverviewGraph extends WI.Ti
     {
         super(timelineOverview);
 
-        this.element.classList.add("layout");
+        this.element.classList.add("layout-overview");
 
         this._layoutTimeline = timeline;
         this._layoutTimeline.addEventListener(WI.Timeline.Event.RecordAdded, this._layoutTimelineRecordAdded, this);
 
         this.reset();
+
+        for (let record of this._layoutTimeline.records)
+            this._processRecord(record);
     }
 
     // Public
@@ -118,14 +121,19 @@ WI.LayoutTimelineOverviewGraph = class LayoutTimelineOverviewGraph extends WI.Ti
 
     _layoutTimelineRecordAdded(event)
     {
-        var layoutTimelineRecord = event.data.record;
+        let layoutTimelineRecord = event.data.record;
         console.assert(layoutTimelineRecord instanceof WI.LayoutTimelineRecord);
 
+        this._processRecord(layoutTimelineRecord);
+
+        this.needsLayout();
+    }
+
+    _processRecord(layoutTimelineRecord)
+    {
         if (layoutTimelineRecord.eventType === WI.LayoutTimelineRecord.EventType.Paint || layoutTimelineRecord.eventType === WI.LayoutTimelineRecord.EventType.Composite)
             this._timelinePaintRecordRow.records.push(layoutTimelineRecord);
         else
             this._timelineLayoutRecordRow.records.push(layoutTimelineRecord);
-
-        this.needsLayout();
     }
 };
index c117240..8f37613 100644 (file)
@@ -98,6 +98,9 @@ WI.LayoutTimelineView = class LayoutTimelineView extends WI.TimelineView
         timeline.addEventListener(WI.Timeline.Event.RecordAdded, this._layoutTimelineRecordAdded, this);
 
         this._pendingRecords = [];
+
+        for (let record of timeline.records)
+            this._processRecord(record);
     }
 
     // Public
@@ -231,16 +234,21 @@ WI.LayoutTimelineView = class LayoutTimelineView extends WI.TimelineView
 
     _layoutTimelineRecordAdded(event)
     {
-        var layoutTimelineRecord = event.data.record;
+        let layoutTimelineRecord = event.data.record;
         console.assert(layoutTimelineRecord instanceof WI.LayoutTimelineRecord);
 
+        this._processRecord(layoutTimelineRecord);
+
+        this.needsLayout();
+    }
+
+    _processRecord(layoutTimelineRecord)
+    {
         // Only add top-level records, to avoid processing child records multiple times.
         if (layoutTimelineRecord.parent instanceof WI.LayoutTimelineRecord)
             return;
 
         this._pendingRecords.push(layoutTimelineRecord);
-
-        this.needsLayout();
     }
 
     _updateHighlight()
index 3b402dc..c1648b2 100644 (file)
@@ -79,6 +79,9 @@ WI.MediaTimelineView = class MediaTimelineView extends WI.TimelineView
         timeline.addEventListener(WI.Timeline.Event.RecordAdded, this._handleRecordAdded, this);
 
         this._pendingRecords = [];
+
+        for (let record of timeline.records)
+            this._processRecord(record);
     }
 
     // Public
@@ -191,14 +194,19 @@ WI.MediaTimelineView = class MediaTimelineView extends WI.TimelineView
 
     _handleRecordAdded(event)
     {
-        let record = event.data.record;
-        console.assert(record instanceof WI.MediaTimelineRecord);
+        let mediaTimelineRecord = event.data.record;
+        console.assert(mediaTimelineRecord instanceof WI.MediaTimelineRecord);
 
-        this._pendingRecords.push(record);
+        this._processRecord(mediaTimelineRecord);
 
         this.needsLayout();
     }
 
+    _processRecord(mediaTimelineRecord)
+    {
+        this._pendingRecords.push(mediaTimelineRecord);
+    }
+
     _handleSelectionPathComponentSiblingSelected(event)
     {
         let pathComponent = event.data.pathComponent;
index c184cdf..496092f 100644 (file)
@@ -52,6 +52,9 @@ WI.MemoryTimelineOverviewGraph = class MemoryTimelineOverviewGraph extends WI.Ti
         this._memoryPressureMarkerElements = [];
 
         this.reset();
+
+        for (let record of this._memoryTimeline.records)
+            this._processRecord(record);
     }
 
     // Protected
@@ -244,7 +247,15 @@ WI.MemoryTimelineOverviewGraph = class MemoryTimelineOverviewGraph extends WI.Ti
     _memoryTimelineRecordAdded(event)
     {
         let memoryTimelineRecord = event.data.record;
+        console.assert(memoryTimelineRecord instanceof WI.MemoryTimelineRecord);
+
+        this._processRecord(memoryTimelineRecord);
+
+        this.needsLayout();
+    }
 
+    _processRecord(memoryTimelineRecord)
+    {
         this._maxSize = Math.max(this._maxSize, memoryTimelineRecord.totalSize);
 
         if (!this._didInitializeCategories) {
@@ -254,8 +265,6 @@ WI.MemoryTimelineOverviewGraph = class MemoryTimelineOverviewGraph extends WI.Ti
                 types.push(category.type);
             this._chart.initializeSections(types);
         }
-
-        this.needsLayout();
     }
 
     _memoryTimelineMemoryPressureEventAdded(event)
index 442b7da..5172770 100644 (file)
@@ -96,6 +96,9 @@ WI.MemoryTimelineView = class MemoryTimelineView extends WI.TimelineView
         timeline.addEventListener(WI.Timeline.Event.RecordAdded, this._memoryTimelineRecordAdded, this);
 
         this.element.addEventListener("mousemove", this._handleGraphMouseMove.bind(this));
+
+        for (let record of timeline.records)
+            this._processRecord(record);
     }
 
     // Static
@@ -452,12 +455,17 @@ WI.MemoryTimelineView = class MemoryTimelineView extends WI.TimelineView
         let memoryTimelineRecord = event.data.record;
         console.assert(memoryTimelineRecord instanceof WI.MemoryTimelineRecord);
 
+        this._processRecord(memoryTimelineRecord);
+
+        if (memoryTimelineRecord.startTime >= this.startTime && memoryTimelineRecord.endTime <= this.endTime)
+            this.needsLayout();
+    }
+
+    _processRecord(memoryTimelineRecord)
+    {
         if (!this._didInitializeCategories)
             this._initializeCategoryViews(memoryTimelineRecord);
 
         this._maxSize = Math.max(this._maxSize, memoryTimelineRecord.totalSize);
-
-        if (memoryTimelineRecord.startTime >= this.startTime && memoryTimelineRecord.endTime <= this.endTime)
-            this.needsLayout();
     }
 };
index 74d94ab..3283f93 100644 (file)
@@ -36,6 +36,9 @@ WI.NetworkTimelineOverviewGraph = class NetworkTimelineOverviewGraph extends WI.
         timeline.addEventListener(WI.Timeline.Event.TimesUpdated, this.needsLayout, this);
 
         this.reset();
+
+        for (let record of timeline.records)
+            this._processRecord(record);
     }
 
     // Public
@@ -47,12 +50,12 @@ WI.NetworkTimelineOverviewGraph = class NetworkTimelineOverviewGraph extends WI.
         this._nextDumpRow = 0;
         this._timelineRecordGridRows = [];
 
-        for (var i = 0; i < WI.NetworkTimelineOverviewGraph.MaximumRowCount; ++i)
+        for (let i = 0; i < WI.NetworkTimelineOverviewGraph.MaximumRowCount; ++i)
             this._timelineRecordGridRows.push([]);
 
         this.element.removeChildren();
 
-        for (var rowRecords of this._timelineRecordGridRows) {
+        for (let rowRecords of this._timelineRecordGridRows) {
             rowRecords.__element = document.createElement("div");
             rowRecords.__element.classList.add("graph-row");
             this.element.appendChild(rowRecords.__element);
@@ -106,21 +109,24 @@ WI.NetworkTimelineOverviewGraph = class NetworkTimelineOverviewGraph extends WI.
 
     _networkTimelineRecordAdded(event)
     {
-        var resourceTimelineRecord = event.data.record;
+        let resourceTimelineRecord = event.data.record;
         console.assert(resourceTimelineRecord instanceof WI.ResourceTimelineRecord);
 
-        function compareByStartTime(a, b)
-        {
-            return a.startTime - b.startTime;
-        }
+        this._processRecord(resourceTimelineRecord);
+
+        this.needsLayout();
+    }
 
+    _processRecord(resourceTimelineRecord)
+    {
+        let compareByStartTime = (a, b) => a.startTime - b.startTime;
         let minimumBarPaddingTime = WI.TimelineOverview.MinimumDurationPerPixel * (WI.TimelineRecordBar.MinimumWidthPixels + WI.TimelineRecordBar.MinimumMarginPixels);
 
         // Try to find a row that has room and does not overlap a previous record.
-        var foundRowForRecord = false;
-        for (var i = 0; i < this._timelineRecordGridRows.length; ++i) {
-            var rowRecords = this._timelineRecordGridRows[i];
-            var lastRecord = rowRecords.lastValue;
+        let foundRowForRecord = false;
+        for (let i = 0; i < this._timelineRecordGridRows.length; ++i) {
+            let rowRecords = this._timelineRecordGridRows[i];
+            let lastRecord = rowRecords.lastValue;
 
             if (!lastRecord || lastRecord.endTime + minimumBarPaddingTime <= resourceTimelineRecord.startTime) {
                 insertObjectIntoSortedArray(resourceTimelineRecord, rowRecords, compareByStartTime);
@@ -132,9 +138,9 @@ WI.NetworkTimelineOverviewGraph = class NetworkTimelineOverviewGraph extends WI.
 
         if (!foundRowForRecord) {
             // Try to find a row that does not overlap a previous record's active time, but it can overlap the inactive time.
-            for (var i = 0; i < this._timelineRecordGridRows.length; ++i) {
-                var rowRecords = this._timelineRecordGridRows[i];
-                var lastRecord = rowRecords.lastValue;
+            for (let i = 0; i < this._timelineRecordGridRows.length; ++i) {
+                let rowRecords = this._timelineRecordGridRows[i];
+                let lastRecord = rowRecords.lastValue;
                 console.assert(lastRecord);
 
                 if (lastRecord.activeStartTime + minimumBarPaddingTime <= resourceTimelineRecord.startTime) {
@@ -152,8 +158,6 @@ WI.NetworkTimelineOverviewGraph = class NetworkTimelineOverviewGraph extends WI.
                 this._nextDumpRow = 0;
             insertObjectIntoSortedArray(resourceTimelineRecord, this._timelineRecordGridRows[this._nextDumpRow++], compareByStartTime);
         }
-
-        this.needsLayout();
     }
 };
 
index f65a495..5aeb504 100644 (file)
@@ -136,6 +136,9 @@ WI.NetworkTimelineView = class NetworkTimelineView extends WI.TimelineView
 
         this._pendingRecords = [];
         this._resourceDataGridNodeMap = new Map;
+
+        for (let record of timeline.records)
+            this._processRecord(record);
     }
 
     // Public
@@ -267,11 +270,16 @@ WI.NetworkTimelineView = class NetworkTimelineView extends WI.TimelineView
 
     _networkTimelineRecordAdded(event)
     {
-        var resourceTimelineRecord = event.data.record;
+        let resourceTimelineRecord = event.data.record;
         console.assert(resourceTimelineRecord instanceof WI.ResourceTimelineRecord);
 
-        this._pendingRecords.push(resourceTimelineRecord);
+        this._processRecord(resourceTimelineRecord);
 
         this.needsLayout();
     }
+
+    _processRecord(resourceTimelineRecord)
+    {
+        this._pendingRecords.push(resourceTimelineRecord);
+    }
 };
index a895b77..e8cefe8 100644 (file)
@@ -52,7 +52,8 @@ WI.OverviewTimelineView = class OverviewTimelineView extends WI.TimelineView
         this._timelineRuler.addMarker(this._currentTimeMarker);
 
         this.element.classList.add("overview");
-        this.addSubview(this._dataGrid);
+        if (!this._recording.imported)
+            this.addSubview(this._dataGrid);
 
         this._networkTimeline = recording.timelines.get(WI.TimelineRecord.Type.Network);
         if (this._networkTimeline)
@@ -125,6 +126,11 @@ WI.OverviewTimelineView = class OverviewTimelineView extends WI.TimelineView
 
     // Protected
 
+    get showsImportedRecordingMessage()
+    {
+        return true;
+    }
+
     dataGridNodePathComponentSelected(event)
     {
         let dataGridNode = event.data.pathComponent.timelineDataGridNode;
@@ -135,6 +141,9 @@ WI.OverviewTimelineView = class OverviewTimelineView extends WI.TimelineView
 
     layout()
     {
+        if (this._recording.imported)
+            return;
+
         let oldZeroTime = this._timelineRuler.zeroTime;
         let oldStartTime = this._timelineRuler.startTime;
         let oldEndTime = this._timelineRuler.endTime;
index 68e0d02..29dead8 100644 (file)
@@ -90,6 +90,9 @@ WI.RenderingFrameTimelineView = class RenderingFrameTimelineView extends WI.Time
         timeline.addEventListener(WI.Timeline.Event.RecordAdded, this._renderingFrameTimelineRecordAdded, this);
 
         this._pendingRecords = [];
+
+        for (let record of timeline.records)
+            this._processRecord(record);
     }
 
     static displayNameForDurationFilter(filter)
@@ -277,15 +280,20 @@ WI.RenderingFrameTimelineView = class RenderingFrameTimelineView extends WI.Time
 
     _renderingFrameTimelineRecordAdded(event)
     {
-        var renderingFrameTimelineRecord = event.data.record;
+        let renderingFrameTimelineRecord = event.data.record;
         console.assert(renderingFrameTimelineRecord instanceof WI.RenderingFrameTimelineRecord);
         console.assert(renderingFrameTimelineRecord.children.length, "Missing child records for rendering frame.");
 
-        this._pendingRecords.push(renderingFrameTimelineRecord);
+        this._processRecord(renderingFrameTimelineRecord);
 
         this.needsLayout();
     }
 
+    _processRecord(renderingFrameTimelineRecord)
+    {
+        this._pendingRecords.push(renderingFrameTimelineRecord);
+    }
+
     _scopeBarSelectionDidChange()
     {
         this._dataGrid.filterDidChange();
index 88c392c..69bc1ae 100644 (file)
@@ -87,6 +87,9 @@ WI.ScriptDetailsTimelineView = class ScriptDetailsTimelineView extends WI.Timeli
         timeline.addEventListener(WI.Timeline.Event.Refreshed, this._scriptTimelineRecordRefreshed, this);
 
         this._pendingRecords = [];
+
+        for (let record of timeline.records)
+            this._processRecord(record);
     }
 
     // Public
@@ -224,14 +227,19 @@ WI.ScriptDetailsTimelineView = class ScriptDetailsTimelineView extends WI.Timeli
 
     _scriptTimelineRecordAdded(event)
     {
-        var scriptTimelineRecord = event.data.record;
+        let scriptTimelineRecord = event.data.record;
         console.assert(scriptTimelineRecord instanceof WI.ScriptTimelineRecord);
 
-        this._pendingRecords.push(scriptTimelineRecord);
+        this._processRecord(scriptTimelineRecord);
 
         this.needsLayout();
     }
 
+    _processRecord(scriptTimelineRecord)
+    {
+        this._pendingRecords.push(scriptTimelineRecord);
+    }
+
     _scriptTimelineRecordRefreshed(event)
     {
         this.needsLayout();
index 57536b7..aa6a3a4 100644 (file)
@@ -116,10 +116,28 @@ WI.TimelineOverview = class TimelineOverview extends WI.View
 
         this._viewModeDidChange();
 
+        WI.timelineManager.addEventListener(WI.TimelineManager.Event.RecordingImported, this._recordingImported, this);
         WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStarted, this._capturingStarted, this);
         WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStopped, this._capturingStopped, this);
     }
 
+    // Import / Export
+
+    exportData()
+    {
+        let json = {
+            secondsPerPixel: this.secondsPerPixel,
+            scrollStartTime: this.scrollStartTime,
+            selectionStartTime: this.selectionStartTime,
+            selectionDuration: this.selectionDuration,
+        };
+
+        if (this._selectedTimeline)
+            json.selectedTimelineType = this._selectedTimeline.type;
+
+        return json;
+    }
+
     // Public
 
     get selectedTimeline()
@@ -657,6 +675,8 @@ WI.TimelineOverview = class TimelineOverview extends WI.View
             overviewGraph.hidden();
             treeElement.hidden = true;
         }
+
+        this.needsLayout();
     }
 
     _instrumentRemoved(event)
@@ -1012,13 +1032,36 @@ WI.TimelineOverview = class TimelineOverview extends WI.View
         this._editingInstrumentsDidChange();
     }
 
-    _capturingStarted()
+    _recordingImported(event)
+    {
+        let {overviewData} = event.data;
+
+        if (overviewData.secondsPerPixel !== undefined)
+            this.secondsPerPixel = overviewData.secondsPerPixel;
+        if (overviewData.scrollStartTime !== undefined)
+            this.scrollStartTime = overviewData.scrollStartTime;
+        if (overviewData.selectionStartTime !== undefined)
+            this.selectionStartTime = overviewData.selectionStartTime;
+        if (overviewData.selectionDuration !== undefined) {
+            if (overviewData.selectionDuration === Number.MAX_VALUE)
+                this._timelineRuler.selectEntireRange();
+            else
+                this.selectionDuration = overviewData.selectionDuration;
+        }
+        if (overviewData.selectedTimelineType !== undefined) {
+            let timeline = this._recording.timelineForRecordType(overviewData.selectedTimelineType);
+            if (timeline)
+                this.selectedTimeline = timeline;
+        }
+    }
+
+    _capturingStarted(event)
     {
         this._editInstrumentsButton.enabled = false;
         this._stopEditingInstruments();
     }
 
-    _capturingStopped()
+    _capturingStopped(event)
     {
         this._editInstrumentsButton.enabled = true;
     }
index 0b30dae..a5d60e5 100644 (file)
@@ -111,8 +111,8 @@ body[dir=rtl] :focus .selected .timeline-record-bar.has-inactive-segment > .segm
 }
 
 
-.timeline-record-bar.timeline-record-type-layout.layout-timeline-record-paint > .segment,
-.timeline-record-bar.timeline-record-type-layout.layout-timeline-record-composite > .segment {
+.timeline-record-bar.timeline-record-type-layout.paint > .segment,
+.timeline-record-bar.timeline-record-type-layout.composite > .segment {
     background-color: hsl(76, 49%, 60%);
     border-color: hsl(79, 45%, 51%);
 }
@@ -122,7 +122,7 @@ body[dir=rtl] :focus .selected .timeline-record-bar.has-inactive-segment > .segm
     border-color: hsl(273, 33%, 58%);
 }
 
-.timeline-record-bar.timeline-record-type-script.script-timeline-record-garbage-collected > .segment {
+.timeline-record-bar.timeline-record-type-script.garbage-collected > .segment {
     background-color: hsl(23, 69%, 73%);
     border-color: hsl(11, 54%, 62%);    
 }
index 825ebad..cf6fbfc 100644 (file)
@@ -59,7 +59,7 @@
     --scope-bar-border-color-override: transparent;
 }
 
-.content-view.timeline-recording > .content-browser .recording-progress {
+.content-view.timeline-recording > .content-browser :matches(.recording-progress, .recording-imported) {
     position: absolute;
     left: 0;
     right: 0;
@@ -69,7 +69,7 @@
     background-color: var(--panel-background-color-light);
 }
 
-.content-view.timeline-recording > .content-browser .recording-progress > .status {
+.content-view.timeline-recording > .content-browser :matches(.recording-progress, .recording-imported) > .status {
     margin-top: 40px;
     margin-bottom: 10px;
     text-align: center;
index 2322b72..9ceb7f6 100644 (file)
@@ -64,6 +64,19 @@ WI.TimelineRecordingContentView = class TimelineRecordingContentView extends WI.
             WI.settings.timelinesAutoStop.addEventListener(WI.Setting.Event.Changed, this._handleTimelinesAutoStopSettingChanged, this);
         }
 
+        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.Low;
+        this._exportButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._exportButtonNavigationItemClicked, this);
+        this._exportButtonNavigationItem.enabled = false;
+
+        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.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
+        this._importButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._importButtonNavigationItemClicked, this);
+
         this._clearTimelineNavigationItem = new WI.ButtonNavigationItem("clear-timeline", WI.UIString("Clear Timeline (%s)").format(WI.clearKeyboardShortcut.displayName), "Images/NavigationItemTrash.svg", 15, 15);
         this._clearTimelineNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
         this._clearTimelineNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._clearTimeline, this);
@@ -74,6 +87,9 @@ WI.TimelineRecordingContentView = class TimelineRecordingContentView extends WI.
         this._progressView = new WI.TimelineRecordingProgressView;
         this._timelineContentBrowser.addSubview(this._progressView);
 
+        this._importedView = new WI.TimelineRecordingImportedView;
+        this._timelineContentBrowser.addSubview(this._importedView);
+
         this._timelineViewMap = new Map;
         this._pathComponentMap = new Map;
 
@@ -109,6 +125,11 @@ WI.TimelineRecordingContentView = class TimelineRecordingContentView extends WI.
             this._instrumentAdded(instrument);
 
         this.showOverviewTimelineView();
+
+        if (this._recording.imported) {
+            let {startTime, endTime} = this._recording;
+            this._updateTimes(startTime, endTime, endTime);
+        }
     }
 
     // Public
@@ -167,6 +188,9 @@ WI.TimelineRecordingContentView = class TimelineRecordingContentView extends WI.
         if (this._autoStopCheckboxNavigationItem)
             navigationItems.push(this._autoStopCheckboxNavigationItem);
         navigationItems.push(new WI.DividerNavigationItem);
+        navigationItems.push(this._importButtonNavigationItem);
+        navigationItems.push(this._exportButtonNavigationItem);
+        navigationItems.push(new WI.DividerNavigationItem);
         navigationItems.push(this._clearTimelineNavigationItem);
         return navigationItems;
     }
@@ -179,14 +203,12 @@ WI.TimelineRecordingContentView = class TimelineRecordingContentView extends WI.
 
     get supportsSave()
     {
-        let currentContentView = this._timelineContentBrowser.currentContentView;
-        return currentContentView && currentContentView.supportsSave;
+        return this._recording.canExport();
     }
 
     get saveData()
     {
-        let currentContentView = this._timelineContentBrowser.currentContentView;
-        return currentContentView && currentContentView.saveData || null;
+        return {customSaveHandler: () => { this._exportTimelineRecording(); }};
     }
 
     get currentTimelineView()
@@ -200,7 +222,9 @@ WI.TimelineRecordingContentView = class TimelineRecordingContentView extends WI.
 
         this._timelineOverview.shown();
         this._timelineContentBrowser.shown();
+
         this._clearTimelineNavigationItem.enabled = !this._recording.readonly && !isNaN(this._recording.startTime);
+        this._exportButtonNavigationItem.enabled = this._recording.canExport();
 
         this._currentContentViewDidChange();
 
@@ -292,6 +316,7 @@ WI.TimelineRecordingContentView = class TimelineRecordingContentView extends WI.
         this._timelineOverview.viewMode = newViewMode;
         this._updateTimelineOverviewHeight();
         this._updateProgressView();
+        this._updateImportedView();
         this._updateFilterBar();
 
         if (timelineView) {
@@ -497,6 +522,7 @@ WI.TimelineRecordingContentView = class TimelineRecordingContentView extends WI.
         if (!this._updating)
             this._startUpdatingCurrentTime(startTime);
         this._clearTimelineNavigationItem.enabled = !this._recording.readonly;
+        this._exportButtonNavigationItem.enabled = false;
 
         // A discontinuity occurs when the recording is stopped and resumed at
         // a future time. Capturing started signals the end of the current
@@ -518,6 +544,8 @@ WI.TimelineRecordingContentView = class TimelineRecordingContentView extends WI.
             this._updateTimelineViewTimes(this.currentTimelineView);
 
         this._discontinuityStartTime = event.data.endTime || this._currentTime;
+
+        this._exportButtonNavigationItem.enabled = this._recording.canExport();
     }
 
     _debuggerPaused(event)
@@ -562,6 +590,42 @@ WI.TimelineRecordingContentView = class TimelineRecordingContentView extends WI.
         this._autoStopCheckboxNavigationItem.checked = WI.settings.timelinesAutoStop.value;
     }
 
+    _exportTimelineRecording()
+    {
+        let json = {
+            version: WI.TimelineRecording.SerializationVersion,
+            recording: this._recording.exportData(),
+            overview: this._timelineOverview.exportData(),
+        };
+        if (!json.recording || !json.overview) {
+            InspectorFrontendHost.beep();
+            return;
+        }
+
+        let frameName = null;
+        let mainFrame = WI.networkManager.mainFrame;
+        if (mainFrame)
+            frameName = mainFrame.mainResource.urlComponents.host || mainFrame.mainResource.displayName;
+
+        let filename = frameName ? `${frameName}-recording` : this._recording.displayName;
+        let url = "web-inspector:///" + encodeURI(filename) + ".json";
+        WI.FileUtilities.save({
+            url,
+            content: JSON.stringify(json),
+            forceSaveAs: true,
+        });
+    }
+
+    _exportButtonNavigationItemClicked(event)
+    {
+        this._exportTimelineRecording();
+    }
+
+    _importButtonNavigationItemClicked(event)
+    {
+        WI.FileUtilities.importJSON((result) => WI.timelineManager.processJSON(result));
+    }
+
     _clearTimeline(event)
     {
         if (this._recording.readonly)
@@ -664,6 +728,7 @@ WI.TimelineRecordingContentView = class TimelineRecordingContentView extends WI.
         this._timelineOverview.reset();
         this._overviewTimelineView.reset();
         this._clearTimelineNavigationItem.enabled = false;
+        this._exportButtonNavigationItem.enabled = false;
     }
 
     _recordingUnloaded(event)
@@ -877,6 +942,11 @@ WI.TimelineRecordingContentView = class TimelineRecordingContentView extends WI.
         this._progressView.visible = isCapturing && this.currentTimelineView && !this.currentTimelineView.showsLiveRecordingData;
     }
 
+    _updateImportedView()
+    {
+        this._importedView.visible = this._recording.imported && this.currentTimelineView && this.currentTimelineView.showsImportedRecordingMessage;
+    }
+
     _updateFilterBar()
     {
         this._filterBarNavigationItem.hidden = !this.currentTimelineView || !this.currentTimelineView.showsFilterBar;
diff --git a/Source/WebInspectorUI/UserInterface/Views/TimelineRecordingImportedView.css b/Source/WebInspectorUI/UserInterface/Views/TimelineRecordingImportedView.css
new file mode 100644 (file)
index 0000000..e5142b9
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2019 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+.content-view.timeline-recording > .content-browser .recording-imported .message-text-view > .message {
+    font-size: var(--message-text-view-large-font-size);
+    font-weight: 600;
+    letter-spacing: 0.02em;
+}
diff --git a/Source/WebInspectorUI/UserInterface/Views/TimelineRecordingImportedView.js b/Source/WebInspectorUI/UserInterface/Views/TimelineRecordingImportedView.js
new file mode 100644 (file)
index 0000000..52717ef
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2019 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+WI.TimelineRecordingImportedView = class TimelineRecordingImportedView extends WI.View
+{
+    constructor()
+    {
+        super();
+
+        this.element.classList.add("recording-imported");
+        this.element.appendChild(WI.createMessageTextView(WI.UIString("Imported Timeline Recording")));
+    }
+
+    // Public
+
+    get visible()
+    {
+        return this._visible;
+    }
+
+    set visible(x)
+    {
+        if (this._visible === x)
+            return;
+
+        // FIXME: remove once <https://webkit.org/b/150741> is fixed.
+        this._visible = x;
+        this.element.classList.toggle("hidden", !this._visible);
+    }
+};
index 68fa7fa..37434d5 100644 (file)
@@ -47,7 +47,10 @@ WI.TimelineRecordingProgressView = class TimelineRecordingProgressView extends W
 
     // Public
 
-    get visible() { return this._visible; }
+    get visible()
+    {
+        return this._visible;
+    }
 
     set visible(x)
     {
index 85e7afd..ab13ccb 100644 (file)
@@ -56,6 +56,12 @@ WI.TimelineView = class TimelineView extends WI.ContentView
         return true;
     }
 
+    get showsImportedRecordingMessage()
+    {
+        // Implemented by sub-classes if needed.
+        return false;
+    }
+
     get showsFilterBar()
     {
         // Implemented by sub-classes if needed.