From de69f27b20aec2718f0e7448556530ad5281f268 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Fri, 4 Oct 2024 14:53:40 +0100 Subject: [PATCH 01/29] bench: Add basic Process::get_all benchmark --- src/parse/proces.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/parse/proces.rs b/src/parse/proces.rs index 6bc70ff..bfb513c 100644 --- a/src/parse/proces.rs +++ b/src/parse/proces.rs @@ -229,3 +229,19 @@ mod tests { } } } + +#[cfg(feature = "unstable")] +#[cfg(test)] +mod bench { + use super::*; + extern crate test; + + #[bench] + /// Bench listing all processes + fn get_all(b: &mut test::Bencher) { + b.iter(move || { + let mut tmpdirs = vec![]; + get_all_info(&[], &mut tmpdirs); + }); + } +} From 90f61777766e6eafda3d0f2642331c22dfc9a07e Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Fri, 4 Oct 2024 15:07:29 +0100 Subject: [PATCH 02/29] proces: Refactor process parsing We now return all processes instead of filtering, hardcode categories instead of taking a param, and parse the parent pid. This cost just 1.5ms (with a running ebuild), and is a step towards displaying the emerge process tree. --- src/parse/current.rs | 28 ++++++++++------ src/parse/proces.rs | 79 ++++++++++++++++++++++++++------------------ 2 files changed, 64 insertions(+), 43 deletions(-) diff --git a/src/parse/current.rs b/src/parse/current.rs index b0365bf..c25911c 100644 --- a/src/parse/current.rs +++ b/src/parse/current.rs @@ -1,6 +1,6 @@ //! Handles parsing of current emerge state. -use super::{proces::{get_all_info, Proc}, +use super::{proces::{get_all_proc, Proc, ProcKind}, Ansi}; use crate::ResumeKind; use log::*; @@ -151,17 +151,23 @@ pub struct EmergeInfo { pub fn get_emerge(tmpdirs: &mut Vec) -> EmergeInfo { let mut res = EmergeInfo { start: i64::MAX, cmds: vec![], pkgs: vec![] }; let re_python = Regex::new("^[a-z/-]+python[0-9.]* [a-z/-]+python[0-9.]*/").unwrap(); - for mut proc in get_all_info(&["emerge", "python"], tmpdirs) { - res.start = std::cmp::min(res.start, proc.start); - if proc.idx == 0 { - proc.cmdline = re_python.replace(&proc.cmdline, "").to_string(); - res.cmds.push(proc); - } else if let Some(a) = proc.cmdline.find("sandbox [") { - if let Some(b) = proc.cmdline.find("] sandbox") { - if let Some(p) = Pkg::try_new(&proc.cmdline[(a + 9)..b]) { - res.pkgs.push(p); + for mut proc in get_all_proc(tmpdirs) { + match proc.kind { + ProcKind::Emerge => { + res.start = std::cmp::min(res.start, proc.start); + proc.cmdline = re_python.replace(&proc.cmdline, "").to_string(); + res.cmds.push(proc); + }, + ProcKind::Python => { + if let Some(a) = proc.cmdline.find("sandbox [") { + if let Some(b) = proc.cmdline.find("] sandbox") { + if let Some(p) = Pkg::try_new(&proc.cmdline[(a + 9)..b]) { + res.pkgs.push(p); + } + } } - } + }, + ProcKind::Other => (), } } trace!("{:?}", res); diff --git a/src/parse/proces.rs b/src/parse/proces.rs index bfb513c..fd1d9eb 100644 --- a/src/parse/proces.rs +++ b/src/parse/proces.rs @@ -8,16 +8,25 @@ use crate::*; use anyhow::{ensure, Context}; +use libc::pid_t; use std::{fs::{read_dir, DirEntry, File}, io::prelude::*, path::PathBuf}; +#[derive(Debug)] +pub enum ProcKind { + Emerge, + Python, + Other, +} + #[derive(Debug)] pub struct Proc { - pub idx: usize, + pub kind: ProcKind, pub cmdline: String, pub start: i64, - pub pid: i32, + pub pid: pid_t, + pub ppid: pid_t, } impl std::fmt::Display for Proc { @@ -36,36 +45,38 @@ impl std::fmt::Display for Proc { } /// Get command name, arguments, start time, and pid for one process. -fn get_proc_info(filter: &[&str], - entry: &DirEntry, - clocktick: i64, - time_ref: i64, - tmpdirs: &mut Vec) - -> Option { +fn get_proc(entry: &DirEntry, + clocktick: i64, + time_ref: i64, + tmpdirs: &mut Vec) + -> Option { // Parse pid. // At this stage we expect `entry` to not always correspond to a process. let pid = i32::from_str(&entry.file_name().to_string_lossy()).ok()?; - // See linux/Documentation/filesystems/proc.txt Table 1-4: Contents of the stat files. + // See linux/Documentation/filesystems/proc.rst Table 1-4: Contents of the stat files. let mut stat = String::new(); File::open(entry.path().join("stat")).ok()?.read_to_string(&mut stat).ok()?; - // Parse command name, bail out now if it doesn't match. - // The command name is surrounded by parens and may contain spaces. + // Parse command name (it's surrounded by parens and may contain spaces) + // If it's emerge, look for portage tmpdir in its fds let (cmd_start, cmd_end) = (stat.find('(')? + 1, stat.rfind(')')?); - let idx = if filter.is_empty() { - usize::MAX + let kind = if &stat[cmd_start..cmd_end] == "emerge" { + extend_tmpdirs(entry.path(), tmpdirs); + ProcKind::Emerge + } else if stat[cmd_start..cmd_end].starts_with("python") { + ProcKind::Python } else { - filter.iter().position(|&f| stat[cmd_start..cmd_end].starts_with(f))? + ProcKind::Other }; - // Parse start time - let start_time = i64::from_str(stat[cmd_end + 1..].split(' ').nth(20)?).ok()?; + // Parse parent pid and start time + let mut fields = stat[cmd_end + 1..].split(' '); + let ppid = i32::from_str(fields.nth(2)?).ok()?; + let start_time = i64::from_str(fields.nth(17)?).ok()?; // Parse arguments let mut cmdline = String::new(); File::open(entry.path().join("cmdline")).ok()?.read_to_string(&mut cmdline).ok()?; cmdline = cmdline.replace('\0', " ").trim().into(); - // Find portage tmpdir - extend_tmpdirs(entry.path(), tmpdirs); // Done - Some(Proc { idx, cmdline, start: time_ref + start_time / clocktick, pid }) + Some(Proc { kind, cmdline, start: time_ref + start_time / clocktick, pid, ppid }) } /// Find tmpdir by looking for "build.log" in the process fds, and add it to the provided vector. @@ -91,13 +102,13 @@ fn extend_tmpdirs(proc: PathBuf, tmpdirs: &mut Vec) { } /// Get command name, arguments, start time, and pid for all processes. -pub fn get_all_info(filter: &[&str], tmpdirs: &mut Vec) -> Vec { - get_all_info_result(filter, tmpdirs).unwrap_or_else(|e| { - log_err(e); - vec![] - }) +pub fn get_all_proc(tmpdirs: &mut Vec) -> Vec { + get_all_proc_result(tmpdirs).unwrap_or_else(|e| { + log_err(e); + vec![] + }) } -fn get_all_info_result(filter: &[&str], tmpdirs: &mut Vec) -> Result, Error> { +fn get_all_proc_result(tmpdirs: &mut Vec) -> Result, Error> { // clocktick and time_ref are needed to interpret stat.start_time. time_ref should correspond to // the system boot time; not sure why it doesn't, but it's still usable as a reference. // SAFETY: returns a system constant, only failure mode should be a zero/negative value @@ -112,7 +123,7 @@ fn get_all_info_result(filter: &[&str], tmpdirs: &mut Vec) -> Result let mut ret: Vec = Vec::new(); for entry in read_dir("/proc/").context("Listing /proc/")? { - if let Some(i) = get_proc_info(filter, &entry?, clocktick, time_ref, tmpdirs) { + if let Some(i) = get_proc(&entry?, clocktick, time_ref, tmpdirs) { ret.push(i) } } @@ -144,7 +155,7 @@ mod tests { // First get the system's process start times using our implementation // Store it as pid => (cmd, rust_time, ps_time) let mut tmpdirs = vec![]; - let mut info = get_all_info(&[], &mut tmpdirs) + let mut info = get_all_proc(&mut tmpdirs) .iter() .map(|i| (i.pid, (i.cmdline.clone(), Some(i.start), None))) .collect::, Option)>>(); @@ -222,7 +233,11 @@ mod tests { (22, 9, 12, "Pid 22: ...i"),]; for (pid, cmdlen, precision, out) in t.into_iter() { dbg!((pid, cmdlen, precision, out)); - let i = Proc { idx: usize::MAX, pid, cmdline: s[..cmdlen].to_string(), start: 0 }; + let i = Proc { kind: ProcKind::Other, + pid, + ppid: -1, + cmdline: s[..cmdlen].to_string(), + start: 0 }; let f = format!("{1:.0$}", precision, i); assert!(precision < 10 || f.len() <= precision, "{} <= {}", f.len(), precision); assert_eq!(f, out); @@ -240,8 +255,8 @@ mod bench { /// Bench listing all processes fn get_all(b: &mut test::Bencher) { b.iter(move || { - let mut tmpdirs = vec![]; - get_all_info(&[], &mut tmpdirs); - }); - } + let mut tmpdirs = vec![]; + get_all_proc(&mut tmpdirs); + }); + } } From a19a85ccb6cd5b6608b845af5b009c4316a47f57 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Sat, 5 Oct 2024 19:45:28 +0100 Subject: [PATCH 03/29] proces: Refactor Proc formating * Formatting/editing of commandline is delayed until display time (potentially saving a few cycles) * Display width is now user-configurable (should be in the `predict` section only, but that's a refactoring for another day) * Switched from std `Display` to our `Disp` trait That last part is the initial motivation, as I want to be able to pass an `indent` argument. --- completion.bash | 5 ++- completion.fish | 1 + completion.zsh | 1 + emlop.toml | 1 + src/commands.rs | 2 +- src/config.rs | 2 + src/config/cli.rs | 12 +++++- src/config/toml.rs | 1 + src/parse.rs | 1 + src/parse/current.rs | 4 +- src/parse/proces.rs | 99 +++++++++++++++++++++++--------------------- 11 files changed, 74 insertions(+), 55 deletions(-) diff --git a/completion.bash b/completion.bash index bdaf7be..7bfcee9 100644 --- a/completion.bash +++ b/completion.bash @@ -167,7 +167,7 @@ _emlop() { return 0 ;; emlop__predict) - opts="-s -N -n -f -t -H -o -F -v -h --show --first --last --tmpdir --resume --unknown --avg --limit --from --to --header --duration --date --utc --color --output --logfile --help" + opts="-s -N -n -f -t -H -o -F -v -h --show --first --last --tmpdir --resume --unknown --avg --limit --from --to --header --duration --date --utc --color --output --procwidth --logfile --help" if [[ ${cur} == -* ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -191,6 +191,9 @@ _emlop() { --color) COMPREPLY=($(compgen -W "yes no auto" "${cur}")) ;; + --procwidth) + COMPREPLY=($(compgen -W "10 20 40 80 160" "${cur}")) + ;; --output|-o) COMPREPLY=($(compgen -W "tab columns auto" "${cur}")) ;; diff --git a/completion.fish b/completion.fish index 16d5ae0..5e2d7ee 100644 --- a/completion.fish +++ b/completion.fish @@ -34,6 +34,7 @@ complete -c emlop -n "__fish_seen_subcommand_from predict" -l resume -d 'Use mai complete -c emlop -n "__fish_seen_subcommand_from predict" -l unknown -d 'Assume unkown packages take seconds to merge' -x -a "0 5 10 20 60" complete -c emlop -n "__fish_seen_subcommand_from predict" -l avg -d 'Select function used to predict durations' -x -a "arith median weighted-arith weighted-median" complete -c emlop -n "__fish_seen_subcommand_from predict" -l limit -d 'Use the last merge times to predict durations' -x -a "1 5 20 999" +complete -c emlop -n "__fish_seen_subcommand_from predict" -l procwidth -d 'Maximum display width for emerge process' -x -a "10 20 40 80 160" complete -c emlop -n "__fish_seen_subcommand_from stats" -s s -l show -d 'Show (p)ackages, (t)otals, (s)yncs, and/or (a)ll' -x -a "ptsa" complete -c emlop -n "__fish_seen_subcommand_from stats" -s g -l groupby -d 'Group by (y)ear, (m)onth, (w)eek, (d)ay, (n)one' -x -a "year month week day none" diff --git a/completion.zsh b/completion.zsh index ff8928b..640fc55 100644 --- a/completion.zsh +++ b/completion.zsh @@ -87,6 +87,7 @@ _emlop() { '--color=[Enable color (yes/no/auto)]' \ '-o+[Ouput format (columns/tab/auto)]:format: ' \ '--output=[Ouput format (columns/tab/auto)]:format: ' \ +'--procwidth=[Maximum display width for emerge process]' '-F+[Location of emerge log file]:file: ' \ '--logfile=[Location of emerge log file]:file: ' \ '*-v[Increase verbosity (can be given multiple times)]' \ diff --git a/emlop.toml b/emlop.toml index ffa379f..5def40f 100644 --- a/emlop.toml +++ b/emlop.toml @@ -12,6 +12,7 @@ # header = true # color = "yes" # output = "columns" +# procwidth = 60 [log] # show = "mus" # starttime = true diff --git a/src/commands.rs b/src/commands.rs index 8e2179d..00907d7 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -304,7 +304,7 @@ pub fn cmd_predict(gc: &Conf, sc: &ConfPred) -> Result { } if sc.show.emerge { for proc in &einfo.cmds { - tbl.row([&[&proc], &[&FmtDur(now - proc.start)], &[]]); + tbl.row([&[&FmtProc(proc)], &[&FmtDur(now - proc.start)], &[]]); } } diff --git a/src/config.rs b/src/config.rs index ac5890f..2ef0d09 100644 --- a/src/config.rs +++ b/src/config.rs @@ -37,6 +37,7 @@ pub struct Conf { pub logfile: String, pub from: Option, pub to: Option, + pub procwidth: usize, } pub struct ConfLog { pub show: Show, @@ -164,6 +165,7 @@ impl Conf { dur_t: sel!(cli, toml, duration, (), DurationStyle::Hms)?, date_offset: offset, date_fmt: sel!(cli, toml, date, (), DateStyle::default())?, + procwidth: sel!(cli, toml, predict, procwidth, 10..1000, 60)? as usize, out: sel!(cli, toml, output, isterm, outdef)? }) } #[cfg(test)] diff --git a/src/config/cli.rs b/src/config/cli.rs index 521c024..e8e0e6f 100644 --- a/src/config/cli.rs +++ b/src/config/cli.rs @@ -231,12 +231,19 @@ pub fn build_cli() -> Command { .display_order(24) .help_heading("Format") .help("Display start time instead of end time"); + let procwidth = Arg::new("procwidth").long("procwidth") + .value_name("num") + .global(true)//TODO should be for predict only, not global + .num_args(1) + .display_order(25) + .help_heading("Format") + .help("Maximum display width for emerge process"); let color = Arg::new("color").long("color") .value_name("bool") .global(true) .num_args(..=1) .default_missing_value("y") - .display_order(25) + .display_order(26) .help_heading("Format") .help("Enable color (yes/no/auto)") .long_help("Enable color (yes/no/auto)\n \ @@ -247,7 +254,7 @@ pub fn build_cli() -> Command { .long("output") .value_name("format") .global(true) - .display_order(26) + .display_order(27) .help_heading("Format") .help("Ouput format (columns/tab/auto)") .long_help("Ouput format (columns/tab/auto)\n \ @@ -389,6 +396,7 @@ pub fn build_cli() -> Command { .arg(date) .arg(utc) .arg(color) + .arg(procwidth) .arg(output) .arg(logfile) .arg(verbose) diff --git a/src/config/toml.rs b/src/config/toml.rs index d7861a6..4d8c5bb 100644 --- a/src/config/toml.rs +++ b/src/config/toml.rs @@ -14,6 +14,7 @@ pub struct TomlPred { pub limit: Option, pub unknown: Option, pub tmpdir: Option>, + pub procwidth: Option, } #[derive(Deserialize, Debug)] pub struct TomlStats { diff --git a/src/parse.rs b/src/parse.rs index ae1da8c..538147c 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -6,3 +6,4 @@ mod proces; pub use ansi::{Ansi, AnsiStr}; pub use current::{get_buildlog, get_emerge, get_pretend, get_resume, Pkg}; pub use history::{get_hist, Hist}; +pub use proces::FmtProc; diff --git a/src/parse/current.rs b/src/parse/current.rs index c25911c..a69e48e 100644 --- a/src/parse/current.rs +++ b/src/parse/current.rs @@ -150,12 +150,10 @@ pub struct EmergeInfo { /// should be the case for almost all users) pub fn get_emerge(tmpdirs: &mut Vec) -> EmergeInfo { let mut res = EmergeInfo { start: i64::MAX, cmds: vec![], pkgs: vec![] }; - let re_python = Regex::new("^[a-z/-]+python[0-9.]* [a-z/-]+python[0-9.]*/").unwrap(); - for mut proc in get_all_proc(tmpdirs) { + for proc in get_all_proc(tmpdirs) { match proc.kind { ProcKind::Emerge => { res.start = std::cmp::min(res.start, proc.start); - proc.cmdline = re_python.replace(&proc.cmdline, "").to_string(); res.cmds.push(proc); }, ProcKind::Python => { diff --git a/src/parse/proces.rs b/src/parse/proces.rs index fd1d9eb..c870e19 100644 --- a/src/parse/proces.rs +++ b/src/parse/proces.rs @@ -6,12 +6,14 @@ //! implementaion (does procinfo crate work on BSDs ?), but it's unit-tested against ps and should //! be fast. -use crate::*; +use crate::{table::Disp, *}; use anyhow::{ensure, Context}; use libc::pid_t; +use regex::Regex; use std::{fs::{read_dir, DirEntry, File}, io::prelude::*, - path::PathBuf}; + path::PathBuf, + sync::OnceLock}; #[derive(Debug)] pub enum ProcKind { @@ -29,18 +31,30 @@ pub struct Proc { pub ppid: pid_t, } -impl std::fmt::Display for Proc { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let pid = format!("Pid {}: ", self.pid); - let capacity = f.precision().unwrap_or(45).saturating_sub(pid.len()); - let cmdlen = self.cmdline.len(); - if capacity >= cmdlen || cmdlen < 4 { - write!(f, "{pid}{}", &self.cmdline) - } else if capacity > 3 { - write!(f, "{pid}...{}", &self.cmdline[(cmdlen - capacity + 3)..]) +/// Rewrite "/usr/bin/python312 /foo/bar/python312/emerge" as "emerge" +// TODO: generalize to other cases, we just want the basename of the script/binary +// TODO: msrv 1.80: switch to LazyLock +static RE_PROC: OnceLock = OnceLock::new(); + +pub struct FmtProc<'a>(pub &'a Proc); +impl Disp for FmtProc<'_> { + fn out(&self, buf: &mut Vec, conf: &Conf) -> usize { + let start = buf.len(); + let prefixlen = (self.0.pid.max(1).ilog10() + 2) as usize; + let re_proc = RE_PROC.get_or_init(|| { + Regex::new("^[a-z/-]+(python|bash|sandbox)[0-9.]* [a-z/-]+python[0-9.]*/").unwrap() +}); + let cmdline = re_proc.replace(self.0.cmdline.replace('\0', " ").trim(), "").to_string(); + let cmdcap = conf.procwidth.saturating_sub(prefixlen); + let cmdlen = cmdline.len(); + if cmdcap >= cmdlen { + wtb!(buf, "{} {}", self.0.pid, cmdline) + } else if cmdcap > 3 { + wtb!(buf, "{} ...{}", self.0.pid, &cmdline[(cmdlen - cmdcap + 3)..]) } else { - write!(f, "{pid}...") + wtb!(buf, "{} ...", self.0.pid) } + buf.len() - start } } @@ -74,7 +88,6 @@ fn get_proc(entry: &DirEntry, // Parse arguments let mut cmdline = String::new(); File::open(entry.path().join("cmdline")).ok()?.read_to_string(&mut cmdline).ok()?; - cmdline = cmdline.replace('\0', " ").trim().into(); // Done Some(Proc { kind, cmdline, start: time_ref + start_time / clocktick, pid, ppid }) } @@ -133,7 +146,7 @@ fn get_all_proc_result(tmpdirs: &mut Vec) -> Result, Error> { #[cfg(test)] mod tests { - use super::*; + use super::{config::Conf, *}; use regex::Regex; use std::{collections::BTreeMap, process::Command}; use time::{macros::format_description, PrimitiveDateTime}; @@ -204,43 +217,33 @@ mod tests { } #[test] - fn format_info() { - let s = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; - let t: Vec<(i32, usize, usize, &str)> = vec![// Precison is way too small, use elipsis starting at 4 chars - (1, 1, 1, "Pid 1: a"), - (1, 2, 1, "Pid 1: ab"), - (2, 3, 1, "Pid 2: abc"), - (3, 4, 1, "Pid 3: ..."), - (4, 5, 1, "Pid 4: ..."), - (330, 1, 1, "Pid 330: a"), - (331, 2, 1, "Pid 331: ab"), - (332, 3, 1, "Pid 332: abc"), - (333, 4, 1, "Pid 333: ..."), - (334, 5, 1, "Pid 334: ..."), - // Here we have enough space - (1, 1, 12, "Pid 1: a"), - (1, 2, 12, "Pid 1: ab"), - (1, 3, 12, "Pid 1: abc"), - (1, 4, 12, "Pid 1: abcd"), - (1, 5, 12, "Pid 1: abcde"), - (12, 4, 12, "Pid 12: abcd"), - (123, 3, 12, "Pid 123: abc"), - (1234, 2, 12, "Pid 1234: ab"), - // Running out of space again, but we can display part of it - (1, 6, 12, "Pid 1: ...ef"), - (1, 7, 12, "Pid 1: ...fg"), - (1, 8, 12, "Pid 1: ...gh"), - (22, 9, 12, "Pid 22: ...i"),]; - for (pid, cmdlen, precision, out) in t.into_iter() { - dbg!((pid, cmdlen, precision, out)); - let i = Proc { kind: ProcKind::Other, + #[rustfmt::skip] + fn format_proc() { + let t: Vec<(i32, usize, &str)> = vec![// Here we have enough space + (1, 1, "1 1"), + (1, 2, "1 12"), + (1, 8, "1 12345678"), + (12, 7, "12 1234567"), + (123, 6, "123 123456"), + (1234, 5, "1234 12345"), + // Running out of space, but we can display part of it + (1, 10, "1 ...67890"), + (12345, 10, "12345 ...0"), + // Capacity is way too small, use elipsis starting at 4 chars + (1234567, 3, "1234567 ..."), + (123456, 3, "123456 123"), + ]; + for (pid, cmdlen, out) in t.into_iter() { + dbg!((pid, cmdlen, out)); + let conf = Conf::from_str(&format!("emlop p --procwidth 10")); + let mut buf = vec![]; + let p = Proc { kind: ProcKind::Other, pid, ppid: -1, - cmdline: s[..cmdlen].to_string(), + cmdline: "1234567890"[..cmdlen].to_string(), start: 0 }; - let f = format!("{1:.0$}", precision, i); - assert!(precision < 10 || f.len() <= precision, "{} <= {}", f.len(), precision); - assert_eq!(f, out); + FmtProc(&p).out(&mut buf, &conf); + assert_eq!(String::from_utf8(buf), Ok(String::from(out))); } } } From f467d53581bab9f0b626de69fe662e4b1ecc8d3b Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Mon, 7 Oct 2024 12:46:01 +0100 Subject: [PATCH 04/29] table: Allow alignment of larger columns --- src/table.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/table.rs b/src/table.rs index ae86029..b0573af 100644 --- a/src/table.rs +++ b/src/table.rs @@ -20,6 +20,8 @@ pub enum Align { Right, } +const SPACES: [u8; 512] = [b' '; 512]; + pub struct Table<'a, const N: usize> { /// Buffer where unaligned entries are written /// @@ -117,7 +119,6 @@ impl<'a, const N: usize> Table<'a, N> { // Check the max len of each column, for the rows we have let widths: [usize; N] = std::array::from_fn(|i| self.rows.iter().fold(0, |m, r| usize::max(m, r[i].0))); - let spaces = [b' '; 128]; for row in &self.rows { let mut first = true; // Clippy suggests `for (i, ) in row.iter().enumerate().take(N)` which IMHO @@ -140,7 +141,7 @@ impl<'a, const N: usize> Table<'a, N> { out.write_all(self.margins[i].as_bytes()).unwrap_or(()); } // Write the cell with alignment - let pad = &spaces[0..usize::min(spaces.len(), widths[i] - len)]; + let pad = &SPACES[0..usize::min(SPACES.len(), widths[i] - len)]; match self.aligns[i] { Align::Right => { out.write_all(pad).unwrap_or(()); From 2912147cdfcdf8a36f24070c875b861da4bb10aa Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Mon, 7 Oct 2024 13:27:02 +0100 Subject: [PATCH 05/29] predict: Display emerge proces tree instead of just initial proces Needs some refinements, but the basic functionality is here. --- src/commands.rs | 16 +++++++++++++--- src/config/cli.rs | 2 +- src/parse.rs | 2 +- src/parse/current.rs | 17 ++++++++++------- src/parse/proces.rs | 42 ++++++++++++++++++++++++------------------ src/table.rs | 2 +- 6 files changed, 50 insertions(+), 31 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 00907d7..e9f51c3 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,4 +1,5 @@ use crate::{datetime::*, parse::*, table::*, *}; +use libc::pid_t; use std::{collections::{BTreeMap, HashMap, HashSet}, io::{stdin, IsTerminal}}; @@ -283,6 +284,15 @@ fn cmd_stats_group(gc: &Conf, } } +fn proc_tree(now: i64, tbl: &mut Table<3>, procs: &HashMap, pid: pid_t, depth: usize) { + if let Some(proc) = procs.get(&pid) { + tbl.row([&[&FmtProc(proc, depth)], &[&FmtDur(now - proc.start)], &[]]); + for child in procs.iter().filter(|(_, p)| p.ppid == pid).map(|(pid, _)| pid) { + proc_tree(now, tbl, procs, *child, depth + 1) + } + } +} + /// Predict future merge time /// /// Very similar to cmd_summary except we want total build time for a list of ebuilds. @@ -295,7 +305,7 @@ pub fn cmd_predict(gc: &Conf, sc: &ConfPred) -> Result { // Gather and print info about current merge process. let einfo = get_emerge(&mut tmpdirs); - if einfo.cmds.is_empty() + if einfo.roots.is_empty() && std::io::stdin().is_terminal() && matches!(sc.resume, ResumeKind::No | ResumeKind::Auto) { @@ -303,8 +313,8 @@ pub fn cmd_predict(gc: &Conf, sc: &ConfPred) -> Result { return Ok(false); } if sc.show.emerge { - for proc in &einfo.cmds { - tbl.row([&[&FmtProc(proc)], &[&FmtDur(now - proc.start)], &[]]); + for p in einfo.roots { + proc_tree(now, &mut tbl, &einfo.procs, p, 0); } } diff --git a/src/config/cli.rs b/src/config/cli.rs index e8e0e6f..e39dbb3 100644 --- a/src/config/cli.rs +++ b/src/config/cli.rs @@ -233,7 +233,7 @@ pub fn build_cli() -> Command { .help("Display start time instead of end time"); let procwidth = Arg::new("procwidth").long("procwidth") .value_name("num") - .global(true)//TODO should be for predict only, not global + .global(true) //TODO should be for predict only, not global .num_args(1) .display_order(25) .help_heading("Format") diff --git a/src/parse.rs b/src/parse.rs index 538147c..477ad2d 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -6,4 +6,4 @@ mod proces; pub use ansi::{Ansi, AnsiStr}; pub use current::{get_buildlog, get_emerge, get_pretend, get_resume, Pkg}; pub use history::{get_hist, Hist}; -pub use proces::FmtProc; +pub use proces::{get_all_proc, FmtProc, Proc, ProcKind}; diff --git a/src/parse/current.rs b/src/parse/current.rs index a69e48e..9eaee95 100644 --- a/src/parse/current.rs +++ b/src/parse/current.rs @@ -1,13 +1,14 @@ //! Handles parsing of current emerge state. -use super::{proces::{get_all_proc, Proc, ProcKind}, - Ansi}; +use super::{get_all_proc, Ansi, Proc, ProcKind}; use crate::ResumeKind; +use libc::pid_t; use log::*; use regex::Regex; use serde::Deserialize; use serde_json::from_reader; -use std::{fs::File, +use std::{collections::HashMap, + fs::File, io::{BufRead, BufReader, Read}, path::PathBuf}; @@ -136,7 +137,8 @@ fn read_buildlog(file: File, max: usize) -> String { #[derive(Debug)] pub struct EmergeInfo { pub start: i64, - pub cmds: Vec, + pub procs: HashMap, + pub roots: Vec, pub pkgs: Vec, } @@ -149,12 +151,13 @@ pub struct EmergeInfo { /// gives us the actually emerging ebuild and stage (depends on portage FEATURES=sandbox, which /// should be the case for almost all users) pub fn get_emerge(tmpdirs: &mut Vec) -> EmergeInfo { - let mut res = EmergeInfo { start: i64::MAX, cmds: vec![], pkgs: vec![] }; - for proc in get_all_proc(tmpdirs) { + let procs = get_all_proc(tmpdirs); + let mut res = EmergeInfo { start: i64::MAX, procs, roots: vec![], pkgs: vec![] }; + for (pid, proc) in &res.procs { match proc.kind { ProcKind::Emerge => { res.start = std::cmp::min(res.start, proc.start); - res.cmds.push(proc); + res.roots.push(*pid); }, ProcKind::Python => { if let Some(a) = proc.cmdline.find("sandbox [") { diff --git a/src/parse/proces.rs b/src/parse/proces.rs index c870e19..e7852fe 100644 --- a/src/parse/proces.rs +++ b/src/parse/proces.rs @@ -10,7 +10,8 @@ use crate::{table::Disp, *}; use anyhow::{ensure, Context}; use libc::pid_t; use regex::Regex; -use std::{fs::{read_dir, DirEntry, File}, +use std::{collections::HashMap, + fs::{read_dir, DirEntry, File}, io::prelude::*, path::PathBuf, sync::OnceLock}; @@ -36,23 +37,28 @@ pub struct Proc { // TODO: msrv 1.80: switch to LazyLock static RE_PROC: OnceLock = OnceLock::new(); -pub struct FmtProc<'a>(pub &'a Proc); +pub struct FmtProc<'a>(pub &'a Proc, pub usize); impl Disp for FmtProc<'_> { fn out(&self, buf: &mut Vec, conf: &Conf) -> usize { let start = buf.len(); - let prefixlen = (self.0.pid.max(1).ilog10() + 2) as usize; - let re_proc = RE_PROC.get_or_init(|| { - Regex::new("^[a-z/-]+(python|bash|sandbox)[0-9.]* [a-z/-]+python[0-9.]*/").unwrap() -}); + let prefixlen = self.0.pid.max(1).ilog10() as usize + 2 * self.1 + 2; + let re_proc = + RE_PROC.get_or_init(|| { + Regex::new("^[a-z/-]+(python|bash|sandbox)[0-9.]* [a-z/-]+python[0-9.]*/").unwrap() + }); let cmdline = re_proc.replace(self.0.cmdline.replace('\0', " ").trim(), "").to_string(); let cmdcap = conf.procwidth.saturating_sub(prefixlen); let cmdlen = cmdline.len(); if cmdcap >= cmdlen { - wtb!(buf, "{} {}", self.0.pid, cmdline) + wtb!(buf, "{}{} {}", " ".repeat(self.1), self.0.pid, cmdline) } else if cmdcap > 3 { - wtb!(buf, "{} ...{}", self.0.pid, &cmdline[(cmdlen - cmdcap + 3)..]) + wtb!(buf, + "{}{} ...{}", + " ".repeat(self.1), + self.0.pid, + &cmdline[(cmdlen - cmdcap + 3)..]) } else { - wtb!(buf, "{} ...", self.0.pid) + wtb!(buf, "{}{} ...", " ".repeat(self.1), self.0.pid) } buf.len() - start } @@ -115,13 +121,13 @@ fn extend_tmpdirs(proc: PathBuf, tmpdirs: &mut Vec) { } /// Get command name, arguments, start time, and pid for all processes. -pub fn get_all_proc(tmpdirs: &mut Vec) -> Vec { +pub fn get_all_proc(tmpdirs: &mut Vec) -> HashMap { get_all_proc_result(tmpdirs).unwrap_or_else(|e| { log_err(e); - vec![] + HashMap::new() }) } -fn get_all_proc_result(tmpdirs: &mut Vec) -> Result, Error> { +fn get_all_proc_result(tmpdirs: &mut Vec) -> Result, Error> { // clocktick and time_ref are needed to interpret stat.start_time. time_ref should correspond to // the system boot time; not sure why it doesn't, but it's still usable as a reference. // SAFETY: returns a system constant, only failure mode should be a zero/negative value @@ -134,10 +140,10 @@ fn get_all_proc_result(tmpdirs: &mut Vec) -> Result, Error> { let uptime = i64::from_str(uptimestr.split('.').next().unwrap()).unwrap(); let time_ref = epoch_now() - uptime; // Now iterate through /proc/ - let mut ret: Vec = Vec::new(); + let mut ret: HashMap = HashMap::new(); for entry in read_dir("/proc/").context("Listing /proc/")? { - if let Some(i) = get_proc(&entry?, clocktick, time_ref, tmpdirs) { - ret.push(i) + if let Some(p) = get_proc(&entry?, clocktick, time_ref, tmpdirs) { + ret.insert(p.pid, p); } } Ok(ret) @@ -170,8 +176,8 @@ mod tests { let mut tmpdirs = vec![]; let mut info = get_all_proc(&mut tmpdirs) .iter() - .map(|i| (i.pid, (i.cmdline.clone(), Some(i.start), None))) - .collect::, Option)>>(); + .map(|(pid, i)| (*pid, (i.cmdline.clone(), Some(i.start), None))) + .collect::, Option)>>(); // Then get them using the ps implementation (merging them into the same data structure) let ps_start = epoch_now(); let cmd = Command::new("ps").env("TZ", "UTC") @@ -242,7 +248,7 @@ mod tests { ppid: -1, cmdline: "1234567890"[..cmdlen].to_string(), start: 0 }; - FmtProc(&p).out(&mut buf, &conf); + FmtProc(&p, 0).out(&mut buf, &conf); assert_eq!(String::from_utf8(buf), Ok(String::from(out))); } } diff --git a/src/table.rs b/src/table.rs index b0573af..417fd4c 100644 --- a/src/table.rs +++ b/src/table.rs @@ -15,7 +15,7 @@ impl Disp for T { } #[derive(Clone, Copy)] -pub enum Align { +enum Align { Left, Right, } From c983deba26f0e747aff1981c4b0f36d0b022ce5d Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Tue, 8 Oct 2024 22:30:10 +0100 Subject: [PATCH 06/29] predict: Make proces depth configurable, move proces width to predict group --- completion.bash | 7 +++++-- completion.fish | 3 ++- completion.zsh | 3 ++- emlop.toml | 3 ++- src/commands.rs | 19 +++++++++++++------ src/config.rs | 8 +++++--- src/config/cli.rs | 26 ++++++++++++++++---------- src/config/toml.rs | 3 ++- src/parse/proces.rs | 15 ++++++++++----- 9 files changed, 57 insertions(+), 30 deletions(-) diff --git a/completion.bash b/completion.bash index 7bfcee9..1f18749 100644 --- a/completion.bash +++ b/completion.bash @@ -167,7 +167,7 @@ _emlop() { return 0 ;; emlop__predict) - opts="-s -N -n -f -t -H -o -F -v -h --show --first --last --tmpdir --resume --unknown --avg --limit --from --to --header --duration --date --utc --color --output --procwidth --logfile --help" + opts="-s -N -n -f -t -H -o -F -v -h --show --first --last --tmpdir --resume --unknown --avg --limit --from --to --header --duration --date --utc --color --output --pdepth --pwidth --logfile --help" if [[ ${cur} == -* ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -191,9 +191,12 @@ _emlop() { --color) COMPREPLY=($(compgen -W "yes no auto" "${cur}")) ;; - --procwidth) + --pwidth) COMPREPLY=($(compgen -W "10 20 40 80 160" "${cur}")) ;; + --pdepth) + COMPREPLY=($(compgen -W "0 1 3 5 7 99" "${cur}")) + ;; --output|-o) COMPREPLY=($(compgen -W "tab columns auto" "${cur}")) ;; diff --git a/completion.fish b/completion.fish index 5e2d7ee..f2f5d8e 100644 --- a/completion.fish +++ b/completion.fish @@ -34,7 +34,8 @@ complete -c emlop -n "__fish_seen_subcommand_from predict" -l resume -d 'Use mai complete -c emlop -n "__fish_seen_subcommand_from predict" -l unknown -d 'Assume unkown packages take seconds to merge' -x -a "0 5 10 20 60" complete -c emlop -n "__fish_seen_subcommand_from predict" -l avg -d 'Select function used to predict durations' -x -a "arith median weighted-arith weighted-median" complete -c emlop -n "__fish_seen_subcommand_from predict" -l limit -d 'Use the last merge times to predict durations' -x -a "1 5 20 999" -complete -c emlop -n "__fish_seen_subcommand_from predict" -l procwidth -d 'Maximum display width for emerge process' -x -a "10 20 40 80 160" +complete -c emlop -n "__fish_seen_subcommand_from predict" -l pwidth -d 'Maximum width of emerge proces comandline' -x -a "10 20 40 80 160" +complete -c emlop -n "__fish_seen_subcommand_from predict" -l pdepth -d 'Maximum depth of emerge proces tree' -x -a "0 1 3 5 7 99" complete -c emlop -n "__fish_seen_subcommand_from stats" -s s -l show -d 'Show (p)ackages, (t)otals, (s)yncs, and/or (a)ll' -x -a "ptsa" complete -c emlop -n "__fish_seen_subcommand_from stats" -s g -l groupby -d 'Group by (y)ear, (m)onth, (w)eek, (d)ay, (n)one' -x -a "year month week day none" diff --git a/completion.zsh b/completion.zsh index 640fc55..f49a7f7 100644 --- a/completion.zsh +++ b/completion.zsh @@ -87,7 +87,8 @@ _emlop() { '--color=[Enable color (yes/no/auto)]' \ '-o+[Ouput format (columns/tab/auto)]:format: ' \ '--output=[Ouput format (columns/tab/auto)]:format: ' \ -'--procwidth=[Maximum display width for emerge process]' +'--pwidth=[Maximum width of emerge proces comandline]' +'--pdepth=[Maximum depth of emerge proces tree]' '-F+[Location of emerge log file]:file: ' \ '--logfile=[Location of emerge log file]:file: ' \ '*-v[Increase verbosity (can be given multiple times)]' \ diff --git a/emlop.toml b/emlop.toml index 5def40f..0c7193d 100644 --- a/emlop.toml +++ b/emlop.toml @@ -12,7 +12,6 @@ # header = true # color = "yes" # output = "columns" -# procwidth = 60 [log] # show = "mus" # starttime = true @@ -22,6 +21,8 @@ # limit = 20 # unknown = 300 # tmpdir = ["/foo", "/bar"] +# pwidth = 60 +# pdepth = 8 [stats] # show = "pts" # avg = "arith" diff --git a/src/commands.rs b/src/commands.rs index e9f51c3..7c5b02f 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -284,11 +284,18 @@ fn cmd_stats_group(gc: &Conf, } } -fn proc_tree(now: i64, tbl: &mut Table<3>, procs: &HashMap, pid: pid_t, depth: usize) { - if let Some(proc) = procs.get(&pid) { - tbl.row([&[&FmtProc(proc, depth)], &[&FmtDur(now - proc.start)], &[]]); - for child in procs.iter().filter(|(_, p)| p.ppid == pid).map(|(pid, _)| pid) { - proc_tree(now, tbl, procs, *child, depth + 1) +fn proc_rows(now: i64, + tbl: &mut Table<3>, + procs: &HashMap, + pid: pid_t, + depth: usize, + sc: &ConfPred) { + if depth < sc.pdepth { + if let Some(proc) = procs.get(&pid) { + tbl.row([&[&FmtProc(proc, depth, sc.pwidth)], &[&FmtDur(now - proc.start)], &[]]); + for child in procs.iter().filter(|(_, p)| p.ppid == pid).map(|(pid, _)| pid) { + proc_rows(now, tbl, procs, *child, depth + 1, sc) + } } } } @@ -314,7 +321,7 @@ pub fn cmd_predict(gc: &Conf, sc: &ConfPred) -> Result { } if sc.show.emerge { for p in einfo.roots { - proc_tree(now, &mut tbl, &einfo.procs, p, 0); + proc_rows(now, &mut tbl, &einfo.procs, p, 0, sc); } } diff --git a/src/config.rs b/src/config.rs index 2ef0d09..5727f08 100644 --- a/src/config.rs +++ b/src/config.rs @@ -37,7 +37,6 @@ pub struct Conf { pub logfile: String, pub from: Option, pub to: Option, - pub procwidth: usize, } pub struct ConfLog { pub show: Show, @@ -56,6 +55,8 @@ pub struct ConfPred { pub resume: ResumeKind, pub unknown: i64, pub tmpdirs: Vec, + pub pwidth: usize, + pub pdepth: usize, } pub struct ConfStats { pub show: Show, @@ -165,7 +166,6 @@ impl Conf { dur_t: sel!(cli, toml, duration, (), DurationStyle::Hms)?, date_offset: offset, date_fmt: sel!(cli, toml, date, (), DateStyle::default())?, - procwidth: sel!(cli, toml, predict, procwidth, 10..1000, 60)? as usize, out: sel!(cli, toml, output, isterm, outdef)? }) } #[cfg(test)] @@ -202,7 +202,9 @@ impl ConfPred { resume: *cli.get_one("resume").unwrap_or(&ResumeKind::Auto), tmpdirs, first: *cli.get_one("first").unwrap_or(&usize::MAX), - last: *cli.get_one("last").unwrap_or(&usize::MAX) }) + last: *cli.get_one("last").unwrap_or(&usize::MAX), + pwidth: sel!(cli, toml, predict, pwidth, 10..1000, 60)? as usize, + pdepth: sel!(cli, toml, predict, pdepth, 0..100, 8)? as usize }) } } diff --git a/src/config/cli.rs b/src/config/cli.rs index e39dbb3..846d992 100644 --- a/src/config/cli.rs +++ b/src/config/cli.rs @@ -231,19 +231,24 @@ pub fn build_cli() -> Command { .display_order(24) .help_heading("Format") .help("Display start time instead of end time"); - let procwidth = Arg::new("procwidth").long("procwidth") - .value_name("num") - .global(true) //TODO should be for predict only, not global - .num_args(1) - .display_order(25) - .help_heading("Format") - .help("Maximum display width for emerge process"); + let pwidth = Arg::new("pwidth").long("pwidth") + .value_name("num") + .num_args(1) + .display_order(25) + .help_heading("Format") + .help("Maximum width of emerge proces commandline"); + let pdepth = Arg::new("pdepth").long("pdepth") + .value_name("num") + .num_args(1) + .display_order(26) + .help_heading("Format") + .help("Maximum depth of emerge proces tree"); let color = Arg::new("color").long("color") .value_name("bool") .global(true) .num_args(..=1) .default_missing_value("y") - .display_order(26) + .display_order(27) .help_heading("Format") .help("Enable color (yes/no/auto)") .long_help("Enable color (yes/no/auto)\n \ @@ -254,7 +259,7 @@ pub fn build_cli() -> Command { .long("output") .value_name("format") .global(true) - .display_order(27) + .display_order(28) .help_heading("Format") .help("Ouput format (columns/tab/auto)") .long_help("Ouput format (columns/tab/auto)\n \ @@ -331,6 +336,8 @@ pub fn build_cli() -> Command { .arg(tmpdir) .arg(resume) .arg(unknown) + .arg(pwidth) + .arg(pdepth) .arg(&avg) .arg(&limit); let h = "Show statistics about syncs, per-package (un)merges, and total (un)merges\n\ @@ -396,7 +403,6 @@ pub fn build_cli() -> Command { .arg(date) .arg(utc) .arg(color) - .arg(procwidth) .arg(output) .arg(logfile) .arg(verbose) diff --git a/src/config/toml.rs b/src/config/toml.rs index 4d8c5bb..23398c0 100644 --- a/src/config/toml.rs +++ b/src/config/toml.rs @@ -14,7 +14,8 @@ pub struct TomlPred { pub limit: Option, pub unknown: Option, pub tmpdir: Option>, - pub procwidth: Option, + pub pwidth: Option, + pub pdepth: Option, } #[derive(Deserialize, Debug)] pub struct TomlStats { diff --git a/src/parse/proces.rs b/src/parse/proces.rs index e7852fe..d4bce75 100644 --- a/src/parse/proces.rs +++ b/src/parse/proces.rs @@ -37,9 +37,14 @@ pub struct Proc { // TODO: msrv 1.80: switch to LazyLock static RE_PROC: OnceLock = OnceLock::new(); -pub struct FmtProc<'a>(pub &'a Proc, pub usize); +pub struct FmtProc<'a>(/// process + pub &'a Proc, + /// Indent + pub usize, + /// Width + pub usize); impl Disp for FmtProc<'_> { - fn out(&self, buf: &mut Vec, conf: &Conf) -> usize { + fn out(&self, buf: &mut Vec, _conf: &Conf) -> usize { let start = buf.len(); let prefixlen = self.0.pid.max(1).ilog10() as usize + 2 * self.1 + 2; let re_proc = @@ -47,7 +52,7 @@ impl Disp for FmtProc<'_> { Regex::new("^[a-z/-]+(python|bash|sandbox)[0-9.]* [a-z/-]+python[0-9.]*/").unwrap() }); let cmdline = re_proc.replace(self.0.cmdline.replace('\0', " ").trim(), "").to_string(); - let cmdcap = conf.procwidth.saturating_sub(prefixlen); + let cmdcap = self.2.saturating_sub(prefixlen); let cmdlen = cmdline.len(); if cmdcap >= cmdlen { wtb!(buf, "{}{} {}", " ".repeat(self.1), self.0.pid, cmdline) @@ -241,14 +246,14 @@ mod tests { ]; for (pid, cmdlen, out) in t.into_iter() { dbg!((pid, cmdlen, out)); - let conf = Conf::from_str(&format!("emlop p --procwidth 10")); + let conf = Conf::from_str(&format!("emlop p")); let mut buf = vec![]; let p = Proc { kind: ProcKind::Other, pid, ppid: -1, cmdline: "1234567890"[..cmdlen].to_string(), start: 0 }; - FmtProc(&p, 0).out(&mut buf, &conf); + FmtProc(&p, 0, 10).out(&mut buf, &conf); assert_eq!(String::from_utf8(buf), Ok(String::from(out))); } } From c77cbacdeef42e404e7f722db50e9bf6e3ccec60 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Tue, 8 Oct 2024 22:41:58 +0100 Subject: [PATCH 07/29] cli: Use RangeInclusive for integer bounds This was the original intent, and fixes an off-by-one in the error message. --- src/config.rs | 12 ++++++------ src/config/types.rs | 14 ++++++++------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/config.rs b/src/config.rs index 5727f08..13ffdc6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -197,14 +197,14 @@ impl ConfPred { }; Ok(Self { show: sel!(cli, toml, predict, show, "emta", Show::emt())?, avg: sel!(cli, toml, predict, avg, (), Average::Median)?, - lim: sel!(cli, toml, predict, limit, 1..65000, 10)? as u16, - unknown: sel!(cli, toml, predict, unknown, 0..3600, 10)?, + lim: sel!(cli, toml, predict, limit, 1..=65000, 10)? as u16, + unknown: sel!(cli, toml, predict, unknown, 0..=3600, 10)?, resume: *cli.get_one("resume").unwrap_or(&ResumeKind::Auto), tmpdirs, first: *cli.get_one("first").unwrap_or(&usize::MAX), last: *cli.get_one("last").unwrap_or(&usize::MAX), - pwidth: sel!(cli, toml, predict, pwidth, 10..1000, 60)? as usize, - pdepth: sel!(cli, toml, predict, pdepth, 0..100, 8)? as usize }) + pwidth: sel!(cli, toml, predict, pwidth, 10..=1000, 60)? as usize, + pdepth: sel!(cli, toml, predict, pdepth, 0..=100, 8)? as usize }) } } @@ -213,7 +213,7 @@ impl ConfStats { Ok(Self { show: sel!(cli, toml, stats, show, "ptsa", Show::p())?, search: cli.get_many("search").unwrap_or_default().cloned().collect(), exact: cli.get_flag("exact"), - lim: sel!(cli, toml, stats, limit, 1..65000, 10)? as u16, + lim: sel!(cli, toml, stats, limit, 1..=65000, 10)? as u16, avg: sel!(cli, toml, stats, avg, (), Average::Median)?, group: sel!(cli, toml, stats, group, (), Timespan::None)? }) } @@ -225,7 +225,7 @@ impl ConfAccuracy { search: cli.get_many("search").unwrap_or_default().cloned().collect(), exact: cli.get_flag("exact"), avg: sel!(cli, toml, accuracy, avg, (), Average::Median)?, - lim: sel!(cli, toml, accuracy, limit, 1..65000, 10)? as u16, + lim: sel!(cli, toml, accuracy, limit, 1..=65000, 10)? as u16, last: *cli.get_one("last").unwrap_or(&usize::MAX) }) } } diff --git a/src/config/types.rs b/src/config/types.rs index 09c0587..826167e 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -1,4 +1,4 @@ -use std::{ops::Range, str::FromStr}; +use std::{ops::RangeInclusive, str::FromStr}; /// Parsing trait for args /// @@ -26,18 +26,20 @@ impl ArgParse for bool { } } } -impl ArgParse> for i64 { - fn parse(s: &String, r: Range, src: &'static str) -> Result { +impl ArgParse> for i64 { + fn parse(s: &String, r: RangeInclusive, src: &'static str) -> Result { let i = i64::from_str(s).map_err(|_| ArgError::new(s, src).msg("Not an integer"))?; Self::parse(&i, r, src) } } -impl ArgParse> for i64 { - fn parse(i: &i64, r: Range, src: &'static str) -> Result { +impl ArgParse> for i64 { + fn parse(i: &i64, r: RangeInclusive, src: &'static str) -> Result { if r.contains(i) { Ok(*i) } else { - Err(ArgError::new(i, src).msg(format!("Should be between {} and {}", r.start, r.end))) + Err(ArgError::new(i, src).msg(format!("Should be between {} and {}", + r.start(), + r.end()))) } } } From b2a9abedcf5100ced5fb05c6ae81ae3e359e4bd6 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Thu, 10 Oct 2024 15:06:29 +0100 Subject: [PATCH 08/29] proces: Refactor indent printing Simpler code, and saves an alloc. --- src/parse/proces.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/parse/proces.rs b/src/parse/proces.rs index d4bce75..db5bdfe 100644 --- a/src/parse/proces.rs +++ b/src/parse/proces.rs @@ -46,24 +46,20 @@ pub struct FmtProc<'a>(/// process impl Disp for FmtProc<'_> { fn out(&self, buf: &mut Vec, _conf: &Conf) -> usize { let start = buf.len(); - let prefixlen = self.0.pid.max(1).ilog10() as usize + 2 * self.1 + 2; + let prefixlen = self.0.pid.max(1).ilog10() as usize + 2 * self.1 + 1; let re_proc = RE_PROC.get_or_init(|| { Regex::new("^[a-z/-]+(python|bash|sandbox)[0-9.]* [a-z/-]+python[0-9.]*/").unwrap() }); let cmdline = re_proc.replace(self.0.cmdline.replace('\0', " ").trim(), "").to_string(); - let cmdcap = self.2.saturating_sub(prefixlen); + let cmdcap = self.2.saturating_sub(prefixlen + 1); let cmdlen = cmdline.len(); if cmdcap >= cmdlen { - wtb!(buf, "{}{} {}", " ".repeat(self.1), self.0.pid, cmdline) + wtb!(buf, "{:prefixlen$} {}", self.0.pid, cmdline) } else if cmdcap > 3 { - wtb!(buf, - "{}{} ...{}", - " ".repeat(self.1), - self.0.pid, - &cmdline[(cmdlen - cmdcap + 3)..]) + wtb!(buf, "{:prefixlen$} ...{}", self.0.pid, &cmdline[(cmdlen - cmdcap + 3)..]) } else { - wtb!(buf, "{}{} ...", " ".repeat(self.1), self.0.pid) + wtb!(buf, "{:prefixlen$} ...", self.0.pid) } buf.len() - start } From 542481dddde856343f05daaa43d980f660e0d972 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Tue, 15 Oct 2024 10:37:16 +0100 Subject: [PATCH 09/29] proces: Refactor commandline formating Better heuristics to isolate the the interesting part of the process name, and switched from regex to plain string search. --- src/parse/proces.rs | 120 ++++++++++++++++++++++++++++---------------- 1 file changed, 76 insertions(+), 44 deletions(-) diff --git a/src/parse/proces.rs b/src/parse/proces.rs index db5bdfe..4e91bc5 100644 --- a/src/parse/proces.rs +++ b/src/parse/proces.rs @@ -9,12 +9,10 @@ use crate::{table::Disp, *}; use anyhow::{ensure, Context}; use libc::pid_t; -use regex::Regex; use std::{collections::HashMap, fs::{read_dir, DirEntry, File}, io::prelude::*, - path::PathBuf, - sync::OnceLock}; + path::PathBuf}; #[derive(Debug)] pub enum ProcKind { @@ -32,10 +30,14 @@ pub struct Proc { pub ppid: pid_t, } -/// Rewrite "/usr/bin/python312 /foo/bar/python312/emerge" as "emerge" -// TODO: generalize to other cases, we just want the basename of the script/binary -// TODO: msrv 1.80: switch to LazyLock -static RE_PROC: OnceLock = OnceLock::new(); +/// Like `Path.file_name()`, but less likely to interpret package categ/name as files +fn approx_filename(s: &str) -> Option { + if s.chars().all(|c| matches!(c, 'A'..='Z' | 'a'..='z' | '0'..='9' | '.' | '-' | '/')) { + s.rfind('/') + } else { + None + } +} pub struct FmtProc<'a>(/// process pub &'a Proc, @@ -45,21 +47,35 @@ pub struct FmtProc<'a>(/// process pub usize); impl Disp for FmtProc<'_> { fn out(&self, buf: &mut Vec, _conf: &Conf) -> usize { + let FmtProc(proc, indent, width) = *self; + + // Skip path and interpreter from command line + let mut cmdstart = 0; + if let Some(z1) = proc.cmdline.find('\0') { + if let Some(f1) = approx_filename(&proc.cmdline[..z1]) { + cmdstart = f1 + 1; + } + if let Some(z2) = proc.cmdline[z1 + 1..].find('\0') { + if let Some(f2) = approx_filename(&proc.cmdline[z1 + 1..z1 + z2]) { + cmdstart = z1 + f2 + 2; + } + } + } + let cmd = proc.cmdline[cmdstart..].replace('\0', " "); + let cmd = cmd.trim(); + + // Figure out how much space we have + let prefixlen = proc.pid.max(1).ilog10() as usize + 2 * indent + 1; + let cmdcap = width.saturating_sub(prefixlen + 1); + + // Output it let start = buf.len(); - let prefixlen = self.0.pid.max(1).ilog10() as usize + 2 * self.1 + 1; - let re_proc = - RE_PROC.get_or_init(|| { - Regex::new("^[a-z/-]+(python|bash|sandbox)[0-9.]* [a-z/-]+python[0-9.]*/").unwrap() - }); - let cmdline = re_proc.replace(self.0.cmdline.replace('\0', " ").trim(), "").to_string(); - let cmdcap = self.2.saturating_sub(prefixlen + 1); - let cmdlen = cmdline.len(); - if cmdcap >= cmdlen { - wtb!(buf, "{:prefixlen$} {}", self.0.pid, cmdline) + if cmdcap >= cmd.len() { + wtb!(buf, "{:prefixlen$} {}", proc.pid, cmd) } else if cmdcap > 3 { - wtb!(buf, "{:prefixlen$} ...{}", self.0.pid, &cmdline[(cmdlen - cmdcap + 3)..]) + wtb!(buf, "{:prefixlen$} ...{}", proc.pid, &cmd[(cmd.len() - cmdcap + 3)..]) } else { - wtb!(buf, "{:prefixlen$} ...", self.0.pid) + wtb!(buf, "{:prefixlen$} ...", proc.pid) } buf.len() - start } @@ -223,34 +239,50 @@ mod tests { assert!(e < 10, "Got failure score of {e}"); } + /// FmtProc should try shorten (elipsis at start) the command line when ther is no space #[test] - #[rustfmt::skip] - fn format_proc() { - let t: Vec<(i32, usize, &str)> = vec![// Here we have enough space - (1, 1, "1 1"), - (1, 2, "1 12"), - (1, 8, "1 12345678"), - (12, 7, "12 1234567"), - (123, 6, "123 123456"), - (1234, 5, "1234 12345"), - // Running out of space, but we can display part of it - (1, 10, "1 ...67890"), - (12345, 10, "12345 ...0"), - // Capacity is way too small, use elipsis starting at 4 chars - (1234567, 3, "1234567 ..."), - (123456, 3, "123456 123"), - ]; - for (pid, cmdlen, out) in t.into_iter() { - dbg!((pid, cmdlen, out)); - let conf = Conf::from_str(&format!("emlop p")); + fn proc_shorten() { + let conf = Conf::from_str(&format!("emlop p")); + let t: Vec<_> = vec![// Here we have enough space + (1, "1", "1 1"), + (1, "12", "1 12"), + (1, "12345678", "1 12345678"), + (12, "1234567", "12 1234567"), + (123, "123456", "123 123456"), + (1234, "12345", "1234 12345"), + // Running out of space, but we can display part of it + (1, "1234567890", "1 ...67890"), + (12345, "1234567890", "12345 ...0"), + // Capacity is way too small, use elipsis starting at 4 chars + (1234567, "123", "1234567 ..."), + (123456, "123", "123456 123"),]; + for (pid, cmd, out) in t.into_iter() { let mut buf = vec![]; - let p = Proc { kind: ProcKind::Other, - pid, - ppid: -1, - cmdline: "1234567890"[..cmdlen].to_string(), - start: 0 }; + let p = Proc { kind: ProcKind::Other, pid, ppid: 1, cmdline: cmd.into(), start: 0 }; FmtProc(&p, 0, 10).out(&mut buf, &conf); - assert_eq!(String::from_utf8(buf), Ok(String::from(out))); + assert_eq!(&String::from_utf8(buf).unwrap(), out, "got left expected right {pid} {cmd:?}"); + } + } + + /// FmtProc should rewrite commands + #[test] + fn proc_cmdline() { + let conf = Conf::from_str(&format!("emlop p")); + let t: Vec<_> = + vec![("foo\0bar", "1 foo bar"), + ("foo\0bar\0", "1 foo bar"), + ("/usr/bin/bash\0toto", "1 bash toto"), + ("/usr/bin/bash\0toto\0", "1 bash toto"), + ("/usr/bin/bash\0toto\0--arg", "1 bash toto --arg"), + ("/usr/bin/bash\0/path/to/toto\0--arg", "1 toto --arg"), + ("bash\0/usr/lib/portage/python3.12/ebuild.sh\0unpack\0", "1 ebuild.sh unpack"), + ("[foo/bar-0.1.600] sandbox\0/path/to/ebuild.sh\0unpack\0", "1 ebuild.sh unpack"), + ("[foo/bar-0.1.600] sandbox\0blah\0", "1 [foo/bar-0.1.600] sandbox blah"),]; + for (cmd, out) in t.into_iter() { + let mut buf = vec![]; + let p = Proc { kind: ProcKind::Other, pid: 1, ppid: 1, cmdline: cmd.into(), start: 0 }; + FmtProc(&p, 0, 100).out(&mut buf, &conf); + assert_eq!(&String::from_utf8(buf).unwrap(), out, "got left expected right {cmd:?}"); } } } From 60ea6f41967faf7023af194b71a03b99212402e7 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Tue, 15 Oct 2024 10:47:24 +0100 Subject: [PATCH 10/29] proces: Output ordered by pid --- src/commands.rs | 2 +- src/parse/current.rs | 4 ++-- src/parse/proces.rs | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 7c5b02f..c90b66d 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -286,7 +286,7 @@ fn cmd_stats_group(gc: &Conf, fn proc_rows(now: i64, tbl: &mut Table<3>, - procs: &HashMap, + procs: &BTreeMap, pid: pid_t, depth: usize, sc: &ConfPred) { diff --git a/src/parse/current.rs b/src/parse/current.rs index 9eaee95..4d4e13c 100644 --- a/src/parse/current.rs +++ b/src/parse/current.rs @@ -7,7 +7,7 @@ use log::*; use regex::Regex; use serde::Deserialize; use serde_json::from_reader; -use std::{collections::HashMap, +use std::{collections::BTreeMap, fs::File, io::{BufRead, BufReader, Read}, path::PathBuf}; @@ -137,7 +137,7 @@ fn read_buildlog(file: File, max: usize) -> String { #[derive(Debug)] pub struct EmergeInfo { pub start: i64, - pub procs: HashMap, + pub procs: BTreeMap, pub roots: Vec, pub pkgs: Vec, } diff --git a/src/parse/proces.rs b/src/parse/proces.rs index 4e91bc5..d1abf59 100644 --- a/src/parse/proces.rs +++ b/src/parse/proces.rs @@ -9,7 +9,7 @@ use crate::{table::Disp, *}; use anyhow::{ensure, Context}; use libc::pid_t; -use std::{collections::HashMap, +use std::{collections::BTreeMap, fs::{read_dir, DirEntry, File}, io::prelude::*, path::PathBuf}; @@ -138,13 +138,13 @@ fn extend_tmpdirs(proc: PathBuf, tmpdirs: &mut Vec) { } /// Get command name, arguments, start time, and pid for all processes. -pub fn get_all_proc(tmpdirs: &mut Vec) -> HashMap { +pub fn get_all_proc(tmpdirs: &mut Vec) -> BTreeMap { get_all_proc_result(tmpdirs).unwrap_or_else(|e| { log_err(e); - HashMap::new() + BTreeMap::new() }) } -fn get_all_proc_result(tmpdirs: &mut Vec) -> Result, Error> { +fn get_all_proc_result(tmpdirs: &mut Vec) -> Result, Error> { // clocktick and time_ref are needed to interpret stat.start_time. time_ref should correspond to // the system boot time; not sure why it doesn't, but it's still usable as a reference. // SAFETY: returns a system constant, only failure mode should be a zero/negative value @@ -157,7 +157,7 @@ fn get_all_proc_result(tmpdirs: &mut Vec) -> Result - let mut ret: HashMap = HashMap::new(); + let mut ret: BTreeMap = BTreeMap::new(); for entry in read_dir("/proc/").context("Listing /proc/")? { if let Some(p) = get_proc(&entry?, clocktick, time_ref, tmpdirs) { ret.insert(p.pid, p); From 4b7659685b0cd11e55694908da294fb5bf2c6896 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Tue, 15 Oct 2024 13:42:54 +0100 Subject: [PATCH 11/29] proces: Replace all control chars, not just \0 Mainly for newlines, but let's match a bit wider to be sure. --- src/parse/proces.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/parse/proces.rs b/src/parse/proces.rs index d1abf59..4ca4921 100644 --- a/src/parse/proces.rs +++ b/src/parse/proces.rs @@ -61,7 +61,7 @@ impl Disp for FmtProc<'_> { } } } - let cmd = proc.cmdline[cmdstart..].replace('\0', " "); + let cmd = proc.cmdline[cmdstart..].replace(|c: char| c.is_control(), " "); let cmd = cmd.trim(); // Figure out how much space we have @@ -260,7 +260,9 @@ mod tests { let mut buf = vec![]; let p = Proc { kind: ProcKind::Other, pid, ppid: 1, cmdline: cmd.into(), start: 0 }; FmtProc(&p, 0, 10).out(&mut buf, &conf); - assert_eq!(&String::from_utf8(buf).unwrap(), out, "got left expected right {pid} {cmd:?}"); + assert_eq!(&String::from_utf8(buf).unwrap(), + out, + "got left expected right {pid} {cmd:?}"); } } From d398e6bbfaa3aa2d4ee084f48070dfe528c8865c Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Tue, 15 Oct 2024 13:48:29 +0100 Subject: [PATCH 12/29] predict: Set default `--pdepth` to 3 Still not sure what the best value is, but this is enough to see the current stage. --- emlop.toml | 2 +- src/config.rs | 2 +- src/config/cli.rs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/emlop.toml b/emlop.toml index 0c7193d..d70934e 100644 --- a/emlop.toml +++ b/emlop.toml @@ -22,7 +22,7 @@ # unknown = 300 # tmpdir = ["/foo", "/bar"] # pwidth = 60 -# pdepth = 8 +# pdepth = 3 [stats] # show = "pts" # avg = "arith" diff --git a/src/config.rs b/src/config.rs index 13ffdc6..0e2019d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -204,7 +204,7 @@ impl ConfPred { first: *cli.get_one("first").unwrap_or(&usize::MAX), last: *cli.get_one("last").unwrap_or(&usize::MAX), pwidth: sel!(cli, toml, predict, pwidth, 10..=1000, 60)? as usize, - pdepth: sel!(cli, toml, predict, pdepth, 0..=100, 8)? as usize }) + pdepth: sel!(cli, toml, predict, pdepth, 0..=100, 3)? as usize }) } } diff --git a/src/config/cli.rs b/src/config/cli.rs index 846d992..7e3ae26 100644 --- a/src/config/cli.rs +++ b/src/config/cli.rs @@ -236,13 +236,13 @@ pub fn build_cli() -> Command { .num_args(1) .display_order(25) .help_heading("Format") - .help("Maximum width of emerge proces commandline"); + .help("Maximum width of emerge proces commandline (default 60)"); let pdepth = Arg::new("pdepth").long("pdepth") .value_name("num") .num_args(1) .display_order(26) .help_heading("Format") - .help("Maximum depth of emerge proces tree"); + .help("Maximum depth of emerge proces tree (default 3)"); let color = Arg::new("color").long("color") .value_name("bool") .global(true) From 9364b2c0e13de1d7798daf015df4be03f395f2c0 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Mon, 21 Oct 2024 18:02:08 +0100 Subject: [PATCH 13/29] proces: Colorize pid in table output --- src/parse/proces.rs | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/parse/proces.rs b/src/parse/proces.rs index 4ca4921..4d7a30b 100644 --- a/src/parse/proces.rs +++ b/src/parse/proces.rs @@ -46,38 +46,40 @@ pub struct FmtProc<'a>(/// process /// Width pub usize); impl Disp for FmtProc<'_> { - fn out(&self, buf: &mut Vec, _conf: &Conf) -> usize { - let FmtProc(proc, indent, width) = *self; + fn out(&self, buf: &mut Vec, gc: &Conf) -> usize { + let FmtProc(Proc { cmdline, pid, .. }, indent, width) = *self; + let (cnt, clr) = (gc.cnt.val, gc.clr.val); // Skip path and interpreter from command line let mut cmdstart = 0; - if let Some(z1) = proc.cmdline.find('\0') { - if let Some(f1) = approx_filename(&proc.cmdline[..z1]) { + if let Some(z1) = cmdline.find('\0') { + if let Some(f1) = approx_filename(&cmdline[..z1]) { cmdstart = f1 + 1; } - if let Some(z2) = proc.cmdline[z1 + 1..].find('\0') { - if let Some(f2) = approx_filename(&proc.cmdline[z1 + 1..z1 + z2]) { + if let Some(z2) = cmdline[z1 + 1..].find('\0') { + if let Some(f2) = approx_filename(&cmdline[z1 + 1..z1 + z2]) { cmdstart = z1 + f2 + 2; } } } - let cmd = proc.cmdline[cmdstart..].replace(|c: char| c.is_control(), " "); + let cmd = cmdline[cmdstart..].replace(|c: char| c.is_control(), " "); let cmd = cmd.trim(); // Figure out how much space we have - let prefixlen = proc.pid.max(1).ilog10() as usize + 2 * indent + 1; - let cmdcap = width.saturating_sub(prefixlen + 1); + let pidlen = pid.max(&1).ilog10() as usize + 2 * indent + 1; + let cmdcap = width.saturating_sub(pidlen + 1); // Output it - let start = buf.len(); if cmdcap >= cmd.len() { - wtb!(buf, "{:prefixlen$} {}", proc.pid, cmd) + wtb!(buf, "{cnt}{pid:pidlen$}{clr} {cmd}"); + pidlen + 1 + cmd.len() } else if cmdcap > 3 { - wtb!(buf, "{:prefixlen$} ...{}", proc.pid, &cmd[(cmd.len() - cmdcap + 3)..]) + wtb!(buf, "{cnt}{pid:pidlen$}{clr} ...{}", &cmd[(cmd.len() - cmdcap + 3)..]); + pidlen + 1 + cmdcap } else { - wtb!(buf, "{:prefixlen$} ...", proc.pid) + wtb!(buf, "{cnt}{pid:pidlen$}{clr} ..."); + pidlen + 4 } - buf.len() - start } } @@ -242,7 +244,7 @@ mod tests { /// FmtProc should try shorten (elipsis at start) the command line when ther is no space #[test] fn proc_shorten() { - let conf = Conf::from_str(&format!("emlop p")); + let conf = Conf::from_str(&format!("emlop p --color=n")); let t: Vec<_> = vec![// Here we have enough space (1, "1", "1 1"), (1, "12", "1 12"), @@ -269,7 +271,7 @@ mod tests { /// FmtProc should rewrite commands #[test] fn proc_cmdline() { - let conf = Conf::from_str(&format!("emlop p")); + let conf = Conf::from_str(&format!("emlop p --color=n")); let t: Vec<_> = vec![("foo\0bar", "1 foo bar"), ("foo\0bar\0", "1 foo bar"), From feb30260b6eb558410d28c4cee13ee938aba0fb1 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Tue, 22 Oct 2024 13:29:03 +0100 Subject: [PATCH 14/29] qa: Move config structs, avoid a clone --- src/commands.rs | 36 +++++++++++++++++------------------- src/main.rs | 10 +++++----- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index c90b66d..60a0b27 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -6,13 +6,13 @@ use std::{collections::{BTreeMap, HashMap, HashSet}, /// Straightforward display of merge events /// /// We store the start times in a hashmap to compute/print the duration when we reach a stop event. -pub fn cmd_log(gc: &Conf, sc: &ConfLog) -> Result { +pub fn cmd_log(gc: Conf, sc: ConfLog) -> Result { let hist = get_hist(&gc.logfile, gc.from, gc.to, sc.show, &sc.search, sc.exact)?; let mut merges: HashMap = HashMap::new(); let mut unmerges: HashMap = HashMap::new(); let mut found = 0; let mut sync_start: Option = None; - let mut tbl = Table::new(gc).align_left(0).align_left(2).margin(2, " ").last(sc.last); + let mut tbl = Table::new(&gc).align_left(0).align_left(2).margin(2, " ").last(sc.last); tbl.header(["Date", "Duration", "Package/Repo"]); for p in hist { match p { @@ -134,11 +134,11 @@ impl Times { /// /// First loop is like cmd_list but we store the merge time for each ebuild instead of printing it. /// Then we compute the stats per ebuild, and print that. -pub fn cmd_stats(gc: &Conf, sc: &ConfStats) -> Result { +pub fn cmd_stats(gc: Conf, sc: ConfStats) -> Result { let hist = get_hist(&gc.logfile, gc.from, gc.to, sc.show, &sc.search, sc.exact)?; - let mut tbls = Table::new(gc).align_left(0).align_left(1).margin(1, " "); + let mut tbls = Table::new(&gc).align_left(0).align_left(1).margin(1, " "); tbls.header([sc.group.name(), "Repo", "Syncs", "Total time", "Predict time"]); - let mut tblp = Table::new(gc).align_left(0).align_left(1).margin(1, " "); + let mut tblp = Table::new(&gc).align_left(0).align_left(1).margin(1, " "); tblp.header([sc.group.name(), "Package", "Merges", @@ -147,7 +147,7 @@ pub fn cmd_stats(gc: &Conf, sc: &ConfStats) -> Result { "Unmerges", "Total time", "Predict time"]); - let mut tblt = Table::new(gc).align_left(0).margin(1, " "); + let mut tblt = Table::new(&gc).align_left(0).margin(1, " "); tblt.header([sc.group.name(), "Merges", "Total time", @@ -170,7 +170,7 @@ pub fn cmd_stats(gc: &Conf, sc: &ConfStats) -> Result { curts = t; } else if t > nextts { let group = sc.group.at(curts, gc.date_offset); - cmd_stats_group(gc, sc, &mut tbls, &mut tblp, &mut tblt, group, &sync_time, + cmd_stats_group(&gc, &sc, &mut tbls, &mut tblp, &mut tblt, group, &sync_time, &pkg_time); sync_time.clear(); pkg_time.clear(); @@ -214,7 +214,7 @@ pub fn cmd_stats(gc: &Conf, sc: &ConfStats) -> Result { } } let group = sc.group.at(curts, gc.date_offset); - cmd_stats_group(gc, sc, &mut tbls, &mut tblp, &mut tblt, group, &sync_time, &pkg_time); + cmd_stats_group(&gc, &sc, &mut tbls, &mut tblp, &mut tblt, group, &sync_time, &pkg_time); // Controlled drop to ensure table order and insert blank lines let (es, ep, et) = (!tbls.is_empty(), !tblp.is_empty(), !tblt.is_empty()); drop(tbls); @@ -303,15 +303,13 @@ fn proc_rows(now: i64, /// Predict future merge time /// /// Very similar to cmd_summary except we want total build time for a list of ebuilds. -pub fn cmd_predict(gc: &Conf, sc: &ConfPred) -> Result { +pub fn cmd_predict(gc: Conf, mut sc: ConfPred) -> Result { let now = epoch_now(); let last = if sc.show.tot { sc.last.saturating_add(1) } else { sc.last }; - let mut tbl = Table::new(gc).align_left(0).align_left(2).margin(2, " ").last(last); - // TODO: should be able to extend inside sc - let mut tmpdirs = sc.tmpdirs.clone(); + let mut tbl = Table::new(&gc).align_left(0).align_left(2).margin(2, " ").last(last); // Gather and print info about current merge process. - let einfo = get_emerge(&mut tmpdirs); + let einfo = get_emerge(&mut sc.tmpdirs); if einfo.roots.is_empty() && std::io::stdin().is_terminal() && matches!(sc.resume, ResumeKind::No | ResumeKind::Auto) @@ -321,7 +319,7 @@ pub fn cmd_predict(gc: &Conf, sc: &ConfPred) -> Result { } if sc.show.emerge { for p in einfo.roots { - proc_rows(now, &mut tbl, &einfo.procs, p, 0, sc); + proc_rows(now, &mut tbl, &einfo.procs, p, 0, &sc); } } @@ -400,7 +398,7 @@ pub fn cmd_predict(gc: &Conf, sc: &ConfPred) -> Result { // Done if sc.show.merge && totcount <= sc.first { if elapsed > 0 { - let stage = get_buildlog(&p, &tmpdirs).unwrap_or_default(); + let stage = get_buildlog(&p, &sc.tmpdirs).unwrap_or_default(); tbl.row([&[&gc.pkg, &p.ebuild_version()], &[&FmtDur(fmtpred)], &[&gc.clr, &"- ", &FmtDur(elapsed), &gc.clr, &stage]]); @@ -437,13 +435,13 @@ pub fn cmd_predict(gc: &Conf, sc: &ConfPred) -> Result { Ok(totcount > 0) } -pub fn cmd_accuracy(gc: &Conf, sc: &ConfAccuracy) -> Result { +pub fn cmd_accuracy(gc: Conf, sc: ConfAccuracy) -> Result { let hist = get_hist(&gc.logfile, gc.from, gc.to, Show::m(), &sc.search, sc.exact)?; let mut pkg_starts: HashMap = HashMap::new(); let mut pkg_times: BTreeMap = BTreeMap::new(); let mut pkg_errs: BTreeMap> = BTreeMap::new(); let mut found = false; - let mut tbl = Table::new(gc).align_left(0).align_left(1).last(sc.last); + let mut tbl = Table::new(&gc).align_left(0).align_left(1).last(sc.last); tbl.header(["Date", "Package", "Real", "Predicted", "Error"]); for p in hist { match p { @@ -487,7 +485,7 @@ pub fn cmd_accuracy(gc: &Conf, sc: &ConfAccuracy) -> Result { } drop(tbl); if sc.show.tot { - let mut tbl = Table::new(gc).align_left(0); + let mut tbl = Table::new(&gc).align_left(0); tbl.header(["Package", "Error"]); for (p, e) in pkg_errs { let avg = e.iter().sum::() / e.len() as f64; @@ -497,7 +495,7 @@ pub fn cmd_accuracy(gc: &Conf, sc: &ConfAccuracy) -> Result { Ok(found) } -pub fn cmd_complete(gc: &Conf, sc: &ConfComplete) -> Result { +pub fn cmd_complete(gc: Conf, sc: ConfComplete) -> Result { // Generate standard clap completions #[cfg(feature = "clap_complete")] if let Some(s) = &sc.shell { diff --git a/src/main.rs b/src/main.rs index 9012528..cb5b153 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,11 +13,11 @@ use std::str::FromStr; fn main() { let res = match Configs::load() { - Ok(Configs::Log(gc, sc)) => commands::cmd_log(&gc, &sc), - Ok(Configs::Stats(gc, sc)) => commands::cmd_stats(&gc, &sc), - Ok(Configs::Predict(gc, sc)) => commands::cmd_predict(&gc, &sc), - Ok(Configs::Accuracy(gc, sc)) => commands::cmd_accuracy(&gc, &sc), - Ok(Configs::Complete(gc, sc)) => commands::cmd_complete(&gc, &sc), + Ok(Configs::Log(gc, sc)) => commands::cmd_log(gc, sc), + Ok(Configs::Stats(gc, sc)) => commands::cmd_stats(gc, sc), + Ok(Configs::Predict(gc, sc)) => commands::cmd_predict(gc, sc), + Ok(Configs::Accuracy(gc, sc)) => commands::cmd_accuracy(gc, sc), + Ok(Configs::Complete(gc, sc)) => commands::cmd_complete(gc, sc), Err(e) => Err(e), }; match res { From 3498fd2f7639c2ccedaf7f37cfbf3b498ca2e3d5 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Wed, 23 Oct 2024 15:35:55 +0100 Subject: [PATCH 15/29] proces: Display number of skipped processes --- src/commands.rs | 53 ++++++++++++++++++++++++++++++++++++++++----- src/config.rs | 6 +++++ src/parse/proces.rs | 2 +- src/table.rs | 31 +++++++++++++------------- 4 files changed, 70 insertions(+), 22 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 60a0b27..7806221 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -284,18 +284,46 @@ fn cmd_stats_group(gc: &Conf, } } +/// Count processes in tree, including given proces +fn proc_count(procs: &BTreeMap, pid: pid_t) -> usize { + let mut count = 1; + for child in procs.iter().filter(|(_, p)| p.ppid == pid).map(|(pid, _)| pid) { + count += proc_count(procs, *child); + } + count +} + +/// Display proces tree fn proc_rows(now: i64, tbl: &mut Table<3>, procs: &BTreeMap, pid: pid_t, depth: usize, sc: &ConfPred) { + // This should always succeed because we're getting pid from procs, but to allow experiments we + // warn instead of panic/ignore. + let proc = match procs.get(&pid) { + Some(p) => p, + None => { + error!("Could not find proces {pid}"); + return; + }, + }; + // Print current level if depth < sc.pdepth { - if let Some(proc) = procs.get(&pid) { - tbl.row([&[&FmtProc(proc, depth, sc.pwidth)], &[&FmtDur(now - proc.start)], &[]]); - for child in procs.iter().filter(|(_, p)| p.ppid == pid).map(|(pid, _)| pid) { - proc_rows(now, tbl, procs, *child, depth + 1, sc) - } + tbl.row([&[&FmtProc(proc, depth, sc.pwidth)], &[&FmtDur(now - proc.start)], &[]]); + } + // Either recurse with children... + if depth + 1 < sc.pdepth { + for child in procs.iter().filter(|(_, p)| p.ppid == pid).map(|(pid, _)| pid) { + proc_rows(now, tbl, procs, *child, depth + 1, sc); + } + } + // ...or print skipped rows + else { + let children = proc_count(procs, pid) - 1; + if children > 0 { + tbl.row([&[&" ".repeat(depth + 1), &"(", &children, &" skipped)"], &[], &[]]); } } } @@ -523,9 +551,10 @@ pub fn cmd_complete(gc: Conf, sc: ConfComplete) -> Result { #[cfg(test)] mod tests { + use super::*; + #[test] fn averages() { - use super::Times; use crate::Average::*; for (a, m, wa, wm, lim, vals) in [(-1, -1, -1, -1, 10, vec![]), @@ -546,4 +575,16 @@ mod tests { assert_eq!(wm, t.pred(lim, WeightedMedian), "weighted median {lim} {vals:?}"); } } + + /// Shows the whole system's processes. + /// Mainly useful as an interactive test, use `cargo test -- --nocapture procs_pid1`. + #[test] + fn procs_pid1() { + let (gc, mut sc) = ConfPred::from_str("emlop p --pdepth 4"); + let mut tbl = Table::new(&gc).align_left(0).align_left(2).margin(2, " "); + let now = epoch_now(); + let einfo = get_emerge(&mut sc.tmpdirs); + proc_rows(now, &mut tbl, &einfo.procs, 1, 0, &sc); + println!("{}", tbl.to_string()); + } } diff --git a/src/config.rs b/src/config.rs index 0e2019d..d60e293 100644 --- a/src/config.rs +++ b/src/config.rs @@ -206,6 +206,12 @@ impl ConfPred { pwidth: sel!(cli, toml, predict, pwidth, 10..=1000, 60)? as usize, pdepth: sel!(cli, toml, predict, pdepth, 0..=100, 3)? as usize }) } + #[cfg(test)] + pub fn from_str(s: impl AsRef) -> (Conf, Self) { + let cli = cli::build_cli().get_matches_from(s.as_ref().split_whitespace()); + (Conf::try_new(&cli, &Toml::default()).unwrap(), + ConfPred::try_new(cli.subcommand().unwrap().1, &Toml::default()).unwrap()) + } } impl ConfStats { diff --git a/src/parse/proces.rs b/src/parse/proces.rs index 4d7a30b..ed3253f 100644 --- a/src/parse/proces.rs +++ b/src/parse/proces.rs @@ -243,7 +243,7 @@ mod tests { /// FmtProc should try shorten (elipsis at start) the command line when ther is no space #[test] - fn proc_shorten() { + fn proc_width() { let conf = Conf::from_str(&format!("emlop p --color=n")); let t: Vec<_> = vec![// Here we have enough space (1, "1", "1 1"), diff --git a/src/table.rs b/src/table.rs index 417fd4c..2863879 100644 --- a/src/table.rs +++ b/src/table.rs @@ -160,6 +160,14 @@ impl<'a, const N: usize> Table<'a, N> { out.write_all(self.conf.lineend).unwrap_or(()); } } + + #[cfg(test)] + pub fn to_string(mut self) -> String { + let mut out = Vec::with_capacity(self.buf.len()); + self.flush(&mut out); + self.rows.clear(); + String::from_utf8(out).expect("Non-utf8 table output") + } } impl Drop for Table<'_, N> { @@ -174,13 +182,6 @@ mod test { use super::*; use crate::parse::Ansi; - fn check(mut tbl: Table, expect: &str) { - let mut out = Vec::with_capacity(tbl.buf.len()); - tbl.flush(&mut out); - tbl.rows.clear(); - assert_eq!(expect, String::from_utf8(out).unwrap()); - } - #[test] fn last() { let conf = Conf::from_str("emlop log --color=n -H"); @@ -190,14 +191,14 @@ mod test { for i in 1..10 { t.row([&[&format!("{i}")]]); } - check(t, "1\n2\n3\n4\n5\n6\n7\n8\n9\n"); + assert_eq!(t.to_string(), "1\n2\n3\n4\n5\n6\n7\n8\n9\n"); // 5 max let mut t = Table::<1>::new(&conf).last(5); for i in 1..10 { t.row([&[&format!("{i}")]]); } - check(t, "5\n6\n7\n8\n9\n"); + assert_eq!(t.to_string(), "5\n6\n7\n8\n9\n"); // 5 max ignoring header let mut t = Table::new(&conf).last(5); @@ -205,7 +206,7 @@ mod test { for i in 1..10 { t.row([&[&format!("{i}")]]); } - check(t, "h\n5\n6\n7\n8\n9\n"); + assert_eq!(t.to_string(), "h\n5\n6\n7\n8\n9\n"); } #[test] @@ -218,7 +219,7 @@ mod test { let res = "short 1\n\ looooooooooooong 1\n\ high 9999\n"; - check(t, res); + assert_eq!(t.to_string(), res); } #[test] @@ -228,7 +229,7 @@ mod test { t.row([&[&"looooooooooooong"], &[&1]]); t.row([&[&"short"], &[&1]]); let res = "short 1\n"; - check(t, res); + assert_eq!(t.to_string(), res); } #[test] @@ -241,7 +242,7 @@ mod test { let res = "short\t1\n\ looooooooooooong\t1\n\ high\t9999\n"; - check(t, res); + assert_eq!(t.to_string(), res); } #[test] @@ -255,7 +256,7 @@ mod test { let (l1, l2) = res.split_once('\n').expect("two lines"); assert_eq!(Ansi::strip(l1, 100), "123 1"); assert_eq!(Ansi::strip(l1, 100), Ansi::strip(l2, 100)); - check(t, res); + assert_eq!(t.to_string(), res); } #[test] @@ -266,6 +267,6 @@ mod test { t.row([&[&conf.merge, &1, &conf.dur, &2, &conf.cnt, &3, &conf.clr], &[&1]]); let res = "123 1\n\ >>> 123 1\n"; - check(t, res); + assert_eq!(t.to_string(), res); } } From f24ac64f69eb35c6dbe34f9cf88e2e3eefad7ebf Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Wed, 23 Oct 2024 15:39:23 +0100 Subject: [PATCH 16/29] conf: Make display of skipped rows optional Making this a global option, as I want to eventually use that for --first/--last skips too. --- completion.bash | 22 +++++++++++----------- completion.fish | 1 + completion.zsh | 5 +++++ emlop.toml | 1 + src/commands.rs | 9 +++++---- src/config.rs | 3 +++ src/config/cli.rs | 12 ++++++++++++ src/config/toml.rs | 1 + src/parse/proces.rs | 3 +-- 9 files changed, 40 insertions(+), 17 deletions(-) diff --git a/completion.bash b/completion.bash index 1f18749..15f60b0 100644 --- a/completion.bash +++ b/completion.bash @@ -27,7 +27,7 @@ _emlop() { case "${cmd}" in emlop) - opts="log predict stats accuracy -f -t -H -o -F -v -h -V --from --to --header --duration --date --utc --color --output --logfile --help --version" + opts="log predict stats accuracy -f -t -H -o -F -v -h -V --from --to --header --elipsis --duration --date --utc --color --output --logfile --help --version" if [[ ${cur} == -* ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -36,7 +36,7 @@ _emlop() { --from|--to|-f|-t) COMPREPLY=($(compgen -W "1h 1d 1w 1m 1h $(date -Is)" "${cur}")) ;; - --header|-H) + --header|-H|--elipsis) COMPREPLY=($(compgen -W "yes no" "${cur}")) ;; --duration) @@ -64,7 +64,7 @@ _emlop() { return 0 ;; emlop__accuracy) - opts="[search]... -e -s -n -f -t -H -o -F -v -h --exact --show --last --avg --limit --from --to --header --duration --date --utc --color --output --logfile --help" + opts="[search]... -e -s -n -f -t -H -o -F -v -h --exact --show --last --avg --limit --from --to --header --elipsis --duration --date --utc --color --output --logfile --help" if [[ ${cur} == -* ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -73,7 +73,7 @@ _emlop() { --from|--to|-f|-t) COMPREPLY=($(compgen -W "1h 1d 1w 1m 1h $(date -Is)" "${cur}")) ;; - --header|-H) + --header|-H|--elipsis) COMPREPLY=($(compgen -W "yes no" "${cur}")) ;; --duration) @@ -117,7 +117,7 @@ _emlop() { return 0 ;; emlop__log) - opts=" [search]... -N -n -s -e -f -t -H -o -F -v -h --starttime --first --last --show --exact --from --to --header --duration --date --utc --color --output --logfile --help" + opts=" [search]... -N -n -s -e -f -t -H -o -F -v -h --starttime --first --last --show --exact --from --to --header --elipsis --duration --date --utc --color --output --logfile --help" if [[ ${cur} == -* ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -126,8 +126,8 @@ _emlop() { --from|--to|-f|-t) COMPREPLY=($(compgen -W "1h 1d 1w 1m 1h $(date -Is)" "${cur}")) ;; - --header|-H) - COMPREPLY=($(compgen -W "yes no ${opts}" "${cur}")) + --header|-H|--elipsis) + COMPREPLY=($(compgen -W "yes no" "${cur}")) ;; --duration) COMPREPLY=($(compgen -W "hms secs hmsfixed human" "${cur}")) @@ -167,7 +167,7 @@ _emlop() { return 0 ;; emlop__predict) - opts="-s -N -n -f -t -H -o -F -v -h --show --first --last --tmpdir --resume --unknown --avg --limit --from --to --header --duration --date --utc --color --output --pdepth --pwidth --logfile --help" + opts="-s -N -n -f -t -H -o -F -v -h --show --first --last --tmpdir --resume --unknown --avg --limit --from --to --header --elipsis --duration --date --utc --color --output --pdepth --pwidth --logfile --help" if [[ ${cur} == -* ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -176,7 +176,7 @@ _emlop() { --from|--to|-f|-t) COMPREPLY=($(compgen -W "1h 1d 1w 1m 1h $(date -Is)" "${cur}")) ;; - --header|-H) + --header|-H|--elipsis) COMPREPLY=($(compgen -W "yes no" "${cur}")) ;; --duration) @@ -231,7 +231,7 @@ _emlop() { return 0 ;; emlop__stats) - opts="[search]... -s -g -e -f -t -H -o -F -v -h --show --groupby --exact --avg --limit --from --to --header --duration --date --utc --color --output --logfile --help" + opts="[search]... -s -g -e -f -t -H -o -F -v -h --show --groupby --exact --avg --limit --from --to --header --elipsis --duration --date --utc --color --output --logfile --help" if [[ ${cur} == -* ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -240,7 +240,7 @@ _emlop() { --from|--to|-f|-t) COMPREPLY=($(compgen -W "1h 1d 1w 1m 1h $(date -Is)" "${cur}")) ;; - --header|-H) + --header|-H|--elipsis) COMPREPLY=($(compgen -W "yes no" "${cur}")) ;; --duration) diff --git a/completion.fish b/completion.fish index f2f5d8e..4c1a014 100644 --- a/completion.fish +++ b/completion.fish @@ -3,6 +3,7 @@ complete -c emlop -f complete -c emlop -s f -l from -d 'Only parse log entries after ' -x -a "{1y 'One year ago',1m 'One month ago',1w 'One week ago',1d 'One day ago',1h 'One hour ago',(date -Is) 'Exact date'}" complete -c emlop -s t -l to -d 'Only parse log entries before ' -x -a "{1y 'One year ago',1m 'One month ago',1w 'One week ago',1d 'One day ago',1h 'One hour ago',(date -Is) 'Exact date'}" complete -c emlop -s H -l header -d 'Show table header' -f -a "yes no" +complete -c emlop -l elipsis -d 'Show skipped rows' -f -a "yes no" complete -c emlop -l duration -d 'Output durations in different formats' -x -a "hms hmsfixed human secs" complete -c emlop -l date -d 'Output dates in different formats' -x -a "ymd ymdhms ymdhmso rfc3339 rfc2822 compact unix" complete -c emlop -l utc -d 'Parse/display dates in UTC instead of local time' -f -a "yes no" diff --git a/completion.zsh b/completion.zsh index f49a7f7..a62cd17 100644 --- a/completion.zsh +++ b/completion.zsh @@ -11,6 +11,7 @@ _emlop() { '--to=[Only parse log entries before ]:date: ' \ '-H+[Show table header]' \ '--header=[Show table header]' \ +'--elipsis=[Show skipped rows]' \ '--duration=[Output durations in different formats]:format: ' \ '--date=[Output dates in different formats]:format: ' \ '--utc=[Parse/display dates in UTC instead of local time]' \ @@ -47,6 +48,7 @@ _emlop() { '--to=[Only parse log entries before ]:date: ' \ '-H+[Show table header]' \ '--header=[Show table header]' \ +'--elipsis=[Show skipped rows]' \ '--duration=[Output durations in different formats]:format: ' \ '--date=[Output dates in different formats]:format: ' \ '--utc=[Parse/display dates in UTC instead of local time]' \ @@ -81,6 +83,7 @@ _emlop() { '--to=[Only parse log entries before ]:date: ' \ '-H+[Show table header]' \ '--header=[Show table header]' \ +'--elipsis=[Show skipped rows]' \ '--duration=[Output durations in different formats]:format: ' \ '--date=[Output dates in different formats]:format: ' \ '--utc=[Parse/display dates in UTC instead of local time]' \ @@ -109,6 +112,7 @@ _emlop() { '--to=[Only parse log entries before ]:date: ' \ '-H+[Show table header]' \ '--header=[Show table header]' \ +'--elipsis=[Show skipped rows]' \ '--duration=[Output durations in different formats]:format: ' \ '--date=[Output dates in different formats]:format: ' \ '--utc=[Parse/display dates in UTC instead of local time]' \ @@ -138,6 +142,7 @@ _emlop() { '--to=[Only parse log entries before ]:date: ' \ '-H+[Show table header]' \ '--header=[Show table header]' \ +'--elipsis=[Show skipped rows]' \ '--duration=[Output durations in different formats]:format: ' \ '--date=[Output dates in different formats]:format: ' \ '--utc=[Parse/display dates in UTC instead of local time]' \ diff --git a/emlop.toml b/emlop.toml index d70934e..6c80bbb 100644 --- a/emlop.toml +++ b/emlop.toml @@ -12,6 +12,7 @@ # header = true # color = "yes" # output = "columns" +# elipsis = true [log] # show = "mus" # starttime = true diff --git a/src/commands.rs b/src/commands.rs index 7806221..f8324ee 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -299,6 +299,7 @@ fn proc_rows(now: i64, procs: &BTreeMap, pid: pid_t, depth: usize, + gc: &Conf, sc: &ConfPred) { // This should always succeed because we're getting pid from procs, but to allow experiments we // warn instead of panic/ignore. @@ -316,11 +317,11 @@ fn proc_rows(now: i64, // Either recurse with children... if depth + 1 < sc.pdepth { for child in procs.iter().filter(|(_, p)| p.ppid == pid).map(|(pid, _)| pid) { - proc_rows(now, tbl, procs, *child, depth + 1, sc); + proc_rows(now, tbl, procs, *child, depth + 1, gc, sc); } } // ...or print skipped rows - else { + else if gc.elipsis { let children = proc_count(procs, pid) - 1; if children > 0 { tbl.row([&[&" ".repeat(depth + 1), &"(", &children, &" skipped)"], &[], &[]]); @@ -347,7 +348,7 @@ pub fn cmd_predict(gc: Conf, mut sc: ConfPred) -> Result { } if sc.show.emerge { for p in einfo.roots { - proc_rows(now, &mut tbl, &einfo.procs, p, 0, &sc); + proc_rows(now, &mut tbl, &einfo.procs, p, 0, &gc, &sc); } } @@ -584,7 +585,7 @@ mod tests { let mut tbl = Table::new(&gc).align_left(0).align_left(2).margin(2, " "); let now = epoch_now(); let einfo = get_emerge(&mut sc.tmpdirs); - proc_rows(now, &mut tbl, &einfo.procs, 1, 0, &sc); + proc_rows(now, &mut tbl, &einfo.procs, 1, 0, &gc, &sc); println!("{}", tbl.to_string()); } } diff --git a/src/config.rs b/src/config.rs index d60e293..9031d7f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -8,6 +8,7 @@ mod types; pub use crate::config::{cli::*, types::*}; use crate::{config::toml::Toml, parse::AnsiStr, *}; use clap::ArgMatches; +pub use libc::pid_t; use std::{io::IsTerminal, path::PathBuf}; /// Global config, one enum variant per command @@ -30,6 +31,7 @@ pub struct Conf { pub clr: AnsiStr, pub lineend: &'static [u8], pub header: bool, + pub elipsis: bool, pub dur_t: DurationStyle, pub date_offset: time::UtcOffset, pub date_fmt: DateStyle, @@ -163,6 +165,7 @@ impl Conf { clr: AnsiStr::from(if color { "\x1B[m" } else { "" }), lineend: if color { b"\x1B[m\n" } else { b"\n" }, header: sel!(cli, toml, header, (), false)?, + elipsis: sel!(cli, toml, elipsis, (), true)?, dur_t: sel!(cli, toml, duration, (), DurationStyle::Hms)?, date_offset: offset, date_fmt: sel!(cli, toml, date, (), DateStyle::default())?, diff --git a/src/config/cli.rs b/src/config/cli.rs index 7e3ae26..c4f94e9 100644 --- a/src/config/cli.rs +++ b/src/config/cli.rs @@ -266,6 +266,17 @@ pub fn build_cli() -> Command { (default)|auto|a: columns on tty, tab otherwise\n \ columns|c: space-aligned columns\n \ tab|t: tab-separated values"); + let elipsis = Arg::new("elipsis").long("elipsis") + .value_name("bool") + .global(true) + .num_args(..=1) + .default_missing_value("y") + .display_order(29) + .help_heading("Format") + .help("Show placeholder for skipped rows (yes/no)") + .long_help("Show placeholder for skipped rows (yes/no)\n \ + (empty)|yes|y: Show ' skipped' placeholder\n \ + no|n: Skip rows silently"); //////////////////////////////////////////////////////////// // Misc arguments @@ -406,6 +417,7 @@ pub fn build_cli() -> Command { .arg(output) .arg(logfile) .arg(verbose) + .arg(elipsis) .subcommand(cmd_log) .subcommand(cmd_pred) .subcommand(cmd_stats) diff --git a/src/config/toml.rs b/src/config/toml.rs index 23398c0..6124ab8 100644 --- a/src/config/toml.rs +++ b/src/config/toml.rs @@ -36,6 +36,7 @@ pub struct Toml { pub date: Option, pub duration: Option, pub header: Option, + pub elipsis: Option, pub utc: Option, pub color: Option, pub output: Option, diff --git a/src/parse/proces.rs b/src/parse/proces.rs index ed3253f..13a52d5 100644 --- a/src/parse/proces.rs +++ b/src/parse/proces.rs @@ -6,9 +6,8 @@ //! implementaion (does procinfo crate work on BSDs ?), but it's unit-tested against ps and should //! be fast. -use crate::{table::Disp, *}; +use crate::{config::*, table::Disp, *}; use anyhow::{ensure, Context}; -use libc::pid_t; use std::{collections::BTreeMap, fs::{read_dir, DirEntry, File}, io::prelude::*, From ab14f88fd81271415b1ad335ee276f493d9dacbe Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Fri, 25 Oct 2024 15:25:11 +0100 Subject: [PATCH 17/29] proces: Add unittest, refactor Basic indentation&skip unittest, plus some refactoring to decouple proces/current/command a bit. --- src/commands.rs | 44 ++++++++++++++++++++++++++++++++++++++------ src/config.rs | 1 - src/parse.rs | 4 +++- src/parse/current.rs | 13 +++++-------- src/parse/proces.rs | 23 +++++++++++++++++++---- 5 files changed, 65 insertions(+), 20 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index f8324ee..84b4822 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -285,7 +285,7 @@ fn cmd_stats_group(gc: &Conf, } /// Count processes in tree, including given proces -fn proc_count(procs: &BTreeMap, pid: pid_t) -> usize { +fn proc_count(procs: &ProcList, pid: pid_t) -> usize { let mut count = 1; for child in procs.iter().filter(|(_, p)| p.ppid == pid).map(|(pid, _)| pid) { count += proc_count(procs, *child); @@ -296,7 +296,7 @@ fn proc_count(procs: &BTreeMap, pid: pid_t) -> usize { /// Display proces tree fn proc_rows(now: i64, tbl: &mut Table<3>, - procs: &BTreeMap, + procs: &ProcList, pid: pid_t, depth: usize, gc: &Conf, @@ -338,7 +338,8 @@ pub fn cmd_predict(gc: Conf, mut sc: ConfPred) -> Result { let mut tbl = Table::new(&gc).align_left(0).align_left(2).margin(2, " ").last(last); // Gather and print info about current merge process. - let einfo = get_emerge(&mut sc.tmpdirs); + let procs = get_all_proc(&mut sc.tmpdirs); + let einfo = get_emerge(&procs); if einfo.roots.is_empty() && std::io::stdin().is_terminal() && matches!(sc.resume, ResumeKind::No | ResumeKind::Auto) @@ -348,7 +349,7 @@ pub fn cmd_predict(gc: Conf, mut sc: ConfPred) -> Result { } if sc.show.emerge { for p in einfo.roots { - proc_rows(now, &mut tbl, &einfo.procs, p, 0, &gc, &sc); + proc_rows(now, &mut tbl, &procs, p, 0, &gc, &sc); } } @@ -553,6 +554,7 @@ pub fn cmd_complete(gc: Conf, sc: ConfComplete) -> Result { #[cfg(test)] mod tests { use super::*; + use crate::parse::procs; #[test] fn averages() { @@ -584,8 +586,38 @@ mod tests { let (gc, mut sc) = ConfPred::from_str("emlop p --pdepth 4"); let mut tbl = Table::new(&gc).align_left(0).align_left(2).margin(2, " "); let now = epoch_now(); - let einfo = get_emerge(&mut sc.tmpdirs); - proc_rows(now, &mut tbl, &einfo.procs, 1, 0, &gc, &sc); + let procs = get_all_proc(&mut sc.tmpdirs); + proc_rows(now, &mut tbl, &procs, 1, 0, &gc, &sc); println!("{}", tbl.to_string()); } + + /// Check indentation and skipping + #[test] + fn procs_hierarchy() { + let (gc, sc) = ConfPred::from_str("emlop p --pdepth 3 --color=n"); + let mut tbl = Table::new(&gc).align_left(0).align_left(2).margin(2, " "); + let procs = procs(&[(ProcKind::Other, "a", 1, 0), + (ProcKind::Other, "a.a", 2, 1), + (ProcKind::Other, "a.b", 3, 1), + (ProcKind::Other, "a.a.a", 4, 2), + (ProcKind::Other, "a.a.b", 5, 2), + (ProcKind::Other, "a.b.a", 6, 3), + // basic skip + (ProcKind::Other, "a.a.a.a", 7, 4), + // nested/sibling skip + (ProcKind::Other, "a.a.b.a", 8, 5), + (ProcKind::Other, "a.a.b.a.a", 9, 8), + (ProcKind::Other, "a.a.b.b", 10, 5)]); + let out = r#"1 a 9 + 2 a.a 8 + 4 a.a.a 6 + (1 skipped) + 5 a.a.b 5 + (3 skipped) + 3 a.b 7 + 6 a.b.a 4 +"#; + proc_rows(10, &mut tbl, &procs, 1, 0, &gc, &sc); + assert_eq!(tbl.to_string(), out); + } } diff --git a/src/config.rs b/src/config.rs index 9031d7f..33b33f0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -8,7 +8,6 @@ mod types; pub use crate::config::{cli::*, types::*}; use crate::{config::toml::Toml, parse::AnsiStr, *}; use clap::ArgMatches; -pub use libc::pid_t; use std::{io::IsTerminal, path::PathBuf}; /// Global config, one enum variant per command diff --git a/src/parse.rs b/src/parse.rs index 477ad2d..fb5fd3b 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -6,4 +6,6 @@ mod proces; pub use ansi::{Ansi, AnsiStr}; pub use current::{get_buildlog, get_emerge, get_pretend, get_resume, Pkg}; pub use history::{get_hist, Hist}; -pub use proces::{get_all_proc, FmtProc, Proc, ProcKind}; +#[cfg(test)] +pub use proces::tests::procs; +pub use proces::{get_all_proc, FmtProc, ProcKind, ProcList}; diff --git a/src/parse/current.rs b/src/parse/current.rs index 4d4e13c..bb5a308 100644 --- a/src/parse/current.rs +++ b/src/parse/current.rs @@ -1,14 +1,13 @@ //! Handles parsing of current emerge state. -use super::{get_all_proc, Ansi, Proc, ProcKind}; +use super::{Ansi, ProcKind, ProcList}; use crate::ResumeKind; use libc::pid_t; use log::*; use regex::Regex; use serde::Deserialize; use serde_json::from_reader; -use std::{collections::BTreeMap, - fs::File, +use std::{fs::File, io::{BufRead, BufReader, Read}, path::PathBuf}; @@ -137,7 +136,6 @@ fn read_buildlog(file: File, max: usize) -> String { #[derive(Debug)] pub struct EmergeInfo { pub start: i64, - pub procs: BTreeMap, pub roots: Vec, pub pkgs: Vec, } @@ -150,10 +148,9 @@ pub struct EmergeInfo { /// [app-portage/dummybuild-0.1.600] sandbox /usr/lib/portage/python3.11/ebuild.sh unpack /// gives us the actually emerging ebuild and stage (depends on portage FEATURES=sandbox, which /// should be the case for almost all users) -pub fn get_emerge(tmpdirs: &mut Vec) -> EmergeInfo { - let procs = get_all_proc(tmpdirs); - let mut res = EmergeInfo { start: i64::MAX, procs, roots: vec![], pkgs: vec![] }; - for (pid, proc) in &res.procs { +pub fn get_emerge(procs: &ProcList) -> EmergeInfo { + let mut res = EmergeInfo { start: i64::MAX, roots: vec![], pkgs: vec![] }; + for (pid, proc) in procs { match proc.kind { ProcKind::Emerge => { res.start = std::cmp::min(res.start, proc.start); diff --git a/src/parse/proces.rs b/src/parse/proces.rs index 13a52d5..72f056b 100644 --- a/src/parse/proces.rs +++ b/src/parse/proces.rs @@ -8,12 +8,13 @@ use crate::{config::*, table::Disp, *}; use anyhow::{ensure, Context}; +use libc::pid_t; use std::{collections::BTreeMap, fs::{read_dir, DirEntry, File}, io::prelude::*, path::PathBuf}; -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] pub enum ProcKind { Emerge, Python, @@ -138,14 +139,16 @@ fn extend_tmpdirs(proc: PathBuf, tmpdirs: &mut Vec) { } } +pub type ProcList = BTreeMap; + /// Get command name, arguments, start time, and pid for all processes. -pub fn get_all_proc(tmpdirs: &mut Vec) -> BTreeMap { +pub fn get_all_proc(tmpdirs: &mut Vec) -> ProcList { get_all_proc_result(tmpdirs).unwrap_or_else(|e| { log_err(e); BTreeMap::new() }) } -fn get_all_proc_result(tmpdirs: &mut Vec) -> Result, Error> { +fn get_all_proc_result(tmpdirs: &mut Vec) -> Result { // clocktick and time_ref are needed to interpret stat.start_time. time_ref should correspond to // the system boot time; not sure why it doesn't, but it's still usable as a reference. // SAFETY: returns a system constant, only failure mode should be a zero/negative value @@ -169,12 +172,24 @@ fn get_all_proc_result(tmpdirs: &mut Vec) -> Result ProcList { + BTreeMap::from_iter(procs.into_iter().map(|p| { + (p.2, + Proc { kind: p.0, + cmdline: p.1.into(), + start: p.2 as i64, + pid: p.2, + ppid: p.3 }) + })) + } + fn parse_ps_time(s: &str) -> i64 { let fmt = format_description!("[month repr:short] [day padding:space] [hour]:[minute]:[second] [year]"); PrimitiveDateTime::parse(s, &fmt).expect(&format!("Cannot parse {}", s)) From 780cebbec517dee1b8cb7e31859601bba04f360d Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Fri, 25 Oct 2024 15:27:11 +0100 Subject: [PATCH 18/29] predict: Make sure we don't display a redundant emerge root proces There's a small time window during which `emerge` might spawn `emerge`, so it looks like we have two running. This is a longstanding emlop bug, but it's much more noticeable now that we display the proces hierarchy. Fix this by removing roots if their (grand)parent is also a root. Added unittest (which prompted the previous refactorings). --- src/parse/current.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/parse/current.rs b/src/parse/current.rs index bb5a308..5d569a7 100644 --- a/src/parse/current.rs +++ b/src/parse/current.rs @@ -168,6 +168,21 @@ pub fn get_emerge(procs: &ProcList) -> EmergeInfo { ProcKind::Other => (), } } + // Remove roots that are (grand)children of another root + if res.roots.len() > 1 { + let origroots = res.roots.clone(); + res.roots.retain(|&r| { + let mut proc = procs.get(&r).expect("Root not in ProcList"); + while let Some(p) = procs.get(&proc.ppid) { + if origroots.contains(&p.pid) { + debug!("Skipping proces {}: grandchild of {}", r, p.pid); + return false; + } + proc = p; + } + true + }); + } trace!("{:?}", res); res } @@ -175,6 +190,7 @@ pub fn get_emerge(procs: &ProcList) -> EmergeInfo { #[cfg(test)] mod tests { use super::*; + use crate::parse::procs; /// Check that `get_pretend()` has the expected output fn check_pretend(file: &str, expect: &[(&str, &str)]) { @@ -247,4 +263,18 @@ mod tests { assert_eq!(format!(" ({res})"), read_buildlog(f, lim)); } } + + /// Check that get_emerge() finds the expected roots + #[test] + fn get_emerge_roots() { + let _ = env_logger::try_init(); + let procs = procs(&[(ProcKind::Emerge, "a", 1, 0), + (ProcKind::Other, "a.a", 2, 1), + (ProcKind::Emerge, "a.a.b", 3, 2), + (ProcKind::Other, "b", 4, 0), + (ProcKind::Emerge, "b.a", 5, 4), + (ProcKind::Other, "b.a.a", 6, 5)]); + let einfo = get_emerge(&procs); + assert_eq!(einfo.roots, vec![1, 5]); + } } From bbca6b472b99f193a395c895560632be75542a18 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Sat, 26 Oct 2024 12:20:11 +0100 Subject: [PATCH 19/29] proces: Colorize 'skip' row --- src/commands.rs | 4 +++- src/config.rs | 4 +++- src/table.rs | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 84b4822..c819fc8 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -324,7 +324,9 @@ fn proc_rows(now: i64, else if gc.elipsis { let children = proc_count(procs, pid) - 1; if children > 0 { - tbl.row([&[&" ".repeat(depth + 1), &"(", &children, &" skipped)"], &[], &[]]); + tbl.row([&[&" ".repeat(depth + 1), &gc.skip, &"(", &children, &" skipped)"], + &[], + &[]]); } } } diff --git a/src/config.rs b/src/config.rs index 33b33f0..9879d43 100644 --- a/src/config.rs +++ b/src/config.rs @@ -27,6 +27,7 @@ pub struct Conf { pub unmerge: AnsiStr, pub dur: AnsiStr, pub cnt: AnsiStr, + pub skip: AnsiStr, pub clr: AnsiStr, pub lineend: &'static [u8], pub header: bool, @@ -160,7 +161,8 @@ impl Conf { merge: AnsiStr::from(if color { "\x1B[1;32m" } else { ">>> " }), unmerge: AnsiStr::from(if color { "\x1B[1;31m" } else { "<<< " }), dur: AnsiStr::from(if color { "\x1B[1;35m" } else { "" }), - cnt: AnsiStr::from(if color { "\x1B[2;33m" } else { "" }), + skip: AnsiStr::from(if color { "\x1B[37m" } else { "" }), + cnt: AnsiStr::from(if color { "\x1B[33m" } else { "" }), clr: AnsiStr::from(if color { "\x1B[m" } else { "" }), lineend: if color { b"\x1B[m\n" } else { b"\n" }, header: sel!(cli, toml, header, (), false)?, diff --git a/src/table.rs b/src/table.rs index 2863879..e867413 100644 --- a/src/table.rs +++ b/src/table.rs @@ -252,7 +252,7 @@ mod test { t.row([&[&"123"], &[&1]]); t.row([&[&conf.merge, &1, &conf.dur, &2, &conf.cnt, &3, &conf.clr], &[&1]]); let res = "123 1\x1B[m\n\ - \x1B[1;32m1\x1B[1;35m2\x1B[2;33m3\x1B[m 1\x1B[m\n"; + \x1B[1;32m1\x1B[1;35m2\x1B[33m3\x1B[m 1\x1B[m\n"; let (l1, l2) = res.split_once('\n').expect("two lines"); assert_eq!(Ansi::strip(l1, 100), "123 1"); assert_eq!(Ansi::strip(l1, 100), Ansi::strip(l2, 100)); From 248e28f9f7d27cee9a7efc3a702afbc8bc6d7bf8 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Mon, 28 Oct 2024 17:51:13 +0000 Subject: [PATCH 20/29] proces: Fix parsing of empty argument --- src/parse/proces.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/parse/proces.rs b/src/parse/proces.rs index 72f056b..13207a1 100644 --- a/src/parse/proces.rs +++ b/src/parse/proces.rs @@ -57,7 +57,7 @@ impl Disp for FmtProc<'_> { cmdstart = f1 + 1; } if let Some(z2) = cmdline[z1 + 1..].find('\0') { - if let Some(f2) = approx_filename(&cmdline[z1 + 1..z1 + z2]) { + if let Some(f2) = approx_filename(&cmdline[z1 + 1..z1 + 1 + z2]) { cmdstart = z1 + f2 + 2; } } @@ -295,7 +295,8 @@ pub mod tests { ("/usr/bin/bash\0/path/to/toto\0--arg", "1 toto --arg"), ("bash\0/usr/lib/portage/python3.12/ebuild.sh\0unpack\0", "1 ebuild.sh unpack"), ("[foo/bar-0.1.600] sandbox\0/path/to/ebuild.sh\0unpack\0", "1 ebuild.sh unpack"), - ("[foo/bar-0.1.600] sandbox\0blah\0", "1 [foo/bar-0.1.600] sandbox blah"),]; + ("[foo/bar-0.1.600] sandbox\0blah\0", "1 [foo/bar-0.1.600] sandbox blah"), + ("/bin/foo\0\0", "1 foo")]; for (cmd, out) in t.into_iter() { let mut buf = vec![]; let p = Proc { kind: ProcKind::Other, pid: 1, ppid: 1, cmdline: cmd.into(), start: 0 }; From 5c34d9e3bfa4a3ce8586dc5f9e68627256102997 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Mon, 28 Oct 2024 18:16:45 +0000 Subject: [PATCH 21/29] test: Make proc_hierarchy work in CI --- src/commands.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands.rs b/src/commands.rs index c819fc8..1a352bf 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -596,7 +596,7 @@ mod tests { /// Check indentation and skipping #[test] fn procs_hierarchy() { - let (gc, sc) = ConfPred::from_str("emlop p --pdepth 3 --color=n"); + let (gc, sc) = ConfPred::from_str("emlop p --pdepth 3 --color=n --output=c"); let mut tbl = Table::new(&gc).align_left(0).align_left(2).margin(2, " "); let procs = procs(&[(ProcKind::Other, "a", 1, 0), (ProcKind::Other, "a.a", 2, 1), From ba073ee05188580cf595119f8d5063393b3ae582 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Mon, 28 Oct 2024 18:17:03 +0000 Subject: [PATCH 22/29] docs: Update changelog --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1474cde..7d9326d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +# unreleased + +## New features + +* `predict` now displays emerge proces tree instead of just top proces + - Bevahior configurable with `--pdepth`, `--pwidth`, `--elipsis` + - Format is a bit nicer and more colorful + +## Bug fixes + +* Don't display child emerge processes as root ones +* Fix off by one upper bound for some cli args +* Allow alignment of wider columns + # 0.7.1 2024-09-30 Maintenance release. From baeb5126df2c7beddc77a08f84c088bcd4c727c0 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Tue, 29 Oct 2024 12:30:22 +0000 Subject: [PATCH 23/29] table: Refactor header handling By storing the header as its own member instead of a row. This simplifies the logic a bit (might even be a micro perf win), but more importantly will simplify inserting a `skipped` row between the header and the actual rows. Added a unittest to make sure we handle the header's width. As a bonus, newer clippy versions don't complain about needless_range_loop, so remove the `allow`. --- src/table.rs | 119 +++++++++++++++++++++++++++++---------------------- 1 file changed, 68 insertions(+), 51 deletions(-) diff --git a/src/table.rs b/src/table.rs index e867413..1ca7bae 100644 --- a/src/table.rs +++ b/src/table.rs @@ -30,8 +30,8 @@ pub struct Table<'a, const N: usize> { buf: Vec, /// Visible length, and start/stop index into buffer rows: VecDeque<[(usize, usize, usize); N]>, - /// Whether a header has been set - have_header: bool, + /// Table header + header: Option<[(usize, usize, usize); N]>, /// Main config conf: &'a Conf, @@ -49,49 +49,48 @@ impl<'a, const N: usize> Table<'a, N> { Self { rows: VecDeque::with_capacity(32), buf: Vec::with_capacity(1024), conf, - have_header: false, + header: None, aligns: [Align::Right; N], margins: [" "; N], last: usize::MAX } } + /// Specify column alignment pub const fn align_left(mut self, col: usize) -> Self { self.aligns[col] = Align::Left; self } + /// Specify column left margin (1st printted column never has a left margin) pub const fn margin(mut self, col: usize, margin: &'static str) -> Self { self.margins[col] = margin; self } + /// Specify column left margin (1st printted column never has a left margin) pub const fn last(mut self, last: usize) -> Self { self.last = last; self } + /// Add a section header pub fn header(&mut self, row: [&str; N]) { if self.conf.header { - self.last = self.last.saturating_add(1); - self.have_header = true; - let mut idxrow = [(0, 0, 0); N]; for i in 0..N { let start = self.buf.len(); self.buf.extend(row[i].as_bytes()); idxrow[i] = (row[i].len(), start, self.buf.len()); } - self.rows.push_back(idxrow); + self.header = Some(idxrow); } } + /// Is there actual data to flush ? pub fn is_empty(&self) -> bool { - if self.have_header { - self.rows.len() == 1 - } else { - self.rows.is_empty() - } + self.rows.is_empty() } + /// Add one row of data /// /// The number of cells is set by const generic. @@ -105,9 +104,6 @@ impl<'a, const N: usize> Table<'a, N> { } self.rows.push_back(idxrow); if self.rows.len() > self.last { - if self.have_header { - self.rows.swap(0, 1); - } self.rows.pop_front(); } } @@ -117,48 +113,56 @@ impl<'a, const N: usize> Table<'a, N> { return; } // Check the max len of each column, for the rows we have - let widths: [usize; N] = - std::array::from_fn(|i| self.rows.iter().fold(0, |m, r| usize::max(m, r[i].0))); + let widths: [usize; N] = std::array::from_fn(|i| { + self.rows.iter().chain(self.header.iter()).fold(0, |m, r| usize::max(m, r[i].0)) + }); + if let Some(h) = self.header { + self.flush_one(&mut out, widths, &h); + } for row in &self.rows { - let mut first = true; - // Clippy suggests `for (i, ) in row.iter().enumerate().take(N)` which IMHO - // doesn't make sense here. - #[allow(clippy::needless_range_loop)] - for i in 0..N { - // Skip fully-empty columns - if widths[i] == 0 { - continue; + self.flush_one(&mut out, widths, row); + } + } + + fn flush_one(&self, + out: &mut impl std::io::Write, + widths: [usize; N], + row: &[(usize, usize, usize); N]) { + let mut first = true; + for i in 0..N { + // Skip fully-empty columns + if widths[i] == 0 { + continue; + } + let (len, pos0, pos1) = row[i]; + if self.conf.out == OutStyle::Tab { + if !first { + out.write_all(b"\t").unwrap_or(()); + } + out.write_all(&self.buf[pos0..pos1]).unwrap_or(()); + } else { + // Space between columns + if !first { + out.write_all(self.margins[i].as_bytes()).unwrap_or(()); } - let (len, pos0, pos1) = row[i]; - if self.conf.out == OutStyle::Tab { - if !first { - out.write_all(b"\t").unwrap_or(()); - } - out.write_all(&self.buf[pos0..pos1]).unwrap_or(()); - } else { - // Space between columns - if !first { - out.write_all(self.margins[i].as_bytes()).unwrap_or(()); - } - // Write the cell with alignment - let pad = &SPACES[0..usize::min(SPACES.len(), widths[i] - len)]; - match self.aligns[i] { - Align::Right => { + // Write the cell with alignment + let pad = &SPACES[0..usize::min(SPACES.len(), widths[i] - len)]; + match self.aligns[i] { + Align::Right => { + out.write_all(pad).unwrap_or(()); + out.write_all(&self.buf[pos0..pos1]).unwrap_or(()); + }, + Align::Left => { + out.write_all(&self.buf[pos0..pos1]).unwrap_or(()); + if i < N - 1 { out.write_all(pad).unwrap_or(()); - out.write_all(&self.buf[pos0..pos1]).unwrap_or(()); - }, - Align::Left => { - out.write_all(&self.buf[pos0..pos1]).unwrap_or(()); - if i < N - 1 { - out.write_all(pad).unwrap_or(()); - } - }, - } + } + }, } - first = false; } - out.write_all(self.conf.lineend).unwrap_or(()); + first = false; } + out.write_all(self.conf.lineend).unwrap_or(()); } #[cfg(test)] @@ -222,6 +226,19 @@ mod test { assert_eq!(t.to_string(), res); } + #[test] + fn align_longheader() { + let conf = Conf::from_str("emlop log --color=n --output=c -H"); + let mut t = Table::<2>::new(&conf).align_left(0); + t.header(["heeeeeeeader", "d"]); + t.row([&[&"short"], &[&1]]); + t.row([&[&"high"], &[&9999]]); + let res = "heeeeeeeader d\n\ + short 1\n\ + high 9999\n"; + assert_eq!(t.to_string(), res); + } + #[test] fn align_cols_last() { let conf = Conf::from_str("emlop log --color=n --output=c"); From cda22170d61b02f92329ca1295efc8965c87eaf8 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Tue, 29 Oct 2024 12:59:26 +0000 Subject: [PATCH 24/29] table: Refactor header() to consume self This makes it a bit clearer API-wise that tables can only have one header. --- src/commands.rs | 50 ++++++++++++++++++++++++------------------------- src/table.rs | 9 ++++----- 2 files changed, 29 insertions(+), 30 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 1a352bf..99e4d4d 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -12,8 +12,9 @@ pub fn cmd_log(gc: Conf, sc: ConfLog) -> Result { let mut unmerges: HashMap = HashMap::new(); let mut found = 0; let mut sync_start: Option = None; - let mut tbl = Table::new(&gc).align_left(0).align_left(2).margin(2, " ").last(sc.last); - tbl.header(["Date", "Duration", "Package/Repo"]); + let h = ["Date", "Duration", "Package/Repo"]; + let mut tbl = + Table::new(&gc).align_left(0).align_left(2).margin(2, " ").last(sc.last).header(h); for p in hist { match p { Hist::MergeStart { ts, key, .. } => { @@ -136,25 +137,25 @@ impl Times { /// Then we compute the stats per ebuild, and print that. pub fn cmd_stats(gc: Conf, sc: ConfStats) -> Result { let hist = get_hist(&gc.logfile, gc.from, gc.to, sc.show, &sc.search, sc.exact)?; - let mut tbls = Table::new(&gc).align_left(0).align_left(1).margin(1, " "); - tbls.header([sc.group.name(), "Repo", "Syncs", "Total time", "Predict time"]); - let mut tblp = Table::new(&gc).align_left(0).align_left(1).margin(1, " "); - tblp.header([sc.group.name(), - "Package", - "Merges", - "Total time", - "Predict time", - "Unmerges", - "Total time", - "Predict time"]); - let mut tblt = Table::new(&gc).align_left(0).margin(1, " "); - tblt.header([sc.group.name(), - "Merges", - "Total time", - "Average time", - "Unmerges", - "Total time", - "Average time"]); + let h = [sc.group.name(), "Repo", "Syncs", "Total time", "Predict time"]; + let mut tbls = Table::new(&gc).align_left(0).align_left(1).margin(1, " ").header(h); + let h = [sc.group.name(), + "Package", + "Merges", + "Total time", + "Predict time", + "Unmerges", + "Total time", + "Predict time"]; + let mut tblp = Table::new(&gc).align_left(0).align_left(1).margin(1, " ").header(h); + let h = [sc.group.name(), + "Merges", + "Total time", + "Average time", + "Unmerges", + "Total time", + "Average time"]; + let mut tblt = Table::new(&gc).align_left(0).margin(1, " ").header(h); let mut merge_start: HashMap = HashMap::new(); let mut unmerge_start: HashMap = HashMap::new(); let mut pkg_time: BTreeMap = BTreeMap::new(); @@ -473,8 +474,8 @@ pub fn cmd_accuracy(gc: Conf, sc: ConfAccuracy) -> Result { let mut pkg_times: BTreeMap = BTreeMap::new(); let mut pkg_errs: BTreeMap> = BTreeMap::new(); let mut found = false; - let mut tbl = Table::new(&gc).align_left(0).align_left(1).last(sc.last); - tbl.header(["Date", "Package", "Real", "Predicted", "Error"]); + let h = ["Date", "Package", "Real", "Predicted", "Error"]; + let mut tbl = Table::new(&gc).align_left(0).align_left(1).last(sc.last).header(h); for p in hist { match p { Hist::MergeStart { ts, key, .. } => { @@ -517,8 +518,7 @@ pub fn cmd_accuracy(gc: Conf, sc: ConfAccuracy) -> Result { } drop(tbl); if sc.show.tot { - let mut tbl = Table::new(&gc).align_left(0); - tbl.header(["Package", "Error"]); + let mut tbl = Table::new(&gc).align_left(0).header(["Package", "Error"]); for (p, e) in pkg_errs { let avg = e.iter().sum::() / e.len() as f64; tbl.row([&[&gc.pkg, &p], &[&gc.cnt, &format!("{avg:.1}%")]]); diff --git a/src/table.rs b/src/table.rs index 1ca7bae..cb29f91 100644 --- a/src/table.rs +++ b/src/table.rs @@ -74,7 +74,7 @@ impl<'a, const N: usize> Table<'a, N> { } /// Add a section header - pub fn header(&mut self, row: [&str; N]) { + pub fn header(mut self, row: [&str; N]) -> Self { if self.conf.header { let mut idxrow = [(0, 0, 0); N]; for i in 0..N { @@ -84,6 +84,7 @@ impl<'a, const N: usize> Table<'a, N> { } self.header = Some(idxrow); } + self } /// Is there actual data to flush ? @@ -205,8 +206,7 @@ mod test { assert_eq!(t.to_string(), "5\n6\n7\n8\n9\n"); // 5 max ignoring header - let mut t = Table::new(&conf).last(5); - t.header(["h"]); + let mut t = Table::new(&conf).last(5).header(["h"]); for i in 1..10 { t.row([&[&format!("{i}")]]); } @@ -229,8 +229,7 @@ mod test { #[test] fn align_longheader() { let conf = Conf::from_str("emlop log --color=n --output=c -H"); - let mut t = Table::<2>::new(&conf).align_left(0); - t.header(["heeeeeeeader", "d"]); + let mut t = Table::<2>::new(&conf).align_left(0).header(["heeeeeeeader", "d"]); t.row([&[&"short"], &[&1]]); t.row([&[&"high"], &[&9999]]); let res = "heeeeeeeader d\n\ From 108c1eb42032ed8466712108c93301321aa29908 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Tue, 29 Oct 2024 13:02:10 +0000 Subject: [PATCH 25/29] table: Show elipsis for rows before `--last` --- src/table.rs | 45 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/src/table.rs b/src/table.rs index cb29f91..f74c7ce 100644 --- a/src/table.rs +++ b/src/table.rs @@ -32,6 +32,8 @@ pub struct Table<'a, const N: usize> { rows: VecDeque<[(usize, usize, usize); N]>, /// Table header header: Option<[(usize, usize, usize); N]>, + /// Number of rows skipped to print only the last N + skip: usize, /// Main config conf: &'a Conf, @@ -48,6 +50,7 @@ impl<'a, const N: usize> Table<'a, N> { pub fn new(conf: &'a Conf) -> Table<'a, N> { Self { rows: VecDeque::with_capacity(32), buf: Vec::with_capacity(1024), + skip: 0, conf, header: None, aligns: [Align::Right; N], @@ -105,6 +108,7 @@ impl<'a, const N: usize> Table<'a, N> { } self.rows.push_back(idxrow); if self.rows.len() > self.last { + self.skip += 1; self.rows.pop_front(); } } @@ -113,13 +117,21 @@ impl<'a, const N: usize> Table<'a, N> { if self.is_empty() { return; } - // Check the max len of each column, for the rows we have + // Check the max len of each column, for the header+rows we have let widths: [usize; N] = std::array::from_fn(|i| { self.rows.iter().chain(self.header.iter()).fold(0, |m, r| usize::max(m, r[i].0)) }); + // Show header if let Some(h) = self.header { self.flush_one(&mut out, widths, &h); } + // Show skip row. Note that it doesn't participate to column alignment. + if self.conf.elipsis && self.skip > 0 { + writeln!(out, + "{}(skipping {} due to --last){}", + self.conf.skip.val, self.skip, self.conf.clr.val).unwrap_or(()); + } + // Show remaining rows for row in &self.rows { self.flush_one(&mut out, widths, row); } @@ -189,30 +201,49 @@ mod test { #[test] fn last() { - let conf = Conf::from_str("emlop log --color=n -H"); + let conf = Conf::from_str("emlop log --color=n -H --elipsis=n"); // No limit let mut t = Table::<1>::new(&conf); - for i in 1..10 { + for i in 0..10 { t.row([&[&format!("{i}")]]); } - assert_eq!(t.to_string(), "1\n2\n3\n4\n5\n6\n7\n8\n9\n"); + assert_eq!(t.to_string(), "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n"); // 5 max let mut t = Table::<1>::new(&conf).last(5); - for i in 1..10 { + for i in 0..10 { t.row([&[&format!("{i}")]]); } assert_eq!(t.to_string(), "5\n6\n7\n8\n9\n"); // 5 max ignoring header let mut t = Table::new(&conf).last(5).header(["h"]); - for i in 1..10 { + for i in 0..10 { t.row([&[&format!("{i}")]]); } assert_eq!(t.to_string(), "h\n5\n6\n7\n8\n9\n"); } + #[test] + fn last_elipsis() { + let conf = Conf::from_str("emlop log --color=n -H --elipsis=y"); + + // 5 max + let mut t = Table::<1>::new(&conf).last(5); + for i in 0..10 { + t.row([&[&format!("{i}")]]); + } + assert_eq!(t.to_string(), "(skipping 5 due to --last)\n5\n6\n7\n8\n9\n"); + + // 5 max ignoring header + let mut t = Table::new(&conf).last(5).header(["h"]); + for i in 0..10 { + t.row([&[&format!("{i}")]]); + } + assert_eq!(t.to_string(), "h\n(skipping 5 due to --last)\n5\n6\n7\n8\n9\n"); + } + #[test] fn align_cols() { let conf = Conf::from_str("emlop log --color=n --output=c"); @@ -240,7 +271,7 @@ mod test { #[test] fn align_cols_last() { - let conf = Conf::from_str("emlop log --color=n --output=c"); + let conf = Conf::from_str("emlop log --color=n --output=c --elipsis=n"); let mut t = Table::<2>::new(&conf).align_left(0).last(1); t.row([&[&"looooooooooooong"], &[&1]]); t.row([&[&"short"], &[&1]]); From 8324d81e204ae626e8eb0a3c8a2030dcae7c6224 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Wed, 30 Oct 2024 21:11:02 +0000 Subject: [PATCH 26/29] table: Add dedicated skiprow() fn, unify skip wording --- src/commands.rs | 24 +++++++++++------------- src/table.rs | 17 ++++++++++++++--- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 99e4d4d..590c81c 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -323,11 +323,9 @@ fn proc_rows(now: i64, } // ...or print skipped rows else if gc.elipsis { - let children = proc_count(procs, pid) - 1; - if children > 0 { - tbl.row([&[&" ".repeat(depth + 1), &gc.skip, &"(", &children, &" skipped)"], - &[], - &[]]); + let count = proc_count(procs, pid) - 1; + if count > 0 { + tbl.skiprow(&[&" ".repeat(depth + 1), &gc.skip, &"(skip ", &count, &" below)"]); } } } @@ -610,14 +608,14 @@ mod tests { (ProcKind::Other, "a.a.b.a", 8, 5), (ProcKind::Other, "a.a.b.a.a", 9, 8), (ProcKind::Other, "a.a.b.b", 10, 5)]); - let out = r#"1 a 9 - 2 a.a 8 - 4 a.a.a 6 - (1 skipped) - 5 a.a.b 5 - (3 skipped) - 3 a.b 7 - 6 a.b.a 4 + let out = r#"1 a 9 + 2 a.a 8 + 4 a.a.a 6 + (skip 1 below) + 5 a.a.b 5 + (skip 3 below) + 3 a.b 7 + 6 a.b.a 4 "#; proc_rows(10, &mut tbl, &procs, 1, 0, &gc, &sc); assert_eq!(tbl.to_string(), out); diff --git a/src/table.rs b/src/table.rs index f74c7ce..7240e96 100644 --- a/src/table.rs +++ b/src/table.rs @@ -113,6 +113,17 @@ impl<'a, const N: usize> Table<'a, N> { } } + /// Add one skip row + /// + /// Like row(), but only one cell and doesn't count toward skipped rows + pub fn skiprow(&mut self, row: &[&dyn Disp]) { + let mut idxrow = [(0, 0, 0); N]; + let start = self.buf.len(); + let len = row.iter().map(|c| c.out(&mut self.buf, self.conf)).sum(); + idxrow[0] = (len, start, self.buf.len()); + self.rows.push_back(idxrow); + } + fn flush(&self, mut out: impl std::io::Write) { if self.is_empty() { return; @@ -128,7 +139,7 @@ impl<'a, const N: usize> Table<'a, N> { // Show skip row. Note that it doesn't participate to column alignment. if self.conf.elipsis && self.skip > 0 { writeln!(out, - "{}(skipping {} due to --last){}", + "{}(skip first {}){}", self.conf.skip.val, self.skip, self.conf.clr.val).unwrap_or(()); } // Show remaining rows @@ -234,14 +245,14 @@ mod test { for i in 0..10 { t.row([&[&format!("{i}")]]); } - assert_eq!(t.to_string(), "(skipping 5 due to --last)\n5\n6\n7\n8\n9\n"); + assert_eq!(t.to_string(), "(skip first 5)\n5\n6\n7\n8\n9\n"); // 5 max ignoring header let mut t = Table::new(&conf).last(5).header(["h"]); for i in 0..10 { t.row([&[&format!("{i}")]]); } - assert_eq!(t.to_string(), "h\n(skipping 5 due to --last)\n5\n6\n7\n8\n9\n"); + assert_eq!(t.to_string(), "h\n(skip first 5)\n5\n6\n7\n8\n9\n"); } #[test] From 026e047c9faec952efb20d225252203ae8ccd915 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Wed, 30 Oct 2024 21:21:34 +0000 Subject: [PATCH 27/29] log: Show skip row for `--first` This means that `--first 1` now needs to read the entire log and is no longer as fast, but the reature is worth it (and is optional via `--elipsis=n`). I'm not too happy with the padding at the end of the skip row, and the inconsistency with the `--last` skip row (no padding and no column align), but no need to be perfect at this stage. --- src/commands.rs | 28 +++++++++++++++++----------- tests/commands.rs | 22 +++++++++++++++++++++- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 590c81c..193eb87 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -24,9 +24,11 @@ pub fn cmd_log(gc: Conf, sc: ConfLog) -> Result { Hist::MergeStop { ts, ref key, .. } => { found += 1; let started = merges.remove(key).unwrap_or(ts + 1); - tbl.row([&[&FmtDate(if sc.starttime { started } else { ts })], - &[&FmtDur(ts - started)], - &[&gc.merge, &p.ebuild_version()]]); + if found <= sc.first { + tbl.row([&[&FmtDate(if sc.starttime { started } else { ts })], + &[&FmtDur(ts - started)], + &[&gc.merge, &p.ebuild_version()]]); + } }, Hist::UnmergeStart { ts, key, .. } => { // This'll overwrite any previous entry, if an unmerge started but never finished @@ -35,29 +37,33 @@ pub fn cmd_log(gc: Conf, sc: ConfLog) -> Result { Hist::UnmergeStop { ts, ref key, .. } => { found += 1; let started = unmerges.remove(key).unwrap_or(ts + 1); - tbl.row([&[&FmtDate(if sc.starttime { started } else { ts })], - &[&FmtDur(ts - started)], - &[&gc.unmerge, &p.ebuild_version()]]); + if found <= sc.first { + tbl.row([&[&FmtDate(if sc.starttime { started } else { ts })], + &[&FmtDur(ts - started)], + &[&gc.unmerge, &p.ebuild_version()]]); + } }, Hist::SyncStart { ts } => { // Some sync starts have multiple entries in old logs sync_start = Some(ts); }, Hist::SyncStop { ts, repo } => { - if let Some(started) = sync_start.take() { - found += 1; + found += 1; + let started = sync_start.take().unwrap_or(ts + 1); + if found <= sc.first { tbl.row([&[&FmtDate(if sc.starttime { started } else { ts })], &[&FmtDur(ts - started)], &[&gc.clr, &"Sync ", &repo]]); - } else { - warn!("Sync stop without a start at {ts}"); } }, } - if found >= sc.first { + if !gc.elipsis && found >= sc.first { break; } } + if gc.elipsis && found >= sc.first { + tbl.skiprow(&[&gc.skip, &"(skip last ", &(found - sc.first), &")"]); + } Ok(found > 0) } diff --git a/tests/commands.rs b/tests/commands.rs index 783c27c..2e26c84 100644 --- a/tests/commands.rs +++ b/tests/commands.rs @@ -106,7 +106,27 @@ fn log() { 2018-03-07 12:49:13 1:01 >>> sys-apps/util-linux-2.30.2-r1\n\ 2018-03-07 13:56:09 40 Sync gentoo\n\ 2018-03-07 13:59:38 2 <<< dev-libs/nspr-4.17\n\ - 2018-03-07 13:59:41 24 >>> dev-libs/nspr-4.18\n")]; + 2018-03-07 13:59:41 24 >>> dev-libs/nspr-4.18\n"), + // skip first + ("%F10000.log l client -oc --first 2", + "2018-02-04 04:55:19 35:46 >>> mail-client/thunderbird-52.6.0\n\ + 2018-02-04 05:42:48 47:29 >>> www-client/firefox-58.0.1\n\ + (skip last 9) \n"), + // skip first + ("%F10000.log l client -oc --last 2", + "(skip first 9)\n\ + 2018-03-12 10:35:22 14 >>> x11-apps/xlsclients-1.1.4\n\ + 2018-03-12 11:03:53 16 >>> kde-frameworks/kxmlrpcclient-5.44.0\n"), + // Skip first and last + ("%F10000.log l client -oc --first 4 --last 2", + "(skip first 2)\n\ + 2018-02-09 11:04:59 47:58 >>> mail-client/thunderbird-52.6.0-r1\n\ + 2018-02-12 10:14:11 31 >>> kde-frameworks/kxmlrpcclient-5.43.0\n\ + (skip last 7) \n"), + // Skip silently + ("%F10000.log l client -oc --first 4 --last 2 --elipsis=n", + "2018-02-09 11:04:59 47:58 >>> mail-client/thunderbird-52.6.0-r1\n\ + 2018-02-12 10:14:11 31 >>> kde-frameworks/kxmlrpcclient-5.43.0\n")]; for (a, o) in t { emlop(a).assert().stdout(o); } From 1481285a1e404d3dfe9e73b01fe840d24621914e Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Wed, 30 Oct 2024 21:57:41 +0000 Subject: [PATCH 28/29] cli/toml: Rename `elipsis` to `showskip` Because I'm afraid that the word "elipsis" is too obscure (especially for non-native speakers), and to make the relationship between the output and the option clearer. --- CHANGELOG.md | 3 +- completion.bash | 20 ++++++------ completion.fish | 2 +- completion.zsh | 10 +++--- emlop.toml | 2 +- src/commands.rs | 6 ++-- src/config.rs | 4 +-- src/config/cli.rs | 77 +++++++++++++++++++++++----------------------- src/config/toml.rs | 2 +- src/table.rs | 10 +++--- tests/commands.rs | 2 +- 11 files changed, 70 insertions(+), 68 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d9326d..9cefe92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,9 @@ ## New features * `predict` now displays emerge proces tree instead of just top proces - - Bevahior configurable with `--pdepth`, `--pwidth`, `--elipsis` + - Bevahvior configurable with `--pdepth`, `--pwidth` - Format is a bit nicer and more colorful +* Display a placeholder for skipped rows, configurable with `--showskip` ## Bug fixes diff --git a/completion.bash b/completion.bash index 15f60b0..4ebfecd 100644 --- a/completion.bash +++ b/completion.bash @@ -27,7 +27,7 @@ _emlop() { case "${cmd}" in emlop) - opts="log predict stats accuracy -f -t -H -o -F -v -h -V --from --to --header --elipsis --duration --date --utc --color --output --logfile --help --version" + opts="log predict stats accuracy -f -t -H -o -F -v -h -V --from --to --header --showskip --duration --date --utc --color --output --logfile --help --version" if [[ ${cur} == -* ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -36,7 +36,7 @@ _emlop() { --from|--to|-f|-t) COMPREPLY=($(compgen -W "1h 1d 1w 1m 1h $(date -Is)" "${cur}")) ;; - --header|-H|--elipsis) + --header|-H|--showskip) COMPREPLY=($(compgen -W "yes no" "${cur}")) ;; --duration) @@ -64,7 +64,7 @@ _emlop() { return 0 ;; emlop__accuracy) - opts="[search]... -e -s -n -f -t -H -o -F -v -h --exact --show --last --avg --limit --from --to --header --elipsis --duration --date --utc --color --output --logfile --help" + opts="[search]... -e -s -n -f -t -H -o -F -v -h --exact --show --last --avg --limit --from --to --header --showskip --duration --date --utc --color --output --logfile --help" if [[ ${cur} == -* ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -73,7 +73,7 @@ _emlop() { --from|--to|-f|-t) COMPREPLY=($(compgen -W "1h 1d 1w 1m 1h $(date -Is)" "${cur}")) ;; - --header|-H|--elipsis) + --header|-H|--showskip) COMPREPLY=($(compgen -W "yes no" "${cur}")) ;; --duration) @@ -117,7 +117,7 @@ _emlop() { return 0 ;; emlop__log) - opts=" [search]... -N -n -s -e -f -t -H -o -F -v -h --starttime --first --last --show --exact --from --to --header --elipsis --duration --date --utc --color --output --logfile --help" + opts=" [search]... -N -n -s -e -f -t -H -o -F -v -h --starttime --first --last --show --exact --from --to --header --showskip --duration --date --utc --color --output --logfile --help" if [[ ${cur} == -* ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -126,7 +126,7 @@ _emlop() { --from|--to|-f|-t) COMPREPLY=($(compgen -W "1h 1d 1w 1m 1h $(date -Is)" "${cur}")) ;; - --header|-H|--elipsis) + --header|-H|--showskip) COMPREPLY=($(compgen -W "yes no" "${cur}")) ;; --duration) @@ -167,7 +167,7 @@ _emlop() { return 0 ;; emlop__predict) - opts="-s -N -n -f -t -H -o -F -v -h --show --first --last --tmpdir --resume --unknown --avg --limit --from --to --header --elipsis --duration --date --utc --color --output --pdepth --pwidth --logfile --help" + opts="-s -N -n -f -t -H -o -F -v -h --show --first --last --tmpdir --resume --unknown --avg --limit --from --to --header --showskip --duration --date --utc --color --output --pdepth --pwidth --logfile --help" if [[ ${cur} == -* ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -176,7 +176,7 @@ _emlop() { --from|--to|-f|-t) COMPREPLY=($(compgen -W "1h 1d 1w 1m 1h $(date -Is)" "${cur}")) ;; - --header|-H|--elipsis) + --header|-H|--showskip) COMPREPLY=($(compgen -W "yes no" "${cur}")) ;; --duration) @@ -231,7 +231,7 @@ _emlop() { return 0 ;; emlop__stats) - opts="[search]... -s -g -e -f -t -H -o -F -v -h --show --groupby --exact --avg --limit --from --to --header --elipsis --duration --date --utc --color --output --logfile --help" + opts="[search]... -s -g -e -f -t -H -o -F -v -h --show --groupby --exact --avg --limit --from --to --header --showskip --duration --date --utc --color --output --logfile --help" if [[ ${cur} == -* ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -240,7 +240,7 @@ _emlop() { --from|--to|-f|-t) COMPREPLY=($(compgen -W "1h 1d 1w 1m 1h $(date -Is)" "${cur}")) ;; - --header|-H|--elipsis) + --header|-H|--showskip) COMPREPLY=($(compgen -W "yes no" "${cur}")) ;; --duration) diff --git a/completion.fish b/completion.fish index 4c1a014..97a35ac 100644 --- a/completion.fish +++ b/completion.fish @@ -3,7 +3,7 @@ complete -c emlop -f complete -c emlop -s f -l from -d 'Only parse log entries after ' -x -a "{1y 'One year ago',1m 'One month ago',1w 'One week ago',1d 'One day ago',1h 'One hour ago',(date -Is) 'Exact date'}" complete -c emlop -s t -l to -d 'Only parse log entries before ' -x -a "{1y 'One year ago',1m 'One month ago',1w 'One week ago',1d 'One day ago',1h 'One hour ago',(date -Is) 'Exact date'}" complete -c emlop -s H -l header -d 'Show table header' -f -a "yes no" -complete -c emlop -l elipsis -d 'Show skipped rows' -f -a "yes no" +complete -c emlop -l showskip -d 'Show skipped rows' -f -a "yes no" complete -c emlop -l duration -d 'Output durations in different formats' -x -a "hms hmsfixed human secs" complete -c emlop -l date -d 'Output dates in different formats' -x -a "ymd ymdhms ymdhmso rfc3339 rfc2822 compact unix" complete -c emlop -l utc -d 'Parse/display dates in UTC instead of local time' -f -a "yes no" diff --git a/completion.zsh b/completion.zsh index a62cd17..9f28d3d 100644 --- a/completion.zsh +++ b/completion.zsh @@ -11,7 +11,7 @@ _emlop() { '--to=[Only parse log entries before ]:date: ' \ '-H+[Show table header]' \ '--header=[Show table header]' \ -'--elipsis=[Show skipped rows]' \ +'--showskip=[Show skipped rows]' \ '--duration=[Output durations in different formats]:format: ' \ '--date=[Output dates in different formats]:format: ' \ '--utc=[Parse/display dates in UTC instead of local time]' \ @@ -48,7 +48,7 @@ _emlop() { '--to=[Only parse log entries before ]:date: ' \ '-H+[Show table header]' \ '--header=[Show table header]' \ -'--elipsis=[Show skipped rows]' \ +'--showskip=[Show skipped rows]' \ '--duration=[Output durations in different formats]:format: ' \ '--date=[Output dates in different formats]:format: ' \ '--utc=[Parse/display dates in UTC instead of local time]' \ @@ -83,7 +83,7 @@ _emlop() { '--to=[Only parse log entries before ]:date: ' \ '-H+[Show table header]' \ '--header=[Show table header]' \ -'--elipsis=[Show skipped rows]' \ +'--showskip=[Show skipped rows]' \ '--duration=[Output durations in different formats]:format: ' \ '--date=[Output dates in different formats]:format: ' \ '--utc=[Parse/display dates in UTC instead of local time]' \ @@ -112,7 +112,7 @@ _emlop() { '--to=[Only parse log entries before ]:date: ' \ '-H+[Show table header]' \ '--header=[Show table header]' \ -'--elipsis=[Show skipped rows]' \ +'--showskip=[Show skipped rows]' \ '--duration=[Output durations in different formats]:format: ' \ '--date=[Output dates in different formats]:format: ' \ '--utc=[Parse/display dates in UTC instead of local time]' \ @@ -142,7 +142,7 @@ _emlop() { '--to=[Only parse log entries before ]:date: ' \ '-H+[Show table header]' \ '--header=[Show table header]' \ -'--elipsis=[Show skipped rows]' \ +'--showskip=[Show skipped rows]' \ '--duration=[Output durations in different formats]:format: ' \ '--date=[Output dates in different formats]:format: ' \ '--utc=[Parse/display dates in UTC instead of local time]' \ diff --git a/emlop.toml b/emlop.toml index 6c80bbb..490615f 100644 --- a/emlop.toml +++ b/emlop.toml @@ -12,7 +12,7 @@ # header = true # color = "yes" # output = "columns" -# elipsis = true +# showskip = true [log] # show = "mus" # starttime = true diff --git a/src/commands.rs b/src/commands.rs index 193eb87..eb23703 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -57,11 +57,11 @@ pub fn cmd_log(gc: Conf, sc: ConfLog) -> Result { } }, } - if !gc.elipsis && found >= sc.first { + if !gc.showskip && found >= sc.first { break; } } - if gc.elipsis && found >= sc.first { + if gc.showskip && found >= sc.first { tbl.skiprow(&[&gc.skip, &"(skip last ", &(found - sc.first), &")"]); } Ok(found > 0) @@ -328,7 +328,7 @@ fn proc_rows(now: i64, } } // ...or print skipped rows - else if gc.elipsis { + else if gc.showskip { let count = proc_count(procs, pid) - 1; if count > 0 { tbl.skiprow(&[&" ".repeat(depth + 1), &gc.skip, &"(skip ", &count, &" below)"]); diff --git a/src/config.rs b/src/config.rs index 9879d43..31553a4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -31,7 +31,7 @@ pub struct Conf { pub clr: AnsiStr, pub lineend: &'static [u8], pub header: bool, - pub elipsis: bool, + pub showskip: bool, pub dur_t: DurationStyle, pub date_offset: time::UtcOffset, pub date_fmt: DateStyle, @@ -166,7 +166,7 @@ impl Conf { clr: AnsiStr::from(if color { "\x1B[m" } else { "" }), lineend: if color { b"\x1B[m\n" } else { b"\n" }, header: sel!(cli, toml, header, (), false)?, - elipsis: sel!(cli, toml, elipsis, (), true)?, + showskip: sel!(cli, toml, showskip, (), true)?, dur_t: sel!(cli, toml, duration, (), DurationStyle::Hms)?, date_offset: offset, date_fmt: sel!(cli, toml, date, (), DateStyle::default())?, diff --git a/src/config/cli.rs b/src/config/cli.rs index c4f94e9..ebf882a 100644 --- a/src/config/cli.rs +++ b/src/config/cli.rs @@ -160,17 +160,17 @@ pub fn build_cli() -> Command { .display_order(11) .help_heading("Stats") .help("Use the last merge times to predict durations"); - let avg = - Arg::new("avg").long("avg") - .value_name("fn") - .display_order(12) - .help_heading("Stats") - .help("Select function used to predict durations") - .long_help("Select function used to predict durations\n \ - arith|a: simple 'sum/count' average\n \ - (defaut)|median|m: middle value, mitigates outliers\n \ - weighted-arith|wa: 'sum/count' with more weight for recent values\n \ - weighted-median|wm: \"middle\" value shifted toward recent values"); + let h = "Select function used to predict durations\n \ + arith|a: simple 'sum/count' average\n \ + (defaut)|median|m: middle value, mitigates outliers\n \ + weighted-arith|wa: 'sum/count' with more weight for recent values\n \ + weighted-median|wm: \"middle\" value shifted toward recent values"; + let avg = Arg::new("avg").long("avg") + .value_name("fn") + .display_order(12) + .help_heading("Stats") + .help(h.split_once('\n').unwrap().0) + .long_help(h); let unknown = Arg::new("unknown").long("unknown") .num_args(1) .value_name("secs") @@ -201,21 +201,21 @@ pub fn build_cli() -> Command { hmsfixed: 0:10:30\n \ secs|s: 630\n \ human|h: 10 minutes, 30 seconds"); - let date = - Arg::new("date").long("date") - .value_name("format") - .global(true) - .display_order(22) - .help_heading("Format") - .help("Output dates in different formats") - .long_help("Output dates in different formats\n \ - ymd|d: 2022-01-31\n \ - (default)|ymdhms|dt: 2022-01-31 08:59:46\n \ - ymdhmso|dto: 2022-01-31 08:59:46 +00:00\n \ - rfc3339|3339: 2022-01-31T08:59:46+00:00\n \ - rfc2822|2822: Mon, 31 Jan 2022 08:59:46 +00:00\n \ - compact: 20220131085946\n \ - unix: 1643619586"); + let h = "Output dates in different formats\n \ + ymd|d: 2022-01-31\n \ + (default)|ymdhms|dt: 2022-01-31 08:59:46\n \ + ymdhmso|dto: 2022-01-31 08:59:46 +00:00\n \ + rfc3339|3339: 2022-01-31T08:59:46+00:00\n \ + rfc2822|2822: Mon, 31 Jan 2022 08:59:46 +00:00\n \ + compact: 20220131085946\n \ + unix: 1643619586"; + let date = Arg::new("date").long("date") + .value_name("format") + .global(true) + .display_order(22) + .help_heading("Format") + .help(h.split_once('\n').unwrap().0) + .long_help(h); let utc = Arg::new("utc").long("utc") .value_name("bool") .global(true) @@ -266,17 +266,18 @@ pub fn build_cli() -> Command { (default)|auto|a: columns on tty, tab otherwise\n \ columns|c: space-aligned columns\n \ tab|t: tab-separated values"); - let elipsis = Arg::new("elipsis").long("elipsis") - .value_name("bool") - .global(true) - .num_args(..=1) - .default_missing_value("y") - .display_order(29) - .help_heading("Format") - .help("Show placeholder for skipped rows (yes/no)") - .long_help("Show placeholder for skipped rows (yes/no)\n \ - (empty)|yes|y: Show ' skipped' placeholder\n \ - no|n: Skip rows silently"); + let h = "Show placeholder for skipped rows (yes/no)\n \ + (empty)|yes|y: Show 'skip ' placeholder\n \ + no|n: Skip rows silently"; + let showskip = Arg::new("showskip").long("showskip") + .value_name("bool") + .global(true) + .num_args(..=1) + .default_missing_value("y") + .display_order(29) + .help_heading("Format") + .help(h.split_once('\n').unwrap().0) + .long_help(h); //////////////////////////////////////////////////////////// // Misc arguments @@ -417,7 +418,7 @@ pub fn build_cli() -> Command { .arg(output) .arg(logfile) .arg(verbose) - .arg(elipsis) + .arg(showskip) .subcommand(cmd_log) .subcommand(cmd_pred) .subcommand(cmd_stats) diff --git a/src/config/toml.rs b/src/config/toml.rs index 6124ab8..82b1a1e 100644 --- a/src/config/toml.rs +++ b/src/config/toml.rs @@ -36,7 +36,7 @@ pub struct Toml { pub date: Option, pub duration: Option, pub header: Option, - pub elipsis: Option, + pub showskip: Option, pub utc: Option, pub color: Option, pub output: Option, diff --git a/src/table.rs b/src/table.rs index 7240e96..2af6a20 100644 --- a/src/table.rs +++ b/src/table.rs @@ -137,7 +137,7 @@ impl<'a, const N: usize> Table<'a, N> { self.flush_one(&mut out, widths, &h); } // Show skip row. Note that it doesn't participate to column alignment. - if self.conf.elipsis && self.skip > 0 { + if self.conf.showskip && self.skip > 0 { writeln!(out, "{}(skip first {}){}", self.conf.skip.val, self.skip, self.conf.clr.val).unwrap_or(()); @@ -212,7 +212,7 @@ mod test { #[test] fn last() { - let conf = Conf::from_str("emlop log --color=n -H --elipsis=n"); + let conf = Conf::from_str("emlop log --color=n -H --showskip=n"); // No limit let mut t = Table::<1>::new(&conf); @@ -237,8 +237,8 @@ mod test { } #[test] - fn last_elipsis() { - let conf = Conf::from_str("emlop log --color=n -H --elipsis=y"); + fn last_showskip() { + let conf = Conf::from_str("emlop log --color=n -H --showskip=y"); // 5 max let mut t = Table::<1>::new(&conf).last(5); @@ -282,7 +282,7 @@ mod test { #[test] fn align_cols_last() { - let conf = Conf::from_str("emlop log --color=n --output=c --elipsis=n"); + let conf = Conf::from_str("emlop log --color=n --output=c --showskip=n"); let mut t = Table::<2>::new(&conf).align_left(0).last(1); t.row([&[&"looooooooooooong"], &[&1]]); t.row([&[&"short"], &[&1]]); diff --git a/tests/commands.rs b/tests/commands.rs index 2e26c84..a018d21 100644 --- a/tests/commands.rs +++ b/tests/commands.rs @@ -124,7 +124,7 @@ fn log() { 2018-02-12 10:14:11 31 >>> kde-frameworks/kxmlrpcclient-5.43.0\n\ (skip last 7) \n"), // Skip silently - ("%F10000.log l client -oc --first 4 --last 2 --elipsis=n", + ("%F10000.log l client -oc --first 4 --last 2 --showskip=n", "2018-02-09 11:04:59 47:58 >>> mail-client/thunderbird-52.6.0-r1\n\ 2018-02-12 10:14:11 31 >>> kde-frameworks/kxmlrpcclient-5.43.0\n")]; for (a, o) in t { From 0f8c759c319025b31bb7b58123724d2ae6f4bd6f Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Thu, 31 Oct 2024 11:02:23 +0000 Subject: [PATCH 29/29] predict: Show skip row for `--first` This one turned out to be trivial. Removing the "hidden" count from the summary line, as it seems redundant and less clear. --- src/commands.rs | 9 ++++--- src/config/cli.rs | 4 +-- tests/commands.rs | 65 ++++++++++++++++++++++++++++++++++------------- 3 files changed, 54 insertions(+), 24 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index eb23703..9b76ad3 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -444,6 +444,11 @@ pub fn cmd_predict(gc: Conf, mut sc: ConfPred) -> Result { } } } + let lastskip = totcount.saturating_sub(sc.first); + if sc.show.merge && gc.showskip && lastskip > 0 { + tbl.skiprow(&[&gc.skip, &"(skip last ", &lastskip, &")"]); + } + // Print summary line if totcount > 0 { if sc.show.tot { let mut s: Vec<&dyn Disp> = vec![&"Estimate for ", @@ -454,10 +459,6 @@ pub fn cmd_predict(gc: Conf, mut sc: ConfPred) -> Result { if totunknown > 0 { s.extend::<[&dyn Disp; 5]>([&", ", &gc.cnt, &totunknown, &gc.clr, &" unknown"]); } - let tothidden = totcount.saturating_sub(sc.first.min(last - 1)); - if tothidden > 0 { - s.extend::<[&dyn Disp; 5]>([&", ", &gc.cnt, &tothidden, &gc.clr, &" hidden"]); - } let e = FmtDur(totelapsed); if totelapsed > 0 { s.extend::<[&dyn Disp; 4]>([&", ", &e, &gc.clr, &" elapsed"]); diff --git a/src/config/cli.rs b/src/config/cli.rs index ebf882a..0328da7 100644 --- a/src/config/cli.rs +++ b/src/config/cli.rs @@ -121,8 +121,8 @@ pub fn build_cli() -> Command { .help_heading("Filter") .help("Show only the last entries") .long_help("Show only the last entries\n \ - (empty)|1: last entry\n \ - 5: last 5 entries\n"); + (empty)|1: last entry\n \ + 5: last 5 entries\n"); let h = "Use main, backup, either, or no portage resume list\n\ This is ignored if STDIN is a piped `emerge -p` output\n \ (default)|auto|a: Use main or backup resume list, if currently emerging\n \ diff --git a/tests/commands.rs b/tests/commands.rs index a018d21..157f907 100644 --- a/tests/commands.rs +++ b/tests/commands.rs @@ -211,26 +211,55 @@ fn predict_tty() { #[ignore] #[test] fn predict_emerge_p() { - let t = [// Check garbage input - ("blah blah\n", format!("No pretended merge found\n"), 1), - // Check all-unknowns - ("[ebuild R ~] dev-lang/unknown-1.42\n", - format!("dev-lang/unknown-1.42 ? \n\ - Estimate for 1 ebuild, 1 unknown 10 @ {}\n", - ts(10)), - 0), - // Check that unknown ebuild don't wreck alignment. Remember that times are {:>9} - ("[ebuild R ~] dev-qt/qtcore-5.9.4-r2\n\ + let t = + [// Check garbage input + ("%F10000.log p --date unix -oc", + "blah blah\n", + format!("No pretended merge found\n"), + 1), + // Check all-unknowns + ("%F10000.log p --date unix -oc", + "[ebuild R ~] dev-lang/unknown-1.42\n", + format!("dev-lang/unknown-1.42 ? \n\ + Estimate for 1 ebuild, 1 unknown 10 @ {}\n", + ts(10)), + 0), + // Check that unknown ebuild don't wreck alignment. Remember that times are {:>9} + ("%F10000.log p --date unix -oc", + "[ebuild R ~] dev-qt/qtcore-5.9.4-r2\n\ [ebuild R ~] dev-lang/unknown-1.42\n\ [ebuild R ~] dev-qt/qtgui-5.9.4-r3\n", - format!("dev-qt/qtcore-5.9.4-r2 3:45 \n\ - dev-lang/unknown-1.42 ? \n\ - dev-qt/qtgui-5.9.4-r3 4:24 \n\ - Estimate for 3 ebuilds, 1 unknown 8:19 @ {}\n", - ts(8 * 60 + 9 + 10)), - 0)]; - for (i, o, e) in t { - emlop("%F10000.log p --date unix -oc").write_stdin(i).assert().code(e).stdout(o); + format!("dev-qt/qtcore-5.9.4-r2 3:45 \n\ + dev-lang/unknown-1.42 ? \n\ + dev-qt/qtgui-5.9.4-r3 4:24 \n\ + Estimate for 3 ebuilds, 1 unknown 8:19 @ {}\n", + ts(8 * 60 + 9 + 10)), + 0), + // Check skip rows + ("%F10000.log p --date unix -oc --show m --first 2", + "[ebuild R ~] dev-qt/qtcore-1\n\ + [ebuild R ~] dev-qt/qtcore-2\n\ + [ebuild R ~] dev-qt/qtcore-3\n\ + [ebuild R ~] dev-qt/qtcore-4\n\ + [ebuild R ~] dev-qt/qtcore-5\n", + "dev-qt/qtcore-1 3:45\n\ + dev-qt/qtcore-2 3:45\n\ + (skip last 3) \n" + .into(), + 0), + ("%F10000.log p --date unix -oc --show m --first 2 --last 1", + "[ebuild R ~] dev-qt/qtcore-1\n\ + [ebuild R ~] dev-qt/qtcore-2\n\ + [ebuild R ~] dev-qt/qtcore-3\n\ + [ebuild R ~] dev-qt/qtcore-4\n\ + [ebuild R ~] dev-qt/qtcore-5\n", + "(skip first 1)\n\ + dev-qt/qtcore-2 3:45\n\ + (skip last 3) \n" + .into(), + 0)]; + for (a, i, o, e) in t { + emlop(a).write_stdin(i).assert().code(e).stdout(o); } }