From b00fe831b3734bc37217808d95145a0072220510 Mon Sep 17 00:00:00 2001 From: Ben Turner Date: Tue, 17 Jan 2012 12:05:25 -0800 Subject: [PATCH] Bug 718100 - 'Web workers should GC more'. r=mrbkap. --HG-- extra : transplant_source : %03D%F4%26%AA%03T%8A%B9%B7%27%AF%D4%8C%85%B2%DB%DFf%EF --- dom/workers/RuntimeService.cpp | 107 +++++++++++---------- dom/workers/RuntimeService.h | 3 + dom/workers/WorkerPrivate.cpp | 167 +++++++++++++++++++++++++++++++++ dom/workers/WorkerPrivate.h | 7 ++ 4 files changed, 236 insertions(+), 48 deletions(-) diff --git a/dom/workers/RuntimeService.cpp b/dom/workers/RuntimeService.cpp index 86ce057d7c63..bf140fa78e76 100644 --- a/dom/workers/RuntimeService.cpp +++ b/dom/workers/RuntimeService.cpp @@ -92,6 +92,8 @@ using namespace mozilla::xpconnect::memory; // 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 @@ -106,7 +108,27 @@ using namespace mozilla::xpconnect::memory; #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 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 { @@ -907,6 +929,15 @@ RuntimeService::Init() 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))) { @@ -1019,7 +1050,7 @@ RuntimeService::Cleanup() while (mDomainMap.Count()) { MutexAutoUnlock unlock(mMutex); - if (NS_FAILED(NS_ProcessNextEvent(currentThread))) { + if (!NS_ProcessNextEvent(currentThread)) { NS_WARNING("Something bad happened!"); break; } @@ -1037,6 +1068,15 @@ RuntimeService::Cleanup() } 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); @@ -1195,66 +1235,29 @@ RuntimeService::NoteIdleThread(nsIThread* aThread) void RuntimeService::UpdateAllWorkerJSContextOptions() { - AssertIsOnMainThread(); - - nsAutoTArray 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 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 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) @@ -1269,6 +1272,14 @@ RuntimeService::Observe(nsISupports* aSubject, const char* aTopic, 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; diff --git a/dom/workers/RuntimeService.h b/dom/workers/RuntimeService.h index df74e23b8a33..64ee8bd13351 100644 --- a/dom/workers/RuntimeService.h +++ b/dom/workers/RuntimeService.h @@ -233,6 +233,9 @@ public: UpdateAllWorkerGCZeal(); #endif + void + GarbageCollectAllWorkers(bool aShrinking); + class AutoSafeJSContext { JSContext* mContext; diff --git a/dom/workers/WorkerPrivate.cpp b/dom/workers/WorkerPrivate.cpp index 90b147eb5770..d5cf0279ca5e 100644 --- a/dom/workers/WorkerPrivate.cpp +++ b/dom/workers/WorkerPrivate.cpp @@ -58,6 +58,7 @@ #include "jsfriendapi.h" #include "jsdbgapi.h" +#include "jsfriendapi.h" #include "jsprf.h" #include "js/MemoryMetrics.h" @@ -91,6 +92,12 @@ #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; @@ -1418,6 +1425,43 @@ public: }; #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; @@ -2170,6 +2214,18 @@ WorkerPrivateParent::UpdateGCZeal(JSContext* aCx, PRUint8 aGCZeal) } #endif +template +void +WorkerPrivateParent::GarbageCollect(JSContext* aCx, bool aShrinking) +{ + nsRefPtr runnable = + new GarbageCollectRunnable(ParentAsWorkerPrivate(), aShrinking, true); + if (!runnable->Dispatch(aCx)) { + NS_WARNING("Failed to update worker heap size!"); + JS_ClearPendingException(aCx); + } +} + template void WorkerPrivateParent::SetBaseURI(nsIURI* aBaseURI) @@ -2495,6 +2551,37 @@ WorkerPrivate::DoRunLoop(JSContext* aCx) 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 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 normalGCEventTarget; + nsCOMPtr 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 idleGCEvent; + { + nsRefPtr 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))) { @@ -2504,6 +2591,8 @@ WorkerPrivate::DoRunLoop(JSContext* aCx) for (;;) { Status currentStatus; + bool scheduleIdleGC; + nsIRunnable* event; { MutexAutoLock lock(mMutex); @@ -2512,19 +2601,72 @@ WorkerPrivate::DoRunLoop(JSContext* aCx) 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 @@ -2552,6 +2694,11 @@ WorkerPrivate::DoRunLoop(JSContext* aCx) // 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(); @@ -3640,6 +3787,26 @@ WorkerPrivate::UpdateGCZealInternal(JSContext* aCx, PRUint8 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 void diff --git a/dom/workers/WorkerPrivate.h b/dom/workers/WorkerPrivate.h index 9294ad0f9c95..e2817f2e7d44 100644 --- a/dom/workers/WorkerPrivate.h +++ b/dom/workers/WorkerPrivate.h @@ -318,6 +318,9 @@ public: UpdateGCZeal(JSContext* aCx, PRUint8 aGCZeal); #endif + void + GarbageCollect(JSContext* aCx, bool aShrinking); + using events::EventTarget::GetEventListenerOnEventTarget; using events::EventTarget::SetEventListenerOnEventTarget; @@ -686,6 +689,10 @@ public: UpdateGCZealInternal(JSContext* aCx, PRUint8 aGCZeal); #endif + void + GarbageCollectInternal(JSContext* aCx, bool aShrinking, + bool aCollectChildren); + JSContext* GetJSContext() const {