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 LOAD_REQUESTS: AtomicU64 = AtomicU64::new(0);
21static MEMORY_HITS: AtomicU64 = AtomicU64::new(0);
22static DISK_HITS: AtomicU64 = AtomicU64::new(0);
23static NETWORK_FETCHES: AtomicU64 = AtomicU64::new(0);
24static IN_FLIGHT_WAITS: AtomicU64 = AtomicU64::new(0);
25
26pub(crate) fn cache() -> &'static RwLock<HashMap<String, RoadNetwork>> {
27    NETWORK_CACHE.get_or_init(|| RwLock::new(HashMap::new()))
28}
29
30pub(crate) fn in_flight_loads() -> &'static Mutex<HashMap<String, Arc<Mutex<()>>>> {
31    IN_FLIGHT_LOADS.get_or_init(|| Mutex::new(HashMap::new()))
32}
33
34pub(crate) fn record_load_request() {
35    LOAD_REQUESTS.fetch_add(1, Ordering::Relaxed);
36}
37
38pub(crate) fn record_memory_hit() {
39    MEMORY_HITS.fetch_add(1, Ordering::Relaxed);
40}
41
42pub(crate) fn record_disk_hit() {
43    DISK_HITS.fetch_add(1, Ordering::Relaxed);
44}
45
46pub(crate) fn record_network_fetch() {
47    NETWORK_FETCHES.fetch_add(1, Ordering::Relaxed);
48}
49
50pub(crate) fn record_in_flight_wait() {
51    IN_FLIGHT_WAITS.fetch_add(1, Ordering::Relaxed);
52}
53
54#[cfg(test)]
55pub(crate) fn reset_cache_metrics() {
56    LOAD_REQUESTS.store(0, Ordering::Relaxed);
57    MEMORY_HITS.store(0, Ordering::Relaxed);
58    DISK_HITS.store(0, Ordering::Relaxed);
59    NETWORK_FETCHES.store(0, Ordering::Relaxed);
60    IN_FLIGHT_WAITS.store(0, Ordering::Relaxed);
61}
62
63#[derive(Debug, Clone)]
64pub struct CacheStats {
65    pub networks_cached: usize,
66    pub total_nodes: usize,
67    pub total_edges: usize,
68    pub memory_bytes: usize,
69    pub load_requests: u64,
70    pub memory_hits: u64,
71    pub disk_hits: u64,
72    pub network_fetches: u64,
73    pub in_flight_waits: u64,
74}
75
76/// RAII guard providing zero-cost access to a cached RoadNetwork.
77pub struct NetworkRef {
78    guard: RwLockReadGuard<'static, HashMap<String, RoadNetwork>>,
79    key: String,
80}
81
82impl Deref for NetworkRef {
83    type Target = RoadNetwork;
84
85    fn deref(&self) -> &RoadNetwork {
86        self.guard
87            .get(&self.key)
88            .expect("cached network disappeared")
89    }
90}
91
92impl NetworkRef {
93    pub(crate) fn new(
94        guard: RwLockReadGuard<'static, HashMap<String, RoadNetwork>>,
95        key: String,
96    ) -> Self {
97        debug_assert!(
98            guard.contains_key(&key),
99            "NetworkRef created for missing key"
100        );
101        Self { guard, key }
102    }
103
104    pub fn cache_key(&self) -> &str {
105        &self.key
106    }
107}
108
109#[derive(Debug, Serialize, Deserialize)]
110pub struct CachedNetwork {
111    pub version: u32,
112    pub nodes: Vec<CachedNode>,
113    pub edges: Vec<CachedEdge>,
114}
115
116#[derive(Debug, Serialize, Deserialize)]
117pub struct CachedNode {
118    pub lat: f64,
119    pub lng: f64,
120}
121
122#[derive(Debug, Serialize, Deserialize)]
123pub struct CachedEdge {
124    pub from: usize,
125    pub to: usize,
126    pub travel_time_s: f64,
127    pub distance_m: f64,
128}
129
130impl RoadNetwork {
131    pub async fn cache_stats() -> CacheStats {
132        let guard = cache().read().await;
133
134        let mut total_nodes = 0usize;
135        let mut total_edges = 0usize;
136        let mut memory_bytes = 0usize;
137
138        for network in guard.values() {
139            let nodes = network.node_count();
140            let edges = network.edge_count();
141            total_nodes += nodes;
142            total_edges += edges;
143
144            // Estimate memory: node data + edge data + hashmap overhead
145            memory_bytes += nodes * (size_of::<f64>() * 2 + size_of::<usize>() * 2);
146            memory_bytes += edges * (size_of::<f64>() * 2 + size_of::<usize>() * 2);
147        }
148
149        CacheStats {
150            networks_cached: guard.len(),
151            total_nodes,
152            total_edges,
153            memory_bytes,
154            load_requests: LOAD_REQUESTS.load(Ordering::Relaxed),
155            memory_hits: MEMORY_HITS.load(Ordering::Relaxed),
156            disk_hits: DISK_HITS.load(Ordering::Relaxed),
157            network_fetches: NETWORK_FETCHES.load(Ordering::Relaxed),
158            in_flight_waits: IN_FLIGHT_WAITS.load(Ordering::Relaxed),
159        }
160    }
161
162    pub async fn clear_cache() {
163        let mut guard = cache().write().await;
164        guard.clear();
165    }
166
167    pub async fn evict(bbox: &BoundingBox) -> bool {
168        let cache_key = bbox.cache_key();
169        let mut guard = cache().write().await;
170        guard.remove(&cache_key).is_some()
171    }
172
173    pub async fn cached_regions() -> Vec<BoundingBox> {
174        let guard = cache().read().await;
175        guard
176            .keys()
177            .filter_map(|key| parse_cache_key(key))
178            .collect()
179    }
180}
181
182fn parse_cache_key(key: &str) -> Option<BoundingBox> {
183    let parts: Vec<&str> = key.split('_').collect();
184    if parts.len() != 4 {
185        return None;
186    }
187
188    let min_lat: f64 = parts[0].parse().ok()?;
189    let min_lng: f64 = parts[1].parse().ok()?;
190    let max_lat: f64 = parts[2].parse().ok()?;
191    let max_lng: f64 = parts[3].parse().ok()?;
192
193    BoundingBox::try_new(min_lat, min_lng, max_lat, max_lng).ok()
194}