metrale_telemetry/
request.rs1use crate::instrument::{Counter, MillijouleHistogram, NanosHistogram};
9
10#[derive(Clone, Copy, Debug, PartialEq)]
14pub struct RequestTiming {
15 pub ttft_ms: f64,
17 pub decode_ms: f64,
19 pub output_tokens: u64,
20}
21
22impl RequestTiming {
23 pub fn e2e_ns(&self) -> u64 {
25 ms_to_ns(self.ttft_ms + self.decode_ms)
26 }
27
28 pub fn tpot_ns(&self) -> Option<u64> {
31 (self.output_tokens >= 2)
32 .then(|| ms_to_ns(self.decode_ms / (self.output_tokens - 1) as f64))
33 }
34}
35
36fn ms_to_ns(ms: f64) -> u64 {
37 if ms.is_finite() && ms > 0.0 {
38 (ms * 1e6) as u64
39 } else {
40 0
41 }
42}
43
44#[derive(Debug)]
45pub struct RequestInstruments {
46 pub finished: Counter,
47 pub output_tokens: Counter,
49 pub ttft: NanosHistogram,
50 pub tpot: NanosHistogram,
51 pub e2e: NanosHistogram,
52 pub energy: MillijouleHistogram,
53 pub energy_unattributed: Counter,
56}
57
58impl Default for RequestInstruments {
59 fn default() -> Self {
60 Self::new()
61 }
62}
63
64impl RequestInstruments {
65 pub const fn new() -> Self {
66 Self {
67 finished: Counter::new(),
68 output_tokens: Counter::new(),
69 ttft: NanosHistogram::new(),
70 tpot: NanosHistogram::new(),
71 e2e: NanosHistogram::new(),
72 energy: MillijouleHistogram::new(),
73 energy_unattributed: Counter::new(),
74 }
75 }
76
77 pub(crate) fn record(&self, r: &RequestTiming, energy_mj: Option<u64>) {
78 self.finished.add(1);
79 self.output_tokens.add(r.output_tokens);
80 self.ttft.observe(ms_to_ns(r.ttft_ms));
81 if let Some(t) = r.tpot_ns() {
82 self.tpot.observe(t);
83 }
84 self.e2e.observe(r.e2e_ns());
85 match energy_mj {
86 Some(mj) => self.energy.observe(mj),
87 None => self.energy_unattributed.add(1),
88 }
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn tpot_excludes_the_first_token_and_needs_two() {
98 let r = RequestTiming {
99 ttft_ms: 100.0,
100 decode_ms: 90.0,
101 output_tokens: 10,
102 };
103 assert_eq!(r.tpot_ns(), Some(10_000_000));
104 assert_eq!(r.e2e_ns(), 190_000_000);
105 let one = RequestTiming {
106 output_tokens: 1,
107 ..r
108 };
109 assert_eq!(one.tpot_ns(), None);
110 }
111
112 #[test]
113 fn a_non_finite_time_records_as_zero_not_a_wrapped_huge_value() {
114 assert_eq!(ms_to_ns(f64::NAN), 0);
115 assert_eq!(ms_to_ns(-5.0), 0);
116 assert_eq!(ms_to_ns(f64::INFINITY), 0);
117 }
118}