Bug 1308039 - GC interrupt callbacks (r=jonco)
authorBill McCloskey <billm@mozilla.com>
Thu, 06 Oct 2016 16:44:58 -0700
changeset 423586 2fc4ea870154988aaabb3df7826e2617b8370062
parent 423585 8aa8395f2e34a25270bce6a0a5ac87ca906122ed
child 423587 3d42c7605d818d6e277d570a8fc4309cf5019ee1
push id31945
push users.kaspari@gmail.com
push dateTue, 11 Oct 2016 12:27:19 +0000
reviewersjonco
bugs1308039
milestone52.0a1
Bug 1308039 - GC interrupt callbacks (r=jonco)
js/public/GCAPI.h
js/public/SliceBudget.h
js/src/gc/GCRuntime.h
js/src/gc/Marking.cpp
js/src/jsgc.cpp
--- a/js/public/GCAPI.h
+++ b/js/public/GCAPI.h
@@ -699,11 +699,23 @@ extern JS_FRIEND_API(void)
 PokeGC(JSContext* cx);
 
 /*
  * Internal to Firefox.
  */
 extern JS_FRIEND_API(void)
 NotifyDidPaint(JSContext* cx);
 
+// GC Interrupt callbacks are run during GC. You should not run JS code or use
+// the JS engine at all while the callback is running. Otherwise they resemble
+// normal JS interrupt callbacks.
+typedef bool
+(* GCInterruptCallback)(JSContext* cx);
+
+extern JS_FRIEND_API(bool)
+AddGCInterruptCallback(JSContext* cx, GCInterruptCallback callback);
+
+extern JS_FRIEND_API(void)
+RequestGCInterruptCallback(JSContext* cx);
+
 } /* namespace JS */
 
 #endif /* js_GCAPI_h */
--- a/js/public/SliceBudget.h
+++ b/js/public/SliceBudget.h
@@ -2,16 +2,18 @@
  * 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_SliceBudget_h
 #define js_SliceBudget_h
 
