agentmux_srv/backend/blockcontroller/shell/
stamps.rs1use crate::backend::storage::error::StoreError;
26
27pub(crate) const MARGIN: i64 = 64 * 1024;
30const PROBE: i64 = 512;
32const SEARCH_STOP: i64 = 4096;
34const CHUNK: i64 = 64 * 1024;
36
37#[derive(serde::Deserialize)]
38struct Record {
39 off: u64,
40 ms: i64,
41}
42
43fn parse(line: &[u8]) -> Option<(u64, i64)> {
44 let line = line.strip_suffix(b"\r").unwrap_or(line);
45 serde_json::from_slice::<Record>(line).ok().map(|r| (r.off, r.ms))
46}
47
48pub(crate) fn stamps_for(
53 read: &dyn Fn(i64, i64) -> Result<Option<Vec<u8>>, StoreError>,
54 size: i64,
55 line_offsets: &[u64],
56) -> Result<Option<Vec<i64>>, StoreError> {
57 let (Some(&first), Some(&last)) = (line_offsets.first(), line_offsets.last()) else {
58 return Ok(Some(Vec::new()));
59 };
60 if size <= 0 {
61 return Ok(None);
62 }
63
64 let (mut lo, mut hi) = (0i64, size);
66 while hi - lo > SEARCH_STOP {
67 let mid = lo + (hi - lo) / 2;
68 match first_record_after(read, mid, hi)? {
69 Probe::Found { start, off } if off <= first => lo = start,
70 Probe::Found { .. } | Probe::NoneBefore => hi = mid,
71 Probe::Unreadable => return Ok(None),
72 }
73 }
74
75 let from = (lo - MARGIN).max(0);
78 let mut records: Vec<(u64, i64)> = Vec::new();
79 let mut pos = from;
80 let mut carry: Vec<u8> = Vec::new();
81 let mut at_line_start = from == 0;
82 let mut stop_at: Option<i64> = None;
83 while pos < size && stop_at.map_or(true, |s| pos < s) {
84 let len = CHUNK.min(size - pos);
85 let Some(chunk) = read(pos, len)? else { return Ok(None) };
86 carry.extend_from_slice(&chunk);
87 pos += len;
88 let Some(last_nl) = carry.iter().rposition(|&b| b == b'\n') else { continue };
89 let complete: Vec<u8> = carry.drain(..=last_nl).collect();
90 let mut lines = complete.split(|&b| b == b'\n');
91 if !at_line_start {
92 lines.next();
94 at_line_start = true;
95 }
96 for line in lines {
97 if let Some((off, ms)) = parse(line) {
98 records.push((off, ms));
99 if off > last && stop_at.is_none() {
100 stop_at = Some(pos + MARGIN);
101 }
102 }
103 }
104 }
105 if pos >= size && at_line_start {
107 if let Some(rec) = parse(&carry) {
108 records.push(rec);
109 }
110 }
111 if records.is_empty() {
112 return Ok(None);
113 }
114 records.sort_by_key(|&(off, _)| off);
115 Ok(Some(
116 line_offsets
117 .iter()
118 .map(|&line_off| match records.partition_point(|&(off, _)| off <= line_off) {
119 0 => 0,
120 p => records[p - 1].1,
121 })
122 .collect(),
123 ))
124}
125
126enum Probe {
127 Found { start: i64, off: u64 },
129 NoneBefore,
131 Unreadable,
133}
134
135fn first_record_after(
138 read: &dyn Fn(i64, i64) -> Result<Option<Vec<u8>>, StoreError>,
139 pos: i64,
140 hi: i64,
141) -> Result<Probe, StoreError> {
142 let mut at = pos;
143 let mut buf: Vec<u8> = Vec::new();
144 let mut boundary: Option<i64> = None;
145 while at < hi {
146 let len = PROBE.min(hi - at);
147 let Some(chunk) = read(at, len)? else { return Ok(Probe::Unreadable) };
148 buf.extend_from_slice(&chunk);
149 at += len;
150 let base = pos;
151 if boundary.is_none() {
152 let Some(nl) = buf.iter().position(|&b| b == b'\n') else { continue };
153 boundary = Some(base + nl as i64 + 1);
154 }
155 let mut start = boundary.unwrap();
157 loop {
158 let rel = (start - base) as usize;
159 if rel >= buf.len() || start >= hi {
160 break;
161 }
162 let Some(len) = buf[rel..].iter().position(|&b| b == b'\n') else { break };
163 if let Some((off, _)) = parse(&buf[rel..rel + len]) {
164 return Ok(Probe::Found { start, off });
165 }
166 start += len as i64 + 1;
167 }
168 boundary = Some(start);
169 }
170 Ok(Probe::NoneBefore)
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use proptest::prelude::*;
177
178 fn reference(sidecar: &[u8], line_offsets: &[u64]) -> Option<Vec<i64>> {
180 let mut entries: Vec<(u64, i64)> = sidecar.split(|&b| b == b'\n').filter_map(parse).collect();
181 if entries.is_empty() {
182 return None;
183 }
184 entries.sort_by_key(|&(off, _)| off);
185 Some(
186 line_offsets
187 .iter()
188 .map(|&l| match entries.partition_point(|&(off, _)| off <= l) {
189 0 => 0,
190 p => entries[p - 1].1,
191 })
192 .collect(),
193 )
194 }
195
196 fn windowed(sidecar: &[u8], line_offsets: &[u64]) -> Option<Vec<i64>> {
197 let read = |off: i64, len: i64| -> Result<Option<Vec<u8>>, StoreError> {
198 let (a, b) = (off as usize, ((off + len) as usize).min(sidecar.len()));
199 Ok(Some(sidecar[a..b].to_vec()))
200 };
201 stamps_for(&read, sidecar.len() as i64, line_offsets).unwrap()
202 }
203
204 fn sidecar(records: &[(u64, i64)]) -> Vec<u8> {
205 records.iter().map(|(o, m)| format!("{{\"off\":{o},\"ms\":{m}}}\n")).collect::<String>().into_bytes()
206 }
207
208 #[test]
209 fn a_large_sidecar_gives_the_same_stamps_reading_a_small_window() {
210 let records: Vec<(u64, i64)> = (0..200_000u64).map(|i| (i * 300, 1_700_000_000_000 + i as i64)).collect();
211 let bytes = sidecar(&records);
212 let lines: Vec<u64> = (0..200u64).map(|k| 45_000_000 + k * 150).collect();
213 let read_bytes = std::cell::Cell::new(0i64);
214 let read = |off: i64, len: i64| -> Result<Option<Vec<u8>>, StoreError> {
215 read_bytes.set(read_bytes.get() + len);
216 let (a, b) = (off as usize, ((off + len) as usize).min(bytes.len()));
217 Ok(Some(bytes[a..b].to_vec()))
218 };
219 let got = stamps_for(&read, bytes.len() as i64, &lines).unwrap();
220 assert_eq!(got, reference(&bytes, &lines));
221 assert!(read_bytes.get() < 400 * 1024, "read {} of {} bytes", read_bytes.get(), bytes.len());
222 }
223
224 #[test]
225 fn lines_before_every_record_are_unknown_and_an_empty_sidecar_has_none() {
226 let bytes = sidecar(&[(100, 7), (200, 8)]);
227 assert_eq!(windowed(&bytes, &[0, 99, 100, 250]), Some(vec![0, 0, 7, 8]));
228 assert_eq!(windowed(b"", &[1]), None);
229 assert_eq!(windowed(b"garbage\n", &[1]), None);
230 assert_eq!(windowed(&bytes, &[]), Some(vec![]));
231 }
232
233 #[test]
234 fn bytes_not_stored_mean_no_stamps() {
235 let bytes = sidecar(&(0..5000u64).map(|i| (i * 10, i as i64)).collect::<Vec<_>>());
236 let read = |off: i64, len: i64| -> Result<Option<Vec<u8>>, StoreError> {
237 Ok((off + len < 60_000).then(|| bytes[off as usize..(off + len) as usize].to_vec()))
238 };
239 assert_eq!(stamps_for(&read, bytes.len() as i64, &[49_000]).unwrap(), None);
240 }
241
242 proptest! {
243 #[test]
246 fn windowed_stamps_equal_the_full_parse(
247 n in 1usize..6000,
248 gaps in prop::collection::vec(0u64..400, 1..64),
249 swaps in prop::collection::vec(0usize..6000, 0..40),
250 garbage in prop::collection::vec(0usize..6000, 0..10),
251 crlf in any::<bool>(),
252 unterminated in any::<bool>(),
253 picks in prop::collection::vec(0u64..2_500_000, 1..60),
254 ) {
255 let mut off = 0u64;
256 let mut records: Vec<(u64, i64)> = (0..n).map(|i| { off += gaps[i % gaps.len()]; (off, i as i64 + 1) }).collect();
257 for s in swaps { if s + 1 < records.len() { records.swap(s, s + 1); } }
259 let mut text = String::new();
260 for (i, (o, m)) in records.iter().enumerate() {
261 if garbage.contains(&i) { text.push_str("{\"off\":oops\n"); }
262 text.push_str(&format!("{{\"off\":{o},\"ms\":{m}}}"));
263 text.push_str(if crlf { "\r\n" } else { "\n" });
264 }
265 if unterminated { let t = text.trim_end().len(); text.truncate(t); }
266 let bytes = text.into_bytes();
267 let mut lines = picks.clone();
268 lines.sort_unstable();
269 let lo = lines[0];
271 let lines: Vec<u64> = lines.into_iter().filter(|&l| l - lo < 20_000).collect();
272 prop_assert_eq!(windowed(&bytes, &lines), reference(&bytes, &lines));
273 }
274 }
275}