Coverage Report

Created: 2026-01-25 15:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/noah/src/trueno/src/brick/perf_metrics.rs
Line
Count
Source
1
//! Performance Metrics Breakdown
2
//!
3
//! LCP-04 pattern from llama.cpp for tracking inference timing.
4
5
// ----------------------------------------------------------------------------
6
// LCP-04: Performance Metrics Breakdown (llama.cpp pattern)
7
// ----------------------------------------------------------------------------
8
9
/// Performance metrics breakdown for inference phases.
10
///
11
/// Tracks timing for each phase of LLM inference:
12
/// - Model loading (t_load_ms)
13
/// - Prompt evaluation / prefill (t_p_eval_ms)
14
/// - Token generation / decode (t_eval_ms)
15
///
16
/// # Example
17
/// ```rust,ignore
18
/// use trueno::brick::PerfMetrics;
19
///
20
/// let mut metrics = PerfMetrics::default();
21
/// metrics.record_load(1500);  // 1.5s model load
22
/// metrics.record_prefill(200, 512);  // 200ms for 512 prompt tokens
23
/// metrics.record_decode(50);  // 50ms per generated token
24
///
25
/// println!("{}", metrics.summary());
26
/// ```
27
#[derive(Debug, Clone, Default)]
28
pub struct PerfMetrics {
29
    /// Model loading time (milliseconds)
30
    pub t_load_ms: u64,
31
    /// Prompt evaluation time - prefill phase (milliseconds)
32
    pub t_p_eval_ms: u64,
33
    /// Token generation time - decode phase (milliseconds)
34
    pub t_eval_ms: u64,
35
    /// Number of tokens in prompt (prefill)
36
    pub n_p_eval: u32,
37
    /// Number of tokens generated (decode)
38
    pub n_eval: u32,
39
    /// Sample count for t_eval (for averaging)
40
    pub n_samples: u32,
41
}
42
43
impl PerfMetrics {
44
    /// Create new metrics instance.
45
0
    pub fn new() -> Self {
46
0
        Self::default()
47
0
    }
48
49
    /// Record model loading time.
50
0
    pub fn record_load(&mut self, ms: u64) {
51
0
        self.t_load_ms = ms;
52
0
    }
53
54
    /// Record prefill (prompt evaluation) time.
55
0
    pub fn record_prefill(&mut self, ms: u64, tokens: u32) {
56
0
        self.t_p_eval_ms = ms;
57
0
        self.n_p_eval = tokens;
58
0
    }
59
60
    /// Record a single decode step.
61
0
    pub fn record_decode(&mut self, ms: u64) {
62
0
        self.t_eval_ms += ms;
63
0
        self.n_eval += 1;
64
0
        self.n_samples += 1;
65
0
    }
66
67
    /// Record batch decode step.
68
0
    pub fn record_decode_batch(&mut self, ms: u64, tokens: u32) {
69
0
        self.t_eval_ms += ms;
70
0
        self.n_eval += tokens;
71
0
        self.n_samples += 1;
72
0
    }
73
74
    /// Tokens per second during generation (decode throughput).
75
    #[must_use]
76
0
    pub fn tokens_per_second(&self) -> f64 {
77
0
        if self.t_eval_ms == 0 {
78
0
            0.0
79
        } else {
80
0
            1000.0 * self.n_eval as f64 / self.t_eval_ms as f64
81
        }
82
0
    }
83
84
    /// Tokens per second during prompt evaluation (prefill throughput).
85
    #[must_use]
86
0
    pub fn prefill_tokens_per_second(&self) -> f64 {
87
0
        if self.t_p_eval_ms == 0 {
88
0
            0.0
89
        } else {
90
0
            1000.0 * self.n_p_eval as f64 / self.t_p_eval_ms as f64
91
        }
92
0
    }
93
94
    /// Total time for complete inference.
95
    #[must_use]
96
0
    pub fn total_ms(&self) -> u64 {
97
0
        self.t_load_ms + self.t_p_eval_ms + self.t_eval_ms
98
0
    }
99
100
    /// Time-to-first-token (TTFT).
101
    #[must_use]
102
0
    pub fn time_to_first_token_ms(&self) -> u64 {
103
0
        self.t_load_ms + self.t_p_eval_ms
104
0
    }
105
106
    /// Average time per token during decode.
107
    #[must_use]
108
0
    pub fn avg_token_latency_ms(&self) -> f64 {
109
0
        if self.n_eval == 0 {
110
0
            0.0
111
        } else {
112
0
            self.t_eval_ms as f64 / self.n_eval as f64
113
        }
114
0
    }
115
116
    /// Formatted summary string.
117
    #[must_use]
118
0
    pub fn summary(&self) -> String {
119
0
        format!(
120
0
            "load: {}ms, prefill: {}ms ({:.1} tok/s, {} tokens), decode: {}ms ({:.1} tok/s, {} tokens), total: {}ms",
121
            self.t_load_ms,
122
            self.t_p_eval_ms,
123
0
            self.prefill_tokens_per_second(),
124
            self.n_p_eval,
125
            self.t_eval_ms,
126
0
            self.tokens_per_second(),
127
            self.n_eval,
128
0
            self.total_ms()
129
        )
130
0
    }
131
132
    /// Reset all metrics.
133
0
    pub fn reset(&mut self) {
134
0
        *self = Self::default();
135
0
    }
136
}
137
138
// ----------------------------------------------------------------------------
139
// LCP-01: Inference Phase (for Arena Allocation)
140
// ----------------------------------------------------------------------------
141
142
/// Inference phase for dual-arena allocation.
143
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
144
pub enum InferencePhase {
145
    /// Processing prompt, large batches
146
    #[default]
147
    Prefill,
148
    /// Generating tokens, small batches
149
    Decode,
150
}
151
152
#[cfg(test)]
153
mod tests {
154
    use super::*;
155
156
    #[test]
157
    fn test_perf_metrics_default() {
158
        let metrics = PerfMetrics::default();
159
        assert_eq!(metrics.t_load_ms, 0);
160
        assert_eq!(metrics.t_p_eval_ms, 0);
161
        assert_eq!(metrics.t_eval_ms, 0);
162
        assert_eq!(metrics.n_p_eval, 0);
163
        assert_eq!(metrics.n_eval, 0);
164
    }
165
166
    #[test]
167
    fn test_perf_metrics_record_load() {
168
        let mut metrics = PerfMetrics::new();
169
        metrics.record_load(1500);
170
        assert_eq!(metrics.t_load_ms, 1500);
171
    }
172
173
    #[test]
174
    fn test_perf_metrics_record_prefill() {
175
        let mut metrics = PerfMetrics::new();
176
        metrics.record_prefill(200, 512);
177
        assert_eq!(metrics.t_p_eval_ms, 200);
178
        assert_eq!(metrics.n_p_eval, 512);
179
    }
180
181
    #[test]
182
    fn test_perf_metrics_record_decode() {
183
        let mut metrics = PerfMetrics::new();
184
        metrics.record_decode(50);
185
        metrics.record_decode(50);
186
        assert_eq!(metrics.t_eval_ms, 100);
187
        assert_eq!(metrics.n_eval, 2);
188
        assert_eq!(metrics.n_samples, 2);
189
    }
190
191
    #[test]
192
    fn test_perf_metrics_tokens_per_second() {
193
        let mut metrics = PerfMetrics::new();
194
        metrics.record_decode_batch(1000, 100); // 100 tokens in 1 second
195
        assert!((metrics.tokens_per_second() - 100.0).abs() < 0.001);
196
    }
197
198
    #[test]
199
    fn test_perf_metrics_prefill_throughput() {
200
        let mut metrics = PerfMetrics::new();
201
        metrics.record_prefill(500, 1000); // 1000 tokens in 500ms = 2000 tok/s
202
        assert!((metrics.prefill_tokens_per_second() - 2000.0).abs() < 0.001);
203
    }
204
205
    #[test]
206
    fn test_perf_metrics_total_ms() {
207
        let mut metrics = PerfMetrics::new();
208
        metrics.record_load(1000);
209
        metrics.record_prefill(200, 512);
210
        metrics.record_decode_batch(300, 100);
211
        assert_eq!(metrics.total_ms(), 1500);
212
    }
213
214
    #[test]
215
    fn test_perf_metrics_time_to_first_token() {
216
        let mut metrics = PerfMetrics::new();
217
        metrics.record_load(1000);
218
        metrics.record_prefill(200, 512);
219
        assert_eq!(metrics.time_to_first_token_ms(), 1200);
220
    }
221
222
    #[test]
223
    fn test_perf_metrics_reset() {
224
        let mut metrics = PerfMetrics::new();
225
        metrics.record_load(1500);
226
        metrics.record_prefill(200, 512);
227
        metrics.reset();
228
        assert_eq!(metrics.t_load_ms, 0);
229
        assert_eq!(metrics.n_p_eval, 0);
230
    }
231
232
    #[test]
233
    fn test_inference_phase_default() {
234
        let phase = InferencePhase::default();
235
        assert_eq!(phase, InferencePhase::Prefill);
236
    }
237
238
    #[test]
239
    fn test_inference_phase_eq() {
240
        assert_eq!(InferencePhase::Prefill, InferencePhase::Prefill);
241
        assert_eq!(InferencePhase::Decode, InferencePhase::Decode);
242
        assert_ne!(InferencePhase::Prefill, InferencePhase::Decode);
243
    }
244
}