use fhirkit::r4::Resource;
use fhirkit::{decode_json, from_json_many, FhirJson, Json, Severity};
use std::time::Instant;
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let Some(dir) = args.first() else {
eprintln!("usage: validate_dir <folder> [--threads N]");
std::process::exit(2);
};
let threads: usize = args
.iter()
.position(|a| a == "--threads")
.and_then(|i| args.get(i + 1))
.and_then(|n| n.parse().ok())
.unwrap_or(0);
let mut docs: Vec<String> = std::fs::read_dir(dir)
.unwrap_or_else(|e| panic!("{dir}: {e}"))
.filter_map(|e| e.ok().map(|e| e.path()))
.filter(|p| p.extension().is_some_and(|x| x == "json"))
.filter(|p| {
!p.file_name()
.is_some_and(|n| n.to_string_lossy().starts_with('.'))
})
.filter_map(|p| std::fs::read_to_string(p).ok())
.filter(|t| t.contains("\"resourceType\""))
.collect();
docs.sort();
let texts: Vec<&str> = docs.iter().map(String::as_str).collect();
if let Some(rounds) = args
.iter()
.position(|a| a == "--sweep")
.and_then(|i| args.get(i + 1))
.and_then(|n| n.parse::<usize>().ok())
{
return sweep(&texts, rounds.max(1));
}
let mb = texts.iter().map(|t| t.len()).sum::<usize>() as f64 / 1e6;
let t = Instant::now();
let parsed: Vec<Json> = texts.iter().filter_map(|t| Json::parse(t).ok()).collect();
let t_parse = t.elapsed();
let t = Instant::now();
let resources: Vec<Resource> = parsed
.iter()
.filter_map(|j| decode_json::<Resource>(j).0)
.collect();
let t_decode = t.elapsed();
let t = Instant::now();
let written: usize = resources
.iter()
.map(|r| r.encode().to_compact().len())
.sum();
let t_write = t.elapsed();
let t = Instant::now();
let many = from_json_many::<Resource>(&texts, threads);
let t_many = t.elapsed();
let invalid = many
.iter()
.filter(|r| match r {
Ok((_, issues)) => issues.iter().any(|i| i.severity == Severity::Error),
Err(_) => true,
})
.count();
println!(
"{} resources, {mb:.1} MB, {invalid} with errors",
many.len()
);
println!(
" 1 thread JSON parse {:>6.0} ms",
t_parse.as_secs_f64() * 1e3
);
println!(
" 1 thread decode+validate {:>6.0} ms",
t_decode.as_secs_f64() * 1e3
);
println!(
" 1 thread write JSON {:>6.0} ms ({:.1} MB)",
t_write.as_secs_f64() * 1e3,
written as f64 / 1e6
);
let n = if threads == 0 {
std::thread::available_parallelism().map_or(1, usize::from)
} else {
threads
};
println!(
" {n:>2} threads parse+validate {:>6.0} ms (from_json_many)",
t_many.as_secs_f64() * 1e3
);
}
fn sweep(texts: &[&str], rounds: usize) {
let cores = std::thread::available_parallelism().map_or(1, usize::from);
let mut times: Vec<Vec<f64>> = vec![Vec::new(); cores];
from_json_many::<Resource>(texts, cores); for _ in 0..rounds {
for (i, slot) in times.iter_mut().enumerate() {
let t = Instant::now();
from_json_many::<Resource>(texts, i + 1);
slot.push(t.elapsed().as_secs_f64() * 1e3);
}
}
println!(
"{} documents, {rounds} rounds, median ms per thread count:",
texts.len()
);
let best = times
.iter()
.map(|t| median(t))
.fold(f64::INFINITY, f64::min);
for (i, t) in times.iter().enumerate() {
let m = median(t);
let lo = t.iter().copied().fold(f64::INFINITY, f64::min);
let hi = t.iter().copied().fold(0.0, f64::max);
let mark = if m == best { " <- fastest" } else { "" };
println!(" {:>2} threads {m:>6.0} ({lo:.0}-{hi:.0}){mark}", i + 1);
}
}
fn median(v: &[f64]) -> f64 {
let mut s = v.to_vec();
s.sort_by(f64::total_cmp);
s[s.len() / 2]
}