Bug 966646 - Use JS helper threads for GC background sweeping / allocation, r=billm.
authorBrian Hackett <bhackett1024@gmail.com>
Tue, 27 May 2014 09:58:33 -0700
changeset 185448 93dce4b831f3b121a6a332ab7e447c2df1137706
parent 185447 6d1aa5734dd2a15fe102a9c391af22dfdb065928
child 185449 a1b417d564b5a250f4564598cb62cc77f7d9e856
push id26854
push userttaubert@mozilla.com
push dateThu, 29 May 2014 06:33:11 +0000
treeherdermozilla-central@1e712b724d17 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbillm
bugs966646
milestone32.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 966646 - Use JS helper threads for GC background sweeping / allocation, r=billm.
js/src/gc/GCRuntime.h
js/src/jsgc.cpp
js/src/jsgc.h
js/src/jsworkers.cpp
js/src/jsworkers.h
--- a/js/src/gc/GCRuntime.h
+++ b/js/src/gc/GCRuntime.h
@@ -139,28 +139,40 @@ class GCRuntime
 #endif
 
   public:
     // Internal public interface
     void recordNativeStackTop();
 #ifdef JS_THREADSAFE
     void notifyRequestEnd() { conservativeGC.updateForRequestEnd(); }
 #endif
-    bool isBackgroundSweeping() { return helperThread.sweeping(); }
-    void waitBackgroundSweepEnd() { helperThread.waitBackgroundSweepEnd(); }
-    void waitBackgroundSweepOrAllocEnd() { helperThread.waitBackgroundSweepOrAllocEnd(); }
-    void startBackgroundShrink() { helperThread.startBackgroundShrink(); }
-    void freeLater(void *p) { helperThread.freeLater(p); }
+    bool isBackgroundSweeping() { return helperState.isBackgroundSweeping(); }
+    void waitBackgroundSweepEnd() { helperState.waitBackgroundSweepEnd(); }
+    void waitBackgroundSweepOrAllocEnd() { helperState.waitBackgroundSweepOrAllocEnd(); }
+    void startBackgroundShrink() { helperState.startBackgroundShrink(); }
+    void startBackgroundAllocationIfIdle() { helperState.startBackgroundAllocationIfIdle(); }
+    void freeLater(void *p) { helperState.freeLater(p); }
+
 #ifdef DEBUG
-    bool onBackgroundThread() { return helperThread.onBackgroundThread(); }
+
+    bool onBackgroundThread() { return helperState.onBackgroundThread(); }
+
+    bool currentThreadOwnsGCLock() {
+#ifdef JS_THREADSAFE
+        return lockOwner == PR_GetCurrentThread();
+#else
+        return true;
 #endif
+    }
+
+#endif // DEBUG
 
 #ifdef JS_THREADSAFE
     void assertCanLock() {
-        JS_ASSERT(lockOwner != PR_GetCurrentThread());
+        JS_ASSERT(!currentThreadOwnsGCLock());
     }
 #endif
 
     void lockGC() {
 #ifdef JS_THREADSAFE
         PR_Lock(lock);
         JS_ASSERT(!lockOwner);
 #ifdef DEBUG
@@ -198,17 +210,17 @@ class GCRuntime
     void startVerifyPostBarriers();
     bool endVerifyPostBarriers();
     void finishVerifier();
 #endif
 
   private:
     // For ArenaLists::allocateFromArenaInline()
     friend class ArenaLists;
-    Chunk *pickChunk(Zone *zone);
+    Chunk *pickChunk(Zone *zone, AutoMaybeStartBackgroundAllocation &maybeStartBackgroundAllocation);
 
     inline bool wantBackgroundAllocation() const;
 
     bool initGCZeal();
     void requestInterrupt(JS::gcreason::Reason reason);
     bool gcCycle(bool incremental, int64_t budget, JSGCInvocationKind gckind,
                  JS::gcreason::Reason reason);
     void budgetIncrementalGC(int64_t *budget);
@@ -528,25 +540,25 @@ class GCRuntime
   private:
     /* Always preserve JIT code during GCs, for testing. */
     bool                  alwaysPreserveCode;
 
 #ifdef DEBUG
     size_t                noGCOrAllocationCheck;
 #endif
 
-    /* Synchronize GC heap access between main thread and GCHelperThread. */
+    /* Synchronize GC heap access between main thread and GCHelperState. */
     PRLock                *lock;
     mozilla::DebugOnly<PRThread *>   lockOwner;
 
-    js::GCHelperThread helperThread;
+    GCHelperState helperState;
 
     ConservativeGCData conservativeGC;
 
     //friend class js::gc::Chunk; // todo: remove
-    friend class js::GCHelperThread;
+    friend class js::GCHelperState;
     friend class js::gc::MarkingValidator;
 };
 
 } /* namespace gc */
 } /* namespace js */
 
 #endif
