servo_base/threadboost.rs
1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! Helper to boost critical threads
6//!
7//! On heterogeneous (e.g. big.LITTLE) CPUs the scheduler might not be able to
8//! determine which threads are critical.
9//! This module serves as an entry point to define policies for thread-affinity,
10//! thread priority and core frequency boosting or other mechanisms a platform
11//! might have for apps to allow the OS to optimize thread performance.
12//!
13//! TODO(#46813): We provide a default implementation here, but the embedder
14//! should be able to customize this (e.g. if they care more about energy efficiency
15//! then raw performance, for example in battery saving mode).
16//! TODO: For android we should look at the performance hint API in the NDK.
17//! TODO: For mobile linux, the ohos implementation could be shared, however other APIs
18//! like uclamp or enhanced thread priorities might work better there?
19
20use servo_config::pref;
21
22#[cfg(target_os = "linux")]
23mod linux_sysfs {
24 use std::fs;
25 use std::sync::LazyLock;
26
27 use log::info;
28
29 // The current maximum supported by CPU_SET is 1024, so u16 is sufficiently large.
30 // <https://man7.org/linux/man-pages/man3/CPU_SET.3.html>
31 type CoreId = u16;
32
33 static NON_LITTLE_CPU_CORES: LazyLock<Option<Box<[CoreId]>>> = LazyLock::new(|| {
34 non_little_cpus()
35 .inspect_err(|error| log::error!("Failed to determine non-little cpu cores: {error}"))
36 .ok()?
37 .map(|cores| cores.into_boxed_slice())
38 });
39
40 fn parse_cpu_list() -> Result<Vec<CoreId>, String> {
41 let cpu_possible_file = "/sys/devices/system/cpu/possible";
42 let list = fs::read_to_string(cpu_possible_file)
43 .map_err(|error| format!("failed to read {cpu_possible_file}: {error:?}"))?;
44
45 let mut cpus = Vec::new();
46 // See <https://docs.kernel.org/admin-guide/cputopology.html> / cpulist_parse
47 for part in list.trim().split(',') {
48 if let Some((a, b)) = part.split_once('-') {
49 if let (Ok(a), Ok(b)) = (a.trim().parse::<CoreId>(), b.trim().parse::<CoreId>()) {
50 cpus.extend(a..=b);
51 }
52 } else if let Ok(core_id) = part.trim().parse::<CoreId>() {
53 cpus.push(core_id);
54 } else {
55 log::warn!("Unexpected CPU line: {part:?} in {cpu_possible_file}.");
56 }
57 }
58 Ok(cpus)
59 }
60
61 /// Per-cpu relative capacity (normalized to 1024 for the strongest core in a system)
62 ///
63 /// <https://www.kernel.org/doc/Documentation/devicetree/bindings/arm/cpu-capacity.txt>
64 fn capacity_of(cpu: CoreId) -> Result<u64, String> {
65 let cpu_capacity_file = format!("/sys/devices/system/cpu/cpu{cpu}/cpu_capacity");
66 fs::read_to_string(cpu_capacity_file)
67 .map_err(|error| error.to_string())?
68 .trim()
69 .parse::<u64>()
70 .map_err(|error| error.to_string())
71 }
72
73 /// Determine the CPU ids of cores not in the little class.
74 ///
75 /// Returns `Err` on internal / parsing errors.
76 /// Returns `Ok(None)` if the cpu is not heterogeneous, or if there would only be a single
77 /// cpu in the non-little group.
78 fn non_little_cpus() -> Result<Option<Vec<CoreId>>, String> {
79 let cpus = parse_cpu_list()?;
80 let cpu_and_caps: Vec<(CoreId, u64)> = cpus
81 .iter()
82 .map(|&cpu| capacity_of(cpu).map(|cap| (cpu, cap)))
83 .collect::<Result<_, _>>()?;
84
85 let mut classes: Vec<u64> = cpu_and_caps.iter().map(|&(_, cap)| cap).collect();
86 classes.sort_unstable();
87 classes.dedup();
88 if classes.len() < 2 {
89 info!("No little/big cores configuration");
90 return Ok(None);
91 }
92 let little = classes[0];
93 let chosen: Vec<CoreId> = cpu_and_caps
94 .iter()
95 .filter(|&&(_, cap)| cap > little)
96 .map(|&(cpu, _)| cpu)
97 .collect();
98 if chosen.len() < 2 {
99 info!("Not enough big cores available ({})", chosen.len());
100 return Ok(None);
101 }
102 info!(
103 "CPU cores configuration: {} big cores out of {}",
104 chosen.len(),
105 cpus.len()
106 );
107 Ok(Some(chosen))
108 }
109
110 /// Try and pin this thread to cpu cores above the smallest capacity class.
111 ///
112 /// If the cpu only has one kind of core, or only one core above the smallest capacity class
113 /// this is a no-op.
114 #[expect(unsafe_code)]
115 pub(super) fn pin_thread_to_medium_or_large_cpus() -> Result<(), String> {
116 // Note: If we encountered an error when parsing the cpu structure, then
117 // we logged the error in the LazyLock (once), which avoids flooding the logs
118 // with error messages for every thread we want to pin.
119 let Some(cpus) = NON_LITTLE_CPU_CORES.as_ref() else {
120 return Ok(());
121 };
122 let ret = unsafe {
123 let mut set: libc::cpu_set_t = std::mem::zeroed();
124 for &cpu in cpus {
125 libc::CPU_SET(cpu.into(), &mut set);
126 }
127 libc::sched_setaffinity(0, size_of::<libc::cpu_set_t>(), &set)
128 };
129 if ret != 0 {
130 return Err(format!(
131 "Failed to set thread affinity: {:?}",
132 std::io::Error::last_os_error()
133 ));
134 }
135 Ok(())
136 }
137}
138
139#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
140mod platform {
141 use super::BoostAffinity;
142 use super::linux_sysfs::pin_thread_to_medium_or_large_cpus;
143
144 pub fn boost_thread(_: super::ThreadPriority, boost_affinity: super::BoostAffinity) {
145 if matches!(boost_affinity, BoostAffinity::Boost) &&
146 let Err(error) = pin_thread_to_medium_or_large_cpus()
147 {
148 log::warn!(
149 "Failed to pin {} to medium or large cpus: {error:?}",
150 std::thread::current().name().unwrap_or("<unnamed>"),
151 );
152 }
153 }
154}
155
156#[cfg(target_env = "ohos")]
157mod platform {
158 //! On `ohos` targets we only have the `OH_QoS_SetThreadQoS` API from qos/qos.h,
159 //! which influences scheduling priority, but empirically does not help with ensuring
160 //! important servo threads like script get scheduled on larger cores, presumably because
161 //! we do a lot of IPC, and the workload doesn't pass heuristic thresholds to get promoted
162 //! to a larger core.
163 //! [uclamp_min](https://docs.kernel.org/scheduler/sched-util-clamp.html) is supported but
164 //! ignored (empirically tested) on the hongmeng kernel.
165 //! That leaves thread affinity as a last fallback, which allows us to prevent scheduling a
166 //! thread on little cores. Android developer docs discourages using thread affinity, since it
167 //! will also negatively affect power consumption if the little cores would have been
168 //! sufficient, but for now this is all we have (pending better official OH APIs, perhaps
169 //! modeled after the android performance hint API).
170
171 use super::linux_sysfs::pin_thread_to_medium_or_large_cpus;
172 use crate::threadboost::{BoostAffinity, ThreadPriority};
173
174 // Constants copied from `qos/qos.h`. Avoids depending on ohos-libqos-sys just for this one function.
175 // See also <https://docs.rs/ohos-libqos-sys/0.1.0/src/ohos_libqos_sys/qos_ffi.rs.html#21>
176 const QOS_USER_INITIATED: i32 = 3;
177 const QOS_USER_INTERACTIVE: i32 = 5;
178
179 #[link(name = "qos")]
180 #[expect(unsafe_code)]
181 unsafe extern "C" {
182 // SAFETY: Calling this function is always safe.
183 safe fn OH_QoS_SetThreadQoS(level: i32) -> i32;
184 }
185
186 pub fn boost_thread(priority: ThreadPriority, boost_affinity: BoostAffinity) {
187 let qos_rc = match priority {
188 ThreadPriority::Elevated => OH_QoS_SetThreadQoS(QOS_USER_INITIATED),
189 ThreadPriority::Critical => OH_QoS_SetThreadQoS(QOS_USER_INTERACTIVE),
190 ThreadPriority::Default => 0,
191 };
192 if qos_rc != 0 {
193 log::warn!("Failed to boost thread priority. `OH_QoS_SetThreadQoS` returned {qos_rc}");
194 }
195 if matches!(boost_affinity, BoostAffinity::Boost) &&
196 let Err(error) = pin_thread_to_medium_or_large_cpus()
197 {
198 log::warn!(
199 "Failed to pin {} to medium or large cpus: {error:?}",
200 std::thread::current().name().unwrap_or("<unnamed>"),
201 );
202 }
203 }
204}
205
206#[cfg(not(any(target_os = "linux", target_env = "ohos")))]
207mod platform {
208 pub fn boost_thread(_: super::ThreadPriority, _: super::BoostAffinity) {}
209}
210
211pub enum ThreadPriority {
212 /// Priority will remain unchanged.
213 Default,
214 /// Increase the thread priority.
215 Elevated,
216 /// Higher priority than `Elevated`, should be used sparingly.
217 Critical,
218}
219
220/// On heterougenous systems (e.g. big.LITTLE architecture), select
221/// whether we should attempt to boost this thread to a larger core.
222/// The exact effect is platform specific, a hint and may be ignored.
223pub enum BoostAffinity {
224 No,
225 /// Prioritize Medium or Large cores and avoid small cores.
226 Boost,
227}
228
229/// Hint to the scheduler that this thread should be prioritised.
230///
231/// No effect if `pref!(perf_thread_boost_enabled)` is `false`.
232///
233/// TODO: The exact API and inner-workings are subject to change:
234/// - This is a hint to servo / the embedder and can be a no-op.
235/// - We might want to pass a thread identifier (enum variant?) so that we
236/// (or the embedder) can customize the optimization based on the thread
237/// without relying on parsing the thread-name.
238/// - Some optimizations like thread affinity selection also affect children threads,
239/// if spawned after this call, so placement can be important.
240#[allow(unsafe_code)]
241pub fn boost_thread(priority: ThreadPriority, boost_affinity: BoostAffinity) {
242 if pref!(perf_thread_boost_enabled) {
243 platform::boost_thread(priority, boost_affinity)
244 }
245}