summaryrefslogtreecommitdiffstats
path: root/js/src/gc/FinalizationObservers.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /js/src/gc/FinalizationObservers.cpp
parentInitial commit. (diff)
downloadfirefox-esr-upstream.tar.xz
firefox-esr-upstream.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--js/src/gc/FinalizationObservers.cpp509
1 files changed, 509 insertions, 0 deletions
diff --git a/js/src/gc/FinalizationObservers.cpp b/js/src/gc/FinalizationObservers.cpp
new file mode 100644
index 0000000000..3a7a114645
--- /dev/null
+++ b/js/src/gc/FinalizationObservers.cpp
@@ -0,0 +1,509 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: set ts=8 sts=2 et sw=2 tw=80:
+ * 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/. */
+
+/*
+ * GC support for FinalizationRegistry and WeakRef objects.
+ */
+
+#include "gc/FinalizationObservers.h"
+
+#include "mozilla/ScopeExit.h"
+
+#include "builtin/FinalizationRegistryObject.h"
+#include "builtin/WeakRefObject.h"
+#include "gc/GCRuntime.h"
+#include "gc/Zone.h"
+#include "vm/JSContext.h"
+
+#include "gc/WeakMap-inl.h"
+#include "vm/JSObject-inl.h"
+#include "vm/NativeObject-inl.h"
+
+using namespace js;
+using namespace js::gc;
+
+FinalizationObservers::FinalizationObservers(Zone* zone)
+ : zone(zone),
+ registries(zone),
+ recordMap(zone),
+ crossZoneRecords(zone),
+ weakRefMap(zone),
+ crossZoneWeakRefs(zone) {}
+
+FinalizationObservers::~FinalizationObservers() {
+ MOZ_ASSERT(registries.empty());
+ MOZ_ASSERT(recordMap.empty());
+ MOZ_ASSERT(crossZoneRecords.empty());
+ MOZ_ASSERT(crossZoneWeakRefs.empty());
+}
+
+bool GCRuntime::addFinalizationRegistry(
+ JSContext* cx, Handle<FinalizationRegistryObject*> registry) {
+ if (!cx->zone()->ensureFinalizationObservers() ||
+ !cx->zone()->finalizationObservers()->addRegistry(registry)) {
+ ReportOutOfMemory(cx);
+ return false;
+ }
+
+ return true;
+}
+
+bool FinalizationObservers::addRegistry(
+ Handle<FinalizationRegistryObject*> registry) {
+ return registries.put(registry);
+}
+
+bool GCRuntime::registerWithFinalizationRegistry(JSContext* cx,
+ HandleObject target,
+ HandleObject record) {
+ MOZ_ASSERT(!IsCrossCompartmentWrapper(target));
+ MOZ_ASSERT(
+ UncheckedUnwrapWithoutExpose(record)->is<FinalizationRecordObject>());
+ MOZ_ASSERT(target->compartment() == record->compartment());
+
+ Zone* zone = cx->zone();
+ if (!zone->ensureFinalizationObservers() ||
+ !zone->finalizationObservers()->addRecord(target, record)) {
+ ReportOutOfMemory(cx);
+ return false;
+ }
+
+ return true;
+}
+
+bool FinalizationObservers::addRecord(HandleObject target,
+ HandleObject record) {
+ // Add a record to the record map and clean up on failure.
+ //
+ // The following must be updated and kept in sync:
+ // - the zone's recordMap (to observe the target)
+ // - the registry's global objects's recordSet (to trace the record)
+ // - the count of cross zone records (to calculate sweep groups)
+
+ MOZ_ASSERT(target->zone() == zone);
+
+ FinalizationRecordObject* unwrappedRecord =
+ &UncheckedUnwrapWithoutExpose(record)->as<FinalizationRecordObject>();
+
+ Zone* registryZone = unwrappedRecord->zone();
+ bool crossZone = registryZone != zone;
+ if (crossZone && !addCrossZoneWrapper(crossZoneRecords, record)) {
+ return false;
+ }
+ auto wrapperGuard = mozilla::MakeScopeExit([&] {
+ if (crossZone) {
+ removeCrossZoneWrapper(crossZoneRecords, record);
+ }
+ });
+
+ GlobalObject* registryGlobal = &unwrappedRecord->global();
+ auto* globalData = registryGlobal->getOrCreateFinalizationRegistryData();
+ if (!globalData || !globalData->addRecord(unwrappedRecord)) {
+ return false;
+ }
+ auto globalDataGuard = mozilla::MakeScopeExit(
+ [&] { globalData->removeRecord(unwrappedRecord); });
+
+ auto ptr = recordMap.lookupForAdd(target);
+ if (!ptr && !recordMap.add(ptr, target, RecordVector(zone))) {
+ return false;
+ }
+
+ if (!ptr->value().append(record)) {
+ return false;
+ }
+
+ unwrappedRecord->setInRecordMap(true);
+
+ globalDataGuard.release();
+ wrapperGuard.release();
+ return true;
+}
+
+bool FinalizationObservers::addCrossZoneWrapper(WrapperWeakSet& weakSet,
+ JSObject* wrapper) {
+ MOZ_ASSERT(IsCrossCompartmentWrapper(wrapper));
+ MOZ_ASSERT(UncheckedUnwrapWithoutExpose(wrapper)->zone() != zone);
+
+ auto ptr = weakSet.lookupForAdd(wrapper);
+ MOZ_ASSERT(!ptr);
+ return weakSet.add(ptr, wrapper, UndefinedValue());
+}
+
+void FinalizationObservers::removeCrossZoneWrapper(WrapperWeakSet& weakSet,
+ JSObject* wrapper) {
+ MOZ_ASSERT(IsCrossCompartmentWrapper(wrapper));
+ MOZ_ASSERT(UncheckedUnwrapWithoutExpose(wrapper)->zone() != zone);
+
+ auto ptr = weakSet.lookupForAdd(wrapper);
+ MOZ_ASSERT(ptr);
+ weakSet.remove(ptr);
+}
+
+static FinalizationRecordObject* UnwrapFinalizationRecord(JSObject* obj) {
+ obj = UncheckedUnwrapWithoutExpose(obj);
+ if (!obj->is<FinalizationRecordObject>()) {
+ MOZ_ASSERT(JS_IsDeadWrapper(obj));
+ // CCWs between the compartments have been nuked. The
+ // FinalizationRegistry's callback doesn't run in this case.
+ return nullptr;
+ }
+ return &obj->as<FinalizationRecordObject>();
+}
+
+void FinalizationObservers::clearRecords() {
+ // Clear table entries related to FinalizationRecordObjects, which are not
+ // processed after the start of shutdown.
+ //
+ // WeakRefs are still updated during shutdown to avoid the possibility of
+ // stale or dangling pointers.
+
+#ifdef DEBUG
+ checkTables();
+#endif
+
+ recordMap.clear();
+ crossZoneRecords.clear();
+}
+
+void GCRuntime::traceWeakFinalizationObserverEdges(JSTracer* trc, Zone* zone) {
+ MOZ_ASSERT(CurrentThreadCanAccessRuntime(trc->runtime()));
+ FinalizationObservers* observers = zone->finalizationObservers();
+ if (observers) {
+ observers->traceWeakEdges(trc);
+ }
+}
+
+void FinalizationObservers::traceRoots(JSTracer* trc) {
+ // The cross-zone wrapper weak maps are traced as roots; this does not keep
+ // any of their entries alive by itself.
+ crossZoneRecords.trace(trc);
+ crossZoneWeakRefs.trace(trc);
+}
+
+void FinalizationObservers::traceWeakEdges(JSTracer* trc) {
+ // Removing dead pointers from vectors may reorder live pointers to gray
+ // things in the vector. This is OK.
+ AutoTouchingGrayThings atgt;
+
+ traceWeakWeakRefEdges(trc);
+ traceWeakFinalizationRegistryEdges(trc);
+}
+
+void FinalizationObservers::traceWeakFinalizationRegistryEdges(JSTracer* trc) {
+ // Sweep finalization registry data and queue finalization records for cleanup
+ // for any entries whose target is dying and remove them from the map.
+
+ GCRuntime* gc = &trc->runtime()->gc;
+
+ for (RegistrySet::Enum e(registries); !e.empty(); e.popFront()) {
+ auto result = TraceWeakEdge(trc, &e.mutableFront(), "FinalizationRegistry");
+ if (result.isDead()) {
+ auto* registry =
+ &result.initialTarget()->as<FinalizationRegistryObject>();
+ registry->queue()->setHasRegistry(false);
+ e.removeFront();
+ } else {
+ result.finalTarget()->as<FinalizationRegistryObject>().traceWeak(trc);
+ }
+ }
+
+ for (RecordMap::Enum e(recordMap); !e.empty(); e.popFront()) {
+ RecordVector& records = e.front().value();
+
+ // Sweep finalization records, updating any pointers moved by the GC and
+ // remove if necessary.
+ records.mutableEraseIf([&](HeapPtr<JSObject*>& heapPtr) {
+ auto result = TraceWeakEdge(trc, &heapPtr, "FinalizationRecord");
+ JSObject* obj =
+ result.isLive() ? result.finalTarget() : result.initialTarget();
+ FinalizationRecordObject* record = UnwrapFinalizationRecord(obj);
+ MOZ_ASSERT_IF(record, record->isInRecordMap());
+
+ bool shouldRemove = !result.isLive() || shouldRemoveRecord(record);
+ if (shouldRemove && record && record->isInRecordMap()) {
+ updateForRemovedRecord(obj, record);
+ }
+
+ return shouldRemove;
+ });
+
+#ifdef DEBUG
+ for (JSObject* obj : records) {
+ MOZ_ASSERT(UnwrapFinalizationRecord(obj)->isInRecordMap());
+ }
+#endif
+
+ // Queue finalization records for targets that are dying.
+ if (!TraceWeakEdge(trc, &e.front().mutableKey(),
+ "FinalizationRecord target")) {
+ for (JSObject* obj : records) {
+ FinalizationRecordObject* record = UnwrapFinalizationRecord(obj);
+ FinalizationQueueObject* queue = record->queue();
+ updateForRemovedRecord(obj, record);
+ queue->queueRecordToBeCleanedUp(record);
+ gc->queueFinalizationRegistryForCleanup(queue);
+ }
+ e.removeFront();
+ }
+ }
+}
+
+// static
+bool FinalizationObservers::shouldRemoveRecord(
+ FinalizationRecordObject* record) {
+ // Records are removed from the target's vector for the following reasons:
+ return !record || // Nuked CCW to record.
+ !record->isRegistered() || // Unregistered record.
+ !record->queue()->hasRegistry(); // Dead finalization registry.
+}
+
+void FinalizationObservers::updateForRemovedRecord(
+ JSObject* wrapper, FinalizationRecordObject* record) {
+ // Remove other references to a record when it has been removed from the
+ // zone's record map. See addRecord().
+ MOZ_ASSERT(record->isInRecordMap());
+
+ Zone* registryZone = record->zone();
+ if (registryZone != zone) {
+ removeCrossZoneWrapper(crossZoneRecords, wrapper);
+ }
+
+ GlobalObject* registryGlobal = &record->global();
+ auto* globalData = registryGlobal->maybeFinalizationRegistryData();
+ globalData->removeRecord(record);
+
+ // The removed record may be gray, and that's OK.
+ AutoTouchingGrayThings atgt;
+
+ record->setInRecordMap(false);
+}
+
+void GCRuntime::nukeFinalizationRecordWrapper(
+ JSObject* wrapper, FinalizationRecordObject* record) {
+ if (record->isInRecordMap()) {
+ FinalizationRegistryObject::unregisterRecord(record);
+ FinalizationObservers* observers = wrapper->zone()->finalizationObservers();
+ observers->updateForRemovedRecord(wrapper, record);
+ }
+}
+
+void GCRuntime::queueFinalizationRegistryForCleanup(
+ FinalizationQueueObject* queue) {
+ // Prod the embedding to call us back later to run the finalization callbacks,
+ // if necessary.
+
+ if (queue->isQueuedForCleanup()) {
+ return;
+ }
+
+ // Derive the incumbent global by unwrapping the incumbent global object and
+ // then getting its global.
+ JSObject* object = UncheckedUnwrapWithoutExpose(queue->incumbentObject());
+ MOZ_ASSERT(object);
+ GlobalObject* incumbentGlobal = &object->nonCCWGlobal();
+
+ callHostCleanupFinalizationRegistryCallback(queue->doCleanupFunction(),
+ incumbentGlobal);
+
+ // The queue object may be gray, and that's OK.
+ AutoTouchingGrayThings atgt;
+
+ queue->setQueuedForCleanup(true);
+}
+
+// Insert a target -> weakRef mapping in the target's Zone so that a dying
+// target will clear out the weakRef's target. If the weakRef is in a different
+// Zone, then the crossZoneWeakRefs table will keep the weakRef alive. If the
+// weakRef is in the same Zone, then it must be the actual WeakRefObject and
+// not a cross-compartment wrapper, since nothing would keep that alive.
+bool GCRuntime::registerWeakRef(HandleObject target, HandleObject weakRef) {
+ MOZ_ASSERT(!IsCrossCompartmentWrapper(target));
+ MOZ_ASSERT(UncheckedUnwrap(weakRef)->is<WeakRefObject>());
+ MOZ_ASSERT_IF(target->zone() != weakRef->zone(),
+ target->compartment() == weakRef->compartment());
+
+ Zone* zone = target->zone();
+ return zone->ensureFinalizationObservers() &&
+ zone->finalizationObservers()->addWeakRefTarget(target, weakRef);
+}
+
+bool FinalizationObservers::addWeakRefTarget(HandleObject target,
+ HandleObject weakRef) {
+ WeakRefObject* unwrappedWeakRef =
+ &UncheckedUnwrapWithoutExpose(weakRef)->as<WeakRefObject>();
+
+ Zone* weakRefZone = unwrappedWeakRef->zone();
+ bool crossZone = weakRefZone != zone;
+ if (crossZone && !addCrossZoneWrapper(crossZoneWeakRefs, weakRef)) {
+ return false;
+ }
+ auto wrapperGuard = mozilla::MakeScopeExit([&] {
+ if (crossZone) {
+ removeCrossZoneWrapper(crossZoneWeakRefs, weakRef);
+ }
+ });
+
+ auto ptr = weakRefMap.lookupForAdd(target);
+ if (!ptr && !weakRefMap.add(ptr, target, WeakRefHeapPtrVector(zone))) {
+ return false;
+ }
+
+ if (!ptr->value().emplaceBack(weakRef)) {
+ return false;
+ }
+
+ wrapperGuard.release();
+ return true;
+}
+
+static WeakRefObject* UnwrapWeakRef(JSObject* obj) {
+ MOZ_ASSERT(!JS_IsDeadWrapper(obj));
+ obj = UncheckedUnwrapWithoutExpose(obj);
+ return &obj->as<WeakRefObject>();
+}
+
+void FinalizationObservers::removeWeakRefTarget(
+ Handle<JSObject*> target, Handle<WeakRefObject*> weakRef) {
+ MOZ_ASSERT(target);
+
+ WeakRefHeapPtrVector& weakRefs = weakRefMap.lookup(target)->value();
+ JSObject* wrapper = nullptr;
+ weakRefs.eraseIf([weakRef, &wrapper](JSObject* obj) {
+ if (UnwrapWeakRef(obj) == weakRef) {
+ wrapper = obj;
+ return true;
+ }
+ return false;
+ });
+
+ MOZ_ASSERT(wrapper);
+ updateForRemovedWeakRef(wrapper, weakRef);
+}
+
+void GCRuntime::nukeWeakRefWrapper(JSObject* wrapper, WeakRefObject* weakRef) {
+ // WeakRef wrappers can exist independently of the ones we create for the
+ // weakRefMap so don't assume |wrapper| is in the same zone as the WeakRef
+ // target.
+ JSObject* target = weakRef->target();
+ if (!target) {
+ return;
+ }
+
+ FinalizationObservers* observers = target->zone()->finalizationObservers();
+ if (observers) {
+ observers->unregisterWeakRefWrapper(wrapper, weakRef);
+ }
+}
+
+void FinalizationObservers::unregisterWeakRefWrapper(JSObject* wrapper,
+ WeakRefObject* weakRef) {
+ JSObject* target = weakRef->target();
+ MOZ_ASSERT(target);
+
+ bool removed = false;
+ WeakRefHeapPtrVector& weakRefs = weakRefMap.lookup(target)->value();
+ weakRefs.eraseIf([wrapper, &removed](JSObject* obj) {
+ bool remove = obj == wrapper;
+ if (remove) {
+ removed = true;
+ }
+ return remove;
+ });
+
+ if (removed) {
+ updateForRemovedWeakRef(wrapper, weakRef);
+ }
+}
+
+void FinalizationObservers::updateForRemovedWeakRef(JSObject* wrapper,
+ WeakRefObject* weakRef) {
+ weakRef->clearTarget();
+
+ Zone* weakRefZone = weakRef->zone();
+ if (weakRefZone != zone) {
+ removeCrossZoneWrapper(crossZoneWeakRefs, wrapper);
+ }
+}
+
+void FinalizationObservers::traceWeakWeakRefEdges(JSTracer* trc) {
+ for (WeakRefMap::Enum e(weakRefMap); !e.empty(); e.popFront()) {
+ // If target is dying, clear the target field of all weakRefs, and remove
+ // the entry from the map.
+ auto result = TraceWeakEdge(trc, &e.front().mutableKey(), "WeakRef target");
+ if (result.isDead()) {
+ for (JSObject* obj : e.front().value()) {
+ updateForRemovedWeakRef(obj, UnwrapWeakRef(obj));
+ }
+ e.removeFront();
+ } else {
+ // Update the target field after compacting.
+ traceWeakWeakRefVector(trc, e.front().value(), result.finalTarget());
+ }
+ }
+}
+
+void FinalizationObservers::traceWeakWeakRefVector(
+ JSTracer* trc, WeakRefHeapPtrVector& weakRefs, JSObject* target) {
+ weakRefs.mutableEraseIf([&](HeapPtr<JSObject*>& obj) -> bool {
+ auto result = TraceWeakEdge(trc, &obj, "WeakRef");
+ if (result.isDead()) {
+ JSObject* wrapper = result.initialTarget();
+ updateForRemovedWeakRef(wrapper, UnwrapWeakRef(wrapper));
+ } else {
+ UnwrapWeakRef(result.finalTarget())->setTargetUnbarriered(target);
+ }
+ return result.isDead();
+ });
+}
+
+#ifdef DEBUG
+void FinalizationObservers::checkTables() const {
+ // Check all cross-zone wrappers are present in the appropriate table.
+ size_t recordCount = 0;
+ for (auto r = recordMap.all(); !r.empty(); r.popFront()) {
+ for (JSObject* object : r.front().value()) {
+ FinalizationRecordObject* record = UnwrapFinalizationRecord(object);
+ if (record && record->isInRecordMap() && record->zone() != zone) {
+ MOZ_ASSERT(crossZoneRecords.has(object));
+ recordCount++;
+ }
+ }
+ }
+ MOZ_ASSERT(crossZoneRecords.count() == recordCount);
+
+ size_t weakRefCount = 0;
+ for (auto r = weakRefMap.all(); !r.empty(); r.popFront()) {
+ for (JSObject* object : r.front().value()) {
+ WeakRefObject* weakRef = UnwrapWeakRef(object);
+ if (weakRef && weakRef->zone() != zone) {
+ MOZ_ASSERT(crossZoneWeakRefs.has(object));
+ weakRefCount++;
+ }
+ }
+ }
+ MOZ_ASSERT(crossZoneWeakRefs.count() == weakRefCount);
+}
+#endif
+
+FinalizationRegistryGlobalData::FinalizationRegistryGlobalData(Zone* zone)
+ : recordSet(zone) {}
+
+bool FinalizationRegistryGlobalData::addRecord(
+ FinalizationRecordObject* record) {
+ return recordSet.putNew(record);
+}
+
+void FinalizationRegistryGlobalData::removeRecord(
+ FinalizationRecordObject* record) {
+ MOZ_ASSERT_IF(!record->runtimeFromMainThread()->gc.isShuttingDown(),
+ recordSet.has(record));
+ recordSet.remove(record);
+}
+
+void FinalizationRegistryGlobalData::trace(JSTracer* trc) {
+ recordSet.trace(trc);
+}