From 6013f82c94c12596670cf03efe451cdd3d5713be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Nordheim?= Date: Tue, 19 Sep 2023 20:13:27 +0200 Subject: [PATCH] Add thread Quality of Service API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With heterogenous CPU topology (and earlier, per-core clocks) OS developers have increasingly added "Quality of Service" APIs to thread scheduling, giving users a way to tell the OS what type of work the thread is expected to do. In the case of Apple they have a more extensive selection of service levels. Unlike what we currently see from Windows, the API there is actually much broader than just High, 'Default' and Eco, and covers more concrete uses like "Utility", "Background", "User-initiated", defined as -1 (Default), and a range from 0x09 (Background) to 0x21 (User-interactive)[0]. Currently there is no equivalent API for Linux though there is some push from various interested parties to add one (e.g. [1]). As mentioned, on Windows there is really only 3 levels, though it's defined as "do/don't throttle this thread" and "do as you wish". For Android I cannot really find anything equivalent beyond the thread priority. Discussed here: https://lists.qt-project.org/pipermail/development/2024-September/045694.html [0] https://developer.apple.com/documentation/foundation/qualityofservice [1] https://www.youtube.com/watch?v=RfgPWpTwTQo (Linux Plumbers Conference) Fixes: QTBUG-93946 Change-Id: Iabeaa7b61cec0bebd5c6a4bcf75a8e60dc0348dc Reviewed-by: Tor Arne Vestbø --- src/corelib/thread/qthread.cpp | 40 +++++++++++++++ src/corelib/thread/qthread.h | 10 ++++ src/corelib/thread/qthread_p.h | 6 +++ src/corelib/thread/qthread_unix.cpp | 44 ++++++++++++++++ src/corelib/thread/qthread_win.cpp | 50 +++++++++++++++++++ .../corelib/thread/qthread/tst_qthread.cpp | 30 +++++++++++ 6 files changed, 180 insertions(+) diff --git a/src/corelib/thread/qthread.cpp b/src/corelib/thread/qthread.cpp index 9d42c5dc02a..4fb9caae1f0 100644 --- a/src/corelib/thread/qthread.cpp +++ b/src/corelib/thread/qthread.cpp @@ -560,6 +560,46 @@ uint QThread::stackSize() const return d->stackSize; } +/*! + \since 6.9 + + Set the Quality of Service level of the thread object to \a serviceLevel. + This can only be called from the thread itself or before the thread is + started! + + This is currently only implemented on Apple platforms, and Windows. + The function call will complete successfully on other platforms but will + not currently have any effect. + + \sa serviceLevel() +*/ +void QThread::setServiceLevel(QualityOfService serviceLevel) +{ + Q_D(QThread); + QMutexLocker locker(&d->mutex); + if (d->threadState != QThreadPrivate::Running) { + d->serviceLevel = serviceLevel; + } else { + Q_ASSERT_X(isCurrentThread(), "QThread::setServiceLevel", + "cannot change quality of service level of a separate, running, thread"); + d->setQualityOfServiceLevel(serviceLevel); + } +} + +/*! + \since 6.9 + + Return the current Quality of Service level of the thread. + + \sa setServiceLevel() +*/ +QThread::QualityOfService QThread::serviceLevel() const +{ + Q_D(const QThread); + QMutexLocker locker(&d->mutex); + return d->serviceLevel; +} + /*! \internal Transitions BindingStatusOrList to the binding status state. If we had a list of diff --git a/src/corelib/thread/qthread.h b/src/corelib/thread/qthread.h index ae060ecf1cf..eea2354ef9e 100644 --- a/src/corelib/thread/qthread.h +++ b/src/corelib/thread/qthread.h @@ -51,6 +51,13 @@ public: InheritPriority }; + enum class QualityOfService { + Auto, + High, + Eco, + }; + Q_ENUM(QualityOfService) + void setPriority(Priority priority); Priority priority() const; @@ -71,6 +78,9 @@ public: bool isCurrentThread() const noexcept; + void setServiceLevel(QualityOfService serviceLevel); + QualityOfService serviceLevel() const; + template [[nodiscard]] static QThread *create(Function &&f, Args &&... args); diff --git a/src/corelib/thread/qthread_p.h b/src/corelib/thread/qthread_p.h index ada010dfefe..f7ee8445711 100644 --- a/src/corelib/thread/qthread_p.h +++ b/src/corelib/thread/qthread_p.h @@ -206,6 +206,12 @@ public: bool wait(QMutexLocker &locker, QDeadlineTimer deadline); + QThread::QualityOfService serviceLevel = QThread::QualityOfService::Auto; + void setQualityOfServiceLevel(QThread::QualityOfService qosLevel); +#ifdef Q_OS_DARWIN + qos_class_t nativeQualityOfServiceClass() const; +#endif + #ifdef Q_OS_UNIX QWaitCondition thread_done; diff --git a/src/corelib/thread/qthread_unix.cpp b/src/corelib/thread/qthread_unix.cpp index c2b8863f044..301543efa03 100644 --- a/src/corelib/thread/qthread_unix.cpp +++ b/src/corelib/thread/qthread_unix.cpp @@ -62,6 +62,9 @@ QT_BEGIN_NAMESPACE +[[maybe_unused]] +Q_STATIC_LOGGING_CATEGORY(lcQThread, "qt.core.thread", QtWarningMsg) + using namespace QtMiscUtils; #if QT_CONFIG(thread) @@ -315,6 +318,10 @@ void *QThreadPrivate::start(void *arg) if (thr->d_func()->priority & ThreadPriorityResetFlag) { thr->d_func()->setPriority(QThread::Priority(thr->d_func()->priority & ~ThreadPriorityResetFlag)); } +#ifndef Q_OS_DARWIN // For Darwin we set it as an attribute when starting the thread + if (thr->d_func()->serviceLevel != QThread::QualityOfService::Auto) + thr->d_func()->setQualityOfServiceLevel(thr->d_func()->serviceLevel); +#endif // threadId is set in QThread::start() Q_ASSERT(data->threadId.loadRelaxed() == QThread::currentThreadId()); @@ -680,6 +687,10 @@ void QThread::start(Priority priority) pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); +#ifdef Q_OS_DARWIN + if (d->serviceLevel != QThread::QualityOfService::Auto) + pthread_attr_set_qos_class_np(&attr, d->nativeQualityOfServiceClass(), 0); +#endif d->priority = priority; @@ -901,6 +912,39 @@ void QThreadPrivate::setPriority(QThread::Priority threadPriority) #endif } +void QThreadPrivate::setQualityOfServiceLevel(QThread::QualityOfService qosLevel) +{ + [[maybe_unused]] + Q_Q(QThread); + serviceLevel = qosLevel; +#ifdef Q_OS_DARWIN + qCDebug(lcQThread) << "Setting thread QoS class to" << serviceLevel << "for thread" << q; + pthread_set_qos_class_self_np(nativeQualityOfServiceClass(), 0); +#endif +} + +#ifdef Q_OS_DARWIN +qos_class_t QThreadPrivate::nativeQualityOfServiceClass() const +{ + // @note Consult table[0] to see what the levels mean + // [0] https://developer.apple.com/library/archive/documentation/Performance/Conceptual/power_efficiency_guidelines_osx/PrioritizeWorkAtTheTaskLevel.html#//apple_ref/doc/uid/TP40013929-CH35-SW5 + // There are more levels but they have two other documented ones, + // QOS_CLASS_BACKGROUND, which is below UTILITY, but has no guarantees + // for scheduling (ie. the OS could choose to never give it CPU time), + // and QOS_CLASS_USER_INITIATED, documented as being intended for + // user-initiated actions, such as loading a text document. + switch (serviceLevel) { + case QThread::QualityOfService::Auto: + return QOS_CLASS_DEFAULT; + case QThread::QualityOfService::High: + return QOS_CLASS_USER_INTERACTIVE; + case QThread::QualityOfService::Eco: + return QOS_CLASS_UTILITY; + } + Q_UNREACHABLE_RETURN(QOS_CLASS_DEFAULT); +} +#endif + #endif // QT_CONFIG(thread) QT_END_NAMESPACE diff --git a/src/corelib/thread/qthread_win.cpp b/src/corelib/thread/qthread_win.cpp index 3fb07db62da..f56da79d78d 100644 --- a/src/corelib/thread/qthread_win.cpp +++ b/src/corelib/thread/qthread_win.cpp @@ -28,8 +28,22 @@ SetThreadDescription( ); } +#ifndef THREAD_POWER_THROTTLING_EXECUTION_SPEED +#define THREAD_POWER_THROTTLING_EXECUTION_SPEED 0x1 +#define THREAD_POWER_THROTTLING_CURRENT_VERSION 1 + +typedef struct _THREAD_POWER_THROTTLING_STATE { + ULONG Version; + ULONG ControlMask; + ULONG StateMask; +} THREAD_POWER_THROTTLING_STATE; +#endif + + QT_BEGIN_NAMESPACE +Q_STATIC_LOGGING_CATEGORY(lcQThread, "qt.core.thread", QtWarningMsg) + #if QT_CONFIG(thread) void qt_watch_adopted_thread(const HANDLE adoptedThreadHandle, QThread *qthread); @@ -251,6 +265,9 @@ unsigned int __stdcall QT_ENSURE_STACK_ALIGNED_FOR_SSE QThreadPrivate::start(voi { QMutexLocker locker(&thr->d_func()->mutex); data->quitNow = thr->d_func()->exited; + + if (thr->d_func()->serviceLevel != QThread::QualityOfService::Auto) + thr->d_func()->setQualityOfServiceLevel(thr->d_func()->serviceLevel); } data->ensureEventDispatcher(); @@ -270,6 +287,39 @@ unsigned int __stdcall QT_ENSURE_STACK_ALIGNED_FOR_SSE QThreadPrivate::start(voi return 0; } +void QThreadPrivate::setQualityOfServiceLevel(QThread::QualityOfService qosLevel) +{ + Q_Q(QThread); + serviceLevel = qosLevel; + +#if (_WIN32_WINNT >= _WIN32_WINNT_WIN10_RS3) + qCDebug(lcQThread) << "Setting thread QoS class to" << qosLevel << "for thread" << q; + + THREAD_POWER_THROTTLING_STATE state; + memset(&state, 0, sizeof(state)); + state.Version = THREAD_POWER_THROTTLING_CURRENT_VERSION; + + switch (qosLevel) { + case QThread::QualityOfService::Auto: + state.ControlMask = 0; // Unset control of QoS + state.StateMask = 0; + break; + case QThread::QualityOfService::Eco: + state.ControlMask = THREAD_POWER_THROTTLING_EXECUTION_SPEED; + state.StateMask = THREAD_POWER_THROTTLING_EXECUTION_SPEED; + break; + case QThread::QualityOfService::High: + state.ControlMask = THREAD_POWER_THROTTLING_EXECUTION_SPEED; + state.StateMask = 0; // Ask to disable throttling + break; + } + if (!SetThreadInformation(::GetCurrentThread(), THREAD_INFORMATION_CLASS::ThreadPowerThrottling, + &state, sizeof(state))) { + qErrnoWarning("Failed to set thread power throttling state"); + } +#endif +} + /* For regularly terminating threads, this will be called and executed by the thread as the last code before the thread exits. In that case, \a arg is the current QThread. diff --git a/tests/auto/corelib/thread/qthread/tst_qthread.cpp b/tests/auto/corelib/thread/qthread/tst_qthread.cpp index 696b649dc76..99602098fac 100644 --- a/tests/auto/corelib/thread/qthread/tst_qthread.cpp +++ b/tests/auto/corelib/thread/qthread/tst_qthread.cpp @@ -105,6 +105,8 @@ private slots: void terminateSelfStressTest(); void bindingListCleanupAfterDelete(); + + void qualityOfService(); }; enum { one_minute = 60 * 1000, five_minutes = 5 * one_minute }; @@ -2104,5 +2106,33 @@ void tst_QThread::bindingListCleanupAfterDelete() QVERIFY(list->empty()); } +void tst_QThread::qualityOfService() +{ + QThread th; + QThread::currentThread()->setObjectName("Main thread"); + th.setObjectName("test thread"); + auto guard = qScopeGuard([&th](){ th.quit(); th.wait(); }); + QCOMPARE(th.serviceLevel(), QThread::QualityOfService::Auto); + th.setServiceLevel(QThread::QualityOfService::High); + QCOMPARE(th.serviceLevel(), QThread::QualityOfService::High); + th.setServiceLevel(QThread::QualityOfService::Eco); + QCOMPARE(th.serviceLevel(), QThread::QualityOfService::Eco); + + th.start(); + auto obj = std::make_unique(); + obj->moveToThread(&th); + + QThread::QualityOfService launchedThreadServiceLevel = {}; + QMetaObject::invokeMethod(obj.get(), [](){ + return QThread::currentThread()->serviceLevel(); + }, Qt::BlockingQueuedConnection, qReturnArg(launchedThreadServiceLevel)); + + QCOMPARE(launchedThreadServiceLevel, QThread::QualityOfService::Eco); + + QMetaObject::invokeMethod(obj.get(), [](){ + QThread::currentThread()->setServiceLevel(QThread::QualityOfService::High); + }, Qt::BlockingQueuedConnection); +} + QTEST_MAIN(tst_QThread) #include "tst_qthread.moc"