use petgraph::graph::{EdgeIndex, NodeIndex};
#[derive(Debug, Clone)]
pub(crate) struct SlotMirror {
free_nodes: Vec<u32>,
free_edges: Vec<u32>,
synced: bool,
}
impl Default for SlotMirror {
#[inline]
fn default() -> Self {
Self::for_empty_graph()
}
}
impl SlotMirror {
#[inline]
pub(crate) fn for_empty_graph() -> Self {
Self {
free_nodes: Vec::new(),
free_edges: Vec::new(),
synced: true,
}
}
#[inline]
pub(crate) fn for_adopted_graph(
node_count: usize,
node_bound: usize,
edge_count: usize,
edge_bound: usize,
) -> Self {
Self {
free_nodes: Vec::new(),
free_edges: Vec::new(),
synced: node_count == node_bound && edge_count == edge_bound,
}
}
#[inline]
pub(crate) fn predict_next_node(&self, bound: usize) -> Option<NodeIndex> {
if !self.synced {
return None;
}
Some(match self.free_nodes.last() {
Some(&slot) => NodeIndex::new(slot as usize),
None => NodeIndex::new(bound),
})
}
#[inline]
pub(crate) fn predict_next_edge(&self, bound: usize) -> Option<EdgeIndex> {
if !self.synced {
return None;
}
Some(match self.free_edges.last() {
Some(&slot) => EdgeIndex::new(slot as usize),
None => EdgeIndex::new(bound),
})
}
#[inline]
pub(crate) fn note_node_added(&mut self, bound_before: usize, actual: NodeIndex) {
if !self.synced {
return;
}
debug_assert_eq!(
self.predict_next_node(bound_before),
Some(actual),
"slot mirror mispredicted a node slot; the free-list mirror has \
drifted from petgraph (see storage/slot_mirror.rs)"
);
self.free_nodes.pop();
}
#[inline]
pub(crate) fn note_edge_added(&mut self, bound_before: usize, actual: EdgeIndex) {
if !self.synced {
return;
}
debug_assert_eq!(
self.predict_next_edge(bound_before),
Some(actual),
"slot mirror mispredicted an edge slot; the free-list mirror has \
drifted from petgraph (see storage/slot_mirror.rs)"
);
self.free_edges.pop();
}
#[inline]
pub(crate) fn note_node_removed(
&mut self,
idx: NodeIndex,
freed_edges: impl Iterator<Item = EdgeIndex>,
) {
if !self.synced {
return;
}
for edge in freed_edges {
self.free_edges.push(edge.index() as u32);
}
self.free_nodes.push(idx.index() as u32);
}
#[inline]
pub(crate) fn note_edge_removed(&mut self, idx: EdgeIndex) {
if !self.synced {
return;
}
self.free_edges.push(idx.index() as u32);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::datatypes::Value;
use crate::graph::schema::{EdgeData, NodeData};
use crate::graph::storage::interner::StringInterner;
use petgraph::stable_graph::StableDiGraph;
use petgraph::visit::{EdgeIndexable, NodeIndexable};
use std::collections::HashMap;
fn node(i: u64, interner: &mut StringInterner) -> NodeData {
NodeData::new(
Value::Int64(i as i64),
Value::String(format!("n{i}")),
"Item".to_string(),
HashMap::new(),
interner,
)
}
fn edge(interner: &mut StringInterner) -> EdgeData {
EdgeData::new("LINKS".to_string(), HashMap::new(), interner)
}
#[test]
fn the_mirror_predicts_every_slot_petgraph_actually_allocates() {
let mut interner = StringInterner::new();
let mut g: StableDiGraph<NodeData, EdgeData> = StableDiGraph::new();
let mut mirror = SlotMirror::for_empty_graph();
let mut nodes = Vec::new();
for i in 0..6 {
let bound_before = g.node_bound();
let predicted = mirror.predict_next_node(bound_before).expect("synced");
let actual = g.add_node(node(i, &mut interner));
assert_eq!(predicted, actual, "node insert {i}");
mirror.note_node_added(bound_before, actual);
nodes.push(actual);
}
let mut edges = Vec::new();
for pair in [(0, 1), (1, 2), (2, 3), (0, 3), (3, 3)] {
let bound_before = g.edge_bound();
let predicted = mirror.predict_next_edge(bound_before).expect("synced");
let actual = g.add_edge(nodes[pair.0], nodes[pair.1], edge(&mut interner));
assert_eq!(predicted, actual, "edge insert {pair:?}");
mirror.note_edge_added(bound_before, actual);
edges.push(actual);
}
g.remove_edge(edges[1]).expect("edge present");
mirror.note_edge_removed(edges[1]);
let bound_before = g.edge_bound();
let predicted = mirror.predict_next_edge(bound_before).expect("synced");
let actual = g.add_edge(nodes[4], nodes[5], edge(&mut interner));
assert_eq!(predicted, actual, "edge slot must be reused LIFO");
mirror.note_edge_added(bound_before, actual);
let victim = nodes[3];
let freed = crate::graph::storage::impls::freed_edges_for_removal(&g, victim);
g.remove_node(victim).expect("node present");
mirror.note_node_removed(victim, freed.into_iter());
for i in 0..3 {
let bound_before = g.edge_bound();
let predicted = mirror.predict_next_edge(bound_before).expect("synced");
let actual = g.add_edge(nodes[0], nodes[1], edge(&mut interner));
assert_eq!(predicted, actual, "freed edge slot {i} must match petgraph");
mirror.note_edge_added(bound_before, actual);
}
let bound_before = g.node_bound();
let predicted = mirror.predict_next_node(bound_before).expect("synced");
let actual = g.add_node(node(99, &mut interner));
assert_eq!(predicted, actual, "freed node slot must be reused");
mirror.note_node_added(bound_before, actual);
}
#[test]
fn an_adopted_graph_with_holes_refuses_to_predict() {
let compact = SlotMirror::for_adopted_graph(10, 10, 4, 4);
assert_eq!(
compact.predict_next_node(10),
Some(NodeIndex::new(10)),
"a hole-free graph has provably empty free lists, so it may predict"
);
let holed = SlotMirror::for_adopted_graph(9, 10, 4, 4);
assert_eq!(
holed.predict_next_node(10),
None,
"a node hole means an unknown free-list order: refuse, never guess"
);
assert_eq!(holed.predict_next_edge(4), None);
let edge_holed = SlotMirror::for_adopted_graph(10, 10, 3, 4);
assert_eq!(
edge_holed.predict_next_node(10),
None,
"an edge hole counts too"
);
}
}