use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[repr(u8)]
pub enum Availability {
Observed = 0,
NotApplicable = 1,
Unknown = 2,
Unsupported = 3,
Unavailable = 4,
PermissionDenied = 5,
Stale = 6,
}
impl Availability {
pub fn as_u8(self) -> u8 {
self as u8
}
pub fn from_u8(value: u8) -> Availability {
match value {
0 => Availability::Observed,
1 => Availability::NotApplicable,
2 => Availability::Unknown,
3 => Availability::Unsupported,
4 => Availability::Unavailable,
5 => Availability::PermissionDenied,
6 => Availability::Stale,
_ => Availability::Unknown,
}
}
pub fn label(self) -> &'static str {
match self {
Availability::Observed => "observed",
Availability::NotApplicable => "not_applicable",
Availability::Unknown => "unknown",
Availability::Unsupported => "unsupported",
Availability::Unavailable => "unavailable",
Availability::PermissionDenied => "permission_denied",
Availability::Stale => "stale",
}
}
pub fn explain(self) -> &'static str {
match self {
Availability::Observed => "the value was read",
Availability::NotApplicable => "this measurement does not apply to this entity",
Availability::Unknown => "no reason was recorded",
Availability::Unsupported => "this hardware or kernel does not expose it",
Availability::Unavailable => "the source exists but returned nothing",
Availability::PermissionDenied => "this process lacks the privilege to read it",
Availability::Stale => "the last reading is too old to be trusted",
}
}
pub fn is_observed(self) -> bool {
self == Availability::Observed
}
pub fn is_privilege_problem(self) -> bool {
self == Availability::PermissionDenied
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AvailabilityMatrix {
rows: usize,
cols: usize,
codes: Vec<u8>,
}
impl AvailabilityMatrix {
pub fn new(rows: usize, cols: usize) -> AvailabilityMatrix {
AvailabilityMatrix {
rows,
cols,
codes: vec![Availability::NotApplicable.as_u8(); rows * cols],
}
}
pub fn rows(&self) -> usize {
self.rows
}
pub fn cols(&self) -> usize {
self.cols
}
pub fn clear(&mut self) {
self.codes.fill(Availability::NotApplicable.as_u8());
}
#[inline]
pub fn get(&self, row: usize, col: usize) -> Availability {
if row >= self.rows || col >= self.cols {
return Availability::NotApplicable;
}
Availability::from_u8(self.codes[row * self.cols + col])
}
#[inline]
pub fn set(&mut self, row: usize, col: usize, availability: Availability) {
if row < self.rows && col < self.cols {
self.codes[row * self.cols + col] = availability.as_u8();
}
}
pub fn as_slice(&self) -> &[u8] {
&self.codes
}
pub fn copy_from_slice(&mut self, codes: &[u8]) {
assert_eq!(codes.len(), self.codes.len(), "availability shape mismatch");
self.codes.copy_from_slice(codes);
}
pub fn tally(&self) -> Vec<(Availability, usize)> {
let mut counts = [0usize; 8];
for code in &self.codes {
counts[(*code as usize).min(7)] += 1;
}
(0..7u8)
.map(|code| (Availability::from_u8(code), counts[code as usize]))
.filter(|(_, count)| *count > 0)
.collect()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct SensorId(pub u16);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[repr(u8)]
pub enum Perturbation {
None = 0,
Negligible = 1,
Low = 2,
Material = 3,
}
impl Perturbation {
pub fn as_u8(self) -> u8 {
self as u8
}
pub fn from_u8(value: u8) -> Perturbation {
match value {
1 => Perturbation::Negligible,
2 => Perturbation::Low,
3 => Perturbation::Material,
_ => Perturbation::None,
}
}
pub fn label(self) -> &'static str {
match self {
Perturbation::None => "none",
Perturbation::Negligible => "negligible",
Perturbation::Low => "low",
Perturbation::Material => "material",
}
}
pub fn is_passive(self) -> bool {
self != Perturbation::Material
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Uncertainty {
pub absolute: Option<f64>,
pub relative: Option<f64>,
pub basis: &'static str,
}
impl Uncertainty {
pub const fn absolute(value: f64, basis: &'static str) -> Uncertainty {
Uncertainty {
absolute: Some(value),
relative: None,
basis,
}
}
pub const fn relative(value: f64, basis: &'static str) -> Uncertainty {
Uncertainty {
absolute: None,
relative: Some(value),
basis,
}
}
pub const fn unknown(basis: &'static str) -> Uncertainty {
Uncertainty {
absolute: None,
relative: None,
basis,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SensorReport {
pub id: SensorId,
pub key: String,
pub perturbation: Perturbation,
pub last_cost_ns: u64,
pub sampling_latency_ns: u64,
pub sample_age_ns: u64,
pub samples: u32,
pub errors: u32,
pub confidence: f64,
pub availability: Availability,
pub inactive: bool,
}
impl SensorReport {
pub fn pending(
id: SensorId,
key: impl Into<String>,
perturbation: Perturbation,
) -> SensorReport {
SensorReport {
id,
key: key.into(),
perturbation,
last_cost_ns: 0,
sampling_latency_ns: 0,
sample_age_ns: 0,
samples: 0,
errors: 0,
confidence: 1.0,
availability: Availability::Unknown,
inactive: false,
}
}
pub fn unavailable(
id: SensorId,
key: impl Into<String>,
perturbation: Perturbation,
availability: Availability,
) -> SensorReport {
SensorReport {
inactive: true,
confidence: 0.0,
availability,
..SensorReport::pending(id, key, perturbation)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn availability_codes_round_trip_and_degrade() {
for availability in [
Availability::Observed,
Availability::NotApplicable,
Availability::Unknown,
Availability::Unsupported,
Availability::Unavailable,
Availability::PermissionDenied,
Availability::Stale,
] {
assert_eq!(Availability::from_u8(availability.as_u8()), availability);
}
assert_eq!(Availability::from_u8(200), Availability::Unknown);
}
#[test]
fn permission_denied_is_distinguishable_from_unsupported() {
assert!(Availability::PermissionDenied.is_privilege_problem());
assert!(!Availability::Unsupported.is_privilege_problem());
assert!(!Availability::Observed.is_privilege_problem());
}
#[test]
fn a_new_availability_matrix_claims_nothing() {
let matrix = AvailabilityMatrix::new(3, 4);
assert_eq!(matrix.get(0, 0), Availability::NotApplicable);
assert_eq!(matrix.tally(), vec![(Availability::NotApplicable, 12)]);
}
#[test]
fn reasons_are_recorded_per_cell() {
let mut matrix = AvailabilityMatrix::new(2, 2);
matrix.set(0, 0, Availability::Observed);
matrix.set(0, 1, Availability::PermissionDenied);
matrix.set(1, 0, Availability::Unsupported);
assert_eq!(matrix.get(0, 0), Availability::Observed);
assert_eq!(matrix.get(0, 1), Availability::PermissionDenied);
assert_eq!(matrix.get(1, 0), Availability::Unsupported);
assert_eq!(matrix.get(1, 1), Availability::NotApplicable);
let tally = matrix.tally();
assert_eq!(tally.len(), 4, "four distinct reasons: {tally:?}");
}
#[test]
fn out_of_range_access_does_not_panic() {
let mut matrix = AvailabilityMatrix::new(2, 2);
matrix.set(99, 99, Availability::Observed);
assert_eq!(matrix.get(99, 99), Availability::NotApplicable);
}
#[test]
fn a_failed_sensor_reports_why_rather_than_zero() {
let report = SensorReport::unavailable(
SensorId(4),
"power",
Perturbation::Negligible,
Availability::PermissionDenied,
);
assert!(report.inactive);
assert_eq!(report.confidence, 0.0);
assert!(report.availability.is_privilege_problem());
assert_eq!(report.samples, 0);
}
#[test]
fn material_perturbation_is_not_passive() {
assert!(!Perturbation::Material.is_passive());
assert!(Perturbation::Low.is_passive());
assert!(Perturbation::None < Perturbation::Negligible);
assert!(Perturbation::Low < Perturbation::Material);
}
}