1use ddsketchy::DDSketch;
5use rustc_hash::FxHashMap;
6use serde::Serialize;
7use serde::ser::{SerializeMap, Serializer};
8use serde_json::Value;
9use std::fmt::{Display, Formatter, Result as FmtResult};
10use uuid::Uuid;
11
12const DDSKETCH_RELATIVE_ACCURACY: f64 = 0.001;
16const DDSKETCH_MAX_BINS: usize = 32_768;
17
18#[derive(Debug, Clone)]
20pub struct ReplayReport {
21 pub request_counts: TraceRequestCounts,
22 pub throughput: TraceThroughputStats,
23 pub prefix_cache_reused_ratio: f64,
24 pub first_admission_prefix_cache_reused_ratio: f64,
25 pub latency: TraceLatencyStats,
26 pub goodput: Option<TraceGoodputStats>,
30 pub per_request: Vec<PerRequestRecord>,
36 pub runtime_evidence: crate::replay::OfflineRuntimeEvidence,
40}
41
42#[derive(Debug, Clone)]
43pub struct TraceRequestCounts {
44 pub num_requests: usize,
45 pub completed_requests: usize,
46 pub total_input_tokens: usize,
47 pub total_output_tokens: usize,
48}
49
50#[derive(Debug, Clone)]
51pub struct TraceThroughputStats {
52 pub duration_ms: f64,
53 pub wall_time_ms: f64,
54 pub request_throughput_rps: f64,
55 pub input_throughput_tok_s: f64,
56 pub output_throughput_tok_s: f64,
57 pub total_throughput_tok_s: f64,
58 pub prefill_worker_seconds: f64,
71 pub decode_worker_seconds: f64,
72 pub prefill_gpus_per_worker: usize,
76 pub decode_gpus_per_worker: usize,
77 pub gpu_hours: f64,
82}
83
84#[derive(Debug, Clone)]
89pub struct TraceGoodputStats {
90 pub completed_requests: usize,
92 pub request_throughput_rps: f64,
94 pub output_throughput_tok_s: f64,
96}
97
98#[derive(Debug, Clone)]
99pub struct TraceDistributionStats {
100 pub mean_ms: f64,
101 pub min_ms: f64,
102 pub max_ms: f64,
103 pub median_ms: f64,
104 pub p75_ms: f64,
105 pub p90_ms: f64,
106 pub p95_ms: f64,
107 pub p99_ms: f64,
108 pub std_ms: f64,
109}
110
111#[derive(Debug, Clone)]
112pub struct TraceLatencyStats {
113 pub ttft: TraceDistributionStats,
114 pub ttst: TraceDistributionStats,
115 pub tpot: TraceDistributionStats,
116 pub itl: TraceInterTokenLatencyStats,
117 pub e2e: TraceDistributionStats,
118 pub output_token_throughput_per_user: TraceDistributionStats,
119}
120
121#[derive(Debug, Clone)]
122pub struct TraceInterTokenLatencyStats {
123 pub distribution: TraceDistributionStats,
124 pub max_ms: f64,
125}
126
127impl ReplayReport {
128 pub fn with_wall_time_ms(mut self, wall_time_ms: f64) -> Self {
129 self.throughput.wall_time_ms = wall_time_ms;
130 self
131 }
132
133 pub fn processed_tokens(&self) -> usize {
134 self.request_counts.total_input_tokens + self.request_counts.total_output_tokens
135 }
136
137 pub fn processed_tokens_per_s(&self) -> f64 {
138 if self.throughput.wall_time_ms <= 0.0 {
139 return 0.0;
140 }
141 self.processed_tokens() as f64 / self.throughput.wall_time_ms * 1000.0
142 }
143
144 pub fn processed_output_tokens_per_s(&self) -> f64 {
145 if self.throughput.wall_time_ms <= 0.0 {
146 return 0.0;
147 }
148 self.request_counts.total_output_tokens as f64 / self.throughput.wall_time_ms * 1000.0
149 }
150}
151
152impl Display for ReplayReport {
153 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
154 writeln!(
155 f,
156 " completed_requests: {}",
157 self.request_counts.completed_requests
158 )?;
159 writeln!(
160 f,
161 " request_throughput_rps: {:.6}",
162 self.throughput.request_throughput_rps
163 )?;
164 writeln!(
165 f,
166 " output_throughput_tok_s: {:.6}",
167 self.throughput.output_throughput_tok_s
168 )?;
169 writeln!(
170 f,
171 " total_input_tokens: {}",
172 self.request_counts.total_input_tokens
173 )?;
174 writeln!(
175 f,
176 " total_output_tokens: {}",
177 self.request_counts.total_output_tokens
178 )?;
179 writeln!(
180 f,
181 " processed_tokens_per_s: {:.6}",
182 self.processed_tokens_per_s()
183 )?;
184 writeln!(
185 f,
186 " processed_output_tokens_per_s: {:.6}",
187 self.processed_output_tokens_per_s()
188 )?;
189 writeln!(f, " mean_ttft_ms: {:.6}", self.latency.ttft.mean_ms)?;
190 writeln!(f, " mean_e2e_latency_ms: {:.6}", self.latency.e2e.mean_ms)?;
191 writeln!(
192 f,
193 " prefix_cache_reused_ratio: {:.6}",
194 self.prefix_cache_reused_ratio
195 )?;
196 writeln!(
197 f,
198 " first_admission_prefix_cache_reused_ratio: {:.6}",
199 self.first_admission_prefix_cache_reused_ratio
200 )?;
201 write!(f, " wall_time_ms: {:.6}", self.throughput.wall_time_ms)
202 }
203}
204
205impl Serialize for ReplayReport {
206 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
207 where
208 S: Serializer,
209 {
210 let mut map = serializer.serialize_map(Some(70))?;
211 map.serialize_entry("num_requests", &self.request_counts.num_requests)?;
212 map.serialize_entry(
213 "completed_requests",
214 &self.request_counts.completed_requests,
215 )?;
216 map.serialize_entry(
217 "total_input_tokens",
218 &self.request_counts.total_input_tokens,
219 )?;
220 map.serialize_entry(
221 "total_output_tokens",
222 &self.request_counts.total_output_tokens,
223 )?;
224 map.serialize_entry("duration_ms", &self.throughput.duration_ms)?;
225 map.serialize_entry("wall_time_ms", &self.throughput.wall_time_ms)?;
226 map.serialize_entry(
227 "request_throughput_rps",
228 &self.throughput.request_throughput_rps,
229 )?;
230 map.serialize_entry(
231 "input_throughput_tok_s",
232 &self.throughput.input_throughput_tok_s,
233 )?;
234 map.serialize_entry(
235 "output_throughput_tok_s",
236 &self.throughput.output_throughput_tok_s,
237 )?;
238 map.serialize_entry(
239 "total_throughput_tok_s",
240 &self.throughput.total_throughput_tok_s,
241 )?;
242 map.serialize_entry(
243 "prefill_worker_seconds",
244 &self.throughput.prefill_worker_seconds,
245 )?;
246 map.serialize_entry(
247 "decode_worker_seconds",
248 &self.throughput.decode_worker_seconds,
249 )?;
250 map.serialize_entry(
251 "prefill_gpus_per_worker",
252 &self.throughput.prefill_gpus_per_worker,
253 )?;
254 map.serialize_entry(
255 "decode_gpus_per_worker",
256 &self.throughput.decode_gpus_per_worker,
257 )?;
258 map.serialize_entry("gpu_hours", &self.throughput.gpu_hours)?;
259 if let Some(goodput) = &self.goodput {
260 map.serialize_entry("goodput_completed_requests", &goodput.completed_requests)?;
261 map.serialize_entry(
262 "goodput_request_throughput_rps",
263 &goodput.request_throughput_rps,
264 )?;
265 map.serialize_entry(
266 "goodput_output_throughput_tok_s",
267 &goodput.output_throughput_tok_s,
268 )?;
269 }
270 map.serialize_entry("processed_tokens", &self.processed_tokens())?;
271 map.serialize_entry("processed_tokens_per_s", &self.processed_tokens_per_s())?;
272 map.serialize_entry(
273 "processed_output_tokens_per_s",
274 &self.processed_output_tokens_per_s(),
275 )?;
276 map.serialize_entry("prefix_cache_reused_ratio", &self.prefix_cache_reused_ratio)?;
277 map.serialize_entry(
278 "first_admission_prefix_cache_reused_ratio",
279 &self.first_admission_prefix_cache_reused_ratio,
280 )?;
281 serialize_distribution(&mut map, "ttft", &self.latency.ttft)?;
282 serialize_distribution(&mut map, "ttst", &self.latency.ttst)?;
283 serialize_distribution(&mut map, "tpot", &self.latency.tpot)?;
284 serialize_distribution(&mut map, "itl", &self.latency.itl.distribution)?;
285 map.serialize_entry("max_itl_ms", &self.latency.itl.max_ms)?;
286 serialize_distribution(&mut map, "e2e_latency", &self.latency.e2e)?;
287 serialize_rate_distribution(
288 &mut map,
289 "output_token_throughput_per_user",
290 &self.latency.output_token_throughput_per_user,
291 )?;
292 map.end()
293 }
294}
295
296fn serialize_distribution<S>(
297 map: &mut S,
298 prefix: &str,
299 stats: &TraceDistributionStats,
300) -> Result<(), S::Error>
301where
302 S: SerializeMap,
303{
304 map.serialize_entry(&format!("mean_{prefix}_ms"), &stats.mean_ms)?;
305 map.serialize_entry(&format!("min_{prefix}_ms"), &stats.min_ms)?;
306 map.serialize_entry(&format!("max_{prefix}_ms"), &stats.max_ms)?;
307 map.serialize_entry(&format!("median_{prefix}_ms"), &stats.median_ms)?;
308 map.serialize_entry(&format!("p75_{prefix}_ms"), &stats.p75_ms)?;
309 map.serialize_entry(&format!("p90_{prefix}_ms"), &stats.p90_ms)?;
310 map.serialize_entry(&format!("p95_{prefix}_ms"), &stats.p95_ms)?;
311 map.serialize_entry(&format!("p99_{prefix}_ms"), &stats.p99_ms)?;
312 map.serialize_entry(&format!("std_{prefix}_ms"), &stats.std_ms)?;
313 Ok(())
314}
315
316fn serialize_rate_distribution<S>(
317 map: &mut S,
318 prefix: &str,
319 stats: &TraceDistributionStats,
320) -> Result<(), S::Error>
321where
322 S: SerializeMap,
323{
324 map.serialize_entry(&format!("mean_{prefix}"), &stats.mean_ms)?;
325 map.serialize_entry(&format!("min_{prefix}"), &stats.min_ms)?;
326 map.serialize_entry(&format!("max_{prefix}"), &stats.max_ms)?;
327 map.serialize_entry(&format!("median_{prefix}"), &stats.median_ms)?;
328 map.serialize_entry(&format!("p75_{prefix}"), &stats.p75_ms)?;
329 map.serialize_entry(&format!("p90_{prefix}"), &stats.p90_ms)?;
330 map.serialize_entry(&format!("p95_{prefix}"), &stats.p95_ms)?;
331 map.serialize_entry(&format!("p99_{prefix}"), &stats.p99_ms)?;
332 map.serialize_entry(&format!("std_{prefix}"), &stats.std_ms)?;
333 Ok(())
334}
335
336#[derive(Debug)]
337struct TraceRequestStats {
338 arrival_time_ms: f64,
339 first_admit_ms: Option<f64>,
340 terminal_time_ms: Option<f64>,
341 terminal_status: Option<ReplayTerminalStatus>,
342 token_timeline: TokenTimeline,
343 input_length: usize,
344 requested_output_length: usize,
345 reused_input_tokens: usize,
346 first_admission_reused_input_tokens: usize,
347 prefill_worker_idx: Option<usize>,
356 decode_worker_idx: Option<usize>,
358 session_id: Option<String>,
362 turn_index: Option<usize>,
363 authored_id: Option<String>,
364 metadata: Value,
365 detail: Option<Box<PerRequestDetail>>,
366}
367
368#[derive(Debug)]
369enum TokenTimeline {
370 Recording(Vec<f64>),
371 Finalized(FinalizedTokenTimeline),
372}
373
374impl Default for TokenTimeline {
375 fn default() -> Self {
376 Self::Recording(Vec::new())
377 }
378}
379
380#[derive(Debug, Clone, Copy)]
381struct FinalizedTokenTimeline {
382 first_ms: f64,
383 second_ms: Option<f64>,
384 last_ms: f64,
385 len: usize,
386}
387
388#[derive(Debug)]
389struct StreamingDistribution {
390 sketch: DDSketch,
391 count: u64,
392 mean: f64,
393 sum_squared_deviations: f64,
394 min: f64,
395 max: f64,
396}
397
398impl Default for StreamingDistribution {
399 fn default() -> Self {
400 let sketch = match DDSketch::with_max_bins(DDSKETCH_RELATIVE_ACCURACY, DDSKETCH_MAX_BINS) {
401 Ok(sketch) => sketch,
402 Err(error) => panic!("invalid built-in DDSketch configuration: {error}"),
403 };
404 Self {
405 sketch,
406 count: 0,
407 mean: 0.0,
408 sum_squared_deviations: 0.0,
409 min: f64::INFINITY,
410 max: f64::NEG_INFINITY,
411 }
412 }
413}
414
415impl StreamingDistribution {
416 fn add(&mut self, value: f64) {
417 if !value.is_finite() {
418 return;
419 }
420
421 self.sketch.add(value);
422 self.count += 1;
423 let delta = value - self.mean;
424 self.mean += delta / self.count as f64;
425 let delta_after_mean_update = value - self.mean;
426 self.sum_squared_deviations += delta * delta_after_mean_update;
427 self.min = self.min.min(value);
428 self.max = self.max.max(value);
429 }
430
431 fn finish(&self) -> TraceDistributionStats {
432 if self.count == 0 {
433 return empty_distribution_stats();
434 }
435
436 TraceDistributionStats {
437 mean_ms: self.mean,
438 min_ms: self.min,
439 max_ms: self.max,
440 median_ms: self.percentile(50.0),
441 p75_ms: self.percentile(75.0),
442 p90_ms: self.percentile(90.0),
443 p95_ms: self.percentile(95.0),
444 p99_ms: self.percentile(99.0),
445 std_ms: (self.sum_squared_deviations / self.count as f64).sqrt(),
446 }
447 }
448
449 fn percentile(&self, percentile: f64) -> f64 {
452 let span = self.count.saturating_sub(1);
453 let rank = (span as f64 * percentile / 100.0).round() as u64;
454 let quantile = if span == 0 || rank >= span {
455 1.0
456 } else {
457 (rank as f64 + 0.5) / span as f64
458 };
459 match self.sketch.quantile(quantile) {
460 Ok(value) => value,
461 Err(error) => panic!("invalid built-in DDSketch quantile {quantile}: {error}"),
462 }
463 }
464}
465
466#[derive(Debug, Default)]
467struct PerRequestDetail {
468 prefill_reused_input_tokens: Option<usize>,
469 prefill_admit_ms: Option<f64>,
470 source_held_ms: Option<f64>,
471 destination_reserved_ms: Option<f64>,
472 destination_activated_ms: Option<f64>,
473 decode_admit_ms: Option<f64>,
474 source_released_ms: Option<f64>,
475 decode_reused_input_tokens: Option<usize>,
476 prefill_route_overlap_tokens: Option<usize>,
477 decode_route_overlap_tokens: Option<usize>,
478 routing_history: Vec<PerRequestRoutingRecord>,
479 admission_history: Vec<PerRequestAdmissionRecord>,
480 pool_admission_counts: [usize; 3],
481 pressure_record_ordinals: Vec<u64>,
482}
483
484#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize)]
485#[serde(rename_all = "snake_case")]
486pub enum ReplayRequestPool {
487 Agg,
488 Prefill,
489 Decode,
490}
491
492impl ReplayRequestPool {
493 const fn index(self) -> usize {
494 match self {
495 Self::Agg => 0,
496 Self::Prefill => 1,
497 Self::Decode => 2,
498 }
499 }
500}
501
502#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize)]
503#[serde(rename_all = "snake_case")]
504pub enum ReplayRoutingOutcome {
505 Immediate,
506 Queued,
507}
508
509#[derive(Debug, Clone, Serialize)]
510pub struct PerRequestRoutingRecord {
511 pub pool: ReplayRequestPool,
512 pub outcome: ReplayRoutingOutcome,
513 pub queue_entered_at_ms: Option<f64>,
514 pub released_at_ms: Option<f64>,
515 pub queue_wait_ms: Option<f64>,
516 pub logical_worker_id: Option<usize>,
517 pub scheduler_id: Option<usize>,
518 pub dp_rank: Option<u32>,
519 pub reported_overlap_tokens: Option<usize>,
520}
521
522#[derive(Debug, Clone, Serialize)]
523pub struct PerRequestAdmissionRecord {
524 pub admission_ordinal: usize,
525 pub pool_admission_ordinal: usize,
526 pub pool: ReplayRequestPool,
527 pub at_ms: f64,
528 pub reused_input_tokens: usize,
529 pub is_readmission: bool,
530}
531
532#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize)]
533#[serde(rename_all = "snake_case")]
534pub enum ReplayTerminalStatus {
535 Completed,
536 Rejected,
537 Canceled,
538 Failed,
539}
540
541#[derive(Debug, Clone, Serialize)]
546pub struct PerRequestRecord {
547 #[serde(skip_serializing_if = "Option::is_none")]
550 pub request_id: Option<String>,
551 pub session_id: Option<String>,
557 pub turn_index: Option<usize>,
559 #[serde(skip_serializing_if = "Value::is_null")]
561 pub metadata: Value,
562 pub uuid: String,
563 pub arrival_time_ms: f64,
564 pub first_admit_ms: Option<f64>,
565 pub terminal_time_ms: f64,
566 pub first_token_ms: Option<f64>,
567 pub last_token_ms: Option<f64>,
568 pub ttft_ms: Option<f64>,
569 pub ttst_ms: Option<f64>,
570 pub e2e_latency_ms: Option<f64>,
571 pub itl_ms: Option<f64>,
574 pub input_length: usize,
575 pub requested_output_length: usize,
577 pub output_length: usize,
579 pub reused_input_tokens: usize,
580 pub prefill_worker_idx: Option<usize>,
581 pub decode_worker_idx: Option<usize>,
582 pub prefill_admit_ms: Option<f64>,
583 pub source_held_ms: Option<f64>,
584 pub destination_reserved_ms: Option<f64>,
585 pub destination_activated_ms: Option<f64>,
586 pub decode_admit_ms: Option<f64>,
587 pub source_released_ms: Option<f64>,
588 pub decode_reused_input_tokens: Option<usize>,
589 pub prefill_route_overlap_tokens: Option<usize>,
590 pub decode_route_overlap_tokens: Option<usize>,
591 pub routing_history: Vec<PerRequestRoutingRecord>,
592 pub admission_history: Vec<PerRequestAdmissionRecord>,
593 pub admission_count: usize,
594 pub readmission_count: usize,
595 pub pressure_record_ordinals: Vec<u64>,
596 pub terminal_status: ReplayTerminalStatus,
597}
598
599#[cfg(test)]
600#[derive(Debug, Clone, PartialEq)]
601pub(crate) struct TraceRequestStatsSnapshot {
602 pub arrival_time_ms: f64,
603 pub first_admit_ms: Option<f64>,
604 pub first_token_ms: Option<f64>,
605 pub last_token_ms: Option<f64>,
606 pub input_length: usize,
607 pub requested_output_length: usize,
608 pub output_length: usize,
609 pub reused_input_tokens: usize,
610 pub first_admission_reused_input_tokens: usize,
611}
612
613#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, serde::Deserialize)]
619pub struct SlaThresholds {
620 pub ttft_ms: Option<f64>,
621 pub itl_ms: Option<f64>,
622 pub e2e_ms: Option<f64>,
623}
624
625impl SlaThresholds {
626 pub fn is_set(&self) -> bool {
627 self.ttft_ms.is_some() || self.itl_ms.is_some() || self.e2e_ms.is_some()
628 }
629
630 pub(crate) fn is_unset(&self) -> bool {
631 !self.is_set()
632 }
633
634 pub(crate) fn validate(&self) -> crate::replay::ReplayResult<()> {
635 for (name, value) in [
636 ("sla.ttft_ms", self.ttft_ms),
637 ("sla.itl_ms", self.itl_ms),
638 ("sla.e2e_ms", self.e2e_ms),
639 ] {
640 if let Some(value) = value
641 && (!value.is_finite() || value < 0.0)
642 {
643 return Err(crate::replay::ReplayError::InvalidSpec(format!(
644 "{name} must be finite and non-negative, got {value}"
645 )));
646 }
647 }
648 Ok(())
649 }
650
651 fn is_good(&self, ttft_ms: f64, e2e_ms: f64, output_length: usize) -> bool {
662 if let Some(bound) = self.e2e_ms
663 && e2e_ms > bound
664 {
665 return false;
666 }
667 if let Some(bound) = self.ttft_ms
668 && ttft_ms > bound
669 {
670 return false;
671 }
672 if let Some(bound) = self.itl_ms
673 && output_length > 1
674 {
675 let avg_itl_ms = (e2e_ms - ttft_ms) / (output_length as f64 - 1.0);
676 if avg_itl_ms > bound {
677 return false;
678 }
679 }
680 true
681 }
682
683 fn is_good_without_tokens(&self, e2e_ms: f64) -> bool {
684 self.ttft_ms.is_none()
685 && self.itl_ms.is_none()
686 && self.e2e_ms.is_some_and(|bound| e2e_ms <= bound)
687 }
688}
689
690#[doc(hidden)]
691#[derive(Debug, Default)]
692pub struct TraceCollector {
693 requests: FxHashMap<Uuid, TraceRequestStats>,
694 itl_distribution: StreamingDistribution,
697 output_token_throughput_per_user: StreamingDistribution,
698 defer_token_timeline_finalization: bool,
701 capture_per_request: bool,
705 sla: SlaThresholds,
708 prefill_worker_seconds: f64,
713 decode_worker_seconds: f64,
714 static_worker_count: Option<(usize, usize)>,
719 prefill_gpus_per_worker: usize,
722 decode_gpus_per_worker: usize,
723 runtime_evidence: crate::replay::OfflineRuntimeEvidence,
724}
725
726impl TraceRequestStats {
727 fn first_token_ms(&self) -> Option<f64> {
728 match &self.token_timeline {
729 TokenTimeline::Recording(times) => times.first().copied(),
730 TokenTimeline::Finalized(summary) => Some(summary.first_ms),
731 }
732 }
733
734 fn last_token_ms(&self) -> Option<f64> {
735 match &self.token_timeline {
736 TokenTimeline::Recording(times) => times.last().copied(),
737 TokenTimeline::Finalized(summary) => Some(summary.last_ms),
738 }
739 }
740
741 fn actual_output_length(&self) -> usize {
742 match &self.token_timeline {
743 TokenTimeline::Recording(times) => times.len(),
744 TokenTimeline::Finalized(summary) => summary.len,
745 }
746 }
747
748 fn mean_tpot_ms(&self) -> Option<f64> {
749 let num_gaps = self.actual_output_length().saturating_sub(1);
750 if num_gaps == 0 {
751 return None;
752 }
753
754 let first_token_ms = self.first_token_ms()?;
755 let last_token_ms = self.last_token_ms()?;
756 Some((last_token_ms - first_token_ms).max(0.0) / num_gaps as f64)
757 }
758
759 fn ttst_ms(&self) -> Option<f64> {
760 let (first_token_ms, second_token_ms) = match &self.token_timeline {
761 TokenTimeline::Recording(times) => {
762 let [first_token_ms, second_token_ms, ..] = times.as_slice() else {
763 return None;
764 };
765 (*first_token_ms, *second_token_ms)
766 }
767 TokenTimeline::Finalized(summary) => (summary.first_ms, summary.second_ms?),
768 };
769 Some((second_token_ms - first_token_ms).max(0.0))
770 }
771
772 fn finalize_token_timeline(
773 &mut self,
774 include_in_distributions: bool,
775 itl_distribution: &mut StreamingDistribution,
776 output_token_throughput_per_user: &mut StreamingDistribution,
777 ) {
778 let TokenTimeline::Recording(times) = &self.token_timeline else {
779 return;
780 };
781
782 if include_in_distributions {
783 for window in times.windows(2) {
784 let itl_ms = (window[1] - window[0]).max(0.0);
785 itl_distribution.add(itl_ms);
786 if itl_ms > 0.0 {
787 output_token_throughput_per_user.add(1000.0 / itl_ms);
788 }
789 }
790 }
791
792 let Some(first_ms) = times.first().copied() else {
793 self.token_timeline = TokenTimeline::default();
794 return;
795 };
796 let summary = FinalizedTokenTimeline {
797 first_ms,
798 second_ms: times.get(1).copied(),
799 last_ms: times.last().copied().unwrap_or(first_ms),
800 len: times.len(),
801 };
802 self.token_timeline = TokenTimeline::Finalized(summary);
803 }
804}
805
806impl TraceCollector {
807 pub fn set_defer_token_timeline_finalization(&mut self, value: bool) {
809 self.defer_token_timeline_finalization = value;
810 }
811
812 pub fn set_capture_per_request(&mut self, value: bool) {
815 self.capture_per_request = value;
816 }
817
818 pub fn set_sla_thresholds(&mut self, sla: SlaThresholds) {
821 self.sla = sla;
822 }
823
824 pub(crate) fn add_worker_seconds(&mut self, prefill: f64, decode: f64) {
831 self.prefill_worker_seconds += prefill;
832 self.decode_worker_seconds += decode;
833 }
834
835 pub fn set_static_worker_count(&mut self, prefill: usize, decode: usize) {
840 self.static_worker_count = Some((prefill, decode));
841 }
842
843 pub(crate) fn clear_static_worker_count(&mut self) {
844 self.static_worker_count = None;
845 }
846
847 pub fn set_gpus_per_worker(&mut self, prefill: usize, decode: usize) {
850 self.prefill_gpus_per_worker = prefill;
851 self.decode_gpus_per_worker = decode;
852 }
853
854 pub(crate) fn set_runtime_evidence(&mut self, evidence: crate::replay::OfflineRuntimeEvidence) {
855 self.runtime_evidence = evidence;
856 }
857
858 pub fn on_arrival(
859 &mut self,
860 uuid: Uuid,
861 arrival_time_ms: f64,
862 input_length: usize,
863 requested_output_length: usize,
864 ) {
865 self.requests.insert(
866 uuid,
867 TraceRequestStats {
868 arrival_time_ms,
869 first_admit_ms: None,
870 terminal_time_ms: None,
871 terminal_status: None,
872 token_timeline: TokenTimeline::default(),
873 input_length,
874 requested_output_length,
875 reused_input_tokens: 0,
876 prefill_worker_idx: None,
877 decode_worker_idx: None,
878 session_id: None,
879 turn_index: None,
880 authored_id: None,
881 metadata: Value::Null,
882 first_admission_reused_input_tokens: 0,
883 detail: self
884 .capture_per_request
885 .then(|| Box::new(PerRequestDetail::default())),
886 },
887 );
888 }
889
890 pub fn on_session_metadata(&mut self, uuid: Uuid, session_id: String, turn_index: usize) {
895 if !self.capture_per_request {
896 return;
897 }
898 if let Some(stats) = self.requests.get_mut(&uuid)
899 && stats.session_id.is_none()
900 {
901 stats.session_id = Some(session_id);
902 stats.turn_index = Some(turn_index);
903 }
904 }
905
906 pub fn on_request_context(
909 &mut self,
910 uuid: Uuid,
911 context: &crate::replay::ReplayRequestContext,
912 ) {
913 if !self.capture_per_request {
914 return;
915 }
916 if let Some(stats) = self.requests.get_mut(&uuid) {
917 stats.authored_id = Some(context.authored_id.clone());
918 stats.session_id = context.session_id.clone().or(stats.session_id.take());
919 stats.turn_index = context.turn_index.or(stats.turn_index);
920 stats.metadata = context.metadata.clone();
921 }
922 }
923
924 pub(crate) fn on_prefill_assigned(&mut self, uuid: Uuid, worker_idx: usize) {
929 if let Some(stats) = self.requests.get_mut(&uuid)
930 && stats.prefill_worker_idx.is_none()
931 {
932 stats.prefill_worker_idx = Some(worker_idx);
933 }
934 }
935
936 pub fn on_decode_assigned(&mut self, uuid: Uuid, worker_idx: usize) {
940 if let Some(stats) = self.requests.get_mut(&uuid)
941 && stats.decode_worker_idx.is_none()
942 {
943 stats.decode_worker_idx = Some(worker_idx);
944 }
945 }
946
947 pub fn on_admit(&mut self, uuid: Uuid, admit_time_ms: f64, reused_input_tokens: usize) {
948 if let Some(stats) = self.requests.get_mut(&uuid) {
949 if stats.first_admit_ms.is_none() {
950 stats.first_admission_reused_input_tokens = reused_input_tokens;
951 stats.first_admit_ms = Some(admit_time_ms);
952 }
953 stats.reused_input_tokens = stats.reused_input_tokens.max(reused_input_tokens);
954 }
955 }
956
957 pub(crate) fn on_prefill_admit(
958 &mut self,
959 uuid: Uuid,
960 admit_time_ms: f64,
961 reused_input_tokens: usize,
962 ) {
963 self.on_admit(uuid, admit_time_ms, reused_input_tokens);
964 self.on_pool_admission(
965 uuid,
966 ReplayRequestPool::Prefill,
967 admit_time_ms,
968 reused_input_tokens,
969 );
970 if let Some(detail) = self.detail_mut(uuid) {
971 detail.prefill_admit_ms.get_or_insert(admit_time_ms);
972 detail.prefill_reused_input_tokens = Some(
973 detail
974 .prefill_reused_input_tokens
975 .unwrap_or_default()
976 .max(reused_input_tokens),
977 );
978 }
979 }
980
981 pub(crate) fn on_decode_admit(
982 &mut self,
983 uuid: Uuid,
984 admit_time_ms: f64,
985 reused_input_tokens: usize,
986 ) {
987 self.on_admit(uuid, admit_time_ms, reused_input_tokens);
988 self.on_pool_admission(
989 uuid,
990 ReplayRequestPool::Decode,
991 admit_time_ms,
992 reused_input_tokens,
993 );
994 if let Some(detail) = self.detail_mut(uuid) {
995 detail.decode_admit_ms.get_or_insert(admit_time_ms);
996 detail.decode_reused_input_tokens = Some(
997 detail
998 .decode_reused_input_tokens
999 .unwrap_or_default()
1000 .max(reused_input_tokens),
1001 );
1002 }
1003 }
1004
1005 pub(crate) fn on_source_held(&mut self, uuid: Uuid, at_ms: f64) {
1006 if let Some(detail) = self.detail_mut(uuid) {
1007 detail.source_held_ms.get_or_insert(at_ms);
1008 }
1009 }
1010
1011 pub(crate) fn on_destination_reserved(&mut self, uuid: Uuid, at_ms: f64) {
1012 if let Some(detail) = self.detail_mut(uuid) {
1013 detail.destination_reserved_ms.get_or_insert(at_ms);
1014 }
1015 }
1016
1017 pub(crate) fn on_destination_activated(&mut self, uuid: Uuid, at_ms: f64) {
1018 if let Some(detail) = self.detail_mut(uuid) {
1019 detail.destination_activated_ms.get_or_insert(at_ms);
1020 }
1021 }
1022
1023 pub(crate) fn on_source_released(&mut self, uuid: Uuid, at_ms: f64) {
1024 if let Some(detail) = self.detail_mut(uuid) {
1025 detail.source_released_ms.get_or_insert(at_ms);
1026 }
1027 }
1028
1029 pub(crate) fn on_prefill_route_overlap(&mut self, uuid: Uuid, tokens: usize) {
1030 if let Some(detail) = self.detail_mut(uuid) {
1031 detail.prefill_route_overlap_tokens.get_or_insert(tokens);
1032 }
1033 }
1034
1035 pub(crate) fn on_decode_route_overlap(&mut self, uuid: Uuid, tokens: usize) {
1036 if let Some(detail) = self.detail_mut(uuid) {
1037 detail.decode_route_overlap_tokens.get_or_insert(tokens);
1038 }
1039 }
1040
1041 #[allow(clippy::too_many_arguments)]
1042 pub(crate) fn on_route_immediate(
1043 &mut self,
1044 uuid: Uuid,
1045 pool: ReplayRequestPool,
1046 logical_worker_id: usize,
1047 scheduler_id: usize,
1048 dp_rank: u32,
1049 reported_overlap_tokens: usize,
1050 ) {
1051 let Some(detail) = self.detail_mut(uuid) else {
1052 return;
1053 };
1054 detail.routing_history.push(PerRequestRoutingRecord {
1055 pool,
1056 outcome: ReplayRoutingOutcome::Immediate,
1057 queue_entered_at_ms: None,
1058 released_at_ms: None,
1059 queue_wait_ms: Some(0.0),
1060 logical_worker_id: Some(logical_worker_id),
1061 scheduler_id: Some(scheduler_id),
1062 dp_rank: Some(dp_rank),
1063 reported_overlap_tokens: Some(reported_overlap_tokens),
1064 });
1065 }
1066
1067 pub(crate) fn on_route_queued(&mut self, uuid: Uuid, pool: ReplayRequestPool, at_ms: f64) {
1068 let Some(detail) = self.detail_mut(uuid) else {
1069 return;
1070 };
1071 detail.routing_history.push(PerRequestRoutingRecord {
1072 pool,
1073 outcome: ReplayRoutingOutcome::Queued,
1074 queue_entered_at_ms: Some(at_ms),
1075 released_at_ms: None,
1076 queue_wait_ms: None,
1077 logical_worker_id: None,
1078 scheduler_id: None,
1079 dp_rank: None,
1080 reported_overlap_tokens: None,
1081 });
1082 }
1083
1084 #[allow(clippy::too_many_arguments)]
1085 pub(crate) fn on_route_released(
1086 &mut self,
1087 uuid: Uuid,
1088 pool: ReplayRequestPool,
1089 at_ms: f64,
1090 logical_worker_id: usize,
1091 scheduler_id: usize,
1092 dp_rank: u32,
1093 reported_overlap_tokens: usize,
1094 ) {
1095 let Some(detail) = self.detail_mut(uuid) else {
1096 return;
1097 };
1098 let Some(route) = detail.routing_history.iter_mut().rev().find(|route| {
1099 route.pool == pool
1100 && route.outcome == ReplayRoutingOutcome::Queued
1101 && route.released_at_ms.is_none()
1102 }) else {
1103 return;
1104 };
1105 let entered_at_ms = route.queue_entered_at_ms.unwrap_or(at_ms);
1106 route.released_at_ms = Some(at_ms);
1107 route.queue_wait_ms = Some((at_ms - entered_at_ms).max(0.0));
1108 route.logical_worker_id = Some(logical_worker_id);
1109 route.scheduler_id = Some(scheduler_id);
1110 route.dp_rank = Some(dp_rank);
1111 route.reported_overlap_tokens = Some(reported_overlap_tokens);
1112 }
1113
1114 pub(crate) fn on_pool_admission(
1115 &mut self,
1116 uuid: Uuid,
1117 pool: ReplayRequestPool,
1118 at_ms: f64,
1119 reused_input_tokens: usize,
1120 ) {
1121 let Some(detail) = self.detail_mut(uuid) else {
1122 return;
1123 };
1124 let admission_ordinal = detail.admission_history.len();
1125 let pool_count = &mut detail.pool_admission_counts[pool.index()];
1126 let pool_admission_ordinal = *pool_count;
1127 *pool_count += 1;
1128 detail.admission_history.push(PerRequestAdmissionRecord {
1129 admission_ordinal,
1130 pool_admission_ordinal,
1131 pool,
1132 at_ms,
1133 reused_input_tokens,
1134 is_readmission: pool_admission_ordinal > 0,
1135 });
1136 }
1137
1138 pub(crate) fn on_pressure_reference(&mut self, uuid: Uuid, pressure_ordinal: u64) {
1139 if let Some(detail) = self.detail_mut(uuid) {
1140 detail.pressure_record_ordinals.push(pressure_ordinal);
1141 }
1142 }
1143
1144 pub fn on_terminal(&mut self, uuid: Uuid, terminal_time_ms: f64, status: ReplayTerminalStatus) {
1145 let Self {
1146 requests,
1147 itl_distribution,
1148 output_token_throughput_per_user,
1149 defer_token_timeline_finalization,
1150 ..
1151 } = self;
1152 if let Some(stats) = requests.get_mut(&uuid)
1153 && stats.terminal_status.is_none()
1154 {
1155 stats.terminal_time_ms = Some(terminal_time_ms);
1156 stats.terminal_status = Some(status);
1157 if !*defer_token_timeline_finalization {
1158 stats.finalize_token_timeline(
1159 status == ReplayTerminalStatus::Completed && stats.first_admit_ms.is_some(),
1160 itl_distribution,
1161 output_token_throughput_per_user,
1162 );
1163 }
1164 }
1165 }
1166
1167 fn detail_mut(&mut self, uuid: Uuid) -> Option<&mut PerRequestDetail> {
1168 if !self.capture_per_request {
1169 return None;
1170 }
1171 self.requests.get_mut(&uuid)?.detail.as_deref_mut()
1172 }
1173
1174 pub fn on_token(&mut self, uuid: Uuid, token_time_ms: f64) {
1175 if let Some(stats) = self.requests.get_mut(&uuid)
1176 && let TokenTimeline::Recording(times) = &mut stats.token_timeline
1177 {
1178 times.push(token_time_ms);
1179 }
1180 }
1181
1182 pub(crate) fn request_latencies(&self, uuid: Uuid) -> Option<(f64, f64)> {
1184 let stats = self.requests.get(&uuid)?;
1185 let first_token_ms = stats.first_token_ms()?;
1186 let ttft_ms = (first_token_ms - stats.arrival_time_ms).max(0.0);
1187 let mean_itl_ms = stats.mean_tpot_ms().unwrap_or(0.0);
1188 Some((ttft_ms, mean_itl_ms))
1189 }
1190
1191 pub(crate) fn actual_output_length(&self, uuid: Uuid) -> Option<usize> {
1192 self.requests
1193 .get(&uuid)
1194 .map(TraceRequestStats::actual_output_length)
1195 }
1196
1197 pub fn finish(mut self) -> ReplayReport {
1198 let Self {
1199 requests,
1200 itl_distribution,
1201 output_token_throughput_per_user,
1202 ..
1203 } = &mut self;
1204 for stats in requests.values_mut() {
1205 stats.finalize_token_timeline(
1206 stats.terminal_status == Some(ReplayTerminalStatus::Completed)
1207 && stats.first_admit_ms.is_some(),
1208 itl_distribution,
1209 output_token_throughput_per_user,
1210 );
1211 }
1212
1213 let per_request = if self.capture_per_request {
1219 self.per_request_records()
1220 } else {
1221 Vec::new()
1222 };
1223 let sla = self.sla;
1224 let static_worker_count = self.static_worker_count;
1225 let accumulated_prefill_worker_seconds = self.prefill_worker_seconds;
1226 let accumulated_decode_worker_seconds = self.decode_worker_seconds;
1227 let prefill_gpus_per_worker = self.prefill_gpus_per_worker;
1228 let decode_gpus_per_worker = self.decode_gpus_per_worker;
1229 let runtime_evidence = self.runtime_evidence;
1230 let itl_distribution = self.itl_distribution.finish();
1231 let output_token_throughput_per_user = self.output_token_throughput_per_user.finish();
1232 let requests = self.requests;
1233 let request_count = requests.len();
1234 let mut ttfts = Vec::with_capacity(request_count);
1235 let mut ttsts = Vec::with_capacity(request_count);
1236 let mut tpots = Vec::with_capacity(request_count);
1237 let mut e2e_latencies = Vec::with_capacity(request_count);
1238 let mut duration_ms = 0.0_f64;
1239 let mut total_input_tokens = 0usize;
1240 let mut total_output_tokens = 0usize;
1241 let mut completed_requests = 0usize;
1242 let mut total_reused_tokens = 0usize;
1243 let mut total_first_admission_reused_tokens = 0usize;
1244 let mut goodput_requests = 0usize;
1246 let mut goodput_output_tokens = 0usize;
1247
1248 for stats in requests.values() {
1249 if stats.first_admit_ms.is_none() {
1250 continue;
1251 }
1252 if stats.terminal_status != Some(ReplayTerminalStatus::Completed) {
1253 continue;
1254 }
1255 let Some(terminal_time_ms) = stats.terminal_time_ms else {
1256 continue;
1257 };
1258
1259 completed_requests += 1;
1260 total_input_tokens += stats.input_length;
1261 let output_length = stats.actual_output_length();
1262 total_output_tokens += output_length;
1263 total_reused_tokens += stats.reused_input_tokens;
1264 total_first_admission_reused_tokens += stats.first_admission_reused_input_tokens;
1265 duration_ms = duration_ms.max(terminal_time_ms);
1266
1267 let (Some(first_token_ms), Some(last_token_ms)) =
1268 (stats.first_token_ms(), stats.last_token_ms())
1269 else {
1270 let e2e_ms = (terminal_time_ms - stats.arrival_time_ms).max(0.0);
1271 if sla.is_set() && sla.is_good_without_tokens(e2e_ms) {
1272 goodput_requests += 1;
1273 }
1274 continue;
1275 };
1276
1277 let ttft_ms = (first_token_ms - stats.arrival_time_ms).max(0.0);
1278 let e2e_ms = (last_token_ms - stats.arrival_time_ms).max(0.0);
1279 ttfts.push(ttft_ms);
1280 e2e_latencies.push(e2e_ms);
1281
1282 if sla.is_set() && sla.is_good(ttft_ms, e2e_ms, output_length) {
1284 goodput_requests += 1;
1285 goodput_output_tokens += output_length;
1286 }
1287
1288 if let Some(ttst_ms) = stats.ttst_ms() {
1289 ttsts.push(ttst_ms);
1290 }
1291
1292 if let Some(tpot_ms) = stats.mean_tpot_ms() {
1293 tpots.push(tpot_ms);
1294 }
1295 }
1296
1297 let duration_s = (duration_ms / 1000.0).max(1e-9);
1298 let (prefill_worker_seconds, decode_worker_seconds) = match static_worker_count {
1301 Some((prefill, decode)) => (prefill as f64 * duration_s, decode as f64 * duration_s),
1302 None => (
1303 accumulated_prefill_worker_seconds,
1304 accumulated_decode_worker_seconds,
1305 ),
1306 };
1307 let gpu_hours = (prefill_worker_seconds * prefill_gpus_per_worker as f64
1310 + decode_worker_seconds * decode_gpus_per_worker as f64)
1311 / 3600.0;
1312 let goodput = sla.is_set().then(|| TraceGoodputStats {
1314 completed_requests: goodput_requests,
1315 request_throughput_rps: goodput_requests as f64 / duration_s,
1316 output_throughput_tok_s: goodput_output_tokens as f64 / duration_s,
1317 });
1318 ReplayReport {
1319 request_counts: TraceRequestCounts {
1320 num_requests: request_count,
1321 completed_requests,
1322 total_input_tokens,
1323 total_output_tokens,
1324 },
1325 throughput: TraceThroughputStats {
1326 duration_ms,
1327 wall_time_ms: 0.0,
1328 request_throughput_rps: completed_requests as f64 / duration_s,
1329 input_throughput_tok_s: total_input_tokens as f64 / duration_s,
1330 output_throughput_tok_s: total_output_tokens as f64 / duration_s,
1331 total_throughput_tok_s: (total_input_tokens + total_output_tokens) as f64
1332 / duration_s,
1333 prefill_worker_seconds,
1334 decode_worker_seconds,
1335 prefill_gpus_per_worker,
1336 decode_gpus_per_worker,
1337 gpu_hours,
1338 },
1339 prefix_cache_reused_ratio: if total_input_tokens == 0 {
1340 0.0
1341 } else {
1342 total_reused_tokens as f64 / total_input_tokens as f64
1343 },
1344 first_admission_prefix_cache_reused_ratio: if total_input_tokens == 0 {
1345 0.0
1346 } else {
1347 total_first_admission_reused_tokens as f64 / total_input_tokens as f64
1348 },
1349 latency: TraceLatencyStats {
1350 ttft: build_distribution_stats(ttfts),
1351 ttst: build_distribution_stats(ttsts),
1352 tpot: build_distribution_stats(tpots),
1353 itl: TraceInterTokenLatencyStats {
1354 max_ms: itl_distribution.max_ms,
1355 distribution: itl_distribution,
1356 },
1357 e2e: build_distribution_stats(e2e_latencies),
1358 output_token_throughput_per_user,
1359 },
1360 goodput,
1361 per_request,
1362 runtime_evidence,
1363 }
1364 }
1365
1366 pub fn per_request_records(&self) -> Vec<PerRequestRecord> {
1373 let mut records = Vec::with_capacity(self.requests.len());
1374 for (uuid, stats) in &self.requests {
1375 let Some(detail) = stats.detail.as_deref() else {
1376 continue;
1377 };
1378 let Some(terminal_status) = stats.terminal_status else {
1379 continue;
1380 };
1381 let Some(terminal_time_ms) = stats.terminal_time_ms else {
1382 continue;
1383 };
1384 let first_token_ms = stats.first_token_ms();
1385 let last_token_ms = stats.last_token_ms();
1386 records.push(PerRequestRecord {
1387 request_id: stats.authored_id.clone(),
1388 session_id: stats.session_id.clone(),
1389 turn_index: stats.turn_index,
1390 metadata: stats.metadata.clone(),
1391 uuid: uuid.to_string(),
1392 arrival_time_ms: stats.arrival_time_ms,
1393 first_admit_ms: stats.first_admit_ms,
1394 terminal_time_ms,
1395 first_token_ms,
1396 last_token_ms,
1397 ttft_ms: first_token_ms.map(|time| (time - stats.arrival_time_ms).max(0.0)),
1398 ttst_ms: stats.ttst_ms(),
1399 e2e_latency_ms: last_token_ms.map(|time| (time - stats.arrival_time_ms).max(0.0)),
1400 itl_ms: stats.mean_tpot_ms(),
1401 input_length: stats.input_length,
1402 requested_output_length: stats.requested_output_length,
1403 output_length: stats.actual_output_length(),
1404 reused_input_tokens: detail
1405 .prefill_reused_input_tokens
1406 .unwrap_or(stats.reused_input_tokens),
1407 prefill_worker_idx: stats.prefill_worker_idx,
1408 decode_worker_idx: stats.decode_worker_idx,
1409 prefill_admit_ms: detail.prefill_admit_ms,
1410 source_held_ms: detail.source_held_ms,
1411 destination_reserved_ms: detail.destination_reserved_ms,
1412 destination_activated_ms: detail.destination_activated_ms,
1413 decode_admit_ms: detail.decode_admit_ms,
1414 source_released_ms: detail.source_released_ms,
1415 decode_reused_input_tokens: detail.decode_reused_input_tokens,
1416 prefill_route_overlap_tokens: detail.prefill_route_overlap_tokens,
1417 decode_route_overlap_tokens: detail.decode_route_overlap_tokens,
1418 routing_history: detail.routing_history.clone(),
1419 admission_count: detail.admission_history.len(),
1420 readmission_count: detail
1421 .pool_admission_counts
1422 .iter()
1423 .map(|count| count.saturating_sub(1))
1424 .sum(),
1425 admission_history: detail.admission_history.clone(),
1426 pressure_record_ordinals: detail.pressure_record_ordinals.clone(),
1427 terminal_status,
1428 });
1429 }
1430 records.sort_by(|a, b| {
1434 a.arrival_time_ms
1435 .total_cmp(&b.arrival_time_ms)
1436 .then_with(|| a.uuid.cmp(&b.uuid))
1437 });
1438 records
1439 }
1440
1441 #[cfg(test)]
1442 pub(crate) fn snapshot(&self, uuid: Uuid) -> Option<TraceRequestStatsSnapshot> {
1443 self.requests
1444 .get(&uuid)
1445 .map(|stats| TraceRequestStatsSnapshot {
1446 arrival_time_ms: stats.arrival_time_ms,
1447 first_admit_ms: stats.first_admit_ms,
1448 first_token_ms: stats.first_token_ms(),
1449 last_token_ms: stats.last_token_ms(),
1450 input_length: stats.input_length,
1451 requested_output_length: stats.requested_output_length,
1452 output_length: stats.actual_output_length(),
1453 reused_input_tokens: stats.reused_input_tokens,
1454 first_admission_reused_input_tokens: stats.first_admission_reused_input_tokens,
1455 })
1456 }
1457
1458 #[cfg(test)]
1459 pub(crate) fn snapshots(&self) -> Vec<TraceRequestStatsSnapshot> {
1460 self.requests
1461 .values()
1462 .map(|stats| TraceRequestStatsSnapshot {
1463 arrival_time_ms: stats.arrival_time_ms,
1464 first_admit_ms: stats.first_admit_ms,
1465 first_token_ms: stats.first_token_ms(),
1466 last_token_ms: stats.last_token_ms(),
1467 input_length: stats.input_length,
1468 requested_output_length: stats.requested_output_length,
1469 output_length: stats.actual_output_length(),
1470 reused_input_tokens: stats.reused_input_tokens,
1471 first_admission_reused_input_tokens: stats.first_admission_reused_input_tokens,
1472 })
1473 .collect()
1474 }
1475
1476 #[cfg(test)]
1477 fn retained_token_timestamps(&self) -> usize {
1478 self.requests
1479 .values()
1480 .map(|stats| match &stats.token_timeline {
1481 TokenTimeline::Recording(times) => times.len(),
1482 TokenTimeline::Finalized(_) => 0,
1483 })
1484 .sum()
1485 }
1486}
1487
1488fn mean(values: &[f64]) -> f64 {
1489 if values.is_empty() {
1490 0.0
1491 } else {
1492 values.iter().sum::<f64>() / values.len() as f64
1493 }
1494}
1495
1496fn build_distribution_stats(mut values: Vec<f64>) -> TraceDistributionStats {
1497 if values.is_empty() {
1498 return empty_distribution_stats();
1499 }
1500
1501 let min_ms = values
1502 .iter()
1503 .copied()
1504 .min_by(|left, right| left.total_cmp(right))
1505 .expect("non-empty values must have a minimum");
1506 let max_ms = values
1507 .iter()
1508 .copied()
1509 .max_by(|left, right| left.total_cmp(right))
1510 .expect("non-empty values must have a maximum");
1511
1512 TraceDistributionStats {
1513 mean_ms: mean(&values),
1514 min_ms,
1515 max_ms,
1516 median_ms: percentile_in_place(&mut values, 50.0),
1517 p75_ms: percentile_in_place(&mut values, 75.0),
1518 p90_ms: percentile_in_place(&mut values, 90.0),
1519 p95_ms: percentile_in_place(&mut values, 95.0),
1520 p99_ms: percentile_in_place(&mut values, 99.0),
1521 std_ms: std_dev(&values),
1522 }
1523}
1524
1525fn empty_distribution_stats() -> TraceDistributionStats {
1526 TraceDistributionStats {
1527 mean_ms: 0.0,
1528 min_ms: 0.0,
1529 max_ms: 0.0,
1530 median_ms: 0.0,
1531 p75_ms: 0.0,
1532 p90_ms: 0.0,
1533 p95_ms: 0.0,
1534 p99_ms: 0.0,
1535 std_ms: 0.0,
1536 }
1537}
1538
1539fn percentile_in_place(values: &mut [f64], percentile: f64) -> f64 {
1540 let rank = percentile_rank(values.len(), percentile);
1541 let (_, selected, _) = values.select_nth_unstable_by(rank, |left, right| left.total_cmp(right));
1542 *selected
1543}
1544
1545fn percentile_rank(len: usize, percentile: f64) -> usize {
1546 let rank = ((len - 1) as f64 * percentile / 100.0).round() as usize;
1547 rank.min(len - 1)
1548}
1549
1550fn std_dev(values: &[f64]) -> f64 {
1551 if values.is_empty() {
1552 return 0.0;
1553 }
1554
1555 let mean = mean(values);
1556 let variance = values
1557 .iter()
1558 .map(|value| {
1559 let centered = value - mean;
1560 centered * centered
1561 })
1562 .sum::<f64>()
1563 / values.len() as f64;
1564 variance.sqrt()
1565}
1566
1567#[cfg(test)]
1568mod tests {
1569 use super::*;
1570
1571 fn build_distribution_stats_sorted(values: &[f64]) -> TraceDistributionStats {
1572 if values.is_empty() {
1573 return TraceDistributionStats {
1574 mean_ms: 0.0,
1575 min_ms: 0.0,
1576 max_ms: 0.0,
1577 median_ms: 0.0,
1578 p75_ms: 0.0,
1579 p90_ms: 0.0,
1580 p95_ms: 0.0,
1581 p99_ms: 0.0,
1582 std_ms: 0.0,
1583 };
1584 }
1585
1586 let mut sorted = values.to_vec();
1587 sorted.sort_by(|left, right| left.total_cmp(right));
1588 TraceDistributionStats {
1589 mean_ms: mean(values),
1590 min_ms: sorted[0],
1591 max_ms: *sorted.last().expect("sorted values must be non-empty"),
1592 median_ms: sorted[percentile_rank(sorted.len(), 50.0)],
1593 p75_ms: sorted[percentile_rank(sorted.len(), 75.0)],
1594 p90_ms: sorted[percentile_rank(sorted.len(), 90.0)],
1595 p95_ms: sorted[percentile_rank(sorted.len(), 95.0)],
1596 p99_ms: sorted[percentile_rank(sorted.len(), 99.0)],
1597 std_ms: std_dev(values),
1598 }
1599 }
1600
1601 #[test]
1602 fn build_distribution_stats_matches_sorted_baseline() {
1603 let values = vec![
1604 0.0, 1.0, 1.0, 2.5, 4.0, 4.0, 7.25, 9.5, 15.0, 22.0, 22.0, 100.0,
1605 ];
1606
1607 let expected = build_distribution_stats_sorted(&values);
1608 let actual = build_distribution_stats(values);
1609
1610 assert_eq!(actual.mean_ms, expected.mean_ms);
1611 assert_eq!(actual.min_ms, expected.min_ms);
1612 assert_eq!(actual.max_ms, expected.max_ms);
1613 assert_eq!(actual.median_ms, expected.median_ms);
1614 assert_eq!(actual.p75_ms, expected.p75_ms);
1615 assert_eq!(actual.p90_ms, expected.p90_ms);
1616 assert_eq!(actual.p95_ms, expected.p95_ms);
1617 assert_eq!(actual.p99_ms, expected.p99_ms);
1618 assert_eq!(actual.std_ms, expected.std_ms);
1619 }
1620
1621 #[test]
1622 fn built_in_ddsketch_configuration_and_quantiles_are_valid() {
1623 let mut distribution = StreamingDistribution::default();
1624 assert!((distribution.sketch.alpha() - DDSKETCH_RELATIVE_ACCURACY).abs() < f64::EPSILON);
1625 for percentile in [50.0, 75.0, 90.0, 95.0, 99.0] {
1626 assert_eq!(distribution.percentile(percentile), 0.0);
1627 }
1628
1629 for value in [1e-9, 1e18] {
1632 distribution.add(value);
1633 }
1634 for (quantile, expected) in [(0.0, 1e-9), (1.0, 1e18)] {
1635 let actual = distribution.sketch.quantile(quantile).unwrap();
1636 assert!((actual - expected).abs() <= expected * DDSKETCH_RELATIVE_ACCURACY);
1637 }
1638 }
1639
1640 #[test]
1641 fn streaming_distribution_preserves_all_zero_samples() {
1642 let mut distribution = StreamingDistribution::default();
1643 for _ in 0..128 {
1644 distribution.add(0.0);
1645 }
1646
1647 assert_eq!(distribution.sketch.get_zero_count(), 128);
1648 let stats = distribution.finish();
1649 for value in [
1650 stats.mean_ms,
1651 stats.min_ms,
1652 stats.max_ms,
1653 stats.median_ms,
1654 stats.p75_ms,
1655 stats.p90_ms,
1656 stats.p95_ms,
1657 stats.p99_ms,
1658 stats.std_ms,
1659 ] {
1660 assert_eq!(value, 0.0);
1661 }
1662 }
1663
1664 #[test]
1665 fn streaming_percentiles_select_the_historical_rounded_rank() {
1666 let percentiles = [
1667 0.0, 1.0, 10.0, 25.0, 49.0, 50.0, 51.0, 75.0, 90.0, 95.0, 99.0, 100.0,
1668 ];
1669 for len in [2, 3, 4, 5, 10, 11, 100, 101, 256, 257] {
1670 let values = (0..len)
1671 .map(|index| 1_000.0 + index as f64 * 10.0)
1672 .collect::<Vec<_>>();
1673 let mut distribution = StreamingDistribution::default();
1674 for &value in &values {
1675 distribution.add(value);
1676 }
1677
1678 for percentile in percentiles {
1679 let expected = values[percentile_rank(values.len(), percentile)];
1680 let actual = distribution.percentile(percentile);
1681 assert!(
1682 (actual - expected).abs() <= expected * DDSKETCH_RELATIVE_ACCURACY,
1683 "len={len} percentile={percentile}: expected rank value {expected}, got {actual}"
1684 );
1685 }
1686 }
1687 }
1688
1689 #[test]
1690 fn completed_zero_output_request_counts_without_latency_samples() {
1691 let mut collector = TraceCollector::default();
1692 collector.set_static_worker_count(0, 1);
1693 collector.set_gpus_per_worker(0, 4);
1694 let uuid = Uuid::from_u128(99);
1695 collector.on_arrival(uuid, 0.0, 32, 0);
1696 collector.on_admit(uuid, 5.0, 8);
1697 collector.on_terminal(uuid, 25.0, ReplayTerminalStatus::Completed);
1698
1699 let report = collector.finish();
1700
1701 assert_eq!(report.request_counts.completed_requests, 1);
1702 assert_eq!(report.request_counts.total_input_tokens, 32);
1703 assert_eq!(report.request_counts.total_output_tokens, 0);
1704 assert_eq!(report.throughput.duration_ms, 25.0);
1705 assert_eq!(report.throughput.decode_worker_seconds, 0.025);
1706 assert!((report.throughput.gpu_hours - 0.1 / 3600.0).abs() < 1e-12);
1707 assert_eq!(report.latency.ttft.mean_ms, 0.0);
1708 assert_eq!(report.latency.e2e.mean_ms, 0.0);
1709 }
1710
1711 #[test]
1712 fn token_before_simulation_cap_does_not_count_as_completion() {
1713 let mut collector = TraceCollector::default();
1714 let uuid = Uuid::from_u128(100);
1715 collector.on_arrival(uuid, 0.0, 32, 4);
1716 collector.on_admit(uuid, 5.0, 0);
1717 collector.on_token(uuid, 25.0);
1718
1719 let report = collector.finish();
1720
1721 assert_eq!(report.request_counts.completed_requests, 0);
1722 assert_eq!(report.request_counts.total_input_tokens, 0);
1723 assert_eq!(report.request_counts.total_output_tokens, 0);
1724 assert_eq!(report.throughput.duration_ms, 0.0);
1725 }
1726
1727 #[test]
1728 fn zero_output_goodput_requires_e2e_only_sla() {
1729 let collect = |sla| {
1730 let mut collector = TraceCollector::default();
1731 collector.set_sla_thresholds(sla);
1732 let uuid = Uuid::from_u128(101);
1733 collector.on_arrival(uuid, 0.0, 32, 0);
1734 collector.on_admit(uuid, 5.0, 0);
1735 collector.on_terminal(uuid, 100.0, ReplayTerminalStatus::Completed);
1736 collector.finish().goodput.unwrap().completed_requests
1737 };
1738
1739 assert_eq!(
1740 collect(SlaThresholds {
1741 e2e_ms: Some(100.0),
1742 ..Default::default()
1743 }),
1744 1
1745 );
1746 assert_eq!(
1747 collect(SlaThresholds {
1748 ttft_ms: Some(1_000.0),
1749 ..Default::default()
1750 }),
1751 0
1752 );
1753 }
1754
1755 #[test]
1759 fn per_request_disagg_record_populates_all_fields() {
1760 let mut collector = TraceCollector::default();
1761 collector.set_capture_per_request(true);
1762 let uuid = Uuid::from_u128(1);
1763 collector.on_arrival(uuid, 0.0, 100, 4);
1764 collector.on_prefill_route_overlap(uuid, 64);
1765 collector.on_prefill_admit(uuid, 5.0, 30);
1766 collector.on_source_held(uuid, 10.0);
1767 collector.on_destination_reserved(uuid, 12.0);
1768 collector.on_destination_activated(uuid, 20.0);
1769 collector.on_source_released(uuid, 21.0);
1770 collector.on_decode_route_overlap(uuid, 32);
1771 collector.on_decode_admit(uuid, 25.0, 40);
1772 collector.on_prefill_assigned(uuid, 2);
1773 collector.on_decode_assigned(uuid, 7);
1774 collector.on_token(uuid, 50.0);
1775 collector.on_token(uuid, 60.0);
1776 collector.on_token(uuid, 75.0);
1777 collector.on_token(uuid, 95.0);
1778 collector.on_terminal(uuid, 95.0, ReplayTerminalStatus::Completed);
1779
1780 let report = collector.finish();
1781 assert_eq!(report.per_request.len(), 1);
1782 let rec = &report.per_request[0];
1783 assert_eq!(rec.uuid, uuid.to_string());
1784 assert_eq!(rec.arrival_time_ms, 0.0);
1785 assert_eq!(rec.first_admit_ms, Some(5.0));
1786 assert_eq!(rec.terminal_time_ms, 95.0);
1787 assert_eq!(rec.first_token_ms, Some(50.0));
1788 assert_eq!(rec.last_token_ms, Some(95.0));
1789 assert_eq!(rec.ttft_ms, Some(50.0));
1790 assert_eq!(rec.ttst_ms, Some(10.0));
1791 assert_eq!(rec.e2e_latency_ms, Some(95.0));
1792 assert_eq!(rec.itl_ms, Some(15.0));
1794 assert_eq!(rec.input_length, 100);
1795 assert_eq!(rec.output_length, 4);
1796 assert_eq!(rec.reused_input_tokens, 30);
1797 assert_eq!(rec.prefill_worker_idx, Some(2));
1798 assert_eq!(rec.decode_worker_idx, Some(7));
1799 assert_eq!(rec.prefill_admit_ms, Some(5.0));
1800 assert_eq!(rec.source_held_ms, Some(10.0));
1801 assert_eq!(rec.destination_reserved_ms, Some(12.0));
1802 assert_eq!(rec.destination_activated_ms, Some(20.0));
1803 assert_eq!(rec.source_released_ms, Some(21.0));
1804 assert_eq!(rec.decode_admit_ms, Some(25.0));
1805 assert_eq!(rec.decode_reused_input_tokens, Some(40));
1806 assert_eq!(rec.prefill_route_overlap_tokens, Some(64));
1807 assert_eq!(rec.decode_route_overlap_tokens, Some(32));
1808 assert_eq!(rec.terminal_status, ReplayTerminalStatus::Completed);
1809 }
1810
1811 #[test]
1815 fn per_request_bypass_leaves_prefill_worker_idx_none() {
1816 let mut collector = TraceCollector::default();
1817 collector.set_capture_per_request(true);
1818 let uuid = Uuid::from_u128(42);
1819 collector.on_arrival(uuid, 0.0, 100, 2);
1820 collector.on_admit(uuid, 5.0, 0);
1821 collector.on_decode_assigned(uuid, 1);
1823 collector.on_token(uuid, 30.0);
1824 collector.on_token(uuid, 45.0);
1825 collector.on_terminal(uuid, 45.0, ReplayTerminalStatus::Completed);
1826
1827 let report = collector.finish();
1828 assert_eq!(report.per_request.len(), 1);
1829 let rec = &report.per_request[0];
1830 assert!(
1831 rec.prefill_worker_idx.is_none(),
1832 "bypassed request must have prefill_worker_idx = None"
1833 );
1834 assert_eq!(rec.decode_worker_idx, Some(1));
1835 }
1836
1837 #[test]
1840 fn per_request_default_off() {
1841 let mut collector = TraceCollector::default();
1842 let uuid = Uuid::from_u128(1);
1844 collector.on_arrival(uuid, 0.0, 100, 2);
1845 collector.on_admit(uuid, 5.0, 0);
1846 collector.on_decode_assigned(uuid, 0);
1847 collector.on_token(uuid, 50.0);
1848 collector.on_token(uuid, 60.0);
1849 collector.on_terminal(uuid, 60.0, ReplayTerminalStatus::Completed);
1850
1851 assert!(collector.requests[&uuid].detail.is_none());
1852
1853 let report = collector.finish();
1854 assert!(report.per_request.is_empty());
1855 assert_eq!(report.request_counts.completed_requests, 1);
1857 }
1858
1859 fn add_completed(
1862 collector: &mut TraceCollector,
1863 uuid_n: u128,
1864 arrival_ms: f64,
1865 output_length: usize,
1866 token_times_ms: &[f64],
1867 ) {
1868 let uuid = Uuid::from_u128(uuid_n);
1869 collector.on_arrival(uuid, arrival_ms, 100, output_length);
1870 collector.on_admit(uuid, arrival_ms, 0);
1871 collector.on_decode_assigned(uuid, 0);
1872 for &t in token_times_ms {
1873 collector.on_token(uuid, t);
1874 }
1875 let terminal_time_ms = token_times_ms.last().copied().unwrap_or(arrival_ms);
1876 collector.on_terminal(uuid, terminal_time_ms, ReplayTerminalStatus::Completed);
1877 }
1878
1879 #[test]
1883 fn goodput_classifies_by_aiperf_avg_itl() {
1884 let mut collector = TraceCollector::default();
1885 collector.set_sla_thresholds(SlaThresholds {
1886 ttft_ms: Some(150.0),
1887 itl_ms: Some(30.0),
1888 e2e_ms: None,
1889 });
1890 add_completed(&mut collector, 1, 0.0, 3, &[100.0, 150.0, 200.0]);
1892 add_completed(&mut collector, 2, 0.0, 3, &[100.0, 120.0, 140.0]);
1894 add_completed(&mut collector, 3, 0.0, 1, &[100.0]);
1896
1897 let goodput = collector
1898 .finish()
1899 .goodput
1900 .expect("SLA set → goodput present");
1901 assert_eq!(goodput.completed_requests, 2); assert!((goodput.output_throughput_tok_s - 4.0 / 0.2).abs() < 1e-6);
1904 assert!((goodput.request_throughput_rps - 2.0 / 0.2).abs() < 1e-6);
1905 }
1906
1907 #[test]
1909 fn goodput_itl_boundary_is_inclusive() {
1910 let sla = SlaThresholds {
1911 ttft_ms: None,
1912 itl_ms: Some(50.0),
1913 e2e_ms: None,
1914 };
1915 let mut at_bound = TraceCollector::default();
1917 at_bound.set_sla_thresholds(sla);
1918 add_completed(&mut at_bound, 1, 0.0, 3, &[100.0, 150.0, 200.0]);
1919 assert_eq!(at_bound.finish().goodput.unwrap().completed_requests, 1);
1920 let mut over = TraceCollector::default();
1922 over.set_sla_thresholds(sla);
1923 add_completed(&mut over, 1, 0.0, 3, &[100.0, 150.0, 201.0]);
1924 assert_eq!(over.finish().goodput.unwrap().completed_requests, 0);
1925 }
1926
1927 #[test]
1929 fn goodput_e2e_only_sla() {
1930 let mut collector = TraceCollector::default();
1931 collector.set_sla_thresholds(SlaThresholds {
1932 ttft_ms: None,
1933 itl_ms: None,
1934 e2e_ms: Some(150.0),
1935 });
1936 add_completed(&mut collector, 1, 0.0, 2, &[100.0, 200.0]); add_completed(&mut collector, 2, 0.0, 2, &[60.0, 120.0]); assert_eq!(collector.finish().goodput.unwrap().completed_requests, 1);
1939 }
1940
1941 #[test]
1943 fn goodput_absent_without_sla() {
1944 let mut collector = TraceCollector::default();
1945 add_completed(&mut collector, 1, 0.0, 2, &[10.0, 20.0]);
1946 assert!(collector.finish().goodput.is_none());
1947 }
1948
1949 #[test]
1952 fn worker_seconds_accumulated_and_static() {
1953 let mut accumulated = TraceCollector::default();
1954 add_completed(&mut accumulated, 1, 0.0, 2, &[10.0, 20.0]);
1955 accumulated.add_worker_seconds(1.5, 4.0);
1956 accumulated.add_worker_seconds(0.5, 1.0);
1957 let report = accumulated.finish();
1958 assert!((report.throughput.prefill_worker_seconds - 2.0).abs() < 1e-9);
1959 assert!((report.throughput.decode_worker_seconds - 5.0).abs() < 1e-9);
1960
1961 let mut static_runtime = TraceCollector::default();
1962 static_runtime.set_static_worker_count(0, 1);
1963 add_completed(&mut static_runtime, 1, 0.0, 2, &[100.0, 200.0]); let report = static_runtime.finish();
1965 assert!(report.throughput.prefill_worker_seconds.abs() < 1e-9);
1966 assert!((report.throughput.decode_worker_seconds - 0.2).abs() < 1e-9);
1967 }
1968
1969 #[test]
1970 fn compact_summary_excludes_execution_evidence() {
1971 let mut collector = TraceCollector::default();
1972 add_completed(&mut collector, 1, 0.0, 2, &[10.0, 20.0]);
1973 let mut report = collector.finish();
1974 report.runtime_evidence.pressure = Some(crate::replay::PressureEvidence::default());
1975
1976 let summary = serde_json::to_value(&report).unwrap();
1977 assert!(summary.get("runtime_evidence").is_none());
1978 }
1979
1980 #[test]
1983 fn gpu_hours_from_worker_seconds_and_gpus_per_worker() {
1984 let mut collector = TraceCollector::default();
1985 collector.set_gpus_per_worker(2, 4); add_completed(&mut collector, 1, 0.0, 2, &[100.0, 200.0]);
1987 collector.add_worker_seconds(10.0, 5.0); let report = collector.finish();
1989 assert_eq!(report.throughput.prefill_gpus_per_worker, 2);
1990 assert_eq!(report.throughput.decode_gpus_per_worker, 4);
1991 assert!((report.throughput.gpu_hours - 40.0 / 3600.0).abs() < 1e-9);
1993 }
1994
1995 #[test]
1998 fn per_request_records_are_sorted_by_arrival_time() {
1999 let mut collector = TraceCollector::default();
2000 collector.set_capture_per_request(true);
2001 for (uuid_n, arrival) in [(3u128, 30.0), (1, 0.0), (2, 10.0)] {
2003 let uuid = Uuid::from_u128(uuid_n);
2004 collector.on_arrival(uuid, arrival, 100, 1);
2005 collector.on_admit(uuid, arrival + 1.0, 0);
2006 collector.on_decode_assigned(uuid, 0);
2007 collector.on_token(uuid, arrival + 5.0);
2008 collector.on_terminal(uuid, arrival + 5.0, ReplayTerminalStatus::Completed);
2009 }
2010 let report = collector.finish();
2011 let arrivals: Vec<f64> = report
2012 .per_request
2013 .iter()
2014 .map(|r| r.arrival_time_ms)
2015 .collect();
2016 assert_eq!(arrivals, vec![0.0, 10.0, 30.0]);
2017 }
2018
2019 #[test]
2023 fn per_request_record_serializes_to_json_object() {
2024 let mut collector = TraceCollector::default();
2025 collector.set_capture_per_request(true);
2026 let uuid = Uuid::from_u128(123);
2027 collector.on_arrival(uuid, 0.0, 50, 2);
2028 collector.on_admit(uuid, 1.0, 10);
2029 collector.on_prefill_assigned(uuid, 0);
2030 collector.on_decode_assigned(uuid, 1);
2031 collector.on_token(uuid, 20.0);
2032 collector.on_token(uuid, 25.0);
2033 collector.on_terminal(uuid, 25.0, ReplayTerminalStatus::Completed);
2034
2035 let report = collector.finish();
2036 let line = serde_json::to_string(&report.per_request[0])
2037 .expect("PerRequestRecord must serialize cleanly");
2038 let parsed: serde_json::Value =
2040 serde_json::from_str(&line).expect("emitted JSON must parse");
2041 assert!(parsed.is_object());
2042 assert_eq!(parsed["uuid"], uuid.to_string());
2043 assert_eq!(parsed["input_length"], 50);
2044 assert_eq!(parsed["output_length"], 2);
2045 assert_eq!(parsed["prefill_worker_idx"], 0);
2046 assert_eq!(parsed["decode_worker_idx"], 1);
2047 assert!(parsed["itl_ms"].is_number());
2048 assert_eq!(parsed["terminal_status"], "completed");
2049 }
2050
2051 #[test]
2052 fn terminal_failures_emit_nullable_latencies_and_unfinished_requests_are_omitted() {
2053 let mut collector = TraceCollector::default();
2054 collector.set_capture_per_request(true);
2055 for (uuid_n, status) in [
2056 (1, ReplayTerminalStatus::Rejected),
2057 (2, ReplayTerminalStatus::Canceled),
2058 (3, ReplayTerminalStatus::Failed),
2059 ] {
2060 let uuid = Uuid::from_u128(uuid_n);
2061 collector.on_arrival(uuid, uuid_n as f64, 64, 2);
2062 collector.on_terminal(uuid, uuid_n as f64 + 1.0, status);
2063 }
2064 collector.on_arrival(Uuid::from_u128(4), 4.0, 64, 2);
2065
2066 let report = collector.finish();
2067
2068 assert_eq!(report.per_request.len(), 3);
2069 assert_eq!(
2070 report
2071 .per_request
2072 .iter()
2073 .map(|record| record.terminal_status)
2074 .collect::<Vec<_>>(),
2075 vec![
2076 ReplayTerminalStatus::Rejected,
2077 ReplayTerminalStatus::Canceled,
2078 ReplayTerminalStatus::Failed,
2079 ]
2080 );
2081 assert!(report.per_request.iter().all(|record| {
2082 record.first_admit_ms.is_none()
2083 && record.first_token_ms.is_none()
2084 && record.last_token_ms.is_none()
2085 && record.ttft_ms.is_none()
2086 && record.e2e_latency_ms.is_none()
2087 }));
2088 }
2089
2090 #[test]
2091 fn first_admission_reuse_ignores_later_readmission_self_reuse() {
2092 let uuid = Uuid::from_u128(1);
2093 let mut collector = TraceCollector::default();
2094 collector.on_arrival(uuid, 0.0, 100, 1);
2095 collector.on_admit(uuid, 1.0, 0);
2096 collector.on_admit(uuid, 2.0, 80);
2097 collector.on_token(uuid, 3.0);
2098 collector.on_terminal(uuid, 3.0, ReplayTerminalStatus::Completed);
2099
2100 let report = collector.finish();
2101
2102 assert_eq!(report.prefix_cache_reused_ratio, 0.8);
2103 assert_eq!(report.first_admission_prefix_cache_reused_ratio, 0.0);
2104 }
2105
2106 #[test]
2107 fn terminal_request_releases_per_token_timestamps() {
2108 let uuid = Uuid::from_u128(7);
2109 let mut collector = TraceCollector::default();
2110 collector.on_arrival(uuid, 0.0, 128, 100_000);
2111 collector.on_admit(uuid, 1.0, 0);
2112 for token_index in 0..100_000 {
2113 collector.on_token(uuid, token_index as f64 + 10.0);
2114 }
2115 assert_eq!(collector.retained_token_timestamps(), 100_000);
2116
2117 collector.on_terminal(uuid, 100_009.0, ReplayTerminalStatus::Completed);
2118
2119 assert_eq!(collector.retained_token_timestamps(), 0);
2120 let snapshot = collector
2121 .snapshot(uuid)
2122 .expect("request must remain summarized");
2123 assert_eq!(snapshot.output_length, 100_000);
2124 assert_eq!(snapshot.first_token_ms, Some(10.0));
2125 assert_eq!(snapshot.last_token_ms, Some(100_009.0));
2126 let report = collector.finish();
2127 assert_eq!(report.latency.itl.distribution.mean_ms, 1.0);
2128 assert_eq!(report.latency.itl.distribution.min_ms, 1.0);
2129 assert_eq!(report.latency.itl.distribution.max_ms, 1.0);
2130 }
2131
2132 #[test]
2133 fn deferred_token_timeline_finalization_folds_at_finish() {
2134 let uuid = Uuid::from_u128(8);
2135 let mut collector = TraceCollector::default();
2136 collector.set_defer_token_timeline_finalization(true);
2137 collector.on_arrival(uuid, 0.0, 128, 3);
2138 collector.on_admit(uuid, 1.0, 0);
2139 collector.on_token(uuid, 10.0);
2140 collector.on_token(uuid, 12.0);
2141 collector.on_token(uuid, 15.0);
2142 collector.on_terminal(uuid, 15.0, ReplayTerminalStatus::Completed);
2143
2144 assert_eq!(collector.retained_token_timestamps(), 3);
2145
2146 let report = collector.finish();
2147 assert_eq!(report.latency.itl.distribution.mean_ms, 2.5);
2148 assert_eq!(report.latency.itl.distribution.min_ms, 2.0);
2149 assert_eq!(report.latency.itl.distribution.max_ms, 3.0);
2150 }
2151}