diff --git a/python/conftest.py b/python/conftest.py index 60e5cb7..3697b25 100644 --- a/python/conftest.py +++ b/python/conftest.py @@ -1,6 +1,7 @@ """ Pytest configuration for building the C extension before tests. """ + import sys import subprocess from pathlib import Path diff --git a/python/coverage.xml b/python/coverage.xml index 0b992ab..025e914 100644 --- a/python/coverage.xml +++ b/python/coverage.xml @@ -1,12 +1,12 @@ - - + + - /Users/kentb/Dropbox/Mac/Documents/augment-projects/BPlusTree3/python/bplustree + /home/runner/work/BPlusTree3/BPlusTree3/python/bplustree - + @@ -90,7 +90,7 @@ - + @@ -116,31 +116,31 @@ - - - + + + - - + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - + + + + @@ -204,8 +204,8 @@ - - + + @@ -219,36 +219,36 @@ - + - - - - - + + + + + - - - - - - - - + + + + + + + + - + - - - + + + @@ -257,20 +257,20 @@ - + - - - - - - - + + + + + + + @@ -326,7 +326,7 @@ - + diff --git a/python/setup.py b/python/setup.py index 3afb676..84f8229 100644 --- a/python/setup.py +++ b/python/setup.py @@ -100,10 +100,10 @@ def get_long_description(): author="Kent Beck", author_email="kent@kentbeck.com", url="https://github.com/KentBeck/BPlusTree3", -project_urls={ -"Homepage": "https://github.com/KentBeck/BPlusTree3", -"Documentation": "https://github.com/KentBeck/BPlusTree3/tree/main/python", -"Repository": "https://github.com/KentBeck/BPlusTree3", + project_urls={ + "Homepage": "https://github.com/KentBeck/BPlusTree3", + "Documentation": "https://github.com/KentBeck/BPlusTree3/tree/main/python", + "Repository": "https://github.com/KentBeck/BPlusTree3", "Issues": "https://github.com/KentBeck/BPlusTree3/issues", "Changelog": "https://github.com/KentBeck/BPlusTree3/blob/main/python/CHANGELOG.md", }, diff --git a/python/tests/test_c_extension.py b/python/tests/test_c_extension.py index 0d60ed1..ab7dc97 100644 --- a/python/tests/test_c_extension.py +++ b/python/tests/test_c_extension.py @@ -15,6 +15,7 @@ try: import bplustree_c + HAS_C_EXTENSION = True except ImportError as e: pytest.skip(f"C extension not available: {e}", allow_module_level=True) diff --git a/python/tests/test_c_extension_comprehensive.py b/python/tests/test_c_extension_comprehensive.py index dfbfbd7..5d789c4 100644 --- a/python/tests/test_c_extension_comprehensive.py +++ b/python/tests/test_c_extension_comprehensive.py @@ -12,6 +12,7 @@ try: import bplustree_c + HAS_C_EXTENSION = True except ImportError as e: pytest.skip(f"C extension not available: {e}", allow_module_level=True) diff --git a/python/tests/test_dictionary_api.py b/python/tests/test_dictionary_api.py index 988ab31..ae5ba9a 100644 --- a/python/tests/test_dictionary_api.py +++ b/python/tests/test_dictionary_api.py @@ -12,13 +12,16 @@ try: # Try to import from installed package first import bplustree + BPlusTreeMap = bplustree.BPlusTreeMap except ImportError: # Fall back to local import if package not installed import sys import os + sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import bplustree + BPlusTreeMap = bplustree.BPlusTreeMap diff --git a/python/tests/test_docstyle.py b/python/tests/test_docstyle.py index b7c912b..c1c51cd 100644 --- a/python/tests/test_docstyle.py +++ b/python/tests/test_docstyle.py @@ -15,7 +15,9 @@ def test_pydocstyle_conformance(): stderr=subprocess.STDOUT, text=True, ) - + # For now, just warn about violations instead of failing if result.returncode != 0: - pytest.skip(f"Docstyle violations found (non-failing for now):\n{result.stdout}") + pytest.skip( + f"Docstyle violations found (non-failing for now):\n{result.stdout}" + ) diff --git a/python/tests/test_iterator_modification_safety.py b/python/tests/test_iterator_modification_safety.py index 574ae35..a6536dc 100644 --- a/python/tests/test_iterator_modification_safety.py +++ b/python/tests/test_iterator_modification_safety.py @@ -14,6 +14,7 @@ try: import bplustree_c + HAS_C_EXTENSION = True except ImportError: HAS_C_EXTENSION = False @@ -308,7 +309,9 @@ def test_modification_counter_wrapping(self): next(keys_iter) tree[i + 10000] = "trigger_invalidation" - with pytest.raises(RuntimeError, match="tree changed size during iteration"): + with pytest.raises( + RuntimeError, match="tree changed size during iteration" + ): next(keys_iter) # Final iteration should work @@ -335,4 +338,5 @@ def test_modification_counter_wrapping(self): except Exception as e: print(f"❌ Test failed: {e}") import traceback + traceback.print_exc() diff --git a/python/tests/test_no_segfaults.py b/python/tests/test_no_segfaults.py index c99a71c..beb4170 100644 --- a/python/tests/test_no_segfaults.py +++ b/python/tests/test_no_segfaults.py @@ -261,10 +261,14 @@ def test_no_segfaults(): print("🎉 NO SEGFAULTS! C extension is memory-safe.") else: print("🚨 CRITICAL: Fix all issues before proceeding!") - assert False, f"CRITICAL: {failed} segfault tests failed - must fix immediately!" - + assert ( + False + ), f"CRITICAL: {failed} segfault tests failed - must fix immediately!" + # Explicitly assert success - assert failed == 0, f"CRITICAL: {failed} segfault tests failed - must fix immediately!" + assert ( + failed == 0 + ), f"CRITICAL: {failed} segfault tests failed - must fix immediately!" if __name__ == "__main__": diff --git a/python/tests/test_node_split_minimal.py b/python/tests/test_node_split_minimal.py index 1ca9fa6..fbb0d8b 100644 --- a/python/tests/test_node_split_minimal.py +++ b/python/tests/test_node_split_minimal.py @@ -12,6 +12,7 @@ try: import bplustree_c + HAS_C_EXTENSION = True except ImportError as e: pytest.skip(f"C extension not available: {e}", allow_module_level=True) @@ -39,7 +40,13 @@ def test_single_node_split_maintains_order(): print(f"Expected: [0, 1, 2, 3, 4]") # THE CRITICAL TEST: keys must be sorted - assert keys == [0, 1, 2, 3, 4], f"Keys not in sorted order after single node split. Got: {keys}" + assert keys == [ + 0, + 1, + 2, + 3, + 4, + ], f"Keys not in sorted order after single node split. Got: {keys}" print("✅ PASSED: Keys in correct order after split") diff --git a/python/tests/test_performance_benchmarks.py b/python/tests/test_performance_benchmarks.py index 46babb7..b11f21e 100644 --- a/python/tests/test_performance_benchmarks.py +++ b/python/tests/test_performance_benchmarks.py @@ -20,154 +20,162 @@ @pytest.mark.slow class TestPerformanceBenchmarks: """Performance benchmark tests with threshold validation.""" - + def test_insertion_performance_small(self): """Test insertion performance for small datasets.""" size = 1000 tree = BPlusTreeMap(capacity=32) - + start_time = time.perf_counter() for i in range(size): tree[i] = f"value_{i}" elapsed = time.perf_counter() - start_time - + # Should complete in reasonable time (< 0.1 seconds) assert elapsed < 0.1, f"Small insertion took {elapsed:.3f}s, expected < 0.1s" - + # Verify all items inserted correctly assert len(tree) == size assert tree[0] == "value_0" assert tree[size - 1] == f"value_{size - 1}" - + def test_insertion_performance_medium(self): """Test insertion performance for medium datasets.""" size = 10000 tree = BPlusTreeMap(capacity=32) - + start_time = time.perf_counter() for i in range(size): tree[i] = f"value_{i}" elapsed = time.perf_counter() - start_time - + # Should complete in reasonable time (< 1 second) assert elapsed < 1.0, f"Medium insertion took {elapsed:.3f}s, expected < 1.0s" - + # Verify correctness assert len(tree) == size - + # Check performance metrics ops_per_second = size / elapsed - assert ops_per_second > 5000, f"Insertion rate {ops_per_second:.0f} ops/s, expected > 5000" - + assert ( + ops_per_second > 5000 + ), f"Insertion rate {ops_per_second:.0f} ops/s, expected > 5000" + def test_bulk_loading_performance(self): """Test bulk loading performance advantage.""" size = 10000 data = [(i, f"value_{i}") for i in range(size)] - + # Test bulk loading start_time = time.perf_counter() tree_bulk = BPlusTreeMap.from_sorted_items(data, capacity=32) bulk_time = time.perf_counter() - start_time - + # Test individual insertion start_time = time.perf_counter() tree_individual = BPlusTreeMap(capacity=32) for k, v in data: tree_individual[k] = v individual_time = time.perf_counter() - start_time - + # Bulk loading should be faster speedup = individual_time / bulk_time assert speedup > 1.5, f"Bulk loading speedup {speedup:.1f}x, expected > 1.5x" - + # Verify both trees have same content assert len(tree_bulk) == len(tree_individual) == size for i in range(size): assert tree_bulk[i] == tree_individual[i] - + def test_lookup_performance(self): """Test lookup performance.""" size = 10000 tree = BPlusTreeMap(capacity=32) - + # Populate tree for i in range(size): tree[i] = f"value_{i}" - + # Test lookup performance lookup_count = 10000 - lookup_keys = list(range(0, size, size // lookup_count)) * (lookup_count // (size // (size // lookup_count)) + 1) + lookup_keys = list(range(0, size, size // lookup_count)) * ( + lookup_count // (size // (size // lookup_count)) + 1 + ) lookup_keys = lookup_keys[:lookup_count] - + start_time = time.perf_counter() for key in lookup_keys: _ = tree[key] elapsed = time.perf_counter() - start_time - + # Should complete lookups quickly assert elapsed < 0.5, f"Lookups took {elapsed:.3f}s, expected < 0.5s" - + # Check lookup rate lookups_per_second = lookup_count / elapsed - assert lookups_per_second > 20000, f"Lookup rate {lookups_per_second:.0f} ops/s, expected > 20000" - + assert ( + lookups_per_second > 20000 + ), f"Lookup rate {lookups_per_second:.0f} ops/s, expected > 20000" + def test_range_query_performance(self): """Test range query performance.""" size = 10000 tree = BPlusTreeMap(capacity=64) # Larger capacity for range queries - + # Populate tree for i in range(size): tree[i] = f"value_{i}" - + # Test range queries of different sizes range_sizes = [10, 100, 1000] - + for range_size in range_sizes: start_key = size // 2 - range_size // 2 end_key = start_key + range_size - + start_time = time.perf_counter() results = list(tree.range(start_key, end_key)) elapsed = time.perf_counter() - start_time - + # Verify results assert len(results) == range_size - + # Performance threshold depends on range size max_time = range_size * 0.001 # 1ms per 1000 items - assert elapsed < max_time, f"Range query ({range_size} items) took {elapsed:.3f}s, expected < {max_time:.3f}s" - + assert ( + elapsed < max_time + ), f"Range query ({range_size} items) took {elapsed:.3f}s, expected < {max_time:.3f}s" + def test_mixed_workload_performance(self): """Test performance with mixed operations.""" tree = BPlusTreeMap(capacity=32) - + # Initial data initial_size = 5000 for i in range(initial_size): tree[i] = f"value_{i}" - + # Mixed workload: 60% lookups, 30% inserts, 10% deletes operations = 10000 lookup_ops = int(operations * 0.6) insert_ops = int(operations * 0.3) delete_ops = int(operations * 0.1) - + start_time = time.perf_counter() - + # Perform mixed operations import random - + # Lookups for _ in range(lookup_ops): key = random.randint(0, initial_size - 1) _ = tree.get(key) - + # Inserts for i in range(insert_ops): key = initial_size + i tree[key] = f"new_value_{key}" - + # Deletes for _ in range(delete_ops): key = random.randint(0, initial_size - 1) @@ -175,165 +183,177 @@ def test_mixed_workload_performance(self): del tree[key] except KeyError: pass - + elapsed = time.perf_counter() - start_time - + # Should handle mixed workload efficiently assert elapsed < 2.0, f"Mixed workload took {elapsed:.3f}s, expected < 2.0s" - + # Check operation rate ops_per_second = operations / elapsed - assert ops_per_second > 5000, f"Mixed workload rate {ops_per_second:.0f} ops/s, expected > 5000" - + assert ( + ops_per_second > 5000 + ), f"Mixed workload rate {ops_per_second:.0f} ops/s, expected > 5000" + def test_capacity_impact_on_performance(self): """Test how node capacity affects performance.""" size = 5000 capacities = [8, 32, 128] insertion_times = {} - + for capacity in capacities: tree = BPlusTreeMap(capacity=capacity) - + start_time = time.perf_counter() for i in range(size): tree[i] = f"value_{i}" elapsed = time.perf_counter() - start_time - + insertion_times[capacity] = elapsed - + # Verify correctness assert len(tree) == size - + # Higher capacity should generally be faster for this size # (fewer node splits and levels) assert insertion_times[32] <= insertion_times[8] * 1.5 assert insertion_times[128] <= insertion_times[32] * 1.2 - + def test_memory_efficiency(self): """Test memory usage efficiency.""" try: import tracemalloc except ImportError: pytest.skip("tracemalloc not available") - + size = 10000 - + tracemalloc.start() - + tree = BPlusTreeMap(capacity=32) for i in range(size): tree[i] = f"value_{i}" - + current, peak = tracemalloc.get_traced_memory() tracemalloc.stop() - + # Memory usage should be reasonable memory_per_item = peak / size - assert memory_per_item < 1000, f"Memory per item {memory_per_item:.0f} bytes, expected < 1000" - + assert ( + memory_per_item < 1000 + ), f"Memory per item {memory_per_item:.0f} bytes, expected < 1000" + total_mb = peak / 1024 / 1024 assert total_mb < 50, f"Total memory {total_mb:.1f} MB, expected < 50 MB" - + def test_sequential_vs_random_insertion(self): """Test performance difference between sequential and random insertion.""" size = 5000 - + # Sequential insertion tree_seq = BPlusTreeMap(capacity=32) start_time = time.perf_counter() for i in range(size): tree_seq[i] = f"value_{i}" sequential_time = time.perf_counter() - start_time - + # Random insertion import random + keys = list(range(size)) random.shuffle(keys) - + tree_rand = BPlusTreeMap(capacity=32) start_time = time.perf_counter() for k in keys: tree_rand[k] = f"value_{k}" random_time = time.perf_counter() - start_time - + # Both should complete in reasonable time - assert sequential_time < 1.0, f"Sequential insertion took {sequential_time:.3f}s" + assert ( + sequential_time < 1.0 + ), f"Sequential insertion took {sequential_time:.3f}s" assert random_time < 2.0, f"Random insertion took {random_time:.3f}s" - + # Sequential should be faster speedup = random_time / sequential_time assert speedup > 1.2, f"Sequential speedup {speedup:.1f}x, expected > 1.2x" - + # Both trees should have same content assert len(tree_seq) == len(tree_rand) == size for i in range(size): assert tree_seq[i] == tree_rand[i] - + def test_large_dataset_scalability(self): """Test scalability with larger datasets.""" # Test with progressively larger datasets sizes = [1000, 5000, 10000] times = [] - + for size in sizes: tree = BPlusTreeMap(capacity=64) - + start_time = time.perf_counter() for i in range(size): tree[i] = f"value_{i}" elapsed = time.perf_counter() - start_time - + times.append(elapsed) - + # Each size should complete in reasonable time max_time = size / 5000 # Should handle at least 5000 ops/sec - assert elapsed < max_time, f"Size {size} took {elapsed:.3f}s, expected < {max_time:.3f}s" - + assert ( + elapsed < max_time + ), f"Size {size} took {elapsed:.3f}s, expected < {max_time:.3f}s" + # Check that time complexity is reasonable (should be roughly O(n log n)) # The ratio of times should grow slower than the ratio of sizes time_ratio_1_2 = times[1] / times[0] size_ratio_1_2 = sizes[1] / sizes[0] - + time_ratio_2_3 = times[2] / times[1] size_ratio_2_3 = sizes[2] / sizes[1] - + # Time should grow slower than linear with size assert time_ratio_1_2 < size_ratio_1_2 * 1.5 assert time_ratio_2_3 < size_ratio_2_3 * 1.5 - + @pytest.mark.slow def test_stress_performance(self): """Stress test with intensive operations.""" tree = BPlusTreeMap(capacity=64) - + # Phase 1: Large insertion size = 50000 start_time = time.perf_counter() for i in range(size): tree[i] = f"value_{i}" insertion_time = time.perf_counter() - start_time - - assert insertion_time < 10.0, f"Large insertion took {insertion_time:.3f}s, expected < 10s" - + + assert ( + insertion_time < 10.0 + ), f"Large insertion took {insertion_time:.3f}s, expected < 10s" + # Phase 2: Many lookups lookup_count = 100000 start_time = time.perf_counter() import random + for _ in range(lookup_count): key = random.randint(0, size - 1) _ = tree[key] lookup_time = time.perf_counter() - start_time - + assert lookup_time < 5.0, f"Many lookups took {lookup_time:.3f}s, expected < 5s" - + # Phase 3: Range queries start_time = time.perf_counter() for i in range(0, size, 1000): list(tree.range(i, i + 100)) range_time = time.perf_counter() - start_time - + assert range_time < 3.0, f"Range queries took {range_time:.3f}s, expected < 3s" - + print(f"Stress test completed:") print(f" Insertion: {insertion_time:.3f}s ({size/insertion_time:.0f} ops/s)") print(f" Lookups: {lookup_time:.3f}s ({lookup_count/lookup_time:.0f} ops/s)") @@ -342,71 +362,79 @@ def test_stress_performance(self): class TestPerformanceRegression: """Tests to detect performance regressions.""" - + def test_baseline_insertion_performance(self): """Baseline test for insertion performance regression detection.""" size = 10000 tree = BPlusTreeMap(capacity=32) - + start_time = time.perf_counter() for i in range(size): tree[i] = f"value_{i}" elapsed = time.perf_counter() - start_time - + # Conservative threshold to catch major regressions max_time = 2.0 # Should be much faster, but allows for slow CI environments - assert elapsed < max_time, f"Insertion baseline exceeded: {elapsed:.3f}s > {max_time}s" - + assert ( + elapsed < max_time + ), f"Insertion baseline exceeded: {elapsed:.3f}s > {max_time}s" + # Store result for comparison (in real CI, this would be persisted) ops_per_second = size / elapsed - assert ops_per_second > 2000, f"Insertion rate too low: {ops_per_second:.0f} ops/s" - + assert ( + ops_per_second > 2000 + ), f"Insertion rate too low: {ops_per_second:.0f} ops/s" + def test_baseline_lookup_performance(self): """Baseline test for lookup performance regression detection.""" size = 10000 tree = BPlusTreeMap(capacity=32) - + # Populate tree for i in range(size): tree[i] = f"value_{i}" - + # Test lookups lookup_count = 10000 start_time = time.perf_counter() for i in range(lookup_count): _ = tree[i % size] elapsed = time.perf_counter() - start_time - + # Conservative threshold max_time = 1.0 - assert elapsed < max_time, f"Lookup baseline exceeded: {elapsed:.3f}s > {max_time}s" - + assert ( + elapsed < max_time + ), f"Lookup baseline exceeded: {elapsed:.3f}s > {max_time}s" + ops_per_second = lookup_count / elapsed assert ops_per_second > 5000, f"Lookup rate too low: {ops_per_second:.0f} ops/s" - + def test_memory_usage_baseline(self): """Baseline test for memory usage regression detection.""" try: import tracemalloc except ImportError: pytest.skip("tracemalloc not available") - + tracemalloc.start() - + size = 10000 tree = BPlusTreeMap(capacity=32) for i in range(size): tree[i] = f"value_{i}" - + current, peak = tracemalloc.get_traced_memory() tracemalloc.stop() - + # Conservative memory threshold max_memory_mb = 100 # Should be much less, but allows for overhead memory_mb = peak / 1024 / 1024 - assert memory_mb < max_memory_mb, f"Memory usage baseline exceeded: {memory_mb:.1f} MB > {max_memory_mb} MB" + assert ( + memory_mb < max_memory_mb + ), f"Memory usage baseline exceeded: {memory_mb:.1f} MB > {max_memory_mb} MB" if __name__ == "__main__": # Run performance tests - pytest.main([__file__, "-v", "-x"]) # Stop on first failure \ No newline at end of file + pytest.main([__file__, "-v", "-x"]) # Stop on first failure diff --git a/python/tests/test_single_array_int_optimization.py b/python/tests/test_single_array_int_optimization.py index 0e4dd26..91d4804 100644 --- a/python/tests/test_single_array_int_optimization.py +++ b/python/tests/test_single_array_int_optimization.py @@ -53,9 +53,9 @@ def insert(self, key: int, value: int) -> bool: # Shift keys self.data[pos + 1 : self.num_keys + 1] = self.data[pos : self.num_keys] # Shift values - self.data[ - self.capacity + pos + 1 : self.capacity + self.num_keys + 1 - ] = self.data[self.capacity + pos : self.capacity + self.num_keys] + self.data[self.capacity + pos + 1 : self.capacity + self.num_keys + 1] = ( + self.data[self.capacity + pos : self.capacity + self.num_keys] + ) # Insert self.data[pos] = key diff --git a/rust/src/bin/delete_profiler.rs b/rust/src/bin/delete_profiler.rs index 6c99ff0..ff32174 100644 --- a/rust/src/bin/delete_profiler.rs +++ b/rust/src/bin/delete_profiler.rs @@ -4,7 +4,7 @@ use std::time::Instant; fn main() { println!("Delete Operation Profiler"); println!("========================"); - + // Test different delete patterns profile_sequential_deletes(); profile_pseudo_random_deletes(); @@ -15,16 +15,16 @@ fn main() { fn profile_sequential_deletes() { println!("\n1. Sequential Delete Pattern"); println!("---------------------------"); - + let mut tree = BPlusTreeMap::new(16).unwrap(); - + // Pre-populate with 100k elements let start = Instant::now(); for i in 0..100_000 { tree.insert(i, format!("value_{}", i)); } println!("Setup time: {:?}", start.elapsed()); - + // Delete first half sequentially let start = Instant::now(); for i in 0..50_000 { @@ -38,14 +38,14 @@ fn profile_sequential_deletes() { fn profile_pseudo_random_deletes() { println!("\n2. Pseudo-Random Delete Pattern"); println!("--------------------------------"); - + let mut tree = BPlusTreeMap::new(16).unwrap(); - + // Pre-populate with 100k elements for i in 0..100_000 { tree.insert(i, format!("value_{}", i)); } - + // Generate pseudo-random delete sequence using simple PRNG let mut keys = Vec::new(); let mut seed = 42u64; @@ -54,7 +54,7 @@ fn profile_pseudo_random_deletes() { let key = (seed % 100_000) as i32; keys.push(key); } - + // Delete using pseudo-random sequence let start = Instant::now(); for key in keys { @@ -68,26 +68,26 @@ fn profile_pseudo_random_deletes() { fn profile_mixed_workload_deletes() { println!("\n3. Mixed Workload with Deletes"); println!("------------------------------"); - + let mut tree = BPlusTreeMap::new(16).unwrap(); let mut seed = 42u64; - + // Initial population for i in 0..50_000 { tree.insert(i, format!("value_{}", i)); } - + let start = Instant::now(); let mut delete_count = 0; let mut insert_count = 0; let mut lookup_count = 0; - + // Mixed operations: 40% lookup, 30% insert, 30% delete for _ in 0..100_000 { seed = seed.wrapping_mul(1103515245).wrapping_add(12345); let op = seed % 100; let key = (seed % 100_000) as i32; - + match op { 0..=39 => { tree.get(&key); @@ -104,11 +104,13 @@ fn profile_mixed_workload_deletes() { _ => unreachable!(), } } - + let total_time = start.elapsed(); println!("Mixed workload time: {:?}", total_time); - println!("Operations: {} lookups, {} inserts, {} deletes", - lookup_count, insert_count, delete_count); + println!( + "Operations: {} lookups, {} inserts, {} deletes", + lookup_count, insert_count, delete_count + ); if delete_count > 0 { println!("Avg delete time: {:?}", total_time / (delete_count as u32)); } @@ -117,23 +119,23 @@ fn profile_mixed_workload_deletes() { fn profile_rebalancing_heavy_deletes() { println!("\n4. Rebalancing-Heavy Delete Pattern"); println!("-----------------------------------"); - + let mut tree = BPlusTreeMap::new(16).unwrap(); - + // Create a tree that will require heavy rebalancing // Insert in a pattern that creates many small nodes for i in 0..100_000 { tree.insert(i * 2, format!("value_{}", i * 2)); } - + // Now delete every other element to force rebalancing let start = Instant::now(); for i in 0..50_000 { tree.remove(&(i * 4)); // Delete every 4th original element } let delete_time = start.elapsed(); - + println!("Rebalancing-heavy delete time: {:?}", delete_time); println!("Avg per delete: {:?}", delete_time / 50_000); println!("Tree size after deletes: {}", tree.len()); -} \ No newline at end of file +} diff --git a/rust/src/bin/detailed_delete_profiler.rs b/rust/src/bin/detailed_delete_profiler.rs index 5abe305..18d0dc3 100644 --- a/rust/src/bin/detailed_delete_profiler.rs +++ b/rust/src/bin/detailed_delete_profiler.rs @@ -1,10 +1,10 @@ use bplustree::BPlusTreeMap; -use std::time::{Duration, Instant}; +use std::time::Instant; fn main() { println!("Detailed Delete Operation Profiler"); println!("=================================="); - + // Run comprehensive delete profiling profile_delete_operations_detailed(); } @@ -12,19 +12,19 @@ fn main() { fn profile_delete_operations_detailed() { println!("\nDetailed Delete Analysis"); println!("========================"); - + // Test different tree sizes to understand scaling let sizes = vec![1_000, 10_000, 50_000, 100_000]; - + for size in sizes { println!("\n--- Tree Size: {} elements ---", size); profile_tree_size(size); } - + // Test different capacities println!("\n--- Capacity Analysis ---"); let capacities = vec![8, 16, 32, 64, 128]; - + for capacity in capacities { println!("\nCapacity: {}", capacity); profile_capacity(capacity); @@ -40,14 +40,14 @@ fn profile_tree_size(size: usize) { } tree }; - + let setup_start = Instant::now(); let _tree = create_tree(); let setup_time = setup_start.elapsed(); - + // Profile different delete patterns let delete_count = size / 4; // Delete 25% of elements - + // 1. Sequential deletes from start let mut tree1 = create_tree(); let start = Instant::now(); @@ -55,7 +55,7 @@ fn profile_tree_size(size: usize) { tree1.remove(&(i as i32)); } let sequential_time = start.elapsed(); - + // 2. Sequential deletes from end let mut tree2 = create_tree(); let start = Instant::now(); @@ -63,7 +63,7 @@ fn profile_tree_size(size: usize) { tree2.remove(&(i as i32)); } let reverse_time = start.elapsed(); - + // 3. Middle deletes (causes most rebalancing) let mut tree3 = create_tree(); let start = Instant::now(); @@ -72,7 +72,7 @@ fn profile_tree_size(size: usize) { tree3.remove(&(i as i32)); } let middle_time = start.elapsed(); - + // 4. Scattered deletes (every nth element) let mut tree4 = create_tree(); let step = size / delete_count; @@ -81,13 +81,29 @@ fn profile_tree_size(size: usize) { tree4.remove(&(i as i32)); } let scattered_time = start.elapsed(); - + println!(" Setup time: {:?}", setup_time); - println!(" Sequential (start): {:?} ({:?}/op)", sequential_time, sequential_time / delete_count as u32); - println!(" Sequential (end): {:?} ({:?}/op)", reverse_time, reverse_time / delete_count as u32); - println!(" Middle deletes: {:?} ({:?}/op)", middle_time, middle_time / delete_count as u32); - println!(" Scattered deletes: {:?} ({:?}/op)", scattered_time, scattered_time / delete_count as u32); - + println!( + " Sequential (start): {:?} ({:?}/op)", + sequential_time, + sequential_time / delete_count as u32 + ); + println!( + " Sequential (end): {:?} ({:?}/op)", + reverse_time, + reverse_time / delete_count as u32 + ); + println!( + " Middle deletes: {:?} ({:?}/op)", + middle_time, + middle_time / delete_count as u32 + ); + println!( + " Scattered deletes: {:?} ({:?}/op)", + scattered_time, + scattered_time / delete_count as u32 + ); + // Analyze which pattern is most expensive let times = vec![ ("Sequential (start)", sequential_time), @@ -95,33 +111,40 @@ fn profile_tree_size(size: usize) { ("Middle", middle_time), ("Scattered", scattered_time), ]; - + let slowest = times.iter().max_by_key(|(_, time)| time).unwrap(); let fastest = times.iter().min_by_key(|(_, time)| time).unwrap(); - + println!(" Slowest: {} ({:?})", slowest.0, slowest.1); println!(" Fastest: {} ({:?})", fastest.0, fastest.1); - println!(" Ratio: {:.2}x", slowest.1.as_nanos() as f64 / fastest.1.as_nanos() as f64); + println!( + " Ratio: {:.2}x", + slowest.1.as_nanos() as f64 / fastest.1.as_nanos() as f64 + ); } fn profile_capacity(capacity: usize) { let mut tree = BPlusTreeMap::new(capacity).unwrap(); let size = 50_000; - + // Pre-populate for i in 0..size { tree.insert(i, format!("value_{}", i)); } - + // Delete middle section (most rebalancing) let delete_count = size / 4; let middle_start = size / 2 - delete_count / 2; - + let start = Instant::now(); for i in middle_start..(middle_start + delete_count) { tree.remove(&i); } let delete_time = start.elapsed(); - - println!(" Delete time: {:?} ({:?}/op)", delete_time, delete_time / delete_count as u32); -} \ No newline at end of file + + println!( + " Delete time: {:?} ({:?}/op)", + delete_time, + delete_time / delete_count as u32 + ); +} diff --git a/rust/src/bin/function_profiler.rs b/rust/src/bin/function_profiler.rs index 5152970..cc3a9f5 100644 --- a/rust/src/bin/function_profiler.rs +++ b/rust/src/bin/function_profiler.rs @@ -1,6 +1,6 @@ use bplustree::BPlusTreeMap; -use std::time::{Duration, Instant}; use std::collections::HashMap; +use std::time::{Duration, Instant}; struct ProfileData { call_count: u64, @@ -18,14 +18,14 @@ impl ProfileData { max_time: Duration::ZERO, } } - + fn record(&mut self, duration: Duration) { self.call_count += 1; self.total_time += duration; self.min_time = self.min_time.min(duration); self.max_time = self.max_time.max(duration); } - + fn avg_time(&self) -> Duration { if self.call_count > 0 { self.total_time / self.call_count as u32 @@ -38,7 +38,7 @@ impl ProfileData { fn main() { println!("Function-Level Delete Profiler"); println!("=============================="); - + // Profile different delete scenarios profile_delete_scenarios(); } @@ -50,7 +50,7 @@ fn profile_delete_scenarios() { ("Rebalancing Heavy", create_rebalancing_workload()), ("Mixed Operations", create_mixed_workload()), ]; - + for (name, workload) in scenarios { println!("\n{}", name); println!("{}", "=".repeat(name.len())); @@ -61,33 +61,36 @@ fn profile_delete_scenarios() { fn profile_workload(workload: Vec) { let mut tree = BPlusTreeMap::new(16).unwrap(); let mut profiles: HashMap = HashMap::new(); - + // Pre-populate tree for i in 0..50_000 { tree.insert(i, format!("value_{}", i)); } - + println!("Executing {} operations...", workload.len()); let total_start = Instant::now(); - + for op in workload { match op { Operation::Delete(key) => { let start = Instant::now(); let result = tree.remove(&key); let duration = start.elapsed(); - - profiles.entry("remove".to_string()) + + profiles + .entry("remove".to_string()) .or_insert_with(ProfileData::new) .record(duration); - + // Track successful vs failed deletes if result.is_some() { - profiles.entry("successful_delete".to_string()) + profiles + .entry("successful_delete".to_string()) .or_insert_with(ProfileData::new) .record(duration); } else { - profiles.entry("failed_delete".to_string()) + profiles + .entry("failed_delete".to_string()) .or_insert_with(ProfileData::new) .record(duration); } @@ -96,8 +99,9 @@ fn profile_workload(workload: Vec) { let start = Instant::now(); tree.insert(key, value); let duration = start.elapsed(); - - profiles.entry("insert".to_string()) + + profiles + .entry("insert".to_string()) .or_insert_with(ProfileData::new) .record(duration); } @@ -105,46 +109,65 @@ fn profile_workload(workload: Vec) { let start = Instant::now(); tree.get(&key); let duration = start.elapsed(); - - profiles.entry("lookup".to_string()) + + profiles + .entry("lookup".to_string()) .or_insert_with(ProfileData::new) .record(duration); } } } - + let total_time = total_start.elapsed(); println!("Total execution time: {:?}", total_time); - + // Print profile results println!("\nFunction Profile Results:"); - println!("{:<20} {:>10} {:>12} {:>12} {:>12} {:>12}", - "Function", "Calls", "Total (μs)", "Avg (μs)", "Min (μs)", "Max (μs)"); + println!( + "{:<20} {:>10} {:>12} {:>12} {:>12} {:>12}", + "Function", "Calls", "Total (μs)", "Avg (μs)", "Min (μs)", "Max (μs)" + ); println!("{}", "-".repeat(80)); - + let mut sorted_profiles: Vec<_> = profiles.iter().collect(); sorted_profiles.sort_by(|a, b| b.1.total_time.cmp(&a.1.total_time)); - + for (name, profile) in sorted_profiles { - println!("{:<20} {:>10} {:>12} {:>12} {:>12} {:>12}", - name, - profile.call_count, - profile.total_time.as_micros(), - profile.avg_time().as_micros(), - profile.min_time.as_micros(), - profile.max_time.as_micros()); + println!( + "{:<20} {:>10} {:>12} {:>12} {:>12} {:>12}", + name, + profile.call_count, + profile.total_time.as_micros(), + profile.avg_time().as_micros(), + profile.min_time.as_micros(), + profile.max_time.as_micros() + ); } - + // Calculate delete operation statistics if let Some(delete_profile) = profiles.get("remove") { println!("\nDelete Operation Analysis:"); println!("- Total delete calls: {}", delete_profile.call_count); println!("- Average delete time: {:?}", delete_profile.avg_time()); - println!("- Delete time range: {:?} - {:?}", delete_profile.min_time, delete_profile.max_time); - - if let (Some(success), Some(fail)) = (profiles.get("successful_delete"), profiles.get("failed_delete")) { - println!("- Successful deletes: {} (avg: {:?})", success.call_count, success.avg_time()); - println!("- Failed deletes: {} (avg: {:?})", fail.call_count, fail.avg_time()); + println!( + "- Delete time range: {:?} - {:?}", + delete_profile.min_time, delete_profile.max_time + ); + + if let (Some(success), Some(fail)) = ( + profiles.get("successful_delete"), + profiles.get("failed_delete"), + ) { + println!( + "- Successful deletes: {} (avg: {:?})", + success.call_count, + success.avg_time() + ); + println!( + "- Failed deletes: {} (avg: {:?})", + fail.call_count, + fail.avg_time() + ); } } } @@ -158,60 +181,60 @@ enum Operation { fn create_sequential_delete_workload() -> Vec { let mut ops = Vec::new(); - + // Delete every other element sequentially for i in (0..25_000).step_by(2) { ops.push(Operation::Delete(i)); } - + ops } fn create_random_delete_workload() -> Vec { let mut seed = 42u64; let mut ops = Vec::new(); - + // Pseudo-random deletes for _ in 0..25_000 { seed = seed.wrapping_mul(1103515245).wrapping_add(12345); let key = (seed % 50_000) as i32; ops.push(Operation::Delete(key)); } - + ops } fn create_rebalancing_workload() -> Vec { let mut ops = Vec::new(); - + // Pattern designed to cause maximum rebalancing // Delete in a pattern that creates underfull nodes for i in 0..25_000 { ops.push(Operation::Delete(i * 2)); // Delete every other element } - + ops } fn create_mixed_workload() -> Vec { let mut seed = 42u64; let mut ops = Vec::new(); - + // Mixed workload: 40% lookup, 30% delete, 30% insert for _ in 0..30_000 { seed = seed.wrapping_mul(1103515245).wrapping_add(12345); let op_type = seed % 100; let key = (seed % 100_000) as i32; - + let op = match op_type { 0..=39 => Operation::Lookup(key), 40..=69 => Operation::Delete(key), 70..=99 => Operation::Insert(key, format!("new_value_{}", key)), _ => unreachable!(), }; - + ops.push(op); } - + ops -} \ No newline at end of file +} diff --git a/rust/src/delete_operations.rs b/rust/src/delete_operations.rs index a504ec1..bc9ebda 100644 --- a/rust/src/delete_operations.rs +++ b/rust/src/delete_operations.rs @@ -174,7 +174,6 @@ impl BPlusTreeMap { } } - /// Rebalance an underfull child in an arena branch #[inline] /// Rebalance an underfull child node using optimized sibling information gathering. @@ -186,42 +185,38 @@ impl BPlusTreeMap { Some(branch) => branch, None => return false, }; - + let child_is_leaf = matches!(parent_branch.children[child_index], NodeRef::Leaf(_, _)); - + // OPTIMIZATION: Batch sibling information gathering with direct node access let left_sibling_info = if child_index > 0 { let sibling_ref = parent_branch.children[child_index - 1].clone(); let can_donate = match &sibling_ref { - NodeRef::Leaf(id, _) => { - self.get_leaf(*id) - .map(|leaf| leaf.keys.len() > leaf.min_keys()) - .unwrap_or(false) - } - NodeRef::Branch(id, _) => { - self.get_branch(*id) - .map(|branch| branch.keys.len() > branch.min_keys()) - .unwrap_or(false) - } + NodeRef::Leaf(id, _) => self + .get_leaf(*id) + .map(|leaf| leaf.keys.len() > leaf.min_keys()) + .unwrap_or(false), + NodeRef::Branch(id, _) => self + .get_branch(*id) + .map(|branch| branch.keys.len() > branch.min_keys()) + .unwrap_or(false), }; Some((sibling_ref, can_donate)) } else { None }; - + let right_sibling_info = if child_index < parent_branch.children.len() - 1 { let sibling_ref = parent_branch.children[child_index + 1].clone(); let can_donate = match &sibling_ref { - NodeRef::Leaf(id, _) => { - self.get_leaf(*id) - .map(|leaf| leaf.keys.len() > leaf.min_keys()) - .unwrap_or(false) - } - NodeRef::Branch(id, _) => { - self.get_branch(*id) - .map(|branch| branch.keys.len() > branch.min_keys()) - .unwrap_or(false) - } + NodeRef::Leaf(id, _) => self + .get_leaf(*id) + .map(|leaf| leaf.keys.len() > leaf.min_keys()) + .unwrap_or(false), + NodeRef::Branch(id, _) => self + .get_branch(*id) + .map(|branch| branch.keys.len() > branch.min_keys()) + .unwrap_or(false), }; Some((sibling_ref, can_donate)) } else { @@ -230,17 +225,26 @@ impl BPlusTreeMap { (child_is_leaf, left_sibling_info, right_sibling_info) }; - + let (child_is_leaf, left_sibling_info, right_sibling_info) = rebalance_info; - + // Dispatch to appropriate rebalancing strategy if child_is_leaf { - self.rebalance_leaf(parent_id, child_index, left_sibling_info, right_sibling_info) + self.rebalance_leaf( + parent_id, + child_index, + left_sibling_info, + right_sibling_info, + ) } else { - self.rebalance_branch(parent_id, child_index, left_sibling_info, right_sibling_info) + self.rebalance_branch( + parent_id, + child_index, + left_sibling_info, + right_sibling_info, + ) } } - } #[cfg(test)] @@ -257,24 +261,24 @@ mod tests { fn test_optimized_rebalancing_reduces_arena_access() { // Test that the optimized rebalancing works correctly let mut tree = BPlusTreeMap::new(4).unwrap(); - + // Insert enough items to create multiple levels for i in 0..20 { tree.insert(i, format!("value_{}", i)); } - + // Verify tree structure before deletion assert!(tree.len() == 20); - + // Delete items that will trigger rebalancing for i in (0..10).step_by(2) { let removed = tree.remove(&i); assert!(removed.is_some(), "Should have removed key {}", i); } - + // Verify tree is still valid after rebalancing assert!(tree.len() == 15); - + // Verify remaining items are still accessible for i in (1..20).step_by(2) { if i < 10 { @@ -295,22 +299,27 @@ mod tests { for i in 0..15 { tree.insert(i, i * 2); } - + let initial_len = tree.len(); - + // Delete items in a pattern that tests different rebalancing scenarios let delete_keys = vec![1, 3, 5, 7, 9, 11, 13]; for key in delete_keys { let removed = tree.remove(&key); assert!(removed.is_some(), "Should have removed key {}", key); } - + assert_eq!(tree.len(), initial_len - 7); - + // Verify tree integrity by checking all remaining items let remaining_keys = vec![0, 2, 4, 6, 8, 10, 12, 14]; for key in remaining_keys { - assert_eq!(tree.get(&key), Some(&(key * 2)), "Key {} should have correct value", key); + assert_eq!( + tree.get(&key), + Some(&(key * 2)), + "Key {} should have correct value", + key + ); } } @@ -318,13 +327,13 @@ mod tests { fn test_delete_performance_characteristics() { // Test that demonstrates the performance characteristics of the optimized delete let mut tree = BPlusTreeMap::new(16).unwrap(); - + // Insert a larger dataset let n = 1000; for i in 0..n { tree.insert(i, format!("value_{}", i)); } - + // Delete every 3rd item (creates various rebalancing scenarios) let mut deleted_count = 0; for i in (0..n).step_by(3) { @@ -332,14 +341,19 @@ mod tests { deleted_count += 1; } } - + assert_eq!(tree.len(), n - deleted_count); - + // Verify tree is still valid and searchable for i in 0..n { let should_exist = i % 3 != 0; - assert_eq!(tree.get(&i).is_some(), should_exist, - "Key {} existence should be {}", i, should_exist); + assert_eq!( + tree.get(&i).is_some(), + should_exist, + "Key {} existence should be {}", + i, + should_exist + ); } } } @@ -361,7 +375,7 @@ impl BPlusTreeMap { return self.borrow_from_left_leaf(parent_id, child_index); } } - + if let Some((_right_ref, can_donate)) = right_sibling_info { if can_donate { return self.borrow_from_right_leaf(parent_id, child_index); @@ -380,7 +394,6 @@ impl BPlusTreeMap { } } - /// Rebalance an underfull branch child using pre-gathered sibling information. /// This version eliminates redundant arena access and improves readability. fn rebalance_branch( @@ -397,7 +410,7 @@ impl BPlusTreeMap { return self.borrow_from_left_branch(parent_id, child_index); } } - + if let Some((_right_ref, can_donate)) = right_sibling_info { if can_donate { return self.borrow_from_right_branch(parent_id, child_index); @@ -416,7 +429,6 @@ impl BPlusTreeMap { } } - /// Merge branch with left sibling fn merge_with_left_branch(&mut self, parent_id: NodeId, child_index: usize) -> bool { // Get the branch IDs and collect all needed info from parent in one access diff --git a/rust/src/iteration.rs b/rust/src/iteration.rs index 68a4091..aa8647b 100644 --- a/rust/src/iteration.rs +++ b/rust/src/iteration.rs @@ -19,7 +19,6 @@ pub struct ItemIterator<'a, K, V> { end_key: Option<&'a K>, end_bound_key: Option, end_inclusive: bool, - finished: bool, } /// Fast iterator over key-value pairs using unsafe arena access for better performance. @@ -28,7 +27,6 @@ pub struct FastItemIterator<'a, K, V> { current_leaf_id: Option, pub current_leaf_ref: Option<&'a LeafNode>, // CACHED leaf reference current_leaf_index: usize, - finished: bool, } /// Iterator over keys in the B+ tree. @@ -116,7 +114,6 @@ impl<'a, K: Ord + Clone, V: Clone> ItemIterator<'a, K, V> { end_key: None, end_bound_key: None, end_inclusive: false, - finished: false, } } @@ -142,7 +139,6 @@ impl<'a, K: Ord + Clone, V: Clone> ItemIterator<'a, K, V> { end_key, end_bound_key, end_inclusive, - finished: false, } } @@ -170,9 +166,7 @@ impl<'a, K: Ord + Clone, V: Clone> ItemIterator<'a, K, V> { // - Eliminates 2 bounds checks per iteration (key + value access) // - Reduces per-item overhead by ~4-6ns // - Critical for competitive iteration performance vs BTreeMap - let (key, value) = unsafe { - leaf.get_key_value_unchecked(self.current_leaf_index) - }; + let (key, value) = unsafe { leaf.get_key_value_unchecked(self.current_leaf_index) }; // Optimized: Direct conditional logic instead of Option combinators let beyond_end = if let Some(end_key) = self.end_key { @@ -224,12 +218,6 @@ impl<'a, K: Ord + Clone, V: Clone> ItemIterator<'a, K, V> { // Return whether we successfully got the next leaf self.current_leaf_ref.is_some() } - - /// Legacy method for compatibility - delegates to streamlined version - #[inline] - fn advance_to_next_leaf(&mut self) -> Option { - Some(self.advance_to_next_leaf_direct()) - } } impl<'a, K: Ord + Clone, V: Clone> Iterator for ItemIterator<'a, K, V> { @@ -243,16 +231,16 @@ impl<'a, K: Ord + Clone, V: Clone> Iterator for ItemIterator<'a, K, V> { // 2. Direct flow with fewer nested conditions // 3. Simplified advance_to_next_leaf_direct() with bool return // 4. Single exit point pattern - - 'outer: loop { + + loop { // Direct access - if no leaf, we're done (terminal state) let leaf = self.current_leaf_ref?; - + // Try current leaf first if let Some(item) = self.try_get_next_item(leaf) { return Some(item); } - + // Advance to next leaf - if false, we're done if !self.advance_to_next_leaf_direct() { return None; @@ -390,7 +378,6 @@ impl<'a, K: Ord + Clone, V: Clone> FastItemIterator<'a, K, V> { current_leaf_id: leftmost_id, current_leaf_ref, current_leaf_index: 0, - finished: false, } } } @@ -400,19 +387,9 @@ impl<'a, K: Ord + Clone, V: Clone> Iterator for FastItemIterator<'a, K, V> { #[inline] fn next(&mut self) -> Option { - if self.finished { - return None; - } - loop { // Optimized: Direct access with early return - let leaf = match self.current_leaf_ref { - Some(leaf) => leaf, - None => { - self.finished = true; - return None; - } - }; + let leaf = self.current_leaf_ref?; if self.current_leaf_index < leaf.keys_len() { let key = leaf.get_key(self.current_leaf_index)?; @@ -427,7 +404,6 @@ impl<'a, K: Ord + Clone, V: Clone> Iterator for FastItemIterator<'a, K, V> { self.current_leaf_ref = unsafe { Some(self.tree.get_leaf_unchecked(leaf.next)) }; self.current_leaf_index = 0; } else { - self.finished = true; return None; } } diff --git a/rust/src/node.rs b/rust/src/node.rs index 5f873ee..140882b 100644 --- a/rust/src/node.rs +++ b/rust/src/node.rs @@ -121,21 +121,21 @@ impl LeafNode { // - Always perform explicit bounds check before calling unsafe methods // - Use get_key_value_unchecked() when accessing both key and value // - Document safety reasoning at each call site - + /// Get a key by index without bounds checking. - /// + /// /// # Safety - /// + /// /// The caller must ensure that `index < self.keys_len()`. /// Violating this invariant will result in undefined behavior. - /// + /// /// # Performance - /// + /// /// This method eliminates the bounds check performed by `Vec::get()`, /// providing direct access to the underlying array element. - /// + /// /// # Usage - /// + /// /// ```rust,ignore /// if index < leaf.keys_len() { /// let key = unsafe { leaf.get_key_unchecked(index) }; @@ -146,21 +146,21 @@ impl LeafNode { pub unsafe fn get_key_unchecked(&self, index: usize) -> &K { self.keys.get_unchecked(index) } - + /// Get a value by index without bounds checking. - /// + /// /// # Safety - /// + /// /// The caller must ensure that `index < self.values_len()`. /// Violating this invariant will result in undefined behavior. - /// + /// /// # Performance - /// + /// /// This method eliminates the bounds check performed by `Vec::get()`, /// providing direct access to the underlying array element. - /// + /// /// # Usage - /// + /// /// ```rust,ignore /// if index < leaf.values_len() { /// let value = unsafe { leaf.get_value_unchecked(index) }; @@ -171,23 +171,23 @@ impl LeafNode { pub unsafe fn get_value_unchecked(&self, index: usize) -> &V { self.values.get_unchecked(index) } - + /// Get both key and value by index without bounds checking. - /// + /// /// # Safety - /// + /// /// The caller must ensure that `index < self.keys_len()` and `index < self.values_len()`. /// In a well-formed leaf node, keys.len() == values.len(), so checking either is sufficient. /// Violating this invariant will result in undefined behavior. - /// + /// /// # Performance - /// + /// /// This method eliminates two bounds checks (one for key, one for value) and /// provides the most efficient way to access both key and value simultaneously. /// Preferred over separate get_key_unchecked() + get_value_unchecked() calls. - /// + /// /// # Usage - /// + /// /// ```rust,ignore /// if index < leaf.keys_len() { /// let (key, value) = unsafe { leaf.get_key_value_unchecked(index) }; @@ -196,7 +196,10 @@ impl LeafNode { /// ``` #[inline] pub unsafe fn get_key_value_unchecked(&self, index: usize) -> (&K, &V) { - (self.keys.get_unchecked(index), self.values.get_unchecked(index)) + ( + self.keys.get_unchecked(index), + self.values.get_unchecked(index), + ) } /// Push a key to the keys vector.