//! 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<Process>,
checked: Option<SystemTime>,
sampled: Option<Instant>,
/// Keeps refreshing while the page is open.
polling: bool,
}
/// Every process descending from this one, this one first.
fn read_processes() -> Vec<Process> {
let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) }.max(1) as u64;
let mut all: HashMap<u32, Process> = 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::<u32>().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::<u64>().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<Self>) {
let ticks_per_sec = unsafe { libc::sysconf(libc::_SC_CLK_TCK) }.max(1) as f32;
let previous: HashMap<u32, u64> = 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<Self>) -> Vec<AnyElement> {
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<Self>| {
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<SettingsView>) -> 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);
}))
}