Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_datime/src/cache/string_intern.rs

8.4 KiB, 33 runs

created by r1870400018:8325, 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//! String interning system for performance optimisation.
2//!
3//! Equal strings share one allocation, which cuts memory use and the cost of
4//! repeated comparison.
5//!
6//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
7//! Anthropic Claude
8
9use oxedyne_fe2o3_core::prelude::*;
10
11use std::{
12 collections::HashMap,
13 sync::{Arc, RwLock, OnceLock},
14};
15
16#[derive(Debug)]
17pub struct GlobalStringInterner {
18 month_names: RwLock<HashMap<String, Arc<String>>>,
19 day_names: RwLock<HashMap<String, Arc<String>>>,
20 timezone_names: RwLock<HashMap<String, Arc<String>>>,
21 format_patterns: RwLock<HashMap<String, Arc<String>>>,
22 general_strings: RwLock<HashMap<String, Arc<String>>>,
23}
24
25static GLOBAL_INTERNER: OnceLock<GlobalStringInterner> = OnceLock::new();
26
27impl GlobalStringInterner {
28 fn new() -> Self {
29 let mut interner = Self {
30 month_names: RwLock::new(HashMap::new()),
31 day_names: RwLock::new(HashMap::new()),
32 timezone_names: RwLock::new(HashMap::new()),
33 format_patterns: RwLock::new(HashMap::new()),
34 general_strings: RwLock::new(HashMap::new()),
35 };
36
37 interner.preload_common_strings();
38 interner
39 }
40
41 pub fn global() -> &'static GlobalStringInterner {
42 GLOBAL_INTERNER.get_or_init(|| GlobalStringInterner::new())
43 }
44
45 fn preload_common_strings(&mut self) {
46 // Preload month names
47 let month_names = [
48 "January", "February", "March", "April", "May", "June",
49 "July", "August", "September", "October", "November", "December",
50 "Jan", "Feb", "Mar", "Apr", "May", "Jun",
51 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
52 ];
53
54 if let Ok(mut cache) = self.month_names.write() {
55 for name in &month_names {
56 cache.insert(name.to_string(), Arc::new(name.to_string()));
57 }
58 }
59
60 // Preload day names
61 let day_names = [
62 "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday",
63 "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun",
64 ];
65
66 if let Ok(mut cache) = self.day_names.write() {
67 for name in &day_names {
68 cache.insert(name.to_string(), Arc::new(name.to_string()));
69 }
70 }
71
72 // Preload timezone names
73 let timezone_names = [
74 "UTC", "GMT", "EST", "CST", "MST", "PST", "EDT", "CDT", "MDT", "PDT",
75 "CET", "EET", "JST", "AEDT", "AEST", "BST", "IST",
76 ];
77
78 if let Ok(mut cache) = self.timezone_names.write() {
79 for name in &timezone_names {
80 cache.insert(name.to_string(), Arc::new(name.to_string()));
81 }
82 }
83
84 // Preload common format patterns
85 let format_patterns = [
86 "yyyy-MM-dd", "dd/MM/yyyy", "MM/dd/yyyy", "yyyy/MM/dd",
87 "HH:mm:ss", "h:mm:ss a", "HH:mm", "h:mm a",
88 "yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd HH:mm:ss",
89 "EEEE, MMMM d, yyyy", "EEE, MMM d, yyyy",
90 ];
91
92 if let Ok(mut cache) = self.format_patterns.write() {
93 for pattern in &format_patterns {
94 cache.insert(pattern.to_string(), Arc::new(pattern.to_string()));
95 }
96 }
97
98 // Preload general strings
99 let general_strings = [
100 "AM", "PM", "am", "pm", "a.m.", "p.m.",
101 "CE", "BCE", "Common Era", "Before Common Era",
102 "st", "nd", "rd", "th", // Ordinal suffixes
103 "T", "Z", "+", "-", ":", "/", "-", ".", ",", " ",
104 ];
105
106 if let Ok(mut cache) = self.general_strings.write() {
107 for s in &general_strings {
108 cache.insert(s.to_string(), Arc::new(s.to_string()));
109 }
110 }
111 }
112
113 pub fn intern_month_name(&self, name: &str) -> Arc<String> {
114 self.intern_in_cache(&self.month_names, name)
115 }
116
117 pub fn intern_day_name(&self, name: &str) -> Arc<String> {
118 self.intern_in_cache(&self.day_names, name)
119 }
120
121 pub fn intern_timezone_name(&self, name: &str) -> Arc<String> {
122 self.intern_in_cache(&self.timezone_names, name)
123 }
124
125 pub fn intern_format_pattern(&self, pattern: &str) -> Arc<String> {
126 self.intern_in_cache(&self.format_patterns, pattern)
127 }
128
129 pub fn intern_general(&self, s: &str) -> Arc<String> {
130 self.intern_in_cache(&self.general_strings, s)
131 }
132
133 fn intern_in_cache(&self, cache: &RwLock<HashMap<String, Arc<String>>>, s: &str) -> Arc<String> {
134 // Try read lock first for common case
135 if let Ok(read_cache) = cache.read() {
136 if let Some(interned) = read_cache.get(s) {
137 return Arc::clone(interned);
138 }
139 }
140
141 // Need to insert - upgrade to write lock
142 if let Ok(mut write_cache) = cache.write() {
143 // Double-check in case another thread inserted while we waited
144 if let Some(interned) = write_cache.get(s) {
145 return Arc::clone(interned);
146 }
147
148 let arc_string = Arc::new(s.to_string());
149 write_cache.insert(s.to_string(), Arc::clone(&arc_string));
150 arc_string
151 } else {
152 // Fallback if lock fails
153 Arc::new(s.to_string())
154 }
155 }
156
157 pub fn stats(&self) -> InternerStats {
158 let month_count = if let Ok(cache) = self.month_names.read() { cache.len() } else { 0 };
159 let day_count = if let Ok(cache) = self.day_names.read() { cache.len() } else { 0 };
160 let timezone_count = if let Ok(cache) = self.timezone_names.read() { cache.len() } else { 0 };
161 let pattern_count = if let Ok(cache) = self.format_patterns.read() { cache.len() } else { 0 };
162 let general_count = if let Ok(cache) = self.general_strings.read() { cache.len() } else { 0 };
163
164 InternerStats {
165 month_names_count: month_count,
166 day_names_count: day_count,
167 timezone_names_count: timezone_count,
168 format_patterns_count: pattern_count,
169 general_strings_count: general_count,
170 total_interned: month_count + day_count + timezone_count + pattern_count + general_count,
171 }
172 }
173
174 pub fn clear_all(&self) {
175 let _ = self.month_names.write().map(|mut cache| cache.clear());
176 let _ = self.day_names.write().map(|mut cache| cache.clear());
177 let _ = self.timezone_names.write().map(|mut cache| cache.clear());
178 let _ = self.format_patterns.write().map(|mut cache| cache.clear());
179 let _ = self.general_strings.write().map(|mut cache| cache.clear());
180 }
181}
182
183#[derive(Debug, Clone)]
184pub struct InternerStats {
185 pub month_names_count: usize,
186 pub day_names_count: usize,
187 pub timezone_names_count: usize,
188 pub format_patterns_count: usize,
189 pub general_strings_count: usize,
190 pub total_interned: usize,
191}
192
193impl InternerStats {
194 pub fn estimated_memory_saved_bytes(&self) -> usize {
195 // Rough estimate: each interned string saves about 24 bytes
196 // (String overhead) per duplicate reference
197 self.total_interned * 24
198 }
199}
200
201pub mod convenience {
202 use super::*;
203
204 pub fn intern_month(name: &str) -> Arc<String> {
205 GlobalStringInterner::global().intern_month_name(name)
206 }
207
208 pub fn intern_day(name: &str) -> Arc<String> {
209 GlobalStringInterner::global().intern_day_name(name)
210 }
211
212 pub fn intern_timezone(name: &str) -> Arc<String> {
213 GlobalStringInterner::global().intern_timezone_name(name)
214 }
215
216 pub fn intern_pattern(pattern: &str) -> Arc<String> {
217 GlobalStringInterner::global().intern_format_pattern(pattern)
218 }
219
220 pub fn intern(s: &str) -> Arc<String> {
221 GlobalStringInterner::global().intern_general(s)
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn test_string_interning() {
231 let interner = GlobalStringInterner::new();
232
233 // Test month name interning
234 let jan1 = interner.intern_month_name("January");
235 let jan2 = interner.intern_month_name("January");
236 assert!(Arc::ptr_eq(&jan1, &jan2)); // Same allocation
237
238 // Test day name interning
239 let mon1 = interner.intern_day_name("Monday");
240 let mon2 = interner.intern_day_name("Monday");
241 assert!(Arc::ptr_eq(&mon1, &mon2)); // Same allocation
242
243 // Test different strings
244 let jan = interner.intern_month_name("January");
245 let feb = interner.intern_month_name("February");
246 assert!(!Arc::ptr_eq(&jan, &feb)); // Different allocations
247 }
248
249 #[test]
250 fn test_preloaded_strings() {
251 let interner = GlobalStringInterner::new();
252
253 // Test that common strings are preloaded
254 let stats = interner.stats();
255 assert!(stats.month_names_count > 0);
256 assert!(stats.day_names_count > 0);
257 assert!(stats.timezone_names_count > 0);
258 assert!(stats.format_patterns_count > 0);
259 assert!(stats.general_strings_count > 0);
260 }
261
262 #[test]
263 fn test_convenience_functions() {
264 // Test convenience functions
265 let jan1 = convenience::intern_month("January");
266 let jan2 = convenience::intern_month("January");
267 assert!(Arc::ptr_eq(&jan1, &jan2));
268
269 let utc1 = convenience::intern_timezone("UTC");
270 let utc2 = convenience::intern_timezone("UTC");
271 assert!(Arc::ptr_eq(&utc1, &utc2));
272 }
273
274 #[test]
275 fn test_interner_stats() {
276 let interner = GlobalStringInterner::new();
277
278 // Add some strings
279 let _ = interner.intern_month_name("NewMonth");
280 let _ = interner.intern_day_name("NewDay");
281
282 let stats = interner.stats();
283 assert!(stats.total_interned > 0);
284 assert!(stats.estimated_memory_saved_bytes() > 0);
285 }
286}