1#![forbid(unsafe_code)]
3
4use alloc::collections::VecDeque;
5use alloc::vec;
6use alloc::vec::Vec;
7
8use lz4rip_core::MAX_DISTANCE;
9use lz4rip_core::MINMATCH;
10
11pub struct DictTrainer {
26 max_dict_size: usize,
27 samples: VecDeque<Vec<u8>>,
28 total_bytes: usize,
29}
30
31impl DictTrainer {
32 pub fn new(max_dict_size: usize) -> Self {
37 let max_dict_size = max_dict_size.min(MAX_DISTANCE);
38 DictTrainer {
39 max_dict_size,
40 samples: VecDeque::new(),
41 total_bytes: 0,
42 }
43 }
44
45 pub fn add_sample(&mut self, data: &[u8]) {
50 if data.len() < MINMATCH || data.len() > self.max_dict_size {
51 return;
52 }
53
54 let budget = self.max_dict_size * 8;
55
56 while self.total_bytes + data.len() > budget && !self.samples.is_empty() {
57 self.total_bytes -= self.samples.pop_front().unwrap().len();
58 }
59
60 self.total_bytes += data.len();
61 self.samples.push_back(data.to_vec());
62 }
63
64 pub fn sample_count(&self) -> usize {
66 self.samples.len()
67 }
68
69 pub fn total_bytes(&self) -> usize {
71 self.total_bytes
72 }
73
74 pub fn train(self) -> Vec<u8> {
80 if self.samples.len() < 2 {
81 return Vec::new();
82 }
83 let sample_refs: Vec<&[u8]> = self.samples.iter().map(|s| s.as_slice()).collect();
84 cover_select(&sample_refs, self.max_dict_size)
85 }
86}
87
88const D: usize = 8;
89const FREQ_BITS: usize = 16;
90const FREQ_SIZE: usize = 1 << FREQ_BITS;
91const FREQ_MASK: usize = FREQ_SIZE - 1;
92
93#[inline(always)]
94fn hash_dmer(data: &[u8]) -> usize {
95 let v = u64::from_le_bytes(data[..8].try_into().unwrap());
96 v.wrapping_mul(0x9E3779B97F4A7C15) as usize
97}
98
99fn cover_select(samples: &[&[u8]], dict_size: usize) -> Vec<u8> {
100 let mut concat = Vec::new();
101 let mut offsets = Vec::new();
102 for &sample in samples {
103 offsets.push(concat.len());
104 concat.extend_from_slice(sample);
105 }
106 offsets.push(concat.len());
107
108 if concat.len() < D {
109 return concat[..dict_size.min(concat.len())].to_vec();
110 }
111
112 let num_dmers = concat.len() - D + 1;
113
114 let mut hashes = vec![0u32; num_dmers];
115 for i in 0..num_dmers {
116 hashes[i] = (hash_dmer(&concat[i..i + D]) & FREQ_MASK) as u32;
117 }
118
119 let mut freqs = vec![0u32; FREQ_SIZE];
120 for s in 0..samples.len() {
121 let start = offsets[s];
122 let end = offsets[s + 1];
123 if end - start < D {
124 continue;
125 }
126 for i in start..end - D + 1 {
127 freqs[hashes[i] as usize] += 1;
128 }
129 }
130
131 let k = dict_size / 4;
132 if concat.len() < k {
133 return concat;
134 }
135
136 let seg_dmers = k - D + 1;
137 let mut used = vec![false; concat.len()];
138 let mut segments: Vec<(usize, u64)> = Vec::new();
139 let mut collected = 0usize;
140
141 while collected < dict_size {
142 let mut prefix = vec![0u64; num_dmers + 1];
143 prefix[0] = 0;
144 for i in 0..num_dmers {
145 prefix[i + 1] = prefix[i] + freqs[hashes[i] as usize] as u64;
146 }
147
148 let mut best_pos = 0;
149 let mut best_score = 0u64;
150 for pos in 0..=concat.len() - k {
151 if !used[pos] {
152 let score = prefix[pos + seg_dmers] - prefix[pos];
153 if score > best_score {
154 best_score = score;
155 best_pos = pos;
156 }
157 }
158 }
159
160 if best_score == 0 {
161 break;
162 }
163
164 segments.push((best_pos, best_score));
165
166 for i in best_pos..best_pos + seg_dmers {
167 freqs[hashes[i] as usize] = 0;
168 }
169 used[best_pos..best_pos + k].fill(true);
170
171 collected += k;
172 }
173
174 let mut dict = Vec::with_capacity(dict_size);
175 for &(pos, _) in segments.iter().rev() {
176 let end = (pos + k).min(concat.len());
177 dict.extend_from_slice(&concat[pos..end]);
178 if dict.len() >= dict_size {
179 break;
180 }
181 }
182 dict.truncate(dict_size);
183 dict
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 fn json_msg(i: u32) -> Vec<u8> {
191 alloc::format!(
192 r#"{{"ts":"2026-04-27T12:00:00.{i:04}Z","level":"INFO","service":"api-gw","trace":"{i:08x}","method":"GET","path":"/v1/users/{i:04}","status":200,"latency_ms":{lat},"region":"us-east-1"}}"#,
193 i = i,
194 lat = 10 + i % 490,
195 )
196 .into_bytes()
197 }
198
199 #[test]
200 fn train_produces_nonempty_dict() {
201 let mut trainer = DictTrainer::new(2048);
202 for i in 0..100 {
203 trainer.add_sample(&json_msg(i));
204 }
205 let dict = trainer.train();
206 assert!(!dict.is_empty(), "dict should not be empty");
207 assert!(dict.len() <= 2048, "dict should respect max size");
208 }
209
210 #[test]
211 fn skips_too_short() {
212 let mut trainer = DictTrainer::new(2048);
213 trainer.add_sample(b"hi");
214 assert_eq!(trainer.sample_count(), 0);
215 }
216
217 #[test]
218 fn skips_too_long() {
219 let mut trainer = DictTrainer::new(64);
220 trainer.add_sample(&[0u8; 100]);
221 assert_eq!(trainer.sample_count(), 0);
222 }
223
224 #[test]
225 fn evicts_old_samples() {
226 let mut trainer = DictTrainer::new(2048);
227 for i in 0..200 {
228 trainer.add_sample(&json_msg(i));
229 }
230 assert!(trainer.total_bytes() <= 2048 * 8);
231 assert!(trainer.sample_count() < 200);
232 }
233
234 #[test]
235 fn too_few_samples_returns_empty() {
236 let mut trainer = DictTrainer::new(2048);
237 trainer.add_sample(b"hello world");
238 assert!(trainer.train().is_empty());
239 }
240}