Skip to main content

solverforge_maps/routing/
cache.rs

1//! Zero-erasure cache for road networks.
2
3use std::collections::HashMap;
4use std::mem::size_of;
5use std::ops::Deref;
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::sync::Arc;
8use std::sync::OnceLock;
9
10use serde::{Deserialize, Serialize};
11use tokio::sync::{Mutex, RwLock, RwLockReadGuard};
12
13use super::bbox::BoundingBox;
14use super::network::RoadNetwork;
15
16pub const CACHE_VERSION: u32 = 5;
17
18static NETWORK_CACHE: OnceLock<RwLock<HashMap<String, RoadNetwork>>> = OnceLock::new();
19static IN_FLIGHT_LOADS: OnceLock<Mutex<HashMap<String, Arc<Mutex<()>>>>> = OnceLock::new();
20static CACHE_HITS: AtomicU64 = AtomicU64::new(0);
21static CACHE_MISSES: AtomicU64 = AtomicU64::new(0);
22
23pub(crate) fn cache() -> &'static RwLock<HashMap<String, RoadNetwork>> {
24    NETWORK_CACHE.get_or_init(|| RwLock::new(HashMap::new()))
25}
26
27pub(crate) fn in_flight_loads() -> &'static Mutex<HashMap<String, Arc<Mutex<()>>>> {
28    IN_FLIGHT_LOADS.get_or_init(|| Mutex::new(HashMap::new()))
29}
30
31pub(crate) fn record_hit() {
32    CACHE_HITS.fetch_add(1, Ordering::Relaxed);
33}
34
35pub(crate) fn record_miss() {
36    CACHE_MISSES.fetch_add(1, Ordering::Relaxed);
37}
38
39#[derive(Debug, Clone)]
40pub struct CacheStats {
41    pub networks_cached: usize,
42    pub total_nodes: usize,
43    pub total_edges: usize,
44    pub memory_bytes: usize,
45    pub hits: u64,
46    pub misses: u64,
47}
48
49impl CacheStats {
50    pub fn hit_ratio(&self) -> f64 {
51        let total = self.hits + self.misses;
52        if total == 0 {
53            0.0
54        } else {
55            self.hits as f64 / total as f64
56        }
57    }
58}
59
60/// RAII guard providing zero-cost access to a cached RoadNetwork.
61pub struct NetworkRef {
62    guard: RwLockReadGuard<'static, HashMap<String, RoadNetwork>>,
63    key: String,
64}
65
66impl Deref for NetworkRef {
67    type Target = RoadNetwork;
68
69    fn deref(&self) -> &RoadNetwork {
70        self.guard
71            .get(&self.key)
72            .expect("cached network disappeared")
73    }
74}
75
76impl NetworkRef {
77    pub(crate) fn new(
78        guard: RwLockReadGuard<'static, HashMap<String, RoadNetwork>>,
79        key: String,
80    ) -> Self {
81        debug_assert!(
82            guard.contains_key(&key),
83            "NetworkRef created for missing key"
84        );
85        Self { guard, key }
86    }
87
88    pub fn cache_key(&self) -> &str {
89        &self.key
90    }
91}
92
93#[derive(Debug, Serialize, Deserialize)]
94pub struct CachedNetwork {
95    pub version: u32,
96    pub nodes: Vec<CachedNode>,
97    pub edges: Vec<CachedEdge>,
98}
99
100#[derive(Debug, Serialize, Deserialize)]
101pub struct CachedNode {
102    pub lat: f64,
103    pub lng: f64,
104}
105
106#[derive(Debug, Serialize, Deserialize)]
107pub struct CachedEdge {
108    pub from: usize,
109    pub to: usize,
110    pub travel_time_s: f64,
111    pub distance_m: f64,
112}
113
114impl RoadNetwork {
115    pub async fn cache_stats() -> CacheStats {
116        let guard = cache().read().await;
117
118        let mut total_nodes = 0usize;
119        let mut total_edges = 0usize;
120        let mut memory_bytes = 0usize;
121
122        for network in guard.values() {
123            let nodes = network.node_count();
124            let edges = network.edge_count();
125            total_nodes += nodes;
126            total_edges += edges;
127
128            // Estimate memory: node data + edge data + hashmap overhead
129            memory_bytes += nodes * (size_of::<f64>() * 2 + size_of::<usize>() * 2);
130            memory_bytes += edges * (size_of::<f64>() * 2 + size_of::<usize>() * 2);
131        }
132
133        CacheStats {
134            networks_cached: guard.len(),
135            total_nodes,
136            total_edges,
137            memory_bytes,
138            hits: CACHE_HITS.load(Ordering::Relaxed),
139            misses: CACHE_MISSES.load(Ordering::Relaxed),
140        }
141    }
142
143    pub async fn clear_cache() {
144        let mut guard = cache().write().await;
145        guard.clear();
146    }
147
148    pub async fn evict(bbox: &BoundingBox) -> bool {
149        let cache_key = bbox.cache_key();
150        let mut guard = cache().write().await;
151        guard.remove(&cache_key).is_some()
152    }
153
154    pub async fn cached_regions() -> Vec<BoundingBox> {
155        let guard = cache().read().await;
156        guard
157            .keys()
158            .filter_map(|key| parse_cache_key(key))
159            .collect()
160    }
161}
162
163fn parse_cache_key(key: &str) -> Option<BoundingBox> {
164    let parts: Vec<&str> = key.split('_').collect();
165    if parts.len() != 4 {
166        return None;
167    }
168
169    let min_lat: f64 = parts[0].parse().ok()?;
170    let min_lng: f64 = parts[1].parse().ok()?;
171    let max_lat: f64 = parts[2].parse().ok()?;
172    let max_lng: f64 = parts[3].parse().ok()?;
173
174    BoundingBox::try_new(min_lat, min_lng, max_lat, max_lng).ok()
175}