Bug 1652962 - Split off FinalizationQueueObject from FinalizationRegistryObject r=sfink
authorJon Coppeard <jcoppeard@mozilla.com>
Sat, 18 Jul 2020 08:03:59 +0000
changeset 541115 d6950b175d80c84baa2c1b5b5702a7ecda6b60b2
parent 541114 6575e34851ec454177d04caadf28574a3f1ac99f
child 541116 1f396812eae85fe830cc9f5c54f470e74eafe720
push id37614
push userccoroiu@mozilla.com
push dateSun, 19 Jul 2020 09:04:14 +0000
treeherdermozilla-central@e785ebabf7e1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssfink
bugs1652962
milestone80.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 1652962 - Split off FinalizationQueueObject from FinalizationRegistryObject r=sfink This splits the FinalizationRegistryObject into two separate objects but doesn't change any behaviour. The objects both have strong edges to each other. Differential Revision: https://phabricator.services.mozilla.com/D83759
js/src/builtin/FinalizationRegistryObject.cpp
js/src/builtin/FinalizationRegistryObject.h
js/src/gc/FinalizationRegistry.cpp
js/src/gc/GCRuntime.h
--- a/js/src/builtin/FinalizationRegistryObject.cpp
+++ b/js/src/builtin/FinalizationRegistryObject.cpp
@@ -302,74 +302,46 @@ bool FinalizationRegistryObject::constru
   }
 
   RootedObject cleanupCallback(
       cx, ValueToCallable(cx, args.get(0), 1, NO_CONSTRUCT));
   if (!cleanupCallback) {
     return false;
   }
 
-  // It's problematic storing a CCW to a global in another compartment because
-  // you don't know how far to unwrap it to get the original object
-  // back. Instead store a CCW to a plain object in the same compartment as the
-  // global (this uses Object.prototype).
-  RootedObject incumbentObject(cx);
-  if (!GetObjectFromIncumbentGlobal(cx, &incumbentObject)) {
-    return false;
-  }
-
   Rooted<UniquePtr<ObjectWeakMap>> registrations(
       cx, cx->make_unique<ObjectWeakMap>(cx));
   if (!registrations) {
     return false;
   }
 
   Rooted<UniquePtr<FinalizationRecordSet>> activeRecords(
       cx, cx->make_unique<FinalizationRecordSet>(cx->zone()));
   if (!activeRecords) {
     return false;
   }
 
-  Rooted<UniquePtr<FinalizationRecordVector>> recordsToBeCleanedUp(
-      cx, cx->make_unique<FinalizationRecordVector>(cx->zone()));
-  if (!recordsToBeCleanedUp) {
-    return false;
-  }
-
-  HandlePropertyName funName = cx->names().empty;
-  RootedFunction doCleanupFunction(
-      cx, NewNativeFunction(cx, doCleanup, 0, funName,
-                            gc::AllocKind::FUNCTION_EXTENDED));
-  if (!doCleanupFunction) {
-    return false;
-  }
-
-  FinalizationRegistryObject* registry =
-      NewObjectWithClassProto<FinalizationRegistryObject>(cx, proto);
+  RootedFinalizationRegistryObject registry(
+      cx, NewObjectWithClassProto<FinalizationRegistryObject>(cx, proto));
   if (!registry) {
     return false;
   }
 
-  registry->initReservedSlot(CleanupCallbackSlot,
-                             ObjectValue(*cleanupCallback));
-  registry->initReservedSlot(IncumbentObjectSlot,
-                             ObjectValue(*incumbentObject));
   InitReservedSlot(registry, RegistrationsSlot, registrations.release(),
                    MemoryUse::FinalizationRegistryRegistrations);
   InitReservedSlot(registry, ActiveRecords, activeRecords.release(),
                    MemoryUse::FinalizationRegistryRecordSet);
-  InitReservedSlot(registry, RecordsToBeCleanedUpSlot,
-                   recordsToBeCleanedUp.release(),
-                   MemoryUse::FinalizationRegistryRecordVector);
-  registry->initReservedSlot(IsQueuedForCleanupSlot, BooleanValue(false));
-  registry->initReservedSlot(DoCleanupFunctionSlot,
-                             ObjectValue(*doCleanupFunction));
 
