agentmux_srv/backend/sampler/
panes.rs1use std::sync::Arc;
14
15use crate::backend::blockcontroller::pidregistry;
16use crate::backend::blockstats_detail::Shell;
17use crate::backend::rpc_types::{TowerProcessRole, TowerSnapshot, TowerTaskKind};
18use crate::backend::tower_sampler::{BlockLabel, Inputs, Tower};
19
20#[derive(Debug, Clone, PartialEq)]
22pub struct PaneStat {
23 pub block_id: String,
24 pub cpu_pct: Option<f64>,
26 pub mem: u64,
30 pub pids: u32,
31 pub shell: Shell,
33}
34
35pub type PaneSampler = Arc<dyn Fn() -> std::io::Result<Vec<PaneStat>> + Send + Sync>;
37
38pub fn from_snapshot(snap: &TowerSnapshot) -> Vec<PaneStat> {
41 snap.tasks
42 .iter()
43 .filter(|t| t.kind != TowerTaskKind::Agentmux)
44 .map(|t| {
45 let main = t.processes.iter().find(|p| p.role == Some(TowerProcessRole::Main));
46 PaneStat {
47 block_id: t.id.clone(),
48 cpu_pct: t.cpu.map(|c| c * 100.0),
49 mem: t.processes.iter().filter_map(|p| p.mem_resident).sum(),
50 pids: t.processes.len() as u32,
51 shell: main.map_or((0, 0), |p| (p.pid, p.started_at_ms.unwrap_or(0))),
52 }
53 })
54 .collect()
55}
56
57pub fn tower_sampler(labels: impl Fn(&str) -> Option<BlockLabel> + Send + Sync + 'static) -> PaneSampler {
60 Arc::new(move || {
61 let inputs = || Inputs {
62 blocks: crate::backend::process_tracker::registry::global()
63 .map(|t| t.members_by_block())
64 .unwrap_or_default(),
65 roots: pidregistry::get_all(),
66 own_pid: std::process::id(),
67 };
68 let snap = Tower::global().sample(false, "", inputs, &labels, |_| None)?;
69 Ok(from_snapshot(&snap))
70 })
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use crate::backend::rpc_types::{TowerProcess, TowerTask};
77
78 fn process(pid: u32, role: TowerProcessRole, started: u64) -> TowerProcess {
79 TowerProcess {
80 pid,
81 role: Some(role),
82 started_at_ms: Some(started),
83 mem: Some(1),
84 mem_resident: Some(1000 + u64::from(pid)),
85 ..Default::default()
86 }
87 }
88
89 fn snapshot(tasks: Vec<TowerTask>) -> TowerSnapshot {
90 TowerSnapshot {
91 ts_ms: 0,
92 hostname: String::new(),
93 os: String::new(),
94 cpu_count: 1,
95 memory_metric: String::new(),
96 interval_ms: 2000,
97 remote: false,
98 tasks,
99 machine: None,
100 host: None,
101 }
102 }
103
104 fn task(id: &str, kind: TowerTaskKind, cpu: Option<f64>, mem: u64, processes: Vec<TowerProcess>) -> TowerTask {
105 TowerTask {
106 id: id.to_string(),
107 kind,
108 label: id.to_string(),
109 tracking: "tree".to_string(),
110 cpu,
111 cpu_account: false,
112 mem,
113 processes,
114 exited: None,
115 }
116 }
117
118 #[test]
119 fn each_pane_task_becomes_a_badge_and_agentmux_itself_does_not() {
120 let snap = snapshot(vec![
121 task(
122 "term-1",
123 TowerTaskKind::Terminal,
124 Some(0.25),
125 4096,
126 vec![process(10, TowerProcessRole::Main, 111), process(11, TowerProcessRole::Started, 112)],
127 ),
128 task("agent-1", TowerTaskKind::Agent, Some(1.5), 8192, vec![process(20, TowerProcessRole::Main, 222)]),
129 task(crate::backend::tower_sampler::AGENTMUX_TASK_ID, TowerTaskKind::Agentmux, Some(0.1), 1, vec![]),
130 ]);
131 let stats = from_snapshot(&snap);
132 assert_eq!(
133 stats,
134 vec![
135 PaneStat { block_id: "term-1".into(), cpu_pct: Some(25.0), mem: 1010 + 1011, pids: 2, shell: (10, 111) },
137 PaneStat { block_id: "agent-1".into(), cpu_pct: Some(150.0), mem: 1020, pids: 1, shell: (20, 222) },
138 ]
139 );
140 }
141
142 #[test]
143 fn a_first_sample_has_no_rate_yet_and_a_task_without_a_main_process_has_no_shell() {
144 let snap = snapshot(vec![task("agent-2", TowerTaskKind::Agent, None, 10, vec![process(30, TowerProcessRole::Support, 5)])]);
145 let s = &from_snapshot(&snap)[0];
146 assert_eq!((s.cpu_pct, s.shell), (None, (0, 0)));
147 }
148}