Skip to main content

vyre_libs/parsing/c/preprocess/gpu_pipeline/
header_reuse.rs

1//! Parallel header-analysis reuse keyed by path, flags, defines, and target triple.
2
3use 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/// Header-analysis cache key.
12#[derive(Debug, Clone, Hash, PartialEq, Eq)]
13pub struct HeaderReuseKey {
14    /// Canonical header path.
15    pub path: PathBuf,
16    /// Header source hash.
17    pub source_hash: [u8; 16],
18    /// Live macro-definition hash at the include site.
19    pub defines_hash: [u8; 16],
20    /// Compiler-flag hash.
21    pub flags_hash: [u8; 16],
22    /// Target triple.
23    pub target_triple: String,
24}
25
26/// Header reuse evidence.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct HeaderReuseEvent {
29    /// Header path.
30    pub path: PathBuf,
31    /// Target triple in the cache key.
32    pub target_triple: String,
33    /// Whether cache lookup hit.
34    pub hit: bool,
35    /// Whether this event stored a freshly computed entry.
36    pub stored: bool,
37    /// Whether GPU-derived header analysis was reused.
38    pub gpu_analysis_reused: bool,
39    /// Live defines hash used for invalidation.
40    pub defines_hash: [u8; 16],
41    /// Compiler flags hash used for invalidation.
42    pub flags_hash: [u8; 16],
43    /// Header source hash used for invalidation.
44    pub source_hash: [u8; 16],
45}
46
47/// Cached GPU-derived header analysis.
48#[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}