ora-timer 0.2.0

Part of the Ora scheduler framework.
Documentation
#![allow(missing_docs, clippy::cast_possible_truncation)]
use std::{mem, time::Duration};

use minstant::Instant;
use ora_timer::{
    resolution::MillisecondResolution, run_binary_heap_timer, run_hierarchical_timer, Delayed,
    TimerLoopAction, TimerOptions,
};
use rand::{rngs::StdRng, Rng, SeedableRng};

const BASE_DELAY: Duration = Duration::from_secs(1);

fn main() {
    let num_jobs = [1, 10, 100, 1000, 10_000, 100_000, 1_000_000, 10_000_000];

    for num_jobs in num_jobs {
        println!("{num_jobs} jobs:");
        bench_timer::<false, false>(num_jobs);
        bench_timer::<true, false>(num_jobs);
        bench_timer::<false, true>(num_jobs);
        bench_timer::<true, true>(num_jobs);
    }
}

fn bench_timer<const RANDOMIZED: bool, const ARBITRARY_RESOLUTION: bool>(num_jobs: usize) {
    let mut rng = StdRng::seed_from_u64(0);

    let start = Instant::now();
    let mut jobs = (0..num_jobs)
        .map(|_| {
            let delay = if RANDOMIZED {
                BASE_DELAY + Duration::from_millis(rng.gen_range(0..2000))
            } else {
                BASE_DELAY
            };

            Delayed::new(
                Job {
                    created: start,
                    delay,
                },
                delay,
            )
        })
        .collect::<Vec<Delayed<Job>>>();

    let initial_delay = Instant::now() - start;

    let mut completed_jobs = Vec::with_capacity(num_jobs);

    let timer_options = TimerOptions::default();

    let cb = |new: &mut Vec<Delayed<Job>>, ready: &mut Vec<Job>| {
        let completed = Instant::now();
        completed_jobs.extend(ready.drain(..).map(|job| CompletedJob {
            created: job.created,
            delay: job.delay,
            completed,
        }));

        if completed_jobs.len() >= num_jobs {
            TimerLoopAction::StopWhenIdle
        } else {
            new.extend(mem::take(&mut jobs));
            TimerLoopAction::Continue
        }
    };

    if ARBITRARY_RESOLUTION {
        run_binary_heap_timer(timer_options, cb);
    } else {
        run_hierarchical_timer::<Job, MillisecondResolution>(timer_options, cb);
    }

    let max_latency = completed_jobs
        .iter()
        .map(|job| (job.completed - job.created - job.delay).saturating_sub(initial_delay))
        .max()
        .unwrap();

    let avg_latency = completed_jobs
        .iter()
        .map(|job| (job.completed - job.created - job.delay).saturating_sub(initial_delay))
        .sum::<Duration>()
        / completed_jobs.len() as u32;

    let min_latency = completed_jobs
        .iter()
        .map(|job| (job.completed - job.created - job.delay).saturating_sub(initial_delay))
        .min()
        .unwrap();

    print!("\tmean: {avg_latency:?}\tmax: {max_latency:?}\tmin: {min_latency:?}");
    println!("\trandomized: {RANDOMIZED}\tarbitrary resolution: {ARBITRARY_RESOLUTION}");
}

struct Job {
    created: Instant,
    delay: Duration,
}

struct CompletedJob {
    created: Instant,
    delay: Duration,
    completed: Instant,
}

impl PartialOrd for Job {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(self.delay.cmp(&other.delay))
    }
}

impl Ord for Job {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        self.delay.cmp(&other.delay)
    }
}

impl PartialEq for Job {
    fn eq(&self, other: &Self) -> bool {
        self.delay == other.delay
    }
}

impl Eq for Job {}