From 407720b557d6ad8038dcd941023b241d14becb5b Mon Sep 17 00:00:00 2001 From: Anthony Nicholls Date: Fri, 14 Jul 2023 14:01:26 +0100 Subject: [PATCH] Thread: Fix realtime threads on macOS - macOS behaviour of setRealtime now matches other platforms MR feedback --- BREAKING-CHANGES.txt | 23 ++++ .../juce_core/native/juce_SharedCode_posix.h | 4 +- modules/juce_core/native/juce_Threads_mac.mm | 109 ++++++++++++----- modules/juce_core/threads/juce_Thread.cpp | 4 +- modules/juce_core/threads/juce_Thread.h | 114 ++++++++++++++++-- 5 files changed, 212 insertions(+), 42 deletions(-) diff --git a/BREAKING-CHANGES.txt b/BREAKING-CHANGES.txt index 932f5ad1ff..805b2e387e 100644 --- a/BREAKING-CHANGES.txt +++ b/BREAKING-CHANGES.txt @@ -4,6 +4,29 @@ JUCE breaking changes develop ======= +Change +------ +RealtimeOptions member workDurationMs was replaced by three optional member +variables in RealtimeOptions, and all RealtimeOptions member variables were +marked private. + +Possible Issues +--------------- +Trying to construct a RealtimeOptions object with one or two values, or access +any of its member variables, will no longer compile. + +Workaround +---------- +Use the withMember functions to construct the object, and the getter functions +to access the member variable values. + +Rationale +--------- +The new approach improves the flexibility for users to specify realtime thread +options on macOS/iOS and improves the flexibility for the API to evolve without +introducing further breaking changes. + + Change ------ JUCE module compilation files with a platform suffix are now checked case diff --git a/modules/juce_core/native/juce_SharedCode_posix.h b/modules/juce_core/native/juce_SharedCode_posix.h index df0ba126af..03313eaa27 100644 --- a/modules/juce_core/native/juce_SharedCode_posix.h +++ b/modules/juce_core/native/juce_SharedCode_posix.h @@ -894,7 +894,7 @@ public: const auto min = jmax (0, sched_get_priority_min (SCHED_RR)); const auto max = jmax (1, sched_get_priority_max (SCHED_RR)); - return jmap (rt->priority, 0, 10, min, max); + return jmap (rt->getPriority(), 0, 10, min, max); } // We only use this helper if we're on an old macos/ios platform that might @@ -959,7 +959,7 @@ private: int priority; }; -static void* makeThreadHandle (PosixThreadAttribute& attr, Thread* userData, void* (*threadEntryProc) (void*)) +static void* makeThreadHandle (PosixThreadAttribute& attr, void* userData, void* (*threadEntryProc) (void*)) { pthread_t handle = {}; diff --git a/modules/juce_core/native/juce_Threads_mac.mm b/modules/juce_core/native/juce_Threads_mac.mm index 7c692be768..1353a14e26 100644 --- a/modules/juce_core/native/juce_Threads_mac.mm +++ b/modules/juce_core/native/juce_Threads_mac.mm @@ -64,28 +64,99 @@ static auto getJucePriority (qos_class_t qos) return Thread::Priority::normal; } +template +static std::optional firstOptionalWithValue (const std::initializer_list>& optionals) +{ + for (const auto& optional : optionals) + if (optional.has_value()) + return optional; + + return {}; +} + +static bool tryToUpgradeCurrentThreadToRealtime (const Thread::RealtimeOptions& options) +{ + const auto periodMs = options.getPeriodMs().value_or (0.0); + + const auto processingTimeMs = firstOptionalWithValue ( + { + options.getProcessingTimeMs(), + options.getMaximumProcessingTimeMs(), + options.getPeriodMs() + }).value_or (10.0); + + const auto maxProcessingTimeMs = options.getMaximumProcessingTimeMs() + .value_or (processingTimeMs); + + // The processing time can not exceed the maximum processing time! + jassert (maxProcessingTimeMs >= processingTimeMs); + + thread_time_constraint_policy_data_t policy; + policy.period = (uint32_t) Time::secondsToHighResolutionTicks (periodMs / 1'000.0); + policy.computation = (uint32_t) Time::secondsToHighResolutionTicks (processingTimeMs / 1'000.0); + policy.constraint = (uint32_t) Time::secondsToHighResolutionTicks (maxProcessingTimeMs / 1'000.0); + policy.preemptible = true; + + const auto result = thread_policy_set (pthread_mach_thread_np (pthread_self()), + THREAD_TIME_CONSTRAINT_POLICY, + (thread_policy_t) &policy, + THREAD_TIME_CONSTRAINT_POLICY_COUNT); + + if (result == KERN_SUCCESS) + return true; + + // testing has shown that passing a computation value > 50ms can + // lead to thread_policy_set returning an error indicating that an + // invalid argument was passed. If that happens this code tries to + // limit that value in the hope of resolving the issue. + + if (result == KERN_INVALID_ARGUMENT && options.getProcessingTimeMs() > 50.0) + return tryToUpgradeCurrentThreadToRealtime (options.withProcessingTimeMs (50.0)); + + return false; +} + bool Thread::createNativeThread (Priority priority) { - PosixThreadAttribute attr { threadStackSize }; + PosixThreadAttribute attribute { threadStackSize }; if (@available (macos 10.10, *)) - pthread_attr_set_qos_class_np (attr.get(), getNativeQOS (priority), 0); + pthread_attr_set_qos_class_np (attribute.get(), getNativeQOS (priority), 0); else - PosixSchedulerPriority::getNativeSchedulerAndPriority (realtimeOptions, priority).apply (attr); + PosixSchedulerPriority::getNativeSchedulerAndPriority (realtimeOptions, priority).apply (attribute); + + struct ThreadData + { + Thread& thread; + std::promise started; + }; + + ThreadData threadData { *this }; - threadId = threadHandle = makeThreadHandle (attr, this, [] (void* userData) -> void* + threadId = threadHandle = makeThreadHandle (attribute, &threadData, [] (void* userData) -> void* { - auto* myself = static_cast (userData); + auto& data { *static_cast (userData) }; + auto& thread = data.thread; + + if (thread.isRealtime() + && ! tryToUpgradeCurrentThreadToRealtime (*thread.realtimeOptions)) + { + data.started.set_value (false); + return nullptr; + } + + data.started.set_value (true); JUCE_AUTORELEASEPOOL { - juce_threadEntryPoint (myself); + juce_threadEntryPoint (&thread); } return nullptr; }); - return threadId != nullptr; + return threadId != nullptr + && threadData.started.get_future().get(); } void Thread::killThread() @@ -122,30 +193,6 @@ bool Thread::setPriority (Priority priority) { jassert (Thread::getCurrentThreadId() == getThreadId()); - if (isRealtime()) - { - // macOS/iOS needs to know how much time you need! - jassert (realtimeOptions->workDurationMs > 0); - - mach_timebase_info_data_t timebase; - mach_timebase_info (&timebase); - - const auto periodMs = realtimeOptions->workDurationMs; - const auto ticksPerMs = ((double) timebase.denom * 1000000.0) / (double) timebase.numer; - const auto periodTicks = (uint32_t) jmin ((double) std::numeric_limits::max(), periodMs * ticksPerMs); - - thread_time_constraint_policy_data_t policy; - policy.period = periodTicks; - policy.computation = jmin ((uint32_t) 50000, policy.period); - policy.constraint = policy.period; - policy.preemptible = true; - - return thread_policy_set (pthread_mach_thread_np (pthread_self()), - THREAD_TIME_CONSTRAINT_POLICY, - (thread_policy_t) &policy, - THREAD_TIME_CONSTRAINT_POLICY_COUNT) == KERN_SUCCESS; - } - if (@available (macOS 10.10, *)) return pthread_set_qos_class_self_np (getNativeQOS (priority), 0) == 0; diff --git a/modules/juce_core/threads/juce_Thread.cpp b/modules/juce_core/threads/juce_Thread.cpp index cae9308695..759b613753 100644 --- a/modules/juce_core/threads/juce_Thread.cpp +++ b/modules/juce_core/threads/juce_Thread.cpp @@ -166,7 +166,7 @@ bool Thread::startRealtimeThread (const RealtimeOptions& options) if (threadHandle == nullptr) { - realtimeOptions = makeOptional (options); + realtimeOptions = std::make_optional (options); if (startThreadInternal (Priority::normal)) return true; @@ -276,7 +276,7 @@ void Thread::removeListener (Listener* listener) bool Thread::isRealtime() const { - return realtimeOptions.hasValue(); + return realtimeOptions.has_value(); } void Thread::setAffinityMask (const uint32 newAffinityMask) diff --git a/modules/juce_core/threads/juce_Thread.h b/modules/juce_core/threads/juce_Thread.h index 03a3ad97cc..65e2f0c5ec 100644 --- a/modules/juce_core/threads/juce_Thread.h +++ b/modules/juce_core/threads/juce_Thread.h @@ -75,14 +75,114 @@ public: */ struct RealtimeOptions { - /** Linux only: A value with a range of 0-10, where 10 is the highest priority. */ - int priority = 5; + /** A value with a range of 0-10, where 10 is the highest priority. - /** iOS/macOS only: A millisecond value representing the estimated time between each - 'Thread::run' call. Your thread may be penalised if you frequently - overrun this. + Currently only used by Posix platforms. + + @see getPriority + */ + [[nodiscard]] RealtimeOptions withPriority (int newPriority) const + { + jassert (isPositiveAndNotGreaterThan (newPriority, 10)); + return withMember (*this, &RealtimeOptions::priority, juce::jlimit (newPriority, 0, 10)); + } + + /** Specify the expected amount of processing time required each time the thread wakes up. + + Only used by macOS/iOS. + + @see getProcessingTimeMs, withMaximumProcessingTimeMs, withPeriodMs, withPeriodHz + */ + [[nodiscard]] RealtimeOptions withProcessingTimeMs (double newProcessingTimeMs) const + { + jassert (newProcessingTimeMs > 0.0); + return withMember (*this, &RealtimeOptions::processingTimeMs, newProcessingTimeMs); + } + + /** Specify the maximum amount of processing time required each time the thread wakes up. + + Only used by macOS/iOS. + + @see getMaximumProcessingTimeMs, withProcessingTimeMs, withPeriodMs, withPeriodHz + */ + [[nodiscard]] RealtimeOptions withMaximumProcessingTimeMs (double newMaximumProcessingTimeMs) const + { + jassert (newMaximumProcessingTimeMs > 0.0); + return withMember (*this, &RealtimeOptions::maximumProcessingTimeMs, newMaximumProcessingTimeMs); + } + + /** Specify the approximate amount of time between each thread wake up. + + Alternatively call withPeriodHz(). + + Only used by macOS/iOS. + + @see getPeriodMs, withPeriodHz, withProcessingTimeMs, withMaximumProcessingTimeMs, + */ + [[nodiscard]] RealtimeOptions withPeriodMs (double newPeriodMs) const + { + jassert (newPeriodMs > 0.0); + return withMember (*this, &RealtimeOptions::periodMs, newPeriodMs); + } + + /** Specify the approximate frequency at which the thread will be woken up. + + Alternatively call withPeriodMs(). + + Only used by macOS/iOS. + + @see getPeriodHz, withPeriodMs, withProcessingTimeMs, withMaximumProcessingTimeMs, + */ + [[nodiscard]] RealtimeOptions withPeriodHz (double newPeriodHz) const + { + jassert (newPeriodHz > 0.0); + return withPeriodMs (1'000.0 / newPeriodHz); + } + + /** Returns a value with a range of 0-10, where 10 is the highest priority. + + @see withPriority */ - uint32_t workDurationMs = 0; + [[nodiscard]] int getPriority() const + { + return priority; + } + + /** Returns the expected amount of processing time required each time the thread + wakes up. + + @see withProcessingTimeMs, getMaximumProcessingTimeMs, getPeriodMs + */ + [[nodiscard]] std::optional getProcessingTimeMs() const + { + return processingTimeMs; + } + + /** Returns the maximum amount of processing time required each time the thread + wakes up. + + @see withMaximumProcessingTimeMs, getProcessingTimeMs, getPeriodMs + */ + [[nodiscard]] std::optional getMaximumProcessingTimeMs() const + { + return maximumProcessingTimeMs; + } + + /** Returns the approximate amount of time between each thread wake up, or + nullopt if there is no inherent periodicity. + + @see withPeriodMs, withPeriodHz, getProcessingTimeMs, getMaximumProcessingTimeMs + */ + [[nodiscard]] std::optional getPeriodMs() const + { + return periodMs; + } + + private: + int priority { 5 }; + std::optional processingTimeMs; + std::optional maximumProcessingTimeMs; + std::optional periodMs{}; }; //============================================================================== @@ -464,7 +564,7 @@ private: const String threadName; std::atomic threadHandle { nullptr }; std::atomic threadId { nullptr }; - Optional realtimeOptions = {}; + std::optional realtimeOptions = {}; CriticalSection startStopLock; WaitableEvent startSuspensionEvent, defaultEvent; size_t threadStackSize;