diff --git a/Cargo.lock b/Cargo.lock index c662ee89455..8e6d59cb213 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -188,7 +188,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -212,7 +212,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1042,7 +1042,7 @@ version = "3.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89ec27229c38ed0eb3c0feee3d2c1d6a4379ae44f418a29a658890e062d8f365" dependencies = [ - "darling 0.23.0", + "darling", "ident_case", "prettyplease", "proc-macro2", @@ -1807,18 +1807,8 @@ version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ - "darling_core 0.21.3", - "darling_macro 0.21.3", -] - -[[package]] -name = "darling" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", + "darling_core", + "darling_macro", ] [[package]] @@ -1835,37 +1825,13 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "darling_core" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.114", -] - [[package]] name = "darling_macro" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ - "darling_core 0.21.3", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "darling_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core 0.23.0", + "darling_core", "quote", "syn 2.0.114", ] @@ -1924,7 +1890,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" dependencies = [ "data-encoding", - "syn 2.0.114", + "syn 1.0.109", ] [[package]] @@ -2342,7 +2308,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -3664,6 +3630,7 @@ dependencies = [ "bitvec", "bump-scope", "codspeed-criterion-compat", + "criterion", "derive_more", "ena", "foldhash 0.2.0", @@ -3673,6 +3640,7 @@ dependencies = [ "insta", "lexical", "memchr", + "perf-event", "pretty", "proptest", "rapidfuzz", @@ -4036,7 +4004,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.2", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -4393,7 +4361,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -5551,7 +5519,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -6266,6 +6234,26 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "perf-event" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2529f39584dbaa980f4959510c963ae95f119c127c046012c2834365f694438" +dependencies = [ + "bitflags 2.10.0", + "libc", + "perf-event-open-sys", +] + +[[package]] +name = "perf-event-open-sys" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5f8d1487a4ffa23c80a1c355dd27235f9b66fb71ba0f261eb417e4fe8451347" +dependencies = [ + "libc", +] + [[package]] name = "petgraph" version = "0.7.1" @@ -6707,7 +6695,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" dependencies = [ "heck", - "itertools 0.10.5", + "itertools 0.14.0", "log", "multimap", "petgraph 0.8.3", @@ -6741,7 +6729,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.114", @@ -6877,7 +6865,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.2", + "socket2 0.5.10", "thiserror 2.0.18", "tokio", "tracing", @@ -6914,7 +6902,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.2", + "socket2 0.5.10", "tracing", "windows-sys 0.60.2", ] @@ -7414,7 +7402,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -7858,7 +7846,7 @@ version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" dependencies = [ - "darling 0.21.3", + "darling", "proc-macro2", "quote", "syn 2.0.114", @@ -8211,6 +8199,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", + "quote", "unicode-ident", ] @@ -8360,7 +8349,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -8426,7 +8415,7 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -9735,7 +9724,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 209b2633cc3..3ba6fb68724 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -193,6 +193,7 @@ opentelemetry-semantic-conventions = { version = "0.30.0", default-features = fa opentelemetry_sdk = { version = "0.30.0", default-features = false } owo-colors = { version = "4.2.3", default-features = false } oxc = { version = "0.95.0", default-features = false } +perf-event = { version = "0.4.8", default-features = false } pin-project = { version = "1.1.10", default-features = false } pin-project-lite = { version = "0.2.16", default-features = false } postgres-protocol = { version = "0.6.9", default-features = false } diff --git a/libs/@local/hashql/core/Cargo.toml b/libs/@local/hashql/core/Cargo.toml index 72fc20456e6..1c737ac3a06 100644 --- a/libs/@local/hashql/core/Cargo.toml +++ b/libs/@local/hashql/core/Cargo.toml @@ -46,11 +46,19 @@ workspace = true [dev-dependencies] codspeed-criterion-compat = { workspace = true } +criterion = { workspace = true } insta = { workspace = true } proptest = { workspace = true } rstest = { workspace = true } test-strategy = { workspace = true } +[target.'cfg(target_os = "linux")'.dev-dependencies] +perf-event = { workspace = true } + [[bench]] name = "type_system" harness = false + +[[bench]] +name = "linked_graph" +harness = false diff --git a/libs/@local/hashql/core/benches/linked_graph.rs b/libs/@local/hashql/core/benches/linked_graph.rs new file mode 100644 index 00000000000..89676140c2a --- /dev/null +++ b/libs/@local/hashql/core/benches/linked_graph.rs @@ -0,0 +1,655 @@ +//! Benchmarks for the [`LinkedGraph`] data structure. +//! +//! This benchmark suite measures the performance of various graph operations using +//! instruction counting on Linux (via `perf_event`) with a fallback to wall-clock +//! time on other platforms. +//! +//! # Running benchmarks +//! +//! ```bash +//! # Run all linked graph benchmarks +//! cargo bench --package hashql-core --bench linked_graph +//! +//! # Run specific benchmark group +//! cargo bench --package hashql-core --bench linked_graph -- "node" +//! ``` +//! +//! # Measurement modes +//! +//! On Linux, benchmarks use hardware instruction counting for deterministic results. +//! On other platforms, wall-clock time is used as a fallback. + +use core::hint::black_box; + +use criterion::{BatchSize, BenchmarkId, Criterion, Throughput}; +#[cfg(not(target_os = "linux"))] +use criterion::measurement::WallTime; +use hashql_core::graph::{ + DirectedGraph, LinkedGraph, NodeId, Predecessors, Successors, Traverse, +}; + +// ============================================================================= +// Instruction Counting Measurement (Linux only) +// ============================================================================= + +/// A measurement type that counts CPU instructions on Linux using perf_event, +/// falling back to wall-clock time on other platforms. +#[cfg(target_os = "linux")] +mod instruction_count { + use core::{cell::RefCell, fmt}; + + use criterion::measurement::{Measurement, ValueFormatter}; + use perf_event::{Builder, Counter, events::Hardware}; + + /// Measures CPU instructions executed using Linux perf_event. + /// + /// Uses `RefCell` for interior mutability since criterion's `Measurement` + /// trait requires `&self` methods. + pub(crate) struct InstructionCount { + counter: RefCell, + } + + impl InstructionCount { + /// Creates a new instruction counter. + /// + /// # Panics + /// + /// Panics if the perf_event counter cannot be created (e.g., insufficient + /// permissions or unsupported hardware). + #[must_use] + pub(crate) fn new() -> Self { + let counter = Builder::new() + .kind(Hardware::INSTRUCTIONS) + .build() + .expect( + "failed to create perf_event counter - ensure you have permissions \ + (try: sudo sysctl -w kernel.perf_event_paranoid=0)", + ); + Self { + counter: RefCell::new(counter), + } + } + } + + impl Default for InstructionCount { + fn default() -> Self { + Self::new() + } + } + + impl Measurement for InstructionCount { + type Intermediate = u64; + type Value = u64; + + fn start(&self) -> Self::Intermediate { + let mut counter = self.counter.borrow_mut(); + counter.reset().expect("failed to reset counter"); + counter.enable().expect("failed to enable counter"); + 0 + } + + fn end(&self, _intermediate: Self::Intermediate) -> Self::Value { + let mut counter = self.counter.borrow_mut(); + counter.disable().expect("failed to disable counter"); + counter.read().expect("failed to read counter") + } + + fn add(&self, v1: &Self::Value, v2: &Self::Value) -> Self::Value { + v1 + v2 + } + + fn zero(&self) -> Self::Value { + 0 + } + + fn to_f64(&self, value: &Self::Value) -> f64 { + *value as f64 + } + + fn formatter(&self) -> &dyn ValueFormatter { + &InstructionFormatter + } + } + + /// Formatter for instruction count values. + struct InstructionFormatter; + + impl ValueFormatter for InstructionFormatter { + fn scale_values(&self, _typical_value: f64, values: &mut [f64]) -> &'static str { + // Find the appropriate scale + let max = values.iter().copied().fold(0.0_f64, f64::max); + + let (divisor, unit) = if max >= 1_000_000_000.0 { + (1_000_000_000.0, "Gi") + } else if max >= 1_000_000.0 { + (1_000_000.0, "Mi") + } else if max >= 1_000.0 { + (1_000.0, "Ki") + } else { + (1.0, "") + }; + + for value in values { + *value /= divisor; + } + + unit + } + + fn scale_throughputs( + &self, + _typical_value: f64, + throughput: &criterion::Throughput, + values: &mut [f64], + ) -> &'static str { + match throughput { + criterion::Throughput::Bytes(bytes) + | criterion::Throughput::BytesDecimal(bytes) => { + for value in values { + *value = (*bytes as f64) / *value; + } + "inst/B" + } + criterion::Throughput::Elements(elements) => { + for value in values { + *value /= *elements as f64; + } + "inst/elem" + } + _ => { + // Handle any future throughput variants + "inst" + } + } + } + + fn scale_for_machines(&self, values: &mut [f64]) -> &'static str { + // No scaling needed for machine-readable output + for value in values { + *value = value.round(); + } + "instructions" + } + } + + impl fmt::Debug for InstructionCount { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("InstructionCount").finish_non_exhaustive() + } + } +} + +// ============================================================================= +// Cross-Platform Measurement Selection +// ============================================================================= + +/// The measurement type used for benchmarking. +/// +/// On Linux, this uses hardware instruction counting for deterministic results. +/// On other platforms, wall-clock time is used. +#[cfg(target_os = "linux")] +type BenchMeasurement = instruction_count::InstructionCount; + +#[cfg(not(target_os = "linux"))] +type BenchMeasurement = WallTime; + +/// Creates a new measurement instance appropriate for the current platform. +fn create_measurement() -> BenchMeasurement { + #[cfg(target_os = "linux")] + { + instruction_count::InstructionCount::new() + } + + #[cfg(not(target_os = "linux"))] + { + WallTime + } +} + +/// Creates a criterion instance configured with the appropriate measurement type. +fn create_criterion() -> Criterion { + Criterion::default().with_measurement(create_measurement()) +} + +// ============================================================================= +// Graph Fixtures +// ============================================================================= + +/// Creates a linear chain graph: 0 -> 1 -> 2 -> ... -> n-1 +fn create_chain_graph(size: usize) -> LinkedGraph { + let mut graph = LinkedGraph::new(); + + for i in 0..size { + graph.add_node(i); + } + + for i in 0..(size.saturating_sub(1)) { + graph.add_edge(NodeId::new(i), NodeId::new(i + 1), ()); + } + + graph +} + +/// Creates a complete graph where every node is connected to every other node. +fn create_complete_graph(size: usize) -> LinkedGraph { + let mut graph = LinkedGraph::new(); + + for i in 0..size { + graph.add_node(i); + } + + for i in 0..size { + for j in 0..size { + if i != j { + graph.add_edge(NodeId::new(i), NodeId::new(j), ()); + } + } + } + + graph +} + +/// Creates a binary tree graph. +fn create_binary_tree(depth: usize) -> LinkedGraph { + let mut graph = LinkedGraph::new(); + let node_count = (1 << depth) - 1; // 2^depth - 1 nodes + + for i in 0..node_count { + graph.add_node(i); + } + + // Connect parent i to children 2i+1 and 2i+2 + for i in 0..(node_count / 2) { + let left_child = 2 * i + 1; + let right_child = 2 * i + 2; + + if left_child < node_count { + graph.add_edge(NodeId::new(i), NodeId::new(left_child), ()); + } + if right_child < node_count { + graph.add_edge(NodeId::new(i), NodeId::new(right_child), ()); + } + } + + graph +} + +/// Creates a sparse random-like graph with a fixed number of edges per node. +fn create_sparse_graph(nodes: usize, edges_per_node: usize) -> LinkedGraph { + let mut graph = LinkedGraph::new(); + + for i in 0..nodes { + graph.add_node(i); + } + + // Create deterministic "random" edges + for i in 0..nodes { + for j in 0..edges_per_node { + let target = (i * 7 + j * 13 + 1) % nodes; + if target != i { + graph.add_edge(NodeId::new(i), NodeId::new(target), ()); + } + } + } + + graph +} + +// ============================================================================= +// Node Operation Benchmarks +// ============================================================================= + +fn bench_node_operations(c: &mut Criterion) { + let mut group = c.benchmark_group("node"); + + // Benchmark adding nodes + for size in [10, 100, 1_000, 10_000] { + group.throughput(Throughput::Elements(size as u64)); + + group.bench_with_input(BenchmarkId::new("add", size), &size, |b, &size| { + b.iter_batched( + LinkedGraph::::new, + |mut graph| { + for i in 0..size { + black_box(graph.add_node(i)); + } + graph + }, + BatchSize::SmallInput, + ); + }); + } + + // Benchmark node lookup + for size in [100, 1_000, 10_000] { + group.bench_with_input(BenchmarkId::new("lookup", size), &size, |b, &size| { + let graph = create_chain_graph(size); + let mid = NodeId::new(size / 2); + b.iter(|| black_box(graph.node(mid))); + }); + } + + // Benchmark node iteration + for size in [100, 1_000, 10_000] { + group.throughput(Throughput::Elements(size as u64)); + + group.bench_with_input(BenchmarkId::new("iter", size), &size, |b, &size| { + let graph = create_chain_graph(size); + b.iter(|| { + let mut count = 0; + for node in graph.iter_nodes() { + black_box(node); + count += 1; + } + count + }); + }); + } + + group.finish(); +} + +// ============================================================================= +// Edge Operation Benchmarks +// ============================================================================= + +fn bench_edge_operations(c: &mut Criterion) { + let mut group = c.benchmark_group("edge"); + + // Benchmark adding edges to an existing graph + for size in [10, 100, 500] { + let edge_count = size * (size - 1); + group.throughput(Throughput::Elements(edge_count as u64)); + + group.bench_with_input(BenchmarkId::new("add_complete", size), &size, |b, &size| { + b.iter_batched( + || { + let mut graph = LinkedGraph::::new(); + for i in 0..size { + graph.add_node(i); + } + graph + }, + |mut graph| { + for i in 0..size { + for j in 0..size { + if i != j { + black_box(graph.add_edge(NodeId::new(i), NodeId::new(j), ())); + } + } + } + graph + }, + BatchSize::SmallInput, + ); + }); + } + + // Benchmark edge lookup + for size in [100, 1_000] { + group.bench_with_input(BenchmarkId::new("lookup", size), &size, |b, &size| { + let graph = create_chain_graph(size); + let mid_edge = hashql_core::graph::EdgeId::new(size / 2); + b.iter(|| black_box(graph.edge(mid_edge))); + }); + } + + // Benchmark edge iteration + for size in [100, 1_000, 10_000] { + group.throughput(Throughput::Elements((size - 1) as u64)); + + group.bench_with_input(BenchmarkId::new("iter", size), &size, |b, &size| { + let graph = create_chain_graph(size); + b.iter(|| { + let mut count = 0; + for edge in graph.iter_edges() { + black_box(edge); + count += 1; + } + count + }); + }); + } + + group.finish(); +} + +// ============================================================================= +// Adjacency Iteration Benchmarks +// ============================================================================= + +fn bench_adjacency_iteration(c: &mut Criterion) { + let mut group = c.benchmark_group("adjacency"); + + // Benchmark successors iteration on different graph topologies + group.bench_function("successors/chain", |b| { + let graph = create_chain_graph(1_000); + let mid = NodeId::new(500); + b.iter(|| { + let count: usize = graph.successors(mid).count(); + black_box(count) + }); + }); + + group.bench_function("successors/complete_10", |b| { + let graph = create_complete_graph(10); + let mid = NodeId::new(5); + b.iter(|| { + let count: usize = graph.successors(mid).count(); + black_box(count) + }); + }); + + group.bench_function("successors/complete_50", |b| { + let graph = create_complete_graph(50); + let mid = NodeId::new(25); + b.iter(|| { + let count: usize = graph.successors(mid).count(); + black_box(count) + }); + }); + + // Benchmark predecessors iteration + group.bench_function("predecessors/chain", |b| { + let graph = create_chain_graph(1_000); + let mid = NodeId::new(500); + b.iter(|| { + let count: usize = graph.predecessors(mid).count(); + black_box(count) + }); + }); + + group.bench_function("predecessors/complete_50", |b| { + let graph = create_complete_graph(50); + let mid = NodeId::new(25); + b.iter(|| { + let count: usize = graph.predecessors(mid).count(); + black_box(count) + }); + }); + + // Benchmark incident edges (both directions) + group.bench_function("incident_edges/sparse_out", |b| { + let graph = create_sparse_graph(1_000, 5); + let mid = NodeId::new(500); + b.iter(|| { + let count: usize = graph.outgoing_edges(mid).count(); + black_box(count) + }); + }); + + group.bench_function("incident_edges/sparse_in", |b| { + let graph = create_sparse_graph(1_000, 5); + let mid = NodeId::new(500); + b.iter(|| { + let count: usize = graph.incoming_edges(mid).count(); + black_box(count) + }); + }); + + group.finish(); +} + +// ============================================================================= +// Traversal Benchmarks +// ============================================================================= + +fn bench_traversals(c: &mut Criterion) { + let mut group = c.benchmark_group("traversal"); + + // DFS on chain + for size in [100, 1_000, 10_000] { + group.throughput(Throughput::Elements(size as u64)); + + group.bench_with_input(BenchmarkId::new("dfs/chain", size), &size, |b, &size| { + let graph = create_chain_graph(size); + let start = NodeId::new(0); + b.iter(|| { + let count: usize = graph.depth_first_traversal([start]).count(); + black_box(count) + }); + }); + } + + // DFS on binary tree + for depth in [5, 8, 10] { + let nodes = (1 << depth) - 1; + group.throughput(Throughput::Elements(nodes as u64)); + + group.bench_with_input( + BenchmarkId::new("dfs/binary_tree", depth), + &depth, + |b, &depth| { + let graph = create_binary_tree(depth); + let root = NodeId::new(0); + b.iter(|| { + let count: usize = graph.depth_first_traversal([root]).count(); + black_box(count) + }); + }, + ); + } + + // DFS post-order + group.bench_function("dfs_postorder/binary_tree_8", |b| { + let graph = create_binary_tree(8); + let root = NodeId::new(0); + b.iter(|| { + let count: usize = graph.depth_first_traversal_post_order([root]).count(); + black_box(count) + }); + }); + + // BFS on chain + for size in [100, 1_000, 10_000] { + group.throughput(Throughput::Elements(size as u64)); + + group.bench_with_input(BenchmarkId::new("bfs/chain", size), &size, |b, &size| { + let graph = create_chain_graph(size); + let start = NodeId::new(0); + b.iter(|| { + let count: usize = graph.breadth_first_traversal([start]).count(); + black_box(count) + }); + }); + } + + // BFS on binary tree + group.bench_function("bfs/binary_tree_8", |b| { + let graph = create_binary_tree(8); + let root = NodeId::new(0); + b.iter(|| { + let count: usize = graph.breadth_first_traversal([root]).count(); + black_box(count) + }); + }); + + // Forest traversal (all nodes) + group.bench_function("dfs_forest/sparse_1000", |b| { + let graph = create_sparse_graph(1_000, 3); + b.iter(|| { + let count: usize = graph.depth_first_forest_post_order().count(); + black_box(count) + }); + }); + + group.finish(); +} + +// ============================================================================= +// Mutation Benchmarks +// ============================================================================= + +fn bench_mutations(c: &mut Criterion) { + let mut group = c.benchmark_group("mutation"); + + // Benchmark clear_edges + for size in [100, 1_000] { + group.bench_with_input(BenchmarkId::new("clear_edges", size), &size, |b, &size| { + b.iter_batched( + || create_sparse_graph(size, 5), + |mut graph| { + graph.clear_edges(); + graph + }, + BatchSize::SmallInput, + ); + }); + } + + // Benchmark clear (full reset) + for size in [100, 1_000] { + group.bench_with_input(BenchmarkId::new("clear", size), &size, |b, &size| { + b.iter_batched( + || create_sparse_graph(size, 5), + |mut graph| { + graph.clear(); + graph + }, + BatchSize::SmallInput, + ); + }); + } + + // Benchmark derive (populate from domain) + for size in [100, 1_000, 10_000] { + group.throughput(Throughput::Elements(size as u64)); + + group.bench_with_input(BenchmarkId::new("derive", size), &size, |b, &size| { + use hashql_core::id::IdVec; + + let mut source: IdVec = IdVec::new(); + for i in 0..size { + source.push(i); + } + + b.iter_batched( + || source.clone(), + |source| { + let mut graph = LinkedGraph::::new(); + graph.derive(source.as_slice(), |_id, &value| value); + graph + }, + BatchSize::SmallInput, + ); + }); + } + + group.finish(); +} + +// ============================================================================= +// Entry Point +// ============================================================================= + +fn main() { + let mut criterion = create_criterion(); + + bench_node_operations(&mut criterion); + bench_edge_operations(&mut criterion); + bench_adjacency_iteration(&mut criterion); + bench_traversals(&mut criterion); + bench_mutations(&mut criterion); + + criterion.final_summary(); +}