Skip to main content

yo_graph/
adjacency.rs

1//! The adjacency plane in its hot form: a run of neighbours per (node, label,
2//! direction), appended to and deleted from in place.
3//!
4//! A graph without a query language is an adjacency structure with good
5//! ergonomics, so this is what everything else in the graph model stands on.
6//! `11` section 2 gives adjacency two forms and this is the mutable one. The
7//! other is zu's node group CSR, which reaches 8 bits an edge because it never
8//! changes; this one is 12 bytes an edge because every operation on it has to
9//! be O(1).
10//!
11//! # The shape
12//!
13//! A run is the neighbours of one node under one label in one direction, and it
14//! is contiguous. That is the whole performance argument: a one hop is a probe
15//! for the run header and then a sequential read, and the read is over `u64`
16//! node ids with nothing else interleaved, so eight neighbours arrive per cache
17//! line.
18//!
19//! The edge slots live in a second array indexed the same way. Keeping them
20//! apart rather than storing `(neighbour, edge)` pairs costs nothing and saves
21//! a third of the memory traffic on the common walk, because a traversal that
22//! only wants to know where it can go next never reads an edge slot at all.
23//! Interleaving them would also make every neighbour an unaligned load out of a
24//! 12 byte stride.
25//!
26//! # Growing and shrinking
27//!
28//! Runs are cut from two shared arenas rather than allocated one by one,
29//! because a graph is mostly nodes with a handful of edges and a `Vec` header
30//! per node would cost more than the edges do. A run's capacity comes off a
31//! fixed ladder: doubling while it is small, then a quarter more each step, so
32//! the slack a hub carries is bounded by 25 per cent instead of by 100. Growing
33//! copies the run into the next size up and gives the old block to a free list,
34//! so the space is reused rather than lost.
35//!
36//! Deleting is swap with last and a decrement, which is how `08` section 4
37//! deletes from a dense member vector and it is the same reason: an O(1) delete
38//! is worth giving up the order of a run that has no order to give up. A run
39//! that falls to half of its capacity is moved down to the smallest size that
40//! fits, which leaves 2x of hysteresis so a run sitting on a boundary does not
41//! copy itself every time it gains and loses one edge.
42//!
43//! # What it costs
44//!
45//! Twelve bytes an edge is the payload and it is not the whole bill. There is
46//! one 32 byte run header per (node, label, direction) that has ever been
47//! linked, and there is the capacity slack. On a graph shaped like LiveJournal,
48//! most nodes with a few edges and a thin tail of hubs, the measured numbers
49//! are 18.1 bytes an edge as it is built and 15.2 after a sweep, which is 12.0
50//! of payload and 3.2 of run headers against an average degree of 13. The test
51//! at the bottom of this file is where those come from.
52//!
53//! The cold form is where 8 bits an edge comes from, and the ladder in `03` is
54//! what lets both be true at once. Against what this replaces it is already the
55//! cheap end: a pointer chased adjacency list is 16 bytes for the pair before
56//! the per node allocation header, and Neo4j's relationship store is 34.
57//!
58//! # What this does not do
59//!
60//! It does not look for a duplicate before it links. That check is linear in
61//! the degree, and the degree is exactly the thing that can be a hub with
62//! twenty thousand edges on it, so paying it on every insert would trade the
63//! write path away to enforce something the layer above can enforce with one
64//! probe. An edge table keyed by (source, destination, label) is where upsert
65//! semantics belong, and it is what `G.EADD` will sit on.
66//!
67//! It also does not delete a node, because finding every run a node has means
68//! knowing every label it was ever linked under, and that is the node table's
69//! job rather than this one's.
70
71use yo_common::prefetch;
72
73/// Which end of an edge a run is stored under.
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
75pub enum Dir {
76    /// Edges leaving the node.
77    Out,
78    /// Edges arriving at the node.
79    In,
80}
81
82/// How many sizes the capacity ladder has.
83///
84/// Eighty eight of them reach a run of four billion edges, which is the most a
85/// `u32` offset can address anyway, and the rest are there so the arithmetic at
86/// the top never has to think about the end.
87const CLASSES: usize = 96;
88
89/// The capacity ladder, in edges.
90const LADDER: [u32; CLASSES] = ladder();
91
92const LIVE: u8 = 1;
93const INCOMING: u8 = 2;
94
95/// Doubling to 16 and then a quarter more, rounded up to four so the steps just
96/// past the change of policy are still steps.
97///
98/// Sixteen is where the doubling stops and it is measured rather than picked. A
99/// run of 30 edges in a run sized 32 wastes nothing worth counting, but a run of
100/// 33 in a run sized 64 wastes half of itself, and the degrees between about 5
101/// and 50 are where most of a real graph's edges live. Doubling all the way to
102/// 64 costs 1.47 bytes of capacity per byte of edge across that band; stopping
103/// at 16 costs 1.16, and the whole graph goes from 1.23 to 1.13. What it buys
104/// back is two more copies per edge over the life of a run, which is 24 bytes of
105/// memcpy against 12 bytes of edge, and that is not a trade anybody notices.
106const fn ladder() -> [u32; CLASSES] {
107    let mut out = [0u32; CLASSES];
108    let mut cap: u64 = 1;
109    let mut i = 0;
110    while i < CLASSES {
111        out[i] = if cap > u32::MAX as u64 {
112            u32::MAX
113        } else {
114            cap as u32
115        };
116        cap = if cap < 16 {
117            cap * 2
118        } else {
119            (cap + cap / 4 + 3) & !3
120        };
121        i += 1;
122    }
123    out
124}
125
126/// The header of one run, and the only thing the table stores.
127///
128/// Thirty two bytes, so two of them fit a cache line and neither straddles it,
129/// which is what makes a probe that misses cost one load and nothing else.
130#[derive(Debug, Clone, Copy, Default)]
131struct Slot {
132    node: u64,
133    at: u32,
134    len: u32,
135    cap: u32,
136    label: u32,
137    flags: u8,
138}
139
140/// The neighbours of every node, under every label, in both directions.
141///
142/// ```
143/// use yo_graph::{Adjacency, Dir};
144///
145/// const FOLLOWS: u32 = 1;
146///
147/// let mut g = Adjacency::new();
148/// g.link(1, 2, FOLLOWS, 100);
149/// g.link(1, 3, FOLLOWS, 101);
150///
151/// assert_eq!(g.neighbours(1, FOLLOWS, Dir::Out), &[2, 3]);
152/// assert_eq!(g.neighbours(2, FOLLOWS, Dir::In), &[1]);
153/// assert_eq!(g.degree(1, FOLLOWS, Dir::Out), 2);
154/// ```
155#[derive(Debug)]
156pub struct Adjacency {
157    slots: Vec<Slot>,
158    live: usize,
159    filled: usize,
160    edges: usize,
161    entries: usize,
162    neighbour: Vec<u64>,
163    edge: Vec<u32>,
164    free: Vec<Vec<u32>>,
165    both: bool,
166}
167
168impl Default for Adjacency {
169    fn default() -> Adjacency {
170        Adjacency::new()
171    }
172}
173
174impl Adjacency {
175    /// An empty plane that indexes both directions, so `In` answers as well as
176    /// `Out` does.
177    #[must_use]
178    pub fn new() -> Adjacency {
179        Adjacency::build(true)
180    }
181
182    /// An empty plane that indexes outgoing edges only.
183    ///
184    /// This halves the memory and halves the work an insert does, and it is the
185    /// right choice whenever nothing asks the graph who points at a node.
186    /// `neighbours` under [`Dir::In`] then answers nothing at all, which is why
187    /// it is a decision at construction rather than a flag on a call: a walk
188    /// that silently found no incoming edges because of how the plane was built
189    /// would look exactly like a node that has none.
190    #[must_use]
191    pub fn out_only() -> Adjacency {
192        Adjacency::build(false)
193    }
194
195    fn build(both: bool) -> Adjacency {
196        Adjacency {
197            slots: Vec::new(),
198            live: 0,
199            filled: 0,
200            edges: 0,
201            entries: 0,
202            neighbour: Vec::new(),
203            edge: Vec::new(),
204            free: Vec::new(),
205            both,
206        }
207    }
208
209    /// Whether incoming edges are indexed.
210    #[must_use]
211    pub fn indexes_incoming(&self) -> bool {
212        self.both
213    }
214
215    /// How many edges have been linked and not unlinked.
216    #[must_use]
217    pub fn edges(&self) -> usize {
218        self.edges
219    }
220
221    /// Whether any edge is linked.
222    #[must_use]
223    pub fn is_empty(&self) -> bool {
224        self.edges == 0
225    }
226
227    /// How many runs hold at least one edge.
228    #[must_use]
229    pub fn runs(&self) -> usize {
230        self.filled
231    }
232
233    /// Add an edge from `src` to `dst` under `label`, carrying `edge` as the
234    /// slot of the edge record.
235    ///
236    /// This appends. It does not look for an edge that is already there, for
237    /// the reason in the module docs, so linking the same pair twice leaves two
238    /// entries and unlinking it once leaves one.
239    pub fn link(&mut self, src: u64, dst: u64, label: u32, edge: u32) {
240        let s = self.run_for(src, label, 0);
241        self.push(s, dst, edge);
242        if self.both {
243            let d = self.run_for(dst, label, INCOMING);
244            self.push(d, src, edge);
245        }
246        self.edges += 1;
247    }
248
249    /// Remove one edge from `src` to `dst` under `label`, and answer with the
250    /// edge slot it was carrying.
251    ///
252    /// Costs a scan of the run at each end, because finding which position an
253    /// edge sits at is the one thing a plane keyed by node rather than by edge
254    /// cannot do in a step. A caller that already knows the position wants
255    /// [`Adjacency::unlink_at`].
256    pub fn unlink(&mut self, src: u64, dst: u64, label: u32) -> Option<u32> {
257        let s = self.find(src, label, 0)?;
258        let i = self.position(s, dst)?;
259        let edge = self.take(s, i).1;
260        if self.both
261            && let Some(d) = self.find(dst, label, INCOMING)
262            && let Some(j) = self.position(d, src)
263        {
264            self.take(d, j);
265        }
266        self.edges -= 1;
267        Some(edge)
268    }
269
270    /// Remove the edge at position `i` of one run, and answer with the
271    /// neighbour and the edge slot that were there.
272    ///
273    /// This is the O(1) primitive and it touches one end only, so the other end
274    /// still holds its half of the edge. It is for a caller that tracks
275    /// positions itself and will do both. Whatever used to be last has moved
276    /// into `i`.
277    pub fn unlink_at(&mut self, node: u64, label: u32, dir: Dir, i: usize) -> Option<(u64, u32)> {
278        let s = self.find(node, label, incoming(dir))?;
279        if i >= self.slots[s].len as usize {
280            return None;
281        }
282        Some(self.take(s, i))
283    }
284
285    /// The neighbours of `node` under `label` in `dir`, in one contiguous run.
286    ///
287    /// One probe and then a sequential read. The order is whatever inserting
288    /// and deleting left behind, because a delete moves the last entry into the
289    /// hole it made.
290    #[must_use]
291    pub fn neighbours(&self, node: u64, label: u32, dir: Dir) -> &[u64] {
292        match self.find(node, label, incoming(dir)) {
293            Some(s) => {
294                let (at, len) = (self.slots[s].at as usize, self.slots[s].len as usize);
295                &self.neighbour[at..at + len]
296            }
297            None => &[],
298        }
299    }
300
301    /// The edge slots of `node` under `label` in `dir`, lined up one for one
302    /// with [`Adjacency::neighbours`].
303    #[must_use]
304    pub fn edge_slots(&self, node: u64, label: u32, dir: Dir) -> &[u32] {
305        match self.find(node, label, incoming(dir)) {
306            Some(s) => {
307                let (at, len) = (self.slots[s].at as usize, self.slots[s].len as usize);
308                &self.edge[at..at + len]
309            }
310            None => &[],
311        }
312    }
313
314    /// How many edges `node` has under `label` in `dir`.
315    #[must_use]
316    pub fn degree(&self, node: u64, label: u32, dir: Dir) -> usize {
317        match self.find(node, label, incoming(dir)) {
318            Some(s) => self.slots[s].len as usize,
319            None => 0,
320        }
321    }
322
323    /// Every non empty run under `label` in `dir`, as the node and its two
324    /// slices, in whatever order the table happens to hold them.
325    ///
326    /// This is the read side of promotion. The cold form is built by walking
327    /// the hot plane once and handing every run to an encoder, and there is no
328    /// other way to get at a run whose node you have not already been told
329    /// about, because the table is keyed by the node rather than ordered by it.
330    /// The order is deliberately not promised: a caller that needs the runs in
331    /// node order is building something sorted anyway and can sort what it
332    /// collects.
333    pub fn for_each_run(&self, label: u32, dir: Dir, mut f: impl FnMut(u64, &[u64], &[u32])) {
334        let want = incoming(dir);
335        for s in &self.slots {
336            if s.flags & LIVE == 0 || s.len == 0 || s.label != label || s.flags & INCOMING != want {
337                continue;
338            }
339            let (at, len) = (s.at as usize, s.len as usize);
340            f(
341                s.node,
342                &self.neighbour[at..at + len],
343                &self.edge[at..at + len],
344            );
345        }
346    }
347
348    /// Ask the cache for the slot a run's header will be found in.
349    ///
350    /// A multi hop walk knows its whole next frontier before it reads any of
351    /// it, so it can issue these across the frontier and then come back and
352    /// read. That is the same two walk shape `04` section 3 drains a command
353    /// batch with, and it is what the two hop budget in G14 is actually
354    /// spending: the probes are dependent loads, and the only way to make them
355    /// cheap is to stop them being serial.
356    pub fn prefetch(&self, node: u64, label: u32, dir: Dir) {
357        if self.slots.is_empty() {
358            return;
359        }
360        let i = bucket(hash(node, label, incoming(dir)), self.slots.len());
361        prefetch(&self.slots[i]);
362    }
363
364    /// Resident bytes, counting the table, both arenas, and everything the free
365    /// lists are still holding.
366    #[must_use]
367    pub fn bytes(&self) -> usize {
368        self.slots.capacity() * size_of::<Slot>()
369            + self.neighbour.capacity() * size_of::<u64>()
370            + self.edge.capacity() * size_of::<u32>()
371            + self.free.capacity() * size_of::<Vec<u32>>()
372            + self
373                .free
374                .iter()
375                .map(|f| f.capacity() * size_of::<u32>())
376                .sum::<usize>()
377    }
378
379    /// Rebuild the table and the arenas with nothing spare in them.
380    ///
381    /// A run that emptied leaves its header behind, a run that shrank leaves
382    /// slack, and a free list holds blocks nothing has asked for again. None of
383    /// that is worth chasing on the write path, so this is the sweep that
384    /// reclaims it, and it is the natural thing to run before a settled part of
385    /// the graph is promoted into the cold form. Every run comes out sized to
386    /// exactly what it holds and laid out one after another, which is also the
387    /// order the promotion wants to read them in.
388    pub fn compact(&mut self) {
389        let mut keep: Vec<Slot> = self
390            .slots
391            .iter()
392            .copied()
393            .filter(|s| s.flags & LIVE != 0 && s.len > 0)
394            .collect();
395        let mut neighbour = Vec::with_capacity(self.entries);
396        let mut edge = Vec::with_capacity(self.entries);
397        for slot in &mut keep {
398            let (at, len) = (slot.at as usize, slot.len as usize);
399            let to = neighbour.len() as u32;
400            neighbour.extend_from_slice(&self.neighbour[at..at + len]);
401            edge.extend_from_slice(&self.edge[at..at + len]);
402            slot.at = to;
403            slot.cap = slot.len;
404        }
405        self.neighbour = neighbour;
406        self.edge = edge;
407        self.free = Vec::new();
408        self.live = keep.len();
409        self.filled = keep.len();
410        // Sized off the live count rather than rounded up to a power of two,
411        // which is the whole reason the table is indexed by a multiply instead
412        // of a mask. A graph with two hundred thousand runs would otherwise get
413        // half a million slots and carry three bytes an edge it never uses.
414        self.slots = vec![Slot::default(); (keep.len() * 4 / 3).max(16)];
415        for slot in keep {
416            let i = self.vacancy(slot.node, slot.label, slot.flags & INCOMING);
417            self.slots[i] = slot;
418        }
419    }
420
421    fn find(&self, node: u64, label: u32, incoming: u8) -> Option<usize> {
422        if self.slots.is_empty() {
423            return None;
424        }
425        let n = self.slots.len();
426        let mut i = bucket(hash(node, label, incoming), n);
427        loop {
428            let s = &self.slots[i];
429            if s.flags & LIVE == 0 {
430                return None;
431            }
432            if s.node == node && s.label == label && s.flags & INCOMING == incoming {
433                return Some(i);
434            }
435            i += 1;
436            if i == n {
437                i = 0;
438            }
439        }
440    }
441
442    fn position(&self, s: usize, node: u64) -> Option<usize> {
443        let (at, len) = (self.slots[s].at as usize, self.slots[s].len as usize);
444        self.neighbour[at..at + len].iter().position(|n| *n == node)
445    }
446
447    /// The slot for a run, made if it was not there.
448    fn run_for(&mut self, node: u64, label: u32, incoming: u8) -> usize {
449        if (self.live + 1) * 4 > self.slots.len() * 3 {
450            self.regrow();
451        }
452        let n = self.slots.len();
453        let mut i = bucket(hash(node, label, incoming), n);
454        loop {
455            let s = &self.slots[i];
456            if s.flags & LIVE == 0 {
457                self.slots[i] = Slot {
458                    node,
459                    at: 0,
460                    len: 0,
461                    cap: 0,
462                    label,
463                    flags: LIVE | incoming,
464                };
465                self.live += 1;
466                return i;
467            }
468            if s.node == node && s.label == label && s.flags & INCOMING == incoming {
469                return i;
470            }
471            i += 1;
472            if i == n {
473                i = 0;
474            }
475        }
476    }
477
478    /// Where a key that is known not to be there belongs.
479    fn vacancy(&self, node: u64, label: u32, incoming: u8) -> usize {
480        let n = self.slots.len();
481        let mut i = bucket(hash(node, label, incoming), n);
482        while self.slots[i].flags & LIVE != 0 {
483            i += 1;
484            if i == n {
485                i = 0;
486            }
487        }
488        i
489    }
490
491    fn regrow(&mut self) {
492        // A quarter more rather than double, for the same reason the table is
493        // not a power of two. Growth by a factor g leaves the load factor
494        // wandering between 0.75 and 0.75 over g, so doubling means half the
495        // table is empty for most of its life. A quarter holds it between 0.6
496        // and 0.75, and the price is four rehashes per run over the whole
497        // build rather than two.
498        let want = (self.slots.len() + self.slots.len() / 4).max(16);
499        let old = core::mem::replace(&mut self.slots, vec![Slot::default(); want]);
500        for slot in old {
501            if slot.flags & LIVE != 0 {
502                let i = self.vacancy(slot.node, slot.label, slot.flags & INCOMING);
503                self.slots[i] = slot;
504            }
505        }
506    }
507
508    fn push(&mut self, s: usize, node: u64, edge: u32) {
509        let Slot {
510            mut at,
511            len,
512            mut cap,
513            ..
514        } = self.slots[s];
515        if len == cap {
516            let want = LADDER[ceil_class(cap + 1)];
517            let to = self.alloc(want);
518            if len > 0 {
519                self.copy_run(at, to, len as usize);
520                self.release(at, cap);
521            }
522            at = to;
523            cap = want;
524        }
525        let i = at as usize + len as usize;
526        self.neighbour[i] = node;
527        self.edge[i] = edge;
528        self.slots[s].at = at;
529        self.slots[s].cap = cap;
530        self.slots[s].len = len + 1;
531        self.entries += 1;
532        if len == 0 {
533            self.filled += 1;
534        }
535    }
536
537    /// Swap with last, decrement, and give back capacity the run has outgrown.
538    fn take(&mut self, s: usize, i: usize) -> (u64, u32) {
539        let Slot { at, len, cap, .. } = self.slots[s];
540        let (at, last) = (at as usize, at as usize + len as usize - 1);
541        let gone = (self.neighbour[at + i], self.edge[at + i]);
542        self.neighbour[at + i] = self.neighbour[last];
543        self.edge[at + i] = self.edge[last];
544        self.slots[s].len = len - 1;
545        self.entries -= 1;
546        if len == 1 {
547            self.filled -= 1;
548        }
549        self.shrink(s, cap);
550        gone
551    }
552
553    fn shrink(&mut self, s: usize, cap: u32) {
554        let len = self.slots[s].len;
555        if len == 0 {
556            self.release(self.slots[s].at, cap);
557            self.slots[s].at = 0;
558            self.slots[s].cap = 0;
559            return;
560        }
561        // Half of the capacity is the trigger and the smallest size that fits
562        // is the destination, so a run has to lose half of itself before it is
563        // moved and it will not be moved again until it has doubled.
564        if len * 2 > cap {
565            return;
566        }
567        let want = LADDER[ceil_class(len)];
568        if want >= cap {
569            return;
570        }
571        let at = self.slots[s].at;
572        let to = self.alloc(want);
573        self.copy_run(at, to, len as usize);
574        self.release(at, cap);
575        self.slots[s].at = to;
576        self.slots[s].cap = want;
577    }
578
579    fn copy_run(&mut self, from: u32, to: u32, len: usize) {
580        let (from, to) = (from as usize, to as usize);
581        self.neighbour.copy_within(from..from + len, to);
582        self.edge.copy_within(from..from + len, to);
583    }
584
585    fn alloc(&mut self, cap: u32) -> u32 {
586        let class = ceil_class(cap);
587        if let Some(list) = self.free.get_mut(class)
588            && let Some(at) = list.pop()
589        {
590            return at;
591        }
592        let cap = cap as usize;
593        let at = self.neighbour.len();
594        assert!(at + cap <= u32::MAX as usize, "the adjacency arena is full");
595        // An eighth more when it has to grow, not double. These are the
596        // biggest things here by a long way, so a doubling that lands one edge
597        // past the last one leaves half the plane allocated and never touched.
598        // Nothing is rehashed on the way, it is one copy, so the growth can be
599        // much finer here than the table's.
600        if at + cap > self.neighbour.capacity() {
601            let cur = self.neighbour.capacity();
602            let want = (cur + cur / 8).max(at + cap).max(64);
603            self.neighbour.reserve_exact(want - at);
604            self.edge.reserve_exact(want - at);
605        }
606        self.neighbour.resize(at + cap, 0);
607        self.edge.resize(at + cap, 0);
608        at as u32
609    }
610
611    /// A block goes back under the largest size that certainly fits inside it,
612    /// so a block that came out of a compaction at some exact length is still
613    /// reusable and is never handed out as more room than it has.
614    fn release(&mut self, at: u32, cap: u32) {
615        let class = floor_class(cap);
616        while self.free.len() <= class {
617            self.free.push(Vec::new());
618        }
619        self.free[class].push(at);
620    }
621}
622
623/// The smallest ladder size that holds `n` edges.
624///
625/// A linear walk, because the answer is in the first few entries for nearly
626/// every node in a real graph and a binary search over 96 entries would be
627/// slower for the case that matters.
628fn ceil_class(n: u32) -> usize {
629    LADDER.iter().position(|c| *c >= n).unwrap_or(CLASSES - 1)
630}
631
632/// The largest ladder size that fits inside `n` edges.
633fn floor_class(n: u32) -> usize {
634    let at = ceil_class(n);
635    if LADDER[at] > n {
636        at.saturating_sub(1)
637    } else {
638        at
639    }
640}
641
642fn incoming(dir: Dir) -> u8 {
643    match dir {
644        Dir::Out => 0,
645        Dir::In => INCOMING,
646    }
647}
648
649/// Lemire's reduction: the top bits of the hash scaled onto `n`, so the table
650/// can be any size at all rather than a power of two. That matters more here
651/// than the one multiply costs, because rounding a run count up to a power of
652/// two is up to twice the table for nothing.
653#[inline]
654fn bucket(h: u64, n: usize) -> usize {
655    ((u128::from(h) * n as u128) >> 64) as usize
656}
657
658/// Node ids are usually dense small integers and there are only ever a handful
659/// of distinct labels, so the label and the direction are spread across the
660/// whole word before the finaliser rather than after it. Without that, every
661/// label of one node lands in a run of neighbouring slots and a probe for one
662/// walks over the others.
663#[inline]
664fn hash(node: u64, label: u32, incoming: u8) -> u64 {
665    let tag = (u64::from(label) << 1) | u64::from(incoming >> 1);
666    let mut x = node ^ tag.wrapping_mul(0x9e37_79b9_7f4a_7c15);
667    x ^= x >> 33;
668    x = x.wrapping_mul(0xff51_afd7_ed55_8ccd);
669    x ^= x >> 29;
670    x = x.wrapping_mul(0xc4ce_b9fe_1a85_ec53);
671    x ^ (x >> 32)
672}
673
674#[cfg(test)]
675mod tests {
676    use super::*;
677    use yo_common::Rng;
678
679    const FOLLOWS: u32 = 1;
680    const WORKS_AT: u32 = 2;
681
682    fn sorted(v: &[u64]) -> Vec<u64> {
683        let mut v = v.to_vec();
684        v.sort_unstable();
685        v
686    }
687
688    #[test]
689    fn a_slot_is_one_half_of_a_cache_line() {
690        assert_eq!(size_of::<Slot>(), 32);
691    }
692
693    #[test]
694    fn the_ladder_only_ever_goes_up() {
695        for w in LADDER.windows(2) {
696            assert!(w[1] > w[0] || w[0] == u32::MAX, "{w:?} does not go up");
697        }
698        assert_eq!(LADDER[4], 16, "doubling should run out at 16");
699        assert_eq!(
700            LADDER[5], 20,
701            "and a quarter more should be the step after it"
702        );
703        assert!(
704            LADDER.contains(&u32::MAX),
705            "the ladder should reach the end of a u32"
706        );
707        // The two ends of a size, which is what the free list keys on.
708        assert_eq!(LADDER[ceil_class(17)], 20);
709        assert_eq!(LADDER[floor_class(19)], 16);
710        assert_eq!(LADDER[floor_class(20)], 20);
711        assert_eq!(LADDER[ceil_class(1)], 1);
712    }
713
714    #[test]
715    fn a_run_is_the_neighbours_that_were_linked_to_it() {
716        let mut g = Adjacency::new();
717        for (i, dst) in [7u64, 9, 11].iter().enumerate() {
718            g.link(1, *dst, FOLLOWS, i as u32);
719        }
720        assert_eq!(g.neighbours(1, FOLLOWS, Dir::Out), &[7, 9, 11]);
721        assert_eq!(g.edge_slots(1, FOLLOWS, Dir::Out), &[0, 1, 2]);
722        assert_eq!(g.degree(1, FOLLOWS, Dir::Out), 3);
723        assert_eq!(g.edges(), 3);
724        assert_eq!(g.runs(), 4);
725    }
726
727    #[test]
728    fn a_node_nobody_linked_has_no_neighbours_rather_than_no_answer() {
729        let g = Adjacency::new();
730        assert!(g.neighbours(1, FOLLOWS, Dir::Out).is_empty());
731        assert!(g.edge_slots(1, FOLLOWS, Dir::Out).is_empty());
732        assert_eq!(g.degree(1, FOLLOWS, Dir::Out), 0);
733        assert!(g.is_empty());
734        g.prefetch(1, FOLLOWS, Dir::Out);
735    }
736
737    #[test]
738    fn an_edge_is_readable_from_both_ends() {
739        let mut g = Adjacency::new();
740        g.link(1, 2, FOLLOWS, 10);
741        assert_eq!(g.neighbours(1, FOLLOWS, Dir::Out), &[2]);
742        assert_eq!(g.neighbours(2, FOLLOWS, Dir::In), &[1]);
743        assert_eq!(g.edge_slots(2, FOLLOWS, Dir::In), &[10]);
744        assert!(g.neighbours(2, FOLLOWS, Dir::Out).is_empty());
745    }
746
747    #[test]
748    fn one_label_is_not_another() {
749        let mut g = Adjacency::new();
750        g.link(1, 2, FOLLOWS, 10);
751        g.link(1, 3, WORKS_AT, 11);
752        assert_eq!(g.neighbours(1, FOLLOWS, Dir::Out), &[2]);
753        assert_eq!(g.neighbours(1, WORKS_AT, Dir::Out), &[3]);
754    }
755
756    #[test]
757    fn a_self_loop_is_at_both_of_its_ends_and_they_are_different_runs() {
758        let mut g = Adjacency::new();
759        g.link(1, 1, FOLLOWS, 5);
760        assert_eq!(g.neighbours(1, FOLLOWS, Dir::Out), &[1]);
761        assert_eq!(g.neighbours(1, FOLLOWS, Dir::In), &[1]);
762        assert_eq!(g.unlink(1, 1, FOLLOWS), Some(5));
763        assert!(g.neighbours(1, FOLLOWS, Dir::Out).is_empty());
764        assert!(g.neighbours(1, FOLLOWS, Dir::In).is_empty());
765    }
766
767    #[test]
768    fn out_only_stores_nothing_incoming() {
769        let mut both = Adjacency::new();
770        let mut out = Adjacency::out_only();
771        // A chain, and the claim is about one node in the middle of it, so
772        // the chain only has to be long enough to have a middle.
773        let n = if cfg!(miri) { 100u64 } else { 1000 };
774        let mid = n / 2;
775        for i in 0..n {
776            both.link(i, i + 1, FOLLOWS, i as u32);
777            out.link(i, i + 1, FOLLOWS, i as u32);
778        }
779        assert_eq!(out.neighbours(mid, FOLLOWS, Dir::Out), &[mid + 1]);
780        assert!(out.neighbours(mid, FOLLOWS, Dir::In).is_empty());
781        assert_eq!(both.neighbours(mid, FOLLOWS, Dir::In), &[mid - 1]);
782        assert!(!out.indexes_incoming());
783        assert!(
784            out.bytes() * 3 < both.bytes() * 2,
785            "{} against {}",
786            out.bytes(),
787            both.bytes()
788        );
789    }
790
791    #[test]
792    fn unlinking_takes_the_edge_off_both_ends() {
793        let mut g = Adjacency::new();
794        g.link(1, 2, FOLLOWS, 10);
795        g.link(1, 3, FOLLOWS, 11);
796        assert_eq!(g.unlink(1, 2, FOLLOWS), Some(10));
797        assert_eq!(g.neighbours(1, FOLLOWS, Dir::Out), &[3]);
798        assert!(g.neighbours(2, FOLLOWS, Dir::In).is_empty());
799        assert_eq!(g.edges(), 1);
800        assert_eq!(g.unlink(1, 2, FOLLOWS), None);
801        assert_eq!(g.unlink(9, 9, FOLLOWS), None);
802    }
803
804    #[test]
805    fn a_delete_moves_the_last_edge_into_the_hole() {
806        let mut g = Adjacency::new();
807        for dst in 1..=5u64 {
808            g.link(0, dst, FOLLOWS, dst as u32);
809        }
810        g.unlink(0, 2, FOLLOWS);
811        // The order is gone but nothing else is, and the edge slot went with
812        // the neighbour it belonged to.
813        let n = g.neighbours(0, FOLLOWS, Dir::Out);
814        let e = g.edge_slots(0, FOLLOWS, Dir::Out);
815        assert_eq!(sorted(n), vec![1, 3, 4, 5]);
816        for (i, node) in n.iter().enumerate() {
817            assert_eq!(u64::from(e[i]), *node, "the pairing survived the swap");
818        }
819    }
820
821    #[test]
822    fn unlink_at_moves_the_last_entry_into_the_position_it_took() {
823        let mut g = Adjacency::new();
824        for dst in 1..=4u64 {
825            g.link(0, dst, FOLLOWS, dst as u32);
826        }
827        assert_eq!(g.unlink_at(0, FOLLOWS, Dir::Out, 0), Some((1, 1)));
828        assert_eq!(g.neighbours(0, FOLLOWS, Dir::Out), &[4, 2, 3]);
829        assert_eq!(g.unlink_at(0, FOLLOWS, Dir::Out, 9), None);
830        assert_eq!(g.unlink_at(7, FOLLOWS, Dir::Out, 0), None);
831    }
832
833    #[test]
834    fn a_run_survives_growing_through_every_size_it_passes() {
835        let mut g = Adjacency::out_only();
836        // Fewer sizes under Miri, not none: the ladder is geometric, so six
837        // hundred still climbs most of the classes five thousand does and the
838        // last few are the ones a run of this shape never reaches anyway.
839        let n = if cfg!(miri) { 600u64 } else { 5000 };
840        for dst in 0..n {
841            g.link(0, dst, FOLLOWS, dst as u32);
842        }
843        assert_eq!(g.degree(0, FOLLOWS, Dir::Out), n as usize);
844        assert_eq!(
845            g.neighbours(0, FOLLOWS, Dir::Out),
846            (0..n).collect::<Vec<_>>()
847        );
848        assert_eq!(
849            g.edge_slots(0, FOLLOWS, Dir::Out),
850            (0..n as u32).collect::<Vec<_>>()
851        );
852    }
853
854    #[test]
855    fn a_hub_that_empties_gives_its_block_back() {
856        let mut g = Adjacency::out_only();
857        // What is checked is that the second hub is free, not that either hub
858        // is any particular size, so both come down together and the claim is
859        // the same comparison between the same two states.
860        let n = if cfg!(miri) { 400u64 } else { 4000 };
861        for dst in 0..n {
862            g.link(0, dst, FOLLOWS, 0);
863        }
864        let full = g.bytes();
865        for dst in 0..n {
866            assert!(g.unlink(0, dst, FOLLOWS).is_some());
867        }
868        assert_eq!(g.degree(0, FOLLOWS, Dir::Out), 0);
869        assert_eq!(g.runs(), 0);
870        // Filling a second node to the same size reuses what the first gave
871        // back rather than asking the arena for more.
872        for dst in 0..n {
873            g.link(1, dst, FOLLOWS, 0);
874        }
875        assert!(g.bytes() <= full + full / 4, "{} against {full}", g.bytes());
876    }
877
878    #[test]
879    fn a_run_that_grows_and_shrinks_does_not_copy_itself_on_a_boundary() {
880        // Sixteen is the last doubling, so this sits astride it. What is
881        // checked is that the capacity settles rather than that anything is
882        // fast: an implementation that shrank on the exact fit would move the
883        // run on every one of these, and the arena would grow without end.
884        let mut g = Adjacency::out_only();
885        for dst in 0..16u64 {
886            g.link(0, dst, FOLLOWS, 0);
887        }
888        // The first cycle does grow the run once, from the 16 it fits exactly
889        // into to the 20 above it. Everything after that is the claim.
890        g.link(0, 999, FOLLOWS, 0);
891        g.unlink(0, 999, FOLLOWS);
892        let settled = g.bytes();
893        for _ in 0..100 {
894            g.link(0, 999, FOLLOWS, 0);
895            g.unlink(0, 999, FOLLOWS);
896        }
897        assert_eq!(g.bytes(), settled);
898        assert_eq!(g.degree(0, FOLLOWS, Dir::Out), 16);
899    }
900
901    #[test]
902    fn a_run_that_loses_most_of_itself_gives_the_room_back() {
903        let mut g = Adjacency::out_only();
904        // Ten survivors out of however many went in, which is the ratio the
905        // claim is about. The ten is written once and the rest follows from it.
906        let n = if cfg!(miri) { 400u64 } else { 4000 };
907        for dst in 0..n {
908            g.link(0, dst, FOLLOWS, 0);
909        }
910        for dst in 0..n - 10 {
911            g.unlink(0, dst, FOLLOWS);
912        }
913        assert_eq!(g.degree(0, FOLLOWS, Dir::Out), 10);
914        // The run itself came down on the way, without waiting for a sweep.
915        assert!(g.slots.iter().any(|s| s.len == 10 && s.cap <= 16));
916        g.compact();
917        assert_eq!(g.degree(0, FOLLOWS, Dir::Out), 10);
918        assert!(g.bytes() < 4000, "{} bytes for ten edges", g.bytes());
919    }
920
921    #[test]
922    fn compact_drops_the_runs_that_emptied() {
923        let mut g = Adjacency::new();
924        // Ten edges left out of however many, so there are twenty runs to find
925        // among a great many that emptied. Every index below is derived from
926        // `n`, because a number written twice stops meaning anything the moment
927        // one of the two moves.
928        let n = if cfg!(miri) { 400u64 } else { 2000 };
929        let live = 10;
930        for i in 0..n {
931            g.link(i, i + 1, FOLLOWS, i as u32);
932        }
933        for i in 0..n - live {
934            g.unlink(i, i + 1, FOLLOWS);
935        }
936        assert_eq!(g.runs(), 20);
937        let before = g.bytes();
938        g.compact();
939        assert_eq!(g.runs(), 20);
940        assert_eq!(g.edges(), live as usize);
941        let mid = n - live / 2;
942        assert_eq!(g.neighbours(mid, FOLLOWS, Dir::Out), &[mid + 1]);
943        assert_eq!(g.neighbours(mid + 1, FOLLOWS, Dir::In), &[mid]);
944        assert!(g.bytes() * 4 < before, "{} against {before}", g.bytes());
945        // And it is still a working plane afterwards, which is the part a
946        // rebuild is easy to get wrong. This one also has to grow a run whose
947        // capacity the sweep cut to exactly what it held.
948        let fresh = n * 2;
949        g.link(mid, fresh, FOLLOWS, 7);
950        assert_eq!(
951            sorted(g.neighbours(mid, FOLLOWS, Dir::Out)),
952            vec![mid + 1, fresh]
953        );
954        assert_eq!(sorted(g.neighbours(fresh, FOLLOWS, Dir::In)), vec![mid]);
955    }
956
957    // Not shrunk. The claim is bytes an edge on a degree distribution with a
958    // tail, and both halves of it need the size: the run headers only average
959    // out over a lot of runs, and the fat tail only appears at all once there
960    // are enough nodes for two percent of them to be hubs. A smaller version
961    // measures a different structure and would pass or fail for its own
962    // reasons.
963    #[cfg_attr(miri, ignore = "bytes an edge is the claim and it needs the graph")]
964    #[test]
965    fn a_hot_run_costs_about_twelve_bytes_an_edge() {
966        // A degree distribution with a tail, because a uniform one hides both
967        // things that could go wrong: the run headers, which a graph of hubs
968        // has too few of to notice, and the capacity slack, which a graph of
969        // leaves never reaches.
970        let mut g = Adjacency::out_only();
971        let mut rng = Rng::new(0x9e3f);
972        let nodes = 200_000u64;
973        let mut edges = 0usize;
974        for src in 0..nodes {
975            let deg = match rng.next_u64() % 1000 {
976                0..=799 => 1 + rng.next_u64() % 4,
977                800..=979 => 5 + rng.next_u64() % 40,
978                _ => 45 + rng.next_u64() % 600,
979            };
980            for _ in 0..deg {
981                g.link(src, rng.next_u64() % nodes, FOLLOWS, 0);
982                edges += 1;
983            }
984        }
985        let per = g.bytes() as f64 / edges as f64;
986        g.compact();
987        let settled = g.bytes() as f64 / edges as f64;
988        // Twelve is the payload and the rest is one 32 byte header per run
989        // against an average degree in the teens, plus the capacity slack.
990        // Both are the price of a structure that inserts and deletes in
991        // constant time, and the cold form is where 8 bits an edge comes from.
992        assert!(per < 19.0, "{per:.2} bytes an edge over {edges} edges");
993        assert!(settled < 16.0, "{settled:.2} bytes an edge once swept");
994    }
995
996    #[test]
997    fn a_two_hop_reaches_what_a_pair_of_one_hops_reaches() {
998        let mut g = Adjacency::new();
999        let mut rng = Rng::new(7);
1000        // The walk is over one node's neighbours and their neighbours, so what
1001        // it costs is the degree and not the graph. The graph only has to be
1002        // wide enough that the eight hops land on eight different nodes.
1003        let nodes = if cfg!(miri) { 300u64 } else { 5000 };
1004        for src in 0..nodes {
1005            for _ in 0..8 {
1006                g.link(src, rng.next_u64() % nodes, FOLLOWS, 0);
1007            }
1008        }
1009        let first = g.neighbours(0, FOLLOWS, Dir::Out).to_vec();
1010        for hop in &first {
1011            g.prefetch(*hop, FOLLOWS, Dir::Out);
1012        }
1013        let mut seen = Vec::new();
1014        for hop in &first {
1015            seen.extend_from_slice(g.neighbours(*hop, FOLLOWS, Dir::Out));
1016        }
1017        assert_eq!(seen.len(), 64);
1018        // Every edge is at both ends, so everything the walk reached agrees it
1019        // was reached from where the walk was standing.
1020        for (i, hop) in first.iter().enumerate() {
1021            for dst in &seen[i * 8..(i + 1) * 8] {
1022                assert!(g.neighbours(*dst, FOLLOWS, Dir::In).contains(hop));
1023            }
1024        }
1025    }
1026
1027    #[test]
1028    fn the_plane_agrees_with_a_list_of_what_was_done_to_it() {
1029        // The reference is a plain vector per node, which is obviously right
1030        // and obviously too expensive, and the point is that the plane matches
1031        // it over a mix of links and unlinks that crosses every capacity size
1032        // in both directions.
1033        let mut g = Adjacency::new();
1034        let mut want: Vec<Vec<u64>> = vec![Vec::new(); 64];
1035        let mut rng = Rng::new(0xbeef);
1036        // Sixty four nodes either way, so the mix still crosses every capacity
1037        // size in both directions. It is the operations that come down, and
1038        // four thousand of them over sixty four nodes is still an average of
1039        // sixty apiece, which is well past the last doubling.
1040        let ops = if cfg!(miri) { 2_000 } else { 200_000 };
1041        for _ in 0..ops {
1042            let src = rng.next_u64() % 64;
1043            let dst = rng.next_u64() % 64;
1044            if rng.next_u64().is_multiple_of(3) {
1045                if let Some(i) = want[src as usize].iter().position(|n| *n == dst) {
1046                    want[src as usize].swap_remove(i);
1047                    assert!(g.unlink(src, dst, FOLLOWS).is_some());
1048                } else {
1049                    assert_eq!(g.unlink(src, dst, FOLLOWS), None);
1050                }
1051            } else {
1052                want[src as usize].push(dst);
1053                g.link(src, dst, FOLLOWS, 0);
1054            }
1055        }
1056        let mut total = 0;
1057        for (src, list) in want.iter().enumerate() {
1058            assert_eq!(
1059                sorted(g.neighbours(src as u64, FOLLOWS, Dir::Out)),
1060                sorted(list),
1061                "node {src}"
1062            );
1063            total += list.len();
1064        }
1065        assert_eq!(g.edges(), total);
1066        // And the incoming side is the transpose of the outgoing one.
1067        let mut incoming: Vec<Vec<u64>> = vec![Vec::new(); 64];
1068        for (src, list) in want.iter().enumerate() {
1069            for dst in list {
1070                incoming[*dst as usize].push(src as u64);
1071            }
1072        }
1073        for (dst, list) in incoming.iter().enumerate() {
1074            assert_eq!(
1075                sorted(g.neighbours(dst as u64, FOLLOWS, Dir::In)),
1076                sorted(list),
1077                "into node {dst}"
1078            );
1079        }
1080    }
1081}