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 pub(crate) fn tp(&self) -> &rayon::ThreadPool {
94 match &self.pool {
95 PoolRef::Borrowed(p) => p,
96 PoolRef::Owned(p) => p,
97 PoolRef::Shared(p) => p,
98 }
99 }
100
101 /// Give back an owned pool (`None` for a borrowed or shared one), for example
102 /// so a C host can drop it and join its workers. A shared wrapper releases
103 /// only its Arc reference; other owners keep the raw pool alive.
104 pub fn into_owned(self) -> Option<rayon::ThreadPool> {
105 match self.pool {
106 PoolRef::Owned(p) => Some(*p),
107 PoolRef::Borrowed(_) | PoolRef::Shared(_) => None,
108 }
109 }
110
111 /// Snapshot of the entry counters.
112 pub fn stats(&self) -> PoolStats {
113 PoolStats {
114 entries: self.entries.load(Ordering::Relaxed),
115 broadcasts: self.broadcasts.load(Ordering::Relaxed),
116 inline_runs: self.inline_runs.load(Ordering::Relaxed),
117 }
118 }
119
120 /// Zero the counters.
121 pub fn reset_stats(&self) {
122 self.entries.store(0, Ordering::Relaxed);
123 self.broadcasts.store(0, Ordering::Relaxed);
124 self.inline_runs.store(0, Ordering::Relaxed);
125 }
126
127 pub(crate) fn is_worker(&self) -> bool {
128 self.tp().current_thread_index().is_some()
129 }
130
131 pub(crate) fn count_entry(&self) {
132 self.entries.fetch_add(1, Ordering::Relaxed);
133 }
134
135 pub(crate) fn count_broadcast(&self) {
136 self.broadcasts.fetch_add(1, Ordering::Relaxed);
137 }
138
139 pub(crate) fn count_inline(&self) {
140 self.inline_runs.fetch_add(1, Ordering::Relaxed);
141 }
142}
143
144impl Pool<'static> {
145 /// Share the host's existing Rayon pool without creating another worker team.
146 ///
147 /// Retain this wrapper across calls and share it (for example through an
148 /// `Arc<Pool<'static>>`) across host clones to reuse workspace and SPMD
149 /// serialization. Other numerical libraries may borrow the host's original
150 /// raw pool, but their work must obey the host's same resource-loan protocol.
151 ///
152 /// # Examples
153 ///
154 /// ```
155 /// use std::sync::Arc;
156 /// let raw = Arc::new(rayon::ThreadPoolBuilder::new().num_threads(2).build().unwrap());
157 /// let pool = tcontract_exec::Pool::shared(Arc::clone(&raw));
158 /// assert_eq!(pool.size(), raw.current_num_threads());
159 /// ```
160 pub fn shared(pool: Arc<rayon::ThreadPool>) -> Self {
161 Self::with(PoolRef::Shared(pool))
162 }
163
164 /// Take ownership of a pool, for hosts without one of their own (the C
165 /// ABI's `tcontract_tapp_executor_create_rayon`). Rust hosts normally [`Pool::borrow`].
166 ///
167 /// # Examples
168 ///
169 /// ```
170 /// let tp = rayon::ThreadPoolBuilder::new().num_threads(2).build().unwrap();
171 /// let pool = tcontract_exec::Pool::owned(tp);
172 /// assert_eq!(pool.size(), 2);
173 /// ```
174 pub fn owned(pool: rayon::ThreadPool) -> Self {
175 Self::with(PoolRef::Owned(Box::new(pool)))
176 }
177}
178
179impl std::fmt::Debug for Pool<'_> {
180 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
181 f.debug_struct("Pool")
182 .field("size", &self.size())
183 .field("stats", &self.stats())
184 .finish()
185 }
186}