diff --git a/src/bin/fipstop/ui/dashboard.rs b/src/bin/fipstop/ui/dashboard.rs index 8662e46..4fa81dd 100644 --- a/src/bin/fipstop/ui/dashboard.rs +++ b/src/bin/fipstop/ui/dashboard.rs @@ -130,6 +130,11 @@ fn draw_state(frame: &mut Frame, data: &serde_json::Value, area: Rect) { let links = helpers::u64_field(data, "link_count"); let transports = helpers::u64_field(data, "transport_count"); let connections = helpers::u64_field(data, "connection_count"); + let mesh_size = data + .get("estimated_mesh_size") + .and_then(|v| v.as_u64()) + .map(|n| format!("~{n}")) + .unwrap_or_else(|| "-".into()); let lines = vec![ Line::from(vec![ @@ -153,6 +158,8 @@ fn draw_state(frame: &mut Frame, data: &serde_json::Value, area: Rect) { Span::styled(transports, count), Span::styled(" connections: ", label), Span::styled(connections, count), + Span::styled(" mesh: ", label), + Span::styled(mesh_size, count), ]), ]; diff --git a/src/control/queries.rs b/src/control/queries.rs index de2c339..1624555 100644 --- a/src/control/queries.rs +++ b/src/control/queries.rs @@ -58,6 +58,7 @@ pub fn show_status(node: &Node) -> Value { "pid": pid, "exe_path": exe_path, "uptime_secs": uptime_secs, + "estimated_mesh_size": node.estimated_mesh_size(), "forwarding": serde_json::to_value(&fwd).unwrap_or_default(), }) } diff --git a/src/node/handlers/rx_loop.rs b/src/node/handlers/rx_loop.rs index e961612..f264306 100644 --- a/src/node/handlers/rx_loop.rs +++ b/src/node/handlers/rx_loop.rs @@ -114,6 +114,7 @@ impl Node { self.process_pending_retries(now_ms).await; self.check_tree_state().await; self.check_bloom_state().await; + self.compute_mesh_size(); self.check_mmp_reports().await; self.check_session_mmp_reports().await; self.check_link_heartbeats().await; diff --git a/src/node/mod.rs b/src/node/mod.rs index 11d812a..7632004 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -378,6 +378,12 @@ pub struct Node { /// Timestamp of last congestion detection log (rate-limited to 5s). last_congestion_log: Option, + // === Mesh Size Estimate === + /// Cached estimated mesh size (computed once per tick from bloom filters). + estimated_mesh_size: Option, + /// Timestamp of last mesh size log emission. + last_mesh_size_log: Option, + // === Display Names === /// Human-readable names for configured peers (alias or short npub). /// Populated at startup from peer config. @@ -496,6 +502,8 @@ impl Node { retry_pending: HashMap::new(), last_parent_reeval: None, last_congestion_log: None, + estimated_mesh_size: None, + last_mesh_size_log: None, peer_aliases: HashMap::new(), host_map, }) @@ -596,6 +604,8 @@ impl Node { retry_pending: HashMap::new(), last_parent_reeval: None, last_congestion_log: None, + estimated_mesh_size: None, + last_mesh_size_log: None, peer_aliases: HashMap::new(), host_map, } @@ -852,6 +862,76 @@ impl Node { &mut self.bloom_state } + // === Mesh Size Estimate === + + /// Get the cached estimated mesh size. + pub fn estimated_mesh_size(&self) -> Option { + self.estimated_mesh_size + } + + /// Compute and cache the estimated mesh size from bloom filters. + /// + /// Uses the spanning tree partition: parent's filter covers nodes reachable + /// upward, children's filters cover disjoint subtrees downward. The sum + /// of estimated entry counts plus one (self) approximates total network size. + pub(crate) fn compute_mesh_size(&mut self) { + let my_addr = *self.tree_state.my_node_addr(); + let parent_id = *self.tree_state.my_declaration().parent_id(); + let is_root = self.tree_state.is_root(); + + let mut total: f64 = 1.0; // count self + let mut child_count: u32 = 0; + let mut has_data = false; + + // Parent's filter: nodes reachable upward through the tree + if !is_root + && let Some(parent) = self.peers.get(&parent_id) + && let Some(filter) = parent.inbound_filter() + { + total += filter.estimated_count(); + has_data = true; + } + + // Children's filters: each child's subtree is disjoint + for (peer_addr, peer) in &self.peers { + if let Some(decl) = self.tree_state.peer_declaration(peer_addr) + && *decl.parent_id() == my_addr + { + child_count += 1; + if let Some(filter) = peer.inbound_filter() { + total += filter.estimated_count(); + has_data = true; + } + } + } + + if !has_data { + self.estimated_mesh_size = None; + return; + } + + let size = total.round() as u64; + self.estimated_mesh_size = Some(size); + + // Periodic logging (reuse MMP default interval: 30s) + let now = std::time::Instant::now(); + let should_log = match self.last_mesh_size_log { + None => true, + Some(last) => now.duration_since(last) >= std::time::Duration::from_secs( + self.config.node.mmp.log_interval_secs, + ), + }; + if should_log { + tracing::info!( + estimated_mesh_size = size, + peers = self.peers.len(), + children = child_count, + "Mesh size estimate" + ); + self.last_mesh_size_log = Some(now); + } + } + // === Coord Cache === /// Get the coordinate cache.