/* 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/. */ #ifndef TimerThread_h_ #define TimerThread_h_ #include "nsIObserver.h" #include "nsIRunnable.h" #include "nsIThread.h" #include "nsTimerImpl.h" #include "nsThreadUtils.h" #include "nsTArray.h" #include "mozilla/Monitor.h" #include "mozilla/ProfilerUtils.h" #include "mozilla/Span.h" #if defined(XP_WIN) # include #endif // Enable this to compute lots of interesting statistics and print them out when // PrintStatistics() is called. #define TIMER_THREAD_STATISTICS 0 class TimerThread final : public mozilla::Runnable, public nsIObserver { private: #if defined(XP_WIN) // HiResWindowsMonitor is a simple (Windows-only) implementaton of a monitor // that uses a Windows waitable timer object and a Windows event object (along // with a mutex) as its synchronization primitives. When precise firing is // needed (as determined by the tolerance parameter when waiting) a special // high-resolution waitable timer will be used. Otherwise a regular waitable // timer will be used. // NOTE: Although it's not documented by Microsoft at this moment (as far as I // can tell), it seems that hi-res timers are fundamentally different under // the hood and don't support a lot of the features that non-hi-res timers // support. You cannot use names, callbacks or tolerances with them. The only // mention I've seen of this is https://stackoverflow.com/questions/73647588. class MOZ_CAPABILITY("monitor") HiResWindowsMonitor final { public: explicit HiResWindowsMonitor(const char* aName) : mMutex(aName), mHandles{{ CreateWaitableTimerEx(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS), CreateEvent(nullptr, FALSE, FALSE, nullptr), CreateWaitableTimerEx(nullptr, nullptr, 0, TIMER_ALL_ACCESS) }} { // These are MOZ_RELEASE_ASSERT's because, if we fail to create either of // those objects, we have no way to continue. GetHiResTimer() is not // checked and could possibly be nullptr on old-enough versions of Windows // that don't support high-res timers. We allow this and fall back to // lo-res timers in that case. MOZ_RELEASE_ASSERT(GetEvent() != nullptr); MOZ_RELEASE_ASSERT(GetLoResTimer() != nullptr); } ~HiResWindowsMonitor() { [[maybe_unused]] const BOOL b0 = CloseHandle(GetLoResTimer()); MOZ_ASSERT(b0 != 0); [[maybe_unused]] const BOOL b1 = CloseHandle(GetEvent()); MOZ_ASSERT(b1 != 0); if (GetHiResTimer()) { [[maybe_unused]] const BOOL b2 = CloseHandle(GetHiResTimer()); MOZ_ASSERT(b2 != 0); } } MOZ_ALWAYS_INLINE void Lock() MOZ_CAPABILITY_ACQUIRE() { mMutex.Lock(); } MOZ_ALWAYS_INLINE void Unlock() MOZ_CAPABILITY_RELEASE() { mMutex.Unlock(); } // Sleep until notified. void Wait() MOZ_REQUIRES(this) { Unlock(); WaitForSingleObject(GetEvent(), INFINITE); Lock(); } private: void WaitHiRes(const LARGE_INTEGER* aDuration) MOZ_REQUIRES(this) { const BOOL b = SetWaitableTimerEx(GetHiResTimer(), aDuration, 0, nullptr, nullptr, nullptr, 0); MOZ_RELEASE_ASSERT(b != 0); mMutex.AssertCurrentThreadOwns(); Unlock(); const mozilla::Span handles{GetHiResHandles()}; WaitForMultipleObjects(handles.size(), handles.data(), FALSE, INFINITE); Lock(); } void WaitLoRes(const LARGE_INTEGER* aDuration, const uint64_t aTolerance_ms) MOZ_REQUIRES(this) { const BOOL b = SetWaitableTimerEx(GetLoResTimer(), aDuration, 0, nullptr, nullptr, nullptr, aTolerance_ms); MOZ_RELEASE_ASSERT(b != 0); mMutex.AssertCurrentThreadOwns(); Unlock(); const mozilla::Span handles{GetLoResHandles()}; WaitForMultipleObjects(handles.size(), handles.data(), FALSE, INFINITE); Lock(); } public: // Sleep for the specified number of microseconds or until notified. void Wait(const uint64_t aDuration_us, const uint64_t aTolerance_ms) MOZ_REQUIRES(this) { // duration needs to be in "hundreds of nanoseconds", negative indicates // value is relative rather than absolute const LARGE_INTEGER duration{ .QuadPart = static_cast(aDuration_us) * -10LL}; if (aTolerance_ms <= sHiResThreshold_ms && GetHiResTimer()) { WaitHiRes(&duration); } else { WaitLoRes(&duration, aTolerance_ms); } } // Sleep for the specified number of microseconds or until notified. // Negative waits are clamped to zero. MOZ_ALWAYS_INLINE void Wait(const double aDuration_us, const double aTolerance_ms) MOZ_REQUIRES(this) { const uint64_t duration_us = static_cast(std::max(aDuration_us, 0.0)); const uint64_t tolerance_ms = static_cast(std::max(aTolerance_ms, 0.0)); Wait(duration_us, tolerance_ms); } // Sleep for the specified duration or until notified. Negative waits are // clamped to zero. void Wait(mozilla::TimeDuration aDuration, mozilla::TimeDuration aTolerance) MOZ_REQUIRES(this) { if (aDuration != TimeDuration::Forever()) { Wait(aDuration.ToMicroseconds(), aTolerance.ToMilliseconds()); } else { Wait(); } } // Wake one thread waiting on the monitor. MOZ_ALWAYS_INLINE void Notify() { const BOOL b = SetEvent(GetEvent()); MOZ_RELEASE_ASSERT(b != 0); } void AssertCurrentThreadOwns() const MOZ_ASSERT_CAPABILITY(this) { mMutex.AssertCurrentThreadOwns(); } void AssertNotCurrentThreadOwns() const MOZ_ASSERT_CAPABILITY(!this) { mMutex.AssertNotCurrentThreadOwns(); } private: // Waits with a tolerance at or below this threshold will use hi-res timers. static constexpr uint64_t sHiResThreshold_ms = 16; // Convenience functions for accessing the handles and not having to // remember which is which. MOZ_ALWAYS_INLINE HANDLE GetHiResTimer() const { return mHandles[0]; } MOZ_ALWAYS_INLINE HANDLE GetEvent() const { return mHandles[1]; } MOZ_ALWAYS_INLINE HANDLE GetLoResTimer() const { return mHandles[2]; } // Returns a span corresponding to the HANDLEs that are needed for hi-res // waiting. MOZ_ALWAYS_INLINE mozilla::Span GetHiResHandles() const { return mozilla::Span{mHandles}.Subspan<0, 2>(); } // Returns a span corresponding to the HANDLEs that are needed for lo-res // waiting. MOZ_ALWAYS_INLINE mozilla::Span GetLoResHandles() const { return mozilla::Span{mHandles}.Subspan<1, 2>(); } mozilla::Mutex mMutex; std::array mHandles; }; typedef HiResWindowsMonitor TimerThreadMonitor; #else typedef mozilla::Monitor TimerThreadMonitor; #endif using TimerThreadMonitorAutoLock = mozilla::MonitorAutoLockBase; using TimerThreadMonitorAutoUnlock = mozilla::MonitorAutoUnlockBase; public: typedef mozilla::MutexAutoLock MutexAutoLock; typedef mozilla::TimeStamp TimeStamp; typedef mozilla::TimeDuration TimeDuration; TimerThread(); NS_DECL_ISUPPORTS_INHERITED NS_DECL_NSIRUNNABLE NS_DECL_NSIOBSERVER nsresult Shutdown(); nsresult AddTimer(nsTimerImpl* aTimer, const MutexAutoLock& aProofOfLock) MOZ_REQUIRES(aTimer->mMutex); nsresult RemoveTimer(nsTimerImpl* aTimer, const MutexAutoLock& aProofOfLock) MOZ_REQUIRES(aTimer->mMutex); // Considering only the first 'aSearchBound' timers (in firing order), returns // the timeout of the first non-low-priority timer, on the current thread, // that will fire before 'aDefault'. If no such timer exists, 'aDefault' is // returned. TimeStamp FindNextFireTimeForCurrentThread(TimeStamp aDefault, uint32_t aSearchBound); void DoBeforeSleep(); void DoAfterSleep(); bool IsOnTimerThread() const { return mThread->IsOnCurrentThread(); } uint32_t AllowedEarlyFiringMicroseconds(); nsresult GetTimers(nsTArray>& aRetVal); private: ~TimerThread(); bool mInitialized; // These internal helper methods must be called while mMonitor is held. void AddTimerInternal(nsTimerImpl& aTimer) MOZ_REQUIRES(mMonitor); bool RemoveTimerInternal(nsTimerImpl& aTimer) MOZ_REQUIRES(mMonitor, aTimer.mMutex); void RemoveLeadingCanceledTimersInternal() MOZ_REQUIRES(mMonitor); nsresult Init() MOZ_REQUIRES(mMonitor); void AssertTimersSortedAndUnique() MOZ_REQUIRES(mMonitor); nsCOMPtr mThread; // Lock ordering requirements: // (optional) ThreadWrapper::sMutex -> // (optional) nsTimerImpl::mMutex -> // TimerThread::mMonitor TimerThreadMonitor mMonitor; bool mShutdown MOZ_GUARDED_BY(mMonitor); bool mWaiting MOZ_GUARDED_BY(mMonitor); bool mNotified MOZ_GUARDED_BY(mMonitor); bool mSleeping MOZ_GUARDED_BY(mMonitor); struct EntryKey { explicit EntryKey(nsTimerImpl& aTimerImpl) : mTimeout(aTimerImpl.mTimeout), mTimerSeq(aTimerImpl.mTimerSeq) {} // The comparison operators must ensure to detect equality only for // equal mTimerImpl except for canceled timers. // This is achieved through the sequence number. // Currently we maintain a FIFO order for timers with equal timeout. // Note that it might make sense to flip the sequence order to favor // timeouts with smaller delay as they are most likely more sensitive // to jitter. But we strictly test for FIFO order in our gtests. bool operator==(const EntryKey& aRhs) const { return (mTimeout == aRhs.mTimeout && mTimerSeq == aRhs.mTimerSeq); } bool operator<(const EntryKey& aRhs) const { if (mTimeout == aRhs.mTimeout) { return mTimerSeq < aRhs.mTimerSeq; } return mTimeout < aRhs.mTimeout; } TimeStamp mTimeout; uint64_t mTimerSeq; }; struct Entry final : EntryKey { explicit Entry(nsTimerImpl& aTimerImpl) : EntryKey(aTimerImpl), mDelay(aTimerImpl.mDelay), mTimerImpl(&aTimerImpl) {} // No copies to not fiddle with mTimerImpl's ref-count. Entry(const Entry&) = delete; Entry& operator=(const Entry&) = delete; Entry(Entry&&) = default; Entry& operator=(Entry&&) = default; #ifdef DEBUG // While the timer is stored in the thread's list, the timeout is // immutable, so it should be OK to read without holding the mutex. // We only allow this in debug builds. bool IsTimerInThreadAndUnchanged() MOZ_NO_THREAD_SAFETY_ANALYSIS { return (mTimerImpl && mTimerImpl->IsInTimerThread() && mTimerImpl->mTimeout == mTimeout); } #endif TimeDuration mDelay; RefPtr mTimerImpl; }; void PostTimerEvent(Entry& aPostMe) MOZ_REQUIRES(mMonitor); // WakeupTime encompasses both a point in time at which we should wake up and // tolerance for how much that wake-up can be delayed. struct WakeupTime { const TimeStamp mWakeupTime; const TimeDuration mDelayTolerance; }; // Computes and returns when we should next try to wake up in order to handle // the triggering of the timers in mTimers. // If mTimers is empty, returns a null TimeStamp. If mTimers is not empty, // returns the timeout of the last timer that can be bundled with the first // timer in mTimers along with a tolerance indicating the most that we can be // delayed and not violate the tolerances of any of the timers in the bundle. WakeupTime ComputeWakeupTimeFromTimers() const MOZ_REQUIRES(mMonitor); // Computes how late a timer can acceptably fire. // timerDuration is the duration of the timer whose delay we are calculating. // Longer timers can tolerate longer firing delays. // minDelay is an amount by which any timer can be delayed. // This function will never return a value smaller than minDelay (unless this // conflicts with maxDelay). maxDelay is the upper limit on the amount by // which we will ever delay any timer. Takes precedence over minDelay if there // is a conflict. (Zero will effectively disable timer coalescing.) TimeDuration ComputeAcceptableFiringDelay(TimeDuration timerDuration, TimeDuration minDelay, TimeDuration maxDelay) const; // Fires and removes all timers in mTimers that are "due" to be fired, // according to the current time and the passed-in early firing tolerance. // Return value is the number of timers that were fired by the operation. uint64_t FireDueTimers(TimeDuration aAllowedEarlyFiring) MOZ_REQUIRES(mMonitor); // Suspends thread execution using mMonitor.Wait(waitFor). Also sets and // clears a few flags before and after. void Wait(TimeDuration aWaitFor, TimeDuration aTolerance) MOZ_REQUIRES(mMonitor); // mTimers is sorted by timeout, followed by a unique sequence number. // Some entries are for cancelled entries, but remain in sorted order based // on the timeout and sequence number they were originally created with. nsTArray mTimers MOZ_GUARDED_BY(mMonitor); // Set only at the start of the thread's Run(): uint32_t mAllowedEarlyFiringMicroseconds MOZ_GUARDED_BY(mMonitor); ProfilerThreadId mProfilerThreadId MOZ_GUARDED_BY(mMonitor); // Time at which we were intending to wake up the last time that we slept. // Is "null" if we have never slept or if our last sleep was "forever". TimeStamp mIntendedWakeupTime; #if TIMER_THREAD_STATISTICS static constexpr size_t sTimersFiredPerWakeupBucketCount = 16; static inline constexpr std::array sTimersFiredPerWakeupThresholds = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 20, 30, 40, 50, 70, (size_t)(-1)}; mutable AutoTArray mTimersFiredPerWakeup MOZ_GUARDED_BY(mMonitor) = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; mutable AutoTArray mTimersFiredPerUnnotifiedWakeup MOZ_GUARDED_BY(mMonitor) = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; mutable AutoTArray mTimersFiredPerNotifiedWakeup MOZ_GUARDED_BY(mMonitor) = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; mutable size_t mTotalTimersAdded MOZ_GUARDED_BY(mMonitor) = 0; mutable size_t mTotalTimersRemoved MOZ_GUARDED_BY(mMonitor) = 0; mutable size_t mTotalTimersFiredNotified MOZ_GUARDED_BY(mMonitor) = 0; mutable size_t mTotalTimersFiredUnnotified MOZ_GUARDED_BY(mMonitor) = 0; mutable size_t mTotalWakeupCount MOZ_GUARDED_BY(mMonitor) = 0; mutable size_t mTotalUnnotifiedWakeupCount MOZ_GUARDED_BY(mMonitor) = 0; mutable size_t mTotalNotifiedWakeupCount MOZ_GUARDED_BY(mMonitor) = 0; mutable double mTotalActualTimerFiringDelayNotified MOZ_GUARDED_BY(mMonitor) = 0.0; mutable double mTotalActualTimerFiringDelayUnnotified MOZ_GUARDED_BY(mMonitor) = 0.0; mutable TimeStamp mFirstTimerAdded MOZ_GUARDED_BY(mMonitor); mutable size_t mEarlyWakeups MOZ_GUARDED_BY(mMonitor) = 0; mutable double mTotalEarlyWakeupTime MOZ_GUARDED_BY(mMonitor) = 0.0; void CollectTimersFiredStatistics(uint64_t timersFiredThisWakeup); void CollectWakeupStatistics(); void PrintStatistics() const; #endif }; #endif /* TimerThread_h_ */