use std::time::Instant;
use ferrox_api::Usage;
pub(super) struct RowClock {
started: Instant,
prefill_done: Option<Instant>,
first_token: Option<Instant>,
}
impl RowClock {
pub(super) fn start() -> Self {
Self {
started: Instant::now(),
prefill_done: None,
first_token: None,
}
}
pub(super) fn prefill_finished(&mut self) {
self.prefill_done = Some(Instant::now());
}
pub(super) fn token(&mut self) {
self.first_token.get_or_insert_with(Instant::now);
}
pub(super) fn usage(&self, prompt_tokens: usize, completion_tokens: usize) -> Usage {
let ended = Instant::now();
let prefill_end = self.prefill_done.unwrap_or(ended);
let prefill_secs = prefill_end.duration_since(self.started).as_secs_f64();
let decode_secs = ended.duration_since(prefill_end).as_secs_f64();
let usage =
Usage::new(prompt_tokens, completion_tokens).with_timings(prefill_secs, decode_secs);
match self.first_token {
Some(first) => usage.with_ttft(first.duration_since(self.started).as_secs_f64()),
None => usage,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_finished_row_reports_both_rates() {
let mut clock = RowClock::start();
std::thread::sleep(std::time::Duration::from_millis(2));
clock.prefill_finished();
clock.token();
std::thread::sleep(std::time::Duration::from_millis(2));
let usage = clock.usage(41, 32);
assert!(
usage.prompt_per_second.is_some(),
"prefill rate missing: {usage:?}"
);
assert!(
usage.predicted_per_second.is_some(),
"decode rate missing: {usage:?}"
);
assert!(
usage.time_to_first_token_ms.is_some(),
"ttft missing: {usage:?}"
);
}
#[test]
fn only_the_first_token_sets_ttft() {
let mut clock = RowClock::start();
clock.prefill_finished();
clock.token();
let first = clock.first_token.expect("first token recorded");
std::thread::sleep(std::time::Duration::from_millis(2));
clock.token();
assert_eq!(
clock.first_token.expect("still recorded"),
first,
"a later token moved the TTFT"
);
}
#[test]
fn a_row_that_never_decoded_has_no_decode_rate() {
let clock = RowClock::start();
let usage = clock.usage(41, 0);
assert_eq!(usage.predicted_per_second, None);
assert_eq!(usage.time_to_first_token_ms, None);
assert!(usage.generation_duration_ms.is_some_and(|ms| ms >= 0.0));
}
}