Web Inspector: Timelines: can't reliably stop/start a recording
authordrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 11 Apr 2019 21:06:47 +0000 (21:06 +0000)
committerdrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 11 Apr 2019 21:06:47 +0000 (21:06 +0000)
https://bugs.webkit.org/show_bug.cgi?id=196778
<rdar://problem/47606798>

Reviewed by Timothy Hatcher.

Source/JavaScriptCore:

* inspector/protocol/ScriptProfiler.json:
* inspector/protocol/Timeline.json:
It is possible to determine when programmatic capturing starts/stops in the frontend based
on the state when the backend causes the state to change, such as if the state is "inactive"
when the frontend is told that the backend has started capturing.

* inspector/protocol/CPUProfiler.json:
* inspector/protocol/Memory.json:
Send an end timestamp to match other instruments.

* inspector/JSGlobalObjectConsoleClient.cpp:
(Inspector::JSGlobalObjectConsoleClient::startConsoleProfile):
(Inspector::JSGlobalObjectConsoleClient::stopConsoleProfile):

* inspector/agents/InspectorScriptProfilerAgent.h:
* inspector/agents/InspectorScriptProfilerAgent.cpp:
(Inspector::InspectorScriptProfilerAgent::trackingComplete):
(Inspector::InspectorScriptProfilerAgent::programmaticCaptureStarted): Deleted.
(Inspector::InspectorScriptProfilerAgent::programmaticCaptureStopped): Deleted.

Source/WebCore:

* inspector/agents/InspectorTimelineAgent.cpp:
(WebCore::InspectorTimelineAgent::startProgrammaticCapture):
(WebCore::InspectorTimelineAgent::stopProgrammaticCapture):
It is possible to determine when programmatic capturing starts/stops in the frontend based
on the state when the backend causes the state to change, such as if the state is "inactive"
when the frontend is told that the backend has started capturing.

* inspector/agents/InspectorCPUProfilerAgent.cpp:
(WebCore::InspectorCPUProfilerAgent::stopTracking):
* inspector/agents/InspectorMemoryAgent.cpp:
(WebCore::InspectorMemoryAgent::stopTracking):
Send an end timestamp to match other instruments.

Source/WebInspectorUI:

Rather than have a binary state of capturing/not-capturing, we should use a four state:
 1. inactive (when the backend has stopped capturing)
 2. starting (when the frontend requests capturing to start)
 3. active (when the backend has started capturing)
 4. stopping (when the frontend requests capturing to stop)

Capturing is considered "on" when not in an "inactive" state. Prevent the frontend from
starting/stopping capturing unless we're in a "stable" ("inactive" or "active") state, not a
"transition" ("starting" or "stopping") state.

One "side effect" of this change is that since the capturing is considered active until the
backend has stopped capturing, we will continue to process records in the frontend even if
the frontend has requested to stop capturing. <https://webkit.org/b/152904>

* UserInterface/Controllers/TimelineManager.js:
(WI.TimelineManager):
(WI.TimelineManager.prototype.get capturingState): Added.
(WI.TimelineManager.prototype.reset):
(WI.TimelineManager.prototype.get activeRecording):
(WI.TimelineManager.prototype.set autoCaptureOnPageLoad):
(WI.TimelineManager.prototype.isCapturing):
(WI.TimelineManager.prototype.startCapturing):
(WI.TimelineManager.prototype.stopCapturing):
(WI.TimelineManager.prototype.processJSON):
(WI.TimelineManager.prototype.capturingStarted):
(WI.TimelineManager.prototype.capturingStopped):
(WI.TimelineManager.prototype.autoCaptureStarted):
(WI.TimelineManager.prototype.eventRecorded):
(WI.TimelineManager.prototype.pageDOMContentLoadedEventFired):
(WI.TimelineManager.prototype.pageLoadEventFired):
(WI.TimelineManager.prototype.cpuProfilerTrackingUpdated):
(WI.TimelineManager.prototype.cpuProfilerTrackingCompleted):
(WI.TimelineManager.prototype.memoryTrackingUpdated):
(WI.TimelineManager.prototype.memoryTrackingCompleted):
(WI.TimelineManager.prototype.heapTrackingStarted):
(WI.TimelineManager.prototype.heapTrackingCompleted):
(WI.TimelineManager.prototype.heapSnapshotAdded):
(WI.TimelineManager.prototype._updateCapturingState): Added.
(WI.TimelineManager.prototype._processRecord):
(WI.TimelineManager.prototype._processEvent):
(WI.TimelineManager.prototype._loadNewRecording):
(WI.TimelineManager.prototype._addRecord):
(WI.TimelineManager.prototype._attemptAutoCapturingForFrame):
(WI.TimelineManager.prototype._legacyAttemptStartAutoCapturingForFrame):
(WI.TimelineManager.prototype._stopAutoRecordingSoon):
(WI.TimelineManager.prototype._resetAutoRecordingDeadTimeTimeout):
(WI.TimelineManager.prototype._resourceWasAdded):
(WI.TimelineManager.prototype._garbageCollected):
(WI.TimelineManager.prototype._memoryPressure):
(WI.TimelineManager.prototype._handleTimelinesAutoStopSettingChanged):
(WI.TimelineManager.prototype.scriptProfilerTrackingCompleted):
(WI.TimelineManager.prototype._handleDOMNodeDidFireEvent):
(WI.TimelineManager.prototype._handleDOMNodeLowPowerChanged):
(WI.TimelineManager.prototype.unloadRecording): Deleted.
(WI.TimelineManager.prototype.programmaticCaptureStarted): Deleted.
(WI.TimelineManager.prototype.programmaticCaptureStopped): Deleted.
(WI.TimelineManager.prototype.scriptProfilerProgrammaticCaptureStarted): Deleted.
(WI.TimelineManager.prototype.scriptProfilerProgrammaticCaptureStopped): Deleted.

* UserInterface/Protocol/ScriptProfilerObserver.js:
(WI.ScriptProfilerObserver.prototype.trackingComplete):
(WI.ScriptProfilerObserver.prototype.programmaticCaptureStarted):
(WI.ScriptProfilerObserver.prototype.programmaticCaptureStopped):
* UserInterface/Protocol/TimelineObserver.js:
(WI.TimelineObserver.prototype.programmaticCaptureStarted):
(WI.TimelineObserver.prototype.programmaticCaptureStopped):
It is possible to determine when programmatic capturing starts/stops in the frontend based
on the state when the backend causes the state to change, such as if the state is "inactive"
when the frontend is told that the backend has started capturing.

* UserInterface/Protocol/CPUProfilerObserver.js:
(WI.CPUProfilerObserver.prototype.trackingComplete):
* UserInterface/Protocol/MemoryObserver.js:
(WI.MemoryObserver.prototype.trackingComplete):
Send an end timestamp to match other instruments.

* UserInterface/Controllers/DebuggerManager.js:
(WI.DebuggerManager):
(WI.DebuggerManager.prototype._handleTimelineCapturingStateChanged): Added.
(WI.DebuggerManager.prototype._timelineCapturingWillStart): Deleted.
(WI.DebuggerManager.prototype._timelineCapturingStopped): Deleted.
* UserInterface/Models/DefaultDashboard.js:
(WI.DefaultDashboard):
(WI.DefaultDashboard.prototype._handleTimelineCapturingStateChanged): Added.
(WI.DefaultDashboard.prototype._capturingStopped): Deleted.
* UserInterface/Views/DebuggerSidebarPanel.js:
(WI.DebuggerSidebarPanel):
(WI.DebuggerSidebarPanel.prototype._handleTimelineCapturingStateChanged): Added.
(WI.DebuggerSidebarPanel.prototype._timelineCapturingWillStart): Deleted.
(WI.DebuggerSidebarPanel.prototype._timelineCapturingStopped): Deleted.
* UserInterface/Views/SourcesNavigationSidebarPanel.js:
(WI.SourcesNavigationSidebarPanel):
(WI.SourcesNavigationSidebarPanel.prototype._handleTimelineCapturingStateChanged): Added.
(WI.SourcesNavigationSidebarPanel.prototype._handleTimelineCapturingWillStart): Deleted.
(WI.SourcesNavigationSidebarPanel.prototype._handleTimelineCapturingStopped): Deleted.
* UserInterface/Views/TimelineOverview.js:
(WI.TimelineOverview):
(WI.TimelineOverview.prototype._handleTimelineCapturingStateChanged): Added.
(WI.TimelineOverview.prototype._capturingStarted): Deleted.
(WI.TimelineOverview.prototype._capturingStopped): Deleted.
* UserInterface/Views/TimelineRecordingContentView.js:
(WI.TimelineRecordingContentView):
(WI.TimelineRecordingContentView.prototype._handleTimelineCapturingStateChanged): Added.
(WI.TimelineRecordingContentView.prototype._recordingUnloaded):
(WI.TimelineRecordingContentView.prototype._capturingStarted): Deleted.
(WI.TimelineRecordingContentView.prototype._capturingStopped): Deleted.
* UserInterface/Views/TimelineTabContentView.js:
(WI.TimelineTabContentView):
(WI.TimelineTabContentView.prototype._handleTimelineCapturingStateChanged): Added.
(WI.TimelineTabContentView.prototype._capturingStartedOrStopped): Deleted.
Use the new single event for all Timelines capture state changes.
Prevent the record button from being clicked when capturing is in a transition state.

LayoutTests:

* inspector/timeline/debugger-paused-while-recording.html:
* inspector/timeline/exception-in-injected-script-while-recording.html:
* inspector/timeline/line-column.html:
* inspector/timeline/recording-start-stop-timestamps.html:
* inspector/timeline/resources/timeline-event-utilities.js:
* inspector/timeline/setInstruments-programmatic-capture.html:
* inspector/timeline/setInstruments-programmatic-capture-expected.txt:
* inspector/timeline/timeline-recording.html:

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

34 files changed:
LayoutTests/ChangeLog
LayoutTests/inspector/timeline/debugger-paused-while-recording.html
LayoutTests/inspector/timeline/exception-in-injected-script-while-recording.html
LayoutTests/inspector/timeline/line-column.html
LayoutTests/inspector/timeline/recording-start-stop-timestamps.html
LayoutTests/inspector/timeline/resources/timeline-event-utilities.js
LayoutTests/inspector/timeline/setInstruments-programmatic-capture-expected.txt
LayoutTests/inspector/timeline/setInstruments-programmatic-capture.html
LayoutTests/inspector/timeline/timeline-recording.html
Source/JavaScriptCore/ChangeLog
Source/JavaScriptCore/inspector/JSGlobalObjectConsoleClient.cpp
Source/JavaScriptCore/inspector/agents/InspectorScriptProfilerAgent.cpp
Source/JavaScriptCore/inspector/agents/InspectorScriptProfilerAgent.h
Source/JavaScriptCore/inspector/protocol/CPUProfiler.json
Source/JavaScriptCore/inspector/protocol/Memory.json
Source/JavaScriptCore/inspector/protocol/ScriptProfiler.json
Source/JavaScriptCore/inspector/protocol/Timeline.json
Source/WebCore/ChangeLog
Source/WebCore/inspector/agents/InspectorCPUProfilerAgent.cpp
Source/WebCore/inspector/agents/InspectorMemoryAgent.cpp
Source/WebCore/inspector/agents/InspectorTimelineAgent.cpp
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/UserInterface/Controllers/DebuggerManager.js
Source/WebInspectorUI/UserInterface/Controllers/TimelineManager.js
Source/WebInspectorUI/UserInterface/Models/DefaultDashboard.js
Source/WebInspectorUI/UserInterface/Protocol/CPUProfilerObserver.js
Source/WebInspectorUI/UserInterface/Protocol/MemoryObserver.js
Source/WebInspectorUI/UserInterface/Protocol/ScriptProfilerObserver.js
Source/WebInspectorUI/UserInterface/Protocol/TimelineObserver.js
Source/WebInspectorUI/UserInterface/Views/DebuggerSidebarPanel.js
Source/WebInspectorUI/UserInterface/Views/SourcesNavigationSidebarPanel.js
Source/WebInspectorUI/UserInterface/Views/TimelineOverview.js
Source/WebInspectorUI/UserInterface/Views/TimelineRecordingContentView.js
Source/WebInspectorUI/UserInterface/Views/TimelineTabContentView.js

