#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum BaseSelection {
ExplicitCommit(String),
MergeBaseWith(String),
}
pub(crate) fn select_base(
explicit_base: Option<&str>,
base_ref_flag: Option<&str>,
env_github_base_ref: Option<&str>,
default_branch: &str,
) -> BaseSelection {
if let Some(commit) = explicit_base {
return BaseSelection::ExplicitCommit(commit.to_string());
}
if let Some(r) = base_ref_flag {
return BaseSelection::MergeBaseWith(r.to_string());
}
if let Some(r) = env_github_base_ref.filter(|s| !s.is_empty()) {
return BaseSelection::MergeBaseWith(format!("origin/{r}"));
}
BaseSelection::MergeBaseWith(default_branch.to_string())
}
use anyhow::{Context, Result};
fn short_hash(repo: &gix::Repository, id: gix::ObjectId) -> String {
let head_oid = repo.head_id().ok().map(|i| i.detach());
let dirty = head_oid
.as_ref()
.and_then(|_| ktstr::test_support::repo_is_dirty(repo))
.unwrap_or(false);
let short = id.to_hex_with_len(7).to_string();
if dirty && head_oid == Some(id) {
format!("{short}-dirty")
} else {
short
}
}
fn rev_parse_commit(repo: &gix::Repository, spec: &str) -> Result<gix::ObjectId> {
let parsed = repo
.rev_parse(spec)
.with_context(|| format!("resolve revision '{spec}'"))?;
match parsed.detach() {
gix::revision::plumbing::Spec::Include(id)
| gix::revision::plumbing::Spec::ExcludeParents(id) => Ok(id),
other => anyhow::bail!("'{spec}' did not resolve to a single commit ({other:?})"),
}
}
pub(crate) fn resolve_baseline(
repo: &gix::Repository,
sel: &BaseSelection,
) -> Result<gix::ObjectId> {
match sel {
BaseSelection::ExplicitCommit(c) => rev_parse_commit(repo, c),
BaseSelection::MergeBaseWith(r) => {
let head = repo.head_id().context("resolve HEAD")?.detach();
let base = rev_parse_commit(repo, r)?;
let mb = repo
.merge_base(head, base)
.with_context(|| format!("compute merge-base(HEAD, {r})"))?;
Ok(mb.detach())
}
}
}
use std::path::{Path, PathBuf};
use std::process::Command;
fn baseline_sidecar_leaf(runs_root_abs: &Path, baseline_short: &str) -> PathBuf {
runs_root_abs.join(format!("perf-delta-baseline-{baseline_short}"))
}
fn worktree_checkout_dir(temp_root: &Path, baseline_short: &str) -> PathBuf {
temp_root.join(format!("ktstr-perf-delta-wt-{baseline_short}"))
}
fn baseline_child_env(sidecar_leaf_abs: &Path) -> Vec<(&'static str, String)> {
vec![
(ktstr::KTSTR_PERF_ONLY_ENV, "1".to_string()),
(
ktstr::KTSTR_SIDECAR_DIR_ENV,
sidecar_leaf_abs.to_string_lossy().into_owned(),
),
]
}
fn worktree_add_argv(wt_dir: &Path, commit_full_hex: &str) -> Vec<String> {
vec![
"worktree".to_string(),
"add".to_string(),
"--detach".to_string(),
wt_dir.to_string_lossy().into_owned(),
commit_full_hex.to_string(),
]
}
fn worktree_remove_argv(wt_dir: &Path) -> Vec<String> {
vec![
"worktree".to_string(),
"remove".to_string(),
"--force".to_string(),
wt_dir.to_string_lossy().into_owned(),
]
}
fn perf_test_argv(kernel: &str, filter: Option<&str>) -> Vec<String> {
let mut v = vec![
"ktstr".to_string(),
"test".to_string(),
"--kernel".to_string(),
kernel.to_string(),
];
if let Some(f) = filter {
v.push("-E".to_string());
v.push(f.to_string());
}
v
}
fn count_sidecars(dir: &Path) -> usize {
std::fs::read_dir(dir)
.into_iter()
.flatten()
.flatten()
.filter(|e| {
e.file_name()
.to_str()
.is_some_and(|n| n.ends_with(".ktstr.json"))
})
.count()
}
struct WorktreeGuard {
repo_root: PathBuf,
wt_dir: PathBuf,
}
impl Drop for WorktreeGuard {
fn drop(&mut self) {
let _ = Command::new("git")
.current_dir(&self.repo_root)
.args(worktree_remove_argv(&self.wt_dir))
.status();
}
}
fn resolve_kernel_for_children(repo_root: &Path, kernel: &str) -> String {
let p = Path::new(kernel);
if p.is_relative() && repo_root.join(p).exists() {
repo_root.join(p).to_string_lossy().into_owned()
} else {
kernel.to_string()
}
}
fn dual_run(
repo_root: &Path,
baseline_full_hex: &str,
baseline_short: &str,
kernel: &str,
filter: Option<&str>,
) -> Result<()> {
let runs_root_abs = repo_root.join(ktstr::test_support::runs_root());
let leaf = baseline_sidecar_leaf(&runs_root_abs, baseline_short);
let wt_dir = worktree_checkout_dir(&std::env::temp_dir(), baseline_short);
let kernel_arg = resolve_kernel_for_children(repo_root, kernel);
let add = Command::new("git")
.current_dir(repo_root)
.args(worktree_add_argv(&wt_dir, baseline_full_hex))
.status()
.with_context(|| format!("spawn `git worktree add` for baseline {baseline_short}"))?;
if !add.success() {
anyhow::bail!(
"`git worktree add {} {baseline_full_hex}` failed ({add}) — \
a leftover worktree from a prior run may need `git worktree prune`",
wt_dir.display(),
);
}
let _guard = WorktreeGuard {
repo_root: repo_root.to_path_buf(),
wt_dir: wt_dir.clone(),
};
println!("perf-delta: running baseline {baseline_short} perf tests in worktree");
let baseline_status = Command::new("cargo")
.current_dir(&wt_dir)
.args(perf_test_argv(&kernel_arg, filter))
.envs(baseline_child_env(&leaf))
.status()
.with_context(|| format!("spawn baseline `cargo ktstr test` in {}", wt_dir.display()))?;
if !baseline_status.success() {
eprintln!(
"perf-delta: warning: baseline perf run exited {baseline_status} — \
comparing the sidecars that were written"
);
}
println!("perf-delta: running HEAD perf tests in the working tree");
let head_status = Command::new("cargo")
.current_dir(repo_root)
.args(perf_test_argv(&kernel_arg, filter))
.env(ktstr::KTSTR_PERF_ONLY_ENV, "1")
.status()
.context("spawn HEAD `cargo ktstr test`")?;
if !head_status.success() {
eprintln!(
"perf-delta: warning: HEAD perf run exited {head_status} — \
comparing the sidecars that were written"
);
}
Ok(())
}
pub(crate) struct PerfDeltaArgs<'a> {
pub base: Option<&'a str>,
pub base_ref: Option<&'a str>,
pub filter: Option<&'a str>,
pub default_branch: &'a str,
pub kernel: Option<&'a str>,
pub dual_run: bool,
pub threshold: Option<f64>,
pub policy: Option<&'a std::path::Path>,
pub phase_display: cli::PhaseDisplayOptions,
pub a_scheduler: Option<&'a str>,
pub b_scheduler: Option<&'a str>,
}
fn config_axis_filters(
a_scheduler: Option<&str>,
b_scheduler: Option<&str>,
has_commit_axis_flag: bool,
) -> Result<Option<crate::stats::BuildCompareFilters>> {
match (a_scheduler, b_scheduler) {
(None, None) => Ok(None),
(Some(a), Some(b)) => {
if has_commit_axis_flag {
anyhow::bail!(
"--a-scheduler/--b-scheduler (config axis) conflicts with the commit axis \
(--base / --base-ref / --dual-run) — pick one axis"
);
}
if a == b {
anyhow::bail!(
"--a-scheduler and --b-scheduler are identical ({a}) — nothing to compare"
);
}
Ok(Some(crate::stats::BuildCompareFilters {
a_scheduler: vec![a.to_string()],
b_scheduler: vec![b.to_string()],
..Default::default()
}))
}
_ => anyhow::bail!("the config axis needs BOTH --a-scheduler and --b-scheduler"),
}
}
pub(crate) fn run(args: &PerfDeltaArgs<'_>) -> Result<i32> {
let policy = cli::ComparisonPolicy::from_cli_flags(args.threshold, args.policy)
.context("resolve --threshold / --policy")?;
let has_commit_axis_flag = args.dual_run || args.base.is_some() || args.base_ref.is_some();
if let Some(build) =
config_axis_filters(args.a_scheduler, args.b_scheduler, has_commit_axis_flag)?
{
let (a, b) = (
args.a_scheduler.unwrap_or(""),
args.b_scheduler.unwrap_or(""),
);
println!("perf-delta: scheduler {b} vs {a} (config axis, same commit)");
println!(
" perf tests: {}",
args.filter.unwrap_or("all performance_mode tests")
);
let (filter_a, filter_b) = build.build();
return cli::compare_partitions(
&filter_a,
&filter_b,
None,
&policy,
None,
false,
&args.phase_display,
);
}
let cwd = std::env::current_dir().context("get cwd")?;
let repo = gix::discover(&cwd).context("discover git repository")?;
let env_base = std::env::var("GITHUB_BASE_REF").ok();
let sel = select_base(
args.base,
args.base_ref,
env_base.as_deref(),
args.default_branch,
);
let baseline_oid = resolve_baseline(&repo, &sel)?;
let baseline = short_hash(&repo, baseline_oid);
let head = short_hash(&repo, repo.head_id().context("resolve HEAD")?.detach());
if baseline == head {
anyhow::bail!(
"baseline ({baseline}) resolves to HEAD — nothing to compare; \
choose a different --base / --base-ref"
);
}
println!("perf-delta: candidate HEAD {head} vs baseline {baseline}");
match &sel {
BaseSelection::ExplicitCommit(c) => println!(" baseline: explicit --base {c}"),
BaseSelection::MergeBaseWith(r) => println!(" baseline: merge-base(HEAD, {r})"),
}
println!(
" perf tests: {}",
args.filter.unwrap_or("all performance_mode tests")
);
if args.dual_run {
let kernel = args
.kernel
.context("--dual-run requires --kernel (the kernel both perf runs boot)")?;
dual_run(
&cwd,
&baseline_oid.to_hex().to_string(),
&baseline,
kernel,
args.filter,
)?;
let leaf = baseline_sidecar_leaf(&cwd.join(ktstr::test_support::runs_root()), &baseline);
if count_sidecars(&leaf) == 0 {
println!(
"perf-delta: no performance_mode sidecars produced at baseline {baseline} \
— nothing to compare (define #[ktstr_test(performance_mode)] tests, or widen \
the -E filter, to enable the delta)"
);
return Ok(0);
}
}
let build = crate::stats::BuildCompareFilters {
a_project_commit: vec![baseline],
b_project_commit: vec![head],
..Default::default()
};
let (filter_a, filter_b) = build.build();
cli::compare_partitions(
&filter_a,
&filter_b,
None,
&policy,
None,
false,
&args.phase_display,
)
}
use ktstr::cli;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn explicit_base_wins_and_skips_merge_base() {
assert_eq!(
select_base(Some("abc123"), Some("main"), Some("release"), "main"),
BaseSelection::ExplicitCommit("abc123".to_string()),
"--base must take precedence over every ref source and skip merge-base",
);
}
#[test]
fn base_ref_flag_beats_env_and_default() {
assert_eq!(
select_base(None, Some("topic"), Some("release"), "main"),
BaseSelection::MergeBaseWith("topic".to_string()),
);
}
#[test]
fn github_base_ref_resolves_to_origin_tracking_ref() {
assert_eq!(
select_base(None, None, Some("release-1.2"), "main"),
BaseSelection::MergeBaseWith("origin/release-1.2".to_string()),
"a PR target branch is the fetched remote-tracking ref",
);
}
#[test]
fn empty_github_base_ref_is_unset_falls_back_to_default() {
assert_eq!(
select_base(None, None, Some(""), "main"),
BaseSelection::MergeBaseWith("main".to_string()),
"empty GITHUB_BASE_REF (non-PR run) must not select an empty ref",
);
}
#[test]
fn no_inputs_falls_back_to_default_branch() {
assert_eq!(
select_base(None, None, None, "main"),
BaseSelection::MergeBaseWith("main".to_string()),
);
}
#[test]
fn baseline_sidecar_leaf_is_distinct_subdir_of_runs_root() {
let root = Path::new("/work/target/ktstr");
let leaf = baseline_sidecar_leaf(root, "abc1234");
assert_eq!(
leaf,
Path::new("/work/target/ktstr/perf-delta-baseline-abc1234")
);
assert_ne!(leaf, baseline_sidecar_leaf(root, "def5678"));
}
#[test]
fn worktree_checkout_dir_under_temp_root() {
assert_eq!(
worktree_checkout_dir(Path::new("/tmp"), "abc1234"),
Path::new("/tmp/ktstr-perf-delta-wt-abc1234"),
);
}
#[test]
fn baseline_child_env_sets_perf_only_and_absolute_sidecar_dir() {
let leaf = Path::new("/work/target/ktstr/perf-delta-baseline-abc1234");
let env = baseline_child_env(leaf);
assert_eq!(
env,
vec![
("KTSTR_PERF_ONLY", "1".to_string()),
(
"KTSTR_SIDECAR_DIR",
"/work/target/ktstr/perf-delta-baseline-abc1234".to_string(),
),
],
);
assert_eq!(env[0].0, ktstr::KTSTR_PERF_ONLY_ENV);
assert_eq!(env[1].0, ktstr::KTSTR_SIDECAR_DIR_ENV);
}
#[test]
fn worktree_add_argv_is_detached_checkout_of_full_oid() {
assert_eq!(
worktree_add_argv(Path::new("/tmp/wt"), "0123456789abcdef"),
vec![
"worktree".to_string(),
"add".to_string(),
"--detach".to_string(),
"/tmp/wt".to_string(),
"0123456789abcdef".to_string(),
],
);
}
#[test]
fn worktree_remove_argv_forces_removal() {
assert_eq!(
worktree_remove_argv(Path::new("/tmp/wt")),
vec![
"worktree".to_string(),
"remove".to_string(),
"--force".to_string(),
"/tmp/wt".to_string(),
],
);
}
#[test]
fn count_sidecars_counts_ktstr_json_and_zero_for_missing() {
let base = std::env::temp_dir().join(format!("ktstr-pd-count-{}", std::process::id()));
assert_eq!(count_sidecars(&base.join("absent")), 0);
std::fs::create_dir_all(&base).expect("mk tempdir");
std::fs::write(base.join("a.ktstr.json"), "{}").expect("write a");
std::fs::write(base.join("b.ktstr.json"), "{}").expect("write b");
std::fs::write(base.join("notes.txt"), "x").expect("write txt");
assert_eq!(
count_sidecars(&base),
2,
"only *.ktstr.json sidecars count, not sibling files",
);
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn perf_test_argv_appends_filter_only_when_present() {
assert_eq!(
perf_test_argv("6.14", None),
vec![
"ktstr".to_string(),
"test".to_string(),
"--kernel".to_string(),
"6.14".to_string(),
],
);
assert_eq!(
perf_test_argv("6.14", Some("test(perf_smoke)")),
vec![
"ktstr".to_string(),
"test".to_string(),
"--kernel".to_string(),
"6.14".to_string(),
"-E".to_string(),
"test(perf_smoke)".to_string(),
],
);
}
#[test]
fn resolve_kernel_absolutizes_relative_existing_path() {
let base = std::env::temp_dir().join(format!("ktstr-pd-kresolve-{}", std::process::id()));
std::fs::create_dir_all(base.join("linux")).expect("mk linux dir");
assert_eq!(
resolve_kernel_for_children(&base, "linux"),
base.join("linux").to_string_lossy(),
);
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn resolve_kernel_passes_version_and_absolute_and_missing_through() {
let repo = Path::new("/no/such/repo-xyz");
assert_eq!(resolve_kernel_for_children(repo, "6.14"), "6.14");
assert_eq!(
resolve_kernel_for_children(repo, "/abs/linux"),
"/abs/linux",
);
assert_eq!(
resolve_kernel_for_children(repo, "../nonexistent-xyz"),
"../nonexistent-xyz",
);
}
#[test]
fn gix_resolution_against_a_temp_repo() {
use std::process::Command;
let dir = std::env::temp_dir().join(format!("ktstr-pd-gitfix-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("mk tempdir");
let run = |args: &[&str]| {
let ok = Command::new("git")
.current_dir(&dir)
.args([
"-c",
"user.email=t@example.invalid",
"-c",
"user.name=t",
"-c",
"commit.gpgsign=false",
])
.args(args)
.status()
.map(|s| s.success())
.unwrap_or(false);
assert!(ok, "git {args:?} failed");
};
run(&["init", "-q"]);
std::fs::write(dir.join("f"), "1").expect("write f");
run(&["add", "."]);
run(&["commit", "-q", "-m", "first"]);
std::fs::write(dir.join("f"), "2").expect("write f");
run(&["add", "."]);
run(&["commit", "-q", "-m", "second"]);
let repo = gix::discover(&dir).expect("discover temp repo");
let head = rev_parse_commit(&repo, "HEAD").expect("rev-parse HEAD");
let first = rev_parse_commit(&repo, "HEAD~1").expect("rev-parse HEAD~1");
assert_ne!(head, first, "HEAD and HEAD~1 are distinct commits");
assert_eq!(
resolve_baseline(&repo, &BaseSelection::ExplicitCommit("HEAD~1".to_string())).unwrap(),
first,
);
assert_eq!(
resolve_baseline(&repo, &BaseSelection::MergeBaseWith("HEAD~1".to_string())).unwrap(),
first,
"merge-base(HEAD, HEAD~1) is HEAD~1",
);
let sh = short_hash(&repo, first);
assert_eq!(sh.len(), 7, "short hash is 7 hex chars: {sh}");
assert!(sh.bytes().all(|b| b.is_ascii_hexdigit()), "hex only: {sh}");
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn config_axis_none_falls_through_to_commit_axis() {
assert!(
config_axis_filters(None, None, false).unwrap().is_none(),
"no scheduler flags -> commit axis (None)",
);
assert!(config_axis_filters(None, None, true).unwrap().is_none());
}
#[test]
fn config_axis_both_set_builds_scheduler_filters() {
let build = config_axis_filters(Some("scx_a"), Some("scx_b"), false)
.unwrap()
.expect("both set -> config axis");
assert_eq!(build.a_scheduler, vec!["scx_a".to_string()]);
assert_eq!(build.b_scheduler, vec!["scx_b".to_string()]);
assert!(build.a_project_commit.is_empty() && build.b_project_commit.is_empty());
let (fa, fb) = build.build();
assert_eq!(fa.schedulers, vec!["scx_a".to_string()]);
assert_eq!(fb.schedulers, vec!["scx_b".to_string()]);
}
#[test]
fn config_axis_rejects_partial_conflict_and_identical() {
assert!(config_axis_filters(Some("scx_a"), None, false).is_err());
assert!(config_axis_filters(None, Some("scx_b"), false).is_err());
assert!(
config_axis_filters(Some("scx_a"), Some("scx_b"), true).is_err(),
"config axis must conflict with the commit axis",
);
assert!(
config_axis_filters(Some("scx_a"), Some("scx_a"), false).is_err(),
"identical schedulers have no contrast",
);
}
}