Skip to main content

vector_core/
stats.rs

1//! Cache statistics and memory measurement for benchmarking.
2//!
3//! This module provides tools for measuring memory usage and performance
4//! of the message cache, enabling before/after comparison during optimization.
5//!
6//! Only compiled in debug builds (`#[cfg(debug_assertions)]`).
7
8use std::time::Duration;
9
10use crate::types::{Message, Attachment, Reaction, EditEntry, ImageMetadata, SiteMetadata};
11use crate::compact::{CompactMessage, CompactMessageVec, CompactReaction, CompactAttachment, MessageFlags, NpubInterner};
12use crate::profile::{Profile, ProfileFlags};
13use crate::chat::Chat;
14
15/// Statistics for message cache operations
16#[derive(Debug, Default, Clone)]
17pub struct CacheStats {
18    /// Total number of messages across all chats
19    pub message_count: usize,
20    /// Total number of chats
21    pub chat_count: usize,
22    /// Estimated total memory in bytes
23    pub total_memory_bytes: usize,
24    /// Duration of last insert operation
25    pub last_insert_duration: Duration,
26    /// Average insert duration in nanoseconds
27    pub avg_insert_duration_ns: u64,
28    /// Number of insert operations recorded
29    pub insert_count: u64,
30    /// Total nanoseconds spent inserting
31    insert_total_ns: u64,
32}
33
34impl CacheStats {
35    pub fn new() -> Self {
36        Self::default()
37    }
38
39    /// Record an insert operation's duration
40    pub fn record_insert(&mut self, duration: Duration) {
41        self.last_insert_duration = duration;
42        self.insert_count += 1;
43        self.insert_total_ns += duration.as_nanos() as u64;
44        self.avg_insert_duration_ns = self.insert_total_ns / self.insert_count;
45    }
46
47    /// Update memory stats from current state
48    pub fn update_from_chats(&mut self, chats: &[Chat]) {
49        self.chat_count = chats.len();
50        self.message_count = chats.iter().map(|c| c.messages.len()).sum();
51        self.total_memory_bytes = chats.iter().map(|c| c.deep_size()).sum();
52    }
53
54    /// Print current stats
55    pub fn log(&self) {
56        println!(
57            "[CacheStats] chats={} messages={} memory={} last_insert={:?} avg_insert={}ns inserts={}",
58            self.chat_count,
59            self.message_count,
60            format_bytes(self.total_memory_bytes),
61            self.last_insert_duration,
62            self.avg_insert_duration_ns,
63            self.insert_count,
64        );
65    }
66
67    /// Get a summary string
68    pub fn summary(&self) -> String {
69        format!(
70            "chats={} msgs={} mem={} avg_insert={}ns",
71            self.chat_count,
72            self.message_count,
73            format_bytes(self.total_memory_bytes),
74            self.avg_insert_duration_ns,
75        )
76    }
77
78    /// Check if we should log (every N inserts)
79    pub fn should_log(&self, interval: u64) -> bool {
80        self.insert_count > 0 && self.insert_count % interval == 0
81    }
82}
83
84/// Format bytes as human-readable string
85fn format_bytes(bytes: usize) -> String {
86    if bytes >= 1024 * 1024 {
87        format!("{:.2}MB", bytes as f64 / (1024.0 * 1024.0))
88    } else if bytes >= 1024 {
89        format!("{:.2}KB", bytes as f64 / 1024.0)
90    } else {
91        format!("{}B", bytes)
92    }
93}
94
95/// Trait for calculating deep/heap memory size of a value
96pub trait DeepSize {
97    fn deep_size(&self) -> usize;
98}
99
100// === Primitive implementations ===
101
102impl DeepSize for String {
103    #[inline]
104    fn deep_size(&self) -> usize {
105        std::mem::size_of::<String>() + self.capacity()
106    }
107}
108
109impl DeepSize for u64 {
110    #[inline]
111    fn deep_size(&self) -> usize { std::mem::size_of::<u64>() }
112}
113
114impl DeepSize for u32 {
115    #[inline]
116    fn deep_size(&self) -> usize { std::mem::size_of::<u32>() }
117}
118
119impl DeepSize for bool {
120    #[inline]
121    fn deep_size(&self) -> usize { std::mem::size_of::<bool>() }
122}
123
124// === Generic implementations ===
125
126impl<T: DeepSize> DeepSize for Vec<T> {
127    fn deep_size(&self) -> usize {
128        std::mem::size_of::<Vec<T>>()
129            + self.capacity() * std::mem::size_of::<T>()
130            + self.iter().map(|item| item.deep_size().saturating_sub(std::mem::size_of::<T>())).sum::<usize>()
131    }
132}
133
134impl<T: DeepSize> DeepSize for Option<T> {
135    fn deep_size(&self) -> usize {
136        std::mem::size_of::<Option<T>>()
137            + self.as_ref().map(|v| v.deep_size().saturating_sub(std::mem::size_of::<T>())).unwrap_or(0)
138    }
139}
140
141// === Domain type implementations ===
142
143impl DeepSize for ImageMetadata {
144    fn deep_size(&self) -> usize {
145        std::mem::size_of::<ImageMetadata>() + self.thumbhash.capacity()
146    }
147}
148
149impl DeepSize for Attachment {
150    fn deep_size(&self) -> usize {
151        std::mem::size_of::<Attachment>()
152            + self.id.capacity() + self.key.capacity() + self.nonce.capacity()
153            + self.extension.capacity() + self.name.capacity()
154            + self.url.capacity() + self.path.capacity()
155            + self.img_meta.as_ref().map(|m| m.thumbhash.capacity()).unwrap_or(0)
156            + self.webxdc_topic.as_ref().map(|s| s.capacity()).unwrap_or(0)
157            + self.group_id.as_ref().map(|s| s.capacity()).unwrap_or(0)
158            + self.original_hash.as_ref().map(|s| s.capacity()).unwrap_or(0)
159            + self.scheme_version.as_ref().map(|s| s.capacity()).unwrap_or(0)
160            + self.mls_filename.as_ref().map(|s| s.capacity()).unwrap_or(0)
161    }
162}
163
164impl DeepSize for Reaction {
165    fn deep_size(&self) -> usize {
166        std::mem::size_of::<Reaction>()
167            + self.id.capacity() + self.reference_id.capacity()
168            + self.author_id.capacity() + self.emoji.capacity()
169    }
170}
171
172impl DeepSize for EditEntry {
173    fn deep_size(&self) -> usize {
174        std::mem::size_of::<EditEntry>() + self.content.capacity()
175    }
176}
177
178impl DeepSize for SiteMetadata {
179    fn deep_size(&self) -> usize {
180        std::mem::size_of::<SiteMetadata>()
181            + self.domain.capacity()
182            + self.og_title.as_ref().map(|s| s.capacity()).unwrap_or(0)
183            + self.og_description.as_ref().map(|s| s.capacity()).unwrap_or(0)
184            + self.og_image.as_ref().map(|s| s.capacity()).unwrap_or(0)
185            + self.og_url.as_ref().map(|s| s.capacity()).unwrap_or(0)
186            + self.og_type.as_ref().map(|s| s.capacity()).unwrap_or(0)
187            + self.title.as_ref().map(|s| s.capacity()).unwrap_or(0)
188            + self.description.as_ref().map(|s| s.capacity()).unwrap_or(0)
189            + self.favicon.as_ref().map(|s| s.capacity()).unwrap_or(0)
190    }
191}
192
193impl DeepSize for Message {
194    fn deep_size(&self) -> usize {
195        std::mem::size_of::<Message>()
196            + self.id.capacity() + self.content.capacity() + self.replied_to.capacity()
197            + self.replied_to_content.as_ref().map(|s| s.capacity()).unwrap_or(0)
198            + self.replied_to_npub.as_ref().map(|s| s.capacity()).unwrap_or(0)
199            + self.npub.as_ref().map(|s| s.capacity()).unwrap_or(0)
200            + self.wrapper_event_id.as_ref().map(|s| s.capacity()).unwrap_or(0)
201            + self.preview_metadata.as_ref().map(|m| m.deep_size()).unwrap_or(0)
202            + self.attachments.iter().map(|a| a.deep_size()).sum::<usize>()
203            + self.reactions.iter().map(|r| r.deep_size()).sum::<usize>()
204            + self.edit_history.as_ref().map(|h| h.iter().map(|e| e.deep_size()).sum::<usize>()).unwrap_or(0)
205    }
206}
207
208impl DeepSize for ProfileFlags {
209    #[inline]
210    fn deep_size(&self) -> usize { std::mem::size_of::<ProfileFlags>() }
211}
212
213impl DeepSize for Profile {
214    fn deep_size(&self) -> usize {
215        std::mem::size_of::<Profile>()
216            + self.name.len() + self.display_name.len() + self.nickname.len()
217            + self.lud06.len() + self.lud16.len()
218            + self.banner.len() + self.avatar.len()
219            + self.about.len() + self.website.len() + self.nip05.len()
220            + self.status_title.len() + self.status_purpose.len() + self.status_url.len()
221            + self.avatar_cached.len() + self.banner_cached.len()
222    }
223}
224
225impl DeepSize for MessageFlags {
226    #[inline]
227    fn deep_size(&self) -> usize { std::mem::size_of::<MessageFlags>() }
228}
229
230impl DeepSize for CompactReaction {
231    fn deep_size(&self) -> usize {
232        std::mem::size_of::<CompactReaction>() + self.emoji.len()
233    }
234}
235
236impl DeepSize for CompactAttachment {
237    fn deep_size(&self) -> usize {
238        std::mem::size_of::<CompactAttachment>()
239            + self.extension.len() + self.url.len() + self.path.len()
240            + self.name.capacity()
241            + self.img_meta.as_ref().map(|m| m.deep_size()).unwrap_or(0)
242            + self.group_id.as_ref().map(|_| 32).unwrap_or(0)
243            + self.original_hash.as_ref().map(|_| 32).unwrap_or(0)
244            + self.webxdc_topic.as_ref().map(|s| s.len()).unwrap_or(0)
245            + self.mls_filename.as_ref().map(|s| s.len()).unwrap_or(0)
246            + self.scheme_version.as_ref().map(|s| s.len()).unwrap_or(0)
247    }
248}
249
250impl DeepSize for CompactMessage {
251    fn deep_size(&self) -> usize {
252        std::mem::size_of::<CompactMessage>()
253            + self.content.len()
254            + self.replied_to_content.as_ref().map(|s| s.len()).unwrap_or(0)
255            + self.preview_metadata.as_ref().map(|m| m.deep_size()).unwrap_or(0)
256            + self.attachments.iter().map(|a| a.deep_size()).sum::<usize>()
257            + self.reactions.iter().map(|r| r.deep_size()).sum::<usize>()
258            + self.edit_history.as_ref().map(|h| h.iter().map(|e| e.deep_size()).sum::<usize>()).unwrap_or(0)
259            + self.addressed_bots.as_ref().map(|b| std::mem::size_of_val(&**b) + b.capacity() * 2).unwrap_or(0)
260    }
261}
262
263impl DeepSize for CompactMessageVec {
264    fn deep_size(&self) -> usize {
265        std::mem::size_of::<CompactMessageVec>()
266            + std::mem::size_of_val(self.messages())
267            + self.iter().map(|m| m.deep_size().saturating_sub(std::mem::size_of::<CompactMessage>())).sum::<usize>()
268            + self.len() * std::mem::size_of::<([u8; 32], u32)>()
269    }
270}
271
272impl DeepSize for NpubInterner {
273    fn deep_size(&self) -> usize {
274        self.memory_usage()
275    }
276}
277
278impl DeepSize for Chat {
279    fn deep_size(&self) -> usize {
280        std::mem::size_of::<Chat>()
281            + self.id.capacity()
282            + 32  // last_read [u8; 32] is inline
283            + self.participants.capacity() * std::mem::size_of::<u16>()
284            + self.messages.deep_size()
285            + self.metadata.custom_fields.iter()
286                .map(|(k, v)| k.capacity() + v.capacity())
287                .sum::<usize>()
288            + self.typing_participants.capacity() * std::mem::size_of::<(u16, u64)>()
289    }
290}