Show memory used by language servers (#48226)

John Tur created

Closes https://github.com/zed-industries/zed/issues/32712

<img width="575" height="131" alt="image"
src="https://github.com/user-attachments/assets/aaad583c-277c-4a84-b2ce-a18b8b38bc0e"
/>

Release Notes:

- The language servers menu now shows the memory used by each language
server.

Change summary

Cargo.lock                                |   1 
crates/language_tools/Cargo.toml          |   1 
crates/language_tools/src/lsp_button.rs   | 158 ++++++++++++++++++++++--
crates/language_tools/src/lsp_log_view.rs |   1 
crates/lsp/src/lsp.rs                     |   5 
crates/project/src/lsp_store.rs           |   4 
6 files changed, 153 insertions(+), 17 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -9207,6 +9207,7 @@ dependencies = [
  "semver",
  "serde_json",
  "settings",
+ "sysinfo 0.37.2",
  "theme",
  "tree-sitter",
  "ui",

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

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<Subscription>,
 }
 
-#[derive(Debug)]
 struct LanguageServerState {
     items: Vec<LspMenuItem>,
     workspace: WeakEntity<Workspace>,
     lsp_store: WeakEntity<LspStore>,
     active_editor: Option<ActiveEditor>,
     language_servers: LanguageServers,
+    process_memory_cache: Rc<RefCell<ProcessMemoryCache>>,
+}
+
+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<u32, u64>,
+    last_refresh: Option<Instant>,
+}
+
+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<Pid, Pid> = 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<Pid, Pid>) -> bool {
+        let mut current = pid;
+        while current != root_pid {
+            match parent_map.get(&current) {
+                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 {

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(),
             },
         }
     }

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<u32> {
+        self.server.lock().as_ref().map(|child| child.id())
+    }
+
     /// Language server's binary information.
     pub fn binary(&self) -> &LanguageServerBinary {
         &self.binary

crates/project/src/lsp_store.rs 🔗

@@ -3908,6 +3908,7 @@ pub struct LanguageServerStatus {
     pub binary: Option<LanguageServerBinary>,
     pub configuration: Option<Value>,
     pub workspace_folders: BTreeSet<Uri>,
+    pub process_id: Option<u32>,
 }
 
 #[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(),
             },
         );