1use indexmap::IndexMap;
7use indexmap::IndexSet;
8use once_cell::sync::Lazy;
9use smartstring::{LazyCompact, SmartString};
10use std::borrow::Cow;
11use std::sync::Arc;
12use string_cache::DefaultAtom;
13
14pub type FastString = SmartString<LazyCompact>;
16
17static COMMON_STRINGS: Lazy<IndexMap<&'static str, &'static str>> = Lazy::new(|| {
19 let mut map = IndexMap::new();
20
21 map.insert("4.3", "4.3");
23 map.insert("4.2", "4.2");
24 map.insert("4.1", "4.1");
25
26 map.insert("NewReleaseMessage", "NewReleaseMessage");
28 map.insert("PurgeReleaseMessage", "PurgeReleaseMessage");
29 map.insert("LiveMessage", "LiveMessage");
30
31 map.insert("MainArtist", "MainArtist");
33 map.insert("FeaturedArtist", "FeaturedArtist");
34 map.insert("Producer", "Producer");
35 map.insert("Composer", "Composer");
36 map.insert("Performer", "Performer");
37 map.insert("Engineer", "Engineer");
38 map.insert("Mixer", "Mixer");
39
40 map.insert("SoundRecording", "SoundRecording");
42 map.insert("Video", "Video");
43 map.insert("Image", "Image");
44 map.insert("Text", "Text");
45
46 map.insert("Single", "Single");
48 map.insert("Album", "Album");
49 map.insert("EP", "EP");
50 map.insert("Compilation", "Compilation");
51
52 map.insert("Rock", "Rock");
54 map.insert("Pop", "Pop");
55 map.insert("Electronic", "Electronic");
56 map.insert("Hip-Hop", "Hip-Hop");
57 map.insert("Classical", "Classical");
58 map.insert("Jazz", "Jazz");
59 map.insert("Country", "Country");
60 map.insert("R&B", "R&B");
61 map.insert("Folk", "Folk");
62 map.insert("Alternative", "Alternative");
63
64 map.insert("en", "en");
66 map.insert("es", "es");
67 map.insert("fr", "fr");
68 map.insert("de", "de");
69 map.insert("it", "it");
70 map.insert("pt", "pt");
71 map.insert("ja", "ja");
72 map.insert("ko", "ko");
73 map.insert("zh", "zh");
74
75 map.insert("US", "US");
77 map.insert("GB", "GB");
78 map.insert("CA", "CA");
79 map.insert("AU", "AU");
80 map.insert("DE", "DE");
81 map.insert("FR", "FR");
82 map.insert("JP", "JP");
83 map.insert("KR", "KR");
84
85 map.insert("SubscriptionModel", "SubscriptionModel");
87 map.insert("PermanentDownload", "PermanentDownload");
88 map.insert("AdSupportedModel", "AdSupportedModel");
89 map.insert("ConditionalDownload", "ConditionalDownload");
90
91 map.insert("℗ ", "℗ ");
93 map.insert("© ", "© ");
94
95 map
96});
97
98#[derive(Debug, Default)]
100pub struct StringInterner {
101 strings: IndexSet<Arc<str>>,
103 atoms: IndexMap<String, DefaultAtom>,
105}
106
107impl StringInterner {
108 pub fn new() -> Self {
110 Self {
111 strings: IndexSet::new(),
112 atoms: IndexMap::new(),
113 }
114 }
115
116 pub fn intern(&mut self, s: &str) -> Arc<str> {
118 if let Some(&static_str) = COMMON_STRINGS.get(s) {
120 return Arc::from(static_str);
121 }
122
123 if let Some(existing) = self.strings.get(s) {
125 return existing.clone();
126 }
127
128 let arc_str: Arc<str> = Arc::from(s);
130 self.strings.insert(arc_str.clone());
131 arc_str
132 }
133
134 pub fn intern_atom(&mut self, s: String) -> DefaultAtom {
136 if let Some(atom) = self.atoms.get(&s) {
137 return atom.clone();
138 }
139
140 let atom = DefaultAtom::from(s.as_str());
141 self.atoms.insert(s, atom.clone());
142 atom
143 }
144
145 pub fn memory_usage(&self) -> usize {
147 self.strings
148 .iter()
149 .map(|s| s.len() + std::mem::size_of::<Arc<str>>())
150 .sum::<usize>()
151 + self.atoms.len() * std::mem::size_of::<DefaultAtom>()
152 }
153
154 pub fn clear(&mut self) {
156 self.strings.clear();
157 self.atoms.clear();
158 }
159}
160
161#[derive(Debug, Clone, PartialEq, Eq, Hash)]
163pub enum OptimizedString {
164 Static(&'static str),
166 Interned(Arc<str>),
168 Small(FastString),
170 Atom(DefaultAtom),
172}
173
174impl OptimizedString {
175 pub fn new(s: &str) -> Self {
177 if let Some(&static_str) = COMMON_STRINGS.get(s) {
179 return OptimizedString::Static(static_str);
180 }
181
182 if s.len() <= 23 {
184 OptimizedString::Small(FastString::from(s))
186 } else {
187 OptimizedString::Small(FastString::from(s))
189 }
190 }
191
192 pub fn interned(s: Arc<str>) -> Self {
194 OptimizedString::Interned(s)
195 }
196
197 pub fn atom(atom: DefaultAtom) -> Self {
199 OptimizedString::Atom(atom)
200 }
201
202 pub fn as_str(&self) -> &str {
204 match self {
205 OptimizedString::Static(s) => s,
206 OptimizedString::Interned(s) => s,
207 OptimizedString::Small(s) => s,
208 OptimizedString::Atom(atom) => atom,
209 }
210 }
211
212 pub fn memory_footprint(&self) -> usize {
214 match self {
215 OptimizedString::Static(_) => 0, OptimizedString::Interned(_) => std::mem::size_of::<Arc<str>>(),
217 OptimizedString::Small(s) => s.capacity(),
218 OptimizedString::Atom(_) => std::mem::size_of::<DefaultAtom>(),
219 }
220 }
221}
222
223impl AsRef<str> for OptimizedString {
224 fn as_ref(&self) -> &str {
225 self.as_str()
226 }
227}
228
229impl std::fmt::Display for OptimizedString {
230 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231 f.write_str(self.as_str())
232 }
233}
234
235pub type CowString = Cow<'static, str>;
237
238#[derive(Debug, Clone, PartialEq, Eq)]
240pub struct OptimizedLocalizedString {
241 pub text: OptimizedString,
243 pub language_code: Option<OptimizedString>,
245}
246
247impl OptimizedLocalizedString {
248 pub fn new(text: &str, language_code: Option<&str>) -> Self {
250 Self {
251 text: OptimizedString::new(text),
252 language_code: language_code.map(OptimizedString::new),
253 }
254 }
255
256 pub fn memory_footprint(&self) -> usize {
258 self.text.memory_footprint()
259 + self
260 .language_code
261 .as_ref()
262 .map(|lc| lc.memory_footprint())
263 .unwrap_or(0)
264 }
265}
266
267#[derive(Debug, Default)]
269pub struct BufferPool {
270 buffers: Vec<String>,
271 current_size: usize,
272}
273
274impl BufferPool {
275 pub fn new() -> Self {
277 Self {
278 buffers: Vec::new(),
279 current_size: 0,
280 }
281 }
282
283 pub fn get_buffer(&mut self, estimated_size: usize) -> String {
285 match self.buffers.pop() {
286 Some(mut buffer) => {
287 buffer.clear();
288 if buffer.capacity() < estimated_size {
289 buffer.reserve(estimated_size - buffer.capacity());
290 }
291 buffer
292 }
293 None => {
294 self.current_size += estimated_size;
295 String::with_capacity(estimated_size)
296 }
297 }
298 }
299
300 pub fn return_buffer(&mut self, buffer: String) {
302 if buffer.capacity() <= 8192 {
303 self.buffers.push(buffer);
305 }
306 }
307
308 pub fn memory_usage(&self) -> usize {
310 self.current_size + self.buffers.iter().map(|b| b.capacity()).sum::<usize>()
311 }
312
313 pub fn clear(&mut self) {
315 self.buffers.clear();
316 self.current_size = 0;
317 }
318}
319
320#[derive(Debug, Default)]
322pub struct BuildContext {
323 pub interner: StringInterner,
325 pub buffer_pool: BufferPool,
327 pub stats: BuildStats,
329}
330
331impl BuildContext {
332 pub fn new() -> Self {
334 Self {
335 interner: StringInterner::new(),
336 buffer_pool: BufferPool::new(),
337 stats: BuildStats::default(),
338 }
339 }
340
341 pub fn optimize_string(&mut self, s: &str) -> OptimizedString {
343 self.stats.strings_processed += 1;
344
345 if COMMON_STRINGS.contains_key(s) {
347 self.stats.static_cache_hits += 1;
348 return OptimizedString::new(s);
349 }
350
351 if s.len() > 23 {
353 let interned = self.interner.intern(s);
355 self.stats.interned_strings += 1;
356 OptimizedString::interned(interned)
357 } else {
358 OptimizedString::new(s)
359 }
360 }
361
362 pub fn get_xml_buffer(&mut self, estimated_size: usize) -> String {
364 self.stats.buffers_requested += 1;
365 self.buffer_pool.get_buffer(estimated_size)
366 }
367
368 pub fn return_xml_buffer(&mut self, buffer: String) {
370 self.buffer_pool.return_buffer(buffer);
371 }
372
373 pub fn memory_usage(&self) -> MemoryUsage {
375 MemoryUsage {
376 interner_bytes: self.interner.memory_usage(),
377 buffer_pool_bytes: self.buffer_pool.memory_usage(),
378 total_bytes: self.interner.memory_usage() + self.buffer_pool.memory_usage(),
379 }
380 }
381
382 pub fn reset_for_next_build(&mut self) {
384 self.buffer_pool.clear();
386 self.stats = BuildStats::default();
387 }
388
389 pub fn full_reset(&mut self) {
391 self.interner.clear();
392 self.buffer_pool.clear();
393 self.stats = BuildStats::default();
394 }
395}
396
397#[derive(Debug, Default, Clone)]
399pub struct BuildStats {
400 pub strings_processed: usize,
402 pub static_cache_hits: usize,
404 pub interned_strings: usize,
406 pub buffers_requested: usize,
408}
409
410#[derive(Debug, Clone)]
412pub struct MemoryUsage {
413 pub interner_bytes: usize,
415 pub buffer_pool_bytes: usize,
417 pub total_bytes: usize,
419}
420
421pub mod buffer_sizes {
423 pub const SINGLE_TRACK_XML: usize = 8_192; pub const ALBUM_12_TRACKS_XML: usize = 65_536; pub const COMPILATION_100_TRACKS_XML: usize = 524_288; pub const BUFFER_OVERHEAD_FACTOR: f32 = 1.2; pub fn estimated_xml_size(track_count: usize) -> usize {
435 let base_size = match track_count {
436 1 => SINGLE_TRACK_XML,
437 2..=20 => ALBUM_12_TRACKS_XML,
438 _ => COMPILATION_100_TRACKS_XML,
439 };
440
441 let scaled = if track_count <= 20 {
443 (base_size * track_count / 12).max(SINGLE_TRACK_XML)
444 } else {
445 COMPILATION_100_TRACKS_XML * track_count / 100
446 };
447
448 (scaled as f32 * BUFFER_OVERHEAD_FACTOR) as usize
449 }
450}
451
452#[cfg(test)]
453mod tests {
454 use super::*;
455
456 #[test]
457 fn test_optimized_string_static_cache() {
458 let s = OptimizedString::new("MainArtist");
459 match s {
460 OptimizedString::Static(val) => assert_eq!(val, "MainArtist"),
461 _ => panic!("Expected static string"),
462 }
463
464 assert_eq!(s.memory_footprint(), 0);
466 }
467
468 #[test]
469 fn test_string_interner() {
470 let mut interner = StringInterner::new();
471
472 let s1 = interner.intern("Custom Artist Name");
473 let s2 = interner.intern("Custom Artist Name");
474
475 assert_eq!(s1.as_ptr(), s2.as_ptr());
477 }
478
479 #[test]
480 fn test_buffer_pool() {
481 let mut pool = BufferPool::new();
482
483 let mut buffer = pool.get_buffer(1024);
484 buffer.push_str("test content");
485
486 assert!(buffer.capacity() >= 1024);
487
488 pool.return_buffer(buffer);
489
490 let buffer2 = pool.get_buffer(512);
491 assert!(buffer2.is_empty());
492 assert!(buffer2.capacity() >= 1024); }
494
495 #[test]
496 fn test_buffer_size_estimation() {
497 assert_eq!(
498 buffer_sizes::estimated_xml_size(1),
499 (buffer_sizes::SINGLE_TRACK_XML as f32 * buffer_sizes::BUFFER_OVERHEAD_FACTOR) as usize
500 );
501
502 assert_eq!(
503 buffer_sizes::estimated_xml_size(12),
504 (buffer_sizes::ALBUM_12_TRACKS_XML as f32 * buffer_sizes::BUFFER_OVERHEAD_FACTOR)
505 as usize
506 );
507
508 let size_100 = buffer_sizes::estimated_xml_size(100);
510 let size_200 = buffer_sizes::estimated_xml_size(200);
511 assert!(size_200 > size_100);
512 }
513}