concinnity_host/store/cache/mod.rs
1//! The runtime cache segment: regenerable artifacts the running application
2//! writes for its own later launches, all of them in `cache/0`.
3//!
4//! One file per writer role is the rule the layout is built on. The application
5//! writes this segment and nothing else, a build writes its own, so a build
6//! running against a live editor never touches the file the editor is writing.
7//! Within the segment an index keyed by producer and key separates the entries,
8//! which is what lets two adapters of one producer share a file, and the shader
9//! cache share it with the driver pipeline blobs.
10//!
11//! A lookup has a second tier behind it: the segment a bundle ships, which `cn
12//! export` warms so a player's first launch does not pay the compile. It
13//! resolves against the content root rather than the writable one, so the two
14//! are one file except on an install that cannot write beside its data --
15//! which is the whole reason the tier exists.
16//!
17//! The file is touched twice: once when the first lookup reads it, and once per
18//! [`flush`]. The bundled tier is read once and never written. Everything
19//! between is memory, so a producer that stores in a loop costs one write
20//! rather than one per entry. A crash before a flush costs the recompute of
21//! whatever had not been written, which is the same price deleting `cache/`
22//! already carries.
23//!
24//! Every operation is best-effort: a miss, an unreadable segment, or a failed
25//! write leaves the caller to produce the artifact the slow way. Two
26//! applications running against one checkout do share this file, and the later
27//! flush wins; what the loser had cached is recomputed on its next launch.
28//!
29//! The container format is `concinnity_core::blob`, which is I/O-free; the file
30//! reads and writes live in `segment`.
31
32mod segment;
33
34use std::path::PathBuf;
35use std::sync::{Mutex, MutexGuard};
36
37pub use concinnity_core::blob::CacheEntryKind;
38pub use segment::Segment;
39
40use super::paths;
41
42/// How much payload the segment may hold. Every shader edit orphans the
43/// artifact it replaces and neither driver pipeline blob evicts internally, so
44/// a long-lived checkout would otherwise accumulate forever. Generous next to
45/// the ~100 live entries one build needs; [`flush`] evicts down to it.
46pub const CACHE_BUDGET_BYTES: u64 = 64 * 1024 * 1024;
47
48/// The bytes `kind` stored under `key`, or `None` when there is no such entry
49/// (or no state root to resolve the segment against).
50pub fn load(kind: CacheEntryKind, key: &str) -> Option<Vec<u8>> {
51 with(|segment| segment.get(kind, key).map(<[u8]>::to_vec)).flatten()
52}
53
54/// The same lookup against the read-only segment a bundle ships, for a caller
55/// [`load`] missed. Read once like the writable one, so a run that consults it
56/// fifty times reads the file once.
57///
58/// Reports a miss when the bundle is one the application can write to: both
59/// roles then name one file, the writable tier already holds its entries, and
60/// answering from a second copy would only risk [`flush`] writing back a view
61/// that was never the shipped one.
62pub fn load_bundled(kind: CacheEntryKind, key: &str) -> Option<Vec<u8>> {
63 bundled(|segment| segment.get(kind, key).map(<[u8]>::to_vec)).flatten()
64}
65
66/// Hold `bytes` under `key` until the next [`flush`], reporting whether the
67/// segment took them: an entry already holding at least as many bytes is left
68/// alone, so a driver blob whose serialization only reshuffles does not make
69/// the flush rewrite the file.
70pub fn store(kind: CacheEntryKind, key: &str, bytes: &[u8]) -> bool {
71 with(|segment| segment.put(kind, key, bytes)).unwrap_or(false)
72}
73
74/// Drop `key`'s entry, for a caller whose artifact turned out unusable.
75pub fn delete(kind: CacheEntryKind, key: &str) {
76 with(|segment| segment.remove(kind, key));
77}
78
79/// Adopt `id` as the host shader toolchain the segment's entries were produced
80/// by, discarding every entry when the segment names another one. Reports
81/// whether it discarded, which the caller logs.
82///
83/// An artifact is a function of its source, not of what compiled it, so an
84/// external compiler upgrade (or one shadowed by another install earlier on
85/// PATH) moves no key: without this its predecessor's output would be replayed
86/// forever.
87pub fn verify_toolchain(id: &str) -> bool {
88 with(|segment| segment.adopt_toolchain(id)).unwrap_or(false)
89}
90
91/// Write the segment to disk, if anything changed it since it was read, and
92/// report whether the file was written. Called when the work producing entries
93/// finishes -- the end of a renderer init, a clean shutdown -- never per entry.
94pub fn flush() -> bool {
95 match lock().as_mut() {
96 Some(loaded) => loaded.segment.write_to(&loaded.path, CACHE_BUDGET_BYTES),
97 None => false,
98 }
99}
100
101// The segment this process read, and the file it came from.
102struct Loaded {
103 path: PathBuf,
104 segment: Segment,
105}
106
107// Run `f` against the loaded segment, reading the file on the first call.
108// `None` when no host installed a state root, which turns every operation above
109// into a miss: artifacts are produced fresh rather than warmed from disk.
110fn with<R>(f: impl FnOnce(&mut Segment) -> R) -> Option<R> {
111 let path = paths::runtime_cache_path()?;
112 let mut held = lock();
113 if held.as_ref().is_some_and(|loaded| loaded.path != path)
114 && let Some(mut previous) = held.take()
115 {
116 // A host moved the writable state root after this segment was read (a
117 // world's own `home` overriding the launcher's). What it holds belongs
118 // to the old root, so write it back there before reading the new one.
119 previous
120 .segment
121 .write_to(&previous.path, CACHE_BUDGET_BYTES);
122 }
123 let loaded = held.get_or_insert_with(|| Loaded {
124 segment: Segment::read_from(&path),
125 path,
126 });
127 Some(f(&mut loaded.segment))
128}
129
130// Serializes this process's access to the one segment it holds. Lookups take it
131// too: a lookup marks the entry it found as one this run needs, so eviction
132// spares it.
133fn lock() -> MutexGuard<'static, Option<Loaded>> {
134 static LOADED: Mutex<Option<Loaded>> = Mutex::new(None);
135 LOADED.lock().unwrap_or_else(|e| e.into_inner())
136}
137
138// Run `f` against the bundled segment, reading the file on the first call.
139// Nothing writes this tier, so a root move just drops what was read rather
140// than writing it back.
141fn bundled<R>(f: impl FnOnce(&mut Segment) -> R) -> Option<R> {
142 let path = shipped_path(
143 paths::bundled_runtime_cache_path()?,
144 paths::runtime_cache_path(),
145 )?;
146 static LOADED: Mutex<Option<Loaded>> = Mutex::new(None);
147 let mut held = LOADED.lock().unwrap_or_else(|e| e.into_inner());
148 if held.as_ref().is_some_and(|loaded| loaded.path != path) {
149 *held = None;
150 }
151 let loaded = held.get_or_insert_with(|| Loaded {
152 segment: Segment::read_from(&path),
153 path,
154 });
155 Some(f(&mut loaded.segment))
156}
157
158// `bundled` unless the application writes that same file, in which case the
159// writable tier is already serving its entries and reading a second copy would
160// only put a stale view in front of what `flush` writes back.
161fn shipped_path(shipped: PathBuf, writable: Option<PathBuf>) -> Option<PathBuf> {
162 (writable.as_deref() != Some(shipped.as_path())).then_some(shipped)
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 // The portable-folder case: one file in both roles, so only the writable
170 // tier reads it and the shipped entries ride its flush back to disk.
171 #[test]
172 fn a_writable_bundle_has_no_second_tier() {
173 let one = PathBuf::from("/bundle/cache/0");
174 assert_eq!(shipped_path(one.clone(), Some(one.clone())), None);
175 }
176
177 // A read-only install: the two roots diverge, so the shipped segment is a
178 // tier of its own.
179 #[test]
180 fn a_read_only_install_reads_the_shipped_segment() {
181 let shipped = PathBuf::from("/opt/app/cache/0");
182 let writable = PathBuf::from("/home/u/.local/share/app/cache/0");
183 assert_eq!(
184 shipped_path(shipped.clone(), Some(writable)),
185 Some(shipped.clone())
186 );
187 // No writable root at all leaves the shipped one readable.
188 assert_eq!(shipped_path(shipped.clone(), None), Some(shipped));
189 }
190}