Bug 718100 - 'Web workers should GC more'. r=mrbkap.
authorBen Turner <bent.mozilla@gmail.com>
Tue, 17 Jan 2012 12:05:25 -0800
changeset 85994 7080b6f34d32d71d3cdd2bc4fb5695d5e550e5f2
parent 85993 2cf8cd8656321a70bef63f9042dfeab9bb660c8c
child 85995 04890cad686c7ab392a27a7db897b18cbe269682
push id805
push userakeybl@mozilla.com
push dateWed, 01 Feb 2012 18:17:35 +0000
treeherdermozilla-aurora@6fb3bf232436 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmrbkap
bugs718100
milestone12.0a1
Bug 718100 - 'Web workers should GC more'. r=mrbkap.
dom/workers/RuntimeService.cpp
dom/workers/RuntimeService.h
dom/workers/WorkerPrivate.cpp
dom/workers/WorkerPrivate.h
--- a/dom/workers/RuntimeService.cpp
+++ b/dom/workers/RuntimeService.cpp
@@ -87,31 +87,53 @@ using namespace mozilla::xpconnect::memo
 
 // The stack limit the JS engine will check. Half the size of the
 // actual C stack, to be safe.
 #define WORKER_CONTEXT_NATIVE_STACK_LIMIT 128 * sizeof(size_t) * 1024
 
 // The maximum number of threads to use for workers, overridable via pref.
 #define MAX_WORKERS_PER_DOMAIN 10
 
+PR_STATIC_ASSERT(MAX_WORKERS_PER_DOMAIN >= 1);
+
 // The default number of seconds that close handlers will be allowed to run.
 #define MAX_SCRIPT_RUN_TIME_SEC 10
 
 // The number of seconds that idle threads can hang around before being killed.
 #define IDLE_THREAD_TIMEOUT_SEC 30
 
 // The maximum number of threads that can be idle at one time.
 #define MAX_IDLE_THREADS 20
 
 #define PREF_WORKERS_ENABLED "dom.workers.enabled"
 #define PREF_WORKERS_MAX_PER_DOMAIN "dom.workers.maxPerDomain"
 #define PREF_WORKERS_GCZEAL "dom.workers.gczeal"
 #define PREF_MAX_SCRIPT_RUN_TIME "dom.max_script_run_time"
 
