手动执行全量同步
This commit is contained in:
1
src/analysis/mod.rs
Normal file
1
src/analysis/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod timing;
|
||||
308
src/analysis/timing.rs
Normal file
308
src/analysis/timing.rs
Normal file
@@ -0,0 +1,308 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TimingHandle {
|
||||
inner: Arc<Mutex<TimingCollector>>,
|
||||
}
|
||||
|
||||
impl TimingHandle {
|
||||
pub fn new(meta: TimingMeta) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(Mutex::new(TimingCollector::new(meta))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn span_phase(&self, phase: &'static str) -> TimingSpanGuard<'_> {
|
||||
TimingSpanGuard {
|
||||
handle: self.clone(),
|
||||
kind: TimingSpanKind::Phase(phase),
|
||||
start: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn span_rrdp_repo<'a>(&self, repo_uri: &'a str) -> TimingSpanGuard<'a> {
|
||||
TimingSpanGuard {
|
||||
handle: self.clone(),
|
||||
kind: TimingSpanKind::RrdpRepo(repo_uri),
|
||||
start: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn span_rrdp_repo_step<'a>(
|
||||
&self,
|
||||
repo_uri: &'a str,
|
||||
step: &'static str,
|
||||
) -> TimingSpanGuard<'a> {
|
||||
TimingSpanGuard {
|
||||
handle: self.clone(),
|
||||
kind: TimingSpanKind::RrdpRepoStep { repo_uri, step },
|
||||
start: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn span_publication_point<'a>(&self, manifest_rsync_uri: &'a str) -> TimingSpanGuard<'a> {
|
||||
TimingSpanGuard {
|
||||
handle: self.clone(),
|
||||
kind: TimingSpanKind::PublicationPoint(manifest_rsync_uri),
|
||||
start: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_meta(&self, update: TimingMetaUpdate<'_>) {
|
||||
let mut g = self.inner.lock().expect("timing lock");
|
||||
if let Some(v) = update.tal_url {
|
||||
g.meta.tal_url = Some(v.to_string());
|
||||
}
|
||||
if let Some(v) = update.db_path {
|
||||
g.meta.db_path = Some(v.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record_count(&self, key: &'static str, inc: u64) {
|
||||
let mut g = self.inner.lock().expect("timing lock");
|
||||
g.counts
|
||||
.entry(key)
|
||||
.and_modify(|v| *v = v.saturating_add(inc))
|
||||
.or_insert(inc);
|
||||
}
|
||||
|
||||
/// Record a phase duration directly in nanoseconds.
|
||||
///
|
||||
/// This is useful when aggregating sub-phase timings locally (to reduce lock contention)
|
||||
/// and then emitting a single record per publication point.
|
||||
pub fn record_phase_nanos(&self, phase: &'static str, nanos: u64) {
|
||||
let mut g = self.inner.lock().expect("timing lock");
|
||||
g.phases.record(phase, nanos);
|
||||
}
|
||||
|
||||
pub fn write_json(&self, path: &Path, top_n: usize) -> Result<(), String> {
|
||||
let report = {
|
||||
let g = self.inner.lock().expect("timing lock");
|
||||
g.to_report(top_n)
|
||||
};
|
||||
|
||||
let f = std::fs::File::create(path)
|
||||
.map_err(|e| format!("create timing json failed: {}: {e}", path.display()))?;
|
||||
serde_json::to_writer_pretty(f, &report)
|
||||
.map_err(|e| format!("write timing json failed: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn record_duration(&self, kind: TimingSpanKind<'_>, duration: Duration) {
|
||||
let nanos_u64 = duration.as_nanos().min(u128::from(u64::MAX)) as u64;
|
||||
let mut g = self.inner.lock().expect("timing lock");
|
||||
match kind {
|
||||
TimingSpanKind::Phase(name) => g.phases.record(name, nanos_u64),
|
||||
TimingSpanKind::RrdpRepo(uri) => g.rrdp_repos.record(uri, nanos_u64),
|
||||
TimingSpanKind::RrdpRepoStep { repo_uri, step } => g
|
||||
.rrdp_repo_steps
|
||||
.record(&format!("{repo_uri}::{step}"), nanos_u64),
|
||||
TimingSpanKind::PublicationPoint(uri) => g.publication_points.record(uri, nanos_u64),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct TimingMeta {
|
||||
pub recorded_at_utc_rfc3339: String,
|
||||
pub validation_time_utc_rfc3339: String,
|
||||
pub tal_url: Option<String>,
|
||||
pub db_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct TimingMetaUpdate<'a> {
|
||||
pub tal_url: Option<&'a str>,
|
||||
pub db_path: Option<&'a str>,
|
||||
}
|
||||
|
||||
pub struct TimingSpanGuard<'a> {
|
||||
handle: TimingHandle,
|
||||
kind: TimingSpanKind<'a>,
|
||||
start: Instant,
|
||||
}
|
||||
|
||||
impl Drop for TimingSpanGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
self.handle
|
||||
.record_duration(self.kind.clone(), self.start.elapsed());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum TimingSpanKind<'a> {
|
||||
Phase(&'static str),
|
||||
RrdpRepo(&'a str),
|
||||
RrdpRepoStep {
|
||||
repo_uri: &'a str,
|
||||
step: &'static str,
|
||||
},
|
||||
PublicationPoint(&'a str),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct DurationStats {
|
||||
pub count: u64,
|
||||
pub total_nanos: u64,
|
||||
}
|
||||
|
||||
impl DurationStats {
|
||||
fn record(&mut self, nanos: u64) {
|
||||
self.count = self.count.saturating_add(1);
|
||||
self.total_nanos = self.total_nanos.saturating_add(nanos);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct DurationStatsMap {
|
||||
map: HashMap<String, DurationStats>,
|
||||
}
|
||||
|
||||
impl DurationStatsMap {
|
||||
fn record(&mut self, key: &str, nanos: u64) {
|
||||
self.map.entry(key.to_string()).or_default().record(nanos);
|
||||
}
|
||||
|
||||
fn top(&self, n: usize) -> Vec<TopDurationEntry> {
|
||||
let mut v = self
|
||||
.map
|
||||
.iter()
|
||||
.map(|(k, s)| TopDurationEntry {
|
||||
key: k.clone(),
|
||||
count: s.count,
|
||||
total_nanos: s.total_nanos,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
v.sort_by(|a, b| b.total_nanos.cmp(&a.total_nanos));
|
||||
v.truncate(n);
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
struct TimingCollector {
|
||||
meta: TimingMeta,
|
||||
counts: HashMap<&'static str, u64>,
|
||||
phases: DurationStatsMap,
|
||||
rrdp_repos: DurationStatsMap,
|
||||
rrdp_repo_steps: DurationStatsMap,
|
||||
publication_points: DurationStatsMap,
|
||||
}
|
||||
|
||||
impl TimingCollector {
|
||||
fn new(meta: TimingMeta) -> Self {
|
||||
Self {
|
||||
meta,
|
||||
counts: HashMap::new(),
|
||||
phases: DurationStatsMap::default(),
|
||||
rrdp_repos: DurationStatsMap::default(),
|
||||
rrdp_repo_steps: DurationStatsMap::default(),
|
||||
publication_points: DurationStatsMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_report(&self, top_n: usize) -> TimingReportV1 {
|
||||
TimingReportV1 {
|
||||
format_version: 1,
|
||||
meta: self.meta.clone(),
|
||||
counts: self
|
||||
.counts
|
||||
.iter()
|
||||
.map(|(k, v)| ((*k).to_string(), *v))
|
||||
.collect(),
|
||||
phases: self
|
||||
.phases
|
||||
.map
|
||||
.iter()
|
||||
.map(|(k, s)| (k.clone(), s.clone()))
|
||||
.collect(),
|
||||
top_rrdp_repos: self.rrdp_repos.top(top_n),
|
||||
top_rrdp_repo_steps: self.rrdp_repo_steps.top(top_n),
|
||||
top_publication_points: self.publication_points.top(top_n),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct TimingReportV1 {
|
||||
pub format_version: u64,
|
||||
pub meta: TimingMeta,
|
||||
pub counts: HashMap<String, u64>,
|
||||
pub phases: HashMap<String, DurationStats>,
|
||||
pub top_rrdp_repos: Vec<TopDurationEntry>,
|
||||
pub top_rrdp_repo_steps: Vec<TopDurationEntry>,
|
||||
pub top_publication_points: Vec<TopDurationEntry>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct TopDurationEntry {
|
||||
pub key: String,
|
||||
pub count: u64,
|
||||
pub total_nanos: u64,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn timing_handle_writes_json_with_phases_and_tops() {
|
||||
let meta = TimingMeta {
|
||||
recorded_at_utc_rfc3339: "2026-02-28T00:00:00Z".to_string(),
|
||||
validation_time_utc_rfc3339: "2026-02-28T00:00:00Z".to_string(),
|
||||
tal_url: Some("https://example.test/x.tal".to_string()),
|
||||
db_path: Some("db".to_string()),
|
||||
};
|
||||
let h = TimingHandle::new(meta);
|
||||
|
||||
{
|
||||
let _p = h.span_phase("tal_bootstrap");
|
||||
}
|
||||
{
|
||||
let _r = h.span_rrdp_repo("https://rrdp.example.test/notification.xml");
|
||||
}
|
||||
{
|
||||
let _s = h.span_rrdp_repo_step(
|
||||
"https://rrdp.example.test/notification.xml",
|
||||
"fetch_notification",
|
||||
);
|
||||
}
|
||||
{
|
||||
let _pp = h.span_publication_point("rsync://example.test/repo/manifest.mft");
|
||||
}
|
||||
h.record_count("vrps", 42);
|
||||
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let path = dir.path().join("timing.json");
|
||||
h.write_json(&path, 10).expect("write_json");
|
||||
|
||||
let rep: TimingReportV1 =
|
||||
serde_json::from_slice(&std::fs::read(&path).expect("read timing.json"))
|
||||
.expect("parse timing.json");
|
||||
assert_eq!(rep.format_version, 1);
|
||||
assert!(rep.phases.contains_key("tal_bootstrap"));
|
||||
assert_eq!(rep.counts.get("vrps").copied(), Some(42));
|
||||
assert!(
|
||||
rep.top_rrdp_repos
|
||||
.iter()
|
||||
.any(|e| e.key.contains("rrdp.example.test")),
|
||||
"expected repo in top list"
|
||||
);
|
||||
assert!(
|
||||
rep.top_rrdp_repo_steps
|
||||
.iter()
|
||||
.any(|e| e.key.contains("fetch_notification")),
|
||||
"expected repo step in top list"
|
||||
);
|
||||
assert!(
|
||||
rep.top_publication_points
|
||||
.iter()
|
||||
.any(|e| e.key.contains("manifest.mft")),
|
||||
"expected PP in top list"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user