Skip to main content

tcontract_exec/
pool.rs

1use crate::ArenaProvider;
2use std::sync::atomic::{AtomicU64, Ordering};
3use std::sync::{Arc, Mutex};
4
5/// A wrapper around a host's borrowed, owned or shared Rayon pool.
6///
7/// tcontract never creates threads beyond this pool. Its size is fixed at
8/// construction (Rayon pools cannot resize). The embedded mutex serializes
9/// SPMD broadcasts through this wrapper. Hosts sharing a raw pool must retain
10/// one wrapper and serialize potentially conflicting numerical work, rather
11/// than constructing independent wrappers with separate SPMD mutexes.
12///
13/// # Examples
14///
15/// ```
16/// let tp = rayon::ThreadPoolBuilder::new().num_threads(3).build().unwrap();
17/// let pool = tcontract_exec::Pool::borrow(&tp);
18/// assert_eq!(pool.size(), 3);
19/// assert_eq!(pool.stats().entries, 0);
20/// ```
21pub struct Pool<'p> {
22    pool: PoolRef<'p>,
23    pub(crate) spmd: Mutex<()>,
24    /// Retained storage for operations through this wrapper. Reusing the wrapper
25    /// reuses its worker buffers, B panel and barriered team sets.
26    workspace: ArenaProvider,
27    entries: AtomicU64,
28    broadcasts: AtomicU64,
29    inline_runs: AtomicU64,
30}
31
32enum PoolRef<'p> {
33    Borrowed(&'p rayon::ThreadPool),
34    Owned(Box<rayon::ThreadPool>),
35    Shared(Arc<rayon::ThreadPool>),
36}
37
38/// Counters of how often a [`Pool`] was entered, for tests and benchmarks.
39///
40/// # Examples
41///
42/// ```
43/// assert_eq!(tcontract_exec::PoolStats::default().entries, 0);
44/// ```
45#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
46pub struct PoolStats {
47    /// Entries from outside the pool (`install` or partitions).
48    pub entries: u64,
49    /// SPMD broadcasts started from outside the pool.
50    pub broadcasts: u64,
51    /// Parallel requests served in place because the caller was a worker.
52    pub inline_runs: u64,
53}
54
55impl<'p> Pool<'p> {
56    /// Borrow a host pool.
57    pub fn borrow(pool: &'p rayon::ThreadPool) -> Self {
58        Self::with(PoolRef::Borrowed(pool))
59    }
60
61    fn with(pool: PoolRef<'p>) -> Self {
62        Self {
63            pool,
64            spmd: Mutex::new(()),
65            workspace: ArenaProvider::new(),
66            entries: AtomicU64::new(0),
67            broadcasts: AtomicU64::new(0),
68            inline_runs: AtomicU64::new(0),
69        }
70    }
71
72    /// The storage this pool lends to every operation that runs on it.
73    ///
74    /// Borrowed, not copied: one pool is one owner, and a driver that is given
75    /// this provider returns its team sets to it on drop.
76    pub fn workspace(&self) -> &ArenaProvider {
77        &self.workspace
78    }
79
80    /// Release the pool's idle workspace storage. Live leases and borrowed
81    /// worker buffers survive; a worker parked on this pool keeps its slot
82    /// until it is idle again.
83    pub fn trim_workspace(&self) {
84        use crate::WorkspaceProvider;
85        self.workspace.trim();
86    }
87
88    /// Number of workers.
89    pub fn size(&self) -> usize {
90        self.tp().current_num_threads()
91    }
92
93    /// The raw Rayon pool this wrapper wraps.
94    ///
95    /// For a provider that takes a `rayon::ThreadPool` directly (faer through
96    /// `tlinalg`, LAPACK) and must run on the host's pool rather than a second
97    /// one. The wrapper's retained workspace and its SPMD gate stay with the
98    /// wrapper: a provider that takes the bare pool does not share them, so a
99    /// host that mixes such a provider with this stack keeps one pool but two
100    /// workspace owners.
101    pub fn rayon_pool(&self) -> &rayon::ThreadPool {
102        self.tp()
103    }
104
105    pub(crate) fn tp(&self) -> &rayon::ThreadPool {
106        match &self.pool {
107            PoolRef::Borrowed(p) => p,
108            PoolRef::Owned(p) => p,
109            PoolRef::Shared(p) => p,
110        }
111    }
112
113    /// Give back an owned pool (`None` for a borrowed or shared one), for example
114    /// so a C host can drop it and join its workers. A shared wrapper releases
115    /// only its Arc reference; other owners keep the raw pool alive.
116    pub fn into_owned(self) -> Option<rayon::ThreadPool> {
117        match self.pool {
118            PoolRef::Owned(p) => Some(*p),
119            PoolRef::Borrowed(_) | PoolRef::Shared(_) => None,
120        }
121    }
122
123    /// Snapshot of the entry counters.
124    pub fn stats(&self) -> PoolStats {
125        PoolStats {
126            entries: self.entries.load(Ordering::Relaxed),
127            broadcasts: self.broadcasts.load(Ordering::Relaxed),
128            inline_runs: self.inline_runs.load(Ordering::Relaxed),
129        }
130    }
131
132    /// Zero the counters.
133    pub fn reset_stats(&self) {
134        self.entries.store(0, Ordering::Relaxed);
135        self.broadcasts.store(0, Ordering::Relaxed);
136        self.inline_runs.store(0, Ordering::Relaxed);
137    }
138
139    pub(crate) fn is_worker(&self) -> bool {
140        self.tp().current_thread_index().is_some()
141    }
142
143    pub(crate) fn count_entry(&self) {
144        self.entries.fetch_add(1, Ordering::Relaxed);
145    }
146
147    pub(crate) fn count_broadcast(&self) {
148        self.broadcasts.fetch_add(1, Ordering::Relaxed);
149    }
150
151    pub(crate) fn count_inline(&self) {
152        self.inline_runs.fetch_add(1, Ordering::Relaxed);
153    }
154}
155
156impl Pool<'static> {
157    /// Share the host's existing Rayon pool without creating another worker team.
158    ///
159    /// Retain this wrapper across calls and share it (for example through an
160    /// `Arc<Pool<'static>>`) across host clones to reuse workspace and SPMD
161    /// serialization. Other numerical libraries may borrow the host's original
162    /// raw pool, but their work must obey the host's same resource-loan protocol.
163    ///
164    /// # Examples
165    ///
166    /// ```
167    /// use std::sync::Arc;
168    /// let raw = Arc::new(rayon::ThreadPoolBuilder::new().num_threads(2).build().unwrap());
169    /// let pool = tcontract_exec::Pool::shared(Arc::clone(&raw));
170    /// assert_eq!(pool.size(), raw.current_num_threads());
171    /// ```
172    pub fn shared(pool: Arc<rayon::ThreadPool>) -> Self {
173        Self::with(PoolRef::Shared(pool))
174    }
175
176    /// Take ownership of a pool, for hosts without one of their own (the C
177    /// ABI's `tcontract_tapp_executor_create_rayon`). Rust hosts normally [`Pool::borrow`].
178    ///
179    /// # Examples
180    ///
181    /// ```
182    /// let tp = rayon::ThreadPoolBuilder::new().num_threads(2).build().unwrap();
183    /// let pool = tcontract_exec::Pool::owned(tp);
184    /// assert_eq!(pool.size(), 2);
185    /// ```
186    pub fn owned(pool: rayon::ThreadPool) -> Self {
187        Self::with(PoolRef::Owned(Box::new(pool)))
188    }
189}
190
191impl std::fmt::Debug for Pool<'_> {
192    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193        f.debug_struct("Pool")
194            .field("size", &self.size())
195            .field("stats", &self.stats())
196            .finish()
197    }
198}