/* * 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", };