Skip to main content

ftui_core/
read_optimized.rs

1#![forbid(unsafe_code)]
2
3//! Read-optimized concurrent stores for read-heavy, write-rare data.
4//!
5//! Terminal UIs are read-dominated: theme colors, terminal capabilities, and
6//! animation clocks are read every frame but changed only on user action or
7//! mode switch. A traditional `RwLock` or `Mutex` adds unnecessary contention
8//! on the hot read path.
9//!
10//! This module provides [`ReadOptimized<T>`], a trait abstracting over
11//! wait-free read stores, plus three concrete implementations:
12//!
13//! | Store | Read | Write | Use case |
14//! |-------|------|-------|----------|
15//! | [`ArcSwapStore`] | wait-free | atomic swap | **Production default** |
16//! | [`RwLockStore`] | shared lock | exclusive lock | Baseline comparison |
17//! | [`MutexStore`] | exclusive lock | exclusive lock | Baseline comparison |
18//!
19//! # Constraints
20//!
21//! - `#![forbid(unsafe_code)]` — all safety delegated to `arc-swap`.
22//! - `T: Clone + Send + Sync` — required for cross-thread sharing.
23//! - Read path allocates nothing (arc-swap `load` returns a guard, no clone).
24//! - Write path allocates one `Arc` per store.
25//!
26//! # Example
27//!
28//! ```
29//! use ftui_core::read_optimized::{ReadOptimized, ArcSwapStore};
30//!
31//! let store = ArcSwapStore::new(42u64);
32//! assert_eq!(store.load(), 42);
33//!
34//! store.store(99);
35//! assert_eq!(store.load(), 99);
36//! ```
37//!
38//! # Design decision: `arc-swap` over `left-right`
39//!
40//! Both crates provide safe, lock-free reads. `arc-swap` was chosen because:
41//!
42//! 1. **Simpler API** — single `ArcSwap<T>` vs read/write handle pairs.
43//! 2. **Lower memory** — one `Arc` vs two full copies of `T`.
44//! 3. **Sufficient for our types** — `ResolvedTheme` (Copy, 76 bytes) and
45//!    `TerminalCapabilities` (Copy, 20 bytes) are tiny; double-buffering
46//!    gains nothing.
47//! 4. **Zero-dependency unsafe** — our crate stays `#![forbid(unsafe_code)]`;
48//!    the unsafe is encapsulated inside `arc-swap`.
49//!
50//! The `seqlock` crate was rejected because its API requires `unsafe` at the
51//! call site.
52
53use std::sync::{Arc, Mutex, RwLock};
54
55use arc_swap::ArcSwap;
56
57// ---------------------------------------------------------------------------
58// Trait
59// ---------------------------------------------------------------------------
60
61/// A concurrent store optimized for read-heavy access patterns.
62///
63/// Implementations must guarantee:
64/// - `load()` never blocks writers (no writer starvation).
65/// - `load()` returns a consistent snapshot (no torn reads).
66/// - `store()` is atomic with respect to concurrent `load()` calls.
67pub trait ReadOptimized<T: Clone + Send + Sync>: Send + Sync {
68    /// Read the current value. Must be wait-free or lock-free.
69    fn load(&self) -> T;
70
71    /// Atomically replace the stored value.
72    fn store(&self, val: T);
73}
74
75// ---------------------------------------------------------------------------
76// ArcSwapStore — production default
77// ---------------------------------------------------------------------------
78
79/// Wait-free reads via [`arc_swap::ArcSwap`].
80///
81/// - `load()`: wait-free, returns cloned `T` from a guard (no allocation).
82/// - `store()`: allocates one `Arc`, atomically swaps.
83///
84/// Best for: read-99%/write-1% data like themes and capabilities.
85pub struct ArcSwapStore<T> {
86    inner: ArcSwap<T>,
87}
88
89impl<T: Clone + Send + Sync> ArcSwapStore<T> {
90    /// Create a new store with an initial value.
91    pub fn new(val: T) -> Self {
92        Self {
93            inner: ArcSwap::from_pointee(val),
94        }
95    }
96
97    /// Read without cloning — returns a guard that derefs to `T`.
98    ///
99    /// Prefer this when you only need a short-lived reference.
100    pub fn load_ref(&self) -> arc_swap::Guard<Arc<T>> {
101        self.inner.load()
102    }
103}
104
105impl<T: Clone + Send + Sync> ReadOptimized<T> for ArcSwapStore<T> {
106    #[inline]
107    fn load(&self) -> T {
108        // Guard derefs to Arc<T>, clone T out.
109        let guard = self.inner.load();
110        T::clone(&guard)
111    }
112
113    #[inline]
114    fn store(&self, val: T) {
115        self.inner.store(Arc::new(val));
116    }
117}
118
119// ---------------------------------------------------------------------------
120// RwLockStore — baseline comparison
121// ---------------------------------------------------------------------------
122
123/// Shared-lock reads via [`std::sync::RwLock`].
124///
125/// Included for benchmark comparison; prefer [`ArcSwapStore`] in production.
126pub struct RwLockStore<T> {
127    inner: RwLock<T>,
128}
129
130impl<T: Clone + Send + Sync> RwLockStore<T> {
131    /// Create a new store with an initial value.
132    pub fn new(val: T) -> Self {
133        Self {
134            inner: RwLock::new(val),
135        }
136    }
137}
138
139impl<T: Clone + Send + Sync> ReadOptimized<T> for RwLockStore<T> {
140    #[inline]
141    fn load(&self) -> T {
142        self.inner.read().expect("RwLock poisoned").clone()
143    }
144
145    #[inline]
146    fn store(&self, val: T) {
147        *self.inner.write().expect("RwLock poisoned") = val;
148    }
149}
150
151// ---------------------------------------------------------------------------
152// MutexStore — baseline comparison
153// ---------------------------------------------------------------------------
154
155/// Exclusive-lock reads via [`std::sync::Mutex`].
156///
157/// Included for benchmark comparison; prefer [`ArcSwapStore`] in production.
158pub struct MutexStore<T> {
159    inner: Mutex<T>,
160}
161
162impl<T: Clone + Send + Sync> MutexStore<T> {
163    /// Create a new store with an initial value.
164    pub fn new(val: T) -> Self {
165        Self {
166            inner: Mutex::new(val),
167        }
168    }
169}
170
171impl<T: Clone + Send + Sync> ReadOptimized<T> for MutexStore<T> {
172    #[inline]
173    fn load(&self) -> T {
174        self.inner.lock().expect("Mutex poisoned").clone()
175    }
176
177    #[inline]
178    fn store(&self, val: T) {
179        *self.inner.lock().expect("Mutex poisoned") = val;
180    }
181}
182
183// ===========================================================================
184// Tests
185// ===========================================================================
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190    use std::sync::Barrier;
191    use std::thread;
192
193    // -- Helpers -----------------------------------------------------------
194
195    /// A non-Copy type to exercise the Clone path.
196    #[derive(Debug, Clone, PartialEq, Eq)]
197    struct Config {
198        name: String,
199        value: u64,
200    }
201
202    fn make_config(n: u64) -> Config {
203        Config {
204            name: format!("cfg-{n}"),
205            value: n,
206        }
207    }
208
209    // -- ArcSwapStore tests -----------------------------------------------
210
211    #[test]
212    fn arcswap_load_returns_initial_value() {
213        let store = ArcSwapStore::new(42u64);
214        assert_eq!(store.load(), 42);
215    }
216
217    #[test]
218    fn arcswap_store_then_load() {
219        let store = ArcSwapStore::new(0u64);
220        store.store(99);
221        assert_eq!(store.load(), 99);
222    }
223
224    #[test]
225    fn arcswap_load_ref_borrows_without_clone() {
226        let store = ArcSwapStore::new(make_config(1));
227        let guard = store.load_ref();
228        assert_eq!(guard.name, "cfg-1");
229        assert_eq!(guard.value, 1);
230    }
231
232    #[test]
233    fn arcswap_multiple_stores_last_wins() {
234        let store = ArcSwapStore::new(0u64);
235        for i in 1..=100 {
236            store.store(i);
237        }
238        assert_eq!(store.load(), 100);
239    }
240
241    #[test]
242    fn arcswap_concurrent_reads_never_panic() {
243        let store = Arc::new(ArcSwapStore::new(make_config(0)));
244        let barrier = Arc::new(Barrier::new(8));
245
246        let handles: Vec<_> = (0..8)
247            .map(|_| {
248                let s = Arc::clone(&store);
249                let b = Arc::clone(&barrier);
250                thread::spawn(move || {
251                    b.wait();
252                    for _ in 0..1000 {
253                        let _ = s.load();
254                    }
255                })
256            })
257            .collect();
258
259        for h in handles {
260            h.join().unwrap();
261        }
262    }
263
264    #[test]
265    fn arcswap_concurrent_read_write() {
266        let store = Arc::new(ArcSwapStore::new(0u64));
267        let barrier = Arc::new(Barrier::new(9)); // 8 readers + 1 writer
268
269        // 8 reader threads
270        let readers: Vec<_> = (0..8)
271            .map(|_| {
272                let s = Arc::clone(&store);
273                let b = Arc::clone(&barrier);
274                thread::spawn(move || {
275                    b.wait();
276                    let mut last = 0u64;
277                    for _ in 0..10_000 {
278                        let v = s.load();
279                        // Values must be monotonically non-decreasing
280                        // (single writer increments sequentially).
281                        assert!(v >= last, "stale read: got {v}, expected >= {last}");
282                        last = v;
283                    }
284                })
285            })
286            .collect();
287
288        // 1 writer thread
289        let writer = {
290            let s = Arc::clone(&store);
291            let b = Arc::clone(&barrier);
292            thread::spawn(move || {
293                b.wait();
294                for i in 1..=10_000u64 {
295                    s.store(i);
296                }
297            })
298        };
299
300        writer.join().unwrap();
301        for h in readers {
302            h.join().unwrap();
303        }
304        assert_eq!(store.load(), 10_000);
305    }
306
307    // -- RwLockStore tests ------------------------------------------------
308
309    #[test]
310    fn rwlock_load_returns_initial_value() {
311        let store = RwLockStore::new(42u64);
312        assert_eq!(store.load(), 42);
313    }
314
315    #[test]
316    fn rwlock_store_then_load() {
317        let store = RwLockStore::new(0u64);
318        store.store(99);
319        assert_eq!(store.load(), 99);
320    }
321
322    #[test]
323    fn rwlock_concurrent_read_write() {
324        let store = Arc::new(RwLockStore::new(0u64));
325        let barrier = Arc::new(Barrier::new(5));
326
327        let readers: Vec<_> = (0..4)
328            .map(|_| {
329                let s = Arc::clone(&store);
330                let b = Arc::clone(&barrier);
331                thread::spawn(move || {
332                    b.wait();
333                    for _ in 0..5_000 {
334                        let _ = s.load();
335                    }
336                })
337            })
338            .collect();
339
340        let writer = {
341            let s = Arc::clone(&store);
342            let b = Arc::clone(&barrier);
343            thread::spawn(move || {
344                b.wait();
345                for i in 1..=5_000u64 {
346                    s.store(i);
347                }
348            })
349        };
350
351        writer.join().unwrap();
352        for h in readers {
353            h.join().unwrap();
354        }
355        assert_eq!(store.load(), 5_000);
356    }
357
358    // -- MutexStore tests -------------------------------------------------
359
360    #[test]
361    fn mutex_load_returns_initial_value() {
362        let store = MutexStore::new(42u64);
363        assert_eq!(store.load(), 42);
364    }
365
366    #[test]
367    fn mutex_store_then_load() {
368        let store = MutexStore::new(0u64);
369        store.store(99);
370        assert_eq!(store.load(), 99);
371    }
372
373    #[test]
374    fn mutex_concurrent_read_write() {
375        let store = Arc::new(MutexStore::new(0u64));
376        let barrier = Arc::new(Barrier::new(5));
377
378        let readers: Vec<_> = (0..4)
379            .map(|_| {
380                let s = Arc::clone(&store);
381                let b = Arc::clone(&barrier);
382                thread::spawn(move || {
383                    b.wait();
384                    for _ in 0..5_000 {
385                        let _ = s.load();
386                    }
387                })
388            })
389            .collect();
390
391        let writer = {
392            let s = Arc::clone(&store);
393            let b = Arc::clone(&barrier);
394            thread::spawn(move || {
395                b.wait();
396                for i in 1..=5_000u64 {
397                    s.store(i);
398                }
399            })
400        };
401
402        writer.join().unwrap();
403        for h in readers {
404            h.join().unwrap();
405        }
406        assert_eq!(store.load(), 5_000);
407    }
408
409    // -- Trait object tests -----------------------------------------------
410
411    #[test]
412    fn trait_object_arcswap() {
413        let store: Box<dyn ReadOptimized<u64>> = Box::new(ArcSwapStore::new(10));
414        assert_eq!(store.load(), 10);
415        store.store(20);
416        assert_eq!(store.load(), 20);
417    }
418
419    #[test]
420    fn trait_object_rwlock() {
421        let store: Box<dyn ReadOptimized<u64>> = Box::new(RwLockStore::new(10));
422        assert_eq!(store.load(), 10);
423        store.store(20);
424        assert_eq!(store.load(), 20);
425    }
426
427    #[test]
428    fn trait_object_mutex() {
429        let store: Box<dyn ReadOptimized<u64>> = Box::new(MutexStore::new(10));
430        assert_eq!(store.load(), 10);
431        store.store(20);
432        assert_eq!(store.load(), 20);
433    }
434
435    // -- Copy type tests (simulating ResolvedTheme / TerminalCapabilities) -
436
437    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
438    struct FakeCaps {
439        true_color: bool,
440        sync_output: bool,
441        mouse_sgr: bool,
442    }
443
444    #[test]
445    fn arcswap_with_copy_type() {
446        let caps = FakeCaps {
447            true_color: true,
448            sync_output: false,
449            mouse_sgr: true,
450        };
451        let store = ArcSwapStore::new(caps);
452        assert_eq!(store.load(), caps);
453
454        let updated = FakeCaps {
455            true_color: true,
456            sync_output: true,
457            mouse_sgr: true,
458        };
459        store.store(updated);
460        assert_eq!(store.load(), updated);
461    }
462
463    #[test]
464    fn concurrent_copy_type_reads() {
465        let caps = FakeCaps {
466            true_color: true,
467            sync_output: false,
468            mouse_sgr: true,
469        };
470        let store = Arc::new(ArcSwapStore::new(caps));
471        let barrier = Arc::new(Barrier::new(8));
472
473        let handles: Vec<_> = (0..8)
474            .map(|_| {
475                let s = Arc::clone(&store);
476                let b = Arc::clone(&barrier);
477                thread::spawn(move || {
478                    b.wait();
479                    for _ in 0..10_000 {
480                        let v = s.load();
481                        // Value must be one of the two valid states.
482                        assert!(v.true_color);
483                        assert!(v.mouse_sgr);
484                    }
485                })
486            })
487            .collect();
488
489        for h in handles {
490            h.join().unwrap();
491        }
492    }
493}