Bug 1150253 - Part 1: SpiderMonkey should call an embedder-provided callback
authorNick Fitzgerald <fitzgen@gmail.com>
Wed, 22 Apr 2015 09:43:02 -0700
changeset 240537 dcf12f5e9a96b02900b5cea3a2793b06b5d28c06
parent 240536 f6565bf7c16a73c1134323ab17d9e0f809d5ff47
child 240538 9ef1368b7c9fbaedafb228581005a3d43370366c
push id58853
push usernfitzgerald@mozilla.com
push dateWed, 22 Apr 2015 16:43:21 +0000
treeherdermozilla-inbound@feeed2f82c6f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1150253
milestone40.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 1150253 - Part 1: SpiderMonkey should call an embedder-provided callback instead of running the onGarbageCollection hook immediately; r=sfink
js/public/Debug.h
js/public/GCAPI.h
js/src/gc/Statistics.cpp
js/src/gc/Zone.cpp
js/src/jsgc.cpp
js/src/vm/Debugger.cpp
js/src/vm/Debugger.h
js/src/vm/Runtime.h
--- a/js/public/Debug.h
+++ b/js/public/Debug.h
@@ -7,28 +7,32 @@
 // Interfaces by which the embedding can interact with the Debugger API.
 
 #ifndef js_Debug_h
 #define js_Debug_h
 
 #include "mozilla/Assertions.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/MemoryReporting.h"
-#include "mozilla/Move.h"
+#include "mozilla/UniquePtr.h"
 
 #include "jspubtd.h"
 
