toolkit/recordreplay/ipc/ChildNavigation.cpp
author Brian Hackett <bhackett1024@gmail.com>
Wed, 17 Oct 2018 09:33:00 -0600
changeset 500489 40a4e0406d3daf01b3e2d37767cd7adce5e11396
parent 491750 2ddf1cb297c5abc89e56df7105a3a595fea9f41d
child 500491 33cf80c7214823e5e38307a666add7a0ef116e17
permissions -rw-r--r--
Bug 1498012 Part 2 - Tolerate time warp targets being created when handling debugger requests, r=mccr8.

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 * vim: set ts=8 sts=4 et sw=4 tw=99:
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "ChildInternal.h"

#include "ProcessRecordReplay.h"

namespace mozilla {
namespace recordreplay {
namespace navigation {

typedef js::BreakpointPosition BreakpointPosition;
typedef js::ExecutionPoint ExecutionPoint;

static void
BreakpointPositionToString(const BreakpointPosition& aPos, nsAutoCString& aStr)
{
  aStr.AppendPrintf("{ Kind: %s, Script: %d, Offset: %d, Frame: %d }",
                    aPos.KindString(), (int) aPos.mScript, (int) aPos.mOffset, (int) aPos.mFrameIndex);
}

static void
ExecutionPointToString(const ExecutionPoint& aPoint, nsAutoCString& aStr)
{
  aStr.AppendPrintf("{ Checkpoint %d", (int) aPoint.mCheckpoint);
  if (aPoint.HasPosition()) {
    aStr.AppendPrintf(" Progress %llu Position ", aPoint.mProgress);
    BreakpointPositionToString(aPoint.mPosition, aStr);
  }
  aStr.AppendPrintf(" }");
}

///////////////////////////////////////////////////////////////////////////////
// Navigation State
///////////////////////////////////////////////////////////////////////////////

// The navigation state of a recording/replaying process describes where the
// process currently is and what it is doing in order to respond to messages
// from the middleman process.
//
// At all times, the navigation state will be in exactly one of the following
// phases:
//
// - Paused: The process is paused somewhere.
// - Forward: The process is running forward and scanning for breakpoint hits.
// - ReachBreakpoint: The process is running forward from a checkpoint to a
//     particular execution point before the next checkpoint.
// - FindLastHit: The process is running forward and keeping track of the last
//     point a breakpoint was hit within an execution region.
//
// This file manages data associated with each of these phases and the
// transitions that occur between them as the process executes or new
// messages are received from the middleman.

typedef AllocPolicy<MemoryKind::Navigation> UntrackedAllocPolicy;

// Abstract class for where we are at in the navigation state machine.
// Each subclass has a single instance contained in NavigationState (see below)
// and it and all its data are allocated using untracked memory that is not
// affected by restoring earlier checkpoints.
class NavigationPhase
{
  // All virtual members should only be accessed through NavigationState.
  friend class NavigationState;

private:
  MOZ_NORETURN void Unsupported(const char* aOperation) {
    nsAutoCString str;
    ToString(str);

    Print("Operation %s not supported: %s\n", aOperation, str.get());
    MOZ_CRASH("Unsupported navigation operation");
  }

public:
  virtual void ToString(nsAutoCString& aStr) = 0;

  // The process has just reached or rewound to a checkpoint.
  virtual void AfterCheckpoint(const CheckpointId& aCheckpoint) {
    Unsupported("AfterCheckpoint");
  }

  // Called when some position with an installed handler has been reached.
  virtual void PositionHit(const ExecutionPoint& aPoint) {
    Unsupported("PositionHit");
  }

  // Called after receiving a resume command from the middleman.
  virtual void Resume(bool aForward) {
    Unsupported("Resume");
  }

  // Called after the middleman tells us to rewind to a specific checkpoint.
  virtual void RestoreCheckpoint(size_t aCheckpoint) {
    Unsupported("RestoreCheckpoint");
  }

  // Called after the middleman tells us to run forward to a specific point.
  virtual void RunToPoint(const ExecutionPoint& aTarget) {
    Unsupported("RunToPoint");
  }

  // Process an incoming debugger request from the middleman.
  virtual void HandleDebuggerRequest(js::CharBuffer* aRequestBuffer) {
    Unsupported("HandleDebuggerRequest");
  }

  // Called when a debugger request wants to try an operation that may
  // trigger an unhandled divergence from the recording.
  virtual bool MaybeDivergeFromRecording() {
    Unsupported("MaybeDivergeFromRecording");
  }

  // Get the current execution point when recording.
  virtual ExecutionPoint GetRecordingEndpoint() {
    Unsupported("GetRecordingEndpoint");
  }

  // Called when execution reaches the endpoint of the recording.
  virtual void HitRecordingEndpoint(const ExecutionPoint& aPoint) {
    Unsupported("HitRecordingEndpoint");
  }
};

// Information about a debugger request sent by the middleman.
struct RequestInfo
{
  // JSON contents for the request and response.
  InfallibleVector<char16_t, 0, UntrackedAllocPolicy> mRequestBuffer;
  InfallibleVector<char16_t, 0, UntrackedAllocPolicy> mResponseBuffer;

  // Whether processing this request triggered an unhandled divergence.
  bool mUnhandledDivergence;

  RequestInfo() : mUnhandledDivergence(false) {}

