/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* 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 "Scheduler.h" #include "jsfriendapi.h" #include "LabeledEventQueue.h" #include "LeakRefPtr.h" #include "mozilla/CooperativeThreadPool.h" #include "mozilla/dom/ScriptSettings.h" #include "mozilla/ipc/BackgroundChild.h" #include "mozilla/SchedulerGroup.h" #include "nsCycleCollector.h" #include "nsIThread.h" #include "nsPrintfCString.h" #include "nsThread.h" #include "nsThreadManager.h" #include "PrioritizedEventQueue.h" #include "xpcpublic.h" // Windows silliness. winbase.h defines an empty no-argument Yield macro. #undef Yield using namespace mozilla; // Using the anonymous namespace here causes GCC to generate: // error: 'mozilla::SchedulerImpl' has a field 'mozilla::SchedulerImpl::mQueue' whose type uses the anonymous namespace namespace mozilla { namespace detail { class SchedulerEventQueue final : public SynchronizedEventQueue { public: explicit SchedulerEventQueue(UniquePtr aQueue) : mLock("Scheduler") , mNonCooperativeCondVar(mLock, "SchedulerNonCoop") , mQueue(Move(aQueue)) , mScheduler(nullptr) {} bool PutEvent(already_AddRefed&& aEvent, EventPriority aPriority) final; void Disconnect(const MutexAutoLock& aProofOfLock) final {} already_AddRefed GetEvent(bool aMayWait, EventPriority* aPriority) final; bool HasPendingEvent() final; bool HasPendingEvent(const MutexAutoLock& aProofOfLock); bool ShutdownIfNoPendingEvents() final; already_AddRefed GetObserver() final; already_AddRefed GetObserverOnThread() final; void SetObserver(nsIThreadObserver* aObserver) final; void EnableInputEventPrioritization() final; void FlushInputEventPrioritization() final; void SuspendInputEventPrioritization() final; void ResumeInputEventPrioritization() final; bool UseCooperativeScheduling() const; void SetScheduler(SchedulerImpl* aScheduler); Mutex& MutexRef() { return mLock; } private: Mutex mLock; CondVar mNonCooperativeCondVar; // Using the actual type here would avoid a virtual dispatch. However, that // would prevent us from switching between EventQueue and LabeledEventQueue at // runtime. UniquePtr mQueue; bool mEventsAreDoomed = false; SchedulerImpl* mScheduler; nsCOMPtr mObserver; }; } // namespace detail } // namespace mozilla using mozilla::detail::SchedulerEventQueue; class mozilla::SchedulerImpl { public: explicit SchedulerImpl(SchedulerEventQueue* aQueue); static already_AddRefed CreateQueue(nsIIdlePeriod* aIdlePeriod, nsThread** aThread); void Start(); void Shutdown(); void Dispatch(already_AddRefed aEvent); void Yield(); static void EnterNestedEventLoop(Scheduler::EventLoopActivation& aOuterActivation); static void ExitNestedEventLoop(Scheduler::EventLoopActivation& aOuterActivation); static void StartEvent(Scheduler::EventLoopActivation& aActivation); static void FinishEvent(Scheduler::EventLoopActivation& aActivation); void SetJSContext(size_t aIndex, JSContext* aCx) { mContexts[aIndex] = aCx; } static void YieldCallback(JSContext* aCx); static bool InterruptCallback(JSContext* aCx); CooperativeThreadPool* GetThreadPool() { return mThreadPool.get(); } static bool UnlabeledEventRunning() { return sUnlabeledEventRunning; } static bool AnyEventRunning() { return sNumThreadsRunning > 0; } CooperativeThreadPool::Resource* GetQueueResource() { return &mQueueResource; } bool UseCooperativeScheduling() const { return mQueue->UseCooperativeScheduling(); } // Preferences. static bool sPrefScheduler; static bool sPrefChaoticScheduling; static bool sPrefPreemption; static size_t sPrefThreadCount; static bool sPrefUseMultipleQueues; private: void Interrupt(JSContext* aCx); void YieldFromJS(JSContext* aCx); static void SwitcherThread(void* aData); void Switcher(); size_t mNumThreads; // Protects mQueue as well as mThreadPool. The lock comes from the SchedulerEventQueue. Mutex& mLock; CondVar mShutdownCondVar; bool mShuttingDown; UniquePtr mThreadPool; RefPtr mQueue; class QueueResource : public CooperativeThreadPool::Resource { public: explicit QueueResource(SchedulerImpl* aScheduler) : mScheduler(aScheduler) {} bool IsAvailable(const MutexAutoLock& aProofOfLock) override; private: SchedulerImpl* mScheduler; }; QueueResource mQueueResource; class SystemZoneResource : public CooperativeThreadPool::Resource { public: explicit SystemZoneResource(SchedulerImpl* aScheduler) : mScheduler(aScheduler) {} bool IsAvailable(const MutexAutoLock& aProofOfLock) override; private: SchedulerImpl* mScheduler; }; SystemZoneResource mSystemZoneResource; class ThreadController : public CooperativeThreadPool::Controller { public: ThreadController(SchedulerImpl* aScheduler, SchedulerEventQueue* aQueue) : mScheduler(aScheduler) , mMainVirtual(GetCurrentVirtualThread()) , mMainLoop(MessageLoop::current()) , mMainQueue(aQueue) {} void OnStartThread(size_t aIndex, const nsACString& aName, void* aStackTop) override; void OnStopThread(size_t aIndex) override; void OnSuspendThread(size_t aIndex) override; void OnResumeThread(size_t aIndex) override; private: SchedulerImpl* mScheduler; PRThread* mMainVirtual; MessageLoop* mMainLoop; MessageLoop* mOldMainLoop; RefPtr mMainQueue; }; ThreadController mController; static size_t sNumThreadsRunning; static bool sUnlabeledEventRunning; JSContext* mContexts[CooperativeThreadPool::kMaxThreads]; }; bool SchedulerImpl::sPrefScheduler; bool SchedulerImpl::sPrefChaoticScheduling; bool SchedulerImpl::sPrefPreemption; bool SchedulerImpl::sPrefUseMultipleQueues; size_t SchedulerImpl::sPrefThreadCount; size_t SchedulerImpl::sNumThreadsRunning; bool SchedulerImpl::sUnlabeledEventRunning; bool SchedulerEventQueue::PutEvent(already_AddRefed&& aEvent, EventPriority aPriority) { // We want to leak the reference when we fail to dispatch it, so that // we won't release the event in a wrong thread. LeakRefPtr event(Move(aEvent)); nsCOMPtr obs; { MutexAutoLock lock(mLock); if (mEventsAreDoomed) { return false; } mQueue->PutEvent(event.take(), aPriority, lock); if (mScheduler) { CooperativeThreadPool* pool = mScheduler->GetThreadPool(); MOZ_ASSERT(pool); pool->RecheckBlockers(lock); } else { mNonCooperativeCondVar.Notify(); } // Make sure to grab the observer before dropping the lock, otherwise the // event that we just placed into the queue could run and eventually delete // this nsThread before the calling thread is scheduled again. We would then // crash while trying to access a dead nsThread. obs = mObserver; } if (obs) { obs->OnDispatchedEvent(); } return true; } already_AddRefed SchedulerEventQueue::GetEvent(bool aMayWait, EventPriority* aPriority) { MutexAutoLock lock(mLock); if (SchedulerImpl::sPrefChaoticScheduling) { CooperativeThreadPool::Yield(nullptr, lock); } nsCOMPtr event; for (;;) { event = mQueue->GetEvent(aPriority, lock); if (event || !aMayWait) { break; } if (mScheduler) { CooperativeThreadPool::Yield(mScheduler->GetQueueResource(), lock); } else { mNonCooperativeCondVar.Wait(); } } return event.forget(); } bool SchedulerEventQueue::HasPendingEvent() { MutexAutoLock lock(mLock); return HasPendingEvent(lock); } bool SchedulerEventQueue::HasPendingEvent(const MutexAutoLock& aProofOfLock) { return mQueue->HasReadyEvent(aProofOfLock); } bool SchedulerEventQueue::ShutdownIfNoPendingEvents() { MutexAutoLock lock(mLock); MOZ_ASSERT(!mScheduler); if (mQueue->IsEmpty(lock)) { mEventsAreDoomed = true; return true; } return false; } bool SchedulerEventQueue::UseCooperativeScheduling() const { MOZ_ASSERT(NS_IsMainThread()); return !!mScheduler; } void SchedulerEventQueue::SetScheduler(SchedulerImpl* aScheduler) { MutexAutoLock lock(mLock); mScheduler = aScheduler; } already_AddRefed SchedulerEventQueue::GetObserver() { MutexAutoLock lock(mLock); return do_AddRef(mObserver.get()); } already_AddRefed SchedulerEventQueue::GetObserverOnThread() { MOZ_ASSERT(NS_IsMainThread()); return do_AddRef(mObserver.get()); } void SchedulerEventQueue::SetObserver(nsIThreadObserver* aObserver) { MutexAutoLock lock(mLock); mObserver = aObserver; } void SchedulerEventQueue::EnableInputEventPrioritization() { MutexAutoLock lock(mLock); mQueue->EnableInputEventPrioritization(lock); } void SchedulerEventQueue::FlushInputEventPrioritization() { MutexAutoLock lock(mLock); mQueue->FlushInputEventPrioritization(lock); } void SchedulerEventQueue::SuspendInputEventPrioritization() { MutexAutoLock lock(mLock); mQueue->SuspendInputEventPrioritization(lock); } void SchedulerEventQueue::ResumeInputEventPrioritization() { MutexAutoLock lock(mLock); mQueue->ResumeInputEventPrioritization(lock); } UniquePtr Scheduler::sScheduler; SchedulerImpl::SchedulerImpl(SchedulerEventQueue* aQueue) : mNumThreads(sPrefThreadCount) , mLock(aQueue->MutexRef()) , mShutdownCondVar(aQueue->MutexRef(), "SchedulerImpl") , mShuttingDown(false) , mQueue(aQueue) , mQueueResource(this) , mSystemZoneResource(this) , mController(this, aQueue) , mContexts() { } void SchedulerImpl::Interrupt(JSContext* aCx) { MutexAutoLock lock(mLock); CooperativeThreadPool::Yield(nullptr, lock); } /* static */ bool SchedulerImpl::InterruptCallback(JSContext* aCx) { Scheduler::sScheduler->Interrupt(aCx); return true; } void SchedulerImpl::YieldFromJS(JSContext* aCx) { MutexAutoLock lock(mLock); CooperativeThreadPool::Yield(&mSystemZoneResource, lock); } /* static */ void SchedulerImpl::YieldCallback(JSContext* aCx) { Scheduler::sScheduler->YieldFromJS(aCx); } void SchedulerImpl::Switcher() { // This thread switcher is extremely basic and only meant for testing. The // goal is to switch as much as possible without regard for performance. MutexAutoLock lock(mLock); while (!mShuttingDown) { CooperativeThreadPool::SelectedThread threadIndex = mThreadPool->CurrentThreadIndex(lock); if (threadIndex.is()) { JSContext* cx = mContexts[threadIndex.as()]; if (cx) { JS_RequestInterruptCallbackCanWait(cx); } } mShutdownCondVar.Wait(PR_MicrosecondsToInterval(50)); } } /* static */ void SchedulerImpl::SwitcherThread(void* aData) { static_cast(aData)->Switcher(); } /* static */ already_AddRefed SchedulerImpl::CreateQueue(nsIIdlePeriod* aIdlePeriod, nsThread** aThread) { UniquePtr queue; RefPtr mainThread; if (sPrefUseMultipleQueues) { using MainThreadQueueT = PrioritizedEventQueue; queue = MakeUnique( MakeUnique(), MakeUnique(), MakeUnique(), MakeUnique(), do_AddRef(aIdlePeriod)); } else { using MainThreadQueueT = PrioritizedEventQueue; queue = MakeUnique( MakeUnique(), MakeUnique(), MakeUnique(), MakeUnique(), do_AddRef(aIdlePeriod)); } auto prioritized = static_cast*>(queue.get()); RefPtr synchronizedQueue = new SchedulerEventQueue(Move(queue)); prioritized->SetMutexRef(synchronizedQueue->MutexRef()); // Setup "main" thread mainThread = new nsThread(WrapNotNull(synchronizedQueue), nsThread::MAIN_THREAD, 0); prioritized->SetNextIdleDeadlineRef(mainThread->NextIdleDeadlineRef()); mainThread.forget(aThread); return synchronizedQueue.forget(); } void SchedulerImpl::Start() { NS_DispatchToMainThread(NS_NewRunnableFunction("Scheduler::Start", [this]() -> void { // Let's pretend the runnable here isn't actually running. MOZ_ASSERT(sUnlabeledEventRunning); sUnlabeledEventRunning = false; MOZ_ASSERT(sNumThreadsRunning == 1); sNumThreadsRunning = 0; mQueue->SetScheduler(this); xpc::YieldCooperativeContext(); mThreadPool = MakeUnique(mNumThreads, mLock, mController); PRThread* switcher = nullptr; if (sPrefPreemption) { switcher = PR_CreateThread(PR_USER_THREAD, SwitcherThread, this, PR_PRIORITY_HIGH, PR_GLOBAL_THREAD, PR_JOINABLE_THREAD, 0); } { MutexAutoLock mutex(mLock); while (!mShuttingDown) { mShutdownCondVar.Wait(); } } if (switcher) { PR_JoinThread(switcher); } mThreadPool->Shutdown(); mThreadPool = nullptr; mQueue->SetScheduler(nullptr); xpc::ResumeCooperativeContext(); // Put things back to the way they were before we started scheduling. MOZ_ASSERT(!sUnlabeledEventRunning); sUnlabeledEventRunning = true; MOZ_ASSERT(sNumThreadsRunning == 0); sNumThreadsRunning = 1; // Delete the SchedulerImpl. Don't use it after this point. Scheduler::sScheduler = nullptr; })); } void SchedulerImpl::Shutdown() { MutexAutoLock lock(mLock); mShuttingDown = true; mShutdownCondVar.Notify(); } bool SchedulerImpl::QueueResource::IsAvailable(const MutexAutoLock& aProofOfLock) { mScheduler->mLock.AssertCurrentThreadOwns(); RefPtr queue = mScheduler->mQueue; return queue->HasPendingEvent(aProofOfLock); } bool SchedulerImpl::SystemZoneResource::IsAvailable(const MutexAutoLock& aProofOfLock) { mScheduler->mLock.AssertCurrentThreadOwns(); JSContext* cx = dom::danger::GetJSContext(); return js::SystemZoneAvailable(cx); } MOZ_THREAD_LOCAL(Scheduler::EventLoopActivation*) Scheduler::EventLoopActivation::sTopActivation; /* static */ void Scheduler::EventLoopActivation::Init() { sTopActivation.infallibleInit(); } Scheduler::EventLoopActivation::EventLoopActivation() : mPrev(sTopActivation.get()) , mProcessingEvent(false) , mIsLabeled(false) { sTopActivation.set(this); if (mPrev && mPrev->mProcessingEvent) { SchedulerImpl::EnterNestedEventLoop(*mPrev); } } Scheduler::EventLoopActivation::~EventLoopActivation() { if (mProcessingEvent) { SchedulerImpl::FinishEvent(*this); } MOZ_ASSERT(sTopActivation.get() == this); sTopActivation.set(mPrev); if (mPrev && mPrev->mProcessingEvent) { SchedulerImpl::ExitNestedEventLoop(*mPrev); } } /* static */ void SchedulerImpl::StartEvent(Scheduler::EventLoopActivation& aActivation) { MOZ_ASSERT(!sUnlabeledEventRunning); if (aActivation.IsLabeled()) { SchedulerGroup::SetValidatingAccess(SchedulerGroup::StartValidation); for (SchedulerGroup* group : aActivation.EventGroupsAffected()) { MOZ_ASSERT(!group->IsRunning()); group->SetIsRunning(true); } } else { sUnlabeledEventRunning = true; } sNumThreadsRunning++; } /* static */ void SchedulerImpl::FinishEvent(Scheduler::EventLoopActivation& aActivation) { if (aActivation.IsLabeled()) { for (SchedulerGroup* group : aActivation.EventGroupsAffected()) { MOZ_ASSERT(group->IsRunning()); group->SetIsRunning(false); } SchedulerGroup::SetValidatingAccess(SchedulerGroup::EndValidation); } else { MOZ_ASSERT(sUnlabeledEventRunning); sUnlabeledEventRunning = false; } MOZ_ASSERT(sNumThreadsRunning > 0); sNumThreadsRunning--; } // When we enter a nested event loop, we act as if the outer event loop's event // finished. When we exit the nested event loop, we "resume" the outer event // loop's event. /* static */ void SchedulerImpl::EnterNestedEventLoop(Scheduler::EventLoopActivation& aOuterActivation) { FinishEvent(aOuterActivation); } /* static */ void SchedulerImpl::ExitNestedEventLoop(Scheduler::EventLoopActivation& aOuterActivation) { StartEvent(aOuterActivation); } void Scheduler::EventLoopActivation::SetEvent(nsIRunnable* aEvent, EventPriority aPriority) { if (nsCOMPtr labelable = do_QueryInterface(aEvent)) { if (labelable->GetAffectedSchedulerGroups(mEventGroups)) { mIsLabeled = true; } } mPriority = aPriority; mProcessingEvent = aEvent != nullptr; if (aEvent) { SchedulerImpl::StartEvent(*this); } } void SchedulerImpl::ThreadController::OnStartThread(size_t aIndex, const nsACString& aName, void* aStackTop) { using mozilla::ipc::BackgroundChild; // Causes GetCurrentVirtualThread() to return mMainVirtual and NS_IsMainThread() // to return true. NS_SetMainThread(mMainVirtual); // This will initialize the thread's mVirtualThread to mMainVirtual since // GetCurrentVirtualThread() now returns mMainVirtual. nsThreadManager::get().CreateCurrentThread(mMainQueue, nsThread::MAIN_THREAD); profiler_register_thread(aName.BeginReading(), &aStackTop); mOldMainLoop = MessageLoop::current(); MessageLoop::set_current(mMainLoop); xpc::CreateCooperativeContext(); JSContext* cx = dom::danger::GetJSContext(); mScheduler->SetJSContext(aIndex, cx); if (sPrefPreemption) { JS_AddInterruptCallback(cx, SchedulerImpl::InterruptCallback); } js::SetCooperativeYieldCallback(cx, SchedulerImpl::YieldCallback); } void SchedulerImpl::ThreadController::OnStopThread(size_t aIndex) { xpc::DestroyCooperativeContext(); NS_UnsetMainThread(); MessageLoop::set_current(mOldMainLoop); RefPtr self = static_cast(NS_GetCurrentThread()); nsThreadManager::get().UnregisterCurrentThread(*self); profiler_unregister_thread(); } void SchedulerImpl::ThreadController::OnSuspendThread(size_t aIndex) { xpc::YieldCooperativeContext(); } void SchedulerImpl::ThreadController::OnResumeThread(size_t aIndex) { xpc::ResumeCooperativeContext(); } void SchedulerImpl::Yield() { MutexAutoLock lock(mLock); CooperativeThreadPool::Yield(nullptr, lock); } /* static */ already_AddRefed Scheduler::Init(nsIIdlePeriod* aIdlePeriod) { MOZ_ASSERT(!sScheduler); RefPtr mainThread; RefPtr queue = SchedulerImpl::CreateQueue(aIdlePeriod, getter_AddRefs(mainThread)); sScheduler = MakeUnique(queue); return mainThread.forget(); } /* static */ void Scheduler::Start() { sScheduler->Start(); } /* static */ void Scheduler::Shutdown() { if (sScheduler) { sScheduler->Shutdown(); } } /* static */ nsCString Scheduler::GetPrefs() { MOZ_ASSERT(XRE_IsParentProcess()); nsPrintfCString result("%d%d%d%d,%d", Preferences::GetBool("dom.ipc.scheduler", false), Preferences::GetBool("dom.ipc.scheduler.chaoticScheduling", false), Preferences::GetBool("dom.ipc.scheduler.preemption", false) , Preferences::GetBool("dom.ipc.scheduler.useMultipleQueues", false), Preferences::GetInt("dom.ipc.scheduler.threadCount", 2)); return result; } /* static */ void Scheduler::SetPrefs(const char* aPrefs) { MOZ_ASSERT(XRE_IsContentProcess()); SchedulerImpl::sPrefScheduler = aPrefs[0] == '1'; SchedulerImpl::sPrefChaoticScheduling = aPrefs[1] == '1'; SchedulerImpl::sPrefPreemption = aPrefs[2] == '1'; SchedulerImpl::sPrefUseMultipleQueues = aPrefs[3] == '1'; MOZ_ASSERT(aPrefs[4] == ','); SchedulerImpl::sPrefThreadCount = atoi(aPrefs + 5); } /* static */ bool Scheduler::IsSchedulerEnabled() { return SchedulerImpl::sPrefScheduler; } /* static */ bool Scheduler::IsCooperativeThread() { return CooperativeThreadPool::IsCooperativeThread(); } /* static */ void Scheduler::Yield() { sScheduler->Yield(); } /* static */ bool Scheduler::UnlabeledEventRunning() { return SchedulerImpl::UnlabeledEventRunning(); } /* static */ bool Scheduler::AnyEventRunning() { return SchedulerImpl::AnyEventRunning(); }