Add thread Quality of Service API

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ø <tor.arne.vestbo@qt.io>
This commit is contained in:
Mårten Nordheim 2023-09-19 20:13:27 +02:00
parent 073fae097c
commit 6013f82c94
6 changed files with 180 additions and 0 deletions

View File

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

View File

@ -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 <typename Function, typename... Args>
[[nodiscard]] static QThread *create(Function &&f, Args &&... args);

View File

@ -206,6 +206,12 @@ public:
bool wait(QMutexLocker<QMutex> &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;

View File

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

View File

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

View File

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