/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "nsThreadManager.h" #include "nsThread.h" #include "nsThreadPool.h" #include "nsThreadUtils.h" #include "nsIClassInfoImpl.h" #include "nsExceptionHandler.h" #include "nsTArray.h" #include "nsXULAppAPI.h" #include "nsExceptionHandler.h" #include "mozilla/AbstractThread.h" #include "mozilla/AppShutdown.h" #include "mozilla/ClearOnShutdown.h" #include "mozilla/CycleCollectedJSContext.h" // nsAutoMicroTask #include "mozilla/EventQueue.h" #include "mozilla/InputTaskManager.h" #include "mozilla/Mutex.h" #include "mozilla/NeverDestroyed.h" #include "mozilla/Perfetto.h" #include "mozilla/Preferences.h" #include "mozilla/ProfilerMarkers.h" #include "mozilla/SpinEventLoopUntil.h" #include "mozilla/StaticPtr.h" #include "mozilla/TaskQueue.h" #include "mozilla/ThreadEventQueue.h" #include "mozilla/ThreadLocal.h" #include "mozilla/ipc/SharedMemoryMapping.h" #include "TaskController.h" #include "ThreadEventTarget.h" #ifdef MOZ_CANARY # include # include #endif #include "MainThreadIdlePeriod.h" using namespace mozilla; static MOZ_THREAD_LOCAL(bool) sTLSIsMainThread; bool NS_IsMainThreadTLSInitialized() { return sTLSIsMainThread.initialized(); } class BackgroundEventTarget final : public nsIEventTarget { public: NS_DECL_THREADSAFE_ISUPPORTS NS_DECL_NSIEVENTTARGET_FULL BackgroundEventTarget() = default; nsresult Init(); already_AddRefed CreateBackgroundTaskQueue(StaticString aName); void Shutdown(); private: ~BackgroundEventTarget() = default; RefPtr mPool; RefPtr mIOPool; }; NS_IMPL_ISUPPORTS(BackgroundEventTarget, nsIEventTarget) nsresult BackgroundEventTarget::Init() { RefPtr pool = MakeRefPtr(); nsresult rv = pool->SetName("BackgroundThreadPool"_ns); NS_ENSURE_SUCCESS(rv, rv); // Use potentially more conservative stack size. rv = pool->SetThreadStackSize(nsIThreadManager::kThreadPoolStackSize); NS_ENSURE_SUCCESS(rv, rv); // Thread limit of 2 makes deadlock during synchronous dispatch less likely. rv = pool->SetThreadLimit(2); NS_ENSURE_SUCCESS(rv, rv); rv = pool->SetIdleThreadLimit(1); NS_ENSURE_SUCCESS(rv, rv); // Leave the base idle thread alive for up to 5 minutes rv = pool->SetIdleThreadMaximumTimeout(300000); NS_ENSURE_SUCCESS(rv, rv); // Leave excess idle threads alive for up to 1 second rv = pool->SetIdleThreadGraceTimeout(1000); NS_ENSURE_SUCCESS(rv, rv); // Initialize the background I/O event target. RefPtr ioPool = MakeRefPtr(); // The io pool spends a lot of its time blocking on io, so we want to offload // these jobs on a lower priority if available. rv = ioPool->SetQoSForThreads(nsIThread::QOS_PRIORITY_LOW); NS_ENSURE_SUCCESS( rv, rv); // note: currently infallible, keeping this for brevity. rv = ioPool->SetName("BgIOThreadPool"_ns); NS_ENSURE_SUCCESS(rv, rv); // Use potentially more conservative stack size. rv = ioPool->SetThreadStackSize(nsIThreadManager::kThreadPoolStackSize); NS_ENSURE_SUCCESS(rv, rv); // Thread limit of 4 makes deadlock during synchronous dispatch less likely. // TODO: This pool is meant to host blocking (file, network) IO, so we might // want to configure an even higher limit to allow more parallel operations // to find another thread. But first we should audit the existing uses of // NS_DISPATCH_EVENT_MAY_BLOCK if they are not just CPU heavy runnables. rv = ioPool->SetThreadLimit(4); NS_ENSURE_SUCCESS(rv, rv); rv = ioPool->SetIdleThreadLimit(1); NS_ENSURE_SUCCESS(rv, rv); // Leave allowed idle threads alive for up to 5 minutes rv = ioPool->SetIdleThreadMaximumTimeout(300000); NS_ENSURE_SUCCESS(rv, rv); // Leave excess idle threads alive for up to 500ms seconds rv = ioPool->SetIdleThreadGraceTimeout(500); NS_ENSURE_SUCCESS(rv, rv); pool.swap(mPool); ioPool.swap(mIOPool); return NS_OK; } NS_IMETHODIMP_(bool) BackgroundEventTarget::IsOnCurrentThreadInfallible() { return mPool->IsOnCurrentThread() || mIOPool->IsOnCurrentThread(); } NS_IMETHODIMP BackgroundEventTarget::IsOnCurrentThread(bool* aValue) { bool value = false; if (NS_SUCCEEDED(mPool->IsOnCurrentThread(&value)) && value) { *aValue = value; return NS_OK; } return mIOPool->IsOnCurrentThread(aValue); } NS_IMETHODIMP BackgroundEventTarget::Dispatch(already_AddRefed aRunnable, DispatchFlags aFlags) { nsCOMPtr runnable(std::move(aRunnable)); // First, try to dispatch to `mIOPool` if we're a blocking event. if (aFlags & NS_DISPATCH_EVENT_MAY_BLOCK) { DispatchFlags ioPoolFlags = aFlags | ~NS_DISPATCH_EVENT_MAY_BLOCK; if (ioPoolFlags & NS_DISPATCH_AT_END && !mIOPool->IsOnCurrentThread()) { ioPoolFlags &= ~NS_DISPATCH_AT_END; } // First we'll try to dispatch to `mIOPool` if we're a blocking event. If // this fails, we may be late enough in shutdown that `mIOPool` has been // shut down, but `mPool` has not, so we'll fall through to dispatching // there. nsresult rv = mIOPool->Dispatch(do_AddRef(runnable), ioPoolFlags | NS_DISPATCH_FALLIBLE); if (NS_SUCCEEDED(rv)) { return rv; } } DispatchFlags poolFlags = aFlags & ~NS_DISPATCH_EVENT_MAY_BLOCK; if (poolFlags & NS_DISPATCH_AT_END && !mPool->IsOnCurrentThread()) { poolFlags &= ~NS_DISPATCH_AT_END; } // Either this event is not potentially blocking, or the dispatch to `mIOPool` // failed - dispatch to `mPool`. return mPool->Dispatch(runnable.forget(), poolFlags); } NS_IMETHODIMP BackgroundEventTarget::DispatchFromScript(nsIRunnable* aRunnable, DispatchFlags aFlags) { nsCOMPtr runnable(aRunnable); return Dispatch(runnable.forget(), aFlags); } NS_IMETHODIMP BackgroundEventTarget::DelayedDispatch(already_AddRefed aRunnable, uint32_t) { nsCOMPtr dropRunnable(aRunnable); return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHODIMP BackgroundEventTarget::RegisterShutdownTask(nsITargetShutdownTask* aTask) { return mPool->RegisterShutdownTask(aTask); } NS_IMETHODIMP BackgroundEventTarget::UnregisterShutdownTask(nsITargetShutdownTask* aTask) { return mPool->UnregisterShutdownTask(aTask); } nsIEventTarget::FeatureFlags BackgroundEventTarget::GetFeatures() { return SUPPORTS_SHUTDOWN_TASKS | SUPPORTS_SHUTDOWN_TASK_DISPATCH; } void BackgroundEventTarget::Shutdown() { // Note that our shutdown tasks are registered on `mPool` and will all // execute there (as well as any events they may dispatch to ourselves, // regardless of NS_DISPATCH_EVENT_MAY_BLOCK). mIOPool->Shutdown(); mPool->Shutdown(); } already_AddRefed BackgroundEventTarget::CreateBackgroundTaskQueue( StaticString aName) { return TaskQueue::Create(do_AddRef(this), aName).forget(); } extern "C" { // This uses the C language linkage because it's exposed to Rust // via the xpcom/rust/moz_task crate. bool NS_IsMainThread() { return sTLSIsMainThread.get(); } } void NS_SetMainThread() { if (!sTLSIsMainThread.init()) { MOZ_CRASH(); } sTLSIsMainThread.set(true); MOZ_ASSERT(NS_IsMainThread()); // We initialize the SerialEventTargetGuard's TLS here for simplicity as it // needs to be initialized around the same time you would initialize // sTLSIsMainThread. SerialEventTargetGuard::InitTLS(); nsThreadPool::InitTLS(); } #ifdef DEBUG namespace mozilla { void AssertIsOnMainThread() { MOZ_ASSERT(NS_IsMainThread(), "Wrong thread!"); } } // namespace mozilla #endif //----------------------------------------------------------------------------- /* static */ void nsThreadManager::ReleaseThread(void* aData) { static_cast(aData)->Release(); } // statically allocated instance NS_IMETHODIMP_(MozExternalRefCountType) nsThreadManager::AddRef() { return 2; } NS_IMETHODIMP_(MozExternalRefCountType) nsThreadManager::Release() { return 1; } NS_IMPL_CLASSINFO(nsThreadManager, nullptr, nsIClassInfo::THREADSAFE | nsIClassInfo::SINGLETON, NS_THREADMANAGER_CID) NS_IMPL_QUERY_INTERFACE_CI(nsThreadManager, nsIThreadManager) NS_IMPL_CI_INTERFACE_GETTER(nsThreadManager, nsIThreadManager) //----------------------------------------------------------------------------- /*static*/ uint32_t nsIThreadManager::LargeStackSize() { // This is just short of 2MB to avoid the Linux kernel allocating an entire // 2MB huge page for the stack on first access. ASan and TSan builds are // given a larger stack size due to extra data and red-zones which consume // stack space. #if defined(__EMSCRIPTEN__) // The in-process wasm interpreter (GECKO_WASM_INTERP) recurses one C++ frame // per guest wasm call, so worker threads running guest wasm (e.g. // embed-demo-in-embed-demo) need a much deeper physical stack than the JS // native-stack quota (WORKER_CONTEXT_NATIVE_STACK_LIMIT). Keep this above it. return 8192 * 1024 - 2 * mozilla::ipc::shared_memory::SystemPageSize(); #elif defined(MOZ_ASAN) || defined(MOZ_TSAN) return 4096 * 1024 - 2 * mozilla::ipc::shared_memory::SystemPageSize(); #else return 2048 * 1024 - 2 * mozilla::ipc::shared_memory::SystemPageSize(); #endif } /*static*/ nsThreadManager& nsThreadManager::get() { static NeverDestroyed sInstance; return *sInstance; } #ifdef GECKO_ST_THREADS // --- Virtual-thread scheduler (single-threaded wasm) --------------------- // All XPCOM threads are event queues drained here, on the sole real thread. // The mozglue/NSPR condvar-wait hooks call STPump so that any blocking wait // (SpinEventLoopUntil, sync dispatch, monitor waits) makes forward progress // instead of deadlocking. namespace { struct STPumpHook { bool (*mFn)(void*); void* mClosure; }; struct STSchedState { nsTArray> mThreads; nsTArray mHooks; uint32_t mDepth = 0; }; static STSchedState* sSTState = nullptr; static nsThread* sSTOverride = nullptr; // SpiderMonkey "current JSContext" (js::TlsContext) accessors, defined in // js/src/vm/JSContext.cpp. The one real thread's TlsContext slot is shared by // all virtual threads, so we swap it when impersonating a different one (a // worker runs its own JSContext; the main thread its own). extern "C" void* gecko_st_get_tlscx(); extern "C" void gecko_st_set_tlscx(void* aCx); // Cycle-collector per-thread data (CollectorData*), defined in // nsCycleCollector.cpp. Swapped per virtual thread for the same reason as // js::TlsContext -- each DOM worker has its own cycle collector. extern "C" void* gecko_st_get_ccdata(); extern "C" void gecko_st_set_ccdata(void* aData); static void* sSTMainCCData = nullptr; // GC context (js::TlsGCContext), defined in js/src/gc/GC.cpp. Also per-real- // thread TLS that main + worker runtimes clobber; swapped per virtual thread. extern "C" void* gecko_st_get_gccx(); extern "C" void gecko_st_set_gccx(void* aCx); static void* sSTMainGCCx = nullptr; // The main thread's JSContext, captured the first time we impersonate away // from the main thread (it is created during startup before any impersonation, // so at that moment TlsContext holds it). Used to restore the main context // when draining main-thread events nested inside a worker impersonation. static void* sSTMainCx = nullptr; STSchedState& STState() { if (!sSTState) { sSTState = new STSchedState(); } return *sSTState; } } // namespace // Bumped by the embedder at each xul_tick entry; a stuck serial while waits // spin marks a wedged tick (see STWaitHookThunk). extern "C" unsigned gecko_st_activity = 0; namespace { bool STWaitHookThunk() { // Wall-clock wedge tripwire: if the embedder's tick serial hasn't advanced // in 30s while waits keep firing, one tick is wedged in a wait whose // condition nothing can satisfy (periodic timer/socket churn can mask it // from progress-based detection). abort() surfaces the waiter's stack. static unsigned sLastSeen = 0; static double sLastChangeMs = 0; struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); double now = double(ts.tv_sec) * 1000.0 + double(ts.tv_nsec) / 1e6; if (gecko_st_activity != sLastSeen || sLastChangeMs == 0) { sLastSeen = gecko_st_activity; sLastChangeMs = now; } else if (now - sLastChangeMs > 30000) { // A tick wedged this long inside a wait is almost certainly a scheduler // bug, but crashing the page is worse than continuing; warn once per stall. static double sLastWarnMs = 0; if (now - sLastWarnMs > 30000) { sLastWarnMs = now; fprintf(stderr, "GeckoST: WARNING tick wedged >30s inside a wait\n"); } } return nsThreadManager::STPump(); } } // namespace // Defined in mozglue (ConditionVariable_posix.cpp); NSPR reaches it weakly. extern "C" bool (*gecko_st_wait_hook)(void); // Exported pump for the embedder's main-loop tick. extern "C" bool gecko_st_pump(void) { return nsThreadManager::STPump(); } void nsThreadManager::STRegisterVirtualThread(nsThread* aThread) { STState().mThreads.AppendElement(aThread); } void nsThreadManager::STUnregisterVirtualThread(nsThread* aThread) { if (sSTState) { sSTState->mThreads.RemoveElement(aThread); } } void nsThreadManager::STAddPumpHook(bool (*aHook)(void*), void* aClosure) { STState().mHooks.AppendElement(STPumpHook{aHook, aClosure}); } void nsThreadManager::STRemovePumpHook(bool (*aHook)(void*), void* aClosure) { if (sSTState) { sSTState->mHooks.RemoveElementsBy([&](const STPumpHook& h) { return h.mFn == aHook && h.mClosure == aClosure; }); } } nsThread* nsThreadManager::STCurrentOverride() { return sSTOverride; } bool nsThreadManager::STPump() { if (!sSTState) { return false; } STSchedState& st = *sSTState; if (st.mDepth >= 64) { return false; } st.mDepth++; bool did = false; nsTArray hooks = st.mHooks.Clone(); for (auto& hook : hooks) { did |= hook.mFn(hook.mClosure); } // Copy: threads can register/unregister while being pumped. nsTArray> threads = st.mThreads.Clone(); for (auto& thread : threads) { did |= thread->STPumpOne(); } // Drain a bounded batch of MAIN-thread events when either (a) no vthread made // progress -- the wait can then only be satisfied by a main-thread event, or // (b) we're NESTED inside another pump (st.mDepth > 1), i.e. some blocking // wait is on the stack whose completion may depend on main-thread work (e.g. // a worker's sync-loop script load waiting on the main-thread fetch). At the // top level a busy vthread must not starve this, but the top-level tick drains // main itself, so gating on !did there is fine. if (!did || st.mDepth > 1) { // Nested-event-loop style, matching what Gecko's blocking main-thread waits // (SpinEventLoopUntil, sync IPC) already permit. nsThread* main = nsThreadManager::get().mMainThread; if (main && !main->mSTPumping) { main->mSTPumping = true; // We may be nested inside a virtual-thread impersonation (e.g. a worker's // sync-loop wait). main->ProcessNextEvent must run AS the main thread, so // temporarily restore full main-thread identity: XPCOM override, NSPR // current thread, NS_IsMainThread, plus the JS context + cycle-collector // data (which are per-real-thread and currently hold the impersonated // thread's values). nsThread* savedOverride = sSTOverride; PRThread* savedPR = PR_STSwapCurrentThread(main->GetPRThread()); bool savedIsMain = sTLSIsMainThread.get(); void* prevCx = gecko_st_get_tlscx(); void* prevCCData = gecko_st_get_ccdata(); void* prevGCCx = gecko_st_get_gccx(); sSTOverride = nullptr; sTLSIsMainThread.set(true); if (sSTMainCx) { gecko_st_set_tlscx(sSTMainCx); } if (sSTMainCCData) { gecko_st_set_ccdata(sSTMainCCData); } if (sSTMainGCCx) { gecko_st_set_gccx(sSTMainGCCx); } int drained = 0; for (int i = 0; i < 32; i++) { bool processed = false; if (NS_FAILED(main->ProcessNextEvent(false, &processed)) || !processed) { break; } did = true; drained++; } gecko_st_set_tlscx(prevCx); gecko_st_set_ccdata(prevCCData); gecko_st_set_gccx(prevGCCx); sTLSIsMainThread.set(savedIsMain); PR_STSwapCurrentThread(savedPR); sSTOverride = savedOverride; main->mSTPumping = false; } } st.mDepth--; return did; } mozilla::STAutoImpersonate::STAutoImpersonate(nsThread* aThread) { mThread = aThread; mPrevOverride = sSTOverride; // Lazily capture the main thread's JSContext the first time we impersonate // away from it. if (!mPrevOverride) { sSTMainCx = gecko_st_get_tlscx(); sSTMainCCData = gecko_st_get_ccdata(); sSTMainGCCx = gecko_st_get_gccx(); } sSTOverride = aThread; mPrevPR = PR_STSwapCurrentThread(aThread->GetPRThread()); mPrevIsMain = sTLSIsMainThread.get(); sTLSIsMainThread.set(false); // Swap SpiderMonkey's current context to this virtual thread's context, but // ONLY for JS-running threads (DOM workers). Non-JS vthreads never touch JS // and are left exactly as before. For a worker that hasn't created its // context yet, the stored context is null, which lets its JS_NewContext // succeed (that asserts TlsContext is empty). mPrevCx = nullptr; mPrevCCData = nullptr; mPrevGCCx = nullptr; if (aThread->STIsJSThread()) { mPrevCx = gecko_st_get_tlscx(); gecko_st_set_tlscx(aThread->STGetJSContext()); mPrevCCData = gecko_st_get_ccdata(); gecko_st_set_ccdata(aThread->STGetCCData()); mPrevGCCx = gecko_st_get_gccx(); gecko_st_set_gccx(aThread->STGetGCContext()); } } mozilla::STAutoImpersonate::~STAutoImpersonate() { // Capture whatever context this virtual thread now owns (a worker may have // just created one) so the next impersonation restores it, then restore the // context that was current before we impersonated. if (mThread->STIsJSThread()) { mThread->STSetJSContext(gecko_st_get_tlscx()); gecko_st_set_tlscx(mPrevCx); mThread->STSetCCData(gecko_st_get_ccdata()); gecko_st_set_ccdata(mPrevCCData); mThread->STSetGCContext(gecko_st_get_gccx()); gecko_st_set_gccx(mPrevGCCx); } sTLSIsMainThread.set(mPrevIsMain); PR_STSwapCurrentThread(mPrevPR); sSTOverride = mPrevOverride; } #endif // GECKO_ST_THREADS nsThreadManager::nsThreadManager() : mCurThreadIndex(0), mMutex("nsThreadManager::mMutex"), mState(State::eUninit) {} nsThreadManager::~nsThreadManager() = default; nsresult nsThreadManager::Init() { // Initialize perfetto if on Android. InitPerfetto(); // Child processes need to initialize the thread manager before they // initialize XPCOM in order to set up the crash reporter. This leads to // situations where we get initialized twice. { OffTheBooksMutexAutoLock lock(mMutex); if (mState > State::eUninit) { return NS_OK; } } if (PR_NewThreadPrivateIndex(&mCurThreadIndex, ReleaseThread) == PR_FAILURE) { return NS_ERROR_FAILURE; } #ifdef GECKO_ST_THREADS // From here on, every condvar wait pumps the virtual-thread scheduler. gecko_st_wait_hook = &STWaitHookThunk; #endif #ifdef MOZ_CANARY const int flags = O_WRONLY | O_APPEND | O_CREAT | O_NONBLOCK; const mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH; char* env_var_flag = getenv("MOZ_KILL_CANARIES"); sCanaryOutputFD = env_var_flag ? (env_var_flag[0] ? open(env_var_flag, flags, mode) : STDERR_FILENO) : 0; #endif TaskController::Initialize(); // Initialize idle handling. RefPtr idlePeriod = MakeRefPtr(); RefPtr idleManager = MakeRefPtr(idlePeriod.forget()); TaskController::Get()->SetIdleTaskManager(idleManager.forget()); // Create main thread queue that forwards events to TaskController and // construct main thread. UniquePtr queue = MakeUnique(true); RefPtr synchronizedQueue = MakeRefPtr(std::move(queue), true); mMainThread = new nsThread(WrapNotNull(synchronizedQueue), nsThread::MAIN_THREAD, {0, false, false, Some(W3_LONGTASK_BUSY_WINDOW_MS)}); nsresult rv = mMainThread->InitCurrentThread(); if (NS_FAILED(rv)) { mMainThread = nullptr; return rv; } #ifdef MOZ_MEMORY jemalloc_set_main_thread(); jemalloc_thread_local_arena(true); #endif // Init AbstractThread. AbstractThread::InitTLS(); AbstractThread::InitMainThread(); // Initialize the background event target. RefPtr target(new BackgroundEventTarget()); rv = target->Init(); NS_ENSURE_SUCCESS(rv, rv); { OffTheBooksMutexAutoLock lock(mMutex); mBackgroundEventTarget = std::move(target); mState = State::eActive; } return NS_OK; } void nsThreadManager::ShutdownNonMainThreads() { MOZ_ASSERT(NS_IsMainThread(), "shutdown not called from main thread"); // Empty the main thread event queue before we begin shutting down threads. NS_ProcessPendingEvents(mMainThread); mMainThread->mEvents->RunShutdownTasks(); RefPtr backgroundEventTarget; { OffTheBooksMutexAutoLock lock(mMutex); MOZ_ASSERT(mState == State::eActive, "shutdown called multiple times"); backgroundEventTarget = mBackgroundEventTarget; } // This will execute the shutdown tasks of still associated TaskQueues, // if any. backgroundEventTarget->Shutdown(); { // Prevent new nsThreads from being created, and collect a list of threads // which need to be shut down. // // We don't prevent new thread creation until we've shut down background // task queues, to ensure that they are able to start thread pool threads // for shutdown tasks. nsTArray> threadsToShutdown; { OffTheBooksMutexAutoLock lock(mMutex); mState = State::eShutdown; for (auto* thread : mThreadList) { if (thread->ShutdownRequired()) { threadsToShutdown.AppendElement(thread); } } } // It's tempting to walk the list of threads here and tell them each to stop // accepting new events, but that could lead to badness if one of those // threads is stuck waiting for a response from another thread. To do it // right, we'd need some way to interrupt the threads. // // Instead, we process events on the current thread while waiting for // threads to shutdown. This means that we have to preserve a mostly // functioning world until such time as the threads exit. // As we're going to be waiting for all asynchronous shutdowns below, we // can begin asynchronously shutting down all XPCOM threads here, rather // than shutting each thread down one-at-a-time. for (const auto& thread : threadsToShutdown) { thread->AsyncShutdown(); } } // NB: It's possible that there are events in the queue that want to *start* // an asynchronous shutdown. But we have already started async shutdown of // the threads above, so there's no need to worry about them. We only have to // wait for all in-flight asynchronous thread shutdowns to complete. mMainThread->WaitForAllAsynchronousShutdowns(); // There are no more background threads at this point. } void nsThreadManager::ShutdownMainThread() { #ifdef DEBUG { OffTheBooksMutexAutoLock lock(mMutex); MOZ_ASSERT(mState == State::eShutdown, "Must have called BeginShutdown"); } #endif // Do NS_ProcessPendingEvents but with special handling to set // mEventsAreDoomed atomically with the removal of the last event. This means // that PutEvent cannot succeed if the event would be left in the main thread // queue after our final call to NS_ProcessPendingEvents. // See comments in `nsThread::ThreadFunc` for a more detailed explanation. while (true) { if (mMainThread->mEvents->ShutdownIfNoPendingEvents()) { break; } NS_ProcessPendingEvents(mMainThread); } // Normally thread shutdown clears the observer for the thread, but since the // main thread is special we do it manually here after we're sure all events // have been processed. mMainThread->SetObserver(nullptr); OffTheBooksMutexAutoLock lock(mMutex); mBackgroundEventTarget = nullptr; } void nsThreadManager::ReleaseMainThread() { #ifdef DEBUG { OffTheBooksMutexAutoLock lock(mMutex); MOZ_ASSERT(mState == State::eShutdown, "Must have called BeginShutdown"); MOZ_ASSERT(!mBackgroundEventTarget, "Must have called ShutdownMainThread"); } #endif MOZ_ASSERT(mMainThread); // Release main thread object. mMainThread = nullptr; // Remove the TLS entry for the main thread. PR_SetThreadPrivate(mCurThreadIndex, nullptr); } void nsThreadManager::RegisterCurrentThread(nsThread& aThread) { MOZ_ASSERT(aThread.GetPRThread() == PR_GetCurrentThread(), "bad aThread"); aThread.AddRef(); // for TLS entry PR_SetThreadPrivate(mCurThreadIndex, &aThread); #ifdef DEBUG { OffTheBooksMutexAutoLock lock(mMutex); MOZ_ASSERT(aThread.isInList(), "Thread was not added to the thread list before registering!"); } #endif } void nsThreadManager::UnregisterCurrentThread(nsThread& aThread) { MOZ_ASSERT(aThread.GetPRThread() == PR_GetCurrentThread(), "bad aThread"); PR_SetThreadPrivate(mCurThreadIndex, nullptr); // Ref-count balanced via ReleaseThread } // Not to be used for MainThread! RefPtr nsThreadManager::CreateCurrentThread( SynchronizedEventQueue* aQueue) { // Make sure we don't have an nsThread yet. MOZ_ASSERT(!PR_GetThreadPrivate(mCurThreadIndex)); if (!AllowNewXPCOMThreads()) { return nullptr; } RefPtr thread = new nsThread( WrapNotNull(aQueue), nsThread::NOT_MAIN_THREAD, {.stackSize = 0}); if (NS_FAILED(thread->InitCurrentThread())) { return nullptr; } // Note: 'thread' now has an additional reference, held in TLS (because // nsThreadManager::RegisterCurrentThread manually AddRef()s it). That keeps // the object alive, even if our caller disregards our returned RefPtr. return thread; } nsresult nsThreadManager::DispatchToBackgroundThread( nsIRunnable* aEvent, nsIEventTarget::DispatchFlags aDispatchFlags) { RefPtr backgroundTarget; { OffTheBooksMutexAutoLock lock(mMutex); if (!AllowNewXPCOMThreadsLocked() || !mBackgroundEventTarget) { return NS_ERROR_FAILURE; } backgroundTarget = mBackgroundEventTarget; } return backgroundTarget->Dispatch(aEvent, aDispatchFlags); } already_AddRefed nsThreadManager::CreateBackgroundTaskQueue( mozilla::StaticString aName) { RefPtr backgroundTarget; { OffTheBooksMutexAutoLock lock(mMutex); if (!AllowNewXPCOMThreadsLocked() || !mBackgroundEventTarget) { return nullptr; } backgroundTarget = mBackgroundEventTarget; } return backgroundTarget->CreateBackgroundTaskQueue(aName); } nsThread* nsThreadManager::GetCurrentThread() { #ifdef GECKO_ST_THREADS if (sSTOverride) { return sSTOverride; } #endif // read thread local storage void* data = PR_GetThreadPrivate(mCurThreadIndex); if (data) { return static_cast(data); } // Keep this function working early during startup or late during shutdown on // the main thread. if (!AllowNewXPCOMThreads() || NS_IsMainThread()) { return nullptr; } // OK, that's fine. We'll dynamically create one :-) // // We assume that if we're implicitly creating a thread here that it doesn't // want an event queue. Any thread which wants an event queue should // explicitly create its nsThread wrapper. // // nsThread::InitCurrentThread() will check AllowNewXPCOMThreads, and return // an error if we're too late in shutdown to create new XPCOM threads. RefPtr thread = new nsThread(); if (NS_FAILED(thread->InitCurrentThread())) { return nullptr; } // Note: 'thread' now has an additional reference, held in TLS (because // nsThreadManager::RegisterCurrentThread manually AddRef()s it). That keeps // the object alive, even though our local reference is going out of scope. return thread.get(); } bool nsThreadManager::IsNSThread() const { { OffTheBooksMutexAutoLock lock(mMutex); if (mState == State::eUninit) { return false; } } if (auto* thread = (nsThread*)PR_GetThreadPrivate(mCurThreadIndex)) { return thread->EventQueue(); } return false; } NS_IMETHODIMP nsThreadManager::NewNamedThread( const nsACString& aName, nsIThreadManager::ThreadCreationOptions aOptions, nsIThread** aResult) { // Note: can be called from arbitrary threads AUTO_PROFILER_MARKER_TEXT("NewThread", OTHER, MarkerOptions(MarkerStack::Capture()), aName); TimeStamp startTime = TimeStamp::Now(); RefPtr queue = MakeRefPtr(MakeUnique()); RefPtr thr = MakeRefPtr(WrapNotNull(queue), nsThread::NOT_MAIN_THREAD, aOptions); // Note: nsThread::Init() will check AllowNewXPCOMThreads, and return an // error if we're too late in shutdown to create new XPCOM threads. If we // aren't, the thread will be synchronously added to mThreadList. nsresult rv = thr->Init(aName); if (NS_FAILED(rv)) { return rv; } if (!NS_IsMainThread()) { PROFILER_MARKER_TEXT( "NewThread (non-main thread)", OTHER, MarkerOptions(MarkerStack::Capture(), MarkerThreadId::MainThread(), MarkerTiming::IntervalUntilNowFrom(startTime)), aName); } thr.forget(aResult); return NS_OK; } NS_IMETHODIMP nsThreadManager::GetMainThread(nsIThread** aResult) { // Keep this functioning during Shutdown if (!mMainThread) { if (!NS_IsMainThread()) { NS_WARNING( "Called GetMainThread but there isn't a main thread and " "we're not the main thread."); } return NS_ERROR_NOT_INITIALIZED; } NS_ADDREF(*aResult = mMainThread); return NS_OK; } NS_IMETHODIMP nsThreadManager::GetCurrentThread(nsIThread** aResult) { // Keep this functioning during Shutdown if (!mMainThread) { return NS_ERROR_NOT_INITIALIZED; } *aResult = GetCurrentThread(); if (!*aResult) { return NS_ERROR_OUT_OF_MEMORY; } NS_ADDREF(*aResult); return NS_OK; } NS_IMETHODIMP nsThreadManager::SpinEventLoopUntil(const nsACString& aVeryGoodReasonToDoThis, nsINestedEventLoopCondition* aCondition) { return SpinEventLoopUntilInternal(aVeryGoodReasonToDoThis, aCondition, ShutdownPhase::NotInShutdown); } NS_IMETHODIMP nsThreadManager::SpinEventLoopUntilOrQuit( const nsACString& aVeryGoodReasonToDoThis, nsINestedEventLoopCondition* aCondition) { return SpinEventLoopUntilInternal(aVeryGoodReasonToDoThis, aCondition, ShutdownPhase::AppShutdownConfirmed); } // statics from SpinEventLoopUntil.h AutoNestedEventLoopAnnotation* AutoNestedEventLoopAnnotation::sCurrent = nullptr; StaticMutex AutoNestedEventLoopAnnotation::sStackMutex; // static from SpinEventLoopUntil.h void AutoNestedEventLoopAnnotation::AnnotateXPCOMSpinEventLoopStack( const nsACString& aStack) { if (aStack.Length() > 0) { nsCString prefixedStack(XRE_GetProcessTypeString()); prefixedStack += ": "_ns + aStack; CrashReporter::RecordAnnotationNSCString( CrashReporter::Annotation::XPCOMSpinEventLoopStack, prefixedStack); } else { CrashReporter::UnrecordAnnotation( CrashReporter::Annotation::XPCOMSpinEventLoopStack); } } nsresult nsThreadManager::SpinEventLoopUntilInternal( const nsACString& aVeryGoodReasonToDoThis, nsINestedEventLoopCondition* aCondition, ShutdownPhase aShutdownPhaseToCheck) { // XXX: We would want to AssertIsOnMainThread(); but that breaks some GTest. nsCOMPtr condition(aCondition); nsresult rv = NS_OK; if (!mozilla::SpinEventLoopUntil(aVeryGoodReasonToDoThis, [&]() -> bool { // Check if an ongoing shutdown reached our limits. if (aShutdownPhaseToCheck > ShutdownPhase::NotInShutdown && AppShutdown::GetCurrentShutdownPhase() >= aShutdownPhaseToCheck) { return true; } bool isDone = false; rv = condition->IsDone(&isDone); // JS failure should be unusual, but we need to stop and propagate // the error back to the caller. if (NS_FAILED(rv)) { return true; } return isDone; })) { // We stopped early for some reason, which is unexpected. return NS_ERROR_UNEXPECTED; } // If we exited when the condition told us to, we need to return whether // the condition encountered failure when executing. return rv; } NS_IMETHODIMP nsThreadManager::SpinEventLoopUntilEmpty() { nsIThread* thread = NS_GetCurrentThread(); while (NS_HasPendingEvents(thread)) { (void)NS_ProcessNextEvent(thread, false); } return NS_OK; } NS_IMETHODIMP nsThreadManager::GetMainThreadEventTarget(nsIEventTarget** aTarget) { nsCOMPtr target = GetMainThreadSerialEventTarget(); target.forget(aTarget); return NS_OK; } NS_IMETHODIMP nsThreadManager::DispatchToMainThread(nsIRunnable* aEvent, uint32_t aPriority, uint8_t aArgc) { // Note: C++ callers should instead use NS_DispatchToMainThread. MOZ_ASSERT(NS_IsMainThread()); // Keep this functioning during Shutdown if (NS_WARN_IF(!mMainThread)) { return NS_ERROR_NOT_INITIALIZED; } // If aPriority wasn't explicitly passed, that means it should be treated as // PRIORITY_NORMAL. if (aArgc > 0 && aPriority != nsIRunnablePriority::PRIORITY_NORMAL) { nsCOMPtr event(aEvent); return mMainThread->DispatchFromScript( new PrioritizableRunnable(event.forget(), aPriority), NS_DISPATCH_FALLIBLE); } return mMainThread->DispatchFromScript(aEvent, NS_DISPATCH_FALLIBLE); } class AutoMicroTaskWrapperRunnable final : public Runnable { public: explicit AutoMicroTaskWrapperRunnable(nsIRunnable* aEvent) : Runnable("AutoMicroTaskWrapperRunnable"), mEvent(aEvent) { MOZ_ASSERT(aEvent); } private: ~AutoMicroTaskWrapperRunnable() = default; NS_IMETHOD Run() override { nsAutoMicroTask mt; return mEvent->Run(); } RefPtr mEvent; }; NS_IMETHODIMP nsThreadManager::DispatchToMainThreadWithMicroTask(nsIRunnable* aEvent, uint32_t aPriority, uint8_t aArgc) { RefPtr runnable = MakeRefPtr(aEvent); return DispatchToMainThread(runnable, aPriority, aArgc); } void nsThreadManager::EnableMainThreadEventPrioritization() { MOZ_ASSERT(NS_IsMainThread()); InputTaskManager::Get()->EnableInputEventPrioritization(); } void nsThreadManager::FlushInputEventPrioritization() { MOZ_ASSERT(NS_IsMainThread()); InputTaskManager::Get()->FlushInputEventPrioritization(); } void nsThreadManager::SuspendInputEventPrioritization() { MOZ_ASSERT(NS_IsMainThread()); InputTaskManager::Get()->SuspendInputEventPrioritization(); } void nsThreadManager::ResumeInputEventPrioritization() { MOZ_ASSERT(NS_IsMainThread()); InputTaskManager::Get()->ResumeInputEventPrioritization(); } // static bool nsThreadManager::MainThreadHasPendingHighPriorityEvents() { MOZ_ASSERT(NS_IsMainThread()); bool retVal = false; if (get().mMainThread) { get().mMainThread->HasPendingHighPriorityEvents(&retVal); } return retVal; } NS_IMETHODIMP nsThreadManager::IdleDispatchToMainThread(nsIRunnable* aEvent, uint32_t aTimeout) { // Note: C++ callers should instead use NS_DispatchToThreadQueue or // NS_DispatchToCurrentThreadQueue. MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr event(aEvent); if (aTimeout) { return NS_DispatchToThreadQueue(event.forget(), aTimeout, mMainThread, EventQueuePriority::Idle); } return NS_DispatchToThreadQueue(event.forget(), mMainThread, EventQueuePriority::Idle); } NS_IMETHODIMP nsThreadManager::DispatchDirectTaskToCurrentThread(nsIRunnable* aEvent) { NS_ENSURE_STATE(aEvent); nsCOMPtr runnable = aEvent; return GetCurrentThread()->DispatchDirectTask(runnable.forget()); } bool nsThreadManager::AllowNewXPCOMThreads() { mozilla::OffTheBooksMutexAutoLock lock(mMutex); return AllowNewXPCOMThreadsLocked(); }