Skip to main content

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}