Bug 718100 - 'Web workers should GC more'. r=mrbkap.
authorBen Turner <bent.mozilla@gmail.com>
Tue, 17 Jan 2012 12:05:25 -0800
changeset 84769 7080b6f34d32d71d3cdd2bc4fb5695d5e550e5f2
parent 84768 2cf8cd8656321a70bef63f9042dfeab9bb660c8c
child 84770 04890cad686c7ab392a27a7db897b18cbe269682
push id21873
push usermlamouri@mozilla.com
push dateWed, 18 Jan 2012 10:29:07 +0000
treeherdermozilla-central@7538f4d4697c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmrbkap
bugs718100
milestone12.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 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