Skip to main content

lmflow/graph/
introspect.rs

1//! 内省:队列深度、节点统计快照、人读 dump。
2//!
3//! 与 [`super::dot`] 同理 —— 纯「读快照 + 格式化」,不参与调度、不碰锁序,
4//! 单独成模块让 `mod.rs` 只剩并发核心。本模块是 `graph` 的子模块,故仍可访问私有字段。
5
6use std::sync::atomic::Ordering;
7
8use crate::config::StatsLevel;
9
10use super::{EdgeId, GraphInner, InputQueueStatsSnapshot, NodeStatsSnapshot};
11
12impl GraphInner {
13    pub(super) fn queue_depth(&self, edge: EdgeId) -> usize {
14        self.edges[edge]
15            .consumers
16            .iter()
17            .map(|&(n, p)| self.nodes[n].queue_len(p))
18            .sum()
19    }
20
21    pub(super) fn node_stats(&self, i: usize) -> Option<NodeStatsSnapshot> {
22        let node = self.nodes.get(i)?;
23        let st = &node.stats;
24        // 先看 in_flight 再用 started_us —— 后者归零时不清,只在「在跑」时有意义。
25        let running = st.in_flight.load(Ordering::Relaxed) > 0;
26        let running_for_us = if running && self.stats_level == StatsLevel::Full {
27            let now_us = self.epoch.elapsed().as_micros() as i64;
28            (now_us - st.started_us.load(Ordering::Relaxed)).max(0)
29        } else {
30            0
31        };
32        Some(NodeStatsSnapshot {
33            node_name: node.name.clone(),
34            kernel_name: node.kernel_name.clone(),
35            running,
36            running_for_us,
37            processed: st.processed.load(Ordering::Relaxed),
38            errors: st.errors.load(Ordering::Relaxed),
39            total_process_us: st.total_us.load(Ordering::Relaxed),
40            max_process_us: st.max_us.load(Ordering::Relaxed),
41            packets_in: st.packets_in.load(Ordering::Relaxed),
42            packets_out: st.packets_out.load(Ordering::Relaxed),
43            peak_queue_depth: st.peak_queue_depth.load(Ordering::Relaxed),
44            queued: (0..node.input_queues.len())
45                .map(|p| node.queue_len(p))
46                .sum(),
47        })
48    }
49
50    /// 导出当前 trace 环内容为 Chrome Trace Event Format 的 JSON(chrome://tracing /
51    /// perfetto 可直接打开)。未开启 trace(`trace_capacity == 0`)时返回一个合法的空
52    /// trace(`{"traceEvents":[],...}`)。多次调用返回环的当前快照,互不影响。
53    pub fn to_chrome_trace(&self) -> String {
54        let events = self.shared.trace_snapshot();
55        let labels: Vec<(String, String)> = self
56            .nodes
57            .iter()
58            .map(|n| (n.name.clone(), n.kernel_name.clone()))
59            .collect();
60        crate::trace::to_chrome_trace_json(&events, &labels)
61    }
62
63    pub(super) fn input_queue_stats(
64        &self,
65        node_id: usize,
66        port: usize,
67    ) -> Option<InputQueueStatsSnapshot> {
68        self.input_queue_stats_at(node_id, port, self.epoch_us())
69    }
70
71    pub(super) fn input_queue_stats_at(
72        &self,
73        node_id: usize,
74        port: usize,
75        now_us: i64,
76    ) -> Option<InputQueueStatsSnapshot> {
77        let node = self.nodes.get(node_id)?;
78        let port_name = node.in_ports.name(port)?.to_string();
79        let stats = node.input_queue_stats.get(port)?;
80        let queued_packets = node.queue_len(port);
81        let queued_bytes = node.input_queue_bytes[port].load(Ordering::SeqCst);
82        let reserved_packets = node.input_queue_reserved[port].load(Ordering::SeqCst);
83        let since = stats.blocked_since_us.load(Ordering::SeqCst);
84        let blocked_for_us = if since == 0 {
85            0
86        } else {
87            now_us.saturating_sub(since.saturating_sub(1)).max(0) as u64
88        };
89        let edge = node.inputs[port];
90        let producer_name = self.edges[edge]
91            .producer
92            .map(|producer| self.nodes[producer].name.clone());
93        Some(InputQueueStatsSnapshot {
94            node_name: node.name.clone(),
95            port_name,
96            producer_name,
97            packet_capacity: node.input_queue_capacity[port],
98            queued_packets,
99            queued_bytes,
100            reserved_packets,
101            peak_queued_packets: stats.peak_packets.load(Ordering::Relaxed),
102            peak_queued_bytes: stats.peak_bytes.load(Ordering::Relaxed),
103            blocked: since != 0,
104            blocked_for_us,
105            block_events: stats.block_events.load(Ordering::Relaxed),
106            total_blocked_us: stats
107                .blocked_total_us
108                .load(Ordering::Relaxed)
109                .saturating_add(blocked_for_us),
110        })
111    }
112
113    pub(super) fn dump(&self) -> String {
114        let mut s = String::new();
115        s.push_str(&format!(
116            "graph state={:?} nodes={} edges={} queued={} ({} bytes)\n",
117            self.state(),
118            self.nodes.len(),
119            self.edges.len(),
120            self.shared.total_queued(),
121            self.shared.total_queued_bytes()
122        ));
123        s.push_str("node              state        queued  processed  errors  avg_us  max_us\n");
124        for i in 0..self.nodes.len() {
125            let st = self.node_stats(i).expect("node exists");
126            let sched = self.nodes[i].sched.lock().expect("scheduler lock poisoned");
127            let state = if st.running {
128                format!("RUNNING {}ms", st.running_for_us / 1000)
129            } else if sched.closed {
130                "closed".to_string()
131            } else if sched.opened {
132                "idle".to_string()
133            } else {
134                "new".to_string()
135            };
136            let avg = if st.processed > 0 {
137                st.total_process_us / st.processed as i64
138            } else {
139                0
140            };
141            s.push_str(&format!(
142                "{:<17} {:<12} {:>6}  {:>9}  {:>6}  {:>6}  {:>6}\n",
143                st.node_name, state, st.queued, st.processed, st.errors, avg, st.max_process_us
144            ));
145            for port in 0..self.nodes[i].input_queues.len() {
146                let queue = self.input_queue_stats(i, port).expect("input port exists");
147                let capacity = queue
148                    .packet_capacity
149                    .map_or_else(|| "unbounded".to_string(), |value| value.to_string());
150                s.push_str(&format!(
151                    "  input `{}` capacity={} packets queued={}/{}B reserved={} peak={}/{}B blocked={} events={} total={}us\n",
152                    queue.port_name,
153                    capacity,
154                    queue.queued_packets,
155                    queue.queued_bytes,
156                    queue.reserved_packets,
157                    queue.peak_queued_packets,
158                    queue.peak_queued_bytes,
159                    queue.blocked,
160                    queue.block_events,
161                    queue.total_blocked_us,
162                ));
163            }
164        }
165        for e in &self.edges {
166            s.push_str(&format!(
167                "edge `{}` closed={} dropped={}\n",
168                e.name,
169                e.is_closed(),
170                e.dropped_count()
171            ));
172            if e.is_graph_input {
173                let watermark = e.watermark_backpressure.snapshot(self.epoch_us());
174                s.push_str(&format!(
175                    "  watermark limit={} queued={} blocked={} waiters={} events={} total={}us\n",
176                    self.shared.config.max_queued_packets,
177                    self.shared.total_queued(),
178                    watermark.blocked,
179                    watermark.active_waiters,
180                    watermark.block_events,
181                    watermark.total_blocked_us,
182                ));
183            }
184            for poller in e.pollers.lock().expect("poller list lock poisoned").iter() {
185                let block = poller.block_backpressure.snapshot(self.epoch_us());
186                let queued = poller.queue.lock().expect("poller lock poisoned").len();
187                s.push_str(&format!(
188                    "  poller policy={:?} capacity={} queued={} dropped={} blocked={} waiters={} events={} total={}us\n",
189                    poller.overflow,
190                    poller
191                        .capacity
192                        .map_or_else(|| "unbounded".to_string(), |value| value.to_string()),
193                    queued,
194                    poller.dropped.load(Ordering::Relaxed),
195                    block.blocked,
196                    block.active_waiters,
197                    block.block_events,
198                    block.total_blocked_us,
199                ));
200            }
201        }
202        s
203    }
204}