use std::{
collections::HashMap,
fs, process,
sync::{
atomic::{AtomicU64, Ordering},
Arc, Mutex,
},
time::Instant,
};
use crossbeam_queue::SegQueue;
use serde::{Deserialize, Serialize};
use crate::Stats;
pub trait IntoStatsId {
fn as_stats_id(&self) -> u64;
}
impl IntoStatsId for u64 {
#[inline]
fn as_stats_id(&self) -> u64 {
*self
}
}
impl IntoStatsId for StatsScope {
#[inline]
fn as_stats_id(&self) -> u64 {
self.id()
}
}
impl<T: IntoStatsId> IntoStatsId for &T {
#[inline]
fn as_stats_id(&self) -> u64 {
(*self).as_stats_id()
}
}
#[cfg(feature = "stats")]
#[macro_export]
macro_rules! stats_begin {
($stats:expr, $parent:expr, $scope:ident, $name:expr, $index:expr) => {
let $scope = $crate::StatsScope::new(
$crate::IntoStatsId::as_stats_id(&$parent),
$stats.next_id(),
$name,
$index,
);
$stats.add_stat(
$scope.parent_id(),
$scope.id(),
$name,
$index,
$crate::ExecutorStatsEvent::Begin,
);
};
}
#[cfg(not(feature = "stats"))]
#[macro_export]
macro_rules! stats_begin {
($stats:expr, $parent:expr, $scope:ident, $name:expr, $index:expr) => {
let $scope = $crate::StatsScope;
};
}
#[cfg(feature = "stats")]
#[macro_export]
macro_rules! stats_end {
($stats:expr, $scope:expr) => {
$stats.add_stat(
$scope.parent_id(),
$scope.id(),
$scope.name(),
$scope.index(),
$crate::ExecutorStatsEvent::End,
);
};
}
#[cfg(not(feature = "stats"))]
#[macro_export]
macro_rules! stats_end {
($stats:expr, $scope:expr) => {};
}
#[cfg(feature = "stats")]
#[macro_export]
macro_rules! stats_mark {
($stats:expr, $parent:expr, $name:expr, $index:expr) => {
let __mark_id = $stats.next_id();
$stats.add_stat(
$crate::IntoStatsId::as_stats_id(&$parent),
__mark_id,
$name,
$index,
$crate::ExecutorStatsEvent::Mark,
);
};
}
#[cfg(not(feature = "stats"))]
#[macro_export]
macro_rules! stats_mark {
($stats:expr, $parent:expr, $name:expr, $index:expr) => {};
}
#[cfg(feature = "stats")]
pub struct StatsScope {
parent_id: u64,
id: u64,
name: &'static str,
index: usize,
}
#[cfg(feature = "stats")]
impl StatsScope {
#[inline]
pub fn new(parent_id: u64, id: u64, name: &'static str, index: usize) -> Self {
Self { parent_id, id, name, index }
}
#[inline]
pub fn parent_id(&self) -> u64 {
self.parent_id
}
#[inline]
pub fn id(&self) -> u64 {
self.id
}
#[inline]
pub fn name(&self) -> &'static str {
self.name
}
#[inline]
pub fn index(&self) -> usize {
self.index
}
}
#[cfg(not(feature = "stats"))]
pub struct StatsScope;
#[cfg(not(feature = "stats"))]
impl StatsScope {
#[inline]
pub fn parent_id(&self) -> u64 {
0
}
#[inline]
pub fn id(&self) -> u64 {
0
}
#[inline]
pub fn name(&self) -> &'static str {
""
}
#[inline]
pub fn index(&self) -> usize {
0
}
}
#[derive(Debug, Clone)]
pub enum ExecutorStatsEvent {
Begin,
End,
Mark,
}
#[derive(Debug, Clone)]
struct ExecutorStatsEntry {
parent_id: u64,
id: u64,
name: &'static str,
index: usize,
event: ExecutorStatsEvent,
timestamp: Instant,
}
#[derive(Debug, Default)]
pub struct ExecutorStats {
start_time: Mutex<Option<Instant>>,
last_id: AtomicU64,
pending: SegQueue<ExecutorStatsEntry>,
finalized: Mutex<Vec<ExecutorStatsEntry>>,
witness_stats: Mutex<HashMap<usize, Stats>>,
}
impl ExecutorStats {
pub fn new() -> Self {
Self::default()
}
pub fn reset(&self) {
*self.start_time.lock().unwrap_or_else(|e| e.into_inner()) = None;
self.last_id.store(0, Ordering::Relaxed);
while self.pending.pop().is_some() {}
self.finalized.lock().unwrap_or_else(|e| e.into_inner()).clear();
self.witness_stats.lock().unwrap_or_else(|e| e.into_inner()).clear();
}
pub fn add_stat(
&self,
parent_id: u64,
id: u64,
name: &'static str,
index: usize,
event: ExecutorStatsEvent,
) {
self.pending.push(ExecutorStatsEntry {
parent_id,
id,
name,
index,
event,
timestamp: Instant::now(),
});
}
pub fn set_start_time(&self, start_time: Instant) {
*self.start_time.lock().unwrap_or_else(|e| e.into_inner()) = Some(start_time);
}
pub fn next_id(&self) -> u64 {
self.last_id.fetch_add(1, Ordering::Relaxed) + 1
}
pub fn insert_witness_stats(&self, airgroup_id: usize, stats: Stats) {
self.witness_stats.lock().unwrap_or_else(|e| e.into_inner()).insert(airgroup_id, stats);
}
pub fn set_witness_duration(&self, airgroup_id: usize, duration: u128) {
if let Some(stats) =
self.witness_stats.lock().unwrap_or_else(|e| e.into_inner()).get_mut(&airgroup_id)
{
stats.witness_duration = duration;
}
}
pub fn witness_stats(&self) -> HashMap<usize, Stats> {
self.witness_stats.lock().unwrap_or_else(|e| e.into_inner()).clone()
}
fn resolved_start_time(&self) -> Instant {
let mut start_time = self.start_time.lock().unwrap_or_else(|e| e.into_inner());
*start_time.get_or_insert_with(Instant::now)
}
fn collect_sorted(&self) -> Vec<ExecutorStatsEntry> {
let mut finalized = self.finalized.lock().unwrap_or_else(|e| e.into_inner());
while let Some(entry) = self.pending.pop() {
finalized.push(entry);
}
let mut entries = finalized.clone();
drop(finalized);
entries.sort_by_key(|e| e.timestamp);
entries
}
pub fn store_stats(&self) {
#[derive(Serialize, Deserialize, Debug)]
struct Task {
parent_id: u64,
id: u64,
name: String,
index: u64,
event: String,
timestamp: u64,
}
let start_time = self.resolved_start_time();
let tasks: Vec<Task> = self
.collect_sorted()
.into_iter()
.map(|stat| Task {
parent_id: stat.parent_id,
id: stat.id,
name: stat.name.to_string(),
index: stat.index as u64,
event: match stat.event {
ExecutorStatsEvent::Begin => "Begin".to_string(),
ExecutorStatsEvent::End => "End".to_string(),
ExecutorStatsEvent::Mark => "Mark".to_string(),
},
timestamp: stat.timestamp.saturating_duration_since(start_time).as_nanos() as u64,
})
.collect();
tracing::info!("Collected a total of {} statistics", tasks.len());
let json = match serde_json::to_string_pretty(&tasks) {
Ok(json) => json,
Err(e) => {
tracing::error!("Failed to serialize stats to JSON: {e}");
return;
}
};
let json_file_name = format!("stats_{}.json", process::id());
let _ = fs::write(&json_file_name, json);
let mut csv = String::new();
for task in tasks {
csv += &format!(
"{},{},{},{},{},{}\n",
task.parent_id, task.id, task.name, task.index, task.event, task.timestamp
);
}
let csv_file_name = format!("stats_{}.csv", process::id());
let _ = fs::write(&csv_file_name, csv);
tracing::info!("Statistics have been saved to {} and {}", json_file_name, csv_file_name);
}
pub fn print_stats(&self) {
let start_time = self.resolved_start_time();
let entries = self.collect_sorted();
println!("Collected a total of {} statistics", entries.len());
for stat in &entries {
println!(
"parent_id={} id={} name={} index={} event={:?} timestamp={}",
stat.parent_id,
stat.id,
stat.name,
stat.index,
stat.event,
stat.timestamp.saturating_duration_since(start_time).as_nanos() as u64
);
}
}
}
#[derive(Debug, Default, Clone)]
pub struct ExecutorStatsHandle {
inner: Arc<ExecutorStats>,
}
impl ExecutorStatsHandle {
pub fn new() -> Self {
Self::default()
}
pub fn reset(&self) {
self.inner.reset();
}
pub fn add_stat(
&self,
parent_id: u64,
id: u64,
name: &'static str,
index: usize,
event: ExecutorStatsEvent,
) {
self.inner.add_stat(parent_id, id, name, index, event);
}
pub fn set_start_time(&self, start_time: Instant) {
self.inner.set_start_time(start_time);
}
pub fn next_id(&self) -> u64 {
self.inner.next_id()
}
pub fn store_stats(&self) {
self.inner.store_stats();
}
pub fn print_stats(&self) {
self.inner.print_stats();
}
pub fn get_inner(&self) -> Arc<ExecutorStats> {
self.inner.clone()
}
pub fn witness_stats(&self) -> HashMap<usize, Stats> {
self.inner.witness_stats()
}
pub fn insert_witness_stats(&self, airgroup_id: usize, stats: Stats) {
self.inner.insert_witness_stats(airgroup_id, stats);
}
pub fn set_witness_duration(&self, airgroup_id: usize, duration: u128) {
self.inner.set_witness_duration(airgroup_id, duration);
}
}