use crate::diagnostic_log::debug;
use crate::discard_audit::append_line;
use crate::iso_time::now_iso;
use crate::json_record::{quote, string_array};
use crate::quiet_ledger::{drain, longest_healthy, object_json};
use crate::service_instance::instance_of;
use crate::service_protocol::RELEASE;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
pub const TIMING_FILE: &str = "cli-git-commit-timing.jsonl";
pub const TIMING_SCHEMA_VERSION: u32 = 1;
const TAG: &str = "commitTiming";
const UNANNOUNCED: &str = "unannounced";
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ReachTrace {
pub tries: usize,
pub refused: usize,
pub start_waited: Duration,
pub retires_sent: usize,
pub releases_seen: Vec<String>,
pub answered_by: Option<String>,
pub elapsed: Duration,
}
impl ReachTrace {
pub fn saw_release(&mut self, release: Option<&str>) {
let word: String = release.unwrap_or(UNANNOUNCED).to_string();
if !self.releases_seen.contains(&word) {
self.releases_seen.push(word);
}
}
}
pub fn holder_kind_of(text: &str) -> &'static str {
return if text.contains("(proven by ") { "proven-alive" } else { "unproven" };
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct SubmitTrace {
pub total: Duration,
pub reach: ReachTrace,
pub answer_wait: Duration,
pub positions: Vec<usize>,
pub waiting_notices: usize,
pub holder_kind: Option<&'static str>,
pub reconnects: usize,
pub sends: usize,
}
fn common_dir_for(git_dir: &Path) -> PathBuf {
let Ok(text) = std::fs::read_to_string(git_dir.join("commondir")) else {
return git_dir.to_path_buf();
};
let named: &str = text.trim_end_matches(['\r', '\n']);
if named.is_empty() {
return git_dir.to_path_buf();
}
let joined: PathBuf = git_dir.join(named);
return std::fs::canonicalize(&joined).unwrap_or(joined);
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct Place {
git_dir: PathBuf,
common_dir: PathBuf,
instance: String,
}
fn whole_milliseconds(elapsed: Duration) -> u128 {
return elapsed.as_millis();
}
fn null_word() -> String {
return String::from("null");
}
impl Place {
fn instance_json(&self) -> String {
return quote(self.instance.as_str());
}
fn git_dir_json(&self) -> String {
return quote(&self.git_dir.to_string_lossy());
}
}
#[derive(Clone, Debug)]
pub struct CommitTiming {
began: Instant,
start_millis: u128,
place: Option<Place>,
pub reference: Option<String>,
pub request: Option<String>,
pub outcome: &'static str,
pub attempts: usize,
pub prepare: Duration,
pub build: Duration,
pub provision: Duration,
pub submit: SubmitTrace,
pub after_landing: Duration,
pub gate: Duration,
pub handoff: Duration,
pub quiet: Vec<(String, Duration)>,
}
impl CommitTiming {
pub fn start() -> Self {
let start_millis: u128 = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(whole_milliseconds)
.unwrap_or_default();
return Self {
began: Instant::now(),
start_millis,
place: None,
reference: None,
request: None,
outcome: "stopped",
attempts: 0,
prepare: Duration::ZERO,
build: Duration::ZERO,
provision: Duration::ZERO,
submit: SubmitTrace::default(),
after_landing: Duration::ZERO,
gate: Duration::ZERO,
handoff: Duration::ZERO,
quiet: Vec::new(),
};
}
pub fn place_at(&mut self, git_dir: &Path) {
self.place = Some(Place { git_dir: git_dir.to_path_buf(), common_dir: common_dir_for(git_dir), instance: instance_of(git_dir) });
}
fn other(&self, total: Duration) -> Duration {
let covered: Duration = self.prepare + self.build + self.provision + self.submit.total + self.after_landing + self.gate + self.handoff;
return total.saturating_sub(covered);
}
pub fn line(&self, total: Duration, exit_code: i32) -> String {
let place: Option<&Place> = self.place.as_ref();
let branch: String = self.reference.as_deref().map_or_else(null_word, quote);
let answered_by: String = self.submit.reach.answered_by.as_deref().map_or_else(null_word, quote);
let request: String = self.request.as_deref().map_or_else(null_word, quote);
let level: &str = if self.outcome == "stopped" { "warn" } else { "info" };
let positions: Vec<String> = self.submit.positions.iter().map(usize::to_string).collect();
return format!(
"{{\"schemaVersion\":{TIMING_SCHEMA_VERSION},\"time\":{},\"level\":\"{level}\",\"startMillis\":{},\"pid\":{},\"release\":{},\"instance\":{},\"gitDir\":{},\
\"requestId\":{request},\"branch\":{branch},\"outcome\":{},\"exitCode\":{exit_code},\"totalMs\":{},\
\"prepareMs\":{},\"attempts\":{},\"buildMs\":{},\"provisionMs\":{},\
\"submitMs\":{},\"sends\":{},\"reachMs\":{},\"connectTries\":{},\"refusedConnects\":{},\"startWaitMs\":{},\
\"updateRounds\":{},\"releasesSeen\":{},\"answeredByRelease\":{answered_by},\
\"answerWaitMs\":{},\"positions\":[{}],\"waitingNotices\":{},\"lockHolderKind\":{},\"reconnects\":{},\
\"afterLandingMs\":{},\"gateMs\":{},\"handoffMs\":{},\"otherMs\":{},\
\"longestQuietMs\":{},\"quietMs\":{}}}\n",
quote(now_iso().as_str()),
self.start_millis,
std::process::id(),
quote(RELEASE),
place.map_or_else(null_word, Place::instance_json),
place.map_or_else(null_word, Place::git_dir_json),
quote(self.outcome),
total.as_millis(),
self.prepare.as_millis(),
self.attempts,
self.build.as_millis(),
self.provision.as_millis(),
self.submit.total.as_millis(),
self.submit.sends,
self.submit.reach.elapsed.as_millis(),
self.submit.reach.tries,
self.submit.reach.refused,
self.submit.reach.start_waited.as_millis(),
self.submit.reach.retires_sent,
string_array(self.submit.reach.releases_seen.as_slice()),
self.submit.answer_wait.as_millis(),
positions.join(","),
self.submit.waiting_notices,
self.submit.holder_kind.map_or_else(null_word, quote),
self.submit.reconnects,
self.after_landing.as_millis(),
self.gate.as_millis(),
self.handoff.as_millis(),
self.other(total).as_millis(),
longest_healthy(self.quiet.as_slice()),
object_json(self.quiet.as_slice()),
);
}
pub fn finish(&self, exit_code: i32) {
let Some(place) = self.place.as_ref() else { return };
let target: PathBuf = place.common_dir.join(TIMING_FILE);
let mut with_quiet: CommitTiming = self.clone();
with_quiet.quiet = drain();
let line: String = with_quiet.line(self.began.elapsed(), exit_code);
if let Err(error) = append_line(target.as_path(), line.as_bytes()) {
debug(TAG, &format!("the timing record could not be written to {}: {error}", target.display()));
}
}
}
#[cfg(test)]
#[path = "commit_timing_tests.rs"]
mod tests;