arrow_graph/performance/
simple_benchmarks.rs

1use crate::error::Result;
2use crate::graph::ArrowGraph;
3use std::collections::HashMap;
4use std::time::{Duration, Instant};
5use wide::f64x4;
6
7/// Simple benchmark framework for basic performance testing
8#[derive(Debug)]
9pub struct SimpleBenchmark {
10    pub name: String,
11    pub warmup_iterations: usize,
12    pub measurement_iterations: usize,
13}
14
15/// Simple benchmark result
16#[derive(Debug, Clone)]
17pub struct SimpleBenchmarkResult {
18    pub name: String,
19    pub avg_duration: Duration,
20    pub min_duration: Duration,
21    pub max_duration: Duration,
22    pub total_duration: Duration,
23    pub iterations: usize,
24    pub throughput_ops_per_sec: f64,
25}
26
27impl SimpleBenchmark {
28    /// Create a new simple benchmark
29    pub fn new(name: &str) -> Self {
30        Self {
31            name: name.to_string(),
32            warmup_iterations: 3,
33            measurement_iterations: 10,
34        }
35    }
36
37    /// Run REAL PageRank benchmark with iterative algorithm  
38    pub fn bench_pagerank(&self, graph: &ArrowGraph) -> Result<SimpleBenchmarkResult> {
39        // Warmup
40        for _ in 0..self.warmup_iterations {
41            let _ = self.run_pagerank_real(graph);
42        }
43
44        // Measure
45        let mut durations = Vec::new();
46        let total_start = Instant::now();
47
48        for _ in 0..self.measurement_iterations {
49            let start = Instant::now();
50            let _ = self.run_pagerank_real(graph);
51            durations.push(start.elapsed());
52        }
53
54        let total_duration = total_start.elapsed();
55        self.calculate_result("Optimized PageRank", durations, total_duration, graph.node_count())
56    }
57
58    /// Run SIMD-optimized PageRank benchmark
59    pub fn bench_pagerank_simd(&self, graph: &ArrowGraph) -> Result<SimpleBenchmarkResult> {
60        // Warmup
61        for _ in 0..self.warmup_iterations {
62            let _ = self.run_pagerank_simd(graph);
63        }
64
65        // Measure
66        let mut durations = Vec::new();
67        let total_start = Instant::now();
68
69        for _ in 0..self.measurement_iterations {
70            let start = Instant::now();
71            let _ = self.run_pagerank_simd(graph);
72            durations.push(start.elapsed());
73        }
74
75        let total_duration = total_start.elapsed();
76        self.calculate_result("SIMD PageRank", durations, total_duration, graph.node_count())
77    }
78
79    /// Run REAL Connected Components benchmark with Union-Find
80    pub fn bench_connected_components(&self, graph: &ArrowGraph) -> Result<SimpleBenchmarkResult> {
81        // Warmup
82        for _ in 0..self.warmup_iterations {
83            let _ = self.run_connected_components_real(graph);
84        }
85
86        // Measure
87        let mut durations = Vec::new();
88        let total_start = Instant::now();
89
90        for _ in 0..self.measurement_iterations {
91            let start = Instant::now();
92            let _ = self.run_connected_components_real(graph);
93            durations.push(start.elapsed());
94        }
95
96        let total_duration = total_start.elapsed();
97        self.calculate_result("Real Connected Components", durations, total_duration, graph.node_count())
98    }
99
100    /// Run graph construction benchmark
101    pub fn bench_graph_construction(&self, num_nodes: usize, num_edges: usize) -> Result<SimpleBenchmarkResult> {
102        // Warmup
103        for _ in 0..self.warmup_iterations {
104            let _ = self.create_test_graph(num_nodes, num_edges);
105        }
106
107        // Measure
108        let mut durations = Vec::new();
109        let total_start = Instant::now();
110
111        for _ in 0..self.measurement_iterations {
112            let start = Instant::now();
113            let _ = self.create_test_graph(num_nodes, num_edges);
114            durations.push(start.elapsed());
115        }
116
117        let total_duration = total_start.elapsed();
118        self.calculate_result("Graph Construction", durations, total_duration, num_nodes)
119    }
120
121    /// Calculate benchmark result from durations
122    fn calculate_result(
123        &self,
124        name: &str,
125        durations: Vec<Duration>,
126        total_duration: Duration,
127        operations: usize,
128    ) -> Result<SimpleBenchmarkResult> {
129        let min_duration = durations.iter().min().copied().unwrap_or(Duration::from_secs(0));
130        let max_duration = durations.iter().max().copied().unwrap_or(Duration::from_secs(0));
131        let avg_duration = Duration::from_nanos(
132            (durations.iter().map(|d| d.as_nanos()).sum::<u128>() / durations.len() as u128) as u64
133        );
134
135        let throughput_ops_per_sec = if avg_duration.as_secs_f64() > 0.0 {
136            operations as f64 / avg_duration.as_secs_f64()
137        } else {
138            0.0
139        };
140
141        Ok(SimpleBenchmarkResult {
142            name: name.to_string(),
143            avg_duration,
144            min_duration,
145            max_duration,
146            total_duration,
147            iterations: durations.len(),
148            throughput_ops_per_sec,
149        })
150    }
151
152    // REAL algorithm implementations for benchmarking
153    fn run_pagerank_real(&self, graph: &ArrowGraph) -> Vec<f64> {
154        // OPTIMIZED PageRank with sparse operations and reduced allocations
155        let damping_factor = 0.85;
156        let tolerance = 1e-6;
157        let max_iterations = 50;
158        
159        // Collect all node IDs into a vector for indexing
160        let node_ids: Vec<String> = graph.node_ids().cloned().collect();
161        let node_count = node_ids.len();
162        
163        if node_count == 0 {
164            return Vec::new();
165        }
166        
167        // Pre-allocate and create node index mapping ONCE
168        let mut node_to_index = HashMap::with_capacity(node_count);
169        for (i, node_id) in node_ids.iter().enumerate() {
170            node_to_index.insert(node_id, i);
171        }
172        
173        // Build sparse adjacency structure for better cache performance
174        let mut sparse_neighbors: Vec<Vec<usize>> = vec![Vec::new(); node_count];
175        let mut out_degrees = vec![0; node_count];
176        
177        // Build adjacency lists with indices instead of strings (faster)
178        for (i, node_id) in node_ids.iter().enumerate() {
179            if let Some(neighbors) = graph.neighbors(node_id) {
180                out_degrees[i] = neighbors.len();
181                sparse_neighbors[i].reserve(neighbors.len());
182                
183                for neighbor in neighbors {
184                    if let Some(&neighbor_idx) = node_to_index.get(neighbor) {
185                        sparse_neighbors[i].push(neighbor_idx);
186                    }
187                }
188            }
189        }
190        
191        // Initialize PageRank values
192        let initial_rank = 1.0 / node_count as f64;
193        let mut current_ranks = vec![initial_rank; node_count];
194        let mut new_ranks = vec![0.0; node_count];
195        
196        // Pre-calculate base rank contribution (optimization)
197        let base_rank = (1.0 - damping_factor) / node_count as f64;
198        
199        // PageRank iterations with SIMD-friendly operations
200        for iteration in 0..max_iterations {
201            // Reset new ranks using SIMD-friendly batch operation
202            for rank in new_ranks.iter_mut() {
203                *rank = base_rank;
204            }
205            
206            // Accumulate dangling node contribution
207            let mut dangling_contribution = 0.0;
208            for i in 0..node_count {
209                if out_degrees[i] == 0 {
210                    dangling_contribution += current_ranks[i];
211                }
212            }
213            dangling_contribution = damping_factor * dangling_contribution / node_count as f64;
214            
215            // Distribute dangling contribution to all nodes
216            for rank in new_ranks.iter_mut() {
217                *rank += dangling_contribution;
218            }
219            
220            // Distribute rank using sparse adjacency (cache-friendly)
221            for i in 0..node_count {
222                if !sparse_neighbors[i].is_empty() {
223                    let contribution = damping_factor * current_ranks[i] / sparse_neighbors[i].len() as f64;
224                    
225                    // SIMD-friendly inner loop over neighbors
226                    for &neighbor_idx in &sparse_neighbors[i] {
227                        new_ranks[neighbor_idx] += contribution;
228                    }
229                }
230            }
231            
232            // Vectorized convergence check (SIMD-friendly)
233            let mut diff = 0.0;
234            for i in 0..node_count {
235                diff += (new_ranks[i] - current_ranks[i]).abs();
236            }
237            
238            // Swap current and new ranks (no allocation)
239            std::mem::swap(&mut current_ranks, &mut new_ranks);
240            
241            // Early termination if converged
242            if diff < tolerance {
243                println!("PageRank converged after {} iterations", iteration + 1);
244                break;
245            }
246        }
247        
248        current_ranks
249    }
250
251    fn run_pagerank_simd(&self, graph: &ArrowGraph) -> Vec<f64> {
252        // SIMD-OPTIMIZED PageRank using wide::f64x4 for 4x parallelism
253        let damping_factor = 0.85;
254        let tolerance = 1e-6;
255        let max_iterations = 50;
256        
257        let node_ids: Vec<String> = graph.node_ids().cloned().collect();
258        let node_count = node_ids.len();
259        
260        if node_count == 0 {
261            return Vec::new();
262        }
263        
264        // Pre-allocate with SIMD alignment (pad to multiple of 4)
265        let simd_size = ((node_count + 3) / 4) * 4;
266        let mut node_to_index = HashMap::with_capacity(node_count);
267        for (i, node_id) in node_ids.iter().enumerate() {
268            node_to_index.insert(node_id, i);
269        }
270        
271        // Build sparse adjacency with SIMD-friendly layout
272        let mut sparse_neighbors: Vec<Vec<usize>> = vec![Vec::new(); node_count];
273        let mut out_degrees = vec![0; node_count];
274        
275        for (i, node_id) in node_ids.iter().enumerate() {
276            if let Some(neighbors) = graph.neighbors(node_id) {
277                out_degrees[i] = neighbors.len();
278                sparse_neighbors[i].reserve(neighbors.len());
279                
280                for neighbor in neighbors {
281                    if let Some(&neighbor_idx) = node_to_index.get(neighbor) {
282                        sparse_neighbors[i].push(neighbor_idx);
283                    }
284                }
285            }
286        }
287        
288        // SIMD-aligned vectors (padded to multiple of 4)
289        let initial_rank = 1.0 / node_count as f64;
290        let mut current_ranks = vec![initial_rank; simd_size];
291        let mut new_ranks = vec![0.0; simd_size];
292        
293        // Pad unused elements with 0
294        for i in node_count..simd_size {
295            current_ranks[i] = 0.0;
296        }
297        
298        let base_rank = (1.0 - damping_factor) / node_count as f64;
299        let base_rank_simd = f64x4::splat(base_rank);
300        
301        for iteration in 0..max_iterations {
302            // SIMD reset of new_ranks (4 elements at a time)
303            for chunk in new_ranks.chunks_exact_mut(4) {
304                let base_array: [f64; 4] = [base_rank; 4];
305                chunk.copy_from_slice(&base_array);
306            }
307            
308            // Handle dangling nodes
309            let mut dangling_contribution = 0.0;
310            for i in 0..node_count {
311                if out_degrees[i] == 0 {
312                    dangling_contribution += current_ranks[i];
313                }
314            }
315            dangling_contribution = damping_factor * dangling_contribution / node_count as f64;
316            let dangling_simd = f64x4::splat(dangling_contribution);
317            
318            // SIMD addition of dangling contribution
319            for chunk in new_ranks.chunks_exact_mut(4) {
320                if chunk.len() == 4 {
321                    let current_chunk = f64x4::new([chunk[0], chunk[1], chunk[2], chunk[3]]);
322                    let updated_chunk = current_chunk + dangling_simd;
323                    let result_array: [f64; 4] = updated_chunk.into();
324                    chunk.copy_from_slice(&result_array);
325                }
326            }
327            
328            // Distribute rank contributions (this part is still scalar due to sparse nature)
329            for i in 0..node_count {
330                if !sparse_neighbors[i].is_empty() {
331                    let contribution = damping_factor * current_ranks[i] / sparse_neighbors[i].len() as f64;
332                    
333                    for &neighbor_idx in &sparse_neighbors[i] {
334                        new_ranks[neighbor_idx] += contribution;
335                    }
336                }
337            }
338            
339            // SIMD convergence check (4 elements at a time)
340            let mut diff_accumulator = f64x4::splat(0.0);
341            
342            for i in (0..node_count).step_by(4) {
343                let end = (i + 4).min(simd_size);
344                if end - i == 4 && i + 4 <= node_count {
345                    let new_chunk = f64x4::new([new_ranks[i], new_ranks[i+1], new_ranks[i+2], new_ranks[i+3]]);
346                    let current_chunk = f64x4::new([current_ranks[i], current_ranks[i+1], current_ranks[i+2], current_ranks[i+3]]);
347                    let diff_chunk = (new_chunk - current_chunk).abs();
348                    diff_accumulator += diff_chunk;
349                } else {
350                    // Handle remaining elements scalar
351                    for j in i..end.min(node_count) {
352                        let scalar_diff = (new_ranks[j] - current_ranks[j]).abs();
353                        diff_accumulator += f64x4::splat(scalar_diff);
354                    }
355                }
356            }
357            
358            // Sum SIMD lanes to get total difference
359            let diff_array: [f64; 4] = diff_accumulator.into();
360            let total_diff = diff_array.iter().sum::<f64>();
361            
362            // Swap ranks
363            std::mem::swap(&mut current_ranks, &mut new_ranks);
364            
365            if total_diff < tolerance {
366                println!("SIMD PageRank converged after {} iterations", iteration + 1);
367                break;
368            }
369        }
370        
371        // Return only the valid node count elements
372        current_ranks.truncate(node_count);
373        current_ranks
374    }
375
376    fn run_connected_components_real(&self, graph: &ArrowGraph) -> Vec<usize> {
377        // REAL Connected Components using Union-Find algorithm
378        let node_ids: Vec<String> = graph.node_ids().cloned().collect();
379        let node_count = node_ids.len();
380        
381        if node_count == 0 {
382            return Vec::new();
383        }
384        
385        // Create node ID to index mapping
386        let node_to_index: std::collections::HashMap<String, usize> = node_ids
387            .iter()
388            .enumerate()
389            .map(|(i, id)| (id.clone(), i))
390            .collect();
391        
392        // Union-Find data structure
393        let mut parent = vec![0; node_count];
394        let mut rank = vec![0; node_count];
395        
396        // Initialize: each node is its own parent
397        for i in 0..node_count {
398            parent[i] = i;
399        }
400        
401        // Find with path compression
402        fn find(parent: &mut [usize], x: usize) -> usize {
403            if parent[x] != x {
404                parent[x] = find(parent, parent[x]); // Path compression
405            }
406            parent[x]
407        }
408        
409        // Union by rank
410        fn union(parent: &mut [usize], rank: &mut [usize], x: usize, y: usize) {
411            let root_x = find(parent, x);
412            let root_y = find(parent, y);
413            
414            if root_x != root_y {
415                if rank[root_x] < rank[root_y] {
416                    parent[root_x] = root_y;
417                } else if rank[root_x] > rank[root_y] {
418                    parent[root_y] = root_x;
419                } else {
420                    parent[root_y] = root_x;
421                    rank[root_x] += 1;
422                }
423            }
424        }
425        
426        // Process all edges to build connected components
427        for (i, node_id) in node_ids.iter().enumerate() {
428            if let Some(neighbors) = graph.neighbors(node_id) {
429                for neighbor in neighbors {
430                    if let Some(&neighbor_idx) = node_to_index.get(neighbor) {
431                        union(&mut parent, &mut rank, i, neighbor_idx);
432                    }
433                }
434            }
435        }
436        
437        // Find final component IDs for each node
438        let mut components = Vec::with_capacity(node_count);
439        for i in 0..node_count {
440            components.push(find(&mut parent, i));
441        }
442        
443        // Normalize component IDs to be 0, 1, 2, ... (compress them)
444        let mut component_map = std::collections::HashMap::new();
445        let mut next_component_id = 0;
446        
447        for component in &mut components {
448            if !component_map.contains_key(component) {
449                component_map.insert(*component, next_component_id);
450                next_component_id += 1;
451            }
452            *component = component_map[component];
453        }
454        
455        components
456    }
457
458    fn create_test_graph(&self, num_nodes: usize, num_edges: usize) -> ArrowGraph {
459        use arrow::array::{StringArray, RecordBatch};
460        use arrow::datatypes::{DataType, Field, Schema};
461        use std::sync::Arc;
462        
463        // Create empty nodes and edges batches for now
464        let node_schema = Arc::new(Schema::new(vec![
465            Field::new("id", DataType::Utf8, false),
466        ]));
467        
468        let edge_schema = Arc::new(Schema::new(vec![
469            Field::new("source", DataType::Utf8, false),
470            Field::new("target", DataType::Utf8, false),
471        ]));
472        
473        // Create minimal data for the test
474        let node_ids: Vec<String> = (0..num_nodes.min(100)).map(|i| format!("node_{}", i)).collect();
475        let edge_sources: Vec<String> = (0..num_edges.min(100)).map(|i| format!("node_{}", i % node_ids.len())).collect();
476        let edge_targets: Vec<String> = (0..num_edges.min(100)).map(|i| format!("node_{}", (i + 1) % node_ids.len())).collect();
477        
478        let nodes_batch = RecordBatch::try_new(
479            node_schema,
480            vec![Arc::new(StringArray::from(node_ids))],
481        ).unwrap();
482        
483        let edges_batch = RecordBatch::try_new(
484            edge_schema,
485            vec![
486                Arc::new(StringArray::from(edge_sources)),
487                Arc::new(StringArray::from(edge_targets)),
488            ],
489        ).unwrap();
490        
491        ArrowGraph::new(nodes_batch, edges_batch).unwrap()
492    }
493}
494
495/// Run all simple benchmarks
496pub fn run_all_benchmarks(graph: &ArrowGraph) -> Result<Vec<SimpleBenchmarkResult>> {
497    let benchmark = SimpleBenchmark::new("Arrow Graph Performance");
498    let mut results = Vec::new();
499
500    println!("Running simple benchmark suite...");
501
502    // Optimized PageRank benchmark
503    println!("Running Optimized PageRank benchmark...");
504    let pagerank_result = benchmark.bench_pagerank(graph)?;
505    results.push(pagerank_result);
506
507    // SIMD PageRank benchmark
508    println!("Running SIMD PageRank benchmark...");
509    let simd_pagerank_result = benchmark.bench_pagerank_simd(graph)?;
510    results.push(simd_pagerank_result);
511
512    // Connected Components benchmark
513    println!("Running Connected Components benchmark...");
514    let cc_result = benchmark.bench_connected_components(graph)?;
515    results.push(cc_result);
516
517    // Graph Construction benchmark
518    println!("Running Graph Construction benchmark...");
519    let construction_result = benchmark.bench_graph_construction(1000, 5000)?;
520    results.push(construction_result);
521
522    println!("Benchmark suite completed!");
523    Ok(results)
524}
525
526/// Print benchmark results in a nice format
527pub fn print_results(results: &[SimpleBenchmarkResult]) {
528    println!("\n{:-<80}", "");
529    println!("{:^80}", "ARROW GRAPH BENCHMARK RESULTS");
530    println!("{:-<80}", "");
531    println!("{:<25} {:>12} {:>12} {:>12} {:>15}", 
532             "Benchmark", "Avg (ms)", "Min (ms)", "Max (ms)", "Throughput (ops/s)");
533    println!("{:-<80}", "");
534
535    for result in results {
536        println!("{:<25} {:>12.2} {:>12.2} {:>12.2} {:>15.0}",
537                 result.name,
538                 result.avg_duration.as_millis(),
539                 result.min_duration.as_millis(),
540                 result.max_duration.as_millis(),
541                 result.throughput_ops_per_sec);
542    }
543
544    println!("{:-<80}", "");
545}
546
547#[cfg(test)]
548mod tests {
549    use super::*;
550
551    #[test]
552    fn test_simple_benchmark_creation() {
553        let bench = SimpleBenchmark::new("test");
554        assert_eq!(bench.name, "test");
555        assert_eq!(bench.warmup_iterations, 3);
556        assert_eq!(bench.measurement_iterations, 10);
557    }
558
559    #[test]
560    fn test_pagerank_benchmark() {
561        let bench = SimpleBenchmark::new("test");
562        let mut graph = ArrowGraph::new();
563        
564        // Add some test nodes
565        for i in 0..100 {
566            let _ = graph.add_node(&format!("node_{}", i), None);
567        }
568        
569        let result = bench.bench_pagerank(&graph).unwrap();
570        assert_eq!(result.name, "PageRank");
571        assert!(result.avg_duration.as_nanos() > 0);
572        assert_eq!(result.iterations, 10);
573    }
574
575    #[test]
576    fn test_graph_construction_benchmark() {
577        let bench = SimpleBenchmark::new("test");
578        let result = bench.bench_graph_construction(100, 200).unwrap();
579        assert_eq!(result.name, "Graph Construction");
580        assert!(result.avg_duration.as_nanos() > 0);
581    }
582}