index 6f03350..4ca18b0 100644 (file)
@@ -1,3 +1,20 @@
+2019-04-11  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: Timelines: can't reliably stop/start a recording
+        https://bugs.webkit.org/show_bug.cgi?id=196778
+        <rdar://problem/47606798>
+
+        Reviewed by Timothy Hatcher.
+
+        * inspector/timeline/debugger-paused-while-recording.html:
+        * inspector/timeline/exception-in-injected-script-while-recording.html:
+        * inspector/timeline/line-column.html:
+        * inspector/timeline/recording-start-stop-timestamps.html:
+        * inspector/timeline/resources/timeline-event-utilities.js:
+        * inspector/timeline/setInstruments-programmatic-capture.html:
+        * inspector/timeline/setInstruments-programmatic-capture-expected.txt:
+        * inspector/timeline/timeline-recording.html:
+
 2019-04-11  Carlos Garcia Campos  <cgarcia@igalia.com>
 
         [GTK] Layout test accessibility/aria-hidden-false-works-in-subtrees.html fails after r184890
index 7cf68b4..915ab09 100644 (file)
@@ -54,7 +54,10 @@ function test()
     });
 
     // When timeline capturing stops, inspect the resulting timeline records for a profile.
-    WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStopped, function(event) {
+    WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStateChanged, (event) => {
+        if (WI.timelineManager.capturingState !== WI.TimelineManager.CapturingState.Inactive)
+            return;
+
         var recording = WI.timelineManager.activeRecording;
         var scriptTimeline = recording.timelines.get(WI.TimelineRecord.Type.Script);
         console.assert(scriptTimeline);
index 381820b..1b52bc0 100644 (file)
@@ -60,7 +60,10 @@ function test()
     }
 
     // When timeline capturing stops, inspect the resulting timeline records for a profile.
-    WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStopped, function(event) {
+    WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStateChanged, (event) => {
+        if (WI.timelineManager.capturingState !== WI.TimelineManager.CapturingState.Inactive)
+            return;
+
         var recording = WI.timelineManager.activeRecording;
         var scriptTimeline = recording.timelines.get(WI.TimelineRecord.Type.Script);
         console.assert(scriptTimeline);
index 8f7c47b..55c5c57 100644 (file)
@@ -77,11 +77,11 @@ function test() {
                     ProtocolTest.log(JSON.stringify(event.params.record, replacer, 2));
                 });
 
-                handleEvent("Timeline.programmaticCaptureStarted", () => {
+                handleEvent("Timeline.recordingStarted", () => {
                     ProtocolTest.pass("Capturing started.");
                 });
 
-                handleEvent("Timeline.programmaticCaptureStopped", () => {
+                handleEvent("Timeline.recordingStopped", () => {
                     ProtocolTest.pass("Capturing stopped.");
 
                     for (let eventName of eventNames)
index fd159b3..bee051d 100644 (file)
@@ -13,39 +13,43 @@ function test()
         InspectorTest.log((condition ? "PASS" : "FAIL") + ": " + message);
     }
 
-    WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStarted, function(event) {
-        InspectorTest.assert(typeof event.data.startTime === "number");
-        InspectorTest.assert(event.data.startTime > 0);
-
-        if (isNaN(recordingStartTime)) {
-            InspectorTest.log("PASS: 1st CapturingStarted had startTime");
-        } else {
-            InspectorTest.assert(event.data.startTime > recordingStartTime, "FAIL: 2nd CapturingStarted should be > 1st CapturingStarted");
-            InspectorTest.log("PASS: 2nd CapturingStarted had startTime > 1st CapturingStarted");
-            InspectorTest.assert(event.data.startTime > recordingEndTime, "FAIL: 2nd CapturingStarted should be > 1st CapturingStopped");
-            InspectorTest.log("PASS: 2nd CapturingStarted had startTime > 1st CapturingStopped");
-        }
+    WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStateChanged, (event) => {
+        if (WI.timelineManager.capturingState === WI.TimelineManager.CapturingState.Active) {
+            InspectorTest.assert(typeof event.data.startTime === "number");
+            InspectorTest.assert(event.data.startTime > 0);
 
-        recordingStartTime = event.data.startTime;
-    });
+            if (isNaN(recordingStartTime)) {
+                InspectorTest.log("PASS: 1st CapturingStarted had startTime");
+            } else {
+                InspectorTest.assert(event.data.startTime > recordingStartTime, "FAIL: 2nd CapturingStarted should be > 1st CapturingStarted");
+                InspectorTest.log("PASS: 2nd CapturingStarted had startTime > 1st CapturingStarted");
+                InspectorTest.assert(event.data.startTime > recordingEndTime, "FAIL: 2nd CapturingStarted should be > 1st CapturingStopped");
+                InspectorTest.log("PASS: 2nd CapturingStarted had startTime > 1st CapturingStopped");
+            }
 
-    WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStopped, function(event) {
-        InspectorTest.assert(typeof event.data.endTime === "number");
-        InspectorTest.assert(event.data.endTime > 0);
-
-        if (isNaN(recordingEndTime)) {
-            InspectorTest.log("PASS: 1st CapturingStopped had endTime");
-            InspectorTest.assert(event.data.endTime > recordingStartTime, "FAIL: 1st CapturingStopped should be > 1st CapturingStarted");
-            InspectorTest.log("PASS: 1st CapturingStopped had endTime > 1st CapturingStarted");
-        } else {
-            InspectorTest.assert(event.data.endTime > recordingEndTime, "FAIL: 2nd CapturingStopped should be > 1st CapturingStopped");
-            InspectorTest.log("PASS: 2nd CapturingStopped had endTime > 1st CapturingStopped");
-            InspectorTest.assert(event.data.endTime > recordingStartTime, "FAIL: 2nd CapturingStopped should be > 2nd CapturingStarted");
-            InspectorTest.log("PASS: 2nd CapturingStopped had endTime > 2nd CapturingStarted");
-            InspectorTest.completeTest();
+            recordingStartTime = event.data.startTime;
+            return;
         }
 
-        recordingEndTime = event.data.endTime;
+        if (WI.timelineManager.capturingState === WI.TimelineManager.CapturingState.Inactive) {
+            InspectorTest.assert(typeof event.data.endTime === "number");
+            InspectorTest.assert(event.data.endTime > 0);
+
+            if (isNaN(recordingEndTime)) {
+                InspectorTest.log("PASS: 1st CapturingStopped had endTime");
+                InspectorTest.assert(event.data.endTime > recordingStartTime, "FAIL: 1st CapturingStopped should be > 1st CapturingStarted");
+                InspectorTest.log("PASS: 1st CapturingStopped had endTime > 1st CapturingStarted");
+            } else {
+                InspectorTest.assert(event.data.endTime > recordingEndTime, "FAIL: 2nd CapturingStopped should be > 1st CapturingStopped");
+                InspectorTest.log("PASS: 2nd CapturingStopped had endTime > 1st CapturingStopped");
+                InspectorTest.assert(event.data.endTime > recordingStartTime, "FAIL: 2nd CapturingStopped should be > 2nd CapturingStarted");
+                InspectorTest.log("PASS: 2nd CapturingStopped had endTime > 2nd CapturingStarted");
+                InspectorTest.completeTest();
+            }
+
+            recordingEndTime = event.data.endTime;
+            return;
+        }
     });
 
     TimelineAgent.start()
index b0e4127..b7cffef 100644 (file)
@@ -10,34 +10,40 @@ TestPage.registerInitializer(() => {
 
         let promise = new WI.WrappedPromise;
 
-        WI.timelineManager.awaitEvent(WI.TimelineManager.Event.CapturingStopped).then((capturingStoppedEvent) => {
-            InspectorTest.assert(pageRecordingData, "savePageData should have been called in the page before capturing was stopped.");
-            promise.resolve(pageRecordingData);
+        let listener = WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStateChanged, (event) => {
+            if (WI.timelineManager.capturingState === WI.TimelineManager.CapturingState.Active) {
+                let recording = WI.timelineManager.activeRecording;
+                let scriptTimeline = recording.timelines.get(WI.TimelineRecord.Type.Script);
+
+                let recordAddedListener = scriptTimeline.addEventListener(WI.Timeline.Event.RecordAdded, (recordAddedEvent) => {
+                    let {record} = recordAddedEvent.data;
+                    if (record.eventType !== eventType)
+                        return;
+
+                    scriptTimeline.removeEventListener(WI.Timeline.Event.RecordAdded, recordAddedListener);
+
+                    InspectorTest.log("Stopping Capture...");
+                    WI.timelineManager.stopCapturing();
+                });
+
+                InspectorTest.log("Evaluating...");
+                InspectorTest.evaluateInPage(expression)
+                .catch(promise.reject);
+                return;
+            }
+
+            if (WI.timelineManager.capturingState === WI.TimelineManager.CapturingState.Inactive) {
+                WI.timelineManager.removeEventListener(WI.TimelineManager.Event.CapturingStateChanged, listener);
+                InspectorTest.assert(pageRecordingData, "savePageData should have been called in the page before capturing was stopped.");
+                promise.resolve(pageRecordingData);
+                return;
+            }
         });
 
         InspectorTest.awaitEvent("SavePageData").then((event) => {
             pageRecordingData = event.data;
         });
 
-        WI.timelineManager.awaitEvent(WI.TimelineManager.Event.CapturingStarted).then((capturingStartedEvent) => {
-            let recording = WI.timelineManager.activeRecording;
-            let scriptTimeline = recording.timelines.get(WI.TimelineRecord.Type.Script);
-
-            let recordAddedListener = scriptTimeline.addEventListener(WI.Timeline.Event.RecordAdded, (recordAddedEvent) => {
-                let {record} = recordAddedEvent.data;
-                if (record.eventType !== eventType)
-                    return;
-
-                scriptTimeline.removeEventListener(WI.Timeline.Event.RecordAdded, recordAddedListener);
-
-                InspectorTest.log("Stopping Capture...");
-                WI.timelineManager.stopCapturing();
-            });
-
-            InspectorTest.log("Evaluating...");
-            return InspectorTest.evaluateInPage(expression);
-        });
-
         InspectorTest.log("Starting Capture...");
         const newRecording = true;
         WI.timelineManager.startCapturing(newRecording);
index 9426bec..0055c44 100644 (file)
@@ -7,20 +7,20 @@ Tests programmatic capture (console.profile/profileEnd) automatically starts ins
 == Running test suite: Timeline.setInstruments.programmatic-capture
 -- Running test case: NoInstrumentsProgrammaticCapture
 PASS: Should not be an error setting valid instruments.
-PASS: Timeline.programmaticCaptureStarted
 PASS: ScriptProfiler.startTracking
-PASS: Timeline.programmaticCaptureStopped
+PASS: Timeline.recordingStarted
+PASS: Timeline.recordingStopped
 
 -- Running test case: MultipleInstrumentsProgrammaticCapture
 PASS: Should not be an error setting valid instruments.
-PASS: Timeline.programmaticCaptureStarted
 PASS: ScriptProfiler.startTracking
+PASS: Timeline.recordingStarted
 PASS: Heap.trackingStart
-PASS: Timeline.programmaticCaptureStopped
+PASS: Timeline.recordingStopped
 
 -- Running test case: NoInstrumentsProgrammaticCaptureAgain
 PASS: Should not be an error setting valid instruments.
-PASS: Timeline.programmaticCaptureStarted
 PASS: ScriptProfiler.startTracking
-PASS: Timeline.programmaticCaptureStopped
+PASS: Timeline.recordingStarted
+PASS: Timeline.recordingStopped
 
index 93e9e15..05fa7ad 100644 (file)
@@ -14,16 +14,16 @@ function test()
     let suite = ProtocolTest.createAsyncSuite("Timeline.setInstruments.programmatic-capture");
 
     let heapExpected = false;
-    let programmaticCaptureStopped = null;
+    let captureStopped = null;
 
     InspectorProtocol.sendCommand("Heap.enable");
 
-    InspectorProtocol.eventHandler["Timeline.programmaticCaptureStarted"] = () => {
-        ProtocolTest.pass("Timeline.programmaticCaptureStarted");
+    InspectorProtocol.eventHandler["Timeline.recordingStarted"] = () => {
+        ProtocolTest.pass("Timeline.recordingStarted");
     };
-    InspectorProtocol.eventHandler["Timeline.programmaticCaptureStopped"] = () => {
-        ProtocolTest.pass("Timeline.programmaticCaptureStopped");
-        programmaticCaptureStopped();
+    InspectorProtocol.eventHandler["Timeline.recordingStopped"] = () => {
+        ProtocolTest.pass("Timeline.recordingStopped");
+        captureStopped();
     };
 
     InspectorProtocol.eventHandler["ScriptProfiler.trackingStart"] = () => {
@@ -41,7 +41,7 @@ function test()
         name: "NoInstrumentsProgrammaticCapture",
         test(resolve, reject) {
             heapExpected = false;
-            programmaticCaptureStopped = resolve;
+            captureStopped = resolve;
 
             InspectorProtocol.sendCommand("Timeline.setInstruments", {instruments: []}, (messageObject) => {
                 ProtocolTest.expectThat(!messageObject.error, "Should not be an error setting valid instruments.");
@@ -54,7 +54,7 @@ function test()
         name: "MultipleInstrumentsProgrammaticCapture",
         test(resolve, reject) {
             heapExpected = true;
-            programmaticCaptureStopped = resolve;
+            captureStopped = resolve;
 
             InspectorProtocol.sendCommand("Timeline.setInstruments", {instruments: ["ScriptProfiler", "Heap"]}, (messageObject) => {
                 ProtocolTest.expectThat(!messageObject.error, "Should not be an error setting valid instruments.");
@@ -67,7 +67,7 @@ function test()
         name: "NoInstrumentsProgrammaticCaptureAgain",
         test(resolve, reject) {
             heapExpected = true;
-            programmaticCaptureStopped = resolve;
+            captureStopped = resolve;
 
             InspectorProtocol.sendCommand("Timeline.setInstruments", {instruments: []}, (messageObject) => {
                 ProtocolTest.expectThat(!messageObject.error, "Should not be an error setting valid instruments.");
index 94f1821..10c1384 100644 (file)
@@ -13,6 +13,18 @@ function test()
         name: "TimelineRecording.Basic",
         description: "Make a basic Timeline recording.",
         async test() {
+            async function awaitCapturingState(capturingState) {
+                return new Promise((resolve, reject) => {
+                    let listener = WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStateChanged, (event) => {
+                        if (WI.timelineManager.capturingState !== capturingState)
+                            return;
+
+                        WI.timelineManager.removeEventListener(WI.TimelineManager.Event.CapturingStateChanged, listener);
+                        resolve();
+                    });
+                });
+            }
+
             let recording = WI.timelineManager.activeRecording;
 
             InspectorTest.log("Loaded");
@@ -25,7 +37,9 @@ function test()
 
             InspectorTest.log("Start");
             WI.timelineManager.startCapturing();
-            await WI.timelineManager.awaitEvent(WI.TimelineManager.Event.CapturingStarted);
+
+            await awaitCapturingState(WI.TimelineManager.CapturingState.Active);
+
             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.");
@@ -39,6 +53,9 @@ function test()
 
             InspectorTest.log("Stop");
             WI.timelineManager.stopCapturing();
+
+            await awaitCapturingState(WI.TimelineManager.CapturingState.Inactive);
+
             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.");
index 669fc58..397c348 100644 (file)
@@ -1,3 +1,31 @@
+2019-04-11  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: Timelines: can't reliably stop/start a recording
+        https://bugs.webkit.org/show_bug.cgi?id=196778
+        <rdar://problem/47606798>
+
+        Reviewed by Timothy Hatcher.
+
+        * inspector/protocol/ScriptProfiler.json:
+        * inspector/protocol/Timeline.json:
+        It is possible to determine when programmatic capturing starts/stops in the frontend based
+        on the state when the backend causes the state to change, such as if the state is "inactive"
+        when the frontend is told that the backend has started capturing.
+
+        * inspector/protocol/CPUProfiler.json:
+        * inspector/protocol/Memory.json:
+        Send an end timestamp to match other instruments.
+
+        * inspector/JSGlobalObjectConsoleClient.cpp:
+        (Inspector::JSGlobalObjectConsoleClient::startConsoleProfile):
+        (Inspector::JSGlobalObjectConsoleClient::stopConsoleProfile):
+
+        * inspector/agents/InspectorScriptProfilerAgent.h:
+        * inspector/agents/InspectorScriptProfilerAgent.cpp:
+        (Inspector::InspectorScriptProfilerAgent::trackingComplete):
+        (Inspector::InspectorScriptProfilerAgent::programmaticCaptureStarted): Deleted.
+        (Inspector::InspectorScriptProfilerAgent::programmaticCaptureStopped): Deleted.
+
 2019-04-11  Saam barati  <sbarati@apple.com>
 
         Rename SetArgument to SetArgumentDefinitely
index d1bd391..0cd9227 100644 (file)
@@ -121,10 +121,6 @@ void JSGlobalObjectConsoleClient::startConsoleProfile()
 {
     ErrorString unused;
 
-    // FIXME: <https://webkit.org/b/158753> Generalize the concept of Instruments on the backend to work equally for JSContext and Web inspection
-    if (m_scriptProfilerAgent)
-        m_scriptProfilerAgent->programmaticCaptureStarted();
-
     if (m_debuggerAgent) {
         m_profileRestoreBreakpointActiveValue = m_debuggerAgent->breakpointsActive();
         m_debuggerAgent->setBreakpointsActive(unused, false);
@@ -145,10 +141,6 @@ void JSGlobalObjectConsoleClient::stopConsoleProfile()
 
     if (m_debuggerAgent)
         m_debuggerAgent->setBreakpointsActive(unused, m_profileRestoreBreakpointActiveValue);
-
-    // FIXME: <https://webkit.org/b/158753> Generalize the concept of Instruments on the backend to work equally for JSContext and Web inspection
-    if (m_scriptProfilerAgent)
-        m_scriptProfilerAgent->programmaticCaptureStopped();
 }
 
 void JSGlobalObjectConsoleClient::takeHeapSnapshot(JSC::ExecState*, const String& title)
index 1ec25c4..56fc1b7 100644 (file)
@@ -203,6 +203,8 @@ static Ref<Protocol::ScriptProfiler::Samples> buildSamples(VM& vm, Vector<Sampli
 
 void InspectorScriptProfilerAgent::trackingComplete()
 {
+    auto timestamp = m_environment.executionStopwatch()->elapsedTime().seconds();
+
 #if ENABLE(SAMPLING_PROFILER)
     if (m_enabledSamplingProfiler) {
         VM& vm = m_environment.scriptDebugServer().vm();
@@ -220,11 +222,11 @@ void InspectorScriptProfilerAgent::trackingComplete()
 
         m_enabledSamplingProfiler = false;
 
-        m_frontendDispatcher->trackingComplete(WTFMove(samples));
+        m_frontendDispatcher->trackingComplete(timestamp, WTFMove(samples));
     } else
-        m_frontendDispatcher->trackingComplete(nullptr);
+        m_frontendDispatcher->trackingComplete(timestamp, nullptr);
 #else
-    m_frontendDispatcher->trackingComplete(nullptr);
+    m_frontendDispatcher->trackingComplete(timestamp, nullptr);
 #endif // ENABLE(SAMPLING_PROFILER)
 }
 
@@ -246,14 +248,4 @@ void InspectorScriptProfilerAgent::stopSamplingWhenDisconnecting()
 #endif
 }
 
-void InspectorScriptProfilerAgent::programmaticCaptureStarted()
-{
-    m_frontendDispatcher->programmaticCaptureStarted();
-}
-
-void InspectorScriptProfilerAgent::programmaticCaptureStopped()
-{
-    m_frontendDispatcher->programmaticCaptureStopped();
-}
-
 } // namespace Inspector
index dab774f..8a287d8 100644 (file)
@@ -53,9 +53,6 @@ public:
     void startTracking(ErrorString&, const bool* includeSamples) override;
     void stopTracking(ErrorString&) override;
 
-    void programmaticCaptureStarted();
-    void programmaticCaptureStopped();
-
     // Debugger::ProfilingClient
     bool isAlreadyProfiling() const override;
     Seconds willEvaluateScript() override;
index cddf9ca..1f60d9d 100644 (file)
         },
         {
             "name": "trackingComplete",
-            "description": "Tracking stopped. Includes any buffered data during tracking, such as profiling information."
+            "description": "Tracking stopped.",
+            "parameters": [
+                { "name": "timestamp", "type": "number" }
+            ]
         }
     ]
 }
index b6b3db9..1c3d01b 100644 (file)
         },
         {
             "name": "trackingComplete",
-            "description": "Tracking stopped."
+            "description": "Tracking stopped.",
+            "parameters": [
+                { "name": "timestamp", "type": "number" }
+            ]
         }
     ]
 }
index 35032a5..60ca687 100644 (file)
             "name": "trackingComplete",
             "description": "Tracking stopped. Includes any buffered data during tracking, such as profiling information.",
             "parameters": [
+                { "name": "timestamp", "type": "number" },
                 { "name": "samples", "$ref": "Samples", "optional": true, "description": "Stack traces." }
             ]
-        },
-        {
-            "name": "programmaticCaptureStarted",
-            "description": "Fired when programmatic capture starts (console.profile). JSContext inspection only."
-        },
-        {
-            "name": "programmaticCaptureStopped",
-            "description": "Fired when programmatic capture stops (console.profileEnd). JSContext inspection only."
         }
     ]
 }
index b9c4f84..9880032 100644 (file)
         {
             "name": "autoCaptureStarted",
             "description": "Fired when auto capture started."
-        },
-        {
-            "name": "programmaticCaptureStarted",
-            "description": "Fired when programmatic capture starts (console.profile)."
-        },
-        {
-            "name": "programmaticCaptureStopped",
-            "description": "Fired when programmatic capture stops (console.profileEnd)."
         }
     ]
 }
index b07f17d..7c1a454 100644 (file)
@@ -1,3 +1,24 @@
+2019-04-11  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: Timelines: can't reliably stop/start a recording
+        https://bugs.webkit.org/show_bug.cgi?id=196778
+        <rdar://problem/47606798>
+
+        Reviewed by Timothy Hatcher.
+
+        * inspector/agents/InspectorTimelineAgent.cpp:
+        (WebCore::InspectorTimelineAgent::startProgrammaticCapture):
+        (WebCore::InspectorTimelineAgent::stopProgrammaticCapture):
+        It is possible to determine when programmatic capturing starts/stops in the frontend based
+        on the state when the backend causes the state to change, such as if the state is "inactive"
+        when the frontend is told that the backend has started capturing.
+
+        * inspector/agents/InspectorCPUProfilerAgent.cpp:
+        (WebCore::InspectorCPUProfilerAgent::stopTracking):
+        * inspector/agents/InspectorMemoryAgent.cpp:
+        (WebCore::InspectorMemoryAgent::stopTracking):
+        Send an end timestamp to match other instruments.
+
 2019-04-11  Truitt Savell  <tsavell@apple.com>
 
         Unreviewed, rolling out r244158.
index 8f15504..6a2972d 100644 (file)
@@ -77,7 +77,7 @@ void InspectorCPUProfilerAgent::stopTracking(ErrorString&)
 
     m_tracking = false;
 
-    m_frontendDispatcher->trackingComplete();
+    m_frontendDispatcher->trackingComplete(m_environment.executionStopwatch()->elapsedTime().seconds());
 }
 
 static Ref<Protocol::CPUProfiler::ThreadInfo> buildThreadInfo(const ThreadCPUInfo& thread)
index 5bc56ea..22d47f5 100644 (file)
@@ -92,7 +92,7 @@ void InspectorMemoryAgent::stopTracking(ErrorString&)
 
     m_tracking = false;
 
-    m_frontendDispatcher->trackingComplete();
+    m_frontendDispatcher->trackingComplete(m_environment.executionStopwatch()->elapsedTime().seconds());
 }
 
 void InspectorMemoryAgent::didHandleMemoryPressure(Critical critical)
index 81dedce..a1d3d91 100644 (file)
@@ -477,8 +477,6 @@ void InspectorTimelineAgent::startProgrammaticCapture()
     } else
         m_programmaticCaptureRestoreBreakpointActiveValue = false;
 
-    m_frontendDispatcher->programmaticCaptureStarted();
-
     toggleScriptProfilerInstrument(InstrumentState::Start); // Ensure JavaScript samping data.
     toggleTimelineInstrument(InstrumentState::Start); // Ensure Console Profile event records.
     toggleInstruments(InstrumentState::Start); // Any other instruments the frontend wants us to record.
@@ -500,8 +498,6 @@ void InspectorTimelineAgent::stopProgrammaticCapture()
             debuggerAgent->setBreakpointsActive(unused, true);
         }
     }
-
-    m_frontendDispatcher->programmaticCaptureStopped();
 }
 
 void InspectorTimelineAgent::toggleInstruments(InstrumentState state)
index 5a0583b..8a103ae 100644 (file)
@@ -1,3 +1,124 @@
+2019-04-11  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: Timelines: can't reliably stop/start a recording
+        https://bugs.webkit.org/show_bug.cgi?id=196778
+        <rdar://problem/47606798>
+
+        Reviewed by Timothy Hatcher.
+
+        Rather than have a binary state of capturing/not-capturing, we should use a four state:
+         1. inactive (when the backend has stopped capturing)
+         2. starting (when the frontend requests capturing to start)
+         3. active (when the backend has started capturing)
+         4. stopping (when the frontend requests capturing to stop)
+
+        Capturing is considered "on" when not in an "inactive" state. Prevent the frontend from
+        starting/stopping capturing unless we're in a "stable" ("inactive" or "active") state, not a
+        "transition" ("starting" or "stopping") state.
+
+        One "side effect" of this change is that since the capturing is considered active until the
+        backend has stopped capturing, we will continue to process records in the frontend even if
+        the frontend has requested to stop capturing. <https://webkit.org/b/152904>
+
+        * UserInterface/Controllers/TimelineManager.js:
+        (WI.TimelineManager):
+        (WI.TimelineManager.prototype.get capturingState): Added.
+        (WI.TimelineManager.prototype.reset):
+        (WI.TimelineManager.prototype.get activeRecording):
+        (WI.TimelineManager.prototype.set autoCaptureOnPageLoad):
+        (WI.TimelineManager.prototype.isCapturing):
+        (WI.TimelineManager.prototype.startCapturing):
+        (WI.TimelineManager.prototype.stopCapturing):
+        (WI.TimelineManager.prototype.processJSON):
+        (WI.TimelineManager.prototype.capturingStarted):
+        (WI.TimelineManager.prototype.capturingStopped):
+        (WI.TimelineManager.prototype.autoCaptureStarted):
+        (WI.TimelineManager.prototype.eventRecorded):
+        (WI.TimelineManager.prototype.pageDOMContentLoadedEventFired):
+        (WI.TimelineManager.prototype.pageLoadEventFired):
+        (WI.TimelineManager.prototype.cpuProfilerTrackingUpdated):
+        (WI.TimelineManager.prototype.cpuProfilerTrackingCompleted):
+        (WI.TimelineManager.prototype.memoryTrackingUpdated):
+        (WI.TimelineManager.prototype.memoryTrackingCompleted):
+        (WI.TimelineManager.prototype.heapTrackingStarted):
+        (WI.TimelineManager.prototype.heapTrackingCompleted):
+        (WI.TimelineManager.prototype.heapSnapshotAdded):
+        (WI.TimelineManager.prototype._updateCapturingState): Added.
+        (WI.TimelineManager.prototype._processRecord):
+        (WI.TimelineManager.prototype._processEvent):
+        (WI.TimelineManager.prototype._loadNewRecording):
+        (WI.TimelineManager.prototype._addRecord):
+        (WI.TimelineManager.prototype._attemptAutoCapturingForFrame):
+        (WI.TimelineManager.prototype._legacyAttemptStartAutoCapturingForFrame):
+        (WI.TimelineManager.prototype._stopAutoRecordingSoon):
+        (WI.TimelineManager.prototype._resetAutoRecordingDeadTimeTimeout):
+        (WI.TimelineManager.prototype._resourceWasAdded):
+        (WI.TimelineManager.prototype._garbageCollected):
+        (WI.TimelineManager.prototype._memoryPressure):
+        (WI.TimelineManager.prototype._handleTimelinesAutoStopSettingChanged):
+        (WI.TimelineManager.prototype.scriptProfilerTrackingCompleted):
+        (WI.TimelineManager.prototype._handleDOMNodeDidFireEvent):
+        (WI.TimelineManager.prototype._handleDOMNodeLowPowerChanged):
+        (WI.TimelineManager.prototype.unloadRecording): Deleted.
+        (WI.TimelineManager.prototype.programmaticCaptureStarted): Deleted.
+        (WI.TimelineManager.prototype.programmaticCaptureStopped): Deleted.
+        (WI.TimelineManager.prototype.scriptProfilerProgrammaticCaptureStarted): Deleted.
+        (WI.TimelineManager.prototype.scriptProfilerProgrammaticCaptureStopped): Deleted.
+
+        * UserInterface/Protocol/ScriptProfilerObserver.js:
+        (WI.ScriptProfilerObserver.prototype.trackingComplete):
+        (WI.ScriptProfilerObserver.prototype.programmaticCaptureStarted):
+        (WI.ScriptProfilerObserver.prototype.programmaticCaptureStopped):
+        * UserInterface/Protocol/TimelineObserver.js:
+        (WI.TimelineObserver.prototype.programmaticCaptureStarted):
+        (WI.TimelineObserver.prototype.programmaticCaptureStopped):
+        It is possible to determine when programmatic capturing starts/stops in the frontend based
+        on the state when the backend causes the state to change, such as if the state is "inactive"
+        when the frontend is told that the backend has started capturing.
+
+        * UserInterface/Protocol/CPUProfilerObserver.js:
+        (WI.CPUProfilerObserver.prototype.trackingComplete):
+        * UserInterface/Protocol/MemoryObserver.js:
+        (WI.MemoryObserver.prototype.trackingComplete):
+        Send an end timestamp to match other instruments.
+
+        * UserInterface/Controllers/DebuggerManager.js:
+        (WI.DebuggerManager):
+        (WI.DebuggerManager.prototype._handleTimelineCapturingStateChanged): Added.
+        (WI.DebuggerManager.prototype._timelineCapturingWillStart): Deleted.
+        (WI.DebuggerManager.prototype._timelineCapturingStopped): Deleted.
+        * UserInterface/Models/DefaultDashboard.js:
+        (WI.DefaultDashboard):
+        (WI.DefaultDashboard.prototype._handleTimelineCapturingStateChanged): Added.
+        (WI.DefaultDashboard.prototype._capturingStopped): Deleted.
+        * UserInterface/Views/DebuggerSidebarPanel.js:
+        (WI.DebuggerSidebarPanel):
+        (WI.DebuggerSidebarPanel.prototype._handleTimelineCapturingStateChanged): Added.
+        (WI.DebuggerSidebarPanel.prototype._timelineCapturingWillStart): Deleted.
+        (WI.DebuggerSidebarPanel.prototype._timelineCapturingStopped): Deleted.
+        * UserInterface/Views/SourcesNavigationSidebarPanel.js:
+        (WI.SourcesNavigationSidebarPanel):
+        (WI.SourcesNavigationSidebarPanel.prototype._handleTimelineCapturingStateChanged): Added.
+        (WI.SourcesNavigationSidebarPanel.prototype._handleTimelineCapturingWillStart): Deleted.
+        (WI.SourcesNavigationSidebarPanel.prototype._handleTimelineCapturingStopped): Deleted.
+        * UserInterface/Views/TimelineOverview.js:
+        (WI.TimelineOverview):
+        (WI.TimelineOverview.prototype._handleTimelineCapturingStateChanged): Added.
+        (WI.TimelineOverview.prototype._capturingStarted): Deleted.
+        (WI.TimelineOverview.prototype._capturingStopped): Deleted.
+        * UserInterface/Views/TimelineRecordingContentView.js:
+        (WI.TimelineRecordingContentView):
+        (WI.TimelineRecordingContentView.prototype._handleTimelineCapturingStateChanged): Added.
+        (WI.TimelineRecordingContentView.prototype._recordingUnloaded):
+        (WI.TimelineRecordingContentView.prototype._capturingStarted): Deleted.
+        (WI.TimelineRecordingContentView.prototype._capturingStopped): Deleted.
+        * UserInterface/Views/TimelineTabContentView.js:
+        (WI.TimelineTabContentView):
+        (WI.TimelineTabContentView.prototype._handleTimelineCapturingStateChanged): Added.
+        (WI.TimelineTabContentView.prototype._capturingStartedOrStopped): Deleted.
+        Use the new single event for all Timelines capture state changes.
+        Prevent the record button from being clicked when capturing is in a transition state.
+
 2019-04-11  Truitt Savell  <tsavell@apple.com>
 
         Unreviewed, rolling out r244158.
index 2a6a20f..68df3af 100644 (file)
@@ -38,8 +38,7 @@ WI.DebuggerManager = class DebuggerManager extends WI.Object
         WI.Breakpoint.addEventListener(WI.Breakpoint.Event.AutoContinueDidChange, this._breakpointEditablePropertyDidChange, this);
         WI.Breakpoint.addEventListener(WI.Breakpoint.Event.ActionsDidChange, this._handleBreakpointActionsDidChange, this);
 
-        WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingWillStart, this._timelineCapturingWillStart, this);
-        WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStopped, this._timelineCapturingStopped, this);
+        WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStateChanged, this._handleTimelineCapturingStateChanged, this);
 
         WI.auditManager.addEventListener(WI.AuditManager.Event.TestScheduled, this._handleAuditManagerTestScheduled, this);
         WI.auditManager.addEventListener(WI.AuditManager.Event.TestCompleted, this._handleAuditManagerTestCompleted, this);
@@ -1137,17 +1136,19 @@ WI.DebuggerManager = class DebuggerManager extends WI.Object
         this.breakpointsEnabled = restoreState;
     }
 
-    _timelineCapturingWillStart(event)
+    _handleTimelineCapturingStateChanged(event)
     {
-        this._startDisablingBreakpointsTemporarily();
-
-        if (this.paused)
-            this.resume();
-    }
+        switch (WI.timelineManager.capturingState) {
+        case WI.TimelineManager.CapturingState.Starting:
+            this._startDisablingBreakpointsTemporarily();
+            if (this.paused)
+                this.resume();
+            break;
 
-    _timelineCapturingStopped(event)
-    {
-        this._stopDisablingBreakpointsTemporarily();
+        case WI.TimelineManager.CapturingState.Inactive:
+            this._stopDisablingBreakpointsTemporarily();
+            break;
+        }
     }
 
     _handleAuditManagerTestScheduled(event)
index e4b72ec..822c405 100644 (file)
@@ -33,30 +33,27 @@ WI.TimelineManager = class TimelineManager extends WI.Object
 
         WI.Frame.addEventListener(WI.Frame.Event.ProvisionalLoadStarted, this._provisionalLoadStarted, this);
         WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
-        WI.Frame.addEventListener(WI.Frame.Event.ResourceWasAdded, this._resourceWasAdded, this);
-        WI.Target.addEventListener(WI.Target.Event.ResourceAdded, this._resourceWasAdded, this);
-
-        WI.heapManager.addEventListener(WI.HeapManager.Event.GarbageCollected, this._garbageCollected, this);
-        WI.memoryManager.addEventListener(WI.MemoryManager.Event.MemoryPressure, this._memoryPressure, this);
-
-        WI.settings.timelinesAutoStop.addEventListener(WI.Setting.Event.Changed, this._handleTimelinesAutoStopSettingChanged, this);
 
         this._enabledTimelineTypesSetting = new WI.Setting("enabled-instrument-types", WI.TimelineManager.defaultTimelineTypes());
 
-        this._isCapturing = false;
+        this._capturingState = TimelineManager.CapturingState.Inactive;
+        this._capturingInstrumentCount = 0;
+        this._capturingStartTime = NaN;
+        this._capturingEndTime = NaN;
+
         this._initiatedByBackendStart = false;
         this._initiatedByBackendStop = false;
-        this._waitingForCapturingStartedEvent = false;
+
         this._isCapturingPageReload = false;
         this._autoCaptureOnPageLoad = false;
         this._mainResourceForAutoCapturing = null;
         this._shouldSetAutoCapturingMainResource = false;
         this._transitioningPageTarget = false;
-        this._boundStopCapturing = this.stopCapturing.bind(this);
 
         this._webTimelineScriptRecordsExpectingScriptProfilerEvents = null;
         this._scriptProfilerRecords = null;
 
+        this._boundStopCapturing = this.stopCapturing.bind(this);
         this._stopCapturingTimeout = undefined;
         this._deadTimeTimeout = undefined;
         this._lastDeadTimeTickle = 0;
@@ -153,9 +150,11 @@ WI.TimelineManager = class TimelineManager extends WI.Object
 
     // Public
 
+    get capturingState() { return this._capturingState; }
+
     reset()
     {
-        if (this._isCapturing)
+        if (this.isCapturing())
             this.stopCapturing();
 
         this._recordings = [];
@@ -168,7 +167,7 @@ WI.TimelineManager = class TimelineManager extends WI.Object
     // The current recording that new timeline records will be appended to, if any.
     get activeRecording()
     {
-        console.assert(this._activeRecording || !this._isCapturing);
+        console.assert(this._activeRecording || !this.isCapturing());
         return this._activeRecording;
     }
 
@@ -191,8 +190,10 @@ WI.TimelineManager = class TimelineManager extends WI.Object
 
         this._autoCaptureOnPageLoad = autoCapture;
 
-        if (window.TimelineAgent && TimelineAgent.setAutoCaptureEnabled)
-            TimelineAgent.setAutoCaptureEnabled(this._autoCaptureOnPageLoad);
+        for (let target of WI.targets) {
+            if (target.TimelineAgent)
+                target.TimelineAgent.setAutoCaptureEnabled(this._autoCaptureOnPageLoad);
+        }
     }
 
     get enabledTimelineTypes()
@@ -210,7 +211,7 @@ WI.TimelineManager = class TimelineManager extends WI.Object
 
     isCapturing()
     {
-        return this._isCapturing;
+        return this._capturingState !== TimelineManager.CapturingState.Inactive;
     }
 
     isCapturingPageReload()
@@ -238,40 +239,29 @@ WI.TimelineManager = class TimelineManager extends WI.Object
 
     startCapturing(shouldCreateRecording)
     {
-        console.assert(!this._isCapturing, "TimelineManager is already capturing.");
+        console.assert(this._capturingState === TimelineManager.CapturingState.Stopping || this._capturingState === TimelineManager.CapturingState.Inactive, "TimelineManager is already capturing.");
+        if (this._capturingState !== TimelineManager.CapturingState.Stopping && this._capturingState !== TimelineManager.CapturingState.Inactive)
+            return;
 
         if (!this._activeRecording || shouldCreateRecording)
             this._loadNewRecording();
 
-        this._waitingForCapturingStartedEvent = true;
-
-        this.dispatchEventToListeners(WI.TimelineManager.Event.CapturingWillStart);
+        this._updateCapturingState(TimelineManager.CapturingState.Starting);
 
+        this._capturingStartTime = NaN;
         this._activeRecording.start(this._initiatedByBackendStart);
     }
 
     stopCapturing()
     {
-        console.assert(this._isCapturing, "TimelineManager is not capturing.");
-
-        this._activeRecording.stop(this._initiatedByBackendStop);
-
-        // NOTE: Always stop immediately instead of waiting for a Timeline.recordingStopped event.
-        // This way the UI feels as responsive to a stop as possible.
-        // FIXME: <https://webkit.org/b/152904> Web Inspector: Timeline UI should keep up with processing all incoming records
-        this.capturingStopped();
-    }
-
-    unloadRecording()
-    {
-        if (!this._activeRecording)
+        console.assert(this._capturingState === TimelineManager.CapturingState.Starting || this._capturingState === TimelineManager.CapturingState.Active, "TimelineManager is not capturing.");
+        if (this._capturingState !== TimelineManager.CapturingState.Starting && this._capturingState !== TimelineManager.CapturingState.Active)
             return;
 
-        if (this._isCapturing)
-            this.stopCapturing();
+        this._updateCapturingState(TimelineManager.CapturingState.Stopping);
 
-        this._activeRecording.unloaded();
-        this._activeRecording = null;
+        this._capturingEndTime = NaN;
+        this._activeRecording.stop(this._initiatedByBackendStop);
     }
 
     processJSON({filename, json, error})
@@ -305,7 +295,7 @@ WI.TimelineManager = class TimelineManager extends WI.Object
 
         this.dispatchEventToListeners(WI.TimelineManager.Event.RecordingCreated, {recording: newRecording});
 
-        if (this._isCapturing)
+        if (this.isCapturing())
             this.stopCapturing();
 
         let oldRecording = this._activeRecording;
@@ -339,51 +329,95 @@ WI.TimelineManager = class TimelineManager extends WI.Object
     {
         // Called from WI.TimelineObserver.
 
-        if (this._isCapturing)
+        // The frontend didn't start capturing, so this was a programmatic start.
+        if (this._capturingState === TimelineManager.CapturingState.Inactive) {
+            this._initiatedByBackendStart = true;
+            this._activeRecording.addScriptInstrumentForProgrammaticCapture();
+            this.startCapturing();
+        }
+
+        if (!isNaN(startTime)) {
+            if (isNaN(this._capturingStartTime) || startTime < this._capturingStartTime)
+                this._capturingStartTime = startTime;
+
+            this._activeRecording.initializeTimeBoundsIfNecessary(startTime);
+        }
+
+        this._capturingInstrumentCount++;
+        console.assert(this._capturingInstrumentCount);
+        if (this._capturingInstrumentCount > 1)
             return;
 
-        this._waitingForCapturingStartedEvent = false;
-        this._isCapturing = true;
+        if (this._capturingState === TimelineManager.CapturingState.Active)
+            return;
 
         this._lastDeadTimeTickle = 0;
 
-        if (startTime)
-            this.activeRecording.initializeTimeBoundsIfNecessary(startTime);
-
         this._webTimelineScriptRecordsExpectingScriptProfilerEvents = [];
 
+        WI.settings.timelinesAutoStop.addEventListener(WI.Setting.Event.Changed, this._handleTimelinesAutoStopSettingChanged, this);
+
+        WI.Frame.addEventListener(WI.Frame.Event.ResourceWasAdded, this._resourceWasAdded, this);
+        WI.Target.addEventListener(WI.Target.Event.ResourceAdded, this._resourceWasAdded, this);
+
+        WI.heapManager.addEventListener(WI.HeapManager.Event.GarbageCollected, this._garbageCollected, this);
+
+        WI.memoryManager.addEventListener(WI.MemoryManager.Event.MemoryPressure, this._memoryPressure, this);
+
         WI.DOMNode.addEventListener(WI.DOMNode.Event.DidFireEvent, this._handleDOMNodeDidFireEvent, this);
         WI.DOMNode.addEventListener(WI.DOMNode.Event.LowPowerChanged, this._handleDOMNodeLowPowerChanged, this);
 
-        this.dispatchEventToListeners(WI.TimelineManager.Event.CapturingStarted, {startTime});
+        this._updateCapturingState(TimelineManager.CapturingState.Active, {startTime: this._capturingStartTime});
     }
 
     capturingStopped(endTime)
     {
         // Called from WI.TimelineObserver.
 
-        if (!this._isCapturing)
+        // The frontend didn't stop capturing, so this was a programmatic stop.
+        if (this._capturingState === TimelineManager.CapturingState.Active) {
+            this._initiatedByBackendStop = true;
+            this.stopCapturing();
+        }
+
+        if (!isNaN(endTime)) {
+            if (isNaN(this._capturingEndTime) || endTime > this._capturingEndTime)
+                this._capturingEndTime = endTime;
+        }
+
+        this._capturingInstrumentCount--;
+        console.assert(this._capturingInstrumentCount >= 0);
+        if (this._capturingInstrumentCount)
+            return;
+
+        if (this._capturingState === TimelineManager.CapturingState.Inactive)
             return;
 
         WI.DOMNode.removeEventListener(null, null, this);
+        WI.memoryManager.removeEventListener(null, null, this);
+        WI.heapManager.removeEventListener(null, null, this);
+        WI.Target.removeEventListener(WI.Target.Event.ResourceAdded, this._resourceWasAdded, this);
+        WI.Frame.removeEventListener(WI.Frame.Event.ResourceWasAdded, this._resourceWasAdded, this);
+        WI.settings.timelinesAutoStop.removeEventListener(null, null, this);
 
         this.relaxAutoStop();
 
-        this._isCapturing = false;
         this._isCapturingPageReload = false;
         this._shouldSetAutoCapturingMainResource = false;
         this._mainResourceForAutoCapturing = null;
         this._initiatedByBackendStart = false;
         this._initiatedByBackendStop = false;
 
-        this.dispatchEventToListeners(WI.TimelineManager.Event.CapturingStopped, {endTime});
+        this._updateCapturingState(TimelineManager.CapturingState.Inactive, {endTime: this._capturingEndTime});
     }
 
     autoCaptureStarted()
     {
         // Called from WI.TimelineObserver.
 
-        if (this._isCapturing)
+        let waitingForCapturingStartedEvent = this._capturingState === TimelineManager.CapturingState.Starting;
+
+        if (this.isCapturing())
             this.stopCapturing();
 
         this._initiatedByBackendStart = true;
@@ -391,7 +425,7 @@ WI.TimelineManager = class TimelineManager extends WI.Object
         // We may already have an fresh TimelineRecording created if autoCaptureStarted is received
         // between sending the Timeline.start command and receiving Timeline.capturingStarted event.
         // In that case, there is no need to call startCapturing again. Reuse the fresh recording.
-        if (!this._waitingForCapturingStartedEvent) {
+        if (!waitingForCapturingStartedEvent) {
             const createNewRecording = true;
             this.startCapturing(createNewRecording);
         }
@@ -399,37 +433,12 @@ WI.TimelineManager = class TimelineManager extends WI.Object
         this._shouldSetAutoCapturingMainResource = true;
     }
 
-    programmaticCaptureStarted()
-    {
-        // Called from WI.TimelineObserver.
-
-        this._initiatedByBackendStart = true;
-
-        this._activeRecording.addScriptInstrumentForProgrammaticCapture();
-
-        const createNewRecording = false;
-        this.startCapturing(createNewRecording);
-    }
-
-    programmaticCaptureStopped()
-    {
-        // Called from WI.TimelineObserver.
-
-        this._initiatedByBackendStop = true;
-
-        // FIXME: This is purely to avoid a noisy assert. Previously
-        // it was impossible to stop without stopping from the UI.
-        console.assert(!this._isCapturing);
-        this._isCapturing = true;
-
-        this.stopCapturing();
-    }
-
     eventRecorded(recordPayload)
     {
         // Called from WI.TimelineObserver.
 
-        if (!this._isCapturing)
+        console.assert(this.isCapturing());
+        if (!this.isCapturing())
             return;
 
         var records = [];
@@ -469,8 +478,6 @@ WI.TimelineManager = class TimelineManager extends WI.Object
         }
     }
 
-    // Protected
-
     pageDOMContentLoadedEventFired(timestamp)
     {
         // Called from WI.PageObserver.
@@ -478,7 +485,7 @@ WI.TimelineManager = class TimelineManager extends WI.Object
         console.assert(this._activeRecording);
         console.assert(isNaN(WI.networkManager.mainFrame.domContentReadyEventTimestamp));
 
-        let computedTimestamp = this.activeRecording.computeElapsedTime(timestamp);
+        let computedTimestamp = this._activeRecording.computeElapsedTime(timestamp);
 
         WI.networkManager.mainFrame.markDOMContentReadyEvent(computedTimestamp);
 
@@ -493,7 +500,7 @@ WI.TimelineManager = class TimelineManager extends WI.Object
         console.assert(this._activeRecording);
         console.assert(isNaN(WI.networkManager.mainFrame.loadEventTimestamp));
 
-        let computedTimestamp = this.activeRecording.computeElapsedTime(timestamp);
+        let computedTimestamp = this._activeRecording.computeElapsedTime(timestamp);
 
         WI.networkManager.mainFrame.markLoadEvent(computedTimestamp);
 
@@ -514,15 +521,18 @@ WI.TimelineManager = class TimelineManager extends WI.Object
     {
         // Called from WI.CPUProfilerObserver.
 
-        if (!this._isCapturing)
+        console.assert(this.isCapturing());
+        if (!this.isCapturing())
             return;
 
         this._addRecord(new WI.CPUTimelineRecord(event));
     }
 
-    cpuProfilerTrackingCompleted()
+    cpuProfilerTrackingCompleted(timestamp)
     {
         // Called from WI.CPUProfilerObserver.
+
+        this.capturingStopped(timestamp);
     }
 
     memoryTrackingStarted(timestamp)
@@ -536,24 +546,27 @@ WI.TimelineManager = class TimelineManager extends WI.Object
     {
         // Called from WI.MemoryObserver.
 
-        if (!this._isCapturing)
+        console.assert(this.isCapturing());
+        if (!this.isCapturing())
             return;
 
         this._addRecord(new WI.MemoryTimelineRecord(event.timestamp, event.categories));
     }
 
-    memoryTrackingCompleted()
+    memoryTrackingCompleted(timestamp)
     {
         // Called from WI.MemoryObserver.
+
+        this.capturingStopped(timestamp);
     }
 
     heapTrackingStarted(timestamp, snapshot)
     {
         // Called from WI.HeapObserver.
 
-        this._addRecord(new WI.HeapAllocationsTimelineRecord(timestamp, snapshot));
-
         this.capturingStarted(timestamp);
+
+        this._addRecord(new WI.HeapAllocationsTimelineRecord(timestamp, snapshot));
     }
 
     heapTrackingCompleted(timestamp, snapshot)
@@ -561,21 +574,39 @@ WI.TimelineManager = class TimelineManager extends WI.Object
         // Called from WI.HeapObserver.
 
         this._addRecord(new WI.HeapAllocationsTimelineRecord(timestamp, snapshot));
+
+        this.capturingStopped();
     }
 
     heapSnapshotAdded(timestamp, snapshot)
     {
         // Called from WI.HeapAllocationsInstrument.
 
+        console.assert(this.isCapturing());
+        if (!this.isCapturing())
+            return;
+
         this._addRecord(new WI.HeapAllocationsTimelineRecord(timestamp, snapshot));
     }
 
     // Private
 
+    _updateCapturingState(state, data = {})
+    {
+        if (this._capturingState === state)
+            return;
+
+        this._capturingState = state;
+
+        this.dispatchEventToListeners(TimelineManager.Event.CapturingStateChanged, data);
+    }
+
     _processRecord(recordPayload, parentRecordPayload)
     {
-        var startTime = this.activeRecording.computeElapsedTime(recordPayload.startTime);
-        var endTime = this.activeRecording.computeElapsedTime(recordPayload.endTime);
+        console.assert(this.isCapturing());
+
+        var startTime = this._activeRecording.computeElapsedTime(recordPayload.startTime);
+        var endTime = this._activeRecording.computeElapsedTime(recordPayload.endTime);
         var callFrames = this._callFramesFromPayload(recordPayload.stackTrace);
 
         var significantCallFrame = null;
@@ -762,9 +793,11 @@ WI.TimelineManager = class TimelineManager extends WI.Object
 
     _processEvent(recordPayload, parentRecordPayload)
     {
+        console.assert(this.isCapturing());
+
         switch (recordPayload.type) {
         case TimelineAgent.EventType.TimeStamp:
-            var timestamp = this.activeRecording.computeElapsedTime(recordPayload.startTime);
+            var timestamp = this._activeRecording.computeElapsedTime(recordPayload.startTime);
             var eventMarker = new WI.TimelineMarker(timestamp, WI.TimelineMarker.Type.TimeStamp, recordPayload.data.message);
             this._activeRecording.addEventMarker(eventMarker);
             break;
@@ -794,7 +827,7 @@ WI.TimelineManager = class TimelineManager extends WI.Object
         this._recordings.push(newRecording);
         this.dispatchEventToListeners(WI.TimelineManager.Event.RecordingCreated, {recording: newRecording});
 
-        if (this._isCapturing)
+        if (this.isCapturing())
             this.stopCapturing();
 
         var oldRecording = this._activeRecording;
@@ -827,6 +860,8 @@ WI.TimelineManager = class TimelineManager extends WI.Object
 
     _addRecord(record)
     {
+        console.assert(this.isCapturing());
+
         this._activeRecording.addRecord(record);
 
         // Only worry about dead time after the load event.
@@ -844,15 +879,15 @@ WI.TimelineManager = class TimelineManager extends WI.Object
 
         // COMPATIBILITY (iOS 9): Timeline.setAutoCaptureEnabled did not exist.
         // Perform auto capture in the frontend.
-        if (!window.TimelineAgent)
+        if (!InspectorBackend.domains.Timeline)
             return false;
-        if (!TimelineAgent.setAutoCaptureEnabled)
+        if (!InspectorBackend.domains.Timeline.setAutoCaptureEnabled)
             return this._legacyAttemptStartAutoCapturingForFrame(frame);
 
         if (!this._shouldSetAutoCapturingMainResource)
             return false;
 
-        console.assert(this._isCapturing, "We saw autoCaptureStarted so we should already be capturing");
+        console.assert(this.isCapturing(), "We saw autoCaptureStarted so we should already be capturing");
 
         let mainResource = frame.provisionalMainResource || frame.mainResource;
         if (mainResource === this._mainResourceForAutoCapturing)
@@ -874,7 +909,7 @@ WI.TimelineManager = class TimelineManager extends WI.Object
 
     _legacyAttemptStartAutoCapturingForFrame(frame)
     {
-        if (this._isCapturing && !this._mainResourceForAutoCapturing)
+        if (this.isCapturing() && !this._mainResourceForAutoCapturing)
             return false;
 
         let mainResource = frame.provisionalMainResource || frame.mainResource;
@@ -884,7 +919,7 @@ WI.TimelineManager = class TimelineManager extends WI.Object
         let oldMainResource = frame.mainResource || null;
         this._isCapturingPageReload = oldMainResource !== null && oldMainResource.url === mainResource.url;
 
-        if (this._isCapturing)
+        if (this.isCapturing())
             this.stopCapturing();
 
         this._mainResourceForAutoCapturing = mainResource;
@@ -906,7 +941,7 @@ WI.TimelineManager = class TimelineManager extends WI.Object
             return;
 
         // Only auto stop when auto capturing.
-        if (!this._isCapturing || !this._mainResourceForAutoCapturing)
+        if (!this.isCapturing() || !this._mainResourceForAutoCapturing)
             return;
 
         if (this._stopCapturingTimeout)
@@ -930,7 +965,7 @@ WI.TimelineManager = class TimelineManager extends WI.Object
             return;
 
         // Only monitor dead time when auto capturing.
-        if (!this._isCapturing || !this._mainResourceForAutoCapturing)
+        if (!this.isCapturing() || !this._mainResourceForAutoCapturing)
             return;
 
         // Avoid unnecessary churning of timeout identifier by not tickling until 10ms have passed.
@@ -971,7 +1006,7 @@ WI.TimelineManager = class TimelineManager extends WI.Object
         if (this._attemptAutoCapturingForFrame(frame))
             return;
 
-        if (!this._isCapturing)
+        if (!this.isCapturing())
             return;
 
         let mainResource = frame.mainResource;
@@ -983,40 +1018,27 @@ WI.TimelineManager = class TimelineManager extends WI.Object
 
     _resourceWasAdded(event)
     {
-
         // Ignore resource events when there isn't a main frame yet. Those events are triggered by
         // loading the cached resources when the inspector opens, and they do not have timing information.
         if (!WI.networkManager.mainFrame)
             return;
 
-        if (!this._isCapturing)
-            return;
-
         this._addRecord(new WI.ResourceTimelineRecord(event.data.resource));
     }
 
     _garbageCollected(event)
     {
-        if (!this._isCapturing)
-            return;
-
-        let collection = event.data.collection;
+        let {collection} = event.data;
         this._addRecord(new WI.ScriptTimelineRecord(WI.ScriptTimelineRecord.EventType.GarbageCollected, collection.startTime, collection.endTime, null, null, collection));
     }
 
     _memoryPressure(event)
     {
-        if (!this._isCapturing)
-            return;
-
-        this.activeRecording.addMemoryPressureEvent(event.data.memoryPressureEvent);
+        this._activeRecording.addMemoryPressureEvent(event.data.memoryPressureEvent);
     }
 
     _handleTimelinesAutoStopSettingChanged(event)
     {
-        if (!this._isCapturing)
-            return;
-
         if (WI.settings.timelinesAutoStop.value) {
             if (this._mainResourceForAutoCapturing && !isNaN(this._mainResourceForAutoCapturing.parentFrame.loadEventTimestamp))
                 this._stopAutoRecordingSoon();
@@ -1039,24 +1061,6 @@ WI.TimelineManager = class TimelineManager extends WI.Object
         }
     }
 
-    scriptProfilerProgrammaticCaptureStarted()
-    {
-        // FIXME: <https://webkit.org/b/158753> Generalize the concept of Instruments on the backend to work equally for JSContext and Web inspection
-        console.assert(WI.sharedApp.debuggableType === WI.DebuggableType.JavaScript);
-        console.assert(!this._isCapturing);
-
-        this.programmaticCaptureStarted();
-    }
-
-    scriptProfilerProgrammaticCaptureStopped()
-    {
-        // FIXME: <https://webkit.org/b/158753> Generalize the concept of Instruments on the backend to work equally for JSContext and Web inspection
-        console.assert(WI.sharedApp.debuggableType === WI.DebuggableType.JavaScript);
-        console.assert(this._isCapturing);
-
-        this.programmaticCaptureStopped();
-    }
-
     scriptProfilerTrackingStarted(timestamp)
     {
         this._scriptProfilerRecords = [];
@@ -1080,13 +1084,13 @@ WI.TimelineManager = class TimelineManager extends WI.Object
             this._addRecord(record);
     }
 
-    scriptProfilerTrackingCompleted(samples)
+    scriptProfilerTrackingCompleted(timestamp, samples)
     {
         console.assert(!this._webTimelineScriptRecordsExpectingScriptProfilerEvents || this._scriptProfilerRecords.length >= this._webTimelineScriptRecordsExpectingScriptProfilerEvents.length);
 
         if (samples) {
             let {stackTraces} = samples;
-            let topDownCallingContextTree = this.activeRecording.topDownCallingContextTree;
+            let topDownCallingContextTree = this._activeRecording.topDownCallingContextTree;
 
             // Calculate a per-sample duration.
             let timestampIndex = 0;
@@ -1120,7 +1124,7 @@ WI.TimelineManager = class TimelineManager extends WI.Object
             if (timestampIndex < timestampCount)
                 sampleDurations.fill(defaultDuration, sampleDurationIndex);
 
-            this.activeRecording.initializeCallingContextTrees(stackTraces, sampleDurations);
+            this._activeRecording.initializeCallingContextTrees(stackTraces, sampleDurations);
 
             // FIXME: This transformation should not be needed after introducing ProfileView.
             // Once we eliminate ProfileNodeTreeElements and ProfileNodeDataGridNodes.
@@ -1140,8 +1144,10 @@ WI.TimelineManager = class TimelineManager extends WI.Object
 
         this._scriptProfilerRecords = null;
 
-        let timeline = this.activeRecording.timelineForRecordType(WI.TimelineRecord.Type.Script);
+        let timeline = this._activeRecording.timelineForRecordType(WI.TimelineRecord.Type.Script);
         timeline.refresh();
+
+        this.capturingStopped(timestamp);
     }
 
     _mergeScriptProfileRecords()
@@ -1240,8 +1246,6 @@ WI.TimelineManager = class TimelineManager extends WI.Object
 
     _handleDOMNodeDidFireEvent(event)
     {
-        console.assert(this._isCapturing);
-
         let {domEvent} = event.data;
 
         this._addRecord(new WI.MediaTimelineRecord(WI.MediaTimelineRecord.EventType.DOMEvent, domEvent.timestamp, {
@@ -1252,8 +1256,6 @@ WI.TimelineManager = class TimelineManager extends WI.Object
 
     _handleDOMNodeLowPowerChanged(event)
     {
-        console.assert(this._isCapturing);
-
         let {timestamp, isLowPower} = event.data;
 
         this._addRecord(new WI.MediaTimelineRecord(WI.MediaTimelineRecord.EventType.LowPower, timestamp, {
@@ -1263,13 +1265,18 @@ WI.TimelineManager = class TimelineManager extends WI.Object
     }
 };
 
+WI.TimelineManager.CapturingState = {
+    Inactive: "inactive",
+    Starting: "starting",
+    Active: "active",
+    Stopping: "stopping",
+};
+
 WI.TimelineManager.Event = {
+    CapturingStateChanged: "timeline-manager-capturing-started",
     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"
 };
 
 WI.TimelineManager.MaximumAutoRecordDuration = 90000; // 90 seconds
index 645f993..c8cb9f5 100644 (file)
@@ -33,7 +33,7 @@ WI.DefaultDashboard = class DefaultDashboard extends WI.Object
 
         // Necessary event required to track page load time and resource sizes.
         WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
-        WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStopped, this._capturingStopped, this);
+        WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStateChanged, this._handleTimelineCapturingStateChanged, this);
 
         // Necessary events required to track load of resources.
         WI.Frame.addEventListener(WI.Frame.Event.ResourceWasAdded, this._resourceWasAdded, this);
@@ -163,8 +163,11 @@ WI.DefaultDashboard = class DefaultDashboard extends WI.Object
             this._transitioningPageTarget = false;
     }
 
-    _capturingStopped(event)
+    _handleTimelineCapturingStateChanged(event)
     {
+        if (WI.timelineManager.isCapturing())
+            return;
+
         // If recording stops, we should stop the timer if it hasn't stopped already.
         this._stopUpdatingTime();
     }
index 49a2345..0513470 100644 (file)
@@ -37,8 +37,8 @@ WI.CPUProfilerObserver = class CPUProfilerObserver
         WI.timelineManager.cpuProfilerTrackingUpdated(event);
     }
 
-    trackingComplete(samples)
+    trackingComplete(timestamp)
     {
-        WI.timelineManager.cpuProfilerTrackingCompleted(samples);
+        WI.timelineManager.cpuProfilerTrackingCompleted(timestamp);
     }
 };
index 7224de2..e84225b 100644 (file)
@@ -42,8 +42,8 @@ WI.MemoryObserver = class MemoryObserver
         WI.timelineManager.memoryTrackingUpdated(event);
     }
 
-    trackingComplete()
+    trackingComplete(timestamp)
     {
-        WI.timelineManager.memoryTrackingCompleted();
+        WI.timelineManager.memoryTrackingCompleted(timestamp);
     }
 };
index f1b54cc..6b7b558 100644 (file)
@@ -37,18 +37,18 @@ WI.ScriptProfilerObserver = class ScriptProfilerObserver
         WI.timelineManager.scriptProfilerTrackingUpdated(event);
     }
 
-    trackingComplete(samples)
+    trackingComplete(timestamp, samples)
     {
-        WI.timelineManager.scriptProfilerTrackingCompleted(samples);
+        WI.timelineManager.scriptProfilerTrackingCompleted(timestamp, samples);
     }
 
     programmaticCaptureStarted()
     {
-        WI.timelineManager.scriptProfilerProgrammaticCaptureStarted();
+        // COMPATIBILITY (iOS 12.2): ScriptProfiler.programmaticCaptureStarted was removed after iOS 12.2.
     }
 
     programmaticCaptureStopped()
     {
-        WI.timelineManager.scriptProfilerProgrammaticCaptureStopped();
+        // COMPATIBILITY (iOS 12.2): ScriptProfiler.programmaticCaptureStopped was removed after iOS 12.2.
     }
 };
index ba9d019..c9b0aa7 100644 (file)
@@ -49,11 +49,11 @@ WI.TimelineObserver = class TimelineObserver
 
     programmaticCaptureStarted()
     {
-        WI.timelineManager.programmaticCaptureStarted();
+        // COMPATIBILITY (iOS 12.2): Timeline.programmaticCaptureStarted was removed after iOS 12.2.
     }
 
     programmaticCaptureStopped()
     {
-        WI.timelineManager.programmaticCaptureStopped();
+        // COMPATIBILITY (iOS 12.2): Timeline.programmaticCaptureStopped was removed after iOS 12.2.
     }
 };
index 8c1c56b..4e7c40a 100644 (file)
@@ -55,8 +55,7 @@ WI.DebuggerSidebarPanel = class DebuggerSidebarPanel extends WI.NavigationSideba
 
         WI.DOMBreakpoint.addEventListener(WI.DOMBreakpoint.Event.DOMNodeChanged, this._handleDOMBreakpointDOMNodeChanged, this);
 
-        WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingWillStart, this._timelineCapturingWillStart, this);
-        WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStopped, this._timelineCapturingStopped, this);
+        WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStateChanged, this._handleTimelineCapturingStateChanged, this);
 
         WI.auditManager.addEventListener(WI.AuditManager.Event.TestScheduled, this._handleAuditManagerTestScheduled, this);
         WI.auditManager.addEventListener(WI.AuditManager.Event.TestCompleted, this._handleAuditManagerTestCompleted, this);
