//! The Diagnostics page (General → Diagnostics), a slice of T3 Code's: what //! the app and the agents it runs use right now, and every process the app //! started, with buttons to interrupt or kill one. Read from `/proc`. use std::{ collections::HashMap, time::{Duration, Instant, SystemTime}, }; use gpui_kit::component::{IconName, h_flex, v_flex}; use gpui_kit::prelude::FluentBuilder as _; use gpui_kit::*; use super::widgets::{Section, ago, empty_row}; use super::{Page, SettingsView}; use crate::ui::metal::{self, MetalButton, MetalStyled as _, Palette}; /// One process under the app, as `/proc` describes it. #[derive(Clone, Debug)] pub(super) struct Process { pub pid: u32, pub parent: u32, pub name: String, pub command: String, /// CPU time so far, in clock ticks. pub ticks: u64, pub rss_bytes: u64, /// Share of one core over the last interval, in percent. pub cpu: f32, } #[derive(Default)] pub(super) struct DiagnosticsState { processes: Vec, checked: Option, sampled: Option, /// Keeps refreshing while the page is open. polling: bool, } /// Every process descending from this one, this one first. fn read_processes() -> Vec { let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) }.max(1) as u64; let mut all: HashMap = HashMap::new(); for entry in std::fs::read_dir("/proc").into_iter().flatten().flatten() { let Some(pid) = entry.file_name().to_str().and_then(|name| name.parse::().ok()) else { continue }; let Ok(stat) = std::fs::read_to_string(entry.path().join("stat")) else { continue }; // `pid (name) state ppid …`; the name may hold spaces and parens. let (Some(open), Some(close)) = (stat.find('('), stat.rfind(')')) else { continue }; let name = stat[open + 1..close].to_string(); let fields: Vec<&str> = stat[close + 2..].split_whitespace().collect(); let field = |ix: usize| fields.get(ix).and_then(|value| value.parse::().ok()).unwrap_or(0); // After the name: state(0) ppid(1) … utime(11) stime(12) … rss(21). let command = std::fs::read(entry.path().join("cmdline")) .map(|bytes| String::from_utf8_lossy(&bytes).replace('\0', " ").trim().to_string()) .unwrap_or_default(); all.insert(pid, Process { pid, parent: field(1) as u32, name, command, ticks: field(11) + field(12), rss_bytes: field(21) * page_size, cpu: 0., }); } let root = std::process::id(); let mut ours = Vec::new(); let mut queue = vec![root]; while let Some(pid) = queue.pop() { if let Some(process) = all.get(&pid) { ours.push(process.clone()); } queue.extend(all.values().filter(|process| process.parent == pid).map(|process| process.pid)); } ours } fn memory(bytes: u64) -> String { match bytes { 0..1_048_576 => format!("{} KB", bytes / 1024), 1_048_576..1_073_741_824 => format!("{:.0} MB", bytes as f64 / 1_048_576.), _ => format!("{:.1} GB", bytes as f64 / 1_073_741_824.), } } impl SettingsView { /// Reads the processes again, working out CPU use since the last read. pub(super) fn refresh_diagnostics(&mut self, cx: &mut Context) { let ticks_per_sec = unsafe { libc::sysconf(libc::_SC_CLK_TCK) }.max(1) as f32; let previous: HashMap = self.diagnostics.processes.iter().map(|process| (process.pid, process.ticks)).collect(); let elapsed = self.diagnostics.sampled.map(|at| at.elapsed().as_secs_f32()).filter(|secs| *secs > 0.05); let mut processes = read_processes(); if let Some(elapsed) = elapsed { for process in &mut processes { if let Some(before) = previous.get(&process.pid) { process.cpu = (process.ticks.saturating_sub(*before) as f32 / ticks_per_sec) / elapsed * 100.; } } } self.diagnostics.processes = processes; self.diagnostics.checked = Some(SystemTime::now()); self.diagnostics.sampled = Some(Instant::now()); if !self.diagnostics.polling { self.diagnostics.polling = true; cx.spawn(async move |this, cx| { loop { cx.background_executor().timer(Duration::from_secs(2)).await; let open = this .update(cx, |this, cx| { let open = this.page == Page::Diagnostics; if open { this.refresh_diagnostics(cx); } else { this.diagnostics.polling = false; } open }) .unwrap_or(false); if !open { break; } } }) .detach(); } cx.notify(); } pub(super) fn render_diagnostics(&mut self, cx: &mut Context) -> Vec { let p = metal::palette(cx); let processes = self.diagnostics.processes.clone(); let checked = self.diagnostics.checked.map(|time| format!("Checked {}", ago(time))).unwrap_or_default(); let refresh = |id: &'static str, cx: &mut Context| { h_flex() .gap(px(8.)) .child(div().text_size(px(12.)).text_color(p.ink3).child(checked.clone())) .child( MetalButton::new(id) .ghost() .mini() .icon(IconName::RefreshCw) .tooltip("Refresh") .on_click(cx.listener(|this, _, _, cx| this.refresh_diagnostics(cx))), ) }; let cpu: f32 = processes.iter().map(|process| process.cpu).sum(); let rss: u64 = processes.iter().map(|process| process.rss_bytes).sum(); let app_rss = processes.first().map_or(0, |process| process.rss_bytes); let stats = div() .grid() .grid_cols(4) .gap(px(8.)) .p(px(12.)) .child(stat("Current CPU", format!("{cpu:.1}%"), p)) .child(stat("Resident memory", memory(rss), p)) .child(stat("Process count", processes.len().to_string(), p)) .child(stat("App memory", memory(app_rss), p)); let monitor = Section::new("Resource monitor") .title_action(metal::tag("Native", p)) .header_action(refresh("refresh-monitor", cx)) .row(stats) .render(cx); let header = h_flex() .px(px(16.)) .py(px(8.)) .gap(px(12.)) .text_size(px(11.)) .font_weight(FontWeight::MEDIUM) .text_color(p.ink3) .child(div().w(px(150.)).child("Name")) .child(div().w(px(60.)).child("CPU")) .child(div().w(px(70.)).child("Memory")) .child(div().flex_1().child("Command")) .child(div().w(px(64.)).child("PID")) .child(div().w(px(72.)).child("Kill")) .into_any_element(); let mut rows = vec![header]; if processes.is_empty() { rows.push(empty_row("Reading processes…", p)); } let own = std::process::id(); for process in processes { let pid = process.pid; rows.push( h_flex() .px(px(16.)) .py(px(8.)) .gap(px(12.)) .text_size(px(12.)) .text_color(p.ink) .child(div().w(px(150.)).truncate().font_weight(FontWeight::MEDIUM).child(process.name.clone())) .child(div().w(px(60.)).font_family(metal::MONO).child(format!("{:.1}%", process.cpu))) .child(div().w(px(70.)).font_family(metal::MONO).child(memory(process.rss_bytes))) .child( div() .flex_1() .min_w_0() .truncate() .font_family(metal::MONO) .text_size(px(11.)) .text_color(p.ink3) .child(process.command.clone()), ) .child(div().w(px(64.)).font_family(metal::MONO).text_color(p.ink2).child(pid.to_string())) .child( h_flex() .w(px(72.)) .gap(px(2.)) .when(pid != own, |this| { this.child(signal_button("INT", pid, libc::SIGINT, "Send SIGINT", cx)) .child(signal_button("KILL", pid, libc::SIGKILL, "Send SIGKILL", cx)) }), ) .into_any_element(), ); } let live = Section::new("Live Processes").header_action(refresh("refresh-processes", cx)).rows(rows).render(cx); vec![monitor, live] } } fn stat(label: &'static str, value: String, p: &Palette) -> AnyElement { v_flex() .px(px(12.)) .py(px(10.)) .gap(px(2.)) .rounded(px(12.)) .well(p) .child(div().text_size(px(11.)).text_color(p.ink3).child(label)) .child(div().font_family(metal::PIXEL).text_size(px(18.)).text_color(p.ink).child(value)) .into_any_element() } fn signal_button(label: &'static str, pid: u32, signal: i32, tooltip: &'static str, cx: &mut Context) -> MetalButton { MetalButton::new(SharedString::from(format!("signal-{label}-{pid}"))) .ghost() .mini() .label(label) .tooltip(tooltip) .on_click(cx.listener(move |this, _, _, cx| { unsafe { libc::kill(pid as i32, signal); } this.refresh_diagnostics(cx); })) }