+#include "js/GCAPI.h"
 #include "js/RootingAPI.h"
 #include "js/TypeDecls.h"
 
 namespace js {
 class Debugger;
 }
 
 namespace JS {
+
+using mozilla::UniquePtr;
+
 namespace dbg {
 
 // Helping embedding code build objects for Debugger
 // -------------------------------------------------
 //
 // Some Debugger API features lean on the embedding application to construct
 // their result values. For example, Debugger.Frame.prototype.scriptEntryReason
 // calls hooks provided by the embedding to construct values explaining why it
@@ -257,16 +261,34 @@ class BuilderOrigin : public Builder {
 // doesn't know which function is appropriate to use, but the embedding does.
 
 // Tell Debuggers in |runtime| to use |mallocSizeOf| to find the size of
 // malloc'd blocks.
 void SetDebuggerMallocSizeOf(JSRuntime* runtime, mozilla::MallocSizeOf mallocSizeOf);
 
 
 
+// Debugger and Garbage Collection Events
+// --------------------------------------
+//
+// The Debugger wants to report about its debuggees' GC cycles, however entering
+// JS after a GC is troublesome since SpiderMonkey will often do something like
+// force a GC and then rely on the nursery being empty. If we call into some
+// Debugger's hook after the GC, then JS runs and the nursery won't be
+// empty. Instead, we rely on embedders to call back into SpiderMonkey after a
+// GC and notify Debuggers to call their onGarbageCollection hook.
+
+
+// For each Debugger that observed a debuggee involved in the given GC event,
+// call its `onGarbageCollection` hook.
+JS_PUBLIC_API(bool)
+FireOnGarbageCollectionHook(JSContext* cx, GarbageCollectionEvent::Ptr&& data);
+
+
+
 // Handlers for observing Promises
 // -------------------------------
 //
 // The Debugger wants to observe behavior of promises, which are implemented by
 // Gecko with webidl and which SpiderMonkey knows nothing about. On the other
 // hand, Gecko knows nothing about which (if any) debuggers are observing a
 // promise's global. The compromise is that Gecko is responsible for calling
 // these handlers at the appropriate times, and SpiderMonkey will handle
--- a/js/public/GCAPI.h
+++ b/js/public/GCAPI.h
@@ -2,22 +2,28 @@
  * 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/. */
 
 #ifndef js_GCAPI_h
 #define js_GCAPI_h
 
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Vector.h"
+
 #include "js/HeapAPI.h"
 
 namespace js {
 namespace gc {
 class GCRuntime;
 }
+namespace gcstats {
+struct Statistics;
+}
 }
 
 typedef enum JSGCMode {
     /* Perform only global GCs. */
     JSGC_MODE_GLOBAL = 0,
 
     /* Perform per-compartment GCs until too much garbage has accumulated. */
     JSGC_MODE_COMPARTMENT = 1,
@@ -37,16 +43,18 @@ typedef enum JSGCInvocationKind {
     GC_NORMAL = 0,
 
     /* Minimize GC triggers and release empty GC chunks right away. */
     GC_SHRINK = 1
 } JSGCInvocationKind;
 
 namespace JS {
 
+using mozilla::UniquePtr;
+
 #define GCREASONS(D)                            \
     /* Reasons internal to the JS engine */     \
     D(API)                                      \
     D(EAGER_ALLOC_TRIGGER)                      \
     D(DESTROY_RUNTIME)                          \
     D(DESTROY_CONTEXT)                          \
     D(LAST_DITCH)                               \
     D(TOO_MUCH_MALLOC)                          \
@@ -249,16 +257,66 @@ FinishIncrementalGC(JSRuntime* rt, gcrea
  * If IsIncrementalGCInProgress(rt), this call aborts the ongoing collection and
  * performs whatever work needs to be done to return the collector to its idle
  * state. This may take an arbitrarily long time. When this function returns,
  * IsIncrementalGCInProgress(rt) will always be false.
  */
 extern JS_PUBLIC_API(void)
 AbortIncrementalGC(JSRuntime* rt);
 
+namespace dbg {
+
+// The `JS::dbg::GarbageCollectionEvent` class is essentially a view of the
+// `js::gcstats::Statistics` data without the uber implementation-specific bits.
+// It should generally be palatable for web developers.
+class GarbageCollectionEvent
+{
+    // The major GC number of the GC cycle this data pertains to.
+    uint64_t majorGCNumber_;
+
+    // Reference to a non-owned, statically allocated C string. This is a very
+    // short reason explaining why a GC was triggered.
+    const char* reason;
+
+    // Reference to a nullable, non-owned, statically allocated C string. If the
+    // collection was forced to be non-incremental, this is a short reason of
+    // why the GC could not perform an incremental collection.
+    const char* nonincrementalReason;
+
+    // Represents a single slice of a possibly multi-slice incremental garbage
+    // collection.
+    struct Collection {
+        int64_t startTimestamp;
+        int64_t endTimestamp;
+    };
+
+    // The set of garbage collection slices that made up this GC cycle.
+    mozilla::Vector<Collection> collections;
+
+    GarbageCollectionEvent(const GarbageCollectionEvent& rhs) = delete;
+    GarbageCollectionEvent& operator=(const GarbageCollectionEvent& rhs) = delete;
+
+  public:
+    explicit GarbageCollectionEvent(uint64_t majorGCNum)
+        : majorGCNumber_(majorGCNum)
+        , reason(nullptr)
+        , nonincrementalReason(nullptr)
+        , collections()
+    { }
+
+    using Ptr = UniquePtr<GarbageCollectionEvent, DeletePolicy<GarbageCollectionEvent>>;
+    static Ptr Create(JSRuntime* rt, ::js::gcstats::Statistics& stats, uint64_t majorGCNumber);
+
+    JSObject* toJSObject(JSContext* cx) const;
+
+    uint64_t majorGCNumber() const { return majorGCNumber_; }
+};
+
+} // namespace dbg
+
 enum GCProgress {
     /*
      * During non-incremental GC, the GC is bracketed by JSGC_CYCLE_BEGIN/END
      * callbacks. During an incremental GC, the sequence of callbacks is as
      * follows:
      *   JSGC_CYCLE_BEGIN, JSGC_SLICE_END  (first slice)
      *   JSGC_SLICE_BEGIN, JSGC_SLICE_END  (second slice)
      *   ...
@@ -275,16 +333,18 @@ struct JS_PUBLIC_API(GCDescription) {
     bool isCompartment_;
     JSGCInvocationKind invocationKind_;
 
     GCDescription(bool isCompartment, JSGCInvocationKind kind)
       : isCompartment_(isCompartment), invocationKind_(kind) {}
 
     char16_t* formatMessage(JSRuntime* rt) const;
     char16_t* formatJSON(JSRuntime* rt, uint64_t timestamp) const;
+
+    JS::dbg::GarbageCollectionEvent::Ptr toGCEvent(JSRuntime* rt) const;
 };
 
 typedef void
 (* GCSliceCallback)(JSRuntime* rt, GCProgress progress, const GCDescription& desc);
 
 /*
  * The GC slice callback is called at the beginning and end of each slice. This
  * callback may be used for GC notifications as well as to perform additional
--- a/js/src/gc/Statistics.cpp
+++ b/js/src/gc/Statistics.cpp
@@ -969,19 +969,16 @@ Statistics::endGC()
     if (!aborted) {
         double mmu50 = computeMMU(50 * PRMJ_USEC_PER_MSEC);
         runtime->addTelemetry(JS_TELEMETRY_GC_MMU_50, mmu50 * 100);
     }
 
     if (fp)
         printStats();
 
-    if (!aborted)
-        Debugger::onGarbageCollection(runtime, *this);
-
     // Clear the timers at the end of a GC because we accumulate time in
     // between GCs for some (which come before PHASE_GC_BEGIN in the list.)
     PodZero(&phaseStartTimes[PHASE_GC_BEGIN], PHASE_LIMIT - PHASE_GC_BEGIN);
     for (size_t d = PHASE_DAG_NONE; d < MAX_MULTIPARENT_PHASES + 1; d++)
         PodZero(&phaseTimes[d][PHASE_GC_BEGIN], PHASE_LIMIT - PHASE_GC_BEGIN);
 
     aborted = false;
 }
--- a/js/src/gc/Zone.cpp
+++ b/js/src/gc/Zone.cpp
@@ -259,26 +259,35 @@ Zone::canCollect()
         return false;
     return true;
 }
 
 void
 Zone::notifyObservingDebuggers()
 {
     for (CompartmentsInZoneIter comps(this); !comps.done(); comps.next()) {
-        RootedGlobalObject global(runtimeFromAnyThread(), comps->maybeGlobal());
+        JSRuntime* rt = runtimeFromAnyThread();
+        RootedGlobalObject global(rt, comps->maybeGlobal());
         if (!global)
             continue;
 
         GlobalObject::DebuggerVector* dbgs = global->getDebuggers();
         if (!dbgs)
             continue;
 
-        for (GlobalObject::DebuggerVector::Range r = dbgs->all(); !r.empty(); r.popFront())
-            r.front()->debuggeeIsBeingCollected();
+        for (GlobalObject::DebuggerVector::Range r = dbgs->all(); !r.empty(); r.popFront()) {
+            if (!r.front()->debuggeeIsBeingCollected(rt->gc.majorGCCount())) {
+#ifdef DEBUG
+                fprintf(stderr,
+                        "OOM while notifying observing Debuggers of a GC: The onGarbageCollection\n"
+                        "hook will not be fired for this GC for some Debuggers!\n");
+#endif
+                return;
+            }
+        }
     }
 }
 
 JS::Zone*
 js::ZoneOfValue(const JS::Value& value)
 {
     MOZ_ASSERT(value.isMarkable());
     if (value.isObject())
--- a/js/src/jsgc.cpp
+++ b/js/src/jsgc.cpp
@@ -7024,16 +7024,22 @@ JS::AbortIncrementalGC(JSRuntime* rt)
 }
 
 char16_t*
 JS::GCDescription::formatMessage(JSRuntime* rt) const
 {
     return rt->gc.stats.formatMessage();
 }
 
+JS::dbg::GarbageCollectionEvent::Ptr
+JS::GCDescription::toGCEvent(JSRuntime* rt) const
+{
+    return JS::dbg::GarbageCollectionEvent::Create(rt, rt->gc.stats, rt->gc.majorGCCount());
+}
+
 char16_t*
 JS::GCDescription::formatJSON(JSRuntime* rt, uint64_t timestamp) const
 {
     return rt->gc.stats.formatJSON(timestamp);
 }
 
 JS_PUBLIC_API(JS::GCSliceCallback)
 JS::SetGCSliceCallback(JSRuntime* rt, GCSliceCallback callback)
--- a/js/src/vm/Debugger.cpp
+++ b/js/src/vm/Debugger.cpp
@@ -39,16 +39,17 @@
 
 using namespace js;
 
 using JS::dbg::Builder;
 using js::frontend::IsIdentifier;
 using mozilla::ArrayLength;
 using mozilla::DebugOnly;
 using mozilla::Maybe;
+using mozilla::UniquePtr;
 
 
 /*** Forward declarations ************************************************************************/
 
 extern const Class DebuggerFrame_class;
 
 enum {
     JSSLOT_DEBUGFRAME_OWNER,
@@ -353,19 +354,18 @@ Breakpoint::nextInSite()
 
 
 /*** Debugger hook dispatch **********************************************************************/
 
 Debugger::Debugger(JSContext* cx, NativeObject* dbg)
   : object(dbg),
     uncaughtExceptionHook(nullptr),
     enabled(true),
+    observedGCs(cx),
     allowUnobservedAsmJS(false),
-    debuggeeWasCollected(false),
-    inOnGCHook(false),
     trackingAllocationSites(false),
     allocationSamplingProbability(1.0),
     allocationsLogLength(0),
     maxAllocationsLogLength(DEFAULT_MAX_ALLOCATIONS_LOG_LENGTH),
     allocationsLogOverflowed(false),
     frames(cx->runtime()),
     scripts(cx),
     sources(cx),
@@ -403,16 +403,17 @@ Debugger::~Debugger()
 bool
 Debugger::init(JSContext* cx)
 {
     bool ok = debuggees.init() &&
               frames.init() &&
               scripts.init() &&
               sources.init() &&
               objects.init() &&
+              observedGCs.init() &&
               environments.init();
     if (!ok)
         ReportOutOfMemory(cx);
     return ok;
 }
 
 JS_STATIC_ASSERT(unsigned(JSSLOT_DEBUGFRAME_OWNER) == unsigned(JSSLOT_DEBUGSCRIPT_OWNER));
 JS_STATIC_ASSERT(unsigned(JSSLOT_DEBUGFRAME_OWNER) == unsigned(JSSLOT_DEBUGSOURCE_OWNER));
@@ -873,81 +874,16 @@ Debugger::wrapDebuggeeValue(JSContext* c
     } else if (!cx->compartment()->wrap(cx, vp)) {
         vp.setUndefined();
         return false;
     }
 
     return true;
 }
 
-JSObject*
-Debugger::translateGCStatistics(JSContext* cx, const gcstats::Statistics& stats)
-{
-    // If this functions triggers a GC then the statistics object will change
-    // underneath us.
-    gc::AutoSuppressGC suppressGC(cx);
-
-    RootedObject obj(cx, NewBuiltinClassInstance<PlainObject>(cx));
-    if (!obj)
-        return nullptr;
-
-    const char* nonincrementalReason = stats.nonincrementalReason();
-    RootedValue nonincrementalReasonValue(cx, NullValue());
-    if (nonincrementalReason) {
-        JSAtom* atomized = Atomize(cx, nonincrementalReason, strlen(nonincrementalReason));
-        if (!atomized)
-            return nullptr;
-        nonincrementalReasonValue.setString(atomized);
-    }
-
-    if (!DefineProperty(cx, obj, cx->names().nonincrementalReason, nonincrementalReasonValue))
-        return nullptr;
-
-    RootedArrayObject slicesArray(cx, NewDenseEmptyArray(cx));
-    if (!slicesArray)
-        return nullptr;
-
-    size_t idx = 0;
-    for (auto range = stats.sliceRange(); !range.empty(); range.popFront()) {
-        if (idx == 0) {
-            // There is only one GC reason for the whole cycle, but for legacy
-            // reasons, this data is stored and replicated on each slice.
-            const char* reason = gcstats::ExplainReason(range.front().reason);
-            JSAtom* atomized = Atomize(cx, reason, strlen(reason));
-            if (!atomized)
-                return nullptr;
-            RootedValue reasonVal(cx, StringValue(atomized));
-            if (!DefineProperty(cx, obj, cx->names().reason, reasonVal))
-                return nullptr;
-        }
-
-        RootedPlainObject collectionObj(cx, NewBuiltinClassInstance<PlainObject>(cx));
-        if (!collectionObj)
-            return nullptr;
-
-        RootedValue start(cx, NumberValue(range.front().start));
-        RootedValue end(cx, NumberValue(range.front().end));
-        if (!DefineProperty(cx, collectionObj, cx->names().startTimestamp, start) ||
-            !DefineProperty(cx, collectionObj, cx->names().endTimestamp, end))
-        {
-            return nullptr;
-        }
-
-        RootedValue collectionVal(cx, ObjectValue(*collectionObj));
-        if (!DefineElement(cx, slicesArray, idx++, collectionVal))
-            return nullptr;
-    }
-
-    RootedValue slicesValue(cx, ObjectValue(*slicesArray));
-    if (!DefineProperty(cx, obj, cx->names().collections, slicesValue))
-        return nullptr;
-
-    return obj.get();
-}
-
 bool
 Debugger::unwrapDebuggeeObject(JSContext* cx, MutableHandleObject obj)
 {
     if (obj->getClass() != &DebuggerObject_class) {
         JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_NOT_EXPECTED_TYPE,
                              "Debugger", "Debugger.Object", obj->getClass()->name);
         return false;
     }
@@ -1329,45 +1265,38 @@ Debugger::fireNewScript(JSContext* cx, H
 
     RootedValue scriptObject(cx, ObjectValue(*dsobj));
     RootedValue rv(cx);
     if (!Invoke(cx, ObjectValue(*object), ObjectValue(*hook), 1, scriptObject.address(), &rv))
         handleUncaughtException(ac, true);
 }
 
 void
-Debugger::fireOnGarbageCollectionHook(JSRuntime* rt, const gcstats::Statistics& stats)
-{
-    if (inOnGCHook)
-        return;
-
-    AutoOnGCHookReentrancyGuard guard(*this);
-
-    MOZ_ASSERT(debuggeeWasCollected);
-    debuggeeWasCollected = false;
-
-    JSContext* cx = DefaultJSContext(rt);
-    MOZ_ASSERT(cx);
+Debugger::fireOnGarbageCollectionHook(JSContext* cx,
+                                      const JS::dbg::GarbageCollectionEvent::Ptr& gcData)
+{
+    MOZ_ASSERT(observedGC(gcData->majorGCNumber()));
+    observedGCs.remove(gcData->majorGCNumber());
 
     RootedObject hook(cx, getHook(OnGarbageCollection));
     MOZ_ASSERT(hook);
     MOZ_ASSERT(hook->isCallable());
 
     Maybe<AutoCompartment> ac;
     ac.emplace(cx, object);
 
-    JSObject* statsObj = translateGCStatistics(cx, stats);
-    if (!statsObj) {
+    JSObject* dataObj = gcData->toJSObject(cx);
+    if (!dataObj) {
         handleUncaughtException(ac, false);
         return;
     }
 
-    RootedValue statsVal(cx, ObjectValue(*statsObj));
+    RootedValue dataVal(cx, ObjectValue(*dataObj));
     RootedValue rv(cx);
-    if (!Invoke(cx, ObjectValue(*object), ObjectValue(*hook), 1, statsVal.address(), &rv))
+    if (!Invoke(cx, ObjectValue(*object), ObjectValue(*hook), 1, dataVal.address(), &rv))
         handleUncaughtException(ac, true);
 }
 
 /* static */ JSTrapStatus
 Debugger::dispatchHook(JSContext* cx, MutableHandleValue vp, Hook which, HandleObject payload)
 {
     MOZ_ASSERT(which == OnDebuggerStatement ||
                which == OnExceptionUnwind ||
@@ -7860,8 +7789,134 @@ JS::dbg::IsDebugger(JS::Value val)
         return false;
 
     JSObject& obj = val.toObject();
     if (obj.getClass() != &Debugger::jsclass)
         return false;
 
     return js::Debugger::fromJSObject(&obj) != nullptr;
 }
+
+
+/*** JS::dbg::GarbageCollectionEvent **************************************************************/
+
+namespace JS {
+namespace dbg {
+
+/* static */ GarbageCollectionEvent::Ptr
+GarbageCollectionEvent::Create(JSRuntime* rt, ::js::gcstats::Statistics& stats, uint64_t gcNumber)
+{
+    auto data = rt->make_unique<GarbageCollectionEvent>(gcNumber);
+    if (!data)
+        return nullptr;
+
+    data->nonincrementalReason = stats.nonincrementalReason();
+
+    for (auto range = stats.sliceRange(); !range.empty(); range.popFront()) {
+        if (!data->reason) {
+            // There is only one GC reason for the whole cycle, but for legacy
+            // reasons this data is stored and replicated on each slice. Each
+            // slice used to have its own GCReason, but now they are all the
+            // same.
+            data->reason = gcstats::ExplainReason(range.front().reason);
+            MOZ_ASSERT(data->reason);
+        }
+
+        if (!data->collections.growBy(1))
+            return nullptr;
+
+        data->collections.back().startTimestamp = range.front().start;
+        data->collections.back().endTimestamp = range.front().end;
+    }
+
+
+    return data;
+}
+
+static bool
+DefineStringProperty(JSContext* cx, HandleObject obj, PropertyName* propName, const char* strVal)
+{
+    RootedValue val(cx, UndefinedValue());
+    if (strVal) {
+        JSAtom* atomized = Atomize(cx, strVal, strlen(strVal));
+        if (!atomized)
+            return false;
+        val = StringValue(atomized);
+    }
+    return DefineProperty(cx, obj, propName, val);
+}
+
+JSObject*
+GarbageCollectionEvent::toJSObject(JSContext* cx) const
+{
+    RootedObject obj(cx, NewBuiltinClassInstance<PlainObject>(cx));
+    if (!obj ||
+        !DefineStringProperty(cx, obj, cx->names().nonincrementalReason, nonincrementalReason) ||
+        !DefineStringProperty(cx, obj, cx->names().reason, reason))
+    {
+        return nullptr;
+    }
+
+    RootedArrayObject slicesArray(cx, NewDenseEmptyArray(cx));
+    if (!slicesArray)
+        return nullptr;
+
+    size_t idx = 0;
+    for (auto range = collections.all(); !range.empty(); range.popFront()) {
+        RootedPlainObject collectionObj(cx, NewBuiltinClassInstance<PlainObject>(cx));
+        if (!collectionObj)
+            return nullptr;
+
+        RootedValue start(cx, NumberValue(range.front().startTimestamp));
+        RootedValue end(cx, NumberValue(range.front().endTimestamp));
+        if (!DefineProperty(cx, collectionObj, cx->names().startTimestamp, start) ||
+            !DefineProperty(cx, collectionObj, cx->names().endTimestamp, end))
+        {
+            return nullptr;
+        }
+
+        RootedValue collectionVal(cx, ObjectValue(*collectionObj));
+        if (!DefineElement(cx, slicesArray, idx++, collectionVal))
+            return nullptr;
+    }
+
+    RootedValue slicesValue(cx, ObjectValue(*slicesArray));
+    if (!DefineProperty(cx, obj, cx->names().collections, slicesValue))
+        return nullptr;
+
+    return obj;
+}
+
+JS_PUBLIC_API(bool)
+FireOnGarbageCollectionHook(JSContext* cx, JS::dbg::GarbageCollectionEvent::Ptr&& data)
+{
+    AutoObjectVector triggered(cx);
+
+    {
+        // We had better not GC (and potentially get a dangling Debugger
+        // pointer) while finding all Debuggers observing a debuggee that
+        // participated in this GC.
+        AutoCheckCannotGC noGC;
+
+        for (Debugger* dbg = cx->runtime()->debuggerList.getFirst(); dbg; dbg = dbg->getNext()) {
+            if (dbg->enabled &&
+                dbg->observedGC(data->majorGCNumber()) &&
+                dbg->getHook(Debugger::OnGarbageCollection))
+            {
+                if (!triggered.append(dbg->object)) {
+                    JS_ReportOutOfMemory(cx);
+                    return false;
+                }
+            }
+        }
+    }
+
+    for ( ; !triggered.empty(); triggered.popBack()) {
+        Debugger* dbg = Debugger::fromJSObject(triggered.back());
+        dbg->fireOnGarbageCollectionHook(cx, data);
+        MOZ_ASSERT(!cx->isExceptionPending());
+    }
+
+    return true;
+}
+
+} // namespace dbg
+} // namespace JS
--- a/js/src/vm/Debugger.h
+++ b/js/src/vm/Debugger.h
@@ -5,16 +5,18 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef vm_Debugger_h
 #define vm_Debugger_h
 
 #include "mozilla/GuardObjects.h"
 #include "mozilla/LinkedList.h"
 #include "mozilla/Range.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Vector.h"
 
 #include "jsclist.h"
 #include "jscntxt.h"
 #include "jscompartment.h"
 #include "jsweakmap.h"
 #include "jswrapper.h"
 
 #include "gc/Barrier.h"
@@ -185,16 +187,18 @@ class Debugger : private mozilla::Linked
     friend class DebuggerMemory;
     friend class SavedStacks;
     friend class mozilla::LinkedListElement<Debugger>;
     friend bool (::JS_DefineDebuggerObject)(JSContext* cx, JS::HandleObject obj);
     friend bool (::JS::dbg::IsDebugger)(JS::Value val);
     friend JSObject* SavedStacksMetadataCallback(JSContext* cx);
     friend void JS::dbg::onNewPromise(JSContext* cx, HandleObject promise);
     friend void JS::dbg::onPromiseSettled(JSContext* cx, HandleObject promise);
+    friend bool JS::dbg::FireOnGarbageCollectionHook(JSContext* cx,
+                                                     JS::dbg::GarbageCollectionEvent::Ptr&& data);
 
   public:
     enum Hook {
         OnDebuggerStatement,
         OnExceptionUnwind,
         OnNewScript,
         OnEnterFrame,
         OnNewGlobalObject,
@@ -237,27 +241,39 @@ class Debugger : private mozilla::Linked
         NotObserving = 0,
         Observing = 1
     };
 
     // Return true if the given compartment is a debuggee of this debugger,
     // false otherwise.
     bool isDebuggee(const JSCompartment* compartment) const;
 
-    // Notify this Debugger that one of its debuggee compartments' zones is
-    // being collected.
-    void debuggeeIsBeingCollected() { debuggeeWasCollected = true; }
+    // Return true if this Debugger observed a debuggee that participated in the
+    // GC identified by the given GC number. Return false otherwise.
+    bool observedGC(uint64_t majorGCNumber) const {
+        return observedGCs.has(majorGCNumber);
+    }
+
+    // Notify this Debugger that one or more of its debuggees is participating
+    // in the GC identified by the given GC number.
+    bool debuggeeIsBeingCollected(uint64_t majorGCNumber) {
+        return observedGCs.put(majorGCNumber);
+    }
 
   private:
     HeapPtrNativeObject object;         /* The Debugger object. Strong reference. */
     WeakGlobalObjectSet debuggees;      /* Debuggee globals. Cross-compartment weak references. */
     js::HeapPtrObject uncaughtExceptionHook; /* Strong reference. */
     bool enabled;
     JSCList breakpoints;                /* Circular list of all js::Breakpoints in this debugger */
 
+    // The set of GC numbers for which one or more of this Debugger's observed
+    // debuggees participated in.
+    js::HashSet<uint64_t> observedGCs;
+
     struct AllocationSite : public mozilla::LinkedListElement<AllocationSite>
     {
         AllocationSite(HandleObject frame, int64_t when, const char* className)
             : frame(frame),
               when(when),
               className(className)
         {
             MOZ_ASSERT_IF(frame, UncheckedUnwrap(frame)->is<SavedFrame>());
@@ -265,48 +281,16 @@ class Debugger : private mozilla::Linked
 
         RelocatablePtrObject frame;
         int64_t when;
         const char* className;
     };
     typedef mozilla::LinkedList<AllocationSite> AllocationSiteList;
 
     bool allowUnobservedAsmJS;
-
-    // During a GC cycle, this is true if one of this Debugger's debuggees was
-    // collected. When the GC cycle completes, this flag is reset.
-    bool debuggeeWasCollected;
-
-    // True while we are executing the onGarbageCollection hook, and therefore
-    // should not fire the hook for this Debugger instance again if there is a
-    // GC while we are executing the hook. See also
-    // `AutoOnGCHookReentrancyGuard` below.
-    bool inOnGCHook;
-
-    // RAII class to automatically guard against reentrancy into the
-    // OnGarbageCollection hook.
-    class MOZ_STACK_CLASS AutoOnGCHookReentrancyGuard {
-        MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER;
-        Debugger& dbg;
-
-    public:
-        explicit AutoOnGCHookReentrancyGuard(Debugger& dbg MOZ_GUARD_OBJECT_NOTIFIER_PARAM)
-            : dbg(dbg)
-        {
-            MOZ_GUARD_OBJECT_NOTIFIER_INIT;
-            MOZ_ASSERT(!dbg.inOnGCHook);
-            dbg.inOnGCHook = true;
-        }
-
-        ~AutoOnGCHookReentrancyGuard() {
-            MOZ_ASSERT(dbg.inOnGCHook);
-            dbg.inOnGCHook = false;
-        }
-    };
-
     bool trackingAllocationSites;
     double allocationSamplingProbability;
     AllocationSiteList allocationsLog;
     size_t allocationsLogLength;
     size_t maxAllocationsLogLength;
     bool allocationsLogOverflowed;
 
     static const size_t DEFAULT_MAX_ALLOCATIONS_LOG_LENGTH = 5000;
@@ -549,19 +533,20 @@ class Debugger : private mozilla::Linked
     /*
      * Receive a "new script" event from the engine. A new script was compiled
      * or deserialized.
      */
     void fireNewScript(JSContext* cx, HandleScript script);
 
     /*
      * Receive a "garbage collection" event from the engine. A GC cycle with the
-     * given statistics was just completed.
+     * given data was recently completed.
      */
-    void fireOnGarbageCollectionHook(JSRuntime* rt, const gcstats::Statistics& stats);
+    void fireOnGarbageCollectionHook(JSContext* cx,
+                                     const JS::dbg::GarbageCollectionEvent::Ptr& gcData);
 
     /*
      * Gets a Debugger.Frame object. If maybeIter is non-null, we eagerly copy
      * its data if we need to make a new Debugger.Frame.
      */
     bool getScriptFrameWithIter(JSContext* cx, AbstractFramePtr frame,
                                 const ScriptFrameIter* maybeIter, MutableHandleValue vp);
 
@@ -670,17 +655,16 @@ class Debugger : private mozilla::Linked
      * alternative path, containing its own call to onLeaveFrame.)
      */
     static inline bool onLeaveFrame(JSContext* cx, AbstractFramePtr frame, bool ok);
 
     static inline void onNewScript(JSContext* cx, HandleScript script);
     static inline void onNewGlobalObject(JSContext* cx, Handle<GlobalObject*> global);
     static inline bool onLogAllocationSite(JSContext* cx, JSObject* obj, HandleSavedFrame frame,
                                            int64_t when);
-    static inline void onGarbageCollection(JSRuntime* rt, const gcstats::Statistics& stats);
     static JSTrapStatus onTrap(JSContext* cx, MutableHandleValue vp);
     static JSTrapStatus onSingleStep(JSContext* cx, MutableHandleValue vp);
     static bool handleBaselineOsr(JSContext* cx, InterpreterFrame* from, jit::BaselineFrame* to);
     static bool handleIonBailout(JSContext* cx, jit::RematerializedFrame* from, jit::BaselineFrame* to);
     static void handleUnrecoverableIonBailoutError(JSContext* cx, jit::RematerializedFrame* frame);
     static void propagateForcedReturn(JSContext* cx, AbstractFramePtr frame, HandleValue rval);
     static bool hasLiveHook(GlobalObject* global, Hook which);
     static bool inFrameMaps(AbstractFramePtr frame);
@@ -720,23 +704,16 @@ class Debugger : private mozilla::Linked
      *
      * If *vp is a magic JS_UNINITIALIZED_LEXICAL value signifying an
      * unaccessible uninitialized binding, this produces a plain object of the
      * form { uninitialized: true }.
      */
     bool wrapDebuggeeValue(JSContext* cx, MutableHandleValue vp);
 
     /*
-     * Converts an implementor level of detail gcstats::Statistics object into a
-     * JSObject that web developers should be able to make sense of. Returns
-     * nullptr on failure.
-     */
-    JSObject* translateGCStatistics(JSContext* cx, const gcstats::Statistics& stats);
-
-    /*
      * Unwrap a Debug.Object, without rewrapping it for any particular debuggee
      * compartment.
      *
      * Preconditions: cx is in the debugger compartment. *vp is a value in that
      * compartment. (*vp should be a "debuggee value", meaning it is the
      * debugger's reflection of a value in the debuggee.)
      *
      * If *vp is a Debugger.Object, store the referent in *vp. Otherwise, if *vp
@@ -987,23 +964,14 @@ Debugger::onLogAllocationSite(JSContext*
 {
     GlobalObject::DebuggerVector* dbgs = cx->global()->getDebuggers();
     if (!dbgs || dbgs->empty())
         return true;
     RootedObject hobj(cx, obj);
     return Debugger::slowPathOnLogAllocationSite(cx, hobj, frame, when, *dbgs);
 }
 
-/* static */ void
-Debugger::onGarbageCollection(JSRuntime* rt, const gcstats::Statistics& stats)
-{
-    for (Debugger* dbg = rt->debuggerList.getFirst(); dbg; dbg = dbg->getNext()) {
-        if (dbg->debuggeeWasCollected && dbg->getHook(OnGarbageCollection)) {
-            dbg->fireOnGarbageCollectionHook(rt, stats);
-        }
-    }
-}
-
 bool ReportObjectRequired(JSContext* cx);
 
 } /* namespace js */
 
+
 #endif /* vm_Debugger_h */
--- a/js/src/vm/Runtime.h
+++ b/js/src/vm/Runtime.h
@@ -26,16 +26,17 @@
 # include "asmjs/AsmJSSignalHandlers.h"
 #endif
 #include "builtin/AtomicsObject.h"
 #include "ds/FixedSizeHash.h"
 #include "frontend/ParseMaps.h"
 #include "gc/GCRuntime.h"
 #include "gc/Tracer.h"
 #include "irregexp/RegExpStack.h"
+#include "js/Debug.h"
 #include "js/HashTable.h"
 #ifdef DEBUG
 # include "js/Proxy.h" // For AutoEnterPolicy
 #endif
 #include "js/Vector.h"
 #include "vm/CommonPropertyNames.h"
 #include "vm/DateTime.h"
 #include "vm/MallocProvider.h"