diff --git a/src/array.rs b/src/array.rs index bf3e364f..03ec693a 100644 --- a/src/array.rs +++ b/src/array.rs @@ -1320,17 +1320,17 @@ impl IArray { } } - pub(crate) fn mem_allocated(&self) -> usize { + pub(crate) fn mem_allocated(&self) -> f64 { if self.is_static() { - 0 + 0.0 } else { let tag = self.header().type_tag(); let layout_size = Self::layout(self.capacity(), tag).unwrap().size(); let contained_size = self .as_slice_of::() - .map(|slice| slice.iter().map(IValue::mem_allocated).sum()) - .unwrap_or(0); - layout_size + contained_size + .map(|slice| slice.iter().map(IValue::mem_allocated).sum::()) + .unwrap_or(0.0); + layout_size as f64 + contained_size } } } diff --git a/src/object.rs b/src/object.rs index 8cbe0062..403266bd 100644 --- a/src/object.rs +++ b/src/object.rs @@ -918,17 +918,17 @@ impl IObject { } } - pub(crate) fn mem_allocated(&self) -> usize { + pub(crate) fn mem_allocated(&self) -> f64 { if self.is_static() { - 0 + 0.0 } else { // Layout of a live object's own capacity; it allocated successfully, so // recomputing its layout cannot fail. - Self::layout(self.capacity()).unwrap().size() + Self::layout(self.capacity()).unwrap().size() as f64 + self .iter() .map(|(k, v)| k.mem_allocated() + v.mem_allocated()) - .sum::() + .sum::() } } } diff --git a/src/string.rs b/src/string.rs index 3dff2c32..9ac0e2ee 100644 --- a/src/string.rs +++ b/src/string.rs @@ -291,11 +291,12 @@ impl IString { } } - pub(crate) fn mem_allocated(&self) -> usize { + pub(crate) fn mem_allocated(&self) -> f64 { if self.is_empty() { - 0 + 0.0 } else { - Self::layout(self.len()).unwrap().size() + Self::layout(self.len()).unwrap().size() as f64 + / self.header().rc.load(AtomicOrdering::Relaxed) as f64 } } } diff --git a/src/unsafe_string.rs b/src/unsafe_string.rs index 4e46729a..54d74792 100644 --- a/src/unsafe_string.rs +++ b/src/unsafe_string.rs @@ -423,11 +423,14 @@ impl IString { self.drop_impl_with_deallocator(|ptr, layout| unsafe { dealloc(ptr, layout) }); } - pub(crate) fn mem_allocated(&self) -> usize { + pub(crate) fn mem_allocated(&self) -> f64 { if self.is_empty() || self.is_inline() { - 0 + 0.0 } else { - Self::layout(self.len()).unwrap().size() + // SAFETY: `self` keeps a counted heap reference alive, so rc is nonzero. + // Relaxed suffices for an estimate that may change with concurrent owners. + Self::layout(self.len()).unwrap().size() as f64 + / self.header().rc.load(std::sync::atomic::Ordering::Relaxed) as f64 } } } @@ -630,7 +633,7 @@ mod tests { assert_eq!(istr.as_bytes(), s.as_bytes()); // Inline strings should have minimal memory overhead - assert_eq!(istr.mem_allocated(), 0); + assert_eq!(istr.mem_allocated(), 0.0); } else { assert!(!istr.is_inline(), "String '{}' should not be inline", s); } @@ -651,7 +654,7 @@ mod tests { assert_eq!(istr.as_bytes(), s.as_bytes()); // Heap strings should have memory overhead - assert!(istr.mem_allocated() > 0); + assert!(istr.mem_allocated() > 0.0); } } diff --git a/src/value.rs b/src/value.rs index 8b99edac..ef14aca8 100644 --- a/src/value.rs +++ b/src/value.rs @@ -351,14 +351,17 @@ impl IValue { } } - /// Reports dynamic memory allocated by this value. - pub fn mem_allocated(&self) -> usize { + /// Reports proportional dynamic memory in bytes, excluding this value itself. + /// Shared strings are divided among all live references (including temporary + /// owners). Container capacity is fully counted; the intern table is excluded. + /// Retain fractional bytes until rounding the final measurement. + pub fn mem_allocated(&self) -> f64 { use ValueType::*; match self.type_() { // inline types consume no extra memory - Null | Bool => 0, + Null | Bool => 0.0, // Safety: We checked the type - Number => unsafe { self.as_number_unchecked() }.mem_allocated(), + Number => unsafe { self.as_number_unchecked() }.mem_allocated() as f64, String => unsafe { self.as_string_unchecked() }.mem_allocated(), Array => unsafe { self.as_array_unchecked() }.mem_allocated(), Object => unsafe { self.as_object_unchecked() }.mem_allocated(), @@ -1091,7 +1094,7 @@ mod tests { assert!(matches!(x.clone().destructure(), Destructured::Null)); assert!(matches!(x.clone().destructure_ref(), DestructuredRef::Null)); assert!(matches!(x.clone().destructure_mut(), DestructuredMut::Null)); - assert_eq!(x.mem_allocated(), 0); + assert_eq!(x.mem_allocated(), 0.0); } #[test] @@ -1112,7 +1115,7 @@ mod tests { } assert_eq!(x.to_bool(), Some(!v)); - assert_eq!(x.mem_allocated(), 0); + assert_eq!(x.mem_allocated(), 0.0); } } @@ -1147,9 +1150,9 @@ mod tests { assert_eq!( x.mem_allocated(), if (INLINE_LOWER..=INLINE_UPPER).contains(&v) { - 0 + 0.0 } else { - mem::size_of::() + mem::size_of::() as f64 } ); } @@ -1181,9 +1184,9 @@ mod tests { assert_eq!( x.mem_allocated(), if v <= i64::MAX as u64 && (INLINE_LOWER..=INLINE_UPPER).contains(&(v as i64)) { - 0 + 0.0 } else { - mem::size_of::() + mem::size_of::() as f64 } ); } @@ -1207,7 +1210,7 @@ mod tests { assert!( matches!(x.clone().destructure_mut(), DestructuredMut::Number(u) if *u == INumber::try_from(v).unwrap()) ); - assert_eq!(x.mem_allocated(), mem::size_of::()); + assert_eq!(x.mem_allocated(), mem::size_of::() as f64); } } } @@ -1224,7 +1227,7 @@ mod tests { assert!(matches!(x.clone().destructure(), Destructured::String(u) if u == s)); assert!(matches!(x.clone().destructure_ref(), DestructuredRef::String(u) if *u == s)); assert!(matches!(x.clone().destructure_mut(), DestructuredMut::String(u) if *u == s)); - assert_eq!(x.mem_allocated(), 0); + assert_eq!(x.mem_allocated(), 0.0); } let s = String::from("foofoofoo"); @@ -1236,7 +1239,7 @@ mod tests { assert!(matches!(x.clone().destructure(), Destructured::String(u) if u == s)); assert!(matches!(x.clone().destructure_ref(), DestructuredRef::String(u) if *u == s)); assert!(matches!(x.clone().destructure_mut(), DestructuredMut::String(u) if *u == s)); - assert_eq!(x.mem_allocated(), 24); + assert_eq!(x.mem_allocated(), 24.0); } #[mockalloc::test] @@ -1253,8 +1256,9 @@ mod tests { assert!(matches!(x.clone().destructure_mut(), DestructuredMut::Array(u) if *u == a)); assert_eq!( x.mem_allocated(), - mem::size_of::() - + ((a.capacity() as usize * mem::size_of::() + 7) & !7) + (mem::size_of::() + + ((a.capacity() as usize * mem::size_of::() + 7) & !7)) + as f64 ); } } @@ -1286,8 +1290,8 @@ mod tests { x.mem_allocated(), o.iter() .map(|(k, v)| k.mem_allocated() + v.mem_allocated()) - .sum::() - + ((raw + 7) & !7) + .sum::() + + ((raw + 7) & !7) as f64 ); } } diff --git a/tests/memory_share.rs b/tests/memory_share.rs new file mode 100644 index 00000000..f62e8709 --- /dev/null +++ b/tests/memory_share.rs @@ -0,0 +1,31 @@ +use ijson::IValue; + +#[test] +fn proportional_memory_across_documents() { + let text = "proportional allocation accounting test string"; + let first = IValue::from(text); + let allocation = first.mem_allocated(); + let mut docs: Vec = (0..99).map(|_| IValue::from(text)).collect(); + docs.push(first); + assert!(docs[0].mem_allocated() < 1.0); + assert!((docs.iter().map(IValue::mem_allocated).sum::() - allocation).abs() < 1e-9); + let one = docs.pop().unwrap(); + drop(docs); + assert_eq!(one.mem_allocated(), allocation); + drop(one); + + // A field name and nested values share one allocation across documents. + let json = format!(r#"{{"{text}":["{text}","{text}"]}}"#); + let doc: IValue = serde_json::from_str(&json).unwrap(); + let original = doc.mem_allocated(); + let other = IValue::from(text); + assert!((doc.mem_allocated() + other.mem_allocated() - original).abs() < 1e-9); + assert!((other.mem_allocated() - allocation / 4.0).abs() < 1e-9); + drop(other); + assert!((doc.mem_allocated() - original).abs() < 1e-9); + + for json in ["null", "true", "[]", "{}", r#""short""#] { + let value: IValue = serde_json::from_str(json).unwrap(); + assert_eq!(value.mem_allocated(), 0.0); + } +}