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 500498 0ce807f9d1f679c848341e4858df243a5c127bdf
parent 500497 0130de663e9f9006da9381d89967758af9974224
child 500499 baaaaa544bb0da29752973de7ce72a7d8d5ee84a
push id1864
push userffxbld-merge
push dateMon, 03 Dec 2018 15:51:40 +0000
treeherdermozilla-release@f040763d99ad [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfroydnj
bugs1488808
milestone64.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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;