use crate::daemon::{
clock_sync_algorithm::ff::{LocalPeriodAndError, UncorrectedClock},
logging::{LogEvent, StructuredLog, emit_event},
time::{Duration, Instant, TscCount, tsc::Period},
};
#[derive(Debug, Clone, serde::Serialize)]
struct ThetaCalculation {
source: &'static str,
theta_ns: Duration,
clock_error_bound_ns: Duration,
period: Period,
uncorrected_k_ns: Instant,
local_period: Period,
local_period_error: Period,
counter_midpoint: TscCount,
time_ns: Instant,
}
impl LogEvent for ThetaCalculation {
const STREAM: StructuredLog = StructuredLog::AlgoAnalysis;
const EVENT: &'static str = "theta_calculation";
}
pub(crate) fn emit_theta_calculation(
source: &'static str,
theta: Duration,
clock_error_bound: Duration,
uncorrected_clock: UncorrectedClock,
local_period: &LocalPeriodAndError,
counter_midpoint: TscCount,
time_ns: Instant,
) {
let snapshot = ThetaCalculation {
source,
theta_ns: theta,
clock_error_bound_ns: clock_error_bound,
period: uncorrected_clock.p_estimate,
uncorrected_k_ns: uncorrected_clock.k,
local_period: local_period.period_local,
local_period_error: local_period.error,
counter_midpoint,
time_ns,
};
emit_event(&snapshot);
}
#[cfg(test)]
mod tests {
use serde_json::{Map, Value};
use super::*;
use crate::daemon::logging::{StructuredLog, test_support::with_layer};
const TEST_VERSION: &str = "test-version";
fn with_algo_analysis_layer(f: impl FnOnce()) -> (String, tempfile::TempDir) {
with_layer(StructuredLog::AlgoAnalysis, TEST_VERSION, f)
}
fn emit_test_record() {
emit_theta_calculation(
"phc",
Duration::from(-1_348), Duration::from(27_528), UncorrectedClock {
p_estimate: Period::from_seconds(9.523_683_333_064_784e-10),
k: Instant::from(1_766_707_502_857_115_074),
},
&LocalPeriodAndError {
period_local: Period::from_seconds(9.523_683_333_064_784e-10),
error: Period::from_seconds(9.065_418_261_058_853e-14),
},
TscCount::from(270_765_653_528_298),
Instant::from(1_766_965_370_641_132_697),
);
}
fn parse_single_record(output: &str) -> Map<String, Value> {
let mut lines = output.lines();
let record = serde_json::from_str(lines.next().expect("expected one record"))
.expect("expected valid JSON");
assert!(lines.next().is_none(), "expected exactly one record");
record
}
#[test]
fn emits_one_record_routed_to_the_analysis_log() {
let (output, _tmp) = with_algo_analysis_layer(emit_test_record);
let record = parse_single_record(&output);
assert_eq!(record["event"], "theta_calculation");
assert_eq!(record["theta_calculation"]["source"], "phc");
}
#[test]
fn record_schema_is_stable() {
let (output, _tmp) = with_algo_analysis_layer(emit_test_record);
let record = parse_single_record(&output);
let keys: Vec<&str> = record.keys().map(String::as_str).collect();
assert_eq!(
keys,
[
"timestamp",
"event",
"theta_calculation",
"version",
]
);
let body = record["theta_calculation"]
.as_object()
.expect("theta_calculation body is a JSON object");
let body_keys: Vec<&str> = body.keys().map(String::as_str).collect();
assert_eq!(
body_keys,
[
"source",
"theta_ns",
"clock_error_bound_ns",
"period",
"uncorrected_k_ns",
"local_period",
"local_period_error",
"counter_midpoint",
"time_ns",
]
);
}
#[test]
fn integer_fields_are_exact_i64() {
let (output, _tmp) = with_algo_analysis_layer(emit_test_record);
let record = parse_single_record(&output);
let body = &record["theta_calculation"];
assert_eq!(body["theta_ns"], Value::from(-1_348_i64));
assert_eq!(body["clock_error_bound_ns"], Value::from(27_528_i64));
assert_eq!(
body["uncorrected_k_ns"],
Value::from(1_766_707_502_857_115_074_i64)
);
assert_eq!(body["time_ns"], Value::from(1_766_965_370_641_132_697_i64));
assert_eq!(
body["counter_midpoint"],
Value::from(270_765_653_528_298_i64)
);
}
#[test]
fn period_fields_round_trip_bit_identically() {
let (output, _tmp) = with_algo_analysis_layer(emit_test_record);
let record = parse_single_record(&output);
let body = &record["theta_calculation"];
let period = body["period"].as_f64().unwrap();
assert_eq!(period.to_bits(), 9.523_683_333_064_784e-10_f64.to_bits());
let error = body["local_period_error"].as_f64().unwrap();
assert_eq!(error.to_bits(), 9.065_418_261_058_853e-14_f64.to_bits());
}
#[test]
fn emission_without_subscriber_is_a_no_op() {
emit_test_record();
}
}