-
Notifications
You must be signed in to change notification settings - Fork 265
fix(memory): prepared statements cache retains gigabytes after statement spike #1282
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
2c42be5
c098adc
f0c78c2
255c5ae
4250a85
bbc9667
6cd9689
ef94d10
9244f67
040d72c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,10 @@ use lru::LruCache; | |
| use std::collections::{BTreeMap, HashMap, HashSet, VecDeque}; | ||
| use std::hash::Hash; | ||
|
|
||
| /// Approximate bytes attributable to a value, for metrics. | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A note on the accounting precision, since it is not obvious from the diff:
The overcount is bounded by |
||
| /// | ||
| /// Scalars report their inline size, containers report allocated capacity | ||
| /// plus the sum over elements: treat results as an upper bound. | ||
| pub trait MemoryUsage { | ||
| fn memory_usage(&self) -> usize; | ||
| } | ||
|
|
@@ -54,12 +58,17 @@ impl<V: MemoryUsage> MemoryUsage for Vec<V> { | |
| } | ||
| } | ||
|
|
||
| impl<K: MemoryUsage, V: MemoryUsage> MemoryUsage for HashMap<K, V> { | ||
| impl<K: MemoryUsage, V: MemoryUsage, S> MemoryUsage for HashMap<K, V, S> { | ||
| #[inline(always)] | ||
| fn memory_usage(&self) -> usize { | ||
| self.iter() | ||
| .map(|(k, v)| k.memory_usage() + v.memory_usage()) | ||
| .sum::<usize>() | ||
| // The table allocates capacity() slots (plus one control byte each), | ||
| // not len(): spare capacity left behind by removed entries still | ||
| // occupies memory and has to be counted. | ||
| self.capacity() * (std::mem::size_of::<(K, V)>() + 1) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is O(n). We should be careful not to call this frequently. I had to debug CPU usage issues with this before.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair concern — the per-entry |
||
| + self | ||
| .iter() | ||
| .map(|(k, v)| k.memory_usage() + v.memory_usage()) | ||
| .sum::<usize>() | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -72,10 +81,11 @@ impl<K: MemoryUsage, V: MemoryUsage> MemoryUsage for BTreeMap<K, V> { | |
| } | ||
| } | ||
|
|
||
| impl<V: MemoryUsage> MemoryUsage for HashSet<V> { | ||
| impl<V: MemoryUsage, S> MemoryUsage for HashSet<V, S> { | ||
| #[inline(always)] | ||
| fn memory_usage(&self) -> usize { | ||
| self.iter().map(|v| v.memory_usage()).sum::<usize>() | ||
| self.capacity() * (std::mem::size_of::<V>() + 1) | ||
| + self.iter().map(|v| v.memory_usage()).sum::<usize>() | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -101,3 +111,38 @@ impl MemoryUsage for Bytes { | |
| 0 | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| #[test] | ||
| fn hash_map_counts_spare_capacity() { | ||
| let mut map: HashMap<usize, usize> = HashMap::new(); | ||
| for i in 0..1000 { | ||
| map.insert(i, i); | ||
| } | ||
| let capacity = map.capacity(); | ||
| for i in 0..1000 { | ||
| map.remove(&i); | ||
| } | ||
| assert!(map.is_empty()); | ||
| // The allocation survives removals; capacity() may dip slightly | ||
| // due to tombstones but stays the same order of magnitude. | ||
| assert!(map.capacity() * 2 >= capacity); | ||
| let floor = map.capacity() * (std::mem::size_of::<(usize, usize)>() + 1); | ||
| assert!(map.memory_usage() >= floor); | ||
| } | ||
|
|
||
| #[test] | ||
| fn hash_set_counts_spare_capacity() { | ||
| let mut set: HashSet<usize> = HashSet::new(); | ||
| for i in 0..1000 { | ||
| set.insert(i); | ||
| } | ||
| let capacity = set.capacity(); | ||
| set.clear(); | ||
| assert_eq!(set.capacity(), capacity); | ||
| assert!(set.memory_usage() >= capacity * (std::mem::size_of::<usize>() + 1)); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not call
shrink_to_fitinstead? https://doc.rust-lang.org/stable/std/collections/struct.HashMap.html#method.shrink_to_fitThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, switched to
shrink_to_fitin d0e0a503. The hysteresis guards stay (only shrink tables over 4096 slots that are less than 1/8 full), so the once-a-second sweep never rehashes in steady state — the shrink fires once after a spike drains.