Bug 1065657 - Allow multiple Debuggers to track allocations at the same time. r=shu
authorNick Fitzgerald <fitzgen@gmail.com>
Wed, 22 Apr 2015 09:49:07 -0700
changeset 241410 07900c19ad29f792a460cdf6c79eb7097d50c87e
parent 241409 67cb93edea83debd7b96c7b3b671a23a719b594f
child 241411 94818e6ab15b2af79b584952feb6cd6eb6ebda4b
push id59108
push userryanvm@gmail.com
push dateTue, 28 Apr 2015 15:03:16 +0000
treeherdermozilla-inbound@96862be5fb53 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersshu
bugs1065657
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 1065657 - Allow multiple Debuggers to track allocations at the same time. r=shu
js/src/doc/Debugger/Debugger.Memory.md
js/src/doc/Debugger/config.sh
js/src/jit-test/tests/debug/Memory-trackingAllocationSites-03.js
js/src/jscompartment.h
js/src/vm/Debugger.cpp
js/src/vm/Debugger.h
js/src/vm/DebuggerMemory.cpp
js/src/vm/GlobalObject.cpp
js/src/vm/GlobalObject.h
js/src/vm/SavedStacks.cpp
--- a/js/src/doc/Debugger/Debugger.Memory.md
+++ b/js/src/doc/Debugger/Debugger.Memory.md
@@ -26,16 +26,22 @@ The JavaScript engine marks each new obj
 allocated, if:
 
 - the object is allocated in the scope of a global object that is a debuggee of
   some [`Debugger`][debugger-object] instance <i>dbg</i>; and
 
 - <code><i>dbg</i>.memory.[trackingAllocationSites][tracking-allocs]</code> is
   set to `true`.
 
+- A [Bernoulli trial][bernoulli-trial] succeeds, with probability equal to the
+  maximum of
+  [`d.memory.allocationSamplingProbability`][alloc-sampling-probability] of all
+  `Debugger` instances `d` that are observing the global that this object is
+  allocated within the scope of.
+
 Given a [`Debugger.Object`][object] instance <i>dobj</i> referring to some
 object, <code><i>dobj</i>.[allocationSite][allocation-site]</code> returns a
 [saved call stack][saved-frame] indicating where <i>dobj</i>'s referent was
 allocated.
 
 
 ### Allocation Logging
 
@@ -75,16 +81,28 @@ following accessor properties from its p
 
     Assignment is fallible: if the Debugger cannot track allocation sites, it
     throws an `Error` instance.
 
     You can retrieve the allocation site for a given object with the
     [`Debugger.Object.prototype.allocationSite`][allocation-site] accessor
     property.
 
