Global Metrics

path: .metrics.nexits.average
old: 0.6666666666666666
new: 0.8648648648648649

path: .metrics.nexits.sum
old: 4.0
new: 32.0

path: .metrics.cognitive.average
old: 0.16666666666666666
new: 4.594594594594595

path: .metrics.cognitive.sum
old: 1.0
new: 170.0

path: .metrics.mi.mi_sei
old: -6.5029402404496395
new: -72.44936525545216

path: .metrics.mi.mi_original
old: 32.47183840675838
new: -26.675408948963252

path: .metrics.mi.mi_visual_studio
old: 18.989379185238818
new: 0.0

path: .metrics.loc.lloc
old: 5.0
new: 304.0

path: .metrics.loc.cloc
old: 16.0
new: 127.0

path: .metrics.loc.sloc
old: 229.0
new: 955.0

path: .metrics.loc.blank
old: 42.0
new: 160.0

path: .metrics.loc.ploc
old: 171.0
new: 668.0

path: .metrics.nargs.average
old: 0.5
new: 0.5405405405405406

path: .metrics.nargs.sum
old: 3.0
new: 20.0

path: .metrics.halstead.time
old: 5795.057307265363
new: 95622.15583520064

path: .metrics.halstead.bugs
old: 0.7386390070274477
new: 4.787396918087377

path: .metrics.halstead.estimated_program_length
old: 1429.2441800947695
new: 2751.7158656869374

path: .metrics.halstead.length
old: 748.0
new: 2991.0

path: .metrics.halstead.N1
old: 402.0
new: 1816.0

path: .metrics.halstead.level
old: 0.05476118040766656
new: 0.014657210401891257

path: .metrics.halstead.volume
old: 5712.1952161666495
new: 25227.973028861445

path: .metrics.halstead.vocabulary
old: 199.0
new: 346.0

path: .metrics.halstead.n1
old: 19.0
new: 36.0

path: .metrics.halstead.N2
old: 346.0
new: 1175.0

path: .metrics.halstead.difficulty
old: 18.261111111111113
new: 68.2258064516129

path: .metrics.halstead.effort
old: 104311.03153077654
new: 1721198.8050336114

path: .metrics.halstead.n2
old: 180.0
new: 310.0

path: .metrics.halstead.purity_ratio
old: 1.910754251463596
new: 0.9199986177488924

path: .metrics.cyclomatic.average
old: 1.0909090909090908
new: 4.083333333333333

path: .metrics.cyclomatic.sum
old: 24.0
new: 147.0

path: .metrics.nom.closures
old: 0.0
new: 4.0

path: .metrics.nom.functions
old: 6.0
new: 33.0

path: .metrics.nom.total
old: 6.0
new: 37.0

Spaces Data

Minimal test - lines (33, 955)

path: .spaces[0].metrics.cognitive.sum
old: 0.0
new: 170.0

path: .spaces[0].metrics.cognitive.average
old: null
new: 4.594594594594595

path: .spaces[0].metrics.loc.blank
old: 0.0
new: 158.0

path: .spaces[0].metrics.loc.ploc
old: 1.0
new: 644.0

path: .spaces[0].metrics.loc.lloc
old: 0.0
new: 304.0

path: .spaces[0].metrics.loc.cloc
old: 0.0
new: 121.0

path: .spaces[0].metrics.loc.sloc
old: 1.0
new: 923.0

path: .spaces[0].metrics.nargs.average
old: null
new: 0.5405405405405406

path: .spaces[0].metrics.nargs.sum
old: 0.0
new: 20.0

path: .spaces[0].metrics.nom.functions
old: 0.0
new: 33.0

path: .spaces[0].metrics.nom.total
old: 0.0
new: 37.0

path: .spaces[0].metrics.nom.closures
old: 0.0
new: 4.0

path: .spaces[0].metrics.nexits.average
old: null
new: 0.8648648648648649

path: .spaces[0].metrics.nexits.sum
old: 0.0
new: 32.0

path: .spaces[0].metrics.halstead.n2
old: 1.0
new: 289.0

path: .spaces[0].metrics.halstead.bugs
old: 0.0
new: 4.881475472096358

path: .spaces[0].metrics.halstead.length
old: 1.0
new: 2960.0

path: .spaces[0].metrics.halstead.estimated_program_length
old: null
new: 2548.6708222946195

path: .spaces[0].metrics.halstead.N1
old: 0.0
new: 1808.0

path: .spaces[0].metrics.halstead.effort
old: 0.0
new: 1772182.930940365

path: .spaces[0].metrics.halstead.purity_ratio
old: null
new: 0.8610374399643985

path: .spaces[0].metrics.halstead.vocabulary
old: 1.0
new: 325.0