--- a/js/src/jsgc.cpp
+++ b/js/src/jsgc.cpp
@@ -955,24 +955,50 @@ Chunk::releaseArena(ArenaHeader *aheader
 inline bool
 GCRuntime::wantBackgroundAllocation() const
 {
     /*
      * To minimize memory waste we do not want to run the background chunk
      * allocation if we have empty chunks or when the runtime needs just few
      * of them.
      */
-    return helperThread.canBackgroundAllocate() &&
+    return helperState.canBackgroundAllocate() &&
            chunkPool.getEmptyCount() == 0 &&
            chunkSet.count() >= 4;
 }
 
+class js::gc::AutoMaybeStartBackgroundAllocation
+{
+  private:
+    JSRuntime *runtime;
+    MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER
+
+  public:
+    AutoMaybeStartBackgroundAllocation(MOZ_GUARD_OBJECT_NOTIFIER_ONLY_PARAM)
+      : runtime(nullptr)
+    {
+        MOZ_GUARD_OBJECT_NOTIFIER_INIT;
+    }
+
+    void tryToStartBackgroundAllocation(JSRuntime *rt) {
+        runtime = rt;
+    }
+
+    ~AutoMaybeStartBackgroundAllocation() {
+        if (runtime && !runtime->currentThreadOwnsInterruptLock()) {
+            AutoLockWorkerThreadState workerLock;
+            AutoLockGC lock(runtime);
+            runtime->gc.startBackgroundAllocationIfIdle();
+        }
+    }
+};
+
 /* The caller must hold the GC lock. */
 Chunk *
-GCRuntime::pickChunk(Zone *zone)
+GCRuntime::pickChunk(Zone *zone, AutoMaybeStartBackgroundAllocation &maybeStartBackgroundAllocation)
 {
     Chunk **listHeadp = GetAvailableChunkList(zone);
     Chunk *chunk = *listHeadp;
     if (chunk)
         return chunk;
 
     chunk = chunkPool.get(rt);
     if (!chunk) {
@@ -981,17 +1007,17 @@ GCRuntime::pickChunk(Zone *zone)
             return nullptr;
         JS_ASSERT(chunk->info.numArenasFreeCommitted == 0);
     }
 
     JS_ASSERT(chunk->unused());
     JS_ASSERT(!chunkSet.has(chunk));
 
     if (wantBackgroundAllocation())
-        helperThread.startBackgroundAllocationIfIdle();
+        maybeStartBackgroundAllocation.tryToStartBackgroundAllocation(rt);
 
     chunkAllocationSinceLastGC = true;
 
     /*
      * FIXME bug 583732 - chunk is newly allocated and cannot be present in
      * the table so using ordinary lookupForAdd is suboptimal here.
      */
     GCChunkSet::AddPtr p = chunkSet.lookupForAdd(chunk);
@@ -1089,17 +1115,17 @@ GCRuntime::GCRuntime(JSRuntime *rt) :
     mallocGCTriggered(false),
     scriptAndCountsVector(nullptr),
     alwaysPreserveCode(false),
 #ifdef DEBUG
     noGCOrAllocationCheck(0),
 #endif
     lock(nullptr),
     lockOwner(nullptr),
-    helperThread(rt)
+    helperState(rt)
 {
 }
 
 #ifdef JS_GC_ZEAL
 
 extern void
 js::SetGCZeal(JSRuntime *rt, uint8_t zeal, uint32_t frequency)
 {
@@ -1186,17 +1212,17 @@ GCRuntime::init(uint32_t maxbytes)
 #endif
 
     if (!chunkSet.init(INITIAL_CHUNK_CAPACITY))
         return false;
 
     if (!rootsHash.init(256))
         return false;
 
-    if (!helperThread.init())
+    if (!helperState.init())
         return false;
 
     /*
      * Separate gcMaxMallocBytes from gcMaxBytes but initialize to maxbytes
      * for default backward API compatibility.
      */
     maxBytes = maxbytes;
     rt->setGCMaxMallocBytes(maxbytes);
@@ -1237,17 +1263,17 @@ GCRuntime::recordNativeStackTop()
 
 void
 GCRuntime::finish()
 {
     /*
      * Wait until the background finalization stops and the helper thread
      * shuts down before we forcefully release any remaining GC memory.
      */
-    helperThread.finish();
+    helperState.finish();
 
 #ifdef JS_GC_ZEAL
     /* Free memory associated with GC verification. */
     finishVerifier();
 #endif
 
     /* Delete all remaining zones. */
     if (rt->gcInitialized) {
@@ -1497,17 +1523,18 @@ ArenaLists::prepareForIncrementalGC(JSRu
 static inline void
 PushArenaAllocatedDuringSweep(JSRuntime *runtime, ArenaHeader *arena)
 {
     arena->setNextAllocDuringSweep(runtime->gc.arenasAllocatedDuringSweep);
     runtime->gc.arenasAllocatedDuringSweep = arena;
 }
 
 inline void *
-ArenaLists::allocateFromArenaInline(Zone *zone, AllocKind thingKind)
+ArenaLists::allocateFromArenaInline(Zone *zone, AllocKind thingKind,
+                                    AutoMaybeStartBackgroundAllocation &maybeStartBackgroundAllocation)
 {
     /*
      * Parallel JS Note:
      *
      * This function can be called from parallel threads all of which
      * are associated with the same compartment. In that case, each
      * thread will have a distinct ArenaLists.  Therefore, whenever we
      * fall through to pickChunk() we must be sure that we are holding
@@ -1570,17 +1597,17 @@ ArenaLists::allocateFromArenaInline(Zone
         JS_ASSERT(thing);   // This allocation is infallible.
         return thing;
     }
 
     /* Make sure we hold the GC lock before we call pickChunk. */
     JSRuntime *rt = zone->runtimeFromAnyThread();
     if (!maybeLock.locked())
         maybeLock.lock(rt);
-    Chunk *chunk = rt->gc.pickChunk(zone);
+    Chunk *chunk = rt->gc.pickChunk(zone, maybeStartBackgroundAllocation);
     if (!chunk)
         return nullptr;
 
     /*
      * While we still hold the GC lock get an arena from some chunk, mark it
      * as full as its single free span is moved to the free lits, and insert
      * it to the list as a fully allocated arena.
      *
@@ -1615,17 +1642,18 @@ ArenaLists::allocateFromArenaInline(Zone
     fullSpan.initFinal(arena->thingsStart(thingKind), arena->thingsEnd() - thingSize, thingSize);
     freeLists[thingKind].setHead(&fullSpan);
     return freeLists[thingKind].allocate(thingSize);
 }
 
 void *
 ArenaLists::allocateFromArena(JS::Zone *zone, AllocKind thingKind)
 {
-    return allocateFromArenaInline(zone, thingKind);
+    AutoMaybeStartBackgroundAllocation maybeStartBackgroundAllocation;
+    return allocateFromArenaInline(zone, thingKind, maybeStartBackgroundAllocation);
 }
 
 void
 ArenaLists::wipeDuringParallelExecution(JSRuntime *rt)
 {
     JS_ASSERT(InParallelSection());
 
     // First, check that we all objects we have allocated are eligible
@@ -1690,17 +1718,17 @@ ArenaLists::queueForForegroundSweep(Free
 }
 
 inline void
 ArenaLists::queueForBackgroundSweep(FreeOp *fop, AllocKind thingKind)
 {
     JS_ASSERT(IsBackgroundFinalized(thingKind));
 
 #ifdef JS_THREADSAFE
-    JS_ASSERT(!fop->runtime()->gc.helperThread.sweeping());
+    JS_ASSERT(!fop->runtime()->gc.isBackgroundSweeping());
 #endif
 
     ArenaList *al = &arenaLists[thingKind];
     if (al->isEmpty()) {
         JS_ASSERT(backgroundFinalizeState[thingKind] == BFS_DONE);
         return;
     }
 
@@ -1862,33 +1890,36 @@ ArenaLists::refillFreeList(ThreadSafeCon
 #endif
 
     for (;;) {
         if (MOZ_UNLIKELY(runGC)) {
             if (void *thing = RunLastDitchGC(cx->asJSContext(), zone, thingKind))
                 return thing;
         }
 
+        AutoMaybeStartBackgroundAllocation maybeStartBackgroundAllocation;
+
         if (cx->isJSContext()) {
             /*
              * allocateFromArena may fail while the background finalization still
              * run. If we are on the main thread, we want to wait for it to finish
              * and restart. However, checking for that is racy as the background
              * finalization could free some things after allocateFromArena decided
              * to fail but at this point it may have already stopped. To avoid
              * this race we always try to allocate twice.
              */
             for (bool secondAttempt = false; ; secondAttempt = true) {
-                void *thing = cx->allocator()->arenas.allocateFromArenaInline(zone, thingKind);
+                void *thing = cx->allocator()->arenas.allocateFromArenaInline(zone, thingKind,
+                                                                              maybeStartBackgroundAllocation);
                 if (MOZ_LIKELY(!!thing))
                     return thing;
                 if (secondAttempt)
                     break;
 
-                cx->asJSContext()->runtime()->gc.helperThread.waitBackgroundSweepEnd();
+                cx->asJSContext()->runtime()->gc.waitBackgroundSweepEnd();
             }
         } else {
 #ifdef JS_THREADSAFE
             /*
              * If we're off the main thread, we try to allocate once and
              * return whatever value we get. If we aren't in a ForkJoin
              * session (i.e. we are in a worker thread async with the main
              * thread), we need to first ensure the main thread is not in a GC
@@ -1897,17 +1928,18 @@ ArenaLists::refillFreeList(ThreadSafeCon
             mozilla::Maybe<AutoLockWorkerThreadState> lock;
             JSRuntime *rt = zone->runtimeFromAnyThread();
             if (rt->exclusiveThreadsPresent()) {
                 lock.construct();
                 while (rt->isHeapBusy())
                     WorkerThreadState().wait(GlobalWorkerThreadState::PRODUCER);
             }
 
-            void *thing = cx->allocator()->arenas.allocateFromArenaInline(zone, thingKind);
+            void *thing = cx->allocator()->arenas.allocateFromArenaInline(zone, thingKind,
+                                                                          maybeStartBackgroundAllocation);
             if (thing)
                 return thing;
 #else
             MOZ_CRASH();
 #endif
         }
 
         if (!cx->allowGC() || !allowGC)
@@ -2097,17 +2129,17 @@ GCRuntime::maybeGC(Zone *zone)
         GCSlice(rt, GC_NORMAL, JS::gcreason::MAYBEGC);
         return;
     }
 
     double factor = highFrequencyGC ? 0.85 : 0.9;
     if (zone->gcBytes > 1024 * 1024 &&
         zone->gcBytes >= factor * zone->gcTriggerBytes &&
         incrementalState == NO_INCREMENTAL &&
-        !helperThread.sweeping())
+        !isBackgroundSweeping())
     {
         PrepareZoneForGC(zone);
         GCSlice(rt, GC_NORMAL, JS::gcreason::MAYBEGC);
         return;
     }
 
 #ifndef JS_MORE_DETERMINISTIC
     /*
@@ -2279,23 +2311,25 @@ SweepBackgroundThings(JSRuntime* rt, boo
 
     rt->gc.sweepingZones = nullptr;
 }
 
 #ifdef JS_THREADSAFE
 static void
 AssertBackgroundSweepingFinished(JSRuntime *rt)
 {
+#ifdef DEBUG
     JS_ASSERT(!rt->gc.sweepingZones);
     for (ZonesIter zone(rt, WithAtoms); !zone.done(); zone.next()) {
         for (unsigned i = 0; i < FINALIZE_LIMIT; ++i) {
             JS_ASSERT(!zone->allocator.arenas.arenaListsToSweep[i]);
             JS_ASSERT(zone->allocator.arenas.doneBackgroundFinalize(AllocKind(i)));
         }
     }
+#endif
 }
 
 unsigned
 js::GetCPUCount()
 {
     static unsigned ncpus = 0;
     if (ncpus == 0) {
 # ifdef XP_WIN
@@ -2307,261 +2341,255 @@ js::GetCPUCount()
         ncpus = (n > 0) ? unsigned(n) : 1;
 # endif
     }
     return ncpus;
 }
 #endif /* JS_THREADSAFE */
 
 bool
-GCHelperThread::init()
+GCHelperState::init()
 {
     if (!rt->useHelperThreads()) {
         backgroundAllocation = false;
         return true;
     }
 
 #ifdef JS_THREADSAFE
-    if (!(wakeup = PR_NewCondVar(rt->gc.lock)))
-        return false;
     if (!(done = PR_NewCondVar(rt->gc.lock)))
         return false;
 
-    thread = PR_CreateThread(PR_USER_THREAD, threadMain, this, PR_PRIORITY_NORMAL,
-                             PR_GLOBAL_THREAD, PR_JOINABLE_THREAD, 0);
-    if (!thread)
-        return false;
-
     backgroundAllocation = (GetCPUCount() >= 2);
+
+    WorkerThreadState().ensureInitialized();
 #endif /* JS_THREADSAFE */
+
     return true;
 }
 
 void
-GCHelperThread::finish()
+GCHelperState::finish()
 {
     if (!rt->useHelperThreads() || !rt->gc.lock) {
-        JS_ASSERT(state == IDLE);
+        JS_ASSERT(state_ == IDLE);
         return;
     }
 
 #ifdef JS_THREADSAFE
-    PRThread *join = nullptr;
-    {
-        AutoLockGC lock(rt);
-        if (thread && state != SHUTDOWN) {
-            /*
-             * We cannot be in the ALLOCATING or CANCEL_ALLOCATION states as
-             * the allocations should have been stopped during the last GC.
-             */
-            JS_ASSERT(state == IDLE || state == SWEEPING);
-            if (state == IDLE)
-                PR_NotifyCondVar(wakeup);
-            state = SHUTDOWN;
-            join = thread;
-        }
-    }
-    if (join) {
-        /* PR_DestroyThread is not necessary. */
-        PR_JoinThread(join);
-    }
-    if (wakeup)
-        PR_DestroyCondVar(wakeup);
+    // Wait for any lingering background sweeping to finish.
+    waitBackgroundSweepEnd();
+
     if (done)
         PR_DestroyCondVar(done);
+#else
+    MOZ_CRASH();
 #endif /* JS_THREADSAFE */
 }
 
-#ifdef JS_THREADSAFE
-#ifdef MOZ_NUWA_PROCESS
-extern "C" {
-MFBT_API bool IsNuwaProcess();
-MFBT_API void NuwaMarkCurrentThread(void (*recreate)(void *), void *arg);
-}
-#endif
-
-/* static */
+GCHelperState::State
+GCHelperState::state()
+{
+    JS_ASSERT(rt->gc.currentThreadOwnsGCLock());
+    return state_;
+}
+
 void
-GCHelperThread::threadMain(void *arg)
-{
-    PR_SetCurrentThreadName("JS GC Helper");
-
-#ifdef MOZ_NUWA_PROCESS
-    if (IsNuwaProcess && IsNuwaProcess()) {
-        JS_ASSERT(NuwaMarkCurrentThread != nullptr);
-        NuwaMarkCurrentThread(nullptr, nullptr);
-    }
-#endif
-
-    static_cast<GCHelperThread *>(arg)->threadLoop();
+GCHelperState::setState(State state)
+{
+    JS_ASSERT(rt->gc.currentThreadOwnsGCLock());
+    state_ = state;
 }
 
 void
-GCHelperThread::wait(PRCondVar *which)
-{
+GCHelperState::startBackgroundThread(State newState)
+{
+#ifdef JS_THREADSAFE
+    JS_ASSERT(!thread && state() == IDLE && newState != IDLE);
+    setState(newState);
+
+    if (!WorkerThreadState().gcHelperWorklist().append(this))
+        CrashAtUnhandlableOOM("Could not add to pending GC helpers list");
+    WorkerThreadState().notifyAll(GlobalWorkerThreadState::PRODUCER);
+#else
+    MOZ_CRASH();
+#endif
+}
+
+void
+GCHelperState::waitForBackgroundThread()
+{
+#ifdef JS_THREADSAFE
+    JS_ASSERT(CurrentThreadCanAccessRuntime(rt));
+
     rt->gc.lockOwner = nullptr;
-    PR_WaitCondVar(which, PR_INTERVAL_NO_TIMEOUT);
+    PR_WaitCondVar(done, PR_INTERVAL_NO_TIMEOUT);
 #ifdef DEBUG
     rt->gc.lockOwner = PR_GetCurrentThread();
 #endif
+#else
+    MOZ_CRASH();
+#endif
 }
 
 void
-GCHelperThread::threadLoop()
-{
+GCHelperState::work()
+{
+#ifdef JS_THREADSAFE
     AutoLockGC lock(rt);
 
-    TraceLogger *logger = TraceLoggerForCurrentThread();
-
-    /*
-     * Even on the first iteration the state can be SHUTDOWN or SWEEPING if
-     * the stop request or the GC and the corresponding startBackgroundSweep call
-     * happen before this thread has a chance to run.
-     */
-    for (;;) {
-        switch (state) {
-          case SHUTDOWN:
-            return;
-          case IDLE:
-            wait(wakeup);
-            break;
-          case SWEEPING: {
-            AutoTraceLog logSweeping(logger, TraceLogger::GCSweeping);
-            doSweep();
-            if (state == SWEEPING)
-                state = IDLE;
-            PR_NotifyAllCondVar(done);
-            break;
-          }
-          case ALLOCATING: {
-            AutoTraceLog logAllocating(logger, TraceLogger::GCAllocation);
-            do {
-                Chunk *chunk;
-                {
-                    AutoUnlockGC unlock(rt);
-                    chunk = Chunk::allocate(rt);
-                }
-
-                /* OOM stops the background allocation. */
-                if (!chunk)
-                    break;
-                JS_ASSERT(chunk->info.numArenasFreeCommitted == 0);
-                rt->gc.chunkPool.put(chunk);
-            } while (state == ALLOCATING && rt->gc.wantBackgroundAllocation());
-            if (state == ALLOCATING)
-                state = IDLE;
-            break;
-          }
-          case CANCEL_ALLOCATION:
-            state = IDLE;
-            PR_NotifyAllCondVar(done);
-            break;
-        }
+    JS_ASSERT(!thread);
+    thread = PR_GetCurrentThread();
+
+    switch (state()) {
+
+      case IDLE:
+        MOZ_ASSUME_UNREACHABLE("GC helper triggered on idle state");
+        break;
+
+      case SWEEPING: {
+#if JS_TRACE_LOGGING
+        AutoTraceLog logger(TraceLogging::getLogger(TraceLogging::GC_BACKGROUND),
+                            TraceLogging::GC_SWEEPING_START,
+                            TraceLogging::GC_SWEEPING_STOP);
+#endif
+        doSweep();
+        JS_ASSERT(state() == SWEEPING);
+        break;
+      }
+
+      case ALLOCATING: {
+#if JS_TRACE_LOGGING
+        AutoTraceLog logger(TraceLogging::getLogger(TraceLogging::GC_BACKGROUND),
+                            TraceLogging::GC_ALLOCATING_START,
+                            TraceLogging::GC_ALLOCATING_STOP);
+#endif
+        do {
+            Chunk *chunk;
+            {
+                AutoUnlockGC unlock(rt);
+                chunk = Chunk::allocate(rt);
+            }
+
+            /* OOM stops the background allocation. */
+            if (!chunk)
+                break;
+            JS_ASSERT(chunk->info.numArenasFreeCommitted == 0);
+            rt->gc.chunkPool.put(chunk);
+        } while (state() == ALLOCATING && rt->gc.wantBackgroundAllocation());
+
+        JS_ASSERT(state() == ALLOCATING || state() == CANCEL_ALLOCATION);
+        break;
+      }
+
+      case CANCEL_ALLOCATION:
+        break;
     }
-}
-#endif /* JS_THREADSAFE */
+
+    setState(IDLE);
+    thread = nullptr;
+
+    PR_NotifyAllCondVar(done);
+#else
+    MOZ_CRASH();
+#endif
+}
 
 void
-GCHelperThread::startBackgroundSweep(bool shouldShrink)
+GCHelperState::startBackgroundSweep(bool shouldShrink)
 {
     JS_ASSERT(rt->useHelperThreads());
 
 #ifdef JS_THREADSAFE
+    AutoLockWorkerThreadState workerLock;
     AutoLockGC lock(rt);
-    JS_ASSERT(state == IDLE);
+    JS_ASSERT(state() == IDLE);
     JS_ASSERT(!sweepFlag);
     sweepFlag = true;
     shrinkFlag = shouldShrink;
-    state = SWEEPING;
-    PR_NotifyCondVar(wakeup);
+    startBackgroundThread(SWEEPING);
 #endif /* JS_THREADSAFE */
 }
 
 /* Must be called with the GC lock taken. */
 void
-GCHelperThread::startBackgroundShrink()
+GCHelperState::startBackgroundShrink()
 {
     JS_ASSERT(rt->useHelperThreads());
 
 #ifdef JS_THREADSAFE
-    switch (state) {
+    switch (state()) {
       case IDLE:
         JS_ASSERT(!sweepFlag);
         shrinkFlag = true;
-        state = SWEEPING;
-        PR_NotifyCondVar(wakeup);
+        startBackgroundThread(SWEEPING);
         break;
       case SWEEPING:
         shrinkFlag = true;
         break;
       case ALLOCATING:
       case CANCEL_ALLOCATION:
         /*
          * If we have started background allocation there is nothing to
          * shrink.
          */
         break;
-      case SHUTDOWN:
-        MOZ_ASSUME_UNREACHABLE("No shrink on shutdown");
     }
 #endif /* JS_THREADSAFE */
 }
 
 void
-GCHelperThread::waitBackgroundSweepEnd()
+GCHelperState::waitBackgroundSweepEnd()
 {
     if (!rt->useHelperThreads()) {
-        JS_ASSERT(state == IDLE);
+        JS_ASSERT(state_ == IDLE);
         return;
     }
 
 #ifdef JS_THREADSAFE
     AutoLockGC lock(rt);
-    while (state == SWEEPING)
-        wait(done);
+    while (state() == SWEEPING)
+        waitForBackgroundThread();
     if (rt->gc.incrementalState == NO_INCREMENTAL)
         AssertBackgroundSweepingFinished(rt);
 #endif /* JS_THREADSAFE */
 }
 
 void
-GCHelperThread::waitBackgroundSweepOrAllocEnd()
+GCHelperState::waitBackgroundSweepOrAllocEnd()
 {
     if (!rt->useHelperThreads()) {
-        JS_ASSERT(state == IDLE);
+        JS_ASSERT(state_ == IDLE);
         return;
     }
 
 #ifdef JS_THREADSAFE
     AutoLockGC lock(rt);
-    if (state == ALLOCATING)
-        state = CANCEL_ALLOCATION;
-    while (state == SWEEPING || state == CANCEL_ALLOCATION)
-        wait(done);
+    if (state() == ALLOCATING)
+        setState(CANCEL_ALLOCATION);
+    while (state() == SWEEPING || state() == CANCEL_ALLOCATION)
+        waitForBackgroundThread();
     if (rt->gc.incrementalState == NO_INCREMENTAL)
         AssertBackgroundSweepingFinished(rt);
 #endif /* JS_THREADSAFE */
 }
 
 /* Must be called with the GC lock taken. */
 inline void
-GCHelperThread::startBackgroundAllocationIfIdle()
+GCHelperState::startBackgroundAllocationIfIdle()
 {
     JS_ASSERT(rt->useHelperThreads());
 
 #ifdef JS_THREADSAFE
-    if (state == IDLE) {
-        state = ALLOCATING;
-        PR_NotifyCondVar(wakeup);
-    }
+    if (state_ == IDLE)
+        startBackgroundThread(ALLOCATING);
 #endif /* JS_THREADSAFE */
 }
 
 void
-GCHelperThread::replenishAndFreeLater(void *ptr)
+GCHelperState::replenishAndFreeLater(void *ptr)
 {
     JS_ASSERT(freeCursor == freeCursorEnd);
     do {
         if (freeCursor && !freeVector.append(freeCursorEnd - FREE_ARRAY_LENGTH))
             break;
         freeCursor = (void **) js_malloc(FREE_ARRAY_SIZE);
         if (!freeCursor) {
             freeCursorEnd = nullptr;
@@ -2572,17 +2600,17 @@ GCHelperThread::replenishAndFreeLater(vo
         return;
     } while (false);
     js_free(ptr);
 }
 
 #ifdef JS_THREADSAFE
 /* Must be called with the GC lock taken. */
 void
-GCHelperThread::doSweep()
+GCHelperState::doSweep()
 {
     if (sweepFlag) {
         sweepFlag = false;
         AutoUnlockGC unlock(rt);
 
         SweepBackgroundThings(rt, true);
 
         if (freeCursor) {
@@ -2612,20 +2640,20 @@ GCHelperThread::doSweep()
     if (!shrinking && shrinkFlag) {
         shrinkFlag = false;
         ExpireChunksAndArenas(rt, true);
     }
 }
 #endif /* JS_THREADSAFE */
 
 bool
-GCHelperThread::onBackgroundThread()
+GCHelperState::onBackgroundThread()
 {
 #ifdef JS_THREADSAFE
-    return PR_GetCurrentThread() == getThread();
+    return PR_GetCurrentThread() == thread;
 #else
     return false;
 #endif
 }
 
 bool
 GCRuntime::releaseObservedTypes()
 {
@@ -4138,17 +4166,17 @@ GCRuntime::endSweepPhase(JSGCInvocationK
          */
         if (!lastGC)
             sweepZones(&fop, lastGC);
 
         if (!sweepOnBackgroundThread) {
             /*
              * Destroy arenas after we finished the sweeping so finalizers can
              * safely use IsAboutToBeFinalized(). This is done on the
-             * GCHelperThread if possible. We acquire the lock only because
+             * GCHelperState if possible. We acquire the lock only because
              * Expire needs to unlock it for other callers.
              */
             AutoLockGC lock(rt);
             ExpireChunksAndArenas(rt, gckind == GC_SHRINK);
         }
     }
 
     {
@@ -4402,17 +4430,17 @@ GCRuntime::resetIncrementalGC(const char
             zone->scheduledForDestruction = false;
 
         /* Finish sweeping the current zone group, then abort. */
         abortSweepAfterCurrentGroup = true;
         incrementalCollectSlice(SliceBudget::Unlimited, JS::gcreason::RESET, GC_NORMAL);
 
         {
             gcstats::AutoPhase ap(stats, gcstats::PHASE_WAIT_BACKGROUND_THREAD);
-            helperThread.waitBackgroundSweepOrAllocEnd();
+            rt->gc.waitBackgroundSweepOrAllocEnd();
         }
         break;
 
       default:
         MOZ_ASSUME_UNREACHABLE("Invalid incremental GC state");
     }
 
     stats.reset(reason);
@@ -4613,17 +4641,17 @@ GCRuntime::incrementalCollectSlice(int64
       case SWEEP: {
         bool finished = sweepPhase(sliceBudget);
         if (!finished)
             break;
 
         endSweepPhase(gckind, lastGC);
 
         if (sweepOnBackgroundThread)
-            helperThread.startBackgroundSweep(gckind == GC_SHRINK);
+            helperState.startBackgroundSweep(gckind == GC_SHRINK);
 
         incrementalState = NO_INCREMENTAL;
         break;
       }
 
       default:
         JS_ASSERT(false);
     }
@@ -4707,17 +4735,17 @@ GCRuntime::gcCycle(bool incremental, int
     /*
      * As we about to purge caches and clear the mark bits we must wait for
      * any background finalization to finish. We must also wait for the
      * background allocation to finish so we can avoid taking the GC lock
      * when manipulating the chunks during the GC.
      */
     {
         gcstats::AutoPhase ap(stats, gcstats::PHASE_WAIT_BACKGROUND_THREAD);
-        helperThread.waitBackgroundSweepOrAllocEnd();
+        waitBackgroundSweepOrAllocEnd();
     }
 
     State prevState = incrementalState;
 
     if (!incremental) {
         /* If non-incremental GC was requested, reset incremental GC. */
         resetIncrementalGC("requested");
         stats.nonincremental("requested");
@@ -4954,16 +4982,17 @@ js::PrepareForDebugGC(JSRuntime *rt)
 {
     if (!ZonesSelected(rt))
         JS::PrepareForFullGC(rt);
 }
 
 JS_FRIEND_API(void)
 JS::ShrinkGCBuffers(JSRuntime *rt)
 {
+    AutoLockWorkerThreadState workerLock;
     AutoLockGC lock(rt);
     JS_ASSERT(!rt->isHeapBusy());
 
     if (!rt->useHelperThreads())
         ExpireChunksAndArenas(rt, true);
     else
         rt->gc.startBackgroundShrink();
 }
--- a/js/src/jsgc.h
+++ b/js/src/jsgc.h
@@ -28,17 +28,16 @@ class JSLinearString;
 namespace js {
 
 class ArgumentsObject;
 class ArrayBufferObject;
 class ArrayBufferViewObject;
 class SharedArrayBufferObject;
 class BaseShape;
 class DebugScopeObject;
-class GCHelperThread;
 class GlobalObject;
 class LazyScript;
 class Nursery;
 class PropertyName;
 class ScopeObject;
 class Shape;
 class UnownedBaseShape;
 
@@ -350,16 +349,22 @@ GetGCKindSlots(AllocKind thingKind, cons
      * space for the extra fields in JSFunction, but have no fixed slots.
      */
     if (clasp == FunctionClassPtr)
         nslots = 0;
 
     return nslots;
 }
 
+// Class to assist in triggering background chunk allocation. This cannot be done
+// while holding the GC or worker thread state lock due to lock ordering issues.
+// As a result, the triggering is delayed using this class until neither of the
+// above locks is held.
+class AutoMaybeStartBackgroundAllocation;
+
 /*
  * Arena lists have a head and a cursor. The cursor conceptually lies on arena
  * boundaries, i.e. before the first arena, between two arenas, or after the
  * last arena.
  *
  * Normally the arena following the cursor is the first arena in the list with
  * some free things and all arenas before the cursor are fully allocated. (And
  * if the cursor is at the end of the list, then all the arenas are full.)
@@ -766,17 +771,18 @@ class ArenaLists
 
   private:
     inline void finalizeNow(FreeOp *fop, AllocKind thingKind);
     inline void forceFinalizeNow(FreeOp *fop, AllocKind thingKind);
     inline void queueForForegroundSweep(FreeOp *fop, AllocKind thingKind);
     inline void queueForBackgroundSweep(FreeOp *fop, AllocKind thingKind);
 
     void *allocateFromArena(JS::Zone *zone, AllocKind thingKind);
-    inline void *allocateFromArenaInline(JS::Zone *zone, AllocKind thingKind);
+    inline void *allocateFromArenaInline(JS::Zone *zone, AllocKind thingKind,
+                                         AutoMaybeStartBackgroundAllocation &maybeStartBackgroundAllocation);
 
     inline void normalizeBackgroundFinalizeState(AllocKind thingKind);
 
     friend class js::Nursery;
 };
 
 /*
  * Initial allocation size for data structures holding chunks is set to hold
@@ -909,52 +915,64 @@ NotifyGCNukeWrapper(JSObject *o);
 
 extern unsigned
 NotifyGCPreSwap(JSObject *a, JSObject *b);
 
 extern void
 NotifyGCPostSwap(JSObject *a, JSObject *b, unsigned preResult);
 
 /*
- * Helper that implements sweeping and allocation for kinds that can be swept
- * and allocated off the main thread.
+ * Helper state for use when JS helper threads sweep and allocate GC thing kinds
+ * that can be swept and allocated off the main thread.
  *
  * In non-threadsafe builds, all actual sweeping and allocation is performed
- * on the main thread, but GCHelperThread encapsulates this from clients as
+ * on the main thread, but GCHelperState encapsulates this from clients as
  * much as possible.
  */
-class GCHelperThread {
+class GCHelperState
+{
     enum State {
         IDLE,
         SWEEPING,
         ALLOCATING,
-        CANCEL_ALLOCATION,
-        SHUTDOWN
+        CANCEL_ALLOCATION
     };
 
     /*
      * During the finalization we do not free immediately. Rather we add the
      * corresponding pointers to a buffer which we later release on a
      * separated thread.
      *
      * The buffer is implemented as a vector of 64K arrays of pointers, not as
      * a simple vector, to avoid realloc calls during the vector growth and to
      * not bloat the binary size of the inlined freeLater method. Any OOM
      * during buffer growth results in the pointer being freed immediately.
      */
     static const size_t FREE_ARRAY_SIZE = size_t(1) << 16;
     static const size_t FREE_ARRAY_LENGTH = FREE_ARRAY_SIZE / sizeof(void *);
 
-    JSRuntime         *const rt;
-    PRThread          *thread;
-    PRCondVar         *wakeup;
-    PRCondVar         *done;
-    volatile State    state;
+    // Associated runtime.
+    JSRuntime *const rt;
+
+    // Condvar for notifying the main thread when work has finished. This is
+    // associated with the runtime's GC lock --- the worker thread state
+    // condvars can't be used here due to lock ordering issues.
+    PRCondVar *done;
 
-    void wait(PRCondVar *which);
+    // Activity for the helper to do, protected by the GC lock.
+    State state_;
+
+    // Thread which work is being performed on, or null.
+    PRThread *thread;
+
+    void startBackgroundThread(State newState);
+    void waitForBackgroundThread();
+
+    State state();
+    void setState(State state);
 
     bool              sweepFlag;
     bool              shrinkFlag;
 
     Vector<void **, 16, js::SystemAllocPolicy> freeVector;
     void            **freeCursor;
     void            **freeCursorEnd;
 
@@ -967,83 +985,77 @@ class GCHelperThread {
 
     static void freeElementsAndArray(void **array, void **end) {
         JS_ASSERT(array <= end);
         for (void **p = array; p != end; ++p)
             js_free(*p);
         js_free(array);
     }
 
-    static void threadMain(void* arg);
-    void threadLoop();
-
     /* Must be called with the GC lock taken. */
     void doSweep();
 
   public:
-    explicit GCHelperThread(JSRuntime *rt)
+    explicit GCHelperState(JSRuntime *rt)
       : rt(rt),
+        done(nullptr),
+        state_(IDLE),
         thread(nullptr),
-        wakeup(nullptr),
-        done(nullptr),
-        state(IDLE),
         sweepFlag(false),
         shrinkFlag(false),
         freeCursor(nullptr),
         freeCursorEnd(nullptr),
         backgroundAllocation(true)
     { }
 
     bool init();
     void finish();
 
+    void work();
+
     /* Must be called with the GC lock taken. */
     void startBackgroundSweep(bool shouldShrink);
 
     /* Must be called with the GC lock taken. */
     void startBackgroundShrink();
 
     /* Must be called without the GC lock taken. */
     void waitBackgroundSweepEnd();
 
     /* Must be called without the GC lock taken. */
     void waitBackgroundSweepOrAllocEnd();
 
     /* Must be called with the GC lock taken. */
-    inline void startBackgroundAllocationIfIdle();
+    void startBackgroundAllocationIfIdle();
 
     bool canBackgroundAllocate() const {
         return backgroundAllocation;
     }
 
     void disableBackgroundAllocation() {
         backgroundAllocation = false;
     }
 
-    PRThread *getThread() const {
-        return thread;
-    }
-
     bool onBackgroundThread();
 
     /*
      * Outside the GC lock may give true answer when in fact the sweeping has
      * been done.
      */
-    bool sweeping() const {
-        return state == SWEEPING;
+    bool isBackgroundSweeping() const {
+        return state_ == SWEEPING;
     }
 
     bool shouldShrink() const {
-        JS_ASSERT(sweeping());
+        JS_ASSERT(isBackgroundSweeping());
         return shrinkFlag;
     }
 
     void freeLater(void *ptr) {
-        JS_ASSERT(!sweeping());
+        JS_ASSERT(!isBackgroundSweeping());
         if (freeCursor != freeCursorEnd)
             *freeCursor++ = ptr;
         else
             replenishAndFreeLater(ptr);
     }
 };
 
 struct GCChunkHasher {
--- a/js/src/jsworkers.cpp
+++ b/js/src/jsworkers.cpp
@@ -573,16 +573,22 @@ GlobalWorkerThreadState::canStartParseTa
 }
 
 bool
 GlobalWorkerThreadState::canStartCompressionTask()
 {
     return !compressionWorklist().empty();
 }
 
+bool
+GlobalWorkerThreadState::canStartGCHelperTask()
+{
+    return !gcHelperWorklist().empty();
+}
+
 static void
 CallNewScriptHookForAllScripts(JSContext *cx, HandleScript script)
 {
     // We should never hit this, since nested scripts are also constructed via
     // BytecodeEmitter instances on the stack.
     JS_CHECK_RECURSION(cx, return);
 
     // Recurse to any nested scripts.
@@ -1016,16 +1022,34 @@ GlobalWorkerThreadState::compressionTask
         SourceCompressionTask *task = threads[i].compressionTask;
         if (task && task->source() == ss)
             return task;
     }
     return nullptr;
 }
 
 void
+WorkerThread::handleGCHelperWorkload()
+{
+    JS_ASSERT(WorkerThreadState().isLocked());
+    JS_ASSERT(WorkerThreadState().canStartGCHelperTask());
+    JS_ASSERT(idle());
+
+    JS_ASSERT(!gcHelperState);
+    gcHelperState = WorkerThreadState().gcHelperWorklist().popCopy();
+
+    {
+        AutoUnlockWorkerThreadState unlock;
+        gcHelperState->work();
+    }
+
+    gcHelperState = nullptr;
+}
+
+void
 WorkerThread::threadLoop()
 {
     JS::AutoAssertNoGC nogc;
     AutoLockWorkerThreadState lock;
 
     js::TlsPerThreadData.set(threadData.addr());
 
     // Compute the thread's stack limit, for over-recursed checks.
@@ -1043,32 +1067,35 @@ WorkerThread::threadLoop()
 
         // Block until a task is available.
         while (true) {
             if (terminate)
                 return;
             if (WorkerThreadState().canStartIonCompile() ||
                 WorkerThreadState().canStartAsmJSCompile() ||
                 WorkerThreadState().canStartParseTask() ||
-                WorkerThreadState().canStartCompressionTask())
+                WorkerThreadState().canStartCompressionTask() ||
+                WorkerThreadState().canStartGCHelperTask())
             {
                 break;
             }
             WorkerThreadState().wait(GlobalWorkerThreadState::PRODUCER);
         }
 
         // Dispatch tasks, prioritizing AsmJS work.
         if (WorkerThreadState().canStartAsmJSCompile())
             handleAsmJSWorkload();
         else if (WorkerThreadState().canStartIonCompile())
             handleIonWorkload();
         else if (WorkerThreadState().canStartParseTask())
             handleParseWorkload();
         else if (WorkerThreadState().canStartCompressionTask())
             handleCompressionWorkload();
+        else if (WorkerThreadState().canStartGCHelperTask())
+            handleGCHelperWorkload();
         else
             MOZ_ASSUME_UNREACHABLE("No task to perform");
     }
 }
 
 #else /* JS_THREADSAFE */
 
 using namespace js;
--- a/js/src/jsworkers.h
+++ b/js/src/jsworkers.h
@@ -43,16 +43,17 @@ class GlobalWorkerThreadState
 
     // Number of threads to create. May be accessed without locking.
     size_t threadCount;
 
     typedef Vector<jit::IonBuilder*, 0, SystemAllocPolicy> IonBuilderVector;
     typedef Vector<AsmJSParallelTask*, 0, SystemAllocPolicy> AsmJSParallelTaskVector;
     typedef Vector<ParseTask*, 0, SystemAllocPolicy> ParseTaskVector;
     typedef Vector<SourceCompressionTask*, 0, SystemAllocPolicy> SourceCompressionTaskVector;
+    typedef Vector<GCHelperState *, 0, SystemAllocPolicy> GCHelperStateVector;
 
     // List of available threads, or null if the thread state has not been initialized.
     WorkerThread *threads;
 
   private:
     // The lists below are all protected by |lock|.
 
     // Ion compilation worklist and finished jobs.
@@ -76,16 +77,19 @@ class GlobalWorkerThreadState
     ParseTaskVector parseWorklist_, parseFinishedList_;
 
     // Parse tasks waiting for an atoms-zone GC to complete.
     ParseTaskVector parseWaitingOnGC_;
 
     // Source compression worklist.
     SourceCompressionTaskVector compressionWorklist_;
 
+    // Runtimes which have sweeping / allocating work to do.
+    GCHelperStateVector gcHelperWorklist_;
+
   public:
     GlobalWorkerThreadState();
 
     void ensureInitialized();
     void finish();
 
     void lock();
     void unlock();
@@ -145,20 +149,26 @@ class GlobalWorkerThreadState
         return parseWaitingOnGC_;
     }
 
     SourceCompressionTaskVector &compressionWorklist() {
         JS_ASSERT(isLocked());
         return compressionWorklist_;
     }
 
+    GCHelperStateVector &gcHelperWorklist() {
+        JS_ASSERT(isLocked());
+        return gcHelperWorklist_;
+    }
+
     bool canStartAsmJSCompile();
     bool canStartIonCompile();
     bool canStartParseTask();
     bool canStartCompressionTask();
+    bool canStartGCHelperTask();
 
     uint32_t harvestFailedAsmJSJobs() {
         JS_ASSERT(isLocked());
         uint32_t n = numAsmJSFailedJobs;
         numAsmJSFailedJobs = 0;
         return n;
     }
     void noteAsmJSFailure(void *func) {
@@ -235,26 +245,30 @@ struct WorkerThread
     AsmJSParallelTask *asmData;
 
     /* Any source being parsed/emitted on this thread. */
     ParseTask *parseTask;
 
     /* Any source being compressed on this thread. */
     SourceCompressionTask *compressionTask;
 
+    /* Any GC state for background sweeping or allocating being performed. */
+    GCHelperState *gcHelperState;
+
     bool idle() const {
-        return !ionBuilder && !asmData && !parseTask && !compressionTask;
+        return !ionBuilder && !asmData && !parseTask && !compressionTask && !gcHelperState;
     }
 
     void destroy();
 
     void handleAsmJSWorkload();
     void handleIonWorkload();
     void handleParseWorkload();
     void handleCompressionWorkload();
+    void handleGCHelperWorkload();
 
     static void ThreadMain(void *arg);
     void threadLoop();
 };
 
 #endif /* JS_THREADSAFE */
 
 /* Methods for interacting with worker threads. */