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
This commit is contained in:
Ben Turner 2012-01-17 12:05:25 -08:00
parent ab87bd22db
commit ea2b9b68ec
4 changed files with 236 additions and 48 deletions

View File

@ -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<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 {
@ -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<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)
@ -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;

View File

@ -233,6 +233,9 @@ public:
UpdateAllWorkerGCZeal();
#endif
void
GarbageCollectAllWorkers(bool aShrinking);
class AutoSafeJSContext
{
JSContext* mContext;

View File

@ -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<Derived>::UpdateGCZeal(JSContext* aCx, PRUint8 aGCZeal)
}
#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)
@ -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<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))) {
@ -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 <class Derived>
void

View File

@ -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
{