//! String interning utilities for memory optimization. //! //! Provides a global string interner to deduplicate frequently repeated //! strings like `input_name` and `group` in playlist items. use crate::model::UUIDType; use serde::{ de::{IgnoredAny, MapAccess, SeqAccess, Visitor}, Deserialize, Deserializer, Serializer, }; use std::{ borrow::Cow, collections::HashSet, fmt, sync::{Arc, LazyLock, RwLock}, }; /// Global interning pool. /// /// ## Performance (millions of entries) /// /// * **Happy-path (already-interned):** one `RwLock::read()` + hash lookup + /// `Arc::clone` (single atomic increment). Multiple threads can read /// concurrently without blocking each other. /// * **First-time intern:** upgrades to a write lock, double-checks, and /// inserts. This only happens *once per unique string value*, so the write /// path is not on the hot parse loop. /// * **`deserialize_string` vs `deserialize_any`:** the former is *faster* /// because saphyr skips bool / int / float parsing attempts and hands the /// raw scalar text directly to the visitor. /// * **Pruning:** call `interner_gc()` periodically (e.g. after a full /// playlist reload) to release strings that are only referenced by the /// pool itself. static INTERNER: LazyLock>>> = LazyLock::new(|| RwLock::new(HashSet::new())); pub trait Internable { fn intern(self) -> Arc; } impl Internable for &Arc { fn intern(self) -> Arc { Arc::clone(self) } } impl Internable for &Cow<'_, str> { fn intern(self) -> Arc { match self { Cow::Borrowed(s) => intern_str(s), Cow::Owned(s) => intern_string(s.clone()), } } } impl Internable for &UUIDType { fn intern(self) -> Arc { intern_string(self.to_string()) } } impl Internable for String { fn intern(self) -> Arc { intern_string(self) } } impl Internable for &String { fn intern(self) -> Arc { intern_str(self.as_str()) } } impl Internable for &str { fn intern(self) -> Arc { intern_str(self) } } impl Internable for u32 { fn intern(self) -> Arc { intern_string(self.to_string()) } } impl Internable for u64 { fn intern(self) -> Arc { intern_string(self.to_string()) } } impl Internable for i64 { fn intern(self) -> Arc { intern_string(self.to_string()) } } /// Interns a string slice. fn intern_str(s: &str) -> Arc { if let Ok(guard) = INTERNER.read() { if let Some(existing) = guard.get(s) { return Arc::clone(existing); } drop(guard); } if let Ok(mut guard) = INTERNER.write() { if let Some(existing) = guard.get(s) { return Arc::clone(existing); } let arc: Arc = Arc::from(s); guard.insert(Arc::clone(&arc)); return arc; } Arc::from(s) } /// Interns an owned string. fn intern_string(s: String) -> Arc { if let Ok(guard) = INTERNER.read() { if let Some(existing) = guard.get(s.as_str()) { return Arc::clone(existing); } drop(guard); } if let Ok(mut guard) = INTERNER.write() { if let Some(existing) = guard.get(s.as_str()) { return Arc::clone(existing); } let arc: Arc = Arc::from(s); guard.insert(Arc::clone(&arc)); return arc; } Arc::from(s) } /// Garbage collection: removes strings that are only referenced by the cache. pub fn interner_gc() -> usize { if let Ok(mut guard) = INTERNER.write() { let before = guard.len(); guard.retain(|s| Arc::strong_count(s) > 1); let removed = before - guard.len(); if removed > 0 { log::debug!("Pruned {removed} unused interned strings ({} remaining)", guard.len()); } return removed; } 0 } /// Convert an `f64` that reached `visit_f64` into a round-trip-safe string /// using the canonical YAML 1.2 spelling (`.inf`, `-.inf`, `.nan`). /// /// `serde_saphyr` recognises these spellings as ambiguous and **quotes** them /// when re-serializing, so the value survives a YAML round-trip as a string. /// /// This is a safety-net: the primary fix is using `deserialize_string` (which /// skips float parsing entirely), so `visit_f64` is normally not reached for /// plain string fields. #[inline] fn f64_to_str(v: f64) -> String { if v.is_infinite() { if v.is_sign_positive() { ".inf".to_owned() } else { "-.inf".to_owned() } } else if v.is_nan() { ".nan".to_owned() } else { v.to_string() } } // // Two reusable visitor types live here so that multiple public entry-points // can share them without code duplication: // // ArcStrVisitor -> Arc (null/empty -> "") // OptionArcStrVisitor -> Option> (null/empty -> None) // // Both visitors use `deserialize_string` inside `visit_some`, which tells // saphyr to return the **raw scalar text** without float-parsing. This is // what makes `name: infinity` survive as the literal string `"infinity"`. /// Visitor that produces `Arc`, mapping null / empty -> `""`. struct ArcStrVisitor; impl<'de> Visitor<'de> for ArcStrVisitor { type Value = Arc; fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str("a string, number, boolean, or null") } fn visit_str(self, v: &str) -> Result { Ok(v.intern()) } fn visit_string(self, v: String) -> Result { Ok(v.intern()) } fn visit_bool(self, v: bool) -> Result { Ok(v.to_string().intern()) } fn visit_i64(self, v: i64) -> Result { Ok(v.to_string().intern()) } fn visit_u64(self, v: u64) -> Result { Ok(v.to_string().intern()) } fn visit_f64(self, v: f64) -> Result { Ok(f64_to_str(v).intern()) } fn visit_unit(self) -> Result { Ok("".intern()) } fn visit_none(self) -> Result { Ok("".intern()) } fn visit_some>(self, d: D) -> Result { // `deserialize_string` returns the raw text -> `infinity` stays `infinity`. d.deserialize_string(self) } } /// Visitor that produces `Option>`, mapping null / empty -> `None`. struct OptionArcStrVisitor; impl<'de> Visitor<'de> for OptionArcStrVisitor { type Value = Option>; fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str("a string, number, boolean, null, or empty") } fn visit_str(self, v: &str) -> Result { Ok(Some(v.intern())) } fn visit_string(self, v: String) -> Result { Ok(Some(v.intern())) } fn visit_bool(self, v: bool) -> Result { Ok(Some(v.to_string().intern())) } fn visit_i64(self, v: i64) -> Result { Ok(Some(v.to_string().intern())) } fn visit_u64(self, v: u64) -> Result { Ok(Some(v.to_string().intern())) } fn visit_f64(self, v: f64) -> Result { Ok(Some(f64_to_str(v).intern())) } fn visit_unit(self) -> Result { Ok(None) } fn visit_none(self) -> Result { Ok(None) } fn visit_some>(self, d: D) -> Result { d.deserialize_string(self) } fn visit_seq>(self, mut seq: A) -> Result { while seq.next_element::()?.is_some() {} Ok(None) } fn visit_map>(self, mut map: A) -> Result { while map.next_entry::()?.is_some() {} Ok(None) } } pub mod arc_str_vec_serde { use super::*; use serde::ser::SerializeSeq; pub fn serialize(value: &Vec>, serializer: S) -> Result where S: Serializer, { let mut seq = serializer.serialize_seq(Some(value.len()))?; for s in value { seq.serialize_element(s.as_ref())?; } seq.end() } pub fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> where D: Deserializer<'de>, { let vec = Vec::::deserialize(deserializer)?; Ok(vec.into_iter().map(|s| s.intern()).collect()) } } pub mod arc_str_serde { use super::*; pub fn serialize(value: &Arc, serializer: S) -> Result where S: Serializer, { serializer.serialize_str(value) } /// Deserialize a YAML scalar as an interned `Arc`. /// /// Uses `deserialize_string` so saphyr hands us the **raw text** without /// first running it through float/bool/int parsing. This preserves values /// like `infinity` as the literal string `"infinity"` instead of silently /// converting them to `".inf"`. pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { deserializer.deserialize_string(ArcStrVisitor) } } pub mod arc_str_option_serde { use super::*; pub fn serialize(value: &Option>, serializer: S) -> Result where S: Serializer, { match value { Some(s) => serializer.serialize_str(s), None => serializer.serialize_none(), } } pub fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> where D: Deserializer<'de>, { deserializer.deserialize_option(OptionArcStrVisitor) } pub fn serialize_null_if_empty(value: &Option>, serializer: S) -> Result where S: Serializer, { match value { None => serializer.serialize_none(), Some(s) if s.is_empty() => serializer.serialize_none(), Some(s) => serializer.serialize_str(s), } } } // // Reuses `ArcStrVisitor` via `deserialize_option`: // - null / ~ / empty -> visit_none / visit_unit -> "" // - any other scalar -> visit_some -> deserialize_string -> visit_str/visit_string -> interned text pub use arc_str_default_on_null as arc_str_none_default_on_null; pub fn arc_str_default_on_null<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { deserializer.deserialize_option(ArcStrVisitor) } pub fn deserialize_as_option_arc_str<'de, D>(deserializer: D) -> Result>, D::Error> where D: Deserializer<'de>, { deserializer.deserialize_option(OptionArcStrVisitor) }