Skip to main content

trueno_graph/gpu/
cache.rs

1//! LRU cache for GPU graph tiles
2//!
3//! Implements Least Recently Used eviction policy for managing GPU memory.
4//! Based on standard LRU cache algorithms with GPU buffer lifecycle management.
5
6use std::collections::{HashMap, VecDeque};
7use wgpu::Buffer;
8
9/// Graph tile identifier (tile index)
10pub type TileId = usize;
11
12/// LRU cache for GPU buffers
13///
14/// Manages a fixed-capacity cache of GPU buffers with LRU eviction policy.
15/// When capacity is reached, least recently used tile is evicted.
16pub struct LruTileCache {
17    /// Maximum number of tiles to cache
18    capacity: usize,
19
20    /// Map from tile ID to GPU buffer
21    buffers: HashMap<TileId, Buffer>,
22
23    /// Access order (front = most recent, back = least recent)
24    access_order: VecDeque<TileId>,
25}
26
27impl LruTileCache {
28    /// Create new LRU cache with given capacity
29    #[must_use]
30    pub fn new(capacity: usize) -> Self {
31        Self { capacity: capacity.max(1), buffers: HashMap::new(), access_order: VecDeque::new() }
32    }
33
34    /// Get buffer from cache (marks as recently used)
35    pub fn get(&mut self, tile_id: TileId) -> Option<&Buffer> {
36        if self.buffers.contains_key(&tile_id) {
37            // Move to front (most recently used)
38            self.access_order.retain(|&id| id != tile_id);
39            self.access_order.push_front(tile_id);
40            self.buffers.get(&tile_id)
41        } else {
42            None
43        }
44    }
45
46    /// Insert buffer into cache (may evict LRU tile)
47    ///
48    /// Returns evicted tile ID if eviction occurred
49    pub fn insert(&mut self, tile_id: TileId, buffer: Buffer) -> Option<TileId> {
50        let mut evicted = None;
51
52        // If already exists, just update access order
53        if self.buffers.contains_key(&tile_id) {
54            self.access_order.retain(|&id| id != tile_id);
55            self.access_order.push_front(tile_id);
56            self.buffers.insert(tile_id, buffer);
57            return None;
58        }
59
60        // Evict LRU if at capacity
61        if self.buffers.len() >= self.capacity {
62            if let Some(lru_id) = self.access_order.pop_back() {
63                self.buffers.remove(&lru_id);
64                evicted = Some(lru_id);
65            }
66        }
67
68        // Insert new buffer
69        self.buffers.insert(tile_id, buffer);
70        self.access_order.push_front(tile_id);
71
72        evicted
73    }
74
75    /// Check if tile is in cache
76    #[must_use]
77    pub fn contains(&self, tile_id: TileId) -> bool {
78        self.buffers.contains_key(&tile_id)
79    }
80
81    /// Get current cache size
82    #[must_use]
83    pub fn len(&self) -> usize {
84        self.buffers.len()
85    }
86
87    /// Check if cache is empty
88    #[must_use]
89    pub fn is_empty(&self) -> bool {
90        self.buffers.is_empty()
91    }
92
93    /// Clear all cached buffers
94    pub fn clear(&mut self) {
95        self.buffers.clear();
96        self.access_order.clear();
97    }
98
99    /// Get cache hit rate statistics
100    #[must_use]
101    pub fn capacity(&self) -> usize {
102        self.capacity
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use crate::gpu::GpuDevice;
110
111    #[tokio::test]
112    async fn test_lru_cache_basic() {
113        if !GpuDevice::is_gpu_available().await {
114            eprintln!("⚠️  Skipping test_lru_cache_basic: GPU not available");
115            return;
116        }
117
118        let device = GpuDevice::new().await.unwrap();
119        let mut cache = LruTileCache::new(3);
120
121        // Create test buffers
122        let buf1 = device.create_buffer("tile_1", 1024, wgpu::BufferUsages::STORAGE).unwrap();
123        let buf2 = device.create_buffer("tile_2", 1024, wgpu::BufferUsages::STORAGE).unwrap();
124        let buf3 = device.create_buffer("tile_3", 1024, wgpu::BufferUsages::STORAGE).unwrap();
125        let buf4 = device.create_buffer("tile_4", 1024, wgpu::BufferUsages::STORAGE).unwrap();
126
127        // Insert 3 buffers
128        assert_eq!(cache.insert(1, buf1), None);
129        assert_eq!(cache.insert(2, buf2), None);
130        assert_eq!(cache.insert(3, buf3), None);
131        assert_eq!(cache.len(), 3);
132
133        // Access tile 1 (makes it most recent)
134        assert!(cache.get(1).is_some());
135
136        // Insert 4th buffer should evict LRU (tile 2)
137        assert_eq!(cache.insert(4, buf4), Some(2));
138        assert_eq!(cache.len(), 3);
139        assert!(cache.contains(1));
140        assert!(!cache.contains(2)); // Evicted
141        assert!(cache.contains(3));
142        assert!(cache.contains(4));
143    }
144
145    #[test]
146    fn test_lru_cache_capacity() {
147        let cache = LruTileCache::new(5);
148        assert_eq!(cache.capacity(), 5);
149        assert_eq!(cache.len(), 0);
150        assert!(cache.is_empty());
151    }
152
153    #[tokio::test]
154    async fn test_lru_cache_reinsertion() {
155        if !GpuDevice::is_gpu_available().await {
156            eprintln!("⚠️  Skipping test_lru_cache_reinsertion: GPU not available");
157            return;
158        }
159
160        let device = GpuDevice::new().await.unwrap();
161        let mut cache = LruTileCache::new(2);
162
163        let buf1 = device.create_buffer("tile_1", 1024, wgpu::BufferUsages::STORAGE).unwrap();
164        let buf1_new =
165            device.create_buffer("tile_1_new", 1024, wgpu::BufferUsages::STORAGE).unwrap();
166
167        // Insert tile 1
168        assert_eq!(cache.insert(1, buf1), None);
169        assert_eq!(cache.len(), 1);
170
171        // Re-insert tile 1 (should not evict)
172        assert_eq!(cache.insert(1, buf1_new), None);
173        assert_eq!(cache.len(), 1);
174        assert!(cache.contains(1));
175    }
176
177    #[tokio::test]
178    async fn test_lru_cache_clear() {
179        if !GpuDevice::is_gpu_available().await {
180            eprintln!("⚠️  Skipping test_lru_cache_clear: GPU not available");
181            return;
182        }
183
184        let device = GpuDevice::new().await.unwrap();
185        let mut cache = LruTileCache::new(3);
186
187        let buf1 = device.create_buffer("tile_1", 1024, wgpu::BufferUsages::STORAGE).unwrap();
188        let buf2 = device.create_buffer("tile_2", 1024, wgpu::BufferUsages::STORAGE).unwrap();
189
190        cache.insert(1, buf1);
191        cache.insert(2, buf2);
192        assert_eq!(cache.len(), 2);
193
194        cache.clear();
195        assert_eq!(cache.len(), 0);
196        assert!(cache.is_empty());
197        assert!(!cache.contains(1));
198        assert!(!cache.contains(2));
199    }
200
201    #[tokio::test]
202    async fn test_lru_cache_get_nonexistent() {
203        if !GpuDevice::is_gpu_available().await {
204            eprintln!("⚠️  Skipping test_lru_cache_get_nonexistent: GPU not available");
205            return;
206        }
207
208        let mut cache = LruTileCache::new(3);
209        assert!(cache.get(999).is_none());
210    }
211
212    #[test]
213    fn test_lru_cache_zero_capacity() {
214        let cache = LruTileCache::new(0);
215        assert_eq!(cache.capacity(), 1); // Minimum capacity is 1
216    }
217}