use crate::wrapper_names_old_and_new::WORKTREE_ADD_TIMING_LOG;
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 = WORKTREE_ADD_TIMING_LOG.current;
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,
}
fn add_spent(list: &mut Vec<Spent>, name: &str, own: Duration) {
for known in list.iter_mut() {
if known.name == name {
known.own += own;
known.calls += 1;
return;
}
}
list.push(Spent { name: name.to_string(), own, calls: 1 });
}
fn micros_to_ms(time: Duration) -> String {
return format!("{:.3}", time.as_micros() as f64 / 1000.0);
}
fn spent_json(each: &Spent) -> String {
return format!("{}:{{\"ms\":{},\"calls\":{}}}", quote(each.name.as_str()), micros_to_ms(each.own), each.calls);
}
fn note_json((key, value): &(String, String)) -> String {
return format!("{}:{}", quote(key.as_str()), quote(value.as_str()));
}
#[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 = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
Ok(elapsed) => elapsed.as_millis(),
Err(_before_epoch) => 0,
};
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;
}
add_spent(&mut self.spent, frame.name.as_str(), own);
}
pub fn note(&mut self, key: &str, value: &str) {
for known in &mut self.notes {
if known.0 == key {
known.1 = value.to_string();
return;
}
}
self.notes.push((key.to_string(), value.to_string()));
}
pub fn add_work(&mut self, name: &str, own: Duration) {
add_spent(&mut self.work, name, own);
}
pub fn line(&self, total: Duration, exit_code: i32) -> String {
let mut covered: Duration = Duration::ZERO;
for each in &self.spent {
covered += each.own;
}
let steps: Vec<String> = self.spent.iter().map(spent_json).collect();
let work: Vec<String> = self.work.iter().map(spent_json).collect();
let notes: Vec<String> = self.notes.iter().map(note_json).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.set(Some((Instant::now(), Recorder::new(common_dir))));
if let Ok(mut work) = WORK.lock() {
*work = Some(Vec::new());
}
}
pub fn begin_step(name: &str) -> bool {
let mut recording: Option<(Instant, Recorder)> = ACTIVE.take();
let entered: bool = match recording.as_mut() {
Some((began, recorder)) => {
recorder.enter(name, began.elapsed());
true
}
None => false,
};
ACTIVE.set(recording);
return entered;
}
pub fn end_step(entered: bool) {
if entered {
let mut recording: Option<(Instant, Recorder)> = ACTIVE.take();
if let Some((began, recorder)) = recording.as_mut() {
recorder.leave(began.elapsed());
}
ACTIVE.set(recording);
}
}
fn option_takes_value(word: &OsStr) -> bool {
for known in ["-C", "-c", "--git-dir", "--work-tree", "--namespace", "--exec-path"] {
if word == OsStr::new(known) {
return true;
}
}
return false;
}
pub fn git_step_label(arguments: &[OsString]) -> String {
let mut words = arguments.iter().map(OsString::as_os_str);
while let Some(word) = words.next() {
if option_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 begin_work() -> Instant {
return Instant::now();
}
pub fn end_work(name: &str, began: Instant) {
let spent: Duration = began.elapsed();
if let Ok(mut work) = WORK.lock()
&& let Some(list) = work.as_mut()
{
list.push((name.to_string(), spent));
}
}
pub fn note(key: &str, value: &str) {
let mut recording: Option<(Instant, Recorder)> = ACTIVE.take();
if let Some((_began, recorder)) = recording.as_mut() {
recorder.note(key, value);
}
ACTIVE.set(recording);
}
pub fn finish(exit_code: i32) {
let taken: Option<(Instant, Recorder)> = ACTIVE.take();
let drained: Option<Vec<(String, Duration)>> = match WORK.lock() {
Ok(mut work) => work.take(),
Err(_poisoned) => None,
};
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;