+#include "mozilla/Atomics.h"
+
 #include <stdint.h>
 
 namespace js {
 
 struct JS_PUBLIC_API(TimeBudget)
 {
     int64_t budget;
 
@@ -31,57 +33,82 @@ struct JS_PUBLIC_API(WorkBudget)
  * to run for unlimited time, and others are bounded. To reduce the number of
  * gettimeofday calls, we only check the time every 1000 operations.
  */
 class JS_PUBLIC_API(SliceBudget)
 {
     static const int64_t unlimitedDeadline = INT64_MAX;
     static const intptr_t unlimitedStartCounter = INTPTR_MAX;
 
-    bool checkOverBudget();
+    bool checkOverBudget(JSContext* maybeCx);
 
     SliceBudget();
 
   public:
     // Memory of the originally requested budget. If isUnlimited, neither of
     // these are in use. If deadline==0, then workBudget is valid. Otherwise
     // timeBudget is valid.
     TimeBudget timeBudget;
     WorkBudget workBudget;
 
     int64_t deadline; /* in microseconds */
-    intptr_t counter;
+    mozilla::Atomic<intptr_t, mozilla::Relaxed> counter;
 
     static const intptr_t CounterReset = 1000;
 
     static const int64_t UnlimitedTimeBudget = -1;
     static const int64_t UnlimitedWorkBudget = -1;
 
     /* Use to create an unlimited budget. */
     static SliceBudget unlimited() { return SliceBudget(); }
 
     /* Instantiate as SliceBudget(TimeBudget(n)). */
     explicit SliceBudget(TimeBudget time);
 
     /* Instantiate as SliceBudget(WorkBudget(n)). */
     explicit SliceBudget(WorkBudget work);
 
+    // Need an explicit copy constructor because Atomic fails to provide one.
+    SliceBudget(const SliceBudget& other)
+        : timeBudget(other.timeBudget),
+          workBudget(other.workBudget),
+          deadline(other.deadline),
+          counter(other.counter)
+    {}
+
+    // Need an explicit operator= because Atomic fails to provide one.
+    SliceBudget& operator=(const SliceBudget& other) {
+        timeBudget = other.timeBudget;
+        workBudget = other.workBudget;
+        deadline = other.deadline;
+        counter = intptr_t(other.counter);
+        return *this;
+    }
+
     void makeUnlimited() {
         deadline = unlimitedDeadline;
         counter = unlimitedStartCounter;
     }
 
+    // Request that checkOverBudget be called the next time isOverBudget is
+    // called.
+    void requestFullCheck() {
+        counter = 0;
+    }
+
     void step(intptr_t amt = 1) {
         counter -= amt;
     }
 
-    bool isOverBudget() {
+    // Only need to pass maybeCx if the GC interrupt callback should be checked
+    // (and possibly invoked).
+    bool isOverBudget(JSContext* maybeCx = nullptr) {
         if (counter > 0)
             return false;
-        return checkOverBudget();
+        return checkOverBudget(maybeCx);
     }
 
     bool isWorkBudget() const { return deadline == 0; }
     bool isTimeBudget() const { return deadline > 0 && !isUnlimited(); }
     bool isUnlimited() const { return deadline == unlimitedDeadline; }
 
     int describe(char* buffer, size_t maxlen) const;
 };
--- a/js/src/gc/GCRuntime.h
+++ b/js/src/gc/GCRuntime.h
@@ -19,16 +19,17 @@
 #include "gc/StoreBuffer.h"
 #include "gc/Tracer.h"
 #include "js/GCAnnotations.h"
 
 namespace js {
 
 class AutoLockGC;
 class AutoLockHelperThreadState;
+class SliceBudget;
 class VerifyPreTracer;
 
 namespace gc {
 
 typedef Vector<JS::Zone*, 4, SystemAllocPolicy> ZoneVector;
 using BlackGrayEdgeVector = Vector<TenuredCell*, 0, SystemAllocPolicy>;
 
 class AutoMaybeStartBackgroundAllocation;
@@ -858,16 +859,29 @@ class GCRuntime
     void startVerifyPreBarriers();
     void endVerifyPreBarriers();
     void finishVerifier();
     bool isVerifyPreBarriersEnabled() const { return !!verifyPreData; }
 #else
     bool isVerifyPreBarriersEnabled() const { return false; }
 #endif
 
+    // GC interrupt callbacks.
+    bool addInterruptCallback(JS::GCInterruptCallback callback);
+    void requestInterruptCallback();
+
+    bool checkInterruptCallback(JSContext* cx) {
+        if (interruptCallbackRequested) {
+            invokeInterruptCallback(cx);
+            return true;
+        }
+        return false;
+    }
+    void invokeInterruptCallback(JSContext* cx);
+
     // Free certain LifoAlloc blocks when it is safe to do so.
     void freeUnusedLifoBlocksAfterSweeping(LifoAlloc* lifo);
     void freeAllLifoBlocksAfterSweeping(LifoAlloc* lifo);
     void freeAllLifoBlocksAfterMinorGC(LifoAlloc* lifo);
 
     // Queue a thunk to run after the next minor GC.
     void callAfterMinorGC(void (*thunk)(void* data), void* data) {
         nursery.queueSweepAction(thunk, data);
@@ -1070,16 +1084,23 @@ class GCRuntime
     mozilla::Atomic<uint64_t, mozilla::ReleaseAcquire> nextCellUniqueId_;
 
     /*
      * Number of the committed arenas in all GC chunks including empty chunks.
      */
     mozilla::Atomic<uint32_t, mozilla::ReleaseAcquire> numArenasFreeCommitted;
     VerifyPreTracer* verifyPreData;
 
+    // GC interrupt callbacks.
+    using GCInterruptCallbackVector = js::Vector<JS::GCInterruptCallback, 2, js::SystemAllocPolicy>;
+    GCInterruptCallbackVector interruptCallbacks;
+
+    mozilla::Atomic<bool, mozilla::Relaxed> interruptCallbackRequested;
+    SliceBudget* currentBudget;
+
   private:
     bool chunkAllocationSinceLastGC;
     int64_t lastGCTime;
 
     JSGCMode mode;
 
     mozilla::Atomic<size_t, mozilla::ReleaseAcquire> numActiveZoneIters;
 
--- a/js/src/gc/Marking.cpp
+++ b/js/src/gc/Marking.cpp
@@ -1540,23 +1540,25 @@ bool
 GCMarker::drainMarkStack(SliceBudget& budget)
 {
 #ifdef DEBUG
     MOZ_ASSERT(!strictCompartmentChecking);
     strictCompartmentChecking = true;
     auto acc = mozilla::MakeScopeExit([&] {strictCompartmentChecking = false;});
 #endif
 
-    if (budget.isOverBudget())
+    JSContext* cx = runtime()->contextFromMainThread();
+
+    if (budget.isOverBudget(cx))
         return false;
 
     for (;;) {
         while (!stack.isEmpty()) {
             processMarkStackTop(budget);
-            if (budget.isOverBudget()) {
+            if (budget.isOverBudget(cx)) {
                 saveValueRanges();
                 return false;
             }
         }
 
         if (!hasDelayedChildren())
             break;
 
@@ -1621,16 +1623,18 @@ GCMarker::processMarkStackTop(SliceBudge
     HeapSlot* end;
     JSObject* obj;
 
     // Decode
     uintptr_t addr = stack.pop();
     uintptr_t tag = addr & StackTagMask;
     addr &= ~StackTagMask;
 
+    JSContext* cx = runtime()->contextFromMainThread();
+
     // Dispatch
     switch (tag) {
       case ValueArrayTag: {
         JS_STATIC_ASSERT(ValueArrayTag == 0);
         MOZ_ASSERT(!(addr & CellMask));
         obj = reinterpret_cast<JSObject*>(addr);
         uintptr_t addr2 = stack.pop();
         uintptr_t addr3 = stack.pop();
@@ -1674,17 +1678,17 @@ GCMarker::processMarkStackTop(SliceBudge
       default: MOZ_CRASH("Invalid tag in mark stack");
     }
     return;
 
   scan_value_array:
     MOZ_ASSERT(vp <= end);
     while (vp != end) {
         budget.step();
-        if (budget.isOverBudget()) {
+        if (budget.isOverBudget(cx)) {
             pushValueArray(obj, vp, end);
             return;
         }
 
         const Value& v = *vp++;
         if (v.isString()) {
             traverseEdge(obj, v.toString());
         } else if (v.isObject()) {
@@ -1704,17 +1708,17 @@ GCMarker::processMarkStackTop(SliceBudge
     }
     return;
 
   scan_obj:
     {
         AssertZoneIsMarking(obj);
 
         budget.step();
-        if (budget.isOverBudget()) {
+        if (budget.isOverBudget(cx)) {
             repush(obj);
             return;
         }
 
         markImplicitEdges(obj);
         ObjectGroup* group = obj->groupFromGC();
         traverseEdge(obj, group);
 
@@ -2152,17 +2156,17 @@ GCMarker::markDelayedChildren(SliceBudge
         unmarkedArenaStackTop = arena->getNextDelayedMarking();
         arena->unsetDelayedMarking();
 #ifdef DEBUG
         markLaterArenas--;
 #endif
         markDelayedChildren(arena);
 
         budget.step(150);
-        if (budget.isOverBudget())
+        if (budget.isOverBudget(runtime()->contextFromMainThread()))
             return false;
     } while (unmarkedArenaStackTop);
     MOZ_ASSERT(!markLaterArenas);
 
     return true;
 }
 
 template<typename T>
--- a/js/src/jsgc.cpp
+++ b/js/src/jsgc.cpp
@@ -183,16 +183,17 @@
 
 #include "jsgcinlines.h"
 
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/DebugOnly.h"
 #include "mozilla/MacroForEach.h"
 #include "mozilla/MemoryReporting.h"
 #include "mozilla/Move.h"
+#include "mozilla/ScopeExit.h"
 
 #include <ctype.h>
 #include <string.h>
 #ifndef XP_WIN
 # include <sys/mman.h>
 # include <unistd.h>
 #endif
 
@@ -501,30 +502,32 @@ FinalizeTypedArenas(FreeOp* fop,
 
     // During background sweeping free arenas are released later on in
     // sweepBackgroundThings().
     MOZ_ASSERT_IF(!fop->onMainThread(), keepArenas == ArenaLists::KEEP_ARENAS);
 
     size_t thingSize = Arena::thingSize(thingKind);
     size_t thingsPerArena = Arena::thingsPerArena(thingKind);
 
+    JSContext* cx = fop->onMainThread() ? fop->runtime()->contextFromMainThread() : nullptr;
+
     while (Arena* arena = *src) {
         *src = arena->next;
         size_t nmarked = arena->finalize<T>(fop, thingKind, thingSize);
         size_t nfree = thingsPerArena - nmarked;
 
         if (nmarked)
             dest.insertAt(arena, nfree);
         else if (keepArenas == ArenaLists::KEEP_ARENAS)
             arena->chunk()->recycleArena(arena, dest, thingsPerArena);
         else
             fop->runtime()->gc.releaseArena(arena, maybeLock.ref());
 
         budget.step(thingsPerArena);
-        if (budget.isOverBudget())
+        if (budget.isOverBudget(cx))
             return false;
     }
 
     return true;
 }
 
 /*
  * Finalize the list. On return, |al|'s cursor points to the first non-empty
@@ -803,16 +806,18 @@ GCRuntime::GCRuntime(JSRuntime* rt) :
     stats(rt),
     marker(rt),
     usage(nullptr),
     mMemProfiler(rt),
     maxMallocBytes(0),
     nextCellUniqueId_(LargestTaggedNullCellPointer + 1), // Ensure disjoint from null tagged pointers.
     numArenasFreeCommitted(0),
     verifyPreData(nullptr),
+    interruptCallbackRequested(false),
+    currentBudget(nullptr),
     chunkAllocationSinceLastGC(false),
     lastGCTime(PRMJ_Now()),
     mode(JSGC_MODE_INCREMENTAL),
     numActiveZoneIters(0),
     cleanUpEverything(false),
     grayBufferState(GCRuntime::GrayBufferState::Unused),
     grayBitsValid(false),
     majorGCTriggerReason(JS::gcreason::NO_REASON),
@@ -2928,18 +2933,21 @@ SliceBudget::describe(char* buffer, size
         return snprintf(buffer, maxlen, "unlimited");
     else if (isWorkBudget())
         return snprintf(buffer, maxlen, "work(%" PRId64 ")", workBudget.budget);
     else
         return snprintf(buffer, maxlen, "%" PRId64 "ms", timeBudget.budget);
 }
 
 bool
-SliceBudget::checkOverBudget()
-{
+SliceBudget::checkOverBudget(JSContext* cx)
+{
+    if (cx)
+        cx->gc.checkInterruptCallback(cx);
+
     bool over = PRMJ_Now() >= deadline;
     if (!over)
         counter = CounterReset;
     return over;
 }
 
 void
 js::MarkCompartmentActive(InterpreterFrame* fp)
@@ -5476,17 +5484,17 @@ GCRuntime::compactPhase(JS::gcreason::Re
 
         Zone* zone = zonesToMaybeCompact.front();
         MOZ_ASSERT(zone->isGCFinished());
         zone->setGCState(Zone::Compact);
         if (relocateArenas(zone, reason, relocatedArenas, sliceBudget))
             updatePointersToRelocatedCells(zone, lock);
         zone->setGCState(Zone::Finished);
         zonesToMaybeCompact.removeFront();
-        if (sliceBudget.isOverBudget())
+        if (sliceBudget.isOverBudget(rt->contextFromMainThread()))
             break;
     }
 
     if (ShouldProtectRelocatedArenas(reason))
         protectAndHoldArenas(relocatedArenas);
     else
         releaseRelocatedArenas(relocatedArenas);
 
@@ -5882,17 +5890,17 @@ GCRuntime::incrementalCollectSlice(Slice
 
         incrementalState = State::Sweep;
 
         /*
          * This runs to completion, but we don't continue if the budget is
          * now exhasted.
          */
         beginSweepPhase(destroyingRuntime, lock);
-        if (budget.isOverBudget())
+        if (budget.isOverBudget(rt->contextFromMainThread()))
             break;
 
         /*
          * Always yield here when running in incremental multi-slice zeal
          * mode, so RunDebugGC can reset the slice buget.
          */
         if (isIncremental && useZeal && hasZealMode(ZealMode::IncrementalMultipleSlices))
             break;
@@ -6289,16 +6297,21 @@ GCRuntime::collect(bool nonincrementalBy
 {
     // Checks run for each request, even if we do not actually GC.
     checkCanCallAPI();
 
     // Check if we are allowed to GC at this time before proceeding.
     if (!checkIfGCAllowedInCurrentState(reason))
         return;
 
+    currentBudget = &budget;
+    auto guard = mozilla::MakeScopeExit([&] {
+        currentBudget = nullptr;
+    });
+
     AutoTraceLog logGC(TraceLoggerForMainThread(rt), TraceLogger_GC);
     AutoStopVerifyingBarriers av(rt, IsShutdownGC(reason));
     AutoEnqueuePendingParseTasksAfterGC aept(*this);
     AutoScheduleZonesForGC asz(rt);
 
     bool repeat = false;
     do {
         poked = false;
@@ -7663,8 +7676,45 @@ js::gc::Cell::dump(FILE* fp) const
 
 // For use in a debugger.
 void
 js::gc::Cell::dump() const
 {
     dump(stderr);
 }
 #endif
+
+bool
+JS::AddGCInterruptCallback(JSContext* cx, GCInterruptCallback callback)
+{
+    return cx->runtime()->gc.addInterruptCallback(callback);
+}
+
+void
+JS::RequestGCInterruptCallback(JSContext* cx)
+{
+    cx->runtime()->gc.requestInterruptCallback();
+}
+
+bool
+GCRuntime::addInterruptCallback(JS::GCInterruptCallback callback)
+{
+    return interruptCallbacks.append(callback);
+}
+
+void
+GCRuntime::requestInterruptCallback()
+{
+    if (currentBudget) {
+        interruptCallbackRequested = true;
+        currentBudget->requestFullCheck();
+    }
+}
+
+void
+GCRuntime::invokeInterruptCallback(JSContext* cx)
+{
+    JS::AutoAssertOnGC nogc(cx);
+    JS::AutoAssertOnBarrier nobarrier(cx);
+    for (JS::GCInterruptCallback callback : interruptCallbacks) {
+        (*callback)(cx);
+    }
+}