jugar_probar/perf_gate/
samples.rs1use std::fs::File;
16use std::io::{BufWriter, Write};
17use std::path::{Path, PathBuf};
18
19use flate2::write::GzEncoder;
20use flate2::Compression;
21use serde::{Deserialize, Serialize};
22use sha2::{Digest, Sha256};
23
24use super::metrics::RequestSample;
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(deny_unknown_fields)]
29pub struct SamplesFile {
30 pub path: PathBuf,
32 pub sha256: String,
34 pub bytes: u64,
36 pub rows: usize,
38}
39
40impl SamplesFile {
41 #[must_use]
44 pub fn exceeds_budget(&self, budget_bytes: u64) -> bool {
45 self.bytes > budget_bytes
46 }
47}
48
49pub fn write_samples_gz(path: &Path, samples: &[RequestSample]) -> std::io::Result<SamplesFile> {
60 if let Some(parent) = path.parent() {
61 std::fs::create_dir_all(parent)?;
62 }
63 {
64 let file = File::create(path)?;
65 let mut gz = GzEncoder::new(BufWriter::new(file), Compression::default());
66 for s in samples {
67 let line = serde_json::to_string(s)
68 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
69 gz.write_all(line.as_bytes())?;
70 gz.write_all(b"\n")?;
71 }
72 gz.finish()?.flush()?;
73 }
74
75 let bytes = std::fs::read(path)?;
76 let mut hasher = Sha256::new();
77 hasher.update(&bytes);
78 Ok(SamplesFile {
79 path: path
80 .file_name()
81 .map_or_else(|| path.to_path_buf(), PathBuf::from),
82 sha256: format!("{:x}", hasher.finalize()),
83 bytes: bytes.len() as u64,
84 rows: samples.len(),
85 })
86}
87
88pub fn read_samples_gz(path: &Path) -> std::io::Result<Vec<RequestSample>> {
95 use std::io::BufRead;
96 let file = File::open(path)?;
97 let gz = flate2::read::GzDecoder::new(file);
98 let reader = std::io::BufReader::new(gz);
99 let mut out = Vec::new();
100 for line in reader.lines() {
101 let line = line?;
102 if line.trim().is_empty() {
103 continue;
104 }
105 out.push(
106 serde_json::from_str(&line)
107 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?,
108 );
109 }
110 Ok(out)
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use crate::perf_gate::bootstrap::bootstrap_agg_tok_s_ci;
117 use crate::perf_gate::protocol::Outcome;
118
119 fn deck(n: usize) -> Vec<RequestSample> {
120 (0..n)
121 .map(|i| RequestSample {
122 index: i,
123 worker: i % 4,
124 start_s: i as f64 * 0.25,
125 end_s: i as f64 * 0.25 + 1.0 + f64::from((i % 3) as u32) * 0.1,
126 token_times_s: vec![i as f64 * 0.25 + 0.05, i as f64 * 0.25 + 0.9],
127 generated_tokens: 128,
128 prompt_tokens: 512,
129 outcome: Outcome::Completed,
130 in_flight_at_start: 4,
131 drained: false,
132 })
133 .collect()
134 }
135
136 fn tmpdir(name: &str) -> PathBuf {
137 let d = std::env::temp_dir().join(format!("perf024-{name}-{}", std::process::id()));
138 let _ = std::fs::remove_dir_all(&d);
139 d
140 }
141
142 #[test]
143 fn samples_round_trip_through_gzip() {
144 let dir = tmpdir("roundtrip");
145 let path = dir.join("samples.jsonl.gz");
146 let want = deck(25);
147 let meta = write_samples_gz(&path, &want).expect("write");
148 assert_eq!(meta.rows, 25);
149 assert!(meta.bytes > 0);
150 assert_eq!(meta.sha256.len(), 64);
151 assert_eq!(meta.path, PathBuf::from("samples.jsonl.gz"));
152
153 let got = read_samples_gz(&path).expect("read");
154 assert_eq!(got, want, "retained samples must survive the round trip");
155 let _ = std::fs::remove_dir_all(&dir);
156 }
157
158 #[test]
161 fn the_ci_is_reproducible_from_the_retained_file_alone() {
162 let dir = tmpdir("rederive");
163 let path = dir.join("samples.jsonl.gz");
164 let original = deck(40);
165 write_samples_gz(&path, &original).expect("write");
166
167 let from_disk = read_samples_gz(&path).expect("read");
168 let a = bootstrap_agg_tok_s_ci(&original, 0.95).expect("n >= 2");
169 let b = bootstrap_agg_tok_s_ci(&from_disk, 0.95).expect("n >= 2");
170 assert_eq!(a, b, "a receipt is only evidence if its CI re-derives");
171 let _ = std::fs::remove_dir_all(&dir);
172 }
173
174 #[test]
176 fn the_file_is_actually_gzip() {
177 let dir = tmpdir("magic");
178 let path = dir.join("samples.jsonl.gz");
179 write_samples_gz(&path, &deck(3)).expect("write");
180 let bytes = std::fs::read(&path).expect("read raw");
181 assert_eq!(&bytes[..2], &[0x1f, 0x8b], "gzip magic bytes absent");
182 let _ = std::fs::remove_dir_all(&dir);
183 }
184
185 #[test]
186 fn digest_matches_the_bytes_on_disk() {
187 let dir = tmpdir("digest");
188 let path = dir.join("samples.jsonl.gz");
189 let meta = write_samples_gz(&path, &deck(5)).expect("write");
190 let bytes = std::fs::read(&path).expect("read raw");
191 let mut h = Sha256::new();
192 h.update(&bytes);
193 assert_eq!(meta.sha256, format!("{:x}", h.finalize()));
194 let _ = std::fs::remove_dir_all(&dir);
195 }
196
197 #[test]
199 fn one_json_object_per_line() {
200 let dir = tmpdir("lines");
201 let path = dir.join("samples.jsonl.gz");
202 write_samples_gz(&path, &deck(7)).expect("write");
203 let file = File::open(&path).expect("open");
204 let mut text = String::new();
205 std::io::Read::read_to_string(&mut flate2::read::GzDecoder::new(file), &mut text)
206 .expect("decode");
207 assert_eq!(text.lines().count(), 7);
208 for line in text.lines() {
209 let _: RequestSample = serde_json::from_str(line).expect("each line stands alone");
210 }
211 let _ = std::fs::remove_dir_all(&dir);
212 }
213
214 #[test]
222 fn a_samples_record_with_an_unknown_key_is_refused() {
223 let honest = r#"{"path":"samples.c1.r1.jsonl.gz","sha256":"ab","bytes":10,"rows":3}"#;
224 let parsed: SamplesFile = serde_json::from_str(honest).expect("the known shape parses");
225 assert_eq!(parsed.rows, 3);
226
227 let extra = r#"{"path":"s.gz","sha256":"ab","bytes":10,"rows":3,"rows_dropped":7}"#;
228 let err = serde_json::from_str::<SamplesFile>(extra)
229 .expect_err("an unknown key must be refused, not dropped");
230 assert!(err.to_string().contains("rows_dropped"), "{err}");
231 }
232
233 #[test]
234 fn budget_check_takes_the_budget_as_an_argument() {
235 let dir = tmpdir("budget");
236 let path = dir.join("samples.jsonl.gz");
237 let meta = write_samples_gz(&path, &deck(10)).expect("write");
238 assert!(meta.exceeds_budget(0));
239 assert!(!meta.exceeds_budget(u64::MAX));
240 let _ = std::fs::remove_dir_all(&dir);
241 }
242}