- macOS behaviour of setRealtime now matches other platforms MR feedbackv7.0.9
| @@ -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 | |||
| @@ -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 = {}; | |||
| @@ -64,28 +64,99 @@ static auto getJucePriority (qos_class_t qos) | |||
| return Thread::Priority::normal; | |||
| } | |||
| template<typename Type> | |||
| static std::optional<Type> firstOptionalWithValue (const std::initializer_list<std::optional<Type>>& 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<bool> started; | |||
| }; | |||
| ThreadData threadData { *this }; | |||
| threadId = threadHandle = makeThreadHandle (attr, this, [] (void* userData) -> void* | |||
| threadId = threadHandle = makeThreadHandle (attribute, &threadData, [] (void* userData) -> void* | |||
| { | |||
| auto* myself = static_cast<Thread*> (userData); | |||
| auto& data { *static_cast<ThreadData*> (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<uint32_t>::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; | |||
| @@ -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) | |||
| @@ -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<double> 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<double> 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<double> getPeriodMs() const | |||
| { | |||
| return periodMs; | |||
| } | |||
| private: | |||
| int priority { 5 }; | |||
| std::optional<double> processingTimeMs; | |||
| std::optional<double> maximumProcessingTimeMs; | |||
| std::optional<double> periodMs{}; | |||
| }; | |||
| //============================================================================== | |||
| @@ -464,7 +564,7 @@ private: | |||
| const String threadName; | |||
| std::atomic<void*> threadHandle { nullptr }; | |||
| std::atomic<ThreadID> threadId { nullptr }; | |||
| Optional<RealtimeOptions> realtimeOptions = {}; | |||
| std::optional<RealtimeOptions> realtimeOptions = {}; | |||
| CriticalSection startStopLock; | |||
| WaitableEvent startSuspensionEvent, defaultEvent; | |||
| size_t threadStackSize; | |||