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        for i in 0..1000u64 {
772            both.link(i, i + 1, FOLLOWS, i as u32);
773            out.link(i, i + 1, FOLLOWS, i as u32);
774        }
775        assert_eq!(out.neighbours(500, FOLLOWS, Dir::Out), &[501]);
776        assert!(out.neighbours(500, FOLLOWS, Dir::In).is_empty());
777        assert_eq!(both.neighbours(500, FOLLOWS, Dir::In), &[499]);
778        assert!(!out.indexes_incoming());
779        assert!(
780            out.bytes() * 3 < both.bytes() * 2,
781            "{} against {}",
782            out.bytes(),
783            both.bytes()
784        );
785    }
786
787    #[test]
788    fn unlinking_takes_the_edge_off_both_ends() {
789        let mut g = Adjacency::new();
790        g.link(1, 2, FOLLOWS, 10);
791        g.link(1, 3, FOLLOWS, 11);
792        assert_eq!(g.unlink(1, 2, FOLLOWS), Some(10));
793        assert_eq!(g.neighbours(1, FOLLOWS, Dir::Out), &[3]);
794        assert!(g.neighbours(2, FOLLOWS, Dir::In).is_empty());
795        assert_eq!(g.edges(), 1);
796        assert_eq!(g.unlink(1, 2, FOLLOWS), None);
797        assert_eq!(g.unlink(9, 9, FOLLOWS), None);
798    }
799
800    #[test]
801    fn a_delete_moves_the_last_edge_into_the_hole() {
802        let mut g = Adjacency::new();
803        for dst in 1..=5u64 {
804            g.link(0, dst, FOLLOWS, dst as u32);
805        }
806        g.unlink(0, 2, FOLLOWS);
807        // The order is gone but nothing else is, and the edge slot went with
808        // the neighbour it belonged to.
809        let n = g.neighbours(0, FOLLOWS, Dir::Out);
810        let e = g.edge_slots(0, FOLLOWS, Dir::Out);
811        assert_eq!(sorted(n), vec![1, 3, 4, 5]);
812        for (i, node) in n.iter().enumerate() {
813            assert_eq!(u64::from(e[i]), *node, "the pairing survived the swap");
814        }
815    }
816
817    #[test]
818    fn unlink_at_moves_the_last_entry_into_the_position_it_took() {
819        let mut g = Adjacency::new();
820        for dst in 1..=4u64 {
821            g.link(0, dst, FOLLOWS, dst as u32);
822        }
823        assert_eq!(g.unlink_at(0, FOLLOWS, Dir::Out, 0), Some((1, 1)));
824        assert_eq!(g.neighbours(0, FOLLOWS, Dir::Out), &[4, 2, 3]);
825        assert_eq!(g.unlink_at(0, FOLLOWS, Dir::Out, 9), None);
826        assert_eq!(g.unlink_at(7, FOLLOWS, Dir::Out, 0), None);
827    }
828
829    #[test]
830    fn a_run_survives_growing_through_every_size_it_passes() {
831        let mut g = Adjacency::out_only();
832        let n = 5000u64;
833        for dst in 0..n {
834            g.link(0, dst, FOLLOWS, dst as u32);
835        }
836        assert_eq!(g.degree(0, FOLLOWS, Dir::Out), n as usize);
837        assert_eq!(
838            g.neighbours(0, FOLLOWS, Dir::Out),
839            (0..n).collect::<Vec<_>>()
840        );
841        assert_eq!(
842            g.edge_slots(0, FOLLOWS, Dir::Out),
843            (0..n as u32).collect::<Vec<_>>()
844        );
845    }
846
847    #[test]
848    fn a_hub_that_empties_gives_its_block_back() {
849        let mut g = Adjacency::out_only();
850        for dst in 0..4000u64 {
851            g.link(0, dst, FOLLOWS, 0);
852        }
853        let full = g.bytes();
854        for dst in 0..4000u64 {
855            assert!(g.unlink(0, dst, FOLLOWS).is_some());
856        }
857        assert_eq!(g.degree(0, FOLLOWS, Dir::Out), 0);
858        assert_eq!(g.runs(), 0);
859        // Filling a second node to the same size reuses what the first gave
860        // back rather than asking the arena for more.
861        for dst in 0..4000u64 {
862            g.link(1, dst, FOLLOWS, 0);
863        }
864        assert!(g.bytes() <= full + full / 4, "{} against {full}", g.bytes());
865    }
866
867    #[test]
868    fn a_run_that_grows_and_shrinks_does_not_copy_itself_on_a_boundary() {
869        // Sixteen is the last doubling, so this sits astride it. What is
870        // checked is that the capacity settles rather than that anything is
871        // fast: an implementation that shrank on the exact fit would move the
872        // run on every one of these, and the arena would grow without end.
873        let mut g = Adjacency::out_only();
874        for dst in 0..16u64 {
875            g.link(0, dst, FOLLOWS, 0);
876        }
877        // The first cycle does grow the run once, from the 16 it fits exactly
878        // into to the 20 above it. Everything after that is the claim.
879        g.link(0, 999, FOLLOWS, 0);
880        g.unlink(0, 999, FOLLOWS);
881        let settled = g.bytes();
882        for _ in 0..100 {
883            g.link(0, 999, FOLLOWS, 0);
884            g.unlink(0, 999, FOLLOWS);
885        }
886        assert_eq!(g.bytes(), settled);
887        assert_eq!(g.degree(0, FOLLOWS, Dir::Out), 16);
888    }
889
890    #[test]
891    fn a_run_that_loses_most_of_itself_gives_the_room_back() {
892        let mut g = Adjacency::out_only();
893        for dst in 0..4000u64 {
894            g.link(0, dst, FOLLOWS, 0);
895        }
896        for dst in 0..3990u64 {
897            g.unlink(0, dst, FOLLOWS);
898        }
899        assert_eq!(g.degree(0, FOLLOWS, Dir::Out), 10);
900        // The run itself came down on the way, without waiting for a sweep.
901        assert!(g.slots.iter().any(|s| s.len == 10 && s.cap <= 16));
902        g.compact();
903        assert_eq!(g.degree(0, FOLLOWS, Dir::Out), 10);
904        assert!(g.bytes() < 4000, "{} bytes for ten edges", g.bytes());
905    }
906
907    #[test]
908    fn compact_drops_the_runs_that_emptied() {
909        let mut g = Adjacency::new();
910        for i in 0..2000u64 {
911            g.link(i, i + 1, FOLLOWS, i as u32);
912        }
913        for i in 0..1990u64 {
914            g.unlink(i, i + 1, FOLLOWS);
915        }
916        assert_eq!(g.runs(), 20);
917        let before = g.bytes();
918        g.compact();
919        assert_eq!(g.runs(), 20);
920        assert_eq!(g.edges(), 10);
921        assert_eq!(g.neighbours(1995, FOLLOWS, Dir::Out), &[1996]);
922        assert_eq!(g.neighbours(1996, FOLLOWS, Dir::In), &[1995]);
923        assert!(g.bytes() * 4 < before, "{} against {before}", g.bytes());
924        // And it is still a working plane afterwards, which is the part a
925        // rebuild is easy to get wrong. This one also has to grow a run whose
926        // capacity the sweep cut to exactly what it held.
927        g.link(1995, 3000, FOLLOWS, 7);
928        assert_eq!(
929            sorted(g.neighbours(1995, FOLLOWS, Dir::Out)),
930            vec![1996, 3000]
931        );
932        assert_eq!(sorted(g.neighbours(3000, FOLLOWS, Dir::In)), vec![1995]);
933    }
934
935    #[test]
936    fn a_hot_run_costs_about_twelve_bytes_an_edge() {
937        // A degree distribution with a tail, because a uniform one hides both
938        // things that could go wrong: the run headers, which a graph of hubs
939        // has too few of to notice, and the capacity slack, which a graph of
940        // leaves never reaches.
941        let mut g = Adjacency::out_only();
942        let mut rng = Rng::new(0x9e3f);
943        let nodes = 200_000u64;
944        let mut edges = 0usize;
945        for src in 0..nodes {
946            let deg = match rng.next_u64() % 1000 {
947                0..=799 => 1 + rng.next_u64() % 4,
948                800..=979 => 5 + rng.next_u64() % 40,
949                _ => 45 + rng.next_u64() % 600,
950            };
951            for _ in 0..deg {
952                g.link(src, rng.next_u64() % nodes, FOLLOWS, 0);
953                edges += 1;
954            }
955        }
956        let per = g.bytes() as f64 / edges as f64;
957        g.compact();
958        let settled = g.bytes() as f64 / edges as f64;
959        // Twelve is the payload and the rest is one 32 byte header per run
960        // against an average degree in the teens, plus the capacity slack.
961        // Both are the price of a structure that inserts and deletes in
962        // constant time, and the cold form is where 8 bits an edge comes from.
963        assert!(per < 19.0, "{per:.2} bytes an edge over {edges} edges");
964        assert!(settled < 16.0, "{settled:.2} bytes an edge once swept");
965    }
966
967    #[test]
968    fn a_two_hop_reaches_what_a_pair_of_one_hops_reaches() {
969        let mut g = Adjacency::new();
970        let mut rng = Rng::new(7);
971        let nodes = 5000u64;
972        for src in 0..nodes {
973            for _ in 0..8 {
974                g.link(src, rng.next_u64() % nodes, FOLLOWS, 0);
975            }
976        }
977        let first = g.neighbours(0, FOLLOWS, Dir::Out).to_vec();
978        for hop in &first {
979            g.prefetch(*hop, FOLLOWS, Dir::Out);
980        }
981        let mut seen = Vec::new();
982        for hop in &first {
983            seen.extend_from_slice(g.neighbours(*hop, FOLLOWS, Dir::Out));
984        }
985        assert_eq!(seen.len(), 64);
986        // Every edge is at both ends, so everything the walk reached agrees it
987        // was reached from where the walk was standing.
988        for (i, hop) in first.iter().enumerate() {
989            for dst in &seen[i * 8..(i + 1) * 8] {
990                assert!(g.neighbours(*dst, FOLLOWS, Dir::In).contains(hop));
991            }
992        }
993    }
994
995    #[test]
996    fn the_plane_agrees_with_a_list_of_what_was_done_to_it() {
997        // The reference is a plain vector per node, which is obviously right
998        // and obviously too expensive, and the point is that the plane matches
999        // it over a mix of links and unlinks that crosses every capacity size
1000        // in both directions.
1001        let mut g = Adjacency::new();
1002        let mut want: Vec<Vec<u64>> = vec![Vec::new(); 64];
1003        let mut rng = Rng::new(0xbeef);
1004        for _ in 0..200_000 {
1005            let src = rng.next_u64() % 64;
1006            let dst = rng.next_u64() % 64;
1007            if rng.next_u64().is_multiple_of(3) {
1008                if let Some(i) = want[src as usize].iter().position(|n| *n == dst) {
1009                    want[src as usize].swap_remove(i);
1010                    assert!(g.unlink(src, dst, FOLLOWS).is_some());
1011                } else {
1012                    assert_eq!(g.unlink(src, dst, FOLLOWS), None);
1013                }
1014            } else {
1015                want[src as usize].push(dst);
1016                g.link(src, dst, FOLLOWS, 0);
1017            }
1018        }
1019        let mut total = 0;
1020        for (src, list) in want.iter().enumerate() {
1021            assert_eq!(
1022                sorted(g.neighbours(src as u64, FOLLOWS, Dir::Out)),
1023                sorted(list),
1024                "node {src}"
1025            );
1026            total += list.len();
1027        }
1028        assert_eq!(g.edges(), total);
1029        // And the incoming side is the transpose of the outgoing one.
1030        let mut incoming: Vec<Vec<u64>> = vec![Vec::new(); 64];
1031        for (src, list) in want.iter().enumerate() {
1032            for dst in list {
1033                incoming[*dst as usize].push(src as u64);
1034            }
1035        }
1036        for (dst, list) in incoming.iter().enumerate() {
1037            assert_eq!(
1038                sorted(g.neighbours(dst as u64, FOLLOWS, Dir::In)),
1039                sorted(list),
1040                "into node {dst}"
1041            );
1042        }
1043    }
1044}