-PR_STATIC_ASSERT(MAX_WORKERS_PER_DOMAIN >= 1);
+#define GC_REQUEST_OBSERVER_TOPIC "child-gc-request"
+#define MEMORY_PRESSURE_OBSERVER_TOPIC "memory-pressure"
+
+#define BROADCAST_ALL_WORKERS(_func, ...)                                      \
+  PR_BEGIN_MACRO                                                               \
+    AssertIsOnMainThread();                                                    \
+                                                                               \
+    nsAutoTArray<WorkerPrivate*, 100> workers;                                 \
+    {                                                                          \
+      MutexAutoLock lock(mMutex);                                              \
+                                                                               \
+      mDomainMap.EnumerateRead(AddAllTopLevelWorkersToArray, &workers);        \
+    }                                                                          \
+                                                                               \
+    if (!workers.IsEmpty()) {                                                  \
+      AutoSafeJSContext cx;                                                    \
+      for (PRUint32 index = 0; index < workers.Length(); index++) {            \
+        workers[index]-> _func (cx, ##__VA_ARGS__);                            \
+      }                                                                        \
+    }                                                                          \
+  PR_END_MACRO
 
 namespace {
 
 const PRUint32 kNoIndex = PRUint32(-1);
 
 const PRUint32 kRequiredJSContextOptions =
   JSOPTION_DONT_REPORT_UNCAUGHT | JSOPTION_NO_SCRIPT_RVAL;
 
@@ -902,16 +924,25 @@ RuntimeService::Init()
   NS_ENSURE_TRUE(obs, NS_ERROR_FAILURE);
 
   nsresult rv =
     obs->AddObserver(this, NS_XPCOM_SHUTDOWN_THREADS_OBSERVER_ID, false);
   NS_ENSURE_SUCCESS(rv, rv);
 
   mObserved = true;
 
+  if (NS_FAILED(obs->AddObserver(this, GC_REQUEST_OBSERVER_TOPIC, false))) {
+    NS_WARNING("Failed to register for GC request notifications!");
+  }
+
+  if (NS_FAILED(obs->AddObserver(this, MEMORY_PRESSURE_OBSERVER_TOPIC,
+                                 false))) {
+    NS_WARNING("Failed to register for memory pressure notifications!");
+  }
+
   for (PRUint32 index = 0; index < ArrayLength(gPrefsToWatch); index++) {
     if (NS_FAILED(Preferences::RegisterCallback(PrefCallback,
                                                 gPrefsToWatch[index], this))) {
       NS_WARNING("Failed to register pref callback?!");
     }
     PrefCallback(gPrefsToWatch[index], this);
   }
 
@@ -1014,17 +1045,17 @@ RuntimeService::Cleanup()
         }
       }
 
       // And make sure all their final messages have run and all their threads
       // have joined.
       while (mDomainMap.Count()) {
         MutexAutoUnlock unlock(mMutex);
 
-        if (NS_FAILED(NS_ProcessNextEvent(currentThread))) {
+        if (!NS_ProcessNextEvent(currentThread)) {
           NS_WARNING("Something bad happened!");
           break;
         }
       }
     }
   }
 
   if (mWindowMap.IsInitialized()) {
@@ -1032,16 +1063,25 @@ RuntimeService::Cleanup()
   }
 
   if (mObserved) {
     for (PRUint32 index = 0; index < ArrayLength(gPrefsToWatch); index++) {
       Preferences::UnregisterCallback(PrefCallback, gPrefsToWatch[index], this);
     }
 
     if (obs) {
+      if (NS_FAILED(obs->RemoveObserver(this, GC_REQUEST_OBSERVER_TOPIC))) {
+        NS_WARNING("Failed to unregister for GC request notifications!");
+      }
+
+      if (NS_FAILED(obs->RemoveObserver(this,
+                                        MEMORY_PRESSURE_OBSERVER_TOPIC))) {
+        NS_WARNING("Failed to unregister for memory pressure notifications!");
+      }
+
       nsresult rv =
         obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_THREADS_OBSERVER_ID);
       mObserved = NS_FAILED(rv);
     }
   }
 }
 
 // static
@@ -1190,90 +1230,61 @@ RuntimeService::NoteIdleThread(nsIThread
                                        nsITimer::TYPE_ONE_SHOT))) {
     NS_ERROR("Can't schedule timer!");
   }
 }
 
 void
 RuntimeService::UpdateAllWorkerJSContextOptions()
 {
-  AssertIsOnMainThread();
-
-  nsAutoTArray<WorkerPrivate*, 100> workers;
-  {
-    MutexAutoLock lock(mMutex);
-
-    mDomainMap.EnumerateRead(AddAllTopLevelWorkersToArray, &workers);
-  }
-
-  if (!workers.IsEmpty()) {
-    AutoSafeJSContext cx;
-    for (PRUint32 index = 0; index < workers.Length(); index++) {
-      workers[index]->UpdateJSContextOptions(cx, GetDefaultJSContextOptions());
-    }
-  }
+  BROADCAST_ALL_WORKERS(UpdateJSContextOptions, GetDefaultJSContextOptions());
 }
 
 void
 RuntimeService::UpdateAllWorkerJSRuntimeHeapSize()
 {
-  AssertIsOnMainThread();
-
-  nsAutoTArray<WorkerPrivate*, 100> workers;
-  {
-    MutexAutoLock lock(mMutex);
-
-    mDomainMap.EnumerateRead(AddAllTopLevelWorkersToArray, &workers);
-  }
-
-  if (!workers.IsEmpty()) {
-    AutoSafeJSContext cx;
-    for (PRUint32 index = 0; index < workers.Length(); index++) {
-      workers[index]->UpdateJSRuntimeHeapSize(cx,
-                                              GetDefaultJSRuntimeHeapSize());
-    }
-  }
+  BROADCAST_ALL_WORKERS(UpdateJSRuntimeHeapSize, GetDefaultJSRuntimeHeapSize());
 }
 
 #ifdef JS_GC_ZEAL
 void
 RuntimeService::UpdateAllWorkerGCZeal()
 {
-  AssertIsOnMainThread();
-
-  nsAutoTArray<WorkerPrivate*, 100> workers;
-  {
-    MutexAutoLock lock(mMutex);
-
-    mDomainMap.EnumerateRead(AddAllTopLevelWorkersToArray, &workers);
-  }
-
-  if (!workers.IsEmpty()) {
-    AutoSafeJSContext cx;
-    for (PRUint32 index = 0; index < workers.Length(); index++) {
-      workers[index]->UpdateGCZeal(cx, GetDefaultGCZeal());
-    }
-  }
+  BROADCAST_ALL_WORKERS(UpdateGCZeal, GetDefaultGCZeal());
 }
 #endif
 
