Skip to main content

typf_core/
cache_config.rs

1//! Global cache configuration
2//!
3//! Provides a simple way to enable/disable all caching globally.
4//! **Caching is disabled by default** and must be explicitly enabled
5//! via `set_caching_enabled(true)` or the `TYPF_CACHE=1` environment variable.
6//!
7//! # Example
8//!
9//! ```
10//! use typf_core::cache_config;
11//!
12//! // Enable caching (disabled by default)
13//! cache_config::set_caching_enabled(true);
14//!
15//! // Check if caching is enabled
16//! if cache_config::is_caching_enabled() {
17//!     println!("Caching is ON");
18//! }
19//!
20//! // Disable caching again
21//! cache_config::set_caching_enabled(false);
22//! ```
23//!
24//! # Environment Variable
25//!
26//! Set `TYPF_CACHE=1` to enable caching at startup:
27//!
28//! ```bash
29//! TYPF_CACHE=1 ./my_app
30//! ```
31
32// this_file: crates/typf-core/src/cache_config.rs
33
34use std::sync::atomic::{AtomicBool, Ordering};
35use std::sync::Mutex;
36use std::sync::OnceLock;
37
38use crate::cache::get_cache_manager;
39
40/// Global flag for whether caching is enabled (disabled by default)
41static CACHING_ENABLED: AtomicBool = AtomicBool::new(false);
42
43/// Serialize mutations of the global caching flag.
44///
45/// This is primarily for correctness in tests (which run in parallel by default), but also makes
46/// `set_caching_enabled` deterministic for consumers that want scoped control.
47static CACHING_CONFIG_LOCK: Mutex<()> = Mutex::new(());
48
49/// Whether the env var has been checked
50static ENV_CHECKED: OnceLock<()> = OnceLock::new();
51
52/// Check environment variable and initialize the global cache state
53fn check_env() {
54    ENV_CHECKED.get_or_init(|| {
55        if let Ok(val) = std::env::var("TYPF_CACHE") {
56            let enabled = matches!(val.to_lowercase().as_str(), "1" | "true" | "yes" | "on");
57            if enabled {
58                CACHING_ENABLED.store(true, Ordering::SeqCst);
59                log::info!("Typf caching enabled via TYPF_CACHE env var");
60            }
61        }
62    });
63}
64
65/// Check if caching is globally enabled
66///
67/// This checks the `TYPF_CACHE` environment variable on first call,
68/// then returns the cached result. Runtime changes via `set_caching_enabled`
69/// are also respected.
70///
71/// **Note:** Caching is disabled by default. Enable it via
72/// `set_caching_enabled(true)` or `TYPF_CACHE=1` environment variable.
73///
74/// # Returns
75///
76/// `true` if caching is enabled, `false` if disabled (default).
77pub fn is_caching_enabled() -> bool {
78    check_env();
79    CACHING_ENABLED.load(Ordering::SeqCst)
80}
81
82fn set_caching_enabled_unlocked(enabled: bool) {
83    check_env(); // Ensure env is checked first
84    CACHING_ENABLED.store(enabled, Ordering::SeqCst);
85    log::debug!(
86        "Typf caching {} via runtime call",
87        if enabled { "enabled" } else { "disabled" }
88    );
89}
90
91/// Enable or disable caching globally at runtime
92///
93/// This overrides the environment variable setting.
94///
95/// # Arguments
96///
97/// * `enabled` - `true` to enable caching, `false` to disable
98pub fn set_caching_enabled(enabled: bool) {
99    let _lock = CACHING_CONFIG_LOCK
100        .lock()
101        .unwrap_or_else(|poisoned| poisoned.into_inner());
102    set_caching_enabled_unlocked(enabled);
103}
104
105/// Scoped override for the global caching flag.
106///
107/// Holds an internal lock for the lifetime of the guard, preventing other threads from flipping
108/// the flag via [`set_caching_enabled`] while the guard is alive. On drop, restores the previous
109/// value.
110pub fn scoped_caching_enabled(enabled: bool) -> ScopedCachingEnabled {
111    let lock = CACHING_CONFIG_LOCK
112        .lock()
113        .unwrap_or_else(|poisoned| poisoned.into_inner());
114    let previous = is_caching_enabled();
115    set_caching_enabled_unlocked(enabled);
116    ScopedCachingEnabled {
117        _lock: lock,
118        previous,
119    }
120}
121
122pub struct ScopedCachingEnabled {
123    _lock: std::sync::MutexGuard<'static, ()>,
124    previous: bool,
125}
126
127impl Drop for ScopedCachingEnabled {
128    fn drop(&mut self) {
129        set_caching_enabled_unlocked(self.previous);
130    }
131}
132
133/// Clear all caches (convenience function)
134///
135/// This doesn't disable caching, just clears existing cached data.
136/// For shapers/renderers with their own caches, call their `clear_cache()` methods.
137pub fn clear_all_caches() {
138    if is_caching_enabled() {
139        get_cache_manager().clear_all();
140        log::debug!("All global caches cleared.");
141    } else {
142        log::debug!("Caching is disabled, no caches to clear.");
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    // Single test to avoid race conditions with other tests that use the global flag.
151    // Tests that set_caching_enabled correctly updates the value returned by is_caching_enabled.
152
153    #[test]
154    fn test_set_caching_roundtrip() {
155        let _lock = CACHING_CONFIG_LOCK
156            .lock()
157            .unwrap_or_else(|poisoned| poisoned.into_inner());
158        let previous = is_caching_enabled();
159
160        set_caching_enabled_unlocked(true);
161        let after_enable = is_caching_enabled();
162
163        set_caching_enabled_unlocked(false);
164        let after_disable = is_caching_enabled();
165
166        set_caching_enabled_unlocked(previous);
167
168        // Assert after all operations to reduce race window
169        assert!(
170            after_enable,
171            "caching should be enabled after set_caching_enabled(true)"
172        );
173        assert!(
174            !after_disable,
175            "caching should be disabled after set_caching_enabled(false)"
176        );
177    }
178
179    #[test]
180    fn test_clear_all_caches_clears_enabled_caches() {
181        // Ensure caching is enabled for this test
182        let _guard = scoped_caching_enabled(true);
183
184        let manager = get_cache_manager();
185
186        // Insert dummy data into shaping cache
187        manager.cache_shaped(
188            crate::cache::ShapingCacheKey {
189                text_hash: 1,
190                font_id: "font1".to_string(),
191                params_hash: 1,
192            },
193            std::sync::Arc::new(vec![1, 2, 3]),
194        );
195        // Insert dummy data into glyph cache
196        manager.cache_glyph(
197            crate::cache::GlyphCacheKey {
198                font_id: "font1".to_string(),
199                glyph_id: 100,
200                size: 16,
201                params_hash: 2,
202            },
203            std::sync::Arc::new(vec![4, 5, 6]),
204        );
205
206        // Ensure caches are not empty
207        assert!(
208            !manager.shaping_cache.is_empty(),
209            "Shaping cache should not be empty before clear"
210        );
211        assert!(
212            !manager.glyph_cache.is_empty(),
213            "Glyph cache should not be empty before clear"
214        );
215
216        // Clear all caches
217        clear_all_caches();
218
219        // Ensure caches are empty after clearing
220        manager.shaping_cache.sync(); // Force pending operations
221        manager.glyph_cache.sync(); // Force pending operations
222        assert!(
223            manager.shaping_cache.is_empty(),
224            "Shaping cache should be empty after clear"
225        );
226        assert!(
227            manager.glyph_cache.is_empty(),
228            "Glyph cache should be empty after clear"
229        );
230    }
231}