Skip to main content

concinnity_host/store/cache/
segment.rs

1// One cache segment, held in memory between its two moments of file I/O: the
2// read that loads it and the write that replaces it.
3//
4// A container is written whole while a cache accumulates, so the segment is
5// read once, every lookup and every store lands in the `Segment` below, and the
6// file is replaced at a flush. A producer that stores in a loop -- a renderer
7// init compiling fifty shaders -- therefore costs one read and one write rather
8// than fifty of each, and the read it does costs less than the first compile it
9// saves.
10//
11// Every function here takes the path, so the segment machinery is exercised
12// without the process-global state root; `super` resolves which file a caller
13// means.
14
15use std::fs;
16use std::io::Write;
17use std::path::Path;
18
19use concinnity_core::blob::{
20    CACHE_SEGMENT_VERSION, CacheEntry, CacheEntryKind, CacheMeta, encode_cnb, parse_cnb,
21};
22
23// One entry held in memory. `used` marks an entry this process looked up or
24// wrote, which eviction spares.
25struct Item {
26    kind: CacheEntryKind,
27    key: String,
28    bytes: Vec<u8>,
29    used: bool,
30}
31
32/// A segment's entries in memory, plus whether they differ from the file they
33/// were read from.
34///
35/// `super` holds one of these per process for the segment this application
36/// writes, and one for the read-only segment a bundle ships. `cn export` owns
37/// a third directly: the segment it warms into a bundle is one this process
38/// never reads back, so it is built and written as a value rather than through
39/// the process-global tiers.
40pub struct Segment {
41    items: Vec<Item>,
42    toolchain: String,
43    dirty: bool,
44}
45
46impl Segment {
47    /// Read `path` into memory. An absent, unreadable, or foreign file reads as
48    /// an empty segment: whatever it held is regenerated, and the next write
49    /// replaces it.
50    pub fn read_from(path: &Path) -> Self {
51        let Ok(image) = fs::read(path) else {
52            return Self::empty();
53        };
54        let Ok((meta, payload_start)) = parse_cnb::<CacheMeta>(CACHE_SEGMENT_VERSION, &image)
55        else {
56            return Self::empty();
57        };
58        Self {
59            items: meta
60                .entries
61                .iter()
62                .filter_map(|entry| {
63                    Some(Item {
64                        kind: entry.kind,
65                        key: entry.key.clone(),
66                        bytes: entry_bytes(&image, payload_start, entry)?.to_vec(),
67                        used: false,
68                    })
69                })
70                .collect(),
71            toolchain: meta.toolchain,
72            dirty: false,
73        }
74    }
75
76    fn empty() -> Self {
77        Self {
78            items: Vec::new(),
79            toolchain: String::new(),
80            dirty: false,
81        }
82    }
83
84    /// The bytes stored for `key`, or `None` when the segment holds no such
85    /// entry. Marks the entry used, so a flush does not evict what this run is
86    /// running on.
87    pub fn get(&mut self, kind: CacheEntryKind, key: &str) -> Option<&[u8]> {
88        let item = self
89            .items
90            .iter_mut()
91            .find(|i| i.kind == kind && i.key == key)?;
92        item.used = true;
93        Some(&item.bytes)
94    }
95
96    /// Take `bytes` as `key`'s entry, reporting whether the segment took them.
97    ///
98    /// An entry already holding at least as many bytes is left alone. Growth is
99    /// the only reliable "new content" signal for a driver pipeline blob, which
100    /// only accumulates but does not serialize deterministically (MoltenVK
101    /// shuffles entry order run to run), so a byte compare would dirty the
102    /// segment every launch. A content-addressed artifact re-stored under its
103    /// own key is by definition the bytes already there.
104    pub fn put(&mut self, kind: CacheEntryKind, key: &str, bytes: &[u8]) -> bool {
105        if bytes.is_empty() {
106            return false;
107        }
108        match self
109            .items
110            .iter_mut()
111            .find(|i| i.kind == kind && i.key == key)
112        {
113            Some(item) if bytes.len() <= item.bytes.len() => {
114                item.used = true;
115                return false;
116            }
117            Some(item) => {
118                item.bytes.clear();
119                item.bytes.extend_from_slice(bytes);
120                item.used = true;
121            }
122            None => self.items.push(Item {
123                kind,
124                key: key.to_owned(),
125                bytes: bytes.to_vec(),
126                used: true,
127            }),
128        }
129        self.dirty = true;
130        true
131    }
132
133    /// Drop `key`'s entry, for a caller whose artifact turned out unusable.
134    pub(super) fn remove(&mut self, kind: CacheEntryKind, key: &str) {
135        let before = self.items.len();
136        self.items.retain(|i| !(i.kind == kind && i.key == key));
137        self.dirty |= self.items.len() != before;
138    }
139
140    /// Adopt `id` as the toolchain the segment's entries were produced by,
141    /// discarding every entry when it names another one. Reports whether it
142    /// discarded.
143    ///
144    /// An unstamped segment predates any entry a toolchain produced -- a
145    /// shader compile stamps the segment before it stores -- so adopting the
146    /// first stamp keeps what is already there.
147    pub(super) fn adopt_toolchain(&mut self, id: &str) -> bool {
148        if self.toolchain == id {
149            return false;
150        }
151        let discarded = !self.toolchain.is_empty() && !self.items.is_empty();
152        if discarded {
153            self.items.clear();
154        }
155        self.toolchain = id.to_owned();
156        self.dirty = true;
157        discarded
158    }
159
160    /// Replace `path` with what the segment now holds, first evicting entries
161    /// until its payload fits `budget`. Reports whether the file was written.
162    ///
163    /// A segment nothing changed is not rewritten: this is where the per-store
164    /// growth check pays off, since a launch that only read the cache leaves
165    /// the file untouched.
166    pub fn write_to(&mut self, path: &Path, budget: u64) -> bool {
167        if !self.dirty {
168            return false;
169        }
170        self.evict_to(budget);
171        if self.items.is_empty() {
172            // A cache nothing needs leaves nothing behind. The stamp goes with
173            // it, which costs the next run a discard of an empty segment.
174            let _ = fs::remove_file(path);
175            self.dirty = false;
176            return false;
177        }
178        let mut payload = Vec::with_capacity(self.items.iter().map(|i| i.bytes.len()).sum());
179        let entries = self
180            .items
181            .iter()
182            .map(|item| {
183                let entry = CacheEntry {
184                    kind: item.kind,
185                    key: item.key.clone(),
186                    offset: payload.len() as u64,
187                    len: item.bytes.len() as u64,
188                };
189                payload.extend_from_slice(&item.bytes);
190                entry
191            })
192            .collect();
193        let meta = CacheMeta {
194            toolchain: self.toolchain.clone(),
195            entries,
196        };
197        let Ok(image) = encode_cnb(CACHE_SEGMENT_VERSION, &meta, &payload) else {
198            return false;
199        };
200        if !crate::store::atomic::replace(path, |out| out.write_all(&image)) {
201            return false;
202        }
203        self.dirty = false;
204        true
205    }
206
207    // Drop entries oldest-first until the payload fits `budget`, sparing the
208    // ones this process used: evicting an artifact the live run is holding
209    // guarantees the next launch recompiles it. Content-addressed entries are
210    // interchangeable, so index order -- the order they were first stored in --
211    // is the same least-recently-written proxy the directory sweep had in mtimes.
212    fn evict_to(&mut self, budget: u64) {
213        let mut total: u64 = self.items.iter().map(|i| i.bytes.len() as u64).sum();
214        if total <= budget {
215            return;
216        }
217        let before = self.items.len();
218        self.items.retain(|item| {
219            if total <= budget || item.used {
220                return true;
221            }
222            total -= item.bytes.len() as u64;
223            false
224        });
225        self.dirty |= self.items.len() != before;
226    }
227}
228
229// An entry's slice of the payload section, or `None` when the index points past
230// the image (a truncated or hand-edited segment).
231fn entry_bytes<'a>(image: &'a [u8], payload_start: usize, entry: &CacheEntry) -> Option<&'a [u8]> {
232    let start = payload_start.checked_add(usize::try_from(entry.offset).ok()?)?;
233    let end = start.checked_add(usize::try_from(entry.len).ok()?)?;
234    image.get(start..end)
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240
241    const PIPELINE: CacheEntryKind = CacheEntryKind::Pipeline;
242    const SHADER: CacheEntryKind = CacheEntryKind::Shader;
243    const BUDGET: u64 = 1024;
244
245    fn segment_path(dir: &tempfile::TempDir) -> std::path::PathBuf {
246        dir.path().join("cache").join("0")
247    }
248
249    // Store into a fresh segment and write it, the shape every test below
250    // starts from.
251    fn written(path: &Path, entries: &[(CacheEntryKind, &str, &[u8])]) -> Segment {
252        let mut segment = Segment::read_from(path);
253        for (kind, key, bytes) in entries {
254            segment.put(*kind, key, bytes);
255        }
256        segment.write_to(path, BUDGET);
257        segment
258    }
259
260    #[test]
261    fn an_entry_round_trips_through_a_segment() {
262        let dir = tempfile::tempdir().unwrap();
263        let path = segment_path(&dir);
264        written(&path, &[(PIPELINE, "vk-aa", &[1, 2, 3])]);
265
266        let mut reread = Segment::read_from(&path);
267        assert_eq!(reread.get(PIPELINE, "vk-aa"), Some(&[1, 2, 3][..]));
268
269        let leftovers = fs::read_dir(path.parent().unwrap())
270            .unwrap()
271            .flatten()
272            .filter(|e| e.path().extension().is_some_and(|x| x == "tmp"))
273            .count();
274        assert_eq!(leftovers, 0, "temp files must not survive a write");
275    }
276
277    // The reason the index exists: two adapters used in one run share the
278    // segment, and the second store must not take the first's bytes with it.
279    // Two producers share it the same way, which is what admits the shader cache.
280    #[test]
281    fn one_entry_does_not_clobber_another() {
282        let dir = tempfile::tempdir().unwrap();
283        let path = segment_path(&dir);
284        written(
285            &path,
286            &[
287                (PIPELINE, "vk-aa", &[1, 2, 3]),
288                (PIPELINE, "vk-bb", &[9]),
289                (SHADER, "vk-aa", &[4, 4]),
290            ],
291        );
292
293        let mut reread = Segment::read_from(&path);
294        assert_eq!(reread.get(PIPELINE, "vk-aa"), Some(&[1, 2, 3][..]));
295        assert_eq!(reread.get(PIPELINE, "vk-bb"), Some(&[9][..]));
296        assert_eq!(reread.get(SHADER, "vk-aa"), Some(&[4, 4][..]));
297
298        // A rewrite of one entry leaves the others intact and readdressed.
299        reread.put(PIPELINE, "vk-aa", &[4, 5, 6, 7]);
300        reread.write_to(&path, BUDGET);
301        let mut last = Segment::read_from(&path);
302        assert_eq!(last.get(PIPELINE, "vk-aa"), Some(&[4, 5, 6, 7][..]));
303        assert_eq!(last.get(PIPELINE, "vk-bb"), Some(&[9][..]));
304        assert_eq!(last.get(SHADER, "vk-aa"), Some(&[4, 4][..]));
305    }
306
307    // The whole point of holding the segment in memory: a run that only reads
308    // it touches the file once, and never writes it.
309    #[test]
310    fn a_segment_nothing_changed_is_not_rewritten() {
311        let dir = tempfile::tempdir().unwrap();
312        let path = segment_path(&dir);
313        written(&path, &[(SHADER, "cafe", &[1, 2, 3])]);
314        let before = fs::metadata(&path).unwrap().len();
315
316        let mut warm = Segment::read_from(&path);
317        assert_eq!(warm.get(SHADER, "cafe"), Some(&[1, 2, 3][..]));
318        assert!(
319            !warm.write_to(&path, BUDGET),
320            "a read-only run writes nothing"
321        );
322        assert_eq!(fs::metadata(&path).unwrap().len(), before);
323    }
324
325    // A driver blob's serialization is nondeterministic, so equal-length bytes
326    // must leave the segment alone; only growth is new content.
327    #[test]
328    fn only_growth_replaces_an_entry() {
329        let dir = tempfile::tempdir().unwrap();
330        let path = segment_path(&dir);
331        let mut segment = written(&path, &[(PIPELINE, "vk-aa", &[5, 5])]);
332
333        assert!(!segment.put(PIPELINE, "vk-aa", &[5, 5]), "unchanged");
334        assert!(!segment.put(PIPELINE, "vk-aa", &[6, 5]), "reshuffled");
335        assert!(!segment.put(PIPELINE, "vk-aa", &[5]), "shrunk");
336        assert!(!segment.put(PIPELINE, "vk-bb", &[]), "empty");
337        assert!(!segment.write_to(&path, BUDGET), "none of those is content");
338
339        assert!(segment.put(PIPELINE, "vk-aa", &[5, 5, 6]), "grew");
340        assert!(segment.write_to(&path, BUDGET));
341        let mut reread = Segment::read_from(&path);
342        assert_eq!(reread.get(PIPELINE, "vk-aa"), Some(&[5, 5, 6][..]));
343    }
344
345    // Deleting `cache/` at any time has to leave the app working, so an absent
346    // segment reads as a miss rather than an error, and the next write recreates
347    // the file and the directory under it.
348    #[test]
349    fn an_absent_segment_reads_as_a_miss_and_is_recreated() {
350        let dir = tempfile::tempdir().unwrap();
351        let path = segment_path(&dir);
352        assert!(!path.exists());
353        assert_eq!(Segment::read_from(&path).get(PIPELINE, "vk-aa"), None);
354
355        written(&path, &[(PIPELINE, "vk-aa", &[1])]);
356        fs::remove_dir_all(path.parent().unwrap()).unwrap();
357        assert_eq!(Segment::read_from(&path).get(PIPELINE, "vk-aa"), None);
358        written(&path, &[(PIPELINE, "vk-aa", &[1])]);
359        assert_eq!(
360            Segment::read_from(&path).get(PIPELINE, "vk-aa"),
361            Some(&[1][..])
362        );
363    }
364
365    #[test]
366    fn a_missing_entry_reads_as_a_miss() {
367        let dir = tempfile::tempdir().unwrap();
368        let path = segment_path(&dir);
369        let mut segment = written(&path, &[(PIPELINE, "vk-aa", &[1])]);
370        assert_eq!(segment.get(PIPELINE, "vk-bb"), None);
371    }
372
373    #[test]
374    fn a_removed_entry_is_gone_and_the_last_one_takes_the_file() {
375        let dir = tempfile::tempdir().unwrap();
376        let path = segment_path(&dir);
377        let mut segment = written(
378            &path,
379            &[(PIPELINE, "vk-aa", &[1]), (PIPELINE, "vk-bb", &[2])],
380        );
381
382        segment.remove(PIPELINE, "vk-aa");
383        assert!(segment.write_to(&path, BUDGET));
384        let mut reread = Segment::read_from(&path);
385        assert_eq!(reread.get(PIPELINE, "vk-aa"), None);
386        assert_eq!(reread.get(PIPELINE, "vk-bb"), Some(&[2][..]));
387
388        reread.remove(PIPELINE, "vk-bb");
389        assert!(!reread.write_to(&path, BUDGET));
390        assert!(!path.exists(), "an empty segment leaves no file behind");
391        // Removing what is not there is a no-op, not a rewrite.
392        reread.remove(PIPELINE, "vk-bb");
393        assert!(!reread.write_to(&path, BUDGET));
394        assert!(!path.exists());
395    }
396
397    // Garbage in the segment's place (a truncated write from an older layout,
398    // a file that was never a segment) costs a regeneration, never a failure.
399    #[test]
400    fn a_corrupt_segment_reads_empty_and_is_replaced() {
401        let dir = tempfile::tempdir().unwrap();
402        let path = segment_path(&dir);
403        fs::create_dir_all(path.parent().unwrap()).unwrap();
404        fs::write(&path, b"not a segment at all").unwrap();
405        assert_eq!(Segment::read_from(&path).get(PIPELINE, "vk-aa"), None);
406
407        written(&path, &[(PIPELINE, "vk-aa", &[7])]);
408        assert_eq!(
409            Segment::read_from(&path).get(PIPELINE, "vk-aa"),
410            Some(&[7][..])
411        );
412    }
413
414    // The index can outlive the payload it addresses if a write was truncated
415    // by something outside the rename. Such an entry reads as absent.
416    #[test]
417    fn an_entry_pointing_past_the_image_reads_as_a_miss() {
418        let dir = tempfile::tempdir().unwrap();
419        let path = segment_path(&dir);
420        written(&path, &[(PIPELINE, "vk-aa", &[1, 2, 3])]);
421        let image = fs::read(&path).unwrap();
422        fs::write(&path, &image[..image.len() - 1]).unwrap();
423        assert_eq!(Segment::read_from(&path).get(PIPELINE, "vk-aa"), None);
424    }
425
426    // The runtime writes its own segment and nothing else: a build segment
427    // beside it is neither read nor disturbed, whether or not it exists.
428    #[test]
429    fn a_sibling_segment_is_left_alone() {
430        let dir = tempfile::tempdir().unwrap();
431        let path = segment_path(&dir);
432        let sibling = path.parent().unwrap().join("1");
433        fs::create_dir_all(path.parent().unwrap()).unwrap();
434        fs::write(&sibling, b"build segment").unwrap();
435
436        let mut segment = written(&path, &[(PIPELINE, "vk-aa", &[1])]);
437        segment.remove(PIPELINE, "vk-aa");
438        segment.write_to(&path, BUDGET);
439        assert!(!path.exists());
440        assert_eq!(fs::read(&sibling).unwrap(), b"build segment");
441    }
442
443    // The stamp rides the segment rather than a sidecar file, and a host
444    // compiler upgrade is what it exists to catch.
445    #[test]
446    fn a_toolchain_change_discards_the_entries() {
447        let dir = tempfile::tempdir().unwrap();
448        let path = segment_path(&dir);
449        let mut segment = written(&path, &[(SHADER, "cafe", &[1, 2])]);
450        // The first stamp claims what is there rather than discarding it: a
451        // compile stamps the segment before it stores, so an unstamped entry
452        // came from no toolchain this could disagree with.
453        assert!(!segment.adopt_toolchain("slang 2026.1"));
454        assert!(segment.write_to(&path, BUDGET), "the stamp is a change");
455
456        // The same toolchain keeps every entry and dirties nothing.
457        let mut warm = Segment::read_from(&path);
458        assert!(!warm.adopt_toolchain("slang 2026.1"));
459        assert_eq!(warm.get(SHADER, "cafe"), Some(&[1, 2][..]));
460        assert!(!warm.write_to(&path, BUDGET));
461
462        // Another one drops what it did not produce, and the drop reaches disk.
463        let mut upgraded = Segment::read_from(&path);
464        assert!(upgraded.adopt_toolchain("slang 2026.2"), "discarded");
465        assert_eq!(upgraded.get(SHADER, "cafe"), None);
466        upgraded.put(SHADER, "f00d", &[3]);
467        upgraded.write_to(&path, BUDGET);
468        let mut reread = Segment::read_from(&path);
469        assert_eq!(reread.get(SHADER, "cafe"), None);
470        assert_eq!(reread.get(SHADER, "f00d"), Some(&[3][..]));
471        assert!(!reread.adopt_toolchain("slang 2026.2"), "stamp persisted");
472    }
473
474    #[test]
475    fn nothing_is_evicted_under_budget() {
476        let dir = tempfile::tempdir().unwrap();
477        let path = segment_path(&dir);
478        let mut segment = written(&path, &[(SHADER, "a", &[0; 8]), (SHADER, "b", &[0; 8])]);
479        segment.evict_to(64);
480        assert!(segment.get(SHADER, "a").is_some());
481        assert!(segment.get(SHADER, "b").is_some());
482    }
483
484    // Eviction takes the oldest entries first, and spares whatever this run
485    // touched: dropping an artifact the live process is running on would only
486    // buy a recompile on the next launch.
487    #[test]
488    fn eviction_drops_oldest_first_and_spares_what_this_run_used() {
489        let dir = tempfile::tempdir().unwrap();
490        let path = segment_path(&dir);
491        written(
492            &path,
493            &[
494                (SHADER, "oldest", &[0; 40]),
495                (SHADER, "middle", &[0; 40]),
496                (SHADER, "newest", &[0; 40]),
497            ],
498        );
499
500        let mut warm = Segment::read_from(&path);
501        assert!(warm.get(SHADER, "oldest").is_some(), "this run needs it");
502        warm.evict_to(80);
503        assert!(warm.get(SHADER, "oldest").is_some());
504        assert!(warm.get(SHADER, "middle").is_none());
505        assert!(warm.get(SHADER, "newest").is_some());
506
507        // An eviction is itself a change, so it reaches disk on the next flush.
508        assert!(warm.write_to(&path, 1024));
509        assert!(Segment::read_from(&path).get(SHADER, "middle").is_none());
510    }
511
512    // A budget nothing untouched can satisfy evicts what it can and keeps the
513    // rest, rather than throwing away the run's own artifacts.
514    #[test]
515    fn eviction_stops_at_the_entries_this_run_used() {
516        let dir = tempfile::tempdir().unwrap();
517        let path = segment_path(&dir);
518        let mut segment = written(&path, &[(SHADER, "a", &[0; 40]), (SHADER, "b", &[0; 40])]);
519        segment.evict_to(0);
520        assert!(segment.get(SHADER, "a").is_some());
521        assert!(segment.get(SHADER, "b").is_some());
522    }
523}