/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Web Workers. * * The Initial Developer of the Original Code is * The Mozilla Foundation. * Portions created by the Initial Developer are Copyright (C) 2011 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Ben Turner (Original Author) * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ #include "RuntimeService.h" #include "nsIDOMChromeWindow.h" #include "nsIDocument.h" #include "nsIEffectiveTLDService.h" #include "nsIObserverService.h" #include "nsIPlatformCharset.h" #include "nsIPrincipal.h" #include "nsIJSContextStack.h" #include "nsIMemoryReporter.h" #include "nsIScriptSecurityManager.h" #include "nsISupportsPriority.h" #include "nsITimer.h" #include "nsPIDOMWindow.h" #include "jsprf.h" #include "mozilla/Preferences.h" #include "nsContentUtils.h" #include "nsDOMJSUtils.h" #include "nsGlobalWindow.h" #include "nsNetUtil.h" #include "nsServiceManagerUtils.h" #include "nsThreadUtils.h" #include "nsXPCOM.h" #include "nsXPCOMPrivate.h" #include "xpcpublic.h" #include "Events.h" #include "EventTarget.h" #include "Worker.h" #include "WorkerPrivate.h" USING_WORKERS_NAMESPACE using mozilla::MutexAutoLock; using mozilla::MutexAutoUnlock; using mozilla::Preferences; using namespace mozilla::xpconnect::memory; // The size of the worker runtime heaps in bytes. #define WORKER_RUNTIME_HEAPSIZE 32 * 1024 * 1024 // The size of the C stack in bytes. #define WORKER_CONTEXT_NATIVE_STACK_LIMIT sizeof(size_t) * 32 * 1024 // The maximum number of threads to use for workers, overridable via pref. #define MAX_WORKERS_PER_DOMAIN 10 // 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); namespace { const PRUint32 kNoIndex = PRUint32(-1); const PRUint32 kRequiredJSContextOptions = JSOPTION_DONT_REPORT_UNCAUGHT | JSOPTION_NO_SCRIPT_RVAL; PRUint32 gMaxWorkersPerDomain = MAX_WORKERS_PER_DOMAIN; // Does not hold an owning reference. RuntimeService* gRuntimeService = nsnull; enum { ID_Worker = 0, ID_ChromeWorker, ID_Event, ID_MessageEvent, ID_ErrorEvent, ID_COUNT }; // These are jsids for the main runtime. Only touched on the main thread. jsid gStringIDs[ID_COUNT] = { JSID_VOID }; const char* gStringChars[] = { "Worker", "ChromeWorker", "WorkerEvent", "WorkerMessageEvent", "WorkerErrorEvent" // XXX Don't care about ProgressEvent since it should never leak to the main // thread. }; PR_STATIC_ASSERT(NS_ARRAY_LENGTH(gStringChars) == ID_COUNT); enum { PREF_strict = 0, PREF_werror, PREF_relimit, PREF_tracejit, PREF_methodjit, PREF_jitprofiling, PREF_methodjit_always, #ifdef JS_GC_ZEAL PREF_gczeal, #endif PREF_COUNT }; #define JS_OPTIONS_DOT_STR "javascript.options." const char* gPrefsToWatch[] = { JS_OPTIONS_DOT_STR "strict", JS_OPTIONS_DOT_STR "werror", JS_OPTIONS_DOT_STR "relimit", JS_OPTIONS_DOT_STR "tracejit.content", JS_OPTIONS_DOT_STR "methodjit.content", JS_OPTIONS_DOT_STR "jitprofiling.content", JS_OPTIONS_DOT_STR "methodjit_always" #ifdef JS_GC_ZEAL , PREF_WORKERS_GCZEAL #endif }; PR_STATIC_ASSERT(NS_ARRAY_LENGTH(gPrefsToWatch) == PREF_COUNT); int PrefCallback(const char* aPrefName, void* aClosure) { AssertIsOnMainThread(); RuntimeService* rts = static_cast(aClosure); NS_ASSERTION(rts, "This should never be null!"); NS_NAMED_LITERAL_CSTRING(jsOptionStr, JS_OPTIONS_DOT_STR); if(StringBeginsWith(nsDependentCString(aPrefName), jsOptionStr)) { PRUint32 newOptions = kRequiredJSContextOptions; if (Preferences::GetBool(gPrefsToWatch[PREF_strict])) { newOptions |= JSOPTION_STRICT; } if (Preferences::GetBool(gPrefsToWatch[PREF_werror])) { newOptions |= JSOPTION_WERROR; } if (Preferences::GetBool(gPrefsToWatch[PREF_relimit])) { newOptions |= JSOPTION_RELIMIT; } if (Preferences::GetBool(gPrefsToWatch[PREF_tracejit])) { newOptions |= JSOPTION_JIT; } if (Preferences::GetBool(gPrefsToWatch[PREF_methodjit])) { newOptions |= JSOPTION_METHODJIT; } if (Preferences::GetBool(gPrefsToWatch[PREF_jitprofiling])) { newOptions |= JSOPTION_PROFILING; } if (Preferences::GetBool(gPrefsToWatch[PREF_methodjit_always])) { newOptions |= JSOPTION_METHODJIT_ALWAYS; } RuntimeService::SetDefaultJSContextOptions(newOptions); rts->UpdateAllWorkerJSContextOptions(); } #ifdef JS_GC_ZEAL else if (!strcmp(aPrefName, gPrefsToWatch[PREF_gczeal])) { PRInt32 gczeal = Preferences::GetInt(gPrefsToWatch[PREF_gczeal]); RuntimeService::SetDefaultGCZeal(PRUint8(NS_MIN(NS_MAX(gczeal, 0), 3))); rts->UpdateAllWorkerGCZeal(); } #endif return 0; } void ErrorReporter(JSContext* aCx, const char* aMessage, JSErrorReport* aReport) { WorkerPrivate* worker = GetWorkerPrivateFromContext(aCx); return worker->ReportError(aCx, aMessage, aReport); } JSBool OperationCallback(JSContext* aCx) { WorkerPrivate* worker = GetWorkerPrivateFromContext(aCx); return worker->OperationCallback(aCx); } JSContext* CreateJSContextForWorker(WorkerPrivate* aWorkerPrivate) { aWorkerPrivate->AssertIsOnWorkerThread(); NS_ASSERTION(!aWorkerPrivate->GetJSContext(), "Already has a context!"); JSRuntime* runtime = JS_NewRuntime(WORKER_RUNTIME_HEAPSIZE); if (!runtime) { NS_WARNING("Could not create new runtime!"); return nsnull; } JSContext* workerCx = JS_NewContext(runtime, 0); if (!workerCx) { JS_DestroyRuntime(runtime); NS_WARNING("Could not create new context!"); return nsnull; } JS_SetContextPrivate(workerCx, aWorkerPrivate); JS_SetErrorReporter(workerCx, ErrorReporter); JS_SetOperationCallback(workerCx, OperationCallback); JS_SetNativeStackQuota(workerCx, WORKER_CONTEXT_NATIVE_STACK_LIMIT); NS_ASSERTION((aWorkerPrivate->GetJSContextOptions() & kRequiredJSContextOptions) == kRequiredJSContextOptions, "Somehow we lost our required options!"); JS_SetOptions(workerCx, aWorkerPrivate->GetJSContextOptions()); #ifdef JS_GC_ZEAL { PRUint8 zeal = aWorkerPrivate->GetGCZeal(); NS_ASSERTION(zeal <= 3, "Bad zeal value!"); PRUint32 frequency = zeal <= 2 ? JS_DEFAULT_ZEAL_FREQ : 1; JS_SetGCZeal(workerCx, zeal, frequency, false); } #endif if (aWorkerPrivate->IsChromeWorker()) { JS_SetVersion(workerCx, JSVERSION_LATEST); } return workerCx; } class WorkerMemoryReporter : public nsIMemoryMultiReporter { WorkerPrivate* mWorkerPrivate; nsCString mPathPrefix; public: NS_DECL_ISUPPORTS WorkerMemoryReporter(WorkerPrivate* aWorkerPrivate) : mWorkerPrivate(aWorkerPrivate) { aWorkerPrivate->AssertIsOnWorkerThread(); nsCString escapedDomain(aWorkerPrivate->Domain()); escapedDomain.ReplaceChar('/', '\\'); NS_ConvertUTF16toUTF8 escapedURL(aWorkerPrivate->ScriptURL()); escapedURL.ReplaceChar('/', '\\'); // 64bit address plus '0x' plus null terminator. char address[21]; JSUint32 addressSize = JS_snprintf(address, sizeof(address), "0x%llx", aWorkerPrivate); if (addressSize == JSUint32(-1)) { NS_WARNING("JS_snprintf failed!"); address[0] = '\0'; addressSize = 0; } mPathPrefix = NS_LITERAL_CSTRING("explicit/dom/workers(") + escapedDomain + NS_LITERAL_CSTRING(")/worker(") + escapedURL + NS_LITERAL_CSTRING(", ") + nsDependentCString(address, addressSize) + NS_LITERAL_CSTRING(")/"); } NS_IMETHOD CollectReports(nsIMemoryMultiReporterCallback* aCallback, nsISupports* aClosure) { AssertIsOnMainThread(); IterateData data; if (!mWorkerPrivate->BlockAndCollectRuntimeStats(&data)) { return NS_ERROR_FAILURE; } ReportJSRuntimeStats(data, mPathPrefix, aCallback, aClosure); return NS_OK; } }; NS_IMPL_THREADSAFE_ISUPPORTS1(WorkerMemoryReporter, nsIMemoryMultiReporter) class WorkerThreadRunnable : public nsRunnable { WorkerPrivate* mWorkerPrivate; public: WorkerThreadRunnable(WorkerPrivate* aWorkerPrivate) : mWorkerPrivate(aWorkerPrivate) { NS_ASSERTION(mWorkerPrivate, "This should never be null!"); } NS_IMETHOD Run() { WorkerPrivate* workerPrivate = mWorkerPrivate; mWorkerPrivate = nsnull; workerPrivate->AssertIsOnWorkerThread(); JSContext* cx = CreateJSContextForWorker(workerPrivate); if (!cx) { // XXX need to fire an error at parent. NS_ERROR("Failed to create runtime and context!"); return NS_ERROR_FAILURE; } nsRefPtr reporter = new WorkerMemoryReporter(workerPrivate); if (NS_FAILED(NS_RegisterMemoryMultiReporter(reporter))) { NS_WARNING("Failed to register memory reporter!"); reporter = nsnull; } { JSAutoRequest ar(cx); workerPrivate->DoRunLoop(cx); } if (reporter) { if (NS_FAILED(NS_UnregisterMemoryMultiReporter(reporter))) { NS_WARNING("Failed to unregister memory reporter!"); } reporter = nsnull; } JSRuntime* rt = JS_GetRuntime(cx); // XXX Bug 666963 - CTypes can create another JSContext for use with // closures, and then it holds that context in a reserved slot on the CType // prototype object. We have to destroy that context before we can destroy // the runtime, and we also have to make sure that it isn't the last context // to be destroyed (otherwise it will assert). To accomplish this we create // an unused dummy context, destroy our real context, and then destroy the // dummy. Once this bug is resolved we can remove this nastiness and simply // call JS_DestroyContextNoGC on our context. JSContext* dummyCx = JS_NewContext(rt, 0); if (dummyCx) { JS_DestroyContext(cx); JS_DestroyContext(dummyCx); } else { NS_WARNING("Failed to create dummy context!"); JS_DestroyContext(cx); } JS_DestroyRuntime(rt); workerPrivate->ScheduleDeletion(false); return NS_OK; } }; } /* anonymous namespace */ BEGIN_WORKERS_NAMESPACE // Entry point for the DOM. JSBool ResolveWorkerClasses(JSContext* aCx, JSObject* aObj, jsid aId, uintN aFlags, JSObject** aObjp) { AssertIsOnMainThread(); // Don't care about assignments or declarations, bail now. if (aFlags & (JSRESOLVE_ASSIGNING | JSRESOLVE_DECLARING)) { *aObjp = nsnull; return true; } // Make sure our strings are interned. if (JSID_IS_VOID(gStringIDs[0])) { for (PRUint32 i = 0; i < ID_COUNT; i++) { JSString* str = JS_InternString(aCx, gStringChars[i]); if (!str) { while (i) { gStringIDs[--i] = JSID_VOID; } return false; } gStringIDs[i] = INTERNED_STRING_TO_JSID(aCx, str); } } bool isChrome = false; bool shouldResolve = false; for (PRUint32 i = 0; i < ID_COUNT; i++) { if (aId == gStringIDs[i]) { nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); NS_ASSERTION(ssm, "This should never be null!"); PRBool enabled; if (NS_FAILED(ssm->IsCapabilityEnabled("UniversalXPConnect", &enabled))) { NS_WARNING("IsCapabilityEnabled failed!"); isChrome = PR_FALSE; } isChrome = !!enabled; // Don't resolve if this is ChromeWorker and we're not chrome. Otherwise // always resolve. shouldResolve = aId == gStringIDs[ID_ChromeWorker] ? isChrome : true; break; } } if (shouldResolve) { // Don't do anything if workers are disabled. if (!isChrome && !Preferences::GetBool(PREF_WORKERS_ENABLED)) { *aObjp = nsnull; return true; } JSObject* eventTarget = events::InitEventTargetClass(aCx, aObj, true); if (!eventTarget) { return false; } JSObject* worker = worker::InitClass(aCx, aObj, eventTarget, true); if (!worker) { return false; } if (isChrome && !chromeworker::InitClass(aCx, aObj, worker, true)) { return false; } if (!events::InitClasses(aCx, aObj, true)) { return false; } *aObjp = aObj; return true; } // Not resolved. *aObjp = nsnull; return true; } void CancelWorkersForWindow(JSContext* aCx, nsPIDOMWindow* aWindow) { AssertIsOnMainThread(); RuntimeService* runtime = RuntimeService::GetService(); if (runtime) { runtime->CancelWorkersForWindow(aCx, aWindow); } } void SuspendWorkersForWindow(JSContext* aCx, nsPIDOMWindow* aWindow) { AssertIsOnMainThread(); RuntimeService* runtime = RuntimeService::GetService(); if (runtime) { runtime->SuspendWorkersForWindow(aCx, aWindow); } } void ResumeWorkersForWindow(JSContext* aCx, nsPIDOMWindow* aWindow) { AssertIsOnMainThread(); RuntimeService* runtime = RuntimeService::GetService(); if (runtime) { runtime->ResumeWorkersForWindow(aCx, aWindow); } } END_WORKERS_NAMESPACE PRUint32 RuntimeService::sDefaultJSContextOptions = kRequiredJSContextOptions; PRInt32 RuntimeService::sCloseHandlerTimeoutSeconds = MAX_SCRIPT_RUN_TIME_SEC; #ifdef JS_GC_ZEAL PRUint8 RuntimeService::sDefaultGCZeal = 0; #endif RuntimeService::RuntimeService() : mMutex("RuntimeService::mMutex"), mObserved(false), mShuttingDown(false), mNavigatorStringsLoaded(false) { AssertIsOnMainThread(); NS_ASSERTION(!gRuntimeService, "More than one service!"); } RuntimeService::~RuntimeService() { AssertIsOnMainThread(); // gRuntimeService can be null if Init() fails. NS_ASSERTION(!gRuntimeService || gRuntimeService == this, "More than one service!"); gRuntimeService = nsnull; } // static RuntimeService* RuntimeService::GetOrCreateService() { AssertIsOnMainThread(); if (!gRuntimeService) { nsRefPtr service = new RuntimeService(); if (NS_FAILED(service->Init())) { NS_WARNING("Failed to initialize!"); service->Cleanup(); return nsnull; } // The observer service now owns us until shutdown. gRuntimeService = service; } return gRuntimeService; } // static RuntimeService* RuntimeService::GetService() { return gRuntimeService; } bool RuntimeService::RegisterWorker(JSContext* aCx, WorkerPrivate* aWorkerPrivate) { aWorkerPrivate->AssertIsOnParentThread(); WorkerPrivate* parent = aWorkerPrivate->GetParent(); if (!parent) { AssertIsOnMainThread(); if (mShuttingDown) { JS_ReportError(aCx, "Cannot create worker during shutdown!"); return false; } } WorkerDomainInfo* domainInfo; bool queued = false; { const nsCString& domain = aWorkerPrivate->Domain(); MutexAutoLock lock(mMutex); if (!mDomainMap.Get(domain, &domainInfo)) { NS_ASSERTION(!parent, "Shouldn't have a parent here!"); domainInfo = new WorkerDomainInfo(); domainInfo->mDomain = domain; if (!mDomainMap.Put(domain, domainInfo)) { delete domainInfo; domainInfo = nsnull; } } if (domainInfo) { queued = gMaxWorkersPerDomain && domainInfo->ActiveWorkerCount() >= gMaxWorkersPerDomain && !domain.IsEmpty(); if (queued) { domainInfo->mQueuedWorkers.AppendElement(aWorkerPrivate); } else if (parent) { domainInfo->mChildWorkerCount++; } else { domainInfo->mActiveWorkers.AppendElement(aWorkerPrivate); } } } if (!domainInfo) { JS_ReportOutOfMemory(aCx); return false; } // From here on out we must call UnregisterWorker if something fails! if (parent) { if (!parent->AddChildWorker(aCx, aWorkerPrivate)) { UnregisterWorker(aCx, aWorkerPrivate); return false; } } else { if (!mNavigatorStringsLoaded) { if (NS_FAILED(NS_GetNavigatorAppName(mNavigatorStrings.mAppName)) || NS_FAILED(NS_GetNavigatorAppVersion(mNavigatorStrings.mAppVersion)) || NS_FAILED(NS_GetNavigatorPlatform(mNavigatorStrings.mPlatform)) || NS_FAILED(NS_GetNavigatorUserAgent(mNavigatorStrings.mUserAgent))) { JS_ReportError(aCx, "Failed to load navigator strings!"); UnregisterWorker(aCx, aWorkerPrivate); return false; } mNavigatorStringsLoaded = true; } nsPIDOMWindow* window = aWorkerPrivate->GetWindow(); nsTArray* windowArray; if (!mWindowMap.Get(window, &windowArray)) { NS_ASSERTION(!parent, "Shouldn't have a parent here!"); windowArray = new nsTArray(1); if (!mWindowMap.Put(window, windowArray)) { delete windowArray; UnregisterWorker(aCx, aWorkerPrivate); JS_ReportOutOfMemory(aCx); return false; } } NS_ASSERTION(!windowArray->Contains(aWorkerPrivate), "Already know about this worker!"); windowArray->AppendElement(aWorkerPrivate); } if (!queued && !ScheduleWorker(aCx, aWorkerPrivate)) { return false; } return true; } void RuntimeService::UnregisterWorker(JSContext* aCx, WorkerPrivate* aWorkerPrivate) { aWorkerPrivate->AssertIsOnParentThread(); WorkerPrivate* parent = aWorkerPrivate->GetParent(); if (!parent) { AssertIsOnMainThread(); } WorkerPrivate* queuedWorker = nsnull; { const nsCString& domain = aWorkerPrivate->Domain(); MutexAutoLock lock(mMutex); WorkerDomainInfo* domainInfo; if (!mDomainMap.Get(domain, &domainInfo)) { NS_ERROR("Don't have an entry for this domain!"); } // Remove old worker from everywhere. PRUint32 index = domainInfo->mQueuedWorkers.IndexOf(aWorkerPrivate); if (index != kNoIndex) { // Was queued, remove from the list. domainInfo->mQueuedWorkers.RemoveElementAt(index); } else if (parent) { NS_ASSERTION(domainInfo->mChildWorkerCount, "Must be non-zero!"); domainInfo->mChildWorkerCount--; } else { NS_ASSERTION(domainInfo->mActiveWorkers.Contains(aWorkerPrivate), "Don't know about this worker!"); domainInfo->mActiveWorkers.RemoveElement(aWorkerPrivate); } // See if there's a queued worker we can schedule. if (domainInfo->ActiveWorkerCount() < gMaxWorkersPerDomain && !domainInfo->mQueuedWorkers.IsEmpty()) { queuedWorker = domainInfo->mQueuedWorkers[0]; domainInfo->mQueuedWorkers.RemoveElementAt(0); if (queuedWorker->GetParent()) { domainInfo->mChildWorkerCount++; } else { domainInfo->mActiveWorkers.AppendElement(queuedWorker); } } if (!domainInfo->ActiveWorkerCount()) { NS_ASSERTION(domainInfo->mQueuedWorkers.IsEmpty(), "Huh?!"); mDomainMap.Remove(domain); } } if (parent) { parent->RemoveChildWorker(aCx, aWorkerPrivate); } else { nsPIDOMWindow* window = aWorkerPrivate->GetWindow(); nsTArray* windowArray; if (!mWindowMap.Get(window, &windowArray)) { NS_ERROR("Don't have an entry for this window!"); } NS_ASSERTION(windowArray->Contains(aWorkerPrivate), "Don't know about this worker!"); windowArray->RemoveElement(aWorkerPrivate); if (windowArray->IsEmpty()) { NS_ASSERTION(!queuedWorker, "How can this be?!"); mWindowMap.Remove(window); } } if (queuedWorker && !ScheduleWorker(aCx, queuedWorker)) { UnregisterWorker(aCx, queuedWorker); } } bool RuntimeService::ScheduleWorker(JSContext* aCx, WorkerPrivate* aWorkerPrivate) { if (!aWorkerPrivate->Start()) { // This is ok, means that we didn't need to make a thread for this worker. return true; } nsCOMPtr thread; { MutexAutoLock lock(mMutex); if (!mIdleThreadArray.IsEmpty()) { PRUint32 index = mIdleThreadArray.Length() - 1; mIdleThreadArray[index].mThread.swap(thread); mIdleThreadArray.RemoveElementAt(index); } } if (!thread) { if (NS_FAILED(NS_NewThread(getter_AddRefs(thread), nsnull))) { UnregisterWorker(aCx, aWorkerPrivate); JS_ReportError(aCx, "Could not create new thread!"); return false; } nsCOMPtr priority = do_QueryInterface(thread); if (!priority || NS_FAILED(priority->SetPriority(nsISupportsPriority::PRIORITY_LOW))) { NS_WARNING("Could not lower the new thread's priority!"); } } #ifdef DEBUG aWorkerPrivate->SetThread(thread); #endif nsCOMPtr runnable = new WorkerThreadRunnable(aWorkerPrivate); if (NS_FAILED(thread->Dispatch(runnable, NS_DISPATCH_NORMAL))) { UnregisterWorker(aCx, aWorkerPrivate); JS_ReportError(aCx, "Could not dispatch to thread!"); return false; } return true; } // static void RuntimeService::ShutdownIdleThreads(nsITimer* aTimer, void* /* aClosure */) { AssertIsOnMainThread(); RuntimeService* runtime = RuntimeService::GetService(); NS_ASSERTION(runtime, "This should never be null!"); NS_ASSERTION(aTimer == runtime->mIdleThreadTimer, "Wrong timer!"); // Cheat a little and grab all threads that expire within one second of now. TimeStamp now = TimeStamp::Now() + TimeDuration::FromSeconds(1); TimeStamp nextExpiration; nsAutoTArray, 20> expiredThreads; { MutexAutoLock lock(runtime->mMutex); for (PRUint32 index = 0; index < runtime->mIdleThreadArray.Length(); index++) { IdleThreadInfo& info = runtime->mIdleThreadArray[index]; if (info.mExpirationTime > now) { nextExpiration = info.mExpirationTime; break; } nsCOMPtr* thread = expiredThreads.AppendElement(); thread->swap(info.mThread); } if (!expiredThreads.IsEmpty()) { runtime->mIdleThreadArray.RemoveElementsAt(0, expiredThreads.Length()); } } NS_ASSERTION(nextExpiration.IsNull() || !expiredThreads.IsEmpty(), "Should have a new time or there should be some threads to shut " "down"); for (PRUint32 index = 0; index < expiredThreads.Length(); index++) { if (NS_FAILED(expiredThreads[index]->Shutdown())) { NS_WARNING("Failed to shutdown thread!"); } } if (!nextExpiration.IsNull()) { TimeDuration delta = nextExpiration - TimeStamp::Now(); PRUint32 delay(delta > TimeDuration(0) ? delta.ToMilliseconds() : 0); // Reschedule the timer. if (NS_FAILED(aTimer->InitWithFuncCallback(ShutdownIdleThreads, nsnull, delay, nsITimer::TYPE_ONE_SHOT))) { NS_ERROR("Can't schedule timer!"); } } } nsresult RuntimeService::Init() { AssertIsOnMainThread(); mIdleThreadTimer = do_CreateInstance(NS_TIMER_CONTRACTID); NS_ENSURE_STATE(mIdleThreadTimer); PRBool ok = mDomainMap.Init(); NS_ENSURE_STATE(ok); ok = mWindowMap.Init(); NS_ENSURE_STATE(ok); nsresult rv; nsCOMPtr obs = do_GetService(NS_OBSERVERSERVICE_CONTRACTID, &rv); NS_ENSURE_SUCCESS(rv, rv); rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_THREADS_OBSERVER_ID, PR_FALSE); NS_ENSURE_SUCCESS(rv, rv); mObserved = true; for (PRUint32 index = 0; index < NS_ARRAY_LENGTH(gPrefsToWatch); index++) { if (NS_FAILED(Preferences::RegisterCallback(PrefCallback, gPrefsToWatch[index], this))) { NS_WARNING("Failed to register pref callback?!"); } PrefCallback(gPrefsToWatch[index], this); } // We assume atomic 32bit reads/writes. If this assumption doesn't hold on // some wacky platform then the worst that could happen is that the close // handler will run for a slightly different amount of time. if (NS_FAILED(Preferences::AddIntVarCache(&sCloseHandlerTimeoutSeconds, PREF_MAX_SCRIPT_RUN_TIME, MAX_SCRIPT_RUN_TIME_SEC))) { NS_WARNING("Failed to register timeout cache?!"); } PRInt32 maxPerDomain = Preferences::GetInt(PREF_WORKERS_MAX_PER_DOMAIN, MAX_WORKERS_PER_DOMAIN); gMaxWorkersPerDomain = NS_MAX(0, maxPerDomain); mDetectorName = Preferences::GetLocalizedCString("intl.charset.detector"); nsCOMPtr platformCharset = do_GetService(NS_PLATFORMCHARSET_CONTRACTID, &rv); if (NS_SUCCEEDED(rv)) { rv = platformCharset->GetCharset(kPlatformCharsetSel_PlainTextInFile, mSystemCharset); } return NS_OK; } // This spins the event loop until all workers are finished and their threads // have been joined. void RuntimeService::Cleanup() { AssertIsOnMainThread(); mShuttingDown = true; if (mIdleThreadTimer) { if (NS_FAILED(mIdleThreadTimer->Cancel())) { NS_WARNING("Failed to cancel idle timer!"); } mIdleThreadTimer = nsnull; } if (mDomainMap.IsInitialized()) { MutexAutoLock lock(mMutex); nsAutoTArray workers; mDomainMap.EnumerateRead(AddAllTopLevelWorkersToArray, &workers); if (!workers.IsEmpty()) { nsIThread* currentThread; // Cancel all top-level workers. { MutexAutoUnlock unlock(mMutex); currentThread = NS_GetCurrentThread(); NS_ASSERTION(currentThread, "This should never be null!"); AutoSafeJSContext cx; for (PRUint32 index = 0; index < workers.Length(); index++) { if (!workers[index]->Kill(cx)) { NS_WARNING("Failed to cancel worker!"); } } } // Shut down any idle threads. if (!mIdleThreadArray.IsEmpty()) { nsAutoTArray, 20> idleThreads; PRUint32 idleThreadCount = mIdleThreadArray.Length(); idleThreads.SetLength(idleThreadCount); for (PRUint32 index = 0; index < idleThreadCount; index++) { NS_ASSERTION(mIdleThreadArray[index].mThread, "Null thread!"); idleThreads[index].swap(mIdleThreadArray[index].mThread); } mIdleThreadArray.Clear(); MutexAutoUnlock unlock(mMutex); for (PRUint32 index = 0; index < idleThreadCount; index++) { if (NS_FAILED(idleThreads[index]->Shutdown())) { NS_WARNING("Failed to shutdown thread!"); } } } // 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))) { NS_WARNING("Something bad happened!"); break; } } } } if (mWindowMap.IsInitialized()) { NS_ASSERTION(!mWindowMap.Count(), "All windows should have been released!"); } if (mObserved) { for (PRUint32 index = 0; index < NS_ARRAY_LENGTH(gPrefsToWatch); index++) { Preferences::UnregisterCallback(PrefCallback, gPrefsToWatch[index], this); } nsCOMPtr obs = do_GetService(NS_OBSERVERSERVICE_CONTRACTID); NS_WARN_IF_FALSE(obs, "Failed to get observer service?!"); if (obs) { nsresult rv = obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_THREADS_OBSERVER_ID); mObserved = !NS_SUCCEEDED(rv); } } } // static PLDHashOperator RuntimeService::AddAllTopLevelWorkersToArray(const nsACString& aKey, WorkerDomainInfo* aData, void* aUserArg) { nsTArray* array = static_cast*>(aUserArg); #ifdef DEBUG for (PRUint32 index = 0; index < aData->mActiveWorkers.Length(); index++) { NS_ASSERTION(!aData->mActiveWorkers[index]->GetParent(), "Shouldn't have a parent in this list!"); } #endif array->AppendElements(aData->mActiveWorkers); // These might not be top-level workers... for (PRUint32 index = 0; index < aData->mQueuedWorkers.Length(); index++) { WorkerPrivate* worker = aData->mQueuedWorkers[index]; if (!worker->GetParent()) { array->AppendElement(worker); } } return PL_DHASH_NEXT; } void RuntimeService::GetWorkersForWindow(nsPIDOMWindow* aWindow, nsTArray& aWorkers) { AssertIsOnMainThread(); nsTArray* workers; if (mWindowMap.Get(aWindow, &workers)) { NS_ASSERTION(!workers->IsEmpty(), "Should have been removed!"); aWorkers.AppendElements(*workers); } else { NS_ASSERTION(aWorkers.IsEmpty(), "Should be empty!"); } } void RuntimeService::CancelWorkersForWindow(JSContext* aCx, nsPIDOMWindow* aWindow) { AssertIsOnMainThread(); nsAutoTArray workers; GetWorkersForWindow(aWindow, workers); if (!workers.IsEmpty()) { AutoSafeJSContext cx(aCx); for (PRUint32 index = 0; index < workers.Length(); index++) { if (!workers[index]->Cancel(aCx)) { NS_WARNING("Failed to cancel worker!"); } } } } void RuntimeService::SuspendWorkersForWindow(JSContext* aCx, nsPIDOMWindow* aWindow) { AssertIsOnMainThread(); nsAutoTArray workers; GetWorkersForWindow(aWindow, workers); if (!workers.IsEmpty()) { JSAutoRequest ar(aCx); for (PRUint32 index = 0; index < workers.Length(); index++) { if (!workers[index]->Suspend(aCx)) { NS_WARNING("Failed to cancel worker!"); } } } } void RuntimeService::ResumeWorkersForWindow(JSContext* aCx, nsPIDOMWindow* aWindow) { AssertIsOnMainThread(); nsAutoTArray workers; GetWorkersForWindow(aWindow, workers); if (!workers.IsEmpty()) { JSAutoRequest ar(aCx); for (PRUint32 index = 0; index < workers.Length(); index++) { if (!workers[index]->Resume(aCx)) { NS_WARNING("Failed to cancel worker!"); } } } } void RuntimeService::NoteIdleThread(nsIThread* aThread) { AssertIsOnMainThread(); NS_ASSERTION(aThread, "Null pointer!"); static TimeDuration timeout = TimeDuration::FromSeconds(IDLE_THREAD_TIMEOUT_SEC); TimeStamp expirationTime = TimeStamp::Now() + timeout; bool shutdown; if (mShuttingDown) { shutdown = true; } else { MutexAutoLock lock(mMutex); if (mIdleThreadArray.Length() < MAX_IDLE_THREADS) { IdleThreadInfo* info = mIdleThreadArray.AppendElement(); info->mThread = aThread; info->mExpirationTime = expirationTime; shutdown = false; } else { shutdown = true; } } // Too many idle threads, just shut this one down. if (shutdown) { if (NS_FAILED(aThread->Shutdown())) { NS_WARNING("Failed to shutdown thread!"); } return; } // Schedule timer. if (NS_FAILED(mIdleThreadTimer-> InitWithFuncCallback(ShutdownIdleThreads, nsnull, IDLE_THREAD_TIMEOUT_SEC * 1000, nsITimer::TYPE_ONE_SHOT))) { NS_ERROR("Can't schedule timer!"); } } 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()); } } } #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()); } } } #endif // 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; } NS_NOTREACHED("Unknown observer topic!"); return NS_OK; } RuntimeService::AutoSafeJSContext::AutoSafeJSContext(JSContext* aCx) : mContext(aCx ? aCx : GetSafeContext()) { AssertIsOnMainThread(); if (mContext) { nsIThreadJSContextStack* stack = nsContentUtils::ThreadJSContextStack(); NS_ASSERTION(stack, "This should never be null!"); if (NS_FAILED(stack->Push(mContext))) { NS_ERROR("Couldn't push safe JSContext!"); mContext = nsnull; return; } JS_BeginRequest(mContext); } } RuntimeService::AutoSafeJSContext::~AutoSafeJSContext() { AssertIsOnMainThread(); if (mContext) { JS_ReportPendingException(mContext); JS_EndRequest(mContext); nsIThreadJSContextStack* stack = nsContentUtils::ThreadJSContextStack(); NS_ASSERTION(stack, "This should never be null!"); JSContext* cx; if (NS_FAILED(stack->Pop(&cx))) { NS_ERROR("Failed to pop safe context!"); } if (cx != mContext) { NS_ERROR("Mismatched context!"); } } } // static JSContext* RuntimeService::AutoSafeJSContext::GetSafeContext() { AssertIsOnMainThread(); nsIThreadJSContextStack* stack = nsContentUtils::ThreadJSContextStack(); NS_ASSERTION(stack, "This should never be null!"); JSContext* cx; if (NS_FAILED(stack->GetSafeJSContext(&cx))) { NS_ERROR("Couldn't get safe JSContext!"); return nsnull; } NS_ASSERTION(!JS_IsExceptionPending(cx), "Already has an exception?!"); return cx; }