Skip to content

Commit

Permalink
Use strings feature of clap crate to handle --duration option properly
Browse files Browse the repository at this point in the history
  • Loading branch information
gmarler committed Mar 23, 2024
1 parent 1f547f1 commit 5e74a3b
Showing 1 changed file with 67 additions and 81 deletions.
148 changes: 67 additions & 81 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,11 @@
use clap::ArgAction;
use clap::Parser;

use inferno::flamegraph;
use std::fmt::Write;
use std::fs::File;
use primal::is_prime;
use std::ops::RangeInclusive;
use tracing::Level;
use tracing_subscriber::fmt::format::FmtSpan;
use tracing_subscriber::FmtSubscriber;

use lightswitch::object::build_id;
use lightswitch::profiler::Collector;
use lightswitch::profiler::Profiler;
use lightswitch::unwind_info::{compact_printing_callback, UnwindInfoBuilder};
use primal::is_prime;
use std::error::Error;
use std::path::PathBuf;

use std::time::Duration;

const SAMPLE_FREQ_RANGE: RangeInclusive<usize> = 1..=1009;
Expand All @@ -38,19 +27,11 @@ fn sample_freq_in_range(s: &str) -> Result<u16, String> {
));
}
if !is_prime(sample_freq.try_into().unwrap()) {
return Err(format!("sample frequency is not prime"));
return Err("sample frequency is not prime".to_string());
}
Ok(sample_freq as u16)
}

// Used to prevent the default value for duration from being dropped before
// we parse the command line args - probably a better way to do this
fn max_duration_as_str() -> &'static str {
let max_duration: &'static str =
Box::leak(Duration::MAX.as_secs().to_string().into_boxed_str());
max_duration
}

#[derive(Parser, Debug)]
struct Cli {
/// Specific PIDs to profile
Expand All @@ -72,13 +53,15 @@ struct Cli {
)]
show_info: Option<String>,
/// How long this agent will run in seconds
#[arg(short='D', long, default_value = max_duration_as_str(), value_parser = parse_duration)]
// #[arg(short='D', long, default_value = max_duration_as_str(), value_parser = parse_duration)]
#[arg(short='D', long, default_value = Duration::MAX.as_secs().to_string(),
value_parser = parse_duration)]
duration: Duration,
// Enable TRACE (max) level logging - defaults to INFO level otherwise
/// Enable TRACE (max) level logging - defaults to INFO level otherwise
#[arg(long, action=ArgAction::SetFalse)]
filter_logs: bool,
// Verification for this option guarantees the only possible selections
// are prime numbers up to and including 1001
// are prime numbers up to and including 1009
/// Per-CPU Sampling Frequency in Hz
#[arg(long, default_value_t = 19, value_name = "SAMPLE_FREQ_IN_HZ",
value_parser = sample_freq_in_range,
Expand All @@ -90,68 +73,14 @@ struct Cli {
}

fn main() -> Result<(), Box<dyn Error>> {
let args = Cli::parse();

let subscriber = FmtSubscriber::builder()
.with_max_level(if args.filter_logs {
Level::TRACE
} else {
Level::INFO
})
.with_span_events(FmtSpan::ENTER | FmtSpan::CLOSE)
.finish();

tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");

if let Some(path) = args.show_unwind_info {
UnwindInfoBuilder::with_callback(&path, compact_printing_callback)?.process()?;
return Ok(());
}

if let Some(path) = args.show_info {
println!("build id {:?}", build_id(&PathBuf::from(path.clone())));
let unwind_info: Result<UnwindInfoBuilder<'_>, anyhow::Error> =
UnwindInfoBuilder::with_callback(&path, |_| {});
println!("unwind info {:?}", unwind_info.unwrap().process());

return Ok(());
}

let collector = Collector::new();

let mut p: Profiler<'_> = Profiler::new(false, args.duration, args.sample_freq);
p.profile_pids(args.pids);
p.run(collector.clone());

let profiles = collector.lock().unwrap().finish();

let mut folded = String::new();
for profile in profiles {
for sample in profile {
let stack = sample
.ustack
.clone()
.into_iter()
.rev()
.collect::<Vec<String>>();
let stack = stack.join(";");
let count: String = sample.count.to_string();

writeln!(folded, "{} {}", stack, count).unwrap();
}
}

let mut options: flamegraph::Options<'_> = flamegraph::Options::default();
let data = folded.as_bytes();
let flame_path = args.flamegraph_file;
let f = File::create(flame_path).unwrap();
flamegraph::from_reader(&mut options, data, f).unwrap();
let _args = Cli::parse();

Ok(())
}

#[cfg(test)]
mod tests {
use super::Cli;
use super::*;
use assert_cmd::Command;
use expect_test::expect;
Expand All @@ -163,5 +92,62 @@ mod tests {
}

#[test]
fn duration_arg() {}
fn cli_help() {
let mut cmd = Command::cargo_bin("claptests").unwrap();

cmd.arg("--help");
let expected = expect![[r#"
"Usage: claptests [OPTIONS]\n\nOptions:\n --pids <PIDS>\n Specific PIDs to profile\n --tid <TID>\n Specific TIDs to profile (these can be outside the PIDs selected above)\n --show-unwind-info <SHOW_UNWIND_INFO>\n \n --show-info <SHOW_INFO>\n \n -D, --duration <DURATION>\n How long this agent will run in seconds [default: 18446744073709551615]\n --filter-logs\n \n --sample-freq <SAMPLE_FREQ>\n Per-CPU Sampling Frequency in Hz [default: 19]\n -o, --output-file <OUTPUT_FILE>\n Output file for Flame Graph in SVG format [default: flame.svg]\n -h, --help\n Print help\n"
"#]];
cmd.assert().success();
let actual = String::from_utf8(cmd.unwrap().stdout).unwrap();
expected.assert_debug_eq(&actual);
// cmd.assert().stdout(predicate::str::contains("junk"));
}

#[test]
fn sample_freq_test() {
let _expected = expect![""];
let argname = "--sample-freq";
let myargs = vec!["claptests", "--sample-freq", "19"];

let result = Cli::try_parse_from(myargs);
let args = match result {
Ok(args) => args,
Err(error) => {
println!("Error: {:?}", error);
return;
}
};
// let args = result.unwrap();
// assert_eq!(val.sample_freq, 19);
// assert!(result.is_ok());

// Test a frequency below the range, which is also prime
if Cli::try_parse_from(vec![argname, "-101"].iter()).is_ok() {
panic!("sample frequency below range should fail");
}
if Cli::try_parse_from([argname, "19"].iter()).is_err() {
panic!("sample frequency in range and prime should succeed");
}
if Cli::try_parse_from(vec![argname, "20"].iter()).is_ok() {
panic!("sample frequency in range but not prime should fail");
}
if Cli::try_parse_from(vec![argname, "49"].iter()).is_ok() {
panic!("sample frequency in range but not prime should fail");
}
if Cli::try_parse_from(vec![argname, "101"].iter()).is_err() {
panic!("sample frequency in range and prime should succeed");
}
if Cli::try_parse_from(vec![argname, "1009"].iter()).is_err() {
panic!("sample frequency in range and prime should succeed");
}
if Cli::try_parse_from(vec![argname, "1010"].iter()).is_ok() {
panic!("sample frequency out of range and not prime should fail");
}
if Cli::try_parse_from(vec![argname, "1013"].iter()).is_ok() {
panic!("sample frequency out of range and prime should fail");
}
// Expected output/results for various inputs
}
}

0 comments on commit 5e74a3b

Please sign in to comment.