Bug 1514927 - Rewrite handling of delayed marking to ensure that we process all delayed arenas r=sfink
authorJon Coppeard <jcoppeard@mozilla.com>
Thu, 20 Dec 2018 10:53:24 +0000
changeset 451481 d61c96936904988b3692d68c209623db54b46046
parent 451480 fbf8034d81325271d3fb4c68d14e018d0dbb6053
child 451483 3ce5db8a3f3251b9332a99ff7f6a923291004406
push id110672
push userjcoppeard@mozilla.com
push dateThu, 20 Dec 2018 10:57:25 +0000
treeherdermozilla-inbound@d61c96936904 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssfink
bugs1514927
milestone66.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 1514927 - Rewrite handling of delayed marking to ensure that we process all delayed arenas r=sfink
js/src/gc/GC.cpp
js/src/gc/GCMarker.h
js/src/gc/Heap-inl.h
js/src/gc/Heap.h
js/src/gc/Marking.cpp
js/src/jit-test/tests/gc/bug-1514927.js
--- a/js/src/gc/GC.cpp
+++ b/js/src/gc/GC.cpp
@@ -560,17 +560,17 @@ inline size_t Arena::finalize(FreeOp* fo
   /* Enforce requirements on size of T. */
   MOZ_ASSERT(thingSize % CellAlignBytes == 0);
   MOZ_ASSERT(thingSize >= MinCellSize);
   MOZ_ASSERT(thingSize <= 255);
 
   MOZ_ASSERT(allocated());
   MOZ_ASSERT(thingKind == getAllocKind());
   MOZ_ASSERT(thingSize == getThingSize());
-  MOZ_ASSERT(!hasDelayedMarking);
+  MOZ_ASSERT(!onDelayedMarkingList_);
 
   uint_fast16_t firstThing = firstThingOffset(thingKind);
   uint_fast16_t firstThingOrSuccessorOfLastMarkedThing = firstThing;
   uint_fast16_t lastThing = ArenaSize - thingSize;
 
   FreeSpan newListHead;
   FreeSpan* newListTail = &newListHead;
   size_t nmarked = 0;
@@ -823,17 +823,17 @@ void Chunk::addArenaToDecommittedList(co
 void Chunk::recycleArena(Arena* arena, SortedArenaList& dest,
                          size_t thingsPerArena) {
   arena->setAsFullyUnused();
   dest.insertAt(arena, thingsPerArena);
 }
 
 void Chunk::releaseArena(JSRuntime* rt, Arena* arena, const AutoLockGC& lock) {
   MOZ_ASSERT(arena->allocated());
-  MOZ_ASSERT(!arena->hasDelayedMarking);
+  MOZ_ASSERT(!arena->onDelayedMarkingList());
 
   arena->release(lock);
   addArenaToFreeList(rt, arena);
   updateChunkListAfterFree(rt, lock);
 }
 
 bool Chunk::decommitOneFreeArena(JSRuntime* rt, AutoLockGC& lock) {
   MOZ_ASSERT(info.numArenasFreeCommitted > 0);
@@ -2056,33 +2056,16 @@ void MemoryCounter::adopt(MemoryCounter&
   other.triggered_ = NoTrigger;
 }
 
 void MemoryCounter::recordTrigger(TriggerKind trigger) {
   MOZ_ASSERT(trigger > triggered_);
   triggered_ = trigger;
 }
 
-void GCMarker::delayMarkingArena(Arena* arena) {
-  if (arena->hasDelayedMarking) {
-    /* Arena already scheduled to be marked later */
-    return;
-  }
-  arena->setNextDelayedMarking(unmarkedArenaStackTop);
-  unmarkedArenaStackTop = arena;
-#ifdef DEBUG
-  markLaterArenas++;
-#endif
-}
-
-void GCMarker::delayMarkingChildren(const void* thing) {
-  const TenuredCell* cell = TenuredCell::fromPointer(thing);
-  delayMarkingArena(cell->arena());
-}
-
 /* Compacting GC */
 
 bool GCRuntime::shouldCompact() {
   // Compact on shrinking GC if enabled.  Skip compacting in incremental GCs
   // if we are currently animating, unless the user is inactive or we're
   // responding to memory pressure.
 
   static const auto oneSecond = TimeDuration::FromSeconds(1);
@@ -2278,17 +2261,17 @@ static void RelocateCell(Zone* zone, Ten
 
   // Mark source cell as forwarded and leave a pointer to the destination.
   RelocationOverlay* overlay = RelocationOverlay::fromCell(src);
   overlay->forwardTo(dst);
 }
 
 static void RelocateArena(Arena* arena, SliceBudget& sliceBudget) {
   MOZ_ASSERT(arena->allocated());
-  MOZ_ASSERT(!arena->hasDelayedMarking);
+  MOZ_ASSERT(!arena->onDelayedMarkingList());
   MOZ_ASSERT(arena->bufferedCells()->isEmpty());
 
   Zone* zone = arena->zone;
 
   AllocKind thingKind = arena->getAllocKind();
   size_t thingSize = arena->getThingSize();
 
   for (ArenaCellIterUnderGC i(arena); !i.done(); i.next()) {
--- a/js/src/gc/GCMarker.h
+++ b/js/src/gc/GCMarker.h
@@ -50,18 +50,18 @@ using WeakKeyTable =
 
 /*
  * When the mark stack is full, the GC does not call js::TraceChildren to mark
  * the reachable "children" of the thing. Rather the thing is put aside and
  * js::TraceChildren is called later when the mark stack is empty.
  *
  * To implement such delayed marking of the children with minimal overhead for
  * the normal case of sufficient stack, we link arenas into a list using
- * Arena::setNextDelayedMarking(). The head of the list is stored in
- * GCMarker::unmarkedArenaStackTop. GCMarker::delayMarkingChildren() adds arenas
+ * Arena::setNextDelayedMarkingArena(). The head of the list is stored in
+ * GCMarker::delayedMarkingList. GCMarker::delayMarkingChildren() adds arenas
  * to the list as necessary while markAllDelayedChildren() pops the arenas from
  * the stack until it is empty.
  */
 class MarkStack {
  public:
   /*
    * We use a common mark stack to mark GC things of different types and use
    * the explicit tags to distinguish them when it cannot be deduced from
@@ -289,24 +289,24 @@ class GCMarker : public JSTracer {
   void enterWeakMarkingMode();
   void leaveWeakMarkingMode();
   void abortLinearWeakMarking() {
     leaveWeakMarkingMode();
     linearWeakMarkingDisabled_ = true;
   }
 
   void delayMarkingArena(gc::Arena* arena);
-  void delayMarkingChildren(const void* thing);
+  void delayMarkingChildren(gc::Cell* cell);
   void markDelayedChildren(gc::Arena* arena, gc::MarkColor color);
   MOZ_MUST_USE bool markAllDelayedChildren(SliceBudget& budget);
-  bool processDelayedMarkingList(gc::Arena** outputList, gc::MarkColor color,
-                                 bool shouldYield, SliceBudget& budget);
-  bool hasDelayedChildren() const { return !!unmarkedArenaStackTop; }
+  bool processDelayedMarkingList(gc::MarkColor color, bool shouldYield,
+                                 SliceBudget& budget);
+  bool hasDelayedChildren() const { return !!delayedMarkingList; }
 
-  bool isDrained() { return isMarkStackEmpty() && !unmarkedArenaStackTop; }
+  bool isDrained() { return isMarkStackEmpty() && !delayedMarkingList; }
 
   MOZ_MUST_USE bool markUntilBudgetExhausted(SliceBudget& budget);
 
   void setGCMode(JSGCMode mode) { stack.setGCMode(mode); }
 
   size_t sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const;
 
 #ifdef DEBUG
@@ -379,27 +379,35 @@ class GCMarker : public JSTracer {
       const gc::MarkStack::SavedValueArray& savedArray);
 
   void saveValueRanges();
   gc::MarkStack::SavedValueArray saveValueRange(
       const gc::MarkStack::ValueArray& array);
 
   inline void processMarkStackTop(SliceBudget& budget);
 
+  void appendToDelayedMarkingList(gc::Arena** listTail, gc::Arena* arena);
+
+  template <typename F>
+  void forEachDelayedMarkingArena(F&& f);
+
   /* The mark stack. Pointers in this stack are "gray" in the GC sense. */
   gc::MarkStack stack;
 
   /* Stack entries at positions below this are considered gray. */
   MainThreadData<size_t> grayPosition;
 
   /* The color is only applied to objects and functions. */
   MainThreadData<gc::MarkColor> color;
 
   /* Pointer to the top of the stack of arenas we are delaying marking on. */
-  MainThreadData<js::gc::Arena*> unmarkedArenaStackTop;
+  MainThreadData<js::gc::Arena*> delayedMarkingList;
+
+  /* Whether more work has been added to the delayed marking list. */
+  MainThreadData<bool> delayedMarkingWorkAdded;
 
   /*
    * If the weakKeys table OOMs, disable the linear algorithm and fall back
    * to iterating until the next GC.
    */
   MainThreadData<bool> linearWeakMarkingDisabled_;
 
   /* The count of marked objects during GC. */
--- a/js/src/gc/Heap-inl.h
+++ b/js/src/gc/Heap-inl.h
@@ -12,25 +12,27 @@
 #include "gc/StoreBuffer.h"
 #include "gc/Zone.h"
 
 inline void js::gc::Arena::init(JS::Zone* zoneArg, AllocKind kind,
                                 const AutoLockGC& lock) {
   MOZ_ASSERT(firstFreeSpan.isEmpty());
   MOZ_ASSERT(!zone);
   MOZ_ASSERT(!allocated());
-  MOZ_ASSERT(!hasDelayedMarking);
-  MOZ_ASSERT(!auxNextLink);
+  MOZ_ASSERT(!onDelayedMarkingList_);
+  MOZ_ASSERT(!hasDelayedMarking_);
+  MOZ_ASSERT(!nextDelayedMarkingArena_);
 
   MOZ_MAKE_MEM_UNDEFINED(this, ArenaSize);
 
   zone = zoneArg;
   allocKind = size_t(kind);
-  hasDelayedMarking = 0;
-  auxNextLink = 0;
+  onDelayedMarkingList_ = 0;
+  hasDelayedMarking_ = 0;
+  nextDelayedMarkingArena_ = 0;
   if (zone->isAtomsZone()) {
     zone->runtimeFromAnyThread()->gc.atomMarking.registerArena(this, lock);
   } else {
     bufferedCells() = &ArenaCellSet::Empty;
   }
 
   setAsFullyUnused();
 }
--- a/js/src/gc/Heap.h
+++ b/js/src/gc/Heap.h
@@ -225,29 +225,29 @@ class Arena {
    * not contain any GC things and is on the list of empty arenas in the GC
    * chunk.
    *
    * We use 8 bits for the alloc kind so the compiler can use byte-level
    * memory instructions to access it.
    */
   size_t allocKind : 8;
 
- public:
+ private:
   /*
    * When recursive marking uses too much stack we delay marking of
    * arenas and link them into a list for later processing. This
    * uses the following fields.
    */
-  size_t hasDelayedMarking : 1;
-  size_t auxNextLink : JS_BITS_PER_WORD - 8 - 1;
-  static_assert(ArenaShift >= 8 + 1,
-                "Arena::auxNextLink packing assumes that ArenaShift has "
-                "enough bits to cover allocKind and hasDelayedMarking.");
+  size_t onDelayedMarkingList_ : 1;
+  size_t hasDelayedMarking_ : 1;
+  size_t nextDelayedMarkingArena_ : JS_BITS_PER_WORD - 8 - 1 - 1;
+  static_assert(ArenaShift >= 8 + 1 + 1,
+                "Arena::nextDelayedMarkingArena_ packing assumes that ArenaShift has "
+                "enough bits to cover allocKind and delayed marking state.");
 
- private:
   union {
     /*
      * For arenas in zones other than the atoms zone, if non-null, points
      * to an ArenaCellSet that represents the set of cells in this arena
      * that are in the nursery's store buffer.
      */
     ArenaCellSet* bufferedCells_;
 
@@ -282,18 +282,19 @@ class Arena {
   }
 
   // Initialize an arena to its unallocated state. For arenas that were
   // previously allocated for some zone, use release() instead.
   void setAsNotAllocated() {
     firstFreeSpan.initAsEmpty();
     zone = nullptr;
     allocKind = size_t(AllocKind::LIMIT);
-    hasDelayedMarking = 0;
-    auxNextLink = 0;
+    onDelayedMarkingList_ = 0;
+    hasDelayedMarking_ = 0;
+    nextDelayedMarkingArena_ = 0;
     bufferedCells_ = nullptr;
   }
 
   // Return an allocated arena to its unallocated state.
   inline void release(const AutoLockGC& lock);
 
   uintptr_t address() const {
     checkAddress();
@@ -382,34 +383,58 @@ class Arena {
   }
 
   static bool isAligned(uintptr_t thing, size_t thingSize) {
     /* Things ends at the arena end. */
     uintptr_t tailOffset = ArenaSize - (thing & ArenaMask);
     return tailOffset % thingSize == 0;
   }
 
+  bool onDelayedMarkingList() const {
+    return onDelayedMarkingList_;
+  }
+
   Arena* getNextDelayedMarking() const {
-    MOZ_ASSERT(hasDelayedMarking);
-    return reinterpret_cast<Arena*>(auxNextLink << ArenaShift);
+    MOZ_ASSERT(onDelayedMarkingList_);
+    return reinterpret_cast<Arena*>(nextDelayedMarkingArena_ << ArenaShift);
   }
 
-  void setNextDelayedMarking(Arena* arena) {
+  void setNextDelayedMarkingArena(Arena* arena) {
     MOZ_ASSERT(!(uintptr_t(arena) & ArenaMask));
-    MOZ_ASSERT(!auxNextLink && !hasDelayedMarking);
-    hasDelayedMarking = 1;
+    MOZ_ASSERT(!onDelayedMarkingList_);
+    MOZ_ASSERT(!hasDelayedMarking_);
+    MOZ_ASSERT(!nextDelayedMarkingArena_);
+    onDelayedMarkingList_ = 1;
+    hasDelayedMarking_ = 1;
     if (arena) {
-      auxNextLink = arena->address() >> ArenaShift;
+      nextDelayedMarkingArena_ = arena->address() >> ArenaShift;
     }
   }
 
-  void unsetDelayedMarking() {
-    MOZ_ASSERT(hasDelayedMarking);
-    hasDelayedMarking = 0;
-    auxNextLink = 0;
+  void updateNextDelayedMarkingArena(Arena* arena) {
+    MOZ_ASSERT(!(uintptr_t(arena) & ArenaMask));
+    MOZ_ASSERT(onDelayedMarkingList_);
+    nextDelayedMarkingArena_ = arena ? arena->address() >> ArenaShift : 0;
+  }
+
+  bool hasDelayedMarking() const {
+    MOZ_ASSERT(onDelayedMarkingList_);
+    return hasDelayedMarking_;
+  }
+
+  void setHasDelayedMarking(bool value) {
+    MOZ_ASSERT(onDelayedMarkingList_);
+    hasDelayedMarking_ = value;
+  }
+
+  void clearDelayedMarkingState() {
+    MOZ_ASSERT(onDelayedMarkingList_);
+    onDelayedMarkingList_ = 0;
+    hasDelayedMarking_ = 0;
+    nextDelayedMarkingArena_ = 0;
   }
 
   inline ArenaCellSet*& bufferedCells();
   inline size_t& atomBitmapStart();
 
   template <typename T>
   size_t finalize(FreeOp* fop, AllocKind thingKind, size_t thingSize);
 
--- a/js/src/gc/Marking.cpp
+++ b/js/src/gc/Marking.cpp
@@ -2362,17 +2362,18 @@ void MarkStackIter::saveValueArray(
  * of which will be consulted in a later phase whenever marking a potential
  * key.
  */
 GCMarker::GCMarker(JSRuntime* rt)
     : JSTracer(rt, JSTracer::TracerKindTag::Marking, ExpandWeakMaps),
       stack(),
       grayPosition(0),
       color(MarkColor::Black),
-      unmarkedArenaStackTop(nullptr)
+      delayedMarkingList(nullptr),
+      delayedMarkingWorkAdded(false)
 #ifdef DEBUG
       ,
       markLaterArenas(0),
       started(false),
       strictCompartmentChecking(false)
 #endif
 {
 }
@@ -2382,58 +2383,70 @@ bool GCMarker::init(JSGCMode gcMode) { r
 void GCMarker::start() {
 #ifdef DEBUG
   MOZ_ASSERT(!started);
   started = true;
 #endif
   color = MarkColor::Black;
   linearWeakMarkingDisabled_ = false;
 
-  MOZ_ASSERT(!unmarkedArenaStackTop);
+  MOZ_ASSERT(!delayedMarkingList);
   MOZ_ASSERT(markLaterArenas == 0);
 }
 
 void GCMarker::stop() {
 #ifdef DEBUG
   MOZ_ASSERT(isDrained());
 
   MOZ_ASSERT(started);
   started = false;
 
-  MOZ_ASSERT(!unmarkedArenaStackTop);
+  MOZ_ASSERT(!delayedMarkingList);
   MOZ_ASSERT(markLaterArenas == 0);
 #endif
 
   /* Free non-ballast stack memory. */
   stack.clear();
   AutoEnterOOMUnsafeRegion oomUnsafe;
   for (GCZonesIter zone(runtime()); !zone.done(); zone.next()) {
     if (!zone->gcWeakKeys().clear()) {
       oomUnsafe.crash("clearing weak keys in GCMarker::stop()");
     }
   }
 }
 
+template <typename F>
+inline void GCMarker::forEachDelayedMarkingArena(F&& f)
+{
+  Arena* arena = delayedMarkingList;
+  Arena* next;
+  while (arena) {
+    next = arena->getNextDelayedMarking();
+    f(arena);
+    arena = next;
+  }
+}
+
 void GCMarker::reset() {
   color = MarkColor::Black;
 
   stack.clear();
   MOZ_ASSERT(isMarkStackEmpty());
 
-  while (unmarkedArenaStackTop) {
-    Arena* arena = unmarkedArenaStackTop;
-    MOZ_ASSERT(arena->hasDelayedMarking);
-    MOZ_ASSERT(markLaterArenas);
-    unmarkedArenaStackTop = arena->getNextDelayedMarking();
-    arena->unsetDelayedMarking();
-
+  forEachDelayedMarkingArena(
+    [&](Arena* arena) {
+      MOZ_ASSERT(arena->onDelayedMarkingList());
+      arena->clearDelayedMarkingState();
 #ifdef DEBUG
-    markLaterArenas--;
+      MOZ_ASSERT(markLaterArenas);
+      markLaterArenas--;
 #endif
-  }
+    });
+  delayedMarkingList = nullptr;
+
   MOZ_ASSERT(isDrained());
   MOZ_ASSERT(!markLaterArenas);
 }
 
 void GCMarker::setMarkColor(gc::MarkColor newColor) {
   if (color == newColor) {
     return;
   }
@@ -2526,16 +2539,38 @@ void GCMarker::leaveWeakMarkingMode() {
   AutoEnterOOMUnsafeRegion oomUnsafe;
   for (GCZonesIter zone(runtime()); !zone.done(); zone.next()) {
     if (!zone->gcWeakKeys().clear()) {
       oomUnsafe.crash("clearing weak keys in GCMarker::leaveWeakMarkingMode()");
     }
   }
 }
 
+void GCMarker::delayMarkingChildren(Cell* cell) {
+  delayMarkingArena(cell->asTenured().arena());
+}
+
+void GCMarker::delayMarkingArena(Arena* arena) {
+  if (arena->onDelayedMarkingList()) {
+    // The arena is already on the delayed marking list, so just set a flag to
+    // ensure it gets processed again.
+    if (!arena->hasDelayedMarking()) {
+      arena->setHasDelayedMarking(true);
+      delayedMarkingWorkAdded = true;
+    }
+    return;
+  }
+  arena->setNextDelayedMarkingArena(delayedMarkingList);
+  delayedMarkingList = arena;
+  delayedMarkingWorkAdded = true;
+#ifdef DEBUG
+  markLaterArenas++;
+#endif
+}
+
 void GCMarker::markDelayedChildren(Arena* arena, MarkColor color) {
   JS::TraceKind kind = MapAllocToTraceKind(arena->getAllocKind());
   MOZ_ASSERT_IF(color == MarkColor::Gray, TraceKindParticipatesInCC(kind));
 
   for (ArenaCellIterUnderGC i(arena); !i.done(); i.next()) {
     TenuredCell* t = i.getCell();
     if ((color == MarkColor::Gray && t->isMarkedGray()) ||
         (color == MarkColor::Black && t->isMarkedBlack())) {
@@ -2545,108 +2580,127 @@ void GCMarker::markDelayedChildren(Arena
 }
 
 static inline bool ArenaCanHaveGrayThings(Arena* arena) {
   JS::TraceKind kind = MapAllocToTraceKind(arena->getAllocKind());
   return TraceKindParticipatesInCC(kind);
 }
 
 /*
- * Process arenas from |unmarkedArenaStackTop| and move them to
- * |*output| (if non-null) marking the unmarked children of marked
- * cells of color |color| if |shouldMarkArena| returns true. If
- * |shouldYield|, return early if the |budget| is exceeded.
+ * Process arenas from |delayedMarkingList| by marking the unmarked children of
+ * marked cells of color |color|. If |shouldYield|, return early if the |budget|
+ * is exceeded.
  *
- * This is called twice, first to mark gray children and then to mark
- * black children.
+ * This is called twice, first to mark gray children and then to mark black
+ * children.
  */
-bool GCMarker::processDelayedMarkingList(Arena** outputList, MarkColor color,
-                                         bool shouldYield,
+bool GCMarker::processDelayedMarkingList(MarkColor color, bool shouldYield,
                                          SliceBudget& budget) {
-  // If marking gets delayed at the same arena again, we must repeat marking
-  // of its things. Therefore we pop arena from the stack and clear its
-  // hasDelayedMarking flag before we begin the marking.
-
-  while (unmarkedArenaStackTop) {
-    Arena* arena = unmarkedArenaStackTop;
-    unmarkedArenaStackTop = arena->getNextDelayedMarking();
-
-    arena->unsetDelayedMarking();
-
-#ifdef DEBUG
-    MOZ_ASSERT(markLaterArenas);
-    if (!outputList) {
-      markLaterArenas--;
-    }
-#endif
-
-    if (color == MarkColor::Black ||
-        (color == MarkColor::Gray && ArenaCanHaveGrayThings(arena))) {
+  // Marking delayed children may add more arenas to the list, including arenas
+  // we are currently or have previously processed. Handle this by setting a
+  // flag on arenas we think we've processed which is cleared if they are
+  // re-added. Iterate the list until the flag is set on all arenas.
+
+  do {
+    delayedMarkingWorkAdded = false;
+    for (Arena* arena = delayedMarkingList;
+         arena;
+         arena = arena->getNextDelayedMarking()) {
+      if (!arena->hasDelayedMarking() ||
+          (color == MarkColor::Gray && !ArenaCanHaveGrayThings(arena))) {
+        continue;
+      }
+      arena->setHasDelayedMarking(false);
       markDelayedChildren(arena, color);
       budget.step(150);
       if (shouldYield && budget.isOverBudget()) {
         return false;
       }
     }
-
-    if (outputList) {
-      arena->setNextDelayedMarking(*outputList);
-      *outputList = arena;
-    }
-  }
+  } while (delayedMarkingWorkAdded);
 
   return true;
 }
 
 bool GCMarker::markAllDelayedChildren(SliceBudget& budget) {
   MOZ_ASSERT(!hasBlackEntries());
   MOZ_ASSERT(markColor() == MarkColor::Black);
 
   GCRuntime& gc = runtime()->gc;
   gcstats::AutoPhase ap(gc.stats(), gc.state() == State::Mark,
                         gcstats::PhaseKind::MARK_DELAYED);
 
-  // We don't know which mark color we were using when an arena was
-  // pushed onto the list so we mark children of marked things both
-  // colors in two passes over the list. Gray marking must be done
-  // first as gray entries always sit before black entries on the
-  // mark stack.
+  // We have a list of arenas containing marked cells with unmarked children
+  // where we ran out of stack space during marking.
+  //
+  // Both black and gray cells in these arenas may have unmarked children, and
+  // we must mark gray children first as gray entries always sit before black
+  // entries on the mark stack. Therefore the list is processed in two stages.
   //
-  // In order to guarantee progress here, the fist pass (gray
-  // marking) is done non-incrementally. We can't remove anything
-  // from the list until the second pass so if we yield during the
-  // first pass we will have to restart and process all the arenas
-  // over again. If there are enough arenas we may never finish
-  // during our timeslice. Disallowing yield during the first pass
-  // ensures that the list will at least shrink by one arena every
-  // time.
-
-  MOZ_ASSERT(unmarkedArenaStackTop);
-
-  Arena* processedList = nullptr;
+  // In order to guarantee progress here, the fist pass (gray marking) is done
+  // non-incrementally. We can't remove anything from the list until the second
+  // pass so if we yield during the first pass we will have to restart and
+  // process all the arenas over again. If there are enough arenas we may never
+  // finish during our timeslice. Disallowing yield during the first pass
+  // ensures that the list will at least shrink by one arena every time.
+
+  MOZ_ASSERT(delayedMarkingList);
+
   bool finished;
-  finished = processDelayedMarkingList(&processedList, MarkColor::Gray,
+  finished = processDelayedMarkingList(MarkColor::Gray,
                                        false, /* don't yield */
                                        budget);
   MOZ_ASSERT(finished);
 
-  unmarkedArenaStackTop = processedList;
-  finished = processDelayedMarkingList(nullptr, MarkColor::Black,
+  forEachDelayedMarkingArena(
+    [&](Arena* arena) {
+      MOZ_ASSERT(!arena->hasDelayedMarking());
+      arena->setHasDelayedMarking(true);
+    });
+
+  finished = processDelayedMarkingList(MarkColor::Black,
                                        true, /* yield if over budget */
                                        budget);
+
+  // Rebuild the list, removing processed arenas.
+  Arena* listTail = nullptr;
+  forEachDelayedMarkingArena(
+    [&](Arena* arena) {
+      if (!arena->hasDelayedMarking()) {
+        arena->clearDelayedMarkingState();
+#ifdef DEBUG
+        MOZ_ASSERT(markLaterArenas);
+        markLaterArenas--;
+#endif
+        return;
+      }
+
+      appendToDelayedMarkingList(&listTail, arena);
+    });
+  appendToDelayedMarkingList(&listTail, nullptr);
+
   if (!finished) {
     return false;
   }
 
-  MOZ_ASSERT(!unmarkedArenaStackTop);
+  MOZ_ASSERT(!delayedMarkingList);
   MOZ_ASSERT(!markLaterArenas);
 
   return true;
 }
 
+inline void GCMarker::appendToDelayedMarkingList(Arena** listTail, Arena* arena) {
+  if (*listTail) {
+    (*listTail)->updateNextDelayedMarkingArena(arena);
+  } else {
+    delayedMarkingList = arena;
+  }
+  *listTail = arena;
+}
+
 template <typename T>
 static void PushArenaTyped(GCMarker* gcmarker, Arena* arena) {
   for (ArenaCellIterUnderGC i(arena); !i.done(); i.next()) {
     gcmarker->traverse(i.get<T>());
   }
 }
 
 struct PushArenaFunctor {
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/gc/bug-1514927.js
@@ -0,0 +1,6 @@
+gczeal(0);
+var x = newGlobal();
+x.evaluate("grayRoot()");
+x = 0;
+gcparam("markStackLimit", 4);
+gc();