diff --git a/site/dashboard.html b/site/dashboard.html index a5780b8..e75b7f6 100644 --- a/site/dashboard.html +++ b/site/dashboard.html @@ -817,13 +817,12 @@ function renderMemory(mem, stats) { document.getElementById('memoryRss').textContent = formatBytes(mem.process_rss_bytes); document.getElementById('memorySub').textContent = 'est. ' + formatBytes(mem.total_estimated_bytes); - // Entry counts from sibling stats objects (avoid duplication in memory payload) const entryCounts = { - cache: stats.cache.entries, - blocklist: stats.blocking.domains_loaded, + cache: mem.cache_entries, + blocklist: mem.blocklist_entries, query_log: mem.query_log_entries, srtt: mem.srtt_entries, - overrides: stats.overrides.active, + overrides: mem.overrides_entries, }; // Sidebar panel @@ -852,7 +851,7 @@ function renderMemory(mem, stats) {
${barSegments}
${rows}
- Process RSS + Process Footprint ${formatBytes(mem.process_rss_bytes)}
`; diff --git a/src/api.rs b/src/api.rs index 3476bc3..f3f7b37 100644 --- a/src/api.rs +++ b/src/api.rs @@ -214,12 +214,15 @@ struct BlockingStatsResponse { #[derive(Serialize)] struct MemoryStats { cache_bytes: usize, + cache_entries: usize, blocklist_bytes: usize, + blocklist_entries: usize, query_log_bytes: usize, query_log_entries: usize, srtt_bytes: usize, srtt_entries: usize, overrides_bytes: usize, + overrides_entries: usize, total_estimated_bytes: usize, process_rss_bytes: usize, } @@ -553,12 +556,15 @@ async fn stats(State(ctx): State>) -> Json { }, memory: MemoryStats { cache_bytes, + cache_entries: cache_len, blocklist_bytes, + blocklist_entries: bl_stats.domains_loaded, query_log_bytes, query_log_entries, srtt_bytes, srtt_entries, overrides_bytes, + overrides_entries: override_count, total_estimated_bytes: total_estimated, process_rss_bytes: crate::stats::process_rss_bytes(), }, diff --git a/src/blocklist.rs b/src/blocklist.rs index 8f1e14c..0a7db4d 100644 --- a/src/blocklist.rs +++ b/src/blocklist.rs @@ -184,17 +184,13 @@ impl BlocklistStore { } pub fn heap_bytes(&self) -> usize { - let domains: usize = self - .domains - .iter() - .map(|d| std::mem::size_of::() + d.capacity()) - .sum(); - let allow: usize = self - .allowlist - .iter() - .map(|d| std::mem::size_of::() + d.capacity()) - .sum(); - domains + allow + // HashSet stores (hash, String) per slot + 1 control byte + let per_slot_overhead = std::mem::size_of::() + std::mem::size_of::() + 1; + let domains_table = self.domains.capacity() * per_slot_overhead; + let domains_heap: usize = self.domains.iter().map(|d| d.capacity()).sum(); + let allow_table = self.allowlist.capacity() * per_slot_overhead; + let allow_heap: usize = self.allowlist.iter().map(|d| d.capacity()).sum(); + domains_table + domains_heap + allow_table + allow_heap } pub fn stats(&self) -> BlocklistStats { @@ -248,6 +244,23 @@ pub fn parse_blocklist(text: &str) -> HashSet { domains } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn heap_bytes_grows_with_domains() { + let mut store = BlocklistStore::new(); + let empty = store.heap_bytes(); + let domains: HashSet = ["example.com", "example.org", "test.net"] + .iter() + .map(|s| s.to_string()) + .collect(); + store.swap_domains(domains, vec![]); + assert!(store.heap_bytes() > empty); + } +} + pub async fn download_blocklists(lists: &[String]) -> Vec<(String, String)> { let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(30)) diff --git a/src/cache.rs b/src/cache.rs index 89b7e75..a329d53 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -143,11 +143,21 @@ impl DnsCache { } pub fn heap_bytes(&self) -> usize { - let mut total = 0; + // Outer HashMap: (hash, String, HashMap) per slot + control byte + let outer_slot = std::mem::size_of::() + + std::mem::size_of::() + + std::mem::size_of::>() + + 1; + let mut total = self.entries.capacity() * outer_slot; for (domain, type_map) in &self.entries { - total += domain.capacity() + std::mem::size_of::(); + total += domain.capacity(); + // Inner HashMap: (hash, QueryType, CacheEntry) per slot + control byte + let inner_slot = std::mem::size_of::() + + std::mem::size_of::() + + std::mem::size_of::() + + 1; + total += type_map.capacity() * inner_slot; for entry in type_map.values() { - total += std::mem::size_of::(); total += entry.packet.heap_bytes(); } } @@ -206,3 +216,23 @@ fn adjust_ttls(records: &mut [DnsRecord], new_ttl: u32) { record.set_ttl(new_ttl); } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::packet::DnsPacket; + + #[test] + fn heap_bytes_grows_with_entries() { + let mut cache = DnsCache::new(100, 1, 3600); + let empty = cache.heap_bytes(); + let mut pkt = DnsPacket::new(); + pkt.answers.push(DnsRecord::A { + domain: "example.com".into(), + addr: "1.2.3.4".parse().unwrap(), + ttl: 300, + }); + cache.insert("example.com", QueryType::A, &pkt); + assert!(cache.heap_bytes() > empty); + } +} diff --git a/src/override_store.rs b/src/override_store.rs index 1545579..96e0179 100644 --- a/src/override_store.rs +++ b/src/override_store.rs @@ -118,16 +118,20 @@ impl OverrideStore { } pub fn heap_bytes(&self) -> usize { - self.entries + // HashMap: (hash, String, OverrideEntry) per slot + control byte + let per_slot = std::mem::size_of::() + + std::mem::size_of::() + + std::mem::size_of::() + + 1; + let table = self.entries.capacity() * per_slot; + let heap: usize = self + .entries .iter() .map(|(k, v)| { - k.capacity() - + std::mem::size_of::() - + v.domain.capacity() - + v.target.capacity() - + v.record.heap_bytes() + k.capacity() + v.domain.capacity() + v.target.capacity() + v.record.heap_bytes() }) - .sum() + .sum(); + table + heap } pub fn active_count(&self) -> usize { @@ -167,3 +171,16 @@ fn parse_target(domain: &str, target: &str, ttl: u32) -> Result<(QueryType, DnsR }, )) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn heap_bytes_grows_with_entries() { + let mut store = OverrideStore::new(); + let empty = store.heap_bytes(); + store.insert("example.com", "1.2.3.4", 300, None).unwrap(); + assert!(store.heap_bytes() > empty); + } +} diff --git a/src/packet.rs b/src/packet.rs index 9dded42..ba9e30a 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -610,4 +610,16 @@ mod tests { panic!("expected DNSKEY"); } } + + #[test] + fn heap_bytes_accounts_for_records() { + let mut pkt = DnsPacket::new(); + let empty = pkt.heap_bytes(); + pkt.answers.push(DnsRecord::A { + domain: "example.com".into(), + addr: "1.2.3.4".parse().unwrap(), + ttl: 300, + }); + assert!(pkt.heap_bytes() > empty); + } } diff --git a/src/query_log.rs b/src/query_log.rs index 23253bd..1dc2d17 100644 --- a/src/query_log.rs +++ b/src/query_log.rs @@ -92,3 +92,25 @@ pub struct QueryLogFilter { pub since: Option, pub limit: Option, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn heap_bytes_grows_with_entries() { + let mut log = QueryLog::new(100); + let empty = log.heap_bytes(); + log.push(QueryLogEntry { + timestamp: SystemTime::now(), + src_addr: "127.0.0.1:1234".parse().unwrap(), + domain: "example.com".into(), + query_type: QueryType::A, + path: QueryPath::Forwarded, + rescode: ResultCode::NOERROR, + latency_us: 500, + dnssec: DnssecStatus::Indeterminate, + }); + assert!(log.heap_bytes() > empty); + } +} diff --git a/src/record.rs b/src/record.rs index 1c5f2db..7de9bb4 100644 --- a/src/record.rs +++ b/src/record.rs @@ -690,4 +690,14 @@ mod tests { let parsed = round_trip(&rec); assert_eq!(rec, parsed); } + + #[test] + fn heap_bytes_reflects_string_capacity() { + let rec = DnsRecord::CNAME { + domain: "a]".repeat(100), + host: "b".repeat(200), + ttl: 60, + }; + assert!(rec.heap_bytes() >= 300); + } } diff --git a/src/srtt.rs b/src/srtt.rs index 8eeb5c0..bf02055 100644 --- a/src/srtt.rs +++ b/src/srtt.rs @@ -101,7 +101,12 @@ impl SrttCache { } pub fn heap_bytes(&self) -> usize { - self.entries.capacity() * (std::mem::size_of::() + std::mem::size_of::()) + // HashMap stores (hash, key, value) per slot + 1 control byte + let per_slot = std::mem::size_of::() + + std::mem::size_of::() + + std::mem::size_of::() + + 1; + self.entries.capacity() * per_slot } pub fn len(&self) -> usize { @@ -307,6 +312,16 @@ mod tests { assert_eq!(addrs, vec![sock(1), sock(2)]); } + #[test] + fn heap_bytes_grows_with_entries() { + let mut cache = SrttCache::new(true); + let empty = cache.heap_bytes(); + for i in 1..=10u8 { + cache.record_rtt(ip(i), 100, false); + } + assert!(cache.heap_bytes() > empty); + } + #[test] fn eviction_removes_oldest() { let mut cache = SrttCache::new(true); diff --git a/src/stats.rs b/src/stats.rs index 4df4f39..32739cc 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -24,33 +24,40 @@ fn macos_rss() -> usize { fn task_info( target_task: u32, flavor: u32, - task_info_out: *mut libc_task_basic_info, + task_info_out: *mut TaskVmInfo, task_info_count: *mut u32, ) -> i32; } + // Partial task_vm_info_data_t — only fields up to phys_footprint. #[repr(C)] - struct libc_task_basic_info { + struct TaskVmInfo { virtual_size: u64, + region_count: i32, + page_size: i32, resident_size: u64, - resident_size_max: u64, - user_time: [u32; 2], - system_time: [u32; 2], - policy: i32, - suspend_count: i32, + resident_size_peak: u64, + device: u64, + device_peak: u64, + internal: u64, + internal_peak: u64, + external: u64, + external_peak: u64, + reusable: u64, + reusable_peak: u64, + purgeable_volatile_pmap: u64, + purgeable_volatile_resident: u64, + purgeable_volatile_virtual: u64, + compressed: u64, + compressed_peak: u64, + compressed_lifetime: u64, + phys_footprint: u64, } - const MACH_TASK_BASIC_INFO: u32 = 20; - let mut info: libc_task_basic_info = unsafe { mem::zeroed() }; - let mut count = (mem::size_of::() / mem::size_of::()) as u32; - let kr = unsafe { - task_info( - mach_task_self(), - MACH_TASK_BASIC_INFO, - &mut info, - &mut count, - ) - }; + const TASK_VM_INFO: u32 = 22; + let mut info: TaskVmInfo = unsafe { mem::zeroed() }; + let mut count = (mem::size_of::() / mem::size_of::()) as u32; + let kr = unsafe { task_info(mach_task_self(), TASK_VM_INFO, &mut info, &mut count) }; if kr == 0 { - info.resident_size as usize + info.phys_footprint as usize } else { 0 } @@ -61,7 +68,7 @@ fn linux_rss() -> usize { extern "C" { fn sysconf(name: i32) -> i64; } - const SC_PAGESIZE: i32 = 30; + const SC_PAGESIZE: i32 = 30; // x86_64 + aarch64; differs on mips (28), sparc (29) let page_size = unsafe { sysconf(SC_PAGESIZE) }; let page_size = if page_size > 0 { page_size as usize