Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,369 changes: 2,245 additions & 124 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ multiple_crate_versions = "allow"
[dependencies]
anyhow = "1.0.104"
clap = { version = "4.6.7", features = ["derive", "env", "string"] }
self_update = { version = "1.3.3", default-features = false, features = ["github", "reqwest", "rustls", "archive-tar", "compression-tar-gz", "checksums"] }
semver = "1.0.28"
serde = { version = "1.0.229", features = ["serde_derive", "derive"] }
sysinfo = "0.40.1"

Expand Down
56 changes: 27 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,32 @@

A command line utility that spawns a process and produces a `csv` report on its resource usage.

```bash
Usage: procplot [OPTIONS] [COMMAND]...
```
Usage: procplot <COMMAND>

Arguments:
[COMMAND]... Command to run
Commands:
exec Spawn a process and record its resource usage [alias: e]
upgrade Update the binary to the latest release [aliases: up, u]
version Print the current version
help Print this message or the help of the given subcommand(s)

Options:
-r, --report <REPORT_PATH> Output path for the generated report [env: PM_REPORT=] [default: procplot.csv]
-i, --interval <POLL_INTERVAL> How often to probe the process for details in milliseconds [env: PM_INTERVAL=] [default: 200]
-m, --mem-units <MEM_UNITS> What units to use for recording memory [env: PM_MEM_UNITS=] [default: mb] [possible values: mb, kb, b]
-t, --time-units <TIME_UNITS> What units to use for recording time [env: PM_TIME_UNITS=] [default: ms] [possible values: s, ms]
-o, --override-report Override report file if exists
--no-cpu Don't measure CPU usage [env: PM_NO_CPU=]
--no-memory Don't measure memory usage [env: PM_NO_MEMORY=]
--no-disk Don't measure disk usage [env: PM_NO_DISK=]
-h, --help Print help
-h, --help Print help
-V, --version Print version
```

# Usage

Run a command and record its resource usage with `exec`:

```bash
procplot -- node -e "setTimeout(() => console.log('Sup'), 4000)"
procplot exec -- node -e "setTimeout(() => console.log('Sup'), 4000)"
```

This will produce a file named `procplot.csv`

```bash
procplot --report something.csv -- node -e "setTimeout(() => console.log('Sup'), 4000)"
procplot exec --report something.csv -- node -e "setTimeout(() => console.log('Sup'), 4000)"
```

This will produce a report called `something.csv`
Expand All @@ -54,36 +52,36 @@ time_s,cpu,memory_mb,disk_read,disk_write

## MacOS

### Upgrading

```bash
mkdir -p $HOME/.local/procplot
curl -L --url https://github.com/alshdavid/procplot/releases/latest/download/macos-arm64.tar.gz | tar -xvzf - -C $HOME/.local/procplot
echo "\nexport \PATH=\$PATH:\$HOME/.local/procplot\n" >> $HOME/.zshrc
source $HOME/.zshrc
procplot upgrade
```

#### Updating
### Install script

```bash
rm -rf $HOME/.local/procplot
mkdir -p $HOME/.local/procplot
curl -L --url https://github.com/alshdavid/procplot/releases/latest/download/macos-arm64.tar.gz | tar -xvzf - -C $HOME/.local/procplot
curl -L --url https://github.com/alshlabs/procplot/releases/latest/download/procplot-macos-arm64.tar.gz | tar -xvzf - -C $HOME/.local/procplot
echo "\nexport \PATH=\$PATH:\$HOME/.local/procplot\n" >> $HOME/.zshrc
source $HOME/.zshrc
```

## Linux

### Upgrading

```bash
mkdir -p $HOME/.local/procplot
curl -L --url https://github.com/alshdavid/procplot/releases/latest/download/linux-amd64.tar.gz | tar -xvzf - -C $HOME/.local/procplot
echo "\nexport \PATH=\$PATH:\$HOME/.local/procplot\n" >> $HOME/.zshrc
source $HOME/.zshrc
procplot upgrade
```

#### Updating
### Install script

```bash
rm -rf $HOME/.local/procplot
mkdir -p $HOME/.local/procplot
curl -L --url https://github.com/alshdavid/procplot/releases/latest/download/linux-amd64.tar.gz | tar -xvzf - -C $HOME/.local/procplot
curl -L --url https://github.com/alshlabs/procplot/releases/latest/download/procplot-linux-amd64.tar.gz | tar -xvzf - -C $HOME/.local/procplot
echo "\nexport \PATH=\$PATH:\$HOME/.local/procplot\n" >> $HOME/.zshrc
source $HOME/.zshrc
```

## Credit
Expand Down
File renamed without changes.
212 changes: 212 additions & 0 deletions src/cmd/exec/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
use std::env;
use std::process::Command;
use std::process::Stdio;
use std::sync::mpsc::channel;
use std::thread;
use std::time::Duration;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;

use anyhow::Context;
use sysinfo::Pid;
use sysinfo::ProcessesToUpdate;
use sysinfo::System;

use crate::cmd::exec::args::MemoryUnits;
use crate::cmd::exec::args::TimeUnits;
use crate::cmd::exec::reporter::Columns;
use crate::cmd::exec::reporter::Reporter;
use crate::cmd::exec::reporter::Row;

#[derive(Debug, Clone, clap::Args)]
// The flags are user-facing CLI options; grouping them into a sub-struct would
// only complicate the interface.
#[allow(clippy::struct_excessive_bools)]
pub struct ExecCommand {
/// Command to run. Use `--` to separate procplot's options from the command
#[clap(allow_hyphen_values = true, trailing_var_arg = true)]
pub command: Vec<String>,

/// Output path for the generated report
#[clap(short = 'r', long = "report", env = "PM_REPORT", value_parser = parse_file_path, default_value = "procplot.csv")]
pub report_path: std::path::PathBuf,

/// How often to probe the process for details in milliseconds
#[clap(short = 'i', long = "interval", env = "PM_INTERVAL", value_parser = parse_duration, default_value = "200")]
pub poll_interval: Duration,

/// What units to use for recording memory
#[clap(
short = 'm',
long = "mem-units",
env = "PM_MEM_UNITS",
value_enum,
default_value = "mb"
)]
pub mem_units: MemoryUnits,

/// What units to use for recording time
#[clap(
short = 't',
long = "time-units",
env = "PM_TIME_UNITS",
value_enum,
default_value = "ms"
)]
pub time_units: TimeUnits,

/// Override report file if exists
#[clap(short = 'o', long = "override-report")]
pub override_report: bool,

/// Don't measure CPU usage
#[clap(long = "no-cpu", env = "PM_NO_CPU")]
pub no_measure_cpu: bool,

/// Don't measure memory usage
#[clap(long = "no-memory", env = "PM_NO_MEMORY")]
pub no_measure_mem: bool,

/// Don't measure disk usage
#[clap(long = "no-disk", env = "PM_NO_DISK")]
pub no_measure_disk: bool,
}

fn parse_file_path(arg: &str) -> anyhow::Result<std::path::PathBuf> {
let mut target_path = std::path::PathBuf::from(arg);
if target_path.is_relative() {
target_path = env::current_dir()?.join(target_path);
}
Ok(target_path)
}

fn parse_duration(arg: &str) -> Result<Duration, std::num::ParseIntError> {
let millis = arg.parse()?;
Ok(Duration::from_millis(millis))
}

// Sysinfo reports a non-negative percentage, so clamping and truncating are safe
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn cpu_usage(cpu: f32) -> u64 {
cpu.round().max(0.0) as u64
}

pub fn main(args: &ExecCommand) -> anyhow::Result<()> {
if args.command.is_empty() {
anyhow::bail!("No command provided. Usage: procplot exec [OPTIONS] -- <COMMAND>");
}

let reporter = Reporter::new(
&args.report_path,
args.override_report,
Columns {
cpu: !args.no_measure_cpu,
memory: !args.no_measure_mem,
disk: !args.no_measure_disk,
},
args.clone(),
)?;

let (sender, receiver) = channel::<(u32, Duration)>();

// Monitor
let h0: thread::JoinHandle<anyhow::Result<()>> = {
let args = args.clone();
let reporter = reporter.clone();

thread::spawn(move || {
let (pid, start_time) = receiver.recv()?;
let pid = Pid::from(pid as usize);

// Please note that we use "new_all" to ensure that all list of
// components, network interfaces, disks and users are already filled!
let mut sys = System::new_all().context("Unable to initialise system")?;

loop {
if sys.refresh_processes(ProcessesToUpdate::Some(&[pid]), true) == 0 {
break;
}
let info = sys.process(pid).context("Unable to get process")?;

let now = SystemTime::now().duration_since(UNIX_EPOCH)?;
let mut row = Row::default();

if !args.no_measure_cpu {
row.cpu = Some(cpu_usage(info.cpu_usage()));
}

if !args.no_measure_mem {
row.memory = Some(info.memory());
}

if !args.no_measure_disk {
let disk = info.disk_usage();
row.disk_read = Some(disk.read_bytes);
row.disk_write = Some(disk.written_bytes);
}

row.time = now
.checked_sub(start_time)
.context("Clock moved backwards")?;

reporter.write(row)?;
thread::sleep(args.poll_interval);
}

Ok(())
})
};

// Process
let h1: thread::JoinHandle<anyhow::Result<()>> = {
let args = args.clone();
let reporter = reporter.clone();

thread::spawn(move || {
let mut command_line = args.command.clone();
let program = command_line.remove(0);

let start_time = SystemTime::now().duration_since(UNIX_EPOCH)?;

reporter.write(Row {
time: Duration::from_millis(0),
memory: Some(0),
cpu: Some(0),
disk_read: Some(0),
disk_write: Some(0),
})?;

let mut child = Command::new(program)
.args(command_line)
.current_dir(env::current_dir()?)
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()?;

sender.send((child.id(), start_time))?;
child.wait()?;

let end_time = SystemTime::now().duration_since(UNIX_EPOCH)?;

reporter.write(Row {
time: end_time
.checked_sub(start_time)
.context("Clock moved backwards")?,
memory: Some(0),
cpu: Some(0),
disk_read: Some(0),
disk_write: Some(0),
})?;

Ok(())
})
};

h1.join()
.map_err(|_| anyhow::anyhow!("Process thread panicked"))??;
h0.join()
.map_err(|_| anyhow::anyhow!("Monitor thread panicked"))??;

Ok(())
}
6 changes: 6 additions & 0 deletions src/cmd/exec/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pub mod args;
mod main;
pub mod reporter;

pub use self::main::ExecCommand;
pub use self::main::main;
12 changes: 7 additions & 5 deletions src/reporter.rs → src/cmd/exec/reporter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ use std::sync::mpsc::channel;
use std::thread::JoinHandle;
use std::time::Duration;

use crate::args::Args;
use crate::args::MemoryUnits;
use crate::cmd::exec::ExecCommand;
use crate::cmd::exec::args::MemoryUnits;

#[derive(Debug)]
pub struct Columns {
Expand Down Expand Up @@ -37,7 +37,7 @@ impl Reporter {
report_file: &Path,
override_report: bool,
columns: Columns,
args: Args,
args: ExecCommand,
) -> anyhow::Result<Self> {
if !override_report && std::fs::exists(report_file)? {
return Err(anyhow::anyhow!(
Expand Down Expand Up @@ -84,8 +84,10 @@ impl Reporter {

line.push({
match args.time_units {
crate::args::TimeUnits::S => format!("{:.3}", args.time_units.to_f64(row.time)),
crate::args::TimeUnits::Ms => format!("{}", row.time.as_millis()),
crate::cmd::exec::args::TimeUnits::S => {
format!("{:.3}", args.time_units.to_f64(row.time))
}
crate::cmd::exec::args::TimeUnits::Ms => format!("{}", row.time.as_millis()),
}
});

Expand Down
2 changes: 2 additions & 0 deletions src/cmd/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod exec;
pub mod upgrade;
2 changes: 2 additions & 0 deletions src/cmd/upgrade/constants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub const BIN_NAME: &str = "procplot";
pub const TARGET_REPO: &str = "alshlabs/procplot";
Loading
Loading