path: .spaces[0].metrics.halstead.volume
old: 0.0
new: 24699.115887430817

path: .spaces[0].metrics.halstead.N2
old: 1.0
new: 1152.0

path: .spaces[0].metrics.halstead.n1
old: 0.0
new: 36.0

path: .spaces[0].metrics.halstead.difficulty
old: 0.0
new: 71.75086505190312

path: .spaces[0].metrics.halstead.level
old: null
new: 0.013937114197530864

path: .spaces[0].metrics.halstead.time
old: 0.0
new: 98454.60727446472

path: .spaces[0].metrics.cyclomatic.average
old: 1.0
new: 4.171428571428572

path: .spaces[0].metrics.cyclomatic.sum
old: 1.0
new: 146.0

path: .spaces[0].metrics.mi.mi_original
old: null
new: -25.78311178425659

path: .spaces[0].metrics.mi.mi_visual_studio
old: null
new: 0.0

path: .spaces[0].metrics.mi.mi_sei
old: null
new: -71.43423738401863

Code

namespace mozilla {

std::unique_ptr TaskController::sSingleton;
thread_local size_t mThreadPoolIndex = -1;
std::atomic Task::sCurrentTaskSeqNo = 0;

const int32_t kMaximumPoolThreadCount = 8;

static int32_t GetPoolThreadCount() {
  if (PR_GetEnv("MOZ_TASKCONTROLLER_THREADCOUNT")) {
    return strtol(PR_GetEnv("MOZ_TASKCONTROLLER_THREADCOUNT"), nullptr, 0);
  }

  int32_t numCores = std::max(1, PR_GetNumberOfProcessors());

  if (numCores == 1) {
    return 1;
  }
  if (numCores == 2) {
    return 2;
  }
  return std::min(kMaximumPoolThreadCount, numCores - 1);
}

#if defined(MOZ_GECKO_PROFILER) && defined(MOZ_COLLECTING_RUNNABLE_TELEMETRY)
#  define AUTO_PROFILE_FOLLOWING_TASK(task)                                  \
    nsAutoCString name;                                                      \
    (task)->GetName(name);                                                   \
    AUTO_PROFILER_LABEL_DYNAMIC_NSCSTRING_NONSENSITIVE("Task", OTHER, name); \
    AUTO_PROFILER_MARKER_TEXT("Runnable", OTHER, {}, name);
#else
#  define AUTO_PROFILE_FOLLOWING_TASK(task)
#endif

bool TaskManager::
    UpdateCachesForCurrentIterationAndReportPriorityModifierChanged(
        const MutexAutoLock& aProofOfLock, IterationType aIterationType) {
  mCurrentSuspended = IsSuspended(aProofOfLock);

  if (aIterationType == IterationType::EVENT_LOOP_TURN) {
    int32_t oldModifier = mCurrentPriorityModifier;
    mCurrentPriorityModifier =
        GetPriorityModifierForEventLoopTurn(aProofOfLock);

    if (mCurrentPriorityModifier != oldModifier) {
      return true;
    }
  }
  return false;
}

Task* Task::GetHighestPriorityDependency() {
  Task* currentTask = this;

  while (!currentTask->mDependencies.empty()) {
    auto iter = currentTask->mDependencies.begin();

    while (iter != currentTask->mDependencies.end()) {
      if ((*iter)->mCompleted) {
        auto oldIter = iter;
        iter++;
        // Completed tasks are removed here to prevent needlessly keeping them
        // alive or iterating over them in the future.
        currentTask->mDependencies.erase(oldIter);
        continue;
      }

      currentTask = iter->get();
      break;
    }
  }

  return currentTask == this ? nullptr : currentTask;
}

TaskController* TaskController::Get() {
  MOZ_ASSERT(sSingleton.get());
  return sSingleton.get();
}

bool TaskController::Initialize() {
  MOZ_ASSERT(!sSingleton);
  sSingleton = std::make_unique();
  return sSingleton->InitializeInternal();
}

void ThreadFuncPoolThread(void* aIndex) {
  mThreadPoolIndex = *reinterpret_cast(aIndex);
  delete reinterpret_cast(aIndex);
  TaskController::Get()->RunPoolThread();
}

#ifdef XP_WIN
static SetThreadDescriptionPtr sSetThreadDescriptionFunc = nullptr;
#endif

bool TaskController::InitializeInternal() {
  InputTaskManager::Init();
  mMTProcessingRunnable = NS_NewRunnableFunction(
      "TaskController::ExecutePendingMTTasks()",
      []() { TaskController::Get()->ProcessPendingMTTask(); });
  mMTBlockingProcessingRunnable = NS_NewRunnableFunction(
      "TaskController::ExecutePendingMTTasks()",
      []() { TaskController::Get()->ProcessPendingMTTask(true); });

#ifdef XP_WIN
  sSetThreadDescriptionFunc =
      reinterpret_cast(::GetProcAddress(
          ::GetModuleHandle(L"Kernel32.dll"), "SetThreadDescription"));
#endif

  return true;
}

// Ample stack size allocated for applications like ImageLib's AV1 decoder.
const PRUint32 sStackSize = 512u * 1024u;

void TaskController::InitializeThreadPool() {
  mPoolInitializationMutex.AssertCurrentThreadOwns();
  MOZ_ASSERT(!mThreadPoolInitialized);
  mThreadPoolInitialized = true;

  int32_t poolSize = GetPoolThreadCount();
  for (int32_t i = 0; i < poolSize; i++) {
    int32_t* index = new int32_t(i);
    mPoolThreads.push_back(
        {PR_CreateThread(PR_USER_THREAD, ThreadFuncPoolThread, index,
                         PR_PRIORITY_NORMAL, PR_GLOBAL_THREAD,
                         PR_JOINABLE_THREAD, 512u * 1024u),
         nullptr});
  }
}

void TaskController::SetPerformanceCounterState(
    PerformanceCounterState* aPerformanceCounterState) {
  mPerformanceCounterState = aPerformanceCounterState;
}

/* static */
void TaskController::Shutdown() {
  InputTaskManager::Cleanup();
  if (sSingleton) {
    sSingleton->ShutdownThreadPoolInternal();
    sSingleton->ShutdownInternal();
  }
  MOZ_ASSERT(!sSingleton);
}

void TaskController::ShutdownThreadPoolInternal() {
  {
    // Prevent racecondition on mShuttingDown and wait.
    MutexAutoLock lock(mGraphMutex);

    mShuttingDown = true;
    mThreadPoolCV.NotifyAll();
  }
  for (PoolThread& thread : mPoolThreads) {
    PR_JoinThread(thread.mThread);
  }
}

void TaskController::ShutdownInternal() { sSingleton = nullptr; }

void TaskController::RunPoolThread() {
  IOInterposer::RegisterCurrentThread();

  // This is used to hold on to a task to make sure it is released outside the
  // lock. This is required since it's perfectly feasible for task destructors
  // to post events themselves.
  RefPtr lastTask;

#ifdef XP_WIN
  nsAutoString threadWName;
  threadWName.AppendLiteral(u"TaskController Thread #");
  threadWName.AppendInt(static_cast(mThreadPoolIndex));

  if (sSetThreadDescriptionFunc) {
    sSetThreadDescriptionFunc(
        ::GetCurrentThread(),
        reinterpret_cast(threadWName.BeginReading()));
  }
#endif
  nsAutoCString threadName;
  threadName.AppendLiteral("TaskController Thread #");
  threadName.AppendInt(static_cast(mThreadPoolIndex));
  PROFILER_REGISTER_THREAD(threadName.BeginReading());

  MutexAutoLock lock(mGraphMutex);
  while (true) {
    bool ranTask = false;

    if (!mThreadableTasks.empty()) {
      for (auto iter = mThreadableTasks.begin(); iter != mThreadableTasks.end();
           ++iter) {
        // Search for the highest priority dependency of the highest priority
        // task.

        // We work with rawptrs to avoid needless refcounting. All our tasks
        // are always kept alive by the graph. If one is removed from the graph
        // it is kept alive by mPoolThreads[mThreadPoolIndex].mCurrentTask.
        Task* task = iter->get();

        MOZ_ASSERT(!task->mTaskManager);

        mPoolThreads[mThreadPoolIndex].mEffectiveTaskPriority =
            task->GetPriority();

        Task* nextTask;
        while ((nextTask = task->GetHighestPriorityDependency())) {
          task = nextTask;
        }

        if (task->IsMainThreadOnly() || task->mInProgress) {
          continue;
        }

        mPoolThreads[mThreadPoolIndex].mCurrentTask = task;
        mThreadableTasks.erase(task->mIterator);
        task->mIterator = mThreadableTasks.end();
        task->mInProgress = true;

        bool taskCompleted = false;
        {
          MutexAutoUnlock unlock(mGraphMutex);
          lastTask = nullptr;
          AUTO_PROFILE_FOLLOWING_TASK(task);
          taskCompleted = task->Run();
          ranTask = true;
        }

        task->mInProgress = false;

        if (!taskCompleted) {
          // Presumably this task was interrupted, leave its dependencies
          // unresolved and reinsert into the queue.
          auto insertion = mThreadableTasks.insert(
              mPoolThreads[mThreadPoolIndex].mCurrentTask);
          MOZ_ASSERT(insertion.second);
          task->mIterator = insertion.first;
        } else {
          task->mCompleted = true;
#ifdef DEBUG
          task->mIsInGraph = false;
#endif
          task->mDependencies.clear();
          // This may have unblocked a main thread task. We could do this only
          // if there was a main thread task before this one in the dependency
          // chain.
          mMayHaveMainThreadTask = true;
          // Since this could have multiple dependencies thare are restricted
          // to the main thread. Let's make sure that's awake.
          EnsureMainThreadTasksScheduled();

          MaybeInterruptTask(GetHighestPriorityMTTask());
        }

        // Store last task for release next time we release the lock or enter
        // wait state.
        lastTask = mPoolThreads[mThreadPoolIndex].mCurrentTask.forget();
        break;
      }
    }

    // Ensure the last task is released before we enter the wait state.
    if (lastTask) {
      MutexAutoUnlock unlock(mGraphMutex);
      lastTask = nullptr;

      // Run another loop iteration, while we were unlocked there was an
      // opportunity for another task to be posted or shutdown to be initiated.
      continue;
    }

    if (!ranTask) {
      if (mShuttingDown) {
        IOInterposer::UnregisterCurrentThread();
        MOZ_ASSERT(mThreadableTasks.empty());
        return;
      }

      AUTO_PROFILER_LABEL("TaskController::RunPoolThread", IDLE);
      mThreadPoolCV.Wait();
    }
  }
}

void TaskController::AddTask(already_AddRefed&& aTask) {
  RefPtr task(aTask);

  if (!task->IsMainThreadOnly()) {
    MutexAutoLock lock(mPoolInitializationMutex);
    if (!mThreadPoolInitialized) {
      InitializeThreadPool();
      mThreadPoolInitialized = true;
    }
  }

  MutexAutoLock lock(mGraphMutex);

  if (TaskManager* manager = task->GetManager()) {
    if (manager->mTaskCount == 0) {
      mTaskManagers.insert(manager);
    }
    manager->DidQueueTask();

    // Set this here since if this manager's priority modifier doesn't change
    // we will not reprioritize when iterating over the queue.
    task->mPriorityModifier = manager->mCurrentPriorityModifier;
  }

#ifdef MOZ_GECKO_PROFILER
  task->mInsertionTime = TimeStamp::Now();
#endif

#ifdef DEBUG
  task->mIsInGraph = true;

  for (const RefPtr& otherTask : task->mDependencies) {
    MOZ_ASSERT(!otherTask->mTaskManager ||
               otherTask->mTaskManager == task->mTaskManager);
  }
#endif

  LogTask::LogDispatch(task);

  std::pair, Task::PriorityCompare>::iterator, bool>
      insertion;
  if (task->IsMainThreadOnly()) {
    insertion = mMainThreadTasks.insert(std::move(task));
  } else {
    insertion = mThreadableTasks.insert(std::move(task));
  }
  (*insertion.first)->mIterator = insertion.first;
  MOZ_ASSERT(insertion.second);

  MaybeInterruptTask(*insertion.first);
}

void TaskController::WaitForTaskOrMessage() {
  MutexAutoLock lock(mGraphMutex);
  while (!mMayHaveMainThreadTask) {
    AUTO_PROFILER_LABEL("TaskController::WaitForTaskOrMessage", IDLE);
    mMainThreadCV.Wait();
  }
}

void TaskController::ExecuteNextTaskOnlyMainThread() {
  MOZ_ASSERT(NS_IsMainThread());
  MutexAutoLock lock(mGraphMutex);
  ExecuteNextTaskOnlyMainThreadInternal(lock);
}

void TaskController::ProcessPendingMTTask(bool aMayWait) {
  MOZ_ASSERT(NS_IsMainThread());
  MutexAutoLock lock(mGraphMutex);

  for (;;) {
    // We only ever process one event here. However we may sometimes
    // not actually process a real event because of suspended tasks.
    // This loop allows us to wait until we've processed something
    // in that scenario.

    mMTTaskRunnableProcessedTask = ExecuteNextTaskOnlyMainThreadInternal(lock);

    if (mMTTaskRunnableProcessedTask || !aMayWait) {
      break;
    }

    BackgroundHangMonitor().NotifyWait();

    {
      // ProcessNextEvent will also have attempted to wait, however we may have
      // given it a Runnable when all the tasks in our task graph were suspended
      // but we weren't able to cheaply determine that.
      AUTO_PROFILER_LABEL("TaskController::ProcessPendingMTTask", IDLE);
      mMainThreadCV.Wait();
    }

    BackgroundHangMonitor().NotifyActivity();
  }

  if (mMayHaveMainThreadTask) {
    EnsureMainThreadTasksScheduled();
  }
}

void TaskController::ReprioritizeTask(Task* aTask, uint32_t aPriority) {
  MutexAutoLock lock(mGraphMutex);
  std::set, Task::PriorityCompare>* queue = &mMainThreadTasks;
  if (!aTask->IsMainThreadOnly()) {
    queue = &mThreadableTasks;
  }

  MOZ_ASSERT(aTask->mIterator != queue->end());
  queue->erase(aTask->mIterator);

  aTask->mPriority = aPriority;

  auto insertion = queue->insert(aTask);
  MOZ_ASSERT(insertion.second);
  aTask->mIterator = insertion.first;

  MaybeInterruptTask(aTask);
}

// Code supporting runnable compatibility.
// Task that wraps a runnable.
class RunnableTask : public Task {
 public:
  RunnableTask(already_AddRefed&& aRunnable, int32_t aPriority,
               bool aMainThread = true)
      : Task(aMainThread, aPriority), mRunnable(aRunnable) {}

