diff --git a/js/public/GCAPI.h b/js/public/GCAPI.h index b1fb098463bc..e58468efb5ee 100644 --- a/js/public/GCAPI.h +++ b/js/public/GCAPI.h @@ -386,6 +386,16 @@ using GCNurseryCollectionCallback = void(*)(JSContext* cx, GCNurseryProgress pro extern JS_PUBLIC_API(GCNurseryCollectionCallback) SetGCNurseryCollectionCallback(JSContext* cx, GCNurseryCollectionCallback callback); +typedef void +(* DoCycleCollectionCallback)(JSContext* cx); + +/** + * The purge gray callback is called after any COMPARTMENT_REVIVED GC in which + * the majority of compartments have been marked gray. + */ +extern JS_PUBLIC_API(DoCycleCollectionCallback) +SetDoCycleCollectionCallback(JSContext* cx, DoCycleCollectionCallback callback); + /** * Incremental GC defaults to enabled, but may be disabled for testing or in * embeddings that have not yet implemented barriers on their native classes. diff --git a/js/src/gc/GCRuntime.h b/js/src/gc/GCRuntime.h index 031789ba36ff..6f117ee216a5 100644 --- a/js/src/gc/GCRuntime.h +++ b/js/src/gc/GCRuntime.h @@ -778,6 +778,8 @@ class GCRuntime JS::GCSliceCallback setSliceCallback(JS::GCSliceCallback callback); JS::GCNurseryCollectionCallback setNurseryCollectionCallback( JS::GCNurseryCollectionCallback callback); + JS::DoCycleCollectionCallback setDoCycleCollectionCallback(JS::DoCycleCollectionCallback callback); + void callDoCycleCollectionCallback(JSContext* cx); void setFullCompartmentChecks(bool enable); @@ -947,6 +949,7 @@ class GCRuntime void traceRuntimeCommon(JSTracer* trc, TraceOrMarkRuntime traceOrMark, AutoLockForExclusiveAccess& lock); void bufferGrayRoots(); + void maybeDoCycleCollection(); void markCompartments(); IncrementalProgress drainMarkStack(SliceBudget& sliceBudget, gcstats::Phase phase); template void markWeakReferences(gcstats::Phase phase); @@ -1309,6 +1312,7 @@ class GCRuntime bool fullCompartmentChecks; Callback gcCallback; + Callback gcDoCycleCollectionCallback; Callback tenuredCallback; CallbackVector finalizeCallbacks; CallbackVector updateWeakPointerZoneGroupCallbacks; diff --git a/js/src/jsgc.cpp b/js/src/jsgc.cpp index dea1e27c7757..59e9fb15a1ba 100644 --- a/js/src/jsgc.cpp +++ b/js/src/jsgc.cpp @@ -1482,6 +1482,21 @@ GCRuntime::setNurseryCollectionCallback(JS::GCNurseryCollectionCallback callback return stats.setNurseryCollectionCallback(callback); } +JS::DoCycleCollectionCallback +GCRuntime::setDoCycleCollectionCallback(JS::DoCycleCollectionCallback callback) +{ + auto prior = gcDoCycleCollectionCallback; + gcDoCycleCollectionCallback = Callback(callback, nullptr); + return prior.op; +} + +void +GCRuntime::callDoCycleCollectionCallback(JSContext* cx) +{ + if (gcDoCycleCollectionCallback.op) + gcDoCycleCollectionCallback.op(cx); +} + bool GCRuntime::addRoot(Value* vp, const char* name) { @@ -6163,6 +6178,31 @@ GCRuntime::scanZonesBeforeGC() return zoneStats; } +// The GC can only clean up scheduledForDestruction compartments that were +// marked live by a barrier (e.g. by RemapWrappers from a navigation event). +// It is also common to have compartments held live because they are part of a +// cycle in gecko, e.g. involving the HTMLDocument wrapper. In this case, we +// need to run the CycleCollector in order to remove these edges before the +// compartment can be freed. +void +GCRuntime::maybeDoCycleCollection() +{ + const static double ExcessiveGrayCompartments = 0.8; + const static size_t LimitGrayCompartments = 200; + + size_t compartmentsTotal = 0; + size_t compartmentsGray = 0; + for (CompartmentsIter c(rt, SkipAtoms); !c.done(); c.next()) { + ++compartmentsTotal; + GlobalObject* global = c->unsafeUnbarrieredMaybeGlobal(); + if (global && global->asTenured().isMarked(GRAY)) + ++compartmentsGray; + } + double grayFraction = double(compartmentsGray) / double(compartmentsTotal); + if (grayFraction > ExcessiveGrayCompartments || compartmentsGray > LimitGrayCompartments) + callDoCycleCollectionCallback(rt->contextFromMainThread()); +} + void GCRuntime::checkCanCallAPI() { @@ -6243,6 +6283,9 @@ GCRuntime::collect(bool nonincrementalByAPI, SliceBudget budget, JS::gcreason::R repeat = (poked && cleanUpEverything) || wasReset || repeatForDeadZone; } while (repeat); + if (reason == JS::gcreason::COMPARTMENT_REVIVED) + maybeDoCycleCollection(); + #ifdef JS_GC_ZEAL if (shouldCompact() && rt->hasZealMode(ZealMode::CheckHeapOnMovingGC)) { gcstats::AutoPhase ap(rt->gc.stats, gcstats::PHASE_TRACE_HEAP); @@ -7206,6 +7249,12 @@ JS::SetGCSliceCallback(JSContext* cx, GCSliceCallback callback) return cx->gc.setSliceCallback(callback); } +JS_PUBLIC_API(JS::DoCycleCollectionCallback) +JS::SetDoCycleCollectionCallback(JSContext* cx, JS::DoCycleCollectionCallback callback) +{ + return cx->gc.setDoCycleCollectionCallback(callback); +} + JS_PUBLIC_API(JS::GCNurseryCollectionCallback) JS::SetGCNurseryCollectionCallback(JSContext* cx, GCNurseryCollectionCallback callback) { diff --git a/js/xpconnect/src/XPCJSRuntime.cpp b/js/xpconnect/src/XPCJSRuntime.cpp index 053cb202ba9d..f829d2c5227c 100644 --- a/js/xpconnect/src/XPCJSRuntime.cpp +++ b/js/xpconnect/src/XPCJSRuntime.cpp @@ -720,6 +720,21 @@ XPCJSRuntime::GCSliceCallback(JSContext* cx, (*self->mPrevGCSliceCallback)(cx, progress, desc); } +/* static */ void +XPCJSRuntime::DoCycleCollectionCallback(JSContext* cx) +{ + XPCJSRuntime* self = nsXPConnect::GetRuntimeInstance(); + if (!self) + return; + + // The GC has detected that a CC at this point would collect a tremendous + // amount of garbage that is being revivified unnecessarily. + nsJSContext::CycleCollectNow(); + + if (self->mPrevDoCycleCollectionCallback) + (*self->mPrevDoCycleCollectionCallback)(cx); +} + void XPCJSRuntime::CustomGCCallback(JSGCStatus status) { @@ -3502,6 +3517,8 @@ XPCJSRuntime::Initialize() JS_SetSizeOfIncludingThisCompartmentCallback(cx, CompartmentSizeOfIncludingThisCallback); JS_SetCompartmentNameCallback(cx, CompartmentNameCallback); mPrevGCSliceCallback = JS::SetGCSliceCallback(cx, GCSliceCallback); + mPrevDoCycleCollectionCallback = JS::SetDoCycleCollectionCallback(cx, + DoCycleCollectionCallback); JS_AddFinalizeCallback(cx, FinalizeCallback, nullptr); JS_AddWeakPointerZoneGroupCallback(cx, WeakPointerZoneGroupCallback, this); JS_AddWeakPointerCompartmentCallback(cx, WeakPointerCompartmentCallback, this); diff --git a/js/xpconnect/src/xpcprivate.h b/js/xpconnect/src/xpcprivate.h index 1a7ea19bc0ad..e81e1e4ad90f 100644 --- a/js/xpconnect/src/xpcprivate.h +++ b/js/xpconnect/src/xpcprivate.h @@ -551,6 +551,7 @@ public: static void GCSliceCallback(JSContext* cx, JS::GCProgress progress, const JS::GCDescription& desc); + static void DoCycleCollectionCallback(JSContext* cx); static void FinalizeCallback(JSFreeOp* fop, JSFinalizeStatus status, bool isCompartmentGC, @@ -628,6 +629,7 @@ private: nsTArray extraGCCallbacks; RefPtr mWatchdogManager; JS::GCSliceCallback mPrevGCSliceCallback; + JS::DoCycleCollectionCallback mPrevDoCycleCollectionCallback; JS::PersistentRootedObject mUnprivilegedJunkScope; JS::PersistentRootedObject mPrivilegedJunkScope; JS::PersistentRootedObject mCompilationScope;