forked from
atscan.net/plcbundle-rs
High-performance implementation of plcbundle written in Rust
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}