use std::collections::{BTreeMap, HashMap};
pub(super) const TRACED_CALLS: &str = "execve,clone,clone3,fork,vfork,fsync,fdatasync,syncfs,sync_file_range";
const MAX_PARENT_HOPS: usize = 64;
#[derive(Clone, Debug, Eq, PartialEq)]
pub(super) enum Program {
Harness,
Wrapper,
Git(String),
Other(String),
}
#[derive(Debug)]
enum Event {
Exec { tid: u32, program: Program, command: String },
Spawn { parent: u32, child: u32 },
Sync { tid: u32 },
}
#[derive(Debug, Default)]
pub(super) struct Counts {
pub(super) git_starts: BTreeMap<String, usize>,
pub(super) other_starts: BTreeMap<String, usize>,
pub(super) wrapper_syncs: usize,
pub(super) git_syncs: BTreeMap<String, usize>,
pub(super) other_syncs: BTreeMap<String, usize>,
pub(super) commands: Vec<String>,
}
fn quoted(text: &str) -> Option<(String, &str)> {
let body: &str = text.strip_prefix('"')?;
let mut decoded: String = String::new();
let mut escaped: bool = false;
for (position, character) in body.char_indices() {
if escaped {
decoded.push(character);
escaped = false;
} else if character == '\\' {
escaped = true;
} else if character == '"' {
let rest: &str = &body[position + 1..];
return Some((decoded, rest.strip_prefix("...").unwrap_or(rest)));
} else {
decoded.push(character);
}
}
return None;
}
fn arguments(text: &str) -> Vec<String> {
let mut found: Vec<String> = Vec::new();
let mut rest: &str = text;
while let Some((argument, after)) = quoted(rest) {
found.push(argument);
rest = after.strip_prefix(", ").unwrap_or(after);
}
return found;
}
fn takes_value(token: &str) -> bool {
return matches!(token, "-c" | "-C" | "--git-dir" | "--work-tree" | "--namespace" | "--config-env" | "--super-prefix");
}
const COMPOUND: [&str; 3] = ["hook", "maintenance", "multi-pack-index"];
fn git_key(arguments_of_start: &[String]) -> String {
let first: &str = arguments_of_start.first().map_or("git", String::as_str);
let program_name: &str = first.rsplit('/').next().unwrap_or(first);
if let Some(dashed) = program_name.strip_prefix("git-") {
return String::from(dashed);
}
let mut operands: Vec<&str> = Vec::new();
let mut index: usize = 1;
while index < arguments_of_start.len() {
let token: &str = arguments_of_start[index].as_str();
if takes_value(token) {
index += 2;
} else {
if !token.starts_with('-') {
operands.push(token);
}
index += 1;
}
}
let Some(subcommand) = operands.first() else {
return String::from("(no subcommand)");
};
if COMPOUND.contains(subcommand)
&& let Some(operand) = operands.get(1)
{
return format!("{subcommand} {operand}");
}
return String::from(*subcommand);
}
fn program_of(path: &str, arguments_of_start: &[String], fixture_root: &str, real_git: &str) -> Program {
if path.starts_with(fixture_root) {
return Program::Wrapper;
}
if path == real_git || path.contains("git-core") {
return Program::Git(git_key(arguments_of_start));
}
let name: &str = path.rsplit('/').next().unwrap_or(path);
if name == "timeout" {
return Program::Harness;
}
return Program::Other(String::from(name));
}
fn result_of(rest: &str) -> Option<&str> {
return rest.rsplit_once(") = ").map(|(_call, result)| { return result; });
}
fn parse_line(line: &str, fixture_root: &str, real_git: &str) -> Option<Event> {
let (id, remainder) = line.split_once(char::is_whitespace)?;
let tid: u32 = id.parse().ok()?;
let call: &str = remainder.trim_start();
if let Some(execve) = call.strip_prefix("execve(") {
if result_of(call).is_some_and(|result| { return result.starts_with('-'); }) {
return None;
}
let (path, after) = quoted(execve)?;
let argv: Vec<String> = arguments(after.strip_prefix(", [")?);
let program: Program = program_of(path.as_str(), argv.as_slice(), fixture_root, real_git);
return Some(Event::Exec { tid, program, command: argv.join(" ") });
}
let spawning: bool = ["clone(", "clone3(", "fork(", "vfork(", "<... clone resumed>", "<... clone3 resumed>",
"<... fork resumed>", "<... vfork resumed>"]
.iter()
.any(|prefix| { return call.starts_with(prefix); });
if spawning {
let result: &str = result_of(call)?;
let digits: String = result.chars().take_while(char::is_ascii_digit).collect();
let child: u32 = digits.parse().ok()?;
return Some(Event::Spawn { parent: tid, child });
}
let syncing: bool = ["fsync(", "fdatasync(", "syncfs(", "sync_file_range(", "<... fsync resumed>",
"<... fdatasync resumed>", "<... syncfs resumed>", "<... sync_file_range resumed>"]
.iter()
.any(|prefix| { return call.starts_with(prefix); });
if syncing {
if call.starts_with("<...") {
return None;
}
return Some(Event::Sync { tid });
}
return None;
}
fn program_at<'a>(
starts: &'a HashMap<u32, Vec<(usize, Program)>>,
parents: &HashMap<u32, u32>,
tid: u32,
before: usize,
) -> &'a Program {
let mut current: u32 = tid;
for _hop in 0..MAX_PARENT_HOPS {
if let Some(known) = starts.get(¤t).and_then(|list| {
return list.iter().rev().find(|(position, _program)| { return *position < before; });
}) {
return &known.1;
}
let Some(parent) = parents.get(¤t) else {
panic!("a sync call by thread {current} cannot be attributed: no program start and no parent in the trace");
};
current = *parent;
}
panic!("thread {tid}: parent chain longer than {MAX_PARENT_HOPS}, so the trace is corrupt");
}
pub(super) fn count(trace: &str, fixture_root: &str, real_git: &str) -> Counts {
let events: Vec<Event> = trace
.lines()
.filter_map(|line| { return parse_line(line, fixture_root, real_git); })
.collect();
let mut parents: HashMap<u32, u32> = HashMap::new();
let mut starts: HashMap<u32, Vec<(usize, Program)>> = HashMap::new();
for (position, event) in events.iter().enumerate() {
match event {
Event::Spawn { parent, child } => {
parents.entry(*child).or_insert(*parent);
}
Event::Exec { tid, program, .. } => {
starts.entry(*tid).or_default().push((position, program.clone()));
}
Event::Sync { .. } => {}
}
}
let mut counts: Counts = Counts::default();
for (position, event) in events.iter().enumerate() {
match event {
Event::Exec { program, command, .. } => match program {
Program::Git(key) => {
*counts.git_starts.entry(key.clone()).or_default() += 1;
counts.commands.push(command.clone());
}
Program::Other(name) => *counts.other_starts.entry(name.clone()).or_default() += 1,
Program::Harness | Program::Wrapper => {}
},
Event::Sync { tid } => match program_at(&starts, &parents, *tid, position) {
Program::Wrapper => counts.wrapper_syncs += 1,
Program::Git(key) => *counts.git_syncs.entry(key.clone()).or_default() += 1,
Program::Other(name) => *counts.other_syncs.entry(name.clone()).or_default() += 1,
Program::Harness => panic!("the harness made a sync call, so the trace is not of the command alone"),
},
Event::Spawn { .. } => {}
}
}
return counts;
}