Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions src/array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<IValue>()
.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::<f64>())
.unwrap_or(0.0);
layout_size as f64 + contained_size
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions src/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<usize>()
.sum::<f64>()
}
}
}
Expand Down
7 changes: 4 additions & 3 deletions src/string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
Expand Down
13 changes: 8 additions & 5 deletions src/unsafe_string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please write a SAFETY comment on why we can deliberately divide by the rc value

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

}
}
}
Expand Down Expand Up @@ -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);
}
Expand All @@ -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);
}
}

Expand Down
38 changes: 21 additions & 17 deletions src/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just verify the callers/client of JSON.DEBUG ... doesn't expect an integer only and this is not an API change on JSON side

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified. RedisJSON rounds the final measurement and converts it to an integer. JSON.DEBUG MEMORY still returns an integer (or an array of integers for JSONPath), and MEMORY USAGE also remains integer-valued. Only the accounting changes, not the command response types.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But can it be changed to return float? If not, I wouldn't return float here wither

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess changing it to float is indeed breaking changed we dont want to do , However ,
I think we should return a float. If we sum up many float values, we could end up with a significant number of bytes that would simply be lost if the result were not a float.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But anyway you say we will round the result. Need to check if we can really return float(I guess we can, redis command api usually dont difference between float and integer AFAIR)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assume we have 1000 diffrent strings with each 3 ref count
each will have mem usage of X.33 .
If we wiil round it in the ijson path we might ignore 1000*0.3 bytes
I prefer return a regular int

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer to be exact if we can, so let's first understand if there is any issue at all

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(),
Expand Down Expand Up @@ -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]
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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::<i64>()
mem::size_of::<i64>() as f64
}
);
}
Expand Down Expand Up @@ -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::<u64>()
mem::size_of::<u64>() as f64
}
);
}
Expand All @@ -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::<f64>());
assert_eq!(x.mem_allocated(), mem::size_of::<f64>() as f64);
}
}
}
Expand All @@ -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");
Expand All @@ -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]
Expand All @@ -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::<usize>()
+ ((a.capacity() as usize * mem::size_of::<i32>() + 7) & !7)
(mem::size_of::<usize>()
+ ((a.capacity() as usize * mem::size_of::<i32>() + 7) & !7))
as f64
);
}
}
Expand Down Expand Up @@ -1286,8 +1290,8 @@ mod tests {
x.mem_allocated(),
o.iter()
.map(|(k, v)| k.mem_allocated() + v.mem_allocated())
.sum::<usize>()
+ ((raw + 7) & !7)
.sum::<f64>()
+ ((raw + 7) & !7) as f64
);
}
}
Expand Down
31 changes: 31 additions & 0 deletions tests/memory_share.rs
Original file line number Diff line number Diff line change
@@ -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<IValue> = (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::<f64>() - 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);
}
}
Loading