@@ -251,8 +250,7 @@ WI.DebuggerSidebarPanel = class DebuggerSidebarPanel extends WI.NavigationSideba
             this._debuggerDidPause(null);
 
         if (WI.debuggerManager.breakpointsDisabledTemporarily) {
-            if (WI.timelineManager.isCapturing())
-                this._timelineCapturingWillStart();
+            this._handleTimelineCapturingStateChanged();
 
             if (WI.auditManager.runningState === WI.AuditManager.RunningState.Active || WI.auditManager.runningState === WI.AuditManager.RunningState.Stopping)
                 this._handleAuditManagerTestScheduled();
@@ -648,19 +646,20 @@ WI.DebuggerSidebarPanel = class DebuggerSidebarPanel extends WI.NavigationSideba
         }
     }
 
-    _timelineCapturingWillStart(event)
+    _handleTimelineCapturingStateChanged(event)
     {
         this._updateTemporarilyDisabledBreakpointsButtons();
 
-        this.contentView.element.insertBefore(this._timelineRecordingWarningElement, this.contentView.element.firstChild);
-        this._updateBreakpointsDisabledBanner();
-    }
+        switch (WI.timelineManager.capturingState) {
+        case WI.TimelineManager.CapturingState.Starting:
+            this.contentView.element.insertBefore(this._timelineRecordingWarningElement, this.contentView.element.firstChild);
+            break;
 
-    _timelineCapturingStopped(event)
-    {
-        this._updateTemporarilyDisabledBreakpointsButtons();
+        case WI.TimelineManager.CapturingState.Inactive:
+            this._timelineRecordingWarningElement.remove();
+            break;
+        }
 
-        this._timelineRecordingWarningElement.remove();
         this._updateBreakpointsDisabledBanner();
     }
 
