From 0588a4fc19e37df44557c0c6f403559dd5addb1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan?= Date: Tue, 24 Oct 2023 19:48:05 +0200 Subject: [PATCH] Make debug info lazy (#10728) # Description * Makes the `debug info` lazy which greatly improves performance. * Adds a `thread id` attribute ![Screenshot 2023-10-15 211940](https://github.com/nushell/nushell/assets/25441359/b8457a30-ebf7-4731-9e13-17635501f029) ![image](https://github.com/nushell/nushell/assets/25441359/010ed35b-9f50-4fc6-8650-b68b29d5a9cd) # User-Facing Changes `threadid` column added. --------- Co-authored-by: Darren Schroeder <343840+fdncred@users.noreply.github.com> --- crates/nu-command/src/debug/info.rs | 324 ++++++++++++------ .../nu-command/tests/commands/debug_info.rs | 7 + crates/nu-command/tests/commands/mod.rs | 1 + 3 files changed, 235 insertions(+), 97 deletions(-) create mode 100644 crates/nu-command/tests/commands/debug_info.rs diff --git a/crates/nu-command/src/debug/info.rs b/crates/nu-command/src/debug/info.rs index 1a921432cf..31480071f5 100644 --- a/crates/nu-command/src/debug/info.rs +++ b/crates/nu-command/src/debug/info.rs @@ -1,16 +1,20 @@ use nu_protocol::ast::Call; use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::{ - record, Category, Example, IntoPipelineData, PipelineData, Record, ShellError, Signature, Span, - Type, Value, + record, Category, Example, IntoPipelineData, LazyRecord, PipelineData, Record, ShellError, + Signature, Span, Type, Value, }; use sysinfo::{Pid, PidExt, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt}; -// Character used to separate directories in a Path Environment variable on windows is ";" -#[cfg(target_family = "windows")] -const ENV_PATH_SEPARATOR_CHAR: char = ';'; -// Character used to separate directories in a Path Environment variable on linux/mac/unix is ":" -#[cfg(not(target_family = "windows"))] -const ENV_PATH_SEPARATOR_CHAR: char = ':'; +const ENV_PATH_SEPARATOR_CHAR: char = { + #[cfg(target_family = "windows")] + { + ';' + } + #[cfg(not(target_family = "windows"))] + { + ':' + } +}; #[derive(Clone)] pub struct DebugInfo; @@ -34,14 +38,6 @@ impl Command for DebugInfo { .category(Category::Debug) } - fn examples(&self) -> Vec { - vec![Example { - description: "View process information", - example: "debug info", - result: None, - }] - } - fn run( &self, _engine_state: &EngineState, @@ -50,9 +46,181 @@ impl Command for DebugInfo { _input: PipelineData, ) -> Result { let span = Span::unknown(); - // get the nushell process id + + let record = LazySystemInfoRecord { span }; + + Ok(Value::lazy_record(Box::new(record), span).into_pipeline_data()) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "View process information", + example: "debug info", + result: None, + }] + } +} + +#[derive(Debug, Clone)] +struct LazySystemInfoRecord { + span: Span, +} + +impl LazySystemInfoRecord { + fn get_column_value_with_system( + &self, + column: &str, + system_option: Option<&System>, + ) -> Result { let pid = Pid::from(std::process::id() as usize); - // only refresh the process and memory information + match column { + "thread_id" => Ok(Value::int(get_thread_id(), self.span)), + "pid" => Ok(Value::int(pid.as_u32() as i64, self.span)), + "ppid" => { + // only get information requested + let system_opt = SystemOpt::from((system_option, || { + RefreshKind::new().with_processes( + ProcessRefreshKind::new() + .without_cpu() + .without_disk_usage() + .without_user(), + ) + })); + + let system = system_opt.get_system(); + // get the process information for the nushell pid + let pinfo = system.process(pid); + + Ok(pinfo + .and_then(|p| p.parent()) + .map(|p| Value::int(p.as_u32() as i64, self.span)) + .unwrap_or(Value::nothing(self.span))) + } + "system" => { + // only get information requested + let system_opt = + SystemOpt::from((system_option, || RefreshKind::new().with_memory())); + + let system = system_opt.get_system(); + + Ok(Value::record( + record! { + "total_memory" => Value::filesize(system.total_memory() as i64, self.span), + "free_memory" => Value::filesize(system.free_memory() as i64, self.span), + "used_memory" => Value::filesize(system.used_memory() as i64, self.span), + "available_memory" => Value::filesize(system.available_memory() as i64, self.span), + }, + self.span, + )) + } + "process" => { + // only get information requested + let system_opt = SystemOpt::from((system_option, || { + RefreshKind::new().with_processes( + ProcessRefreshKind::new() + .without_cpu() + .without_disk_usage() + .without_user(), + ) + })); + + let system = system_opt.get_system(); + // get the process information for the nushell pid + let pinfo = system.process(pid); + + if let Some(p) = pinfo { + Ok(Value::record( + record! { + "memory" => Value::filesize(p.memory() as i64, self.span), + "virtual_memory" => Value::filesize(p.virtual_memory() as i64, self.span), + "status" => Value::string(p.status().to_string(), self.span), + "root" => { + if let Some(filename) = p.exe().parent() { + Value::string(filename.to_string_lossy().to_string(), self.span) + } else { + Value::nothing(self.span) + } + }, + "cwd" => Value::string(p.cwd().to_string_lossy().to_string(), self.span), + "exe_path" => Value::string(p.exe().to_string_lossy().to_string(), self.span), + "command" => Value::string(p.cmd().join(" "), self.span), + "name" => Value::string(p.name().to_string(), self.span), + "environment" => { + let mut env_rec = Record::new(); + for val in p.environ() { + if let Some((key, value)) = val.split_once('=') { + let is_env_var_a_list = { + { + #[cfg(target_family = "windows")] + { + key == "Path" || key == "PATHEXT" || key == "PSMODULEPATH" || key == "PSModulePath" + } + #[cfg(not(target_family = "windows"))] + { + key == "PATH" || key == "DYLD_FALLBACK_LIBRARY_PATH" + } + } + }; + if is_env_var_a_list { + let items = value.split(ENV_PATH_SEPARATOR_CHAR).map(|r| Value::string(r.to_string(), self.span)).collect::>(); + env_rec.push(key.to_string(), Value::list(items, self.span)); + } else if key == "LS_COLORS" { // LS_COLORS is a special case, it's a colon separated list of key=value pairs + let items = value.split(':').map(|r| Value::string(r.to_string(), self.span)).collect::>(); + env_rec.push(key.to_string(), Value::list(items, self.span)); + } else { + env_rec.push(key.to_string(), Value::string(value.to_string(), self.span)); + } + } + } + Value::record(env_rec, self.span) + }, + }, + self.span, + )) + } else { + // If we can't get the process information, just return the system information + // only get information requested + let system_opt = + SystemOpt::from((system_option, || RefreshKind::new().with_memory())); + let system = system_opt.get_system(); + + Ok(Value::record( + record! { + "total_memory" => Value::filesize(system.total_memory() as i64, self.span), + "free_memory" => Value::filesize(system.free_memory() as i64, self.span), + "used_memory" => Value::filesize(system.used_memory() as i64, self.span), + "available_memory" => Value::filesize(system.available_memory() as i64, self.span), + }, + self.span, + )) + } + } + _ => Err(ShellError::IncompatibleParametersSingle { + msg: format!("Unknown column: {}", column), + span: self.span, + }), + } + } +} + +impl<'a> LazyRecord<'a> for LazySystemInfoRecord { + fn column_names(&'a self) -> Vec<&'a str> { + vec!["thread_id", "pid", "ppid", "process", "system"] + } + + fn get_column_value(&self, column: &str) -> Result { + self.get_column_value_with_system(column, None) + } + + fn span(&self) -> Span { + self.span + } + + fn clone_value(&self, span: Span) -> Value { + Value::lazy_record(Box::new(LazySystemInfoRecord { span }), span) + } + + fn collect(&'a self) -> Result { let rk = RefreshKind::new() .with_processes( ProcessRefreshKind::new() @@ -63,86 +231,48 @@ impl Command for DebugInfo { .with_memory(); // only get information requested let system = System::new_with_specifics(rk); - // get the process information for the nushell pid - let pinfo = system.process(pid); - if let Some(p) = pinfo { - Ok(Value::record( - record! { - "pid" => Value::int(p.pid().as_u32() as i64, span), - "ppid" => Value::int(p.parent().unwrap_or(0.into()).as_u32() as i64, span), - "process" => { - Value::record( - record! { - "memory" => Value::filesize(p.memory() as i64, span), - "virtual_memory" => Value::filesize(p.virtual_memory() as i64, span), - "status" => Value::string(p.status().to_string(), span), - // This is a hack to get the "root" since p.root() doesn't work on macos - // Would probably puke if nu was on the root of a drive, maybe other ways too. - "root" => { - if let Some(filename) = p.exe().parent() { - Value::string(filename.to_string_lossy().to_string(), span) - } else { - Value::nothing(span) - } - }, - // "root" => Value::string(p.root().to_string_lossy().to_string(), span), - "cwd" => Value::string(p.cwd().to_string_lossy().to_string(), span), - "exe_path" => Value::string(p.exe().to_string_lossy().to_string(), span), - "command" => Value::string(p.cmd().join(" "), span), - "name" => Value::string(p.name().to_string(), span), - "environment" => { - let mut env_rec = Record::new(); - for val in p.environ() { - let (key, value) = val.split_once('=').unwrap_or(("", "")); - // Let's make some of the known list-variables into lists - if key == "PATH" || - key == "Path" || - key == "DYLD_FALLBACK_LIBRARY_PATH" || - key == "PATHEXT" || - key == "PSMODULEPATH" || - key == "PSModulePath" { - let items = value.split(ENV_PATH_SEPARATOR_CHAR).map(|r| Value::string(r.to_string(), span)).collect::>(); - env_rec.push(key.to_string(), Value::list(items, span)); - } else if key == "LS_COLORS" { // LS_COLORS is a special case, it's a colon separated list of key=value pairs - let items = value.split(':').map(|r| Value::string(r.to_string(), span)).collect::>(); - env_rec.push(key.to_string(), Value::list(items, span)); - } else { - env_rec.push(key.to_string(), Value::string(value.to_string(), span)); - } - } - Value::record(env_rec, span) - }, - }, - span, - ) - }, - "system" => { - Value::record( - record! { - "total_memory" => Value::filesize(system.total_memory() as i64, span), - "free_memory" => Value::filesize(system.free_memory() as i64, span), - "used_memory" => Value::filesize(system.used_memory() as i64, span), - "available_memory" => Value::filesize(system.available_memory() as i64, span), - }, - span, - ) - } - }, - span, - ).into_pipeline_data()) - } else { - // If we can't get the process information, just return the system information - Ok(Value::record( - record! { - "total_memory" => Value::filesize(system.total_memory() as i64, span), - "free_memory" => Value::filesize(system.free_memory() as i64, span), - "used_memory" => Value::filesize(system.used_memory() as i64, span), - "available_memory" => Value::filesize(system.available_memory() as i64, span), - }, - span, - ) - .into_pipeline_data()) + self.column_names() + .into_iter() + .map(|col| { + let val = self.get_column_value_with_system(col, Some(&system))?; + Ok((col.to_owned(), val)) + }) + .collect::>() + .map(|record| Value::record(record, self.span())) + } +} + +enum SystemOpt<'a> { + Ptr(&'a System), + Owned(Box), +} + +impl<'a> SystemOpt<'a> { + fn get_system(&'a self) -> &'a System { + match self { + SystemOpt::Ptr(system) => system, + SystemOpt::Owned(system) => system, } } } + +impl<'a, F: Fn() -> RefreshKind> From<(Option<&'a System>, F)> for SystemOpt<'a> { + fn from((system_opt, refresh_kind_create): (Option<&'a System>, F)) -> Self { + match system_opt { + Some(system) => SystemOpt::<'a>::Ptr(system), + None => SystemOpt::Owned(Box::new(System::new_with_specifics(refresh_kind_create()))), + } + } +} + +fn get_thread_id() -> i64 { + #[cfg(target_family = "windows")] + { + unsafe { windows::Win32::System::Threading::GetCurrentThreadId() as i64 } + } + #[cfg(not(target_family = "windows"))] + { + unsafe { libc::pthread_self() as i64 } + } +} diff --git a/crates/nu-command/tests/commands/debug_info.rs b/crates/nu-command/tests/commands/debug_info.rs new file mode 100644 index 0000000000..71f8e17cc9 --- /dev/null +++ b/crates/nu-command/tests/commands/debug_info.rs @@ -0,0 +1,7 @@ +use nu_test_support::nu; + +#[test] +fn runs_successfully() { + let actual = nu!("debug info"); + assert_eq!(actual.err, ""); +} diff --git a/crates/nu-command/tests/commands/mod.rs b/crates/nu-command/tests/commands/mod.rs index 987b106f14..38b7c2bf5a 100644 --- a/crates/nu-command/tests/commands/mod.rs +++ b/crates/nu-command/tests/commands/mod.rs @@ -13,6 +13,7 @@ mod continue_; mod conversions; mod cp; mod date; +mod debug_info; mod def; mod default; mod detect_columns;