Skip to main content

tuwunel_core/utils/sys/
compute.rs

1//! System utilities related to compute/processing
2
3use std::{cell::Cell, fmt::Debug, path::PathBuf, sync::LazyLock};
4
5use crate::{Result, is_equal_to};
6
7type Id = usize;
8
9type Mask = u128;
10type Masks = [Mask; MASK_BITS];
11
12const MASK_BITS: usize = CORES_MAX;
13
14/// Maximum number of cores we support; for now limited to bits of our mask
15/// integral.
16pub const CORES_MAX: usize = 128;
17
18/// The mask of logical cores available to the process (at startup).
19static CORES_AVAILABLE: LazyLock<Mask> = LazyLock::new(|| into_mask(query_cores_available()));
20
21/// Stores the mask of logical-cores with thread/HT/SMT association. Each group
22/// here makes up a physical-core.
23static SMT_TOPOLOGY: LazyLock<Masks> = LazyLock::new(init_smt_topology);
24
25/// Stores the mask of logical-core associations on a node/socket. Bits are set
26/// for all logical cores within all physical cores of the node.
27static NODE_TOPOLOGY: LazyLock<Masks> = LazyLock::new(init_node_topology);
28
29thread_local! {
30	/// Tracks the affinity for this thread. This is updated when affinities
31	/// are set via our set_affinity() interface.
32	static CORE_AFFINITY: Cell<Mask> = const { Cell::new(0) };
33}
34
35/// Set the core affinity for this thread. The ID should be listed in
36/// CORES_AVAILABLE. Empty input is a no-op; prior affinity unchanged.
37#[tracing::instrument(
38	level = "debug",
39	skip_all,
40	fields(
41		id = ?std::thread::current().id(),
42		name = %std::thread::current().name().unwrap_or("None"),
43		set = ?ids.clone().collect::<Vec<_>>(),
44		CURRENT = %format!("[b{:b}]", CORE_AFFINITY.get()),
45		AVAILABLE = %format!("[b{:b}]", *CORES_AVAILABLE),
46	),
47)]
48pub fn set_affinity<I>(mut ids: I)
49where
50	I: Iterator<Item = Id> + Clone + Debug,
51{
52	use core_affinity::{CoreId, set_each_for_current, set_for_current};
53
54	let n = ids.clone().count();
55	let mask: Mask = ids.clone().fold(0, |mask, id| {
56		debug_assert!(is_core_available(id), "setting affinity to unavailable core");
57		mask | (1 << id)
58	});
59
60	if n > 1 {
61		set_each_for_current(ids.map(|id| CoreId { id }));
62	} else if n > 0 {
63		set_for_current(CoreId { id: ids.next().expect("n > 0") });
64	}
65
66	if mask.count_ones() > 0 {
67		CORE_AFFINITY.replace(mask);
68	}
69}
70
71/// Get the core affinity for this thread.
72pub fn get_affinity() -> impl Iterator<Item = Id> {
73	CORE_AFFINITY
74		.get()
75		.ne(&0)
76		.then_some(from_mask(CORE_AFFINITY.get()))
77		.or_else(|| Some(from_mask(*CORES_AVAILABLE)))
78		.into_iter()
79		.flatten()
80}
81
82/// List the cores sharing SMT-tier resources
83pub fn smt_siblings() -> impl Iterator<Item = Id> {
84	from_mask(get_affinity().fold(0_u128, |mask, id| {
85		mask | SMT_TOPOLOGY
86			.get(id)
87			.expect("ID must not exceed max cpus")
88	}))
89}
90
91/// List the cores sharing Node-tier resources relative to this threads current
92/// affinity.
93pub fn node_siblings() -> impl Iterator<Item = Id> {
94	from_mask(get_affinity().fold(0_u128, |mask, id| {
95		mask | NODE_TOPOLOGY
96			.get(id)
97			.expect("Id must not exceed max cpus")
98	}))
99}
100
101/// Get the cores sharing SMT resources relative to id.
102#[inline]
103pub fn smt_affinity(id: Id) -> impl Iterator<Item = Id> {
104	from_mask(
105		*SMT_TOPOLOGY
106			.get(id)
107			.expect("ID must not exceed max cpus"),
108	)
109}
110
111/// Get the cores sharing Node resources relative to id.
112#[inline]
113pub fn node_affinity(id: Id) -> impl Iterator<Item = Id> {
114	from_mask(
115		*NODE_TOPOLOGY
116			.get(id)
117			.expect("ID must not exceed max cpus"),
118	)
119}
120
121/// Get the number of threads which could execute in parallel based on hardware
122/// constraints of this system.
123#[cfg(not(target_os = "openbsd"))]
124#[inline]
125#[must_use]
126pub fn available_parallelism() -> usize { cores_available().count() }
127
128#[cfg(target_os = "openbsd")]
129#[inline]
130#[must_use]
131pub fn available_parallelism() -> usize { num_cpus::get() }
132
133/// Gets the ID of the nth core available. This bijects our sequence of cores to
134/// actual ID's which may have gaps for cores which are not available.
135#[inline]
136#[must_use]
137pub fn nth_core_available(i: usize) -> Option<Id> { cores_available().nth(i) }
138
139/// Determine if core (by id) is available to the process.
140#[inline]
141#[must_use]
142pub fn is_core_available(id: Id) -> bool { cores_available().any(is_equal_to!(id)) }
143
144/// Get the list of cores available. The values were recorded at program start.
145#[inline]
146pub fn cores_available() -> impl Iterator<Item = Id> { from_mask(*CORES_AVAILABLE) }
147
148#[cfg(target_os = "linux")]
149#[inline]
150pub fn getcpu() -> Result<usize> {
151	use crate::{Error, utils::math};
152
153	// SAFETY: This is part of an interface with many low-level calls taking many
154	// raw params, but it's unclear why this specific call is unsafe. Nevertheless
155	// the value obtained here is semantically unsafe because it can change on the
156	// instruction boundary trailing its own acquisition and also any other time.
157	let ret: i32 = unsafe { libc::sched_getcpu() };
158
159	#[cfg(target_os = "linux")]
160	// SAFETY: On modern linux systems with a vdso if we can optimize away the branch checking
161	// for error (see getcpu(2)) then this system call becomes a memory access.
162	unsafe {
163		std::hint::assert_unchecked(ret >= 0);
164	};
165
166	if ret == -1 {
167		return Err(Error::from_errno());
168	}
169
170	math::try_into(ret)
171}
172
173#[cfg(not(target_os = "linux"))]
174#[inline]
175pub fn getcpu() -> Result<usize> { Err(crate::Error::Io(std::io::ErrorKind::Unsupported.into())) }
176
177#[cfg(not(target_os = "openbsd"))]
178fn query_cores_available() -> impl Iterator<Item = Id> {
179	core_affinity::get_core_ids()
180		.unwrap_or_default()
181		.into_iter()
182		.map(|core_id| core_id.id)
183}
184
185#[cfg(target_os = "openbsd")]
186fn query_cores_available() -> impl Iterator<Item = Id> { 0..num_cpus::get() }
187
188fn init_smt_topology() -> [Mask; MASK_BITS] { [Mask::default(); MASK_BITS] }
189
190fn init_node_topology() -> [Mask; MASK_BITS] { [Mask::default(); MASK_BITS] }
191
192fn into_mask<I>(ids: I) -> Mask
193where
194	I: Iterator<Item = Id>,
195{
196	ids.inspect(|&id| {
197		debug_assert!(id < MASK_BITS, "Core ID must be < Mask::BITS at least for now");
198	})
199	.fold(Mask::default(), |mask, id| mask | (1 << id))
200}
201
202fn from_mask(v: Mask) -> impl Iterator<Item = Id> {
203	(0..MASK_BITS).filter(move |&i| (v & (1 << i)) != 0)
204}
205
206fn _sys_path(id: usize, suffix: &str) -> PathBuf {
207	format!("/sys/devices/system/cpu/cpu{id}/{suffix}").into()
208}