diff --git a/modules/juce_core/juce_core.cpp b/modules/juce_core/juce_core.cpp index a61a55d0d2..d7f5e4911b 100644 --- a/modules/juce_core/juce_core.cpp +++ b/modules/juce_core/juce_core.cpp @@ -226,6 +226,7 @@ #include "native/juce_Registry_windows.cpp" #include "native/juce_SystemStats_windows.cpp" #include "native/juce_Threads_windows.cpp" + #include "native/juce_PlatformTimer_generic.cpp" #include "native/juce_PlatformTimer_windows.cpp" //============================================================================== diff --git a/modules/juce_core/native/juce_PlatformTimer_generic.cpp b/modules/juce_core/native/juce_PlatformTimer_generic.cpp index 3f6ea3d2fe..0a37ce88bd 100644 --- a/modules/juce_core/native/juce_PlatformTimer_generic.cpp +++ b/modules/juce_core/native/juce_PlatformTimer_generic.cpp @@ -35,17 +35,21 @@ namespace juce { -class PlatformTimer final : private Thread +class GenericPlatformTimer final : private Thread { public: - explicit PlatformTimer (PlatformTimerListener& ptl) + explicit GenericPlatformTimer (PlatformTimerListener& ptl) : Thread { "HighResolutionTimerThread" }, listener { ptl } { - startThread (Priority::highest); + if (startThread (Priority::highest)) + return; + + // This likely suggests there are too many threads running! + jassertfalse; } - ~PlatformTimer() + ~GenericPlatformTimer() override { stopThread (-1); } @@ -154,8 +158,12 @@ private: mutable std::mutex runCopyMutex; std::shared_ptr timer; - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PlatformTimer) - JUCE_DECLARE_NON_MOVEABLE (PlatformTimer) + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GenericPlatformTimer) + JUCE_DECLARE_NON_MOVEABLE (GenericPlatformTimer) }; +#if ! JUCE_WINDOWS +using PlatformTimer = GenericPlatformTimer; +#endif + } // namespace juce diff --git a/modules/juce_core/native/juce_PlatformTimer_windows.cpp b/modules/juce_core/native/juce_PlatformTimer_windows.cpp index 2b0b34c127..7b437db086 100644 --- a/modules/juce_core/native/juce_PlatformTimer_windows.cpp +++ b/modules/juce_core/native/juce_PlatformTimer_windows.cpp @@ -51,27 +51,59 @@ public: }; timerId = timeSetEvent ((UINT) newIntervalMs, 1, callback, (DWORD_PTR) &listener, TIME_PERIODIC | TIME_CALLBACK_FUNCTION); - intervalMs = timerId != 0 ? newIntervalMs : 0; + const auto timerStarted = timerId != 0; + + if (timerStarted) + { + intervalMs = newIntervalMs; + return; + } + + if (fallbackTimer == nullptr) + { + // This assertion indicates that the creation of a high-resolution timer + // failed, and the timer is falling back to a less accurate implementation. + // Timer callbacks will still fire but the timing precision of the callbacks + // will be significantly compromised! + // The most likely reason for this is that more than the system limit of 16 + // HighResolutionTimers are trying to run simultaneously in the same process. + // You may be able to reduce the number of HighResolutionTimer instances by + // only creating one instance that is shared (see SharedResourcePointer). + // + // However, if this is a plugin running inside a host, other plugins could + // be creating timers in the same process. In most cases it's best to find + // an alternative approach than relying on the precision of any timer! + #if ! JUCE_UNIT_TESTS + jassertfalse; + #endif + + fallbackTimer = std::make_unique (listener); + } + + fallbackTimer->startTimer (newIntervalMs); + intervalMs = fallbackTimer->getIntervalMs(); } void cancelTimer() { - jassert (timerId != 0); + if (timerId != 0) + timeKillEvent (timerId); + else if (fallbackTimer != nullptr) + fallbackTimer->cancelTimer(); + else + jassertfalse; - timeKillEvent (timerId); timerId = 0; intervalMs = 0; } - int getIntervalMs() const - { - return intervalMs; - } + int getIntervalMs() const { return intervalMs; } private: PlatformTimerListener& listener; UINT timerId { 0 }; int intervalMs { 0 }; + std::unique_ptr fallbackTimer; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PlatformTimer) JUCE_DECLARE_NON_MOVEABLE (PlatformTimer) diff --git a/modules/juce_core/threads/juce_HighResolutionTimer.cpp b/modules/juce_core/threads/juce_HighResolutionTimer.cpp index 6845bbdabd..d9b755fafa 100644 --- a/modules/juce_core/threads/juce_HighResolutionTimer.cpp +++ b/modules/juce_core/threads/juce_HighResolutionTimer.cpp @@ -158,9 +158,30 @@ public: void runTest() override { - constexpr int maximumTimeoutMs {30'000}; + runBehaviourTestsWithBackgroundThreads<0>(); + runBehaviourTestsWithBackgroundThreads<16>(); + runStressTests(); + } - beginTest ("Start/stop a timer"); + template + void runBehaviourTestsWithBackgroundThreads() + { + constexpr int maximumTimeoutMs { 30'000 }; + + const auto beginBehaviourTest = [&] (const auto& testName) + { + beginTest (String (testName) + " (" + String (NumBackgroundThreads) + " background timers)"); + }; + + [[maybe_unused]] std::array backgroundTimers; + + beginBehaviourTest ("Background timer preconditions"); + { + for (auto& t : backgroundTimers) + expect (t.isTimerRunning()); + } + + beginBehaviourTest ("Start/stop a timer"); { WaitableEvent timerFiredOnce; WaitableEvent timerFiredTwice; @@ -189,7 +210,7 @@ public: expect (timer.getTimerInterval() == 0); } - beginTest ("Stop a timer from the timer callback"); + beginBehaviourTest ("Stop a timer from the timer callback"); { WaitableEvent stoppedTimer; @@ -206,7 +227,7 @@ public: expect (stoppedTimer.wait (maximumTimeoutMs)); } - beginTest ("Restart a timer from the timer callback"); + beginBehaviourTest ("Restart a timer from the timer callback"); { WaitableEvent restartTimer; WaitableEvent timerRestarted; @@ -246,7 +267,7 @@ public: timer.stopTimer(); } - beginTest ("Calling stopTimer on a timer, waits for any timer callbacks to finish"); + beginBehaviourTest ("Calling stopTimer on a timer, waits for any timer callbacks to finish"); { WaitableEvent timerCallbackStarted; WaitableEvent stoppingTimer; @@ -276,13 +297,13 @@ public: expect (timerCallbackFinished); } - beginTest ("Calling stopTimer on a timer, waits for any timer callbacks to finish, even if the timer callback calls stopTimer first"); + beginBehaviourTest ("Calling stopTimer on a timer, waits for any timer callbacks to finish, even if the timer callback calls stopTimer first"); { WaitableEvent stoppedFromInsideTimerCallback; WaitableEvent stoppingFromOutsideTimerCallback; std::atomic timerCallbackFinished { false }; - Timer timer {[&]() + Timer timer {[&] { timer.stopTimer(); stoppedFromInsideTimerCallback.signal(); @@ -300,7 +321,7 @@ public: expect (timerCallbackFinished); } - beginTest ("Adjusting a timer period from outside the timer callback doesn't cause data races"); + beginBehaviourTest ("Adjusting a timer period from outside the timer callback doesn't cause data races"); { WaitableEvent timerCallbackStarted; WaitableEvent timerRestarted; @@ -343,7 +364,7 @@ public: expect (lastCallbackCount == 2); } - beginTest ("A timer can be restarted externally, after being stopped internally"); + beginBehaviourTest ("A timer can be restarted externally, after being stopped internally"); { WaitableEvent timerStopped; WaitableEvent timerFiredAfterRestart; @@ -378,7 +399,7 @@ public: expect (timerFiredAfterRestart.wait (maximumTimeoutMs)); } - beginTest ("Calls to `startTimer` and `getTimerInterval` succeed while a callback is blocked"); + beginBehaviourTest ("Calls to `startTimer` and `getTimerInterval` succeed while a callback is blocked"); { WaitableEvent timerBlocked; WaitableEvent unblockTimer; @@ -400,32 +421,33 @@ public: unblockTimer.signal(); timer.stopTimer(); } + } + void runStressTests() + { beginTest ("Stress test"); { - constexpr auto maxNumTimers { 100 }; + std::vector timers (100); - std::vector> timers; - timers.reserve (maxNumTimers); - - for (int i = 0; i < maxNumTimers; ++i) + for (auto& timer : timers) { - auto timer = std::make_unique ([]{}); - timer->startTimer (1); - - if (! timer->isTimerRunning()) - break; - - timers.push_back (std::move (timer)); + timer.startTimer (1); + expect (timer.isTimerRunning()); } - expect (timers.size() >= 16); + for (auto& timer : timers) + { + timer.stopTimer(); + expect (! timer.isTimerRunning()); + } } } class Timer final : public HighResolutionTimer { public: + Timer() = default; + explicit Timer (std::function fn) : callback (std::move (fn)) {} @@ -434,7 +456,17 @@ public: void hiResTimerCallback() override { callback(); } private: - std::function callback; + std::function callback = []{}; + }; + + class BackgroundTimer + { + public: + BackgroundTimer() { timer.startTimer (1); } + bool isTimerRunning() const { return timer.isTimerRunning(); } + + private: + Timer timer { []{} }; }; }; diff --git a/modules/juce_core/threads/juce_HighResolutionTimer.h b/modules/juce_core/threads/juce_HighResolutionTimer.h index ac8f44c9bb..294fc55cf1 100644 --- a/modules/juce_core/threads/juce_HighResolutionTimer.h +++ b/modules/juce_core/threads/juce_HighResolutionTimer.h @@ -83,13 +83,23 @@ public: //============================================================================== /** Starts the timer and sets the length of interval required. - If the timer has already started, this will reset the timer, so the - time between calling this method and the next timer callback - will not be less than the interval length passed in. + If the timer has already started, this will reset the timer, so the time + between calling this method and the next timer callback will not be less + than the interval length passed in. In exceptional circumstances the dedicated timer thread may not start, - if this is a potential concern for your use case, you can call isTimerRunning() - to confirm if the timer actually started. + if this is a potential concern for your use case, you can call + isTimerRunning() to confirm if the timer actually started. + + On Windows the underlying API only allows 16 high-resolution timers to + run simultaneously in the same process. A fallback timer will be used + when this limit is exceeded but the precision may be significantly + compromised. This is a particular concern for plugins, because other + plugins in the same host process may already have timers running. + To avoid issues, try to use the minimum number of HighResolutionTimers + possible. For example, consider using a SharedResourcePointer so that + all instances of the same plugin running in the same same process can + share a single HighResolutionTimer instance. @param intervalInMilliseconds the interval to use (a value of zero or less will stop the timer) */ @@ -97,12 +107,14 @@ public: /** Stops the timer. - This method may block while it waits for pending callbacks to complete. Once it - returns, no more callbacks will be made. If it is called from the timer's own thread, - it will cancel the timer after the current callback returns. + This method may block while it waits for pending callbacks to complete. + Once it returns, no more callbacks will be made. If it is called from + the timer's own thread, it will cancel the timer after the current + callback returns. - To prevent data races it's normally best practice to call this in the derived classes - destructor, even if stopTimer() was called in the hiResTimerCallback(). + To prevent data races it's normally best practice to call this in the + derived classes destructor, even if stopTimer() was called in the + hiResTimerCallback(). */ void stopTimer();