Skip to main content

xtask/
metrics.rs

1use anyhow::{Context, Result, ensure};
2use regex::Regex;
3use serde_json::{Value, json};
4
5const BYTES_PER_GB: f64 = 1_000_000_000.0;
6
7fn fields(text: &str, label: &str, pattern: &str) -> Result<Vec<f64>> {
8    let regex = Regex::new(pattern)?;
9    let captures = regex
10        .captures(text)
11        .with_context(|| format!("incomplete engine telemetry: {label}"))?;
12
13    captures
14        .iter()
15        .skip(1)
16        .map(|m| Ok(m.unwrap().as_str().parse()?))
17        .collect()
18}
19
20pub fn parse(text: &str) -> Result<Value> {
21    let load = fields(text, "load", r"load ([\d.]+)s, clock probe ([\d.]+) ms")?;
22    let prefill = fields(
23        text,
24        "prefill",
25        r"prefill (\d+) tokens in ([\d.]+)s \(([\d.]+) tok/s\)",
26    )?;
27    let decode = fields(
28        text,
29        "decode",
30        r"decode (\d+) tokens in ([\d.]+)s \(([\d.]+) tok/s\).*?\| (\d+) steps, mean step ([\d.]+) ms.*?gpu-(?:active|span) ([\d.]+) ms.*?io wait ([\d.]+) ms",
31    )?;
32    let memory = memory_stats(text)?;
33    let metal_gb = match &memory {
34        Some(stats) => {
35            stats["metal_allocated_bytes_observed"]
36                .as_u64()
37                .context("memory_stats: expected integer Metal bytes")? as f64
38                / BYTES_PER_GB
39        }
40        None => fields(text, "memory", r"\| ([\d.]+) GB Metal")?[0],
41    };
42    let clock = fields(text, "clock_end", r"clock probe at end ([\d.]+) ms")?;
43    let drafts = fields(text, "drafts", r"drafts (\d+), accepted ([\d.]+)/step")?;
44    let build = fields(
45        text,
46        "store_build",
47        r"built the [23]-bit store in ([\d.]+)s",
48    )
49    .ok();
50
51    Ok(json!({
52        "store_build_seconds":build.map(|v|v[0]), "load_seconds":load[0], "clock_start_ms":load[1],
53        "prompt_tokens":prefill[0] as u64, "prefill_seconds":prefill[1], "pp_s":prefill[2],
54        "output_tokens":decode[0] as u64, "decode_seconds":decode[1], "tg_s":decode[2],
55        "steps":decode[3] as u64, "mean_step_ms":decode[4], "gpu_span_ms":decode[5], "io_wait_ms":decode[6],
56        "metal_gb":metal_gb, "memory":memory,
57        "clock_end_ms":clock[0], "drafts":drafts[0] as u64, "accepted_drafts_per_step":drafts[1]
58    }))
59}
60
61fn memory_stats(text: &str) -> Result<Option<Value>> {
62    let mut snapshots = text
63        .lines()
64        .filter_map(|line| line.strip_prefix("memory_stats "));
65    let Some(line) = snapshots.next() else {
66        return Ok(None);
67    };
68
69    ensure!(
70        snapshots.next().is_none(),
71        "multiple memory_stats snapshots in one run"
72    );
73
74    let value: Value = serde_json::from_str(line).context("invalid memory_stats JSON")?;
75
76    ensure!(value.is_object(), "memory_stats must be an object");
77
78    Ok(Some(value))
79}
80
81pub fn extract_svg(text: &str) -> Result<(&str, usize)> {
82    let regex = Regex::new(r"(?is)<svg\b.*?</svg\s*>")?;
83    let svg = regex
84        .find(text)
85        .context("no complete SVG: output may have hit its token limit")?
86        .as_str();
87    let document = roxmltree::Document::parse(svg)?;
88
89    anyhow::ensure!(
90        document.root_element().tag_name().name() == "svg",
91        "root element is not svg"
92    );
93
94    Ok((
95        svg,
96        document.descendants().filter(|n| n.is_element()).count(),
97    ))
98}
99
100pub fn repeated_tail(text: &str) -> Option<Value> {
101    let mut words: Vec<&str> = text.split_whitespace().rev().skip(1).take(4096).collect();
102
103    words.reverse();
104
105    for period in 32..=512.min(words.len() / 4) {
106        let tail = &words[words.len() - 4 * period..];
107        let block = &words[words.len() - period..];
108
109        if tail.chunks_exact(period).all(|chunk| chunk == block) {
110            return Some(
111                json!({"words_per_cycle":period,"repetitions":4,"example":block.join(" ").chars().take(240).collect::<String>()}),
112            );
113        }
114    }
115
116    None
117}