1use std::env;
2use std::num::NonZeroUsize;
3use std::sync::Arc;
4
5use rayon::prelude::*;
6use thiserror::Error as ThisError;
7
8use crate::affinity::{CpuAffinityError, SystemThreadAffinity, ThreadAffinity};
9use crate::arbiter::{
10 current_execution_owner, register_worker_execution_scope, worker_execution_scope_matches,
11 ExecutionScopeState,
12};
13use crate::domain_executor::{
14 CpuDomainExecutor, CpuDomainExecutorCapabilities, CpuDomainExecutorError, CpuExecutorAffinity,
15 CpuExecutorReentrancy, CpuExecutorShutdown, CpuInnerParallelism, ScopedCpuJob, ScopedCpuJobs,
16};
17use crate::{CpuId, CpuSet, Error, ErrorKind, Result, ValidationKind};
18
19#[derive(Debug, ThisError)]
30pub enum CpuContextError {
31 #[error("thread count must be at least 1")]
33 InvalidThreadCount,
34 #[error("requested {workers} workers for only {cpus} assigned CPUs")]
36 TooManyWorkers {
37 workers: usize,
39 cpus: usize,
41 },
42 #[error("failed to build pinned CPU thread pool: {source}")]
44 PoolBuild {
45 #[source]
47 source: rayon::ThreadPoolBuildError,
48 },
49 #[error("failed to pin worker {worker} to CPU {cpu}: {source}")]
51 WorkerPinning {
52 worker: usize,
54 cpu: CpuId,
56 #[source]
58 source: CpuAffinityError,
59 },
60 #[error("worker startup channel closed before all workers reported: {source}")]
62 WorkerStartupClosed {
63 #[source]
65 source: std::sync::mpsc::RecvError,
66 },
67}
68
69#[derive(Clone, Debug)]
85pub struct CpuContext {
86 num_threads: usize,
87 pool: Option<Arc<rayon::ThreadPool>>,
88 pinned_cpus: Option<CpuSet>,
89 execution_scope: Arc<ExecutionScopeState>,
90 #[cfg(test)]
91 executor_install_calls: Arc<std::sync::atomic::AtomicUsize>,
92}
93
94impl CpuContext {
95 pub fn from_env() -> Self {
107 Self::try_from_env().unwrap_or_else(|err| {
108 eprintln!(
109 "tenferro_cpu: falling back to single-threaded CPU context after configuration error: {err}"
110 );
111 Self::single_threaded()
112 })
113 }
114
115 pub fn try_from_env() -> Result<Self> {
132 match env::var("RAYON_NUM_THREADS") {
133 Ok(value) => {
134 let num_threads = value.parse::<usize>().map_err(|err| {
135 Error::extension(
136 "CpuContext::try_from_env",
137 "cpu",
138 ErrorKind::Validation(ValidationKind::InvalidArgument),
139 err,
140 )
141 })?;
142 Self::with_threads(num_threads).map_err(|err| match err {
143 Error::Validation { source, .. } => {
144 Error::validation("CpuContext::try_from_env", source)
145 }
146 err => err,
147 })
148 }
149 Err(env::VarError::NotPresent) => {
150 Self::with_threads(super::affinity::available_parallelism())
151 }
152 Err(err) => Err(Error::extension(
153 "CpuContext::try_from_env",
154 "cpu",
155 ErrorKind::Validation(ValidationKind::InvalidArgument),
156 err,
157 )),
158 }
159 }
160
161 pub fn with_threads(num_threads: usize) -> Result<Self> {
178 if num_threads == 0 {
179 return Err(Error::invalid_argument(
180 "CpuContext::with_threads",
181 "configuration",
182 "thread count must be at least 1",
183 ));
184 }
185 let execution_scope = Arc::new(ExecutionScopeState::default());
186 let pool = if num_threads == 1 {
187 None
188 } else {
189 let (startup_tx, startup_rx) = std::sync::mpsc::channel();
190 let worker_scope = Arc::clone(&execution_scope);
191 let pool = rayon::ThreadPoolBuilder::new()
192 .num_threads(num_threads)
193 .start_handler(move |_| {
194 register_worker_execution_scope(Arc::clone(&worker_scope));
195 let _ = startup_tx.send(());
196 })
197 .build()
198 .map_err(|source| Error::backend_source("CpuContext::with_threads", source))?;
199 for _ in 0..num_threads {
200 startup_rx
201 .recv()
202 .map_err(|source| Error::backend_source("CpuContext::with_threads", source))?;
203 }
204 Some(Arc::new(pool))
205 };
206 Ok(Self {
207 num_threads,
208 pool,
209 pinned_cpus: None,
210 execution_scope,
211 #[cfg(test)]
212 executor_install_calls: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
213 })
214 }
215
216 pub fn with_pinned_cpus(
240 cpus: CpuSet,
241 num_threads: usize,
242 ) -> std::result::Result<Self, CpuContextError> {
243 Self::with_pinned_cpus_using(cpus, num_threads, SystemThreadAffinity)
244 }
245
246 pub(crate) fn with_pinned_cpus_using<A: ThreadAffinity>(
247 cpus: CpuSet,
248 num_threads: usize,
249 affinity: A,
250 ) -> std::result::Result<Self, CpuContextError> {
251 if num_threads == 0 {
252 return Err(CpuContextError::InvalidThreadCount);
253 }
254 if num_threads > cpus.len() {
255 return Err(CpuContextError::TooManyWorkers {
256 workers: num_threads,
257 cpus: cpus.len(),
258 });
259 }
260
261 let execution_scope = Arc::new(ExecutionScopeState::default());
262 let assigned_cpus = Arc::new(select_worker_cpus(&cpus, num_threads));
263 let (startup_tx, startup_rx) = std::sync::mpsc::channel();
264 let pool_assigned_cpus = Arc::clone(&assigned_cpus);
265 let worker_scope = Arc::clone(&execution_scope);
266 let pool = rayon::ThreadPoolBuilder::new()
267 .num_threads(num_threads)
268 .spawn_handler(move |thread| {
269 let worker = thread.index();
270 let cpu = pool_assigned_cpus[worker];
271 let startup_tx = startup_tx.clone();
272 let affinity = affinity.clone();
273 let worker_scope = Arc::clone(&worker_scope);
274 std::thread::Builder::new()
275 .name(format!("tenferro-cpu-{cpu}"))
276 .spawn(move || {
277 register_worker_execution_scope(Arc::clone(&worker_scope));
278 let result = affinity.pin_current(cpu).and_then(|observed| {
279 (observed.len() == 1 && observed.contains(cpu))
280 .then_some(())
281 .ok_or_else(|| CpuAffinityError::Verification {
282 observed: observed.as_slice().to_vec(),
283 })
284 });
285 let _ = startup_tx.send((worker, cpu, result));
286 thread.run();
287 })
288 .map(|_| ())
289 })
290 .build()
291 .map_err(|source| CpuContextError::PoolBuild { source })?;
292 let pool = Arc::new(pool);
293 for _ in 0..num_threads {
294 let (worker, cpu, result) = startup_rx
295 .recv()
296 .map_err(|source| CpuContextError::WorkerStartupClosed { source })?;
297 if let Err(source) = result {
298 return Err(CpuContextError::WorkerPinning {
299 worker,
300 cpu,
301 source,
302 });
303 }
304 }
305 Ok(Self {
306 num_threads,
307 pool: Some(pool),
308 pinned_cpus: Some(cpus),
309 execution_scope,
310 #[cfg(test)]
311 executor_install_calls: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
312 })
313 }
314
315 fn single_threaded() -> Self {
316 Self {
317 num_threads: 1,
318 pool: None,
319 pinned_cpus: None,
320 execution_scope: Arc::new(ExecutionScopeState::default()),
321 #[cfg(test)]
322 executor_install_calls: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
323 }
324 }
325
326 pub fn num_threads(&self) -> usize {
337 self.num_threads
338 }
339
340 pub fn pinned_cpus(&self) -> Option<&CpuSet> {
354 self.pinned_cpus.as_ref()
355 }
356
357 pub fn install<R: Send>(&self, op: impl FnOnce() -> R + Send) -> R {
369 match &self.pool {
370 Some(pool) => pool.install(op),
371 None => op(),
372 }
373 }
374
375 pub(crate) fn install_if_needed<R: Send>(&self, op: impl FnOnce() -> R + Send) -> R {
376 if self.pool.is_some() && worker_execution_scope_matches(&self.execution_scope) {
377 op()
378 } else {
379 self.install(op)
380 }
381 }
382
383 #[cfg(test)]
384 pub(crate) fn owns_current_worker_for_test(&self) -> bool {
385 worker_execution_scope_matches(&self.execution_scope)
386 }
387
388 #[cfg(test)]
389 pub(crate) fn executor_install_calls_for_test(&self) -> usize {
390 self.executor_install_calls
391 .load(std::sync::atomic::Ordering::Relaxed)
392 }
393}
394
395impl CpuDomainExecutor for CpuContext {
396 fn capabilities(&self) -> CpuDomainExecutorCapabilities {
397 let worker_count = match NonZeroUsize::new(self.num_threads) {
400 Some(worker_count) => worker_count,
401 None => unreachable!("CpuContext must contain at least one worker"),
402 };
403 CpuDomainExecutorCapabilities {
404 worker_count,
405 outer_parallelism: self.num_threads > 1,
406 inner_parallelism: if self.pool.is_some() {
407 CpuInnerParallelism::Rayon
408 } else {
409 CpuInnerParallelism::None
410 },
411 reentrancy: CpuExecutorReentrancy::SameExecutor,
414 affinity: if self.pinned_cpus.is_some() {
415 CpuExecutorAffinity::TenferroPinnedVerified
416 } else {
417 CpuExecutorAffinity::None
418 },
419 shutdown: CpuExecutorShutdown::TenferroOwned,
420 }
421 }
422
423 fn submit(&self, jobs: &dyn ScopedCpuJobs) -> std::result::Result<(), CpuDomainExecutorError> {
424 let _scope = current_execution_owner().map(|owner| self.execution_scope.enter(owner));
425 if self.pool.is_none() {
426 return (0..jobs.len()).try_for_each(|index| jobs.run(index));
427 }
428 self.install_if_needed(|| {
429 (0..jobs.len())
430 .into_par_iter()
431 .try_for_each(|index| jobs.run(index))
432 })
433 }
434
435 fn install(
436 &self,
437 job: &mut dyn ScopedCpuJob,
438 ) -> std::result::Result<(), CpuDomainExecutorError> {
439 #[cfg(test)]
440 self.executor_install_calls
441 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
442 let _scope = current_execution_owner().map(|owner| self.execution_scope.enter(owner));
443 self.install_if_needed(|| job.run())
444 }
445}
446
447fn select_worker_cpus(cpus: &CpuSet, num_threads: usize) -> Vec<CpuId> {
448 if num_threads == 1 {
449 return vec![cpus.as_slice()[cpus.len() / 2]];
450 }
451 (0..num_threads)
452 .map(|worker| {
453 let index = ((worker as u128) * ((cpus.len() - 1) as u128)
454 / ((num_threads - 1) as u128)) as usize;
455 cpus.as_slice()[index]
456 })
457 .collect()
458}
459
460#[cfg(test)]
461mod tests;