-  doCleanupFunction->setExtendedSlot(DoCleanupFunction_RegistrySlot,
-                                     ObjectValue(*registry));
+  FinalizationQueueObject* queue =
+      FinalizationQueueObject::create(cx, registry, cleanupCallback);
+  if (!queue) {
+    return false;
+  }
+
+  registry->initReservedSlot(QueueSlot, ObjectValue(*queue));
 
   if (!cx->runtime()->gc.addFinalizationRegistry(cx, registry)) {
     return false;
   }
 
   args.rval().setObject(*registry);
   return true;
 }
@@ -383,20 +355,16 @@ void FinalizationRegistryObject::trace(J
   // objects are weakly held and are not traced.
   if (ObjectWeakMap* registrations = registry->registrations()) {
     registrations->trace(trc);
   }
 
   // The active record set is weakly held and is not traced. For moving GC this
   // is updated in sweep(), which is called for all FinalizationRegistryObjects
   // in a zone.
-
-  if (FinalizationRecordVector* records = registry->recordsToBeCleanedUp()) {
-    records->trace(trc);
-  }
 }
 
 void FinalizationRegistryObject::sweep() {
   // Sweep the set of active records. These may die if CCWs to record objects
   // get nuked.
   MOZ_ASSERT(activeRecords());
   activeRecords()->sweep();
 
@@ -433,34 +401,24 @@ void FinalizationRegistryObject::finaliz
       record->clear();
     }
   }
 
   fop->delete_(obj, registry->registrations(),
                MemoryUse::FinalizationRegistryRegistrations);
   fop->delete_(obj, registry->activeRecords(),
                MemoryUse::FinalizationRegistryRecordSet);
-  fop->delete_(obj, registry->recordsToBeCleanedUp(),
-               MemoryUse::FinalizationRegistryRecordVector);
 }
 