index 568a3cd..f0e94fb 100644 (file)
@@ -282,8 +282,7 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W
         WI.consoleManager.addEventListener(WI.ConsoleManager.Event.IssueAdded, this._handleConsoleIssueAdded, this);
         WI.consoleManager.addEventListener(WI.ConsoleManager.Event.Cleared, this._handleConsoleCleared, this);
 
-        WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingWillStart, this._handleTimelineCapturingWillStart, this);
-        WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStopped, this._handleTimelineCapturingStopped, this);
+        WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStateChanged, this._handleTimelineCapturingStateChanged, this);
 
         WI.auditManager.addEventListener(WI.AuditManager.Event.TestScheduled, this._handleAuditManagerTestScheduled, this);
         WI.auditManager.addEventListener(WI.AuditManager.Event.TestCompleted, this._handleAuditManagerTestCompleted, this);
@@ -337,8 +336,7 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W
             this._handleDebuggerPaused();
 
         if (WI.debuggerManager.breakpointsDisabledTemporarily) {
-            if (WI.timelineManager.isCapturing())
-                this._handleTimelineCapturingWillStart();
+            this._handleTimelineCapturingStateChanged();
 
             if (WI.auditManager.runningState === WI.AuditManager.RunningState.Active || WI.auditManager.runningState === WI.AuditManager.RunningState.Stopping)
                 this._handleAuditManagerTestScheduled();
@@ -1820,34 +1818,33 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W
         issueTreeElements.forEach((treeElement) => treeElement.parent.removeChild(treeElement));
     }
 