  RequestInfo(const RequestInfo& o)
    : mUnhandledDivergence(o.mUnhandledDivergence)
  {
    mRequestBuffer.append(o.mRequestBuffer.begin(), o.mRequestBuffer.length());
    mResponseBuffer.append(o.mResponseBuffer.begin(), o.mResponseBuffer.length());
  }
};
typedef InfallibleVector<RequestInfo, 4, UntrackedAllocPolicy> UntrackedRequestVector;

typedef InfallibleVector<uint32_t> BreakpointVector;

// Phase when the replaying process is paused.
class PausedPhase final : public NavigationPhase
{
  // Location of the pause.
  ExecutionPoint mPoint;

  // Whether we are paused at the end of the recording.
  bool mRecordingEndpoint;

  // All debugger requests we have seen while paused here.
  UntrackedRequestVector mRequests;

  // Index of the request currently being processed. Normally this is the
  // last entry in |mRequests|, though may be earlier if we are recovering
  // from an unhandled divergence.
  size_t mRequestIndex;

  // Whether we have saved a temporary checkpoint.
  bool mSavedTemporaryCheckpoint;

  // Whether we had to restore a checkpoint to deal with an unhandled
  // recording divergence, and haven't finished rehandling old requests.
  bool mRecoveringFromDivergence;

  // Set when we were told to resume forward and need to clean up our state.
  bool mResumeForward;

public:
  void Enter(const ExecutionPoint& aPoint,
             const BreakpointVector& aBreakpoints = BreakpointVector(),
             bool aRewind = false, bool aRecordingEndpoint = false);

  void ToString(nsAutoCString& aStr) override {
    aStr.AppendPrintf("Paused RecoveringFromDivergence %d", mRecoveringFromDivergence);
  }

  void AfterCheckpoint(const CheckpointId& aCheckpoint) override;
  void PositionHit(const ExecutionPoint& aPoint) override;
  void Resume(bool aForward) override;
  void RestoreCheckpoint(size_t aCheckpoint) override;
  void RunToPoint(const ExecutionPoint& aTarget) override;
  void HandleDebuggerRequest(js::CharBuffer* aRequestBuffer) override;
  bool MaybeDivergeFromRecording() override;
  ExecutionPoint GetRecordingEndpoint() override;

  bool EnsureTemporaryCheckpoint();
};

// Phase when execution is proceeding forwards in search of breakpoint hits.
class ForwardPhase final : public NavigationPhase
{
  // Some execution point in the recent past. There are no checkpoints or
  // breakpoint hits between this point and the current point of execution.
  ExecutionPoint mPoint;

public:
  void Enter(const ExecutionPoint& aPoint);

  void ToString(nsAutoCString& aStr) override {
    aStr.AppendPrintf("Forward");
  }

  void AfterCheckpoint(const CheckpointId& aCheckpoint) override;
  void PositionHit(const ExecutionPoint& aPoint) override;
  void HitRecordingEndpoint(const ExecutionPoint& aPoint) override;
};

// Phase when the replaying process is running forward from a checkpoint to a
// breakpoint at a particular execution point.
class ReachBreakpointPhase final : public NavigationPhase
{
private:
  // Where to start running from.
  CheckpointId mStart;

  // The point we are running to.
  ExecutionPoint mPoint;

  // Point at which to decide whether to save a temporary checkpoint.
  Maybe<ExecutionPoint> mTemporaryCheckpoint;

  // Whether we have saved a temporary checkpoint at the specified point.
  bool mSavedTemporaryCheckpoint;

  // The time at which we started running forward from the initial
  // checkpoint, in microseconds.
  double mStartTime;

public:
  void Enter(const CheckpointId& aStart,
             bool aRewind,
             const ExecutionPoint& aPoint,
             const Maybe<ExecutionPoint>& aTemporaryCheckpoint);

  void ToString(nsAutoCString& aStr) override {
    aStr.AppendPrintf("ReachBreakpoint: ");
    ExecutionPointToString(mPoint, aStr);
    if (mTemporaryCheckpoint.isSome()) {
      aStr.AppendPrintf(" TemporaryCheckpoint: ");
      ExecutionPointToString(mTemporaryCheckpoint.ref(), aStr);
    }
  }

  void AfterCheckpoint(const CheckpointId& aCheckpoint) override;
  void PositionHit(const ExecutionPoint& aPoint) override;
};

// Phase when the replaying process is searching forward from a checkpoint to
// find the last point a breakpoint is hit before reaching an execution point.
class FindLastHitPhase final : public NavigationPhase
{
  // Where we started searching from.
  CheckpointId mStart;

  // Endpoint of the search, nothing if the endpoint is the next checkpoint.
  Maybe<ExecutionPoint> mEnd;

  // Counter that increases as we run forward, for ordering hits.
  size_t mCounter;

  // All positions we are interested in hits for, including all breakpoint
  // positions (and possibly other positions).
  struct TrackedPosition {
    BreakpointPosition mPosition;

    // The last time this was hit so far, or invalid.
    ExecutionPoint mLastHit;

    // The value of the counter when the last hit occurred.
    size_t mLastHitCount;

    explicit TrackedPosition(const BreakpointPosition& aPosition)
      : mPosition(aPosition), mLastHitCount(0)
    {}
  };
  InfallibleVector<TrackedPosition, 4, UntrackedAllocPolicy> mTrackedPositions;

  const TrackedPosition& FindTrackedPosition(const BreakpointPosition& aPos);
  void OnRegionEnd();

public:
  // Note: this always rewinds.
  void Enter(const CheckpointId& aStart, const Maybe<ExecutionPoint>& aEnd);

