/* -*- 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 "PrioritizedEventQueue.h" #include "mozilla/EventQueue.h" #include "mozilla/ScopeExit.h" #include "mozilla/StaticPrefs.h" #include "nsThreadManager.h" #include "nsXPCOMPrivate.h" // for gXPCOMThreadsShutDown #include "InputEventStatistics.h" using namespace mozilla; template void PrioritizedEventQueue::PutEvent( already_AddRefed&& aEvent, EventQueuePriority aPriority, const MutexAutoLock& aProofOfLock) { static_assert(IsBaseOf::value, "InnerQueueT must be an AbstractEventQueue subclass"); // Double check the priority with a QI. RefPtr event(aEvent); EventQueuePriority priority = aPriority; if (priority == EventQueuePriority::Input && mInputQueueState == STATE_DISABLED) { priority = EventQueuePriority::Normal; } else if (priority == EventQueuePriority::MediumHigh && !StaticPrefs::medium_high_event_queue_enabled()) { priority = EventQueuePriority::Normal; } switch (priority) { case EventQueuePriority::High: mHighQueue->PutEvent(event.forget(), priority, aProofOfLock); break; case EventQueuePriority::Input: mInputQueue->PutEvent(event.forget(), priority, aProofOfLock); break; case EventQueuePriority::MediumHigh: mMediumHighQueue->PutEvent(event.forget(), priority, aProofOfLock); break; case EventQueuePriority::Normal: mNormalQueue->PutEvent(event.forget(), priority, aProofOfLock); break; case EventQueuePriority::DeferredTimers: mDeferredTimersQueue->PutEvent(event.forget(), priority, aProofOfLock); break; case EventQueuePriority::Idle: mIdleQueue->PutEvent(event.forget(), priority, aProofOfLock); break; case EventQueuePriority::Count: MOZ_CRASH("EventQueuePriority::Count isn't a valid priority"); break; } } template TimeStamp PrioritizedEventQueue::GetIdleDeadline() { // If we are shutting down, we won't honor the idle period, and we will // always process idle runnables. This will ensure that the idle queue // gets exhausted at shutdown time to prevent intermittently leaking // some runnables inside that queue and even worse potentially leaving // some important cleanup work unfinished. if (gXPCOMThreadsShutDown || nsThreadManager::get().GetCurrentThread()->ShuttingDown()) { return TimeStamp::Now(); } TimeStamp idleDeadline; { // Releasing the lock temporarily since getting the idle period // might need to lock the timer thread. Unlocking here might make // us receive an event on the main queue, but we've committed to // run an idle event anyhow. MutexAutoUnlock unlock(*mMutex); mIdlePeriod->GetIdlePeriodHint(&idleDeadline); } // If HasPendingEvents() has been called and it has returned true because of // pending idle events, there is a risk that we may decide here that we aren't // idle and return null, in which case HasPendingEvents() has effectively // lied. Since we can't go back and fix the past, we have to adjust what we // do here and forcefully pick the idle queue task here. Note that this means // that we are choosing to run a task from the idle queue when we would // normally decide that we aren't in an idle period, but this can only happen // if we fall out of the idle period in between the call to HasPendingEvents() // and here, which should hopefully be quite rare. We are effectively // choosing to prioritize the sanity of our API semantics over the optimal // scheduling. if (!mHasPendingEventsPromisedIdleEvent && (!idleDeadline || idleDeadline < TimeStamp::Now())) { return TimeStamp(); } if (mHasPendingEventsPromisedIdleEvent && !idleDeadline) { // If HasPendingEvents() has been called and it has returned true, but we're // no longer in the idle period, we must return a valid timestamp to pretend // that we are still in the idle period. return TimeStamp::Now(); } return idleDeadline; } template EventQueuePriority PrioritizedEventQueue::SelectQueue( bool aUpdateState, const MutexAutoLock& aProofOfLock) { size_t inputCount = mInputQueue->Count(aProofOfLock); if (aUpdateState && mInputQueueState == STATE_ENABLED && mInputHandlingStartTime.IsNull() && inputCount > 0) { mInputHandlingStartTime = InputEventStatistics::Get().GetInputHandlingStartTime(inputCount); } // We check the different queues in the following order. The conditions we use // are meant to avoid starvation and to ensure that we don't process an event // at the wrong time. // // HIGH: if mProcessHighPriorityQueue // INPUT: if inputCount > 0 && TimeStamp::Now() > mInputHandlingStartTime // MEDIUMHIGH: if medium high pending // NORMAL: if normal pending // // If we still don't have an event, then we take events from the queues // in the following order: // // HIGH // INPUT // DEFERREDTIMERS: if GetIdleDeadline() // IDLE: if GetIdleDeadline() // // If we don't get an event in this pass, then we return null since no events // are ready. // This variable determines which queue we will take an event from. EventQueuePriority queue; bool highPending = !mHighQueue->IsEmpty(aProofOfLock); if (mProcessHighPriorityQueue) { queue = EventQueuePriority::High; } else if (inputCount > 0 && (mInputQueueState == STATE_FLUSHING || (mInputQueueState == STATE_ENABLED && !mInputHandlingStartTime.IsNull() && TimeStamp::Now() > mInputHandlingStartTime))) { queue = EventQueuePriority::Input; } else if (!mMediumHighQueue->IsEmpty(aProofOfLock)) { MOZ_ASSERT( mInputQueueState != STATE_FLUSHING, "Shouldn't consume medium high event when flushing input events"); queue = EventQueuePriority::MediumHigh; } else if (!mNormalQueue->IsEmpty(aProofOfLock)) { MOZ_ASSERT(mInputQueueState != STATE_FLUSHING, "Shouldn't consume normal event when flushing input events"); queue = EventQueuePriority::Normal; } else if (highPending) { queue = EventQueuePriority::High; } else if (inputCount > 0 && mInputQueueState != STATE_SUSPEND) { MOZ_ASSERT( mInputQueueState != STATE_DISABLED, "Shouldn't consume input events when the input queue is disabled"); queue = EventQueuePriority::Input; } else if (!mDeferredTimersQueue->IsEmpty(aProofOfLock)) { // We may not actually return an idle event in this case. queue = EventQueuePriority::DeferredTimers; } else { // We may not actually return an idle event in this case. queue = EventQueuePriority::Idle; } MOZ_ASSERT_IF( queue == EventQueuePriority::Input, mInputQueueState != STATE_DISABLED && mInputQueueState != STATE_SUSPEND); if (aUpdateState) { mProcessHighPriorityQueue = highPending; } return queue; } template already_AddRefed PrioritizedEventQueue::GetEvent( EventQueuePriority* aPriority, const MutexAutoLock& aProofOfLock) { auto guard = MakeScopeExit([&] { mHasPendingEventsPromisedIdleEvent = false; }); #ifndef RELEASE_OR_BETA // Clear mNextIdleDeadline so that it is possible to determine that // we're running an idle runnable in ProcessNextEvent. *mNextIdleDeadline = TimeStamp(); #endif EventQueuePriority queue = SelectQueue(true, aProofOfLock); if (aPriority) { *aPriority = queue; } if (queue == EventQueuePriority::High) { nsCOMPtr event = mHighQueue->GetEvent(aPriority, aProofOfLock); MOZ_ASSERT(event); mInputHandlingStartTime = TimeStamp(); mProcessHighPriorityQueue = false; return event.forget(); } if (queue == EventQueuePriority::Input) { nsCOMPtr event = mInputQueue->GetEvent(aPriority, aProofOfLock); MOZ_ASSERT(event); return event.forget(); } if (queue == EventQueuePriority::MediumHigh) { nsCOMPtr event = mMediumHighQueue->GetEvent(aPriority, aProofOfLock); return event.forget(); } if (queue == EventQueuePriority::Normal) { nsCOMPtr event = mNormalQueue->GetEvent(aPriority, aProofOfLock); return event.forget(); } // If we get here, then all queues except deferredtimers and idle are empty. MOZ_ASSERT(queue == EventQueuePriority::Idle || queue == EventQueuePriority::DeferredTimers); if (mIdleQueue->IsEmpty(aProofOfLock) && mDeferredTimersQueue->IsEmpty(aProofOfLock)) { MOZ_ASSERT(!mHasPendingEventsPromisedIdleEvent); return nullptr; } TimeStamp idleDeadline = GetIdleDeadline(); if (!idleDeadline) { return nullptr; } nsCOMPtr event = mDeferredTimersQueue->GetEvent(aPriority, aProofOfLock); if (!event) { event = mIdleQueue->GetEvent(aPriority, aProofOfLock); } if (event) { nsCOMPtr idleEvent = do_QueryInterface(event); if (idleEvent) { idleEvent->SetDeadline(idleDeadline); } #ifndef RELEASE_OR_BETA // Store the next idle deadline to be able to determine budget use // in ProcessNextEvent. *mNextIdleDeadline = idleDeadline; #endif } return event.forget(); } template bool PrioritizedEventQueue::IsEmpty( const MutexAutoLock& aProofOfLock) { // Just check IsEmpty() on the sub-queues. Don't bother checking the idle // deadline since that only determines whether an idle event is ready or not. return mHighQueue->IsEmpty(aProofOfLock) && mInputQueue->IsEmpty(aProofOfLock) && mMediumHighQueue->IsEmpty(aProofOfLock) && mNormalQueue->IsEmpty(aProofOfLock) && mDeferredTimersQueue->IsEmpty(aProofOfLock) && mIdleQueue->IsEmpty(aProofOfLock); } template bool PrioritizedEventQueue::HasReadyEvent( const MutexAutoLock& aProofOfLock) { mHasPendingEventsPromisedIdleEvent = false; EventQueuePriority queue = SelectQueue(false, aProofOfLock); if (queue == EventQueuePriority::High) { return mHighQueue->HasReadyEvent(aProofOfLock); } else if (queue == EventQueuePriority::Input) { return mInputQueue->HasReadyEvent(aProofOfLock); } else if (queue == EventQueuePriority::MediumHigh) { return mMediumHighQueue->HasReadyEvent(aProofOfLock); } else if (queue == EventQueuePriority::Normal) { return mNormalQueue->HasReadyEvent(aProofOfLock); } MOZ_ASSERT(queue == EventQueuePriority::Idle || queue == EventQueuePriority::DeferredTimers); // If we get here, then both the high and normal queues are empty. if (mDeferredTimersQueue->IsEmpty(aProofOfLock) && mIdleQueue->IsEmpty(aProofOfLock)) { return false; } TimeStamp idleDeadline = GetIdleDeadline(); if (idleDeadline && (mDeferredTimersQueue->HasReadyEvent(aProofOfLock) || mIdleQueue->HasReadyEvent(aProofOfLock))) { mHasPendingEventsPromisedIdleEvent = true; return true; } return false; } template bool PrioritizedEventQueue::HasPendingHighPriorityEvents( const MutexAutoLock& aProofOfLock) { return !mHighQueue->IsEmpty(aProofOfLock); } template size_t PrioritizedEventQueue::Count( const MutexAutoLock& aProofOfLock) const { MOZ_CRASH("unimplemented"); } template void PrioritizedEventQueue::EnableInputEventPrioritization( const MutexAutoLock& aProofOfLock) { MOZ_ASSERT(mInputQueueState == STATE_DISABLED); mInputQueueState = STATE_ENABLED; mInputHandlingStartTime = TimeStamp(); } template void PrioritizedEventQueue::FlushInputEventPrioritization( const MutexAutoLock& aProofOfLock) { MOZ_ASSERT(mInputQueueState == STATE_ENABLED || mInputQueueState == STATE_SUSPEND); mInputQueueState = mInputQueueState == STATE_ENABLED ? STATE_FLUSHING : STATE_SUSPEND; } template void PrioritizedEventQueue::SuspendInputEventPrioritization( const MutexAutoLock& aProofOfLock) { MOZ_ASSERT(mInputQueueState == STATE_ENABLED || mInputQueueState == STATE_FLUSHING); mInputQueueState = STATE_SUSPEND; } template void PrioritizedEventQueue::ResumeInputEventPrioritization( const MutexAutoLock& aProofOfLock) { MOZ_ASSERT(mInputQueueState == STATE_SUSPEND); mInputQueueState = STATE_ENABLED; } namespace mozilla { template class PrioritizedEventQueue; } // namespace mozilla