use parking_lot::RwLock;
use std::collections::HashMap;
pub struct StringInterner {
string_to_id: RwLock<HashMap<String, u32>>,
id_to_string: RwLock<HashMap<u32, String>>,
next_id: RwLock<u32>,
max_strings: usize,
}
impl StringInterner {
pub fn new(max_strings: usize) -> Self {
Self {
string_to_id: RwLock::new(HashMap::with_capacity(max_strings)),
id_to_string: RwLock::new(HashMap::with_capacity(max_strings)),
next_id: RwLock::new(1), max_strings,
}
}
pub fn intern(&self, s: &str) -> u32 {
{
let string_to_id = self.string_to_id.read();
if let Some(&id) = string_to_id.get(s) {
return id;
}
}
let mut string_to_id = self.string_to_id.write();
let mut id_to_string = self.id_to_string.write();
let mut next_id = self.next_id.write();
if let Some(&id) = string_to_id.get(s) {
return id;
}
if string_to_id.len() >= self.max_strings {
return *next_id - 1;
}
let id = *next_id;
string_to_id.insert(s.to_string(), id);
id_to_string.insert(id, s.to_string());
*next_id += 1;
id
}
pub fn get(&self, id: u32) -> Option<String> {
self.id_to_string.read().get(&id).cloned()
}
pub fn get_id(&self, s: &str) -> Option<u32> {
self.string_to_id.read().get(s).copied()
}
pub fn stats(&self) -> InternerStats {
let string_to_id = self.string_to_id.read();
let total_string_bytes: usize = string_to_id.keys().map(|s| s.len()).sum();
InternerStats {
interned_strings: string_to_id.len(),
max_strings: self.max_strings,
next_id: *self.next_id.read(),
total_string_bytes,
hash_map_overhead: string_to_id.capacity() * std::mem::size_of::<(String, u32)>(),
}
}
pub fn clear(&self) {
self.string_to_id.write().clear();
self.id_to_string.write().clear();
*self.next_id.write() = 1;
}
pub fn estimate_savings(&self) -> MemorySavings {
let stats = self.stats();
let string_to_id = self.string_to_id.read();
let mut total_references = 0;
let mut total_deduplicated_bytes = 0;
for (string, _id) in string_to_id.iter() {
let estimated_references = match string.len() {
0..=10 => 5, 11..=20 => 3, _ => 2, };
total_references += estimated_references;
total_deduplicated_bytes += string.len() * (estimated_references - 1);
}
MemorySavings {
total_references,
bytes_saved: total_deduplicated_bytes,
overhead_bytes: stats.total_string_bytes + stats.hash_map_overhead,
net_savings: total_deduplicated_bytes.saturating_sub(stats.hash_map_overhead),
}
}
}
#[derive(Debug, Clone)]
pub struct InternerStats {
pub interned_strings: usize,
pub max_strings: usize,
pub next_id: u32,
pub total_string_bytes: usize,
pub hash_map_overhead: usize,
}
impl InternerStats {
pub fn utilization(&self) -> f64 {
self.interned_strings as f64 / self.max_strings as f64
}
pub fn is_nearly_full(&self) -> bool {
self.utilization() > 0.9
}
}
#[derive(Debug, Clone)]
pub struct MemorySavings {
pub total_references: usize,
pub bytes_saved: usize,
pub overhead_bytes: usize,
pub net_savings: usize,
}
impl MemorySavings {
pub fn compression_ratio(&self) -> f64 {
if self.total_references == 0 {
1.0
} else {
(self.bytes_saved + self.overhead_bytes) as f64 / self.overhead_bytes as f64
}
}
pub fn is_beneficial(&self) -> bool {
self.bytes_saved > self.overhead_bytes
}
}
pub struct GraphStringInterner {
interner: StringInterner,
common_strings: Vec<&'static str>,
}
impl GraphStringInterner {
pub fn new(max_strings: usize) -> Self {
let common_strings = vec![
"id",
"name",
"type",
"value",
"label",
"properties",
"created_at",
"updated_at",
"timestamp",
"version",
"source",
"target",
"weight",
"distance",
"cost",
"true",
"false",
"null",
"undefined",
"active",
"inactive",
"pending",
"complete",
"failed",
"public",
"private",
"internal",
"external",
"CONNECTS",
"CONTAINS",
"BELONGS_TO",
"REFERENCES",
"FOLLOWS",
"FRIEND_OF",
"MEMBER_OF",
"PART_OF",
"HAS",
"OWNS",
"MANAGES",
"USES",
"DEPENDS_ON",
];
let interner = StringInterner::new(max_strings);
for &s in &common_strings {
interner.intern(s);
}
Self {
interner,
common_strings,
}
}
pub fn intern(&self, s: &str) -> u32 {
self.interner.intern(s)
}
pub fn get(&self, id: u32) -> Option<String> {
self.interner.get(id)
}
pub fn stats(&self) -> InternerStats {
self.interner.stats()
}
pub fn savings(&self) -> MemorySavings {
self.interner.estimate_savings()
}
pub fn is_common_string(&self, s: &str) -> bool {
self.common_strings.contains(&s)
}
pub fn common_strings(&self) -> &[&'static str] {
&self.common_strings
}
}
#[derive(Debug, Clone)]
pub enum InternedString {
Interned(u32),
Owned(String),
}
impl InternedString {
pub fn interned(id: u32) -> Self {
Self::Interned(id)
}
pub fn owned(s: String) -> Self {
Self::Owned(s)
}
pub fn resolve(&self, interner: &StringInterner) -> Option<String> {
match self {
Self::Interned(id) => interner.get(*id),
Self::Owned(s) => Some(s.clone()),
}
}
pub fn memory_footprint(&self) -> usize {
match self {
Self::Interned(_) => std::mem::size_of::<u32>(),
Self::Owned(s) => std::mem::size_of::<String>() + s.capacity(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_string_interner() {
let interner = StringInterner::new(100);
let id1 = interner.intern("hello");
let id2 = interner.intern("world");
let id3 = interner.intern("hello");
assert_eq!(id1, id3);
assert_ne!(id1, id2);
assert_eq!(interner.get(id1), Some("hello".to_string()));
assert_eq!(interner.get(id2), Some("world".to_string()));
assert_eq!(interner.get(999), None);
let stats = interner.stats();
assert_eq!(stats.interned_strings, 2);
assert!(stats.total_string_bytes > 0);
}
#[test]
fn test_graph_string_interner() {
let interner = GraphStringInterner::new(1000);
let id1 = interner.intern("name");
let id2 = interner.intern("name"); assert_eq!(id1, id2);
assert!(interner.is_common_string("name"));
assert!(!interner.is_common_string("some_unique_property"));
let stats = interner.stats();
assert!(stats.interned_strings >= interner.common_strings().len());
}
#[test]
fn test_interned_string() {
let interner = StringInterner::new(100);
let id = interner.intern("test");
let interned = InternedString::interned(id);
let owned = InternedString::owned("unique".to_string());
assert_eq!(interned.resolve(&interner), Some("test".to_string()));
assert_eq!(owned.resolve(&interner), Some("unique".to_string()));
assert_eq!(interned.memory_footprint(), 4); assert!(owned.memory_footprint() > 4); }
#[test]
fn test_memory_savings() {
let interner = StringInterner::new(100);
for _ in 0..10 {
interner.intern("name");
interner.intern("type");
interner.intern("value");
}
let savings = interner.estimate_savings();
assert!(savings.bytes_saved > 0);
assert!(savings.total_references > 0);
}
#[test]
fn test_interner_capacity() {
let interner = StringInterner::new(2);
let id1 = interner.intern("first");
let id2 = interner.intern("second");
let id3 = interner.intern("third");
assert_ne!(id1, id2);
assert_eq!(id3, id2);
}
#[test]
fn test_interner_stats() {
let interner = StringInterner::new(100);
interner.intern("test1");
interner.intern("test2");
interner.intern("longer_string_for_testing");
let stats = interner.stats();
assert_eq!(stats.interned_strings, 3);
assert!(stats.total_string_bytes >= 30); assert!(!stats.is_nearly_full());
assert!(stats.utilization() < 0.1);
}
}