/* 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 "mozilla/IdlePeriodState.h"

#include "mozilla/AppShutdown.h"
#include "mozilla/StaticPrefs_idle_period.h"
#include "mozilla/dom/ContentChild.h"
#include "mozilla/ipc/IdleSchedulerChild.h"
#include "nsIIdlePeriod.h"
#include "nsThreadManager.h"
#include "nsXPCOM.h"
#include "nsXULAppAPI.h"

static uint64_t sIdleRequestCounter = 0;

namespace mozilla {

IdlePeriodState::IdlePeriodState(already_AddRefed<nsIIdlePeriod> aIdlePeriod)
    : mIdlePeriod(aIdlePeriod) {
  MOZ_ASSERT(NS_IsMainThread(),
             "Why are we touching idle state off the main thread?");
}

IdlePeriodState::~IdlePeriodState() {
  MOZ_ASSERT(NS_IsMainThread(),
             "Why are we touching idle state off the main thread?");
  if (mIdleScheduler) {
    mIdleScheduler->Disconnect();
  }
}

size_t IdlePeriodState::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
  size_t n = 0;
  if (mIdlePeriod) {
    n += aMallocSizeOf(mIdlePeriod);
  }

  return n;
}

void IdlePeriodState::FlagNotIdle() {
  MOZ_ASSERT(NS_IsMainThread(),
             "Why are we touching idle state off the main thread?");

  EnsureIsActive();
  if (mIdleToken && mIdleToken < TimeStamp::Now()) {
    ClearIdleToken();
  }
}

void IdlePeriodState::RanOutOfTasks(const MutexAutoUnlock& aProofOfUnlock) {
  MOZ_ASSERT(NS_IsMainThread(),
             "Why are we touching idle state off the main thread?");
  MOZ_ASSERT(!mHasPendingEventsPromisedIdleEvent);
  EnsureIsPaused(aProofOfUnlock);
  ClearIdleToken();
}

TimeStamp IdlePeriodState::GetIdleDeadlineInternal(
    bool aIsPeek, const MutexAutoUnlock& aProofOfUnlock) {
  MOZ_ASSERT(NS_IsMainThread(),
             "Why are we touching idle state off the main thread?");

  bool shuttingDown;
  TimeStamp localIdleDeadline =
      GetLocalIdleDeadline(shuttingDown, aProofOfUnlock);
  if (!localIdleDeadline) {
    if (!aIsPeek) {
      EnsureIsPaused(aProofOfUnlock);
      ClearIdleToken();
    }
    return TimeStamp();
  }

  TimeStamp idleDeadline =
      mHasPendingEventsPromisedIdleEvent || shuttingDown
          ? localIdleDeadline
          : GetIdleToken(localIdleDeadline, aProofOfUnlock);
  if (!idleDeadline) {
    if (!aIsPeek) {
      EnsureIsPaused(aProofOfUnlock);

      // Don't call ClearIdleToken() here, since we may have a pending
      // request already.
      //
      // RequestIdleToken can do all sorts of IPC stuff that might
      // take mutexes.  This is one reason why we need the
      // MutexAutoUnlock reference!
      RequestIdleToken(localIdleDeadline);
    }
    return TimeStamp();
  }

  if (!aIsPeek) {
    EnsureIsActive();
  }
  return idleDeadline;
}

TimeStamp IdlePeriodState::GetLocalIdleDeadline(
    bool& aShuttingDown, const MutexAutoUnlock& aProofOfUnlock) {
  MOZ_ASSERT(NS_IsMainThread(),
             "Why are we touching idle state off the main thread?");
  // 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 (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownThreads) ||
      nsThreadManager::get().GetCurrentThread()->ShuttingDown()) {
    aShuttingDown = true;
    return TimeStamp::Now();
  }

  aShuttingDown = false;
  TimeStamp idleDeadline;
  // This GetIdlePeriodHint() call is the reason we need a MutexAutoUnlock here.
  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;
}

TimeStamp IdlePeriodState::GetIdleToken(TimeStamp aLocalIdlePeriodHint,
                                        const MutexAutoUnlock& aProofOfUnlock) {
  MOZ_ASSERT(NS_IsMainThread(),
             "Why are we touching idle state off the main thread?");

  if (!ShouldGetIdleToken()) {
    // If the process was in background, it may have an idle token, but it can
    // be cleared now.
    ClearIdleToken();
    return aLocalIdlePeriodHint;
  }

  if (mIdleToken) {
    TimeStamp now = TimeStamp::Now();
    if (mIdleToken < now) {
      ClearIdleToken();
      return mIdleToken;
    }
    return mIdleToken < aLocalIdlePeriodHint ? mIdleToken
                                             : aLocalIdlePeriodHint;
  }
  return TimeStamp();
}

void IdlePeriodState::RequestIdleToken(TimeStamp aLocalIdlePeriodHint) {
  MOZ_ASSERT(NS_IsMainThread(),
             "Why are we touching idle state off the main thread?");
  MOZ_ASSERT(!mActive);

  if (!mIdleScheduler && ShouldGetIdleToken()) {
    // For now cross-process idle scheduler is supported only on the main
    // threads of the child processes.
    mIdleScheduler = ipc::IdleSchedulerChild::GetMainThreadIdleScheduler();
    if (mIdleScheduler) {
      mIdleScheduler->Init(this);
    }
  }

  if (mIdleScheduler && !mIdleRequestId) {
    TimeStamp now = TimeStamp::Now();
    if (aLocalIdlePeriodHint <= now) {
      return;
    }

    mIdleRequestId = ++sIdleRequestCounter;
    mIdleScheduler->SendRequestIdleTime(mIdleRequestId,
                                        aLocalIdlePeriodHint - now);
  }
}

void IdlePeriodState::SetIdleToken(uint64_t aId, TimeDuration aDuration) {
  MOZ_ASSERT(NS_IsMainThread(),
             "Why are we touching idle state off the main thread?");

  // We check the request ID.  It's possible that the server may be granting a
  // an ealier request that the client has since cancelled and re-requested.
  if (mIdleRequestId == aId) {
    mIdleToken = TimeStamp::Now() + aDuration;
  }
}

void IdlePeriodState::SetActive() {
  MOZ_ASSERT(NS_IsMainThread(),
             "Why are we touching idle state off the main thread?");
  MOZ_ASSERT(!mActive);
  if (mIdleScheduler) {
    mIdleScheduler->SetActive();
  }
  mActive = true;
}

void IdlePeriodState::SetPaused(const MutexAutoUnlock& aProofOfUnlock) {
  MOZ_ASSERT(NS_IsMainThread(),
             "Why are we touching idle state off the main thread?");
  MOZ_ASSERT(mActive);
  if (mIdleScheduler && mIdleScheduler->SetPaused()) {
    // We may have gotten a free cpu core for running idle tasks.
    // We don't try to catch the case when there are prioritized processes
    // running.

    // This SendSchedule call is why we need the MutexAutoUnlock here, because
    // IPC can do weird things with mutexes.
    mIdleScheduler->SendSchedule();
  }
  mActive = false;
}

void IdlePeriodState::ClearIdleToken() {
  MOZ_ASSERT(NS_IsMainThread(),
             "Why are we touching idle state off the main thread?");

  if (mIdleRequestId) {
    if (mIdleScheduler) {
      // This SendIdleTimeUsed call is why we need to not be holding
      // any locks here, because IPC can do weird things with mutexes.
      // Ideally we'd have a MutexAutoUnlock& reference here, but some
      // callers end up here while just not holding any locks at all.
      mIdleScheduler->SendIdleTimeUsed(mIdleRequestId);
    }
    mIdleRequestId = 0;
    mIdleToken = TimeStamp();
  }
}

bool IdlePeriodState::ShouldGetIdleToken() {
  return StaticPrefs::idle_period_cross_process_scheduling() &&
         dom::ContentChild::GetSingleton() &&
         dom::ContentChild::GetSingleton()->GetProcessPriority() <
             hal::ProcessPriority::PROCESS_PRIORITY_FOREGROUND;
}
}  // namespace mozilla