+<code id='alloc-sampling-probability'>allocationSamplingProbability</code>
+:   A number between 0 and 1 that indicates the probability with which each new
+    allocation should be entered into the allocations log. 0 is equivalent to
+    "never", 1 is "always", and .05 would be "one out of twenty".
+
+    The default is 1, or logging every allocation.
+
+    Note that in the presence of multiple <code>Debugger</code> instances
+    observing the same allocations within a global's scope, the maximum
+    <code>allocationSamplingProbability</code> of all the
+    <code>Debugger</code>s is used.
+
 <code id='max-alloc-log'>maxAllocationsLogLength</code>
 :   The maximum number of allocation sites to accumulate in the allocations log
     at a time. This accessor can be both fetched and stored to. Its default
     value is `5000`.
 
 <code id='allocationsLogOverflowed'>allocationsLogOverflowed</code>
 :   Returns `true` if there have been more than
     [`maxAllocationsLogLength`][#max-alloc-log] allocations since the last time
--- a/js/src/doc/Debugger/config.sh
+++ b/js/src/doc/Debugger/config.sh
@@ -34,21 +34,22 @@ markdown Debugger.Object.md Debugger-API
 
 markdown Debugger.Script.md Debugger-API/Debugger.Script
   label 'script'                                "Debugger.Script"
 
 markdown Debugger.Source.md Debugger-API/Debugger.Source
   label 'source'                                "Debugger.Source"
 
 markdown Debugger.Memory.md Debugger-API/Debugger.Memory
-  label 'memory'                                "Debugger.Memory"
-  label 'tracking-allocs' '#trackingallocationsites' "Debugger.Memory: trackingAllocationSites"
-  label 'drain-alloc-log' '#drain-alloc-log'    "Debugger.Memory: drainAllocationsLog"
-  label 'max-alloc-log' '#max-alloc-log'        "Debugger.Memory: maxAllocationsLogLength"
-  label 'take-census'   '#take-census'          "Debugger.Memory: takeCensus"
+  label 'memory'                                                   "Debugger.Memory"
+  label 'tracking-allocs'            '#trackingallocationsites'    "Debugger.Memory: trackingAllocationSites"
+  label 'drain-alloc-log'            '#drain-alloc-log'            "Debugger.Memory: drainAllocationsLog"
+  label 'max-alloc-log'              '#max-alloc-log'              "Debugger.Memory: maxAllocationsLogLength"
+  label 'alloc-sampling-probability' '#alloc-sampling-probability' "Debugger.Memory: allocationSamplingProbability"
+  label 'take-census'                '#take-census'                "Debugger.Memory: takeCensus"
 
 markdown Tutorial-Debugger-Statement.md Debugger-API/Tutorial-Debugger-Statement
   label 'tut debugger'                          "Tutorial: the debugger; statement"
 
 markdown Tutorial-Alloc-Log-Tree.md Debugger-API/Tutorial-Allocation-Log-Tree
   label 'tut alloc log'                         "Tutorial: the allocation log"
 
 # Images:
@@ -57,8 +58,9 @@ resource 'img-shadows'            shadow
 resource 'img-chrome-pref'        enable-chrome-devtools.png         $RBASE/7233/enable-chrome-devtools.png
 resource 'img-scratchpad-browser' scratchpad-browser-environment.png $RBASE/7229/scratchpad-browser-environment.png
 resource 'img-example-alert'      debugger-alert.png                 $RBASE/7231/debugger-alert.png
 resource 'img-alloc-plot'         alloc-plot-console.png             $RBASE/8461/alloc-plot-console.png
 
 # External links:
 absolute-label 'protocol' https://wiki.mozilla.org/Remote_Debugging_Protocol "Remote Debugging Protocol"
 absolute-label 'saved-frame' https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/SavedFrame "SavedFrame"
+absolute-label 'bernoulli-trial' https://en.wikipedia.org/wiki/Bernoulli_trial "Bernoulli Trial"
--- a/js/src/jit-test/tests/debug/Memory-trackingAllocationSites-03.js
+++ b/js/src/jit-test/tests/debug/Memory-trackingAllocationSites-03.js
@@ -1,64 +1,107 @@
-// Test that multiple Debuggers behave reasonably. Since we're not keeping a
-// per-compartment count of how many Debuggers have requested allocation
-// tracking, assert that attempts to request allocation tracking from multiple
-// debuggers throws.
+// Test that multiple Debuggers behave reasonably.
 
 load(libdir + "asserts.js");
 
-let root1 = newGlobal();
-let root2 = newGlobal();
+let dbg1, dbg2, root1, root2;
 
-let dbg1 = new Debugger();
-let dbg2 = new Debugger();
-
-let d1r1 = dbg1.addDebuggee(root1);
-let d2r1 = dbg2.addDebuggee(root1);
-
-let wrappedObj, allocationSite;
+function FakeMetadata() {}
 
 function isTrackingAllocations(global, dbgObj) {
   const site = dbgObj.makeDebuggeeValue(global.eval("({})")).allocationSite;
   if (site) {
     assertEq(typeof site, "object");
   }
   return !!site;
 }
 
-// Can't track allocations if a different debugger is already tracking them.
-dbg1.memory.trackingAllocationSites = true;
-assertThrowsInstanceOf(() => dbg2.memory.trackingAllocationSites = true,
-                       Error);
+function test(name, fn) {
+  print();
+  print(name);
 
-// Removing root as a debuggee from dbg1 should disable the allocation hook.
-dbg1.removeDebuggee(root1);
-assertEq(isTrackingAllocations(root1, d1r1), false);
+  // Reset state.
+  root1 = newGlobal();
+  root2 = newGlobal();
+  dbg1 = new Debugger;
+  dbg2 = new Debugger;
+
+  // Run the test.
+  fn();
+
+  print("  OK");
+}
 
-// Tracking allocations in dbg2 should work now that dbg1 isn't debugging root1.
-dbg2.memory.trackingAllocationSites = true;
-assertEq(isTrackingAllocations(root1, d2r1), true);
+test("Can track allocations even if a different debugger is already tracking " +
+     "them.",
+     () => {
+       let d1r1 = dbg1.addDebuggee(root1);
+       let d2r1 = dbg2.addDebuggee(root1);
+       dbg1.memory.trackingAllocationSites = true;
+       dbg2.memory.trackingAllocationSites = true;
+       assertEq(isTrackingAllocations(root1, d1r1), true);
+       assertEq(isTrackingAllocations(root1, d2r1), true);
+     });
 
-// Adding root back as a debuggee in dbg1 should fail now because it will
-// attempt to track allocations in root, but dbg2 is already doing that.
-assertThrowsInstanceOf(() => dbg1.addDebuggee(root1),
-                       Error);
-assertEq(dbg1.hasDebuggee(root1), false);
+test("Removing root1 as a debuggee from all debuggers should disable the " +
+     "allocation hook.",
+     () => {
+       dbg1.memory.trackingAllocationSites = true;
+       let d1r1 = dbg1.addDebuggee(root1);
+       dbg1.removeAllDebuggees();
+       assertEq(isTrackingAllocations(root1, d1r1), false);
+     });
+
+test("Adding a new debuggee to a debugger that is tracking allocations should " +
+     "enable the hook for the new debuggee.",
+     () => {
+       dbg1.memory.trackingAllocationSites = true;
+       let d1r1 = dbg1.addDebuggee(root1);
+       assertEq(isTrackingAllocations(root1, d1r1), true);
+     });
 
-// Adding a new debuggee to a debugger that is tracking allocations should
-// enable the hook for the new debuggee.
-dbg2.removeDebuggee(root1);
-d1r1 = dbg1.addDebuggee(root1);
-assertEq(isTrackingAllocations(root1, d1r1), true);
+test("Setting trackingAllocationSites to true should throw if the debugger " +
+     "cannot install the allocation hooks for *every* debuggee.",
+     () => {
+       let d1r1 = dbg1.addDebuggee(root1);
+       let d1r2 = dbg1.addDebuggee(root2);
+
+       // Can't install allocation hooks for root2 with this set.
+       root2.setObjectMetadataCallback(function () { return new FakeMetadata; });
+
+       assertThrowsInstanceOf(() => dbg1.memory.trackingAllocationSites = true,
+                              Error);
+
+       // And after it throws, its trackingAllocationSites accessor should reflect that
+       // allocation site tracking is still disabled in that Debugger.
+       assertEq(dbg1.memory.trackingAllocationSites, false);
+       assertEq(isTrackingAllocations(root1, d1r1), false);
+       assertEq(isTrackingAllocations(root2, d1r2), false);
+     });
+
+test("A Debugger isn't tracking allocation sites when disabled.",
+     () => {
+       dbg1.memory.trackingAllocationSites = true;
+       let d1r1 = dbg1.addDebuggee(root1);
 
-// Setting trackingAllocationSites to true should throw if the debugger cannot
-// install the allocation hooks for *every* debuggee.
-dbg1.memory.trackingAllocationSites = true;
-dbg1.addDebuggee(root1);
-dbg2.memory.trackingAllocationSites = false;
-let d2r2 = dbg2.addDebuggee(root2);
-dbg2.addDebuggee(root1);
-assertThrowsInstanceOf(() => dbg2.memory.trackingAllocationSites = true,
-                       Error);
+       assertEq(isTrackingAllocations(root1, d1r1), true);
+       dbg1.enabled = false;
+       assertEq(isTrackingAllocations(root1, d1r1), false);
+     });
 
-// And after it throws, its trackingAllocationSites accessor should reflect that
-// allocation site tracking is still disabled in that Debugger.
-assertEq(isTrackingAllocations(root2, d2r2), false);
+test("Re-enabling throws an error if we can't reinstall allocations tracking " +
+     "for all debuggees.",
+     () => {
+       dbg1.enabled = false
+       dbg1.memory.trackingAllocationSites = true;
+       let d1r1 = dbg1.addDebuggee(root1);
+       let d1r2 = dbg1.addDebuggee(root2);
+
+       // Can't install allocation hooks for root2 with this set.
+       root2.setObjectMetadataCallback(function () { return new FakeMetadata; });
+
+       assertThrowsInstanceOf(() => dbg1.enabled = true,
+                              Error);
+
+       assertEq(dbg1.enabled, false);
+       assertEq(isTrackingAllocations(root1, d1r1), false);
+       assertEq(isTrackingAllocations(root2, d1r2), false);
+     });
--- a/js/src/jscompartment.h
+++ b/js/src/jscompartment.h
@@ -430,16 +430,17 @@ struct JSCompartment
     void purge();
     void clearTables();
 
     void fixupInitialShapeTable();
     void fixupAfterMovingGC();
     void fixupGlobal();
 
     bool hasObjectMetadataCallback() const { return objectMetadataCallback; }
+    js::ObjectMetadataCallback getObjectMetadataCallback() const { return objectMetadataCallback; }
     void setObjectMetadataCallback(js::ObjectMetadataCallback callback);
     void forgetObjectMetadataCallback() {
         objectMetadataCallback = nullptr;
     }
     void setNewObjectMetadata(JSContext* cx, JSObject* obj);
     void clearObjectMetadata();
     const void* addressOfMetadataCallback() const {
         return &objectMetadataCallback;
--- a/js/src/vm/Debugger.cpp
+++ b/js/src/vm/Debugger.cpp
@@ -1676,16 +1676,18 @@ Debugger::AllocationSite::create(JSConte
     return allocSite;
 }
 
 
 bool
 Debugger::appendAllocationSite(JSContext* cx, HandleObject obj, HandleSavedFrame frame,
                                int64_t when)
 {
+    MOZ_ASSERT(trackingAllocationSites);
+
     AutoCompartment ac(cx, object);
     RootedObject wrappedFrame(cx, frame);
     if (!cx->compartment()->wrap(cx, &wrappedFrame))
         return false;
 
     AllocationSite* allocSite = AllocationSite::create(cx, wrappedFrame, when, obj);
     if (!allocSite)
         return false;
@@ -2146,16 +2148,106 @@ Debugger::updateObservesAsmJSOnDebuggees
             continue;
 
         comp->updateDebuggerObservesAsmJS();
     }
 }
 
 
 
+/*** Allocations Tracking *************************************************************************/
+
+/* static */ bool
+Debugger::cannotTrackAllocations(const GlobalObject& global)
+{
+    auto existingCallback = global.compartment()->getObjectMetadataCallback();
+    return existingCallback && existingCallback != SavedStacksMetadataCallback;
+}
+
+/* static */ bool
+Debugger::isObservedByDebuggerTrackingAllocations(const GlobalObject& debuggee)
+{
+    if (auto* v = debuggee.getDebuggers()) {
+        Debugger** p;
+        for (p = v->begin(); p != v->end(); p++) {
+            if ((*p)->trackingAllocationSites) {
+                return true;
+            }
+        }
+    }
+
+    return false;
+}
+
+/* static */ bool
+Debugger::addAllocationsTracking(JSContext* cx, GlobalObject& debuggee)
+{
+    // Precondition: the given global object is being observed by at least one
+    // Debugger that is tracking allocations.
+    MOZ_ASSERT(isObservedByDebuggerTrackingAllocations(debuggee));
+
+    if (Debugger::cannotTrackAllocations(debuggee)) {
+        JS_ReportErrorNumber(cx, GetErrorMessage, nullptr,
+                             JSMSG_OBJECT_METADATA_CALLBACK_ALREADY_SET);
+        return false;
+    }
+
+    debuggee.compartment()->setObjectMetadataCallback(SavedStacksMetadataCallback);
+    return true;
+}
+
+/* static */ void
+Debugger::removeAllocationsTracking(GlobalObject& global)
+{
+    // If there are still Debuggers that are observing allocations, we cannot
+    // remove the metadata callback yet.
+    if (isObservedByDebuggerTrackingAllocations(global))
+        return;
+
+    global.compartment()->forgetObjectMetadataCallback();
+}
+
+bool
+Debugger::addAllocationsTrackingForAllDebuggees(JSContext* cx)
+{
+    MOZ_ASSERT(trackingAllocationSites);
+
+    // We don't want to end up in a state where we added allocations
+    // tracking to some of our debuggees, but failed to do so for
+    // others. Before attempting to start tracking allocations in *any* of
+    // our debuggees, ensure that we will be able to track allocations for
+    // *all* of our debuggees.
+    for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty(); r.popFront()) {
+        if (Debugger::cannotTrackAllocations(*r.front().get())) {
+            JS_ReportErrorNumber(cx, GetErrorMessage, nullptr,
+                                 JSMSG_OBJECT_METADATA_CALLBACK_ALREADY_SET);
+            return false;
+        }
+    }
+
+    for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty(); r.popFront()) {
+        // This should always succeed, since we already checked for the
+        // error case above.
+        MOZ_ALWAYS_TRUE(Debugger::addAllocationsTracking(cx, *r.front().get()));
+    }
+
+    return true;
+}
+
+void
+Debugger::removeAllocationsTrackingForAllDebuggees()
+{
+    for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty(); r.popFront()) {
+        Debugger::removeAllocationsTracking(*r.front().get());
+    }
+    emptyAllocationsLog();
+}
+
+
+
 /*** Debugger JSObjects **************************************************************************/
 
 void
 Debugger::markCrossCompartmentEdges(JSTracer* trc)
 {
     objects.markCrossCompartmentEdges<DebuggerObject_trace>(trc);
     environments.markCrossCompartmentEdges<DebuggerEnv_trace>(trc);
     scripts.markCrossCompartmentEdges<DebuggerScript_trace>(trc);
@@ -2484,16 +2576,27 @@ Debugger::setEnabled(JSContext* cx, unsi
     THIS_DEBUGGER(cx, argc, vp, "set enabled", args, dbg);
     if (!args.requireAtLeast(cx, "Debugger.set enabled", 1))
         return false;
 
     bool wasEnabled = dbg->enabled;
     dbg->enabled = ToBoolean(args[0]);
 
     if (wasEnabled != dbg->enabled) {
+        if (dbg->trackingAllocationSites) {
+            if (wasEnabled) {
+                dbg->removeAllocationsTrackingForAllDebuggees();
+            } else {
+                if (!dbg->addAllocationsTrackingForAllDebuggees(cx)) {
+                    dbg->enabled = false;
+                    return false;
+                }
+            }
+        }
+
         for (Breakpoint* bp = dbg->firstBreakpoint(); bp; bp = bp->nextInDebugger()) {
             if (!wasEnabled)
                 bp->site->inc(cx->runtime()->defaultFreeOp());
             else
                 bp->site->dec(cx->runtime()->defaultFreeOp());
         }
 
         /*
@@ -3072,65 +3175,49 @@ Debugger::addDebuggeeGlobal(JSContext* c
                 JSCompartment* next = (*p)->object->compartment();
                 if (Find(visited, next) == visited.end() && !visited.append(next))
                     return false;
             }
         }
     }
 
     /*
-     * If we are tracking allocation sites, we need to add the object metadata
-     * callback to this debuggee compartment.
-     */
-    bool setMetadataCallback = false;
-    if (trackingAllocationSites) {
-        if (debuggeeCompartment->hasObjectMetadataCallback()) {
-            JS_ReportErrorNumber(cx, GetErrorMessage, nullptr,
-                                 JSMSG_OBJECT_METADATA_CALLBACK_ALREADY_SET);
-            return false;
-        }
-
-        debuggeeCompartment->setObjectMetadataCallback(SavedStacksMetadataCallback);
-        setMetadataCallback = true;
-    }
-
-    /*
      * For global to become this js::Debugger's debuggee:
      * - global must be in this->debuggees,
      * - this js::Debugger must be in global->getDebuggers(), and
      * - JSCompartment::isDebuggee()'s bit must be set.
-     * All three indications must be kept consistent.
+     * - If we are tracking allocations, the SavedStacksMetadataCallback must be
+     *   installed for this compartment.
+     * All four indications must be kept consistent.
      */
     AutoCompartment ac(cx, global);
     GlobalObject::DebuggerVector* v = GlobalObject::getOrCreateDebuggers(cx, global);
     if (!v || !v->append(this)) {
         ReportOutOfMemory(cx);
     } else {
         if (!debuggees.put(global)) {
             ReportOutOfMemory(cx);
         } else {
-            debuggeeCompartment->setIsDebuggee();
-            debuggeeCompartment->updateDebuggerObservesAsmJS();
-            if (!observesAllExecution())
-                return true;
-            if (ensureExecutionObservabilityOfCompartment(cx, debuggeeCompartment))
-                return true;
+            if (!trackingAllocationSites || Debugger::addAllocationsTracking(cx, *global)) {
+                debuggeeCompartment->setIsDebuggee();
+                debuggeeCompartment->updateDebuggerObservesAsmJS();
+                if (!observesAllExecution())
+                    return true;
+                if (ensureExecutionObservabilityOfCompartment(cx, debuggeeCompartment))
+                    return true;
+            }
 
             /* Maintain consistency on error. */
             debuggees.remove(global);
         }
 
         MOZ_ASSERT(v->back() == this);
         v->popBack();
     }
 
-    /* Don't leave the object metadata hook set if we OOM'd. */
-    if (setMetadataCallback)
-        debuggeeCompartment->forgetObjectMetadataCallback();
-
     return false;
 }
 
 void
 Debugger::removeDebuggeeGlobal(FreeOp* fop, GlobalObject* global,
                                WeakGlobalObjectSet::Enum* debugEnum)
 {
     /*
@@ -3187,29 +3274,27 @@ Debugger::removeDebuggeeGlobal(FreeOp* f
     }
     MOZ_ASSERT_IF(debuggees.empty(), !firstBreakpoint());
 
     /*
      * If we are tracking allocation sites, we need to remove the object
      * metadata callback from this global's compartment.
      */
     if (trackingAllocationSites)
-        global->compartment()->forgetObjectMetadataCallback();
-
-    // Clear out all object metadata in the compartment.
-    global->compartment()->clearObjectMetadata();
+        Debugger::removeAllocationsTracking(*global);
 
     if (global->getDebuggers()->empty()) {
         global->compartment()->unsetIsDebuggee();
     } else {
         global->compartment()->updateDebuggerObservesAllExecution();
         global->compartment()->updateDebuggerObservesAsmJS();
     }
 }
 
+
 static inline ScriptSourceObject* GetSourceReferent(JSObject* obj);
 
 /*
  * A class for parsing 'findScripts' query arguments and searching for
  * scripts that match the criteria they represent.
  */
 class MOZ_STACK_CLASS Debugger::ScriptQuery
 {
@@ -6708,24 +6793,38 @@ DebuggerObject_getGlobal(JSContext* cx, 
     RootedValue v(cx, ObjectValue(obj->global()));
     if (!dbg->wrapDebuggeeValue(cx, &v))
         return false;
     args.rval().set(v);
     return true;
 }
 
 static bool
+null(CallArgs& args)
+{
+    args.rval().setNull();
+    return true;
+}
+
+static bool
 DebuggerObject_getAllocationSite(JSContext* cx, unsigned argc, Value* vp)
 {
     THIS_DEBUGOBJECT_REFERENT(cx, argc, vp, "get allocationSite", args, obj);
 
     RootedObject metadata(cx, GetObjectMetadata(obj));
+    if (!metadata)
+        return null(args);
+
+    metadata = CheckedUnwrap(metadata);
+    if (!metadata || !SavedFrame::isSavedFrameAndNotProto(*metadata))
+        return null(args);
+
     if (!cx->compartment()->wrap(cx, &metadata))
         return false;
-    args.rval().setObjectOrNull(metadata);
+    args.rval().setObject(*metadata);
     return true;
 }
 
 static bool
 DebuggerObject_getOwnPropertyDescriptor(JSContext* cx, unsigned argc, Value* vp)
 {
     THIS_DEBUGOBJECT_OWNER_REFERENT(cx, argc, vp, "getOwnPropertyDescriptor", args, dbg, obj);
 
--- a/js/src/vm/Debugger.h
+++ b/js/src/vm/Debugger.h
@@ -184,17 +184,16 @@ typedef JSObject Env;
 class Debugger : private mozilla::LinkedListElement<Debugger>
 {
     friend class Breakpoint;
     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,
@@ -300,16 +299,49 @@ class Debugger : private mozilla::Linked
 
     static const size_t DEFAULT_MAX_ALLOCATIONS_LOG_LENGTH = 5000;
 
     bool appendAllocationSite(JSContext* cx, HandleObject obj, HandleSavedFrame frame,
                               int64_t when);
     void emptyAllocationsLog();
 
     /*
+     * Return true if there is an existing object metadata callback for the
+     * given global's compartment that will prevent our instrumentation of
+     * allocations.
+     */
+    static bool cannotTrackAllocations(const GlobalObject& global);
+
+    /*
+     * Return true if the given global is being observed by at least one
+     * Debugger that is tracking allocations.
+     */
+    static bool isObservedByDebuggerTrackingAllocations(const GlobalObject& global);
+
+    /*
+     * Add allocations tracking for objects allocated within the given
+     * debuggee's compartment. The given debuggee global must be observed by at
+     * least one Debugger that is enabled and tracking allocations.
+     */
+    static bool addAllocationsTracking(JSContext* cx, GlobalObject& debuggee);
+
+    /*
+     * Remove allocations tracking for objects allocated within the given
+     * global's compartment. This is a no-op if there are still Debuggers
+     * observing this global and who are tracking allocations.
+     */
+    static void removeAllocationsTracking(GlobalObject& global);
+
+    /*
+     * Add or remove allocations tracking for all debuggees.
+     */
+    bool addAllocationsTrackingForAllDebuggees(JSContext* cx);
+    void removeAllocationsTrackingForAllDebuggees();
+
+    /*
      * If this Debugger is enabled, and has a onNewGlobalObject handler, then
      * this link is inserted into the circular list headed by
      * JSRuntime::onNewGlobalObjectWatchers. Otherwise, this is set to a
      * singleton cycle.
      */
     JSCList onNewGlobalObjectWatchersLink;
 
     /*
--- a/js/src/vm/DebuggerMemory.cpp
+++ b/js/src/vm/DebuggerMemory.cpp
@@ -112,61 +112,55 @@ DebuggerMemory::checkThis(JSContext* cx,
  * - Value* vp
  * - const char* fnName
  * These parameters will be defined after calling this macro:
  * - CallArgs args
  * - DebuggerMemory* memory (will be non-null)
  */
 #define THIS_DEBUGGER_MEMORY(cx, argc, vp, fnName, args, memory)        \
     CallArgs args = CallArgsFromVp(argc, vp);                           \
-    Rooted<DebuggerMemory*> memory(cx, checkThis(cx, args, fnName));   \
+    Rooted<DebuggerMemory*> memory(cx, checkThis(cx, args, fnName));    \
     if (!memory)                                                        \
         return false
 
+static bool
+undefined(CallArgs& args)
+{
+    args.rval().setUndefined();
+    return true;
+}
+
 /* static */ bool
 DebuggerMemory::setTrackingAllocationSites(JSContext* cx, unsigned argc, Value* vp)
 {
     THIS_DEBUGGER_MEMORY(cx, argc, vp, "(set trackingAllocationSites)", args, memory);
     if (!args.requireAtLeast(cx, "(set trackingAllocationSites)", 1))
         return false;
 
     Debugger* dbg = memory->getDebugger();
     bool enabling = ToBoolean(args[0]);
 
-    if (enabling == dbg->trackingAllocationSites) {
-        // Nothing to do here...
-        args.rval().setUndefined();
-        return true;
-    }
+    if (enabling == dbg->trackingAllocationSites)
+        return undefined(args);
+
+    dbg->trackingAllocationSites = enabling;
+
+    if (!dbg->enabled)
+        return undefined(args);
 
     if (enabling) {
-        for (WeakGlobalObjectSet::Range r = dbg->debuggees.all(); !r.empty(); r.popFront()) {
-            JSCompartment* compartment = r.front()->compartment();
-            if (compartment->hasObjectMetadataCallback()) {
-                JS_ReportErrorNumber(cx, GetErrorMessage, nullptr,
-                                     JSMSG_OBJECT_METADATA_CALLBACK_ALREADY_SET);
-                return false;
-            }
+        if (!dbg->addAllocationsTrackingForAllDebuggees(cx)) {
+            dbg->trackingAllocationSites = false;
+            return false;
         }
+    } else {
+        dbg->removeAllocationsTrackingForAllDebuggees();
     }
 
-    for (WeakGlobalObjectSet::Range r = dbg->debuggees.all(); !r.empty(); r.popFront()) {
-        if (enabling) {
-            r.front()->compartment()->setObjectMetadataCallback(SavedStacksMetadataCallback);
-        } else {
-            r.front()->compartment()->forgetObjectMetadataCallback();
-        }
-    }
-
-    if (!enabling)
-        dbg->emptyAllocationsLog();
-
-    dbg->trackingAllocationSites = enabling;
-    args.rval().setUndefined();
-    return true;
+    return undefined(args);
 }
 
 /* static */ bool
 DebuggerMemory::getTrackingAllocationSites(JSContext* cx, unsigned argc, Value* vp)
 {
     THIS_DEBUGGER_MEMORY(cx, argc, vp, "(get trackingAllocationSites)", args, memory);
     args.rval().setBoolean(memory->getDebugger()->trackingAllocationSites);
     return true;
--- a/js/src/vm/GlobalObject.cpp
+++ b/js/src/vm/GlobalObject.cpp
@@ -506,17 +506,17 @@ GlobalDebuggees_finalize(FreeOp* fop, JS
 static const Class
 GlobalDebuggees_class = {
     "GlobalDebuggee", JSCLASS_HAS_PRIVATE,
     nullptr, nullptr, nullptr, nullptr, nullptr,
     nullptr, nullptr, nullptr, GlobalDebuggees_finalize
 };
 
 GlobalObject::DebuggerVector*
-GlobalObject::getDebuggers()
+GlobalObject::getDebuggers() const
 {
     Value debuggers = getReservedSlot(DEBUGGERS);
     if (debuggers.isUndefined())
         return nullptr;
     MOZ_ASSERT(debuggers.toObject().getClass() == &GlobalDebuggees_class);
     return (DebuggerVector*) debuggers.toObject().as<NativeObject>().getPrivate();
 }
 
--- a/js/src/vm/GlobalObject.h
+++ b/js/src/vm/GlobalObject.h
@@ -685,17 +685,17 @@ class GlobalObject : public NativeObject
                                         const JSFunctionSpec* builtins);
 
     typedef js::Vector<js::Debugger*, 0, js::SystemAllocPolicy> DebuggerVector;
 
     /*
      * The collection of Debugger objects debugging this global. If this global
      * is not a debuggee, this returns either nullptr or an empty vector.
      */
-    DebuggerVector* getDebuggers();
+    DebuggerVector* getDebuggers() const;
 
     /*
      * The same, but create the empty vector if one does not already
      * exist. Returns nullptr only on OOM.
      */
     static DebuggerVector* getOrCreateDebuggers(JSContext* cx, Handle<GlobalObject*> global);
 
     inline NativeObject* getForOfPICObject() {
--- a/js/src/vm/SavedStacks.cpp
+++ b/js/src/vm/SavedStacks.cpp
@@ -5,16 +5,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "vm/SavedStacks.h"
 
 #include "mozilla/Attributes.h"
 #include "mozilla/DebugOnly.h"
 #include "mozilla/Maybe.h"
 
+#include <algorithm>
 #include <math.h>
 
 #include "jsapi.h"
 #include "jscompartment.h"
 #include "jsfriendapi.h"
 #include "jshashutil.h"
 #include "jsmath.h"
 #include "jsnum.h"
@@ -1157,32 +1158,29 @@ SavedStacks::getLocation(JSContext* cx, 
 
 void
 SavedStacks::chooseSamplingProbability(JSContext* cx)
 {
     GlobalObject::DebuggerVector* dbgs = cx->global()->getDebuggers();
     if (!dbgs || dbgs->empty())
         return;
 
-    Debugger* allocationTrackingDbg = nullptr;
     mozilla::DebugOnly<Debugger**> begin = dbgs->begin();
 
+    allocationSamplingProbability = 0;
     for (Debugger** dbgp = dbgs->begin(); dbgp < dbgs->end(); dbgp++) {
         // The set of debuggers had better not change while we're iterating,
         // such that the vector gets reallocated.
         MOZ_ASSERT(dbgs->begin() == begin);
 
-        if ((*dbgp)->trackingAllocationSites && (*dbgp)->enabled)
-            allocationTrackingDbg = *dbgp;
+        if ((*dbgp)->trackingAllocationSites && (*dbgp)->enabled) {
+            allocationSamplingProbability = std::max((*dbgp)->allocationSamplingProbability,
+                                                     allocationSamplingProbability);
+        }
     }
-
-    if (!allocationTrackingDbg)
-        return;
-
-    allocationSamplingProbability = allocationTrackingDbg->allocationSamplingProbability;
 }
 
 JSObject*
 SavedStacksMetadataCallback(JSContext* cx, JSObject* target)
 {
     RootedObject obj(cx, target);
 
     SavedStacks& stacks = cx->compartment()->savedStacks();