Skip to main content

wsi_annotation_tests/
scale.rs

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
78/// Generate and roundtrip large-coordinate ANN cases with digest payloads.
79///
80/// # Errors
81///
82/// Returns an error only when the scale output directory cannot be created.
83/// Each workload failure is retained in its observation.
84pub 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}