|  | // Copyright 2023 The Chromium Authors | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | #include <sys/resource.h> | 
|  |  | 
|  | #include <type_traits> | 
|  |  | 
|  | #include "base/base_switches.h" | 
|  | #include "base/command_line.h" | 
|  | #include "base/dcheck_is_on.h" | 
|  | #include "base/feature_list.h" | 
|  | #include "base/files/file_util.h" | 
|  | #include "base/metrics/field_trial_params.h" | 
|  | #include "base/no_destructor.h" | 
|  | #include "base/process/internal_linux.h" | 
|  | #include "base/process/process.h" | 
|  | #include "base/sequence_checker.h" | 
|  | #include "base/strings/stringprintf.h" | 
|  | #include "base/threading/cross_process_platform_thread_delegate.h" | 
|  | #include "base/threading/platform_thread.h" | 
|  | #include "base/threading/platform_thread_internal_posix.h" | 
|  |  | 
|  | // Description: ChromeOS specific Linux code layered on top of | 
|  | // base/threading/platform_thread_linux{,_base}.cc. | 
|  |  | 
|  | namespace base { | 
|  |  | 
|  | BASE_FEATURE(kSchedUtilHints, base::FEATURE_ENABLED_BY_DEFAULT); | 
|  |  | 
|  | BASE_FEATURE(kSetThreadBgForBgProcess, FEATURE_DISABLED_BY_DEFAULT); | 
|  |  | 
|  | BASE_FEATURE(kSetRtForDisplayThreads, FEATURE_DISABLED_BY_DEFAULT); | 
|  | namespace { | 
|  |  | 
|  | CrossProcessPlatformThreadDelegate* g_cross_process_platform_thread_delegate = | 
|  | nullptr; | 
|  |  | 
|  | std::atomic<bool> g_use_sched_util(true); | 
|  | std::atomic<bool> g_scheduler_hints_adjusted(false); | 
|  | std::atomic<bool> g_threads_bg_enabled(false); | 
|  | std::atomic<bool> g_display_threads_rt(false); | 
|  |  | 
|  | // When a device doesn't specify uclamp values via chrome switches, | 
|  | // default boosting for urgent tasks is hardcoded here as 20%. | 
|  | // Higher values can lead to higher power consumption thus this value | 
|  | // is chosen conservatively where it does not show noticeable | 
|  | // power usage increased from several perf/power tests. | 
|  | const int kSchedulerBoostDef = 20; | 
|  | const int kSchedulerLimitDef = 100; | 
|  | const bool kSchedulerUseLatencyTuneDef = true; | 
|  |  | 
|  | int g_scheduler_boost_adj; | 
|  | int g_scheduler_limit_adj; | 
|  | bool g_scheduler_use_latency_tune_adj; | 
|  |  | 
|  | // Defined by linux uclamp ABI of sched_setattr(). | 
|  | constexpr uint32_t kSchedulerUclampMin = 0; | 
|  | constexpr uint32_t kSchedulerUclampMax = 1024; | 
|  |  | 
|  | // sched_attr is used to set scheduler attributes for Linux. It is not a POSIX | 
|  | // struct and glibc does not expose it. | 
|  | struct sched_attr { | 
|  | uint32_t size; | 
|  |  | 
|  | uint32_t sched_policy; | 
|  | uint64_t sched_flags; | 
|  |  | 
|  | /* SCHED_NORMAL, SCHED_BATCH */ | 
|  | int32_t sched_nice; | 
|  |  | 
|  | /* SCHED_FIFO, SCHED_RR */ | 
|  | uint32_t sched_priority; | 
|  |  | 
|  | /* SCHED_DEADLINE */ | 
|  | uint64_t sched_runtime; | 
|  | uint64_t sched_deadline; | 
|  | uint64_t sched_period; | 
|  |  | 
|  | /* Utilization hints */ | 
|  | uint32_t sched_util_min; | 
|  | uint32_t sched_util_max; | 
|  | }; | 
|  |  | 
|  | #if !defined(__NR_sched_setattr) | 
|  | #if defined(__x86_64__) | 
|  | #define __NR_sched_setattr 314 | 
|  | #define __NR_sched_getattr 315 | 
|  | #elif defined(__i386__) | 
|  | #define __NR_sched_setattr 351 | 
|  | #define __NR_sched_getattr 352 | 
|  | #elif defined(__arm__) | 
|  | #define __NR_sched_setattr 380 | 
|  | #define __NR_sched_getattr 381 | 
|  | #elif defined(__aarch64__) | 
|  | #define __NR_sched_setattr 274 | 
|  | #define __NR_sched_getattr 275 | 
|  | #else | 
|  | #error "We don't have an __NR_sched_setattr for this architecture." | 
|  | #endif | 
|  | #endif | 
|  |  | 
|  | #if !defined(SCHED_FLAG_UTIL_CLAMP_MIN) | 
|  | #define SCHED_FLAG_UTIL_CLAMP_MIN 0x20 | 
|  | #endif | 
|  |  | 
|  | #if !defined(SCHED_FLAG_UTIL_CLAMP_MAX) | 
|  | #define SCHED_FLAG_UTIL_CLAMP_MAX 0x40 | 
|  | #endif | 
|  |  | 
|  | long sched_getattr(pid_t pid, | 
|  | const struct sched_attr* attr, | 
|  | unsigned int size, | 
|  | unsigned int flags) { | 
|  | return syscall(__NR_sched_getattr, pid, attr, size, flags); | 
|  | } | 
|  |  | 
|  | long sched_setattr(pid_t pid, | 
|  | const struct sched_attr* attr, | 
|  | unsigned int flags) { | 
|  | return syscall(__NR_sched_setattr, pid, attr, flags); | 
|  | } | 
|  |  | 
|  | // Setup whether a thread is latency sensitive. The thread_id should | 
|  | // always be the value in the root PID namespace (see FindThreadID). | 
|  | void SetThreadLatencySensitivity(ProcessId process_id, | 
|  | PlatformThreadId thread_id, | 
|  | ThreadType thread_type) { | 
|  | struct sched_attr attr; | 
|  | bool is_urgent = false; | 
|  | int boost_percent, limit_percent; | 
|  | int latency_sensitive_urgent; | 
|  |  | 
|  | // Scheduler boost defaults to true unless disabled. | 
|  | if (!g_use_sched_util.load()) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | // FieldTrial API can be called only once features were parsed. | 
|  | if (g_scheduler_hints_adjusted.load()) { | 
|  | boost_percent = g_scheduler_boost_adj; | 
|  | limit_percent = g_scheduler_limit_adj; | 
|  | latency_sensitive_urgent = g_scheduler_use_latency_tune_adj; | 
|  | } else { | 
|  | boost_percent = kSchedulerBoostDef; | 
|  | limit_percent = kSchedulerLimitDef; | 
|  | latency_sensitive_urgent = kSchedulerUseLatencyTuneDef; | 
|  | } | 
|  |  | 
|  | // The thread_id passed in here is either 0 (in which case we ste for current | 
|  | // thread), or is a tid that is not the NS tid but the global one. The | 
|  | // conversion from NS tid to global tid is done by the callers using | 
|  | // FindThreadID(). | 
|  | FilePath thread_dir; | 
|  | if (thread_id != kInvalidThreadId && | 
|  | thread_id != PlatformThread::CurrentId()) { | 
|  | thread_dir = FilePath( | 
|  | StringPrintf("/proc/%d/task/%d/", process_id, thread_id.raw())); | 
|  | } else { | 
|  | thread_dir = FilePath("/proc/thread-self/"); | 
|  | } | 
|  |  | 
|  | FilePath latency_sensitive_file = thread_dir.Append("latency_sensitive"); | 
|  |  | 
|  | if (!PathExists(latency_sensitive_file)) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | // Silently ignore if getattr fails due to sandboxing. | 
|  | if (sched_getattr(thread_id.raw(), &attr, sizeof(attr), 0) == -1 || | 
|  | attr.size != sizeof(attr)) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | switch (thread_type) { | 
|  | case ThreadType::kBackground: | 
|  | case ThreadType::kUtility: | 
|  | case ThreadType::kDefault: | 
|  | break; | 
|  | case ThreadType::kDisplayCritical: | 
|  | case ThreadType::kInteractive: | 
|  | // Compositing and display critical threads need a boost for consistent 60 | 
|  | // fps. | 
|  | [[fallthrough]]; | 
|  | case ThreadType::kRealtimeAudio: | 
|  | is_urgent = true; | 
|  | break; | 
|  | } | 
|  |  | 
|  | // Logging error only if failed to write when latency sensitive, otherwise | 
|  | // silently ignore as "0" is the default value. | 
|  | if (is_urgent && latency_sensitive_urgent) { | 
|  | PLOG_IF(ERROR, !WriteFile(latency_sensitive_file, | 
|  | base::byte_span_from_cstring("1"))) | 
|  | << "Failed to write latency file."; | 
|  | } else { | 
|  | WriteFile(latency_sensitive_file, base::byte_span_from_cstring("0")); | 
|  | } | 
|  |  | 
|  | attr.sched_flags |= SCHED_FLAG_UTIL_CLAMP_MIN; | 
|  | attr.sched_flags |= SCHED_FLAG_UTIL_CLAMP_MAX; | 
|  |  | 
|  | if (is_urgent) { | 
|  | attr.sched_util_min = | 
|  | (saturated_cast<uint32_t>(boost_percent) * kSchedulerUclampMax + 50) / | 
|  | 100; | 
|  | attr.sched_util_max = kSchedulerUclampMax; | 
|  | } else { | 
|  | attr.sched_util_min = kSchedulerUclampMin; | 
|  | attr.sched_util_max = | 
|  | (saturated_cast<uint32_t>(limit_percent) * kSchedulerUclampMax + 50) / | 
|  | 100; | 
|  | } | 
|  |  | 
|  | DCHECK_GE(attr.sched_util_min, kSchedulerUclampMin); | 
|  | DCHECK_LE(attr.sched_util_max, kSchedulerUclampMax); | 
|  |  | 
|  | attr.size = sizeof(struct sched_attr); | 
|  | if (sched_setattr(thread_id.raw(), &attr, 0) == -1) { | 
|  | // We log it as an error because, if the PathExists above succeeded, we | 
|  | // expect this syscall to also work since the kernel is new'ish. | 
|  | PLOG_IF(ERROR, errno != E2BIG) | 
|  | << "Failed to set sched_util_min, performance may be effected."; | 
|  | } | 
|  | } | 
|  |  | 
|  | // Get the type by reading through kThreadTypeToNiceValueMap | 
|  | std::optional<ThreadType> GetThreadTypeForNiceValue(int nice_value) { | 
|  | switch (nice_value) { | 
|  | case 10: | 
|  | return ThreadType::kBackground; | 
|  | case 2: | 
|  | return ThreadType::kUtility; | 
|  | case 0: | 
|  | return ThreadType::kDefault; | 
|  | case -8: | 
|  | return ThreadType::kDisplayCritical; | 
|  | case -10: | 
|  | return ThreadType::kRealtimeAudio; | 
|  | default: | 
|  | return std::nullopt; | 
|  | } | 
|  | } | 
|  |  | 
|  | std::optional<int> GetNiceValueForThreadId(PlatformThreadId thread_id) { | 
|  | // Get the current nice value of the thread_id | 
|  | errno = 0; | 
|  | int nice_value = | 
|  | getpriority(PRIO_PROCESS, static_cast<id_t>(thread_id.raw())); | 
|  | if (nice_value == -1 && errno != 0) { | 
|  | // The thread may disappear for any reason so ignore ESRCH. | 
|  | DVPLOG_IF(1, errno != ESRCH) | 
|  | << "Failed to call getpriority for thread id " << thread_id | 
|  | << ", performance may be effected."; | 
|  | return std::nullopt; | 
|  | } | 
|  | return nice_value; | 
|  | } | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | void SetThreadTypeOtherAttrs(ProcessId process_id, | 
|  | PlatformThreadId thread_id, | 
|  | ThreadType thread_type) { | 
|  | // For cpuset and legacy schedtune interface | 
|  | PlatformThreadLinux::SetThreadCgroupsForThreadType(thread_id, thread_type); | 
|  |  | 
|  | // For upstream uclamp interface. We try both legacy (schedtune, as done | 
|  | // earlier) and upstream (uclamp) interfaces, and whichever succeeds wins. | 
|  | SetThreadLatencySensitivity(process_id, thread_id, thread_type); | 
|  | } | 
|  |  | 
|  | // Set or reset the RT priority of a thread based on its type | 
|  | // and whether the process it is in is backgrounded. | 
|  | // Setting an RT task to CFS retains the task's nice value. | 
|  | void SetThreadRTPrioFromType(ProcessId process_id, | 
|  | PlatformThreadId thread_id, | 
|  | ThreadType thread_type, | 
|  | bool proc_bg) { | 
|  | struct sched_param prio; | 
|  | int policy; | 
|  |  | 
|  | switch (thread_type) { | 
|  | case ThreadType::kRealtimeAudio: | 
|  | prio = PlatformThreadChromeOS::kRealTimeAudioPrio; | 
|  | policy = SCHED_RR; | 
|  | break; | 
|  | case ThreadType::kDisplayCritical: | 
|  | case ThreadType::kInteractive: | 
|  | if (!PlatformThreadChromeOS::IsDisplayThreadsRtFeatureEnabled()) { | 
|  | return; | 
|  | } | 
|  | if (proc_bg) { | 
|  | // Per manpage, must be 0. Otherwise could have passed nice value here. | 
|  | // Note that even though the prio.sched_priority passed to the | 
|  | // sched_setscheduler() syscall is 0, the old nice value (which holds | 
|  | // the ThreadType of the thread) is retained. | 
|  | prio.sched_priority = 0; | 
|  | policy = SCHED_OTHER; | 
|  | } else { | 
|  | prio = PlatformThreadChromeOS::kRealTimeDisplayPrio; | 
|  | policy = SCHED_RR; | 
|  | } | 
|  | break; | 
|  | default: | 
|  | return; | 
|  | } | 
|  |  | 
|  | pid_t syscall_tid = | 
|  | thread_id == PlatformThread::CurrentId() ? 0 : thread_id.raw(); | 
|  | if (sched_setscheduler(syscall_tid, policy, &prio) != 0) { | 
|  | DVPLOG(1) << "Failed to set policy/priority for thread " << thread_id; | 
|  | } | 
|  | } | 
|  |  | 
|  | void SetThreadNiceFromType(ProcessId process_id, | 
|  | PlatformThreadId thread_id, | 
|  | ThreadType thread_type) { | 
|  | pid_t syscall_tid = | 
|  | thread_id == PlatformThread::CurrentId() ? 0 : thread_id.raw(); | 
|  | const int nice_setting = internal::ThreadTypeToNiceValue(thread_type); | 
|  | if (setpriority(PRIO_PROCESS, static_cast<id_t>(syscall_tid), nice_setting)) { | 
|  | DVPLOG(1) << "Failed to set nice value of thread " << thread_id << " to " | 
|  | << nice_setting; | 
|  | } | 
|  | } | 
|  |  | 
|  | void PlatformThreadChromeOS::InitializeFeatures() { | 
|  | DCHECK(FeatureList::GetInstance()); | 
|  | g_threads_bg_enabled.store(FeatureList::IsEnabled(kSetThreadBgForBgProcess)); | 
|  | g_display_threads_rt.store(FeatureList::IsEnabled(kSetRtForDisplayThreads)); | 
|  | if (!FeatureList::IsEnabled(kSchedUtilHints)) { | 
|  | g_use_sched_util.store(false); | 
|  | return; | 
|  | } | 
|  |  | 
|  | int boost_def = kSchedulerBoostDef; | 
|  |  | 
|  | if (CommandLine::ForCurrentProcess()->HasSwitch( | 
|  | switches::kSchedulerBoostUrgent)) { | 
|  | std::string boost_switch_str = | 
|  | CommandLine::ForCurrentProcess()->GetSwitchValueASCII( | 
|  | switches::kSchedulerBoostUrgent); | 
|  |  | 
|  | int boost_switch_val; | 
|  | if (!StringToInt(boost_switch_str, &boost_switch_val) || | 
|  | boost_switch_val < 0 || boost_switch_val > 100) { | 
|  | DVLOG(1) << "Invalid input for " << switches::kSchedulerBoostUrgent; | 
|  | } else { | 
|  | boost_def = boost_switch_val; | 
|  | } | 
|  | } | 
|  |  | 
|  | g_scheduler_boost_adj = GetFieldTrialParamByFeatureAsInt( | 
|  | kSchedUtilHints, "BoostUrgent", boost_def); | 
|  | g_scheduler_limit_adj = GetFieldTrialParamByFeatureAsInt( | 
|  | kSchedUtilHints, "LimitNonUrgent", kSchedulerLimitDef); | 
|  | g_scheduler_use_latency_tune_adj = GetFieldTrialParamByFeatureAsBool( | 
|  | kSchedUtilHints, "LatencyTune", kSchedulerUseLatencyTuneDef); | 
|  |  | 
|  | g_scheduler_hints_adjusted.store(true); | 
|  | } | 
|  |  | 
|  | // static | 
|  | void PlatformThreadChromeOS::SetCrossProcessPlatformThreadDelegate( | 
|  | CrossProcessPlatformThreadDelegate* delegate) { | 
|  | // A component cannot override a delegate set by another component, thus | 
|  | // disallow setting a delegate when one already exists. | 
|  | DCHECK_NE(!!g_cross_process_platform_thread_delegate, !!delegate); | 
|  |  | 
|  | g_cross_process_platform_thread_delegate = delegate; | 
|  | } | 
|  |  | 
|  | // static | 
|  | bool PlatformThreadChromeOS::IsThreadsBgFeatureEnabled() { | 
|  | return g_threads_bg_enabled.load(); | 
|  | } | 
|  |  | 
|  | // static | 
|  | bool PlatformThreadChromeOS::IsDisplayThreadsRtFeatureEnabled() { | 
|  | return g_display_threads_rt.load(); | 
|  | } | 
|  |  | 
|  | // static | 
|  | std::optional<ThreadType> PlatformThreadChromeOS::GetThreadTypeFromThreadId( | 
|  | ProcessId process_id, | 
|  | PlatformThreadId thread_id) { | 
|  | // Get the current nice_value of the thread_id | 
|  | std::optional<int> nice_value = GetNiceValueForThreadId(thread_id); | 
|  | if (!nice_value.has_value()) { | 
|  | return std::nullopt; | 
|  | } | 
|  | return GetThreadTypeForNiceValue(nice_value.value()); | 
|  | } | 
|  |  | 
|  | // static | 
|  | void PlatformThreadChromeOS::SetThreadType(ProcessId process_id, | 
|  | PlatformThreadId thread_id, | 
|  | ThreadType thread_type, | 
|  | IsViaIPC via_ipc) { | 
|  | if (g_cross_process_platform_thread_delegate && | 
|  | g_cross_process_platform_thread_delegate->HandleThreadTypeChange( | 
|  | process_id, thread_id, thread_type)) { | 
|  | return; | 
|  | } | 
|  | internal::SetThreadType(process_id, thread_id, thread_type, via_ipc); | 
|  | } | 
|  |  | 
|  | void PlatformThreadChromeOS::SetThreadBackgrounded(ProcessId process_id, | 
|  | PlatformThreadId thread_id, | 
|  | bool backgrounded) { | 
|  | // Get the current nice value of the thread_id | 
|  | std::optional<int> nice_value = GetNiceValueForThreadId(thread_id); | 
|  | if (!nice_value.has_value()) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | std::optional<ThreadType> type = | 
|  | GetThreadTypeForNiceValue(nice_value.value()); | 
|  | if (!type.has_value()) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | // kRealtimeAudio threads are not backgrounded or foregrounded. | 
|  | if (type == ThreadType::kRealtimeAudio) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | SetThreadTypeOtherAttrs( | 
|  | process_id, thread_id, | 
|  | backgrounded ? ThreadType::kBackground : type.value()); | 
|  | SetThreadRTPrioFromType(process_id, thread_id, type.value(), backgrounded); | 
|  | } | 
|  |  | 
|  | void PlatformThreadChromeOS::DcheckCrossProcessThreadPrioritySequence() { | 
|  | // The `NoDestructor` instantiation here must be guarded, since with DCHECKs | 
|  | // disabled, `SequenceChecker` is trivially destructible, which triggers a | 
|  | // `static_assert` in `NoDestructor`. | 
|  | #if DCHECK_IS_ON() | 
|  | static NoDestructor<SequenceChecker> instance; | 
|  | DCHECK_CALLED_ON_VALID_SEQUENCE(*instance); | 
|  | #endif | 
|  | } | 
|  |  | 
|  | namespace internal { | 
|  |  | 
|  | void SetThreadTypeChromeOS(ProcessId process_id, | 
|  | PlatformThreadId thread_id, | 
|  | ThreadType thread_type, | 
|  | IsViaIPC via_ipc) { | 
|  | // TODO(b/262267726): Re-use common code with SetThreadTypeLinux. | 
|  | // Should not be called concurrently with | 
|  | // other functions like SetThreadBackgrounded. | 
|  | if (via_ipc) { | 
|  | PlatformThreadChromeOS::DcheckCrossProcessThreadPrioritySequence(); | 
|  | } | 
|  |  | 
|  | auto proc = Process::Open(process_id); | 
|  | bool backgrounded = false; | 
|  | if (PlatformThread::IsThreadsBgFeatureEnabled() && | 
|  | thread_type != ThreadType::kRealtimeAudio && proc.IsValid() && | 
|  | proc.GetPriority() == base::Process::Priority::kBestEffort) { | 
|  | backgrounded = true; | 
|  | } | 
|  |  | 
|  | SetThreadTypeOtherAttrs(process_id, thread_id, | 
|  | backgrounded ? ThreadType::kBackground : thread_type); | 
|  |  | 
|  | SetThreadRTPrioFromType(process_id, thread_id, thread_type, backgrounded); | 
|  | SetThreadNiceFromType(process_id, thread_id, thread_type); | 
|  | } | 
|  |  | 
|  | }  // namespace internal | 
|  |  | 
|  | }  // namespace base |