use std::collections::BTreeMap;
use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Instant, SystemTime, UNIX_EPOCH};
use serde::Serialize;
pub fn stage_log_with<F, T>(name: &str, f: F) -> T
where
F: FnOnce() -> (T, Option<String>),
{
eprintln!("[merge] {name}: start");
let started = Instant::now();
let (value, summary) = f();
let elapsed_ms = started.elapsed().as_millis();
match summary {
Some(s) => eprintln!("[merge] {name}: done elapsed_ms={elapsed_ms} {s}"),
None => eprintln!("[merge] {name}: done elapsed_ms={elapsed_ms}"),
}
record_stage(name, started);
value
}
pub fn stage<T>(name: &str, f: impl FnOnce() -> T) -> T {
stage_log_with(name, || (f(), None))
}
#[derive(Clone, Debug, Serialize)]
pub struct StageTiming {
pub name: String,
pub start_ms: u64,
pub elapsed_ms: u64,
}
struct ActiveRecording {
started: Instant,
stages: Vec<StageTiming>,
}
static RECORDING: Mutex<Option<ActiveRecording>> = Mutex::new(None);
pub(crate) fn record_stage(name: &str, started: Instant) {
let elapsed = started.elapsed();
let mut recording = RECORDING.lock().unwrap_or_else(|error| error.into_inner());
if let Some(recording) = recording.as_mut() {
recording.stages.push(StageTiming {
name: name.to_owned(),
start_ms: millis(started.saturating_duration_since(recording.started)),
elapsed_ms: millis(elapsed),
});
}
}
pub struct TimingRecording {
started: Instant,
started_at: SystemTime,
kernels: KernelTimes,
}
impl TimingRecording {
pub fn begin() -> Self {
let started = Instant::now();
*RECORDING.lock().unwrap_or_else(|error| error.into_inner()) = Some(ActiveRecording {
started,
stages: Vec::new(),
});
Self {
started,
started_at: SystemTime::now(),
kernels: KernelTimes::now(),
}
}
pub fn finish(self) -> RunTiming {
let stages = RECORDING
.lock()
.unwrap_or_else(|error| error.into_inner())
.take()
.map(|recording| recording.stages)
.unwrap_or_default();
RunTiming {
started_at_unix_ms: self.started_at.duration_since(UNIX_EPOCH).map_or(0, millis),
total_ms: millis(self.started.elapsed()),
stages,
kernel_cpu_ms: KernelTimes::now().cpu_ms_since(&self.kernels),
}
}
}
#[derive(Clone, Debug, Serialize)]
pub struct RunTiming {
pub started_at_unix_ms: u64,
pub total_ms: u64,
pub stages: Vec<StageTiming>,
pub kernel_cpu_ms: BTreeMap<&'static str, u64>,
}
#[derive(Debug, Serialize)]
pub struct MergeTimingEntry<'a> {
pub foch_version: &'static str,
pub analysis_rules_version: &'static str,
pub input: &'a str,
pub out_dir: &'a str,
pub status: &'a str,
#[serde(flatten)]
pub timing: &'a RunTiming,
}
pub fn merge_timing_history_path() -> PathBuf {
crate::platform::cache_store::default_foch_cache_dir()
.join("timings")
.join("merge-timings.jsonl")
}
pub fn append_merge_timing(path: &Path, entry: &MergeTimingEntry<'_>) -> io::Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let mut line = serde_json::to_vec(entry).map_err(io::Error::other)?;
line.push(b'\n');
fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)?
.write_all(&line)
}
fn millis(duration: std::time::Duration) -> u64 {
u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum KernelStep {
ObserveNormalize,
ObserveMatch,
ObserveDelta,
ObserveLineage,
JoinCanonicalize,
JoinNormalize,
JoinMerge,
JoinRender,
JoinFinish,
JoinLineage,
JoinResolve,
}
impl KernelStep {
const ALL: [Self; 11] = [
Self::ObserveNormalize,
Self::ObserveMatch,
Self::ObserveDelta,
Self::ObserveLineage,
Self::JoinCanonicalize,
Self::JoinNormalize,
Self::JoinMerge,
Self::JoinRender,
Self::JoinFinish,
Self::JoinLineage,
Self::JoinResolve,
];
const fn name(self) -> &'static str {
match self {
Self::ObserveNormalize => "observe.normalize",
Self::ObserveMatch => "observe.match",
Self::ObserveDelta => "observe.delta",
Self::ObserveLineage => "observe.lineage",
Self::JoinCanonicalize => "join.canonicalize",
Self::JoinNormalize => "join.normalize",
Self::JoinMerge => "join.merge",
Self::JoinRender => "join.render",
Self::JoinFinish => "join.finish",
Self::JoinLineage => "join.lineage",
Self::JoinResolve => "join.resolve",
}
}
}
static KERNEL_MICROS: [AtomicU64; KernelStep::ALL.len()] =
[const { AtomicU64::new(0) }; KernelStep::ALL.len()];
pub(crate) fn charge(step: KernelStep, started: Instant) {
KERNEL_MICROS[step as usize].fetch_add(started.elapsed().as_micros() as u64, Ordering::Relaxed);
}
pub(crate) fn kernel<T>(step: KernelStep, f: impl FnOnce() -> T) -> T {
let started = Instant::now();
let value = f();
charge(step, started);
value
}
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct KernelTimes([u64; KernelStep::ALL.len()]);
impl KernelTimes {
pub(crate) fn now() -> Self {
Self(std::array::from_fn(|index| {
KERNEL_MICROS[index].load(Ordering::Relaxed)
}))
}
fn cpu_ms_since(&self, earlier: &Self) -> BTreeMap<&'static str, u64> {
KernelStep::ALL
.iter()
.map(|step| {
let index = *step as usize;
(
step.name(),
self.0[index].saturating_sub(earlier.0[index]) / 1000,
)
})
.collect()
}
pub(crate) fn summary_since(&self, earlier: &Self) -> String {
self.cpu_ms_since(earlier)
.into_iter()
.map(|(name, ms)| format!("{name}_cpu_ms={ms}"))
.collect::<Vec<_>>()
.join(" ")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_recording_keeps_its_stages_and_appends_one_line_per_run() {
let recording = TimingRecording::begin();
stage("outer", || stage("inner", || ()));
let timing = recording.finish();
let find = |name: &str| {
timing
.stages
.iter()
.find(|stage| stage.name == name)
.unwrap_or_else(|| panic!("{name} was not recorded: {:?}", timing.stages))
};
let (outer, inner) = (find("outer"), find("inner"));
assert!(outer.start_ms <= inner.start_ms && inner.elapsed_ms <= outer.elapsed_ms);
let dir = tempfile::tempdir().expect("temp dir");
let path = dir.path().join("timings").join("merge-timings.jsonl");
for _ in 0..2 {
append_merge_timing(
&path,
&MergeTimingEntry {
foch_version: "test",
analysis_rules_version: "test",
input: "in",
out_dir: "out",
status: "ready",
timing: &timing,
},
)
.expect("append timing");
}
let lines = fs::read_to_string(&path).expect("read history");
assert_eq!(lines.lines().count(), 2);
let entry: serde_json::Value =
serde_json::from_str(lines.lines().next().unwrap()).expect("one JSON object per line");
assert!(entry["stages"].is_array());
assert!(entry["kernel_cpu_ms"]["join.merge"].is_u64());
}
}