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