Skip to main content

tenferro_cpu/
context.rs

1use std::env;
2use std::sync::Arc;
3
4use crate::{Error, Result};
5
6/// Reusable CPU execution context carrying CPU parallelism policy.
7///
8/// `CpuContext` stores the requested thread count as a kernel-level
9/// parallelism hint and owns the Rayon pool used by multi-threaded CPU work.
10///
11/// # Examples
12///
13/// ```
14/// use tenferro_cpu::CpuContext;
15///
16/// let ctx = CpuContext::with_threads(1).unwrap();
17/// let value = ctx.install(|| 1 + 1);
18/// assert_eq!(value, 2);
19/// assert_eq!(ctx.num_threads(), 1);
20/// ```
21#[derive(Clone, Debug)]
22pub struct CpuContext {
23    num_threads: usize,
24    pool: Option<Arc<rayon::ThreadPool>>,
25}
26
27impl CpuContext {
28    /// Create a CPU context from `RAYON_NUM_THREADS`, or fall back to a
29    /// single-threaded context with a stderr warning when validation fails.
30    ///
31    /// # Examples
32    ///
33    /// ```
34    /// use tenferro_cpu::CpuContext;
35    ///
36    /// let ctx = CpuContext::from_env();
37    /// assert!(ctx.num_threads() >= 1);
38    /// ```
39    pub fn from_env() -> Self {
40        Self::try_from_env().unwrap_or_else(|err| {
41            eprintln!(
42                "tenferro_cpu: falling back to single-threaded CPU context after configuration error: {err}"
43            );
44            Self::single_threaded()
45        })
46    }
47
48    /// Try to create a CPU context from `RAYON_NUM_THREADS`.
49    ///
50    /// # Examples
51    ///
52    /// ```
53    /// use tenferro_cpu::CpuContext;
54    ///
55    /// let ctx = CpuContext::try_from_env()
56    ///     .unwrap_or_else(|_| CpuContext::with_threads(1).unwrap());
57    /// assert!(ctx.num_threads() >= 1);
58    /// ```
59    pub fn try_from_env() -> Result<Self> {
60        match env::var("RAYON_NUM_THREADS") {
61            Ok(value) => {
62                let num_threads = value.parse::<usize>().map_err(|err| Error::InvalidConfig {
63                    op: "CpuContext::try_from_env",
64                    message: format!("invalid RAYON_NUM_THREADS value {value:?}: {err}"),
65                })?;
66                Self::with_threads(num_threads).map_err(|err| match err {
67                    Error::InvalidConfig { message, .. } => Error::InvalidConfig {
68                        op: "CpuContext::try_from_env",
69                        message: format!("invalid RAYON_NUM_THREADS value {value:?}: {message}"),
70                    },
71                    err => err,
72                })
73            }
74            Err(env::VarError::NotPresent) => {
75                Self::with_threads(super::affinity::available_parallelism())
76            }
77            Err(err) => Err(Error::InvalidConfig {
78                op: "CpuContext::try_from_env",
79                message: format!("failed to read RAYON_NUM_THREADS: {err}"),
80            }),
81        }
82    }
83
84    /// Create a CPU context with a fixed parallelism hint.
85    ///
86    /// # Examples
87    ///
88    /// ```
89    /// use tenferro_cpu::CpuContext;
90    ///
91    /// let ctx = CpuContext::with_threads(2).unwrap();
92    /// assert_eq!(ctx.num_threads(), 2);
93    /// ```
94    ///
95    /// # Errors
96    ///
97    /// Returns an error when `num_threads` is zero or Rayon rejects the pool.
98    pub fn with_threads(num_threads: usize) -> Result<Self> {
99        if num_threads == 0 {
100            return Err(Error::InvalidConfig {
101                op: "CpuContext::with_threads",
102                message: "thread count must be at least 1".into(),
103            });
104        }
105        let pool = if num_threads == 1 {
106            None
107        } else {
108            Some(Arc::new(
109                rayon::ThreadPoolBuilder::new()
110                    .num_threads(num_threads)
111                    .build()
112                    .map_err(|err| Error::InvalidConfig {
113                        op: "CpuContext::with_threads",
114                        message: format!("failed to build CPU thread pool: {err}"),
115                    })?,
116            ))
117        };
118        Ok(Self { num_threads, pool })
119    }
120
121    fn single_threaded() -> Self {
122        Self {
123            num_threads: 1,
124            pool: None,
125        }
126    }
127
128    /// Return this context's CPU parallelism hint.
129    ///
130    /// # Examples
131    ///
132    /// ```
133    /// use tenferro_cpu::CpuContext;
134    ///
135    /// let ctx = CpuContext::with_threads(2).unwrap();
136    /// assert_eq!(ctx.num_threads(), 2);
137    /// ```
138    pub fn num_threads(&self) -> usize {
139        self.num_threads
140    }
141
142    /// Run a closure inside this context's CPU execution scope.
143    ///
144    /// # Examples
145    ///
146    /// ```
147    /// use tenferro_cpu::CpuContext;
148    ///
149    /// let ctx = CpuContext::with_threads(1).unwrap();
150    /// let value = ctx.install(|| 1 + 1);
151    /// assert_eq!(value, 2);
152    /// ```
153    pub fn install<R: Send>(&self, op: impl FnOnce() -> R + Send) -> R {
154        match &self.pool {
155            Some(pool) => pool.install(op),
156            None => op(),
157        }
158    }
159
160    /// Return the faer parallelism policy for work run inside this context.
161    ///
162    /// `Par::rayon(0)` is intentional for multi-threaded contexts: faer reads
163    /// `rayon::current_num_threads()`, so calls made under [`Self::install`]
164    /// inherit this context's Rayon pool size.
165    #[cfg(feature = "cpu-faer")]
166    #[doc(hidden)]
167    pub fn faer_par(&self) -> faer::Par {
168        if self.num_threads == 1 {
169            faer::Par::Seq
170        } else {
171            faer::Par::rayon(0)
172        }
173    }
174
175    #[cfg(feature = "cpu-faer")]
176    #[doc(hidden)]
177    pub fn faer_seq(&self) -> faer::Par {
178        faer::Par::Seq
179    }
180}
181
182#[cfg(test)]
183mod tests;