diff --git a/Cargo.lock b/Cargo.lock index c9f79292eb0fb83d2288880aa43394ebb9f6a6b0..a42202047ba7d4952cdce6c91b1e749781e3efb0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9207,6 +9207,7 @@ dependencies = [ "semver", "serde_json", "settings", + "sysinfo 0.37.2", "theme", "tree-sitter", "ui", diff --git a/crates/language_tools/Cargo.toml b/crates/language_tools/Cargo.toml index 989e1769ec84c06ea41dadc7187dacf5894e7ca3..d5e1ab90b26d6cb8cd18025ec8f5fa9a940b3e59 100644 --- a/crates/language_tools/Cargo.toml +++ b/crates/language_tools/Cargo.toml @@ -30,6 +30,7 @@ serde_json.workspace = true settings.workspace = true theme.workspace = true tree-sitter.workspace = true +sysinfo.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true diff --git a/crates/language_tools/src/lsp_button.rs b/crates/language_tools/src/lsp_button.rs index c5500b57769e22d39e06166e4e0d3fbae0192c9d..9dc97080c0c3f5bc28ff862851e9c95d687c48aa 100644 --- a/crates/language_tools/src/lsp_button.rs +++ b/crates/language_tools/src/lsp_button.rs @@ -1,10 +1,13 @@ use std::{ + cell::RefCell, collections::{BTreeMap, HashMap}, path::{Path, PathBuf}, rc::Rc, - time::Duration, + time::{Duration, Instant}, }; +use sysinfo::{Pid, ProcessRefreshKind, RefreshKind, System}; + use client::proto; use collections::HashSet; use editor::{Editor, EditorEvent}; @@ -41,13 +44,93 @@ pub struct LspButton { _subscriptions: Vec, } -#[derive(Debug)] struct LanguageServerState { items: Vec, workspace: WeakEntity, lsp_store: WeakEntity, active_editor: Option, language_servers: LanguageServers, + process_memory_cache: Rc>, +} + +impl std::fmt::Debug for LanguageServerState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("LanguageServerState") + .field("items", &self.items) + .field("workspace", &self.workspace) + .field("lsp_store", &self.lsp_store) + .field("active_editor", &self.active_editor) + .field("language_servers", &self.language_servers) + .finish_non_exhaustive() + } +} + +const PROCESS_MEMORY_CACHE_DURATION: Duration = Duration::from_secs(5); + +struct ProcessMemoryCache { + system: System, + memory_usage: HashMap, + last_refresh: Option, +} + +impl ProcessMemoryCache { + fn new() -> Self { + Self { + system: System::new(), + memory_usage: HashMap::new(), + last_refresh: None, + } + } + + fn get_memory_usage(&mut self, process_id: u32) -> u64 { + let cache_expired = self + .last_refresh + .map(|last| last.elapsed() >= PROCESS_MEMORY_CACHE_DURATION) + .unwrap_or(true); + + if cache_expired { + let refresh_kind = + RefreshKind::nothing().with_processes(ProcessRefreshKind::nothing().with_memory()); + self.system.refresh_specifics(refresh_kind); + self.memory_usage.clear(); + self.last_refresh = Some(Instant::now()); + } + + if let Some(&memory) = self.memory_usage.get(&process_id) { + return memory; + } + + let root_pid = Pid::from_u32(process_id); + + let parent_map: HashMap = self + .system + .processes() + .iter() + .filter_map(|(&pid, process)| Some((pid, process.parent()?))) + .collect(); + + let total_memory = self + .system + .processes() + .iter() + .filter(|(pid, _)| self.is_descendant_of(**pid, root_pid, &parent_map)) + .map(|(_, process)| process.memory()) + .sum(); + + self.memory_usage.insert(process_id, total_memory); + total_memory + } + + fn is_descendant_of(&self, pid: Pid, root_pid: Pid, parent_map: &HashMap) -> bool { + let mut current = pid; + while current != root_pid { + match parent_map.get(¤t) { + Some(&parent) => current = parent, + None => return false, + } + } + true + } } struct ActiveEditor { @@ -143,6 +226,7 @@ impl LanguageServerState { ( status.server_version.clone(), status.binary.as_ref().map(|b| b.path.clone()), + status.process_id, ), ) }) @@ -150,6 +234,8 @@ impl LanguageServerState { }) .unwrap_or_default(); + let process_memory_cache = self.process_memory_cache.clone(); + let mut first_button_encountered = false; for item in &self.items { if let LspMenuItem::ToggleServersButton { restart } = item { @@ -274,16 +360,17 @@ impl LanguageServerState { .or_else(|| server_info.binary_status.as_ref()?.message.as_ref()) .cloned(); - let (server_version, binary_path) = server_metadata + let (server_version, binary_path, process_id) = server_metadata .get(&server_info.id) - .map(|(version, path)| { + .map(|(version, path, process_id)| { ( version.clone(), path.as_ref() .map(|p| SharedString::from(p.compact().to_string_lossy().to_string())), + *process_id, ) }) - .unwrap_or((None, None)); + .unwrap_or((None, None, None)); let truncated_message = message.as_ref().and_then(|message| { message @@ -293,17 +380,6 @@ impl LanguageServerState { .next() }); - let metadata_label = match (&server_version, &truncated_message) { - (None, None) => None, - (Some(version), None) => Some(SharedString::from(format!("v{}", version.as_ref()))), - (None, Some(message)) => Some(message.clone()), - (Some(version), Some(message)) => Some(SharedString::from(format!( - "v{}\n\n{}", - version.as_ref(), - message.as_ref() - ))), - }; - let submenu_server_name = server_info.name.clone(); let submenu_server_info = server_info.clone(); @@ -319,6 +395,7 @@ impl LanguageServerState { let lsp_store = self.lsp_store.clone(); let state = cx.entity().downgrade(); let can_stop = submenu_server_info.can_stop(); + let process_memory_cache = process_memory_cache.clone(); move |menu, _window, _cx| { let mut submenu = menu; @@ -509,9 +586,55 @@ impl LanguageServerState { } submenu = submenu.separator().custom_row({ - let metadata_label = metadata_label.clone(); let binary_path = binary_path.clone(); + let server_version = server_version.clone(); + let truncated_message = truncated_message.clone(); + let process_memory_cache = process_memory_cache.clone(); move |_, _| { + let memory_usage = process_id.map(|pid| { + process_memory_cache.borrow_mut().get_memory_usage(pid) + }); + + let memory_label = memory_usage.map(|bytes| { + if bytes >= 1024 * 1024 * 1024 { + format!( + "{:.1} GB", + bytes as f64 / (1024.0 * 1024.0 * 1024.0) + ) + } else { + format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0)) + } + }); + + let metadata_label = + match (&server_version, &memory_label, &truncated_message) { + (None, None, None) => None, + (Some(version), None, None) => { + Some(format!("v{}", version.as_ref())) + } + (None, Some(memory), None) => Some(memory.clone()), + (Some(version), Some(memory), None) => { + Some(format!("v{} • {}", version.as_ref(), memory)) + } + (None, None, Some(message)) => Some(message.to_string()), + (Some(version), None, Some(message)) => Some(format!( + "v{}\n\n{}", + version.as_ref(), + message.as_ref() + )), + (None, Some(memory), Some(message)) => { + Some(format!("{}\n\n{}", memory, message.as_ref())) + } + (Some(version), Some(memory), Some(message)) => { + Some(format!( + "v{} • {}\n\n{}", + version.as_ref(), + memory, + message.as_ref() + )) + } + }; + h_flex() .id("metadata-container") .ml_neg_1() @@ -744,6 +867,7 @@ impl LspButton { lsp_store: lsp_store.downgrade(), active_editor: None, language_servers, + process_memory_cache: Rc::new(RefCell::new(ProcessMemoryCache::new())), }); let mut lsp_button = Self { diff --git a/crates/language_tools/src/lsp_log_view.rs b/crates/language_tools/src/lsp_log_view.rs index a83ebaabc219cc131ed6dd499cfbd3c663e5d9ae..e4ed650464ce5272356b031df362290289981f5b 100644 --- a/crates/language_tools/src/lsp_log_view.rs +++ b/crates/language_tools/src/lsp_log_view.rs @@ -1349,6 +1349,7 @@ impl ServerInfo { binary: Some(server.binary().clone()), configuration: Some(server.configuration().clone()), workspace_folders: server.workspace_folders(), + process_id: server.process_id(), }, } } diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index d1ce36c462181544bcf4c4dec26b4e4d3ee9ec2d..0785b31cd6f8d813e45d5afab11dff7eae5451e1 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -1208,6 +1208,11 @@ impl LanguageServer { self.server_id } + /// Get the process ID of the running language server, if available. + pub fn process_id(&self) -> Option { + self.server.lock().as_ref().map(|child| child.id()) + } + /// Language server's binary information. pub fn binary(&self) -> &LanguageServerBinary { &self.binary diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 9410697d03dd10ca0ce4f9e459a20c1a8975f370..7c2a3d5a83854d13b95e6f6618382e484ed7817a 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -3908,6 +3908,7 @@ pub struct LanguageServerStatus { pub binary: Option, pub configuration: Option, pub workspace_folders: BTreeSet, + pub process_id: Option, } #[derive(Clone, Debug)] @@ -8487,6 +8488,7 @@ impl LspStore { binary: None, configuration: None, workspace_folders: BTreeSet::new(), + process_id: None, }, ) }) @@ -9554,6 +9556,7 @@ impl LspStore { binary: None, configuration: None, workspace_folders: BTreeSet::new(), + process_id: None, }, ); cx.emit(LspStoreEvent::LanguageServerAdded( @@ -11610,6 +11613,7 @@ impl LspStore { binary: Some(language_server.binary().clone()), configuration: Some(language_server.configuration().clone()), workspace_folders: language_server.workspace_folders(), + process_id: language_server.process_id(), }, );