mirror of
https://github.com/ClementTsang/bottom.git
synced 2025-07-27 15:44:17 +02:00
feature: show running time of processes (#801)
* feature: show running time of processes * fix clippy * add time searching * update changelog * use safer duration for linux in case of 0 * some cleanup * quick hack to deal with some Windows processes returning unix epoch time as start time --------- Co-authored-by: Clement Tsang <34804052+ClementTsang@users.noreply.github.com>
This commit is contained in:
parent
7edc2fc7e5
commit
80183b8b1c
@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- [#1063](https://github.com/ClementTsang/bottom/pull/1063): Add buffer and cache memory tracking.
|
- [#1063](https://github.com/ClementTsang/bottom/pull/1063): Add buffer and cache memory tracking.
|
||||||
- [#1106](https://github.com/ClementTsang/bottom/pull/1106): Add current battery charging state.
|
- [#1106](https://github.com/ClementTsang/bottom/pull/1106): Add current battery charging state.
|
||||||
- [#1115](https://github.com/ClementTsang/bottom/pull/1115): Add customizable process columns to config file.
|
- [#1115](https://github.com/ClementTsang/bottom/pull/1115): Add customizable process columns to config file.
|
||||||
|
- [#801](https://github.com/ClementTsang/bottom/pull/801): Add optional process time column and querying.
|
||||||
|
|
||||||
## Changes
|
## Changes
|
||||||
|
|
||||||
|
@ -28,6 +28,8 @@ cfg_if::cfg_if! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use std::{borrow::Cow, time::Duration};
|
||||||
|
|
||||||
use crate::Pid;
|
use crate::Pid;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
@ -35,7 +37,7 @@ pub struct ProcessHarvest {
|
|||||||
/// The pid of the process.
|
/// The pid of the process.
|
||||||
pub pid: Pid,
|
pub pid: Pid,
|
||||||
|
|
||||||
/// The parent PID of the process. Remember, parent_pid 0 is root.
|
/// The parent PID of the process. A `parent_pid` of 0 is usually the root.
|
||||||
pub parent_pid: Option<Pid>,
|
pub parent_pid: Option<Pid>,
|
||||||
|
|
||||||
/// CPU usage as a percentage.
|
/// CPU usage as a percentage.
|
||||||
@ -65,15 +67,18 @@ pub struct ProcessHarvest {
|
|||||||
/// The total number of bytes written by the process.
|
/// The total number of bytes written by the process.
|
||||||
pub total_write_bytes: u64,
|
pub total_write_bytes: u64,
|
||||||
|
|
||||||
/// The current state of the process (e.g. zombie, asleep)
|
/// The current state of the process (e.g. zombie, asleep).
|
||||||
pub process_state: (String, char),
|
pub process_state: (String, char),
|
||||||
|
|
||||||
|
/// Cumulative total CPU time used.
|
||||||
|
pub time: Duration,
|
||||||
|
|
||||||
/// This is the *effective* user ID of the process. This is only used on Unix platforms.
|
/// This is the *effective* user ID of the process. This is only used on Unix platforms.
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
pub uid: Option<libc::uid_t>,
|
pub uid: Option<libc::uid_t>,
|
||||||
|
|
||||||
/// This is the process' user.
|
/// This is the process' user.
|
||||||
pub user: std::borrow::Cow<'static, str>,
|
pub user: Cow<'static, str>,
|
||||||
// TODO: Additional fields
|
// TODO: Additional fields
|
||||||
// pub rss_kb: u64,
|
// pub rss_kb: u64,
|
||||||
// pub virt_kb: u64,
|
// pub virt_kb: u64,
|
||||||
@ -88,5 +93,6 @@ impl ProcessHarvest {
|
|||||||
self.write_bytes_per_sec += rhs.write_bytes_per_sec;
|
self.write_bytes_per_sec += rhs.write_bytes_per_sec;
|
||||||
self.total_read_bytes += rhs.total_read_bytes;
|
self.total_read_bytes += rhs.total_read_bytes;
|
||||||
self.total_write_bytes += rhs.total_write_bytes;
|
self.total_write_bytes += rhs.total_write_bytes;
|
||||||
|
self.time += rhs.time;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{BufRead, BufReader};
|
use std::io::{BufRead, BufReader};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use hashbrown::{HashMap, HashSet};
|
use hashbrown::{HashMap, HashSet};
|
||||||
use procfs::process::{Process, Stat};
|
use procfs::process::{Process, Stat};
|
||||||
@ -196,6 +197,16 @@ fn read_proc(
|
|||||||
|
|
||||||
let uid = process.uid()?;
|
let uid = process.uid()?;
|
||||||
|
|
||||||
|
let time = if let Ok(ticks_per_sec) = u32::try_from(procfs::ticks_per_second()) {
|
||||||
|
if ticks_per_sec == 0 {
|
||||||
|
Duration::ZERO
|
||||||
|
} else {
|
||||||
|
Duration::from_secs(stat.utime + stat.stime) / ticks_per_sec
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Duration::ZERO
|
||||||
|
};
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
ProcessHarvest {
|
ProcessHarvest {
|
||||||
pid: process.pid,
|
pid: process.pid,
|
||||||
@ -215,6 +226,7 @@ fn read_proc(
|
|||||||
.get_uid_to_username_mapping(uid)
|
.get_uid_to_username_mapping(uid)
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
.unwrap_or_else(|_| "N/A".into()),
|
.unwrap_or_else(|_| "N/A".into()),
|
||||||
|
time,
|
||||||
},
|
},
|
||||||
new_process_times,
|
new_process_times,
|
||||||
))
|
))
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
//! Shared process data harvesting code from macOS and FreeBSD via sysinfo.
|
//! Shared process data harvesting code from macOS and FreeBSD via sysinfo.
|
||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use hashbrown::HashMap;
|
use hashbrown::HashMap;
|
||||||
use sysinfo::{CpuExt, PidExt, ProcessExt, ProcessStatus, System, SystemExt};
|
use sysinfo::{CpuExt, PidExt, ProcessExt, ProcessStatus, System, SystemExt};
|
||||||
@ -109,6 +110,7 @@ where
|
|||||||
.ok()
|
.ok()
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| "N/A".into()),
|
.unwrap_or_else(|| "N/A".into()),
|
||||||
|
time: Duration::from_secs(process_val.run_time()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
//! Process data collection for Windows. Uses sysinfo.
|
//! Process data collection for Windows. Uses sysinfo.
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use sysinfo::{CpuExt, PidExt, ProcessExt, System, SystemExt, UserExt};
|
use sysinfo::{CpuExt, PidExt, ProcessExt, System, SystemExt, UserExt};
|
||||||
|
|
||||||
use super::ProcessHarvest;
|
use super::ProcessHarvest;
|
||||||
@ -79,6 +81,13 @@ pub fn get_process_data(
|
|||||||
.user_id()
|
.user_id()
|
||||||
.and_then(|uid| sys.get_user_by_id(uid))
|
.and_then(|uid| sys.get_user_by_id(uid))
|
||||||
.map_or_else(|| "N/A".into(), |user| user.name().to_owned().into()),
|
.map_or_else(|| "N/A".into(), |user| user.name().to_owned().into()),
|
||||||
|
time: if process_val.start_time() == 0 {
|
||||||
|
// Workaround for Windows occasionally returning a start time equal to UNIX epoch, giving a run time
|
||||||
|
// in the range of 50+ years. We just return a time of zero in this case for simplicity.
|
||||||
|
Duration::ZERO
|
||||||
|
} else {
|
||||||
|
Duration::from_secs(process_val.run_time())
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
110
src/app/query.rs
110
src/app/query.rs
@ -1,6 +1,9 @@
|
|||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
use std::time::Duration;
|
||||||
use std::{borrow::Cow, collections::VecDeque};
|
use std::{borrow::Cow, collections::VecDeque};
|
||||||
|
|
||||||
|
use humantime::parse_duration;
|
||||||
|
|
||||||
use super::data_harvester::processes::ProcessHarvest;
|
use super::data_harvester::processes::ProcessHarvest;
|
||||||
use crate::utils::error::{
|
use crate::utils::error::{
|
||||||
BottomError::{self, QueryError},
|
BottomError::{self, QueryError},
|
||||||
@ -279,12 +282,63 @@ pub fn parse_query(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
PrefixType::Time => {
|
||||||
|
let mut condition: Option<QueryComparison> = None;
|
||||||
|
let mut duration_string: Option<String> = None;
|
||||||
|
|
||||||
|
if content == "=" {
|
||||||
|
condition = Some(QueryComparison::Equal);
|
||||||
|
duration_string = query.pop_front();
|
||||||
|
} else if content == ">" || content == "<" {
|
||||||
|
if let Some(queue_next) = query.pop_front() {
|
||||||
|
if queue_next == "=" {
|
||||||
|
condition = Some(if content == ">" {
|
||||||
|
QueryComparison::GreaterOrEqual
|
||||||
|
} else {
|
||||||
|
QueryComparison::LessOrEqual
|
||||||
|
});
|
||||||
|
duration_string = query.pop_front();
|
||||||
|
} else {
|
||||||
|
condition = Some(if content == ">" {
|
||||||
|
QueryComparison::Greater
|
||||||
|
} else {
|
||||||
|
QueryComparison::Less
|
||||||
|
});
|
||||||
|
duration_string = Some(queue_next);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(QueryError("Missing value".into()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(condition) = condition {
|
||||||
|
let duration = parse_duration(
|
||||||
|
&duration_string.ok_or(QueryError("Missing value".into()))?,
|
||||||
|
)
|
||||||
|
.map_err(|err| QueryError(err.to_string().into()))?;
|
||||||
|
|
||||||
|
return Ok(Prefix {
|
||||||
|
or: None,
|
||||||
|
regex_prefix: None,
|
||||||
|
compare_prefix: Some((
|
||||||
|
prefix_type,
|
||||||
|
ComparableQuery::Time(TimeQuery {
|
||||||
|
condition,
|
||||||
|
duration,
|
||||||
|
}),
|
||||||
|
)),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
// Assume it's some numerical value.
|
||||||
// Now we gotta parse the content... yay.
|
// Now we gotta parse the content... yay.
|
||||||
|
|
||||||
let mut condition: Option<QueryComparison> = None;
|
let mut condition: Option<QueryComparison> = None;
|
||||||
let mut value: Option<f64> = None;
|
let mut value: Option<f64> = None;
|
||||||
|
|
||||||
|
// TODO: Jeez, what the heck did I write here... add some tests and clean this up, please.
|
||||||
if content == "=" {
|
if content == "=" {
|
||||||
condition = Some(QueryComparison::Equal);
|
condition = Some(QueryComparison::Equal);
|
||||||
if let Some(queue_next) = query.pop_front() {
|
if let Some(queue_next) = query.pop_front() {
|
||||||
@ -321,11 +375,8 @@ pub fn parse_query(
|
|||||||
|
|
||||||
if let Some(condition) = condition {
|
if let Some(condition) = condition {
|
||||||
if let Some(read_value) = value {
|
if let Some(read_value) = value {
|
||||||
// Now we want to check one last thing - is there a unit?
|
// Note that the values *might* have a unit or need to be parsed differently
|
||||||
// If no unit, assume base.
|
// based on the prefix type!
|
||||||
// Furthermore, base must be PEEKED at initially, and will
|
|
||||||
// require (likely) prefix_type specific checks
|
|
||||||
// Lastly, if it *is* a unit, remember to POP!
|
|
||||||
|
|
||||||
let mut value = read_value;
|
let mut value = read_value;
|
||||||
|
|
||||||
@ -335,6 +386,11 @@ pub fn parse_query(
|
|||||||
| PrefixType::Wps
|
| PrefixType::Wps
|
||||||
| PrefixType::TRead
|
| PrefixType::TRead
|
||||||
| PrefixType::TWrite => {
|
| PrefixType::TWrite => {
|
||||||
|
// If no unit, assume base.
|
||||||
|
// Furthermore, base must be PEEKED at initially, and will
|
||||||
|
// require (likely) prefix_type specific checks
|
||||||
|
// Lastly, if it *is* a unit, remember to POP!
|
||||||
|
|
||||||
if let Some(potential_unit) = query.front() {
|
if let Some(potential_unit) = query.front() {
|
||||||
match potential_unit.to_lowercase().as_str() {
|
match potential_unit.to_lowercase().as_str() {
|
||||||
"tb" => {
|
"tb" => {
|
||||||
@ -385,7 +441,10 @@ pub fn parse_query(
|
|||||||
regex_prefix: None,
|
regex_prefix: None,
|
||||||
compare_prefix: Some((
|
compare_prefix: Some((
|
||||||
prefix_type,
|
prefix_type,
|
||||||
NumericalQuery { condition, value },
|
ComparableQuery::Numerical(NumericalQuery {
|
||||||
|
condition,
|
||||||
|
value,
|
||||||
|
}),
|
||||||
)),
|
)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -568,6 +627,7 @@ pub enum PrefixType {
|
|||||||
Name,
|
Name,
|
||||||
State,
|
State,
|
||||||
User,
|
User,
|
||||||
|
Time,
|
||||||
__Nonexhaustive,
|
__Nonexhaustive,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -591,16 +651,18 @@ impl std::str::FromStr for PrefixType {
|
|||||||
"pid" => Ok(Pid),
|
"pid" => Ok(Pid),
|
||||||
"state" => Ok(State),
|
"state" => Ok(State),
|
||||||
"user" => Ok(User),
|
"user" => Ok(User),
|
||||||
|
"time" => Ok(Time),
|
||||||
_ => Ok(Name),
|
_ => Ok(Name),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: This is also jank and could be better represented. Add tests, then clean up!
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Prefix {
|
pub struct Prefix {
|
||||||
pub or: Option<Box<Or>>,
|
pub or: Option<Box<Or>>,
|
||||||
pub regex_prefix: Option<(PrefixType, StringQuery)>,
|
pub regex_prefix: Option<(PrefixType, StringQuery)>,
|
||||||
pub compare_prefix: Option<(PrefixType, NumericalQuery)>,
|
pub compare_prefix: Option<(PrefixType, ComparableQuery)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Prefix {
|
impl Prefix {
|
||||||
@ -658,6 +720,16 @@ impl Prefix {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn matches_duration(condition: &QueryComparison, lhs: Duration, rhs: Duration) -> bool {
|
||||||
|
match condition {
|
||||||
|
QueryComparison::Equal => lhs == rhs,
|
||||||
|
QueryComparison::Less => lhs < rhs,
|
||||||
|
QueryComparison::Greater => lhs > rhs,
|
||||||
|
QueryComparison::LessOrEqual => lhs <= rhs,
|
||||||
|
QueryComparison::GreaterOrEqual => lhs >= rhs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(and) = &self.or {
|
if let Some(and) = &self.or {
|
||||||
and.check(process, is_using_command)
|
and.check(process, is_using_command)
|
||||||
} else if let Some((prefix_type, query_content)) = &self.regex_prefix {
|
} else if let Some((prefix_type, query_content)) = &self.regex_prefix {
|
||||||
@ -676,8 +748,9 @@ impl Prefix {
|
|||||||
} else {
|
} else {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
} else if let Some((prefix_type, numerical_query)) = &self.compare_prefix {
|
} else if let Some((prefix_type, comparable_query)) = &self.compare_prefix {
|
||||||
match prefix_type {
|
match comparable_query {
|
||||||
|
ComparableQuery::Numerical(numerical_query) => match prefix_type {
|
||||||
PrefixType::PCpu => matches_condition(
|
PrefixType::PCpu => matches_condition(
|
||||||
&numerical_query.condition,
|
&numerical_query.condition,
|
||||||
process.cpu_usage_percent,
|
process.cpu_usage_percent,
|
||||||
@ -714,6 +787,13 @@ impl Prefix {
|
|||||||
numerical_query.value,
|
numerical_query.value,
|
||||||
),
|
),
|
||||||
_ => true,
|
_ => true,
|
||||||
|
},
|
||||||
|
ComparableQuery::Time(time_query) => match prefix_type {
|
||||||
|
PrefixType::Time => {
|
||||||
|
matches_duration(&time_query.condition, process.time, time_query.duration)
|
||||||
|
}
|
||||||
|
_ => true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Somehow we have an empty condition... oh well. Return true.
|
// Somehow we have an empty condition... oh well. Return true.
|
||||||
@ -751,8 +831,20 @@ pub enum StringQuery {
|
|||||||
Regex(regex::Regex),
|
Regex(regex::Regex),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ComparableQuery {
|
||||||
|
Numerical(NumericalQuery),
|
||||||
|
Time(TimeQuery),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct NumericalQuery {
|
pub struct NumericalQuery {
|
||||||
pub condition: QueryComparison,
|
pub condition: QueryComparison,
|
||||||
pub value: f64,
|
pub value: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct TimeQuery {
|
||||||
|
pub condition: QueryComparison,
|
||||||
|
pub duration: Duration,
|
||||||
|
}
|
||||||
|
@ -24,7 +24,7 @@ mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
fn process_column_settings() {
|
fn process_column_settings() {
|
||||||
let config = r#"
|
let config = r#"
|
||||||
columns = ["CPU%", "PiD", "user", "MEM", "Tread", "T.Write", "Rps", "W/s"]
|
columns = ["CPU%", "PiD", "user", "MEM", "Tread", "T.Write", "Rps", "W/s", "tiMe", "USER", "state"]
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
let generated: ProcessConfig = toml_edit::de::from_str(config).unwrap();
|
let generated: ProcessConfig = toml_edit::de::from_str(config).unwrap();
|
||||||
@ -39,26 +39,23 @@ mod test {
|
|||||||
ProcColumn::TotalWrite,
|
ProcColumn::TotalWrite,
|
||||||
ProcColumn::ReadPerSecond,
|
ProcColumn::ReadPerSecond,
|
||||||
ProcColumn::WritePerSecond,
|
ProcColumn::WritePerSecond,
|
||||||
|
ProcColumn::Time,
|
||||||
|
ProcColumn::User,
|
||||||
|
ProcColumn::State,
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn process_column_settings_2() {
|
fn process_column_settings_2() {
|
||||||
let config = r#"
|
let config = r#"columns = ["MEM%"]"#;
|
||||||
columns = ["MEM%"]
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let generated: ProcessConfig = toml_edit::de::from_str(config).unwrap();
|
let generated: ProcessConfig = toml_edit::de::from_str(config).unwrap();
|
||||||
assert_eq!(generated.columns, Some(vec![ProcColumn::MemoryPercent]));
|
assert_eq!(generated.columns, Some(vec![ProcColumn::MemoryPercent]));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn process_column_settings_3() {
|
fn process_column_settings_3() {
|
||||||
let config = r#"
|
let config = r#"columns = ["MEM%", "TWrite", "Cpuz", "read", "wps"]"#;
|
||||||
columns = ["MEM%", "TWrite", "Cpuz", "read", "wps"]
|
|
||||||
"#;
|
|
||||||
|
|
||||||
toml_edit::de::from_str::<ProcessConfig>(config).expect_err("Should error out!");
|
toml_edit::de::from_str::<ProcessConfig>(config).expect_err("Should error out!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,6 +35,8 @@ pub enum BottomError {
|
|||||||
/// An error that just signifies something minor went wrong; no message.
|
/// An error that just signifies something minor went wrong; no message.
|
||||||
#[error("Minor error.")]
|
#[error("Minor error.")]
|
||||||
MinorError,
|
MinorError,
|
||||||
|
#[error("Error casting integers {0}")]
|
||||||
|
TryFromIntError(#[from] std::num::TryFromIntError),
|
||||||
/// An error to represent errors with procfs
|
/// An error to represent errors with procfs
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
#[error("Procfs error, {0}")]
|
#[error("Procfs error, {0}")]
|
||||||
|
@ -89,6 +89,7 @@ fn make_column(column: ProcColumn) -> SortColumn<ProcColumn> {
|
|||||||
TotalWrite => SortColumn::hard(TotalWrite, 8).default_descending(),
|
TotalWrite => SortColumn::hard(TotalWrite, 8).default_descending(),
|
||||||
User => SortColumn::soft(User, Some(0.05)),
|
User => SortColumn::soft(User, Some(0.05)),
|
||||||
State => SortColumn::hard(State, 7),
|
State => SortColumn::hard(State, 7),
|
||||||
|
Time => SortColumn::new(Time),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,6 +115,7 @@ pub enum ProcWidgetColumn {
|
|||||||
TotalWrite,
|
TotalWrite,
|
||||||
User,
|
User,
|
||||||
State,
|
State,
|
||||||
|
Time,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ProcWidgetState {
|
pub struct ProcWidgetState {
|
||||||
@ -251,6 +253,7 @@ impl ProcWidgetState {
|
|||||||
TotalWrite => ProcWidgetColumn::TotalWrite,
|
TotalWrite => ProcWidgetColumn::TotalWrite,
|
||||||
State => ProcWidgetColumn::State,
|
State => ProcWidgetColumn::State,
|
||||||
User => ProcWidgetColumn::User,
|
User => ProcWidgetColumn::User,
|
||||||
|
Time => ProcWidgetColumn::Time,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<IndexSet<_>>();
|
.collect::<IndexSet<_>>();
|
||||||
@ -936,6 +939,8 @@ fn sort_skip_pid_asc(column: &ProcColumn, data: &mut [ProcWidgetData], order: So
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::widgets::MemUsage;
|
use crate::widgets::MemUsage;
|
||||||
|
|
||||||
@ -959,6 +964,7 @@ mod test {
|
|||||||
user: "N/A".to_string(),
|
user: "N/A".to_string(),
|
||||||
num_similar: 0,
|
num_similar: 0,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
time: Duration::from_secs(0),
|
||||||
};
|
};
|
||||||
|
|
||||||
let b = ProcWidgetData {
|
let b = ProcWidgetData {
|
||||||
|
@ -23,6 +23,7 @@ pub enum ProcColumn {
|
|||||||
TotalWrite,
|
TotalWrite,
|
||||||
State,
|
State,
|
||||||
User,
|
User,
|
||||||
|
Time,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for ProcColumn {
|
impl<'de> Deserialize<'de> for ProcColumn {
|
||||||
@ -45,6 +46,7 @@ impl<'de> Deserialize<'de> for ProcColumn {
|
|||||||
"twrite" | "t.write" => Ok(ProcColumn::TotalWrite),
|
"twrite" | "t.write" => Ok(ProcColumn::TotalWrite),
|
||||||
"state" => Ok(ProcColumn::State),
|
"state" => Ok(ProcColumn::State),
|
||||||
"user" => Ok(ProcColumn::User),
|
"user" => Ok(ProcColumn::User),
|
||||||
|
"time" => Ok(ProcColumn::Time),
|
||||||
_ => Err(D::Error::custom("doesn't match any column type")),
|
_ => Err(D::Error::custom("doesn't match any column type")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -75,6 +77,7 @@ impl ColumnHeader for ProcColumn {
|
|||||||
ProcColumn::TotalWrite => "T.Write",
|
ProcColumn::TotalWrite => "T.Write",
|
||||||
ProcColumn::State => "State",
|
ProcColumn::State => "State",
|
||||||
ProcColumn::User => "User",
|
ProcColumn::User => "User",
|
||||||
|
ProcColumn::Time => "Time",
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
@ -94,6 +97,7 @@ impl ColumnHeader for ProcColumn {
|
|||||||
ProcColumn::TotalWrite => "T.Write",
|
ProcColumn::TotalWrite => "T.Write",
|
||||||
ProcColumn::State => "State",
|
ProcColumn::State => "State",
|
||||||
ProcColumn::User => "User",
|
ProcColumn::User => "User",
|
||||||
|
ProcColumn::Time => "Time",
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
@ -151,6 +155,9 @@ impl SortsRow for ProcColumn {
|
|||||||
data.sort_by_cached_key(|pd| pd.user.to_lowercase());
|
data.sort_by_cached_key(|pd| pd.user.to_lowercase());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ProcColumn::Time => {
|
||||||
|
data.sort_by(|a, b| sort_partial_fn(descending)(a.time, b.time));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use std::{
|
use std::{
|
||||||
cmp::{max, Ordering},
|
cmp::{max, Ordering},
|
||||||
fmt::Display,
|
fmt::Display,
|
||||||
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use concat_string::concat_string;
|
use concat_string::concat_string;
|
||||||
@ -107,6 +108,63 @@ impl Display for MemUsage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trait DurationExt {
|
||||||
|
fn num_days(&self) -> u64;
|
||||||
|
fn num_hours(&self) -> u64;
|
||||||
|
fn num_minutes(&self) -> u64;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SECS_PER_DAY: u64 = SECS_PER_HOUR * 24;
|
||||||
|
const SECS_PER_HOUR: u64 = SECS_PER_MINUTE * 60;
|
||||||
|
const SECS_PER_MINUTE: u64 = 60;
|
||||||
|
|
||||||
|
impl DurationExt for Duration {
|
||||||
|
/// Number of full days in this duration.
|
||||||
|
#[inline]
|
||||||
|
fn num_days(&self) -> u64 {
|
||||||
|
self.as_secs() / SECS_PER_DAY
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of full hours in this duration.
|
||||||
|
#[inline]
|
||||||
|
fn num_hours(&self) -> u64 {
|
||||||
|
self.as_secs() / SECS_PER_HOUR
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of full minutes in this duration.
|
||||||
|
#[inline]
|
||||||
|
fn num_minutes(&self) -> u64 {
|
||||||
|
self.as_secs() / SECS_PER_MINUTE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_time(dur: Duration) -> String {
|
||||||
|
if dur.num_days() > 0 {
|
||||||
|
format!(
|
||||||
|
"{}d {}h {}m",
|
||||||
|
dur.num_days(),
|
||||||
|
dur.num_hours() % 24,
|
||||||
|
dur.num_minutes() % 60
|
||||||
|
)
|
||||||
|
} else if dur.num_hours() > 0 {
|
||||||
|
format!(
|
||||||
|
"{}h {}m {}s",
|
||||||
|
dur.num_hours(),
|
||||||
|
dur.num_minutes() % 60,
|
||||||
|
dur.as_secs() % 60
|
||||||
|
)
|
||||||
|
} else if dur.num_minutes() > 0 {
|
||||||
|
format!(
|
||||||
|
"{}m {}.{:02}s",
|
||||||
|
dur.num_minutes(),
|
||||||
|
dur.as_secs() % 60,
|
||||||
|
dur.as_millis() % 1000 / 10
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!("{}.{:03}s", dur.as_secs(), dur.as_millis() % 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct ProcWidgetData {
|
pub struct ProcWidgetData {
|
||||||
pub pid: Pid,
|
pub pid: Pid,
|
||||||
@ -123,6 +181,7 @@ pub struct ProcWidgetData {
|
|||||||
pub user: String,
|
pub user: String,
|
||||||
pub num_similar: u64,
|
pub num_similar: u64,
|
||||||
pub disabled: bool,
|
pub disabled: bool,
|
||||||
|
pub time: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProcWidgetData {
|
impl ProcWidgetData {
|
||||||
@ -157,6 +216,7 @@ impl ProcWidgetData {
|
|||||||
user: process.user.to_string(),
|
user: process.user.to_string(),
|
||||||
num_similar: 1,
|
num_similar: 1,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
time: process.time,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,6 +264,7 @@ impl ProcWidgetData {
|
|||||||
ProcColumn::TotalWrite => dec_bytes_string(self.total_write),
|
ProcColumn::TotalWrite => dec_bytes_string(self.total_write),
|
||||||
ProcColumn::State => self.process_char.to_string(),
|
ProcColumn::State => self.process_char.to_string(),
|
||||||
ProcColumn::User => self.user.clone(),
|
ProcColumn::User => self.user.clone(),
|
||||||
|
ProcColumn::Time => format_time(self.time),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -237,6 +298,7 @@ impl DataToCell<ProcColumn> for ProcWidgetData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ProcColumn::User => self.user.clone(),
|
ProcColumn::User => self.user.clone(),
|
||||||
|
ProcColumn::Time => format_time(self.time),
|
||||||
},
|
},
|
||||||
calculated_width,
|
calculated_width,
|
||||||
))
|
))
|
||||||
@ -266,3 +328,41 @@ impl DataToCell<ProcColumn> for ProcWidgetData {
|
|||||||
widths
|
widths
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::widgets::proc_widget_data::format_time;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_format_time() {
|
||||||
|
const ONE_DAY: u64 = 24 * 60 * 60;
|
||||||
|
|
||||||
|
assert_eq!(format_time(Duration::from_millis(500)), "0.500s");
|
||||||
|
assert_eq!(format_time(Duration::from_millis(900)), "0.900s");
|
||||||
|
assert_eq!(format_time(Duration::from_secs(1)), "1.000s");
|
||||||
|
assert_eq!(format_time(Duration::from_secs(10)), "10.000s");
|
||||||
|
assert_eq!(format_time(Duration::from_secs(60)), "1m 0.00s");
|
||||||
|
assert_eq!(format_time(Duration::from_secs(61)), "1m 1.00s");
|
||||||
|
assert_eq!(format_time(Duration::from_secs(600)), "10m 0.00s");
|
||||||
|
assert_eq!(format_time(Duration::from_secs(601)), "10m 1.00s");
|
||||||
|
assert_eq!(format_time(Duration::from_secs(3600)), "1h 0m 0s");
|
||||||
|
assert_eq!(format_time(Duration::from_secs(3601)), "1h 0m 1s");
|
||||||
|
assert_eq!(format_time(Duration::from_secs(3660)), "1h 1m 0s");
|
||||||
|
assert_eq!(format_time(Duration::from_secs(3661)), "1h 1m 1s");
|
||||||
|
assert_eq!(format_time(Duration::from_secs(ONE_DAY - 1)), "23h 59m 59s");
|
||||||
|
assert_eq!(format_time(Duration::from_secs(ONE_DAY)), "1d 0h 0m");
|
||||||
|
assert_eq!(format_time(Duration::from_secs(ONE_DAY + 1)), "1d 0h 0m");
|
||||||
|
assert_eq!(format_time(Duration::from_secs(ONE_DAY + 60)), "1d 0h 1m");
|
||||||
|
assert_eq!(
|
||||||
|
format_time(Duration::from_secs(ONE_DAY + 3600 - 1)),
|
||||||
|
"1d 0h 59m"
|
||||||
|
);
|
||||||
|
assert_eq!(format_time(Duration::from_secs(ONE_DAY + 3600)), "1d 1h 0m");
|
||||||
|
assert_eq!(
|
||||||
|
format_time(Duration::from_secs(ONE_DAY * 365 - 1)),
|
||||||
|
"364d 23h 59m"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user