Diff
Modified: trunk/Source/WebCore/ChangeLog (172086 => 172087)
--- trunk/Source/WebCore/ChangeLog 2014-08-05 21:41:11 UTC (rev 172086)
+++ trunk/Source/WebCore/ChangeLog 2014-08-05 21:56:57 UTC (rev 172087)
@@ -1,3 +1,21 @@
+2014-08-05 Brian J. Burg <b...@cs.washington.edu>
+
+ Web Inspector: ReplayManager shouldn't assume replay status when the inspector is opened
+ https://bugs.webkit.org/show_bug.cgi?id=135212
+
+ Reviewed by Timothy Hatcher.
+
+ The frontend should be able to introspect the session and segment state machines,
+ currently loaded segment and session identifiers, and replay position.
+
+ * inspector/InspectorReplayAgent.cpp:
+ (WebCore::buildInspectorObjectForSessionState): Added.
+ (WebCore::buildInspectorObjectForSegmentState): Added.
+ (WebCore::InspectorReplayAgent::currentReplayState): Added.
+ * inspector/InspectorReplayAgent.h:
+ * inspector/protocol/Replay.json: Add currentReplayState query command.
+ * replay/ReplayController.h: Add some accessors.
+
2014-08-05 Dean Jackson <d...@apple.com>
[iOS] Media controls layout incorrectly in RTL content
Modified: trunk/Source/WebCore/inspector/InspectorReplayAgent.cpp (172086 => 172087)
--- trunk/Source/WebCore/inspector/InspectorReplayAgent.cpp 2014-08-05 21:41:11 UTC (rev 172086)
+++ trunk/Source/WebCore/inspector/InspectorReplayAgent.cpp 2014-08-05 21:56:57 UTC (rev 172087)
@@ -93,6 +93,25 @@
return sessionObject.release();
}
+static Inspector::TypeBuilder::Replay::SessionState::Enum buildInspectorObjectForSessionState(SessionState sessionState)
+{
+ switch (sessionState) {
+ case SessionState::Capturing: return Inspector::TypeBuilder::Replay::SessionState::Capturing;
+ case SessionState::Inactive: return Inspector::TypeBuilder::Replay::SessionState::Inactive;
+ case SessionState::Replaying: return Inspector::TypeBuilder::Replay::SessionState::Replaying;
+ }
+}
+
+static Inspector::TypeBuilder::Replay::SegmentState::Enum buildInspectorObjectForSegmentState(SegmentState segmentState)
+{
+ switch (segmentState) {
+ case SegmentState::Appending: return Inspector::TypeBuilder::Replay::SegmentState::Appending;
+ case SegmentState::Unloaded: return Inspector::TypeBuilder::Replay::SegmentState::Unloaded;
+ case SegmentState::Loaded: return Inspector::TypeBuilder::Replay::SegmentState::Loaded;
+ case SegmentState::Dispatching: return Inspector::TypeBuilder::Replay::SegmentState::Dispatching;
+ }
+}
+
class SerializeInputToJSONFunctor {
public:
typedef PassRefPtr<TypeBuilder::Array<TypeBuilder::Replay::ReplayInput>> ReturnType;
@@ -464,6 +483,18 @@
return it->value;
}
+void InspectorReplayAgent::currentReplayState(ErrorString*, SessionIdentifier* sessionIdentifier, Inspector::TypeBuilder::OptOutput<int>* segmentIdentifier, Inspector::TypeBuilder::Replay::SessionState::Enum* sessionState, Inspector::TypeBuilder::Replay::SegmentState::Enum* segmentState, RefPtr<Inspector::TypeBuilder::Replay::ReplayPosition>& replayPosition)
+{
+ *sessionState = buildInspectorObjectForSessionState(m_page.replayController().sessionState());
+ *segmentState = buildInspectorObjectForSegmentState(m_page.replayController().segmentState());
+
+ *sessionIdentifier = m_page.replayController().loadedSession()->identifier();
+ if (m_page.replayController().loadedSegment())
+ *segmentIdentifier = m_page.replayController().loadedSegment()->identifier();
+
+ replayPosition = buildInspectorObjectForPosition(m_page.replayController().currentPosition());
+}
+
void InspectorReplayAgent::getAvailableSessions(ErrorString*, RefPtr<Inspector::TypeBuilder::Array<SessionIdentifier>>& sessionsList)
{
sessionsList = TypeBuilder::Array<SessionIdentifier>::create();
Modified: trunk/Source/WebCore/inspector/InspectorReplayAgent.h (172086 => 172087)
--- trunk/Source/WebCore/inspector/InspectorReplayAgent.h 2014-08-05 21:41:11 UTC (rev 172086)
+++ trunk/Source/WebCore/inspector/InspectorReplayAgent.h 2014-08-05 21:56:57 UTC (rev 172087)
@@ -104,6 +104,7 @@
virtual void insertSessionSegment(ErrorString*, SessionIdentifier, SegmentIdentifier, int segmentIndex) override;
virtual void removeSessionSegment(ErrorString*, SessionIdentifier, int segmentIndex) override;
+ virtual void currentReplayState(ErrorString*, SessionIdentifier*, Inspector::TypeBuilder::OptOutput<int>* segmentIdentifier, Inspector::TypeBuilder::Replay::SessionState::Enum* sessionState, Inspector::TypeBuilder::Replay::SegmentState::Enum* segmentState, RefPtr<Inspector::TypeBuilder::Replay::ReplayPosition>&) override;
virtual void getAvailableSessions(ErrorString*, RefPtr<Inspector::TypeBuilder::Array<SessionIdentifier>>&) override;
virtual void getSessionData(ErrorString*, SessionIdentifier, RefPtr<Inspector::TypeBuilder::Replay::ReplaySession>&) override;
virtual void getSegmentData(ErrorString*, SegmentIdentifier, RefPtr<Inspector::TypeBuilder::Replay::SessionSegment>&) override;
Modified: trunk/Source/WebCore/inspector/protocol/Replay.json (172086 => 172087)
--- trunk/Source/WebCore/inspector/protocol/Replay.json 2014-08-05 21:41:11 UTC (rev 172086)
+++ trunk/Source/WebCore/inspector/protocol/Replay.json 2014-08-05 21:56:57 UTC (rev 172087)
@@ -11,6 +11,16 @@
"type": "integer"
},
{
+ "id": "SessionState", "description": "State machine's state for the session.",
+ "type": "string",
+ "enum": ["Capturing", "Inactive", "Replaying"]
+ },
+ {
+ "id": "SegmentState", "description": "State machine's state for the session segment.",
+ "type": "string",
+ "enum": ["Appending", "Unloaded", "Loaded", "Dispatching"]
+ },
+ {
"id": "ReplayPosition",
"type": "object",
"properties": [
@@ -112,6 +122,17 @@
]
},
{
+ "name": "currentReplayState",
+ "description": "Returns the identifier, position, session state and segment state of the currently loaded session. This is necessary because the inspector may be closed and reopened in the middle of replay.",
+ "returns": [
+ { "name": "sessionIdentifier", "$ref": "SessionIdentifier" },
+ { "name": "segmentIdentifier", "$ref": "SegmentIdentifier", "optional": true, "description": "If no segment is currently loaded, then there is no valid segment identifier." },
+ { "name": "sessionState", "$ref": "SessionState" },
+ { "name": "segmentState", "$ref": "SegmentState" },
+ { "name": "replayPosition", "$ref": "ReplayPosition" }
+ ]
+ },
+ {
"name": "getAvailableSessions",
"description": "Returns identifiers of all available sessions.",
"returns": [
Modified: trunk/Source/WebCore/replay/ReplayController.h (172086 => 172087)
--- trunk/Source/WebCore/replay/ReplayController.h 2014-08-05 21:41:11 UTC (rev 172086)
+++ trunk/Source/WebCore/replay/ReplayController.h 2014-08-05 21:56:57 UTC (rev 172087)
@@ -136,10 +136,15 @@
void willDispatchEvent(const Event&, Frame*);
Page& page() const { return m_page; }
+
SessionState sessionState() const { return m_sessionState; }
+ SegmentState segmentState() const { return m_segmentState; }
+
PassRefPtr<ReplaySession> loadedSession() const;
PassRefPtr<ReplaySessionSegment> loadedSegment() const;
+
JSC::InputCursor& activeInputCursor() const;
+ ReplayPosition currentPosition() const { return m_currentPosition; }
private:
// EventLoopInputDispatcherClient API
Modified: trunk/Source/WebInspectorUI/ChangeLog (172086 => 172087)
--- trunk/Source/WebInspectorUI/ChangeLog 2014-08-05 21:41:11 UTC (rev 172086)
+++ trunk/Source/WebInspectorUI/ChangeLog 2014-08-05 21:56:57 UTC (rev 172087)
@@ -1,5 +1,59 @@
2014-08-05 Brian J. Burg <b...@cs.washington.edu>
+ Web Inspector: ReplayManager shouldn't assume replay status when the inspector is opened
+ https://bugs.webkit.org/show_bug.cgi?id=135212
+
+ Reviewed by Timothy Hatcher.
+
+ The inspector could be closed and reopened at any point during capturing or replaying.
+ ReplayManager should query the current state on initialization rather than assuming
+ that the replay controller is still in its initial state.
+
+ ReplayManager's initialization code requires querying the backend for the current replay
+ state. This could race with replay protocol events that mutate the manager's state before
+ it is fully initialized, leading to undefined behavior.
+
+ To mitigate this, all protocol event handlers (called by ReplayObserver) are wrapped
+ with a guard that enqueues the callback if initialization is not yet complete. This
+ queue is implemented via multiple then-chaining of a shared 'initialization' promise
+ which resolves when initialization completes.
+
+ * UserInterface/Controllers/ReplayManager.js:
+ (WebInspector.ReplayManager.then):
+ (WebInspector.ReplayManager.catch):
+ (WebInspector.ReplayManager): Rewrite the initialization code to first query the replay
+ state, set the initialization flag to true, and then request and update session records.
+ The sessions must be loaded after querying initial state because ReplayManager.sessionCreated
+ requires replay state to be initialized.
+
+ (WebInspector.ReplayManager.prototype.get sessionState):
+ (WebInspector.ReplayManager.prototype.get segmentState):
+ (WebInspector.ReplayManager.prototype.get activeSessionIdentifier):
+ (WebInspector.ReplayManager.prototype.get activeSegmentIdentifier):
+ (WebInspector.ReplayManager.prototype.get playbackSpeed):
+ (WebInspector.ReplayManager.prototype.set playbackSpeed):
+ (WebInspector.ReplayManager.prototype.get currentPosition): Add assertions to catch uses of
+ manager state before the manager is fully initialized.
+
+ (WebInspector.ReplayManager.prototype.waitUntilInitialized): Added. It returns a shared promise
+ that is fulfilled when initialization is complete.
+
+ (WebInspector.ReplayManager.prototype.captureStarted):
+ (WebInspector.ReplayManager.prototype.captureStopped):
+ (WebInspector.ReplayManager.prototype.playbackStarted):
+ (WebInspector.ReplayManager.prototype.playbackHitPosition):
+ (WebInspector.ReplayManager.prototype.playbackPaused):
+ (WebInspector.ReplayManager.prototype.playbackFinished):
+ (WebInspector.ReplayManager.prototype.sessionModified):
+ (WebInspector.ReplayManager.prototype.sessionLoaded):
+ (WebInspector.ReplayManager.prototype.segmentCompleted.set catch):
+ (WebInspector.ReplayManager.prototype.segmentCompleted):
+ (WebInspector.ReplayManager.prototype.segmentRemoved.then):
+ (WebInspector.ReplayManager.prototype.segmentRemoved):
+ (WebInspector.ReplayManager.prototype.segmentLoaded): Add initialization guards.
+
+2014-08-05 Brian J. Burg <b...@cs.washington.edu>
+
Web Replay: rename protocol methods for getting replay session/segment data
https://bugs.webkit.org/show_bug.cgi?id=135618
Modified: trunk/Source/WebInspectorUI/UserInterface/Controllers/ReplayManager.js (172086 => 172087)
--- trunk/Source/WebInspectorUI/UserInterface/Controllers/ReplayManager.js 2014-08-05 21:41:11 UTC (rev 172086)
+++ trunk/Source/WebInspectorUI/UserInterface/Controllers/ReplayManager.js 2014-08-05 21:56:57 UTC (rev 172087)
@@ -34,6 +34,7 @@
this._activeSessionIdentifier = null;
this._activeSegmentIdentifier = null;
this._currentPosition = new WebInspector.ReplayPosition(0, 0);
+ this._initialized = false;
// These hold actual instances of sessions and segments.
this._sessions = new Map;
@@ -49,10 +50,27 @@
if (!window.ReplayAgent)
return;
- ReplayAgent.getAvailableSessions.promise()
+ var instance = this;
+
+ this._initializationPromise = ReplayAgent.currentReplayState.promise()
.then(function(payload) {
+ console.assert(payload.sessionState in WebInspector.ReplayManager.SessionState, "Unknown session state: " + payload.sessionState);
+ console.assert(payload.segmentState in WebInspector.ReplayManager.SegmentState, "Unknown segment state: " + payload.segmentState);
+
+ instance._activeSessionIdentifier = payload.sessionIdentifier;
+ instance._activeSegmentIdentifier = payload.segmentIdentifier;
+ instance._sessionState = WebInspector.ReplayManager.SessionState[payload.sessionState];
+ instance._segmentState = WebInspector.ReplayManager.SegmentState[payload.segmentState];
+ instance._currentPosition = payload.replayPosition;
+
+ instance._initialized = true;
+ }).then(function() {
+ return ReplayAgent.getAvailableSessions.promise();
+ }).then(function(payload) {
for (var sessionId of payload.ids)
- WebInspector.replayManager.sessionCreated(sessionId);
+ instance.sessionCreated(sessionId);
+ }).catch(function(err) {
+ console.error("ReplayManager initialization failed: ", err);
});
};
@@ -99,42 +117,56 @@
// Public
+ // The following state is invalid unless called from a function that's chained
+ // to the (resolved) ReplayManager.waitUntilInitialized promise.
get sessionState()
{
+ console.assert(this._initialized);
return this._sessionState;
},
get segmentState()
{
+ console.assert(this._initialized);
return this._segmentState;
},
get activeSessionIdentifier()
{
+ console.assert(this._initialized);
return this._activeSessionIdentifier;
},
get activeSegmentIdentifier()
{
+ console.assert(this._initialized);
return this._activeSegmentIdentifier;
},
get playbackSpeed()
{
+ console.assert(this._initialized);
return this._playbackSpeed;
},
set playbackSpeed(value)
{
+ console.assert(this._initialized);
this._playbackSpeed = value;
},
get currentPosition()
{
+ console.assert(this._initialized);
return this._currentPosition;
},
// These return promises even if the relevant instance is already created.
+ waitUntilInitialized: function()
+ {
+ return this._initializationPromise;
+ },
+
getSession: function(sessionId)
{
if (this._sessionPromises.has(sessionId))
@@ -165,8 +197,15 @@
// Protected (called by ReplayObserver)
+ // Since these methods update session and segment state, they depend on the manager
+ // being properly initialized. So, each function body is prepended with a retry guard.
+ // This makes call sites simpler and avoids an extra event loop turn in the common case.
+
captureStarted: function()
{
+ if (!this._initialized)
+ return this.waitUntilInitialized().then(this.captureStarted.bind(this));
+
this._changeSessionState(WebInspector.ReplayManager.SessionState.Capturing);
this.dispatchEventToListeners(WebInspector.ReplayManager.Event.CaptureStarted);
@@ -174,6 +213,9 @@
captureStopped: function()
{
+ if (!this._initialized)
+ return this.waitUntilInitialized().then(this.captureStopped.bind(this));
+
this._changeSessionState(WebInspector.ReplayManager.SessionState.Inactive);
this._changeSegmentState(WebInspector.ReplayManager.SegmentState.Unloaded);
@@ -182,6 +224,9 @@
playbackStarted: function()
{
+ if (!this._initialized)
+ return this.waitUntilInitialized().then(this.playbackStarted.bind(this));
+
if (this.sessionState === WebInspector.ReplayManager.SessionState.Inactive)
this._changeSessionState(WebInspector.ReplayManager.SessionState.Replaying);
@@ -192,6 +237,9 @@
playbackHitPosition: function(replayPosition, timestamp)
{
+ if (!this._initialized)
+ return this.waitUntilInitialized().then(this.playbackHitPosition.bind(this, replayPosition, timestamp));
+
console.assert(this.sessionState === WebInspector.ReplayManager.SessionState.Replaying);
console.assert(this.segmentState === WebInspector.ReplayManager.SegmentState.Dispatching);
console.assert(replayPosition instanceof WebInspector.ReplayPosition);
@@ -200,8 +248,11 @@
this.dispatchEventToListeners(WebInspector.ReplayManager.Event.PlaybackPositionChanged);
},
- playbackPaused: function(mark)
+ playbackPaused: function(position)
{
+ if (!this._initialized)
+ return this.waitUntilInitialized().then(this.playbackPaused.bind(this, position));
+
console.assert(this.sessionState === WebInspector.ReplayManager.SessionState.Replaying);
this._changeSegmentState(WebInspector.ReplayManager.SegmentState.Loaded);
@@ -210,6 +261,9 @@
playbackFinished: function()
{
+ if (!this._initialized)
+ return this.waitUntilInitialized().then(this.playbackFinished.bind(this));
+
this._changeSessionState(WebInspector.ReplayManager.SessionState.Inactive);
console.assert(this.segmentState === WebInspector.ReplayManager.SegmentState.Unloaded);
@@ -218,13 +272,15 @@
sessionCreated: function(sessionId)
{
+ if (!this._initialized)
+ return this.waitUntilInitialized().then(this.sessionCreated.bind(this, sessionId));
+
console.assert(!this._sessions.has(sessionId), "Tried to add duplicate session identifier:", sessionId);
var sessionMap = this._sessions;
this.getSession(sessionId)
.then(function(session) {
sessionMap.set(sessionId, session);
- })
- .catch(function(error) {
+ }).catch(function(error) {
console.error("Error obtaining session data: ", error);
});
@@ -233,6 +289,9 @@
sessionModified: function(sessionId)
{
+ if (!this._initialized)
+ return this.waitUntilInitialized().then(this.sessionModified.bind(this, sessionId));
+
this.getSession(sessionId).then(function(session) {
session.segmentsChanged();
});
@@ -240,6 +299,9 @@
sessionRemoved: function(sessionId)
{
+ if (!this._initialized)
+ return this.waitUntilInitialized().then(this.sessionRemoved.bind(this, sessionId));
+
console.assert(this._sessions.has(sessionId), "Unknown session identifier:", sessionId);
if (!this._sessionPromises.has(sessionId))
@@ -251,8 +313,7 @@
this.getSession(sessionId)
.catch(function(error) {
return Promise.resolve();
- })
- .then(function() {
+ }).then(function() {
manager._sessionPromises.delete(sessionId);
var removedSession = manager._sessions.take(sessionId);
console.assert(removedSession);
@@ -262,6 +323,9 @@
segmentCreated: function(segmentId)
{
+ if (!this._initialized)
+ return this.waitUntilInitialized().then(this.segmentCreated.bind(this, segmentId));
+
console.assert(!this._segments.has(segmentId), "Tried to add duplicate segment identifier:", segmentId);
this._changeSegmentState(WebInspector.ReplayManager.SegmentState.Appending);
@@ -277,6 +341,9 @@
segmentCompleted: function(segmentId)
{
+ if (!this._initialized)
+ return this.waitUntilInitialized().then(this.segmentCompleted.bind(this, segmentId));
+
var placeholderSegment = this._segments.take(segmentId);
console.assert(placeholderSegment instanceof WebInspector.IncompleteSessionSegment);
this._segmentPromises.delete(segmentId);
@@ -285,14 +352,16 @@
this.getSegment(segmentId)
.then(function(segment) {
segmentMap.set(segmentId, segment);
- })
- .catch(function(error) {
+ }).catch(function(error) {
console.error("Error obtaining segment data: ", error);
});
},
segmentRemoved: function(segmentId)
{
+ if (!this._initialized)
+ return this.waitUntilInitialized().then(this.segmentRemoved.bind(this, segmentId));
+
console.assert(this._segments.has(segmentId), "Unknown segment identifier:", segmentId);
if (!this._segmentPromises.has(segmentId))
@@ -304,8 +373,7 @@
this.getSegment(segmentId)
.catch(function(error) {
return Promise.resolve();
- })
- .then(function() {
+ }).then(function() {
manager._segmentPromises.delete(segmentId);
var removedSegment = manager._segments.take(segmentId);
console.assert(removedSegment);
@@ -315,6 +383,9 @@
segmentLoaded: function(segmentId)
{
+ if (!this._initialized)
+ return this.waitUntilInitialized().then(this.segmentLoaded.bind(this, segmentId));
+
console.assert(this._segments.has(segmentId), "Unknown segment identifier:", segmentId);
console.assert(this.sessionState !== WebInspector.ReplayManager.SessionState.Capturing);
@@ -327,6 +398,9 @@
segmentUnloaded: function()
{
+ if (!this._initialized)
+ return this.waitUntilInitialized().then(this.segmentUnloaded.bind(this));
+
console.assert(this.sessionState === WebInspector.ReplayManager.SessionState.Replaying);
this._changeSegmentState(WebInspector.ReplayManager.SegmentState.Unloaded);