Bug 1130439 - Trigger compacting GCs after the user becomes inactive r=smaug r=terrence
authorJon Coppeard <jcoppeard@mozilla.com>
Tue, 24 Feb 2015 09:40:02 +0000
changeset 230966 95425fd7c738e353500b33950ddaa7181e6161b3
parent 230965 670bdd1f10a783707b348f18ee3c74e27f160eae
child 230967 f7104b435b475d4c4ba6b3ff215694d091df8875
push id28341
push userkwierso@gmail.com
push dateFri, 27 Feb 2015 02:25:40 +0000
treeherdermozilla-central@93707c1b4edb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug, terrence
bugs1130439
milestone39.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 1130439 - Trigger compacting GCs after the user becomes inactive r=smaug r=terrence
dom/base/nsJSEnvironment.cpp
dom/base/nsJSEnvironment.h
js/public/GCAPI.h
js/src/jsfriendapi.cpp
js/src/jsgc.cpp
js/src/vm/Runtime.cpp
js/src/vm/Runtime.h
modules/libpref/init/all.js
--- a/dom/base/nsJSEnvironment.cpp
+++ b/dom/base/nsJSEnvironment.cpp
@@ -102,16 +102,20 @@ const size_t gStackSize = 8192;
 #define NS_SHRINK_GC_BUFFERS_DELAY  4000 // ms
 
 // The amount of time we wait from the first request to GC to actually
 // doing the first GC.
 #define NS_FIRST_GC_DELAY           10000 // ms
 
 #define NS_FULL_GC_DELAY            60000 // ms
 
+// The amount of time to wait from the user being idle to starting a shrinking
+// GC.
+#define NS_SHRINKING_GC_DELAY       15000 // ms
+
 // Maximum amount of time that should elapse between incremental GC slices
 #define NS_INTERSLICE_GC_DELAY      100 // ms
 
 // If we haven't painted in 100ms, we allow for a longer GC budget
 #define NS_INTERSLICE_GC_BUDGET     40 // ms
 
 // The amount of time we wait between a request to CC (after GC ran)
 // and doing the actual CC.
@@ -143,16 +147,17 @@ static const uint32_t kMaxICCDuration = 
 
 // Large value used to specify that a script should run essentially forever
 #define NS_UNLIMITED_SCRIPT_RUNTIME (0x40000000LL << 32)
 
 // if you add statics here, add them to the list in StartupJSEnvironment
 
 static nsITimer *sGCTimer;
 static nsITimer *sShrinkGCBuffersTimer;
+static nsITimer *sShrinkingGCTimer;
 static nsITimer *sCCTimer;
 static nsITimer *sICCTimer;
 static nsITimer *sFullGCTimer;
 static nsITimer *sInterSliceGCTimer;
 
 static TimeStamp sLastCCEndTime;
 
 static bool sCCLockedOut;
@@ -207,16 +212,17 @@ static int32_t sContextCount;
 
 static nsIScriptSecurityManager *sSecurityManager;
 
 // nsJSEnvironmentObserver observes the memory-pressure notifications
 // and forces a garbage collection and cycle collection when it happens, if
 // the appropriate pref is set.
 
 static bool sGCOnMemoryPressure;
+static bool sCompactOnUserInactive;
 
 // In testing, we call RunNextCollectorTimer() to ensure that the collectors are run more
 // aggressively than they would be in regular browsing. sExpensiveCollectorPokes keeps
 // us from triggering expensive full collections too frequently.
 static int32_t sExpensiveCollectorPokes = 0;
 static const int32_t kPokesBetweenExpensiveCollectorTriggers = 5;
 
 static PRTime
@@ -229,16 +235,17 @@ GetCollectionTimeDelta()
   sFirstCollectionTime = now;
   return 0;
 }
 
 static void
 KillTimers()
 {
   nsJSContext::KillGCTimer();
+  nsJSContext::KillShrinkingGCTimer();
   nsJSContext::KillShrinkGCBuffersTimer();
   nsJSContext::KillCCTimer();
   nsJSContext::KillICCTimer();
   nsJSContext::KillFullGCTimer();
   nsJSContext::KillInterSliceGCTimer();
 }
 
 // If we collected a substantial amount of cycles, poke the GC since more objects
