michael@0: /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- michael@0: * vim: sw=4 ts=4 et : michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: #ifndef mozilla_DeadlockDetector_h michael@0: #define mozilla_DeadlockDetector_h michael@0: michael@0: #include "mozilla/Attributes.h" michael@0: michael@0: #include michael@0: michael@0: #include "plhash.h" michael@0: #include "prlock.h" michael@0: michael@0: #include "nsTArray.h" michael@0: michael@0: #ifdef NS_TRACE_MALLOC michael@0: # include "nsTraceMalloc.h" michael@0: #endif // ifdef NS_TRACE_MALLOC michael@0: michael@0: namespace mozilla { michael@0: michael@0: michael@0: // FIXME bug 456272: split this off into a convenience API on top of michael@0: // nsStackWalk? michael@0: class NS_COM_GLUE CallStack michael@0: { michael@0: private: michael@0: #ifdef NS_TRACE_MALLOC michael@0: typedef nsTMStackTraceID callstack_id; michael@0: // needs to be a macro to avoid disturbing the backtrace michael@0: # define NS_GET_BACKTRACE() NS_TraceMallocGetStackTrace() michael@0: # define NS_DEADLOCK_DETECTOR_CONSTEXPR michael@0: #else michael@0: typedef void* callstack_id; michael@0: # define NS_GET_BACKTRACE() 0 michael@0: # define NS_DEADLOCK_DETECTOR_CONSTEXPR MOZ_CONSTEXPR michael@0: #endif // ifdef NS_TRACE_MALLOC michael@0: michael@0: callstack_id mCallStack; michael@0: michael@0: public: michael@0: /** michael@0: * CallStack michael@0: * *ALWAYS* *ALWAYS* *ALWAYS* call this with no arguments. This michael@0: * constructor takes an argument *ONLY* so that |GET_BACKTRACE()| michael@0: * can be evaluated in the stack frame of the caller, rather than michael@0: * that of the constructor. michael@0: * michael@0: * *BEWARE*: this means that calling this constructor with no michael@0: * arguments is not the same as a "default, do-nothing" michael@0: * constructor: it *will* construct a backtrace. This can cause michael@0: * unexpected performance issues. michael@0: */ michael@0: NS_DEADLOCK_DETECTOR_CONSTEXPR michael@0: CallStack(const callstack_id aCallStack = NS_GET_BACKTRACE()) : michael@0: mCallStack(aCallStack) michael@0: { michael@0: } michael@0: NS_DEADLOCK_DETECTOR_CONSTEXPR michael@0: CallStack(const CallStack& aFrom) : michael@0: mCallStack(aFrom.mCallStack) michael@0: { michael@0: } michael@0: CallStack& operator=(const CallStack& aFrom) michael@0: { michael@0: mCallStack = aFrom.mCallStack; michael@0: return *this; michael@0: } michael@0: bool operator==(const CallStack& aOther) const michael@0: { michael@0: return mCallStack == aOther.mCallStack; michael@0: } michael@0: bool operator!=(const CallStack& aOther) const michael@0: { michael@0: return mCallStack != aOther.mCallStack; michael@0: } michael@0: michael@0: // FIXME bug 456272: if this is split off, michael@0: // NS_TraceMallocPrintStackTrace should be modified to print into michael@0: // an nsACString michael@0: void Print(FILE* f) const michael@0: { michael@0: #ifdef NS_TRACE_MALLOC michael@0: if (this != &kNone && mCallStack) { michael@0: NS_TraceMallocPrintStackTrace(f, mCallStack); michael@0: return; michael@0: } michael@0: #endif michael@0: fputs(" [stack trace unavailable]\n", f); michael@0: } michael@0: michael@0: /** The "null" callstack. */ michael@0: static const CallStack kNone; michael@0: }; michael@0: michael@0: michael@0: /** michael@0: * DeadlockDetector michael@0: * michael@0: * The following is an approximate description of how the deadlock detector michael@0: * works. michael@0: * michael@0: * The deadlock detector ensures that all blocking resources are michael@0: * acquired according to a partial order P. One type of blocking michael@0: * resource is a lock. If a lock l1 is acquired (locked) before l2, michael@0: * then we say that |l1 <_P l2|. The detector flags an error if two michael@0: * locks l1 and l2 have an inconsistent ordering in P; that is, if michael@0: * both |l1 <_P l2| and |l2 <_P l1|. This is a potential error michael@0: * because a thread acquiring l1,l2 according to the first order might michael@0: * race with a thread acquiring them according to the second order. michael@0: * If this happens under the right conditions, then the acquisitions michael@0: * will deadlock. michael@0: * michael@0: * This deadlock detector doesn't know at compile-time what P is. So, michael@0: * it tries to discover the order at run time. More precisely, it michael@0: * finds some order P, then tries to find chains of resource michael@0: * acquisitions that violate P. An example acquisition sequence, and michael@0: * the orders they impose, is michael@0: * l1.lock() // current chain: [ l1 ] michael@0: * // order: { } michael@0: * michael@0: * l2.lock() // current chain: [ l1, l2 ] michael@0: * // order: { l1 <_P l2 } michael@0: * michael@0: * l3.lock() // current chain: [ l1, l2, l3 ] michael@0: * // order: { l1 <_P l2, l2 <_P l3, l1 <_P l3 } michael@0: * // (note: <_P is transitive, so also |l1 <_P l3|) michael@0: * michael@0: * l2.unlock() // current chain: [ l1, l3 ] michael@0: * // order: { l1 <_P l2, l2 <_P l3, l1 <_P l3 } michael@0: * // (note: it's OK, but weird, that l2 was unlocked out michael@0: * // of order. we still have l1 <_P l3). michael@0: * michael@0: * l2.lock() // current chain: [ l1, l3, l2 ] michael@0: * // order: { l1 <_P l2, l2 <_P l3, l1 <_P l3, michael@0: * l3 <_P l2 (!!!) } michael@0: * BEEP BEEP! Here the detector will flag a potential error, since michael@0: * l2 and l3 were used inconsistently (and potentially in ways that michael@0: * would deadlock). michael@0: */ michael@0: template michael@0: class DeadlockDetector michael@0: { michael@0: public: michael@0: /** michael@0: * ResourceAcquisition michael@0: * Consists simply of a resource and the calling context from michael@0: * which it was acquired. We pack this information together so michael@0: * that it can be returned back to the caller when a potential michael@0: * deadlock has been found. michael@0: */ michael@0: struct ResourceAcquisition michael@0: { michael@0: const T* mResource; michael@0: CallStack mCallContext; michael@0: michael@0: ResourceAcquisition( michael@0: const T* aResource, michael@0: const CallStack aCallContext=CallStack::kNone) : michael@0: mResource(aResource), michael@0: mCallContext(aCallContext) michael@0: { michael@0: } michael@0: ResourceAcquisition(const ResourceAcquisition& aFrom) : michael@0: mResource(aFrom.mResource), michael@0: mCallContext(aFrom.mCallContext) michael@0: { michael@0: } michael@0: ResourceAcquisition& operator=(const ResourceAcquisition& aFrom) michael@0: { michael@0: mResource = aFrom.mResource; michael@0: mCallContext = aFrom.mCallContext; michael@0: return *this; michael@0: } michael@0: }; michael@0: typedef nsTArray ResourceAcquisitionArray; michael@0: michael@0: private: michael@0: typedef nsTArray HashEntryArray; michael@0: typedef typename HashEntryArray::index_type index_type; michael@0: typedef typename HashEntryArray::size_type size_type; michael@0: enum { michael@0: NoIndex = HashEntryArray::NoIndex michael@0: }; michael@0: michael@0: /** michael@0: * Value type for the ordering table. Contains the other michael@0: * resources on which an ordering constraint |key < other| michael@0: * exists. The catch is that we also store the calling context at michael@0: * which the other resource was acquired; this improves the michael@0: * quality of error messages when potential deadlock is detected. michael@0: */ michael@0: struct OrderingEntry michael@0: { michael@0: OrderingEntry() : michael@0: mFirstSeen(CallStack::kNone), michael@0: mOrderedLT() // FIXME bug 456272: set to empirical michael@0: { // dep size? michael@0: } michael@0: ~OrderingEntry() michael@0: { michael@0: } michael@0: michael@0: CallStack mFirstSeen; // first site from which the resource appeared michael@0: HashEntryArray mOrderedLT; // this <_o Other michael@0: }; michael@0: michael@0: static void* TableAlloc(void* /*pool*/, size_t size) michael@0: { michael@0: return operator new(size); michael@0: } michael@0: static void TableFree(void* /*pool*/, void* item) michael@0: { michael@0: operator delete(item); michael@0: } michael@0: static PLHashEntry* EntryAlloc(void* /*pool*/, const void* key) michael@0: { michael@0: return new PLHashEntry; michael@0: } michael@0: static void EntryFree(void* /*pool*/, PLHashEntry* entry, unsigned flag) michael@0: { michael@0: delete static_cast(const_cast(entry->key)); michael@0: delete static_cast(entry->value); michael@0: entry->value = 0; michael@0: if (HT_FREE_ENTRY == flag) michael@0: delete entry; michael@0: } michael@0: static PLHashNumber HashKey(const void* aKey) michael@0: { michael@0: return NS_PTR_TO_INT32(aKey) >> 2; michael@0: } michael@0: static const PLHashAllocOps kAllocOps; michael@0: michael@0: // Hash table "interface" the rest of the code should use michael@0: michael@0: PLHashEntry** GetEntry(const T* aKey) michael@0: { michael@0: return PL_HashTableRawLookup(mOrdering, HashKey(aKey), aKey); michael@0: } michael@0: michael@0: void PutEntry(T* aKey) michael@0: { michael@0: PL_HashTableAdd(mOrdering, aKey, new OrderingEntry()); michael@0: } michael@0: michael@0: // XXX need these helper methods because OrderingEntry doesn't have michael@0: // XXX access to underlying PLHashEntry michael@0: michael@0: /** michael@0: * Add the order |aFirst <_o aSecond|. michael@0: * michael@0: * WARNING: this does not check whether it's sane to add this michael@0: * order. In the "best" bad case, when this order already exists, michael@0: * adding it anyway may unnecessarily result in O(n^2) space. In michael@0: * the "worst" bad case, adding it anyway will cause michael@0: * |InTransitiveClosure()| to diverge. michael@0: */ michael@0: void AddOrder(PLHashEntry* aLT, PLHashEntry* aGT) michael@0: { michael@0: static_cast(aLT->value)->mOrderedLT michael@0: .InsertElementSorted(aGT); michael@0: } michael@0: michael@0: /** michael@0: * Return true iff the order |aFirst < aSecond| has been michael@0: * *explicitly* added. michael@0: * michael@0: * Does not consider transitivity. michael@0: */ michael@0: bool IsOrdered(const PLHashEntry* aFirst, const PLHashEntry* aSecond) michael@0: const michael@0: { michael@0: return NoIndex != michael@0: static_cast(aFirst->value)->mOrderedLT michael@0: .BinaryIndexOf(aSecond); michael@0: } michael@0: michael@0: /** michael@0: * Return a pointer to the array of all elements "that" for michael@0: * which the order |this < that| has been explicitly added. michael@0: * michael@0: * NOTE: this does *not* consider transitive orderings. michael@0: */ michael@0: PLHashEntry* const* GetOrders(const PLHashEntry* aEntry) const michael@0: { michael@0: return static_cast(aEntry->value)->mOrderedLT michael@0: .Elements(); michael@0: } michael@0: michael@0: /** michael@0: * Return the number of elements "that" for which the order michael@0: * |this < that| has been explicitly added. michael@0: * michael@0: * NOTE: this does *not* consider transitive orderings. michael@0: */ michael@0: size_type NumOrders(const PLHashEntry* aEntry) const michael@0: { michael@0: return static_cast(aEntry->value)->mOrderedLT michael@0: .Length(); michael@0: } michael@0: michael@0: /** Make a ResourceAcquisition out of |aEntry|. */ michael@0: ResourceAcquisition MakeResourceAcquisition(const PLHashEntry* aEntry) michael@0: const michael@0: { michael@0: return ResourceAcquisition( michael@0: static_cast(aEntry->key), michael@0: static_cast(aEntry->value)->mFirstSeen); michael@0: } michael@0: michael@0: // Throwaway RAII lock to make the following code safer. michael@0: struct PRAutoLock michael@0: { michael@0: PRAutoLock(PRLock* aLock) : mLock(aLock) { PR_Lock(mLock); } michael@0: ~PRAutoLock() { PR_Unlock(mLock); } michael@0: PRLock* mLock; michael@0: }; michael@0: michael@0: public: michael@0: static const uint32_t kDefaultNumBuckets; michael@0: michael@0: /** michael@0: * DeadlockDetector michael@0: * Create a new deadlock detector. michael@0: * michael@0: * @param aNumResourcesGuess Guess at approximate number of resources michael@0: * that will be checked. michael@0: */ michael@0: DeadlockDetector(uint32_t aNumResourcesGuess = kDefaultNumBuckets) michael@0: { michael@0: mOrdering = PL_NewHashTable(aNumResourcesGuess, michael@0: HashKey, michael@0: PL_CompareValues, PL_CompareValues, michael@0: &kAllocOps, 0); michael@0: if (!mOrdering) michael@0: NS_RUNTIMEABORT("couldn't initialize resource ordering table"); michael@0: michael@0: mLock = PR_NewLock(); michael@0: if (!mLock) michael@0: NS_RUNTIMEABORT("couldn't allocate deadlock detector lock"); michael@0: } michael@0: michael@0: /** michael@0: * ~DeadlockDetector michael@0: * michael@0: * *NOT* thread safe. michael@0: */ michael@0: ~DeadlockDetector() michael@0: { michael@0: PL_HashTableDestroy(mOrdering); michael@0: PR_DestroyLock(mLock); michael@0: } michael@0: michael@0: /** michael@0: * Add michael@0: * Make the deadlock detector aware of |aResource|. michael@0: * michael@0: * WARNING: The deadlock detector owns |aResource|. michael@0: * michael@0: * Thread safe. michael@0: * michael@0: * @param aResource Resource to make deadlock detector aware of. michael@0: */ michael@0: void Add(T* aResource) michael@0: { michael@0: PRAutoLock _(mLock); michael@0: PutEntry(aResource); michael@0: } michael@0: michael@0: // Nb: implementing a Remove() method makes the detector "more michael@0: // unsound." By removing a resource from the orderings, deadlocks michael@0: // may be missed that would otherwise have been found. However, michael@0: // removing resources possibly reduces the # of false positives, michael@0: // and additionally saves space. So it's a trade off; we have michael@0: // chosen to err on the side of caution and not implement Remove(). michael@0: michael@0: /** michael@0: * CheckAcquisition This method is called after acquiring |aLast|, michael@0: * but before trying to acquire |aProposed| from |aCallContext|. michael@0: * It determines whether actually trying to acquire |aProposed| michael@0: * will create problems. It is OK if |aLast| is nullptr; this is michael@0: * interpreted as |aProposed| being the thread's first acquisition michael@0: * of its current chain. michael@0: * michael@0: * Iff acquiring |aProposed| may lead to deadlock for some thread michael@0: * interleaving (including the current one!), the cyclical michael@0: * dependency from which this was deduced is returned. Otherwise, michael@0: * 0 is returned. michael@0: * michael@0: * If a potential deadlock is detected and a resource cycle is michael@0: * returned, it is the *caller's* responsibility to free it. michael@0: * michael@0: * Thread safe. michael@0: * michael@0: * @param aLast Last resource acquired by calling thread (or 0). michael@0: * @param aProposed Resource calling thread proposes to acquire. michael@0: * @param aCallContext Calling context whence acquisiton request came. michael@0: */ michael@0: ResourceAcquisitionArray* CheckAcquisition(const T* aLast, michael@0: const T* aProposed, michael@0: const CallStack& aCallContext) michael@0: { michael@0: NS_ASSERTION(aProposed, "null resource"); michael@0: PRAutoLock _(mLock); michael@0: michael@0: PLHashEntry* second = *GetEntry(aProposed); michael@0: OrderingEntry* e = static_cast(second->value); michael@0: if (CallStack::kNone == e->mFirstSeen) michael@0: e->mFirstSeen = aCallContext; michael@0: michael@0: if (!aLast) michael@0: // don't check if |0 < proposed|; just vamoose michael@0: return 0; michael@0: michael@0: PLHashEntry* first = *GetEntry(aLast); michael@0: michael@0: // this is the crux of the deadlock detector algorithm michael@0: michael@0: if (first == second) { michael@0: // reflexive deadlock. fastpath b/c InTransitiveClosure is michael@0: // not applicable here. michael@0: ResourceAcquisitionArray* cycle = new ResourceAcquisitionArray(); michael@0: if (!cycle) michael@0: NS_RUNTIMEABORT("can't allocate dep. cycle array"); michael@0: cycle->AppendElement(MakeResourceAcquisition(first)); michael@0: cycle->AppendElement(ResourceAcquisition(aProposed, michael@0: aCallContext)); michael@0: return cycle; michael@0: } michael@0: if (InTransitiveClosure(first, second)) { michael@0: // we've already established |last < proposed|. all is well. michael@0: return 0; michael@0: } michael@0: if (InTransitiveClosure(second, first)) { michael@0: // the order |proposed < last| has been deduced, perhaps michael@0: // transitively. we're attempting to violate that michael@0: // constraint by acquiring resources in the order michael@0: // |last < proposed|, and thus we may deadlock under the michael@0: // right conditions. michael@0: ResourceAcquisitionArray* cycle = GetDeductionChain(second, first); michael@0: // show how acquiring |proposed| would complete the cycle michael@0: cycle->AppendElement(ResourceAcquisition(aProposed, michael@0: aCallContext)); michael@0: return cycle; michael@0: } michael@0: // |last|, |proposed| are unordered according to our michael@0: // poset. this is fine, but we now need to add this michael@0: // ordering constraint. michael@0: AddOrder(first, second); michael@0: return 0; michael@0: } michael@0: michael@0: /** michael@0: * Return true iff |aTarget| is in the transitive closure of |aStart| michael@0: * over the ordering relation `<_this'. michael@0: * michael@0: * @precondition |aStart != aTarget| michael@0: */ michael@0: bool InTransitiveClosure(const PLHashEntry* aStart, michael@0: const PLHashEntry* aTarget) const michael@0: { michael@0: if (IsOrdered(aStart, aTarget)) michael@0: return true; michael@0: michael@0: index_type i = 0; michael@0: size_type len = NumOrders(aStart); michael@0: for (const PLHashEntry* const* it = GetOrders(aStart); michael@0: i < len; ++i, ++it) michael@0: if (InTransitiveClosure(*it, aTarget)) michael@0: return true; michael@0: return false; michael@0: } michael@0: michael@0: /** michael@0: * Return an array of all resource acquisitions michael@0: * aStart <_this r1 <_this r2 <_ ... <_ aTarget michael@0: * from which |aStart <_this aTarget| was deduced, including michael@0: * |aStart| and |aTarget|. michael@0: * michael@0: * Nb: there may be multiple deductions of |aStart <_this michael@0: * aTarget|. This function returns the first ordering found by michael@0: * depth-first search. michael@0: * michael@0: * Nb: |InTransitiveClosure| could be replaced by this function. michael@0: * However, this one is more expensive because we record the DFS michael@0: * search stack on the heap whereas the other doesn't. michael@0: * michael@0: * @precondition |aStart != aTarget| michael@0: */ michael@0: ResourceAcquisitionArray* GetDeductionChain( michael@0: const PLHashEntry* aStart, michael@0: const PLHashEntry* aTarget) michael@0: { michael@0: ResourceAcquisitionArray* chain = new ResourceAcquisitionArray(); michael@0: if (!chain) michael@0: NS_RUNTIMEABORT("can't allocate dep. cycle array"); michael@0: chain->AppendElement(MakeResourceAcquisition(aStart)); michael@0: michael@0: NS_ASSERTION(GetDeductionChain_Helper(aStart, aTarget, chain), michael@0: "GetDeductionChain called when there's no deadlock"); michael@0: return chain; michael@0: } michael@0: michael@0: // precondition: |aStart != aTarget| michael@0: // invariant: |aStart| is the last element in |aChain| michael@0: bool GetDeductionChain_Helper(const PLHashEntry* aStart, michael@0: const PLHashEntry* aTarget, michael@0: ResourceAcquisitionArray* aChain) michael@0: { michael@0: if (IsOrdered(aStart, aTarget)) { michael@0: aChain->AppendElement(MakeResourceAcquisition(aTarget)); michael@0: return true; michael@0: } michael@0: michael@0: index_type i = 0; michael@0: size_type len = NumOrders(aStart); michael@0: for (const PLHashEntry* const* it = GetOrders(aStart); michael@0: i < len; ++i, ++it) { michael@0: aChain->AppendElement(MakeResourceAcquisition(*it)); michael@0: if (GetDeductionChain_Helper(*it, aTarget, aChain)) michael@0: return true; michael@0: aChain->RemoveElementAt(aChain->Length() - 1); michael@0: } michael@0: return false; michael@0: } michael@0: michael@0: /** michael@0: * The partial order on resource acquisitions used by the deadlock michael@0: * detector. michael@0: */ michael@0: PLHashTable* mOrdering; // T* -> PLHashEntry michael@0: michael@0: /** michael@0: * Protects contentious methods. michael@0: * Nb: can't use mozilla::Mutex since we are used as its deadlock michael@0: * detector. michael@0: */ michael@0: PRLock* mLock; michael@0: michael@0: private: michael@0: DeadlockDetector(const DeadlockDetector& aDD) MOZ_DELETE; michael@0: DeadlockDetector& operator=(const DeadlockDetector& aDD) MOZ_DELETE; michael@0: }; michael@0: michael@0: michael@0: template michael@0: const PLHashAllocOps DeadlockDetector::kAllocOps = { michael@0: DeadlockDetector::TableAlloc, DeadlockDetector::TableFree, michael@0: DeadlockDetector::EntryAlloc, DeadlockDetector::EntryFree michael@0: }; michael@0: michael@0: michael@0: template michael@0: // FIXME bug 456272: tune based on average workload michael@0: const uint32_t DeadlockDetector::kDefaultNumBuckets = 64; michael@0: michael@0: michael@0: } // namespace mozilla michael@0: michael@0: #endif // ifndef mozilla_DeadlockDetector_h