use std::fmt;
use std::time::Duration;
pub use reverie::DispatchStats;
use serde::Deserialize;
use serde::Serialize;
use crate::pid::DetTid;
use crate::time::LogicalTime;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PathEvidence {
pub schema: u8,
pub guest_rpc_observed: bool,
pub ptrace_fallback_sites: usize,
pub trusted_shared_object_sites: usize,
pub trusted_shared_objects: Vec<String>,
}
impl PathEvidence {
pub const SCHEMA: u8 = 1;
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default)]
pub struct TimesliceStats {
pub count: u64,
pub sum_ns: u64,
pub min_ns: u64,
pub max_ns: u64,
}
impl TimesliceStats {
pub fn record(&mut self, ns: u64) {
if self.count == 0 {
self.min_ns = ns;
self.max_ns = ns;
} else {
self.min_ns = self.min_ns.min(ns);
self.max_ns = self.max_ns.max(ns);
}
self.sum_ns += ns;
self.count += 1;
}
pub fn merge(&mut self, other: &TimesliceStats) {
if other.count == 0 {
return;
}
if self.count == 0 {
*self = *other;
return;
}
self.min_ns = self.min_ns.min(other.min_ns);
self.max_ns = self.max_ns.max(other.max_ns);
self.sum_ns += other.sum_ns;
self.count += other.count;
}
pub fn mean_ns(&self) -> u64 {
self.sum_ns.checked_div(self.count).unwrap_or(0)
}
pub fn is_empty(&self) -> bool {
self.count == 0
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct RunSummary {
pub sched_turns: u64,
pub schedevent_replayed: u64,
pub schedevent_recorded: u64,
pub schedevent_desynced: u64,
pub desync_descrip: Option<String>,
pub reprio_descrip: Option<String>,
pub threads_descrip: String,
pub num_processes: u64,
pub num_threads: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub syscalls: Option<u64>,
pub virttime_elapsed: u64,
pub virttime_final: u64,
pub realtime_elapsed: Option<Duration>,
pub timeslice_stats: TimesliceStats,
pub per_thread_timeslice: Vec<(DetTid, TimesliceStats)>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dispatch_stats: Option<DispatchStats>,
}
impl fmt::Display for RunSummary {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.fmt_summary(f, true, self.reprio_descrip.as_deref())
}
}
pub struct RunSummaryInfo<'a> {
summary: &'a RunSummary,
reprio_description: Option<&'a str>,
}
impl fmt::Display for RunSummaryInfo<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.summary.fmt_summary(f, false, self.reprio_description)
}
}
impl RunSummary {
pub fn info<'a>(&'a self, reprio_description: Option<&'a str>) -> RunSummaryInfo<'a> {
RunSummaryInfo {
summary: self,
reprio_description,
}
}
fn fmt_summary(
&self,
f: &mut fmt::Formatter<'_>,
include_replay_bookkeeping: bool,
reprio_description: Option<&str>,
) -> fmt::Result {
let RunSummary {
sched_turns,
schedevent_replayed,
schedevent_recorded,
schedevent_desynced,
desync_descrip,
reprio_descrip: _,
num_processes,
num_threads,
syscalls,
virttime_elapsed,
virttime_final,
realtime_elapsed,
threads_descrip,
timeslice_stats,
per_thread_timeslice,
dispatch_stats: _,
} = self;
writeln!(f, "Final thread-tree was: {}", threads_descrip)?;
writeln!(
f,
"There were {} group leaders of {} thread(s) total.",
num_processes, num_threads
)?;
if let Some(syscalls) = syscalls {
writeln!(f, "Guest threads completed {syscalls} syscalls.")?;
}
if include_replay_bookkeeping {
writeln!(
f,
"Internally, the hermit scheduler ran {} turns, recorded {} events, replayed {} events ({} desynced)",
sched_turns, schedevent_recorded, schedevent_replayed, schedevent_desynced,
)?;
} else {
writeln!(
f,
"Internally, the hermit scheduler ran {} turns, recorded {} events ({} desynced)",
sched_turns, schedevent_recorded, schedevent_desynced,
)?;
}
if let Some(txt) = desync_descrip {
write!(f, "{}", txt)?;
}
if let Some(txt) = reprio_description {
write!(f, "{}", txt)?;
}
writeln!(
f,
"Final virtual global (cpu) time: {}",
LogicalTime::from_nanos(*virttime_final)
)?;
writeln!(
f,
"Elapsed virtual global (cpu) time: {}",
LogicalTime::from_nanos(*virttime_elapsed)
)?;
if timeslice_stats.is_empty() {
writeln!(f, "Timeslice stats: none recorded")?;
} else {
writeln!(
f,
"Timeslice stats: min={}ns max={}ns mean={}ns count={}",
timeslice_stats.min_ns,
timeslice_stats.max_ns,
timeslice_stats.mean_ns(),
timeslice_stats.count,
)?;
if per_thread_timeslice.len() > 1 {
for (dettid, st) in per_thread_timeslice {
if st.is_empty() {
continue;
}
writeln!(
f,
" timeslice thread {}: min={}ns max={}ns mean={}ns count={}",
dettid,
st.min_ns,
st.max_ns,
st.mean_ns(),
st.count,
)?;
}
}
}
if let Some(rt) = realtime_elapsed {
writeln!(f, "Nondeterministic realtime elapsed: {:?}", rt)?
};
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::PathEvidence;
use super::RunSummary;
use super::TimesliceStats;
#[test]
fn path_evidence_json_is_exact() {
let evidence = PathEvidence {
schema: PathEvidence::SCHEMA,
guest_rpc_observed: true,
ptrace_fallback_sites: 0,
trusted_shared_object_sites: 1,
trusted_shared_objects: vec!["/usr/lib/libc.so.6".into()],
};
let json = serde_json::to_string(&evidence).unwrap();
assert_eq!(
json,
r#"{"schema":1,"guest_rpc_observed":true,"ptrace_fallback_sites":0,"trusted_shared_object_sites":1,"trusted_shared_objects":["/usr/lib/libc.so.6"]}"#
);
assert_eq!(
serde_json::from_str::<PathEvidence>(&json).unwrap(),
evidence
);
let unknown = json.replace(
r#""trusted_shared_objects""#,
r#""unexpected":0,"trusted_shared_objects""#,
);
let error = serde_json::from_str::<PathEvidence>(&unknown).unwrap_err();
assert!(error.to_string().contains("unknown field `unexpected`"));
let missing = json.replace(r#""guest_rpc_observed":true,"#, "");
let error = serde_json::from_str::<PathEvidence>(&missing).unwrap_err();
assert!(
error
.to_string()
.contains("missing field `guest_rpc_observed`")
);
}
#[test]
fn timeslice_stats_empty() {
let s = TimesliceStats::default();
assert!(s.is_empty());
assert_eq!(s.count, 0);
assert_eq!(s.mean_ns(), 0); }
#[test]
fn timeslice_stats_record() {
let mut s = TimesliceStats::default();
s.record(10);
s.record(30);
s.record(20);
assert!(!s.is_empty());
assert_eq!(s.count, 3);
assert_eq!(s.min_ns, 10);
assert_eq!(s.max_ns, 30);
assert_eq!(s.sum_ns, 60);
assert_eq!(s.mean_ns(), 20);
}
#[test]
fn timeslice_stats_merge() {
let mut a = TimesliceStats::default();
a.record(10);
a.record(40);
let mut b = TimesliceStats::default();
b.record(5);
b.record(100);
a.merge(&b);
assert_eq!(a.count, 4);
assert_eq!(a.min_ns, 5);
assert_eq!(a.max_ns, 100);
assert_eq!(a.sum_ns, 155);
let before = a;
a.merge(&TimesliceStats::default());
assert_eq!(a, before);
let mut empty = TimesliceStats::default();
empty.merge(&b);
assert_eq!(empty, b);
}
#[test]
fn older_summary_json_keeps_an_unrecorded_syscall_total_absent() {
let value = serde_json::to_value(RunSummary::default()).unwrap();
let mut object = value.as_object().unwrap().clone();
object.remove("syscalls");
let parsed: RunSummary = serde_json::from_value(object.into()).unwrap();
assert_eq!(parsed.syscalls, None);
let measured = RunSummary {
syscalls: Some(0),
..Default::default()
};
let value = serde_json::to_value(&measured).unwrap();
assert_eq!(value["syscalls"], 0);
let parsed: RunSummary = serde_json::from_value(value).unwrap();
assert_eq!(parsed.syscalls, Some(0));
}
}