use std::collections::HashMap;
use std::hint::black_box;
use std::time::Instant;
use super::DirGraph;
use crate::graph::schema::EmbeddingStore;
use crate::graph::session::execute::{execute_mut, ExecuteOptions};
use crate::graph::storage::GraphRead;
const NODES: usize = 1_000_000;
const EMBED_DIM: usize = 64;
const REPS: usize = 3;
fn run(graph: &mut DirGraph, query: &str) {
let params = HashMap::new();
execute_mut(graph, query, &ExecuteOptions::eager(¶ms))
.unwrap_or_else(|e| panic!("fixture query failed: {query}: {e}"));
}
fn min_clone_us<T: Clone>(value: &T, reps: usize) -> f64 {
let mut best = f64::MAX;
for _ in 0..reps {
let started = Instant::now();
let copy = value.clone();
let elapsed = started.elapsed().as_secs_f64() * 1e6;
drop(black_box(copy));
best = best.min(elapsed);
}
best
}
struct FieldCost {
name: &'static str,
micros: f64,
extent: usize,
}
fn apportion(graph: &DirGraph) -> (Vec<FieldCost>, f64) {
let fields = vec![
FieldCost {
name: "graph (backend)",
micros: min_clone_us(&graph.graph, REPS),
extent: graph.graph.node_count(),
},
FieldCost {
name: "type_indices",
micros: min_clone_us(&graph.type_indices, REPS),
extent: graph.graph.node_count(),
},
FieldCost {
name: "id_indices",
micros: min_clone_us(&graph.id_indices, REPS),
extent: graph.id_indices.overlay_len("Item").unwrap_or(0),
},
FieldCost {
name: "property_indices",
micros: min_clone_us(&graph.property_indices, REPS),
extent: graph.property_indices.values().map(|m| m.len()).sum(),
},
FieldCost {
name: "composite_indices",
micros: min_clone_us(&graph.composite_indices, REPS),
extent: graph.composite_indices.values().map(|m| m.len()).sum(),
},
FieldCost {
name: "range_indices",
micros: min_clone_us(&graph.range_indices, REPS),
extent: graph.range_indices.values().map(|m| m.len()).sum(),
},
FieldCost {
name: "secondary_label_index",
micros: min_clone_us(&graph.secondary_label_index, REPS),
extent: graph.secondary_label_index.values().map(|v| v.len()).sum(),
},
FieldCost {
name: "embeddings",
micros: min_clone_us(&graph.embeddings, REPS),
extent: graph
.embeddings
.values()
.map(|s| s.slot_to_node.len())
.sum(),
},
FieldCost {
name: "timeseries_store",
micros: min_clone_us(&graph.timeseries_store, REPS),
extent: graph.timeseries_store.len(),
},
FieldCost {
name: "unique_indices",
micros: min_clone_us(&graph.unique_indices, REPS),
extent: graph.unique_indices.values().map(|m| m.len()).sum(),
},
];
let total = min_clone_us(graph, REPS);
(fields, total)
}
fn report(fixture: &str, graph: &DirGraph) {
let (fields, total) = apportion(graph);
let sum: f64 = fields.iter().map(|f| f.micros).sum();
println!(
"\n### fixture `{fixture}` — nodes {}, columnar {}",
graph.graph.node_count(),
graph.is_columnar()
);
println!("| field | µs | % of total | extent |");
println!("|---|---:|---:|---:|");
for field in &fields {
println!(
"| `{}` | {:.0} | {:.1}% | {} |",
field.name,
field.micros,
100.0 * field.micros / total,
field.extent
);
}
println!(
"| **sum of the ten** | **{sum:.0}** | **{:.1}%** | |",
100.0 * sum / total
);
println!("| **whole `DirGraph::clone`** | **{total:.0}** | 100% | |");
println!(
"| *residue (O(schema) shell)* | {:.0} | {:.1}% | |",
total - sum,
100.0 * (total - sum) / total
);
}
fn build_plain(nodes: usize) -> DirGraph {
let mut graph = DirGraph::new();
run(
&mut graph,
&format!(
"UNWIND range(0, {}) AS i CREATE (:Item {{id: i, name: 'item-' + toString(i), \
code: 'code-' + toString(i), qty: i % 977}})",
nodes - 1
),
);
run(&mut graph, "MATCH (n:Item {id: 0}) RETURN n.id");
assert_eq!(
graph.graph.node_count(),
nodes,
"fixture must have the nodes it claims"
);
assert!(
graph.id_indices.overlay_len("Item").unwrap_or(0) > 0,
"the id index must be warm, or its row is vacuously zero"
);
graph
}
#[test]
#[ignore = "measurement instrument: builds 1M-node fixtures. \
cargo test -p kglite --release fork_apportionment -- --ignored --nocapture"]
fn apportion_the_fork_across_the_data_scale_fields() {
if cfg!(debug_assertions) {
panic!("run this in release; a debug-profile clone time is not a measurement (CLAUDE.md)");
}
let base = build_plain(NODES);
report("plain", &base);
{
let mut saved = build_plain(NODES);
saved.enable_columnar();
assert!(
saved.is_columnar(),
"the saved fixture must own master column stores"
);
report("saved (enable_columnar, as save() does)", &saved);
}
{
let mut indexed = build_plain(NODES);
indexed.create_index("Item", "code");
indexed.create_index("Item", "qty");
indexed.create_range_index("Item", "qty");
indexed.create_composite_index("Item", &["code", "qty"]);
assert!(
!indexed.property_indices.is_empty()
&& !indexed.range_indices.is_empty()
&& !indexed.composite_indices.is_empty(),
"all three user-index families must be live or the indexed rows are vacuous"
);
report("indexed (2 property + 1 range + 1 composite)", &indexed);
}
{
let mut labelled = build_plain(NODES);
run(&mut labelled, "MATCH (n:Item) SET n:Featured");
assert!(
!labelled.secondary_label_index.is_empty(),
"the labelled fixture must populate secondary_label_index"
);
report("labelled (one secondary label on every node)", &labelled);
}
{
let mut embedded = build_plain(NODES);
let mut store = EmbeddingStore::new(EMBED_DIM);
let vector: Vec<f32> = (0..EMBED_DIM).map(|i| i as f32 * 0.01).collect();
for idx in 0..NODES {
store.set_embedding(idx, &vector);
}
assert_eq!(
store.slot_to_node.len(),
NODES,
"every node must carry an embedding"
);
embedded
.embeddings
.insert(("Item".to_string(), "name".to_string()), store);
report(
&format!("embeddings (dim {EMBED_DIM}, no HNSW index)"),
&embedded,
);
}
}