1use std::fmt::Write as _;
4use std::time::Duration;
5
6use crate::pipeline::tts_transformer::CodecRolloutSubTimings;
7
8#[derive(Debug, Clone, Default, PartialEq, Eq)]
15pub struct SynthesisStageTimings {
16 pub speaker_encode: Duration,
18 pub tokenize: Duration,
20 pub prefill_build: Duration,
22 pub codec_rollout: Duration,
24 pub vocoder_decode: Duration,
26 pub post: Duration,
28 pub pipeline_overlap: Duration,
31 pub first_frame_latency: Duration,
35 pub codec_rollout_detail: CodecRolloutSubTimings,
37 pub generated_samples: usize,
39 pub sample_rate_hz: u32,
41}
42
43impl SynthesisStageTimings {
44 #[must_use]
45 pub fn total(&self) -> Duration {
46 let sum = self.speaker_encode
47 + self.tokenize
48 + self.prefill_build
49 + self.codec_rollout
50 + self.vocoder_decode
51 + self.post;
52 sum.saturating_sub(self.pipeline_overlap)
53 }
54
55 #[must_use]
57 pub fn average(samples: &[Self]) -> Option<Self> {
58 if samples.is_empty() {
59 return None;
60 }
61 let n = samples.len() as u128;
62 let avg = |field: fn(&Self) -> Duration| {
63 let sum: u128 = samples.iter().map(|s| field(s).as_nanos()).sum();
64 let nanos = (sum / n).min(u128::from(u64::MAX)) as u64;
65 Duration::from_nanos(nanos)
66 };
67 let avg_usize = |field: fn(&Self) -> usize| {
68 let sum: u128 = samples.iter().map(|s| field(s) as u128).sum();
69 (sum / n) as usize
70 };
71 Some(Self {
72 speaker_encode: avg(|s| s.speaker_encode),
73 tokenize: avg(|s| s.tokenize),
74 prefill_build: avg(|s| s.prefill_build),
75 codec_rollout: avg(|s| s.codec_rollout),
76 vocoder_decode: avg(|s| s.vocoder_decode),
77 post: avg(|s| s.post),
78 pipeline_overlap: avg(|s| s.pipeline_overlap),
79 first_frame_latency: avg(|s| s.first_frame_latency),
80 codec_rollout_detail: CodecRolloutSubTimings {
81 talker_prefill: avg(|s| s.codec_rollout_detail.talker_prefill),
82 talker_steps: avg(|s| s.codec_rollout_detail.talker_steps),
83 code_pred_total: avg(|s| s.codec_rollout_detail.code_pred_total),
84 kv_writeback: avg(|s| s.codec_rollout_detail.kv_writeback),
85 kv_download: avg(|s| s.codec_rollout_detail.kv_download),
86 kv_quantize: avg(|s| s.codec_rollout_detail.kv_quantize),
87 kv_upload: avg(|s| s.codec_rollout_detail.kv_upload),
88 talker_kv_bytes: avg_usize(|s| s.codec_rollout_detail.talker_kv_bytes),
89 },
90 generated_samples: avg_usize(|s| s.generated_samples),
91 sample_rate_hz: samples[0].sample_rate_hz,
92 })
93 }
94
95 #[must_use]
97 pub fn format_table(&self) -> String {
98 let total = self.total();
99 let total_ms = duration_ms(total);
100 let mut out = String::new();
101 let _ = writeln!(
102 out,
103 "synthesis stage timings (wall clock, single run){}",
104 if total_ms > 0.0 {
105 String::new()
106 } else {
107 " — total 0ms".to_string()
108 }
109 );
110 let rows: [(&str, Duration); 6] = [
111 ("speaker_encode", self.speaker_encode),
112 ("tokenize", self.tokenize),
113 ("prefill_build", self.prefill_build),
114 ("codec_rollout (transformer)", self.codec_rollout),
115 ("vocoder_decode", self.vocoder_decode),
116 ("post (flatten/trim)", self.post),
117 ];
118 for (name, d) in rows {
119 let ms = duration_ms(d);
120 let pct = if total.is_zero() {
121 0.0
122 } else {
123 100.0 * ms / total_ms.max(f64::EPSILON)
124 };
125 let _ = writeln!(out, " {name:<28} {ms:>10.3} ms ({pct:>5.1}%)");
126 }
127 let d = &self.codec_rollout_detail;
128 let has_detail = !d.talker_prefill.is_zero()
129 || !d.talker_steps.is_zero()
130 || !d.code_pred_total.is_zero()
131 || !d.kv_writeback.is_zero()
132 || !d.kv_download.is_zero()
133 || !d.kv_quantize.is_zero()
134 || !d.kv_upload.is_zero()
135 || d.talker_kv_bytes > 0;
136 if has_detail {
137 let sub_rows: [(&str, Duration); 7] = [
138 (" talker_prefill", d.talker_prefill),
139 (" talker_steps", d.talker_steps),
140 (" code_pred_total", d.code_pred_total),
141 (" kv_writeback", d.kv_writeback),
142 (" kv_download", d.kv_download),
143 (" kv_quantize", d.kv_quantize),
144 (" kv_upload", d.kv_upload),
145 ];
146 for (name, sd) in sub_rows {
147 let _ = writeln!(out, " {name:<26} {:>10.3} ms", duration_ms(sd));
148 }
149 if d.talker_kv_bytes > 0 {
150 let mib = d.talker_kv_bytes as f64 / (1024.0 * 1024.0);
151 let _ = writeln!(out, " {:<26} {:>10.3} MiB", "talker_kv_bytes", mib);
152 }
153 }
154 if !self.pipeline_overlap.is_zero() {
155 let overlap_ms = duration_ms(self.pipeline_overlap);
156 let _ = writeln!(
157 out,
158 " {:<28} {:>+10.3} ms (pipeline overlap)",
159 "pipeline_overlap", -overlap_ms
160 );
161 }
162 if !self.first_frame_latency.is_zero() {
163 let _ = writeln!(
164 out,
165 " {:<28} {:>10.3} ms",
166 "first_frame_latency",
167 duration_ms(self.first_frame_latency)
168 );
169 }
170 let _ = writeln!(out, " {:<28} {:>10.3} ms (100.0%)", "total", total_ms);
171 if self.sample_rate_hz > 0 && self.generated_samples > 0 {
172 let audio_secs = self.generated_samples as f64 / self.sample_rate_hz as f64;
173 let audio_ms = audio_secs * 1_000.0;
174 let rtf = if audio_secs > 0.0 {
175 total.as_secs_f64() / audio_secs
176 } else {
177 0.0
178 };
179 let _ = writeln!(
180 out,
181 " {:<28} {:>10.3} ms (RTF = {:.3}x)",
182 "audio duration", audio_ms, rtf
183 );
184 }
185 out
186 }
187}
188
189fn duration_ms(d: Duration) -> f64 {
190 d.as_secs_f64() * 1_000.0
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn format_table_zero_total() {
199 let t = SynthesisStageTimings::default();
200 let s = t.format_table();
201 assert!(s.contains("speaker_encode"));
202 assert!(s.contains("total"));
203 }
204
205 #[test]
206 fn average_two_samples() {
207 let a = SynthesisStageTimings {
208 tokenize: Duration::from_millis(2),
209 codec_rollout: Duration::from_millis(8),
210 ..Default::default()
211 };
212 let b = SynthesisStageTimings {
213 tokenize: Duration::from_millis(4),
214 codec_rollout: Duration::from_millis(12),
215 ..Default::default()
216 };
217 let m = SynthesisStageTimings::average(&[a, b]).unwrap();
218 assert_eq!(m.tokenize, Duration::from_millis(3));
219 assert_eq!(m.codec_rollout, Duration::from_millis(10));
220 }
221}