/* -*- 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 #include #include #include #include #include #include #if !defined(MOZ_PROFILING) # error "DMD requires MOZ_PROFILING" #endif #ifdef XP_WIN # include # include #else # include # include # include #endif #ifdef ANDROID # include #endif #include "nscore.h" #include "mozilla/Assertions.h" #include "mozilla/FastBernoulliTrial.h" #include "mozilla/HashFunctions.h" #include "mozilla/HashTable.h" #include "mozilla/IntegerPrintfMacros.h" #include "mozilla/JSONWriter.h" #include "mozilla/Likely.h" #include "mozilla/MemoryReporting.h" #include "mozilla/PodOperations.h" #include "mozilla/StackWalk.h" #include "mozilla/ThreadLocal.h" // CodeAddressService is defined entirely in the header, so this does not make // DMD depend on XPCOM's object file. #include "CodeAddressService.h" // replace_malloc.h needs to be included before replace_malloc_bridge.h, // which DMD.h includes, so DMD.h needs to be included after replace_malloc.h. #include "replace_malloc.h" #include "DMD.h" namespace mozilla { namespace dmd { class DMDBridge : public ReplaceMallocBridge { virtual DMDFuncs* GetDMDFuncs() override; }; static DMDBridge* gDMDBridge; static DMDFuncs gDMDFuncs; DMDFuncs* DMDBridge::GetDMDFuncs() { return &gDMDFuncs; } MOZ_FORMAT_PRINTF(1, 2) inline void StatusMsg(const char* aFmt, ...) { va_list ap; va_start(ap, aFmt); gDMDFuncs.StatusMsg(aFmt, ap); va_end(ap); } //--------------------------------------------------------------------------- // Utilities //--------------------------------------------------------------------------- #ifndef DISALLOW_COPY_AND_ASSIGN # define DISALLOW_COPY_AND_ASSIGN(T) \ T(const T&); \ void operator=(const T&) #endif static malloc_table_t gMallocTable; // This provides infallible allocations (they abort on OOM). We use it for all // of DMD's own allocations, which fall into the following three cases. // // - Direct allocations (the easy case). // // - Indirect allocations in mozilla::{Vector,HashSet,HashMap} -- this class // serves as their AllocPolicy. // // - Other indirect allocations (e.g. MozStackWalk) -- see the comments on // Thread::mBlockIntercepts and in replace_malloc for how these work. // // It would be nice if we could use the InfallibleAllocPolicy from mozalloc, // but DMD cannot use mozalloc. // class InfallibleAllocPolicy { static void ExitOnFailure(const void* aP); public: template static T* maybe_pod_malloc(size_t aNumElems) { if (aNumElems & mozilla::tl::MulOverflowMask::value) return nullptr; return (T*)gMallocTable.malloc(aNumElems * sizeof(T)); } template static T* maybe_pod_calloc(size_t aNumElems) { return (T*)gMallocTable.calloc(aNumElems, sizeof(T)); } template static T* maybe_pod_realloc(T* aPtr, size_t aOldSize, size_t aNewSize) { if (aNewSize & mozilla::tl::MulOverflowMask::value) return nullptr; return (T*)gMallocTable.realloc(aPtr, aNewSize * sizeof(T)); } static void* malloc_(size_t aSize) { void* p = gMallocTable.malloc(aSize); ExitOnFailure(p); return p; } template static T* pod_malloc(size_t aNumElems) { T* p = maybe_pod_malloc(aNumElems); ExitOnFailure(p); return p; } static void* calloc_(size_t aCount, size_t aSize) { void* p = gMallocTable.calloc(aCount, aSize); ExitOnFailure(p); return p; } template static T* pod_calloc(size_t aNumElems) { T* p = maybe_pod_calloc(aNumElems); ExitOnFailure(p); return p; } static void* realloc_(void* aPtr, size_t aNewSize) { void* p = gMallocTable.realloc(aPtr, aNewSize); ExitOnFailure(p); return p; } template static T* pod_realloc(T* aPtr, size_t aOldSize, size_t aNewSize) { T* p = maybe_pod_realloc(aPtr, aOldSize, aNewSize); ExitOnFailure(p); return p; } static void* memalign_(size_t aAlignment, size_t aSize) { void* p = gMallocTable.memalign(aAlignment, aSize); ExitOnFailure(p); return p; } template static void free_(T* aPtr, size_t aSize = 0) { gMallocTable.free(aPtr); } static char* strdup_(const char* aStr) { char* s = (char*)InfallibleAllocPolicy::malloc_(strlen(aStr) + 1); strcpy(s, aStr); return s; } template static T* new_() { void* mem = malloc_(sizeof(T)); return new (mem) T; } template static T* new_(const P1& aP1) { void* mem = malloc_(sizeof(T)); return new (mem) T(aP1); } template static void delete_(T* aPtr) { if (aPtr) { aPtr->~T(); InfallibleAllocPolicy::free_(aPtr); } } static void reportAllocOverflow() { ExitOnFailure(nullptr); } bool checkSimulatedOOM() const { return true; } }; // This is only needed because of the |const void*| vs |void*| arg mismatch. static size_t MallocSizeOf(const void* aPtr) { return gMallocTable.malloc_usable_size(const_cast(aPtr)); } void DMDFuncs::StatusMsg(const char* aFmt, va_list aAp) { #ifdef ANDROID __android_log_vprint(ANDROID_LOG_INFO, "DMD", aFmt, aAp); #else // The +64 is easily enough for the "DMD[] " prefix and the NUL. size_t size = strlen(aFmt) + 64; char* fmt = (char*)InfallibleAllocPolicy::malloc_(size); snprintf(fmt, size, "DMD[%d] %s", getpid(), aFmt); vfprintf(stderr, fmt, aAp); InfallibleAllocPolicy::free_(fmt); #endif } /* static */ void InfallibleAllocPolicy::ExitOnFailure(const void* aP) { if (!aP) { MOZ_CRASH("DMD out of memory; aborting"); } } static double Percent(size_t part, size_t whole) { return (whole == 0) ? 0 : 100 * (double)part / whole; } // Commifies the number. static char* Show(size_t n, char* buf, size_t buflen) { int nc = 0, i = 0, lasti = buflen - 2; buf[lasti + 1] = '\0'; if (n == 0) { buf[lasti - i] = '0'; i++; } else { while (n > 0) { if (((i - nc) % 3) == 0 && i != 0) { buf[lasti - i] = ','; i++; nc++; } buf[lasti - i] = static_cast((n % 10) + '0'); i++; n /= 10; } } int firstCharIndex = lasti - i + 1; MOZ_ASSERT(firstCharIndex >= 0); return &buf[firstCharIndex]; } //--------------------------------------------------------------------------- // Options (Part 1) //--------------------------------------------------------------------------- class Options { template struct NumOption { const T mDefault; const T mMax; T mActual; NumOption(T aDefault, T aMax) : mDefault(aDefault), mMax(aMax), mActual(aDefault) {} }; // DMD has several modes. These modes affect what data is recorded and // written to the output file, and the written data affects the // post-processing that dmd.py can do. // // Users specify the mode as soon as DMD starts. This leads to minimal memory // usage and log file size. It has the disadvantage that is inflexible -- if // you want to change modes you have to re-run DMD. But in practice changing // modes seems to be rare, so it's not much of a problem. // // An alternative possibility would be to always record and output *all* the // information needed for all modes. This would let you choose the mode when // running dmd.py, and so you could do multiple kinds of profiling on a // single DMD run. But if you are only interested in one of the simpler // modes, you'd pay the price of (a) increased memory usage and (b) *very* // large log files. // // Finally, another alternative possibility would be to do mode selection // partly at DMD startup or recording, and then partly in dmd.py. This would // give some extra flexibility at moderate memory and file size cost. But // certain mode pairs wouldn't work, which would be confusing. // enum class Mode { // For each live block, this mode outputs: size (usable and slop) and // (possibly) and allocation stack. This mode is good for live heap // profiling. Live, // Like "Live", but for each live block it also outputs: zero or more // report stacks. This mode is good for identifying where memory reporters // should be added. This is the default mode. DarkMatter, // Like "Live", but also outputs the same data for dead blocks. This mode // does cumulative heap profiling, which is good for identifying where large // amounts of short-lived allocations ("heap churn") occur. Cumulative, // Like "Live", but this mode also outputs for each live block the address // of the block and the values contained in the blocks. This mode is useful // for investigating leaks, by helping to figure out which blocks refer to // other blocks. This mode force-enables full stacks coverage. Scan }; // With full stacks, every heap block gets a stack trace recorded for it. // This is complete but slow. // // With partial stacks, not all heap blocks will get a stack trace recorded. // A Bernoulli trial (see mfbt/FastBernoulliTrial.h for details) is performed // for each heap block to decide if it gets one. Because bigger heap blocks // are more likely to get a stack trace, even though most heap *blocks* won't // get a stack trace, most heap *bytes* will. enum class Stacks { Full, Partial }; char* mDMDEnvVar; // a saved copy, for later printing Mode mMode; Stacks mStacks; bool mShowDumpStats; void BadArg(const char* aArg); static const char* ValueIfMatch(const char* aArg, const char* aOptionName); static bool GetLong(const char* aArg, const char* aOptionName, long aMin, long aMax, long* aValue); static bool GetBool(const char* aArg, const char* aOptionName, bool* aValue); public: explicit Options(const char* aDMDEnvVar); bool IsLiveMode() const { return mMode == Mode::Live; } bool IsDarkMatterMode() const { return mMode == Mode::DarkMatter; } bool IsCumulativeMode() const { return mMode == Mode::Cumulative; } bool IsScanMode() const { return mMode == Mode::Scan; } const char* ModeString() const; const char* DMDEnvVar() const { return mDMDEnvVar; } bool DoFullStacks() const { return mStacks == Stacks::Full; } size_t ShowDumpStats() const { return mShowDumpStats; } }; static Options* gOptions; //--------------------------------------------------------------------------- // The global lock //--------------------------------------------------------------------------- // MutexBase implements the platform-specific parts of a mutex. #ifdef XP_WIN class MutexBase { CRITICAL_SECTION mCS; DISALLOW_COPY_AND_ASSIGN(MutexBase); public: MutexBase() { InitializeCriticalSection(&mCS); } ~MutexBase() { DeleteCriticalSection(&mCS); } void Lock() { EnterCriticalSection(&mCS); } void Unlock() { LeaveCriticalSection(&mCS); } }; #else class MutexBase { pthread_mutex_t mMutex; MutexBase(const MutexBase&) = delete; const MutexBase& operator=(const MutexBase&) = delete; public: MutexBase() { pthread_mutex_init(&mMutex, nullptr); } void Lock() { pthread_mutex_lock(&mMutex); } void Unlock() { pthread_mutex_unlock(&mMutex); } }; #endif class Mutex : private MutexBase { bool mIsLocked; Mutex(const Mutex&) = delete; const Mutex& operator=(const Mutex&) = delete; public: Mutex() : mIsLocked(false) {} void Lock() { MutexBase::Lock(); MOZ_ASSERT(!mIsLocked); mIsLocked = true; } void Unlock() { MOZ_ASSERT(mIsLocked); mIsLocked = false; MutexBase::Unlock(); } bool IsLocked() { return mIsLocked; } }; // This lock must be held while manipulating global state such as // gStackTraceTable, gLiveBlockTable, gDeadBlockTable. Note that gOptions is // *not* protected by this lock because it is only written to by Options(), // which is only invoked at start-up and in ResetEverything(), which is only // used by SmokeDMD.cpp. static Mutex* gStateLock = nullptr; class AutoLockState { AutoLockState(const AutoLockState&) = delete; const AutoLockState& operator=(const AutoLockState&) = delete; public: AutoLockState() { gStateLock->Lock(); } ~AutoLockState() { gStateLock->Unlock(); } }; class AutoUnlockState { AutoUnlockState(const AutoUnlockState&) = delete; const AutoUnlockState& operator=(const AutoUnlockState&) = delete; public: AutoUnlockState() { gStateLock->Unlock(); } ~AutoUnlockState() { gStateLock->Lock(); } }; //--------------------------------------------------------------------------- // Per-thread blocking of intercepts //--------------------------------------------------------------------------- // On MacOS, the first __thread/thread_local access calls malloc, which leads // to an infinite loop. So we use pthread-based TLS instead, which somehow // doesn't have this problem. #if !defined(XP_DARWIN) # define DMD_THREAD_LOCAL(T) MOZ_THREAD_LOCAL(T) #else # define DMD_THREAD_LOCAL(T) \ detail::ThreadLocal #endif class Thread { // Required for allocation via InfallibleAllocPolicy::new_. friend class InfallibleAllocPolicy; // When true, this blocks intercepts, which allows malloc interception // functions to themselves call malloc. (Nb: for direct calls to malloc we // can just use InfallibleAllocPolicy::{malloc_,new_}, but we sometimes // indirectly call vanilla malloc via functions like MozStackWalk.) bool mBlockIntercepts; Thread() : mBlockIntercepts(false) {} Thread(const Thread&) = delete; const Thread& operator=(const Thread&) = delete; static DMD_THREAD_LOCAL(Thread*) tlsThread; public: static void Init() { if (!tlsThread.init()) { MOZ_CRASH(); } } static Thread* Fetch() { Thread* t = tlsThread.get(); if (MOZ_UNLIKELY(!t)) { // This memory is never freed, even if the thread dies. It's a leak, but // only a tiny one. t = InfallibleAllocPolicy::new_(); tlsThread.set(t); } return t; } bool BlockIntercepts() { MOZ_ASSERT(!mBlockIntercepts); return mBlockIntercepts = true; } bool UnblockIntercepts() { MOZ_ASSERT(mBlockIntercepts); return mBlockIntercepts = false; } bool InterceptsAreBlocked() const { return mBlockIntercepts; } }; DMD_THREAD_LOCAL(Thread*) Thread::tlsThread; // An object of this class must be created (on the stack) before running any // code that might allocate. class AutoBlockIntercepts { Thread* const mT; AutoBlockIntercepts(const AutoBlockIntercepts&) = delete; const AutoBlockIntercepts& operator=(const AutoBlockIntercepts&) = delete; public: explicit AutoBlockIntercepts(Thread* aT) : mT(aT) { mT->BlockIntercepts(); } ~AutoBlockIntercepts() { MOZ_ASSERT(mT->InterceptsAreBlocked()); mT->UnblockIntercepts(); } }; //--------------------------------------------------------------------------- // Location service //--------------------------------------------------------------------------- struct DescribeCodeAddressLock { static void Unlock() { gStateLock->Unlock(); } static void Lock() { gStateLock->Lock(); } static bool IsLocked() { return gStateLock->IsLocked(); } }; typedef CodeAddressService CodeAddressService; //--------------------------------------------------------------------------- // Stack traces //--------------------------------------------------------------------------- class StackTrace { public: static const uint32_t MaxFrames = 24; private: uint32_t mLength; // The number of PCs. const void* mPcs[MaxFrames]; // The PCs themselves. public: StackTrace() : mLength(0) {} StackTrace(const StackTrace& aOther) : mLength(aOther.mLength) { PodCopy(mPcs, aOther.mPcs, mLength); } uint32_t Length() const { return mLength; } const void* Pc(uint32_t i) const { MOZ_ASSERT(i < mLength); return mPcs[i]; } uint32_t Size() const { return mLength * sizeof(mPcs[0]); } // The stack trace returned by this function is interned in gStackTraceTable, // and so is immortal and unmovable. static const StackTrace* Get(Thread* aT); // Hash policy. typedef StackTrace* Lookup; static mozilla::HashNumber hash(const StackTrace* const& aSt) { return mozilla::HashBytes(aSt->mPcs, aSt->Size()); } static bool match(const StackTrace* const& aA, const StackTrace* const& aB) { return aA->mLength == aB->mLength && memcmp(aA->mPcs, aB->mPcs, aA->Size()) == 0; } private: static void StackWalkCallback(uint32_t aFrameNumber, void* aPc, void* aSp, void* aClosure) { StackTrace* st = (StackTrace*)aClosure; MOZ_ASSERT(st->mLength < MaxFrames); st->mPcs[st->mLength] = aPc; st->mLength++; MOZ_ASSERT(st->mLength == aFrameNumber); } }; typedef mozilla::HashSet StackTraceTable; static StackTraceTable* gStackTraceTable = nullptr; typedef mozilla::HashSet, InfallibleAllocPolicy> StackTraceSet; typedef mozilla::HashSet, InfallibleAllocPolicy> PointerSet; typedef mozilla::HashMap, InfallibleAllocPolicy> PointerIdMap; // We won't GC the stack trace table until it this many elements. static uint32_t gGCStackTraceTableWhenSizeExceeds = 4 * 1024; /* static */ const StackTrace* StackTrace::Get(Thread* aT) { MOZ_ASSERT(gStateLock->IsLocked()); MOZ_ASSERT(aT->InterceptsAreBlocked()); // On Windows, MozStackWalk can acquire a lock from the shared library // loader. Another thread might call malloc while holding that lock (when // loading a shared library). So we can't be in gStateLock during the call // to MozStackWalk. For details, see // https://bugzilla.mozilla.org/show_bug.cgi?id=374829#c8 // On Linux, something similar can happen; see bug 824340. // So let's just release it on all platforms. StackTrace tmp; { AutoUnlockState unlock; // In each of the following cases, skipFrames is chosen so that the // first frame in each stack trace is a replace_* function (or as close as // possible, given the vagaries of inlining on different platforms). #if defined(XP_WIN) && defined(_M_IX86) // This avoids MozStackWalk(), which causes unusably slow startup on Win32 // when it is called during static initialization (see bug 1241684). // // This code is cribbed from the Gecko Profiler, which also uses // FramePointerStackWalk() on Win32: Registers::SyncPopulate() for the // frame pointer, and GetStackTop() for the stack end. CONTEXT context; RtlCaptureContext(&context); void** fp = reinterpret_cast(context.Ebp); PNT_TIB pTib = reinterpret_cast(NtCurrentTeb()); void* stackEnd = static_cast(pTib->StackBase); FramePointerStackWalk(StackWalkCallback, MaxFrames, &tmp, fp, stackEnd); #elif defined(XP_MACOSX) // This avoids MozStackWalk(), which has become unusably slow on Mac due to // changes in libunwind. // // This code is cribbed from the Gecko Profiler, which also uses // FramePointerStackWalk() on Mac: Registers::SyncPopulate() for the frame // pointer, and GetStackTop() for the stack end. # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wframe-address" void** fp = reinterpret_cast(__builtin_frame_address(1)); # pragma GCC diagnostic pop void* stackEnd = pthread_get_stackaddr_np(pthread_self()); FramePointerStackWalk(StackWalkCallback, MaxFrames, &tmp, fp, stackEnd); #else MozStackWalk(StackWalkCallback, nullptr, MaxFrames, &tmp); #endif } StackTraceTable::AddPtr p = gStackTraceTable->lookupForAdd(&tmp); if (!p) { StackTrace* stnew = InfallibleAllocPolicy::new_(tmp); MOZ_ALWAYS_TRUE(gStackTraceTable->add(p, stnew)); } return *p; } //--------------------------------------------------------------------------- // Heap blocks //--------------------------------------------------------------------------- // This class combines a 2-byte-aligned pointer (i.e. one whose bottom bit // is zero) with a 1-bit tag. // // |T| is the pointer type, e.g. |int*|, not the pointed-to type. This makes // is easier to have const pointers, e.g. |TaggedPtr|. template class TaggedPtr { union { T mPtr; uintptr_t mUint; }; static const uintptr_t kTagMask = uintptr_t(0x1); static const uintptr_t kPtrMask = ~kTagMask; static bool IsTwoByteAligned(T aPtr) { return (uintptr_t(aPtr) & kTagMask) == 0; } public: TaggedPtr() : mPtr(nullptr) {} TaggedPtr(T aPtr, bool aBool) : mPtr(aPtr) { MOZ_ASSERT(IsTwoByteAligned(aPtr)); uintptr_t tag = uintptr_t(aBool); MOZ_ASSERT(tag <= kTagMask); mUint |= (tag & kTagMask); } void Set(T aPtr, bool aBool) { MOZ_ASSERT(IsTwoByteAligned(aPtr)); mPtr = aPtr; uintptr_t tag = uintptr_t(aBool); MOZ_ASSERT(tag <= kTagMask); mUint |= (tag & kTagMask); } T Ptr() const { return reinterpret_cast(mUint & kPtrMask); } bool Tag() const { return bool(mUint & kTagMask); } }; // A live heap block. Stores both basic data and data about reports, if we're // in DarkMatter mode. class LiveBlock { const void* mPtr; const size_t mReqSize; // size requested // The stack trace where this block was allocated, or nullptr if we didn't // record one. const StackTrace* const mAllocStackTrace; // This array has two elements because we record at most two reports of a // block. // - Ptr: |mReportStackTrace| - stack trace where this block was reported. // nullptr if not reported. // - Tag bit 0: |mReportedOnAlloc| - was the block reported immediately on // allocation? If so, DMD must not clear the report at the end of // Analyze(). Only relevant if |mReportStackTrace| is non-nullptr. // // |mPtr| is used as the key in LiveBlockTable, so it's ok for this member // to be |mutable|. // // Only used in DarkMatter mode. mutable TaggedPtr mReportStackTrace_mReportedOnAlloc[2]; public: LiveBlock(const void* aPtr, size_t aReqSize, const StackTrace* aAllocStackTrace) : mPtr(aPtr), mReqSize(aReqSize), mAllocStackTrace(aAllocStackTrace), mReportStackTrace_mReportedOnAlloc() // all fields get zeroed {} const void* Address() const { return mPtr; } size_t ReqSize() const { return mReqSize; } size_t SlopSize() const { return MallocSizeOf(mPtr) - mReqSize; } const StackTrace* AllocStackTrace() const { return mAllocStackTrace; } const StackTrace* ReportStackTrace1() const { MOZ_ASSERT(gOptions->IsDarkMatterMode()); return mReportStackTrace_mReportedOnAlloc[0].Ptr(); } const StackTrace* ReportStackTrace2() const { MOZ_ASSERT(gOptions->IsDarkMatterMode()); return mReportStackTrace_mReportedOnAlloc[1].Ptr(); } bool ReportedOnAlloc1() const { MOZ_ASSERT(gOptions->IsDarkMatterMode()); return mReportStackTrace_mReportedOnAlloc[0].Tag(); } bool ReportedOnAlloc2() const { MOZ_ASSERT(gOptions->IsDarkMatterMode()); return mReportStackTrace_mReportedOnAlloc[1].Tag(); } void AddStackTracesToTable(StackTraceSet& aStackTraces) const { if (AllocStackTrace()) { MOZ_ALWAYS_TRUE(aStackTraces.put(AllocStackTrace())); } if (gOptions->IsDarkMatterMode()) { if (ReportStackTrace1()) { MOZ_ALWAYS_TRUE(aStackTraces.put(ReportStackTrace1())); } if (ReportStackTrace2()) { MOZ_ALWAYS_TRUE(aStackTraces.put(ReportStackTrace2())); } } } uint32_t NumReports() const { MOZ_ASSERT(gOptions->IsDarkMatterMode()); if (ReportStackTrace2()) { MOZ_ASSERT(ReportStackTrace1()); return 2; } if (ReportStackTrace1()) { return 1; } return 0; } // This is |const| thanks to the |mutable| fields above. void Report(Thread* aT, bool aReportedOnAlloc) const { MOZ_ASSERT(gOptions->IsDarkMatterMode()); // We don't bother recording reports after the 2nd one. uint32_t numReports = NumReports(); if (numReports < 2) { mReportStackTrace_mReportedOnAlloc[numReports].Set(StackTrace::Get(aT), aReportedOnAlloc); } } void UnreportIfNotReportedOnAlloc() const { MOZ_ASSERT(gOptions->IsDarkMatterMode()); if (!ReportedOnAlloc1() && !ReportedOnAlloc2()) { mReportStackTrace_mReportedOnAlloc[0].Set(nullptr, 0); mReportStackTrace_mReportedOnAlloc[1].Set(nullptr, 0); } else if (!ReportedOnAlloc1() && ReportedOnAlloc2()) { // Shift the 2nd report down to the 1st one. mReportStackTrace_mReportedOnAlloc[0] = mReportStackTrace_mReportedOnAlloc[1]; mReportStackTrace_mReportedOnAlloc[1].Set(nullptr, 0); } else if (ReportedOnAlloc1() && !ReportedOnAlloc2()) { mReportStackTrace_mReportedOnAlloc[1].Set(nullptr, 0); } } // Hash policy. typedef const void* Lookup; static mozilla::HashNumber hash(const void* const& aPtr) { return mozilla::HashGeneric(aPtr); } static bool match(const LiveBlock& aB, const void* const& aPtr) { return aB.mPtr == aPtr; } }; // A table of live blocks where the lookup key is the block address. typedef mozilla::HashSet LiveBlockTable; static LiveBlockTable* gLiveBlockTable = nullptr; class AggregatedLiveBlockHashPolicy { public: typedef const LiveBlock* const Lookup; static mozilla::HashNumber hash(const LiveBlock* const& aB) { return gOptions->IsDarkMatterMode() ? mozilla::HashGeneric( aB->ReqSize(), aB->SlopSize(), aB->AllocStackTrace(), aB->ReportedOnAlloc1(), aB->ReportedOnAlloc2()) : mozilla::HashGeneric(aB->ReqSize(), aB->SlopSize(), aB->AllocStackTrace()); } static bool match(const LiveBlock* const& aA, const LiveBlock* const& aB) { return gOptions->IsDarkMatterMode() ? aA->ReqSize() == aB->ReqSize() && aA->SlopSize() == aB->SlopSize() && aA->AllocStackTrace() == aB->AllocStackTrace() && aA->ReportStackTrace1() == aB->ReportStackTrace1() && aA->ReportStackTrace2() == aB->ReportStackTrace2() : aA->ReqSize() == aB->ReqSize() && aA->SlopSize() == aB->SlopSize() && aA->AllocStackTrace() == aB->AllocStackTrace(); } }; // A table of live blocks where the lookup key is everything but the block // address. For aggregating similar live blocks at output time. typedef mozilla::HashMap AggregatedLiveBlockTable; // A freed heap block. class DeadBlock { const size_t mReqSize; // size requested const size_t mSlopSize; // slop above size requested // The stack trace where this block was allocated. const StackTrace* const mAllocStackTrace; public: DeadBlock() : mReqSize(0), mSlopSize(0), mAllocStackTrace(nullptr) {} explicit DeadBlock(const LiveBlock& aLb) : mReqSize(aLb.ReqSize()), mSlopSize(aLb.SlopSize()), mAllocStackTrace(aLb.AllocStackTrace()) {} ~DeadBlock() {} size_t ReqSize() const { return mReqSize; } size_t SlopSize() const { return mSlopSize; } const StackTrace* AllocStackTrace() const { return mAllocStackTrace; } void AddStackTracesToTable(StackTraceSet& aStackTraces) const { if (AllocStackTrace()) { MOZ_ALWAYS_TRUE(aStackTraces.put(AllocStackTrace())); } } // Hash policy. typedef DeadBlock Lookup; static mozilla::HashNumber hash(const DeadBlock& aB) { return mozilla::HashGeneric(aB.ReqSize(), aB.SlopSize(), aB.AllocStackTrace()); } static bool match(const DeadBlock& aA, const DeadBlock& aB) { return aA.ReqSize() == aB.ReqSize() && aA.SlopSize() == aB.SlopSize() && aA.AllocStackTrace() == aB.AllocStackTrace(); } }; // For each unique DeadBlock value we store a count of how many actual dead // blocks have that value. typedef mozilla::HashMap DeadBlockTable; static DeadBlockTable* gDeadBlockTable = nullptr; // Add the dead block to the dead block table, if that's appropriate. void MaybeAddToDeadBlockTable(const DeadBlock& aDb) { if (gOptions->IsCumulativeMode() && aDb.AllocStackTrace()) { AutoLockState lock; if (DeadBlockTable::AddPtr p = gDeadBlockTable->lookupForAdd(aDb)) { p->value() += 1; } else { MOZ_ALWAYS_TRUE(gDeadBlockTable->add(p, aDb, 1)); } } } // Add a pointer to each live stack trace into the given StackTraceSet. (A // stack trace is live if it's used by one of the live blocks.) static void GatherUsedStackTraces(StackTraceSet& aStackTraces) { MOZ_ASSERT(gStateLock->IsLocked()); MOZ_ASSERT(Thread::Fetch()->InterceptsAreBlocked()); aStackTraces.clear(); MOZ_ALWAYS_TRUE(aStackTraces.reserve(512)); for (auto iter = gLiveBlockTable->iter(); !iter.done(); iter.next()) { iter.get().AddStackTracesToTable(aStackTraces); } for (auto iter = gDeadBlockTable->iter(); !iter.done(); iter.next()) { iter.get().key().AddStackTracesToTable(aStackTraces); } } // Delete stack traces that we aren't using, and compact our hashtable. static void GCStackTraces() { MOZ_ASSERT(gStateLock->IsLocked()); MOZ_ASSERT(Thread::Fetch()->InterceptsAreBlocked()); StackTraceSet usedStackTraces; GatherUsedStackTraces(usedStackTraces); // Delete all unused stack traces from gStackTraceTable. The ModIterator // destructor will automatically rehash and compact the table. for (auto iter = gStackTraceTable->modIter(); !iter.done(); iter.next()) { StackTrace* const& st = iter.get(); if (!usedStackTraces.has(st)) { iter.remove(); InfallibleAllocPolicy::delete_(st); } } // Schedule a GC when we have twice as many stack traces as we had right after // this GC finished. gGCStackTraceTableWhenSizeExceeds = 2 * gStackTraceTable->count(); } //--------------------------------------------------------------------------- // malloc/free callbacks //--------------------------------------------------------------------------- static FastBernoulliTrial* gBernoulli; // In testing, a probability of 0.003 resulted in ~25% of heap blocks getting // a stack trace and ~80% of heap bytes getting a stack trace. (This is // possible because big heap blocks are more likely to get a stack trace.) // // We deliberately choose not to give the user control over this probability // (other than effectively setting it to 1 via --stacks=full) because it's // quite inscrutable and generally the user just wants "faster and imprecise" // or "slower and precise". // // The random number seeds are arbitrary and were obtained from random.org. If // you change them you'll need to change the tests as well, because their // expected output is based on the particular sequence of trial results that we // get with these seeds. static void ResetBernoulli() { new (gBernoulli) FastBernoulliTrial(0.003, 0x8e26eeee166bc8ca, 0x56820f304a9c9ae0); } static void AllocCallback(void* aPtr, size_t aReqSize, Thread* aT) { if (!aPtr) { return; } AutoLockState lock; AutoBlockIntercepts block(aT); size_t actualSize = gMallocTable.malloc_usable_size(aPtr); // We may or may not record the allocation stack trace, depending on the // options and the outcome of a Bernoulli trial. bool getTrace = gOptions->DoFullStacks() || gBernoulli->trial(actualSize); LiveBlock b(aPtr, aReqSize, getTrace ? StackTrace::Get(aT) : nullptr); LiveBlockTable::AddPtr p = gLiveBlockTable->lookupForAdd(aPtr); if (!p) { // Most common case: there wasn't a record already. MOZ_ALWAYS_TRUE(gLiveBlockTable->add(p, b)); } else { // Edge-case: there was a record for the same address. We'll assume the // allocator is not giving out a pointer to an existing allocation, so // this means the previously recorded allocation was freed while we were // blocking interceptions. This can happen while processing the data in // e.g. AnalyzeImpl. if (gOptions->IsCumulativeMode()) { // Copy it out so it can be added to the dead block list later. DeadBlock db(*p); MaybeAddToDeadBlockTable(db); } gLiveBlockTable->remove(p); MOZ_ALWAYS_TRUE(gLiveBlockTable->putNew(aPtr, b)); } } static void FreeCallback(void* aPtr, Thread* aT, DeadBlock* aDeadBlock) { if (!aPtr) { return; } AutoLockState lock; AutoBlockIntercepts block(aT); if (LiveBlockTable::Ptr lb = gLiveBlockTable->lookup(aPtr)) { if (gOptions->IsCumulativeMode()) { // Copy it out so it can be added to the dead block list later. new (aDeadBlock) DeadBlock(*lb); } gLiveBlockTable->remove(lb); } else { // We have no record of the block. It must be a bogus pointer, or one that // DMD wasn't able to see allocated. This should be extremely rare. } if (gStackTraceTable->count() > gGCStackTraceTableWhenSizeExceeds) { GCStackTraces(); } } //--------------------------------------------------------------------------- // malloc/free interception //--------------------------------------------------------------------------- static bool Init(malloc_table_t* aMallocTable); } // namespace dmd } // namespace mozilla static void* replace_malloc(size_t aSize) { using namespace mozilla::dmd; Thread* t = Thread::Fetch(); if (t->InterceptsAreBlocked()) { // Intercepts are blocked, which means this must be a call to malloc // triggered indirectly by DMD (e.g. via MozStackWalk). Be infallible. return InfallibleAllocPolicy::malloc_(aSize); } // This must be a call to malloc from outside DMD. Intercept it. void* ptr = gMallocTable.malloc(aSize); AllocCallback(ptr, aSize, t); return ptr; } static void* replace_calloc(size_t aCount, size_t aSize) { using namespace mozilla::dmd; Thread* t = Thread::Fetch(); if (t->InterceptsAreBlocked()) { return InfallibleAllocPolicy::calloc_(aCount, aSize); } // |aCount * aSize| could overflow, but if that happens then // |gMallocTable.calloc()| will return nullptr and |AllocCallback()| will // return immediately without using the overflowed value. void* ptr = gMallocTable.calloc(aCount, aSize); AllocCallback(ptr, aCount * aSize, t); return ptr; } static void* replace_realloc(void* aOldPtr, size_t aSize) { using namespace mozilla::dmd; Thread* t = Thread::Fetch(); if (t->InterceptsAreBlocked()) { return InfallibleAllocPolicy::realloc_(aOldPtr, aSize); } // If |aOldPtr| is nullptr, the call is equivalent to |malloc(aSize)|. if (!aOldPtr) { return replace_malloc(aSize); } // Be very careful here! Must remove the block from the table before doing // the realloc to avoid races, just like in replace_free(). // Nb: This does an unnecessary hashtable remove+add if the block doesn't // move, but doing better isn't worth the effort. DeadBlock db; FreeCallback(aOldPtr, t, &db); void* ptr = gMallocTable.realloc(aOldPtr, aSize); if (ptr) { AllocCallback(ptr, aSize, t); MaybeAddToDeadBlockTable(db); } else { // If realloc fails, we undo the prior operations by re-inserting the old // pointer into the live block table. We don't have to do anything with the // dead block list because the dead block hasn't yet been inserted. The // block will end up looking like it was allocated for the first time here, // which is untrue, and the slop bytes will be zero, which may be untrue. // But this case is rare and doing better isn't worth the effort. AllocCallback(aOldPtr, gMallocTable.malloc_usable_size(aOldPtr), t); } return ptr; } static void* replace_memalign(size_t aAlignment, size_t aSize) { using namespace mozilla::dmd; Thread* t = Thread::Fetch(); if (t->InterceptsAreBlocked()) { return InfallibleAllocPolicy::memalign_(aAlignment, aSize); } void* ptr = gMallocTable.memalign(aAlignment, aSize); AllocCallback(ptr, aSize, t); return ptr; } static void replace_free(void* aPtr) { using namespace mozilla::dmd; Thread* t = Thread::Fetch(); if (t->InterceptsAreBlocked()) { return InfallibleAllocPolicy::free_(aPtr); } // Do the actual free after updating the table. Otherwise, another thread // could call malloc and get the freed block and update the table, and then // our update here would remove the newly-malloc'd block. DeadBlock db; FreeCallback(aPtr, t, &db); MaybeAddToDeadBlockTable(db); gMallocTable.free(aPtr); } void replace_init(malloc_table_t* aMallocTable, ReplaceMallocBridge** aBridge) { if (mozilla::dmd::Init(aMallocTable)) { #define MALLOC_FUNCS MALLOC_FUNCS_MALLOC_BASE #define MALLOC_DECL(name, ...) aMallocTable->name = replace_##name; #include "malloc_decls.h" *aBridge = mozilla::dmd::gDMDBridge; } } namespace mozilla { namespace dmd { //--------------------------------------------------------------------------- // Options (Part 2) //--------------------------------------------------------------------------- // Given an |aOptionName| like "foo", succeed if |aArg| has the form "foo=blah" // (where "blah" is non-empty) and return the pointer to "blah". |aArg| can // have leading space chars (but not other whitespace). const char* Options::ValueIfMatch(const char* aArg, const char* aOptionName) { MOZ_ASSERT(!isspace(*aArg)); // any leading whitespace should not remain size_t optionLen = strlen(aOptionName); if (strncmp(aArg, aOptionName, optionLen) == 0 && aArg[optionLen] == '=' && aArg[optionLen + 1]) { return aArg + optionLen + 1; } return nullptr; } // Extracts a |long| value for an option from an argument. It must be within // the range |aMin..aMax| (inclusive). bool Options::GetLong(const char* aArg, const char* aOptionName, long aMin, long aMax, long* aValue) { if (const char* optionValue = ValueIfMatch(aArg, aOptionName)) { char* endPtr; *aValue = strtol(optionValue, &endPtr, /* base */ 10); if (!*endPtr && aMin <= *aValue && *aValue <= aMax && *aValue != LONG_MIN && *aValue != LONG_MAX) { return true; } } return false; } // Extracts a |bool| value for an option -- encoded as "yes" or "no" -- from an // argument. bool Options::GetBool(const char* aArg, const char* aOptionName, bool* aValue) { if (const char* optionValue = ValueIfMatch(aArg, aOptionName)) { if (strcmp(optionValue, "yes") == 0) { *aValue = true; return true; } if (strcmp(optionValue, "no") == 0) { *aValue = false; return true; } } return false; } Options::Options(const char* aDMDEnvVar) : mDMDEnvVar(aDMDEnvVar ? InfallibleAllocPolicy::strdup_(aDMDEnvVar) : nullptr), mMode(Mode::DarkMatter), mStacks(Stacks::Partial), mShowDumpStats(false) { char* e = mDMDEnvVar; if (e && strcmp(e, "1") != 0) { bool isEnd = false; while (!isEnd) { // Consume leading whitespace. while (isspace(*e)) { e++; } // Save the start of the arg. const char* arg = e; // Find the first char after the arg, and temporarily change it to '\0' // to isolate the arg. while (!isspace(*e) && *e != '\0') { e++; } char replacedChar = *e; isEnd = replacedChar == '\0'; *e = '\0'; // Handle arg bool myBool; if (strcmp(arg, "--mode=live") == 0) { mMode = Mode::Live; } else if (strcmp(arg, "--mode=dark-matter") == 0) { mMode = Mode::DarkMatter; } else if (strcmp(arg, "--mode=cumulative") == 0) { mMode = Mode::Cumulative; } else if (strcmp(arg, "--mode=scan") == 0) { mMode = Mode::Scan; } else if (strcmp(arg, "--stacks=full") == 0) { mStacks = Stacks::Full; } else if (strcmp(arg, "--stacks=partial") == 0) { mStacks = Stacks::Partial; } else if (GetBool(arg, "--show-dump-stats", &myBool)) { mShowDumpStats = myBool; } else if (strcmp(arg, "") == 0) { // This can only happen if there is trailing whitespace. Ignore. MOZ_ASSERT(isEnd); } else { BadArg(arg); } // Undo the temporary isolation. *e = replacedChar; } } if (mMode == Mode::Scan) { mStacks = Stacks::Full; } } void Options::BadArg(const char* aArg) { StatusMsg("\n"); StatusMsg("Bad entry in the $DMD environment variable: '%s'.\n", aArg); StatusMsg("See the output of |mach help run| for the allowed options.\n"); exit(1); } const char* Options::ModeString() const { switch (mMode) { case Mode::Live: return "live"; case Mode::DarkMatter: return "dark-matter"; case Mode::Cumulative: return "cumulative"; case Mode::Scan: return "scan"; default: MOZ_ASSERT(false); return "(unknown DMD mode)"; } } //--------------------------------------------------------------------------- // DMD start-up //--------------------------------------------------------------------------- #ifndef XP_WIN static void prefork() { if (gStateLock) { gStateLock->Lock(); } } static void postfork() { if (gStateLock) { gStateLock->Unlock(); } } #endif // WARNING: this function runs *very* early -- before all static initializers // have run. For this reason, non-scalar globals such as gStateLock and // gStackTraceTable are allocated dynamically (so we can guarantee their // construction in this function) rather than statically. static bool Init(malloc_table_t* aMallocTable) { // DMD is controlled by the |DMD| environment variable. const char* e = getenv("DMD"); if (!e) { return false; } // Initialize the function table first, because StatusMsg uses // InfallibleAllocPolicy::malloc_, which uses it. gMallocTable = *aMallocTable; StatusMsg("$DMD = '%s'\n", e); gDMDBridge = InfallibleAllocPolicy::new_(); #ifndef XP_WIN // Avoid deadlocks when forking by acquiring our state lock prior to forking // and releasing it after forking. See |LogAlloc|'s |replace_init| for // in-depth details. // // Note: This must run after attempting an allocation so as to give the // system malloc a chance to insert its own atfork handler. pthread_atfork(prefork, postfork, postfork); #endif // Parse $DMD env var. gOptions = InfallibleAllocPolicy::new_(e); gStateLock = InfallibleAllocPolicy::new_(); gBernoulli = (FastBernoulliTrial*)InfallibleAllocPolicy::malloc_( sizeof(FastBernoulliTrial)); ResetBernoulli(); Thread::Init(); { AutoLockState lock; gStackTraceTable = InfallibleAllocPolicy::new_(8192); gLiveBlockTable = InfallibleAllocPolicy::new_(8192); // Create this even if the mode isn't Cumulative (albeit with a small // size), in case the mode is changed later on (as is done by SmokeDMD.cpp, // for example). size_t tableSize = gOptions->IsCumulativeMode() ? 8192 : 4; gDeadBlockTable = InfallibleAllocPolicy::new_(tableSize); } return true; } //--------------------------------------------------------------------------- // Block reporting and unreporting //--------------------------------------------------------------------------- static void ReportHelper(const void* aPtr, bool aReportedOnAlloc) { if (!gOptions->IsDarkMatterMode() || !aPtr) { return; } Thread* t = Thread::Fetch(); AutoBlockIntercepts block(t); AutoLockState lock; if (LiveBlockTable::Ptr p = gLiveBlockTable->lookup(aPtr)) { p->Report(t, aReportedOnAlloc); } else { // We have no record of the block. It must be a bogus pointer. This should // be extremely rare because Report() is almost always called in // conjunction with a malloc_size_of-style function. Print a message so // that we get some feedback. StatusMsg("Unknown pointer %p\n", aPtr); } } void DMDFuncs::Report(const void* aPtr) { ReportHelper(aPtr, /* onAlloc */ false); } void DMDFuncs::ReportOnAlloc(const void* aPtr) { ReportHelper(aPtr, /* onAlloc */ true); } //--------------------------------------------------------------------------- // DMD output //--------------------------------------------------------------------------- // The version number of the output format. Increment this if you make // backwards-incompatible changes to the format. See DMD.h for the version // history. static const int kOutputVersionNumber = 5; // Note that, unlike most SizeOf* functions, this function does not take a // |mozilla::MallocSizeOf| argument. That's because those arguments are // primarily to aid DMD track heap blocks... but DMD deliberately doesn't track // heap blocks it allocated for itself! // // SizeOfInternal should be called while you're holding the state lock and // while intercepts are blocked; SizeOf acquires the lock and blocks // intercepts. static void SizeOfInternal(Sizes* aSizes) { MOZ_ASSERT(gStateLock->IsLocked()); MOZ_ASSERT(Thread::Fetch()->InterceptsAreBlocked()); aSizes->Clear(); StackTraceSet usedStackTraces; GatherUsedStackTraces(usedStackTraces); for (auto iter = gStackTraceTable->iter(); !iter.done(); iter.next()) { StackTrace* const& st = iter.get(); if (usedStackTraces.has(st)) { aSizes->mStackTracesUsed += MallocSizeOf(st); } else { aSizes->mStackTracesUnused += MallocSizeOf(st); } } aSizes->mStackTraceTable = gStackTraceTable->shallowSizeOfIncludingThis(MallocSizeOf); aSizes->mLiveBlockTable = gLiveBlockTable->shallowSizeOfIncludingThis(MallocSizeOf); aSizes->mDeadBlockTable = gDeadBlockTable->shallowSizeOfIncludingThis(MallocSizeOf); } void DMDFuncs::SizeOf(Sizes* aSizes) { aSizes->Clear(); AutoBlockIntercepts block(Thread::Fetch()); AutoLockState lock; SizeOfInternal(aSizes); } void DMDFuncs::ClearReports() { if (!gOptions->IsDarkMatterMode()) { return; } AutoLockState lock; // Unreport all blocks that were marked reported by a memory reporter. This // excludes those that were reported on allocation, because they need to keep // their reported marking. for (auto iter = gLiveBlockTable->iter(); !iter.done(); iter.next()) { iter.get().UnreportIfNotReportedOnAlloc(); } } class ToIdStringConverter final { public: ToIdStringConverter() : mIdMap(512), mNextId(0) {} // Converts a pointer to a unique ID. Reuses the existing ID for the pointer // if it's been seen before. const char* ToIdString(const void* aPtr) { uint32_t id; PointerIdMap::AddPtr p = mIdMap.lookupForAdd(aPtr); if (!p) { id = mNextId++; MOZ_ALWAYS_TRUE(mIdMap.add(p, aPtr, id)); } else { id = p->value(); } return Base32(id); } size_t sizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const { return mIdMap.shallowSizeOfExcludingThis(aMallocSizeOf); } private: // This function converts an integer to base-32. We use base-32 values for // indexing into the traceTable and the frameTable, for the following reasons. // // - Base-32 gives more compact indices than base-16. // // - 32 is a power-of-two, which makes the necessary div/mod calculations // fast. // // - We can (and do) choose non-numeric digits for base-32. When // inspecting/debugging the JSON output, non-numeric indices are easier to // search for than numeric indices. // char* Base32(uint32_t aN) { static const char digits[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef"; char* b = mIdBuf + kIdBufLen - 1; *b = '\0'; do { b--; if (b == mIdBuf) { MOZ_CRASH("Base32 buffer too small"); } *b = digits[aN % 32]; aN /= 32; } while (aN); return b; } PointerIdMap mIdMap; uint32_t mNextId; // |mIdBuf| must have space for at least eight chars, which is the space // needed to hold 'Dffffff' (including the terminating null char), which is // the base-32 representation of 0xffffffff. static const size_t kIdBufLen = 16; char mIdBuf[kIdBufLen]; }; // Helper class for converting a pointer value to a string. class ToStringConverter { public: const char* ToPtrString(const void* aPtr) { snprintf(kPtrBuf, sizeof(kPtrBuf) - 1, "%" PRIxPTR, (uintptr_t)aPtr); return kPtrBuf; } private: char kPtrBuf[32]; }; static void WriteBlockContents(JSONWriter& aWriter, const LiveBlock& aBlock) { size_t numWords = aBlock.ReqSize() / sizeof(uintptr_t*); if (numWords == 0) { return; } aWriter.StartArrayProperty("contents", aWriter.SingleLineStyle); { const uintptr_t** block = (const uintptr_t**)aBlock.Address(); ToStringConverter sc; for (size_t i = 0; i < numWords; ++i) { aWriter.StringElement(MakeStringSpan(sc.ToPtrString(block[i]))); } } aWriter.EndArray(); } static void AnalyzeImpl(UniquePtr aWriter) { // Some blocks may have been allocated while creating |aWriter|. Those blocks // will be freed at the end of this function when |write| is destroyed. The // allocations will have occurred while intercepts were not blocked, so the // frees better be as well, otherwise we'll get assertion failures. // Therefore, this declaration must precede the AutoBlockIntercepts // declaration, to ensure that |write| is destroyed *after* intercepts are // unblocked. JSONWriter writer(std::move(aWriter)); AutoBlockIntercepts block(Thread::Fetch()); AutoLockState lock; // Allocate this on the heap instead of the stack because it's fairly large. auto locService = InfallibleAllocPolicy::new_(); StackTraceSet usedStackTraces(512); PointerSet usedPcs(512); size_t iscSize; static int analysisCount = 1; StatusMsg("Dump %d {\n", analysisCount++); writer.Start(); { writer.IntProperty("version", kOutputVersionNumber); writer.StartObjectProperty("invocation"); { const char* var = gOptions->DMDEnvVar(); if (var) { writer.StringProperty("dmdEnvVar", MakeStringSpan(var)); } else { writer.NullProperty("dmdEnvVar"); } writer.StringProperty("mode", MakeStringSpan(gOptions->ModeString())); } writer.EndObject(); StatusMsg(" Constructing the heap block list...\n"); ToIdStringConverter isc; ToStringConverter sc; writer.StartArrayProperty("blockList"); { // Lambda that writes out a live block. auto writeLiveBlock = [&](const LiveBlock& aB, size_t aNum) { aB.AddStackTracesToTable(usedStackTraces); MOZ_ASSERT_IF(gOptions->IsScanMode(), aNum == 1); writer.StartObjectElement(writer.SingleLineStyle); { if (gOptions->IsScanMode()) { writer.StringProperty("addr", MakeStringSpan(sc.ToPtrString(aB.Address()))); WriteBlockContents(writer, aB); } writer.IntProperty("req", aB.ReqSize()); if (aB.SlopSize() > 0) { writer.IntProperty("slop", aB.SlopSize()); } if (aB.AllocStackTrace()) { writer.StringProperty( "alloc", MakeStringSpan(isc.ToIdString(aB.AllocStackTrace()))); } if (gOptions->IsDarkMatterMode() && aB.NumReports() > 0) { writer.StartArrayProperty("reps"); { if (aB.ReportStackTrace1()) { writer.StringElement( MakeStringSpan(isc.ToIdString(aB.ReportStackTrace1()))); } if (aB.ReportStackTrace2()) { writer.StringElement( MakeStringSpan(isc.ToIdString(aB.ReportStackTrace2()))); } } writer.EndArray(); } if (aNum > 1) { writer.IntProperty("num", aNum); } } writer.EndObject(); }; // Live blocks. if (!gOptions->IsScanMode()) { // At this point we typically have many LiveBlocks that differ only in // their address. Aggregate them to reduce the size of the output file. AggregatedLiveBlockTable agg(8192); for (auto iter = gLiveBlockTable->iter(); !iter.done(); iter.next()) { const LiveBlock& b = iter.get(); b.AddStackTracesToTable(usedStackTraces); if (AggregatedLiveBlockTable::AddPtr p = agg.lookupForAdd(&b)) { p->value() += 1; } else { MOZ_ALWAYS_TRUE(agg.add(p, &b, 1)); } } // Now iterate over the aggregated table. for (auto iter = agg.iter(); !iter.done(); iter.next()) { const LiveBlock& b = *iter.get().key(); size_t num = iter.get().value(); writeLiveBlock(b, num); } } else { // In scan mode we cannot aggregate because we print each live block's // address and contents. for (auto iter = gLiveBlockTable->iter(); !iter.done(); iter.next()) { const LiveBlock& b = iter.get(); b.AddStackTracesToTable(usedStackTraces); writeLiveBlock(b, 1); } } // Dead blocks. for (auto iter = gDeadBlockTable->iter(); !iter.done(); iter.next()) { const DeadBlock& b = iter.get().key(); b.AddStackTracesToTable(usedStackTraces); size_t num = iter.get().value(); MOZ_ASSERT(num > 0); writer.StartObjectElement(writer.SingleLineStyle); { writer.IntProperty("req", b.ReqSize()); if (b.SlopSize() > 0) { writer.IntProperty("slop", b.SlopSize()); } if (b.AllocStackTrace()) { writer.StringProperty( "alloc", MakeStringSpan(isc.ToIdString(b.AllocStackTrace()))); } if (num > 1) { writer.IntProperty("num", num); } } writer.EndObject(); } } writer.EndArray(); StatusMsg(" Constructing the stack trace table...\n"); writer.StartObjectProperty("traceTable"); { for (auto iter = usedStackTraces.iter(); !iter.done(); iter.next()) { const StackTrace* const st = iter.get(); writer.StartArrayProperty(MakeStringSpan(isc.ToIdString(st)), writer.SingleLineStyle); { for (uint32_t i = 0; i < st->Length(); i++) { const void* pc = st->Pc(i); writer.StringElement(MakeStringSpan(isc.ToIdString(pc))); MOZ_ALWAYS_TRUE(usedPcs.put(pc)); } } writer.EndArray(); } } writer.EndObject(); StatusMsg(" Constructing the stack frame table...\n"); writer.StartObjectProperty("frameTable"); { static const size_t locBufLen = 1024; char locBuf[locBufLen]; for (auto iter = usedPcs.iter(); !iter.done(); iter.next()) { const void* const pc = iter.get(); // Use 0 for the frame number. See the JSON format description comment // in DMD.h to understand why. locService->GetLocation(0, pc, locBuf, locBufLen); writer.StringProperty(MakeStringSpan(isc.ToIdString(pc)), MakeStringSpan(locBuf)); } } writer.EndObject(); iscSize = isc.sizeOfExcludingThis(MallocSizeOf); } writer.End(); if (gOptions->ShowDumpStats()) { Sizes sizes; SizeOfInternal(&sizes); static const size_t kBufLen = 64; char buf1[kBufLen]; char buf2[kBufLen]; char buf3[kBufLen]; StatusMsg(" Execution measurements {\n"); StatusMsg(" Data structures that persist after Dump() ends {\n"); StatusMsg(" Used stack traces: %10s bytes\n", Show(sizes.mStackTracesUsed, buf1, kBufLen)); StatusMsg(" Unused stack traces: %10s bytes\n", Show(sizes.mStackTracesUnused, buf1, kBufLen)); StatusMsg(" Stack trace table: %10s bytes (%s entries, %s used)\n", Show(sizes.mStackTraceTable, buf1, kBufLen), Show(gStackTraceTable->capacity(), buf2, kBufLen), Show(gStackTraceTable->count(), buf3, kBufLen)); StatusMsg(" Live block table: %10s bytes (%s entries, %s used)\n", Show(sizes.mLiveBlockTable, buf1, kBufLen), Show(gLiveBlockTable->capacity(), buf2, kBufLen), Show(gLiveBlockTable->count(), buf3, kBufLen)); StatusMsg(" Dead block table: %10s bytes (%s entries, %s used)\n", Show(sizes.mDeadBlockTable, buf1, kBufLen), Show(gDeadBlockTable->capacity(), buf2, kBufLen), Show(gDeadBlockTable->count(), buf3, kBufLen)); StatusMsg(" }\n"); StatusMsg(" Data structures that are destroyed after Dump() ends {\n"); StatusMsg( " Location service: %10s bytes\n", Show(locService->SizeOfIncludingThis(MallocSizeOf), buf1, kBufLen)); StatusMsg(" Used stack traces set: %10s bytes\n", Show(usedStackTraces.shallowSizeOfExcludingThis(MallocSizeOf), buf1, kBufLen)); StatusMsg( " Used PCs set: %10s bytes\n", Show(usedPcs.shallowSizeOfExcludingThis(MallocSizeOf), buf1, kBufLen)); StatusMsg(" Pointer ID map: %10s bytes\n", Show(iscSize, buf1, kBufLen)); StatusMsg(" }\n"); StatusMsg(" Counts {\n"); size_t hits = locService->NumCacheHits(); size_t misses = locService->NumCacheMisses(); size_t requests = hits + misses; StatusMsg(" Location service: %10s requests\n", Show(requests, buf1, kBufLen)); size_t count = locService->CacheCount(); size_t capacity = locService->CacheCapacity(); StatusMsg( " Location service cache: " "%4.1f%% hit rate, %.1f%% occupancy at end\n", Percent(hits, requests), Percent(count, capacity)); StatusMsg(" }\n"); StatusMsg(" }\n"); } InfallibleAllocPolicy::delete_(locService); StatusMsg("}\n"); } void DMDFuncs::Analyze(UniquePtr aWriter) { AnalyzeImpl(std::move(aWriter)); ClearReports(); } //--------------------------------------------------------------------------- // Testing //--------------------------------------------------------------------------- void DMDFuncs::ResetEverything(const char* aOptions) { AutoLockState lock; // Reset options. InfallibleAllocPolicy::delete_(gOptions); gOptions = InfallibleAllocPolicy::new_(aOptions); // Clear all existing blocks. gLiveBlockTable->clear(); gDeadBlockTable->clear(); // Reset gBernoulli to a deterministic state. (Its current state depends on // all previous trials.) ResetBernoulli(); } } // namespace dmd } // namespace mozilla