vyre_libs/parsing/c/preprocess/gpu_pipeline/
header_reuse.rs1use std::path::{Path, PathBuf};
4use std::sync::{Arc, Mutex, OnceLock};
5
6use super::byte_lru_cache::{ByteBoundLruCache, ByteLruPanicLabels};
7use super::classified_size::classified_tokens_bytes;
8use super::payload_size::directive_payloads_bytes;
9use super::{ClassifiedTokens, DirectivePayload, MacroDef};
10
11#[derive(Debug, Clone, Hash, PartialEq, Eq)]
13pub struct HeaderReuseKey {
14 pub path: PathBuf,
16 pub source_hash: [u8; 16],
18 pub defines_hash: [u8; 16],
20 pub flags_hash: [u8; 16],
22 pub target_triple: String,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct HeaderReuseEvent {
29 pub path: PathBuf,
31 pub target_triple: String,
33 pub hit: bool,
35 pub stored: bool,
37 pub gpu_analysis_reused: bool,
39 pub defines_hash: [u8; 16],
41 pub flags_hash: [u8; 16],
43 pub source_hash: [u8; 16],
45}
46
47#[derive(Debug, Clone)]
49pub(super) struct HeaderReuseEntry {
50 pub classified: Arc<ClassifiedTokens>,
51 pub payloads: Arc<[DirectivePayload]>,
52}
53
54const HEADER_REUSE_CACHE_MAX_ENTRIES: usize = 8192;
55const HEADER_REUSE_CACHE_MAX_BYTES: usize = 512 * 1024 * 1024;
56
57const HEADER_REUSE_CACHE_LABELS: ByteLruPanicLabels = ByteLruPanicLabels {
58 byte_add_overflow: "vyre-libs gpu preprocessor header reuse cache byte accounting overflowed during insert. Fix: lower header reuse cache limits or shard preprocessing sessions.",
59 byte_sub_underflow: "vyre-libs gpu preprocessor header reuse cache byte accounting underflowed during eviction. Fix: repair header reuse cache accounting before relying on memory limits.",
60 epoch_overflow: "vyre-libs gpu preprocessor header reuse cache epoch overflowed. Fix: recreate process-local header reuse cache before continuing an unbounded include stream.",
61};
62
63#[cfg(test)]
64pub(super) fn header_reuse_key(
65 path: &Path,
66 source: &[u8],
67 defines_hash: [u8; 16],
68) -> HeaderReuseKey {
69 header_reuse_key_from_hash(path, hash_bytes(source), defines_hash)
70}
71
72pub(super) fn header_reuse_key_from_hash(
73 path: &Path,
74 source_hash: [u8; 16],
75 defines_hash: [u8; 16],
76) -> HeaderReuseKey {
77 HeaderReuseKey {
78 path: path.to_path_buf(),
79 source_hash,
80 defines_hash,
81 flags_hash: header_flags_hash(),
82 target_triple: header_target_triple().to_string(),
83 }
84}
85
86pub(super) fn load_header_reuse(key: &HeaderReuseKey) -> Result<Option<HeaderReuseEntry>, String> {
87 header_cache()
88 .lock()
89 .map_err(|_| "vyre-libs::gpu_pipeline: header-analysis reuse cache poisoned".to_string())
90 .map(|mut cache| cache.lookup(key))
91}
92
93pub(super) fn store_header_reuse(
94 key: HeaderReuseKey,
95 entry: HeaderReuseEntry,
96) -> Result<(), String> {
97 let mut cache = header_cache().lock().map_err(|_| {
98 "vyre-libs::gpu_pipeline: header-analysis reuse cache poisoned while inserting".to_string()
99 })?;
100 cache.insert(key, entry);
101 Ok(())
102}
103
104pub(super) fn reuse_event(key: &HeaderReuseKey, hit: bool, stored: bool) -> HeaderReuseEvent {
105 HeaderReuseEvent {
106 path: key.path.clone(),
107 target_triple: key.target_triple.clone(),
108 hit,
109 stored,
110 gpu_analysis_reused: hit,
111 defines_hash: key.defines_hash,
112 flags_hash: key.flags_hash,
113 source_hash: key.source_hash,
114 }
115}
116
117struct HeaderReuseCache {
118 inner: ByteBoundLruCache<HeaderReuseKey, HeaderReuseEntry>,
119}
120
121impl HeaderReuseCache {
122 fn new() -> Self {
123 Self {
124 inner: ByteBoundLruCache::new(
125 HEADER_REUSE_CACHE_MAX_ENTRIES,
126 HEADER_REUSE_CACHE_MAX_BYTES,
127 HEADER_REUSE_CACHE_LABELS,
128 ),
129 }
130 }
131
132 #[cfg(test)]
133 fn with_limits(max_entries: usize, max_bytes: usize) -> Self {
134 Self {
135 inner: ByteBoundLruCache::new(max_entries, max_bytes, HEADER_REUSE_CACHE_LABELS),
136 }
137 }
138
139 fn lookup(&mut self, key: &HeaderReuseKey) -> Option<HeaderReuseEntry> {
140 self.inner.lookup_cloned(key)
141 }
142
143 fn insert(&mut self, key: HeaderReuseKey, value: HeaderReuseEntry) {
144 let entry_bytes = header_reuse_entry_bytes(&value);
145 self.inner.insert(key, value, entry_bytes);
146 }
147
148 #[cfg(test)]
149 fn len(&self) -> usize {
150 self.inner.len()
151 }
152
153 #[cfg(test)]
154 fn byte_len(&self) -> usize {
155 self.inner.byte_len()
156 }
157
158 #[cfg(test)]
159 fn contains_key(&self, key: &HeaderReuseKey) -> bool {
160 self.inner.contains_key(key)
161 }
162
163 #[cfg(test)]
164 fn lru_index_len(&self) -> usize {
165 self.inner.lru_index_len()
166 }
167}
168
169fn header_reuse_entry_bytes(entry: &HeaderReuseEntry) -> usize {
170 classified_tokens_bytes(&entry.classified)
171 .checked_add(directive_payloads_bytes(&entry.payloads))
172 .unwrap_or(usize::MAX)
173}
174
175fn header_cache() -> &'static Mutex<HeaderReuseCache> {
176 static CACHE: OnceLock<Mutex<HeaderReuseCache>> = OnceLock::new();
177 CACHE.get_or_init(|| Mutex::new(HeaderReuseCache::new()))
178}
179
180fn header_flags_hash() -> [u8; 16] {
181 static FLAGS_HASH: OnceLock<[u8; 16]> = OnceLock::new();
182 *FLAGS_HASH.get_or_init(|| {
183 let flags = std::env::var("VYRE_C_HEADER_CACHE_FLAGS").unwrap_or_default();
184 hash_bytes(flags.as_bytes())
185 })
186}
187
188fn header_target_triple() -> &'static str {
189 static TARGET_TRIPLE: OnceLock<String> = OnceLock::new();
190 TARGET_TRIPLE
191 .get_or_init(|| {
192 std::env::var("VYRE_TARGET_TRIPLE")
193 .unwrap_or_else(|_| "x86_64-unknown-linux-gnu".to_string())
194 })
195 .as_str()
196}
197
198pub(super) fn hash_defines(macros: &[MacroDef]) -> [u8; 16] {
199 let mut sorted = macros.iter().collect::<Vec<_>>();
200 sorted.sort_by(|a, b| {
201 a.name
202 .cmp(&b.name)
203 .then_with(|| a.args.cmp(&b.args))
204 .then_with(|| a.body.cmp(&b.body))
205 .then_with(|| a.is_function_like.cmp(&b.is_function_like))
206 });
207 let mut hasher = blake3::Hasher::new();
208 for mac in sorted {
209 update_len_bytes(&mut hasher, &mac.name);
210 update_len_bytes(&mut hasher, &mac.args);
211 update_len_bytes(&mut hasher, &mac.body);
212 hasher.update(&[u8::from(mac.is_function_like)]);
213 }
214 finish128(hasher)
215}
216
217fn update_len_bytes(hasher: &mut blake3::Hasher, bytes: &[u8]) {
218 hasher.update(&(bytes.len() as u64).to_le_bytes());
219 hasher.update(bytes);
220}
221
222fn hash_bytes(bytes: &[u8]) -> [u8; 16] {
223 let mut hasher = blake3::Hasher::new();
224 hasher.update(bytes);
225 finish128(hasher)
226}
227
228fn finish128(hasher: blake3::Hasher) -> [u8; 16] {
229 let digest = hasher.finalize();
230 let mut out = [0u8; 16];
231 out.copy_from_slice(&digest.as_bytes()[..16]);
232 out
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 fn macro_def(name: &[u8], args: &[u8], body: &[u8], is_function_like: bool) -> MacroDef {
240 MacroDef {
241 name: name.to_vec(),
242 args: args.to_vec(),
243 body: body.to_vec(),
244 is_function_like,
245 }
246 }
247
248 #[test]
249 fn defines_hash_is_order_independent_without_cloning_macro_bodies() {
250 let left = vec![
251 macro_def(b"B", b"x", b"((x)+1)", true),
252 macro_def(b"A", b"", b"1", false),
253 ];
254 let right = vec![
255 macro_def(b"A", b"", b"1", false),
256 macro_def(b"B", b"x", b"((x)+1)", true),
257 ];
258 assert_eq!(hash_defines(&left), hash_defines(&right));
259 }
260
261 #[test]
262 fn header_reuse_key_matches_prehashed_constructor() {
263 let path = Path::new("/tmp/header-reuse-direct.h");
264 let source = b"#define DIRECT 1\n";
265 let defines_hash = [7; 16];
266
267 assert_eq!(
268 header_reuse_key(path, source, defines_hash),
269 header_reuse_key_from_hash(path, hash_bytes(source), defines_hash)
270 );
271 }
272
273 fn key(id: u8) -> HeaderReuseKey {
274 HeaderReuseKey {
275 path: PathBuf::from(format!("/tmp/header-reuse-{id}.h")),
276 source_hash: [id; 16],
277 defines_hash: [0; 16],
278 flags_hash: [0; 16],
279 target_triple: "test-target".to_string(),
280 }
281 }
282
283 fn entry(id: u8, source_len: usize) -> HeaderReuseEntry {
284 HeaderReuseEntry {
285 classified: Arc::new(ClassifiedTokens {
286 tok_types: vec![id as u32],
287 tok_starts: vec![0],
288 tok_lens: vec![source_len as u32],
289 directive_kinds: vec![0],
290 directive_count: 0,
291 source: Arc::from(vec![id; source_len].into_boxed_slice()),
292 }),
293 payloads: Arc::from(vec![DirectivePayload::None].into_boxed_slice()),
294 }
295 }
296
297 #[test]
298 fn header_reuse_cache_rejects_entries_over_byte_budget() {
299 let mut cache = HeaderReuseCache::with_limits(8, 16);
300 cache.insert(key(1), entry(1, 64));
301 assert_eq!(cache.len(), 0);
302 assert_eq!(cache.byte_len(), 0);
303 }
304
305 #[test]
306 fn header_reuse_cache_evicts_lru_to_byte_budget() {
307 let a = key(1);
308 let b = key(2);
309 let c = key(3);
310 let a_entry = entry(1, 16);
311 let b_entry = entry(2, 16);
312 let c_entry = entry(3, 96);
313 let budget = header_reuse_entry_bytes(&a_entry)
314 .checked_add(header_reuse_entry_bytes(&c_entry))
315 .expect("Fix: test cache budget must fit usize");
316 let mut cache = HeaderReuseCache::with_limits(8, budget);
317 cache.insert(a.clone(), a_entry);
318 cache.insert(b.clone(), b_entry);
319 assert!(cache.lookup(&a).is_some());
320 cache.insert(c.clone(), c_entry);
321 assert!(cache.contains_key(&a));
322 assert!(!cache.contains_key(&b));
323 assert!(cache.contains_key(&c));
324 assert!(cache.byte_len() <= budget);
325 }
326
327 #[test]
328 fn header_reuse_cache_lru_index_stays_capacity_scale() {
329 let mut cache = HeaderReuseCache::with_limits(4, 1 << 20);
330
331 for id in 0..96u8 {
332 let key = key(id);
333 cache.insert(key.clone(), entry(id, 8));
334 assert!(cache.lookup(&key).is_some());
335 }
336
337 assert_eq!(cache.len(), 4);
338 assert!(
339 cache.lru_index_len() <= cache.len().saturating_mul(4).max(8),
340 "Fix: header reuse cache LRU index must compact stale touches to cache-capacity scale"
341 );
342 }
343}