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