-    _handleTimelineCapturingWillStart(event)
+    _handleTimelineCapturingStateChanged(event)
     {
         this._updateTemporarilyDisabledBreakpointsButtons();
 
-        if (!this._timelineRecordingWarningElement) {
-            let stopRecordingButton = document.createElement("button");
-            stopRecordingButton.textContent = WI.UIString("Stop recording");
-            stopRecordingButton.addEventListener("click", () => {
-                WI.timelineManager.stopCapturing();
-            });
-
-            this._timelineRecordingWarningElement = document.createElement("div");
-            this._timelineRecordingWarningElement.classList.add("warning-banner");
-            this._timelineRecordingWarningElement.append(WI.UIString("Debugger disabled during Timeline recording"), document.createElement("br"), stopRecordingButton);
-        }
-
-        this.contentView.element.insertBefore(this._timelineRecordingWarningElement, this.contentView.element.firstChild);
+        switch (WI.timelineManager.capturingState) {
+        case WI.TimelineManager.CapturingState.Starting:
+            if (!this._timelineRecordingWarningElement) {
+                let stopRecordingButton = document.createElement("button");
+                stopRecordingButton.textContent = WI.UIString("Stop recording");
+                stopRecordingButton.addEventListener("click", () => {
+                    WI.timelineManager.stopCapturing();
+                });
 
-        this._updateBreakpointsDisabledBanner();
-    }
+                this._timelineRecordingWarningElement = document.createElement("div");
+                this._timelineRecordingWarningElement.classList.add("warning-banner");
+                this._timelineRecordingWarningElement.append(WI.UIString("Debugger disabled during Timeline recording"), document.createElement("br"), stopRecordingButton);
+            }
 
-    _handleTimelineCapturingStopped(event)
-    {
-        this._updateTemporarilyDisabledBreakpointsButtons();
+            this.contentView.element.insertBefore(this._timelineRecordingWarningElement, this.contentView.element.firstChild);
+            break;
 
-        if (this._timelineRecordingWarningElement) {
-            this._timelineRecordingWarningElement.remove();
-            this._timelineRecordingWarningElement = null;
+        case WI.TimelineManager.CapturingState.Inactive:
+            if (this._timelineRecordingWarningElement) {
+                this._timelineRecordingWarningElement.remove();
+                this._timelineRecordingWarningElement = null;
+            }
+            break;
         }
 
         this._updateBreakpointsDisabledBanner();
index a1560e9..3cec4fb 100644 (file)
@@ -116,9 +116,8 @@ WI.TimelineOverview = class TimelineOverview extends WI.View
 
         this._viewModeDidChange();
 
+        WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStateChanged, this._handleTimelineCapturingStateChanged, this);
         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
@@ -1032,6 +1031,20 @@ WI.TimelineOverview = class TimelineOverview extends WI.View
         this._editingInstrumentsDidChange();
     }
 
