ktstr/workload/config/
mempolicy.rs

1//! NUMA memory-placement policy types for the workload pipeline.
2//!
3//! Holds [`MemPolicy`] (the `MPOL_*` enum the worker process
4//! applies via `set_mempolicy(2)` after fork) and [`MpolFlags`]
5//! (the `MPOL_F_*` flag bag OR'd into the mode argument). Validation
6//! lives on [`MemPolicy::validate`] — each variant that takes a
7//! node set rejects empty sets with an actionable diagnostic.
8
9use std::collections::BTreeSet;
10
11/// NUMA memory placement policy for worker processes.
12///
13/// Applied via `set_mempolicy(2)` after fork, before the work loop.
14/// Maps to Linux `MPOL_*` constants. When `Default`, no syscall is
15/// made (inherits the parent's policy).
16///
17/// Optional [`MpolFlags`] modify behavior (e.g. `STATIC_NODES` to
18/// keep the nodemask absolute across cpuset changes).
19#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
20#[serde(rename_all = "snake_case")]
21pub enum MemPolicy {
22    /// Inherit the parent process's memory policy (no syscall).
23    #[default]
24    Default,
25    /// Allocate only from the specified NUMA nodes (`MPOL_BIND`).
26    Bind(BTreeSet<usize>),
27    /// Prefer allocations from the specified node, falling back to
28    /// others when the preferred node is full (`MPOL_PREFERRED`).
29    Preferred(usize),
30    /// Interleave allocations round-robin across the specified nodes
31    /// (`MPOL_INTERLEAVE`).
32    Interleave(BTreeSet<usize>),
33    /// Prefer the nearest node to the CPU where the allocation occurs
34    /// (`MPOL_LOCAL`). No nodemask.
35    Local,
36    /// Prefer allocations from any of the specified nodes, falling back
37    /// to others when all preferred nodes are full
38    /// (`MPOL_PREFERRED_MANY`, kernel 5.15+).
39    PreferredMany(BTreeSet<usize>),
40    /// Weighted interleave across the specified nodes. Page distribution
41    /// is proportional to per-node weights set via
42    /// `/sys/kernel/mm/mempolicy/weighted_interleave/nodeN`
43    /// (`MPOL_WEIGHTED_INTERLEAVE`, kernel 6.9+).
44    WeightedInterleave(BTreeSet<usize>),
45}
46
47/// Optional mode flags for `set_mempolicy(2)`.
48///
49/// OR'd into the mode argument. See `MPOL_F_*` in
50/// `include/uapi/linux/mempolicy.h`.
51#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
52#[serde(transparent)]
53pub struct MpolFlags(u32);
54
55impl MpolFlags {
56    /// No flags.
57    pub const NONE: Self = Self(0);
58    /// `MPOL_F_STATIC_NODES` (1 << 15): nodemask is absolute, not
59    /// remapped when the task's cpuset changes.
60    pub const STATIC_NODES: Self = Self(1 << 15);
61    /// `MPOL_F_RELATIVE_NODES` (1 << 14): nodemask is relative to
62    /// the task's current cpuset.
63    pub const RELATIVE_NODES: Self = Self(1 << 14);
64    /// `MPOL_F_NUMA_BALANCING` (1 << 13): enable NUMA balancing
65    /// optimization for this policy.
66    pub const NUMA_BALANCING: Self = Self(1 << 13);
67
68    /// Test-only raw-bit constructor. Lets unknown-bit guards
69    /// (e.g. `validate_mempolicy_cpuset` in src/scenario/ops/mod.rs)
70    /// be tested against bit patterns that are not reachable via
71    /// the documented `STATIC_NODES | RELATIVE_NODES |
72    /// NUMA_BALANCING` constants. Production callers must use the
73    /// named constants + `union` / `BitOr` so the model stays in
74    /// sync with the validator's known-bits mask.
75    #[cfg(test)]
76    pub(crate) const fn from_bits_for_test(bits: u32) -> Self {
77        Self(bits)
78    }
79
80    /// Combine two flag sets.
81    pub const fn union(self, other: Self) -> Self {
82        Self(self.0 | other.0)
83    }
84
85    /// Raw flag bits for passing to the syscall.
86    pub const fn bits(self) -> u32 {
87        self.0
88    }
89
90    /// Whether every bit in `other` is set in `self`.
91    ///
92    /// Set-theoretic, not syntactic: `contains(NONE)` returns `true`
93    /// for any `self` (vacuous truth — the empty set is a subset of
94    /// everything). Callers who want "has a non-empty intersection
95    /// with `other`" must compare `self.bits() & other.bits() != 0`
96    /// explicitly; using `contains` for that query silently returns
97    /// `true` when the operand is `NONE` regardless of `self`.
98    pub const fn contains(self, other: Self) -> bool {
99        (self.0 & other.0) == other.0
100    }
101}
102
103impl std::ops::BitOr for MpolFlags {
104    type Output = Self;
105    /// Delegates to [`MpolFlags::union`] so the bitwise-OR logic
106    /// lives in one place. `union` is `const fn` (usable in
107    /// const contexts like `const` initializers); `BitOr::bitor`
108    /// cannot currently be `const` on stable, so keeping both
109    /// entry points is necessary, but they must never diverge.
110    fn bitor(self, rhs: Self) -> Self {
111        self.union(rhs)
112    }
113}
114
115impl MemPolicy {
116    /// Construct a `Bind` policy from any iterator of NUMA node IDs.
117    ///
118    /// Accepts arrays, ranges, `Vec`, `BTreeSet`, or any `IntoIterator<Item = usize>`.
119    pub fn bind(nodes: impl IntoIterator<Item = usize>) -> Self {
120        MemPolicy::Bind(nodes.into_iter().collect())
121    }
122
123    /// Construct a `Preferred` policy for a single NUMA node.
124    pub const fn preferred(node: usize) -> Self {
125        MemPolicy::Preferred(node)
126    }
127
128    /// Construct an `Interleave` policy from any iterator of NUMA node IDs.
129    ///
130    /// Accepts arrays, ranges, `Vec`, `BTreeSet`, or any `IntoIterator<Item = usize>`.
131    pub fn interleave(nodes: impl IntoIterator<Item = usize>) -> Self {
132        MemPolicy::Interleave(nodes.into_iter().collect())
133    }
134
135    /// Construct a `PreferredMany` policy from any iterator of NUMA node IDs.
136    pub fn preferred_many(nodes: impl IntoIterator<Item = usize>) -> Self {
137        MemPolicy::PreferredMany(nodes.into_iter().collect())
138    }
139
140    /// Construct a `WeightedInterleave` policy from any iterator of NUMA node IDs.
141    pub fn weighted_interleave(nodes: impl IntoIterator<Item = usize>) -> Self {
142        MemPolicy::WeightedInterleave(nodes.into_iter().collect())
143    }
144
145    /// NUMA node IDs referenced by this policy.
146    ///
147    /// Returns the node set for `Bind`, `Interleave`, `PreferredMany`,
148    /// and `WeightedInterleave`, a single-element set for `Preferred`,
149    /// and an empty set for `Default`/`Local`.
150    pub fn node_set(&self) -> BTreeSet<usize> {
151        match self {
152            MemPolicy::Default | MemPolicy::Local => BTreeSet::new(),
153            MemPolicy::Bind(nodes)
154            | MemPolicy::Interleave(nodes)
155            | MemPolicy::PreferredMany(nodes)
156            | MemPolicy::WeightedInterleave(nodes) => nodes.clone(),
157            MemPolicy::Preferred(node) => [*node].into_iter().collect(),
158        }
159    }
160
161    /// Validate that this policy's node set is non-empty where required.
162    ///
163    /// Returns `Err` with a description when a node-set-bearing policy
164    /// has an empty set. Each diagnostic names the offending variant,
165    /// the constraint ("at least one NUMA node"), and the actionable
166    /// fix — both the matching constructor and the recommended
167    /// fallbacks for "I don't want node-restricted placement" — so the
168    /// operator sees the next mouse-click inline rather than having to
169    /// `cargo doc --open` to discover the constructor menu. Mirrors the
170    /// workers_pct rejection trailer at `WorkSpec::resolve_workers_pct`.
171    pub fn validate(&self) -> std::result::Result<(), String> {
172        match self {
173            MemPolicy::Default | MemPolicy::Local => Ok(()),
174            MemPolicy::Preferred(_) => Ok(()),
175            MemPolicy::Bind(nodes) if nodes.is_empty() => Err(
176                "Bind policy requires at least one NUMA node — \
177                 use `MemPolicy::bind([node, ...])` with one or more node IDs, \
178                 or `MemPolicy::Default` / `MemPolicy::Local` for non-bound placement"
179                    .into(),
180            ),
181            MemPolicy::Interleave(nodes) if nodes.is_empty() => Err(
182                "Interleave policy requires at least one NUMA node — \
183                 use `MemPolicy::interleave([node, ...])` with one or more node IDs, \
184                 or `MemPolicy::Default` for unrestricted placement"
185                    .into(),
186            ),
187            MemPolicy::PreferredMany(nodes) if nodes.is_empty() => Err(
188                "PreferredMany policy requires at least one NUMA node — \
189                 use `MemPolicy::preferred_many([node, ...])` with one or more node IDs, \
190                 or `MemPolicy::Preferred(node)` / `MemPolicy::Default` / `MemPolicy::Local`"
191                    .into(),
192            ),
193            MemPolicy::WeightedInterleave(nodes) if nodes.is_empty() => Err(
194                "WeightedInterleave policy requires at least one NUMA node — \
195                 use `MemPolicy::weighted_interleave([node, ...])` with one or more node IDs, \
196                 or `MemPolicy::interleave([node, ...])` / `MemPolicy::Default` for unweighted placement"
197                    .into(),
198            ),
199            _ => Ok(()),
200        }
201    }
202}