High-performance implementation of plcbundle written in Rust
at main 782 lines 23 kB view raw
1use super::progress::ProgressBar; 2use super::utils::format_number; 3use anyhow::Result; 4use clap::Args; 5use plcbundle::BundleManager; 6use std::io::Read; 7use std::path::PathBuf; 8use std::time::Instant; 9 10#[derive(Args)] 11#[command( 12 about = "Benchmark bundle operations", 13 long_about = "Measure performance of various bundle operations to understand system 14behavior and identify bottlenecks. Benchmarks cover bundle loading, 15decompression, operation reads, DID index lookups, DID resolution, and 16sequential access patterns. 17 18Each benchmark runs multiple iterations with warmup periods to ensure 19accurate measurements. Results include statistical summaries with percentiles 20(p50, p95, p99), standard deviation, and throughput metrics where applicable. 21 22Use --interactive to see progress bars during benchmarking. Use --json to 23output results in machine-readable format for automated analysis or 24tracking performance over time. The --warmup flag controls how many 25iterations are used to warm up caches before measurement begins. 26 27This tool is essential for performance regression testing, capacity planning, 28and understanding how repository size and access patterns affect performance.", 29 help_template = crate::clap_help!( 30 examples: " # Run all benchmarks with default iterations\n \ 31 {bin} bench\n\n \ 32 # Benchmark specific operation\n \ 33 {bin} bench --op-read --iterations 1000\n\n \ 34 # Benchmark DID lookup\n \ 35 {bin} bench --did-lookup -n 500\n\n \ 36 # Run on specific bundle\n \ 37 {bin} bench --bundles 100\n\n \ 38 # JSON output for analysis\n \ 39 {bin} bench --json > benchmark.json" 40 ) 41)] 42pub struct BenchCommand { 43 /// Number of iterations for each benchmark 44 #[arg(short = 'n', long, default_value = "100")] 45 pub iterations: usize, 46 47 /// Bundle number to benchmark (default: uses multiple bundles) 48 #[arg(long)] 49 pub bundles: Option<String>, 50 51 /// Run all benchmarks (default) 52 #[arg(short, long)] 53 pub all: bool, 54 55 /// Benchmark operation reading 56 #[arg(long)] 57 pub op_read: bool, 58 59 /// Benchmark DID index lookup 60 #[arg(long)] 61 pub did_lookup: bool, 62 63 /// Benchmark bundle loading 64 #[arg(long)] 65 pub bundle_load: bool, 66 67 /// Benchmark bundle decompression 68 #[arg(long)] 69 pub decompress: bool, 70 71 /// Benchmark DID resolution (includes index + operations) 72 #[arg(long)] 73 pub did_resolve: bool, 74 75 /// Benchmark sequential bundle access pattern 76 #[arg(long)] 77 pub sequential: bool, 78 79 /// Warmup iterations before benchmarking 80 #[arg(long, default_value = "10")] 81 pub warmup: usize, 82 83 /// Show interactive progress during benchmarks 84 #[arg(long)] 85 pub interactive: bool, 86 87 /// Output as JSON 88 #[arg(long)] 89 pub json: bool, 90} 91 92#[derive(Debug, serde::Serialize)] 93struct BenchmarkResult { 94 name: String, 95 iterations: usize, 96 total_ms: f64, 97 avg_ms: f64, 98 min_ms: f64, 99 max_ms: f64, 100 p50_ms: f64, 101 p95_ms: f64, 102 p99_ms: f64, 103 stddev_ms: f64, 104 ops_per_sec: f64, 105 #[serde(skip_serializing_if = "Option::is_none")] 106 throughput_mbs: Option<f64>, 107 #[serde(skip_serializing_if = "Option::is_none")] 108 avg_size_bytes: Option<u64>, 109 #[serde(skip_serializing_if = "Option::is_none")] 110 total_bytes: Option<u64>, 111 #[serde(skip_serializing_if = "Option::is_none")] 112 bundles_accessed: Option<usize>, 113 #[serde(skip_serializing_if = "Option::is_none")] 114 cache_hits: Option<usize>, 115 #[serde(skip_serializing_if = "Option::is_none")] 116 cache_misses: Option<usize>, 117} 118 119pub fn run(cmd: BenchCommand, dir: PathBuf, global_verbose: bool) -> Result<()> { 120 let manager = super::utils::create_manager(dir.clone(), global_verbose, false, false)?; 121 122 // Determine which benchmarks to run 123 let run_all = cmd.all 124 || (!cmd.op_read 125 && !cmd.did_lookup 126 && !cmd.bundle_load 127 && !cmd.decompress 128 && !cmd.did_resolve 129 && !cmd.sequential); 130 131 // Get repository info 132 if super::utils::is_repository_empty(&manager) { 133 anyhow::bail!("No bundles found in repository"); 134 } 135 let last_bundle = manager.get_last_bundle(); 136 137 // Print benchmark header 138 eprintln!("\n{}", "=".repeat(80)); 139 eprintln!("{:^80}", "BENCHMARK SUITE"); 140 eprintln!("{}", "=".repeat(80)); 141 eprintln!("Repository: {} ({} bundles)", dir.display(), last_bundle); 142 eprintln!("Iterations: {} (warmup: {})", cmd.iterations, cmd.warmup); 143 eprintln!("Interactive: {}", cmd.interactive); 144 eprintln!("{}", "=".repeat(80)); 145 eprintln!(); 146 147 let mut results = Vec::new(); 148 149 // All benchmarks now use random data from across the repository 150 if run_all || cmd.bundle_load { 151 results.push(bench_bundle_load( 152 &manager, 153 last_bundle, 154 cmd.iterations, 155 cmd.warmup, 156 cmd.interactive, 157 )?); 158 } 159 160 if run_all || cmd.decompress { 161 results.push(bench_bundle_decompress( 162 &manager, 163 last_bundle, 164 cmd.iterations, 165 cmd.warmup, 166 cmd.interactive, 167 )?); 168 } 169 170 if run_all || cmd.op_read { 171 results.push(bench_operation_read( 172 &manager, 173 last_bundle, 174 cmd.iterations, 175 cmd.warmup, 176 cmd.interactive, 177 )?); 178 } 179 180 if run_all || cmd.did_lookup { 181 results.push(bench_did_index_lookup( 182 &manager, 183 last_bundle, 184 cmd.iterations, 185 cmd.warmup, 186 cmd.interactive, 187 )?); 188 } 189 190 if run_all || cmd.did_resolve { 191 results.push(bench_did_resolution( 192 &manager, 193 last_bundle, 194 cmd.iterations, 195 cmd.warmup, 196 cmd.interactive, 197 )?); 198 } 199 200 if cmd.sequential { 201 results.push(bench_sequential_access( 202 &manager, 203 last_bundle, 204 cmd.iterations.min(50), 205 cmd.warmup, 206 cmd.interactive, 207 )?); 208 } 209 210 // Output results 211 if cmd.json { 212 print_json_results(&results)?; 213 } else { 214 print_human_results(&results); 215 } 216 217 Ok(()) 218} 219 220/// Generate random bundle numbers using deterministic hash 221fn generate_random_bundles(last_bundle: u32, count: usize) -> Vec<u32> { 222 use std::collections::hash_map::DefaultHasher; 223 use std::hash::{Hash, Hasher}; 224 225 (0..count) 226 .map(|i| { 227 let mut hasher = DefaultHasher::new(); 228 i.hash(&mut hasher); 229 (hasher.finish() % last_bundle as u64) as u32 + 1 230 }) 231 .collect() 232} 233 234fn bundle_compressed_size(manager: &BundleManager, bundle_num: u32) -> Result<Option<u64>> { 235 Ok(manager 236 .get_bundle_metadata(bundle_num)? 237 .map(|meta| meta.compressed_size)) 238} 239 240/// Benchmark bundle loading (full bundle read + decompression + parsing) 241/// Iterates over random bundles from the entire repository 242fn bench_bundle_load( 243 manager: &BundleManager, 244 last_bundle: u32, 245 iterations: usize, 246 warmup: usize, 247 interactive: bool, 248) -> Result<BenchmarkResult> { 249 use plcbundle::LoadOptions; 250 251 if interactive { 252 eprintln!("[Benchmark] Bundle Load (full)..."); 253 } 254 255 let bundles = generate_random_bundles(last_bundle, iterations); 256 257 // Warmup 258 for i in 0..warmup.min(10) { 259 let _ = manager.load_bundle(bundles[i % bundles.len()], LoadOptions::default())?; 260 } 261 262 // Benchmark - iterate over different bundles each time 263 manager.clear_caches(); 264 let mut timings = Vec::with_capacity(iterations); 265 let mut total_bytes = 0u64; 266 267 let pb = if interactive { 268 Some(ProgressBar::new(iterations)) 269 } else { 270 None 271 }; 272 273 for (i, &bundle_num) in bundles.iter().enumerate() { 274 if let Some(ref pb) = pb { 275 pb.set(i + 1); 276 } 277 278 if let Some(size) = bundle_compressed_size(manager, bundle_num)? { 279 total_bytes += size; 280 } 281 282 let start = Instant::now(); 283 let _ = manager.load_bundle(bundle_num, LoadOptions::default())?; 284 timings.push(start.elapsed().as_secs_f64() * 1000.0); 285 } 286 287 if let Some(ref pb) = pb { 288 pb.finish(); 289 } 290 291 let unique_bundles = bundles 292 .iter() 293 .collect::<std::collections::HashSet<_>>() 294 .len(); 295 let avg_size = total_bytes / iterations as u64; 296 297 let mut result = calculate_stats("Bundle Load (full)", iterations, timings); 298 result.avg_size_bytes = Some(avg_size); 299 result.total_bytes = Some(total_bytes); 300 result.bundles_accessed = Some(unique_bundles); 301 result.throughput_mbs = 302 Some((total_bytes as f64 / 1024.0 / 1024.0) / (result.total_ms / 1000.0)); 303 Ok(result) 304} 305 306/// Benchmark bundle decompression only (read + decompress, no parsing) 307/// Iterates over random bundles from the entire repository 308fn bench_bundle_decompress( 309 manager: &BundleManager, 310 last_bundle: u32, 311 iterations: usize, 312 warmup: usize, 313 interactive: bool, 314) -> Result<BenchmarkResult> { 315 if interactive { 316 eprintln!("[Benchmark] Bundle Decompression..."); 317 } 318 319 let bundles = generate_random_bundles(last_bundle, iterations); 320 321 // Warmup 322 for i in 0..warmup.min(10) { 323 let bundle_num = bundles[i % bundles.len()]; 324 if bundle_compressed_size(manager, bundle_num)?.is_none() { 325 continue; 326 } 327 328 let file = manager.stream_bundle_raw(bundle_num)?; 329 let mut decoder = zstd::Decoder::new(file)?; 330 let mut buffer = Vec::new(); 331 decoder.read_to_end(&mut buffer)?; 332 } 333 334 // Benchmark - iterate over different bundles each time 335 let mut timings = Vec::with_capacity(iterations); 336 let mut total_bytes = 0u64; 337 338 let pb = if interactive { 339 Some(ProgressBar::new(iterations)) 340 } else { 341 None 342 }; 343 344 let mut processed = 0; 345 for &bundle_num in bundles.iter() { 346 let size = match bundle_compressed_size(manager, bundle_num)? { 347 Some(size) => size, 348 None => continue, 349 }; 350 total_bytes += size; 351 processed += 1; 352 353 if let Some(ref pb) = pb { 354 pb.set(processed); 355 } 356 357 let start = Instant::now(); 358 let file = manager.stream_bundle_raw(bundle_num)?; 359 let mut decoder = zstd::Decoder::new(file)?; 360 let mut buffer = Vec::new(); 361 decoder.read_to_end(&mut buffer)?; 362 timings.push(start.elapsed().as_secs_f64() * 1000.0); 363 } 364 365 if let Some(ref pb) = pb { 366 pb.finish(); 367 } 368 369 let unique_bundles = bundles 370 .iter() 371 .collect::<std::collections::HashSet<_>>() 372 .len(); 373 let avg_size = total_bytes / timings.len() as u64; 374 375 let mut result = calculate_stats("Bundle Decompression", timings.len(), timings); 376 result.avg_size_bytes = Some(avg_size); 377 result.total_bytes = Some(total_bytes); 378 result.bundles_accessed = Some(unique_bundles); 379 result.throughput_mbs = 380 Some((total_bytes as f64 / 1024.0 / 1024.0) / (result.total_ms / 1000.0)); 381 Ok(result) 382} 383 384/// Benchmark single operation read from random bundles and positions 385fn bench_operation_read( 386 manager: &BundleManager, 387 last_bundle: u32, 388 iterations: usize, 389 warmup: usize, 390 interactive: bool, 391) -> Result<BenchmarkResult> { 392 use plcbundle::LoadOptions; 393 use std::collections::hash_map::DefaultHasher; 394 use std::hash::{Hash, Hasher}; 395 396 if interactive { 397 eprintln!("[Benchmark] Operation Read..."); 398 } 399 400 let bundles = generate_random_bundles(last_bundle, iterations); 401 402 // Load bundles to get operation counts 403 let mut bundle_op_counts = Vec::with_capacity(bundles.len()); 404 for &bundle_num in &bundles { 405 if let Ok(bundle) = manager.load_bundle(bundle_num, LoadOptions::default()) 406 && !bundle.operations.is_empty() 407 { 408 bundle_op_counts.push((bundle_num, bundle.operations.len())); 409 } 410 } 411 412 if bundle_op_counts.is_empty() { 413 anyhow::bail!("No bundles with operations found"); 414 } 415 416 // Warmup 417 for i in 0..warmup.min(10) { 418 let (bundle_num, op_count) = bundle_op_counts[i % bundle_op_counts.len()]; 419 let pos = op_count / 2; 420 let _ = manager.get_operation_raw(bundle_num, pos)?; 421 } 422 423 // Benchmark - random bundle and random position each iteration 424 let mut timings = Vec::with_capacity(iterations); 425 426 let pb = if interactive { 427 Some(ProgressBar::new(iterations)) 428 } else { 429 None 430 }; 431 432 for i in 0..iterations { 433 if let Some(ref pb) = pb { 434 pb.set(i + 1); 435 } 436 437 let (bundle_num, op_count) = bundle_op_counts[i % bundle_op_counts.len()]; 438 439 // Generate random position within this bundle 440 let mut hasher = DefaultHasher::new(); 441 (i * 1000).hash(&mut hasher); 442 let pos = (hasher.finish() % op_count as u64) as usize; 443 444 let start = Instant::now(); 445 let _ = manager.get_operation_raw(bundle_num, pos)?; 446 timings.push(start.elapsed().as_secs_f64() * 1000.0); 447 } 448 449 if let Some(ref pb) = pb { 450 pb.finish(); 451 } 452 453 let unique_bundles = bundle_op_counts 454 .iter() 455 .map(|(b, _)| b) 456 .collect::<std::collections::HashSet<_>>() 457 .len(); 458 let mut result = calculate_stats("Operation Read", iterations, timings); 459 result.bundles_accessed = Some(unique_bundles); 460 Ok(result) 461} 462 463/// Benchmark DID index lookup from random DIDs across repository 464fn bench_did_index_lookup( 465 manager: &BundleManager, 466 _last_bundle: u32, 467 iterations: usize, 468 warmup: usize, 469 interactive: bool, 470) -> Result<BenchmarkResult> { 471 if interactive { 472 eprintln!("[Benchmark] DID Index Lookup..."); 473 } 474 475 let sample_count = iterations.max(warmup.min(10)).max(1); 476 let dids = manager.sample_random_dids(sample_count, None)?; 477 478 if dids.is_empty() { 479 anyhow::bail!("No DIDs found in repository"); 480 } 481 482 // Ensure DID index is loaded (sample_random_dids already does this, but be explicit) 483 // The did_index will be loaded by sample_random_dids above 484 let did_index = manager.get_did_index(); 485 486 // Ensure it's actually loaded (in case sample_random_dids didn't load it) 487 { 488 let guard = did_index.read().unwrap(); 489 if guard.is_none() { 490 anyhow::bail!("DID index not available"); 491 } 492 } 493 494 // Warmup 495 for i in 0..warmup.min(10) { 496 let _ = did_index 497 .read() 498 .unwrap() 499 .as_ref() 500 .unwrap() 501 .get_did_locations(&dids[i % dids.len()])?; 502 } 503 504 // Benchmark - different DID each iteration 505 let mut timings = Vec::with_capacity(iterations); 506 507 let pb = if interactive { 508 Some(ProgressBar::new(iterations)) 509 } else { 510 None 511 }; 512 513 for i in 0..iterations { 514 if let Some(ref pb) = pb { 515 pb.set(i + 1); 516 } 517 518 let did = &dids[i % dids.len()]; 519 let start = Instant::now(); 520 let _ = did_index 521 .read() 522 .unwrap() 523 .as_ref() 524 .unwrap() 525 .get_did_locations(did)?; 526 timings.push(start.elapsed().as_secs_f64() * 1000.0); 527 } 528 529 if let Some(ref pb) = pb { 530 pb.finish(); 531 } 532 533 Ok(calculate_stats("DID Index Lookup", iterations, timings)) 534} 535 536/// Benchmark DID resolution from random DIDs: index lookup → operations → W3C document 537fn bench_did_resolution( 538 manager: &BundleManager, 539 _last_bundle: u32, 540 iterations: usize, 541 warmup: usize, 542 interactive: bool, 543) -> Result<BenchmarkResult> { 544 if interactive { 545 eprintln!("[Benchmark] DID Resolution (index→document)..."); 546 } 547 548 let sample_count = iterations.max(warmup.min(10)).max(1); 549 let dids = manager.sample_random_dids(sample_count, None)?; 550 551 if dids.is_empty() { 552 anyhow::bail!("No DIDs found in repository"); 553 } 554 555 // Warmup 556 for i in 0..warmup.min(10) { 557 let _ = manager.resolve_did(&dids[i % dids.len()])?.document; 558 } 559 560 // Benchmark - different DID each iteration 561 manager.clear_caches(); 562 let mut timings = Vec::with_capacity(iterations); 563 564 let pb = if interactive { 565 Some(ProgressBar::new(iterations)) 566 } else { 567 None 568 }; 569 570 for i in 0..iterations { 571 if let Some(ref pb) = pb { 572 pb.set(i + 1); 573 } 574 575 let did = &dids[i % dids.len()]; 576 let start = Instant::now(); 577 let _ = manager.resolve_did(did)?.document; 578 timings.push(start.elapsed().as_secs_f64() * 1000.0); 579 } 580 581 if let Some(ref pb) = pb { 582 pb.finish(); 583 } 584 585 Ok(calculate_stats( 586 "DID Resolution (index→document)", 587 iterations, 588 timings, 589 )) 590} 591 592fn calculate_stats(name: &str, iterations: usize, mut timings: Vec<f64>) -> BenchmarkResult { 593 timings.sort_by(|a, b| a.partial_cmp(b).unwrap()); 594 595 let total_ms: f64 = timings.iter().sum(); 596 let avg_ms = total_ms / iterations as f64; 597 let min_ms = timings[0]; 598 let max_ms = timings[timings.len() - 1]; 599 600 let p50_idx = (iterations as f64 * 0.50) as usize; 601 let p95_idx = (iterations as f64 * 0.95) as usize; 602 let p99_idx = (iterations as f64 * 0.99) as usize; 603 604 let p50_ms = timings[p50_idx.min(timings.len() - 1)]; 605 let p95_ms = timings[p95_idx.min(timings.len() - 1)]; 606 let p99_ms = timings[p99_idx.min(timings.len() - 1)]; 607 608 // Calculate standard deviation 609 let variance: f64 = timings 610 .iter() 611 .map(|&x| { 612 let diff = x - avg_ms; 613 diff * diff 614 }) 615 .sum::<f64>() 616 / iterations as f64; 617 let stddev_ms = variance.sqrt(); 618 619 let ops_per_sec = 1000.0 / avg_ms; 620 621 BenchmarkResult { 622 name: name.to_string(), 623 iterations, 624 total_ms, 625 avg_ms, 626 min_ms, 627 max_ms, 628 p50_ms, 629 p95_ms, 630 p99_ms, 631 stddev_ms, 632 ops_per_sec, 633 throughput_mbs: None, 634 avg_size_bytes: None, 635 total_bytes: None, 636 bundles_accessed: None, 637 cache_hits: None, 638 cache_misses: None, 639 } 640} 641 642/// Benchmark sequential bundle access 643fn bench_sequential_access( 644 manager: &BundleManager, 645 last_bundle: u32, 646 iterations: usize, 647 warmup: usize, 648 interactive: bool, 649) -> Result<BenchmarkResult> { 650 use plcbundle::LoadOptions; 651 652 if interactive { 653 eprintln!("[Benchmark] Sequential Bundle Access..."); 654 } 655 656 let count = iterations.min(last_bundle as usize).min(50); 657 let start_bundle = (last_bundle / 2).saturating_sub(count as u32 / 2).max(1); 658 659 // Warmup 660 for i in 0..warmup.min(5) { 661 let bundle_num = start_bundle + (i as u32 % count as u32); 662 let _ = manager.load_bundle(bundle_num, LoadOptions::default())?; 663 } 664 665 // Benchmark 666 manager.clear_caches(); 667 let start_stats = manager.get_stats(); 668 let mut timings = Vec::with_capacity(count); 669 let mut total_bytes = 0u64; 670 671 let pb = if interactive { 672 Some(ProgressBar::new(count)) 673 } else { 674 None 675 }; 676 677 for i in 0..count { 678 if let Some(ref pb) = pb { 679 pb.set(i + 1); 680 } 681 682 let bundle_num = start_bundle + i as u32; 683 if let Some(size) = bundle_compressed_size(manager, bundle_num)? { 684 total_bytes += size; 685 } 686 687 let start = Instant::now(); 688 let _ = manager.load_bundle(bundle_num, LoadOptions::default())?; 689 timings.push(start.elapsed().as_secs_f64() * 1000.0); 690 } 691 692 if let Some(ref pb) = pb { 693 pb.finish(); 694 } 695 696 let end_stats = manager.get_stats(); 697 let mut result = calculate_stats("Sequential Bundle Access", count, timings); 698 result.bundles_accessed = Some(count); 699 result.total_bytes = Some(total_bytes); 700 result.throughput_mbs = 701 Some((total_bytes as f64 / 1024.0 / 1024.0) / (result.total_ms / 1000.0)); 702 result.cache_hits = Some((end_stats.cache_hits - start_stats.cache_hits) as usize); 703 result.cache_misses = Some((end_stats.cache_misses - start_stats.cache_misses) as usize); 704 705 Ok(result) 706} 707 708/// Format time with appropriate units (ms, μs, or ns) 709fn format_time(ms: f64) -> String { 710 if ms >= 1.0 { 711 format!("{:.3} ms", ms) 712 } else if ms >= 0.001 { 713 format!("{:.3} μs", ms * 1000.0) 714 } else { 715 format!("{:.1} ns", ms * 1_000_000.0) 716 } 717} 718 719fn print_human_results(results: &[BenchmarkResult]) { 720 println!("\n{}", "=".repeat(80)); 721 println!("{:^80}", "BENCHMARK RESULTS"); 722 println!("{}", "=".repeat(80)); 723 println!(); 724 725 for result in results { 726 println!("{}:", result.name); 727 println!( 728 " Iterations: {}", 729 format_number(result.iterations as u64) 730 ); 731 println!(" Total Time: {:.2} ms", result.total_ms); 732 println!( 733 " Average: {} ({:.0} ops/sec)", 734 format_time(result.avg_ms), 735 result.ops_per_sec 736 ); 737 738 if let Some(size) = result.avg_size_bytes { 739 println!(" Bundle Size: {:.2} MB", size as f64 / 1024.0 / 1024.0); 740 } 741 if let Some(throughput) = result.throughput_mbs { 742 println!(" Throughput: {:.2} MB/s", throughput); 743 } 744 745 println!(" Min: {}", format_time(result.min_ms)); 746 println!(" Max: {}", format_time(result.max_ms)); 747 println!(" Median (p50): {}", format_time(result.p50_ms)); 748 println!(" p95: {}", format_time(result.p95_ms)); 749 println!(" p99: {}", format_time(result.p99_ms)); 750 751 if result.stddev_ms > 0.0 { 752 println!(" Std Dev: {}", format_time(result.stddev_ms)); 753 } 754 755 if let Some(bundles) = result.bundles_accessed { 756 println!(" Bundles: {}", bundles); 757 } 758 if let Some(hits) = result.cache_hits { 759 println!(" Cache Hits: {}", format_number(hits as u64)); 760 } 761 if let Some(misses) = result.cache_misses { 762 println!(" Cache Misses: {}", format_number(misses as u64)); 763 if let Some(hits) = result.cache_hits { 764 let total = hits + misses; 765 if total > 0 { 766 let hit_rate = (hits as f64 / total as f64) * 100.0; 767 println!(" Cache Hit Rate: {:.1}%", hit_rate); 768 } 769 } 770 } 771 772 println!(); 773 } 774 775 println!("{}", "=".repeat(80)); 776} 777 778fn print_json_results(results: &[BenchmarkResult]) -> Result<()> { 779 let json = sonic_rs::to_string_pretty(results)?; 780 println!("{}", json); 781 Ok(()) 782}