oxedyne/fe2o3/fe2o3_datime/src/cache/timezone_cache.rs
7.7 KiB, 32 runs
created by r1870400018:8327, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! Timezone calculation caching for performance optimisation. |
| 2 | //! |
| 3 | //! Offset calculations and DST transitions are expensive, and the same values |
| 4 | //! are asked for repeatedly. |
| 5 | //! |
| 6 | //! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\ |
| 7 | //! Anthropic Claude |
| 8 | |
| 9 | use crate::{ |
| 10 | cache::LruCache, |
| 11 | time::CalClockZone, |
| 12 | }; |
| 13 | |
| 14 | use oxedyne_fe2o3_core::prelude::*; |
| 15 | |
| 16 | use std::{ |
| 17 | sync::{Arc, OnceLock}, |
| 18 | collections::HashMap, |
| 19 | }; |
| 20 | |
| 21 | #[derive(Debug, Clone, Hash, PartialEq, Eq)] |
| 22 | struct TimezoneOffsetKey { |
| 23 | timezone_id: String, |
| 24 | timestamp_millis: i64, |
| 25 | } |
| 26 | |
| 27 | #[derive(Debug, Clone, Hash, PartialEq, Eq)] |
| 28 | struct DstCalculationKey { |
| 29 | timezone_id: String, |
| 30 | year: i32, |
| 31 | timestamp_millis: i64, |
| 32 | } |
| 33 | |
| 34 | #[derive(Debug, Clone)] |
| 35 | pub struct TimezoneCache { |
| 36 | offset_cache: LruCache<TimezoneOffsetKey, i32>, |
| 37 | dst_cache: LruCache<DstCalculationKey, bool>, |
| 38 | name_cache: Arc<HashMap<String, String>>, |
| 39 | } |
| 40 | |
| 41 | static TIMEZONE_CACHE: OnceLock<TimezoneCache> = OnceLock::new(); |
| 42 | |
| 43 | impl TimezoneCache { |
| 44 | fn new() -> Self { |
| 45 | Self { |
| 46 | offset_cache: LruCache::new(1000), // Cache up to 1000 offset calculations |
| 47 | dst_cache: LruCache::new(500), // Cache up to 500 DST calculations |
| 48 | name_cache: Arc::new(Self::build_name_cache()), |
| 49 | } |
| 50 | } |
| 51 | |
| 52 | pub fn global() -> &'static TimezoneCache { |
| 53 | TIMEZONE_CACHE.get_or_init(|| TimezoneCache::new()) |
| 54 | } |
| 55 | |
| 56 | fn build_name_cache() -> HashMap<String, String> { |
| 57 | let mut cache = HashMap::new(); |
| 58 | |
| 59 | // Common timezone abbreviations |
| 60 | cache.insert("America/New_York".to_string(), "EST".to_string()); |
| 61 | cache.insert("America/Chicago".to_string(), "CST".to_string()); |
| 62 | cache.insert("America/Denver".to_string(), "MST".to_string()); |
| 63 | cache.insert("America/Los_Angeles".to_string(), "PST".to_string()); |
| 64 | cache.insert("Europe/London".to_string(), "GMT".to_string()); |
| 65 | cache.insert("Europe/Paris".to_string(), "CET".to_string()); |
| 66 | cache.insert("Europe/Berlin".to_string(), "CET".to_string()); |
| 67 | cache.insert("Asia/Tokyo".to_string(), "JST".to_string()); |
| 68 | cache.insert("Asia/Shanghai".to_string(), "CST".to_string()); |
| 69 | cache.insert("Australia/Sydney".to_string(), "AEDT".to_string()); |
| 70 | cache.insert("UTC".to_string(), "UTC".to_string()); |
| 71 | cache.insert("GMT".to_string(), "GMT".to_string()); |
| 72 | |
| 73 | cache |
| 74 | } |
| 75 | |
| 76 | pub fn get_timezone_offset_cached<F>(&self, zone: &CalClockZone, timestamp_millis: i64, compute_fn: F) -> i32 |
| 77 | where |
| 78 | F: FnOnce() -> i32, |
| 79 | { |
| 80 | let key = TimezoneOffsetKey { |
| 81 | timezone_id: zone.id().to_string(), |
| 82 | timestamp_millis, |
| 83 | }; |
| 84 | |
| 85 | if let Some(cached_offset) = self.offset_cache.get(&key) { |
| 86 | cached_offset |
| 87 | } else { |
| 88 | let offset = compute_fn(); |
| 89 | self.offset_cache.insert(key, offset); |
| 90 | offset |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | pub fn get_dst_cached<F>(&self, zone: &CalClockZone, year: i32, timestamp_millis: i64, compute_fn: F) -> bool |
| 95 | where |
| 96 | F: FnOnce() -> bool, |
| 97 | { |
| 98 | let key = DstCalculationKey { |
| 99 | timezone_id: zone.id().to_string(), |
| 100 | year, |
| 101 | timestamp_millis, |
| 102 | }; |
| 103 | |
| 104 | if let Some(cached_dst) = self.dst_cache.get(&key) { |
| 105 | cached_dst |
| 106 | } else { |
| 107 | let is_dst = compute_fn(); |
| 108 | self.dst_cache.insert(key, is_dst); |
| 109 | is_dst |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | pub fn get_timezone_name(&self, timezone_id: &str) -> Option<String> { |
| 114 | self.name_cache.get(timezone_id).cloned() |
| 115 | } |
| 116 | |
| 117 | pub fn stats(&self) -> TimezoneStats { |
| 118 | let (offset_hits, offset_misses, offset_ratio) = self.offset_cache.stats(); |
| 119 | let (dst_hits, dst_misses, dst_ratio) = self.dst_cache.stats(); |
| 120 | |
| 121 | TimezoneStats { |
| 122 | offset_cache_hits: offset_hits, |
| 123 | offset_cache_misses: offset_misses, |
| 124 | offset_cache_hit_ratio: offset_ratio, |
| 125 | dst_cache_hits: dst_hits, |
| 126 | dst_cache_misses: dst_misses, |
| 127 | dst_cache_hit_ratio: dst_ratio, |
| 128 | name_cache_size: self.name_cache.len(), |
| 129 | } |
| 130 | } |
| 131 | |
| 132 | pub fn clear(&self) { |
| 133 | self.offset_cache.clear(); |
| 134 | self.dst_cache.clear(); |
| 135 | } |
| 136 | |
| 137 | pub fn preload_common_timezones(&self) { |
| 138 | // This could be enhanced to preload common timezone calculations |
| 139 | // for the current year and next year during application startup |
| 140 | // For now, it's a placeholder for future optimisation |
| 141 | } |
| 142 | } |
| 143 | |
| 144 | #[derive(Debug, Clone)] |
| 145 | pub struct TimezoneStats { |
| 146 | pub offset_cache_hits: u64, |
| 147 | pub offset_cache_misses: u64, |
| 148 | pub offset_cache_hit_ratio: f64, |
| 149 | pub dst_cache_hits: u64, |
| 150 | pub dst_cache_misses: u64, |
| 151 | pub dst_cache_hit_ratio: f64, |
| 152 | pub name_cache_size: usize, |
| 153 | } |
| 154 | |
| 155 | impl TimezoneStats { |
| 156 | /// A percentage, not a ratio. |
| 157 | pub fn overall_efficiency(&self) -> f64 { |
| 158 | let total_hits = self.offset_cache_hits + self.dst_cache_hits; |
| 159 | let total_requests = total_hits + self.offset_cache_misses + self.dst_cache_misses; |
| 160 | |
| 161 | if total_requests > 0 { |
| 162 | (total_hits as f64 / total_requests as f64) * 100.0 |
| 163 | } else { |
| 164 | 0.0 |
| 165 | } |
| 166 | } |
| 167 | |
| 168 | /// Well means a hit rate of 80% or better. |
| 169 | pub fn is_performing_well(&self) -> bool { |
| 170 | self.overall_efficiency() >= 80.0 |
| 171 | } |
| 172 | } |
| 173 | |
| 174 | pub trait CalClockZoneCached { |
| 175 | fn offset_millis_at_time_cached(&self, timestamp_millis: i64) -> Outcome<i32>; |
| 176 | |
| 177 | fn in_daylight_time_cached(&self, timestamp_millis: i64) -> Outcome<bool>; |
| 178 | |
| 179 | fn name_cached(&self) -> Option<String>; |
| 180 | } |
| 181 | |
| 182 | impl CalClockZoneCached for CalClockZone { |
| 183 | fn offset_millis_at_time_cached(&self, timestamp_millis: i64) -> Outcome<i32> { |
| 184 | let cache = TimezoneCache::global(); |
| 185 | |
| 186 | Ok(cache.get_timezone_offset_cached(self, timestamp_millis, || { |
| 187 | // Fallback to original calculation if cache fails |
| 188 | self.offset_millis_at_time(timestamp_millis).unwrap_or(0) |
| 189 | })) |
| 190 | } |
| 191 | |
| 192 | fn in_daylight_time_cached(&self, timestamp_millis: i64) -> Outcome<bool> { |
| 193 | let cache = TimezoneCache::global(); |
| 194 | |
| 195 | // Extract year from timestamp for cache key |
| 196 | let year = { |
| 197 | let days_since_epoch = timestamp_millis / (24 * 60 * 60 * 1000); |
| 198 | let epoch_year = 1970; |
| 199 | // Rough approximation - could be more accurate |
| 200 | epoch_year + (days_since_epoch / 365) as i32 |
| 201 | }; |
| 202 | |
| 203 | Ok(cache.get_dst_cached(self, year, timestamp_millis, || { |
| 204 | // Fallback to original calculation if cache fails |
| 205 | self.in_daylight_time(timestamp_millis).unwrap_or(false) |
| 206 | })) |
| 207 | } |
| 208 | |
| 209 | fn name_cached(&self) -> Option<String> { |
| 210 | let cache = TimezoneCache::global(); |
| 211 | cache.get_timezone_name(self.id()) |
| 212 | } |
| 213 | } |
| 214 | |
| 215 | #[cfg(test)] |
| 216 | mod tests { |
| 217 | use super::*; |
| 218 | use crate::time::CalClockZone; |
| 219 | |
| 220 | #[test] |
| 221 | fn test_timezone_cache_basic() { |
| 222 | let cache = TimezoneCache::new(); |
| 223 | let utc_zone = CalClockZone::utc(); |
| 224 | |
| 225 | // Test offset caching |
| 226 | let timestamp = 1640995200000; // 2022-01-01 00:00:00 UTC |
| 227 | let offset1 = cache.get_timezone_offset_cached(&utc_zone, timestamp, || 0); |
| 228 | let offset2 = cache.get_timezone_offset_cached(&utc_zone, timestamp, || 999); // Should not be called |
| 229 | |
| 230 | assert_eq!(offset1, 0); |
| 231 | assert_eq!(offset2, 0); // Should come from cache |
| 232 | } |
| 233 | |
| 234 | #[test] |
| 235 | fn test_timezone_name_cache() { |
| 236 | let cache = TimezoneCache::new(); |
| 237 | |
| 238 | // Test known timezone |
| 239 | assert_eq!(cache.get_timezone_name("America/New_York"), Some("EST".to_string())); |
| 240 | assert_eq!(cache.get_timezone_name("Europe/London"), Some("GMT".to_string())); |
| 241 | |
| 242 | // Test unknown timezone |
| 243 | assert_eq!(cache.get_timezone_name("Unknown/Timezone"), None); |
| 244 | } |
| 245 | |
| 246 | #[test] |
| 247 | fn test_timezone_stats() { |
| 248 | let cache = TimezoneCache::new(); |
| 249 | let utc_zone = CalClockZone::utc(); |
| 250 | |
| 251 | // Generate some cache activity |
| 252 | let _ = cache.get_timezone_offset_cached(&utc_zone, 1000, || 0); |
| 253 | let _ = cache.get_timezone_offset_cached(&utc_zone, 1000, || 0); // Cache hit |
| 254 | let _ = cache.get_timezone_offset_cached(&utc_zone, 2000, || 0); // Cache miss |
| 255 | |
| 256 | let stats = cache.stats(); |
| 257 | assert_eq!(stats.offset_cache_hits, 1); |
| 258 | assert_eq!(stats.offset_cache_misses, 2); |
| 259 | assert_eq!(stats.offset_cache_hit_ratio, 1.0 / 3.0); |
| 260 | } |
| 261 | |
| 262 | #[test] |
| 263 | fn test_cached_zone_extension() -> Outcome<()> { |
| 264 | let utc_zone = CalClockZone::utc(); |
| 265 | |
| 266 | // Test cached offset calculation |
| 267 | let offset = res!(utc_zone.offset_millis_at_time_cached(1640995200000)); |
| 268 | assert_eq!(offset, 0); |
| 269 | |
| 270 | // Test cached name lookup |
| 271 | let name = utc_zone.name_cached(); |
| 272 | assert_eq!(name, Some("UTC".to_string())); |
| 273 | |
| 274 | Ok(()) |
| 275 | } |
| 276 | } |