scx_wd40/
tuner.rs

1// Copyright (c) Meta Platforms, Inc. and affiliates.
2
3// This software may be used and distributed according to the terms of the
4// GNU General Public License version 2.
5use std::collections::BTreeMap;
6use std::sync::Arc;
7
8use ::fb_procfs as procfs;
9use anyhow::anyhow;
10use anyhow::bail;
11use anyhow::Result;
12use scx_utils::Cpumask;
13
14use crate::sub_or_zero;
15use crate::update_bpf_mask;
16use crate::BpfSkel;
17use crate::DomainGroup;
18
19fn calc_util(curr: &procfs::CpuStat, prev: &procfs::CpuStat) -> Result<f64> {
20    match (curr, prev) {
21        (
22            procfs::CpuStat {
23                user_usec: Some(curr_user),
24                nice_usec: Some(curr_nice),
25                system_usec: Some(curr_system),
26                idle_usec: Some(curr_idle),
27                iowait_usec: Some(curr_iowait),
28                irq_usec: Some(curr_irq),
29                softirq_usec: Some(curr_softirq),
30                stolen_usec: Some(curr_stolen),
31                ..
32            },
33            procfs::CpuStat {
34                user_usec: Some(prev_user),
35                nice_usec: Some(prev_nice),
36                system_usec: Some(prev_system),
37                idle_usec: Some(prev_idle),
38                iowait_usec: Some(prev_iowait),
39                irq_usec: Some(prev_irq),
40                softirq_usec: Some(prev_softirq),
41                stolen_usec: Some(prev_stolen),
42                ..
43            },
44        ) => {
45            let idle_usec = sub_or_zero(curr_idle, prev_idle);
46            let iowait_usec = sub_or_zero(curr_iowait, prev_iowait);
47            let user_usec = sub_or_zero(curr_user, prev_user);
48            let system_usec = sub_or_zero(curr_system, prev_system);
49            let nice_usec = sub_or_zero(curr_nice, prev_nice);
50            let irq_usec = sub_or_zero(curr_irq, prev_irq);
51            let softirq_usec = sub_or_zero(curr_softirq, prev_softirq);
52            let stolen_usec = sub_or_zero(curr_stolen, prev_stolen);
53
54            let busy_usec =
55                user_usec + system_usec + nice_usec + irq_usec + softirq_usec + stolen_usec;
56            let total_usec = idle_usec + busy_usec + iowait_usec;
57            if total_usec > 0 {
58                Ok(((busy_usec as f64) / (total_usec as f64)).clamp(0.0, 1.0))
59            } else {
60                Ok(1.0)
61            }
62        }
63        _ => {
64            bail!("Missing stats in cpustat");
65        }
66    }
67}
68
69pub struct Tuner {
70    pub direct_greedy_mask: Cpumask,
71    pub kick_greedy_mask: Cpumask,
72    pub fully_utilized: bool,
73    pub slice_ns: u64,
74    underutil_slice_ns: u64,
75    overutil_slice_ns: u64,
76    dom_group: Arc<DomainGroup>,
77    direct_greedy_under: f64,
78    kick_greedy_under: f64,
79    proc_reader: procfs::ProcReader,
80    prev_cpu_stats: BTreeMap<u32, procfs::CpuStat>,
81}
82
83impl Tuner {
84    pub fn new(
85        dom_group: Arc<DomainGroup>,
86        direct_greedy_under: f64,
87        kick_greedy_under: f64,
88        underutil_slice_ns: u64,
89        overutil_slice_ns: u64,
90    ) -> Result<Self> {
91        let proc_reader = procfs::ProcReader::new();
92        let prev_cpu_stats = proc_reader
93            .read_stat()?
94            .cpus_map
95            .ok_or_else(|| anyhow!("Expected cpus_map to exist"))?;
96
97        Ok(Self {
98            direct_greedy_mask: Cpumask::new(),
99            kick_greedy_mask: Cpumask::new(),
100            fully_utilized: false,
101            direct_greedy_under: direct_greedy_under / 100.0,
102            kick_greedy_under: kick_greedy_under / 100.0,
103            proc_reader,
104            prev_cpu_stats,
105            slice_ns: underutil_slice_ns,
106            underutil_slice_ns,
107            overutil_slice_ns,
108            dom_group,
109        })
110    }
111
112    /// Apply a step in the Tuner by:
113    ///
114    /// 1. Recording CPU stats from procfs
115    /// 2. Calculating current per-domain and host-wide utilization
116    /// 3. Updating direct_greedy_under and kick_greedy_under cpumasks according
117    ///    to the observed utilization
118    pub fn step(&mut self, skel: &mut BpfSkel) -> Result<()> {
119        let curr_cpu_stats = self
120            .proc_reader
121            .read_stat()?
122            .cpus_map
123            .ok_or_else(|| anyhow!("Expected cpus_map to exist"))?;
124        let mut dom_util_sum = vec![0.0f64; self.dom_group.nr_doms()];
125
126        let mut avg_util = 0.0f64;
127        for (dom_id, dom) in self.dom_group.doms().iter() {
128            for cpu in dom.mask().iter() {
129                let cpu32 = cpu as u32;
130                if let (Some(curr), Some(prev)) =
131                    (curr_cpu_stats.get(&cpu32), self.prev_cpu_stats.get(&cpu32))
132                {
133                    let util = calc_util(curr, prev)?;
134                    dom_util_sum[*dom_id] += util;
135                    avg_util += util;
136                }
137            }
138        }
139        avg_util /= self.dom_group.weight() as f64;
140        self.fully_utilized = avg_util >= 0.99999;
141
142        self.direct_greedy_mask.clear_all();
143        self.kick_greedy_mask.clear_all();
144
145        for (dom_id, dom) in self.dom_group.doms().iter() {
146            // Calculate the domain avg util. If there are no active CPUs,
147            // it doesn't really matter. Go with 0.0 as that's less likely
148            // to confuse users.
149            let util = match dom.weight() {
150                0 => 0.0,
151                nr => dom_util_sum[*dom_id] / nr as f64,
152            };
153
154            let enable_direct =
155                self.direct_greedy_under > 0.99999 || util < self.direct_greedy_under;
156            let enable_kick = self.kick_greedy_under > 0.99999 || util < self.kick_greedy_under;
157
158            if enable_direct {
159                self.direct_greedy_mask |= &dom.mask();
160
161                let domc = dom.ctx().unwrap();
162
163                update_bpf_mask(domc.direct_greedy_cpumask, &dom.mask())?;
164            }
165
166            if enable_kick {
167                self.kick_greedy_mask |= &dom.mask();
168            }
169        }
170
171        let bss_data = skel.maps.bss_data.as_mut().unwrap();
172        update_bpf_mask(bss_data.direct_greedy_cpumask, &self.direct_greedy_mask)?;
173        update_bpf_mask(bss_data.kick_greedy_cpumask, &self.kick_greedy_mask)?;
174
175        if self.fully_utilized {
176            self.slice_ns = self.overutil_slice_ns;
177        } else {
178            self.slice_ns = self.underutil_slice_ns;
179        }
180
181        bss_data.slice_ns = self.slice_ns;
182
183        // For domain in domains
184        // Get the dom_ctx and the internal mask
185        // And the direct greedy cpumask with the domain mask
186
187        self.prev_cpu_stats = curr_cpu_stats;
188
189        Ok(())
190    }
191}