rtx-csm: bench harness parses /metrics phase gauges
Pulls the new per-phase server-side gauges (rtx_csm_recv_phase_ms_avg,
rtx_csm_llm_to_first_audio_ms_avg, rtx_csm_conv_total_ms_avg,
rtx_csm_total_turn_ms_avg, plus the older stt/tts/e2e_first ones) and
pretty-prints them in a labeled block right after the client-side
percentile stats. Raw /metrics is still emitted at the bottom for
anyone who wants the original output.
Sample output (mock LLM, 2 turns, FP CSM):
--- per-phase stats (client-side) ---
audio_send (n=2): mean=1ms p50=1ms p95=1ms min=1ms max=1ms
transcript_ms (n=2): mean=4450ms p50=4497ms ...
first_audio_ms (n=2): mean=3908ms p50=3915ms ...
turn_total_ms (n=2): mean=17171ms p50=17176ms ...
--- server-side phase averages (across all turns) ---
recv_phase 4345ms
llm_to_first_audio 3940ms
conv_total 12742ms
total_turn 17127ms
stt_post 0ms
tts_per_utterance 2679ms
e2e_first_audio 8325ms
Client/server numbers align tightly: client transcript_ms ≈ server
recv_phase, client first_audio_ms ≈ server llm_to_first_audio.
Discrepancies above ~5% indicate machine variance or non-realtime
client pacing artifacts.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
@@ -225,7 +225,56 @@ async fn main() -> Result<()> {
|
|||||||
.await?
|
.await?
|
||||||
.text()
|
.text()
|
||||||
.await?;
|
.await?;
|
||||||
println!("\n--- server /metrics snapshot ---\n{body}");
|
|
||||||
|
// Pretty-print just the per-phase server-side gauges. These are
|
||||||
|
// averages over the lifetime of the server (i.e. all turns we just
|
||||||
|
// ran, since the server has been single-use). They complement the
|
||||||
|
// client-side stats above by including time the client can't see
|
||||||
|
// (mutex contention, internal mpsc, etc.).
|
||||||
|
let phase_keys = [
|
||||||
|
("rtx_csm_recv_phase_ms_avg", "recv_phase"),
|
||||||
|
("rtx_csm_llm_to_first_audio_ms_avg", "llm_to_first_audio"),
|
||||||
|
("rtx_csm_conv_total_ms_avg", "conv_total"),
|
||||||
|
("rtx_csm_total_turn_ms_avg", "total_turn"),
|
||||||
|
("rtx_csm_stt_latency_ms_avg", "stt_post"),
|
||||||
|
("rtx_csm_tts_latency_ms_avg", "tts_per_utterance"),
|
||||||
|
("rtx_csm_e2e_first_audio_ms_avg", "e2e_first_audio"),
|
||||||
|
];
|
||||||
|
println!("\n--- server-side phase averages (across all turns) ---");
|
||||||
|
for (key, label) in phase_keys.iter() {
|
||||||
|
if let Some(v) = parse_gauge(&body, key) {
|
||||||
|
println!(" {label:<22}{v:>6}ms");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user wants the raw text we still emit it, but at the
|
||||||
|
// bottom — most of the time the parsed view is what's wanted.
|
||||||
|
println!("\n--- raw /metrics ---\n{body}");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extract a single Prometheus gauge value (`name <int>` or `name <float>`).
|
||||||
|
/// Comments (`# ...`) and unrelated lines are skipped. Returns `None` if
|
||||||
|
/// the gauge isn't present (e.g., zero turns ran).
|
||||||
|
fn parse_gauge(body: &str, name: &str) -> Option<u64> {
|
||||||
|
for line in body.lines() {
|
||||||
|
let line = line.trim_start();
|
||||||
|
if line.starts_with('#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut parts = line.split_whitespace();
|
||||||
|
if parts.next()? != name {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let v = parts.next()?;
|
||||||
|
// Gauges are written as integers in the server but accept floats.
|
||||||
|
if let Ok(i) = v.parse::<u64>() {
|
||||||
|
return Some(i);
|
||||||
|
}
|
||||||
|
if let Ok(f) = v.parse::<f64>() {
|
||||||
|
return Some(f as u64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user