xpcom/glue/DeadlockDetector.h

branch
TOR_BUG_3246
changeset 7
129ffea94266
equal deleted inserted replaced
-1:000000000000 0:7c14298e258f
1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
2 * vim: sw=4 ts=4 et :
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #ifndef mozilla_DeadlockDetector_h
7 #define mozilla_DeadlockDetector_h
8
9 #include "mozilla/Attributes.h"
10
11 #include <stdlib.h>
12
13 #include "plhash.h"
14 #include "prlock.h"
15
16 #include "nsTArray.h"
17
18 #ifdef NS_TRACE_MALLOC
19 # include "nsTraceMalloc.h"
20 #endif // ifdef NS_TRACE_MALLOC
21
22 namespace mozilla {
23
24
25 // FIXME bug 456272: split this off into a convenience API on top of
26 // nsStackWalk?
27 class NS_COM_GLUE CallStack
28 {
29 private:
30 #ifdef NS_TRACE_MALLOC
31 typedef nsTMStackTraceID callstack_id;
32 // needs to be a macro to avoid disturbing the backtrace
33 # define NS_GET_BACKTRACE() NS_TraceMallocGetStackTrace()
34 # define NS_DEADLOCK_DETECTOR_CONSTEXPR
35 #else
36 typedef void* callstack_id;
37 # define NS_GET_BACKTRACE() 0
38 # define NS_DEADLOCK_DETECTOR_CONSTEXPR MOZ_CONSTEXPR
39 #endif // ifdef NS_TRACE_MALLOC
40
41 callstack_id mCallStack;
42
43 public:
44 /**
45 * CallStack
46 * *ALWAYS* *ALWAYS* *ALWAYS* call this with no arguments. This
47 * constructor takes an argument *ONLY* so that |GET_BACKTRACE()|
48 * can be evaluated in the stack frame of the caller, rather than
49 * that of the constructor.
50 *
51 * *BEWARE*: this means that calling this constructor with no
52 * arguments is not the same as a "default, do-nothing"
53 * constructor: it *will* construct a backtrace. This can cause
54 * unexpected performance issues.
55 */
56 NS_DEADLOCK_DETECTOR_CONSTEXPR
57 CallStack(const callstack_id aCallStack = NS_GET_BACKTRACE()) :
58 mCallStack(aCallStack)
59 {
60 }
61 NS_DEADLOCK_DETECTOR_CONSTEXPR
62 CallStack(const CallStack& aFrom) :
63 mCallStack(aFrom.mCallStack)
64 {
65 }
66 CallStack& operator=(const CallStack& aFrom)
67 {
68 mCallStack = aFrom.mCallStack;
69 return *this;
70 }
71 bool operator==(const CallStack& aOther) const
72 {
73 return mCallStack == aOther.mCallStack;
74 }
75 bool operator!=(const CallStack& aOther) const
76 {
77 return mCallStack != aOther.mCallStack;
78 }
79
80 // FIXME bug 456272: if this is split off,
81 // NS_TraceMallocPrintStackTrace should be modified to print into
82 // an nsACString
83 void Print(FILE* f) const
84 {
85 #ifdef NS_TRACE_MALLOC
86 if (this != &kNone && mCallStack) {
87 NS_TraceMallocPrintStackTrace(f, mCallStack);
88 return;
89 }
90 #endif
91 fputs(" [stack trace unavailable]\n", f);
92 }
93
94 /** The "null" callstack. */
95 static const CallStack kNone;
96 };
97
98
99 /**
100 * DeadlockDetector
101 *
102 * The following is an approximate description of how the deadlock detector
103 * works.
104 *
105 * The deadlock detector ensures that all blocking resources are
106 * acquired according to a partial order P. One type of blocking
107 * resource is a lock. If a lock l1 is acquired (locked) before l2,
108 * then we say that |l1 <_P l2|. The detector flags an error if two
109 * locks l1 and l2 have an inconsistent ordering in P; that is, if
110 * both |l1 <_P l2| and |l2 <_P l1|. This is a potential error
111 * because a thread acquiring l1,l2 according to the first order might
112 * race with a thread acquiring them according to the second order.
113 * If this happens under the right conditions, then the acquisitions
114 * will deadlock.
115 *
116 * This deadlock detector doesn't know at compile-time what P is. So,
117 * it tries to discover the order at run time. More precisely, it
118 * finds <i>some</i> order P, then tries to find chains of resource
119 * acquisitions that violate P. An example acquisition sequence, and
120 * the orders they impose, is
121 * l1.lock() // current chain: [ l1 ]
122 * // order: { }
123 *
124 * l2.lock() // current chain: [ l1, l2 ]
125 * // order: { l1 <_P l2 }
126 *
127 * l3.lock() // current chain: [ l1, l2, l3 ]
128 * // order: { l1 <_P l2, l2 <_P l3, l1 <_P l3 }
129 * // (note: <_P is transitive, so also |l1 <_P l3|)
130 *
131 * l2.unlock() // current chain: [ l1, l3 ]
132 * // order: { l1 <_P l2, l2 <_P l3, l1 <_P l3 }
133 * // (note: it's OK, but weird, that l2 was unlocked out
134 * // of order. we still have l1 <_P l3).
135 *
136 * l2.lock() // current chain: [ l1, l3, l2 ]
137 * // order: { l1 <_P l2, l2 <_P l3, l1 <_P l3,
138 * l3 <_P l2 (!!!) }
139 * BEEP BEEP! Here the detector will flag a potential error, since
140 * l2 and l3 were used inconsistently (and potentially in ways that
141 * would deadlock).
142 */
143 template <typename T>
144 class DeadlockDetector
145 {
146 public:
147 /**
148 * ResourceAcquisition
149 * Consists simply of a resource and the calling context from
150 * which it was acquired. We pack this information together so
151 * that it can be returned back to the caller when a potential
152 * deadlock has been found.
153 */
154 struct ResourceAcquisition
155 {
156 const T* mResource;
157 CallStack mCallContext;
158
159 ResourceAcquisition(
160 const T* aResource,
161 const CallStack aCallContext=CallStack::kNone) :
162 mResource(aResource),
163 mCallContext(aCallContext)
164 {
165 }
166 ResourceAcquisition(const ResourceAcquisition& aFrom) :
167 mResource(aFrom.mResource),
168 mCallContext(aFrom.mCallContext)
169 {
170 }
171 ResourceAcquisition& operator=(const ResourceAcquisition& aFrom)
172 {
173 mResource = aFrom.mResource;
174 mCallContext = aFrom.mCallContext;
175 return *this;
176 }
177 };
178 typedef nsTArray<ResourceAcquisition> ResourceAcquisitionArray;
179
180 private:
181 typedef nsTArray<PLHashEntry*> HashEntryArray;
182 typedef typename HashEntryArray::index_type index_type;
183 typedef typename HashEntryArray::size_type size_type;
184 enum {
185 NoIndex = HashEntryArray::NoIndex
186 };
187
188 /**
189 * Value type for the ordering table. Contains the other
190 * resources on which an ordering constraint |key < other|
191 * exists. The catch is that we also store the calling context at
192 * which the other resource was acquired; this improves the
193 * quality of error messages when potential deadlock is detected.
194 */
195 struct OrderingEntry
196 {
197 OrderingEntry() :
198 mFirstSeen(CallStack::kNone),
199 mOrderedLT() // FIXME bug 456272: set to empirical
200 { // dep size?
201 }
202 ~OrderingEntry()
203 {
204 }
205
206 CallStack mFirstSeen; // first site from which the resource appeared
207 HashEntryArray mOrderedLT; // this <_o Other
208 };
209
210 static void* TableAlloc(void* /*pool*/, size_t size)
211 {
212 return operator new(size);
213 }
214 static void TableFree(void* /*pool*/, void* item)
215 {
216 operator delete(item);
217 }
218 static PLHashEntry* EntryAlloc(void* /*pool*/, const void* key)
219 {
220 return new PLHashEntry;
221 }
222 static void EntryFree(void* /*pool*/, PLHashEntry* entry, unsigned flag)
223 {
224 delete static_cast<T*>(const_cast<void*>(entry->key));
225 delete static_cast<OrderingEntry*>(entry->value);
226 entry->value = 0;
227 if (HT_FREE_ENTRY == flag)
228 delete entry;
229 }
230 static PLHashNumber HashKey(const void* aKey)
231 {
232 return NS_PTR_TO_INT32(aKey) >> 2;
233 }
234 static const PLHashAllocOps kAllocOps;
235
236 // Hash table "interface" the rest of the code should use
237
238 PLHashEntry** GetEntry(const T* aKey)
239 {
240 return PL_HashTableRawLookup(mOrdering, HashKey(aKey), aKey);
241 }
242
243 void PutEntry(T* aKey)
244 {
245 PL_HashTableAdd(mOrdering, aKey, new OrderingEntry());
246 }
247
248 // XXX need these helper methods because OrderingEntry doesn't have
249 // XXX access to underlying PLHashEntry
250
251 /**
252 * Add the order |aFirst <_o aSecond|.
253 *
254 * WARNING: this does not check whether it's sane to add this
255 * order. In the "best" bad case, when this order already exists,
256 * adding it anyway may unnecessarily result in O(n^2) space. In
257 * the "worst" bad case, adding it anyway will cause
258 * |InTransitiveClosure()| to diverge.
259 */
260 void AddOrder(PLHashEntry* aLT, PLHashEntry* aGT)
261 {
262 static_cast<OrderingEntry*>(aLT->value)->mOrderedLT
263 .InsertElementSorted(aGT);
264 }
265
266 /**
267 * Return true iff the order |aFirst < aSecond| has been
268 * *explicitly* added.
269 *
270 * Does not consider transitivity.
271 */
272 bool IsOrdered(const PLHashEntry* aFirst, const PLHashEntry* aSecond)
273 const
274 {
275 return NoIndex !=
276 static_cast<const OrderingEntry*>(aFirst->value)->mOrderedLT
277 .BinaryIndexOf(aSecond);
278 }
279
280 /**
281 * Return a pointer to the array of all elements "that" for
282 * which the order |this < that| has been explicitly added.
283 *
284 * NOTE: this does *not* consider transitive orderings.
285 */
286 PLHashEntry* const* GetOrders(const PLHashEntry* aEntry) const
287 {
288 return static_cast<const OrderingEntry*>(aEntry->value)->mOrderedLT
289 .Elements();
290 }
291
292 /**
293 * Return the number of elements "that" for which the order
294 * |this < that| has been explicitly added.
295 *
296 * NOTE: this does *not* consider transitive orderings.
297 */
298 size_type NumOrders(const PLHashEntry* aEntry) const
299 {
300 return static_cast<const OrderingEntry*>(aEntry->value)->mOrderedLT
301 .Length();
302 }
303
304 /** Make a ResourceAcquisition out of |aEntry|. */
305 ResourceAcquisition MakeResourceAcquisition(const PLHashEntry* aEntry)
306 const
307 {
308 return ResourceAcquisition(
309 static_cast<const T*>(aEntry->key),
310 static_cast<const OrderingEntry*>(aEntry->value)->mFirstSeen);
311 }
312
313 // Throwaway RAII lock to make the following code safer.
314 struct PRAutoLock
315 {
316 PRAutoLock(PRLock* aLock) : mLock(aLock) { PR_Lock(mLock); }
317 ~PRAutoLock() { PR_Unlock(mLock); }
318 PRLock* mLock;
319 };
320
321 public:
322 static const uint32_t kDefaultNumBuckets;
323
324 /**
325 * DeadlockDetector
326 * Create a new deadlock detector.
327 *
328 * @param aNumResourcesGuess Guess at approximate number of resources
329 * that will be checked.
330 */
331 DeadlockDetector(uint32_t aNumResourcesGuess = kDefaultNumBuckets)
332 {
333 mOrdering = PL_NewHashTable(aNumResourcesGuess,
334 HashKey,
335 PL_CompareValues, PL_CompareValues,
336 &kAllocOps, 0);
337 if (!mOrdering)
338 NS_RUNTIMEABORT("couldn't initialize resource ordering table");
339
340 mLock = PR_NewLock();
341 if (!mLock)
342 NS_RUNTIMEABORT("couldn't allocate deadlock detector lock");
343 }
344
345 /**
346 * ~DeadlockDetector
347 *
348 * *NOT* thread safe.
349 */
350 ~DeadlockDetector()
351 {
352 PL_HashTableDestroy(mOrdering);
353 PR_DestroyLock(mLock);
354 }
355
356 /**
357 * Add
358 * Make the deadlock detector aware of |aResource|.
359 *
360 * WARNING: The deadlock detector owns |aResource|.
361 *
362 * Thread safe.
363 *
364 * @param aResource Resource to make deadlock detector aware of.
365 */
366 void Add(T* aResource)
367 {
368 PRAutoLock _(mLock);
369 PutEntry(aResource);
370 }
371
372 // Nb: implementing a Remove() method makes the detector "more
373 // unsound." By removing a resource from the orderings, deadlocks
374 // may be missed that would otherwise have been found. However,
375 // removing resources possibly reduces the # of false positives,
376 // and additionally saves space. So it's a trade off; we have
377 // chosen to err on the side of caution and not implement Remove().
378
379 /**
380 * CheckAcquisition This method is called after acquiring |aLast|,
381 * but before trying to acquire |aProposed| from |aCallContext|.
382 * It determines whether actually trying to acquire |aProposed|
383 * will create problems. It is OK if |aLast| is nullptr; this is
384 * interpreted as |aProposed| being the thread's first acquisition
385 * of its current chain.
386 *
387 * Iff acquiring |aProposed| may lead to deadlock for some thread
388 * interleaving (including the current one!), the cyclical
389 * dependency from which this was deduced is returned. Otherwise,
390 * 0 is returned.
391 *
392 * If a potential deadlock is detected and a resource cycle is
393 * returned, it is the *caller's* responsibility to free it.
394 *
395 * Thread safe.
396 *
397 * @param aLast Last resource acquired by calling thread (or 0).
398 * @param aProposed Resource calling thread proposes to acquire.
399 * @param aCallContext Calling context whence acquisiton request came.
400 */
401 ResourceAcquisitionArray* CheckAcquisition(const T* aLast,
402 const T* aProposed,
403 const CallStack& aCallContext)
404 {
405 NS_ASSERTION(aProposed, "null resource");
406 PRAutoLock _(mLock);
407
408 PLHashEntry* second = *GetEntry(aProposed);
409 OrderingEntry* e = static_cast<OrderingEntry*>(second->value);
410 if (CallStack::kNone == e->mFirstSeen)
411 e->mFirstSeen = aCallContext;
412
413 if (!aLast)
414 // don't check if |0 < proposed|; just vamoose
415 return 0;
416
417 PLHashEntry* first = *GetEntry(aLast);
418
419 // this is the crux of the deadlock detector algorithm
420
421 if (first == second) {
422 // reflexive deadlock. fastpath b/c InTransitiveClosure is
423 // not applicable here.
424 ResourceAcquisitionArray* cycle = new ResourceAcquisitionArray();
425 if (!cycle)
426 NS_RUNTIMEABORT("can't allocate dep. cycle array");
427 cycle->AppendElement(MakeResourceAcquisition(first));
428 cycle->AppendElement(ResourceAcquisition(aProposed,
429 aCallContext));
430 return cycle;
431 }
432 if (InTransitiveClosure(first, second)) {
433 // we've already established |last < proposed|. all is well.
434 return 0;
435 }
436 if (InTransitiveClosure(second, first)) {
437 // the order |proposed < last| has been deduced, perhaps
438 // transitively. we're attempting to violate that
439 // constraint by acquiring resources in the order
440 // |last < proposed|, and thus we may deadlock under the
441 // right conditions.
442 ResourceAcquisitionArray* cycle = GetDeductionChain(second, first);
443 // show how acquiring |proposed| would complete the cycle
444 cycle->AppendElement(ResourceAcquisition(aProposed,
445 aCallContext));
446 return cycle;
447 }
448 // |last|, |proposed| are unordered according to our
449 // poset. this is fine, but we now need to add this
450 // ordering constraint.
451 AddOrder(first, second);
452 return 0;
453 }
454
455 /**
456 * Return true iff |aTarget| is in the transitive closure of |aStart|
457 * over the ordering relation `<_this'.
458 *
459 * @precondition |aStart != aTarget|
460 */
461 bool InTransitiveClosure(const PLHashEntry* aStart,
462 const PLHashEntry* aTarget) const
463 {
464 if (IsOrdered(aStart, aTarget))
465 return true;
466
467 index_type i = 0;
468 size_type len = NumOrders(aStart);
469 for (const PLHashEntry* const* it = GetOrders(aStart);
470 i < len; ++i, ++it)
471 if (InTransitiveClosure(*it, aTarget))
472 return true;
473 return false;
474 }
475
476 /**
477 * Return an array of all resource acquisitions
478 * aStart <_this r1 <_this r2 <_ ... <_ aTarget
479 * from which |aStart <_this aTarget| was deduced, including
480 * |aStart| and |aTarget|.
481 *
482 * Nb: there may be multiple deductions of |aStart <_this
483 * aTarget|. This function returns the first ordering found by
484 * depth-first search.
485 *
486 * Nb: |InTransitiveClosure| could be replaced by this function.
487 * However, this one is more expensive because we record the DFS
488 * search stack on the heap whereas the other doesn't.
489 *
490 * @precondition |aStart != aTarget|
491 */
492 ResourceAcquisitionArray* GetDeductionChain(
493 const PLHashEntry* aStart,
494 const PLHashEntry* aTarget)
495 {
496 ResourceAcquisitionArray* chain = new ResourceAcquisitionArray();
497 if (!chain)
498 NS_RUNTIMEABORT("can't allocate dep. cycle array");
499 chain->AppendElement(MakeResourceAcquisition(aStart));
500
501 NS_ASSERTION(GetDeductionChain_Helper(aStart, aTarget, chain),
502 "GetDeductionChain called when there's no deadlock");
503 return chain;
504 }
505
506 // precondition: |aStart != aTarget|
507 // invariant: |aStart| is the last element in |aChain|
508 bool GetDeductionChain_Helper(const PLHashEntry* aStart,
509 const PLHashEntry* aTarget,
510 ResourceAcquisitionArray* aChain)
511 {
512 if (IsOrdered(aStart, aTarget)) {
513 aChain->AppendElement(MakeResourceAcquisition(aTarget));
514 return true;
515 }
516
517 index_type i = 0;
518 size_type len = NumOrders(aStart);
519 for (const PLHashEntry* const* it = GetOrders(aStart);
520 i < len; ++i, ++it) {
521 aChain->AppendElement(MakeResourceAcquisition(*it));
522 if (GetDeductionChain_Helper(*it, aTarget, aChain))
523 return true;
524 aChain->RemoveElementAt(aChain->Length() - 1);
525 }
526 return false;
527 }
528
529 /**
530 * The partial order on resource acquisitions used by the deadlock
531 * detector.
532 */
533 PLHashTable* mOrdering; // T* -> PLHashEntry<OrderingEntry>
534
535 /**
536 * Protects contentious methods.
537 * Nb: can't use mozilla::Mutex since we are used as its deadlock
538 * detector.
539 */
540 PRLock* mLock;
541
542 private:
543 DeadlockDetector(const DeadlockDetector& aDD) MOZ_DELETE;
544 DeadlockDetector& operator=(const DeadlockDetector& aDD) MOZ_DELETE;
545 };
546
547
548 template<typename T>
549 const PLHashAllocOps DeadlockDetector<T>::kAllocOps = {
550 DeadlockDetector<T>::TableAlloc, DeadlockDetector<T>::TableFree,
551 DeadlockDetector<T>::EntryAlloc, DeadlockDetector<T>::EntryFree
552 };
553
554
555 template<typename T>
556 // FIXME bug 456272: tune based on average workload
557 const uint32_t DeadlockDetector<T>::kDefaultNumBuckets = 64;
558
559
560 } // namespace mozilla
561
562 #endif // ifndef mozilla_DeadlockDetector_h

mercurial