+void
+RuntimeService::GarbageCollectAllWorkers(bool aShrinking)
+{
+  BROADCAST_ALL_WORKERS(GarbageCollect, aShrinking);
+}
+
 // nsISupports
 NS_IMPL_ISUPPORTS1(RuntimeService, nsIObserver)
 
 // nsIObserver
 NS_IMETHODIMP
 RuntimeService::Observe(nsISupports* aSubject, const char* aTopic,
                         const PRUnichar* aData)
 {
   AssertIsOnMainThread();
 
   if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_THREADS_OBSERVER_ID)) {
     Cleanup();
     return NS_OK;
   }
+  if (!strcmp(aTopic, GC_REQUEST_OBSERVER_TOPIC)) {
+    GarbageCollectAllWorkers(false);
+    return NS_OK;
+  }
+  if (!strcmp(aTopic, MEMORY_PRESSURE_OBSERVER_TOPIC)) {
+    GarbageCollectAllWorkers(true);
+    return NS_OK;
+  }
 
   NS_NOTREACHED("Unknown observer topic!");
   return NS_OK;
 }
 
 RuntimeService::AutoSafeJSContext::AutoSafeJSContext(JSContext* aCx)
 : mContext(aCx ? aCx : GetSafeContext())
 {
--- a/dom/workers/RuntimeService.h
+++ b/dom/workers/RuntimeService.h
@@ -228,16 +228,19 @@ public:
     AssertIsOnMainThread();
     sDefaultGCZeal = aGCZeal;
   }
 
   void
   UpdateAllWorkerGCZeal();
 #endif
 
+  void
+  GarbageCollectAllWorkers(bool aShrinking);
+
   class AutoSafeJSContext
   {
     JSContext* mContext;
 
   public:
     AutoSafeJSContext(JSContext* aCx = nsnull);
     ~AutoSafeJSContext();
 
--- a/dom/workers/WorkerPrivate.cpp
+++ b/dom/workers/WorkerPrivate.cpp
@@ -53,16 +53,17 @@
 #include "nsITextToSubURI.h"
 #include "nsITimer.h"
 #include "nsIURI.h"
 #include "nsIURL.h"
 #include "nsIXPConnect.h"
 
 #include "jsfriendapi.h"
 #include "jsdbgapi.h"
+#include "jsfriendapi.h"
 #include "jsprf.h"
 #include "js/MemoryMetrics.h"
 
 #include "nsAlgorithm.h"
 #include "nsContentUtils.h"
 #include "nsDOMClassInfo.h"
 #include "nsDOMJSUtils.h"
 #include "nsGUIEvent.h"
@@ -86,16 +87,22 @@
 #endif
 
 #include "WorkerInlines.h"
 
 #if 0 // Define to run GC more often.
 #define EXTRA_GC
 #endif
 
+// GC will run once every thirty seconds during normal execution.
+#define NORMAL_GC_TIMER_DELAY_MS 30000
+
+// GC will run five seconds after the last event is processed.
+#define IDLE_GC_TIMER_DELAY_MS 5000
+
 using mozilla::MutexAutoLock;
 using mozilla::TimeDuration;
 using mozilla::TimeStamp;
 using mozilla::dom::workers::exceptions::ThrowDOMExceptionForCode;
 using mozilla::xpconnect::memory::ReportJSRuntimeStats;
 
 USING_WORKERS_NAMESPACE
 
@@ -1413,16 +1420,53 @@ public:
   WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate)
   {
     aWorkerPrivate->UpdateGCZealInternal(aCx, mGCZeal);
     return true;
   }
 };
 #endif
 
