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:
osobh
2026-04-27 04:04:27 -07:00
co-authored by Claude Opus 4.7
parent abc07ffd7e
commit 2ce6f8ff32
@@ -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
}