ReplayController.cpp   [plain text]


/*
 * 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 {

static void logDispatchedDOMEvent(const Event& event, bool eventIsUnrelated)
{
#if !LOG_DISABLED
    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=%lu/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=%lu/window[%p] %s\n", "ReplayEvents",
            (eventIsUnrelated) ? "Unrelated" : "Dispatching",
            event.type().string().utf8().data(),
            frameIndexFromDocument(window->document()),
            window,
            window->location()->href().utf8().data());
    }
#else
    UNUSED_PARAM(event);
    UNUSED_PARAM(eventIsUnrelated);
#endif
}

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);

    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::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);

    m_segmentState = 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 = m_loadedSegment->createCapturingCursor(m_page);
    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;
    m_segmentState = SegmentState::Loaded;
    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;
    m_segmentState = SegmentState::Loaded;

    m_currentPosition.segmentOffset = segmentIndex;
    m_currentPosition.inputOffset = 0;

    m_activeCursor = m_loadedSegment->createReplayingCursor(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::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);

    m_segmentState = 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();

    m_segmentState = 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();
    bool eventIsUnrelated = !cursor.isCapturing() && !cursor.isReplaying();
    logDispatchedDOMEvent(event, eventIsUnrelated);

#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())
        ASSERT(static_cast<CapturingInputCursor&>(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++;
    if (m_currentPosition == m_targetPosition)
        pausePlayback();
}

void ReplayController::didDispatchInput(const EventLoopInputBase&)
{
    ASSERT(m_sessionState == SessionState::Replaying);
    ASSERT(m_segmentState == SegmentState::Dispatching);

    InspectorInstrumentation::playbackHitPosition(&m_page, m_currentPosition);
}

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)