+class GarbageCollectRunnable : public WorkerControlRunnable
+{
+protected:
+  bool mShrinking;
+  bool mCollectChildren;
+
+public:
+  GarbageCollectRunnable(WorkerPrivate* aWorkerPrivate, bool aShrinking,
+                         bool aCollectChildren)
+  : WorkerControlRunnable(aWorkerPrivate, WorkerThread, UnchangedBusyCount),
+    mShrinking(aShrinking), mCollectChildren(aCollectChildren)
+  { }
+
+  bool
+  PreDispatch(JSContext* aCx, WorkerPrivate* aWorkerPrivate)
+  {
+    // Silence bad assertions, this can be dispatched from either the main
+    // thread or the timer thread..
+    return true;
+  }
+
+  void
+  PostDispatch(JSContext* aCx, WorkerPrivate* aWorkerPrivate,
+                bool aDispatchResult)
+  {
+    // Silence bad assertions, this can be dispatched from either the main
+    // thread or the timer thread..
+  }
+
+  bool
+  WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate)
+  {
+    aWorkerPrivate->GarbageCollectInternal(aCx, mShrinking, mCollectChildren);
+    return true;
+  }
+};
+
 class CollectRuntimeStatsRunnable : public WorkerControlRunnable
 {
   typedef mozilla::Mutex Mutex;
   typedef mozilla::CondVar CondVar;
 
   Mutex mMutex;
   CondVar mCondVar;
   volatile bool mDone;
@@ -2167,16 +2211,28 @@ WorkerPrivateParent<Derived>::UpdateGCZe
     NS_WARNING("Failed to update worker gczeal!");
     JS_ClearPendingException(aCx);
   }
 }
 #endif
 
 template <class Derived>
 void