  void ToString(nsAutoCString& aStr) override {
    aStr.AppendPrintf("FindLastHit");
  }

  void AfterCheckpoint(const CheckpointId& aCheckpoint) override;
  void PositionHit(const ExecutionPoint& aPoint) override;
  void HitRecordingEndpoint(const ExecutionPoint& aPoint) override;
};

// Structure which manages state about the breakpoints in existence and about
// how the process is being navigated through. This is allocated in untracked
// memory and its contents will not change when restoring an earlier
// checkpoint.
class NavigationState
{
  // When replaying, the last known recording endpoint. There may be other,
  // later endpoints we haven't been informed about.
  ExecutionPoint mRecordingEndpoint;
  size_t mRecordingEndpointIndex;

  // The last checkpoint we ran forward or rewound to.
  CheckpointId mLastCheckpoint;

  // The locations of all temporary checkpoints we have saved. Temporary
  // checkpoints are taken immediately prior to reaching these points.
  InfallibleVector<ExecutionPoint, 0, UntrackedAllocPolicy> mTemporaryCheckpoints;

public:
  // All the currently installed breakpoints, indexed by their ID.
  InfallibleVector<BreakpointPosition, 4, UntrackedAllocPolicy> mBreakpoints;

  BreakpointPosition& GetBreakpoint(size_t id) {
    while (id >= mBreakpoints.length()) {
      mBreakpoints.emplaceBack();
    }
    return mBreakpoints[id];
  }

  CheckpointId LastCheckpoint() {
    return mLastCheckpoint;
  }

  // The current phase of the process.
  NavigationPhase* mPhase;

  void SetPhase(NavigationPhase* phase) {
    mPhase = phase;

    if (SpewEnabled()) {
      nsAutoCString str;
      mPhase->ToString(str);

      PrintSpew("SetNavigationPhase %s\n", str.get());
    }
  }

  PausedPhase mPausedPhase;
  ForwardPhase mForwardPhase;
  ReachBreakpointPhase mReachBreakpointPhase;
  FindLastHitPhase mFindLastHitPhase;

  // For testing, specify that temporary checkpoints should be taken regardless
  // of how much time has elapsed.
  bool mAlwaysSaveTemporaryCheckpoints;

  // Checkpoints for all time warp targets that have been generated.
  InfallibleVector<std::pair<ProgressCounter, size_t>, 0, UntrackedAllocPolicy> mTimeWarpTargetCheckpoints;

  // Note: NavigationState is initially zeroed.
  NavigationState()
    : mPhase(&mForwardPhase)
  {
    if (IsReplaying()) {
      // The recording must include everything up to the first
      // checkpoint. After that point we will ask the record/replay
      // system to notify us about any further endpoints.
      mRecordingEndpoint = ExecutionPoint(CheckpointId::First);
    }
  }

  void AfterCheckpoint(const CheckpointId& aCheckpoint) {
    mLastCheckpoint = aCheckpoint;

    // Forget any temporary checkpoints we just rewound past, or made
    // obsolete by reaching the next normal checkpoint.
    while (mTemporaryCheckpoints.length() > aCheckpoint.mTemporary) {
      mTemporaryCheckpoints.popBack();
    }

    mPhase->AfterCheckpoint(aCheckpoint);

    // Make sure we don't run past the end of the recording.
    if (!aCheckpoint.mTemporary) {
      ExecutionPoint point(aCheckpoint.mNormal);
      CheckForRecordingEndpoint(point);
    }

    MOZ_RELEASE_ASSERT(IsRecording() ||
                       aCheckpoint.mNormal <= mRecordingEndpoint.mCheckpoint);
    if (aCheckpoint.mNormal == mRecordingEndpoint.mCheckpoint &&
        mRecordingEndpoint.HasPosition()) {
      js::EnsurePositionHandler(mRecordingEndpoint.mPosition);
    }
  }

  void PositionHit(const ExecutionPoint& aPoint) {
    mPhase->PositionHit(aPoint);
    CheckForRecordingEndpoint(aPoint);
  }

  void Resume(bool aForward) {
    mPhase->Resume(aForward);
  }

  void RestoreCheckpoint(size_t aCheckpoint) {
    mPhase->RestoreCheckpoint(aCheckpoint);
  }

  void RunToPoint(const ExecutionPoint& aTarget) {
    mPhase->RunToPoint(aTarget);
  }

  void HandleDebuggerRequest(js::CharBuffer* aRequestBuffer) {
    mPhase->HandleDebuggerRequest(aRequestBuffer);
  }

  bool MaybeDivergeFromRecording() {
    return mPhase->MaybeDivergeFromRecording();
  }

  ExecutionPoint GetRecordingEndpoint() {
    return mPhase->GetRecordingEndpoint();
  }

  void SetRecordingEndpoint(size_t aIndex, const ExecutionPoint& aEndpoint) {
    // Ignore endpoints older than the last one we know about.
    if (aIndex <= mRecordingEndpointIndex) {
      return;
    }
    MOZ_RELEASE_ASSERT(mRecordingEndpoint.mCheckpoint <= aEndpoint.mCheckpoint);
    mRecordingEndpointIndex = aIndex;
    mRecordingEndpoint = aEndpoint;
    if (aEndpoint.HasPosition()) {
      js::EnsurePositionHandler(aEndpoint.mPosition);
    }
  }

