| /* |
| * Copyright (C) 2011-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 THE COPYRIGHT HOLDERS AND 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 THE COPYRIGHT |
| * HOLDER OR 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. |
| */ |
| |
| #include "config.h" |
| #include "ReplayController.h" |
| |
| #if ENABLE(WEB_REPLAY) |
| |
| #include "AllReplayInputs.h" |
| #include "CapturingInputCursor.h" |
| #include "DOMWindow.h" |
| #include "DocumentLoader.h" |
| #include "Frame.h" |
| #include "FrameTree.h" |
| #include "InspectorInstrumentation.h" |
| #include "Location.h" |
| #include "Logging.h" |
| #include "MainFrame.h" |
| #include "Page.h" |
| #include "ReplaySession.h" |
| #include "ReplaySessionSegment.h" |
| #include "ReplayingInputCursor.h" |
| #include "ScriptController.h" |
| #include "SerializationMethods.h" |
| #include "Settings.h" |
| #include "UserInputBridge.h" |
| #include "WebReplayInputs.h" |
| #include <replay/EmptyInputCursor.h> |
| #include <wtf/text/CString.h> |
| |
| #if ENABLE(ASYNC_SCROLLING) |
| #include "ScrollingCoordinator.h" |
| #endif |
| |
| namespace WebCore { |
| |
| #if !LOG_DISABLED |
| static void logDispatchedDOMEvent(const Event& event, bool eventIsUnrelated) |
| { |
| EventTarget* target = event.target(); |
| if (!target) |
| return; |
| |
| // A DOM event is unrelated if it is being dispatched to a document that is neither capturing nor replaying. |
| if (Node* node = target->toNode()) { |
| LOG(WebReplay, "%-20s --->%s DOM event: type=%s, target=%u/node[%p] %s\n", "ReplayEvents", |
| (eventIsUnrelated) ? "Unrelated" : "Dispatching", |
| event.type().string().utf8().data(), |
| frameIndexFromDocument((node->inDocument()) ? &node->document() : node->ownerDocument()), |
| node, |
| node->nodeName().utf8().data()); |
| } else if (DOMWindow* window = target->toDOMWindow()) { |
| LOG(WebReplay, "%-20s --->%s DOM event: type=%s, target=%u/window[%p] %s\n", "ReplayEvents", |
| (eventIsUnrelated) ? "Unrelated" : "Dispatching", |
| event.type().string().utf8().data(), |
| frameIndexFromDocument(window->document()), |
| window, |
| window->location()->href().utf8().data()); |
| } |
| } |
| |
| static const char* sessionStateToString(SessionState state) |
| { |
| switch (state) { |
| case SessionState::Capturing: |
| return "Capturing"; |
| case SessionState::Inactive: |
| return "Inactive"; |
| case SessionState::Replaying: |
| return "Replaying"; |
| } |
| } |
| |
| static const char* segmentStateToString(SegmentState state) |
| { |
| switch (state) { |
| case SegmentState::Appending: |
| return "Appending"; |
| case SegmentState::Unloaded: |
| return "Unloaded"; |
| case SegmentState::Loaded: |
| return "Loaded"; |
| case SegmentState::Dispatching: |
| return "Dispatching"; |
| } |
| } |
| |
| #endif // !LOG_DISABLED |
| |
| ReplayController::ReplayController(Page& page) |
| : m_page(page) |
| , m_loadedSegment(nullptr) |
| , m_loadedSession(ReplaySession::create()) |
| , m_emptyCursor(EmptyInputCursor::create()) |
| , m_activeCursor(nullptr) |
| , m_targetPosition(ReplayPosition(0, 0)) |
| , m_currentPosition(ReplayPosition(0, 0)) |
| , m_segmentState(SegmentState::Unloaded) |
| , m_sessionState(SessionState::Inactive) |
| , m_dispatchSpeed(DispatchSpeed::FastForward) |
| { |
| } |
| |
| void ReplayController::setForceDeterministicSettings(bool shouldForceDeterministicBehavior) |
| { |
| ASSERT(shouldForceDeterministicBehavior ^ (m_sessionState == SessionState::Inactive)); |
| |
| if (shouldForceDeterministicBehavior) { |
| m_savedSettings.usesPageCache = m_page.settings().usesPageCache(); |
| |
| m_page.settings().setUsesPageCache(false); |
| } else { |
| m_page.settings().setUsesPageCache(m_savedSettings.usesPageCache); |
| } |
| |
| #if ENABLE(ASYNC_SCROLLING) |
| if (ScrollingCoordinator* scrollingCoordinator = m_page.scrollingCoordinator()) |
| scrollingCoordinator->replaySessionStateDidChange(); |
| #endif |
| } |
| |
| void ReplayController::setSessionState(SessionState state) |
| { |
| ASSERT(state != m_sessionState); |
| |
| LOG(WebReplay, "%-20s SessionState transition: %10s --> %10s.\n", "ReplayController", sessionStateToString(m_sessionState), sessionStateToString(state)); |
| |
| switch (m_sessionState) { |
| case SessionState::Capturing: |
| ASSERT(state == SessionState::Inactive); |
| |
| m_sessionState = state; |
| m_page.userInputBridge().setState(UserInputBridge::State::Open); |
| break; |
| |
| case SessionState::Inactive: |
| m_sessionState = state; |
| m_page.userInputBridge().setState(state == SessionState::Capturing ? UserInputBridge::State::Capturing : UserInputBridge::State::Replaying); |
| break; |
| |
| case SessionState::Replaying: |
| ASSERT(state == SessionState::Inactive); |
| |
| m_sessionState = state; |
| m_page.userInputBridge().setState(UserInputBridge::State::Open); |
| break; |
| } |
| } |
| |
| void ReplayController::setSegmentState(SegmentState state) |
| { |
| ASSERT(state != m_segmentState); |
| |
| LOG(WebReplay, "%-20s SegmentState transition: %10s --> %10s.\n", "ReplayController", segmentStateToString(m_segmentState), segmentStateToString(state)); |
| |
| switch (m_segmentState) { |
| case SegmentState::Appending: |
| ASSERT(state == SegmentState::Unloaded); |
| break; |
| |
| case SegmentState::Unloaded: |
| ASSERT(state == SegmentState::Appending || state == SegmentState::Loaded); |
| break; |
| |
| case SegmentState::Loaded: |
| ASSERT(state == SegmentState::Unloaded || state == SegmentState::Dispatching); |
| break; |
| |
| case SegmentState::Dispatching: |
| ASSERT(state == SegmentState::Loaded); |
| break; |
| } |
| |
| m_segmentState = state; |
| } |
| |
| void ReplayController::switchSession(PassRefPtr<ReplaySession> session) |
| { |
| ASSERT(m_segmentState == SegmentState::Unloaded); |
| ASSERT(m_sessionState == SessionState::Inactive); |
| |
| m_loadedSession = session; |
| m_currentPosition = ReplayPosition(0, 0); |
| |
| LOG(WebReplay, "%-20sSwitching sessions from %p to %p.\n", "ReplayController", m_loadedSession.get(), session.get()); |
| InspectorInstrumentation::sessionLoaded(&m_page, m_loadedSession); |
| } |
| |
| void ReplayController::createSegment() |
| { |
| ASSERT(m_sessionState == SessionState::Capturing); |
| ASSERT(m_segmentState == SegmentState::Unloaded); |
| |
| setSegmentState(SegmentState::Appending); |
| |
| // Create a new segment but don't associate it with the current session |
| // until we stop appending to it. This preserves the invariant that |
| // segments associated with a replay session have immutable data. |
| m_loadedSegment = ReplaySessionSegment::create(); |
| |
| LOG(WebReplay, "%-20s Created segment: %p.\n", "ReplayController", m_loadedSegment.get()); |
| InspectorInstrumentation::segmentCreated(&m_page, m_loadedSegment); |
| |
| m_activeCursor = CapturingInputCursor::create(m_loadedSegment); |
| m_activeCursor->appendInput<BeginSegmentSentinel>(); |
| |
| std::unique_ptr<InitialNavigation> navigationInput = InitialNavigation::createFromPage(m_page); |
| // Dispatching this input schedules navigation of the main frame, causing a refresh. |
| navigationInput->dispatch(*this); |
| m_activeCursor->storeInput(WTF::move(navigationInput)); |
| } |
| |
| void ReplayController::completeSegment() |
| { |
| ASSERT(m_sessionState == SessionState::Capturing); |
| ASSERT(m_segmentState == SegmentState::Appending); |
| |
| m_activeCursor->appendInput<EndSegmentSentinel>(); |
| |
| // Hold on to a reference so unloading the segment doesn't deallocate it. |
| RefPtr<ReplaySessionSegment> segment = m_loadedSegment; |
| bool shouldSuppressNotifications = true; |
| unloadSegment(shouldSuppressNotifications); |
| |
| LOG(WebReplay, "%-20s Completed segment: %p.\n", "ReplayController", segment.get()); |
| InspectorInstrumentation::segmentCompleted(&m_page, segment); |
| |
| m_loadedSession->appendSegment(segment); |
| InspectorInstrumentation::sessionModified(&m_page, m_loadedSession); |
| } |
| |
| void ReplayController::loadSegmentAtIndex(size_t segmentIndex) |
| { |
| ASSERT(segmentIndex < m_loadedSession->size()); |
| RefPtr<ReplaySessionSegment> segment = m_loadedSession->at(segmentIndex); |
| |
| ASSERT(m_sessionState == SessionState::Replaying); |
| ASSERT(m_segmentState == SegmentState::Unloaded); |
| ASSERT(segment); |
| ASSERT(!m_loadedSegment); |
| |
| m_loadedSegment = segment; |
| setSegmentState(SegmentState::Loaded); |
| |
| m_currentPosition.segmentOffset = segmentIndex; |
| m_currentPosition.inputOffset = 0; |
| |
| m_activeCursor = ReplayingInputCursor::create(m_loadedSegment, m_page, this); |
| |
| LOG(WebReplay, "%-20sLoading segment: %p.\n", "ReplayController", segment.get()); |
| InspectorInstrumentation::segmentLoaded(&m_page, segment); |
| } |
| |
| void ReplayController::unloadSegment(bool suppressNotifications) |
| { |
| ASSERT(m_sessionState != SessionState::Inactive); |
| ASSERT(m_segmentState == SegmentState::Loaded || m_segmentState == SegmentState::Appending); |
| |
| setSegmentState(SegmentState::Unloaded); |
| |
| LOG(WebReplay, "%-20s Clearing input cursors for page: %p\n", "ReplayController", &m_page); |
| |
| m_activeCursor = nullptr; |
| RefPtr<ReplaySessionSegment> unloadedSegment = m_loadedSegment.release(); |
| for (Frame* frame = &m_page.mainFrame(); frame; frame = frame->tree().traverseNext()) { |
| frame->script().globalObject(mainThreadNormalWorld())->setInputCursor(m_emptyCursor); |
| frame->document()->setInputCursor(m_emptyCursor); |
| } |
| |
| // When we stop capturing, don't send out segment unloaded events since we |
| // didn't send out the corresponding segmentLoaded event at the start of capture. |
| if (!suppressNotifications) { |
| LOG(WebReplay, "%-20sUnloading segment: %p.\n", "ReplayController", unloadedSegment.get()); |
| InspectorInstrumentation::segmentUnloaded(&m_page); |
| } |
| } |
| |
| void ReplayController::startCapturing() |
| { |
| ASSERT(m_sessionState == SessionState::Inactive); |
| ASSERT(m_segmentState == SegmentState::Unloaded); |
| |
| setSessionState(SessionState::Capturing); |
| setForceDeterministicSettings(true); |
| |
| LOG(WebReplay, "%-20s Starting capture.\n", "ReplayController"); |
| InspectorInstrumentation::captureStarted(&m_page); |
| |
| m_currentPosition = ReplayPosition(0, 0); |
| |
| createSegment(); |
| } |
| |
| void ReplayController::stopCapturing() |
| { |
| ASSERT(m_sessionState == SessionState::Capturing); |
| ASSERT(m_segmentState == SegmentState::Appending); |
| |
| completeSegment(); |
| |
| setSessionState(SessionState::Inactive); |
| setForceDeterministicSettings(false); |
| |
| LOG(WebReplay, "%-20s Stopping capture.\n", "ReplayController"); |
| InspectorInstrumentation::captureStopped(&m_page); |
| } |
| |
| void ReplayController::startPlayback() |
| { |
| ASSERT(m_sessionState == SessionState::Replaying); |
| ASSERT(m_segmentState == SegmentState::Loaded); |
| |
| setSegmentState(SegmentState::Dispatching); |
| |
| LOG(WebReplay, "%-20s Starting playback to position (segment: %d, input: %d).\n", "ReplayController", m_targetPosition.segmentOffset, m_targetPosition.inputOffset); |
| InspectorInstrumentation::playbackStarted(&m_page); |
| |
| dispatcher().setDispatchSpeed(m_dispatchSpeed); |
| dispatcher().run(); |
| } |
| |
| void ReplayController::pausePlayback() |
| { |
| ASSERT(m_sessionState == SessionState::Replaying); |
| ASSERT(m_segmentState == SegmentState::Dispatching); |
| |
| if (dispatcher().isRunning()) |
| dispatcher().pause(); |
| |
| setSegmentState(SegmentState::Loaded); |
| |
| LOG(WebReplay, "%-20s Pausing playback at position (segment: %d, input: %d).\n", "ReplayController", m_currentPosition.segmentOffset, m_currentPosition.inputOffset); |
| InspectorInstrumentation::playbackPaused(&m_page, m_currentPosition); |
| } |
| |
| void ReplayController::cancelPlayback() |
| { |
| ASSERT(m_sessionState == SessionState::Replaying); |
| ASSERT(m_segmentState != SegmentState::Appending); |
| |
| if (m_segmentState == SegmentState::Unloaded) |
| return; |
| |
| if (m_segmentState == SegmentState::Dispatching) |
| pausePlayback(); |
| |
| ASSERT(m_segmentState == SegmentState::Loaded); |
| unloadSegment(); |
| m_sessionState = SessionState::Inactive; |
| setForceDeterministicSettings(false); |
| InspectorInstrumentation::playbackFinished(&m_page); |
| } |
| |
| void ReplayController::replayToPosition(const ReplayPosition& position, DispatchSpeed speed) |
| { |
| ASSERT(m_sessionState != SessionState::Capturing); |
| ASSERT(m_segmentState == SegmentState::Loaded || m_segmentState == SegmentState::Unloaded); |
| ASSERT(position.segmentOffset < m_loadedSession->size()); |
| |
| m_dispatchSpeed = speed; |
| |
| if (m_sessionState != SessionState::Replaying) { |
| setSessionState(SessionState::Replaying); |
| setForceDeterministicSettings(true); |
| } |
| |
| if (m_segmentState == SegmentState::Unloaded) |
| loadSegmentAtIndex(position.segmentOffset); |
| else if (position.segmentOffset != m_currentPosition.segmentOffset || m_currentPosition.inputOffset > position.inputOffset) { |
| // If the desired segment is not loaded or we have gone past the desired input |
| // offset, then unload the current segment and load the appropriate segment. |
| unloadSegment(); |
| loadSegmentAtIndex(position.segmentOffset); |
| } |
| |
| ASSERT(m_currentPosition.segmentOffset == position.segmentOffset); |
| ASSERT(m_loadedSession->at(position.segmentOffset) == m_loadedSegment); |
| |
| m_targetPosition = position; |
| startPlayback(); |
| } |
| |
| void ReplayController::frameNavigated(DocumentLoader* loader) |
| { |
| ASSERT(m_sessionState != SessionState::Inactive); |
| |
| // The initial capturing segment is created prior to main frame navigation. |
| // Otherwise, the prior capturing segment was completed when the frame detached, |
| // and it is now time to create a new segment. |
| if (m_sessionState == SessionState::Capturing && m_segmentState == SegmentState::Unloaded) { |
| m_currentPosition = ReplayPosition(m_currentPosition.segmentOffset + 1, 0); |
| createSegment(); |
| } |
| |
| // During playback, the next segment is loaded when the final input is dispatched, |
| // so nothing needs to be done here. |
| |
| // We store the input cursor in both Document and JSDOMWindow, so that |
| // replay state is accessible from JavaScriptCore and script-free layout code. |
| loader->frame()->document()->setInputCursor(m_activeCursor.get()); |
| loader->frame()->script().globalObject(mainThreadNormalWorld())->setInputCursor(m_activeCursor.get()); |
| } |
| |
| void ReplayController::frameDetached(Frame* frame) |
| { |
| ASSERT(m_sessionState != SessionState::Inactive); |
| ASSERT(frame); |
| |
| if (!frame->document()) |
| return; |
| |
| // If the frame's cursor isn't capturing or replaying, we should do nothing. |
| // This is the case for the "outbound" frame when starting capture, or when |
| // we clear the input cursor to finish or prematurely unload a segment. |
| if (frame->document()->inputCursor().isCapturing()) { |
| ASSERT(m_segmentState == SegmentState::Appending); |
| completeSegment(); |
| } |
| |
| // During playback, the segments are unloaded and loaded when the final |
| // input has been dispatched. So, nothing needs to be done here. |
| } |
| |
| void ReplayController::willDispatchEvent(const Event& event, Frame* frame) |
| { |
| EventTarget* target = event.target(); |
| if (!target && !frame) |
| return; |
| |
| Document* document = frame ? frame->document() : nullptr; |
| // Fetch the document from the event target, because the target could be detached. |
| if (Node* node = target->toNode()) |
| document = node->inDocument() ? &node->document() : node->ownerDocument(); |
| else if (DOMWindow* window = target->toDOMWindow()) |
| document = window->document(); |
| |
| ASSERT(document); |
| InputCursor& cursor = document->inputCursor(); |
| |
| #if !LOG_DISABLED |
| bool eventIsUnrelated = !cursor.isCapturing() && !cursor.isReplaying(); |
| logDispatchedDOMEvent(event, eventIsUnrelated); |
| #else |
| UNUSED_PARAM(cursor); |
| #endif |
| |
| #if ENABLE_AGGRESSIVE_DETERMINISM_CHECKS |
| // To ensure deterministic JS execution, all DOM events must be dispatched deterministically. |
| // If these assertions fail, then this DOM event is being dispatched by a nondeterministic EventLoop |
| // cycle, and may cause program execution to diverge if any JS code runs because of the DOM event. |
| if (cursor.isCapturing() || cursor.isReplaying()) |
| ASSERT(cursor.withinEventLoopInputExtent()); |
| else if (cursor.isReplaying()) |
| ASSERT(dispatcher().isDispatching()); |
| #endif |
| } |
| |
| PassRefPtr<ReplaySession> ReplayController::loadedSession() const |
| { |
| return m_loadedSession; |
| } |
| |
| PassRefPtr<ReplaySessionSegment> ReplayController::loadedSegment() const |
| { |
| return m_loadedSegment; |
| } |
| |
| InputCursor& ReplayController::activeInputCursor() const |
| { |
| return m_activeCursor ? *m_activeCursor : *m_emptyCursor; |
| } |
| |
| EventLoopInputDispatcher& ReplayController::dispatcher() const |
| { |
| ASSERT(m_sessionState == SessionState::Replaying); |
| ASSERT(m_segmentState == SegmentState::Dispatching); |
| ASSERT(m_activeCursor); |
| ASSERT(m_activeCursor->isReplaying()); |
| |
| return static_cast<ReplayingInputCursor&>(*m_activeCursor).dispatcher(); |
| } |
| |
| void ReplayController::willDispatchInput(const EventLoopInputBase&) |
| { |
| ASSERT(m_sessionState == SessionState::Replaying); |
| ASSERT(m_segmentState == SegmentState::Dispatching); |
| |
| m_currentPosition.inputOffset++; |
| |
| InspectorInstrumentation::playbackHitPosition(&m_page, m_currentPosition); |
| |
| if (m_currentPosition == m_targetPosition) |
| pausePlayback(); |
| } |
| |
| void ReplayController::didDispatchInput(const EventLoopInputBase&) |
| { |
| ASSERT(m_sessionState == SessionState::Replaying); |
| ASSERT(m_segmentState == SegmentState::Dispatching); |
| } |
| |
| void ReplayController::didDispatchFinalInput() |
| { |
| ASSERT(m_segmentState == SegmentState::Dispatching); |
| |
| // No more segments left to replay; stop. |
| if (m_currentPosition.segmentOffset + 1 == m_loadedSession->size()) { |
| // Normally the position is adjusted when loading the next segment. |
| m_currentPosition.segmentOffset++; |
| m_currentPosition.inputOffset = 0; |
| |
| cancelPlayback(); |
| return; |
| } |
| |
| unloadSegment(); |
| loadSegmentAtIndex(m_currentPosition.segmentOffset + 1); |
| startPlayback(); |
| } |
| |
| } // namespace WebCore |
| |
| #endif // ENABLE(WEB_REPLAY) |