Bug 1068123 - Make the MOZ_GCTIMER logs more verbose and readable; r=jonco
☠☠ backed out by 6dc8272079e6 ☠ ☠
authorTerrence Cole <terrence@mozilla.com>
Fri, 19 Sep 2014 08:39:54 -0700
changeset 206269 a53aa43ab5f4957321a5805f0ea171098847891f
parent 206268 230ec8030aa790965446443a7801019148aeba8a
child 206270 f9bfd3909264f1b5e57dcae9eeb16f957b075f9c
push id27520
push userkwierso@gmail.com
push dateSat, 20 Sep 2014 00:25:19 +0000
treeherdermozilla-central@27253887d2cc [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjonco
bugs1068123
milestone35.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1068123 - Make the MOZ_GCTIMER logs more verbose and readable; r=jonco
js/public/Utility.h
js/src/gc/Statistics.cpp
js/src/gc/Statistics.h
js/src/jsgc.cpp
--- a/js/public/Utility.h
+++ b/js/public/Utility.h
@@ -9,16 +9,17 @@
 
 #include "mozilla/Assertions.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/Compiler.h"
 #include "mozilla/Move.h"
 #include "mozilla/NullPtr.h"
 #include "mozilla/Scoped.h"
 #include "mozilla/TemplateLib.h"
+#include "mozilla/UniquePtr.h"
 
 #include <stdlib.h>
 #include <string.h>
 
 #ifdef JS_OOM_DO_BACKTRACES
 #include <execinfo.h>
 #include <stdio.h>
 #endif
@@ -621,16 +622,24 @@ struct FreePolicy
 } // namespace JS
 
 namespace js {
 
 /* Integral types for all hash functions. */
 typedef uint32_t HashNumber;
 const unsigned HashNumberSizeBits = 32;
 
+typedef mozilla::UniquePtr<char, JS::FreePolicy> UniqueChars;
+
+static inline UniqueChars make_string_copy(const char* str)
+{
+    JS_OOM_POSSIBLY_FAIL();
+    return UniqueChars(strdup(str));
+}
+
 namespace detail {
 
 /*
  * Given a raw hash code, h, return a number that can be used to select a hash
  * bucket.
  *
  * This function aims to produce as uniform an output distribution as possible,
  * especially in the most significant (leftmost) bits, even though the input
--- a/js/src/gc/Statistics.cpp
+++ b/js/src/gc/Statistics.cpp
@@ -2,16 +2,17 @@
  * vim: set ts=8 sts=4 et sw=4 tw=99:
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "gc/Statistics.h"
 
 #include "mozilla/PodOperations.h"
+#include "mozilla/UniquePtr.h"
 
 #include <ctype.h>
 #include <stdarg.h>
 #include <stdio.h>
 
 #include "jscrashreport.h"
 #include "jsprf.h"
 #include "jsutil.h"
@@ -21,18 +22,18 @@
 #include "vm/Runtime.h"
 
 using namespace js;
 using namespace js::gc;
 using namespace js::gcstats;
 
 using mozilla::PodArrayZero;
 
-/* Except for the first and last, slices of less than 42ms are not reported. */
-static const int64_t SLICE_MIN_REPORT_TIME = 42 * PRMJ_USEC_PER_MSEC;
+/* Except for the first and last, slices of less than 10ms are not reported. */
+static const int64_t SLICE_MIN_REPORT_TIME = 10 * PRMJ_USEC_PER_MSEC;
 
 class gcstats::StatisticsSerializer
 {
     typedef Vector<char, 128, SystemAllocPolicy> CharBuffer;
     CharBuffer buf_;
     bool asJSON_;
     bool needComma_;
     bool oom_;
@@ -271,54 +272,54 @@ struct PhaseInfo
 static const Phase PHASE_NO_PARENT = PHASE_LIMIT;
 
 static const PhaseInfo phases[] = {
     { PHASE_GC_BEGIN, "Begin Callback", PHASE_NO_PARENT },
     { PHASE_WAIT_BACKGROUND_THREAD, "Wait Background Thread", PHASE_NO_PARENT },
     { PHASE_MARK_DISCARD_CODE, "Mark Discard Code", PHASE_NO_PARENT },
     { PHASE_PURGE, "Purge", PHASE_NO_PARENT },
     { PHASE_MARK, "Mark", PHASE_NO_PARENT },
-    { PHASE_MARK_ROOTS, "Mark Roots", PHASE_MARK },
-    { PHASE_MARK_DELAYED, "Mark Delayed", PHASE_MARK },
+        { PHASE_MARK_ROOTS, "Mark Roots", PHASE_MARK },
+        { PHASE_MARK_DELAYED, "Mark Delayed", PHASE_MARK },
     { PHASE_SWEEP, "Sweep", PHASE_NO_PARENT },
-    { PHASE_SWEEP_MARK, "Mark During Sweeping", PHASE_SWEEP },
-    { PHASE_SWEEP_MARK_TYPES, "Mark Types During Sweeping", PHASE_SWEEP_MARK },
-    { PHASE_SWEEP_MARK_INCOMING_BLACK, "Mark Incoming Black Pointers", PHASE_SWEEP_MARK },
-    { PHASE_SWEEP_MARK_WEAK, "Mark Weak", PHASE_SWEEP_MARK },
-    { PHASE_SWEEP_MARK_INCOMING_GRAY, "Mark Incoming Gray Pointers", PHASE_SWEEP_MARK },
-    { PHASE_SWEEP_MARK_GRAY, "Mark Gray", PHASE_SWEEP_MARK },
-    { PHASE_SWEEP_MARK_GRAY_WEAK, "Mark Gray and Weak", PHASE_SWEEP_MARK },
-    { PHASE_FINALIZE_START, "Finalize Start Callback", PHASE_SWEEP },
-    { PHASE_SWEEP_ATOMS, "Sweep Atoms", PHASE_SWEEP },
-    { PHASE_SWEEP_SYMBOL_REGISTRY, "Sweep Symbol Registry", PHASE_SWEEP },
-    { PHASE_SWEEP_COMPARTMENTS, "Sweep Compartments", PHASE_SWEEP },
-    { PHASE_SWEEP_DISCARD_CODE, "Sweep Discard Code", PHASE_SWEEP_COMPARTMENTS },
-    { PHASE_SWEEP_TABLES, "Sweep Tables", PHASE_SWEEP_COMPARTMENTS },
-    { PHASE_SWEEP_TABLES_INNER_VIEWS, "Sweep Inner Views", PHASE_SWEEP_TABLES },
-    { PHASE_SWEEP_TABLES_WRAPPER, "Sweep Cross Compartment Wrappers", PHASE_SWEEP_TABLES },
-    { PHASE_SWEEP_TABLES_BASE_SHAPE, "Sweep Base Shapes", PHASE_SWEEP_TABLES },
-    { PHASE_SWEEP_TABLES_INITIAL_SHAPE, "Sweep Initial Shapes", PHASE_SWEEP_TABLES },
-    { PHASE_SWEEP_TABLES_TYPE_OBJECT, "Sweep Type Objects", PHASE_SWEEP_TABLES },
-    { PHASE_SWEEP_TABLES_BREAKPOINT, "Sweep Breakpoints", PHASE_SWEEP_TABLES },
-    { PHASE_SWEEP_TABLES_REGEXP, "Sweep Regexps", PHASE_SWEEP_TABLES },
-    { PHASE_DISCARD_ANALYSIS, "Discard Analysis", PHASE_SWEEP_COMPARTMENTS },
-    { PHASE_DISCARD_TI, "Discard TI", PHASE_DISCARD_ANALYSIS },
-    { PHASE_FREE_TI_ARENA, "Free TI Arena", PHASE_DISCARD_ANALYSIS },
-    { PHASE_SWEEP_TYPES, "Sweep Types", PHASE_DISCARD_ANALYSIS },
-    { PHASE_SWEEP_OBJECT, "Sweep Object", PHASE_SWEEP },
-    { PHASE_SWEEP_STRING, "Sweep String", PHASE_SWEEP },
-    { PHASE_SWEEP_SCRIPT, "Sweep Script", PHASE_SWEEP },
-    { PHASE_SWEEP_SHAPE, "Sweep Shape", PHASE_SWEEP },
-    { PHASE_SWEEP_JITCODE, "Sweep JIT code", PHASE_SWEEP },
+        { PHASE_SWEEP_MARK, "Mark During Sweeping", PHASE_SWEEP },
+            { PHASE_SWEEP_MARK_TYPES, "Mark Types During Sweeping", PHASE_SWEEP_MARK },
+            { PHASE_SWEEP_MARK_INCOMING_BLACK, "Mark Incoming Black Pointers", PHASE_SWEEP_MARK },
+            { PHASE_SWEEP_MARK_WEAK, "Mark Weak", PHASE_SWEEP_MARK },
+            { PHASE_SWEEP_MARK_INCOMING_GRAY, "Mark Incoming Gray Pointers", PHASE_SWEEP_MARK },
+            { PHASE_SWEEP_MARK_GRAY, "Mark Gray", PHASE_SWEEP_MARK },
+            { PHASE_SWEEP_MARK_GRAY_WEAK, "Mark Gray and Weak", PHASE_SWEEP_MARK },
+        { PHASE_FINALIZE_START, "Finalize Start Callback", PHASE_SWEEP },
+        { PHASE_SWEEP_ATOMS, "Sweep Atoms", PHASE_SWEEP },
+        { PHASE_SWEEP_SYMBOL_REGISTRY, "Sweep Symbol Registry", PHASE_SWEEP },
+        { PHASE_SWEEP_COMPARTMENTS, "Sweep Compartments", PHASE_SWEEP },
+            { PHASE_SWEEP_DISCARD_CODE, "Sweep Discard Code", PHASE_SWEEP_COMPARTMENTS },
+            { PHASE_SWEEP_TABLES, "Sweep Tables", PHASE_SWEEP_COMPARTMENTS },
+                { PHASE_SWEEP_TABLES_INNER_VIEWS, "Sweep Inner Views", PHASE_SWEEP_TABLES },
+                { PHASE_SWEEP_TABLES_WRAPPER, "Sweep Cross Compartment Wrappers", PHASE_SWEEP_TABLES },
+                { PHASE_SWEEP_TABLES_BASE_SHAPE, "Sweep Base Shapes", PHASE_SWEEP_TABLES },
+                { PHASE_SWEEP_TABLES_INITIAL_SHAPE, "Sweep Initial Shapes", PHASE_SWEEP_TABLES },
+                { PHASE_SWEEP_TABLES_TYPE_OBJECT, "Sweep Type Objects", PHASE_SWEEP_TABLES },
+                { PHASE_SWEEP_TABLES_BREAKPOINT, "Sweep Breakpoints", PHASE_SWEEP_TABLES },
+                { PHASE_SWEEP_TABLES_REGEXP, "Sweep Regexps", PHASE_SWEEP_TABLES },
+            { PHASE_DISCARD_ANALYSIS, "Discard Analysis", PHASE_SWEEP_COMPARTMENTS },
+                { PHASE_DISCARD_TI, "Discard TI", PHASE_DISCARD_ANALYSIS },
+                { PHASE_FREE_TI_ARENA, "Free TI Arena", PHASE_DISCARD_ANALYSIS },
+                { PHASE_SWEEP_TYPES, "Sweep Types", PHASE_DISCARD_ANALYSIS },
+        { PHASE_SWEEP_OBJECT, "Sweep Object", PHASE_SWEEP },
+        { PHASE_SWEEP_STRING, "Sweep String", PHASE_SWEEP },
+        { PHASE_SWEEP_SCRIPT, "Sweep Script", PHASE_SWEEP },
+        { PHASE_SWEEP_SHAPE, "Sweep Shape", PHASE_SWEEP },
+        { PHASE_SWEEP_JITCODE, "Sweep JIT code", PHASE_SWEEP },
+        { PHASE_FINALIZE_END, "Finalize End Callback", PHASE_SWEEP },
+        { PHASE_DESTROY, "Deallocate", PHASE_SWEEP },
     { PHASE_COMPACT, "Compact", PHASE_NO_PARENT },
-    { PHASE_COMPACT_MOVE, "Compact Move", PHASE_COMPACT },
-    { PHASE_COMPACT_UPDATE, "Compact Update", PHASE_COMPACT, },
-    { PHASE_COMPACT_UPDATE_GRAY, "Compact Update Gray", PHASE_COMPACT_UPDATE, },
-    { PHASE_FINALIZE_END, "Finalize End Callback", PHASE_SWEEP },
-    { PHASE_DESTROY, "Deallocate", PHASE_SWEEP },
+        { PHASE_COMPACT_MOVE, "Compact Move", PHASE_COMPACT },
+        { PHASE_COMPACT_UPDATE, "Compact Update", PHASE_COMPACT, },
+            { PHASE_COMPACT_UPDATE_GRAY, "Compact Update Gray", PHASE_COMPACT_UPDATE, },
     { PHASE_GC_END, "End Callback", PHASE_NO_PARENT },
     { PHASE_LIMIT, nullptr, PHASE_NO_PARENT }
 };
 
 static void
 FormatPhaseTimes(StatisticsSerializer &ss, const char *name, int64_t *times)
 {
     ss.beginObject(name);
@@ -365,17 +366,17 @@ Statistics::formatData(StatisticsSeriali
     ss.beginObject(nullptr);
     if (ss.isJSON())
         ss.appendNumber("Timestamp", "%llu", "", (unsigned long long)timestamp);
     if (slices.length() > 1 || ss.isJSON())
         ss.appendDecimal("Max Pause", "ms", t(longest));
     else
         ss.appendString("Reason", ExplainReason(slices[0].reason));
     ss.appendDecimal("Total Time", "ms", t(total));
-    ss.appendNumber("Zones Collected", "%d", "", zoneStats.collectedCount);
+    ss.appendNumber("Zones Collected", "%d", "", zoneStats.collectedZoneCount);
     ss.appendNumber("Total Zones", "%d", "", zoneStats.zoneCount);
     ss.appendNumber("Total Compartments", "%d", "", zoneStats.compartmentCount);
     ss.appendNumber("Minor GCs", "%d", "", counts[STAT_MINOR_GC]);
     ss.appendNumber("MMU (20ms)", "%d", "%", int(mmu20 * 100));
     ss.appendNumber("MMU (50ms)", "%d", "%", int(mmu50 * 100));
     ss.appendDecimal("SCC Sweep Total", "ms", t(sccTotal));
     ss.appendDecimal("SCC Sweep Max Pause", "ms", t(sccLongest));
     if (nonincrementalReason || ss.isJSON()) {
@@ -422,16 +423,184 @@ Statistics::formatData(StatisticsSeriali
     }
     ss.extra("    Totals: ");
     FormatPhaseTimes(ss, "Totals", phaseTimes);
     ss.endObject();
 
     return !ss.isOOM();
 }
 
+typedef Vector<UniqueChars, 8, SystemAllocPolicy> FragmentVector;
+
+static UniqueChars
+Join(const FragmentVector &fragments) {
+    size_t length = 0;
+    for (size_t i = 0; i < fragments.length(); ++i)
+        length += fragments[i] ? strlen(fragments[i].get()) : 0;
+
+    char *joined = js_pod_malloc<char>(length + 1);
+    joined[length] = '\0';
+
+    char *cursor = joined;
+    for (size_t i = 0; i < fragments.length(); ++i) {
+        if (fragments[i])
+            strcpy(cursor, fragments[i].get());
+        cursor += fragments[i] ? strlen(fragments[i].get()) : 0;
+    }
+
+    return UniqueChars(joined);
+}
+
+UniqueChars
+Statistics::formatDescription()
+{
+    int64_t sccTotal, sccLongest;
+    sccDurations(&sccTotal, &sccLongest);
+
+    double mmu20 = computeMMU(20 * PRMJ_USEC_PER_MSEC);
+    double mmu50 = computeMMU(50 * PRMJ_USEC_PER_MSEC);
+
+    const char *format =
+"=================================================================\n\
+  Reason: %s\n\
+  Incremental: %s%s\n\
+  Zones Collected: %d of %d\n\
+  Compartments Collected: %d of %d\n\
+  MinorGCs since last GC: %d\n\
+  MMU 20ms:%.3f; 50ms:%.3f\n\
+  SCC Sweep Total (MaxPause): %.3fms (%.3fms)\n\
+  HeapSize: %.3f MiB\n\
+  Chunk Delta (magnitude): %+d  (%d)\n\
+";
+    char buffer[1024];
+    memset(buffer, 0, sizeof(buffer));
+    snprintf(buffer, sizeof(buffer), format,
+             ExplainReason(slices[0].reason),
+             nonincrementalReason ? "no - " : "yes", nonincrementalReason ? nonincrementalReason : "",
+             zoneStats.collectedZoneCount, zoneStats.zoneCount,
+             zoneStats.collectedCompartmentCount, zoneStats.compartmentCount,
+             counts[STAT_MINOR_GC],
+             mmu20, mmu50,
+             t(sccTotal), t(sccLongest),
+             double(preBytes) / 1024. / 1024.,
+             counts[STAT_NEW_CHUNK] - counts[STAT_DESTROY_CHUNK], counts[STAT_NEW_CHUNK] +
+                                                                  counts[STAT_DESTROY_CHUNK]);
+    return make_string_copy(buffer);
+}
+
+UniqueChars
+Statistics::formatSliceDescription(unsigned i, const SliceData &slice)
+{
+    const char *format =
+"\
+  ---- Slice %u ----\n\
+    Reason: %s\n\
+    Reset: %s%s\n\
+    Page Faults: %ld\n\
+    Pause: %.3fms  (@ %.3fms)\n\
+";
+    char buffer[1024];
+    memset(buffer, 0, sizeof(buffer));
+    snprintf(buffer, sizeof(buffer), format, i,
+             ExplainReason(slice.reason),
+             slice.resetReason ? "yes - " : "no", slice.resetReason ? slice.resetReason : "",
+             uint64_t(slice.endFaults - slice.startFaults),
+             t(slice.duration()), t(slice.start - slices[0].start));
+    return make_string_copy(buffer);
+}
+
+UniqueChars
+Statistics::formatTotals()
+{
+    int64_t total, longest;
+    gcDuration(&total, &longest);
+
+    const char *format =
+"\
+  ---- Totals ----\n\
+    Total Time: %.3f\n\
+    Max Pause: %.3f\n\
+";
+    char buffer[1024];
+    memset(buffer, 0, sizeof(buffer));
+    snprintf(buffer, sizeof(buffer), format, t(total), t(longest));
+    return make_string_copy(buffer);
+}
+
+static int64_t
+SumChildTimes(Phase phase, int64_t *phaseTimes)
+{
+    int64_t total = 0;
+    for (unsigned i = 0; phases[i].name; i++) {
+        if (phases[i].parent == phase)
+            total += phaseTimes[phases[i].index];
+    }
+    return total;
+}
+
+UniqueChars
+Statistics::formatPhaseTimes(int64_t *phaseTimes)
+{
+    static const char *LevelToIndent[] = { "", "  ", "    ", "      " };
+
+    FragmentVector fragments;
+    char buffer[128];
+    for (unsigned i = 0; phases[i].name; i++) {
+        unsigned level = 0;
+        unsigned current = i;
+        while (phases[current].parent != PHASE_NO_PARENT) {
+            current = phases[current].parent;
+            level++;
+        }
+        MOZ_ASSERT(level < 4);
+
+        int64_t ownTime = phaseTimes[phases[i].index];
+        int64_t childTime = SumChildTimes(Phase(i), phaseTimes);
+        if (ownTime > 0) {
+            // If we have children and large parts of the time spent in this
+            // phase are not accounted to specific children, report the difference
+            // so that it is more obvious that we are losing time somewhere.
+            if (childTime && ownTime - childTime > 50) {
+                snprintf(buffer, sizeof(buffer), "      %s%s: %.3fms  (unaccounted: %.3fms)\n",
+                         LevelToIndent[level], phases[i].name, t(ownTime), t(ownTime - childTime));
+            } else {
+                snprintf(buffer, sizeof(buffer), "      %s%s: %.3fms\n",
+                         LevelToIndent[level], phases[i].name, t(ownTime));
+            }
+            if (!fragments.append(make_string_copy(buffer)))
+                return UniqueChars(nullptr);
+        }
+    }
+    return Join(fragments);
+}
+
+UniqueChars
+Statistics::formatDetailedMessage()
+{
+    FragmentVector fragments;
+
+    if (!fragments.append(formatDescription()))
+        return UniqueChars(nullptr);
+
+    if (slices.length() > 1) {
+        for (unsigned i = 0; i < slices.length(); i++) {
+            if (!fragments.append(formatSliceDescription(i, slices[i])))
+                return UniqueChars(nullptr);
+            if (!fragments.append(formatPhaseTimes(slices[i].phaseTimes)))
+                return UniqueChars(nullptr);
+        }
+    }
+    if (!fragments.append(formatTotals()))
+        return UniqueChars(nullptr);
+    if (!fragments.append(formatPhaseTimes(phaseTimes)))
+        return UniqueChars(nullptr);
+
+    return Join(fragments);
+}
+
 char16_t *
 Statistics::formatMessage()
 {
     StatisticsSerializer ss(StatisticsSerializer::AsText);
     formatData(ss, 0);
     return ss.finishJSString();
 }
 
@@ -517,23 +686,19 @@ Statistics::getMaxGCPauseSinceClear()
 {
     return maxPauseInInterval;
 }
 
 void
 Statistics::printStats()
 {
     if (fullFormat) {
-        StatisticsSerializer ss(StatisticsSerializer::AsText);
-        formatData(ss, 0);
-        char *msg = ss.finishCString();
-        if (msg) {
-            fprintf(fp, "GC(T+%.3fs) %s\n", t(slices[0].start - startupTime) / 1000.0, msg);
-            js_free(msg);
-        }
+        UniqueChars msg = formatDetailedMessage();
+        if (msg)
+            fprintf(fp, "GC(T+%.3fs) %s\n", t(slices[0].start - startupTime) / 1000.0, msg.get());
     } else {
         int64_t total, longest;
         gcDuration(&total, &longest);
 
         fprintf(fp, "%f %f %f\n",
                 t(total),
                 t(phaseTimes[PHASE_MARK]),
                 t(phaseTimes[PHASE_SWEEP]));
--- a/js/src/gc/Statistics.h
+++ b/js/src/gc/Statistics.h
@@ -4,16 +4,17 @@
  * 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 gc_Statistics_h
 #define gc_Statistics_h
 
 #include "mozilla/DebugOnly.h"
 #include "mozilla/PodOperations.h"
+#include "mozilla/UniquePtr.h"
 
 #include "jsalloc.h"
 #include "jspubtd.h"
 
 #include "js/GCAPI.h"
 #include "js/Vector.h"
 
 struct JSCompartment;
@@ -54,22 +55,22 @@ enum Phase {
     PHASE_DISCARD_TI,
     PHASE_FREE_TI_ARENA,
     PHASE_SWEEP_TYPES,
     PHASE_SWEEP_OBJECT,
     PHASE_SWEEP_STRING,
     PHASE_SWEEP_SCRIPT,
     PHASE_SWEEP_SHAPE,
     PHASE_SWEEP_JITCODE,
+    PHASE_FINALIZE_END,
+    PHASE_DESTROY,
     PHASE_COMPACT,
     PHASE_COMPACT_MOVE,
     PHASE_COMPACT_UPDATE,
     PHASE_COMPACT_UPDATE_GRAY,
-    PHASE_FINALIZE_END,
-    PHASE_DESTROY,
     PHASE_GC_END,
 
     PHASE_LIMIT
 };
 
 enum Stat {
     STAT_NEW_CHUNK,
     STAT_DESTROY_CHUNK,
@@ -78,27 +79,32 @@ enum Stat {
     STAT_LIMIT
 };
 
 class StatisticsSerializer;
 
 struct ZoneGCStats
 {
     /* Number of zones collected in this GC. */
-    int collectedCount;
+    int collectedZoneCount;
 
     /* Total number of zones in the Runtime at the start of this GC. */
     int zoneCount;
 
+    /* Total number of comaprtments in all zones collected. */
+    int collectedCompartmentCount;
+
     /* Total number of compartments in the Runtime at the start of this GC. */
     int compartmentCount;
 
-    bool isCollectingAllZones() const { return collectedCount == zoneCount; }
+    bool isCollectingAllZones() const { return collectedZoneCount == zoneCount; }
 
-    ZoneGCStats() : collectedCount(0), zoneCount(0), compartmentCount(0) {}
+    ZoneGCStats()
+      : collectedZoneCount(0), zoneCount(0), collectedCompartmentCount(0), compartmentCount(0)
+    {}
 };
 
 struct Statistics
 {
     explicit Statistics(JSRuntime *rt);
     ~Statistics();
 
     void beginPhase(Phase phase);
@@ -115,16 +121,17 @@ struct Statistics
         counts[s]++;
     }
 
     int64_t beginSCC();
     void endSCC(unsigned scc, int64_t start);
 
     char16_t *formatMessage();
     char16_t *formatJSON(uint64_t timestamp);
+    UniqueChars formatDetailedMessage();
 
     JS::GCSliceCallback setSliceCallback(JS::GCSliceCallback callback);
 
     int64_t clearMaxGCPauseAccumulator();
     int64_t getMaxGCPauseSinceClear();
 
   private:
     JSRuntime *runtime;
@@ -195,16 +202,21 @@ struct Statistics
     void beginGC();
     void endGC();
 
     void gcDuration(int64_t *total, int64_t *maxPause);
     void sccDurations(int64_t *total, int64_t *maxPause);
     void printStats();
     bool formatData(StatisticsSerializer &ss, uint64_t timestamp);
 
+    UniqueChars formatDescription();
+    UniqueChars formatSliceDescription(unsigned i, const SliceData &slice);
+    UniqueChars formatTotals();
+    UniqueChars formatPhaseTimes(int64_t *phaseTimes);
+
     double computeMMU(int64_t resolution);
 };
 
 struct AutoGCSlice
 {
     AutoGCSlice(Statistics &stats, const ZoneGCStats &zoneStats, JS::gcreason::Reason reason
                 MOZ_GUARD_OBJECT_NOTIFIER_PARAM)
       : stats(stats)
--- a/js/src/jsgc.cpp
+++ b/js/src/jsgc.cpp
@@ -5632,18 +5632,20 @@ GCRuntime::scanZonesBeforeGC()
         if (mode == JSGC_MODE_GLOBAL)
             zone->scheduleGC();
 
         /* This is a heuristic to avoid resets. */
         if (incrementalState != NO_INCREMENTAL && zone->needsIncrementalBarrier())
             zone->scheduleGC();
 
         zoneStats.zoneCount++;
-        if (zone->isGCScheduled())
-            zoneStats.collectedCount++;
+        if (zone->isGCScheduled()) {
+            zoneStats.collectedZoneCount++;
+            zoneStats.collectedCompartmentCount += zone->compartments.length();
+        }
     }
 
     for (CompartmentsIter c(rt, WithAtoms); !c.done(); c.next())
         zoneStats.compartmentCount++;
 
     return zoneStats;
 }