Bug 1488808 Part 9 - Specify regions of code where recording is not interrupted and threads can diverge, r=froydnj.
authorBrian Hackett <bhackett1024@gmail.com>
Wed, 17 Oct 2018 10:04:29 -0600
changeset 490276 0ce807f9d1f679c848341e4858df243a5c127bdf
parent 490275 0130de663e9f9006da9381d89967758af9974224
child 490277 baaaaa544bb0da29752973de7ce72a7d8d5ee84a
push id247
push userfmarier@mozilla.com
push dateSat, 27 Oct 2018 01:06:44 +0000
reviewersfroydnj
bugs1488808
milestone64.0a1
Bug 1488808 Part 9 - Specify regions of code where recording is not interrupted and threads can diverge, r=froydnj.
toolkit/recordreplay/Callback.cpp
toolkit/recordreplay/File.cpp
toolkit/recordreplay/File.h
toolkit/recordreplay/Lock.cpp
toolkit/recordreplay/ProcessRecordReplay.cpp
toolkit/recordreplay/ProcessRedirect.h
toolkit/recordreplay/Thread.cpp
toolkit/recordreplay/Thread.h
toolkit/recordreplay/Trigger.cpp
--- a/toolkit/recordreplay/Callback.cpp
+++ b/toolkit/recordreplay/Callback.cpp
@@ -44,16 +44,19 @@ BeginCallback(size_t aCallbackId)
   MOZ_RELEASE_ASSERT(!AreThreadEventsDisallowed());
 
   Thread* thread = Thread::Current();
   if (thread->IsMainThread()) {
     child::EndIdleTime();
   }
   thread->SetPassThrough(false);
 
