use super::{Backend, Dependency, DependencyCheck, SpawnConfig, shell_escape};
use crate::check::find_bin;
use crate::daemon::session_tmp;
pub struct GhcProfBackend;
impl Backend for GhcProfBackend {
fn name(&self) -> &'static str {
"ghc-profile"
}
fn description(&self) -> &'static str {
"Haskell profiler (GHC)"
}
fn types(&self) -> &'static [&'static str] {
&["haskell-profile", "hs-profile"]
}
fn spawn_config(&self, target: &str, args: &[String]) -> anyhow::Result<SpawnConfig> {
let out_dir = session_tmp("ghcprof");
let out_dir_str = out_dir.display().to_string();
let prof_file = out_dir.join("ghc.prof");
let prof_str = prof_file.display().to_string();
let cg_file = out_dir.join("callgrind.out");
let cg_str = cg_file.display().to_string();
let (binary, compile_cmd) = if target.ends_with(".hs") {
let bin = out_dir.join("profiled");
let bin_str = bin.display().to_string();
let ghc_bin = find_bin("ghc");
let cmd = format!(
"mkdir -p {} && {} -prof -fprof-late -rtsopts -o {} {}",
out_dir_str, shell_escape(&ghc_bin), bin_str, shell_escape(target)
);
(bin_str, Some(cmd))
} else {
(target.to_string(), None)
};
let mut run_cmd = format!(
"cd {} && {} +RTS -p -RTS",
out_dir_str, shell_escape(&binary)
);
if !args.is_empty() {
let escaped_args: Vec<String> = args.iter().map(|a| shell_escape(a)).collect();
run_cmd = format!(
"cd {} && {} {} +RTS -p -RTS",
out_dir_str, shell_escape(&binary), escaped_args.join(" ")
);
}
let rename_cmd = format!(
"mv {}/*.prof {}",
out_dir_str, prof_str
);
let dbg_bin = super::self_exe();
let convert_cmd = format!(
"{} --ghcprof-convert {} {}",
dbg_bin, prof_str, cg_str
);
let exec_repl = format!(
"exec {} --phpprofile-repl {} --profile-prompt 'haskell-profile> '",
dbg_bin, cg_str
);
let mut init_commands = Vec::new();
if let Some(cmd) = compile_cmd {
init_commands.push(cmd);
}
init_commands.push(run_cmd);
init_commands.push(rename_cmd);
init_commands.push(convert_cmd);
init_commands.push(exec_repl);
Ok(SpawnConfig {
bin: "bash".into(),
args: vec!["--norc".into(), "--noprofile".into()],
env: vec![("PS1".into(), "haskell-profile> ".into())],
init_commands,
})
}
fn prompt_pattern(&self) -> &str {
r"haskell-profile> $"
}
fn dependencies(&self) -> Vec<Dependency> {
vec![Dependency {
name: "ghc",
check: DependencyCheck::Binary {
name: "ghc",
alternatives: &["ghc"],
version_cmd: Some(("ghc", &["--version"])),
},
install: "curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh # or: sudo apt install ghc",
}]
}
fn run_command(&self) -> &'static str {
"hotspots"
}
fn quit_command(&self) -> &'static str {
"exit"
}
fn parse_help(&self, _raw: &str) -> String {
"haskell-profile: hotspots, flat, calls, callers, inspect, stats, memory, search, tree, hotpath, focus, ignore, reset, help".to_string()
}
fn adapters(&self) -> Vec<(&'static str, &'static str)> {
vec![("haskell-profile.md", include_str!("../../skills/adapters/haskell-profile.md"))]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn spawn_config_compiles_source() {
let cfg = GhcProfBackend.spawn_config("test.hs", &[]).unwrap();
assert_eq!(cfg.bin, "bash");
assert!(cfg.init_commands[0].contains("ghc -prof"));
assert!(cfg.init_commands[0].contains("test.hs"));
assert!(cfg.init_commands.iter().any(|c| c.contains("--ghcprof-convert")));
assert!(cfg.init_commands.last().unwrap().contains("--phpprofile-repl"));
}
#[test]
fn spawn_config_precompiled_binary() {
let cfg = GhcProfBackend.spawn_config("./myapp", &[]).unwrap();
assert!(cfg.init_commands[0].contains("./myapp"));
assert!(cfg.init_commands[0].contains("+RTS -p -RTS"));
assert!(!cfg.init_commands[0].contains("ghc -prof"));
}
#[test]
fn spawn_config_includes_args() {
let cfg = GhcProfBackend
.spawn_config("test.hs", &["--input".into(), "data.txt".into()])
.unwrap();
let run_cmd = cfg.init_commands.iter().find(|c| c.contains("+RTS")).unwrap();
assert!(run_cmd.contains("--input"));
assert!(run_cmd.contains("data.txt"));
}
#[test]
fn prompt_pattern_matches() {
let re = regex::Regex::new(GhcProfBackend.prompt_pattern()).unwrap();
assert!(re.is_match("haskell-profile> "));
}
#[test]
fn format_breakpoint_empty() {
assert_eq!(GhcProfBackend.format_breakpoint("anything"), "");
}
}