+    _handleTimelineCapturingStateChanged(event)
+    {
+        switch (WI.timelineManager.capturingState) {
+        case WI.TimelineManager.CapturingState.Active:
+            this._editInstrumentsButton.enabled = false;
+            this._stopEditingInstruments();
+            break;
+
+        case WI.TimelineManager.CapturingState.Inactive:
+            this._editInstrumentsButton.enabled = true;
+            break;
+        }
+    }
+
     _recordingImported(event)
     {
         let {overviewData} = event.data;
@@ -1055,17 +1068,6 @@ WI.TimelineOverview = class TimelineOverview extends WI.View
         }
     }
 
-    _capturingStarted(event)
-    {
-        this._editInstrumentsButton.enabled = false;
-        this._stopEditingInstruments();
-    }
-
-    _capturingStopped(event)
-    {
-        this._editInstrumentsButton.enabled = true;
-    }
-
     _compareTimelineTreeElements(a, b)
     {
         let aTimelineType = a.representedObject.type;
index 0e16973..56292cc 100644 (file)
@@ -102,8 +102,7 @@ WI.TimelineRecordingContentView = class TimelineRecordingContentView extends WI.
         this._recording.addEventListener(WI.TimelineRecording.Event.Reset, this._recordingReset, this);
         this._recording.addEventListener(WI.TimelineRecording.Event.Unloaded, this._recordingUnloaded, this);
 
-        WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStarted, this._capturingStarted, this);
-        WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStopped, this._capturingStopped, this);
+        WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStateChanged, this._handleTimelineCapturingStateChanged, this);
 
         WI.debuggerManager.addEventListener(WI.DebuggerManager.Event.Paused, this._debuggerPaused, this);
         WI.debuggerManager.addEventListener(WI.DebuggerManager.Event.Resumed, this._debuggerResumed, this);