  virtual bool Run() override {
#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY
    MOZ_ASSERT(NS_IsMainThread());
    // If we're on the main thread, we want to record our current
    // runnable's name in a static so that BHR can record it.
    Array restoreRunnableName;
    restoreRunnableName[0] = '\0';
    auto clear = MakeScopeExit([&] {
      MOZ_ASSERT(NS_IsMainThread());
      nsThread::sMainThreadRunnableName = restoreRunnableName;
    });
    nsAutoCString name;
    nsThread::GetLabeledRunnableName(mRunnable, name,
                                     EventQueuePriority(GetPriority()));

    restoreRunnableName = nsThread::sMainThreadRunnableName;

    // Copy the name into sMainThreadRunnableName's buffer, and append a
    // terminating null.
    uint32_t length = std::min((uint32_t)nsThread::kRunnableNameBufSize - 1,
                               (uint32_t)name.Length());
    memcpy(nsThread::sMainThreadRunnableName.begin(), name.BeginReading(),
           length);
    nsThread::sMainThreadRunnableName[length] = '\0';
#endif

    mRunnable->Run();
    mRunnable = nullptr;
    return true;
  }

  void SetIdleDeadline(TimeStamp aDeadline) override {
    nsCOMPtr idleRunnable = do_QueryInterface(mRunnable);
    if (idleRunnable) {
      idleRunnable->SetDeadline(aDeadline);
    }
  }

