Bug 1044709 - DMD: emit JSON output and use Python for post-processing. r=mccr8.
authorNicholas Nethercote <nnethercote@mozilla.com>
Tue, 02 Sep 2014 20:34:58 -0700
changeset 208903 155f170fa5eaa95fd8026aa9982ba26b1fa45f21
parent 208902 0b61bbe1dda70ac8b27e0199c892a527d70aac53
child 208904 d73853c26a1a571c1970295d71f38d075d9458ec
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersmccr8
bugs1044709
milestone35.0a1
Bug 1044709 - DMD: emit JSON output and use Python for post-processing. r=mccr8.
memory/replace/dmd/DMD.cpp
memory/replace/dmd/DMD.h
memory/replace/dmd/check_test_output.py
memory/replace/dmd/dmd.py
memory/replace/dmd/test-expected.dmd
memory/replace/dmd/test/full-heap-expected1.txt
memory/replace/dmd/test/full-heap-expected2.txt
memory/replace/dmd/test/full-heap-expected3.txt
memory/replace/dmd/test/full-heap-expected4.txt
memory/replace/dmd/test/full-reports-expected1.txt
memory/replace/dmd/test/full-reports-expected2.txt
memory/replace/dmd/test/full-reports-expected3.txt
memory/replace/dmd/test/full-reports-expected4.txt
python/mozbuild/mozbuild/mach_commands.py
xpcom/base/nsMemoryInfoDumper.cpp
--- a/memory/replace/dmd/DMD.cpp
+++ b/memory/replace/dmd/DMD.cpp
@@ -31,21 +31,23 @@
 #include "nscore.h"
 #include "nsStackWalk.h"
 
 #include "js/HashTable.h"
 #include "js/Vector.h"
 
 #include "mozilla/Assertions.h"
 #include "mozilla/HashFunctions.h"
+#include "mozilla/IntegerPrintfMacros.h"
+#include "mozilla/JSONWriter.h"
 #include "mozilla/Likely.h"
 #include "mozilla/MemoryReporting.h"
 
-// CodeAddressService is defined entirely in the header, so this does not make DMD
-// depend on XPCOM's object file.
+// CodeAddressService is defined entirely in the header, so this does not make
+// DMD depend on XPCOM's object file.
 #include "CodeAddressService.h"
 
 // MOZ_REPLACE_ONLY_MEMALIGN saves us from having to define
 // replace_{posix_memalign,aligned_alloc,valloc}.  It requires defining
 // PAGE_SIZE.  Nb: sysconf() is expensive, but it's only used for (the obsolete
 // and rarely used) valloc.
 #define MOZ_REPLACE_ONLY_MEMALIGN 1
 
@@ -216,53 +218,41 @@ StatusMsg(const char* aFmt, ...)
 #endif
   va_end(ap);
 }
 
 /* static */ void
 InfallibleAllocPolicy::ExitOnFailure(const void* aP)
 {
   if (!aP) {
-    StatusMsg("out of memory;  aborting\n");
-    MOZ_CRASH();
+    MOZ_CRASH("DMD out of memory; aborting");
   }
 }
 
-void
-Writer::Write(const char* aFmt, ...) const
+class FpWriteFunc : public JSONWriteFunc
 {
-  va_list ap;
-  va_start(ap, aFmt);
-  mWriterFun(mWriteState, aFmt, ap);
-  va_end(ap);
-}
+public:
+  explicit FpWriteFunc(FILE* aFp) : mFp(aFp) {}
+  ~FpWriteFunc() { fclose(mFp); }
 
-#define W(...) aWriter.Write(__VA_ARGS__);
-
-#define WriteSeparator(...) \
-  W("#-----------------------------------------------------------------\n\n");
+  void Write(const char* aStr) { fputs(aStr, mFp); }
 
-MOZ_EXPORT void
-FpWrite(void* aWriteState, const char* aFmt, va_list aAp)
-{
-  FILE* fp = static_cast<FILE*>(aWriteState);
-  vfprintf(fp, aFmt, aAp);
-}
+private:
+  FILE* mFp;
+};
 
 static double
 Percent(size_t part, size_t whole)
 {
   return (whole == 0) ? 0 : 100 * (double)part / whole;
 }
 
-// Commifies the number and prepends a '~' if requested.  Best used with
-// |kBufLen| and |gBuf[1234]|, because they should be big enough for any number
-// we'll see.
+// Commifies the number.
 static char*
-Show(size_t n, char* buf, size_t buflen, bool addTilde = false)
+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) {
@@ -273,37 +263,20 @@ Show(size_t n, char* buf, size_t buflen,
       }
       buf[lasti - i] = static_cast<char>((n % 10) + '0');
       i++;
       n /= 10;
     }
   }
   int firstCharIndex = lasti - i + 1;
 
-  if (addTilde) {
-    firstCharIndex--;
-    buf[firstCharIndex] = '~';
-  }
-
   MOZ_ASSERT(firstCharIndex >= 0);
   return &buf[firstCharIndex];
 }
 
-static const char*
-Plural(size_t aN)
-{
-  return aN == 1 ? "" : "s";
-}
-
-// Used by calls to Show().
-static const size_t kBufLen = 64;
-static char gBuf1[kBufLen];
-static char gBuf2[kBufLen];
-static char gBuf3[kBufLen];
-
 //---------------------------------------------------------------------------
 // Options (Part 1)
 //---------------------------------------------------------------------------
 
 class Options
 {
   template <typename T>
   struct NumOption
@@ -321,32 +294,33 @@ class Options
     Test,     // do some basic correctness tests
     Stress    // do some performance stress tests
   };
 
   char* mDMDEnvVar;   // a saved copy, for later printing
 
   NumOption<size_t>   mSampleBelowSize;
   NumOption<uint32_t> mMaxFrames;
-  NumOption<uint32_t> mMaxRecords;
+  bool mShowDumpStats;
   Mode mMode;
 
   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* aN);
+                      long aMin, long aMax, long* aValue);
+  static bool GetBool(const char* aArg, const char* aOptionName, bool* aValue);
 
 public:
   explicit Options(const char* aDMDEnvVar);
 
   const char* DMDEnvVar() const { return mDMDEnvVar; }
 
   size_t SampleBelowSize() const { return mSampleBelowSize.mActual; }
   size_t MaxFrames()       const { return mMaxFrames.mActual; }
-  size_t MaxRecords()      const { return mMaxRecords.mActual; }
+  size_t ShowDumpStats()   const { return mShowDumpStats; }
 
   void SetSampleBelowSize(size_t aN) { mSampleBelowSize.mActual = aN; }
 
   bool IsTestMode()   const { return mMode == Test; }
   bool IsStressMode() const { return mMode == Stress; }
 };
 
 static Options *gOptions;
@@ -673,40 +647,37 @@ typedef CodeAddressService<StringTable, 
 //---------------------------------------------------------------------------
 
 class StackTrace
 {
 public:
   static const uint32_t MaxFrames = 24;
 
 private:
-  uint32_t mLength;         // The number of PCs.
-  void* mPcs[MaxFrames];    // The PCs themselves.  If --max-frames is less
-                            // than 24, this array is bigger than necessary,
-                            // but that case is unusual.
+  uint32_t mLength;             // The number of PCs.
+  const void* mPcs[MaxFrames];  // The PCs themselves.  If --max-frames is less
+                                // than 24, this array is bigger than
+                                // necessary, but that case is unusual.
 
 public:
   StackTrace() : mLength(0) {}
 
   uint32_t Length() const { return mLength; }
-  void* Pc(uint32_t i) const { MOZ_ASSERT(i < mLength); return mPcs[i]; }
+  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);
 
-  void Sort()
-  {
-    qsort(mPcs, mLength, sizeof(mPcs[0]), StackTrace::Cmp);
-  }
-
-  void Print(const Writer& aWriter, CodeAddressService* aLocService) const;
-
   // Hash policy.
 
   typedef StackTrace* Lookup;
 
   static uint32_t hash(const StackTrace* const& aSt)
   {
     return mozilla::HashBytes(aSt->mPcs, aSt->Size());
   }
@@ -723,50 +694,36 @@ private:
                                 void* aClosure)
   {
     StackTrace* st = (StackTrace*) aClosure;
     MOZ_ASSERT(st->mLength < MaxFrames);
     st->mPcs[st->mLength] = aPc;
     st->mLength++;
     MOZ_ASSERT(st->mLength == aFrameNumber);
   }
-
-  static int Cmp(const void* aA, const void* aB)
-  {
-    const void* const a = *static_cast<const void* const*>(aA);
-    const void* const b = *static_cast<const void* const*>(aB);
-    if (a < b) return -1;
-    if (a > b) return  1;
-    return 0;
-  }
 };
 
 typedef js::HashSet<StackTrace*, StackTrace, InfallibleAllocPolicy>
         StackTraceTable;
 static StackTraceTable* gStackTraceTable = nullptr;
 
+typedef js::HashSet<const StackTrace*, js::DefaultHasher<const StackTrace*>,
+                    InfallibleAllocPolicy>
+        StackTraceSet;
+
+typedef js::HashSet<const void*, js::DefaultHasher<const void*>,
+                    InfallibleAllocPolicy>
+        PointerSet;
+typedef js::HashMap<const void*, uint32_t, js::DefaultHasher<const void*>,
+                    InfallibleAllocPolicy>
+        PointerIdMap;
+
 // We won't GC the stack trace table until it this many elements.
 static uint32_t gGCStackTraceTableWhenSizeExceeds = 4 * 1024;
 
-void
-StackTrace::Print(const Writer& aWriter, CodeAddressService* aLocService) const
-{
-  if (mLength == 0) {
-    W("    (empty)\n");  // StackTrace::Get() must have failed
-    return;
-  }
-
-  static const size_t buflen = 1024;
-  char buf[buflen];
-  for (uint32_t i = 0; i < mLength; i++) {
-    aLocService->GetLocation(i + 1, Pc(i), buf, buflen);
-    aWriter.Write("    %s\n", buf);
-  }
-}
-
 /* static */ const StackTrace*
 StackTrace::Get(Thread* aT)
 {
   MOZ_ASSERT(gStateLock->IsLocked());
   MOZ_ASSERT(aT->InterceptsAreBlocked());
 
   // On Windows, NS_StackWalk can acquire a lock from the shared library
   // loader.  Another thread might call malloc while holding that lock (when
@@ -794,19 +751,19 @@ StackTrace::Get(Thread* aT)
     // attempt to create a semaphore (which can happen in printf) could
     // deadlock.
     //
     // However, the most complex thing DMD does after Get() returns is to put
     // something in a hash table, which might call
     // InfallibleAllocPolicy::malloc_.  I'm not yet sure if this needs special
     // handling, hence the forced abort.  Sorry.  If you hit this, please file
     // a bug and CC nnethercote.
-    MOZ_CRASH();
+    MOZ_CRASH("unexpected case in StackTrace::Get()");
   } else {
-    MOZ_CRASH();  // should be impossible
+    MOZ_CRASH("impossible case in StackTrace::Get()");
   }
 
   StackTraceTable::AddPtr p = gStackTraceTable->lookupForAdd(&tmp);
   if (!p) {
     StackTrace* stnew = InfallibleAllocPolicy::new_<StackTrace>(tmp);
     (void)gStackTraceTable->add(p, stnew);
   }
   return *p;
@@ -895,16 +852,18 @@ public:
     : mPtr(aPtr),
       mReqSize(aReqSize),
       mAllocStackTrace_mSampled(aAllocStackTrace, aSampled),
       mReportStackTrace_mReportedOnAlloc()     // all fields get zeroed
   {
     MOZ_ASSERT(aAllocStackTrace);
   }
 
+  const void* Address() const { return mPtr; }
+
   size_t ReqSize() const { return mReqSize; }
 
   // Sampled blocks always have zero slop.
   size_t SlopSize() const
   {
     return IsSampled() ? 0 : MallocSizeOf(mPtr) - mReqSize;
   }
 
@@ -918,33 +877,50 @@ public:
     return mAllocStackTrace_mSampled.Tag();
   }
 
   const StackTrace* AllocStackTrace() const
   {
     return mAllocStackTrace_mSampled.Ptr();
   }
 
