use fnx_classes::digraph::{DiGraph, MultiDiGraph};
use fnx_classes::{AttrMap, Graph, MultiGraph};
use fnx_runtime::CgseValue;
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};
static ALLOCS: AtomicUsize = AtomicUsize::new(0);
struct CountingAllocator;
unsafe impl GlobalAlloc for CountingAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
ALLOCS.fetch_add(1, Ordering::Relaxed);
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) }
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
ALLOCS.fetch_add(1, Ordering::Relaxed);
unsafe { System.alloc_zeroed(layout) }
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
ALLOCS.fetch_add(1, Ordering::Relaxed);
unsafe { System.realloc(ptr, layout, new_size) }
}
}
#[global_allocator]
static ALLOCATOR: CountingAllocator = CountingAllocator;
fn allocs_per_call(rounds: usize, calls: usize, mut body: impl FnMut()) -> f64 {
let mut best = usize::MAX;
for _ in 0..rounds {
let start = ALLOCS.load(Ordering::Relaxed);
for _ in 0..calls {
body();
}
let used = ALLOCS.load(Ordering::Relaxed) - start;
best = best.min(used);
}
#[allow(clippy::cast_precision_loss)]
{
best as f64 / calls as f64
}
}
#[test]
#[ignore = "profiling target; run under callgrind, see the doc comment"]
fn add_edge_profile_target() {
const CALLS: usize = 100_000;
let mut graph = Graph::strict();
for index in 0..1_000_usize {
graph
.add_edge_with_attrs(index.to_string(), (index + 1).to_string(), weight_attrs())
.expect("path construction is allowed in strict mode");
}
for _ in 0..CALLS {
graph
.add_edge_with_attrs("10", "11", weight_attrs())
.expect("re-adding an existing edge is allowed");
}
assert_eq!(graph.node_count(), 1_001);
}
#[test]
#[ignore = "profiling target; run under callgrind, see the doc comment"]
fn add_edge_build_profile_target() {
const EDGES: usize = 100_000;
let keys: Vec<String> = (0..=EDGES).map(|index| index.to_string()).collect();
let mut graph = Graph::strict();
for index in 0..EDGES {
graph
.add_edge_with_attrs(&keys[index], &keys[index + 1], weight_attrs())
.expect("path construction is allowed in strict mode");
}
assert_eq!(graph.node_count(), EDGES + 1);
}
#[test]
#[ignore = "profiling target; run under callgrind, see the doc comment"]
fn multigraph_build_profile_target() {
const EDGES: usize = 100_000;
let keys: Vec<String> = (0..=EDGES).map(|index| index.to_string()).collect();
let mut graph = MultiGraph::strict();
for index in 0..EDGES {
graph
.add_edge_with_attrs(&keys[index], &keys[index + 1], weight_attrs())
.expect("path construction is allowed in strict mode");
}
assert_eq!(graph.node_count(), EDGES + 1);
}
#[test]
#[ignore = "profiling target; run under callgrind, see the doc comment"]
fn multidigraph_build_profile_target() {
const EDGES: usize = 100_000;
let keys: Vec<String> = (0..=EDGES).map(|index| index.to_string()).collect();
let mut graph = MultiDiGraph::strict();
for index in 0..EDGES {
graph
.add_edge_with_attrs(&keys[index], &keys[index + 1], weight_attrs())
.expect("path construction is allowed in strict mode");
}
assert_eq!(graph.node_count(), EDGES + 1);
}
fn weight_attrs() -> AttrMap {
let mut attrs = AttrMap::new();
attrs.insert("weight".to_owned(), CgseValue::Int(1));
attrs
}
#[test]
fn mutation_entry_allocation_census() {
const ROUNDS: usize = 9;
const CALLS: usize = 2_000;
let mut graph = Graph::strict();
for index in 0..1_000_usize {
graph
.add_edge_with_attrs(index.to_string(), (index + 1).to_string(), weight_attrs())
.expect("path construction is allowed in strict mode");
}
let edge_noop = allocs_per_call(ROUNDS, CALLS, || {
graph
.add_edge_with_attrs("10", "11", weight_attrs())
.expect("re-adding an existing edge is allowed");
});
let node_noop = allocs_per_call(ROUNDS, CALLS, || {
graph.add_node_with_attrs("10", AttrMap::new());
});
let read_control = allocs_per_call(ROUNDS, CALLS, || {
assert!(graph.has_node("10"));
});
let caller_attrs = allocs_per_call(ROUNDS, CALLS, || {
let attrs = weight_attrs();
assert_eq!(attrs.len(), 1);
});
let mut owned_keys: Vec<String> = (0..ROUNDS * CALLS).map(|_| "10".to_owned()).collect();
let node_noop_owned_key = allocs_per_call(ROUNDS, CALLS, || {
let key = owned_keys
.pop()
.expect("one pre-built key per counted call");
graph.add_node_with_attrs(key, AttrMap::new());
});
let mut digraph = DiGraph::strict();
for index in 0..1_000_usize {
digraph
.add_edge_with_attrs(index.to_string(), (index + 1).to_string(), weight_attrs())
.expect("path construction is allowed in strict mode");
}
let di_edge_noop = allocs_per_call(ROUNDS, CALLS, || {
digraph
.add_edge_with_attrs("10", "11", weight_attrs())
.expect("re-adding an existing edge is allowed");
});
let di_node_noop = allocs_per_call(ROUNDS, CALLS, || {
digraph.add_node_with_attrs("10", AttrMap::new());
});
let mut multigraph = MultiGraph::strict();
let mut multidigraph = MultiDiGraph::strict();
for index in 0..1_000_usize {
multigraph
.add_edge_with_attrs(index.to_string(), (index + 1).to_string(), weight_attrs())
.expect("path construction is allowed in strict mode");
multidigraph
.add_edge_with_attrs(index.to_string(), (index + 1).to_string(), weight_attrs())
.expect("path construction is allowed in strict mode");
}
let mg_node_noop = allocs_per_call(ROUNDS, CALLS, || {
multigraph.add_node_with_attrs("10", AttrMap::new());
});
let mdg_node_noop = allocs_per_call(ROUNDS, CALLS, || {
multidigraph.add_node_with_attrs("10", AttrMap::new());
});
println!("mutation_entry_allocation_census rounds={ROUNDS} calls={CALLS}");
println!(" add_edge_with_attrs, existing edge : {edge_noop:6.3} allocs/call");
println!(" add_node_with_attrs, existing node : {node_noop:6.3} allocs/call");
println!(" has_node (read control) : {read_control:6.3} allocs/call");
println!(" caller-built AttrMap (charged in) : {caller_attrs:6.3} allocs/call");
println!(" add_node_with_attrs, pre-owned key : {node_noop_owned_key:6.3} allocs/call");
println!(
" -> add_edge entry, caller attrs out: {:6.3} allocs/call",
edge_noop - caller_attrs
);
println!(" DiGraph add_edge, existing edge : {di_edge_noop:6.3} allocs/call");
println!(" DiGraph add_node, existing node : {di_node_noop:6.3} allocs/call");
println!(" MultiGraph add_node, existing : {mg_node_noop:6.3} allocs/call");
println!(" MultiDiGraph add_node, existing : {mdg_node_noop:6.3} allocs/call");
println!(
" -> owning the key costs (2-5) : {:6.3} allocs/call",
node_noop - node_noop_owned_key
);
println!(
" -> DiGraph add_edge entry : {:6.3} allocs/call",
di_edge_noop - caller_attrs
);
assert!(
(edge_noop - caller_attrs - 1.0).abs() < f64::EPSILON,
"add_edge entry allocated {:.3} per call excluding caller attrs, expected 1 \
(the record's evidence Vec; neither endpoint key is owned)",
edge_noop - caller_attrs
);
assert!(
(node_noop - 1.0).abs() < f64::EPSILON,
"add_node entry allocated {node_noop:.3} per call, expected 1 \
(the record's evidence Vec; the occupied path owns no key)"
);
assert!(
(node_noop - node_noop_owned_key).abs() < f64::EPSILON,
"add_node on an EXISTING node must not own its key, so a &str caller \
and a ready-made String caller must cost the same; the gap was {:.3}",
node_noop - node_noop_owned_key
);
assert!(
read_control.abs() < f64::EPSILON,
"has_node is a read and must allocate NOTHING per call: {read_control:.3}"
);
assert!(
(di_edge_noop - caller_attrs - 1.0).abs() < f64::EPSILON,
"DiGraph add_edge entry allocated {:.3} per call excluding caller attrs, expected 1",
di_edge_noop - caller_attrs
);
assert!(
(di_node_noop - 1.0).abs() < f64::EPSILON,
"DiGraph add_node entry allocated {di_node_noop:.3} per call, expected 1"
);
assert!(
(mg_node_noop - 1.0).abs() < f64::EPSILON,
"MultiGraph add_node entry allocated {mg_node_noop:.3} per call, expected 1"
);
assert!(
(mdg_node_noop - 1.0).abs() < f64::EPSILON,
"MultiDiGraph add_node entry allocated {mdg_node_noop:.3} per call, expected 1"
);
}