1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00

HighResolutionTimer: Add fallback implementation for Windows

This commit is contained in:
Anthony Nicholls 2024-10-08 15:31:54 +01:00
parent 57aa8f07a6
commit b78ff3bf7e
5 changed files with 132 additions and 47 deletions

View file

@ -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"
//==============================================================================

View file

@ -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> 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

View file

@ -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<GenericPlatformTimer> (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<GenericPlatformTimer> fallbackTimer;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PlatformTimer)
JUCE_DECLARE_NON_MOVEABLE (PlatformTimer)

View file

@ -158,9 +158,30 @@ public:
void runTest() override
{
constexpr int maximumTimeoutMs {30'000};
runBehaviourTestsWithBackgroundThreads<0>();
runBehaviourTestsWithBackgroundThreads<16>();
runStressTests();
}
beginTest ("Start/stop a timer");
template <size_t NumBackgroundThreads>
void runBehaviourTestsWithBackgroundThreads()
{
constexpr int maximumTimeoutMs { 30'000 };
const auto beginBehaviourTest = [&] (const auto& testName)
{
beginTest (String (testName) + " (" + String (NumBackgroundThreads) + " background timers)");
};
[[maybe_unused]] std::array<BackgroundTimer, NumBackgroundThreads> 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<bool> 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<Timer> timers (100);
std::vector<std::unique_ptr<Timer>> timers;
timers.reserve (maxNumTimers);
for (int i = 0; i < maxNumTimers; ++i)
for (auto& timer : timers)
{
auto timer = std::make_unique<Timer> ([]{});
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<void()> fn)
: callback (std::move (fn)) {}
@ -434,7 +456,17 @@ public:
void hiResTimerCallback() override { callback(); }
private:
std::function<void()> callback;
std::function<void()> callback = []{};
};
class BackgroundTimer
{
public:
BackgroundTimer() { timer.startTimer (1); }
bool isTimerRunning() const { return timer.isTimerRunning(); }
private:
Timer timer { []{} };
};
};

View file

@ -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();