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}
`;
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