@@ -510,38 +509,41 @@ WI.TimelineRecordingContentView = class TimelineRecordingContentView extends WI.
         }
     }
 
-    _capturingStarted(event)
+    _handleTimelineCapturingStateChanged(event)
     {
+        let {startTime, endTime} = event.data;
+
         this._updateProgressView();
 
-        let startTime = event.data.startTime;
-        if (!this._updating)
-            this._startUpdatingCurrentTime(startTime);
-        this._clearTimelineNavigationItem.enabled = !this._recording.readonly;
-        this._exportButtonNavigationItem.enabled = false;
+        switch (WI.timelineManager.capturingState) {
+        case WI.TimelineManager.CapturingState.Active:
+            if (!this._updating)
+                this._startUpdatingCurrentTime(startTime);
 
-        // A discontinuity occurs when the recording is stopped and resumed at
-        // a future time. Capturing started signals the end of the current
-        // discontinuity, if one exists.
-        if (!isNaN(this._discontinuityStartTime)) {
-            this._recording.addDiscontinuity(this._discontinuityStartTime, startTime);
-            this._discontinuityStartTime = NaN;
-        }
-    }
+            this._clearTimelineNavigationItem.enabled = !this._recording.readonly;
+            this._exportButtonNavigationItem.enabled = false;
 
-    _capturingStopped(event)
-    {
-        this._updateProgressView();
+            // A discontinuity occurs when the recording is stopped and resumed at
+            // a future time. Capturing started signals the end of the current
+            // discontinuity, if one exists.
+            if (!isNaN(this._discontinuityStartTime)) {
+                this._recording.addDiscontinuity(this._discontinuityStartTime, startTime);
+                this._discontinuityStartTime = NaN;
+            }
+            break;
 
-        if (this._updating)
-            this._stopUpdatingCurrentTime();
+        case WI.TimelineManager.CapturingState.Inactive:
+            if (this._updating)
+                this._stopUpdatingCurrentTime();
 
-        if (this.currentTimelineView)
-            this._updateTimelineViewTimes(this.currentTimelineView);
+            if (this.currentTimelineView)
+                this._updateTimelineViewTimes(this.currentTimelineView);
 
-        this._discontinuityStartTime = event.data.endTime || this._currentTime;
+            this._discontinuityStartTime = endTime || this._currentTime;
 
-        this._exportButtonNavigationItem.enabled = this._recording.canExport();
+            this._exportButtonNavigationItem.enabled = this._recording.canExport();
+            break;
+        }
     }
 
     _debuggerPaused(event)