  PerformanceCounter* GetPerformanceCounter() const override {
    return nsThread::GetPerformanceCounterBase(mRunnable);
  }

  virtual bool GetName(nsACString& aName) override {
#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY
    nsThread::GetLabeledRunnableName(mRunnable, aName,
                                     EventQueuePriority(GetPriority()));
    return true;
#else
    return false;
#endif
  }

 private:
  RefPtr mRunnable;
};

void TaskController::DispatchRunnable(already_AddRefed&& aRunnable,
                                      uint32_t aPriority,
                                      TaskManager* aManager) {
  RefPtr task = new RunnableTask(std::move(aRunnable), aPriority);

  task->SetManager(aManager);
  TaskController::Get()->AddTask(task.forget());
}

nsIRunnable* TaskController::GetRunnableForMTTask(bool aReallyWait) {
  MutexAutoLock lock(mGraphMutex);

  while (mMainThreadTasks.empty()) {
    if (!aReallyWait) {
      return nullptr;
    }

    AUTO_PROFILER_LABEL("TaskController::GetRunnableForMTTask::Wait", IDLE);
    mMainThreadCV.Wait();
  }

  return aReallyWait ? mMTBlockingProcessingRunnable : mMTProcessingRunnable;
}

bool TaskController::HasMainThreadPendingTasks() {
  auto resetIdleState = MakeScopeExit([&idleManager = mIdleTaskManager] {
    if (idleManager) {
      idleManager->State().ClearCachedIdleDeadline();
    }
  });

  for (bool considerIdle : {false, true}) {
    if (considerIdle && !mIdleTaskManager) {
      continue;
    }

    MutexAutoLock lock(mGraphMutex);

    if (considerIdle) {
      mIdleTaskManager->State().ForgetPendingTaskGuarantee();
      // Temporarily unlock so we can peek our idle deadline.
      // XXX We could do this _before_ we take the lock if the API would let us.
      // We do want to do this before looking at mMainThreadTasks, in case
      // someone adds one while we're unlocked.
      {
        MutexAutoUnlock unlock(mGraphMutex);
        mIdleTaskManager->State().CachePeekedIdleDeadline(unlock);
      }
    }

    // Return early if there's no tasks at all.
    if (mMainThreadTasks.empty()) {
      return false;
    }

    // We can cheaply count how many tasks are suspended.
    uint64_t totalSuspended = 0;
    for (TaskManager* manager : mTaskManagers) {
      DebugOnly modifierChanged =
          manager
              ->UpdateCachesForCurrentIterationAndReportPriorityModifierChanged(
                  lock, TaskManager::IterationType::NOT_EVENT_LOOP_TURN);
      MOZ_ASSERT(!modifierChanged);

      // The idle manager should be suspended unless we're doing the idle pass.
      MOZ_ASSERT(manager != mIdleTaskManager || manager->mCurrentSuspended ||
                     considerIdle,
                 "Why are idle tasks not suspended here?");

      if (manager->mCurrentSuspended) {
        // XXX - If managers manage off-main-thread tasks this breaks! This
        // scenario is explicitly not supported.
        //
        // This is only incremented inside the lock -or- decremented on the main
        // thread so this is safe.
        totalSuspended += manager->mTaskCount;
      }
    }

    // Thi would break down if we have a non-suspended task depending on a
    // suspended task. This is why for the moment we do not allow tasks
    // to be dependent on tasks managed by another taskmanager.
    if (mMainThreadTasks.size() > totalSuspended) {
      // If mIdleTaskManager->mTaskCount is 0, we never updated the suspended
      // state of mIdleTaskManager above, hence shouldn't even check it here.
      // But in that case idle tasks are not contributing to our suspended task
      // count anyway.
      if (mIdleTaskManager && mIdleTaskManager->mTaskCount &&
          !mIdleTaskManager->mCurrentSuspended) {
        MOZ_ASSERT(considerIdle, "Why is mIdleTaskManager not suspended?");
        // Check whether the idle tasks were really needed to make our "we have
        // an unsuspended task" decision.  If they were, we need to force-enable
        // idle tasks until we run our next task.
        if (mMainThreadTasks.size() - mIdleTaskManager->mTaskCount <=
            totalSuspended) {
          mIdleTaskManager->State().EnforcePendingTaskGuarantee();
        }
      }
      return true;
    }
  }
  return false;
}

bool TaskController::ExecuteNextTaskOnlyMainThreadInternal(
    const MutexAutoLock& aProofOfLock) {
  // Block to make it easier to jump to our cleanup.
  bool taskRan = false;
  do {
    taskRan = DoExecuteNextTaskOnlyMainThreadInternal(aProofOfLock);
    if (taskRan) {
      break;
    }

    if (!mIdleTaskManager) {
      break;
    }

    if (mIdleTaskManager->mTaskCount) {
      // We have idle tasks that we may not have gotten above because
      // our idle state is not up to date.  We need to update the idle state
      // and try again.  We need to temporarily release the lock while we do
      // that.
      MutexAutoUnlock unlock(mGraphMutex);
      mIdleTaskManager->State().UpdateCachedIdleDeadline(unlock);
    } else {
      MutexAutoUnlock unlock(mGraphMutex);
      mIdleTaskManager->State().RanOutOfTasks(unlock);
    }

    // When we unlocked, someone may have queued a new task on us.  So try to
    // see whether we can run things again.
    taskRan = DoExecuteNextTaskOnlyMainThreadInternal(aProofOfLock);
  } while (false);

  if (mIdleTaskManager) {
    // The pending task guarantee is not needed anymore, since we just tried
    // running a task
    mIdleTaskManager->State().ForgetPendingTaskGuarantee();

    if (mMainThreadTasks.empty()) {
      // XXX the IdlePeriodState API demands we have a MutexAutoUnlock for it.
      // Otherwise we could perhaps just do this after we exit the locked block,
      // by pushing the lock down into this method.  Though it's not clear that
      // we could check mMainThreadTasks.size() once we unlock, and whether we
      // could maybe substitute mMayHaveMainThreadTask for that check.
      MutexAutoUnlock unlock(mGraphMutex);
      mIdleTaskManager->State().RanOutOfTasks(unlock);
    }
  }

  return taskRan;
}

bool TaskController::DoExecuteNextTaskOnlyMainThreadInternal(
    const MutexAutoLock& aProofOfLock) {
#ifdef MOZ_GECKO_PROFILER
  nsCOMPtr mainIThread;
  NS_GetMainThread(getter_AddRefs(mainIThread));

  nsThread* mainThread = static_cast(mainIThread.get());
  if (mainThread) {
    mainThread->SetRunningEventDelay(TimeDuration(), TimeStamp());
  }
#endif

  uint32_t totalSuspended = 0;
  for (TaskManager* manager : mTaskManagers) {
    bool modifierChanged =
        manager
            ->UpdateCachesForCurrentIterationAndReportPriorityModifierChanged(
                aProofOfLock, TaskManager::IterationType::EVENT_LOOP_TURN);
    if (modifierChanged) {
      ProcessUpdatedPriorityModifier(manager);
    }
    if (manager->mCurrentSuspended) {
      totalSuspended += manager->mTaskCount;
    }
  }

  MOZ_ASSERT(mMainThreadTasks.size() >= totalSuspended);

  // This would break down if we have a non-suspended task depending on a
  // suspended task. This is why for the moment we do not allow tasks
  // to be dependent on tasks managed by another taskmanager.
  if (mMainThreadTasks.size() > totalSuspended) {
    for (auto iter = mMainThreadTasks.begin(); iter != mMainThreadTasks.end();
         iter++) {
      Task* task = iter->get();

      if (task->mTaskManager && task->mTaskManager->mCurrentSuspended) {
        // Even though we may want to run some dependencies of this task, we
        // will run them at their own priority level and not the priority
        // level of their dependents.
        continue;
      }

      task = GetFinalDependency(task);

      if (!task->IsMainThreadOnly() || task->mInProgress ||
          (task->mTaskManager && task->mTaskManager->mCurrentSuspended)) {
        continue;
      }

      mCurrentTasksMT.push(task);
      mMainThreadTasks.erase(task->mIterator);
      task->mIterator = mMainThreadTasks.end();
      task->mInProgress = true;
      TaskManager* manager = task->GetManager();
      bool result = false;

      {
        MutexAutoUnlock unlock(mGraphMutex);
        if (manager) {
          manager->WillRunTask();
          if (manager != mIdleTaskManager) {
            // Notify the idle period state that we're running a non-idle task.
            // This needs to happen while our mutex is not locked!
            mIdleTaskManager->State().FlagNotIdle();
          } else {
            TimeStamp idleDeadline =
                mIdleTaskManager->State().GetCachedIdleDeadline();
            MOZ_ASSERT(
                idleDeadline,
                "How can we not have a deadline if our manager is enabled?");
            task->SetIdleDeadline(idleDeadline);
          }
        }
        if (mIdleTaskManager) {
          // We found a task to run; we can clear the idle deadline on our idle
          // task manager.  This _must_ be done before we actually run the task,
          // because running the task could reenter via spinning the event loop
          // and we want to make sure there's no cached idle deadline at that
          // point.  But we have to make sure we do it after out SetIdleDeadline
          // call above, in the case when the task is actually an idle task.
          mIdleTaskManager->State().ClearCachedIdleDeadline();
        }

        TimeStamp now = TimeStamp::Now();

#ifdef MOZ_GECKO_PROFILER
        if (mainThread) {
          if (task->GetPriority() < uint32_t(EventQueuePriority::InputHigh)) {
            mainThread->SetRunningEventDelay(TimeDuration(), now);
          } else {
            mainThread->SetRunningEventDelay(now - task->mInsertionTime, now);
          }
        }
#endif

        PerformanceCounterState::Snapshot snapshot =
            mPerformanceCounterState->RunnableWillRun(
                task->GetPerformanceCounter(), now,
                manager == mIdleTaskManager);

        {
          LogTask::Run log(task);
          AUTO_PROFILE_FOLLOWING_TASK(task);
          result = task->Run();
        }

        // Task itself should keep manager alive.
        if (manager) {
          manager->DidRunTask();
        }

        mPerformanceCounterState->RunnableDidRun(std::move(snapshot));
      }

      // Task itself should keep manager alive.
      if (manager && result && manager->mTaskCount == 0) {
        mTaskManagers.erase(manager);
      }

      task->mInProgress = false;

      if (!result) {
        // Presumably this task was interrupted, leave its dependencies
        // unresolved and reinsert into the queue.
        auto insertion =
            mMainThreadTasks.insert(std::move(mCurrentTasksMT.top()));
        MOZ_ASSERT(insertion.second);
        task->mIterator = insertion.first;
        manager->WillRunTask();
      } else {
        task->mCompleted = true;
#ifdef DEBUG
        task->mIsInGraph = false;
#endif
        // Clear dependencies to release references.
        task->mDependencies.clear();

        if (!mThreadableTasks.empty()) {
          // Since this could have multiple dependencies thare are not
          // restricted to the main thread. Let's wake up our thread pool.
          // There is a cost to this, it's possible we will want to wake up
          // only as many threads as we have unblocked tasks, but we currently
          // have no way to determine that easily.
          mThreadPoolCV.NotifyAll();
        }
      }

      mCurrentTasksMT.pop();
      return true;
    }
  }

  mMayHaveMainThreadTask = false;
  if (mIdleTaskManager) {
    // We did not find a task to run.  We still need to clear the cached idle
    // deadline on our idle state, because that deadline was only relevant to
    // the execution of this function.  Had we found a task, we would have
    // cleared the deadline before running that task.
    mIdleTaskManager->State().ClearCachedIdleDeadline();
  }
  return false;
}

Task* TaskController::GetFinalDependency(Task* aTask) {
  Task* nextTask;

  while ((nextTask = aTask->GetHighestPriorityDependency())) {
    aTask = nextTask;
  }

  return aTask;
}

void TaskController::MaybeInterruptTask(Task* aTask) {
  mGraphMutex.AssertCurrentThreadOwns();

  if (!aTask) {
    return;
  }

  // This optimization prevents many slow lookups in long chains of similar
  // priority.
  if (!aTask->mDependencies.empty()) {
    Task* firstDependency = aTask->mDependencies.begin()->get();
    if (aTask->GetPriority() <= firstDependency->GetPriority() &&
        !firstDependency->mCompleted &&
        aTask->IsMainThreadOnly() == firstDependency->IsMainThreadOnly()) {
      // This task has the same or a higher priority as one of its dependencies,
      // never any need to interrupt.
      return;
    }
  }

  Task* finalDependency = GetFinalDependency(aTask);

  if (finalDependency->mInProgress) {
    // No need to wake anything, we can't schedule this task right now anyway.
    return;
  }

  if (aTask->IsMainThreadOnly()) {
    mMayHaveMainThreadTask = true;

    EnsureMainThreadTasksScheduled();

    if (mCurrentTasksMT.empty()) {
      return;
    }

    // We could go through the steps above here and interrupt an off main
    // thread task in case it has a lower priority.
    if (!finalDependency->IsMainThreadOnly()) {
      return;
    }

    if (mCurrentTasksMT.top()->GetPriority() < aTask->GetPriority()) {
      mCurrentTasksMT.top()->RequestInterrupt(aTask->GetPriority());
    }
  } else {
    Task* lowestPriorityTask = nullptr;
    for (PoolThread& thread : mPoolThreads) {
      if (!thread.mCurrentTask) {
        mThreadPoolCV.Notify();
        // There's a free thread, no need to interrupt anything.
        return;
      }

      if (!lowestPriorityTask) {
        lowestPriorityTask = thread.mCurrentTask.get();
        continue;
      }

      // This should possibly select the lowest priority task which was started
      // the latest. But for now we ignore that optimization.
      // This also doesn't guarantee a task is interruptable, so that's an
      // avenue for improvements as well.
      if (lowestPriorityTask->GetPriority() > thread.mEffectiveTaskPriority) {
        lowestPriorityTask = thread.mCurrentTask.get();
      }
    }

    if (lowestPriorityTask->GetPriority() < aTask->GetPriority()) {
      lowestPriorityTask->RequestInterrupt(aTask->GetPriority());
    }

    // We choose not to interrupt main thread tasks for tasks which may be
    // executed off the main thread.
  }
}

Task* TaskController::GetHighestPriorityMTTask() {
  mGraphMutex.AssertCurrentThreadOwns();

  if (!mMainThreadTasks.empty()) {
    return mMainThreadTasks.begin()->get();
  }
  return nullptr;
}

void TaskController::EnsureMainThreadTasksScheduled() {
  if (mObserver) {
    mObserver->OnDispatchedEvent();
  }
  if (mExternalCondVar) {
    mExternalCondVar->Notify();
  }
  mMainThreadCV.Notify();
}

void TaskController::ProcessUpdatedPriorityModifier(TaskManager* aManager) {
  mGraphMutex.AssertCurrentThreadOwns();

  MOZ_ASSERT(NS_IsMainThread());

  int32_t modifier = aManager->mCurrentPriorityModifier;

  std::vector> storedTasks;
  // Find all relevant tasks.
  for (auto iter = mMainThreadTasks.begin(); iter != mMainThreadTasks.end();) {
    if ((*iter)->mTaskManager == aManager) {
      storedTasks.push_back(*iter);
      iter = mMainThreadTasks.erase(iter);
    } else {
      iter++;
    }
  }

  // Reinsert found tasks with their new priorities.
  for (RefPtr& ref : storedTasks) {
    // Kept alive at first by the vector and then by mMainThreadTasks.
    Task* task = ref;
    task->mPriorityModifier = modifier;
    auto insertion = mMainThreadTasks.insert(std::move(ref));
    MOZ_ASSERT(insertion.second);
    task->mIterator = insertion.first;
  }
}

}  // namespace mozilla