-  const StackTrace* ReportStackTrace1() const {
+  const StackTrace* ReportStackTrace1() const
+  {
     return mReportStackTrace_mReportedOnAlloc[0].Ptr();
   }
 
-  const StackTrace* ReportStackTrace2() const {
+  const StackTrace* ReportStackTrace2() const
+  {
     return mReportStackTrace_mReportedOnAlloc[1].Ptr();
   }
 
-  bool ReportedOnAlloc1() const {
+  bool ReportedOnAlloc1() const
+  {
     return mReportStackTrace_mReportedOnAlloc[0].Tag();
   }
 
-  bool ReportedOnAlloc2() const {
+  bool ReportedOnAlloc2() const
+  {
     return mReportStackTrace_mReportedOnAlloc[1].Tag();
   }
 
-  uint32_t NumReports() const {
+  void AddStackTracesToTable(StackTraceSet& aStackTraces) const
+  {
+    aStackTraces.put(AllocStackTrace());  // never null
+    const StackTrace* st;
+    if ((st = ReportStackTrace1())) {     // may be null
+      aStackTraces.put(st);
+    }
+    if ((st = ReportStackTrace2())) {     // may be null
+      aStackTraces.put(st);
+    }
+  }
+
+  uint32_t NumReports() const
+  {
     if (ReportStackTrace2()) {
       MOZ_ASSERT(ReportStackTrace1());
       return 2;
     }
     if (ReportStackTrace1()) {
       return 1;
     }
     return 0;
@@ -991,60 +967,47 @@ public:
   {
     return aB.mPtr == aPtr;
   }
 };
 
 typedef js::HashSet<Block, Block, InfallibleAllocPolicy> BlockTable;
 static BlockTable* gBlockTable = nullptr;
 
-typedef js::HashSet<const StackTrace*, js::DefaultHasher<const StackTrace*>,
-                    InfallibleAllocPolicy>
-        StackTraceSet;
-
 // 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.finish();
-  aStackTraces.init(1024);
+  aStackTraces.init(512);
 
   for (BlockTable::Range r = gBlockTable->all(); !r.empty(); r.popFront()) {
     const Block& b = r.front();
-    aStackTraces.put(b.AllocStackTrace());
-    aStackTraces.put(b.ReportStackTrace1());
-    aStackTraces.put(b.ReportStackTrace2());
+    b.AddStackTracesToTable(aStackTraces);
   }
-
-  // Any of the stack traces added above may have been null.  For the sake of
-  // cleanliness, don't leave the null pointer in the set.
-  aStackTraces.remove(nullptr);
 }
 
 // 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 Enum destructor
   // will automatically rehash and compact the table.
-  for (StackTraceTable::Enum e(*gStackTraceTable);
-       !e.empty();
-       e.popFront()) {
+  for (StackTraceTable::Enum e(*gStackTraceTable); !e.empty(); e.popFront()) {
     StackTrace* const& st = e.front();
-
     if (!usedStackTraces.has(st)) {
       e.removeFront();
       InfallibleAllocPolicy::delete_(st);
     }
   }
 
   // Schedule a GC when we have twice as many stack traces as we had right after
   // this GC finished.
@@ -1247,203 +1210,16 @@ replace_free(void* aPtr)
   FreeCallback(aPtr, t);
   gMallocTable->free(aPtr);
 }
 
 namespace mozilla {
 namespace dmd {
 
 //---------------------------------------------------------------------------
-// Heap block records
-//---------------------------------------------------------------------------
-
-class RecordKey
-{
-public:
-  const StackTrace* const mAllocStackTrace;   // never null
-protected:
-  const StackTrace* const mReportStackTrace1; // nullptr if unreported
-  const StackTrace* const mReportStackTrace2; // nullptr if not 2x-reported
-
-public:
-  explicit RecordKey(const Block& aB)
-    : mAllocStackTrace(aB.AllocStackTrace()),
-      mReportStackTrace1(aB.ReportStackTrace1()),
-      mReportStackTrace2(aB.ReportStackTrace2())
-  {
-    MOZ_ASSERT(mAllocStackTrace);
-  }
-
-  // Hash policy.
-
-  typedef RecordKey Lookup;
-
-  static uint32_t hash(const RecordKey& aKey)
-  {
-    return mozilla::HashGeneric(aKey.mAllocStackTrace,
-                                aKey.mReportStackTrace1,
-                                aKey.mReportStackTrace2);
-  }
-
-  static bool match(const RecordKey& aA, const RecordKey& aB)
-  {
-    return aA.mAllocStackTrace   == aB.mAllocStackTrace &&
-           aA.mReportStackTrace1 == aB.mReportStackTrace1 &&
-           aA.mReportStackTrace2 == aB.mReportStackTrace2;
-  }
-};
-
-class RecordSize
-{
-  static const size_t kReqBits = sizeof(size_t) * 8 - 1;  // 31 or 63
-
-  size_t mReq;              // size requested
-  size_t mSlop:kReqBits;    // slop bytes
-  size_t mSampled:1;        // were one or more blocks contributing to this
-                            //   RecordSize sampled?
-public:
-  RecordSize()
-    : mReq(0),
-      mSlop(0),
-      mSampled(false)
-  {}
-
-  size_t Req()    const { return mReq; }
-  size_t Slop()   const { return mSlop; }
-  size_t Usable() const { return mReq + mSlop; }
-
-  bool IsSampled() const { return mSampled; }
-
-  void Add(const Block& aB)
-  {
-    mReq  += aB.ReqSize();
-    mSlop += aB.SlopSize();
-    mSampled = mSampled || aB.IsSampled();
-  }
-
-  void Add(const RecordSize& aRecordSize)
-  {
-    mReq  += aRecordSize.Req();
-    mSlop += aRecordSize.Slop();
-    mSampled = mSampled || aRecordSize.IsSampled();
-  }
-
-  static int CmpByUsable(const RecordSize& aA, const RecordSize& aB)
-  {
-    // Primary sort: put bigger usable sizes first.
-    if (aA.Usable() > aB.Usable()) return -1;
-    if (aA.Usable() < aB.Usable()) return  1;
-
-    // Secondary sort: put bigger requested sizes first.
-    if (aA.Req() > aB.Req()) return -1;
-    if (aA.Req() < aB.Req()) return  1;
-
-    // Tertiary sort: put non-sampled records before sampled records.
-    if (!aA.mSampled &&  aB.mSampled) return -1;
-    if ( aA.mSampled && !aB.mSampled) return  1;
-
-    return 0;
-  }
-};
-
-// A collection of one or more heap blocks with a common RecordKey.
-class Record : public RecordKey
-{
-  // The RecordKey base class serves as the key in RecordTables.  These two
-  // fields constitute the value, so it's ok for them to be |mutable|.
-  mutable uint32_t    mNumBlocks; // number of blocks with this RecordKey
-  mutable RecordSize mRecordSize; // combined size of those blocks
-
-public:
-  explicit Record(const RecordKey& aKey)
-    : RecordKey(aKey),
-      mNumBlocks(0),
-      mRecordSize()
-  {}
-
-  uint32_t NumBlocks() const { return mNumBlocks; }
-
-  const RecordSize& GetRecordSize() const { return mRecordSize; }
-
-  // This is |const| thanks to the |mutable| fields above.
-  void Add(const Block& aB) const
-  {
-    mNumBlocks++;
-    mRecordSize.Add(aB);
-  }
-
-  void Print(const Writer& aWriter, CodeAddressService* aLocService,
-             uint32_t aM, uint32_t aN, const char* aStr, const char* astr,
-             size_t aCategoryUsableSize, size_t aCumulativeUsableSize,
-             size_t aTotalUsableSize, bool aShowCategoryPercentage,
-             bool aShowReportedAt) const;
-
-  static int CmpByUsable(const void* aA, const void* aB)
-  {
-    const Record* const a = *static_cast<const Record* const*>(aA);
-    const Record* const b = *static_cast<const Record* const*>(aB);
-
-    return RecordSize::CmpByUsable(a->mRecordSize, b->mRecordSize);
-  }
-};
-
-typedef js::HashSet<Record, Record, InfallibleAllocPolicy> RecordTable;
-
-void
-Record::Print(const Writer& aWriter, CodeAddressService* aLocService,
-              uint32_t aM, uint32_t aN, const char* aStr, const char* astr,
-              size_t aCategoryUsableSize, size_t aCumulativeUsableSize,
-              size_t aTotalUsableSize, bool aShowCategoryPercentage,
-              bool aShowReportedAt) const
-{
-  bool showTilde = mRecordSize.IsSampled();
-
-  W("%s {\n", aStr);
-  W("  %s block%s in heap block record %s of %s\n",
-    Show(mNumBlocks, gBuf1, kBufLen, showTilde), Plural(mNumBlocks),
-    Show(aM, gBuf2, kBufLen),
-    Show(aN, gBuf3, kBufLen));
-
-  W("  %s bytes (%s requested / %s slop)\n",
-    Show(mRecordSize.Usable(), gBuf1, kBufLen, showTilde),
-    Show(mRecordSize.Req(),    gBuf2, kBufLen, showTilde),
-    Show(mRecordSize.Slop(),   gBuf3, kBufLen, showTilde));
-
-  W("  %4.2f%% of the heap (%4.2f%% cumulative)\n",
-    Percent(mRecordSize.Usable(), aTotalUsableSize),
-    Percent(aCumulativeUsableSize, aTotalUsableSize));
-
-  if (aShowCategoryPercentage) {
-    W("  %4.2f%% of %s (%4.2f%% cumulative)\n",
-      Percent(mRecordSize.Usable(), aCategoryUsableSize),
-      astr,
-      Percent(aCumulativeUsableSize, aCategoryUsableSize));
-  }
-
-  W("  Allocated at {\n");
-  mAllocStackTrace->Print(aWriter, aLocService);
-  W("  }\n");
-
-  if (aShowReportedAt) {
-    if (mReportStackTrace1) {
-      W("  Reported at {\n");
-      mReportStackTrace1->Print(aWriter, aLocService);
-      W("  }\n");
-    }
-    if (mReportStackTrace2) {
-      W("  Reported again at {\n");
-      mReportStackTrace2->Print(aWriter, aLocService);
-      W("  }\n");
-    }
-  }
-
-  W("}\n\n");
-}
-
-//---------------------------------------------------------------------------
 // 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)
@@ -1456,23 +1232,41 @@ Options::ValueIfMatch(const char* aArg, 
   }
   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* aN)
+                 long aMin, long aMax, long* aValue)
 {
   if (const char* optionValue = ValueIfMatch(aArg, aOptionName)) {
     char* endPtr;
-    *aN = strtol(optionValue, &endPtr, /* base */ 10);
-    if (!*endPtr && aMin <= *aN && *aN <= aMax &&
-        *aN != LONG_MIN && *aN != LONG_MAX) {
+    *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;
 }
 
 // The sample-below default is a prime number close to 4096.
 // - Why that size?  Because it's *much* faster but only moderately less precise
@@ -1481,17 +1275,17 @@ Options::GetLong(const char* aArg, const
 //   of 4096, for example, then our alloc counter would only take on even
 //   values, because jemalloc always rounds up requests sizes.  In contrast, a
 //   prime size will explore all possible values of the alloc counter.
 //
 Options::Options(const char* aDMDEnvVar)
   : mDMDEnvVar(InfallibleAllocPolicy::strdup_(aDMDEnvVar)),
     mSampleBelowSize(4093, 100 * 100 * 1000),
     mMaxFrames(StackTrace::MaxFrames, StackTrace::MaxFrames),
-    mMaxRecords(1000, 1000000),
+    mShowDumpStats(false),
     mMode(Normal)
 {
   char* e = mDMDEnvVar;
   if (strcmp(e, "1") != 0) {
     bool isEnd = false;
     while (!isEnd) {
       // Consume leading whitespace.
       while (isspace(*e)) {
@@ -1507,24 +1301,25 @@ Options::Options(const char* aDMDEnvVar)
         e++;
       }
       char replacedChar = *e;
       isEnd = replacedChar == '\0';
       *e = '\0';
 
       // Handle arg
       long myLong;
+      bool myBool;
       if (GetLong(arg, "--sample-below", 1, mSampleBelowSize.mMax, &myLong)) {
         mSampleBelowSize.mActual = myLong;
 
       } else if (GetLong(arg, "--max-frames", 1, mMaxFrames.mMax, &myLong)) {
         mMaxFrames.mActual = myLong;
 
-      } else if (GetLong(arg, "--max-records", 1, mMaxRecords.mMax, &myLong)) {
-        mMaxRecords.mActual = myLong;
+      } else if (GetBool(arg, "--show-dump-stats", &myBool)) {
+        mShowDumpStats = myBool;
 
       } else if (strcmp(arg, "--mode=normal") == 0) {
         mMode = Options::Normal;
       } else if (strcmp(arg, "--mode=test")   == 0) {
         mMode = Options::Test;
       } else if (strcmp(arg, "--mode=stress") == 0) {
         mMode = Options::Stress;
 
@@ -1557,19 +1352,17 @@ Options::BadArg(const char* aArg)
   StatusMsg("The following options are allowed;  defaults are shown in [].\n");
   StatusMsg("  --sample-below=<1..%d> Sample blocks smaller than this [%d]\n",
             int(mSampleBelowSize.mMax),
             int(mSampleBelowSize.mDefault));
   StatusMsg("                               (prime numbers are recommended)\n");
   StatusMsg("  --max-frames=<1..%d>         Max. depth of stack traces [%d]\n",
             int(mMaxFrames.mMax),
             int(mMaxFrames.mDefault));
-  StatusMsg("  --max-records=<1..%u>   Max. number of records printed [%u]\n",
-            mMaxRecords.mMax,
-            mMaxRecords.mDefault);
+  StatusMsg("  --show-dump-stats=<yes|no>   Show stats about dumps? [no]\n");
   StatusMsg("  --mode=<normal|test|stress>  Mode of operation [normal]\n");
   StatusMsg("\n");
   exit(1);
 }
 
 //---------------------------------------------------------------------------
 // DMD start-up
 //---------------------------------------------------------------------------
@@ -1589,18 +1382,19 @@ OpenOutputFile(const char* aFilename)
   FILE* fp = fopen(aFilename, "w");
   if (!fp) {
     StatusMsg("can't create %s file: %s\n", aFilename, strerror(errno));
     exit(1);
   }
   return fp;
 }
 
-static void RunTestMode(FILE* fp);
-static void RunStressMode(FILE* fp);
+static void RunTestMode(UniquePtr<FpWriteFunc> aF1, UniquePtr<FpWriteFunc> aF2,
+                        UniquePtr<FpWriteFunc> aF3, UniquePtr<FpWriteFunc> aF4);
+static void RunStressMode(UniquePtr<FpWriteFunc> aF);
 
 // 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 void
 Init(const malloc_table_t* aMallocTable)
 {
@@ -1648,37 +1442,38 @@ Init(const malloc_table_t* aMallocTable)
     gStackTraceTable = InfallibleAllocPolicy::new_<StackTraceTable>();
     gStackTraceTable->init(8192);
 
     gBlockTable = InfallibleAllocPolicy::new_<BlockTable>();
     gBlockTable->init(8192);
   }
 
   if (gOptions->IsTestMode()) {
-    // OpenOutputFile() can allocate.  So do this before setting
-    // gIsDMDRunning so those allocations don't show up in our results.  Once
-    // gIsDMDRunning is set we are intercepting malloc et al. in earnest.
-    FILE* fp = OpenOutputFile("test.dmd");
+    // Do all necessary allocations before setting gIsDMDRunning so those
+    // allocations don't show up in our results.  Once gIsDMDRunning is set we
+    // are intercepting malloc et al. in earnest.
+    auto f1 = MakeUnique<FpWriteFunc>(OpenOutputFile("full1.json"));
+    auto f2 = MakeUnique<FpWriteFunc>(OpenOutputFile("full2.json"));
+    auto f3 = MakeUnique<FpWriteFunc>(OpenOutputFile("full3.json"));
+    auto f4 = MakeUnique<FpWriteFunc>(OpenOutputFile("full4.json"));
     gIsDMDRunning = true;
 
     StatusMsg("running test mode...\n");
-    RunTestMode(fp);
+    RunTestMode(Move(f1), Move(f2), Move(f3), Move(f4));
     StatusMsg("finished test mode\n");
-    fclose(fp);
     exit(0);
   }
 
   if (gOptions->IsStressMode()) {
-    FILE* fp = OpenOutputFile("stress.dmd");
+    auto f = MakeUnique<FpWriteFunc>(OpenOutputFile("stress.json"));
     gIsDMDRunning = true;
 
     StatusMsg("running stress mode...\n");
-    RunStressMode(fp);
+    RunStressMode(Move(f));
     StatusMsg("finished stress mode\n");
-    fclose(fp);
     exit(0);
   }
 
   gIsDMDRunning = true;
 }
 
 //---------------------------------------------------------------------------
 // DMD reporting and unreporting
@@ -1717,66 +1512,23 @@ ReportOnAlloc(const void* aPtr)
 {
   ReportHelper(aPtr, /* onAlloc */ true);
 }
 
 //---------------------------------------------------------------------------
 // DMD output
 //---------------------------------------------------------------------------
 
-static void
-PrintSortedRecords(const Writer& aWriter, CodeAddressService* aLocService,
-                   int (*aCmp)(const void*, const void*),
-                   const char* aStr, const char* astr,
-                   const RecordTable& aRecordTable,
-                   size_t aCategoryUsableSize, size_t aTotalUsableSize,
-                   bool aShowCategoryPercentage, bool aShowReportedAt)
-{
-  StatusMsg("  creating and sorting %s heap block record array...\n", astr);
-
-  // Convert the table into a sorted array.
-  js::Vector<const Record*, 0, InfallibleAllocPolicy> recordArray;
-  recordArray.reserve(aRecordTable.count());
-  for (RecordTable::Range r = aRecordTable.all();
-       !r.empty();
-       r.popFront()) {
-    recordArray.infallibleAppend(&r.front());
-  }
-  qsort(recordArray.begin(), recordArray.length(), sizeof(recordArray[0]),
-        aCmp);
-
-  WriteSeparator();
-
-  if (recordArray.length() == 0) {
-    W("# no %s heap blocks\n\n", astr);
-    return;
-  }
-
-  StatusMsg("  printing %s heap block record array...\n", astr);
-  size_t cumulativeUsableSize = 0;
-
-  // Limit the number of records printed, because fix_linux_stack.py is too
-  // damn slow.  Note that we don't break out of this loop because we need to
-  // keep adding to |cumulativeUsableSize|.
-  uint32_t numRecords = recordArray.length();
-  uint32_t maxRecords = gOptions->MaxRecords();
-  for (uint32_t i = 0; i < numRecords; i++) {
-    const Record* r = recordArray[i];
-    cumulativeUsableSize += r->GetRecordSize().Usable();
-    if (i < maxRecords) {
-      r->Print(aWriter, aLocService, i+1, numRecords, aStr, astr,
-               aCategoryUsableSize, cumulativeUsableSize, aTotalUsableSize,
-               aShowCategoryPercentage, aShowReportedAt);
-    } else if (i == maxRecords) {
-      W("# %s: stopping after %s heap block records\n\n", aStr,
-        Show(maxRecords, gBuf1, kBufLen));
-    }
-  }
-  MOZ_ASSERT(aCategoryUsableSize == cumulativeUsableSize);
-}
+// The version number of the output format. Increment this if you make
+// backwards-incompatible changes to the format.
+//
+// Version history:
+// - 1: The original format (bug 1044709).
+//
+static const int kOutputVersionNumber = 1;
 
 // 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
@@ -1847,335 +1599,254 @@ ClearReports()
 }
 
 MOZ_EXPORT bool
 IsRunning()
 {
   return gIsDMDRunning;
 }
 
-// AnalyzeReports() and AnalyzeHeap() have a lot in common. This abstract class
-// encapsulates the operations that are not shared.
-class Analyzer
-{
-public:
-  virtual const char* AnalyzeFunctionName() const = 0;
-
-  virtual RecordTable* ProcessBlock(const Block& aBlock) = 0;
-
-  virtual void PrintRecords(const Writer& aWriter,
-                            CodeAddressService* aLocService) const = 0;
-  virtual void PrintSummary(const Writer& aWriter, bool aShowTilde) const = 0;
-  virtual void PrintStats(const Writer& aWriter) const = 0;
-
-  struct RecordKindData
-  {
-    RecordTable mRecordTable;
-    size_t mUsableSize;
-    size_t mNumBlocks;
-
-    explicit RecordKindData(size_t aN)
-      : mUsableSize(0), mNumBlocks(0)
-    {
-      mRecordTable.init(aN);
-    }
-
-    void processBlock(const Block& aBlock)
-    {
-      mUsableSize += aBlock.UsableSize();
-      mNumBlocks++;
-    }
-  };
-};
-
-class ReportsAnalyzer MOZ_FINAL : public Analyzer
+// This function converts an integer to base-32. |aBuf| 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.
+//
+// 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, char* aBuf, size_t aBufLen)
 {
-  RecordKindData mUnreported;
-  RecordKindData mOnceReported;
-  RecordKindData mTwiceReported;
-
-  size_t mTotalUsableSize;
-  size_t mTotalNumBlocks;
-
-public:
-  ReportsAnalyzer()
-    : mUnreported(1024), mOnceReported(1024), mTwiceReported(0),
-      mTotalUsableSize(0), mTotalNumBlocks(0)
-  {}
-
-  ~ReportsAnalyzer()
-  {
-    ClearReports();
-  }
-
-  virtual const char* AnalyzeFunctionName() const { return "AnalyzeReports"; }
-
-  virtual RecordTable* ProcessBlock(const Block& aBlock)
-  {
-    RecordKindData* data;
-    uint32_t numReports = aBlock.NumReports();
-    if (numReports == 0) {
-      data = &mUnreported;
-    } else if (numReports == 1) {
-      data = &mOnceReported;
-    } else {
-      MOZ_ASSERT(numReports == 2);
-      data = &mTwiceReported;
-    }
-    data->processBlock(aBlock);
-
-    mTotalUsableSize += aBlock.UsableSize();
-    mTotalNumBlocks++;
-
-    return &data->mRecordTable;
-  }
-
-  virtual void PrintRecords(const Writer& aWriter,
-                            CodeAddressService* aLocService) const
-  {
-    PrintSortedRecords(aWriter, aLocService, Record::CmpByUsable,
-                       "Twice-reported", "twice-reported",
-                       mTwiceReported.mRecordTable,
-                       mTwiceReported.mUsableSize, mTotalUsableSize,
-                       /* showCategoryPercentage = */ true,
-                       /* showReportedAt = */ true);
-
-    PrintSortedRecords(aWriter, aLocService, Record::CmpByUsable,
-                       "Unreported", "unreported",
-                       mUnreported.mRecordTable,
-                       mUnreported.mUsableSize, mTotalUsableSize,
-                       /* showCategoryPercentage = */ true,
-                       /* showReportedAt = */ true);
+  static const char digits[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef";
 
-    PrintSortedRecords(aWriter, aLocService, Record::CmpByUsable,
-                       "Once-reported", "once-reported",
-                       mOnceReported.mRecordTable,
-                       mOnceReported.mUsableSize, mTotalUsableSize,
-                       /* showCategoryPercentage = */ true,
-                       /* showReportedAt = */ true);
-  }
-
-  virtual void PrintSummary(const Writer& aWriter, bool aShowTilde) const
-  {
-    W("  Total:          %12s bytes (%6.2f%%) in %7s blocks (%6.2f%%)\n",
-      Show(mTotalUsableSize, gBuf1, kBufLen, aShowTilde),
-      100.0,
-      Show(mTotalNumBlocks,  gBuf2, kBufLen, aShowTilde),
-      100.0);
-
-    W("  Unreported:     %12s bytes (%6.2f%%) in %7s blocks (%6.2f%%)\n",
-      Show(mUnreported.mUsableSize, gBuf1, kBufLen, aShowTilde),
-      Percent(mUnreported.mUsableSize, mTotalUsableSize),
-      Show(mUnreported.mNumBlocks, gBuf2, kBufLen, aShowTilde),
-      Percent(mUnreported.mNumBlocks, mTotalNumBlocks));
+  char* b = aBuf + aBufLen - 1;
+  *b = '\0';
+  do {
+    b--;
+    if (b == aBuf) {
+      MOZ_CRASH("Base32 buffer too small");
+    }
+    *b = digits[aN % 32];
+    aN /= 32;
+  } while (aN);
 
-    W("  Once-reported:  %12s bytes (%6.2f%%) in %7s blocks (%6.2f%%)\n",
-      Show(mOnceReported.mUsableSize, gBuf1, kBufLen, aShowTilde),
-      Percent(mOnceReported.mUsableSize, mTotalUsableSize),
-      Show(mOnceReported.mNumBlocks, gBuf2, kBufLen, aShowTilde),
-      Percent(mOnceReported.mNumBlocks, mTotalNumBlocks));
-
-    W("  Twice-reported: %12s bytes (%6.2f%%) in %7s blocks (%6.2f%%)\n",
-      Show(mTwiceReported.mUsableSize, gBuf1, kBufLen, aShowTilde),
-      Percent(mTwiceReported.mUsableSize, mTotalUsableSize),
-      Show(mTwiceReported.mNumBlocks, gBuf2, kBufLen, aShowTilde),
-      Percent(mTwiceReported.mNumBlocks, mTotalNumBlocks));
-  }
-
-  virtual void PrintStats(const Writer& aWriter) const
-  {
-    size_t unreportedSize =
-      mUnreported.mRecordTable.sizeOfIncludingThis(MallocSizeOf);
-    W("    Unreported table:     %10s bytes (%s entries, %s used)\n",
-      Show(unreportedSize,                      gBuf1, kBufLen),
-      Show(mUnreported.mRecordTable.capacity(), gBuf2, kBufLen),
-      Show(mUnreported.mRecordTable.count(),    gBuf3, kBufLen));
-
-    size_t onceReportedSize =
-      mOnceReported.mRecordTable.sizeOfIncludingThis(MallocSizeOf);
-    W("    Once-reported table:  %10s bytes (%s entries, %s used)\n",
-      Show(onceReportedSize,                      gBuf1, kBufLen),
-      Show(mOnceReported.mRecordTable.capacity(), gBuf2, kBufLen),
-      Show(mOnceReported.mRecordTable.count(),    gBuf3, kBufLen));
+  return b;
+}
 
-    size_t twiceReportedSize =
-      mTwiceReported.mRecordTable.sizeOfIncludingThis(MallocSizeOf);
-    W("    Twice-reported table: %10s bytes (%s entries, %s used)\n",
-      Show(twiceReportedSize,                      gBuf1, kBufLen),
-      Show(mTwiceReported.mRecordTable.capacity(), gBuf2, kBufLen),
-      Show(mTwiceReported.mRecordTable.count(),    gBuf3, kBufLen));
-  }
-};
-
-class HeapAnalyzer MOZ_FINAL : public Analyzer
+// Converts a pointer to a unique ID. Reuses the existing ID for the pointer if
+// it's been seen before.
+static const char* Id(PointerIdMap& aIdMap, uint32_t& aNextId,
+                      const void* aPtr, char* aBuf, size_t aBufLen)
 {
-  RecordKindData mLive;
-
-public:
-  HeapAnalyzer() : mLive(1024) {}
-
-  virtual const char* AnalyzeFunctionName() const { return "AnalyzeHeap"; }
-
-  virtual RecordTable* ProcessBlock(const Block& aBlock)
-  {
-    mLive.processBlock(aBlock);
-
-    return &mLive.mRecordTable;
+  uint32_t id;
+  PointerIdMap::AddPtr p = aIdMap.lookupForAdd(aPtr);
+  if (!p) {
+    id = aNextId++;
+    (void)aIdMap.add(p, aPtr, id);
+  } else {
+    id = p->value();
   }
-
-  virtual void PrintRecords(const Writer& aWriter,
-                            CodeAddressService* aLocService) const
-  {
-    size_t totalUsableSize = mLive.mUsableSize;
-    PrintSortedRecords(aWriter, aLocService, Record::CmpByUsable,
-                       "Live", "live", mLive.mRecordTable, totalUsableSize,
-                       mLive.mUsableSize,
-                       /* showReportedAt = */ false,
-                       /* showCategoryPercentage = */ false);
-  }
-
-  virtual void PrintSummary(const Writer& aWriter, bool aShowTilde) const
-  {
-    W("  Total: %s bytes in %s blocks\n",
-      Show(mLive.mUsableSize, gBuf1, kBufLen, aShowTilde),
-      Show(mLive.mNumBlocks,  gBuf2, kBufLen, aShowTilde));
-  }
-
-  virtual void PrintStats(const Writer& aWriter) const
-  {
-    size_t liveSize = mLive.mRecordTable.sizeOfIncludingThis(MallocSizeOf);
-    W("    Live table:           %10s bytes (%s entries, %s used)\n",
-      Show(liveSize,                      gBuf1, kBufLen),
-      Show(mLive.mRecordTable.capacity(), gBuf2, kBufLen),
-      Show(mLive.mRecordTable.count(),    gBuf3, kBufLen));
-  }
-};
+  return Base32(id, aBuf, aBufLen);
+}
 
 static void
-AnalyzeImpl(Analyzer *aAnalyzer, const Writer& aWriter)
+AnalyzeReportsImpl(JSONWriter& aWriter)
 {
   if (!gIsDMDRunning) {
     return;
   }
 
   AutoBlockIntercepts block(Thread::Fetch());
   AutoLockState lock;
 
-  static int analysisCount = 1;
-  StatusMsg("%s %d {\n", aAnalyzer->AnalyzeFunctionName(), analysisCount++);
+  // Allocate this on the heap instead of the stack because it's fairly large.
+  auto locService = InfallibleAllocPolicy::new_<CodeAddressService>();
 
-  StatusMsg("  gathering heap block records...\n");
+  StackTraceSet usedStackTraces;
+  usedStackTraces.init(512);
+
+  PointerSet usedPcs;
+  usedPcs.init(512);
 
-  bool anyBlocksSampled = false;
+  PointerIdMap idMap;
+  idMap.init(512);
 
-  for (BlockTable::Range r = gBlockTable->all(); !r.empty(); r.popFront()) {
-    const Block& b = r.front();
-    RecordTable* table = aAnalyzer->ProcessBlock(b);
+  static int analysisCount = 1;
+  StatusMsg("Dump %d {\n", analysisCount++);
+
+  aWriter.Start();
+  {
+    #define ID(p) Id(idMap, id, p, idBuf, idBufLen)
 
-    RecordKey key(b);
-    RecordTable::AddPtr p = table->lookupForAdd(key);
-    if (!p) {
-      Record tr(key);
-      (void)table->add(p, tr);
+    aWriter.IntProperty("version", kOutputVersionNumber);
+
+    aWriter.StartObjectProperty("invocation");
+    {
+      aWriter.StringProperty("dmdEnvVar", gOptions->DMDEnvVar());
+      aWriter.IntProperty("sampleBelowSize", gOptions->SampleBelowSize());
     }
-    p->Add(b);
+    aWriter.EndObject();
+
+    StatusMsg("  Constructing the heap block list...\n");
 
-    anyBlocksSampled = anyBlocksSampled || b.IsSampled();
-  }
+    static const size_t idBufLen = 16;
+    char idBuf[idBufLen];
+    uint32_t id = 0;
+
+    aWriter.StartArrayProperty("blockList");
+    {
+      for (BlockTable::Range r = gBlockTable->all(); !r.empty(); r.popFront()) {
+        const Block& b = r.front();
+        b.AddStackTracesToTable(usedStackTraces);
 
-  WriteSeparator();
-  W("Invocation {\n");
-  W("  $DMD = '%s'\n", gOptions->DMDEnvVar());
-  W("  Function = %s\n", aAnalyzer->AnalyzeFunctionName());
-  W("  Sample-below size = %lld\n", (long long)(gOptions->SampleBelowSize()));
-  W("}\n\n");
+        aWriter.StartObjectElement(aWriter.SingleLineStyle);
+        {
+          if (!b.IsSampled()) {
+            aWriter.IntProperty("req", b.ReqSize());
+            if (b.SlopSize() > 0) {
+              aWriter.IntProperty("slop", b.SlopSize());
+            }
+          }
+          aWriter.StringProperty("alloc", ID(b.AllocStackTrace()));
+          if (b.NumReports() > 0) {
+            aWriter.StartArrayProperty("reps");
+            {
+              if (b.ReportStackTrace1()) {
+                aWriter.StringElement(ID(b.ReportStackTrace1()));
+              }
+              if (b.ReportStackTrace2()) {
+                aWriter.StringElement(ID(b.ReportStackTrace2()));
+              }
+            }
+            aWriter.EndArray();
+          }
+        }
+        aWriter.EndObject();
+      }
+    }
+    aWriter.EndArray();
 
-  // Allocate this on the heap instead of the stack because it's fairly large.
-  CodeAddressService* locService = InfallibleAllocPolicy::new_<CodeAddressService>();
+    StatusMsg("  Constructing the stack trace table...\n");
 
-  aAnalyzer->PrintRecords(aWriter, locService);
+    aWriter.StartObjectProperty("traceTable");
+    {
+      for (StackTraceSet::Enum e(usedStackTraces); !e.empty(); e.popFront()) {
+        const StackTrace* const st = e.front();
+        aWriter.StartArrayProperty(ID(st), aWriter.SingleLineStyle);
+        {
+          for (uint32_t i = 0; i < st->Length(); i++) {
+            const void* pc = st->Pc(i);
+            aWriter.StringElement(ID(pc));
+            usedPcs.put(pc);
+          }
+        }
+        aWriter.EndArray();
+      }
+    }
+    aWriter.EndObject();
 
-  WriteSeparator();
-  W("Summary {\n");
+    StatusMsg("  Constructing the stack frame table...\n");
 
-  bool showTilde = anyBlocksSampled;
-  aAnalyzer->PrintSummary(aWriter, showTilde);
+    aWriter.StartObjectProperty("frameTable");
+    {
+      static const size_t locBufLen = 1024;
+      char locBuf[locBufLen];
+
+      for (PointerSet::Enum e(usedPcs); !e.empty(); e.popFront()) {
+        const void* const pc = e.front();
 
-  W("}\n\n");
+        // Use 0 for the frame number. See the JSON format description comment
+        // in DMD.h to understand why.
+        locService->GetLocation(0, pc, locBuf, locBufLen);
+        aWriter.StringProperty(ID(pc), locBuf);
+      }
+    }
+    aWriter.EndObject();
 
-  // Stats are non-deterministic, so don't show them in test mode.
-  if (!gOptions->IsTestMode()) {
+    #undef ID
+  }
+  aWriter.End();
+
+  if (gOptions->ShowDumpStats()) {
     Sizes sizes;
     SizeOfInternal(&sizes);
 
-    WriteSeparator();
-    W("Execution measurements {\n");
+    static const size_t kBufLen = 64;
+    char buf1[kBufLen];
+    char buf2[kBufLen];
+    char buf3[kBufLen];
 
-    W("  Data structures that persist after Dump() ends {\n");
+    StatusMsg("  Execution measurements {\n");
 
-    W("    Used stack traces:    %10s bytes\n",
-      Show(sizes.mStackTracesUsed, gBuf1, kBufLen));
+    StatusMsg("    Data structures that persist after Dump() ends {\n");
 
-    W("    Unused stack traces:  %10s bytes\n",
-      Show(sizes.mStackTracesUnused, gBuf1, kBufLen));
+    StatusMsg("      Used stack traces:    %10s bytes\n",
+      Show(sizes.mStackTracesUsed, buf1, kBufLen));
 
-    W("    Stack trace table:    %10s bytes (%s entries, %s used)\n",
-      Show(sizes.mStackTraceTable,       gBuf1, kBufLen),
-      Show(gStackTraceTable->capacity(), gBuf2, kBufLen),
-      Show(gStackTraceTable->count(),    gBuf3, 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));
 
-    W("    Block table:          %10s bytes (%s entries, %s used)\n",
-      Show(sizes.mBlockTable,       gBuf1, kBufLen),
-      Show(gBlockTable->capacity(), gBuf2, kBufLen),
-      Show(gBlockTable->count(),    gBuf3, kBufLen));
+    StatusMsg("      Block table:          %10s bytes (%s entries, %s used)\n",
+      Show(sizes.mBlockTable,       buf1, kBufLen),
+      Show(gBlockTable->capacity(), buf2, kBufLen),
+      Show(gBlockTable->count(),    buf3, kBufLen));
 
-    W("  }\n");
-    W("  Data structures that are destroyed after Dump() ends {\n");
+    StatusMsg("    }\n");
+    StatusMsg("    Data structures that are destroyed after Dump() ends {\n");
 
-    aAnalyzer->PrintStats(aWriter);
+    StatusMsg("      Location service:      %10s bytes\n",
+      Show(locService->SizeOfIncludingThis(MallocSizeOf), buf1, kBufLen));
+    StatusMsg("      Used stack traces set: %10s bytes\n",
+      Show(usedStackTraces.sizeOfExcludingThis(MallocSizeOf), buf1, kBufLen));
+    StatusMsg("      Used PCs set:          %10s bytes\n",
+      Show(usedPcs.sizeOfExcludingThis(MallocSizeOf), buf1, kBufLen));
+    StatusMsg("      Pointer ID map:        %10s bytes\n",
+      Show(idMap.sizeOfExcludingThis(MallocSizeOf), buf1, kBufLen));
 
-    W("    Location service:     %10s bytes\n",
-      Show(locService->SizeOfIncludingThis(MallocSizeOf), gBuf1, kBufLen));
-
-    W("  }\n");
-    W("  Counts {\n");
+    StatusMsg("    }\n");
+    StatusMsg("    Counts {\n");
 
     size_t hits   = locService->NumCacheHits();
     size_t misses = locService->NumCacheMisses();
     size_t requests = hits + misses;
-    W("    Location service:    %10s requests\n",
-      Show(requests, gBuf1, kBufLen));
+    StatusMsg("      Location service:    %10s requests\n",
+      Show(requests, buf1, kBufLen));
 
     size_t count    = locService->CacheCount();
     size_t capacity = locService->CacheCapacity();
-    W("    Location service cache:  "
+    StatusMsg("      Location service cache:  "
       "%4.1f%% hit rate, %.1f%% occupancy at end\n",
       Percent(hits, requests), Percent(count, capacity));
 
-    W("  }\n");
-    W("}\n\n");
+    StatusMsg("    }\n");
+    StatusMsg("  }\n");
   }
 
   InfallibleAllocPolicy::delete_(locService);
 
   StatusMsg("}\n");
 }
 
 MOZ_EXPORT void
-AnalyzeReports(const Writer& aWriter)
+AnalyzeReports(JSONWriter& aWriter)
 {
-  ReportsAnalyzer aAnalyzer;
-  AnalyzeImpl(&aAnalyzer, aWriter);
-}
-
-MOZ_EXPORT void
-AnalyzeHeap(const Writer& aWriter)
-{
-  HeapAnalyzer analyzer;
-  AnalyzeImpl(&analyzer, aWriter);
+  AnalyzeReportsImpl(aWriter);
+  ClearReports();
 }
 
 //---------------------------------------------------------------------------
 // Testing
 //---------------------------------------------------------------------------
 
 // This function checks that heap blocks that have the same stack trace but
 // different (or no) reporters get aggregated separately.
@@ -2197,30 +1868,33 @@ void foo()
 static void
 UseItOrLoseIt(void* a)
 {
   char buf[64];
   sprintf(buf, "%p\n", a);
   fwrite(buf, 1, strlen(buf) + 1, stderr);
 }
 
-// The output from this should be compared against test-expected.dmd.  It's
-// been tested on Linux64, and probably will give different results on other
+// The output from this should be tested with check_test_output.py.  It's been
+// tested on Linux64, and probably will give different results on other
 // platforms.
 static void
-RunTestMode(FILE* fp)
+RunTestMode(UniquePtr<FpWriteFunc> aF1, UniquePtr<FpWriteFunc> aF2,
+            UniquePtr<FpWriteFunc> aF3, UniquePtr<FpWriteFunc> aF4)
 {
-  Writer writer(FpWrite, fp);
-
   // The first part of this test requires sampling to be disabled.
   gOptions->SetSampleBelowSize(1);
 
+  //---------
+
   // AnalyzeReports 1.  Zero for everything.
-  AnalyzeReports(writer);
-  AnalyzeHeap(writer);
+  JSONWriter writer1(Move(aF1));
+  AnalyzeReports(writer1);
+
+  //---------
 
   // AnalyzeReports 2: 1 freed, 9 out of 10 unreported.
   // AnalyzeReports 3: still present and unreported.
   int i;
   char* a;
   for (i = 0; i < 10; i++) {
       a = (char*) malloc(100);
       UseItOrLoseIt(a);
@@ -2324,34 +1998,34 @@ RunTestMode(FILE* fp)
 //posix_memalign(&y, 128, 129);         // rounds up to 256
 //UseItOrLoseIt(y);
   // XXX: valloc doesn't work on Windows.
 //void* z = valloc(1);                  // rounds up to 4096
 //UseItOrLoseIt(z);
 //aligned_alloc(64, 256);               // XXX: C11 only
 
   // AnalyzeReports 2.
-  AnalyzeReports(writer);
-  AnalyzeHeap(writer);
+  JSONWriter writer2(Move(aF2));
+  AnalyzeReports(writer2);
 
   //---------
 
   Report(a2);
   Report(a2);
   free(c);
   free(e);
   Report(e2);
   free(e3);
 //free(x);
 //free(y);
 //free(z);
 
   // AnalyzeReports 3.
-  AnalyzeReports(writer);
-  AnalyzeHeap(writer);
+  JSONWriter writer3(Move(aF3));
+  AnalyzeReports(writer3);
 
   //---------
 
   // Clear all knowledge of existing blocks to give us a clean slate.
   gBlockTable->clear();
 
   gOptions->SetSampleBelowSize(128);
 
@@ -2405,18 +2079,18 @@ RunTestMode(FILE* fp)
     UseItOrLoseIt(s);
   }
   MOZ_ASSERT(gSmallBlockActualSizeCounter == 64);
 
   // At the end we're 64 bytes into the current sample so we report ~1,424
   // bytes of allocation overall, which is 64 less than the real value 1,488.
 
   // AnalyzeReports 4.
-  AnalyzeReports(writer);
-  AnalyzeHeap(writer);
+  JSONWriter writer4(Move(aF4));
+  AnalyzeReports(writer4);
 }
 
 //---------------------------------------------------------------------------
 // Stress testing microbenchmark
 //---------------------------------------------------------------------------
 
 // This stops otherwise-unused variables from being optimized away.
 static void
@@ -2472,19 +2146,19 @@ stress1()
 // This stress test does lots of allocations and frees, which is where most of
 // DMD's overhead occurs.  It allocates 1,000,000 64-byte blocks, spread evenly
 // across 1,000 distinct stack traces.  It frees every second one immediately
 // after allocating it.
 //
 // It's highly artificial, but it's deterministic and easy to run.  It can be
 // timed under different conditions to glean performance data.
 static void
-RunStressMode(FILE* fp)
+RunStressMode(UniquePtr<FpWriteFunc> aF)
 {
-  Writer writer(FpWrite, fp);
+  JSONWriter writer(Move(aF));
 
   // Disable sampling for maximum stress.
   gOptions->SetSampleBelowSize(1);
 
   stress1(); stress1(); stress1(); stress1(); stress1();
   stress1(); stress1(); stress1(); stress1(); stress1();
 
   AnalyzeReports(writer);
--- a/memory/replace/dmd/DMD.h
+++ b/memory/replace/dmd/DMD.h
@@ -2,76 +2,131 @@
 /* vim: set ts=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/. */
 
 #ifndef DMD_h___
 #define DMD_h___
 
-#include <stdarg.h>
 #include <string.h>
 
 #include "mozilla/Types.h"
 
 namespace mozilla {
+
+class JSONWriter;
+
 namespace dmd {
 
 // Mark a heap block as reported by a memory reporter.
 MOZ_EXPORT void
 Report(const void* aPtr);
 
 // Mark a heap block as reported immediately on allocation.
 MOZ_EXPORT void
 ReportOnAlloc(const void* aPtr);
 
-class Writer
-{
-public:
-  typedef void (*WriterFun)(void* aWriteState, const char* aFmt, va_list aAp);
-
-  Writer(WriterFun aWriterFun, void* aWriteState)
-    : mWriterFun(aWriterFun), mWriteState(aWriteState)
-  {}
-
-  void Write(const char* aFmt, ...) const;
-
-private:
-  WriterFun mWriterFun;
-  void*     mWriteState;
-};
-
 // Clears existing reportedness data from any prior runs of the memory
 // reporters.  The following sequence should be used.
 // - ClearReports()
 // - run the memory reporters
 // - AnalyzeReports()
 // This sequence avoids spurious twice-reported warnings.
 MOZ_EXPORT void
 ClearReports();
 
-// Determines which heap blocks have been reported, and dumps a human-readable
-// summary (via |aWrite|).  If |aWrite| is nullptr it will dump to stderr.
-// Beware: this output may have very long lines.
-MOZ_EXPORT void
-AnalyzeReports(const Writer& aWriter);
-
-// Measures all heap blocks, and dumps a human-readable summary (via |aWrite|).
-// If |aWrite| is nullptr it will dump to stderr.  Beware: this output may
-// have very long lines.
+// Determines which heap blocks have been reported, and dumps JSON output
+// (via |aWriter|) describing the heap.
+//
+// The following sample output contains comments that explain the format and
+// design choices. The output files can be quite large, so a number of
+// decisions were made to minimize size, such as using short property names and
+// omitting properties whenever possible.
+//
+// {
+//   // The version number of the format, which will be incremented each time
+//   // backwards-incompatible changes are made. A mandatory integer.
+//   "version": 1,
+//
+//   // Information about how DMD was invoked. A mandatory object.
+//   "invocation": {
+//     // The contents of the $DMD environment variable. A mandatory string.
+//     "dmdEnvVar": "1",
+//
+//     // The value of the --sample-below-size option. A mandatory integer.
+//     "sampleBelowSize": 4093
+//   },
+//
+//   // Details of all analyzed heap blocks. A mandatory array.
+//   "blockList": [
+//     // An example of a non-sampled heap block.
+//     {
+//       // Requested size, in bytes. In non-sampled blocks this is a
+//       // mandatory integer. In sampled blocks this is not present, and the
+//       // requested size is equal to the "sampleBelowSize" value. Therefore,
+//       // the block is sampled if and only if this property is absent.
+//       "req": 3584,
+//
+//       // Requested slop size, in bytes. This is mandatory if it is non-zero,
+//       // but omitted otherwise. Because sampled blocks never have slop, this
+//       // property is never present for non-sampled blocks.
+//       "slop": 512,
+//
+//       // The stack trace at which the block was allocated. A mandatory
+//       // string which indexes into the "traceTable" object.
+//       "alloc": "A"
+//     },
+//
+//     // An example of a sampled heap block.
+//     {
+//       "alloc": "B",
+//
+//       // One or more stack traces at which this heap block was reported by a
+//       // memory reporter. An optional array. The elements are strings that
+//       // index into the "traceTable" object.
+//       "reps": ["C"]
+//     }
+//   ],
+//
+//   // The stack traces referenced by elements of the "blockList" array. This
+//   // could be an array, but making it an object makes it easier to see
+//   // which stacks correspond to which references in the "blockList" array.
+//   "traceTable": {
+//     // Each property corresponds to a stack trace mentioned in the "blocks"
+//     // object. Each element is an index into the "frameTable" object.
+//     "A": ["D", "E"],
+//     "B": ["D", "F"],
+//     "C": ["G", "H"]
+//   },
+//
+//   // The stack frames referenced by the "traceTable" object. The
+//   // descriptions can be quite long, so they are stored separately from the
+//   // "traceTable" object so that each one only has to be written once.
+//   // This could also be an array, but again, making it an object makes it
+//   // easier to see which frames correspond to which references in the
+//   // "traceTable" object.
+//   "frameTable": {
+//     // Each property key is a frame key mentioned in the "traceTable" object.
+//     // Each property value is a string containing a frame description. Each
+//     // frame description must be in a format recognized by the stack-fixing
+//     // scripts (e.g. fix_linux_stack.py), which require a frame number at
+//     // the start. Because each stack frame description in this table can
+//     // be shared between multiple stack traces, we use a dummy value of
+//     // #00. The proper frame number can be reconstructed later by scripts
+//     // that output stack traces in a conventional non-shared format.
+//     "D": "#00: foo (Foo.cpp:123)",
+//     "E": "#00: bar (Bar.cpp:234)",
+//     "F": "#00: baz (Baz.cpp:345)",
+//     "G": "#00: quux (Quux.cpp:456)",
+//     "H": "#00: quuux (Quux.cpp:567)"
+//   }
+// }
 MOZ_EXPORT void
-AnalyzeHeap(const Writer& aWriter);
-
-// A useful |WriterFun|.  For example, if |fp| is a FILE* you want
-// |AnalyzeReports|'s output to be written to, call:
-//
-//   dmd::Writer writer(FpWrite, fp);
-//   dmd::AnalyzeReports(writer);
-MOZ_EXPORT void
-FpWrite(void* aFp, const char* aFmt, va_list aAp);
+AnalyzeReports(mozilla::JSONWriter& aWriter);
 
 struct Sizes
 {
   size_t mStackTracesUsed;
   size_t mStackTracesUnused;
   size_t mStackTraceTable;
   size_t mBlockTable;
 
--- a/memory/replace/dmd/check_test_output.py
+++ b/memory/replace/dmd/check_test_output.py
@@ -1,14 +1,18 @@
 #! /usr/bin/python
+#
+# 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/.
 
 """This script takes the file produced by DMD's test mode and checks its
 correctness.
 
-It produces the following output files: $TMP/test-{fixed,filtered,diff}.dmd.
+It produces the following output files: $TMP/full-{fixed,filtered,diff}.dmd.
 
 It runs the appropriate fix* script to get nice stack traces.  It also
 filters out platform-specific details from the test output file.
 
 Note: you must run this from the same directory that you invoked DMD's test
 mode, otherwise the fix* script will not work properly, because some of the
 paths in the test output are relative.
 
@@ -18,63 +22,55 @@ from __future__ import print_function
 
 import os
 import platform
 import re
 import subprocess
 import sys
 import tempfile
 
-
-def main():
-
-    # Arguments
-
-    if (len(sys.argv) != 3):
-        print("usage:", sys.argv[0], "<topsrcdir> <test.dmd>")
-        sys.exit(1)
-
-    srcdir = sys.argv[1]
-
+def test(src_dir, kind, options, i):
     # Filenames
-
-    tempdir = tempfile.gettempdir()
-    in_name       = sys.argv[2]
-    fixed_name    = tempdir + os.sep + "test-fixed.dmd"
-    filtered_name = tempdir + os.sep + "test-filtered.dmd"
-    diff_name     = tempdir + os.sep + "test-diff.dmd"
-    expected_name = srcdir + os.sep + \
-                    "memory/replace/dmd/test-expected.dmd"
+    tmp_dir = tempfile.gettempdir()
+    in_name        = os.path.join(src_dir, "full{:d}.json".format(i))
+    fixed_name     = os.path.join(tmp_dir, "full-{:}-fixed{:d}.json".format(kind, i))
+    converted_name = os.path.join(tmp_dir, "full-{:}-converted{:d}.txt".format(kind, i))
+    filtered_name  = os.path.join(tmp_dir, "full-{:}-filtered{:d}.txt".format(kind, i))
+    diff_name      = os.path.join(tmp_dir, "full-{:}-diff{:d}.txt".format(kind, i))
+    expected_name  = os.path.join(src_dir, "memory", "replace", "dmd", "test", "full-{:}-expected{:d}.txt".format(kind, i))
 
     # Fix stack traces
 
-    print("fixing output to", fixed_name)
-
-    sysname = platform.system()
-    if sysname == "Linux":
-        fix = srcdir + os.sep + "tools/rb/fix_linux_stack.py"
-    elif sysname == "Darwin":
-        fix = srcdir + os.sep + "tools/rb/fix_macosx_stack.py"
+    sys_name = platform.system()
+    fix = os.path.join(src_dir, "tools", "rb")
+    if sys_name == "Linux":
+        fix = os.path.join(fix, "fix_linux_stack.py")
+    elif sys_name == "Darwin":
+        fix = os.path.join(fix, "fix_macosx_stack.py")
     else:
-        print("unhandled platform: " + sysname, file=sys.stderr)
+        print("unhandled platform: " + sys_name, file=sys.stderr)
         sys.exit(1)
 
     subprocess.call(fix, stdin=open(in_name, "r"),
                          stdout=open(fixed_name, "w"))
 
+    # Convert from JSON
+
+    convert = [os.path.join(src_dir, "memory", "replace", "dmd", "dmd.py")] + \
+               options + [fixed_name]
+    subprocess.call(convert, stdout=open(converted_name, "w"))
+
     # Filter output
 
     # In heap block records we filter out most stack frames.  The only thing
     # we leave behind is a "DMD.cpp" entry if we see one or more frames that
     # have DMD.cpp in them.  There is simply too much variation to do anything
     # better than that.
 
-    print("filtering output to", filtered_name)
-
-    with open(fixed_name, "r") as fin, \
+    with open(converted_name, "r") as fin, \
          open(filtered_name, "w") as fout:
 
         test_frame_re = re.compile(r".*(DMD.cpp)")
 
         for line in fin:
             if re.match(r"  (Allocated at {|Reported( again)? at {)", line):
                 # It's a heap block record.
                 print(line, end='', file=fout)
@@ -95,22 +91,37 @@ def main():
                         break
 
             else:
                 # A line that needs no special handling.  Copy it through.
                 print(line, end='', file=fout)
 
     # Compare with expected output
 
-    print("diffing output to", diff_name)
-
     ret = subprocess.call(["diff", "-u", expected_name, filtered_name],
                           stdout=open(diff_name, "w"))
 
     if ret == 0:
-        print("test PASSED")
+        print("TEST-PASS | {:} {:d} | ok".format(kind, i))
     else:
-        print("test FAILED (did you remember to run this script and Firefox "
-              "in the same directory?)")
+        print("TEST-UNEXPECTED-FAIL | {:} {:d} | mismatch".format(kind, i))
+        print("Output files:")
+        print("- " + fixed_name);
+        print("- " + converted_name);
+        print("- " + filtered_name);
+        print("- " + diff_name);
+
+
+def main():
+    if (len(sys.argv) != 2):
+        print("usage:", sys.argv[0], "<topsrcdir>")
+        sys.exit(1)
+
+    src_dir = sys.argv[1]
+
+    ntests = 4
+    for i in range(1, ntests+1):
+        test(src_dir, "reports", [], i)
+        test(src_dir, "heap", ["--ignore-reports"], i)
 
 
 if __name__ == "__main__":
     main()
new file mode 100755
--- /dev/null
+++ b/memory/replace/dmd/dmd.py
@@ -0,0 +1,407 @@
+#! /usr/bin/python
+#
+# 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/.
+
+'''This script analyzes a JSON file emitted by DMD.'''
+
+from __future__ import print_function, division
+
+import argparse
+import collections
+import json
+import re
+import sys
+
+# The DMD output version this script handles.
+outputVersion = 1
+
+# If --ignore-alloc-fns is specified, stack frames containing functions that
+# match these strings will be removed.
+allocatorFns = [
+    'replace_malloc',
+    'replace_calloc',
+    'replace_realloc',
+    'replace_memalign',
+    'replace_posix_memalign',
+    'moz_xmalloc',
+    'moz_xcalloc',
+    'moz_xrealloc',
+    'operator new(',
+    'operator new[](',
+    'g_malloc',
+    'g_slice_alloc',
+    'callocCanGC',
+    'reallocCanGC',
+    'vpx_malloc',
+    'vpx_calloc',
+    'vpx_realloc',
+    'vpx_memalign',
+    'js_malloc',
+    'js_calloc',
+    'js_realloc',
+    'pod_malloc',
+    'pod_calloc',
+    'pod_realloc',
+]
+
+class Record(object):
+    def __init__(self):
+        self.numBlocks = 0
+        self.reqSize = 0
+        self.slopSize = 0
+        self.usableSize = 0
+        self.isSampled = False
+        self.usableSizes = collections.defaultdict(int)
+
+    @staticmethod
+    def cmpByIsSampled(r1, r2):
+        # Treat sampled as smaller than non-sampled.
+        return cmp(r2.isSampled, r1.isSampled)
+
+    @staticmethod
+    def cmpByUsableSize(r1, r2):
+        # Sort by usable size, then req size, then by isSampled.
+        return cmp(r1.usableSize, r2.usableSize) or Record.cmpByReqSize(r1, r2)
+
+    @staticmethod
+    def cmpByReqSize(r1, r2):
+        # Sort by req size, then by isSampled.
+        return cmp(r1.reqSize, r2.reqSize) or Record.cmpByIsSampled(r1, r2)
+
+    @staticmethod
+    def cmpBySlopSize(r1, r2):
+        # Sort by slop size, then by isSampled.
+        return cmp(r1.slopSize, r2.slopSize) or Record.cmpByIsSampled(r1, r2)
+
+
+sortByChoices = {
+    'usable': Record.cmpByUsableSize,   # the default
+    'req':    Record.cmpByReqSize,
+    'slop':   Record.cmpBySlopSize,
+}
+
+
+def parseCommandLine():
+    # 24 is the maximum number of frames that DMD will produce.
+    def range_1_24(string):
+        value = int(string)
+        if value < 1 or value > 24:
+            msg = '{:s} is not in the range 1..24'.format(string)
+            raise argparse.ArgumentTypeError(msg)
+        return value
+
+    description = '''
+Analyze heap data produced by DMD.
+If no files are specified, read from stdin.
+Write to stdout unless -o/--output is specified.
+'''
+    p = argparse.ArgumentParser(description=description)
+
+    p.add_argument('-o', '--output', type=argparse.FileType('w'),
+                   help='output file; stdout if unspecified')
+
+    p.add_argument('-f', '--max-frames', type=range_1_24,
+                   help='maximum number of frames to consider in each trace')
+
+    p.add_argument('-r', '--ignore-reports', action='store_true',
+                   help='ignore memory reports data; useful if you just ' +
+                        'want basic heap profiling')
+
+    p.add_argument('-s', '--sort-by', choices=sortByChoices.keys(),
+                   default=sortByChoices.keys()[0],
+                   help='sort the records by a particular metric')
+
+    p.add_argument('-a', '--ignore-alloc-fns', action='store_true',
+                   help='ignore allocation functions at the start of traces')
+
+    p.add_argument('-b', '--show-all-block-sizes', action='store_true',
+                   help='show individual block sizes for each record')
+
+    p.add_argument('input_file', type=argparse.FileType('r'))
+
+    return p.parse_args(sys.argv[1:])
+
+
+def main():
+    args = parseCommandLine()
+
+    j = json.load(args.input_file)
+
+    if j['version'] != outputVersion:
+        raise Exception("'version' property isn't '{:d}'".format(outputVersion))
+
+    # Extract the main parts of the JSON object.
+    invocation = j['invocation']
+    dmdEnvVar = invocation['dmdEnvVar']
+    sampleBelowSize = invocation['sampleBelowSize']
+    blockList = j['blockList']
+    traceTable = j['traceTable']
+    frameTable = j['frameTable']
+
+    heapIsSampled = sampleBelowSize > 1     # is sampling present?
+
+    # Remove allocation functions at the start of traces.
+    if args.ignore_alloc_fns:
+        # Build a regexp that matches every function in allocatorFns.
+        escapedAllocatorFns = map(re.escape, allocatorFns)
+        fn_re = re.compile('|'.join(escapedAllocatorFns))
+
+        # Remove allocator fns from each stack trace.
+        for traceKey, frameKeys in traceTable.items():
+            numSkippedFrames = 0
+            for frameKey in frameKeys:
+                frameDesc = frameTable[frameKey]
+                if re.search(fn_re, frameDesc):
+                    numSkippedFrames += 1
+                else:
+                    break
+            if numSkippedFrames > 0:
+                traceTable[traceKey] = frameKeys[numSkippedFrames:]
+
+    # Trim the number of frames.
+    for traceKey, frameKeys in traceTable.items():
+        if len(frameKeys) > args.max_frames:
+            traceTable[traceKey] = frameKeys[:args.max_frames]
+
+    # Aggregate blocks into records. All sufficiently similar blocks go into a
+    # single record.
+
+    if args.ignore_reports:
+        liveRecords = collections.defaultdict(Record)
+    else:
+        unreportedRecords    = collections.defaultdict(Record)
+        onceReportedRecords  = collections.defaultdict(Record)
+        twiceReportedRecords = collections.defaultdict(Record)
+
+    heapUsableSize = 0
+    heapBlocks = 0
+
+    for block in blockList:
+        # For each block we compute a |recordKey|, and all blocks with the same
+        # |recordKey| are aggregated into a single record. The |recordKey| is
+        # derived from the block's 'alloc' and 'reps' (if present) stack
+        # traces.
+        #
+        # Each stack trace has a key in the JSON file. But we don't use that
+        # key to construct |recordKey|; instead we use the frame keys.
+        # This is because the stack trimming done for --max-frames can cause
+        # stack traces with distinct trace keys to end up with the same frame
+        # keys, and these should be considered equivalent. E.g. if we have
+        # distinct traces T1:[A,B,C] and T2:[A,B,D] and we trim the final frame
+        # of each they should be considered equivalent.
+        allocatedAt = block['alloc']
+        if args.ignore_reports:
+            recordKey = str(traceTable[allocatedAt])
+            records = liveRecords
+        else:
+            recordKey = str(traceTable[allocatedAt])
+            if 'reps' in block:
+                reportedAts = block['reps']
+                for reportedAt in reportedAts:
+                    recordKey += str(traceTable[reportedAt])
+                if len(reportedAts) == 1:
+                    records = onceReportedRecords
+                else:
+                    records = twiceReportedRecords
+            else:
+                records = unreportedRecords
+
+        record = records[recordKey]
+
+        if 'req' in block:
+            # not sampled
+            reqSize = block['req']
+            slopSize = block.get('slop', 0)
+            isSampled = False
+        else:
+            # sampled
+            reqSize = sampleBelowSize
+            if 'slop' in block:
+                raise Exception("'slop' property in sampled block'")
+            slopSize = 0
+            isSampled = True
+
+        usableSize = reqSize + slopSize
+        heapUsableSize += usableSize
+        heapBlocks += 1
+
+        record.numBlocks  += 1
+        record.reqSize    += reqSize
+        record.slopSize   += slopSize
+        record.usableSize += usableSize
+        record.isSampled   = record.isSampled or isSampled
+        record.allocatedAt = block['alloc']
+        if args.ignore_reports:
+            pass
+        else:
+            if 'reps' in block:
+                record.reportedAts = block['reps']
+        record.usableSizes[(usableSize, isSampled)] += 1
+
+    # Print records.
+
+    separator = '#' + '-' * 65 + '\n'
+
+    def number(n, isSampled):
+        '''Format a number, with comma as a separator and a '~' prefix if it's
+        sampled.'''
+        return '{:}{:,d}'.format('~' if isSampled else '', n)
+
+    def perc(m, n):
+        return 0 if n == 0 else (100 * m / n)
+
+    def plural(n):
+        return '' if n == 1 else 's'
+
+    # Prints to stdout, or to file if -o/--output was specified.
+    def out(*arguments, **kwargs):
+        print(*arguments, file=args.output, **kwargs)
+
+    def printStack(traceTable, frameTable, traceKey):
+        # The frame number is always '#00' (see DMD.h for why), so we have to
+        # replace that with the correct frame number.
+        for n, frameKey in enumerate(traceTable[traceKey], start=1):
+            out('    #{:02d}{:}'.format(n, frameTable[frameKey][3:]))
+
+    def printRecords(recordKind, records, heapUsableSize):
+        RecordKind = recordKind.capitalize()
+        out(separator)
+        numRecords = len(records)
+        cmpRecords = sortByChoices[args.sort_by]
+        sortedRecords = sorted(records.values(), cmp=cmpRecords, reverse=True)
+        kindBlocks = 0
+        kindUsableSize = 0
+        maxRecord = 1000
+
+        # First iteration: get totals, etc.
+        for record in sortedRecords:
+            kindBlocks     += record.numBlocks
+            kindUsableSize += record.usableSize
+
+        # Second iteration: print.
+        if numRecords == 0:
+            out('# no {:} heap blocks\n'.format(recordKind))
+
+        kindCumulativeUsableSize = 0
+        for i, record in enumerate(sortedRecords, start=1):
+            # Stop printing at the |maxRecord|th record.
+            if i == maxRecord:
+                out('# {:}: stopping after {:,d} heap block records\n'.
+                    format(RecordKind, i))
+                break
+
+            kindCumulativeUsableSize += record.usableSize
+
+            isSampled = record.isSampled
+
+            out(RecordKind + ' {')
+            out('  {:} block{:} in heap block record {:,d} of {:,d}'.
+                format(number(record.numBlocks, isSampled),
+                       plural(record.numBlocks), i, numRecords))
+            out('  {:} bytes ({:} requested / {:} slop)'.
+                format(number(record.usableSize, isSampled),
+                       number(record.reqSize, isSampled),
+                       number(record.slopSize, isSampled)))
+            out('  {:4.2f}% of the heap ({:4.2f}% cumulative)'.
+                format(perc(record.usableSize, heapUsableSize),
+                       perc(kindCumulativeUsableSize, heapUsableSize)))
+            if args.ignore_reports:
+                pass
+            else:
+                out('  {:4.2f}% of {:} ({:4.2f}% cumulative)'.
+                    format(perc(record.usableSize, kindUsableSize),
+                           recordKind,
+                           perc(kindCumulativeUsableSize, kindUsableSize)))
+
+            if args.show_all_block_sizes:
+                usableSizes = sorted(record.usableSizes.items(), reverse=True)
+
+                out('  Individual block sizes: ', end='')
+                isFirst = True
+                for (usableSize, isSampled), count in usableSizes:
+                    if not isFirst:
+                        out('; ', end='')
+                    out('{:}'.format(number(usableSize, isSampled)), end='')
+                    if count > 1:
+                        out(' x {:,d}'.format(count), end='')
+                    isFirst = False
+                out()
+
+            out('  Allocated at {')
+            printStack(traceTable, frameTable, record.allocatedAt)
+            out('  }')
+            if args.ignore_reports:
+                pass
+            else:
+                if hasattr(record, 'reportedAts'):
+                    for n, reportedAt in enumerate(record.reportedAts):
+                        again = 'again ' if n > 0 else ''
+                        out('  Reported {:}at {{'.format(again))
+                        printStack(traceTable, frameTable, reportedAt)
+                        out('  }')
+            out('}\n')
+
+        return (kindUsableSize, kindBlocks)
+
+
+    # Print header.
+    out(separator)
+    out('Invocation {')
+    out('  $DMD = \'' + dmdEnvVar + '\'')
+    out('  Sample-below size = ' + str(sampleBelowSize))
+    out('}\n')
+
+    # Print records.
+    if args.ignore_reports:
+        liveUsableSize, liveBlocks = \
+            printRecords('live', liveRecords, heapUsableSize)
+    else:
+        twiceReportedUsableSize, twiceReportedBlocks = \
+            printRecords('twice-reported', twiceReportedRecords, heapUsableSize)
+
+        unreportedUsableSize, unreportedBlocks = \
+            printRecords('unreported',     unreportedRecords, heapUsableSize)
+
+        onceReportedUsableSize, onceReportedBlocks = \
+            printRecords('once-reported',  onceReportedRecords, heapUsableSize)
+
+    # Print summary.
+    out(separator)
+    out('Summary {')
+    if args.ignore_reports:
+        out('  Total: {:} bytes in {:} blocks'.
+            format(number(liveUsableSize, heapIsSampled),
+                   number(liveBlocks, heapIsSampled)))
+    else:
+        fmt = '  {:15} {:>12} bytes ({:6.2f}%) in {:>7} blocks ({:6.2f}%)'
+        out(fmt.
+            format('Total:',
+                   number(heapUsableSize, heapIsSampled),
+                   100,
+                   number(heapBlocks, heapIsSampled),
+                   100))
+        out(fmt.
+            format('Unreported:',
+                   number(unreportedUsableSize, heapIsSampled),
+                   perc(unreportedUsableSize, heapUsableSize),
+                   number(unreportedBlocks, heapIsSampled),
+                   perc(unreportedBlocks, heapBlocks)))
+        out(fmt.
+            format('Once-reported:',
+                   number(onceReportedUsableSize, heapIsSampled),
+                   perc(onceReportedUsableSize, heapUsableSize),
+                   number(onceReportedBlocks, heapIsSampled),
+                   perc(onceReportedBlocks, heapBlocks)))
+        out(fmt.
+            format('Twice-reported:',
+                   number(twiceReportedUsableSize, heapIsSampled),
+                   perc(twiceReportedUsableSize, heapUsableSize),
+                   number(twiceReportedBlocks, heapIsSampled),
+                   perc(twiceReportedBlocks, heapBlocks)))
+    out('}\n')
+
+
+if __name__ == '__main__':
+    main()
deleted file mode 100644
--- a/memory/replace/dmd/test-expected.dmd
+++ /dev/null
@@ -1,841 +0,0 @@
-#-----------------------------------------------------------------
-
-Invocation {
-  $DMD = '--mode=test'
-  Function = AnalyzeReports
-  Sample-below size = 1
-}
-
-#-----------------------------------------------------------------
-
-# no twice-reported heap blocks
-
-#-----------------------------------------------------------------
-
-# no unreported heap blocks
-
-#-----------------------------------------------------------------
-
-# no once-reported heap blocks
-
-#-----------------------------------------------------------------
-
-Summary {
-  Total:                     0 bytes (100.00%) in       0 blocks (100.00%)
-  Unreported:                0 bytes (  0.00%) in       0 blocks (  0.00%)
-  Once-reported:             0 bytes (  0.00%) in       0 blocks (  0.00%)
-  Twice-reported:            0 bytes (  0.00%) in       0 blocks (  0.00%)
-}
-
-#-----------------------------------------------------------------
-
-Invocation {
-  $DMD = '--mode=test'
-  Function = AnalyzeHeap
-  Sample-below size = 1
-}
-
-#-----------------------------------------------------------------
-
-# no live heap blocks
-
-#-----------------------------------------------------------------
-
-Summary {
-  Total: 0 bytes in 0 blocks
-}
-
-#-----------------------------------------------------------------
-
-Invocation {
-  $DMD = '--mode=test'
-  Function = AnalyzeReports
-  Sample-below size = 1
-}
-
-#-----------------------------------------------------------------
-
-Twice-reported {
-  1 block in heap block record 1 of 4
-  80 bytes (79 requested / 1 slop)
-  0.66% of the heap (0.66% cumulative)
-  29.41% of twice-reported (29.41% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-  Reported at {
-    ... DMD.cpp
-  }
-  Reported again at {
-    ... DMD.cpp
-  }
-}
-
-Twice-reported {
-  1 block in heap block record 2 of 4
-  80 bytes (78 requested / 2 slop)
-  0.66% of the heap (1.32% cumulative)
-  29.41% of twice-reported (58.82% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-  Reported at {
-    ... DMD.cpp
-  }
-  Reported again at {
-    ... DMD.cpp
-  }
-}
-
-Twice-reported {
-  1 block in heap block record 3 of 4
-  80 bytes (77 requested / 3 slop)
-  0.66% of the heap (1.99% cumulative)
-  29.41% of twice-reported (88.24% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-  Reported at {
-    ... DMD.cpp
-  }
-  Reported again at {
-    ... DMD.cpp
-  }
-}
-
-Twice-reported {
-  1 block in heap block record 4 of 4
-  32 bytes (30 requested / 2 slop)
-  0.26% of the heap (2.25% cumulative)
-  11.76% of twice-reported (100.00% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-  Reported at {
-    ... DMD.cpp
-  }
-  Reported again at {
-    ... DMD.cpp
-  }
-}
-
-#-----------------------------------------------------------------
-
-Unreported {
-  9 blocks in heap block record 1 of 3
-  1,008 bytes (900 requested / 108 slop)
-  8.34% of the heap (8.34% cumulative)
-  81.82% of unreported (81.82% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Unreported {
-  2 blocks in heap block record 2 of 3
-  112 bytes (112 requested / 0 slop)
-  0.93% of the heap (9.27% cumulative)
-  9.09% of unreported (90.91% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Unreported {
-  2 blocks in heap block record 3 of 3
-  112 bytes (112 requested / 0 slop)
-  0.93% of the heap (10.19% cumulative)
-  9.09% of unreported (100.00% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-#-----------------------------------------------------------------
-
-Once-reported {
-  1 block in heap block record 1 of 11
-  8,192 bytes (4,097 requested / 4,095 slop)
-  67.77% of the heap (67.77% cumulative)
-  77.40% of once-reported (77.40% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-  Reported at {
-    ... DMD.cpp
-  }
-}
-
-Once-reported {
-  1 block in heap block record 2 of 11
-  1,024 bytes (1,023 requested / 1 slop)
-  8.47% of the heap (76.24% cumulative)
-  9.67% of once-reported (87.07% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-  Reported at {
-    ... DMD.cpp
-  }
-}
-
-Once-reported {
-  1 block in heap block record 3 of 11
-  512 bytes (512 requested / 0 slop)
-  4.24% of the heap (80.48% cumulative)
-  4.84% of once-reported (91.91% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-  Reported at {
-    ... DMD.cpp
-  }
-}
-
-Once-reported {
-  2 blocks in heap block record 4 of 11
-  240 bytes (240 requested / 0 slop)
-  1.99% of the heap (82.46% cumulative)
-  2.27% of once-reported (94.18% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-  Reported at {
-    ... DMD.cpp
-  }
-}
-
-Once-reported {
-  2 blocks in heap block record 5 of 11
-  240 bytes (240 requested / 0 slop)
-  1.99% of the heap (84.45% cumulative)
-  2.27% of once-reported (96.45% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-  Reported at {
-    ... DMD.cpp
-  }
-}
-
-Once-reported {
-  1 block in heap block record 6 of 11
-  96 bytes (96 requested / 0 slop)
-  0.79% of the heap (85.24% cumulative)
-  0.91% of once-reported (97.35% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-  Reported at {
-    ... DMD.cpp
-  }
-}
-
-Once-reported {
-  1 block in heap block record 7 of 11
-  96 bytes (96 requested / 0 slop)
-  0.79% of the heap (86.04% cumulative)
-  0.91% of once-reported (98.26% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-  Reported at {
-    ... DMD.cpp
-  }
-}
-
-Once-reported {
-  1 block in heap block record 8 of 11
-  80 bytes (80 requested / 0 slop)
-  0.66% of the heap (86.70% cumulative)
-  0.76% of once-reported (99.02% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-  Reported at {
-    ... DMD.cpp
-  }
-}
-
-Once-reported {
-  1 block in heap block record 9 of 11
-  80 bytes (80 requested / 0 slop)
-  0.66% of the heap (87.36% cumulative)
-  0.76% of once-reported (99.77% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-  Reported at {
-    ... DMD.cpp
-  }
-}
-
-Once-reported {
-  1 block in heap block record 10 of 11
-  16 bytes (10 requested / 6 slop)
-  0.13% of the heap (87.49% cumulative)
-  0.15% of once-reported (99.92% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-  Reported at {
-    ... DMD.cpp
-  }
-}
-
-Once-reported {
-  1 block in heap block record 11 of 11
-  8 bytes (0 requested / 8 slop)
-  0.07% of the heap (87.56% cumulative)
-  0.08% of once-reported (100.00% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-  Reported at {
-    ... DMD.cpp
-  }
-}
-
-#-----------------------------------------------------------------
-
-Summary {
-  Total:                12,088 bytes (100.00%) in      30 blocks (100.00%)
-  Unreported:            1,232 bytes ( 10.19%) in      13 blocks ( 43.33%)
-  Once-reported:        10,584 bytes ( 87.56%) in      13 blocks ( 43.33%)
-  Twice-reported:          272 bytes (  2.25%) in       4 blocks ( 13.33%)
-}
-
-#-----------------------------------------------------------------
-
-Invocation {
-  $DMD = '--mode=test'
-  Function = AnalyzeHeap
-  Sample-below size = 1
-}
-
-#-----------------------------------------------------------------
-
-Live {
-  1 block in heap block record 1 of 12
-  8,192 bytes (4,097 requested / 4,095 slop)
-  67.77% of the heap (67.77% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Live {
-  1 block in heap block record 2 of 12
-  1,024 bytes (1,023 requested / 1 slop)
-  8.47% of the heap (76.24% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Live {
-  9 blocks in heap block record 3 of 12
-  1,008 bytes (900 requested / 108 slop)
-  8.34% of the heap (84.58% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Live {
-  6 blocks in heap block record 4 of 12
-  528 bytes (528 requested / 0 slop)
-  4.37% of the heap (88.95% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Live {
-  6 blocks in heap block record 5 of 12
-  528 bytes (528 requested / 0 slop)
-  4.37% of the heap (93.32% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Live {
-  1 block in heap block record 6 of 12
-  512 bytes (512 requested / 0 slop)
-  4.24% of the heap (97.55% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Live {
-  1 block in heap block record 7 of 12
-  80 bytes (79 requested / 1 slop)
-  0.66% of the heap (98.21% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Live {
-  1 block in heap block record 8 of 12
-  80 bytes (78 requested / 2 slop)
-  0.66% of the heap (98.87% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Live {
-  1 block in heap block record 9 of 12
-  80 bytes (77 requested / 3 slop)
-  0.66% of the heap (99.54% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Live {
-  1 block in heap block record 10 of 12
-  32 bytes (30 requested / 2 slop)
-  0.26% of the heap (99.80% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Live {
-  1 block in heap block record 11 of 12
-  16 bytes (10 requested / 6 slop)
-  0.13% of the heap (99.93% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Live {
-  1 block in heap block record 12 of 12
-  8 bytes (0 requested / 8 slop)
-  0.07% of the heap (100.00% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-#-----------------------------------------------------------------
-
-Summary {
-  Total: 12,088 bytes in 30 blocks
-}
-
-#-----------------------------------------------------------------
-
-Invocation {
-  $DMD = '--mode=test'
-  Function = AnalyzeReports
-  Sample-below size = 1
-}
-
-#-----------------------------------------------------------------
-
-Twice-reported {
-  1 block in heap block record 1 of 2
-  80 bytes (77 requested / 3 slop)
-  2.82% of the heap (2.82% cumulative)
-  90.91% of twice-reported (90.91% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-  Reported at {
-    ... DMD.cpp
-  }
-  Reported again at {
-    ... DMD.cpp
-  }
-}
-
-Twice-reported {
-  1 block in heap block record 2 of 2
-  8 bytes (0 requested / 8 slop)
-  0.28% of the heap (3.10% cumulative)
-  9.09% of twice-reported (100.00% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-  Reported at {
-    ... DMD.cpp
-  }
-  Reported again at {
-    ... DMD.cpp
-  }
-}
-
-#-----------------------------------------------------------------
-
-Unreported {
-  9 blocks in heap block record 1 of 3
-  1,008 bytes (900 requested / 108 slop)
-  35.49% of the heap (35.49% cumulative)
-  48.84% of unreported (48.84% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Unreported {
-  6 blocks in heap block record 2 of 3
-  528 bytes (528 requested / 0 slop)
-  18.59% of the heap (54.08% cumulative)
-  25.58% of unreported (74.42% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Unreported {
-  6 blocks in heap block record 3 of 3
-  528 bytes (528 requested / 0 slop)
-  18.59% of the heap (72.68% cumulative)
-  25.58% of unreported (100.00% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-#-----------------------------------------------------------------
-
-Once-reported {
-  1 block in heap block record 1 of 4
-  512 bytes (512 requested / 0 slop)
-  18.03% of the heap (18.03% cumulative)
-  74.42% of once-reported (74.42% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-  Reported at {
-    ... DMD.cpp
-  }
-}
-
-Once-reported {
-  1 block in heap block record 2 of 4
-  80 bytes (79 requested / 1 slop)
-  2.82% of the heap (20.85% cumulative)
-  11.63% of once-reported (86.05% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-  Reported at {
-    ... DMD.cpp
-  }
-}
-
-Once-reported {
-  1 block in heap block record 3 of 4
-  80 bytes (78 requested / 2 slop)
-  2.82% of the heap (23.66% cumulative)
-  11.63% of once-reported (97.67% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-  Reported at {
-    ... DMD.cpp
-  }
-}
-
-Once-reported {
-  1 block in heap block record 4 of 4
-  16 bytes (10 requested / 6 slop)
-  0.56% of the heap (24.23% cumulative)
-  2.33% of once-reported (100.00% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-  Reported at {
-    ... DMD.cpp
-  }
-}
-
-#-----------------------------------------------------------------
-
-Summary {
-  Total:                 2,840 bytes (100.00%) in      27 blocks (100.00%)
-  Unreported:            2,064 bytes ( 72.68%) in      21 blocks ( 77.78%)
-  Once-reported:           688 bytes ( 24.23%) in       4 blocks ( 14.81%)
-  Twice-reported:           88 bytes (  3.10%) in       2 blocks (  7.41%)
-}
-
-#-----------------------------------------------------------------
-
-Invocation {
-  $DMD = '--mode=test'
-  Function = AnalyzeHeap
-  Sample-below size = 1
-}
-
-#-----------------------------------------------------------------
-
-Live {
-  9 blocks in heap block record 1 of 9
-  1,008 bytes (900 requested / 108 slop)
-  35.49% of the heap (35.49% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Live {
-  6 blocks in heap block record 2 of 9
-  528 bytes (528 requested / 0 slop)
-  18.59% of the heap (54.08% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Live {
-  6 blocks in heap block record 3 of 9
-  528 bytes (528 requested / 0 slop)
-  18.59% of the heap (72.68% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Live {
-  1 block in heap block record 4 of 9
-  512 bytes (512 requested / 0 slop)
-  18.03% of the heap (90.70% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Live {
-  1 block in heap block record 5 of 9
-  80 bytes (79 requested / 1 slop)
-  2.82% of the heap (93.52% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Live {
-  1 block in heap block record 6 of 9
-  80 bytes (78 requested / 2 slop)
-  2.82% of the heap (96.34% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Live {
-  1 block in heap block record 7 of 9
-  80 bytes (77 requested / 3 slop)
-  2.82% of the heap (99.15% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Live {
-  1 block in heap block record 8 of 9
-  16 bytes (10 requested / 6 slop)
-  0.56% of the heap (99.72% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Live {
-  1 block in heap block record 9 of 9
-  8 bytes (0 requested / 8 slop)
-  0.28% of the heap (100.00% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-#-----------------------------------------------------------------
-
-Summary {
-  Total: 2,840 bytes in 27 blocks
-}
-
-#-----------------------------------------------------------------
-
-Invocation {
-  $DMD = '--mode=test'
-  Function = AnalyzeReports
-  Sample-below size = 128
-}
-
-#-----------------------------------------------------------------
-
-# no twice-reported heap blocks
-
-#-----------------------------------------------------------------
-
-Unreported {
-  ~4 blocks in heap block record 1 of 7
-  ~512 bytes (~512 requested / ~0 slop)
-  35.96% of the heap (35.96% cumulative)
-  35.96% of unreported (35.96% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Unreported {
-  1 block in heap block record 2 of 7
-  256 bytes (256 requested / 0 slop)
-  17.98% of the heap (53.93% cumulative)
-  17.98% of unreported (53.93% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Unreported {
-  1 block in heap block record 3 of 7
-  144 bytes (144 requested / 0 slop)
-  10.11% of the heap (64.04% cumulative)
-  10.11% of unreported (64.04% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Unreported {
-  1 block in heap block record 4 of 7
-  128 bytes (128 requested / 0 slop)
-  8.99% of the heap (73.03% cumulative)
-  8.99% of unreported (73.03% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Unreported {
-  ~1 block in heap block record 5 of 7
-  ~128 bytes (~128 requested / ~0 slop)
-  8.99% of the heap (82.02% cumulative)
-  8.99% of unreported (82.02% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Unreported {
-  ~1 block in heap block record 6 of 7
-  ~128 bytes (~128 requested / ~0 slop)
-  8.99% of the heap (91.01% cumulative)
-  8.99% of unreported (91.01% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Unreported {
-  ~1 block in heap block record 7 of 7
-  ~128 bytes (~128 requested / ~0 slop)
-  8.99% of the heap (100.00% cumulative)
-  8.99% of unreported (100.00% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-#-----------------------------------------------------------------
-
-# no once-reported heap blocks
-
-#-----------------------------------------------------------------
-
-Summary {
-  Total:                ~1,424 bytes (100.00%) in     ~10 blocks (100.00%)
-  Unreported:           ~1,424 bytes (100.00%) in     ~10 blocks (100.00%)
-  Once-reported:            ~0 bytes (  0.00%) in      ~0 blocks (  0.00%)
-  Twice-reported:           ~0 bytes (  0.00%) in      ~0 blocks (  0.00%)
-}
-
-#-----------------------------------------------------------------
-
-Invocation {
-  $DMD = '--mode=test'
-  Function = AnalyzeHeap
-  Sample-below size = 128
-}
-
-#-----------------------------------------------------------------
-
-Live {
-  ~4 blocks in heap block record 1 of 7
-  ~512 bytes (~512 requested / ~0 slop)
-  35.96% of the heap (35.96% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Live {
-  1 block in heap block record 2 of 7
-  256 bytes (256 requested / 0 slop)
-  17.98% of the heap (53.93% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Live {
-  1 block in heap block record 3 of 7
-  144 bytes (144 requested / 0 slop)
-  10.11% of the heap (64.04% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Live {
-  1 block in heap block record 4 of 7
-  128 bytes (128 requested / 0 slop)
-  8.99% of the heap (73.03% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Live {
-  ~1 block in heap block record 5 of 7
-  ~128 bytes (~128 requested / ~0 slop)
-  8.99% of the heap (82.02% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Live {
-  ~1 block in heap block record 6 of 7
-  ~128 bytes (~128 requested / ~0 slop)
-  8.99% of the heap (91.01% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-Live {
-  ~1 block in heap block record 7 of 7
-  ~128 bytes (~128 requested / ~0 slop)
-  8.99% of the heap (100.00% cumulative)
-  Allocated at {
-    ... DMD.cpp
-  }
-}
-
-#-----------------------------------------------------------------
-
-Summary {
-  Total: ~1,424 bytes in ~10 blocks
-}
-
new file mode 100644
--- /dev/null
+++ b/memory/replace/dmd/test/full-heap-expected1.txt
@@ -0,0 +1,17 @@
+#-----------------------------------------------------------------
+
+Invocation {
+  $DMD = '--mode=test'
+  Sample-below size = 1
+}
+
+#-----------------------------------------------------------------
+
+# no live heap blocks
+
+#-----------------------------------------------------------------
+
+Summary {
+  Total: 0 bytes in 0 blocks
+}
+
new file mode 100644
--- /dev/null
+++ b/memory/replace/dmd/test/full-heap-expected2.txt
@@ -0,0 +1,123 @@
+#-----------------------------------------------------------------
+
+Invocation {
+  $DMD = '--mode=test'
+  Sample-below size = 1
+}
+
+#-----------------------------------------------------------------
+
+Live {
+  1 block in heap block record 1 of 12
+  8,192 bytes (4,097 requested / 4,095 slop)
+  67.77% of the heap (67.77% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Live {
+  1 block in heap block record 2 of 12
+  1,024 bytes (1,023 requested / 1 slop)
+  8.47% of the heap (76.24% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Live {
+  9 blocks in heap block record 3 of 12
+  1,008 bytes (900 requested / 108 slop)
+  8.34% of the heap (84.58% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Live {
+  6 blocks in heap block record 4 of 12
+  528 bytes (528 requested / 0 slop)
+  4.37% of the heap (88.95% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Live {
+  6 blocks in heap block record 5 of 12
+  528 bytes (528 requested / 0 slop)
+  4.37% of the heap (93.32% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Live {
+  1 block in heap block record 6 of 12
+  512 bytes (512 requested / 0 slop)
+  4.24% of the heap (97.55% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Live {
+  1 block in heap block record 7 of 12
+  80 bytes (79 requested / 1 slop)
+  0.66% of the heap (98.21% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Live {
+  1 block in heap block record 8 of 12
+  80 bytes (78 requested / 2 slop)
+  0.66% of the heap (98.87% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Live {
+  1 block in heap block record 9 of 12
+  80 bytes (77 requested / 3 slop)
+  0.66% of the heap (99.54% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Live {
+  1 block in heap block record 10 of 12
+  32 bytes (30 requested / 2 slop)
+  0.26% of the heap (99.80% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Live {
+  1 block in heap block record 11 of 12
+  16 bytes (10 requested / 6 slop)
+  0.13% of the heap (99.93% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Live {
+  1 block in heap block record 12 of 12
+  8 bytes (0 requested / 8 slop)
+  0.07% of the heap (100.00% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+#-----------------------------------------------------------------
+
+Summary {
+  Total: 12,088 bytes in 30 blocks
+}
+
new file mode 100644
--- /dev/null
+++ b/memory/replace/dmd/test/full-heap-expected3.txt
@@ -0,0 +1,96 @@
+#-----------------------------------------------------------------
+
+Invocation {
+  $DMD = '--mode=test'
+  Sample-below size = 1
+}
+
+#-----------------------------------------------------------------
+
+Live {
+  9 blocks in heap block record 1 of 9
+  1,008 bytes (900 requested / 108 slop)
+  35.49% of the heap (35.49% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Live {
+  6 blocks in heap block record 2 of 9
+  528 bytes (528 requested / 0 slop)
+  18.59% of the heap (54.08% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Live {
+  6 blocks in heap block record 3 of 9
+  528 bytes (528 requested / 0 slop)
+  18.59% of the heap (72.68% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Live {
+  1 block in heap block record 4 of 9
+  512 bytes (512 requested / 0 slop)
+  18.03% of the heap (90.70% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Live {
+  1 block in heap block record 5 of 9
+  80 bytes (79 requested / 1 slop)
+  2.82% of the heap (93.52% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Live {
+  1 block in heap block record 6 of 9
+  80 bytes (78 requested / 2 slop)
+  2.82% of the heap (96.34% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Live {
+  1 block in heap block record 7 of 9
+  80 bytes (77 requested / 3 slop)
+  2.82% of the heap (99.15% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Live {
+  1 block in heap block record 8 of 9
+  16 bytes (10 requested / 6 slop)
+  0.56% of the heap (99.72% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Live {
+  1 block in heap block record 9 of 9
+  8 bytes (0 requested / 8 slop)
+  0.28% of the heap (100.00% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+#-----------------------------------------------------------------
+
+Summary {
+  Total: 2,840 bytes in 27 blocks
+}
+
new file mode 100644
--- /dev/null
+++ b/memory/replace/dmd/test/full-heap-expected4.txt
@@ -0,0 +1,78 @@
+#-----------------------------------------------------------------
+
+Invocation {
+  $DMD = '--mode=test'
+  Sample-below size = 128
+}
+
+#-----------------------------------------------------------------
+
+Live {
+  ~4 blocks in heap block record 1 of 7
+  ~512 bytes (~512 requested / ~0 slop)
+  35.96% of the heap (35.96% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Live {
+  1 block in heap block record 2 of 7
+  256 bytes (256 requested / 0 slop)
+  17.98% of the heap (53.93% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Live {
+  1 block in heap block record 3 of 7
+  144 bytes (144 requested / 0 slop)
+  10.11% of the heap (64.04% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Live {
+  1 block in heap block record 4 of 7
+  128 bytes (128 requested / 0 slop)
+  8.99% of the heap (73.03% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Live {
+  ~1 block in heap block record 5 of 7
+  ~128 bytes (~128 requested / ~0 slop)
+  8.99% of the heap (82.02% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Live {
+  ~1 block in heap block record 6 of 7
+  ~128 bytes (~128 requested / ~0 slop)
+  8.99% of the heap (91.01% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Live {
+  ~1 block in heap block record 7 of 7
+  ~128 bytes (~128 requested / ~0 slop)
+  8.99% of the heap (100.00% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+#-----------------------------------------------------------------
+
+Summary {
+  Total: ~1,424 bytes in ~10 blocks
+}
+
new file mode 100644
--- /dev/null
+++ b/memory/replace/dmd/test/full-reports-expected1.txt
@@ -0,0 +1,28 @@
+#-----------------------------------------------------------------
+
+Invocation {
+  $DMD = '--mode=test'
+  Sample-below size = 1
+}
+
+#-----------------------------------------------------------------
+
+# no twice-reported heap blocks
+
+#-----------------------------------------------------------------
+
+# no unreported heap blocks
+
+#-----------------------------------------------------------------
+
+# no once-reported heap blocks
+
+#-----------------------------------------------------------------
+
+Summary {
+  Total:                     0 bytes (100.00%) in       0 blocks (100.00%)
+  Unreported:                0 bytes (  0.00%) in       0 blocks (  0.00%)
+  Once-reported:             0 bytes (  0.00%) in       0 blocks (  0.00%)
+  Twice-reported:            0 bytes (  0.00%) in       0 blocks (  0.00%)
+}
+
new file mode 100644
--- /dev/null
+++ b/memory/replace/dmd/test/full-reports-expected2.txt
@@ -0,0 +1,259 @@
+#-----------------------------------------------------------------
+
+Invocation {
+  $DMD = '--mode=test'
+  Sample-below size = 1
+}
+
+#-----------------------------------------------------------------
+
+Twice-reported {
+  1 block in heap block record 1 of 4
+  80 bytes (79 requested / 1 slop)
+  0.66% of the heap (0.66% cumulative)
+  29.41% of twice-reported (29.41% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+  Reported at {
+    ... DMD.cpp
+  }
+  Reported again at {
+    ... DMD.cpp
+  }
+}
+
+Twice-reported {
+  1 block in heap block record 2 of 4
+  80 bytes (78 requested / 2 slop)
+  0.66% of the heap (1.32% cumulative)
+  29.41% of twice-reported (58.82% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+  Reported at {
+    ... DMD.cpp
+  }
+  Reported again at {
+    ... DMD.cpp
+  }
+}
+
+Twice-reported {
+  1 block in heap block record 3 of 4
+  80 bytes (77 requested / 3 slop)
+  0.66% of the heap (1.99% cumulative)
+  29.41% of twice-reported (88.24% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+  Reported at {
+    ... DMD.cpp
+  }
+  Reported again at {
+    ... DMD.cpp
+  }
+}
+
+Twice-reported {
+  1 block in heap block record 4 of 4
+  32 bytes (30 requested / 2 slop)
+  0.26% of the heap (2.25% cumulative)
+  11.76% of twice-reported (100.00% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+  Reported at {
+    ... DMD.cpp
+  }
+  Reported again at {
+    ... DMD.cpp
+  }
+}
+
+#-----------------------------------------------------------------
+
+Unreported {
+  9 blocks in heap block record 1 of 3
+  1,008 bytes (900 requested / 108 slop)
+  8.34% of the heap (8.34% cumulative)
+  81.82% of unreported (81.82% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Unreported {
+  2 blocks in heap block record 2 of 3
+  112 bytes (112 requested / 0 slop)
+  0.93% of the heap (9.27% cumulative)
+  9.09% of unreported (90.91% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Unreported {
+  2 blocks in heap block record 3 of 3
+  112 bytes (112 requested / 0 slop)
+  0.93% of the heap (10.19% cumulative)
+  9.09% of unreported (100.00% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+#-----------------------------------------------------------------
+
+Once-reported {
+  1 block in heap block record 1 of 11
+  8,192 bytes (4,097 requested / 4,095 slop)
+  67.77% of the heap (67.77% cumulative)
+  77.40% of once-reported (77.40% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+  Reported at {
+    ... DMD.cpp
+  }
+}
+
+Once-reported {
+  1 block in heap block record 2 of 11
+  1,024 bytes (1,023 requested / 1 slop)
+  8.47% of the heap (76.24% cumulative)
+  9.67% of once-reported (87.07% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+  Reported at {
+    ... DMD.cpp
+  }
+}
+
+Once-reported {
+  1 block in heap block record 3 of 11
+  512 bytes (512 requested / 0 slop)
+  4.24% of the heap (80.48% cumulative)
+  4.84% of once-reported (91.91% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+  Reported at {
+    ... DMD.cpp
+  }
+}
+
+Once-reported {
+  2 blocks in heap block record 4 of 11
+  240 bytes (240 requested / 0 slop)
+  1.99% of the heap (82.46% cumulative)
+  2.27% of once-reported (94.18% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+  Reported at {
+    ... DMD.cpp
+  }
+}
+
+Once-reported {
+  2 blocks in heap block record 5 of 11
+  240 bytes (240 requested / 0 slop)
+  1.99% of the heap (84.45% cumulative)
+  2.27% of once-reported (96.45% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+  Reported at {
+    ... DMD.cpp
+  }
+}
+
+Once-reported {
+  1 block in heap block record 6 of 11
+  96 bytes (96 requested / 0 slop)
+  0.79% of the heap (85.24% cumulative)
+  0.91% of once-reported (97.35% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+  Reported at {
+    ... DMD.cpp
+  }
+}
+
+Once-reported {
+  1 block in heap block record 7 of 11
+  96 bytes (96 requested / 0 slop)
+  0.79% of the heap (86.04% cumulative)
+  0.91% of once-reported (98.26% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+  Reported at {
+    ... DMD.cpp
+  }
+}
+
+Once-reported {
+  1 block in heap block record 8 of 11
+  80 bytes (80 requested / 0 slop)
+  0.66% of the heap (86.70% cumulative)
+  0.76% of once-reported (99.02% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+  Reported at {
+    ... DMD.cpp
+  }
+}
+
+Once-reported {
+  1 block in heap block record 9 of 11
+  80 bytes (80 requested / 0 slop)
+  0.66% of the heap (87.36% cumulative)
+  0.76% of once-reported (99.77% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+  Reported at {
+    ... DMD.cpp
+  }
+}
+
+Once-reported {
+  1 block in heap block record 10 of 11
+  16 bytes (10 requested / 6 slop)
+  0.13% of the heap (87.49% cumulative)
+  0.15% of once-reported (99.92% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+  Reported at {
+    ... DMD.cpp
+  }
+}
+
+Once-reported {
+  1 block in heap block record 11 of 11
+  8 bytes (0 requested / 8 slop)
+  0.07% of the heap (87.56% cumulative)
+  0.08% of once-reported (100.00% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+  Reported at {
+    ... DMD.cpp
+  }
+}
+
+#-----------------------------------------------------------------
+
+Summary {
+  Total:                12,088 bytes (100.00%) in      30 blocks (100.00%)
+  Unreported:            1,232 bytes ( 10.19%) in      13 blocks ( 43.33%)
+  Once-reported:        10,584 bytes ( 87.56%) in      13 blocks ( 43.33%)
+  Twice-reported:          272 bytes (  2.25%) in       4 blocks ( 13.33%)
+}
+
new file mode 100644
--- /dev/null
+++ b/memory/replace/dmd/test/full-reports-expected3.txt
@@ -0,0 +1,136 @@
+#-----------------------------------------------------------------
+
+Invocation {
+  $DMD = '--mode=test'
+  Sample-below size = 1
+}
+
+#-----------------------------------------------------------------
+
+Twice-reported {
+  1 block in heap block record 1 of 2
+  80 bytes (77 requested / 3 slop)
+  2.82% of the heap (2.82% cumulative)
+  90.91% of twice-reported (90.91% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+  Reported at {
+    ... DMD.cpp
+  }
+  Reported again at {
+    ... DMD.cpp
+  }
+}
+
+Twice-reported {
+  1 block in heap block record 2 of 2
+  8 bytes (0 requested / 8 slop)
+  0.28% of the heap (3.10% cumulative)
+  9.09% of twice-reported (100.00% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+  Reported at {
+    ... DMD.cpp
+  }
+  Reported again at {
+    ... DMD.cpp
+  }
+}
+
+#-----------------------------------------------------------------
+
+Unreported {
+  9 blocks in heap block record 1 of 3
+  1,008 bytes (900 requested / 108 slop)
+  35.49% of the heap (35.49% cumulative)
+  48.84% of unreported (48.84% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Unreported {
+  6 blocks in heap block record 2 of 3
+  528 bytes (528 requested / 0 slop)
+  18.59% of the heap (54.08% cumulative)
+  25.58% of unreported (74.42% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Unreported {
+  6 blocks in heap block record 3 of 3
+  528 bytes (528 requested / 0 slop)
+  18.59% of the heap (72.68% cumulative)
+  25.58% of unreported (100.00% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+#-----------------------------------------------------------------
+
+Once-reported {
+  1 block in heap block record 1 of 4
+  512 bytes (512 requested / 0 slop)
+  18.03% of the heap (18.03% cumulative)
+  74.42% of once-reported (74.42% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+  Reported at {
+    ... DMD.cpp
+  }
+}
+
+Once-reported {
+  1 block in heap block record 2 of 4
+  80 bytes (79 requested / 1 slop)
+  2.82% of the heap (20.85% cumulative)
+  11.63% of once-reported (86.05% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+  Reported at {
+    ... DMD.cpp
+  }
+}
+
+Once-reported {
+  1 block in heap block record 3 of 4
+  80 bytes (78 requested / 2 slop)
+  2.82% of the heap (23.66% cumulative)
+  11.63% of once-reported (97.67% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+  Reported at {
+    ... DMD.cpp
+  }
+}
+
+Once-reported {
+  1 block in heap block record 4 of 4
+  16 bytes (10 requested / 6 slop)
+  0.56% of the heap (24.23% cumulative)
+  2.33% of once-reported (100.00% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+  Reported at {
+    ... DMD.cpp
+  }
+}
+
+#-----------------------------------------------------------------
+
+Summary {
+  Total:                 2,840 bytes (100.00%) in      27 blocks (100.00%)
+  Unreported:            2,064 bytes ( 72.68%) in      21 blocks ( 77.78%)
+  Once-reported:           688 bytes ( 24.23%) in       4 blocks ( 14.81%)
+  Twice-reported:           88 bytes (  3.10%) in       2 blocks (  7.41%)
+}
+
new file mode 100644
--- /dev/null
+++ b/memory/replace/dmd/test/full-reports-expected4.txt
@@ -0,0 +1,96 @@
+#-----------------------------------------------------------------
+
+Invocation {
+  $DMD = '--mode=test'
+  Sample-below size = 128
+}
+
+#-----------------------------------------------------------------
+
+# no twice-reported heap blocks
+
+#-----------------------------------------------------------------
+
+Unreported {
+  ~4 blocks in heap block record 1 of 7
+  ~512 bytes (~512 requested / ~0 slop)
+  35.96% of the heap (35.96% cumulative)
+  35.96% of unreported (35.96% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Unreported {
+  1 block in heap block record 2 of 7
+  256 bytes (256 requested / 0 slop)
+  17.98% of the heap (53.93% cumulative)
+  17.98% of unreported (53.93% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Unreported {
+  1 block in heap block record 3 of 7
+  144 bytes (144 requested / 0 slop)
+  10.11% of the heap (64.04% cumulative)
+  10.11% of unreported (64.04% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Unreported {
+  1 block in heap block record 4 of 7
+  128 bytes (128 requested / 0 slop)
+  8.99% of the heap (73.03% cumulative)
+  8.99% of unreported (73.03% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Unreported {
+  ~1 block in heap block record 5 of 7
+  ~128 bytes (~128 requested / ~0 slop)
+  8.99% of the heap (82.02% cumulative)
+  8.99% of unreported (82.02% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Unreported {
+  ~1 block in heap block record 6 of 7
+  ~128 bytes (~128 requested / ~0 slop)
+  8.99% of the heap (91.01% cumulative)
+  8.99% of unreported (91.01% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+Unreported {
+  ~1 block in heap block record 7 of 7
+  ~128 bytes (~128 requested / ~0 slop)
+  8.99% of the heap (100.00% cumulative)
+  8.99% of unreported (100.00% cumulative)
+  Allocated at {
+    ... DMD.cpp
+  }
+}
+
+#-----------------------------------------------------------------
+
+# no once-reported heap blocks
+
+#-----------------------------------------------------------------
+
+Summary {
+  Total:                ~1,424 bytes (100.00%) in     ~10 blocks (100.00%)
+  Unreported:           ~1,424 bytes (100.00%) in     ~10 blocks (100.00%)
+  Once-reported:            ~0 bytes (  0.00%) in      ~0 blocks (  0.00%)
+  Twice-reported:           ~0 bytes (  0.00%) in      ~0 blocks (  0.00%)
+}
+
--- a/python/mozbuild/mozbuild/mach_commands.py
+++ b/python/mozbuild/mozbuild/mach_commands.py
@@ -933,19 +933,19 @@ class RunDmd(MachCommandBase):
     @CommandArgument('--remote', '-r', action='store_true',
         help='Do not pass the -no-remote argument by default.')
     @CommandArgument('--background', '-b', action='store_true',
         help='Do not pass the -foreground argument by default on Mac')
     @CommandArgument('--sample-below', default=None, type=str,
         help='The sample size to use, [1..n]. Default is 4093.')
     @CommandArgument('--max-frames', default=None, type=str,
         help='The max number of stack frames to capture in allocation traces, [1..24] Default is 24.')
-    @CommandArgument('--max-records', default=None, type=str,
-        help='Number of stack trace records to print of each kind, [1..1000000]. Default is 1000.')
-    def dmd(self, params, remote, background, sample_below, max_frames, max_records):
+    @CommandArgument('--show-dump-stats', action='store_true',
+        help='Show stats when doing dumps.')
+    def dmd(self, params, remote, background, sample_below, max_frames, show_dump_stats):
         args = get_run_args(self, params, remote, background)
         if not args:
             return 1
 
         lib_dir = os.path.join(self.distdir, 'lib')
         lib_name = self.substs['DLL_PREFIX'] + 'dmd' + self.substs['DLL_SUFFIX']
         dmd_lib = os.path.join(lib_dir, lib_name)
         if not os.path.exists(dmd_lib):
@@ -953,18 +953,18 @@ class RunDmd(MachCommandBase):
             return 1
 
         dmd_params = []
 
         if sample_below:
             dmd_params.append('--sample-below=' + sample_below)
         if max_frames:
             dmd_params.append('--max-frames=' + max_frames)
-        if max_records:
-            dmd_params.append('--max-records=' + max_records)
+        if show_dump_stats:
+            dmd_params.append('--show-dump-stats=yes')
 
         if dmd_params:
             dmd_str = " ".join(dmd_params)
         else:
             dmd_str = "1"
 
         env_vars = {
             "Darwin": {
--- a/xpcom/base/nsMemoryInfoDumper.cpp
+++ b/xpcom/base/nsMemoryInfoDumper.cpp
@@ -425,38 +425,16 @@ MakeFilename(const char* aPrefix, const 
              int aPid, const char* aSuffix, nsACString& aResult)
 {
   aResult = nsPrintfCString("%s-%s-%d.%s",
                             aPrefix,
                             NS_ConvertUTF16toUTF8(aIdentifier).get(),
                             aPid, aSuffix);
 }
 
-#ifdef MOZ_DMD
-struct DMDWriteState
-{
-  static const size_t kBufSize = 4096;
-  char mBuf[kBufSize];
-  nsRefPtr<nsGZFileWriter> mGZWriter;
-
-  DMDWriteState(nsGZFileWriter* aGZWriter)
-    : mGZWriter(aGZWriter)
-  {
-  }
-};
-
-static void
-DMDWrite(void* aState, const char* aFmt, va_list ap)
-{
-  DMDWriteState* state = (DMDWriteState*)aState;
-  vsnprintf(state->mBuf, state->kBufSize, aFmt, ap);
-  unused << state->mGZWriter->Write(state->mBuf);
-}
-#endif
-
 // This class wraps GZFileWriter so it can be used with JSONWriter, overcoming
 // the following two problems:
 // - It provides a JSONWriterFunc::Write() that calls nsGZFileWriter::Write().
 // - It can be stored as a UniquePtr, whereas nsGZFileWriter is refcounted.
 class GZWriterWrapper : public JSONWriteFunc
 {
 public:
   explicit GZWriterWrapper(nsGZFileWriter* aGZWriter)
@@ -786,20 +764,20 @@ nsresult
 nsMemoryInfoDumper::OpenDMDFile(const nsAString& aIdentifier, int aPid,
                                 FILE** aOutFile)
 {
   if (!dmd::IsRunning()) {
     *aOutFile = nullptr;
     return NS_OK;
   }
 
-  // Create a filename like dmd-<identifier>-<pid>.txt.gz, which will be used
+  // Create a filename like dmd-<identifier>-<pid>.json.gz, which will be used
   // if DMD is enabled.
   nsCString dmdFilename;
-  MakeFilename("dmd", aIdentifier, aPid, "txt.gz", dmdFilename);
+  MakeFilename("dmd", aIdentifier, aPid, "json.gz", dmdFilename);
 
   // Open a new DMD file named |dmdFilename| in NS_OS_TEMP_DIR for writing,
   // and dump DMD output to it.  This must occur after the memory reporters
   // have been run (above), but before the memory-reports file has been
   // renamed (so scripts can detect the DMD file, if present).
 
   nsresult rv;
   nsCOMPtr<nsIFile> dmdFile;
@@ -821,25 +799,24 @@ nsMemoryInfoDumper::OpenDMDFile(const ns
   dmd::StatusMsg("opened %s for writing\n", path.get());
 
   return rv;
 }
 
 nsresult
 nsMemoryInfoDumper::DumpDMDToFile(FILE* aFile)
 {
-  nsRefPtr<nsGZFileWriter> dmdWriter = new nsGZFileWriter();
-  nsresult rv = dmdWriter->InitANSIFileDesc(aFile);
+  nsRefPtr<nsGZFileWriter> gzWriter = new nsGZFileWriter();
+  nsresult rv = gzWriter->InitANSIFileDesc(aFile);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   // Dump DMD's memory reports analysis to the file.
-  DMDWriteState state(dmdWriter);
-  dmd::Writer w(DMDWrite, &state);
-  dmd::AnalyzeReports(w);
+  JSONWriter jsonWriter(MakeUnique<GZWriterWrapper>(gzWriter));
+  dmd::AnalyzeReports(jsonWriter);
 
-  rv = dmdWriter->Finish();
+  rv = gzWriter->Finish();
   NS_WARN_IF(NS_FAILED(rv));
   return rv;
 }
 #endif  // MOZ_DMD