@@ -731,8 +733,7 @@ WI.TimelineRecordingContentView = class TimelineRecordingContentView extends WI.
     {
         console.assert(!this._updating);
 
-        WI.timelineManager.removeEventListener(WI.TimelineManager.Event.CapturingStarted, this._capturingStarted, this);
-        WI.timelineManager.removeEventListener(WI.TimelineManager.Event.CapturingStopped, this._capturingStopped, this);
+        WI.timelineManager.removeEventListener(null, null, this);
     }
 
     _timeRangeSelectionChanged(event)
index 4d085f7..8dea771 100644 (file)
@@ -71,12 +71,10 @@ WI.TimelineTabContentView = class TimelineTabContentView extends WI.ContentBrows
             this.contentBrowser.navigationBar.addEventListener(WI.NavigationBar.Event.NavigationItemSelected, this._viewModeSelected, this);
         }
 
+        WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStateChanged, this._handleTimelineCapturingStateChanged, this);
         WI.timelineManager.addEventListener(WI.TimelineManager.Event.RecordingCreated, this._recordingCreated, this);
         WI.timelineManager.addEventListener(WI.TimelineManager.Event.RecordingLoaded, this._recordingLoaded, this);
 
-        WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStarted, this._capturingStartedOrStopped, this);
-        WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStopped, this._capturingStartedOrStopped, this);
-
         WI.notifications.addEventListener(WI.Notification.VisibilityStateDidChange, this._inspectorVisibilityChanged, this);
         WI.notifications.addEventListener(WI.Notification.GlobalModifierKeysDidChange, this._globalModifierKeysDidChange, this);
 
@@ -425,10 +423,15 @@ WI.TimelineTabContentView = class TimelineTabContentView extends WI.ContentBrows
             this._showContinueButton();
     }
 
-    _capturingStartedOrStopped(event)
+    _handleTimelineCapturingStateChanged(event)
     {
-        let isCapturing = WI.timelineManager.isCapturing();
-        this._recordButton.toggled = isCapturing;
+        let enabled = WI.timelineManager.capturingState === WI.TimelineManager.CapturingState.Active || WI.timelineManager.capturingState === WI.TimelineManager.CapturingState.Inactive;
+
+        this._toggleRecordingShortcut.disabled = !enabled;
+        this._toggleNewRecordingShortcut.disabled = !enabled;
+
+        this._recordButton.toggled = WI.timelineManager.isCapturing();
+        this._recordButton.enabled = enabled;
 
         this._updateNavigationBarButtons();
     }