+WorkerPrivateParent<Derived>::GarbageCollect(JSContext* aCx, bool aShrinking)
+{
+  nsRefPtr<GarbageCollectRunnable> runnable =
+    new GarbageCollectRunnable(ParentAsWorkerPrivate(), aShrinking, true);
+  if (!runnable->Dispatch(aCx)) {
+    NS_WARNING("Failed to update worker heap size!");
+    JS_ClearPendingException(aCx);
+  }
+}
+
+template <class Derived>
+void
 WorkerPrivateParent<Derived>::SetBaseURI(nsIURI* aBaseURI)
 {
   AssertIsOnMainThread();
 
   mBaseURI = aBaseURI;
 
   if (NS_FAILED(aBaseURI->GetSpec(mLocationInfo.mHref))) {
     mLocationInfo.mHref.Truncate();
@@ -2490,46 +2546,132 @@ WorkerPrivate::DoRunLoop(JSContext* aCx)
   {
     MutexAutoLock lock(mMutex);
     mJSContext = aCx;
 
     NS_ASSERTION(mStatus == Pending, "Huh?!");
     mStatus = Running;
   }
 
+  // We need a timer for GC. The basic plan is to run a normal (non-shrinking)
+  // GC periodically (NORMAL_GC_TIMER_DELAY_MS) while the worker is running.
+  // Once the worker goes idle we set a short (IDLE_GC_TIMER_DELAY_MS) timer to
+  // run a shrinking GC. If the worker receives more messages then the short
+  // timer is canceled and the periodic timer resumes.
+  nsCOMPtr<nsITimer> gcTimer = do_CreateInstance(NS_TIMER_CONTRACTID);
+  if (!gcTimer) {
+    JS_ReportError(aCx, "Failed to create GC timer!");
+    return;
+  }
+
+  bool normalGCTimerRunning = false;
+
+  // We need to swap event targets below to get different types of GC behavior.
+  nsCOMPtr<nsIEventTarget> normalGCEventTarget;
+  nsCOMPtr<nsIEventTarget> idleGCEventTarget;
+
+  // We also need to track the idle GC event so that we don't confuse it with a
+  // generic event that should re-trigger the idle GC timer.
+  nsCOMPtr<nsIRunnable> idleGCEvent;
+  {
+    nsRefPtr<GarbageCollectRunnable> runnable =
+      new GarbageCollectRunnable(this, false, false);
+    normalGCEventTarget = new WorkerRunnableEventTarget(runnable);
+
+    runnable = new GarbageCollectRunnable(this, true, false);
+    idleGCEventTarget = new WorkerRunnableEventTarget(runnable);
+
+    idleGCEvent = runnable;
+  }
+
   mMemoryReporter = new WorkerMemoryReporter(this);
 
   if (NS_FAILED(NS_RegisterMemoryMultiReporter(mMemoryReporter))) {
     NS_WARNING("Failed to register memory reporter!");
     mMemoryReporter = nsnull;
   }
 
   for (;;) {
     Status currentStatus;
+    bool scheduleIdleGC;
+
     nsIRunnable* event;
     {
       MutexAutoLock lock(mMutex);
 
       while (!mControlQueue.Pop(event) && !mQueue.Pop(event)) {
         mCondVar.Wait();
       }
 
+      bool eventIsNotIdleGCEvent;
+      currentStatus = mStatus;
+
       {
         MutexAutoUnlock unlock(mMutex);
 
+        if (!normalGCTimerRunning &&
+            event != idleGCEvent &&
+            currentStatus <= Terminating) {
+          // Must always cancel before changing the timer's target.
+          if (NS_FAILED(gcTimer->Cancel())) {
+            NS_WARNING("Failed to cancel GC timer!");
+          }
+
+          if (NS_SUCCEEDED(gcTimer->SetTarget(normalGCEventTarget)) &&
+              NS_SUCCEEDED(gcTimer->InitWithFuncCallback(
+                                             DummyCallback, nsnull,
+                                             NORMAL_GC_TIMER_DELAY_MS,
+                                             nsITimer::TYPE_REPEATING_SLACK))) {
+            normalGCTimerRunning = true;
+          }
+          else {
+            JS_ReportError(aCx, "Failed to start normal GC timer!");
+          }
+        }
+
 #ifdef EXTRA_GC
         // Find GC bugs...
         JS_GC(aCx);
 #endif
 
+        // Keep track of whether or not this is the idle GC event.
+        eventIsNotIdleGCEvent = event != idleGCEvent;
+
         event->Run();
         NS_RELEASE(event);
       }
 
       currentStatus = mStatus;
+      scheduleIdleGC = mControlQueue.IsEmpty() &&
+                       mQueue.IsEmpty() &&
+                       eventIsNotIdleGCEvent;
+    }
+
+    // Take care of the GC timer. If we're starting the close sequence then we
+    // kill the timer once and for all. Otherwise we schedule the idle timeout
+    // if there are no more events.
+    if (currentStatus > Terminating || scheduleIdleGC) {
+      if (NS_SUCCEEDED(gcTimer->Cancel())) {
+        normalGCTimerRunning = false;
+      }
+      else {
+        NS_WARNING("Failed to cancel GC timer!");
+      }
+    }
+
+    if (scheduleIdleGC) {
+      if (NS_SUCCEEDED(gcTimer->SetTarget(idleGCEventTarget)) &&
+          NS_SUCCEEDED(gcTimer->InitWithFuncCallback(
+                                                    DummyCallback, nsnull,
+                                                    IDLE_GC_TIMER_DELAY_MS,
+                                                    nsITimer::TYPE_ONE_SHOT))) {
+      }
+      else {
+        JS_ReportError(aCx, "Failed to start idle GC timer!");
+      }
     }
 
 #ifdef EXTRA_GC
     // Find GC bugs...
     JS_GC(aCx);
 #endif
 
     if (currentStatus != Running && !HasActiveFeatures()) {
@@ -2547,16 +2689,21 @@ WorkerPrivate::DoRunLoop(JSContext* aCx)
         NS_ASSERTION(currentStatus == Killing, "Should have changed status!");
 #else
         currentStatus = Killing;
 #endif
       }
 
       // If we're supposed to die then we should exit the loop.
       if (currentStatus == Killing) {
+        // Always make sure the timer is canceled.
+        if (NS_FAILED(gcTimer->Cancel())) {
+          NS_WARNING("Failed to cancel the GC timer!");
+        }
+
         // Call this before unregistering the reporter as we may be racing with
         // the main thread.
         DisableMemoryReporter();
 
         if (mMemoryReporter) {
           if (NS_FAILED(NS_UnregisterMemoryMultiReporter(mMemoryReporter))) {
             NS_WARNING("Failed to unregister memory reporter!");
           }
@@ -3635,16 +3782,36 @@ WorkerPrivate::UpdateGCZealInternal(JSCo
   JS_SetGCZeal(aCx, aGCZeal, frequency, false);
 
   for (PRUint32 index = 0; index < mChildWorkers.Length(); index++) {
     mChildWorkers[index]->UpdateGCZeal(aCx, aGCZeal);
   }
 }
 #endif
 
+void
+WorkerPrivate::GarbageCollectInternal(JSContext* aCx, bool aShrinking,
+                                      bool aCollectChildren)
+{
+  AssertIsOnWorkerThread();
+
+  if (aShrinking) {
+    JS_ShrinkingGC(aCx);
+  }
+  else {
+    JS_GC(aCx);
+  }
+
+  if (aCollectChildren) {
+    for (PRUint32 index = 0; index < mChildWorkers.Length(); index++) {
+      mChildWorkers[index]->GarbageCollect(aCx, aShrinking);
+    }
+  }
+}
+
 #ifdef DEBUG
 template <class Derived>
 void
 WorkerPrivateParent<Derived>::AssertIsOnParentThread() const
 {
   if (GetParent()) {
     GetParent()->AssertIsOnWorkerThread();
   }
--- a/dom/workers/WorkerPrivate.h
+++ b/dom/workers/WorkerPrivate.h
@@ -313,16 +313,19 @@ public:
   void
   UpdateJSRuntimeHeapSize(JSContext* aCx, PRUint32 aJSRuntimeHeapSize);
 
 #ifdef JS_GC_ZEAL
   void
   UpdateGCZeal(JSContext* aCx, PRUint8 aGCZeal);
 #endif
 
+  void
+  GarbageCollect(JSContext* aCx, bool aShrinking);
+
   using events::EventTarget::GetEventListenerOnEventTarget;
   using events::EventTarget::SetEventListenerOnEventTarget;
 
   void
   QueueRunnable(WorkerRunnable* aRunnable)
   {
     AssertIsOnMainThread();
     mQueuedRunnables.AppendElement(aRunnable);
@@ -681,16 +684,20 @@ public:
   bool
   DisableMemoryReporter();
 
 #ifdef JS_GC_ZEAL
   void
   UpdateGCZealInternal(JSContext* aCx, PRUint8 aGCZeal);
 #endif
 
+  void
+  GarbageCollectInternal(JSContext* aCx, bool aShrinking,
+                         bool aCollectChildren);
+
   JSContext*
   GetJSContext() const
   {
     AssertIsOnWorkerThread();
     return mJSContext;
   }
 
 #ifdef DEBUG