#pragma once // C++17 / HarmonyOS native QoS (API 12+). Construct, use and finish on the // actual native compute thread. This teaching scope does not create a timer, // observer thread, signal handler or process-wide CPUFreq/cpuset request. #include #include #include #include #include #include namespace single_core { struct Options { bool useQos = true; bool requestRealtime = false; }; struct Call { bool attempted = false; int result = -1; int error = 0; // POSIX errno only. QoS documents its return, not errno. }; struct SessionReport { int ownerTid = -1, requestedCpu = -1, actualCpu = -1, effectiveQos = -1; bool validTarget = false, affinityCaptured = false, attrsCaptured = false; bool qosCaptured = false, qosSkippedUnknown = false, snapshotComplete = false; bool performanceEligible = false, targetInOriginalMask = false; bool exactAtReadback = false, wrongThread = false, finished = false; bool restoreSucceeded = false; unsigned refreshCount = 0; // 0 = none, 1 = FIFO, 2 = OTHER/nice -20, 3 = current policy + clamp. int selectedRequest = 0; uint32_t effectivePolicy = 0, effectiveUtilMin = 0, effectiveUtilMax = 0; int32_t effectiveNice = 0; Call captureAffinity, captureAttr, captureQos; Call qosSet, fifoSet, normalSet, clampSet, affinitySet; Call affinityRead, attrRead, qosRead; Call qosRestore, attrRestore, affinityRestore; Call qosVerify, attrVerify, affinityVerify; }; class ThreadSession { // Local Linux UAPI sched_attr v1 avoids the sched_param collision between // POSIX sched.h and linux/sched/types.h. v0 has no uclamp fields. struct SchedAttr { uint32_t size, policy; uint64_t flags; int32_t nice; uint32_t priority; uint64_t runtime, deadline, period; uint32_t utilMin, utilMax; }; static_assert(sizeof(SchedAttr) == 56, "sched_attr v1 ABI size"); static constexpr uint64_t kResetOnFork = 0x01; static constexpr uint64_t kKeepPolicyAndParams = 0x18; static constexpr uint64_t kClamp = 0x60; Options options_; SessionReport report_; cpu_set_t originalMask_{}; SchedAttr originalAttr_{}; QoS_Level originalQos_ = QOS_DEFAULT; bool qosTouched_ = false, attrTouched_ = false, affinityTouched_ = false; static int Tid() noexcept { return static_cast(::syscall(SYS_gettid)); } static int Posix(Call &call, int result) noexcept { call = {true, result, result == 0 ? 0 : errno}; return result; } static int Qos(Call &call, int result) noexcept { call = {true, result, 0}; return result; } static int GetAttr(SchedAttr &attr, Call &call) noexcept { #if defined(SYS_sched_getattr) attr = {}; attr.size = sizeof(attr); const int rc = static_cast(::syscall(SYS_sched_getattr, 0, &attr, sizeof(attr), 0)); if (rc == 0 && attr.size != sizeof(attr)) { call = {true, -1, ENOTSUP}; // Only the captured v1 layout is supported. return -1; } return Posix(call, rc); #else (void)attr; call = {true, -1, ENOSYS}; return -1; #endif } static int SetAttr(SchedAttr &attr, Call &call) noexcept { #if defined(SYS_sched_setattr) return Posix(call, static_cast(::syscall(SYS_sched_setattr, 0, &attr, 0))); #else (void)attr; call = {true, -1, ENOSYS}; return -1; #endif } bool OnOwner() noexcept { if (Tid() == report_.ownerTid) return true; report_.wrongThread = true; return false; } static bool SameMask(const cpu_set_t &a, const cpu_set_t &b) noexcept { for (int cpu = 0; cpu < CPU_SETSIZE; ++cpu) if (CPU_ISSET(cpu, &a) != CPU_ISSET(cpu, &b)) return false; return true; } static bool SameAttr(const SchedAttr &a, const SchedAttr &b) noexcept { return a.policy == b.policy && a.nice == b.nice && a.priority == b.priority && a.runtime == b.runtime && a.deadline == b.deadline && a.period == b.period && a.utilMin == b.utilMin && a.utilMax == b.utilMax && (a.flags & kResetOnFork) == (b.flags & kResetOnFork); } void Capture() noexcept { report_.affinityCaptured = Posix(report_.captureAffinity, ::sched_getaffinity(0, sizeof(originalMask_), &originalMask_)) == 0; report_.attrsCaptured = GetAttr(originalAttr_, report_.captureAttr) == 0; // The minimal example only modifies ordinary-policy threads. Capturing // a pre-existing realtime/deadline policy is not proof we can restore it. report_.performanceEligible = report_.attrsCaptured && (originalAttr_.flags & ~kResetOnFork) == 0 && (originalAttr_.policy == SCHED_OTHER || originalAttr_.policy == SCHED_BATCH || originalAttr_.policy == SCHED_IDLE); report_.qosCaptured = Qos(report_.captureQos, OH_QoS_GetThreadQoS(&originalQos_)) == 0 && originalQos_ >= QOS_BACKGROUND && originalQos_ <= QOS_USER_INTERACTIVE; report_.qosSkippedUnknown = options_.useQos && !report_.qosCaptured; report_.snapshotComplete = report_.affinityCaptured && report_.attrsCaptured; report_.targetInOriginalMask = report_.validTarget && report_.affinityCaptured && CPU_ISSET(report_.requestedCpu, &originalMask_); } void ApplyPerformance() noexcept { // QoS can also affect scheduling state; capture both attr and mask first. if (!report_.snapshotComplete || !report_.performanceEligible) return; if (options_.useQos && report_.qosCaptured) { qosTouched_ = true; Qos(report_.qosSet, OH_QoS_SetThreadQoS(QOS_USER_INTERACTIVE)); } SchedAttr request{}; request.size = sizeof(request); request.flags = (originalAttr_.flags & kResetOnFork) | kClamp; request.utilMin = request.utilMax = 1024; attrTouched_ = true; report_.selectedRequest = 0; if (options_.requestRealtime) { request.policy = SCHED_FIFO; request.priority = 1; if (SetAttr(request, report_.fifoSet) == 0) report_.selectedRequest = 1; } if (report_.selectedRequest == 0) { request.policy = SCHED_OTHER; request.priority = 0; request.nice = -20; if (SetAttr(request, report_.normalSet) == 0) report_.selectedRequest = 2; } if (report_.selectedRequest == 0) { request = originalAttr_; request.size = sizeof(request); request.flags = (originalAttr_.flags & kResetOnFork) | kClamp | kKeepPolicyAndParams; request.utilMin = request.utilMax = 1024; if (SetAttr(request, report_.clampSet) == 0) report_.selectedRequest = 3; } } void Observe() noexcept { cpu_set_t effective{}; const bool maskRead = Posix(report_.affinityRead, ::sched_getaffinity(0, sizeof(effective), &effective)) == 0; report_.actualCpu = ::sched_getcpu(); report_.exactAtReadback = report_.affinitySet.result == 0 && maskRead && CPU_COUNT(&effective) == 1 && CPU_ISSET(report_.requestedCpu, &effective) && report_.actualCpu == report_.requestedCpu; SchedAttr actual{}; if (GetAttr(actual, report_.attrRead) == 0) { report_.effectivePolicy = actual.policy; report_.effectiveNice = actual.nice; report_.effectiveUtilMin = actual.utilMin; report_.effectiveUtilMax = actual.utilMax; } QoS_Level qos = QOS_DEFAULT; report_.effectiveQos = Qos(report_.qosRead, OH_QoS_GetThreadQoS(&qos)) == 0 ? qos : -1; } public: explicit ThreadSession(int cpu, Options options = {}) noexcept : options_(options) { report_.ownerTid = Tid(); report_.requestedCpu = cpu; report_.validTarget = cpu >= 0 && cpu < CPU_SETSIZE; Capture(); if (report_.validTarget) Refresh(); } ThreadSession(const ThreadSession &) = delete; ThreadSession &operator=(const ThreadSession &) = delete; ThreadSession(ThreadSession &&) = delete; ThreadSession &operator=(ThreadSession &&) = delete; ~ThreadSession() noexcept { if (!report_.finished) (void)Finish(); } const SessionReport &Report() const noexcept { return report_; } // Read only: inspect the owner again outside the timed workload. bool ObserveNow() noexcept { if (report_.finished || !OnOwner() || !report_.validTarget) return false; Observe(); return true; } // Use at a chosen non-timed boundary on the owner thread. A true return // means this operation ran; inspect exactAtReadback and each request result. bool Refresh() noexcept { if (report_.finished || !OnOwner() || !report_.validTarget || !report_.affinityCaptured) return false; ++report_.refreshCount; ApplyPerformance(); cpu_set_t target{}; CPU_ZERO(&target); CPU_SET(report_.requestedCpu, &target); affinityTouched_ = true; Posix(report_.affinitySet, ::sched_setaffinity(0, sizeof(target), &target)); Observe(); return true; } bool Finish() noexcept { if (report_.finished) return report_.restoreSucceeded; if (!OnOwner()) return false; bool okay = true; // Stop/join any external maintenance before calling Finish(). if (qosTouched_) { const bool set = Qos(report_.qosRestore, OH_QoS_SetThreadQoS(originalQos_)) == 0; QoS_Level qos = QOS_DEFAULT; const bool get = Qos(report_.qosVerify, OH_QoS_GetThreadQoS(&qos)) == 0; okay = set && get && qos == originalQos_ && okay; } if (attrTouched_ || qosTouched_) { SchedAttr saved = originalAttr_; saved.size = sizeof(saved); // KEEP_* are commands, not saved policy. Explicitly request clamp // restoration; getters need not return UTIL_CLAMP_* command bits. saved.flags = (originalAttr_.flags & kResetOnFork) | kClamp; const bool set = SetAttr(saved, report_.attrRestore) == 0; SchedAttr actual{}; const bool get = GetAttr(actual, report_.attrVerify) == 0; okay = set && get && SameAttr(actual, originalAttr_) && okay; } if (affinityTouched_ || attrTouched_ || qosTouched_) { const bool set = Posix(report_.affinityRestore, ::sched_setaffinity(0, sizeof(originalMask_), &originalMask_)) == 0; cpu_set_t actual{}; const bool get = Posix(report_.affinityVerify, ::sched_getaffinity(0, sizeof(actual), &actual)) == 0; okay = set && get && SameMask(actual, originalMask_) && okay; } report_.finished = true; report_.restoreSucceeded = okay; return okay; } }; } // namespace single_core