blob: ab04ddf102a618e70384d7f6ac17c670e25d62a7 [file] [log] [blame]
/*
* Copyright (C) 2013 University of Washington. All rights reserved.
* Copyright (C) 2014 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
WebInspector.ReplayManager = class ReplayManager extends WebInspector.Object
{
constructor()
{
super();
this._sessionState = WebInspector.ReplayManager.SessionState.Inactive;
this._segmentState = WebInspector.ReplayManager.SegmentState.Unloaded;
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;
this._segments = new Map;
// These hold promises that resolve when the instance data is recieved.
this._sessionPromises = new Map;
this._segmentPromises = new Map;
// Playback speed is specified in replayToPosition commands, and persists
// for the duration of the playback command until another playback begins.
this._playbackSpeed = WebInspector.ReplayManager.PlaybackSpeed.RealTime;
if (window.ReplayAgent) {
var instance = this;
this._initializationPromise = ReplayAgent.currentReplayState()
.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();
}).then(function(payload) {
for (var sessionId of payload.ids)
instance.sessionCreated(sessionId);
}).catch(function(error) {
console.error("ReplayManager initialization failed: ", error);
throw error;
});
}
}
// 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() // --> ()
{
return this._initializationPromise;
}
// Return a promise that resolves to a session, if it exists.
getSession(sessionId) // --> (WebInspector.ReplaySession)
{
if (this._sessionPromises.has(sessionId))
return this._sessionPromises.get(sessionId);
var newPromise = ReplayAgent.getSessionData(sessionId)
.then(function(payload) {
return Promise.resolve(WebInspector.ReplaySession.fromPayload(sessionId, payload));
});
this._sessionPromises.set(sessionId, newPromise);
return newPromise;
}
// Return a promise that resolves to a session segment, if it exists.
getSegment(segmentId) // --> (WebInspector.ReplaySessionSegment)
{
if (this._segmentPromises.has(segmentId))
return this._segmentPromises.get(segmentId);
var newPromise = ReplayAgent.getSegmentData(segmentId)
.then(function(payload) {
return Promise.resolve(new WebInspector.ReplaySessionSegment(segmentId, payload));
});
this._segmentPromises.set(segmentId, newPromise);
return newPromise;
}
// Switch to the specified session.
// Returns a promise that resolves when the switch completes.
switchSession(sessionId) // --> ()
{
var manager = this;
var result = this.waitUntilInitialized();
if (this.sessionState === WebInspector.ReplayManager.SessionState.Capturing) {
result = result.then(function() {
return WebInspector.replayManager.stopCapturing();
});
}
if (this.sessionState === WebInspector.ReplayManager.SessionState.Replaying) {
result = result.then(function() {
return WebInspector.replayManager.cancelPlayback();
});
}
result = result.then(function() {
console.assert(manager.sessionState === WebInspector.ReplayManager.SessionState.Inactive);
console.assert(manager.segmentState === WebInspector.ReplayManager.SegmentState.Unloaded);
return manager.getSession(sessionId);
}).then(function ensureSessionDataIsLoaded(session) {
return ReplayAgent.switchSession(session.identifier);
}).catch(function(error) {
console.error("Failed to switch to session: ", error);
throw error;
});
return result;
}
// Start capturing into the current session as soon as possible.
// Returns a promise that resolves when capturing begins.
startCapturing() // --> ()
{
var manager = this;
var result = this.waitUntilInitialized();
if (this.sessionState === WebInspector.ReplayManager.SessionState.Capturing)
return result; // Already capturing.
if (this.sessionState === WebInspector.ReplayManager.SessionState.Replaying) {
result = result.then(function() {
return WebInspector.replayManager.cancelPlayback();
});
}
result = result.then(this._suppressBreakpointsAndResumeIfNeeded());
result = result.then(function() {
console.assert(manager.sessionState === WebInspector.ReplayManager.SessionState.Inactive);
console.assert(manager.segmentState === WebInspector.ReplayManager.SegmentState.Unloaded);
return ReplayAgent.startCapturing();
}).catch(function(error) {
console.error("Failed to start capturing: ", error);
throw error;
});
return result;
}
// Stop capturing into the current session as soon as possible.
// Returns a promise that resolves when capturing ends.
stopCapturing() // --> ()
{
console.assert(this.sessionState === WebInspector.ReplayManager.SessionState.Capturing, "Cannot stop capturing unless capture is active.");
console.assert(this.segmentState === WebInspector.ReplayManager.SegmentState.Appending);
return ReplayAgent.stopCapturing()
.catch(function(error) {
console.error("Failed to stop capturing: ", error);
throw error;
});
}
// Pause playback as soon as possible.
// Returns a promise that resolves when playback is paused.
pausePlayback() // --> ()
{
console.assert(this.sessionState !== WebInspector.ReplayManager.SessionState.Capturing, "Cannot pause playback while capturing.");
var manager = this;
var result = this.waitUntilInitialized();
if (this.sessionState === WebInspector.ReplayManager.SessionState.Inactive)
return result; // Already stopped.
if (this.segmentState !== WebInspector.ReplayManager.SegmentState.Dispatching)
return result; // Already stopped.
result = result.then(function() {
console.assert(manager.sessionState === WebInspector.ReplayManager.SessionState.Replaying);
console.assert(manager.segmentState === WebInspector.ReplayManager.SegmentState.Dispatching);
return ReplayAgent.pausePlayback();
}).catch(function(error) {
console.error("Failed to pause playback: ", error);
throw error;
});
return result;
}
// Pause playback and unload the current session segment as soon as possible.
// Returns a promise that resolves when the current segment is unloaded.
cancelPlayback() // --> ()
{
console.assert(this.sessionState !== WebInspector.ReplayManager.SessionState.Capturing, "Cannot stop playback while capturing.");
var manager = this;
var result = this.waitUntilInitialized();
if (this.sessionState === WebInspector.ReplayManager.SessionState.Inactive)
return result; // Already stopped.
result = result.then(function() {
console.assert(manager.sessionState === WebInspector.ReplayManager.SessionState.Replaying);
console.assert(manager.segmentState !== WebInspector.ReplayManager.SegmentState.Appending);
return ReplayAgent.cancelPlayback();
}).catch(function(error) {
console.error("Failed to stop playback: ", error);
throw error;
});
return result;
}
// Replay to the specified position as soon as possible using the current replay speed.
// Returns a promise that resolves when replay has begun (NOT when the position is reached).
replayToPosition(replayPosition) // --> ()
{
console.assert(replayPosition instanceof WebInspector.ReplayPosition, "Cannot replay to a position while capturing.");
var manager = this;
var result = this.waitUntilInitialized();
if (this.sessionState === WebInspector.ReplayManager.SessionState.Capturing) {
result = result.then(function() {
return WebInspector.replayManager.stopCapturing();
});
}
result = result.then(this._suppressBreakpointsAndResumeIfNeeded());
result = result.then(function() {
console.assert(manager.sessionState !== WebInspector.ReplayManager.SessionState.Capturing);
console.assert(manager.segmentState !== WebInspector.ReplayManager.SegmentState.Appending);
return ReplayAgent.replayToPosition(replayPosition, manager.playbackSpeed === WebInspector.ReplayManager.PlaybackSpeed.FastForward);
}).catch(function(error) {
console.error("Failed to start playback to position: ", replayPosition, error);
throw error;
});
return result;
}
// Replay to the end of the session as soon as possible using the current replay speed.
// Returns a promise that resolves when replay has begun (NOT when the end is reached).
replayToCompletion() // --> ()
{
var manager = this;
var result = this.waitUntilInitialized();
if (this.segmentState === WebInspector.ReplayManager.SegmentState.Dispatching)
return result; // Already running.
if (this.sessionState === WebInspector.ReplayManager.SessionState.Capturing) {
result = result.then(function() {
return WebInspector.replayManager.stopCapturing();
});
}
result = result.then(this._suppressBreakpointsAndResumeIfNeeded());
result = result.then(function() {
console.assert(manager.sessionState !== WebInspector.ReplayManager.SessionState.Capturing);
console.assert(manager.segmentState === WebInspector.ReplayManager.SegmentState.Loaded || manager.segmentState === WebInspector.ReplayManager.SegmentState.Unloaded);
return ReplayAgent.replayToCompletion(manager.playbackSpeed === WebInspector.ReplayManager.PlaybackSpeed.FastForward);
}).catch(function(error) {
console.error("Failed to start playback to completion: ", error);
throw error;
});
return result;
}
// 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()
{
if (!this._initialized)
return this.waitUntilInitialized().then(this.captureStarted.bind(this));
this._changeSessionState(WebInspector.ReplayManager.SessionState.Capturing);
this.dispatchEventToListeners(WebInspector.ReplayManager.Event.CaptureStarted);
}
captureStopped()
{
if (!this._initialized)
return this.waitUntilInitialized().then(this.captureStopped.bind(this));
this._changeSessionState(WebInspector.ReplayManager.SessionState.Inactive);
this._changeSegmentState(WebInspector.ReplayManager.SegmentState.Unloaded);
if (this._breakpointsWereSuppressed) {
delete this._breakpointsWereSuppressed;
WebInspector.debuggerManager.breakpointsEnabled = true;
}
this.dispatchEventToListeners(WebInspector.ReplayManager.Event.CaptureStopped);
}
playbackStarted()
{
if (!this._initialized)
return this.waitUntilInitialized().then(this.playbackStarted.bind(this));
if (this.sessionState === WebInspector.ReplayManager.SessionState.Inactive)
this._changeSessionState(WebInspector.ReplayManager.SessionState.Replaying);
this._changeSegmentState(WebInspector.ReplayManager.SegmentState.Dispatching);
this.dispatchEventToListeners(WebInspector.ReplayManager.Event.PlaybackStarted);
}
playbackHitPosition(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);
this._currentPosition = replayPosition;
this.dispatchEventToListeners(WebInspector.ReplayManager.Event.PlaybackPositionChanged);
}
playbackPaused(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);
if (this._breakpointsWereSuppressed) {
delete this._breakpointsWereSuppressed;
WebInspector.debuggerManager.breakpointsEnabled = true;
}
this.dispatchEventToListeners(WebInspector.ReplayManager.Event.PlaybackPaused);
}
playbackFinished()
{
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);
if (this._breakpointsWereSuppressed) {
delete this._breakpointsWereSuppressed;
WebInspector.debuggerManager.breakpointsEnabled = true;
}
this.dispatchEventToListeners(WebInspector.ReplayManager.Event.PlaybackFinished);
}
sessionCreated(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) {
console.error("Error obtaining session data: ", error);
throw error;
});
this.dispatchEventToListeners(WebInspector.ReplayManager.Event.SessionAdded, {sessionId});
}
sessionModified(sessionId)
{
if (!this._initialized)
return this.waitUntilInitialized().then(this.sessionModified.bind(this, sessionId));
this.getSession(sessionId).then(function(session) {
session.segmentsChanged();
});
}
sessionRemoved(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))
return;
var manager = this;
this.getSession(sessionId)
.catch(function(error) {
// Wait for any outstanding promise to settle so it doesn't get re-added.
}).then(function() {
manager._sessionPromises.delete(sessionId);
var removedSession = manager._sessions.take(sessionId);
console.assert(removedSession);
manager.dispatchEventToListeners(WebInspector.ReplayManager.Event.SessionRemoved, {removedSession});
});
}
segmentCreated(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);
// Create a dummy segment, and don't try to load any data for it. It will
// be removed once the segment is complete, and then its data will be fetched.
var incompleteSegment = new WebInspector.IncompleteSessionSegment(segmentId);
this._segments.set(segmentId, incompleteSegment);
this._segmentPromises.set(segmentId, Promise.resolve(incompleteSegment));
this.dispatchEventToListeners(WebInspector.ReplayManager.Event.SessionSegmentAdded, {segmentIdentifier: segmentId});
}
segmentCompleted(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);
var segmentMap = this._segments;
this.getSegment(segmentId)
.then(function(segment) {
segmentMap.set(segmentId, segment);
}).catch(function(error) {
console.error("Error obtaining segment data: ", error);
throw error;
});
}
segmentRemoved(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))
return;
var manager = this;
// Wait for any outstanding promise to settle so it doesn't get re-added.
this.getSegment(segmentId)
.catch(function(error) {
return Promise.resolve();
}).then(function() {
manager._segmentPromises.delete(segmentId);
var removedSegment = manager._segments.take(segmentId);
console.assert(removedSegment);
manager.dispatchEventToListeners(WebInspector.ReplayManager.Event.SessionSegmentRemoved, {removedSegment});
});
}
segmentLoaded(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);
this._changeSegmentState(WebInspector.ReplayManager.SegmentState.Loaded);
var previousIdentifier = this._activeSegmentIdentifier;
this._activeSegmentIdentifier = segmentId;
this.dispatchEventToListeners(WebInspector.ReplayManager.Event.ActiveSegmentChanged, {previousSegmentIdentifier: previousIdentifier});
}
segmentUnloaded()
{
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);
var previousIdentifier = this._activeSegmentIdentifier;
this._activeSegmentIdentifier = null;
this.dispatchEventToListeners(WebInspector.ReplayManager.Event.ActiveSegmentChanged, {previousSegmentIdentifier: previousIdentifier});
}
// Private
_changeSessionState(newState)
{
// Warn about no-op state changes. We shouldn't be seeing them.
var isAllowed = this._sessionState !== newState;
switch (this._sessionState) {
case WebInspector.ReplayManager.SessionState.Capturing:
isAllowed &= newState === WebInspector.ReplayManager.SessionState.Inactive;
break;
case WebInspector.ReplayManager.SessionState.Replaying:
isAllowed &= newState === WebInspector.ReplayManager.SessionState.Inactive;
break;
}
console.assert(isAllowed, "Invalid session state change: ", this._sessionState, " to ", newState);
if (isAllowed)
this._sessionState = newState;
}
_changeSegmentState(newState)
{
// Warn about no-op state changes. We shouldn't be seeing them.
var isAllowed = this._segmentState !== newState;
switch (this._segmentState) {
case WebInspector.ReplayManager.SegmentState.Appending:
isAllowed &= newState === WebInspector.ReplayManager.SegmentState.Unloaded;
break;
case WebInspector.ReplayManager.SegmentState.Unloaded:
isAllowed &= newState === WebInspector.ReplayManager.SegmentState.Appending || newState === WebInspector.ReplayManager.SegmentState.Loaded;
break;
case WebInspector.ReplayManager.SegmentState.Loaded:
isAllowed &= newState === WebInspector.ReplayManager.SegmentState.Unloaded || newState === WebInspector.ReplayManager.SegmentState.Dispatching;
break;
case WebInspector.ReplayManager.SegmentState.Dispatching:
isAllowed &= newState === WebInspector.ReplayManager.SegmentState.Loaded;
break;
}
console.assert(isAllowed, "Invalid segment state change: ", this._segmentState, " to ", newState);
if (isAllowed)
this._segmentState = newState;
}
_suppressBreakpointsAndResumeIfNeeded()
{
var manager = this;
return new Promise(function(resolve, reject) {
manager._breakpointsWereSuppressed = WebInspector.debuggerManager.breakpointsEnabled;
WebInspector.debuggerManager.breakpointsEnabled = false;
return WebInspector.debuggerManager.resume();
});
}
};
WebInspector.ReplayManager.Event = {
CaptureStarted: "replay-manager-capture-started",
CaptureStopped: "replay-manager-capture-stopped",
PlaybackStarted: "replay-manager-playback-started",
PlaybackPaused: "replay-manager-playback-paused",
PlaybackFinished: "replay-manager-playback-finished",
PlaybackPositionChanged: "replay-manager-play-back-position-changed",
ActiveSessionChanged: "replay-manager-active-session-changed",
ActiveSegmentChanged: "replay-manager-active-segment-changed",
SessionSegmentAdded: "replay-manager-session-segment-added",
SessionSegmentRemoved: "replay-manager-session-segment-removed",
SessionAdded: "replay-manager-session-added",
SessionRemoved: "replay-manager-session-removed",
};
WebInspector.ReplayManager.SessionState = {
Capturing: "replay-manager-session-state-capturing",
Inactive: "replay-manager-session-state-inactive",
Replaying: "replay-manager-session-state-replaying",
};
WebInspector.ReplayManager.SegmentState = {
Appending: "replay-manager-segment-state-appending",
Unloaded: "replay-manager-segment-state-unloaded",
Loaded: "replay-manager-segment-state-loaded",
Dispatching: "replay-manager-segment-state-dispatching",
};
WebInspector.ReplayManager.PlaybackSpeed = {
RealTime: "replay-manager-playback-speed-real-time",
FastForward: "replay-manager-playback-speed-fast-forward",
};