Skip to content

Commit

Permalink
Make it possible to run framework with different time sources
Browse files Browse the repository at this point in the history
Don't hardcode usage of std::time::{Duration, Instant}. Instead
make it generic. Still defaults to std time for ergonomics.

Should make it possible to use maybenot with for example `coarsetime`
crate.
  • Loading branch information
faern committed Jul 18, 2024
1 parent ae855b7 commit 559eb6f
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 42 deletions.
11 changes: 5 additions & 6 deletions crates/maybenot/src/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ use crate::constants::*;
use crate::*;
use std::fmt;
use std::hash::Hash;
use std::time::Duration;

use self::dist::Dist;

Expand Down Expand Up @@ -164,7 +163,7 @@ impl Action {

/// The action to be taken by the framework user.
#[derive(PartialEq, Eq, Debug, Clone)]
pub enum TriggerAction {
pub enum TriggerAction<T: crate::time::Instant = std::time::Instant> {
/// Cancel the timer for a machine.
Cancel { machine: MachineId, timer: Timer },
/// Schedule padding to be injected after the given timeout for a machine.
Expand All @@ -181,7 +180,7 @@ pub enum TriggerAction {
/// blocking may be bypassed, then non-padding packets MAY replace the
/// padding packet AND bypass the active blocking.
SendPadding {
timeout: Duration,
timeout: T::Duration,
bypass: bool,
replace: bool,
machine: MachineId,
Expand All @@ -196,8 +195,8 @@ pub enum TriggerAction {
/// currently ongoing blocking of outgoing traffic. If the flag is false,
/// the longest of the two durations MUST be used.
BlockOutgoing {
timeout: Duration,
duration: Duration,
timeout: T::Duration,
duration: T::Duration,
bypass: bool,
replace: bool,
machine: MachineId,
Expand All @@ -208,7 +207,7 @@ pub enum TriggerAction {
/// timer duration. If the flag is false, the longest of the two durations
/// MUST be used.
UpdateTimer {
duration: Duration,
duration: T::Duration,
replace: bool,
machine: MachineId,
},
Expand Down
79 changes: 43 additions & 36 deletions crates/maybenot/src/framework.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@
use rand_core::RngCore;

use crate::*;
use std::time::Duration;
use std::time::Instant;

use self::action::Action;
use self::constants::STATE_END;
use self::constants::STATE_LIMIT_MAX;
use self::counter::Counter;
use self::counter::Operation;
use self::event::Event;
use crate::time::Duration as _;

/// An opaque token representing one machine running inside the framework.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
Expand All @@ -38,13 +37,13 @@ impl MachineId {
}

#[derive(Debug, Clone)]
struct MachineRuntime {
struct MachineRuntime<T: crate::time::Instant> {
current_state: usize,
state_limit: u64,
padding_sent: u64,
normal_sent: u64,
blocking_duration: Duration,
machine_start: Instant,
blocking_duration: T::Duration,
machine_start: T,
counter_a: u64,
counter_b: u64,
}
Expand All @@ -62,49 +61,57 @@ enum StateChange {
/// channel, and produces as *output* zero or more [`TriggerAction`], such as to
/// *send padding* traffic or *block outgoing* traffic. One or more [`Machine`]
/// determine what [`TriggerAction`] to take based on [`TriggerEvent`].
pub struct Framework<M, R> {
pub struct Framework<M, R, T = std::time::Instant>
where
T: crate::time::Instant,
{
// updated each time the framework is triggered
current_time: Instant,
current_time: T,
// random number generator, used for sampling distributions and transitions
rng: R,
// we allocate the actions vector once and reuse it, handing out references
// as part of the iterator in [`Framework::trigger_events`].
actions: Vec<Option<TriggerAction>>,
actions: Vec<Option<TriggerAction<T>>>,
// the machines are immutable, but we need to keep track of their runtime
// state (size independent of number of states in the machine).
machines: M,
runtime: Vec<MachineRuntime>,
runtime: Vec<MachineRuntime<T>>,
// padding accounting
max_padding_frac: f64,
normal_sent_packets: u64,
padding_sent_packets: u64,
// blocking accounting
max_blocking_frac: f64,
blocking_duration: Duration,
blocking_started: Instant,
blocking_duration: T::Duration,
blocking_started: T,
blocking_active: bool,
framework_start: Instant,
framework_start: T,
}

impl<M, R> Framework<M, R>
impl<M, R, T> Framework<M, R, T>
where
M: AsRef<[Machine]>,
R: RngCore,
T: crate::time::Instant,
{
/// Create a new framework instance with zero or more [`Machine`]. The max
/// padding/blocking fractions are enforced as a total across all machines.
/// Create a new framework instance with zero or more [`Machine`].
///
/// The max padding/blocking fractions are enforced as a total across all machines.
/// The only way those limits can be violated are through
/// [`Machine::allowed_padding_packets`] and
/// [`Machine::allowed_blocked_microsec`], respectively. The current time is
/// handed to the framework here (and later in [`Self::trigger_events()`]) to
/// make some types of use cases of the framework easier (weird machines and
/// for simulation). Returns an error on any invalid [`Machine`] or limits
/// not being fractions [0.0, 1.0].
/// [`Machine::allowed_blocked_microsec`], respectively.
///
/// The current time is handed to the framework here (and later in [`Self::trigger_events()`])
/// to make some types of use cases of the framework easier (weird machines and
/// for simulation). The generic time type also allows for using custom time sources.
/// This can for example improve performance.
///
/// Returns an error on any invalid [`Machine`] or limits not being fractions [0.0, 1.0].
pub fn new(
machines: M,
max_padding_frac: f64,
max_blocking_frac: f64,
current_time: Instant,
current_time: T,
rng: R,
) -> Result<Self, Error> {
for m in machines.as_ref() {
Expand All @@ -124,7 +131,7 @@ where
state_limit: 0,
padding_sent: 0,
normal_sent: 0,
blocking_duration: Duration::from_secs(0),
blocking_duration: T::Duration::zero(),
machine_start: current_time,
counter_a: 0,
counter_b: 0,
Expand All @@ -146,7 +153,7 @@ where
framework_start: current_time,
blocking_active: false,
blocking_started: current_time,
blocking_duration: Duration::from_secs(0),
blocking_duration: T::Duration::zero(),
padding_sent_packets: 0,
normal_sent_packets: 0,
};
Expand All @@ -167,13 +174,13 @@ where

/// Trigger zero or more [`TriggerEvent`] for all machines running in the
/// framework. The current time SHOULD be the current time at time of
/// calling the method (e.g., [`Instant::now()`]). Returns an iterator of
/// zero or more [`TriggerAction`] that MUST be taken by the caller.
/// calling the method (e.g., [`Instant::now()`](std::time::Instant::now())).
/// Returns an iterator of zero or more [`TriggerAction`] that MUST be taken by the caller.
pub fn trigger_events(
&mut self,
events: &[TriggerEvent],
current_time: Instant,
) -> impl Iterator<Item = &TriggerAction> {
current_time: T,
) -> impl Iterator<Item = &TriggerAction<T>> {
// reset all actions
self.actions.fill(None);

Expand Down Expand Up @@ -260,7 +267,7 @@ where
}
}
TriggerEvent::BlockingEnd => {
let mut blocked: Duration = Duration::from_secs(0);
let mut blocked = T::Duration::zero();
if self.blocking_active {
blocked = self.current_time.duration_since(self.blocking_started);
self.blocking_duration += blocked;
Expand Down Expand Up @@ -408,22 +415,22 @@ where
Action::SendPadding {
bypass, replace, ..
} => Some(TriggerAction::SendPadding {
timeout: Duration::from_micros(action.sample_timeout(&mut self.rng)),
timeout: T::Duration::from_micros(action.sample_timeout(&mut self.rng)),
bypass,
replace,
machine: index,
}),
Action::BlockOutgoing {
bypass, replace, ..
} => Some(TriggerAction::BlockOutgoing {
timeout: Duration::from_micros(action.sample_timeout(&mut self.rng)),
duration: Duration::from_micros(action.sample_duration(&mut self.rng)),
timeout: T::Duration::from_micros(action.sample_timeout(&mut self.rng)),
duration: T::Duration::from_micros(action.sample_duration(&mut self.rng)),
bypass,
replace,
machine: index,
}),
Action::UpdateTimer { replace, .. } => Some(TriggerAction::UpdateTimer {
duration: Duration::from_micros(action.sample_duration(&mut self.rng)),
duration: T::Duration::from_micros(action.sample_duration(&mut self.rng)),
replace,
machine: index,
}),
Expand All @@ -448,7 +455,7 @@ where
}
}

fn below_action_limits(&self, runtime: &MachineRuntime, machine: &Machine) -> bool {
fn below_action_limits(&self, runtime: &MachineRuntime<T>, machine: &Machine) -> bool {
let current = &machine.states[runtime.current_state];

let Some(action) = current.action else {
Expand All @@ -463,7 +470,7 @@ where
}
}

fn below_limit_blocking(&self, runtime: &MachineRuntime, machine: &Machine) -> bool {
fn below_limit_blocking(&self, runtime: &MachineRuntime<T>, machine: &Machine) -> bool {
let current = &machine.states[runtime.current_state];
// blocking action

Expand All @@ -490,7 +497,7 @@ where

// machine allowed blocking duration first, since it bypasses the
// other two types of limits
if m_block_dur < Duration::from_micros(machine.allowed_blocked_microsec) {
if m_block_dur < T::Duration::from_micros(machine.allowed_blocked_microsec) {
// we still check against state limit, because it's machine internal
return runtime.state_limit > 0;
}
Expand Down Expand Up @@ -525,7 +532,7 @@ where
runtime.state_limit > 0
}

fn below_limit_padding(&self, runtime: &MachineRuntime, machine: &Machine) -> bool {
fn below_limit_padding(&self, runtime: &MachineRuntime<T>, machine: &Machine) -> bool {
// no limits apply if not made up padding count
if runtime.padding_sent < machine.allowed_padding_packets {
return runtime.state_limit > 0;
Expand Down
1 change: 1 addition & 0 deletions crates/maybenot/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ pub mod event;
mod framework;
mod machine;
pub mod state;
pub mod time;

pub use crate::action::{Timer, TriggerAction};
pub use crate::error::Error;
Expand Down
61 changes: 61 additions & 0 deletions crates/maybenot/src/time.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use std::ops::AddAssign;

/// Trait representing instants in time. Allows using maybenot frameworks with
/// custom time sources. If you want to use maybenot with a different time source
/// than `std::time::Instant`, implement this trait for your instant type, and the
/// [`Duration`] trait for your corresponding duration type.
pub trait Instant: Clone + Copy {
type Duration: Duration;

/// Returns the amount of time elapsed from another instant to this one.
///
/// Should return a zero duration if `earlier` is later than `self`
fn duration_since(&self, earlier: Self) -> Self::Duration;
}

pub trait Duration: Clone + Copy + AddAssign + PartialOrd {
/// Creates a new duration, spanning no time.
fn zero() -> Self;

/// Creates a new duration from the specified number of microseconds.
fn from_micros(micros: u64) -> Self;

/// Returns the total number of whole microseconds contained by this duration.
fn as_micros(&self) -> u128;

/// Returns true if this duration spans no time.
fn is_zero(&self) -> bool;
}

impl Instant for std::time::Instant {
type Duration = std::time::Duration;

#[inline(always)]
fn duration_since(&self, earlier: Self) -> Self::Duration {
// Not using `duration_since` directly since it panics on Rust <= 1.59
// instead of returning a zero time when `earlier` is later than `self`
self.checked_duration_since(earlier).unwrap_or_default()
}
}

impl Duration for std::time::Duration {
#[inline(always)]
fn zero() -> Self {
Self::ZERO
}

#[inline(always)]
fn from_micros(micros: u64) -> Self {
Self::from_micros(micros)
}

#[inline(always)]
fn as_micros(&self) -> u128 {
self.as_micros()
}

#[inline(always)]
fn is_zero(&self) -> bool {
self.is_zero()
}
}

0 comments on commit 559eb6f

Please sign in to comment.