use crate::diagnostic_log::debug;
use crate::discard_audit::append_line;
use crate::iso_time::now_iso;
use crate::json_record::quote;
use crate::quiet_ledger::{drain, object_json};
use crate::service_protocol::RELEASE;
use std::cell::RefCell;
use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::time::{Duration, Instant};
pub const ADD_TIMING_FILE: &str = "cli-git-worktree-add-timing.jsonl";
pub const ADD_TIMING_SCHEMA_VERSION: u32 = 1;
const TAG: &str = "addTiming";
#[derive(Debug)]
struct Frame {
name: String,
entered: Duration,
inside: Duration,
}
#[derive(Debug)]
struct Spent {
name: String,
own: Duration,
calls: usize,
}
#[derive(Debug)]
pub struct Recorder {
common_dir: PathBuf,
open: Vec<Frame>,
spent: Vec<Spent>,
notes: Vec<(String, String)>,
start_millis: u128,
work: Vec<Spent>,
quiet: Vec<(String, Duration)>,
}
impl Recorder {
pub fn new(common_dir: PathBuf) -> Self {
let start_millis: u128 = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|elapsed: Duration| { return elapsed.as_millis(); }).unwrap_or_default();
return Self { common_dir, open: Vec::new(), spent: Vec::new(), notes: Vec::new(), start_millis, work: Vec::new(), quiet: Vec::new() };
}
pub fn enter(&mut self, name: &str, at: Duration) {
self.open.push(Frame { name: name.to_string(), entered: at, inside: Duration::ZERO });
}
pub fn leave(&mut self, at: Duration) {
let Some(frame) = self.open.pop() else { return };
let whole: Duration = at.saturating_sub(frame.entered);
let own: Duration = whole.saturating_sub(frame.inside);
if let Some(outer) = self.open.last_mut() {
outer.inside += whole;
}
match self.spent.iter_mut().find(|known: &&mut Spent| { return known.name == frame.name; }) {
Some(known) => {
known.own += own;
known.calls += 1;
}
None => self.spent.push(Spent { name: frame.name, own, calls: 1 }),
}
}
pub fn note(&mut self, key: &str, value: &str) {
match self.notes.iter_mut().find(|known: &&mut (String, String)| { return known.0 == key; }) {
Some(known) => known.1 = value.to_string(),
None => self.notes.push((key.to_string(), value.to_string())),
}
}
pub fn add_work(&mut self, name: &str, own: Duration) {
match self.work.iter_mut().find(|known: &&mut Spent| { return known.name == name; }) {
Some(known) => {
known.own += own;
known.calls += 1;
}
None => self.work.push(Spent { name: name.to_string(), own, calls: 1 }),
}
}
pub fn line(&self, total: Duration, exit_code: i32) -> String {
let micros_to_ms = |time: Duration| -> String { return format!("{:.3}", time.as_micros() as f64 / 1000.0); };
let covered: Duration = self.spent.iter().map(|each: &Spent| { return each.own; }).sum();
let steps: Vec<String> = self.spent.iter().map(|each: &Spent| { return format!("{}:{{\"ms\":{},\"calls\":{}}}", quote(each.name.as_str()), micros_to_ms(each.own), each.calls); }).collect();
let work: Vec<String> = self.work.iter().map(|each: &Spent| { return format!("{}:{{\"ms\":{},\"calls\":{}}}", quote(each.name.as_str()), micros_to_ms(each.own), each.calls); }).collect();
let notes: Vec<String> = self.notes.iter().map(|(key, value): &(String, String)| { return format!("{}:{}", quote(key.as_str()), quote(value.as_str())); }).collect();
return format!(
"{{\"schemaVersion\":{ADD_TIMING_SCHEMA_VERSION},\"time\":{},\"startMillis\":{},\"pid\":{},\"release\":{},\"exitCode\":{exit_code},\"totalMs\":{},\"steps\":{{{}}},\"otherMs\":{},\"work\":{{{}}},\"notes\":{{{}}},\"quiet\":{}}}\n",
quote(now_iso().as_str()),
self.start_millis,
std::process::id(),
quote(RELEASE),
micros_to_ms(total),
steps.join(","),
micros_to_ms(total.saturating_sub(covered)),
work.join(","),
notes.join(","),
object_json(self.quiet.as_slice()),
);
}
pub fn common_dir(&self) -> &Path {
return self.common_dir.as_path();
}
}
static WORK: Mutex<Option<Vec<(String, Duration)>>> = Mutex::new(None);
thread_local! {
static ACTIVE: RefCell<Option<(Instant, Recorder)>> = const { RefCell::new(None) };
}
pub fn begin(common_dir: PathBuf) {
ACTIVE.with(|active: &RefCell<Option<(Instant, Recorder)>>| { *active.borrow_mut() = Some((Instant::now(), Recorder::new(common_dir))); });
if let Ok(mut work) = WORK.lock() {
*work = Some(Vec::new());
}
}
pub fn step<T>(name: &str, body: impl FnOnce() -> T) -> T {
let entered: bool = ACTIVE.with(|active: &RefCell<Option<(Instant, Recorder)>>| {
return match active.borrow_mut().as_mut() {
Some((began, recorder)) => {
recorder.enter(name, began.elapsed());
true
}
None => false,
};
});
let value: T = body();
if entered {
ACTIVE.with(|active: &RefCell<Option<(Instant, Recorder)>>| {
if let Some((began, recorder)) = active.borrow_mut().as_mut() {
recorder.leave(began.elapsed());
}
});
}
return value;
}
pub fn git_step_label(arguments: &[OsString]) -> String {
let takes_value = |word: &OsStr| -> bool { return ["-C", "-c", "--git-dir", "--work-tree", "--namespace", "--exec-path"].iter().any(|known: &&str| { return word == OsStr::new(known); }); };
let mut words = arguments.iter().map(OsString::as_os_str);
while let Some(word) = words.next() {
if takes_value(word) {
words.next();
continue;
}
if word.to_string_lossy().starts_with('-') {
continue;
}
return format!("git {}", word.to_string_lossy());
}
return String::from("git");
}
pub fn work<T>(name: &str, body: impl FnOnce() -> T) -> T {
let began: Instant = Instant::now();
let value: T = body();
let spent: Duration = began.elapsed();
if let Ok(mut work) = WORK.lock()
&& let Some(list) = work.as_mut()
{
list.push((name.to_string(), spent));
}
return value;
}
pub fn note(key: &str, value: &str) {
ACTIVE.with(|active: &RefCell<Option<(Instant, Recorder)>>| {
if let Some((_began, recorder)) = active.borrow_mut().as_mut() {
recorder.note(key, value);
}
});
}
pub fn finish(exit_code: i32) {
let taken: Option<(Instant, Recorder)> = ACTIVE.with(|active: &RefCell<Option<(Instant, Recorder)>>| { return active.borrow_mut().take(); });
let drained: Option<Vec<(String, Duration)>> = WORK.lock().ok().and_then(|mut work: std::sync::MutexGuard<'_, Option<Vec<(String, Duration)>>>| { return work.take(); });
let Some((began, mut recorder)) = taken else { return };
for (name, spent) in drained.unwrap_or_default() {
recorder.add_work(name.as_str(), spent);
}
recorder.quiet = drain();
let target: PathBuf = recorder.common_dir().join(ADD_TIMING_FILE);
let line: String = recorder.line(began.elapsed(), exit_code);
if let Err(error) = append_line(target.as_path(), line.as_bytes()) {
debug(TAG, &format!("the add timing record could not be written to {}: {error}", target.display()));
}
}
#[cfg(test)]
#[path = "add_timing_tests.rs"]
mod tests;