Skip to main content

lora_store/
dict.rs

1//! Per-graph name dictionaries.
2//!
3//! A graph numbers its labels, relationship types and property keys in
4//! first-use order, so stored records and adjacency entries hold a small
5//! integer where they would repeat a name. Numbers are never reused: a
6//! name whose last user was deleted keeps its number.
7//!
8//! The numbers are private to one graph and its clones, and are not
9//! persisted: snapshots and the WAL write names.
10//!
11//! A fourth dictionary numbers short string *values* of the keys that
12//! repeat a handful of them (a source, a kind, a run id), so a record
13//! holds a number in their place (see [`ValueDict`]).
14
15use std::collections::HashMap;
16use std::hash::{BuildHasher, Hasher};
17use std::sync::{Arc, OnceLock};
18
19use crate::Name;
20
21/// Dictionaries up to this size are searched linearly: comparing a
22/// handful of short names beats hashing one.
23const LINEAR_LOOKUP_MAX: usize = 8;
24
25/// A random value per process, mixed into the starting state of the
26/// store's fast hashers ([`NameHasher`] and the index `KeyHasher`). They
27/// hash attacker-chosen strings (property keys, indexed values) with
28/// cheap non-cryptographic functions; with a fixed start, colliding
29/// inputs could be computed once and replayed against every process.
30/// Nothing hashed with it is persisted or ordered by its hash.
31#[inline]
32pub(crate) fn process_hash_seed() -> u64 {
33    static SEED: OnceLock<u64> = OnceLock::new();
34    *SEED.get_or_init(|| {
35        // `RandomState` draws its keys from the OS once per thread.
36        std::collections::hash_map::RandomState::new().hash_one(0x5eed_u64) | 1
37    })
38}
39
40/// FNV-1a from a per-process random basis. A property read hashes its
41/// key, so the hash has to be cheap. It is not collision-resistant, but
42/// the seed keeps collisions from being precomputed: someone who can
43/// write arbitrary property keys would have to find them per process.
44#[derive(Clone, Copy)]
45pub(crate) struct NameHasher(u64);
46
47/// Builds [`NameHasher`]s from the seeded basis, read once per table
48/// rather than once per hash.
49#[derive(Clone, Copy)]
50pub(crate) struct NameState(u64);
51
52impl Default for NameState {
53    #[inline]
54    fn default() -> Self {
55        Self(0xcbf2_9ce4_8422_2325 ^ process_hash_seed())
56    }
57}
58
59impl BuildHasher for NameState {
60    type Hasher = NameHasher;
61
62    #[inline]
63    fn build_hasher(&self) -> NameHasher {
64        NameHasher(self.0)
65    }
66}
67
68impl Hasher for NameHasher {
69    #[inline]
70    fn write(&mut self, bytes: &[u8]) {
71        for &b in bytes {
72            self.0 = (self.0 ^ u64::from(b)).wrapping_mul(0x0000_0100_0000_01b3);
73        }
74    }
75
76    #[inline]
77    fn finish(&self) -> u64 {
78        self.0
79    }
80}
81
82/// One dictionary: names numbered from zero.
83///
84/// Cloning shares the table; the first `id_or_insert` of a new name on
85/// either side copies it.
86#[derive(Clone, Default)]
87pub(crate) struct NameDict {
88    inner: Arc<Inner>,
89}
90
91#[derive(Clone, Default)]
92struct Inner {
93    names: Vec<Name>,
94    ids: HashMap<Name, u32, NameState>,
95}
96
97impl NameDict {
98    #[inline]
99    pub(crate) fn id_of(&self, name: &str) -> Option<u32> {
100        let inner = &*self.inner;
101        if inner.names.len() <= LINEAR_LOOKUP_MAX {
102            inner
103                .names
104                .iter()
105                .position(|n| n.as_str() == name)
106                .map(|i| i as u32)
107        } else {
108            inner.ids.get(name).copied()
109        }
110    }
111
112    /// The id for `name`, assigning the next one on first use.
113    pub(crate) fn id_or_insert(&mut self, name: &Name) -> u32 {
114        self.id_or_insert_with(name, || name.clone())
115    }
116
117    /// As [`Self::id_or_insert`] for a name held as a `&str`: `make` builds
118    /// the shared name, and only when it is new.
119    ///
120    /// A new name copies the table when a graph clone still shares it, so
121    /// the first write of each new name after a clone costs O(names).
122    pub(crate) fn id_or_insert_with(&mut self, name: &str, make: impl FnOnce() -> Name) -> u32 {
123        if let Some(id) = self.id_of(name) {
124            return id;
125        }
126        let name = make();
127        let inner = Arc::make_mut(&mut self.inner);
128        let id = inner.names.len() as u32;
129        inner.names.push(name.clone());
130        inner.ids.insert(name, id);
131        id
132    }
133
134    /// As [`Self::id_or_insert`], for a name not yet interned.
135    #[cfg(test)]
136    pub(crate) fn id_or_insert_str(&mut self, name: &str) -> u32 {
137        match self.id_of(name) {
138            Some(id) => id,
139            None => self.id_or_insert(&Name::new(name)),
140        }
141    }
142
143    /// The name numbered `id`. Ids come from this dictionary or one it
144    /// was cloned from, so the lookup cannot miss.
145    #[inline]
146    pub(crate) fn name(&self, id: u32) -> &Name {
147        &self.inner.names[id as usize]
148    }
149
150    pub(crate) fn len(&self) -> usize {
151        self.inner.names.len()
152    }
153
154    /// Heap bytes the table owns. Name buffers are interned and shared.
155    pub(crate) fn heap_bytes(&self) -> usize {
156        let inner = &*self.inner;
157        inner.names.capacity() * std::mem::size_of::<Name>()
158            + inner.ids.capacity() * (std::mem::size_of::<Name>() + std::mem::size_of::<u32>() + 1)
159    }
160}
161
162/// The dictionaries of a graph.
163#[derive(Clone, Default)]
164pub(crate) struct Dicts {
165    pub(crate) labels: NameDict,
166    pub(crate) types: NameDict,
167    pub(crate) keys: NameDict,
168    pub(crate) values: ValueDict,
169}
170
171impl Dicts {
172    pub(crate) fn heap_bytes(&self) -> usize {
173        self.labels.heap_bytes()
174            + self.types.heap_bytes()
175            + self.keys.heap_bytes()
176            + self.values.heap_bytes()
177    }
178}
179
180/// Longest string value a record refers to by number. Longer ones are
181/// rarely repeated labels of a category, and a number saves little on
182/// them next to their length.
183const VALUE_MAX_LEN: usize = 32;
184/// String values of a key a record stores in full before the key is
185/// judged: [`ValueDict`] numbers a key's values once this many show at
186/// most [`TRIAL_DISTINCT`] distinct ones.
187const TRIAL_VALUES: u16 = 64;
188const TRIAL_DISTINCT: usize = 8;
189/// New values a numbered key may add before it stops adding them: a key
190/// that kept a few values during its trial and then took many more is
191/// not one whose values repeat.
192const KEY_MISSES: u16 = 64;
193/// Values the dictionary holds at most. Numbers are never reused, so this
194/// bounds what it can keep.
195const VALUES_MAX: usize = 4096;
196
197/// Short string values of the keys that repeat a few of them, numbered
198/// like names so a record holds the number (one or two bytes) instead of
199/// the string. `source: 'musicbrainz'` in every record of a sync tracker
200/// costs a byte where it cost twelve.
201///
202/// Which keys: each key's first [`TRIAL_VALUES`] short string values are
203/// stored in full while their hashes are counted. A key that showed at
204/// most [`TRIAL_DISTINCT`] distinct values has its short values numbered
205/// from then on, up to [`KEY_MISSES`] new ones; every other key is never
206/// numbered. The judgement only changes how a value is stored: a record
207/// reads the same string either way.
208#[derive(Clone, Default)]
209pub(crate) struct ValueDict {
210    strings: NameDict,
211    /// By key id.
212    keys: Arc<Vec<KeyValues>>,
213}
214
215#[derive(Clone)]
216enum KeyValues {
217    /// Hashes of the distinct short values seen, and how many were seen.
218    Trial {
219        seen: u16,
220        distinct: Vec<u64>,
221    },
222    /// Values are numbered; `misses` counts the new ones added.
223    Numbered {
224        misses: u16,
225    },
226    Stored,
227}
228
229/// A graph's value dictionary as a snapshot carries it, so a restarted
230/// graph stores the values it numbered by number again. Only a hint: a
231/// graph without it reads every value the same, and stores more bytes.
232#[derive(Debug, Clone, Default, PartialEq, Eq)]
233pub struct ValueDictionary {
234    /// The numbered strings, in number order.
235    pub strings: Vec<Arc<str>>,
236    /// The keys judged after their trial.
237    pub keys: Vec<(Name, ValueKeyState)>,
238}
239
240/// How a key's short string values are stored, once judged.
241#[derive(Debug, Clone, Copy, PartialEq, Eq)]
242pub enum ValueKeyState {
243    /// By number; `misses` new values have been added since the trial.
244    Numbered { misses: u16 },
245    /// In full.
246    Stored,
247}
248
249impl Default for KeyValues {
250    fn default() -> Self {
251        KeyValues::Trial {
252            seen: 0,
253            distinct: Vec::new(),
254        }
255    }
256}
257
258impl ValueDict {
259    /// The number to store for `value` under key `key`, if it is to be
260    /// stored by number; counts it towards the key's trial otherwise.
261    #[inline]
262    pub(crate) fn number(&mut self, key: u32, value: &str) -> Option<u32> {
263        if value.len() > VALUE_MAX_LEN {
264            return None;
265        }
266        match self.keys.get(key as usize) {
267            Some(KeyValues::Stored) => None,
268            Some(KeyValues::Numbered { misses }) => {
269                if let Some(id) = self.strings.id_of(value) {
270                    return Some(id);
271                }
272                if *misses >= KEY_MISSES || self.strings.len() >= VALUES_MAX {
273                    return None;
274                }
275                let keys = Arc::make_mut(&mut self.keys);
276                if let KeyValues::Numbered { misses } = &mut keys[key as usize] {
277                    *misses += 1;
278                }
279                Some(
280                    self.strings
281                        .id_or_insert_with(value, || crate::Name::from_arc(Arc::from(value))),
282                )
283            }
284            _ => {
285                self.observe(key, value);
286                None
287            }
288        }
289    }
290
291    /// Count `value` towards key `key`'s trial, and judge the key once
292    /// the trial is over.
293    fn observe(&mut self, key: u32, value: &str) {
294        let keys = Arc::make_mut(&mut self.keys);
295        if keys.len() <= key as usize {
296            keys.resize_with(key as usize + 1, KeyValues::default);
297        }
298        let state = &mut keys[key as usize];
299        let KeyValues::Trial { seen, distinct } = state else {
300            return;
301        };
302        let hash = NameState::default().hash_one(value);
303        if !distinct.contains(&hash) {
304            if distinct.len() == TRIAL_DISTINCT {
305                *state = KeyValues::Stored;
306                return;
307            }
308            distinct.push(hash);
309        }
310        *seen += 1;
311        if *seen == TRIAL_VALUES {
312            *state = KeyValues::Numbered { misses: 0 };
313        }
314    }
315
316    /// [`Self::number`] without changing the dictionary: the number of a
317    /// value already numbered under a numbered key, `None` for anything
318    /// else. Encoders on other threads share one dictionary this way; a
319    /// value they store in full reads the same as a numbered one.
320    #[inline]
321    pub(crate) fn number_existing(&self, key: u32, value: &str) -> Option<u32> {
322        if value.len() > VALUE_MAX_LEN {
323            return None;
324        }
325        match self.keys.get(key as usize) {
326            Some(KeyValues::Numbered { .. }) => self.strings.id_of(value),
327            _ => None,
328        }
329    }
330
331    /// The numbered strings in number order, and the keys that finished
332    /// their trial with the judgement each got. A key still on trial is
333    /// left out: its trial starts over.
334    pub(crate) fn export(&self, key_name: impl Fn(u32) -> Name) -> ValueDictionary {
335        let strings = (0..self.strings.len() as u32)
336            .map(|id| Arc::from(self.string(id)))
337            .collect();
338        let keys = self
339            .keys
340            .iter()
341            .enumerate()
342            .filter_map(|(key, state)| {
343                let state = match state {
344                    KeyValues::Trial { .. } => return None,
345                    KeyValues::Numbered { misses } => ValueKeyState::Numbered { misses: *misses },
346                    KeyValues::Stored => ValueKeyState::Stored,
347                };
348                Some((key_name(key as u32), state))
349            })
350            .collect();
351        ValueDictionary { strings, keys }
352    }
353
354    /// Restore what [`Self::export`] wrote into an empty dictionary; `key_id`
355    /// numbers a key name. Strings past [`VALUES_MAX`], too long to be
356    /// numbered, or repeated are refused, so a restored dictionary is one
357    /// [`Self::number`] could have built.
358    pub(crate) fn import(
359        &mut self,
360        dictionary: &ValueDictionary,
361        mut key_id: impl FnMut(&Name) -> u32,
362    ) -> Result<(), String> {
363        if dictionary.strings.len() > VALUES_MAX {
364            return Err("value dictionary holds too many strings".into());
365        }
366        for value in &dictionary.strings {
367            if value.len() > VALUE_MAX_LEN {
368                return Err("value dictionary holds a string too long to number".into());
369            }
370            let before = self.strings.len();
371            self.strings
372                .id_or_insert_with(value, || crate::Name::from_arc(Arc::clone(value)));
373            if self.strings.len() == before {
374                return Err("value dictionary repeats a string".into());
375            }
376        }
377        let keys = Arc::make_mut(&mut self.keys);
378        for (name, state) in &dictionary.keys {
379            let key = key_id(name) as usize;
380            if keys.len() <= key {
381                keys.resize_with(key + 1, KeyValues::default);
382            }
383            keys[key] = match *state {
384                ValueKeyState::Numbered { misses } => KeyValues::Numbered {
385                    misses: misses.min(KEY_MISSES),
386                },
387                ValueKeyState::Stored => KeyValues::Stored,
388            };
389        }
390        Ok(())
391    }
392
393    /// The string numbered `id`.
394    #[inline]
395    pub(crate) fn string(&self, id: u32) -> &str {
396        self.strings.name(id).as_str()
397    }
398
399    pub(crate) fn heap_bytes(&self) -> usize {
400        // Values are not interned process-wide: count their bytes too.
401        let strings: usize = (0..self.strings.len() as u32)
402            .map(|id| 2 * std::mem::size_of::<usize>() + self.string(id).len())
403            .sum();
404        self.strings.heap_bytes()
405            + strings
406            + self.keys.capacity() * std::mem::size_of::<KeyValues>()
407    }
408}
409
410#[cfg(test)]
411mod tests {
412    use super::*;
413
414    /// A key that repeats a few values has them numbered after its trial;
415    /// one with many distinct values never does.
416    #[test]
417    fn values_are_numbered_only_for_keys_that_repeat_a_few() {
418        let mut values = ValueDict::default();
419        for i in 0..TRIAL_VALUES {
420            assert_eq!(values.number(0, ["a", "b", "c"][i as usize % 3]), None);
421            assert_eq!(values.number(1, &format!("id-{i}")), None);
422        }
423        let a = values.number(0, "a").expect("key 0 is numbered");
424        assert_eq!(values.string(a), "a");
425        assert_eq!(values.number(0, "a"), Some(a));
426        assert_eq!(values.number(1, "id-1"), None);
427        assert_eq!(values.number(0, &"x".repeat(VALUE_MAX_LEN + 1)), None);
428        // New values stop being added after `KEY_MISSES` of them, "a"
429        // the first.
430        let added = (0..2 * KEY_MISSES)
431            .filter(|i| values.number(0, &format!("new-{i}")).is_some())
432            .count();
433        assert_eq!(added, KEY_MISSES as usize - 1);
434        assert_eq!(values.number(0, "a"), Some(a));
435    }
436
437    #[test]
438    fn names_are_numbered_once_in_both_lookup_modes() {
439        let mut dict = NameDict::default();
440        let names: Vec<Name> = (0..20).map(|i| Name::new(&format!("N{i}"))).collect();
441        for (i, name) in names.iter().enumerate() {
442            assert_eq!(dict.id_or_insert(name), i as u32);
443            // Both the linear and the hashed lookup see every earlier name.
444            for (j, earlier) in names[..=i].iter().enumerate() {
445                assert_eq!(dict.id_of(earlier), Some(j as u32));
446                assert_eq!(dict.name(j as u32), earlier);
447            }
448        }
449        assert_eq!(dict.id_or_insert(&names[3]), 3);
450        assert_eq!(dict.id_or_insert_str("N7"), 7);
451        assert_eq!(dict.id_or_insert_str("fresh"), 20);
452        assert_eq!(dict.id_of("missing"), None);
453        assert_eq!(dict.len(), 21);
454    }
455
456    #[test]
457    fn a_clone_shares_the_table_until_one_side_adds_a_name() {
458        let mut dict = NameDict::default();
459        dict.id_or_insert_str("A");
460        let mut copy = dict.clone();
461        assert_eq!(copy.id_or_insert_str("B"), 1);
462        assert_eq!(dict.id_of("B"), None);
463        assert_eq!(dict.id_or_insert_str("C"), 1);
464        assert_eq!(copy.name(1).as_str(), "B");
465        assert_eq!(dict.name(1).as_str(), "C");
466    }
467
468    /// The name hasher starts from the per-process seed, not a
469    /// fixed basis, so colliding names cannot be precomputed.
470    #[test]
471    fn hashers_are_seeded_per_process() {
472        use std::hash::BuildHasher;
473        let unseeded = {
474            let mut h: u64 = 0xcbf2_9ce4_8422_2325;
475            for &b in b"name" {
476                h = (h ^ u64::from(b)).wrapping_mul(0x0000_0100_0000_01b3);
477            }
478            h
479        };
480        let mut hasher = NameState::default().build_hasher();
481        hasher.write(b"name");
482        assert_ne!(hasher.finish(), unseeded);
483        assert_eq!(
484            NameState::default().hash_one("name"),
485            NameState::default().hash_one("name")
486        );
487    }
488}