зеркало из https://github.com/mozilla/gecko-dev.git
1861 строка
57 KiB
C++
1861 строка
57 KiB
C++
/* -*- 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 <ctype.h>
|
|
#include <errno.h>
|
|
#include <limits.h>
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#if !defined(MOZ_PROFILING)
|
|
# error "DMD requires MOZ_PROFILING"
|
|
#endif
|
|
|
|
#ifdef XP_WIN
|
|
# include <windows.h>
|
|
# include <process.h>
|
|
#else
|
|
# include <pthread.h>
|
|
# include <sys/types.h>
|
|
# include <unistd.h>
|
|
#endif
|
|
|
|
#ifdef ANDROID
|
|
# include <android/log.h>
|
|
#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 <typename T>
|
|
static T* maybe_pod_malloc(size_t aNumElems) {
|
|
if (aNumElems & mozilla::tl::MulOverflowMask<sizeof(T)>::value)
|
|
return nullptr;
|
|
return (T*)gMallocTable.malloc(aNumElems * sizeof(T));
|
|
}
|
|
|
|
template <typename T>
|
|
static T* maybe_pod_calloc(size_t aNumElems) {
|
|
return (T*)gMallocTable.calloc(aNumElems, sizeof(T));
|
|
}
|
|
|
|
template <typename T>
|
|
static T* maybe_pod_realloc(T* aPtr, size_t aOldSize, size_t aNewSize) {
|
|
if (aNewSize & mozilla::tl::MulOverflowMask<sizeof(T)>::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 <typename T>
|
|
static T* pod_malloc(size_t aNumElems) {
|
|
T* p = maybe_pod_malloc<T>(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 <typename T>
|
|
static T* pod_calloc(size_t aNumElems) {
|
|
T* p = maybe_pod_calloc<T>(aNumElems);
|
|
ExitOnFailure(p);
|
|
return p;
|
|
}
|
|
|
|
static void* realloc_(void* aPtr, size_t aNewSize) {
|
|
void* p = gMallocTable.realloc(aPtr, aNewSize);
|
|
ExitOnFailure(p);
|
|
return p;
|
|
}
|
|
|
|
template <typename T>
|
|
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 <typename T>
|
|
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 <class T>
|
|
static T* new_() {
|
|
void* mem = malloc_(sizeof(T));
|
|
return new (mem) T;
|
|
}
|
|
|
|
template <class T, typename P1>
|
|
static T* new_(const P1& aP1) {
|
|
void* mem = malloc_(sizeof(T));
|
|
return new (mem) T(aP1);
|
|
}
|
|
|
|
template <class T>
|
|
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<void*>(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[<pid>] " prefix and the NUL.
|
|
char* fmt = (char*)InfallibleAllocPolicy::malloc_(strlen(aFmt) + 64);
|
|
sprintf(fmt, "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<char>((n % 10) + '0');
|
|
i++;
|
|
n /= 10;
|
|
}
|
|
}
|
|
int firstCharIndex = lasti - i + 1;
|
|
|
|
MOZ_ASSERT(firstCharIndex >= 0);
|
|
return &buf[firstCharIndex];
|
|
}
|
|
|
|
//---------------------------------------------------------------------------
|
|
// Options (Part 1)
|
|
//---------------------------------------------------------------------------
|
|
|
|
class Options {
|
|
template <typename T>
|
|
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;
|
|
|
|
DISALLOW_COPY_AND_ASSIGN(MutexBase);
|
|
|
|
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;
|
|
|
|
DISALLOW_COPY_AND_ASSIGN(Mutex);
|
|
|
|
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 {
|
|
DISALLOW_COPY_AND_ASSIGN(AutoLockState);
|
|
|
|
public:
|
|
AutoLockState() { gStateLock->Lock(); }
|
|
~AutoLockState() { gStateLock->Unlock(); }
|
|
};
|
|
|
|
class AutoUnlockState {
|
|
DISALLOW_COPY_AND_ASSIGN(AutoUnlockState);
|
|
|
|
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<T, detail::ThreadLocalKeyStorage>
|
|
#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) {}
|
|
|
|
DISALLOW_COPY_AND_ASSIGN(Thread);
|
|
|
|
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_<Thread>();
|
|
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;
|
|
|
|
DISALLOW_COPY_AND_ASSIGN(AutoBlockIntercepts);
|
|
|
|
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<InfallibleAllocPolicy, DescribeCodeAddressLock>
|
|
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<StackTrace*, StackTrace, InfallibleAllocPolicy>
|
|
StackTraceTable;
|
|
static StackTraceTable* gStackTraceTable = nullptr;
|
|
|
|
typedef mozilla::HashSet<const StackTrace*,
|
|
mozilla::DefaultHasher<const StackTrace*>,
|
|
InfallibleAllocPolicy>
|
|
StackTraceSet;
|
|
|
|
typedef mozilla::HashSet<const void*, mozilla::DefaultHasher<const void*>,
|
|
InfallibleAllocPolicy>
|
|
PointerSet;
|
|
typedef mozilla::HashMap<const void*, uint32_t,
|
|
mozilla::DefaultHasher<const void*>,
|
|
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<void**>(context.Ebp);
|
|
|
|
PNT_TIB pTib = reinterpret_cast<PNT_TIB>(NtCurrentTeb());
|
|
void* stackEnd = static_cast<void*>(pTib->StackBase);
|
|
FramePointerStackWalk(StackWalkCallback, /* skipFrames = */ 0, 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.
|
|
void** fp;
|
|
asm(
|
|
// Dereference %rbp to get previous %rbp
|
|
"movq (%%rbp), %0\n\t"
|
|
: "=r"(fp));
|
|
void* stackEnd = pthread_get_stackaddr_np(pthread_self());
|
|
FramePointerStackWalk(StackWalkCallback, /* skipFrames = */ 0, MaxFrames,
|
|
&tmp, fp, stackEnd);
|
|
#else
|
|
# if defined(XP_WIN) && defined(_M_X64)
|
|
int skipFrames = 1;
|
|
# else
|
|
int skipFrames = 2;
|
|
# endif
|
|
MozStackWalk(StackWalkCallback, skipFrames, MaxFrames, &tmp);
|
|
#endif
|
|
}
|
|
|
|
StackTraceTable::AddPtr p = gStackTraceTable->lookupForAdd(&tmp);
|
|
if (!p) {
|
|
StackTrace* stnew = InfallibleAllocPolicy::new_<StackTrace>(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<const int*>|.
|
|
template <typename T>
|
|
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<T>(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<const StackTrace*> 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<LiveBlock, LiveBlock, InfallibleAllocPolicy>
|
|
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<const LiveBlock*, size_t,
|
|
AggregatedLiveBlockHashPolicy, InfallibleAllocPolicy>
|
|
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<DeadBlock, size_t, DeadBlock, InfallibleAllocPolicy>
|
|
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);
|
|
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_<DMDBridge>();
|
|
|
|
#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_<Options>(e);
|
|
|
|
gStateLock = InfallibleAllocPolicy::new_<Mutex>();
|
|
|
|
gBernoulli = (FastBernoulliTrial*)InfallibleAllocPolicy::malloc_(
|
|
sizeof(FastBernoulliTrial));
|
|
ResetBernoulli();
|
|
|
|
Thread::Init();
|
|
|
|
{
|
|
AutoLockState lock;
|
|
|
|
gStackTraceTable = InfallibleAllocPolicy::new_<StackTraceTable>(8192);
|
|
gLiveBlockTable = InfallibleAllocPolicy::new_<LiveBlockTable>(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_<DeadBlockTable>(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(sc.ToPtrString(block[i]));
|
|
}
|
|
}
|
|
aWriter.EndArray();
|
|
}
|
|
|
|
static void AnalyzeImpl(UniquePtr<JSONWriteFunc> 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_<CodeAddressService>();
|
|
|
|
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", var);
|
|
} else {
|
|
writer.NullProperty("dmdEnvVar");
|
|
}
|
|
|
|
writer.StringProperty("mode", 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", 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",
|
|
isc.ToIdString(aB.AllocStackTrace()));
|
|
}
|
|
|
|
if (gOptions->IsDarkMatterMode() && aB.NumReports() > 0) {
|
|
writer.StartArrayProperty("reps");
|
|
{
|
|
if (aB.ReportStackTrace1()) {
|
|
writer.StringElement(isc.ToIdString(aB.ReportStackTrace1()));
|
|
}
|
|
if (aB.ReportStackTrace2()) {
|
|
writer.StringElement(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", 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(isc.ToIdString(st), writer.SingleLineStyle);
|
|
{
|
|
for (uint32_t i = 0; i < st->Length(); i++) {
|
|
const void* pc = st->Pc(i);
|
|
writer.StringElement(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(isc.ToIdString(pc), 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<JSONWriteFunc> aWriter) {
|
|
AnalyzeImpl(std::move(aWriter));
|
|
ClearReports();
|
|
}
|
|
|
|
//---------------------------------------------------------------------------
|
|
// Testing
|
|
//---------------------------------------------------------------------------
|
|
|
|
void DMDFuncs::ResetEverything(const char* aOptions) {
|
|
AutoLockState lock;
|
|
|
|
// Reset options.
|
|
InfallibleAllocPolicy::delete_(gOptions);
|
|
gOptions = InfallibleAllocPolicy::new_<Options>(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
|