@@ -261,32 +268,40 @@ public:
 };
 
 NS_IMPL_ISUPPORTS(nsJSEnvironmentObserver, nsIObserver)
 
 NS_IMETHODIMP
 nsJSEnvironmentObserver::Observe(nsISupports* aSubject, const char* aTopic,
                                  const char16_t* aData)
 {
-  if (sGCOnMemoryPressure && !nsCRT::strcmp(aTopic, "memory-pressure")) {
-    if(StringBeginsWith(nsDependentString(aData),
-                        NS_LITERAL_STRING("low-memory-ongoing"))) {
-      // Don't GC/CC if we are in an ongoing low-memory state since its very
-      // slow and it likely won't help us anyway.
-      return NS_OK;
-    }
-    nsJSContext::GarbageCollectNow(JS::gcreason::MEM_PRESSURE,
-                                   nsJSContext::NonIncrementalGC,
-                                   nsJSContext::ShrinkingGC);
-    nsJSContext::CycleCollectNow();
-    if (NeedsGCAfterCC()) {
+  if (!nsCRT::strcmp(aTopic, "memory-pressure")) {
+    if (sGCOnMemoryPressure) {
+      if(StringBeginsWith(nsDependentString(aData),
+                          NS_LITERAL_STRING("low-memory-ongoing"))) {
+        // Don't GC/CC if we are in an ongoing low-memory state since its very
+        // slow and it likely won't help us anyway.
+        return NS_OK;
+      }
       nsJSContext::GarbageCollectNow(JS::gcreason::MEM_PRESSURE,
                                      nsJSContext::NonIncrementalGC,
                                      nsJSContext::ShrinkingGC);
+      nsJSContext::CycleCollectNow();
+      if (NeedsGCAfterCC()) {
+        nsJSContext::GarbageCollectNow(JS::gcreason::MEM_PRESSURE,
+                                       nsJSContext::NonIncrementalGC,
+                                       nsJSContext::ShrinkingGC);
+      }
     }
+  } else if (!nsCRT::strcmp(aTopic, "user-interaction-inactive")) {
+    if (sCompactOnUserInactive) {
+      nsJSContext::PokeShrinkingGC();
+    }
+  } else if (!nsCRT::strcmp(aTopic, "user-interaction-active")) {
+    nsJSContext::KillShrinkingGCTimer();
   } else if (!nsCRT::strcmp(aTopic, "quit-application") ||
              !nsCRT::strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
     sShuttingDown = true;
     KillTimers();
   }
 
   return NS_OK;
 }
@@ -1279,22 +1294,21 @@ nsJSContext::GarbageCollectNow(JS::gcrea
 
   if (sCCLockedOut && aIncremental == IncrementalGC) {
     // We're in the middle of incremental GC. Do another slice.
     JS::PrepareForIncrementalGC(sRuntime);
     JS::IncrementalGCSlice(sRuntime, aReason, aSliceMillis);
     return;
   }
 
+  JSGCInvocationKind gckind = aShrinking == ShrinkingGC ? GC_SHRINK : GC_NORMAL;
   JS::PrepareForFullGC(sRuntime);
   if (aIncremental == IncrementalGC) {
-    MOZ_ASSERT(aShrinking == NonShrinkingGC);
-    JS::StartIncrementalGC(sRuntime, GC_NORMAL, aReason, aSliceMillis);
+    JS::StartIncrementalGC(sRuntime, gckind, aReason, aSliceMillis);
   } else {
-    JSGCInvocationKind gckind = aShrinking == ShrinkingGC ? GC_SHRINK : GC_NORMAL;
     JS::GCForReason(sRuntime, gckind, aReason);
   }
 }
 
 //static
 void
 nsJSContext::ShrinkGCBuffersNow()
 {
@@ -1793,16 +1807,26 @@ GCTimerFired(nsITimer *aTimer, void *aCl
 
 void
 ShrinkGCBuffersTimerFired(nsITimer *aTimer, void *aClosure)
 {
   nsJSContext::KillShrinkGCBuffersTimer();
   nsJSContext::ShrinkGCBuffersNow();
 }
 
+// static
+void
+ShrinkingGCTimerFired(nsITimer* aTimer, void* aClosure)
+{
+  nsJSContext::KillShrinkingGCTimer();
+  nsJSContext::GarbageCollectNow(JS::gcreason::USER_INACTIVE,
+                                 nsJSContext::IncrementalGC,
+                                 nsJSContext::ShrinkingGC);
+}
+
 static bool
 ShouldTriggerCC(uint32_t aSuspected)
 {
   return sNeedsFullCC ||
          aSuspected > NS_CC_PURPLE_LIMIT ||
          (aSuspected > NS_CC_FORCED_PURPLE_LIMIT &&
           TimeUntilNow(sLastCCEndTime) > NS_CC_FORCED);
 }
@@ -2027,16 +2051,36 @@ nsJSContext::PokeShrinkGCBuffers()
 
   sShrinkGCBuffersTimer->InitWithFuncCallback(ShrinkGCBuffersTimerFired, nullptr,
                                               NS_SHRINK_GC_BUFFERS_DELAY,
                                               nsITimer::TYPE_ONE_SHOT);
 }
 
 // static
 void
+nsJSContext::PokeShrinkingGC()
+{
+  if (sShrinkingGCTimer || sShuttingDown) {
+    return;
+  }
+
+  CallCreateInstance("@mozilla.org/timer;1", &sShrinkingGCTimer);
+
+  if (!sShrinkingGCTimer) {
+    // Failed to create timer (probably because we're in XPCOM shutdown)
+    return;
+  }
+
+  sShrinkingGCTimer->InitWithFuncCallback(ShrinkingGCTimerFired, nullptr,
+                                          NS_SHRINKING_GC_DELAY,
+                                          nsITimer::TYPE_ONE_SHOT);
+}
+
+// static
+void
 nsJSContext::MaybePokeCC()
 {
   if (sCCTimer || sICCTimer || sShuttingDown || !sHasRunGC) {
     return;
   }
 
   if (ShouldTriggerCC(nsCycleCollector_suspectedCount())) {
     sCCTimerFireCount = 0;
@@ -2088,16 +2132,26 @@ nsJSContext::KillShrinkGCBuffersTimer()
   if (sShrinkGCBuffersTimer) {
     sShrinkGCBuffersTimer->Cancel();
     NS_RELEASE(sShrinkGCBuffersTimer);
   }
 }
 
 //static
 void
+nsJSContext::KillShrinkingGCTimer()
+{
+  if (sShrinkingGCTimer) {
+    sShrinkingGCTimer->Cancel();
+    NS_RELEASE(sShrinkingGCTimer);
+  }
+}
+
+//static
+void
 nsJSContext::KillCCTimer()
 {
   sCCLockedOutTime = 0;
   if (sCCTimer) {
     sCCTimer->Cancel();
     NS_RELEASE(sCCTimer);
   }
 }
@@ -2277,17 +2331,17 @@ nsJSContext::LikelyShortLivingObjectCrea
 {
   ++sLikelyShortLivingObjectsNeedingGC;
 }
 
 void
 mozilla::dom::StartupJSEnvironment()
 {
   // initialize all our statics, so that we can restart XPCOM
-  sGCTimer = sFullGCTimer = sCCTimer = sICCTimer = nullptr;
+  sGCTimer = sShrinkingGCTimer = sFullGCTimer = sCCTimer = sICCTimer = nullptr;
   sCCLockedOut = false;
   sCCLockedOutTime = 0;
   sLastCCEndTime = TimeStamp();
   sHasRunGC = false;
   sPendingLoadCount = 0;
   sLoadingInProgress = false;
   sCCollectedWaitingForGC = 0;
   sCCollectedZonesWaitingForGC = 0;
@@ -2682,18 +2736,24 @@ nsJSContext::EnsureStatics()
   if (!obs) {
     MOZ_CRASH();
   }
 
   Preferences::AddBoolVarCache(&sGCOnMemoryPressure,
                                "javascript.options.gc_on_memory_pressure",
                                true);
 
+  Preferences::AddBoolVarCache(&sCompactOnUserInactive,
+                               "javascript.options.compact_on_user_inactive",
+                               true);
+
   nsIObserver* observer = new nsJSEnvironmentObserver();
   obs->AddObserver(observer, "memory-pressure", false);
+  obs->AddObserver(observer, "user-interaction-inactive", false);
+  obs->AddObserver(observer, "user-interaction-active", false);
   obs->AddObserver(observer, "quit-application", false);
   obs->AddObserver(observer, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
 
   // Bug 907848 - We need to explicitly get the nsIDOMScriptObjectFactory
   // service in order to force its constructor to run, which registers a
   // shutdown observer. It would be nice to make this more explicit and less
   // side-effect-y.
   nsCOMPtr<nsIDOMScriptObjectFactory> factory = do_GetService(kDOMScriptObjectFactoryCID);
--- a/dom/base/nsJSEnvironment.h
+++ b/dom/base/nsJSEnvironment.h
@@ -111,16 +111,19 @@ public:
   static void RunNextCollectorTimer();
 
   static void PokeGC(JS::gcreason::Reason aReason, int aDelay = 0);
   static void KillGCTimer();
 
   static void PokeShrinkGCBuffers();
   static void KillShrinkGCBuffersTimer();
 
+  static void PokeShrinkingGC();
+  static void KillShrinkingGCTimer();
+
   static void MaybePokeCC();
   static void KillCCTimer();
   static void KillICCTimer();
   static void KillFullGCTimer();
   static void KillInterSliceGCTimer();
 
   // Calling LikelyShortLivingObjectCreated() makes a GC more likely.
   static void LikelyShortLivingObjectCreated();
--- a/js/public/GCAPI.h
+++ b/js/public/GCAPI.h
@@ -94,17 +94,18 @@ namespace JS {
     D(SET_DOC_SHELL)                            \
     D(DOM_UTILS)                                \
     D(DOM_IPC)                                  \
     D(DOM_WORKER)                               \
     D(INTER_SLICE_GC)                           \
     D(REFRESH_FRAME)                            \
     D(FULL_GC_TIMER)                            \
     D(SHUTDOWN_CC)                              \
-    D(FINISH_LARGE_EVALUATE)
+    D(FINISH_LARGE_EVALUATE)                    \
+    D(USER_INACTIVE)
 
 namespace gcreason {
 
 /* GCReasons will end up looking like JSGC_MAYBEGC */
 enum Reason {
 #define MAKE_REASON(name) name,
     GCREASONS(MAKE_REASON)
 #undef MAKE_REASON
--- a/js/src/jsfriendapi.cpp
+++ b/js/src/jsfriendapi.cpp
@@ -359,17 +359,19 @@ js::AssertSameCompartment(JSObject *objA
 {
     MOZ_ASSERT(objA->compartment() == objB->compartment());
 }
 #endif
 
 JS_FRIEND_API(void)
 js::NotifyAnimationActivity(JSObject *obj)
 {
-    obj->compartment()->lastAnimationTime = PRMJ_Now();
+    int64_t timeNow = PRMJ_Now();
+    obj->compartment()->lastAnimationTime = timeNow;
+    obj->runtimeFromMainThread()->lastAnimationTime = timeNow;
 }
 
 JS_FRIEND_API(uint32_t)
 js::GetObjectSlotSpan(JSObject *obj)
 {
     return obj->as<NativeObject>().slotSpan();
 }
 
--- a/js/src/jsgc.cpp
+++ b/js/src/jsgc.cpp
@@ -1883,17 +1883,20 @@ ArenaLists::allocateFromArenaInner(JS::Z
     return thing;
 }
 
 /* Compacting GC */
 
 bool
 GCRuntime::shouldCompact()
 {
-    return invocationKind == GC_SHRINK && isCompactingGCEnabled();
+    // Compact on shrinking GC if enabled, but skip compacting in incremental
+    // GCs if we are currently animating.
+    return invocationKind == GC_SHRINK && isCompactingGCEnabled() &&
+        (!isIncremental || rt->lastAnimationTime + PRMJ_USEC_PER_SEC < PRMJ_Now());
 }
 
 void
 GCRuntime::disableCompactingGC()
 {
     MOZ_ASSERT(CurrentThreadCanAccessRuntime(rt));
     ++compactingDisabledCount;
 }
--- a/js/src/vm/Runtime.cpp
+++ b/js/src/vm/Runtime.cpp
@@ -207,17 +207,18 @@ JSRuntime::JSRuntime(JSRuntime *parentRu
     ctypesActivityCallback(nullptr),
     offthreadIonCompilationEnabled_(true),
     parallelParsingEnabled_(true),
 #ifdef DEBUG
     enteredPolicy(nullptr),
 #endif
     largeAllocationFailureCallback(nullptr),
     oomCallback(nullptr),
-    debuggerMallocSizeOf(ReturnZeroSize)
+    debuggerMallocSizeOf(ReturnZeroSize),
+    lastAnimationTime(0)
 {
     setGCStoreBufferPtr(&gc.storeBuffer);
 
     liveRuntimesCount++;
 
     /* Initialize infallibly first, so we can goto bad and JS_DestroyRuntime. */
     JS_INIT_CLIST(&onNewGlobalObjectWatchers);
 
--- a/js/src/vm/Runtime.h
+++ b/js/src/vm/Runtime.h
@@ -1412,16 +1412,19 @@ struct JSRuntime : public JS::shadow::Ru
         return (T *)onOutOfMemoryCanGC(p, newSize * sizeof(T));
     }
 
     /*
      * Debugger.Memory functions like takeCensus use this embedding-provided
      * function to assess the size of malloc'd blocks of memory.
      */
     mozilla::MallocSizeOf debuggerMallocSizeOf;
+
+    /* Last time at which an animation was played for this runtime. */
+    int64_t lastAnimationTime;
 };
 
 namespace js {
 
 // When entering JIT code, the calling JSContext* is stored into the thread's
 // PerThreadData. This function retrieves the JSContext with the pre-condition
 // that the caller is JIT code or C++ called directly from JIT code. This
 // function should not be called from arbitrary locations since the JSContext
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -1076,16 +1076,17 @@ pref("javascript.options.mem.high_water_
 pref("javascript.options.mem.max", -1);
 pref("javascript.options.mem.gc_per_compartment", true);
 pref("javascript.options.mem.gc_incremental", true);
 pref("javascript.options.mem.gc_incremental_slice_ms", 10);
 pref("javascript.options.mem.gc_compacting", true);
 pref("javascript.options.mem.log", false);
 pref("javascript.options.mem.notify", false);
 pref("javascript.options.gc_on_memory_pressure", true);
+pref("javascript.options.compact_on_user_inactive", true);
 
 pref("javascript.options.mem.gc_high_frequency_time_limit_ms", 1000);
 pref("javascript.options.mem.gc_high_frequency_low_limit_mb", 100);
 pref("javascript.options.mem.gc_high_frequency_high_limit_mb", 500);
 pref("javascript.options.mem.gc_high_frequency_heap_growth_max", 300);
 pref("javascript.options.mem.gc_high_frequency_heap_growth_min", 150);
 pref("javascript.options.mem.gc_low_frequency_heap_growth", 150);
 pref("javascript.options.mem.gc_dynamic_heap_growth", true);