gecko-dev/xpcom/threads/PrioritizedEventQueue.cpp

447 lines
17 KiB
C++

/* -*- 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_threads.h"
#include "mozilla/ipc/IdleSchedulerChild.h"
#include "nsThreadManager.h"
#include "nsXPCOMPrivate.h" // for gXPCOMThreadsShutDown
#include "InputEventStatistics.h"
#include "MainThreadUtils.h"
using namespace mozilla;
PrioritizedEventQueue::PrioritizedEventQueue(
already_AddRefed<nsIIdlePeriod>&& aIdlePeriod)
: mHighQueue(MakeUnique<EventQueue>(EventQueuePriority::High)),
mInputQueue(
MakeUnique<EventQueueSized<32>>(EventQueuePriority::InputHigh)),
mMediumHighQueue(MakeUnique<EventQueue>(EventQueuePriority::MediumHigh)),
mNormalQueue(MakeUnique<EventQueueSized<64>>(EventQueuePriority::Normal)),
mDeferredTimersQueue(
MakeUnique<EventQueue>(EventQueuePriority::DeferredTimers)),
mIdleQueue(MakeUnique<EventQueue>(EventQueuePriority::Idle)) {
mInputTaskManager = new InputTaskManager();
mIdleTaskManager = new IdleTaskManager(std::move(aIdlePeriod));
TaskController::Get()->SetIdleTaskManager(mIdleTaskManager);
}
PrioritizedEventQueue::~PrioritizedEventQueue() = default;
void PrioritizedEventQueue::PutEvent(already_AddRefed<nsIRunnable>&& aEvent,
EventQueuePriority aPriority,
const MutexAutoLock& aProofOfLock,
mozilla::TimeDuration* aDelay) {
RefPtr<nsIRunnable> event(aEvent);
if (UseTaskController()) {
TaskController* tc = TaskController::Get();
TaskManager* manager = nullptr;
if (aPriority == EventQueuePriority::InputHigh) {
if (mInputTaskManager->State() == InputTaskManager::STATE_DISABLED) {
aPriority = EventQueuePriority::Normal;
} else {
manager = mInputTaskManager;
}
} else if (aPriority == EventQueuePriority::DeferredTimers ||
aPriority == EventQueuePriority::Idle) {
manager = mIdleTaskManager;
}
tc->DispatchRunnable(event.forget(), static_cast<uint32_t>(aPriority),
manager);
return;
}
// Double check the priority with a QI.
EventQueuePriority priority = aPriority;
if (priority == EventQueuePriority::InputHigh &&
mInputTaskManager->State() == InputTaskManager::STATE_DISABLED) {
priority = EventQueuePriority::Normal;
} else if (priority == EventQueuePriority::MediumHigh &&
!StaticPrefs::threads_medium_high_event_queue_enabled()) {
priority = EventQueuePriority::Normal;
}
switch (priority) {
case EventQueuePriority::High:
mHighQueue->PutEvent(event.forget(), priority, aProofOfLock, aDelay);
break;
case EventQueuePriority::InputHigh:
mInputQueue->PutEvent(event.forget(), priority, aProofOfLock, aDelay);
break;
case EventQueuePriority::MediumHigh:
mMediumHighQueue->PutEvent(event.forget(), priority, aProofOfLock,
aDelay);
break;
case EventQueuePriority::Normal:
mNormalQueue->PutEvent(event.forget(), priority, aProofOfLock, aDelay);
break;
case EventQueuePriority::InputLow:
MOZ_ASSERT(false, "InputLow is a TaskController's internal priority!");
break;
case EventQueuePriority::DeferredTimers: {
if (NS_IsMainThread()) {
mDeferredTimersQueue->PutEvent(event.forget(), priority, aProofOfLock,
aDelay);
} else {
// We don't want to touch our idle queues from off the main
// thread. Queue it indirectly.
IndirectlyQueueRunnable(event.forget(), priority, aProofOfLock, aDelay);
}
break;
}
case EventQueuePriority::Idle: {
if (NS_IsMainThread()) {
mIdleQueue->PutEvent(event.forget(), priority, aProofOfLock, aDelay);
} else {
// We don't want to touch our idle queues from off the main
// thread. Queue it indirectly.
IndirectlyQueueRunnable(event.forget(), priority, aProofOfLock, aDelay);
}
break;
}
case EventQueuePriority::Count:
MOZ_CRASH("EventQueuePriority::Count isn't a valid priority");
break;
}
}
EventQueuePriority PrioritizedEventQueue::SelectQueue(
bool aUpdateState, const MutexAutoLock& aProofOfLock) {
size_t inputCount = mInputQueue->Count(aProofOfLock);
if (aUpdateState &&
mInputTaskManager->State() == InputTaskManager::STATE_ENABLED &&
mInputTaskManager->InputHandlingStartTime().IsNull() && inputCount > 0) {
mInputTaskManager->SetInputHandlingStartTime(
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:
// INPUT: if inputCount > 0 && TimeStamp::Now() >
// mInputTaskManager->InputHandlingStartTime(...);
// 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:
// INPUT
// DEFERREDTIMERS: if GetLocalIdleDeadline()
// IDLE: if GetLocalIdleDeadline()
//
// 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;
if (!mHighQueue->IsEmpty(aProofOfLock)) {
queue = EventQueuePriority::High;
} else if (inputCount > 0 &&
(mInputTaskManager->State() == InputTaskManager::STATE_FLUSHING ||
(mInputTaskManager->State() == InputTaskManager::STATE_ENABLED &&
!mInputTaskManager->InputHandlingStartTime().IsNull() &&
TimeStamp::Now() >
mInputTaskManager->InputHandlingStartTime()))) {
queue = EventQueuePriority::InputHigh;
} else if (!mMediumHighQueue->IsEmpty(aProofOfLock)) {
MOZ_ASSERT(
mInputTaskManager->State() != InputTaskManager::STATE_FLUSHING,
"Shouldn't consume medium high event when flushing input events");
queue = EventQueuePriority::MediumHigh;
} else if (!mNormalQueue->IsEmpty(aProofOfLock)) {
MOZ_ASSERT(mInputTaskManager->State() != InputTaskManager::STATE_FLUSHING,
"Shouldn't consume normal event when flushing input events");
queue = EventQueuePriority::Normal;
} else if (inputCount > 0 &&
mInputTaskManager->State() != InputTaskManager::STATE_SUSPEND) {
MOZ_ASSERT(
mInputTaskManager->State() != InputTaskManager::STATE_DISABLED,
"Shouldn't consume input events when the input queue is disabled");
queue = EventQueuePriority::InputHigh;
} 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::InputHigh ||
queue == EventQueuePriority::InputLow,
mInputTaskManager->State() != InputTaskManager::STATE_DISABLED &&
mInputTaskManager->State() != InputTaskManager::STATE_SUSPEND);
return queue;
}
void PrioritizedEventQueue::IndirectlyQueueRunnable(
already_AddRefed<nsIRunnable>&& aEvent, EventQueuePriority aPriority,
const MutexAutoLock& aProofOfLock, mozilla::TimeDuration* aDelay) {
nsCOMPtr<nsIRunnable> event(aEvent);
nsCOMPtr<nsIRunnable> mainThreadRunnable = NS_NewRunnableFunction(
"idle runnable shim",
[event = std::move(event), priority = aPriority]() mutable {
NS_DispatchToCurrentThreadQueue(event.forget(), priority);
});
mNormalQueue->PutEvent(mainThreadRunnable.forget(),
EventQueuePriority::Normal, aProofOfLock, aDelay);
}
// The delay returned is the queuing delay a hypothetical Input event would
// see due to the current running event if it had arrived while the current
// event was queued. This means that any event running at priority below
// Input doesn't cause queuing delay for Input events, and we return
// TimeDuration() for those cases.
already_AddRefed<nsIRunnable> PrioritizedEventQueue::GetEvent(
EventQueuePriority* aPriority, const MutexAutoLock& aProofOfLock,
TimeDuration* aHypotheticalInputEventDelay) {
MOZ_ASSERT_UNREACHABLE("Who is managing to call this?");
bool ignored;
return GetEvent(aPriority, aProofOfLock, aHypotheticalInputEventDelay,
&ignored);
}
already_AddRefed<nsIRunnable> PrioritizedEventQueue::GetEvent(
EventQueuePriority* aPriority, const MutexAutoLock& aProofOfLock,
TimeDuration* aHypotheticalInputEventDelay, bool* aIsIdleEvent) {
EventQueuePriority queue = SelectQueue(true, aProofOfLock);
if (aPriority) {
*aPriority = queue;
}
*aIsIdleEvent = false;
// Since Input events will only be delayed behind Input or High events,
// the amount of time a lower-priority event spent in the queue is
// irrelevant in knowing how long an input event would be delayed.
// Alternatively, we could export the delay and let the higher-level code
// key off the returned priority level (though then it'd need to know
// if the thread's queue was a PrioritizedEventQueue or normal/other
// EventQueue).
nsCOMPtr<nsIRunnable> event;
switch (queue) {
default:
MOZ_CRASH();
break;
case EventQueuePriority::High:
event = mHighQueue->GetEvent(nullptr, aProofOfLock,
aHypotheticalInputEventDelay);
MOZ_ASSERT(event);
mInputTaskManager->SetInputHandlingStartTime(TimeStamp());
break;
case EventQueuePriority::InputHigh:
event = mInputQueue->GetEvent(nullptr, aProofOfLock,
aHypotheticalInputEventDelay);
MOZ_ASSERT(event);
break;
// All queue priorities below Input don't add their queuing time to the
// time an input event will be delayed, so report 0 for time-in-queue
// if we're below Input; input events will only be delayed by the time
// an event actually runs (if the event is below Input event's priority)
case EventQueuePriority::MediumHigh:
event = mMediumHighQueue->GetEvent(nullptr, aProofOfLock);
*aHypotheticalInputEventDelay = TimeDuration();
break;
case EventQueuePriority::Normal:
event = mNormalQueue->GetEvent(nullptr, aProofOfLock);
*aHypotheticalInputEventDelay = TimeDuration();
break;
case EventQueuePriority::Idle:
case EventQueuePriority::DeferredTimers:
*aHypotheticalInputEventDelay = TimeDuration();
// If we get here, then all queues except deferredtimers and idle are
// empty.
if (!HasIdleRunnables(aProofOfLock)) {
return nullptr;
}
TimeStamp idleDeadline =
mIdleTaskManager->State().GetCachedIdleDeadline();
if (!idleDeadline) {
return nullptr;
}
event = mDeferredTimersQueue->GetEvent(nullptr, aProofOfLock);
if (!event) {
event = mIdleQueue->GetEvent(nullptr, aProofOfLock);
}
if (event) {
*aIsIdleEvent = true;
nsCOMPtr<nsIIdleRunnable> idleEvent = do_QueryInterface(event);
if (idleEvent) {
idleEvent->SetDeadline(idleDeadline);
}
}
break;
} // switch (queue)
if (!event) {
*aHypotheticalInputEventDelay = TimeDuration();
}
MOZ_ASSERT_IF(aPriority, *aPriority == queue);
return event.forget();
}
void PrioritizedEventQueue::DidRunEvent(const MutexAutoLock& aProofOfLock) {
if (IsEmpty(aProofOfLock)) {
// Certainly no more idle tasks. Unlocking here is OK, because
// our caller does nothing after this except return, which unlocks
// its lock anyway.
MutexAutoUnlock unlock(*mMutex);
mIdleTaskManager->State().RanOutOfTasks(unlock);
}
}
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);
}
bool PrioritizedEventQueue::HasReadyEvent(const MutexAutoLock& aProofOfLock) {
mIdleTaskManager->State().ForgetPendingTaskGuarantee();
EventQueuePriority queue = SelectQueue(false, aProofOfLock);
if (queue == EventQueuePriority::High) {
return mHighQueue->HasReadyEvent(aProofOfLock);
} else if (queue == EventQueuePriority::InputHigh) {
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;
}
// Temporarily unlock so we can peek our idle deadline.
{
MutexAutoUnlock unlock(*mMutex);
mIdleTaskManager->State().CachePeekedIdleDeadline(unlock);
}
TimeStamp idleDeadline = mIdleTaskManager->State().GetCachedIdleDeadline();
mIdleTaskManager->State().ClearCachedIdleDeadline();
// Re-check the emptiness of the queues, since we had the lock released for a
// bit.
if (idleDeadline && (mDeferredTimersQueue->HasReadyEvent(aProofOfLock) ||
mIdleQueue->HasReadyEvent(aProofOfLock))) {
mIdleTaskManager->State().EnforcePendingTaskGuarantee();
return true;
}
return false;
}
bool PrioritizedEventQueue::HasPendingHighPriorityEvents(
const MutexAutoLock& aProofOfLock) {
return !mHighQueue->IsEmpty(aProofOfLock);
}
bool PrioritizedEventQueue::HasIdleRunnables(
const MutexAutoLock& aProofOfLock) const {
return !mIdleQueue->IsEmpty(aProofOfLock) ||
!mDeferredTimersQueue->IsEmpty(aProofOfLock);
}
size_t PrioritizedEventQueue::Count(const MutexAutoLock& aProofOfLock) const {
MOZ_CRASH("unimplemented");
}
void InputTaskManager::EnableInputEventPrioritization() {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(mInputQueueState == STATE_DISABLED);
mInputQueueState = STATE_ENABLED;
mInputHandlingStartTime = TimeStamp();
}
void InputTaskManager::FlushInputEventPrioritization() {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(mInputQueueState == STATE_ENABLED ||
mInputQueueState == STATE_SUSPEND);
mInputQueueState =
mInputQueueState == STATE_ENABLED ? STATE_FLUSHING : STATE_SUSPEND;
}
void InputTaskManager::SuspendInputEventPrioritization() {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(mInputQueueState == STATE_ENABLED ||
mInputQueueState == STATE_FLUSHING);
mInputQueueState = STATE_SUSPEND;
}
void InputTaskManager::ResumeInputEventPrioritization() {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(mInputQueueState == STATE_SUSPEND);
mInputQueueState = STATE_ENABLED;
}
int32_t InputTaskManager::GetPriorityModifierForEventLoopTurn(
const MutexAutoLock& aProofOfLock) {
size_t inputCount = PendingTaskCount();
if (State() == STATE_ENABLED && InputHandlingStartTime().IsNull() &&
inputCount > 0) {
SetInputHandlingStartTime(
InputEventStatistics::Get().GetInputHandlingStartTime(inputCount));
}
if (inputCount > 0 && (State() == InputTaskManager::STATE_FLUSHING ||
(State() == InputTaskManager::STATE_ENABLED &&
!InputHandlingStartTime().IsNull() &&
TimeStamp::Now() > InputHandlingStartTime()))) {
return 0;
}
int32_t modifier = static_cast<int32_t>(EventQueuePriority::InputLow) -
static_cast<int32_t>(EventQueuePriority::MediumHigh);
return modifier;
}
void InputTaskManager::WillRunTask() {
TaskManager::WillRunTask();
mStartTimes.AppendElement(TimeStamp::Now());
}
void InputTaskManager::DidRunTask() {
TaskManager::DidRunTask();
MOZ_ASSERT(!mStartTimes.IsEmpty());
TimeStamp start = mStartTimes.PopLastElement();
InputEventStatistics::Get().UpdateInputDuration(TimeStamp::Now() - start);
}