use crate::logger;
use clap::ArgMatches;
use rjprof::{
configure_profiler, get_spring_excludes, ExportFormat, ProfileMode, ProfilerConfig, SortOption,
};
use std::env;
use std::fs;
use std::path::Path;
use std::process::{Command as ProcessCommand, Stdio};
pub fn parse_config(matches: &ArgMatches) -> Result<ProfilerConfig, String> {
let mut config = ProfilerConfig::default();
if let Some(pid_str) = matches.get_one::<String>("experimental-pid") {
config.target_pid = Some(
pid_str
.parse()
.map_err(|_| format!("Invalid PID: {}", pid_str))?,
);
config.jar_file = String::new(); } else {
config.jar_file = matches
.get_one::<String>("jar")
.ok_or("JAR file is required when not using --experimental-pid")?
.clone();
if !Path::new(&config.jar_file).exists() {
return Err(format!("JAR file not found: {}", config.jar_file));
}
}
if let Some(opts) = matches.get_many::<String>("java-opts") {
config.java_opts = opts.cloned().collect();
}
config.stack_size = matches.get_one::<String>("stack-size").unwrap().clone();
config.output_dir = matches.get_one::<String>("output").unwrap().clone();
config.java_executable = matches
.get_one::<String>("java-executable")
.unwrap()
.clone();
if let Some(agent_path) = matches.get_one::<String>("agent-path") {
let path = Path::new(agent_path);
if path.is_absolute() {
config.agent_path = agent_path.clone();
} else {
let current_dir = env::current_dir()
.map_err(|e| format!("Failed to get current directory: {}", e))?;
config.agent_path = current_dir.join(path).to_string_lossy().to_string();
}
} else {
config.agent_path = detect_agent_path()?;
}
if !Path::new(&config.agent_path).exists() {
return Err(format!("Agent library not found: {}", config.agent_path));
}
config.flamegraph = !matches.get_flag("no-flamegraph");
config.allocation_tracking = !matches.get_flag("no-allocation");
config.call_graph = !matches.get_flag("no-call-graph");
if let Some(interval) = matches.get_one::<String>("sampling-interval") {
config.sampling_interval = Some(interval.parse().map_err(|_| "Invalid sampling interval")?);
}
if let Some(sort) = matches.get_one::<String>("sort") {
config.sort_by = match sort.as_str() {
"total" => SortOption::TotalTime,
"self" => SortOption::SelfTime,
"calls" => SortOption::Calls,
"name" => SortOption::Name,
"percentage" | "pct" => SortOption::Percentage,
_ => {
return Err(format!(
"Invalid sort option: {}. Use: total, self, calls, name, percentage",
sort
))
}
};
}
if let Some(min_total) = matches.get_one::<String>("min-total") {
config.min_total_ns = Some(parse_time_to_nanos(min_total)?);
}
if let Some(min_pct) = matches.get_one::<String>("min-percentage") {
config.min_percentage = Some(min_pct.parse().map_err(|_| "Invalid percentage value")?);
}
config.colorized = !matches.get_flag("no-color");
config.human_readable = !matches.get_flag("raw-times");
if let Some(format) = matches.get_one::<String>("export") {
config.export_format = Some(match format.as_str() {
"json" => ExportFormat::Json,
"csv" => ExportFormat::Csv,
_ => return Err(format!("Invalid export format: {}. Use: json, csv", format)),
});
}
if matches.get_flag("spring") {
config.exclude_packages = get_spring_excludes();
} else if let Some(excludes) = matches.get_many::<String>("exclude") {
config.exclude_packages = excludes.cloned().collect();
}
if let Some(includes) = matches.get_many::<String>("include") {
config.include_packages = includes.cloned().collect();
}
if let Some(min_self) = matches.get_one::<String>("min-self-time") {
config.min_self_time_ns = Some(parse_time_to_nanos(min_self)?);
}
if let Some(mode) = matches.get_one::<String>("mode") {
config.profile_mode = match mode.as_str() {
"all" => ProfileMode::All,
"user" | "usercode" => ProfileMode::UserCode,
"hotspots" => ProfileMode::Hotspots,
"allocation" | "alloc" => ProfileMode::Allocation,
_ => {
return Err(format!(
"Invalid profile mode: {}. Use: all, user, hotspots, allocation",
mode
))
}
};
}
Ok(config)
}
fn parse_time_to_nanos(time_str: &str) -> Result<u64, String> {
let time_str = time_str.trim().to_lowercase();
if let Some(pos) = time_str.find(|c: char| c.is_alphabetic()) {
let (number_part, unit_part) = time_str.split_at(pos);
let number: f64 = number_part.parse().map_err(|_| "Invalid time number")?;
let multiplier = match unit_part {
"ns" => 1,
"us" | "μs" => 1_000,
"ms" => 1_000_000,
"s" => 1_000_000_000,
_ => {
return Err(format!(
"Invalid time unit: {}. Use: ns, us, ms, s",
unit_part
))
}
};
Ok((number * multiplier as f64) as u64)
} else {
time_str
.parse()
.map_err(|_| "Invalid time value".to_string())
}
}
pub fn detect_agent_path() -> Result<String, String> {
let current_dir =
env::current_dir().map_err(|e| format!("Failed to get current directory: {}", e))?;
let possible_paths = vec![
current_dir.join("target/release/librjprof.dylib"),
current_dir.join("target/release/librjprof.so"),
current_dir.join("target/release/rjprof.dll"),
current_dir.join("target/debug/librjprof.dylib"),
current_dir.join("target/debug/librjprof.so"),
current_dir.join("target/debug/rjprof.dll"),
];
for path in possible_paths {
if path.exists() {
return Ok(path.to_string_lossy().to_string());
}
}
Err("Could not find profiler agent library. Please specify with --agent-path".to_string())
}
pub fn run_profiler(config: &ProfilerConfig, verbose: bool) -> Result<(), String> {
rjprof::set_profiler_config(config.clone());
if let Err(e) = fs::create_dir_all(&config.output_dir) {
return Err(format!("Failed to create output directory: {}", e));
}
let original_dir =
env::current_dir().map_err(|e| format!("Failed to get current directory: {}", e))?;
env::set_current_dir(&config.output_dir)
.map_err(|e| format!("Failed to change to output directory: {}", e))?;
let mut java_cmd = ProcessCommand::new(&config.java_executable);
java_cmd.arg(format!("-agentpath:{}", config.agent_path));
java_cmd.arg(format!("-Xss{}", config.stack_size));
for opt in &config.java_opts {
java_cmd.arg(opt);
}
java_cmd.arg("-jar");
java_cmd.arg(Path::new(&original_dir).join(&config.jar_file));
if verbose {
logger::get_logger().debug(&format!("Executing command: {:?}", java_cmd));
}
let output = java_cmd
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.map_err(|e| format!("Failed to execute Java command: {}", e))?;
env::set_current_dir(original_dir)
.map_err(|e| format!("Failed to restore original directory: {}", e))?;
if !output.status.success() {
return Err(format!(
"Java process failed with exit code: {:?}",
output.status.code()
));
}
Ok(())
}
pub fn generate_flamegraph_svg(config: &ProfilerConfig) -> Result<(), String> {
let folded_path = Path::new(&config.output_dir).join("flamegraph.folded");
let svg_path = Path::new(&config.output_dir).join("flamegraph.svg");
if !folded_path.exists() {
return Err("flamegraph.folded file not found".to_string());
}
let flamegraph_commands = vec![
(
"flamegraph.pl",
vec![folded_path.to_string_lossy().to_string()],
),
(
"inferno-flamegraph",
vec![folded_path.to_string_lossy().to_string()],
),
];
for (cmd, args) in flamegraph_commands {
let mut command = ProcessCommand::new(cmd);
command.args(&args);
command.stdout(Stdio::piped());
command.stderr(Stdio::piped());
match command.output() {
Ok(output) => {
if output.status.success() {
fs::write(&svg_path, output.stdout)
.map_err(|e| format!("Failed to write SVG file: {}", e))?;
logger::get_logger()
.result(&format!("Flamegraph SVG generated: {}", svg_path.display()));
return Ok(());
} else {
eprintln!(
"Command '{}' failed: {}",
cmd,
String::from_utf8_lossy(&output.stderr)
);
}
}
Err(_) => {
continue;
}
}
}
Err("No flamegraph generator found. Install flamegraph.pl or inferno-flamegraph".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use clap::{Arg, Command};
#[test]
fn test_config_parsing() {
let matches = Command::new("rjprof")
.arg(Arg::new("jar").short('j').long("jar").required(true))
.try_get_matches_from(vec!["rjprof", "-j", "test.jar"])
.unwrap();
}
#[test]
fn test_agent_path_detection() {
let _result = detect_agent_path();
}
}