+  RecordingEventSection res(thread);
+  MOZ_RELEASE_ASSERT(res.CanAccessEvents());
+
   thread->Events().RecordOrReplayThreadEvent(ThreadEvent::ExecuteCallback);
   thread->Events().WriteScalar(aCallbackId);
 }
 
 void
 EndCallback()
 {
   MOZ_RELEASE_ASSERT(IsRecording());
@@ -68,17 +71,18 @@ EndCallback()
 }
 
 void
 SaveOrRestoreCallbackData(void** aData)
 {
   MOZ_RELEASE_ASSERT(gCallbackData);
 
   Thread* thread = Thread::Current();
-  MOZ_RELEASE_ASSERT(thread->CanAccessRecording());
+  RecordingEventSection res(thread);
+  MOZ_RELEASE_ASSERT(res.CanAccessEvents());
 
   thread->Events().RecordOrReplayThreadEvent(ThreadEvent::RestoreCallbackData);
 
   size_t index = 0;
   if (IsRecording() && *aData) {
     StaticMutexAutoLock lock(gCallbackMutex);
     index = gCallbackData->GetIndex(*aData);
   }
@@ -97,17 +101,18 @@ RemoveCallbackData(void* aData)
   StaticMutexAutoLock lock(gCallbackMutex);
   gCallbackData->Remove(aData);
 }
 
 void
 PassThroughThreadEventsAllowCallbacks(const std::function<void()>& aFn)
 {
   Thread* thread = Thread::Current();
-  MOZ_RELEASE_ASSERT(thread->CanAccessRecording());
+  RecordingEventSection res(thread);
+  MOZ_RELEASE_ASSERT(res.CanAccessEvents());
 
   if (IsRecording()) {
     if (thread->IsMainThread()) {
       child::BeginIdleTime();
     }
     thread->SetPassThrough(true);
     aFn();
     if (thread->IsMainThread()) {
--- a/toolkit/recordreplay/File.cpp
+++ b/toolkit/recordreplay/File.cpp
@@ -42,23 +42,17 @@ Stream::ReadBytes(void* aData, size_t aS
     totalRead += bufRead;
     aSize -= bufRead;
 
     if (!aSize) {
       return;
     }
 
     MOZ_RELEASE_ASSERT(mBufferPos == mBufferLength);
-
-    // If we try to read off the end of a stream then we must have hit the end
-    // of the replay for this thread.
-    while (mChunkIndex == mChunks.length()) {
-      MOZ_RELEASE_ASSERT(mName == StreamName::Event);
-      HitEndOfRecording();
-    }
+    MOZ_RELEASE_ASSERT(mChunkIndex < mChunks.length());
 
     const StreamChunkLocation& chunk = mChunks[mChunkIndex++];
 
     EnsureMemory(&mBallast, &mBallastSize, chunk.mCompressedSize, BallastMaxSize(),
                  DontCopyExistingData);
     mFile->ReadChunk(mBallast.get(), chunk);
 
     EnsureMemory(&mBuffer, &mBufferSize, chunk.mDecompressedSize, BUFFER_MAX,
@@ -84,16 +78,17 @@ Stream::AtEnd()
 
   return mBufferPos == mBufferLength && mChunkIndex == mChunks.length();
 }
 
 void
 Stream::WriteBytes(const void* aData, size_t aSize)
 {
   MOZ_RELEASE_ASSERT(mFile->OpenForWriting());
+  MOZ_RELEASE_ASSERT(mName != StreamName::Event || mInRecordingEventSection);
 
   // Prevent the entire file from being flushed while we write this data.
   AutoReadSpinLock streamLock(mFile->mStreamLock);
 
   while (true) {
     // Fill up the data buffer first.
     MOZ_RELEASE_ASSERT(mBufferPos <= mBufferSize);
     size_t bufAvailable = mBufferSize - mBufferPos;
--- a/toolkit/recordreplay/File.h
+++ b/toolkit/recordreplay/File.h
@@ -55,20 +55,22 @@ enum class StreamName
 {
   Main,
   Lock,
   Event,
   Count
 };
 
 class File;
+class RecordingEventSection;
 
 class Stream
 {
   friend class File;
+  friend class RecordingEventSection;
 
   // File this stream belongs to.
   File* mFile;
 
   // Prefix name for this stream.
   StreamName mName;
 
   // Index which, when combined to mName, uniquely identifies this stream in
@@ -112,32 +114,36 @@ class Stream
   // The number of chunks that have been completely read or written. When
   // writing, this equals mChunks.length().
   size_t mChunkIndex;
 
   // When writing, the number of chunks in this stream when the file was last
   // flushed.
   size_t mFlushedChunks;
 
+  // Whether there is a RecordingEventSection instance active for this stream.
+  bool mInRecordingEventSection;
+
   Stream(File* aFile, StreamName aName, size_t aNameIndex)
     : mFile(aFile)
     , mName(aName)
     , mNameIndex(aNameIndex)
     , mBuffer(nullptr)
     , mBufferSize(0)
     , mBufferLength(0)
     , mBufferPos(0)
     , mStreamPos(0)
     , mBallast(nullptr)
     , mBallastSize(0)
     , mInputBallast(nullptr)
     , mInputBallastSize(0)
     , mLastEvent((ThreadEvent) 0)
     , mChunkIndex(0)
     , mFlushedChunks(0)
+    , mInRecordingEventSection(false)
   {}
 
 public:
   StreamName Name() const { return mName; }
   size_t NameIndex() const { return mNameIndex; }
 
   void ReadBytes(void* aData, size_t aSize);
   void WriteBytes(const void* aData, size_t aSize);
@@ -199,16 +205,17 @@ class File
 {
 public:
   enum Mode {
     WRITE,
     READ
   };
 
   friend class Stream;
+  friend class RecordingEventSection;
 
 private:
   // Open file handle, or 0 if closed.
   FileHandle mFd;
 
   // Whether this file is open for writing or reading.
   Mode mMode;
 
--- a/toolkit/recordreplay/Lock.cpp
+++ b/toolkit/recordreplay/Lock.cpp
@@ -1,18 +1,16 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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 "Lock.h"
 
-#include "mozilla/StaticMutex.h"
-
 #include "ChunkAllocator.h"
 #include "InfallibleVector.h"
 #include "SpinLock.h"
 #include "Thread.h"
 
 #include <unordered_map>
 
 namespace mozilla {
@@ -64,23 +62,22 @@ static ChunkAllocator<LockAcquires> gLoc
 typedef std::unordered_map<void*, Lock*> LockMap;
 static LockMap* gLocks;
 static ReadWriteSpinLock gLocksLock;
 
 /* static */ void
 Lock::New(void* aNativeLock)
 {
   Thread* thread = Thread::Current();
-  if (!thread || thread->PassThroughEvents() || HasDivergedFromRecording()) {
+  RecordingEventSection res(thread);
+  if (!res.CanAccessEvents()) {
     Destroy(aNativeLock); // Clean up any old lock, as below.
     return;
   }
 
-  MOZ_RELEASE_ASSERT(thread->CanAccessRecording());
-
   thread->Events().RecordOrReplayThreadEvent(ThreadEvent::CreateLock);
 
   size_t id;
   if (IsRecording()) {
     id = gNumLocks++;
   }
   thread->Events().RecordOrReplayScalar(&id);
 
@@ -147,55 +144,59 @@ Lock::Find(void* aNativeLock)
 
   return nullptr;
 }
 
 void
 Lock::Enter()
 {
   Thread* thread = Thread::Current();
-  MOZ_RELEASE_ASSERT(thread->CanAccessRecording());
+
+  RecordingEventSection res(thread);
+  if (!res.CanAccessEvents()) {
+    return;
+  }
 
   // Include an event in each thread's record when a lock acquire begins. This
   // is not required by the replay but is used to check that lock acquire order
   // is consistent with the recording and that we will fail explicitly instead
   // of deadlocking.
   thread->Events().RecordOrReplayThreadEvent(ThreadEvent::Lock);
   thread->Events().CheckInput(mId);
 
   LockAcquires* acquires = gLockAcquires.Get(mId);
   if (IsRecording()) {
     acquires->mAcquires->WriteScalar(thread->Id());
   } else {
-    // Wait until this thread is next in line to acquire the lock.
-    while (thread->Id() != acquires->mNextOwner) {
+    // Wait until this thread is next in line to acquire the lock, or until it
+    // has been instructed to diverge from the recording.
+    while (thread->Id() != acquires->mNextOwner && !thread->MaybeDivergeFromRecording()) {
       Thread::Wait();
     }
   }
 }
 
 void
 Lock::Exit()
 {
-  if (IsReplaying()) {
+  Thread* thread = Thread::Current();
+  if (IsReplaying() && !thread->HasDivergedFromRecording()) {
     // Notify the next owner before releasing the lock.
     LockAcquires* acquires = gLockAcquires.Get(mId);
-    acquires->ReadAndNotifyNextOwner(Thread::Current());
+    acquires->ReadAndNotifyNextOwner(thread);
   }
 }
 
 struct AtomicLock : public detail::MutexImpl
 {
   using detail::MutexImpl::lock;
   using detail::MutexImpl::unlock;
 };
 
-// Lock which is held during code sections that run atomically. This is a
-// PRLock instead of an OffTheBooksMutex because the latter performs atomic
-// operations during initialization.
+// Lock which is held during code sections that run atomically.
 static AtomicLock* gAtomicLock = nullptr;
 
 /* static */ void
 Lock::InitializeLocks()
 {
   gNumLocks = gAtomicLockId;
   gAtomicLock = new AtomicLock();
 
--- a/toolkit/recordreplay/ProcessRecordReplay.cpp
+++ b/toolkit/recordreplay/ProcessRecordReplay.cpp
@@ -161,37 +161,35 @@ RecordReplayInterface_Initialize(int aAr
 
   gInitialized = true;
 }
 
 MOZ_EXPORT size_t
 RecordReplayInterface_InternalRecordReplayValue(size_t aValue)
 {
   Thread* thread = Thread::Current();
-  if (thread->PassThroughEvents()) {
+  RecordingEventSection res(thread);
+  if (!res.CanAccessEvents()) {
     return aValue;
   }
-  EnsureNotDivergedFromRecording();
 
-  MOZ_RELEASE_ASSERT(thread->CanAccessRecording());
   thread->Events().RecordOrReplayThreadEvent(ThreadEvent::Value);
   thread->Events().RecordOrReplayValue(&aValue);
   return aValue;
 }
 
 MOZ_EXPORT void
 RecordReplayInterface_InternalRecordReplayBytes(void* aData, size_t aSize)
 {
   Thread* thread = Thread::Current();
-  if (thread->PassThroughEvents()) {
+  RecordingEventSection res(thread);
+  if (!res.CanAccessEvents()) {
     return;
   }
-  EnsureNotDivergedFromRecording();
 
-  MOZ_RELEASE_ASSERT(thread->CanAccessRecording());
   thread->Events().RecordOrReplayThreadEvent(ThreadEvent::Bytes);
   thread->Events().CheckInput(aSize);
   thread->Events().RecordOrReplayBytes(aData, aSize);
 }
 
 MOZ_EXPORT void
 RecordReplayInterface_InternalInvalidateRecording(const char* aWhy)
 {
@@ -342,37 +340,37 @@ GetRecordingPid()
 ///////////////////////////////////////////////////////////////////////////////
 
 extern "C" {
 
 MOZ_EXPORT void
 RecordReplayInterface_InternalRecordReplayAssert(const char* aFormat, va_list aArgs)
 {
   Thread* thread = Thread::Current();
-  if (thread->PassThroughEvents() || thread->HasDivergedFromRecording()) {
+  RecordingEventSection res(thread);
+  if (!res.CanAccessEvents()) {
     return;
   }
-  MOZ_RELEASE_ASSERT(thread->CanAccessRecording());
 
   // Add the asserted string to the recording.
   char text[1024];
   VsprintfLiteral(text, aFormat, aArgs);
 
   thread->Events().RecordOrReplayThreadEvent(ThreadEvent::Assert);
   thread->Events().CheckInput(text);
 }
 
 MOZ_EXPORT void
 RecordReplayInterface_InternalRecordReplayAssertBytes(const void* aData, size_t aSize)
 {
   Thread* thread = Thread::Current();
-  if (thread->PassThroughEvents() || thread->HasDivergedFromRecording()) {
+  RecordingEventSection res(thread);
+  if (!res.CanAccessEvents()) {
     return;
   }
-  MOZ_RELEASE_ASSERT(thread->CanAccessRecording());
 
   thread->Events().RecordOrReplayThreadEvent(ThreadEvent::AssertBytes);
   thread->Events().CheckInput(aData, aSize);
 }
 
 } // extern "C"
 
 static ValueIndex* gGenericThings;
--- a/toolkit/recordreplay/ProcessRedirect.h
+++ b/toolkit/recordreplay/ProcessRedirect.h
@@ -521,25 +521,16 @@ RR_WriteBufferViaRval(Stream& aEvents, C
   auto& count = aArguments->Arg<CountArg, size_t>();
   aEvents.CheckInput(count);
 
   auto& rval = aArguments->Rval<size_t>();
   MOZ_RELEASE_ASSERT(rval + Offset <= count);
   aEvents.RecordOrReplayBytes(buf, rval + Offset);
 }
 
-// Insert an atomic access while recording/replaying so that calls to this
-// function replay in the same order they occurred in while recording. This is
-// used for functions that are used in inter-thread synchronization.
-static inline void
-RR_OrderCall(Stream& aEvents, CallArguments* aArguments, ErrorType* aError)
-{
-  AutoOrderedAtomicAccess();
-}
-
 // Record/replay a scalar return value.
 static inline void
 RR_ScalarRval(Stream& aEvents, CallArguments* aArguments, ErrorType* aError)
 {
   aEvents.RecordOrReplayValue(&aArguments->Rval<size_t>());
 }
 
 // Record/replay a complex scalar return value that fits in two registers.
--- a/toolkit/recordreplay/Thread.cpp
+++ b/toolkit/recordreplay/Thread.cpp
@@ -229,20 +229,22 @@ Thread::SpawnThread(Thread* aThread)
 {
   DirectSpawnThread(ThreadMain, aThread);
   WaitUntilInitialized(aThread);
 }
 
 /* static */ NativeThreadId
 Thread::StartThread(Callback aStart, void* aArgument, bool aNeedsJoin)
 {
-  EnsureNotDivergedFromRecording();
-
   Thread* thread = Thread::Current();
-  MOZ_RELEASE_ASSERT(thread->CanAccessRecording());
+  RecordingEventSection res(thread);
+  if (!res.CanAccessEvents()) {
+    EnsureNotDivergedFromRecording();
+    Unreachable();
+  }
 
   MonitorAutoLock lock(*gMonitor);
 
   size_t id = 0;
   if (IsRecording()) {
     // Look for an idle thread.
     for (id = MainThreadId + 1; id <= MaxRecordedThreadId; id++) {
       Thread* targetThread = Thread::GetById(id);
--- a/toolkit/recordreplay/Thread.h
+++ b/toolkit/recordreplay/Thread.h
@@ -93,16 +93,20 @@ private:
   // Whether to crash if we try to record/replay thread events. This is only
   // used by the associated thread.
   size_t mDisallowEvents;
 
   // Whether execution has diverged from the recording and the thread's
   // recorded events cannot be accessed.
   bool mDivergedFromRecording;
 
+  // Whether this thread should diverge from the recording at the next
+  // opportunity. This can be set from any thread.
+  Atomic<bool, SequentiallyConsistent, Behavior::DontPreserve> mShouldDivergeFromRecording;
+
   // Start routine and argument which the thread is currently executing. This
   // is cleared after the routine finishes and another start routine may be
   // assigned to the thread. mNeedsJoin specifies whether the thread must be
   // joined before it is completely dead and can be reused. This is protected
   // by the thread monitor.
   Callback mStart;
   void* mStartArg;
   bool mNeedsJoin;
@@ -174,16 +178,36 @@ public:
   // flag is clear.
   void DivergeFromRecording() {
     mDivergedFromRecording = true;
   }
   bool HasDivergedFromRecording() const {
     return mDivergedFromRecording;
   }
 
+  // Mark this thread as needing to diverge from the recording soon, and wake
+  // it up in case it can make progress now. The mShouldDivergeFromRecording
+  // flag is separate from mDivergedFromRecording so that the thread can only
+  // begin diverging from the recording at calls to MaybeDivergeFromRecording.
+  void SetShouldDivergeFromRecording() {
+    MOZ_RELEASE_ASSERT(CurrentIsMainThread());
+    mShouldDivergeFromRecording = true;
+    Notify(mId);
+  }
+  bool WillDivergeFromRecordingSoon() {
+    MOZ_RELEASE_ASSERT(CurrentIsMainThread());
+    return mShouldDivergeFromRecording;
+  }
+  bool MaybeDivergeFromRecording() {
+    if (mShouldDivergeFromRecording) {
+      mDivergedFromRecording = true;
+    }
+    return mDivergedFromRecording;
+  }
+
   // Return whether this thread may read or write to its recorded event stream.
   bool CanAccessRecording() const {
     return !PassThroughEvents() && !AreEventsDisallowed() && !HasDivergedFromRecording();
   }
 
   // The actual start routine at the root of all recorded threads, and of all
   // threads when replaying.
   static void ThreadMain(void* aArgument);
@@ -285,12 +309,66 @@ public:
   ~AutoEnsurePassThroughThreadEventsUseStackPointer()
   {
     if (!mPassedThrough) {
       mThread->SetPassThrough(false);
     }
   }
 };
 
+// Mark a region of code where a thread's event stream can be accessed.
+// This class has several properties:
+//
+// - When recording, all writes to the thread's event stream occur atomically
+//   within the class: the end of the stream cannot be hit at an intermediate
+//   point.
+//
+// - When replaying, this checks for the end of the stream, and blocks the
+//   thread if necessary.
+//
+// - When replaying, this is a point where the thread can begin diverging from
+//   the recording. Checks for divergence should occur after the constructor
+//   finishes.
+class MOZ_RAII RecordingEventSection
+{
+  Thread* mThread;
+
+public:
+  explicit RecordingEventSection(Thread* aThread)
+    : mThread(aThread)
+  {
+    if (!aThread || !aThread->CanAccessRecording()) {
+      return;
+    }
+    if (IsRecording()) {
+      MOZ_RELEASE_ASSERT(!aThread->Events().mInRecordingEventSection);
+      aThread->Events().mFile->mStreamLock.ReadLock();
+      aThread->Events().mInRecordingEventSection = true;
+    } else {
+      while (!aThread->MaybeDivergeFromRecording() && aThread->Events().AtEnd()) {
+        HitEndOfRecording();
+      }
+    }
+  }
+
+  ~RecordingEventSection() {
+    if (!mThread || !mThread->CanAccessRecording()) {
+      return;
+    }
+    if (IsRecording()) {
+      mThread->Events().mFile->mStreamLock.ReadUnlock();
+      mThread->Events().mInRecordingEventSection = false;
+    }
+  }
+
+  bool CanAccessEvents() {
+    if (!mThread || mThread->PassThroughEvents() || mThread->HasDivergedFromRecording()) {
+      return false;
+    }
+    MOZ_RELEASE_ASSERT(mThread->CanAccessRecording());
+    return true;
+  }
+};
+
 } // namespace recordreplay
 } // namespace mozilla
 
 #endif // mozilla_recordreplay_Thread_h
--- a/toolkit/recordreplay/Trigger.cpp
+++ b/toolkit/recordreplay/Trigger.cpp
@@ -54,16 +54,17 @@ InitializeTriggers()
 }
 
 extern "C" {
 
 MOZ_EXPORT void
 RecordReplayInterface_RegisterTrigger(void* aObj, const std::function<void()>& aCallback)
 {
   MOZ_RELEASE_ASSERT(aObj);
+  MOZ_RELEASE_ASSERT(!AreThreadEventsPassedThrough());
 
   Thread* thread = Thread::Current();
   if (thread->HasDivergedFromRecording()) {
     return;
   }
   MOZ_RELEASE_ASSERT(thread->CanAccessRecording());
 
   size_t id;
@@ -79,16 +80,19 @@ RecordReplayInterface_RegisterTrigger(vo
       iter->second.mRegisterCount++;
     } else {
       id = gTriggers->Insert(aObj);
       TriggerInfo info(thread->Id(), aCallback);
       gTriggerInfoMap->insert(TriggerInfoMap::value_type(aObj, info));
     }
   }
 
+  RecordingEventSection res(thread);
+  MOZ_RELEASE_ASSERT(res.CanAccessEvents());
+
   thread->Events().RecordOrReplayThreadEvent(ThreadEvent::RegisterTrigger);
   thread->Events().CheckInput(id);
 }
 
 MOZ_EXPORT void
 RecordReplayInterface_UnregisterTrigger(void* aObj)
 {
   MOZ_ASSERT(IsRecordingOrReplaying());
@@ -152,17 +156,20 @@ RemoveTriggerCallbackForThreadId(size_t 
   }
   return Nothing();
 }
 
 MOZ_EXPORT void
 RecordReplayInterface_ExecuteTriggers()
 {
   Thread* thread = Thread::Current();
-  MOZ_RELEASE_ASSERT(thread->CanAccessRecording());
+  RecordingEventSection res(thread);
+  if (!res.CanAccessEvents()) {
+    return;
+  }
 
   if (IsRecording()) {
     // Invoke the callbacks for any triggers waiting for execution, including
     // any whose callbacks are triggered by earlier callback invocations.
     while (true) {
       Maybe<size_t> id = RemoveTriggerCallbackForThreadId(thread->Id());
       if (id.isNothing()) {
         break;