1use std::collections::BTreeMap;
2use std::fs;
3use std::path::Path;
4
5use serde::Serialize;
6use serde_json::Value;
7
8use crate::compare::compare_ann;
9use crate::probe::{PayloadMode, ProbeError, ProbeMeasurements, ProbeObservation, ViewerProbe};
10use crate::shim::ReferenceShim;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct ScaleCase {
14 pub coordinate_values: usize,
15 pub required: bool,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
19#[serde(rename_all = "lowercase")]
20pub enum ScaleStatus {
21 Passed,
22 Failed,
23}
24
25impl ScaleCase {
26 #[must_use]
27 pub fn id(self) -> String {
28 format!("ann-scale-{}", self.coordinate_values)
29 }
30}
31
32#[derive(Debug, Serialize)]
33pub struct ScaleObservation {
34 pub case_id: String,
35 pub coordinate_values: usize,
36 pub required: bool,
37 pub status: ScaleStatus,
38 pub input_bytes: Option<u64>,
39 pub output_bytes: Option<u64>,
40 pub runtime_ms: Option<f64>,
41 pub peak_rss_bytes: Option<u64>,
42 pub viewer_runtime: BTreeMap<String, Value>,
43 pub peak_tracked_heap_bytes: Option<u64>,
44 pub viewer_implementation: Value,
45 pub probe_commands: BTreeMap<String, Vec<String>>,
46 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
47 pub execution_errors: BTreeMap<String, ProbeError>,
48 pub coordinate_error_max_px: Option<f64>,
49 pub message: String,
50}
51
52#[must_use]
53pub fn default_scale_cases() -> Vec<ScaleCase> {
54 vec![
55 ScaleCase {
56 coordinate_values: 1_000,
57 required: false,
58 },
59 ScaleCase {
60 coordinate_values: 10_000,
61 required: false,
62 },
63 ScaleCase {
64 coordinate_values: 100_000,
65 required: false,
66 },
67 ScaleCase {
68 coordinate_values: 1_000_000,
69 required: true,
70 },
71 ScaleCase {
72 coordinate_values: 5_000_000,
73 required: false,
74 },
75 ]
76}
77
78pub fn run_scale_cases(
85 reference: &ReferenceShim,
86 probe: &ViewerProbe,
87 source: &Path,
88 directory: &Path,
89 cases: &[ScaleCase],
90) -> Result<Vec<ScaleObservation>, String> {
91 fs::create_dir_all(directory)
92 .map_err(|error| format!("could not create scale directory: {error}"))?;
93 Ok(cases
94 .iter()
95 .copied()
96 .map(|case| run_scale_case(reference, probe, source, directory, case))
97 .collect())
98}
99
100fn run_scale_case(
101 reference: &ReferenceShim,
102 probe: &ViewerProbe,
103 source: &Path,
104 directory: &Path,
105 case: ScaleCase,
106) -> ScaleObservation {
107 let case_id = case.id();
108 let input = directory.join(format!("{case_id}-input.dcm"));
109 let output = directory.join(format!("{case_id}-roundtrip.dcm"));
110 let mut observation = failed(directory, case, String::new());
111 let result = (|| -> Result<(), String> {
112 reference
113 .build_scale_ann(source, &input, case.coordinate_values)
114 .map_err(|error| error.to_string())?;
115 let inspected = probe
116 .inspect(source, &input, None, PayloadMode::Digest)
117 .map_err(|error| observation.record_error("inspect", error))?;
118 observation.record_probe("inspect", &inspected);
119 let roundtripped = probe
120 .roundtrip(source, &input, &output, None, PayloadMode::Digest, false)
121 .map_err(|error| observation.record_error("roundtrip", error))?;
122 observation.record_probe("roundtrip", &roundtripped);
123 let comparison = compare_ann(&inspected.report, &roundtripped.report, 1e-6, 1e-9)?;
124 observation.status = if comparison.is_ok() {
125 ScaleStatus::Passed
126 } else {
127 ScaleStatus::Failed
128 };
129 observation.coordinate_error_max_px = Some(comparison.coordinate_error.max);
130 observation.message = comparison
131 .findings
132 .into_iter()
133 .map(|finding| finding.message)
134 .collect::<Vec<_>>()
135 .join("; ");
136 observation.input_bytes = Some(file_size(&input)?);
137 observation.output_bytes = Some(file_size(&output)?);
138 Ok(())
139 })();
140 if let Err(message) = result {
141 observation.status = ScaleStatus::Failed;
142 observation.message = message;
143 observation.input_bytes = fs::metadata(&input).ok().map(|metadata| metadata.len());
144 observation.output_bytes = fs::metadata(&output).ok().map(|metadata| metadata.len());
145 }
146 observation
147}
148
149impl ScaleObservation {
150 fn record_measurements(&mut self, measurements: ProbeMeasurements) {
151 if let Some(runtime) = measurements.runtime_ms {
152 self.runtime_ms = Some(self.runtime_ms.unwrap_or(0.0) + runtime);
153 }
154 if let Some(rss) = measurements.peak_rss_bytes {
155 self.peak_rss_bytes = Some(self.peak_rss_bytes.unwrap_or(0).max(rss));
156 }
157 if let Some(heap) = measurements.peak_tracked_heap_bytes {
158 self.peak_tracked_heap_bytes =
159 Some(self.peak_tracked_heap_bytes.unwrap_or(0).max(heap));
160 }
161 }
162
163 fn record_probe(&mut self, phase: &str, probe: &ProbeObservation) {
164 self.record_measurements(probe.measurements());
165 self.viewer_runtime
166 .insert(phase.to_owned(), probe.report["runtime"].clone());
167 self.viewer_implementation = probe.report["implementation"].clone();
168 self.probe_commands
169 .insert(phase.to_owned(), probe.command.clone());
170 }
171
172 fn record_error(&mut self, phase: &str, error: ProbeError) -> String {
173 let message = error.to_string();
174 self.record_measurements(error.measurements());
175 if let Some(command) = error.command.as_deref() {
176 self.probe_commands
177 .insert(phase.to_owned(), command.clone());
178 }
179 self.execution_errors.insert(phase.to_owned(), error);
180 message
181 }
182}
183
184fn file_size(path: &Path) -> Result<u64, String> {
185 fs::metadata(path)
186 .map(|metadata| metadata.len())
187 .map_err(|error| format!("could not stat {}: {error}", path.display()))
188}
189
190fn failed(directory: &Path, case: ScaleCase, message: String) -> ScaleObservation {
191 let case_id = case.id();
192 let input = directory.join(format!("{case_id}-input.dcm"));
193 let output = directory.join(format!("{case_id}-roundtrip.dcm"));
194 ScaleObservation {
195 case_id,
196 coordinate_values: case.coordinate_values,
197 required: case.required,
198 status: ScaleStatus::Failed,
199 input_bytes: fs::metadata(input).ok().map(|metadata| metadata.len()),
200 output_bytes: fs::metadata(output).ok().map(|metadata| metadata.len()),
201 runtime_ms: None,
202 peak_rss_bytes: None,
203 viewer_runtime: BTreeMap::new(),
204 peak_tracked_heap_bytes: None,
205 viewer_implementation: Value::Object(serde_json::Map::new()),
206 probe_commands: BTreeMap::new(),
207 execution_errors: BTreeMap::new(),
208 coordinate_error_max_px: None,
209 message,
210 }
211}