diff options
Diffstat (limited to '')
-rw-r--r-- | xpcom/threads/BlockingResourceBase.cpp | 547 |
1 files changed, 547 insertions, 0 deletions
diff --git a/xpcom/threads/BlockingResourceBase.cpp b/xpcom/threads/BlockingResourceBase.cpp new file mode 100644 index 0000000000..c2ba82e07a --- /dev/null +++ b/xpcom/threads/BlockingResourceBase.cpp @@ -0,0 +1,547 @@ +/* -*- 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 "mozilla/BlockingResourceBase.h" + +#ifdef DEBUG +# include "prthread.h" + +# ifndef MOZ_CALLSTACK_DISABLED +# include "CodeAddressService.h" +# include "nsHashKeys.h" +# include "mozilla/StackWalk.h" +# include "nsTHashtable.h" +# endif + +# include "mozilla/Attributes.h" +# include "mozilla/CondVar.h" +# include "mozilla/DeadlockDetector.h" +# include "mozilla/RecursiveMutex.h" +# include "mozilla/ReentrantMonitor.h" +# include "mozilla/Mutex.h" +# include "mozilla/RWLock.h" +# include "mozilla/UniquePtr.h" + +# if defined(MOZILLA_INTERNAL_API) +# include "mozilla/ProfilerThreadSleep.h" +# endif // MOZILLA_INTERNAL_API + +#endif // ifdef DEBUG + +namespace mozilla { +// +// BlockingResourceBase implementation +// + +// static members +const char* const BlockingResourceBase::kResourceTypeName[] = { + // needs to be kept in sync with BlockingResourceType + "Mutex", "ReentrantMonitor", "CondVar", "RecursiveMutex"}; + +#ifdef DEBUG + +PRCallOnceType BlockingResourceBase::sCallOnce; +MOZ_THREAD_LOCAL(BlockingResourceBase*) +BlockingResourceBase::sResourceAcqnChainFront; +BlockingResourceBase::DDT* BlockingResourceBase::sDeadlockDetector; + +void BlockingResourceBase::StackWalkCallback(uint32_t aFrameNumber, void* aPc, + void* aSp, void* aClosure) { +# ifndef MOZ_CALLSTACK_DISABLED + AcquisitionState* state = (AcquisitionState*)aClosure; + state->ref().AppendElement(aPc); +# endif +} + +void BlockingResourceBase::GetStackTrace(AcquisitionState& aState, + const void* aFirstFramePC) { +# ifndef MOZ_CALLSTACK_DISABLED + // Clear the array... + aState.reset(); + // ...and create a new one; this also puts the state to 'acquired' status + // regardless of whether we obtain a stack trace or not. + aState.emplace(); + + MozStackWalk(StackWalkCallback, aFirstFramePC, kAcquisitionStateStackSize, + aState.ptr()); +# endif +} + +/** + * PrintCycle + * Append to |aOut| detailed information about the circular + * dependency in |aCycle|. Returns true if it *appears* that this + * cycle may represent an imminent deadlock, but this is merely a + * heuristic; the value returned may be a false positive or false + * negative. + * + * *NOT* thread safe. Calls |Print()|. + * + * FIXME bug 456272 hack alert: because we can't write call + * contexts into strings, all info is written to stderr, but only + * some info is written into |aOut| + */ +static bool PrintCycle( + const BlockingResourceBase::DDT::ResourceAcquisitionArray& aCycle, + nsACString& aOut) { + NS_ASSERTION(aCycle.Length() > 1, "need > 1 element for cycle!"); + + bool maybeImminent = true; + + fputs("=== Cyclical dependency starts at\n", stderr); + aOut += "Cyclical dependency starts at\n"; + + const BlockingResourceBase::DDT::ResourceAcquisitionArray::value_type res = + aCycle.ElementAt(0); + maybeImminent &= res->Print(aOut); + + BlockingResourceBase::DDT::ResourceAcquisitionArray::index_type i; + BlockingResourceBase::DDT::ResourceAcquisitionArray::size_type len = + aCycle.Length(); + const BlockingResourceBase::DDT::ResourceAcquisitionArray::value_type* it = + 1 + aCycle.Elements(); + for (i = 1; i < len - 1; ++i, ++it) { + fputs("\n--- Next dependency:\n", stderr); + aOut += "\nNext dependency:\n"; + + maybeImminent &= (*it)->Print(aOut); + } + + fputs("\n=== Cycle completed at\n", stderr); + aOut += "Cycle completed at\n"; + (*it)->Print(aOut); + + return maybeImminent; +} + +bool BlockingResourceBase::Print(nsACString& aOut) const { + fprintf(stderr, "--- %s : %s", kResourceTypeName[mType], mName); + aOut += BlockingResourceBase::kResourceTypeName[mType]; + aOut += " : "; + aOut += mName; + + bool acquired = IsAcquired(); + + if (acquired) { + fputs(" (currently acquired)\n", stderr); + aOut += " (currently acquired)\n"; + } + + fputs(" calling context\n", stderr); +# ifdef MOZ_CALLSTACK_DISABLED + fputs(" [stack trace unavailable]\n", stderr); +# else + const AcquisitionState& state = acquired ? mAcquired : mFirstSeen; + + CodeAddressService<> addressService; + + for (uint32_t i = 0; i < state.ref().Length(); i++) { + const size_t kMaxLength = 1024; + char buffer[kMaxLength]; + addressService.GetLocation(i + 1, state.ref()[i], buffer, kMaxLength); + const char* fmt = " %s\n"; + aOut.AppendLiteral(" "); + aOut.Append(buffer); + aOut.AppendLiteral("\n"); + fprintf(stderr, fmt, buffer); + } + +# endif + + return acquired; +} + +BlockingResourceBase::BlockingResourceBase( + const char* aName, BlockingResourceBase::BlockingResourceType aType) + : mName(aName), + mType(aType) +# ifdef MOZ_CALLSTACK_DISABLED + , + mAcquired(false) +# else + , + mAcquired() +# endif +{ + MOZ_ASSERT(mName, "Name must be nonnull"); + // PR_CallOnce guaranatees that InitStatics is called in a + // thread-safe way + if (PR_SUCCESS != PR_CallOnce(&sCallOnce, InitStatics)) { + MOZ_CRASH("can't initialize blocking resource static members"); + } + + mChainPrev = 0; + sDeadlockDetector->Add(this); +} + +BlockingResourceBase::~BlockingResourceBase() { + // we don't check for really obviously bad things like freeing + // Mutexes while they're still locked. it is assumed that the + // base class, or its underlying primitive, will check for such + // stupid mistakes. + mChainPrev = 0; // racy only for stupidly buggy client code + if (sDeadlockDetector) { + sDeadlockDetector->Remove(this); + } +} + +size_t BlockingResourceBase::SizeOfDeadlockDetector( + MallocSizeOf aMallocSizeOf) { + return sDeadlockDetector + ? sDeadlockDetector->SizeOfIncludingThis(aMallocSizeOf) + : 0; +} + +PRStatus BlockingResourceBase::InitStatics() { + MOZ_ASSERT(sResourceAcqnChainFront.init()); + sDeadlockDetector = new DDT(); + if (!sDeadlockDetector) { + MOZ_CRASH("can't allocate deadlock detector"); + } + return PR_SUCCESS; +} + +void BlockingResourceBase::Shutdown() { + delete sDeadlockDetector; + sDeadlockDetector = 0; +} + +MOZ_NEVER_INLINE void BlockingResourceBase::CheckAcquire() { + if (mType == eCondVar) { + MOZ_ASSERT_UNREACHABLE( + "FIXME bug 456272: annots. to allow CheckAcquire()ing condvars"); + return; + } + + BlockingResourceBase* chainFront = ResourceChainFront(); + mozilla::UniquePtr<DDT::ResourceAcquisitionArray> cycle( + sDeadlockDetector->CheckAcquisition(chainFront ? chainFront : 0, this)); + if (!cycle) { + return; + } + +# ifndef MOZ_CALLSTACK_DISABLED + // Update the current stack before printing. + GetStackTrace(mAcquired, CallerPC()); +# endif + + fputs("###!!! ERROR: Potential deadlock detected:\n", stderr); + nsAutoCString out("Potential deadlock detected:\n"); + bool maybeImminent = PrintCycle(*cycle, out); + + if (maybeImminent) { + fputs("\n###!!! Deadlock may happen NOW!\n\n", stderr); + out.AppendLiteral("\n###!!! Deadlock may happen NOW!\n\n"); + } else { + fputs("\nDeadlock may happen for some other execution\n\n", stderr); + out.AppendLiteral("\nDeadlock may happen for some other execution\n\n"); + } + + // Only error out if we think a deadlock is imminent. + if (maybeImminent) { + NS_ERROR(out.get()); + } else { + NS_WARNING(out.get()); + } +} + +MOZ_NEVER_INLINE void BlockingResourceBase::Acquire() { + if (mType == eCondVar) { + MOZ_ASSERT_UNREACHABLE( + "FIXME bug 456272: annots. to allow Acquire()ing condvars"); + return; + } + NS_ASSERTION(!IsAcquired(), "reacquiring already acquired resource"); + + ResourceChainAppend(ResourceChainFront()); + +# ifdef MOZ_CALLSTACK_DISABLED + mAcquired = true; +# else + // Take a stack snapshot. + GetStackTrace(mAcquired, CallerPC()); + MOZ_ASSERT(IsAcquired()); + + if (!mFirstSeen) { + mFirstSeen = mAcquired.map( + [](AcquisitionState::ValueType& state) { return state.Clone(); }); + } +# endif +} + +void BlockingResourceBase::Release() { + if (mType == eCondVar) { + MOZ_ASSERT_UNREACHABLE( + "FIXME bug 456272: annots. to allow Release()ing condvars"); + return; + } + + BlockingResourceBase* chainFront = ResourceChainFront(); + NS_ASSERTION(chainFront && IsAcquired(), + "Release()ing something that hasn't been Acquire()ed"); + + if (chainFront == this) { + ResourceChainRemove(); + } else { + // remove this resource from wherever it lives in the chain + // we walk backwards in order of acquisition: + // (1) ...node<-prev<-curr... + // / / + // (2) ...prev<-curr... + BlockingResourceBase* curr = chainFront; + BlockingResourceBase* prev = nullptr; + while (curr && (prev = curr->mChainPrev) && (prev != this)) { + curr = prev; + } + if (prev == this) { + curr->mChainPrev = prev->mChainPrev; + } + } + + ClearAcquisitionState(); +} + +// +// Debug implementation of (OffTheBooks)Mutex +void OffTheBooksMutex::Lock() { + CheckAcquire(); + this->lock(); + mOwningThread = PR_GetCurrentThread(); + Acquire(); +} + +bool OffTheBooksMutex::TryLock() { + bool locked = this->tryLock(); + if (locked) { + mOwningThread = PR_GetCurrentThread(); + Acquire(); + } + return locked; +} + +void OffTheBooksMutex::Unlock() { + Release(); + mOwningThread = nullptr; + this->unlock(); +} + +void OffTheBooksMutex::AssertCurrentThreadOwns() const { + MOZ_ASSERT(IsAcquired() && mOwningThread == PR_GetCurrentThread()); +} + +// +// Debug implementation of RWLock +// + +bool RWLock::TryReadLock() { + bool locked = this->detail::RWLockImpl::tryReadLock(); + MOZ_ASSERT_IF(locked, mOwningThread == nullptr); + return locked; +} + +void RWLock::ReadLock() { + // All we want to ensure here is that we're not attempting to acquire the + // read lock while this thread is holding the write lock. + CheckAcquire(); + this->detail::RWLockImpl::readLock(); + MOZ_ASSERT(mOwningThread == nullptr); +} + +void RWLock::ReadUnlock() { + MOZ_ASSERT(mOwningThread == nullptr); + this->detail::RWLockImpl::readUnlock(); +} + +bool RWLock::TryWriteLock() { + bool locked = this->detail::RWLockImpl::tryWriteLock(); + if (locked) { + mOwningThread = PR_GetCurrentThread(); + Acquire(); + } + return locked; +} + +void RWLock::WriteLock() { + CheckAcquire(); + this->detail::RWLockImpl::writeLock(); + mOwningThread = PR_GetCurrentThread(); + Acquire(); +} + +void RWLock::WriteUnlock() { + Release(); + mOwningThread = nullptr; + this->detail::RWLockImpl::writeUnlock(); +} + +// +// Debug implementation of ReentrantMonitor +void ReentrantMonitor::Enter() { + BlockingResourceBase* chainFront = ResourceChainFront(); + + // the code below implements monitor reentrancy semantics + + if (this == chainFront) { + // immediately re-entered the monitor: acceptable + PR_EnterMonitor(mReentrantMonitor); + ++mEntryCount; + return; + } + + // this is sort of a hack around not recording the thread that + // owns this monitor + if (chainFront) { + for (BlockingResourceBase* br = ResourceChainPrev(chainFront); br; + br = ResourceChainPrev(br)) { + if (br == this) { + NS_WARNING( + "Re-entering ReentrantMonitor after acquiring other resources."); + + // show the caller why this is potentially bad + CheckAcquire(); + + PR_EnterMonitor(mReentrantMonitor); + ++mEntryCount; + return; + } + } + } + + CheckAcquire(); + PR_EnterMonitor(mReentrantMonitor); + NS_ASSERTION(mEntryCount == 0, "ReentrantMonitor isn't free!"); + Acquire(); // protected by mReentrantMonitor + mEntryCount = 1; +} + +void ReentrantMonitor::Exit() { + if (--mEntryCount == 0) { + Release(); // protected by mReentrantMonitor + } + PRStatus status = PR_ExitMonitor(mReentrantMonitor); + NS_ASSERTION(PR_SUCCESS == status, "bad ReentrantMonitor::Exit()"); +} + +nsresult ReentrantMonitor::Wait(PRIntervalTime aInterval) { + AssertCurrentThreadIn(); + + // save monitor state and reset it to empty + int32_t savedEntryCount = mEntryCount; + AcquisitionState savedAcquisitionState = TakeAcquisitionState(); + BlockingResourceBase* savedChainPrev = mChainPrev; + mEntryCount = 0; + mChainPrev = 0; + + nsresult rv; + { +# if defined(MOZILLA_INTERNAL_API) + AUTO_PROFILER_THREAD_SLEEP; +# endif + // give up the monitor until we're back from Wait() + rv = PR_Wait(mReentrantMonitor, aInterval) == PR_SUCCESS ? NS_OK + : NS_ERROR_FAILURE; + } + + // restore saved state + mEntryCount = savedEntryCount; + SetAcquisitionState(std::move(savedAcquisitionState)); + mChainPrev = savedChainPrev; + + return rv; +} + +// +// Debug implementation of RecursiveMutex +void RecursiveMutex::Lock() { + BlockingResourceBase* chainFront = ResourceChainFront(); + + // the code below implements mutex reentrancy semantics + + if (this == chainFront) { + // immediately re-entered the mutex: acceptable + LockInternal(); + ++mEntryCount; + return; + } + + // this is sort of a hack around not recording the thread that + // owns this monitor + if (chainFront) { + for (BlockingResourceBase* br = ResourceChainPrev(chainFront); br; + br = ResourceChainPrev(br)) { + if (br == this) { + NS_WARNING( + "Re-entering RecursiveMutex after acquiring other resources."); + + // show the caller why this is potentially bad + CheckAcquire(); + + LockInternal(); + ++mEntryCount; + return; + } + } + } + + CheckAcquire(); + LockInternal(); + NS_ASSERTION(mEntryCount == 0, "RecursiveMutex isn't free!"); + Acquire(); // protected by us + mOwningThread = PR_GetCurrentThread(); + mEntryCount = 1; +} + +void RecursiveMutex::Unlock() { + if (--mEntryCount == 0) { + Release(); // protected by us + mOwningThread = nullptr; + } + UnlockInternal(); +} + +void RecursiveMutex::AssertCurrentThreadIn() const { + MOZ_ASSERT(IsAcquired() && mOwningThread == PR_GetCurrentThread()); +} + +// +// Debug implementation of CondVar +void OffTheBooksCondVar::Wait() { + // Forward to the timed version of OffTheBooksCondVar::Wait to avoid code + // duplication. + CVStatus status = Wait(TimeDuration::Forever()); + MOZ_ASSERT(status == CVStatus::NoTimeout); +} + +CVStatus OffTheBooksCondVar::Wait(TimeDuration aDuration) { + AssertCurrentThreadOwnsMutex(); + + // save mutex state and reset to empty + AcquisitionState savedAcquisitionState = mLock->TakeAcquisitionState(); + BlockingResourceBase* savedChainPrev = mLock->mChainPrev; + PRThread* savedOwningThread = mLock->mOwningThread; + mLock->mChainPrev = 0; + mLock->mOwningThread = nullptr; + + // give up mutex until we're back from Wait() + CVStatus status; + { +# if defined(MOZILLA_INTERNAL_API) + AUTO_PROFILER_THREAD_SLEEP; +# endif + status = mImpl.wait_for(*mLock, aDuration); + } + + // restore saved state + mLock->SetAcquisitionState(std::move(savedAcquisitionState)); + mLock->mChainPrev = savedChainPrev; + mLock->mOwningThread = savedOwningThread; + + return status; +} + +#endif // ifdef DEBUG + +} // namespace mozilla |