-JSObject* FinalizationRegistryObject::cleanupCallback() const {
-  Value value = getReservedSlot(CleanupCallbackSlot);
+FinalizationQueueObject* FinalizationRegistryObject::queue() const {
+  Value value = getReservedSlot(QueueSlot);
   if (value.isUndefined()) {
     return nullptr;
   }
-  return &value.toObject();
-}
-
-JSObject* FinalizationRegistryObject::incumbentObject() const {
-  Value value = getReservedSlot(IncumbentObjectSlot);
-  if (value.isUndefined()) {
-    return nullptr;
-  }
-  return &value.toObject();
+  return &value.toObject().as<FinalizationQueueObject>();
 }
 
 ObjectWeakMap* FinalizationRegistryObject::registrations() const {
   Value value = getReservedSlot(RegistrationsSlot);
   if (value.isUndefined()) {
     return nullptr;
   }
   return static_cast<ObjectWeakMap*>(value.toPrivate());
@@ -469,50 +427,16 @@ ObjectWeakMap* FinalizationRegistryObjec
 FinalizationRecordSet* FinalizationRegistryObject::activeRecords() const {
   Value value = getReservedSlot(ActiveRecords);
   if (value.isUndefined()) {
     return nullptr;
   }
   return static_cast<FinalizationRecordSet*>(value.toPrivate());
 }
 
-FinalizationRecordVector* FinalizationRegistryObject::recordsToBeCleanedUp()
-    const {
-  Value value = getReservedSlot(RecordsToBeCleanedUpSlot);
-  if (value.isUndefined()) {
-    return nullptr;
-  }
-  return static_cast<FinalizationRecordVector*>(value.toPrivate());
-}
-
-bool FinalizationRegistryObject::isQueuedForCleanup() const {
-  return getReservedSlot(IsQueuedForCleanupSlot).toBoolean();
-}
-
-JSFunction* FinalizationRegistryObject::doCleanupFunction() const {
-  Value value = getReservedSlot(DoCleanupFunctionSlot);
-  if (value.isUndefined()) {
-    return nullptr;
-  }
-  return &value.toObject().as<JSFunction>();
-}
-
-void FinalizationRegistryObject::queueRecordToBeCleanedUp(
-    FinalizationRecordObject* record) {
-  AutoEnterOOMUnsafeRegion oomUnsafe;
-  if (!recordsToBeCleanedUp()->append(record)) {
-    oomUnsafe.crash("FinalizationRegistryObject::queueRecordsToBeCleanedUp");
-  }
-}
-
-void FinalizationRegistryObject::setQueuedForCleanup(bool value) {
-  MOZ_ASSERT(value != isQueuedForCleanup());
-  setReservedSlot(IsQueuedForCleanupSlot, BooleanValue(value));
-}
-
 // FinalizationRegistry.prototype.register(target, heldValue [, unregisterToken
 // ])
 // https://tc39.es/proposal-weakrefs/#sec-finalization-registry.prototype.register
 /* static */
 bool FinalizationRegistryObject::register_(JSContext* cx, unsigned argc,
                                            Value* vp) {
   CallArgs args = CallArgsFromVp(argc, vp);
 
@@ -797,69 +721,223 @@ bool FinalizationRegistryObject::cleanup
   RootedObject cleanupCallback(cx);
   if (!args.get(0).isUndefined()) {
     cleanupCallback = ValueToCallable(cx, args.get(0), -1, NO_CONSTRUCT);
     if (!cleanupCallback) {
       return false;
     }
   }
 
-  if (!cleanupQueuedRecords(cx, registry, cleanupCallback)) {
+  RootedFinalizationQueueObject queue(cx, registry->queue());
+  if (!FinalizationQueueObject::cleanupQueuedRecords(cx, queue,
+                                                     cleanupCallback)) {
     return false;
   }
 
   args.rval().setUndefined();
   return true;
 }
 
+///////////////////////////////////////////////////////////////////////////
+// FinalizationQueueObject
+
+// Bug 1600300: FinalizationQueueObject is foreground finalized so that
+// HeapPtr destructors never see referents with released arenas. When this is
+// fixed we may be able to make this background finalized again.
+const JSClass FinalizationQueueObject::class_ = {
+    "FinalizationQueue",
+    JSCLASS_HAS_RESERVED_SLOTS(SlotCount) | JSCLASS_FOREGROUND_FINALIZE,
+    &classOps_};
+
+const JSClassOps FinalizationQueueObject::classOps_ = {
+    nullptr,                            // addProperty
+    nullptr,                            // delProperty
+    nullptr,                            // enumerate
+    nullptr,                            // newEnumerate
+    nullptr,                            // resolve
+    nullptr,                            // mayResolve
+    FinalizationQueueObject::finalize,  // finalize
+    nullptr,                            // call
+    nullptr,                            // hasInstance
+    nullptr,                            // construct
+    FinalizationQueueObject::trace,     // trace
+};
+
 /* static */
-bool FinalizationRegistryObject::doCleanup(JSContext* cx, unsigned argc,
-                                           Value* vp) {
+FinalizationQueueObject* FinalizationQueueObject::create(
+    JSContext* cx, HandleFinalizationRegistryObject registry,
+    HandleObject cleanupCallback) {
+  MOZ_ASSERT(registry);
+  MOZ_ASSERT(cleanupCallback);
+
+  Rooted<UniquePtr<FinalizationRecordVector>> recordsToBeCleanedUp(
+      cx, cx->make_unique<FinalizationRecordVector>(cx->zone()));
+  if (!recordsToBeCleanedUp) {
+    return nullptr;
+  }
+
+  HandlePropertyName funName = cx->names().empty;
+  RootedFunction doCleanupFunction(
+      cx, NewNativeFunction(cx, doCleanup, 0, funName,
+                            gc::AllocKind::FUNCTION_EXTENDED));
+  if (!doCleanupFunction) {
+    return nullptr;
+  }
+
+  // It's problematic storing a CCW to a global in another compartment because
+  // you don't know how far to unwrap it to get the original object
+  // back. Instead store a CCW to a plain object in the same compartment as the
+  // global (this uses Object.prototype).
+  RootedObject incumbentObject(cx);
+  if (!GetObjectFromIncumbentGlobal(cx, &incumbentObject)) {
+    return nullptr;
+  }
+
+  FinalizationQueueObject* queue =
+      NewObjectWithGivenProto<FinalizationQueueObject>(cx, nullptr);
+  if (!queue) {
+    return nullptr;
+  }
+
+  queue->initReservedSlot(RegistrySlot, ObjectValue(*registry));
+  queue->initReservedSlot(CleanupCallbackSlot, ObjectValue(*cleanupCallback));
+  queue->initReservedSlot(IncumbentObjectSlot, ObjectValue(*incumbentObject));
+  InitReservedSlot(queue, RecordsToBeCleanedUpSlot,
+                   recordsToBeCleanedUp.release(),
+                   MemoryUse::FinalizationRegistryRecordVector);
+  queue->initReservedSlot(IsQueuedForCleanupSlot, BooleanValue(false));
+  queue->initReservedSlot(DoCleanupFunctionSlot,
+                          ObjectValue(*doCleanupFunction));
+
+  doCleanupFunction->setExtendedSlot(DoCleanupFunction_QueueSlot,
+                                     ObjectValue(*queue));
+
+  return queue;
+}
+
+/* static */
+void FinalizationQueueObject::trace(JSTracer* trc, JSObject* obj) {
+  auto queue = &obj->as<FinalizationQueueObject>();
+
+  if (FinalizationRecordVector* records = queue->recordsToBeCleanedUp()) {
+    records->trace(trc);
+  }
+}
+
+/* static */
+void FinalizationQueueObject::finalize(JSFreeOp* fop, JSObject* obj) {
+  auto queue = &obj->as<FinalizationQueueObject>();
+
+  fop->delete_(obj, queue->recordsToBeCleanedUp(),
+               MemoryUse::FinalizationRegistryRecordVector);
+}
+
+inline FinalizationRegistryObject* FinalizationQueueObject::registry() const {
+  Value value = getReservedSlot(RegistrySlot);
+  if (value.isUndefined()) {
+    return nullptr;
+  }
+  return static_cast<FinalizationRegistryObject*>(&value.toObject());
+}
+
+inline JSObject* FinalizationQueueObject::cleanupCallback() const {
+  Value value = getReservedSlot(CleanupCallbackSlot);
+  if (value.isUndefined()) {
+    return nullptr;
+  }
+  return &value.toObject();
+}
+
+JSObject* FinalizationQueueObject::incumbentObject() const {
+  Value value = getReservedSlot(IncumbentObjectSlot);
+  if (value.isUndefined()) {
+    return nullptr;
+  }
+  return &value.toObject();
+}
+
+FinalizationRecordVector* FinalizationQueueObject::recordsToBeCleanedUp()
+    const {
+  Value value = getReservedSlot(RecordsToBeCleanedUpSlot);
+  if (value.isUndefined()) {
+    return nullptr;
+  }
+  return static_cast<FinalizationRecordVector*>(value.toPrivate());
+}
+
+bool FinalizationQueueObject::isQueuedForCleanup() const {
+  return getReservedSlot(IsQueuedForCleanupSlot).toBoolean();
+}
+
+JSFunction* FinalizationQueueObject::doCleanupFunction() const {
+  Value value = getReservedSlot(DoCleanupFunctionSlot);
+  if (value.isUndefined()) {
+    return nullptr;
+  }
+  return &value.toObject().as<JSFunction>();
+}
+
+void FinalizationQueueObject::queueRecordToBeCleanedUp(
+    FinalizationRecordObject* record) {
+  AutoEnterOOMUnsafeRegion oomUnsafe;
+  if (!recordsToBeCleanedUp()->append(record)) {
+    oomUnsafe.crash("FinalizationQueueObject::queueRecordsToBeCleanedUp");
+  }
+}
+
+void FinalizationQueueObject::setQueuedForCleanup(bool value) {
+  MOZ_ASSERT(value != isQueuedForCleanup());
+  setReservedSlot(IsQueuedForCleanupSlot, BooleanValue(value));
+}
+
+/* static */
+bool FinalizationQueueObject::doCleanup(JSContext* cx, unsigned argc,
+                                        Value* vp) {
   CallArgs args = CallArgsFromVp(argc, vp);
 
   RootedFunction callee(cx, &args.callee().as<JSFunction>());
 
-  Value value = callee->getExtendedSlot(DoCleanupFunction_RegistrySlot);
-  RootedFinalizationRegistryObject registry(
-      cx, &value.toObject().as<FinalizationRegistryObject>());
+  Value value = callee->getExtendedSlot(DoCleanupFunction_QueueSlot);
+  RootedFinalizationQueueObject queue(
+      cx, &value.toObject().as<FinalizationQueueObject>());
 
-  registry->setQueuedForCleanup(false);
-  return cleanupQueuedRecords(cx, registry);
+  queue->setQueuedForCleanup(false);
+  return cleanupQueuedRecords(cx, queue);
 }
 
 // CleanupFinalizationRegistry ( finalizationRegistry [ , callback ] )
 // https://tc39.es/proposal-weakrefs/#sec-cleanup-finalization-registry
 /* static */
-bool FinalizationRegistryObject::cleanupQueuedRecords(
-    JSContext* cx, HandleFinalizationRegistryObject registry,
+bool FinalizationQueueObject::cleanupQueuedRecords(
+    JSContext* cx, HandleFinalizationQueueObject queue,
     HandleObject callbackArg) {
-  MOZ_ASSERT(cx->compartment() == registry->compartment());
+  MOZ_ASSERT(cx->compartment() == queue->compartment());
 
   // 2. If callback is undefined, set callback to
   //    finalizationRegistry.[[CleanupCallback]].
   RootedValue callback(cx);
   if (callbackArg) {
     callback.setObject(*callbackArg);
   } else {
-    JSObject* cleanupCallback = registry->cleanupCallback();
+    JSObject* cleanupCallback = queue->cleanupCallback();
     MOZ_ASSERT(cleanupCallback);
     callback.setObject(*cleanupCallback);
   }
 
   // 3. While finalizationRegistry.[[Cells]] contains a Record cell such that
   //    cell.[[WeakRefTarget]] is empty, then an implementation may perform the
   //    following steps,
   //    a. Choose any such cell.
   //    b. Remove cell from finalizationRegistry.[[Cells]].
   //    c. Perform ? Call(callback, undefined, « cell.[[HeldValue]] »).
 
   RootedValue heldValue(cx);
   RootedValue rval(cx);
-  FinalizationRecordVector* records = registry->recordsToBeCleanedUp();
-  FinalizationRecordSet* activeRecords = registry->activeRecords();
+  FinalizationRecordVector* records = queue->recordsToBeCleanedUp();
+  FinalizationRecordSet* activeRecords = queue->registry()->activeRecords();
   while (!records->empty()) {
     FinalizationRecordObject* record = records->popCopy();
 
     // Skip over records that have been unregistered.
     if (!record->isActive()) {
       continue;
     }
 
--- a/js/src/builtin/FinalizationRegistryObject.h
+++ b/js/src/builtin/FinalizationRegistryObject.h
@@ -80,22 +80,25 @@
 #include "gc/Barrier.h"
 #include "js/GCVector.h"
 #include "vm/NativeObject.h"
 
 namespace js {
 
 class FinalizationRegistryObject;
 class FinalizationRecordObject;
+class FinalizationQueueObject;
 class ObjectWeakMap;
 
 using HandleFinalizationRegistryObject = Handle<FinalizationRegistryObject*>;
 using HandleFinalizationRecordObject = Handle<FinalizationRecordObject*>;
+using HandleFinalizationQueueObject = Handle<FinalizationQueueObject*>;
 using RootedFinalizationRegistryObject = Rooted<FinalizationRegistryObject*>;
 using RootedFinalizationRecordObject = Rooted<FinalizationRecordObject*>;
+using RootedFinalizationQueueObject = Rooted<FinalizationQueueObject*>;
 
 // A finalization record: a pair of finalization registry and held value.
 //
 // A finalization record represents the registered interest of a finalization
 // registry in a target's finalization.
 //
 // Finalization records are initially 'active' but may be cleared and become
 // inactive. This happens when:
@@ -103,17 +106,16 @@ using RootedFinalizationRecordObject = R
 //  - the registry's unregister method removes the registration
 //  - the FinalizationRegistry dies
 class FinalizationRecordObject : public NativeObject {
   enum { WeakRegistrySlot = 0, HeldValueSlot, SlotCount };
 
  public:
   static const JSClass class_;
 
-  // The registry can be a CCW to a FinalizationRegistryObject.
   static FinalizationRecordObject* create(
       JSContext* cx, HandleFinalizationRegistryObject registry,
       HandleValue heldValue);
 
   // Read weak registry pointer and perform read barrier during GC.
   FinalizationRegistryObject* registryDuringGC(gc::GCRuntime* gc) const;
 
   FinalizationRegistryObject* registryUnbarriered() const;
@@ -166,56 +168,35 @@ class FinalizationRegistrationsObject : 
 };
 
 using FinalizationRecordVector =
     GCVector<HeapPtr<FinalizationRecordObject*>, 1, js::ZoneAllocPolicy>;
 
 using FinalizationRecordSet =
     GCHashSet<HeapPtrObject, MovableCellHasher<HeapPtrObject>, ZoneAllocPolicy>;
 
-// The FinalizationRegistry object itself.
+// The JS FinalizationRegistry object itself.
 class FinalizationRegistryObject : public NativeObject {
-  enum {
-    CleanupCallbackSlot = 0,
-    IncumbentObjectSlot,
-    RegistrationsSlot,
-    ActiveRecords,
-    RecordsToBeCleanedUpSlot,
-    IsQueuedForCleanupSlot,
-    DoCleanupFunctionSlot,
-    SlotCount
-  };
-
-  enum DoCleanupFunctionSlots {
-    DoCleanupFunction_RegistrySlot = 0,
-  };
+  enum { QueueSlot = 0, RegistrationsSlot, ActiveRecords, SlotCount };
 
  public:
   static const JSClass class_;
   static const JSClass protoClass_;
 
-  JSObject* cleanupCallback() const;
-  JSObject* incumbentObject() const;
+  FinalizationQueueObject* queue() const;
   ObjectWeakMap* registrations() const;
   FinalizationRecordSet* activeRecords() const;
-  FinalizationRecordVector* recordsToBeCleanedUp() const;
-  bool isQueuedForCleanup() const;
-  JSFunction* doCleanupFunction() const;
-
-  void queueRecordToBeCleanedUp(FinalizationRecordObject* record);
-  void setQueuedForCleanup(bool value);
 
   void sweep();
 
   static bool unregisterRecord(FinalizationRecordObject* record);
 
   static bool cleanupQueuedRecords(JSContext* cx,
                                    HandleFinalizationRegistryObject registry,
                                    HandleObject callback = nullptr);
-
  private:
   static const JSClassOps classOps_;
   static const ClassSpec classSpec_;
   static const JSFunctionSpec methods_[];
   static const JSPropertySpec properties_[];
 
   static bool construct(JSContext* cx, unsigned argc, Value* vp);
   static bool register_(JSContext* cx, unsigned argc, Value* vp);
@@ -227,16 +208,61 @@ class FinalizationRegistryObject : publi
                               HandleObject unregisterToken,
                               HandleFinalizationRecordObject record);
   static void removeRegistrationOnError(
       HandleFinalizationRegistryObject registry, HandleObject unregisterToken,
       HandleFinalizationRecordObject record);
 
   static bool preserveDOMWrapper(JSContext* cx, HandleObject obj);
 
+  static void trace(JSTracer* trc, JSObject* obj);
+  static void finalize(JSFreeOp* fop, JSObject* obj);
+};
+
+// Contains information about the cleanup callback and the records queued to
+// be cleaned up. This is not exposed to content JS.
+class FinalizationQueueObject : public NativeObject {
+  enum {
+    RegistrySlot = 0,
+    CleanupCallbackSlot,
+    IncumbentObjectSlot,
+    RecordsToBeCleanedUpSlot,
+    IsQueuedForCleanupSlot,
+    DoCleanupFunctionSlot,
+    SlotCount
+  };
+
+  enum DoCleanupFunctionSlots {
+    DoCleanupFunction_QueueSlot = 0,
+  };
+
+ public:
+  static const JSClass class_;
+
+  FinalizationRegistryObject* registry() const;
+  JSObject* cleanupCallback() const;
+  JSObject* incumbentObject() const;
+  FinalizationRecordVector* recordsToBeCleanedUp() const;
+  bool isQueuedForCleanup() const;
+  JSFunction* doCleanupFunction() const;
+
+  void queueRecordToBeCleanedUp(FinalizationRecordObject* record);
+  void setQueuedForCleanup(bool value);
+
+  static FinalizationQueueObject* create(
+      JSContext* cx, HandleFinalizationRegistryObject registry,
+      HandleObject cleanupCallback);
+
+  static bool cleanupQueuedRecords(JSContext* cx,
+                                   HandleFinalizationQueueObject registry,
+                                   HandleObject callback = nullptr);
+
+ private:
+  static const JSClassOps classOps_;
+
   static bool doCleanup(JSContext* cx, unsigned argc, Value* vp);
 
   static void trace(JSTracer* trc, JSObject* obj);
   static void finalize(JSFreeOp* fop, JSObject* obj);
 };
 
 }  // namespace js
 
--- a/js/src/gc/FinalizationRegistry.cpp
+++ b/js/src/gc/FinalizationRegistry.cpp
@@ -102,37 +102,37 @@ void GCRuntime::sweepFinalizationRegistr
                                      // group.
     });
 
     // Queue finalization records for targets that are dying.
     if (IsAboutToBeFinalized(&e.front().mutableKey())) {
       for (JSObject* obj : records) {
         FinalizationRecordObject* record = UnwrapFinalizationRecord(obj);
         FinalizationRegistryObject* registry = record->registryDuringGC(this);
-        registry->queueRecordToBeCleanedUp(record);
-        queueFinalizationRegistryForCleanup(registry);
+        FinalizationQueueObject* queue = registry->queue();
+        queue->queueRecordToBeCleanedUp(record);
+        queueFinalizationRegistryForCleanup(queue);
       }
       e.removeFront();
     }
   }
 }
 
 void GCRuntime::queueFinalizationRegistryForCleanup(
-    FinalizationRegistryObject* registry) {
+    FinalizationQueueObject* queue) {
   // Prod the embedding to call us back later to run the finalization callbacks,
   // if necessary.
 
-  if (registry->isQueuedForCleanup()) {
+  if (queue->isQueuedForCleanup()) {
     return;
   }
 
   // Derive the incumbent global by unwrapping the incumbent global object and
   // then getting its global.
-  JSObject* object =
-      UncheckedUnwrapWithoutExpose(registry->incumbentObject());
+  JSObject* object = UncheckedUnwrapWithoutExpose(queue->incumbentObject());
   MOZ_ASSERT(object);
   GlobalObject* incumbentGlobal = &object->nonCCWGlobal();
 
-  callHostCleanupFinalizationRegistryCallback(registry->doCleanupFunction(),
+  callHostCleanupFinalizationRegistryCallback(queue->doCleanupFunction(),
                                               incumbentGlobal);
 
-  registry->setQueuedForCleanup(true);
+  queue->setQueuedForCleanup(true);
 }
--- a/js/src/gc/GCRuntime.h
+++ b/js/src/gc/GCRuntime.h
@@ -26,16 +26,17 @@
 
 namespace js {
 
 class AutoAccessAtomsZone;
 class AutoLockGC;
 class AutoLockGCBgAlloc;
 class AutoLockHelperThreadState;
 class FinalizationRegistryObject;
+class FinalizationQueueObject;
 class VerifyPreTracer;
 class WeakRefObject;
 class ZoneAllocator;
 
 namespace gc {
 
 using BlackGrayEdgeVector = Vector<TenuredCell*, 0, SystemAllocPolicy>;
 using ZoneVector = Vector<JS::Zone*, 4, SystemAllocPolicy>;
@@ -733,18 +734,17 @@ class GCRuntime {
   void sweepMisc();
   void sweepCompressionTasks();
   void sweepWeakMaps();
   void sweepUniqueIds();
   void sweepDebuggerOnMainThread(JSFreeOp* fop);
   void sweepJitDataOnMainThread(JSFreeOp* fop);
   void sweepFinalizationRegistriesOnMainThread();
   void sweepFinalizationRegistries(Zone* zone);
-  void queueFinalizationRegistryForCleanup(
-      FinalizationRegistryObject* registry);
+  void queueFinalizationRegistryForCleanup(FinalizationQueueObject* queue);
   void sweepWeakRefs();
   IncrementalProgress endSweepingSweepGroup(JSFreeOp* fop, SliceBudget& budget);
   IncrementalProgress performSweepActions(SliceBudget& sliceBudget);
   IncrementalProgress sweepTypeInformation(JSFreeOp* fop, SliceBudget& budget);
   IncrementalProgress releaseSweptEmptyArenas(JSFreeOp* fop,
                                               SliceBudget& budget);
   void startSweepingAtomsTable();
   IncrementalProgress sweepAtomsTable(JSFreeOp* fop, SliceBudget& budget);