  void CheckForRecordingEndpoint(const ExecutionPoint& aPoint) {
    while (aPoint == mRecordingEndpoint) {
      // The recording ended after the checkpoint, but maybe there is
      // another, later endpoint now. This may call back into
      // setRecordingEndpoint and notify us there is more recording data
      // available.
      if (!recordreplay::HitRecordingEndpoint()) {
        mPhase->HitRecordingEndpoint(mRecordingEndpoint);
      }
    }
  }

  size_t NumTemporaryCheckpoints() {
    return mTemporaryCheckpoints.length();
  }

  bool SaveTemporaryCheckpoint(const ExecutionPoint& aPoint) {
    MOZ_RELEASE_ASSERT(aPoint.mCheckpoint == mLastCheckpoint.mNormal);
    mTemporaryCheckpoints.append(aPoint);
    return NewCheckpoint(/* aTemporary = */ true);
  }

  ExecutionPoint LastTemporaryCheckpointLocation() {
    MOZ_RELEASE_ASSERT(!mTemporaryCheckpoints.empty());
    return mTemporaryCheckpoints.back();
  }
};

static NavigationState* gNavigation;

static void
GetAllBreakpointHits(const ExecutionPoint& aPoint, BreakpointVector& aHitBreakpoints)
{
  MOZ_RELEASE_ASSERT(aPoint.HasPosition());
  for (size_t id = 0; id < gNavigation->mBreakpoints.length(); id++) {
    const BreakpointPosition& breakpoint = gNavigation->mBreakpoints[id];
    if (breakpoint.IsValid() && breakpoint.Subsumes(aPoint.mPosition)) {
      aHitBreakpoints.append(id);
    }
  }
}

///////////////////////////////////////////////////////////////////////////////
// Paused Phase
///////////////////////////////////////////////////////////////////////////////

static bool
ThisProcessCanRewind()
{
  return HasSavedCheckpoint();
}

void
PausedPhase::Enter(const ExecutionPoint& aPoint, const BreakpointVector& aBreakpoints,
                   bool aRewind, bool aRecordingEndpoint)
{
  mPoint = aPoint;
  mRecordingEndpoint = aRecordingEndpoint;
  mRequests.clear();
  mRequestIndex = 0;
  mSavedTemporaryCheckpoint = false;
  mRecoveringFromDivergence = false;
  mResumeForward = false;

  gNavigation->SetPhase(this);

  // Breakpoints will never be hit if we are at a checkpoint.
  MOZ_RELEASE_ASSERT(aPoint.HasPosition() || aBreakpoints.empty());

  if (aRewind) {
    MOZ_RELEASE_ASSERT(!aPoint.HasPosition());
    RestoreCheckpointAndResume(CheckpointId(aPoint.mCheckpoint));
    Unreachable();
  }

  if (aPoint.HasPosition()) {
    child::HitBreakpoint(aRecordingEndpoint, aBreakpoints.begin(), aBreakpoints.length());
  } else {
    child::HitCheckpoint(aPoint.mCheckpoint, aRecordingEndpoint);
  }
}

void
PausedPhase::AfterCheckpoint(const CheckpointId& aCheckpoint)
{
  MOZ_RELEASE_ASSERT(!mRecoveringFromDivergence);
  if (!aCheckpoint.mTemporary) {
    // We just rewound here, and are now where we should pause.
    MOZ_RELEASE_ASSERT(mPoint == ExecutionPoint(aCheckpoint.mNormal));
    child::HitCheckpoint(mPoint.mCheckpoint, mRecordingEndpoint);
  } else {
    // We just saved or restored the temporary checkpoint taken while
    // processing debugger requests here.
    MOZ_RELEASE_ASSERT(ThisProcessCanRewind());
    MOZ_RELEASE_ASSERT(mSavedTemporaryCheckpoint);
  }
}

void
PausedPhase::PositionHit(const ExecutionPoint& aPoint)
{
  // Ignore positions hit while paused (we're probably doing an eval).
}

void
PausedPhase::Resume(bool aForward)
{
  MOZ_RELEASE_ASSERT(!mRecoveringFromDivergence);
  MOZ_RELEASE_ASSERT(!mResumeForward);

  if (aForward) {
    // If we have saved any temporary checkpoint, we performed an operation
    // that may have side effects. Clear these unwanted changes by restoring
    // the temporary checkpoint we saved earlier.
    if (mSavedTemporaryCheckpoint) {
      mResumeForward = true;
      RestoreCheckpointAndResume(gNavigation->LastCheckpoint());
      Unreachable();
    }

    js::ClearPausedState();

    // Run forward from the current execution point.
    gNavigation->mForwardPhase.Enter(mPoint);
    return;
  }

  // Search backwards in the execution space.
  if (mPoint.HasPosition()) {
    CheckpointId start = gNavigation->LastCheckpoint();

    // Skip over any temporary checkpoint we saved.
    if (mSavedTemporaryCheckpoint) {
      MOZ_RELEASE_ASSERT(start.mTemporary);
      start.mTemporary--;
    }
    gNavigation->mFindLastHitPhase.Enter(start, Some(mPoint));
  } else {
    // We can't rewind past the beginning of the replay.
    MOZ_RELEASE_ASSERT(mPoint.mCheckpoint != CheckpointId::First);

    CheckpointId start(mPoint.mCheckpoint - 1);
    gNavigation->mFindLastHitPhase.Enter(start, Nothing());
  }
  Unreachable();
}

void
PausedPhase::RestoreCheckpoint(size_t aCheckpoint)
{
  ExecutionPoint target(aCheckpoint);
  bool rewind = target != mPoint;
  Enter(target, BreakpointVector(), rewind, /* aRecordingEndpoint = */ false);
}

void
PausedPhase::RunToPoint(const ExecutionPoint& aTarget)
{
  // This may only be used when we are paused at a normal checkpoint.
  MOZ_RELEASE_ASSERT(!mPoint.HasPosition());
  size_t checkpoint = mPoint.mCheckpoint;

  MOZ_RELEASE_ASSERT(aTarget.mCheckpoint == checkpoint);
  ResumeExecution();
  gNavigation->mReachBreakpointPhase.Enter(CheckpointId(checkpoint), /* aRewind = */ false,
                                           aTarget, /* aTemporaryCheckpoint = */ Nothing());
}

void
PausedPhase::HandleDebuggerRequest(js::CharBuffer* aRequestBuffer)
{
  MOZ_RELEASE_ASSERT(!mRecoveringFromDivergence);
  MOZ_RELEASE_ASSERT(!mResumeForward);

  mRequests.emplaceBack();
  size_t index = mRequests.length() - 1;
  mRequests[index].mRequestBuffer.append(aRequestBuffer->begin(), aRequestBuffer->length());

  mRequestIndex = index;

  js::CharBuffer responseBuffer;
  js::ProcessRequest(aRequestBuffer->begin(), aRequestBuffer->length(), &responseBuffer);

  delete aRequestBuffer;

  if (gNavigation->mPhase != this) {
    // We saved a temporary checkpoint by calling MaybeDivergeFromRecording
    // within ProcessRequest, then restored it while scanning backwards.
    ResumeExecution();
    return;
  }

  if (!mResumeForward && !mRecoveringFromDivergence) {
    // We processed this request normally. Remember the response and send it to
    // the middleman process.
    MOZ_RELEASE_ASSERT(index == mRequestIndex);
    mRequests[index].mResponseBuffer.append(responseBuffer.begin(), responseBuffer.length());
    child::RespondToRequest(responseBuffer);
    return;
  }

  if (mResumeForward) {
    // We rewound to erase side effects from the temporary checkpoint we saved
    // under ProcessRequest. Just start running forward.
    MOZ_RELEASE_ASSERT(!mRecoveringFromDivergence);
    gNavigation->mForwardPhase.Enter(mPoint);
    return;
  }

  // We rewound after having an unhandled recording divergence while processing
  // mRequests[index] or some later request. We need to redo all requests up to
  // the last request we received.

  // Remember that the last request triggered an unhandled divergence.
  MOZ_RELEASE_ASSERT(!mRequests.back().mUnhandledDivergence);
  mRequests.back().mUnhandledDivergence = true;

  for (size_t i = index; i < mRequests.length(); i++) {
    RequestInfo& info = mRequests[i];
    mRequestIndex = i;

    if (i == index) {
      // We just performed this request, and responseBuffer has the right contents.
    } else {
      responseBuffer.clear();
      js::ProcessRequest(info.mRequestBuffer.begin(), info.mRequestBuffer.length(),
                         &responseBuffer);
    }

    if (i < mRequests.length() - 1) {
      // This is an old request, and we don't need to send another
      // response to it. Make sure the response we just generated matched
      // the earlier one we sent, though.
      MOZ_RELEASE_ASSERT(responseBuffer.length() == info.mResponseBuffer.length());
      MOZ_RELEASE_ASSERT(memcmp(responseBuffer.begin(), info.mResponseBuffer.begin(),
                                responseBuffer.length() * sizeof(char16_t)) == 0);
    } else {
      // This is the current request we need to respond to.
      MOZ_RELEASE_ASSERT(info.mResponseBuffer.empty());
      info.mResponseBuffer.append(responseBuffer.begin(), responseBuffer.length());
      child::RespondToRequest(responseBuffer);
    }
  }

  // We've finished recovering, and can now process new incoming requests.
  mRecoveringFromDivergence = false;
}

bool
PausedPhase::MaybeDivergeFromRecording()
{
  if (!ThisProcessCanRewind()) {
    // Recording divergence is not supported if we can't rewind. We can't
    // simply allow execution to proceed from here as if we were not
    // diverged, since any events or other activity that show up afterwards
    // will not be reflected in the recording.
    return false;
  }

  if (!EnsureTemporaryCheckpoint()) {
    // One of the premature exit cases was hit in EnsureTemporaryCheckpoint.
    // Don't allow any operations that can diverge from the recording.
    return false;
  }

  if (mRequests[mRequestIndex].mUnhandledDivergence) {
    // We tried to process this request before and had an unhandled divergence.
    // Disallow the request handler from doing anything that might diverge from
    // the recording.
    return false;
  }

  DivergeFromRecording();
  return true;
}

bool
PausedPhase::EnsureTemporaryCheckpoint()
{
  if (mSavedTemporaryCheckpoint) {
    return true;
  }

  // We need to save a temporary checkpoint that we can restore if we hit
  // a recording divergence.
  mSavedTemporaryCheckpoint = true;

  size_t index = mRequestIndex;
  if (gNavigation->SaveTemporaryCheckpoint(mPoint)) {
    // We just saved the temporary checkpoint.
    return true;
  }

  // We just rewound here.
  if (gNavigation->mPhase != this) {
    // We are no longer paused at this point. We should be searching
    // backwards in the region after this temporary checkpoint was taken.
    // Return false to ensure we don't perform any side effects before
    // resuming forward.
    return false;
  }

  // We are still paused at this point. Either we had an unhandled
  // recording divergence, or we intentionally rewound to erase side
  // effects that occurred while paused here.
  MOZ_RELEASE_ASSERT(!mRecoveringFromDivergence);

  if (mResumeForward) {
    // We can't diverge from the recording before resuming forward execution.
    return false;
  }

  mRecoveringFromDivergence = true;

  if (index == mRequestIndex) {
    // We had an unhandled divergence for the same request where we
    // created the temporary checkpoint. mUnhandledDivergence hasn't been
    // set yet, but return now to avoid triggering the same divergence
    // and rewinding again.
    return false;
  }

  // Allow the caller to check mUnhandledDivergence.
  return true;
}

ExecutionPoint
PausedPhase::GetRecordingEndpoint()
{
  MOZ_RELEASE_ASSERT(IsRecording());
  return mPoint;
}

///////////////////////////////////////////////////////////////////////////////
// ForwardPhase
///////////////////////////////////////////////////////////////////////////////

void
ForwardPhase::Enter(const ExecutionPoint& aPoint)
{
  mPoint = aPoint;

  gNavigation->SetPhase(this);

  // Install handlers for all breakpoints.
  for (const BreakpointPosition& breakpoint : gNavigation->mBreakpoints) {
    if (breakpoint.IsValid()) {
      js::EnsurePositionHandler(breakpoint);
    }
  }

  ResumeExecution();
}

void
ForwardPhase::AfterCheckpoint(const CheckpointId& aCheckpoint)
{
  MOZ_RELEASE_ASSERT(!aCheckpoint.mTemporary &&
                     aCheckpoint.mNormal == mPoint.mCheckpoint + 1);
  gNavigation->mPausedPhase.Enter(ExecutionPoint(aCheckpoint.mNormal));
}

void
ForwardPhase::PositionHit(const ExecutionPoint& aPoint)
{
  BreakpointVector hitBreakpoints;
  GetAllBreakpointHits(aPoint, hitBreakpoints);

  if (!hitBreakpoints.empty()) {
    gNavigation->mPausedPhase.Enter(aPoint, hitBreakpoints);
  }
}

void
ForwardPhase::HitRecordingEndpoint(const ExecutionPoint& aPoint)
{
  nsAutoCString str;
  ExecutionPointToString(aPoint, str);

  // Use an empty vector even if there are breakpoints here. If we started
  // running forward from aPoint and immediately hit the recording endpoint,
  // we don't want to hit the breakpoints again.
  gNavigation->mPausedPhase.Enter(aPoint, BreakpointVector(),
                                  /* aRewind = */ false, /* aRecordingEndpoint = */ true);
}

///////////////////////////////////////////////////////////////////////////////
// ReachBreakpointPhase
///////////////////////////////////////////////////////////////////////////////

void
ReachBreakpointPhase::Enter(const CheckpointId& aStart,
                            bool aRewind,
                            const ExecutionPoint& aPoint,
                            const Maybe<ExecutionPoint>& aTemporaryCheckpoint)
{
  MOZ_RELEASE_ASSERT(aPoint.HasPosition());
  MOZ_RELEASE_ASSERT(aTemporaryCheckpoint.isNothing() ||
                     (aTemporaryCheckpoint.ref().HasPosition() &&
                      aTemporaryCheckpoint.ref() != aPoint));
  mStart = aStart;
  mPoint = aPoint;
  mTemporaryCheckpoint = aTemporaryCheckpoint;
  mSavedTemporaryCheckpoint = false;

  gNavigation->SetPhase(this);

  if (aRewind) {
    RestoreCheckpointAndResume(aStart);
    Unreachable();
  } else {
    AfterCheckpoint(aStart);
  }
}

void
ReachBreakpointPhase::AfterCheckpoint(const CheckpointId& aCheckpoint)
{
  if (aCheckpoint == mStart && mTemporaryCheckpoint.isSome()) {
    js::EnsurePositionHandler(mTemporaryCheckpoint.ref().mPosition);

    // Remember the time we started running forwards from the initial checkpoint.
    mStartTime = CurrentTime();
  } else {
    MOZ_RELEASE_ASSERT((aCheckpoint == mStart && mTemporaryCheckpoint.isNothing()) ||
                       (aCheckpoint == mStart.NextCheckpoint(/* aTemporary = */ true) &&
                        mSavedTemporaryCheckpoint));
  }

  js::EnsurePositionHandler(mPoint.mPosition);
}

// The number of milliseconds to elapse during a ReachBreakpoint search before
// we will save a temporary checkpoint.
static const double kTemporaryCheckpointThresholdMs = 10;

void
AlwaysSaveTemporaryCheckpoints()
{
  gNavigation->mAlwaysSaveTemporaryCheckpoints = true;
}

void
ReachBreakpointPhase::PositionHit(const ExecutionPoint& aPoint)
{
  if (mTemporaryCheckpoint.isSome() && mTemporaryCheckpoint.ref() == aPoint) {
    // We've reached the point at which we have the option of saving a
    // temporary checkpoint.
    double elapsedMs = (CurrentTime() - mStartTime) / 1000.0;
    if (elapsedMs >= kTemporaryCheckpointThresholdMs ||
        gNavigation->mAlwaysSaveTemporaryCheckpoints)
    {
      MOZ_RELEASE_ASSERT(!mSavedTemporaryCheckpoint);
      mSavedTemporaryCheckpoint = true;

      if (!gNavigation->SaveTemporaryCheckpoint(aPoint)) {
        // We just restored the checkpoint, and could be in any phase.
        gNavigation->PositionHit(aPoint);
        return;
      }
    }
  }

  if (mPoint == aPoint) {
    BreakpointVector hitBreakpoints;
    GetAllBreakpointHits(aPoint, hitBreakpoints);
    gNavigation->mPausedPhase.Enter(aPoint, hitBreakpoints);
  }
}

///////////////////////////////////////////////////////////////////////////////
// FindLastHitPhase
///////////////////////////////////////////////////////////////////////////////

void
FindLastHitPhase::Enter(const CheckpointId& aStart, const Maybe<ExecutionPoint>& aEnd)
{
  MOZ_RELEASE_ASSERT(aEnd.isNothing() || aEnd.ref().HasPosition());

  mStart = aStart;
  mEnd = aEnd;
  mCounter = 0;
  mTrackedPositions.clear();

  gNavigation->SetPhase(this);

  // All breakpoints are tracked positions.
  for (const BreakpointPosition& breakpoint : gNavigation->mBreakpoints) {
    if (breakpoint.IsValid()) {
      mTrackedPositions.emplaceBack(breakpoint);
    }
  }

  // Entry points to scripts containing breakpoints are tracked positions.
  for (const BreakpointPosition& breakpoint : gNavigation->mBreakpoints) {
    Maybe<BreakpointPosition> entry = GetEntryPosition(breakpoint);
    if (entry.isSome()) {
      mTrackedPositions.emplaceBack(entry.ref());
    }
  }

  RestoreCheckpointAndResume(mStart);
  Unreachable();
}

void
FindLastHitPhase::AfterCheckpoint(const CheckpointId& aCheckpoint)
{
  if (aCheckpoint == mStart.NextCheckpoint(/* aTemporary = */ false)) {
    // We reached the next checkpoint, and are done searching.
    MOZ_RELEASE_ASSERT(mEnd.isNothing());
    OnRegionEnd();
    Unreachable();
  }

  // We are at the start of the search.
  MOZ_RELEASE_ASSERT(aCheckpoint == mStart);

  for (const TrackedPosition& tracked : mTrackedPositions) {
    js::EnsurePositionHandler(tracked.mPosition);
  }

  if (mEnd.isSome()) {
    js::EnsurePositionHandler(mEnd.ref().mPosition);
  }
}

void
FindLastHitPhase::PositionHit(const ExecutionPoint& aPoint)
{
  if (mEnd.isSome() && mEnd.ref() == aPoint) {
    OnRegionEnd();
    Unreachable();
  }

  ++mCounter;

  for (TrackedPosition& tracked : mTrackedPositions) {
    if (tracked.mPosition.Subsumes(aPoint.mPosition)) {
      tracked.mLastHit = aPoint;
      tracked.mLastHitCount = mCounter;
      break;
    }
  }
}

void
FindLastHitPhase::HitRecordingEndpoint(const ExecutionPoint& aPoint)
{
  OnRegionEnd();
  Unreachable();
}

const FindLastHitPhase::TrackedPosition&
FindLastHitPhase::FindTrackedPosition(const BreakpointPosition& aPos)
{
  for (const TrackedPosition& tracked : mTrackedPositions) {
    if (tracked.mPosition == aPos) {
      return tracked;
    }
  }
  MOZ_CRASH("Could not find tracked position");
}

void
FindLastHitPhase::OnRegionEnd()
{
  // Find the point of the last hit which coincides with a breakpoint.
  Maybe<TrackedPosition> lastBreakpoint;
  for (const BreakpointPosition& breakpoint : gNavigation->mBreakpoints) {
    if (!breakpoint.IsValid()) {
      continue;
    }
    const TrackedPosition& tracked = FindTrackedPosition(breakpoint);
    if (tracked.mLastHit.HasPosition() &&
        (lastBreakpoint.isNothing() ||
         lastBreakpoint.ref().mLastHitCount < tracked.mLastHitCount))
    {
      lastBreakpoint = Some(tracked);
    }
  }

  if (lastBreakpoint.isNothing()) {
    // No breakpoints were encountered in the search space.
    if (mStart.mTemporary) {
      // We started searching forwards from a temporary checkpoint.
      // Continue searching backwards without notifying the middleman.
      CheckpointId start = mStart;
      start.mTemporary--;
      ExecutionPoint end = gNavigation->LastTemporaryCheckpointLocation();
      if (end.HasPosition()) {
        gNavigation->mFindLastHitPhase.Enter(start, Some(end));
        Unreachable();
      } else {
        // The last temporary checkpoint may be at the same execution point as
        // the last normal checkpoint, if it was created while handling
        // debugger requests there.
      }
    }

    // Rewind to the last normal checkpoint and pause.
    gNavigation->mPausedPhase.Enter(ExecutionPoint(mStart.mNormal), BreakpointVector(),
                                    /* aRewind = */ true);
    Unreachable();
  }

  // When running backwards, we don't want to place temporary checkpoints at
  // the breakpoint where we are going to stop at. If the user continues
  // rewinding then we will just have to discard the checkpoint and waste the
  // work we did in saving it.
  //
  // Instead, try to place a temporary checkpoint at the last time the
  // breakpoint's script was entered. This optimizes for the case of stepping
  // around within a frame.
  Maybe<BreakpointPosition> baseEntry = GetEntryPosition(lastBreakpoint.ref().mPosition);
  if (baseEntry.isSome()) {
    const TrackedPosition& tracked = FindTrackedPosition(baseEntry.ref());
    if (tracked.mLastHit.HasPosition() &&
        tracked.mLastHitCount < lastBreakpoint.ref().mLastHitCount)
    {
      gNavigation->mReachBreakpointPhase.Enter(mStart, /* aRewind = */ true,
                                               lastBreakpoint.ref().mLastHit,
                                               Some(tracked.mLastHit));
      Unreachable();
    }
  }

  // There was no suitable place for a temporary checkpoint, so rewind to the
  // last checkpoint and play forward to the last breakpoint hit we found.
  gNavigation->mReachBreakpointPhase.Enter(mStart, /* aRewind = */ true,
                                           lastBreakpoint.ref().mLastHit, Nothing());
  Unreachable();
}

///////////////////////////////////////////////////////////////////////////////
// Hooks
///////////////////////////////////////////////////////////////////////////////

bool
IsInitialized()
{
  return !!gNavigation;
}

void
BeforeCheckpoint()
{
  if (!IsInitialized()) {
    void* navigationMem =
      AllocateMemory(sizeof(NavigationState), MemoryKind::Navigation);
    gNavigation = new (navigationMem) NavigationState();

    js::SetupDevtoolsSandbox();
  }

  AutoDisallowThreadEvents disallow;

  // Reset the debugger to a consistent state before each checkpoint.
  js::ClearPositionHandlers();
}

void
AfterCheckpoint(const CheckpointId& aCheckpoint)
{
  AutoDisallowThreadEvents disallow;

  MOZ_RELEASE_ASSERT(IsRecordingOrReplaying());
  gNavigation->AfterCheckpoint(aCheckpoint);
}

void
DebuggerRequest(js::CharBuffer* aRequestBuffer)
{
  gNavigation->HandleDebuggerRequest(aRequestBuffer);
}

void
SetBreakpoint(size_t aId, const BreakpointPosition& aPosition)
{
  gNavigation->GetBreakpoint(aId) = aPosition;
}

void
Resume(bool aForward)
{
  // For the primordial resume sent at startup, the navigation state will not
  // have been initialized yet.
  if (!gNavigation) {
    ResumeExecution();
    return;
  }
  gNavigation->Resume(aForward);
}

void
RestoreCheckpoint(size_t aId)
{
  gNavigation->RestoreCheckpoint(aId);
}

void
RunToPoint(const ExecutionPoint& aTarget)
{
  gNavigation->RunToPoint(aTarget);
}

ExecutionPoint
GetRecordingEndpoint()
{
  MOZ_RELEASE_ASSERT(IsRecording());
  return gNavigation->GetRecordingEndpoint();
}

void
SetRecordingEndpoint(size_t aIndex, const ExecutionPoint& aEndpoint)
{
  MOZ_RELEASE_ASSERT(IsReplaying());
  gNavigation->SetRecordingEndpoint(aIndex, aEndpoint);
}

static ProgressCounter gProgressCounter;

extern "C" {

MOZ_EXPORT ProgressCounter*
RecordReplayInterface_ExecutionProgressCounter()
{
  return &gProgressCounter;
}

} // extern "C"

ExecutionPoint
CurrentExecutionPoint(const BreakpointPosition& aPosition)
{
  return ExecutionPoint(gNavigation->LastCheckpoint().mNormal,
                        gProgressCounter, aPosition);
}

void
PositionHit(const BreakpointPosition& position)
{
  AutoDisallowThreadEvents disallow;
  gNavigation->PositionHit(CurrentExecutionPoint(position));
}

extern "C" {

MOZ_EXPORT ProgressCounter
RecordReplayInterface_NewTimeWarpTarget()
{
  if (AreThreadEventsDisallowed()) {
    return 0;
  }

  // NewTimeWarpTarget() must be called at consistent points between recording
  // and replaying.
  RecordReplayAssert("NewTimeWarpTarget");

  if (!gNavigation) {
    return 0;
  }

  // Advance the progress counter for each time warp target. This can be called
  // at any place and any number of times where recorded events are allowed.
  ProgressCounter progress = ++gProgressCounter;

  PositionHit(BreakpointPosition(BreakpointPosition::WarpTarget));

  // Remember the checkpoint associated with each time warp target we have
  // generated, so we can convert them to ExecutionPoints later. Ignore warp
  // targets we have already encountered.
  if (gNavigation->mTimeWarpTargetCheckpoints.empty() ||
      progress > gNavigation->mTimeWarpTargetCheckpoints.back().first)
  {
    size_t checkpoint = gNavigation->LastCheckpoint().mNormal;
    gNavigation->mTimeWarpTargetCheckpoints.emplaceBack(progress, checkpoint);
  }

  return progress;
}

} // extern "C"

ExecutionPoint
TimeWarpTargetExecutionPoint(ProgressCounter aTarget)
{
  Maybe<size_t> checkpoint;
  for (auto entry : gNavigation->mTimeWarpTargetCheckpoints) {
    if (entry.first == aTarget) {
      checkpoint.emplace(entry.second);
      break;
    }
  }
  MOZ_RELEASE_ASSERT(checkpoint.isSome());

  return ExecutionPoint(checkpoint.ref(), aTarget,
                        BreakpointPosition(BreakpointPosition::WarpTarget));
}

bool
MaybeDivergeFromRecording()
{
  return gNavigation->MaybeDivergeFromRecording();
}

} // namespace navigation
} // namespace recordreplay
} // namespace mozilla