use std::sync::Arc;
use std::time::{Duration, Instant};
use tract_core::internal::*;
use tract_core::num_traits::Zero;
use tract_core::runtime::Runnable;
use crate::profile::{BenchLimits, reusable_state};
use crate::tensor::RunTensors;
impl BenchLimits {
pub fn bench_streams(
&self,
runnable: &Arc<dyn Runnable>,
inputs: &RunTensors,
streams: usize,
) -> TractResult<StreamsBench> {
ensure!(streams > 0, "A saturating bench needs a stream at least");
let reuse = reusable_state(runnable);
let max_loops = if self.max_loops.is_zero() { usize::MAX } else { self.max_loops };
let max_time = if self.max_time.is_zero() { Duration::MAX } else { self.max_time };
let start = Instant::now();
let turns: Vec<Vec<Duration>> = std::thread::scope(|scope| {
let running: Vec<_> = (0..streams)
.map(|_| {
scope.spawn(|| -> TractResult<Vec<Duration>> {
let mut state = runnable.spawn()?;
let mut turns = vec![];
while turns.len() < max_loops && start.elapsed() < max_time {
if !reuse {
state = runnable.spawn()?;
}
let turn = Instant::now();
state.run(inputs.sources[0].clone())?;
turns.push(turn.elapsed());
}
Ok(turns)
})
})
.collect();
running.into_iter().map(|stream| stream.join().unwrap()).collect::<Vec<_>>()
})
.into_iter()
.collect::<TractResult<_>>()?;
let wall = start.elapsed();
let mut latencies: Vec<Duration> = turns.into_iter().flatten().collect();
latencies.sort();
Ok(StreamsBench { streams, wall, latencies })
}
pub fn bench_streams_paced(
&self,
runnable: &Arc<dyn Runnable>,
inputs: &RunTensors,
streams: usize,
pacing: &Pacing,
) -> TractResult<PacedBench> {
use rand::SeedableRng;
ensure!(streams > 0, "A paced bench needs a stream at least");
ensure!(!pacing.period.is_zero(), "A paced bench needs a turn period");
let max_loops = if self.max_loops.is_zero() { usize::MAX } else { self.max_loops };
let max_time = if self.max_time.is_zero() { Duration::MAX } else { self.max_time };
ensure!(
max_time >= 4 * pacing.period,
"A turn period of {:?} wants four periods of run at least, --max-time gives {max_time:?}",
pacing.period
);
let reuse = reusable_state(runnable);
ensure!(
pacing.churn.is_none() || reuse,
"Churn ends a session and admits another in its place, which only means something for a model carrying a session across turns"
);
let start = Instant::now();
let served: Vec<StreamTurns> = std::thread::scope(|scope| {
let running: Vec<_> = (0..streams)
.map(|stream| {
scope.spawn(move || -> TractResult<StreamTurns> {
let mut rng = rand::rngs::StdRng::seed_from_u64(stream as u64);
let phase = pacing.period.mul_f64(stream as f64 / streams as f64);
let mut turns = StreamTurns::default();
let mut grid: Option<Instant> = None;
let mut slot = 0usize;
while turns.service.len() < max_loops && start.elapsed() < max_time {
let (mut state, starved, took) = admit(runnable, &start, max_time)?;
turns.admissions += 1;
turns.starved += usize::from(starved);
turns.admitting.push(took);
state.run(inputs.sources[0].clone())?;
let admitted = Instant::now();
let grid = *grid.get_or_insert(admitted + phase);
if let Some(late) = admitted.checked_duration_since(grid) {
let missed = late.div_duration_f64(pacing.period).ceil();
slot = slot.max(missed as usize);
}
let hold = pacing
.churn
.map(|churn| exponential(&mut rng, churn.hold).max(pacing.period));
while turns.service.len() < max_loops && start.elapsed() < max_time {
if hold.is_some_and(|hold| admitted.elapsed() >= hold) {
break;
}
if !reuse {
state = runnable.spawn()?;
}
let due = grid + pacing.period * slot as u32;
slot += 1;
if let Some(wait) = due.checked_duration_since(Instant::now()) {
std::thread::sleep(wait);
}
let turn = Instant::now();
state.run(inputs.sources[0].clone())?;
let done = Instant::now();
turns.service.push(done - turn);
turns.added.push(done.saturating_duration_since(due));
turns.span = done - grid;
}
}
Ok(turns)
})
})
.collect();
running.into_iter().map(|stream| stream.join().unwrap()).collect::<Vec<_>>()
})
.into_iter()
.collect::<TractResult<_>>()?;
let wall = served.iter().map(|turns| turns.span).max().unwrap_or_default();
let admissions = served.iter().map(|turns| turns.admissions).sum();
let starved = served.iter().map(|turns| turns.starved).sum();
let mut admitting: Vec<Duration> =
served.iter().flat_map(|turns| turns.admitting.iter().copied()).collect();
admitting.sort();
let mut service: Vec<Duration> =
served.iter().flat_map(|turns| turns.service.iter().copied()).collect();
let mut added: Vec<Duration> = served.into_iter().flat_map(|turns| turns.added).collect();
service.sort();
added.sort();
Ok(PacedBench {
streams,
deadline: pacing.deadline,
wall,
service,
added,
admissions,
starved,
admitting,
})
}
}
fn admit(
runnable: &Arc<dyn Runnable>,
start: &Instant,
max_time: Duration,
) -> TractResult<(Box<dyn State>, bool, Duration)> {
let mut starved = false;
let asked = Instant::now();
loop {
match runnable.spawn() {
Ok(state) => return Ok((state, starved, asked.elapsed())),
Err(e) => {
if start.elapsed() >= max_time {
return Err(e).context("Admitting a session");
}
starved = true;
std::thread::sleep(Duration::from_micros(200));
}
}
}
}
fn exponential(rng: &mut impl rand::RngExt, mean: Duration) -> Duration {
let uniform: f64 = rng.random::<f64>();
mean.mul_f64(-(1. - uniform).max(f64::MIN_POSITIVE).ln())
}
#[derive(Clone, Copy, Debug)]
pub struct Pacing {
pub period: Duration,
pub deadline: Duration,
pub churn: Option<Churn>,
}
#[derive(Clone, Copy, Debug)]
pub struct Churn {
pub hold: Duration,
}
#[derive(Default)]
struct StreamTurns {
service: Vec<Duration>,
added: Vec<Duration>,
span: Duration,
admissions: usize,
starved: usize,
admitting: Vec<Duration>,
}
pub struct PacedBench {
pub streams: usize,
pub deadline: Duration,
pub wall: Duration,
pub service: Vec<Duration>,
pub added: Vec<Duration>,
pub admissions: usize,
pub starved: usize,
pub admitting: Vec<Duration>,
}
impl PacedBench {
pub fn admissions_per_second(&self) -> f64 {
self.admissions as f64 / self.wall.as_secs_f64()
}
pub fn admission_quantile(&self, q: f64) -> Duration {
quantile(&self.admitting, q)
}
pub fn starve_fraction(&self) -> f64 {
if self.admissions == 0 {
return 0.;
}
self.starved as f64 / self.admissions as f64
}
pub fn turns(&self) -> usize {
self.service.len()
}
pub fn turns_per_second(&self) -> f64 {
self.turns() as f64 / self.wall.as_secs_f64()
}
pub fn mean_service(&self) -> Duration {
self.service.iter().sum::<Duration>().checked_div(self.turns() as u32).unwrap_or_default()
}
pub fn added_quantile(&self, q: f64) -> Duration {
quantile(&self.added, q)
}
pub fn late_fraction(&self) -> f64 {
if self.added.is_empty() {
return 0.;
}
let in_time = self.added.partition_point(|added| *added <= self.deadline);
(self.turns() - in_time) as f64 / self.turns() as f64
}
pub fn holds(&self, q: f64) -> bool {
!self.added.is_empty() && self.added_quantile(q) <= self.deadline
}
}
fn quantile(sorted: &[Duration], q: f64) -> Duration {
if sorted.is_empty() {
return Duration::default();
}
sorted[((sorted.len() - 1) as f64 * q).round() as usize]
}
pub struct StreamsBench {
pub streams: usize,
pub wall: Duration,
pub latencies: Vec<Duration>,
}
impl StreamsBench {
pub fn turns(&self) -> usize {
self.latencies.len()
}
pub fn turns_per_second(&self) -> f64 {
self.turns() as f64 / self.wall.as_secs_f64()
}
pub fn mean_latency(&self) -> Duration {
self.latencies.iter().sum::<Duration>().checked_div(self.turns() as u32).unwrap_or_default()
}
pub fn latency_quantile(&self, q: f64) -> Duration {
quantile(&self.latencies, q)
}
}