/* -*- 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 "TimeoutExecutor.h" #include "mozilla/EventQueue.h" #include "mozilla/Logging.h" #include "mozilla/dom/TimeoutManager.h" #include "nsComponentManagerUtils.h" #include "nsIEventTarget.h" #include "nsString.h" #include "nsThreadUtils.h" extern mozilla::LazyLogModule gTimeoutLog; namespace mozilla::dom { NS_IMPL_ISUPPORTS(TimeoutExecutor, nsIRunnable, nsITimerCallback, nsINamed) TimeoutExecutor::~TimeoutExecutor() { // The TimeoutManager should keep the Executor alive until its destroyed, // and then call Shutdown() explicitly. MOZ_DIAGNOSTIC_ASSERT(mMode == Mode::Shutdown); MOZ_DIAGNOSTIC_ASSERT(!mOwner); MOZ_DIAGNOSTIC_ASSERT(!mTimer); } nsresult TimeoutExecutor::ScheduleImmediate(const TimeStamp& aDeadline, const TimeStamp& aNow) { MOZ_DIAGNOSTIC_ASSERT(mDeadline.IsNull()); MOZ_DIAGNOSTIC_ASSERT(mMode == Mode::None); MOZ_DIAGNOSTIC_ASSERT(aDeadline <= (aNow + mAllowedEarlyFiringTime)); nsresult rv; if (mIsIdleQueue) { RefPtr runnable(this); MOZ_LOG(gTimeoutLog, LogLevel::Debug, ("Starting IdleDispatch runnable")); rv = NS_DispatchToCurrentThreadQueue(runnable.forget(), mMaxIdleDeferMS, EventQueuePriority::DeferredTimers); } else { rv = mOwner->EventTarget()->Dispatch(this, nsIEventTarget::DISPATCH_NORMAL); } NS_ENSURE_SUCCESS(rv, rv); mMode = Mode::Immediate; mDeadline = aDeadline; return NS_OK; } nsresult TimeoutExecutor::ScheduleDelayed(const TimeStamp& aDeadline, const TimeStamp& aNow, const TimeDuration& aMinDelay) { MOZ_DIAGNOSTIC_ASSERT(mDeadline.IsNull()); MOZ_DIAGNOSTIC_ASSERT(mMode == Mode::None); MOZ_DIAGNOSTIC_ASSERT(!aMinDelay.IsZero() || aDeadline > (aNow + mAllowedEarlyFiringTime)); nsresult rv = NS_OK; if (mIsIdleQueue) { // Nothing goes into the idletimeouts list if it wasn't going to // fire at that time, so we can always schedule idle-execution of // these immediately return ScheduleImmediate(aNow, aNow); } if (!mTimer) { mTimer = NS_NewTimer(mOwner->EventTarget()); NS_ENSURE_TRUE(mTimer, NS_ERROR_OUT_OF_MEMORY); uint32_t earlyMicros = 0; MOZ_ALWAYS_SUCCEEDS( mTimer->GetAllowedEarlyFiringMicroseconds(&earlyMicros)); mAllowedEarlyFiringTime = TimeDuration::FromMicroseconds(earlyMicros); // Re-evaluate if we should have scheduled this immediately if (aDeadline <= (aNow + mAllowedEarlyFiringTime)) { return ScheduleImmediate(aDeadline, aNow); } } else { // Always call Cancel() in case we are re-using a timer. rv = mTimer->Cancel(); NS_ENSURE_SUCCESS(rv, rv); } // Calculate the delay based on the deadline and current time. If we have // a minimum delay set then clamp to that value. // // Note, we don't actually adjust our mDeadline for the minimum delay, just // the nsITimer value. This is necessary to avoid lots of needless // rescheduling if more deadlines come in between now and the minimum delay // firing time. TimeDuration delay = TimeDuration::Max(aMinDelay, aDeadline - aNow); // Note, we cannot use the normal nsITimer init methods that take // integer milliseconds. We need higher precision. Consider this // situation: // // 1. setTimeout(f, 1); // 2. do some work for 500us // 3. setTimeout(g, 1); // // This should fire f() and g() 500us apart. // // In the past worked because each setTimeout() got its own nsITimer. The 1ms // was preserved and passed through to nsITimer which converted it to a // TimeStamp, etc. // // Now, however, there is only one nsITimer. We fire f() and then try to // schedule a new nsITimer for g(). Its only 500us in the future, though. We // must be able to pass this fractional value to nsITimer in order to get an // accurate wakeup time. rv = mTimer->InitHighResolutionWithCallback(this, delay, nsITimer::TYPE_ONE_SHOT); NS_ENSURE_SUCCESS(rv, rv); mMode = Mode::Delayed; mDeadline = aDeadline; return NS_OK; } nsresult TimeoutExecutor::Schedule(const TimeStamp& aDeadline, const TimeDuration& aMinDelay) { TimeStamp now(TimeStamp::Now()); // Schedule an immediate runnable if the desired deadline has passed // or is slightly in the future. This is similar to how nsITimer will // fire timers early based on the interval resolution. if (aMinDelay.IsZero() && aDeadline <= (now + mAllowedEarlyFiringTime)) { return ScheduleImmediate(aDeadline, now); } return ScheduleDelayed(aDeadline, now, aMinDelay); } nsresult TimeoutExecutor::MaybeReschedule(const TimeStamp& aDeadline, const TimeDuration& aMinDelay) { MOZ_DIAGNOSTIC_ASSERT(!mDeadline.IsNull()); MOZ_DIAGNOSTIC_ASSERT(mMode == Mode::Immediate || mMode == Mode::Delayed); if (aDeadline >= mDeadline) { return NS_OK; } if (mMode == Mode::Immediate) { // Don't reduce the deadline here as we want to execute the // timer we originally scheduled even if its a few microseconds // in the future. return NS_OK; } Cancel(); return Schedule(aDeadline, aMinDelay); } void TimeoutExecutor::MaybeExecute() { MOZ_DIAGNOSTIC_ASSERT(mMode != Mode::Shutdown && mMode != Mode::None); MOZ_DIAGNOSTIC_ASSERT(mOwner); MOZ_DIAGNOSTIC_ASSERT(!mDeadline.IsNull()); TimeStamp deadline(mDeadline); // Sometimes nsITimer or canceled timers will fire too early. If this // happens then just cap our deadline to our maximum time in the future // and proceed. If there are no timers ready we will get rescheduled // by TimeoutManager. TimeStamp now(TimeStamp::Now()); TimeStamp limit = now + mAllowedEarlyFiringTime; if (deadline > limit) { deadline = limit; } Cancel(); mOwner->RunTimeout(now, deadline, mIsIdleQueue); } TimeoutExecutor::TimeoutExecutor(TimeoutManager* aOwner, bool aIsIdleQueue, uint32_t aMaxIdleDeferMS) : mOwner(aOwner), mIsIdleQueue(aIsIdleQueue), mMaxIdleDeferMS(aMaxIdleDeferMS), mMode(Mode::None) { MOZ_DIAGNOSTIC_ASSERT(mOwner); } void TimeoutExecutor::Shutdown() { mOwner = nullptr; if (mTimer) { mTimer->Cancel(); mTimer = nullptr; } mMode = Mode::Shutdown; mDeadline = TimeStamp(); } nsresult TimeoutExecutor::MaybeSchedule(const TimeStamp& aDeadline, const TimeDuration& aMinDelay) { MOZ_DIAGNOSTIC_ASSERT(!aDeadline.IsNull()); if (mMode == Mode::Shutdown) { return NS_OK; } if (mMode == Mode::Immediate || mMode == Mode::Delayed) { return MaybeReschedule(aDeadline, aMinDelay); } return Schedule(aDeadline, aMinDelay); } void TimeoutExecutor::Cancel() { if (mTimer) { mTimer->Cancel(); } mMode = Mode::None; mDeadline = TimeStamp(); } // MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT. See // bug 1535398. MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP TimeoutExecutor::Run() { // If the executor is canceled and then rescheduled its possible to get // spurious executions here. Ignore these unless our current mode matches. MOZ_LOG(gTimeoutLog, LogLevel::Debug, ("Running Immediate %stimers", mIsIdleQueue ? "Idle" : "")); if (mMode == Mode::Immediate) { MaybeExecute(); } return NS_OK; } // MOZ_CAN_RUN_SCRIPT_BOUNDARY until nsITimerCallback::Notify is // MOZ_CAN_RUN_SCRIPT. MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP TimeoutExecutor::Notify(nsITimer* aTimer) { // If the executor is canceled and then rescheduled its possible to get // spurious executions here. Ignore these unless our current mode matches. if (mMode == Mode::Delayed) { MaybeExecute(); } return NS_OK; } NS_IMETHODIMP TimeoutExecutor::GetName(nsACString& aNameOut) { aNameOut.AssignLiteral("TimeoutExecutor Runnable"); return NS_OK; } } // namespace mozilla::dom