Files
lightcraft/crates/preview/src/hash.rs
T
Brandon ThomasandClaude Opus 5.5 bcecab7d84 M1.5: thumbnail/preview cache (disk + memory LRU, prioritized job pool)
- new crate lightcraft-preview (L3): 128-bit content hashing (SipHash-1-3),
  cost-bounded Lru, DiskCache (JPEG q90 files keyed by content hash +
  settings hash + size + RENDER_CACHE_VERSION; temp+rename writes; LRU prune
  by mtime to 80% of budget; corrupt files dropped), PreviewCache (memory
  LRU over disk), JobPool (per-slot dedupe, priorities, reprioritize/drop,
  inline mode for wasm).
- engine: Session::thumb_job buckets thumbnails to 128/256/384/512 and serves
  them from the cache without decoding the source; decoded thumbnail
  sources now in a 384 MB LRU (was unbounded); library attaches
  <library>/thumbs; library.info cache stats; library.clearPreviews.
- ui-egui: Renderer runs on JobPool; thumbnails scrolled out of the
  prefetch area are dropped from the queue; on-screen thumbnails get
  re-prioritized; ui.inspect reports in-flight renders + thumb textures.
- Demo library (24 photos): grid fully drawn 4.4 s cold -> 0.21 s warm
  (24/24 disk hits, no source decoded).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:51:47 -04:00

62 lines
1.7 KiB
Rust

//! 128-bit content hashing (SipHash-1-3, fixed key). Not cryptographic — it identifies files
//! for duplicate detection and keys caches; collisions are astronomically unlikely by accident.
use std::fmt;
use std::hash::Hasher;
use siphasher::sip128::{Hasher128 as _, SipHasher13};
/// A 128-bit hash, shown as 32 hex digits.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Hash128(pub u128);
impl fmt::Display for Hash128 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:032x}", self.0)
}
}
impl Hash128 {
pub fn parse(s: &str) -> Option<Hash128> {
(s.len() == 32).then(|| u128::from_str_radix(s, 16).ok().map(Hash128)).flatten()
}
}
/// Streaming hasher.
#[derive(Clone, Debug)]
pub struct Hasher128(SipHasher13);
impl Default for Hasher128 {
fn default() -> Self {
Hasher128(SipHasher13::new_with_keys(0x4c69_6768_7443_7261, 0x6674_2043_6f6e_7465))
}
}
impl Hasher128 {
pub fn new() -> Self {
Self::default()
}
pub fn update(&mut self, bytes: &[u8]) -> &mut Self {
self.0.write(bytes);
self
}
/// Length-prefixed string (so `("ab","c")` and `("a","bc")` differ).
pub fn str(&mut self, s: &str) -> &mut Self {
self.0.write(&(s.len() as u64).to_le_bytes());
self.0.write(s.as_bytes());
self
}
pub fn u64(&mut self, v: u64) -> &mut Self {
self.0.write(&v.to_le_bytes());
self
}
pub fn finish(&self) -> Hash128 {
Hash128(self.0.finish128().as_u128())
}
}
/// Hash of a whole byte string.
pub fn hash_bytes(bytes: &[u8]) -> Hash128 {
Hasher128::new().update(bytes).finish()
}