Skip to main content

wsi_dicom/
report.rs

1//! Report types returned by export and route profiling APIs.
2
3use std::path::PathBuf;
4use std::time::Duration;
5
6use serde::{ser::SerializeStruct, Serialize};
7
8use crate::encode;
9use crate::tile::PixelProfile;
10use crate::time::duration_as_reported_micros;
11
12macro_rules! saturating_add_fields {
13    ($target:expr, $source:expr, [$($field:ident),+ $(,)?]) => {
14        $(
15            $target.$field = $target.$field.saturating_add($source.$field);
16        )+
17    };
18}
19
20macro_rules! saturating_add_values {
21    ($target:expr, [$($field:ident => $value:expr),+ $(,)?]) => {
22        $(
23            $target.$field = $target.$field.saturating_add($value);
24        )+
25    };
26}
27
28macro_rules! max_fields {
29    ($target:expr, $source:expr, [$($field:ident),+ $(,)?]) => {
30        $(
31            $target.$field = $target.$field.max($source.$field);
32        )+
33    };
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub(crate) enum JpegRetileRejectionReason {
38    SourceUnsupported,
39    GeometryMismatch,
40    ProfileUnsupported,
41    McuInvalid,
42}
43
44/// Top-level report returned by a DICOM export.
45#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
46#[non_exhaustive]
47pub struct ExportReport {
48    /// Output directory containing generated DICOM instances.
49    pub output_dir: PathBuf,
50    /// Per-instance reports, one per generated VL WSI DICOM object.
51    pub instances: Vec<InstanceReport>,
52    /// Aggregate metrics across all generated instances.
53    pub metrics: ExportMetrics,
54}
55
56/// Finished compressed frame bytes ready for DICOM encapsulated Pixel Data insertion.
57#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
58#[non_exhaustive]
59pub struct EncodedFrame {
60    /// DICOM transfer syntax UID for the encoded frame.
61    pub transfer_syntax_uid: &'static str,
62    /// Encoded codestream bytes for one DICOM Pixel Data fragment.
63    pub bytes: Vec<u8>,
64    /// Whether frame encoding used a device backend.
65    pub used_device_encode: bool,
66    /// Whether validation decode used a device backend.
67    pub used_device_validation: bool,
68    /// Encode duration in microseconds.
69    pub encode_micros: u128,
70    /// Validation decode duration in microseconds.
71    pub validation_micros: u128,
72}
73
74/// Route profiling report for one source level.
75#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
76#[non_exhaustive]
77pub struct RouteProfileReport {
78    /// Source slide path.
79    pub source_path: PathBuf,
80    /// Transfer syntax UID used for route planning.
81    pub transfer_syntax_uid: &'static str,
82    /// Source pyramid level that was profiled.
83    pub level: u32,
84    /// Number of frames requested for profiling.
85    pub requested_frames: u64,
86    /// Number of frames available at this level.
87    pub available_frames: u64,
88    /// Route metrics collected during profiling.
89    pub metrics: ExportMetrics,
90    /// Wall-clock elapsed time in microseconds.
91    pub elapsed_micros: u128,
92}
93
94/// Route coverage report across source levels.
95#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
96#[non_exhaustive]
97pub struct RouteCoverageReport {
98    /// Source slide path.
99    pub source_path: PathBuf,
100    /// Transfer syntax UID used for route planning.
101    pub transfer_syntax_uid: &'static str,
102    /// Requested frame sample count per level.
103    pub requested_frames_per_level: u64,
104    /// Total frames available across reported levels.
105    pub available_frames: u64,
106    /// Whether every available frame in scope was sampled.
107    pub complete_frame_coverage: bool,
108    /// Per-level route profile reports.
109    pub levels: Vec<RouteProfileReport>,
110    /// Aggregate metrics across reported levels.
111    pub metrics: ExportMetrics,
112    /// Wall-clock elapsed time in microseconds.
113    pub elapsed_micros: u128,
114}
115
116/// Failure encountered while profiling one source in a corpus.
117#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
118#[non_exhaustive]
119pub struct RouteCorpusCoverageFailure {
120    /// Source path that failed.
121    pub source_path: PathBuf,
122    /// Error message for the failed source.
123    pub message: String,
124}
125
126/// Route coverage report for a source corpus.
127#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
128#[non_exhaustive]
129pub struct RouteCorpusCoverageReport {
130    /// Root directory scanned for source slides.
131    pub source_root: PathBuf,
132    /// Common transfer syntax UID used by all successful reports, when one exists.
133    pub transfer_syntax_uid: Option<&'static str>,
134    /// Unique transfer syntax UIDs used by successful per-source reports.
135    pub transfer_syntax_uids: Vec<&'static str>,
136    /// Requested frame sample count per level.
137    pub requested_frames_per_level: u64,
138    /// Optional cap on levels inspected per source.
139    pub max_levels: Option<u32>,
140    /// Number of source files considered.
141    pub sources_considered: usize,
142    /// Total frames available across successful reports.
143    pub available_frames: u64,
144    /// Whether every available frame in successful reports was sampled.
145    pub complete_frame_coverage: bool,
146    /// Successful per-source reports.
147    pub reports: Vec<RouteCoverageReport>,
148    /// Per-source failures that did not stop corpus aggregation.
149    pub failures: Vec<RouteCorpusCoverageFailure>,
150    /// Aggregate metrics across successful reports.
151    pub metrics: ExportMetrics,
152    /// Wall-clock elapsed time in microseconds.
153    pub elapsed_micros: u128,
154}
155
156/// Provenance for the ICC profile written to a DICOM instance.
157#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
158#[serde(rename_all = "snake_case")]
159#[non_exhaustive]
160pub enum IccProfileSource {
161    /// ICC profile came from source-level metadata.
162    Source,
163    /// ICC profile came from an embedded JPEG APP2 profile.
164    SourceJpeg,
165    /// ICC profile was synthesized as sRGB fallback.
166    SynthesizedSrgb,
167    /// ICC profile was synthesized as Display P3 fallback.
168    SynthesizedDisplayP3,
169    /// ICC Profile attribute was intentionally omitted because source metadata was missing.
170    #[default]
171    OmittedMissing,
172}
173
174/// Report for one generated DICOM instance.
175#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
176#[non_exhaustive]
177pub struct InstanceReport {
178    /// Generated DICOM file path.
179    pub path: PathBuf,
180    /// SOP Instance UID written to the file.
181    pub sop_instance_uid: String,
182    /// Series Instance UID written to the file.
183    pub series_instance_uid: String,
184    /// DICOM transfer syntax UID written to the file meta.
185    pub transfer_syntax_uid: &'static str,
186    /// ICC profile provenance for the instance.
187    pub icc_profile_source: IccProfileSource,
188    /// Source scene index.
189    pub scene: usize,
190    /// Source series index within the scene.
191    pub series: usize,
192    /// Source pyramid level exported.
193    pub level: u32,
194    /// Z stack index.
195    pub z: u32,
196    /// Channel index.
197    pub c: u32,
198    /// Timepoint index.
199    pub t: u32,
200    /// Number of DICOM frames in this instance.
201    pub frame_count: u32,
202    /// Per-instance metrics.
203    pub metrics: ExportMetrics,
204}
205
206/// Route, pixel-profile, and non-GPU-specific frame counters.
207#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
208#[non_exhaustive]
209pub struct RouteCounters {
210    /// Total frames handled by the report scope.
211    pub total_frames: u64,
212    /// Frames whose source pixels were prepared on CPU.
213    pub cpu_input_frames: u64,
214    /// Frames decoded from source tiles on GPU.
215    pub gpu_input_decode_frames: u64,
216    /// Frames encoded on GPU.
217    pub gpu_encode_frames: u64,
218    /// Frames validated with GPU decode.
219    pub gpu_validation_frames: u64,
220    /// Frames classified as grayscale.
221    pub gray_frames: u64,
222    /// Frames classified as RGB-like.
223    pub rgb_like_frames: u64,
224    /// Frames with a component count other than gray or RGB-like.
225    pub other_component_frames: u64,
226    /// Frames whose pixel profile could not be classified.
227    pub unknown_pixel_profile_frames: u64,
228    /// Frames with 8-bit samples.
229    pub bits8_frames: u64,
230    /// Frames with 16-bit samples.
231    pub bits16_frames: u64,
232    /// Frames with another bit depth.
233    pub other_bit_depth_frames: u64,
234    /// Frames using a GPU transcode route.
235    pub gpu_transcode_frames: u64,
236    /// Frames using a resident GPU transcode route.
237    pub resident_gpu_transcode_frames: u64,
238    /// Frames using a mixed CPU/GPU transcode route.
239    pub partial_gpu_transcode_frames: u64,
240    /// GPU input decode batch count.
241    pub gpu_input_decode_batches: u64,
242    /// GPU composition batch count.
243    pub gpu_compose_batches: u64,
244    /// GPU encode batch count.
245    pub gpu_encode_batches: u64,
246    /// Frames sampled by automatic route probes.
247    pub auto_route_probe_frames: u64,
248    /// GPU batches dispatched by automatic route probes.
249    pub auto_route_probe_gpu_batches: u64,
250    /// CPU-side automatic route probe time in microseconds.
251    pub auto_route_probe_cpu_micros: u128,
252    /// GPU-side automatic route probe time in microseconds.
253    pub auto_route_probe_gpu_micros: u128,
254    /// Frames routed to GPU input because an automatic probe selected it.
255    pub auto_route_probe_selected_gpu_input_frames: u64,
256    /// Frames routed through CPU fallback.
257    pub cpu_fallback_frames: u64,
258    /// Frames emitted by JPEG Baseline passthrough.
259    pub jpeg_passthrough_frames: u64,
260    /// Frames emitted by JPEG 2000 passthrough.
261    pub j2k_passthrough_frames: u64,
262    /// Frames emitted by direct J2K-to-HTJ2K recoding.
263    pub j2k_direct_htj2k_frames: u64,
264    /// Frames re-tiled in the JPEG compressed domain.
265    pub jpeg_retile_frames: u64,
266    /// Frames rejected from JPEG compressed-domain retiling.
267    pub jpeg_retile_rejected_frames: u64,
268    /// JPEG retile rejections because the source could not provide raw compressed tiles.
269    pub jpeg_retile_source_unsupported_frames: u64,
270    /// JPEG retile rejections because raw tile geometry did not match the requested frame.
271    pub jpeg_retile_geometry_mismatch_frames: u64,
272    /// JPEG retile rejections because the JPEG pixel profile is unsupported for passthrough.
273    pub jpeg_retile_profile_unsupported_frames: u64,
274    /// JPEG retile rejections because the source MCU/restart table was invalid.
275    pub jpeg_retile_mcu_invalid_frames: u64,
276    /// JPEG compressed-domain retiling time in microseconds.
277    pub jpeg_retile_us: u128,
278    /// JPEG-retiled frames then transcoded through HTJ2K 5/3.
279    pub jpeg_retile_to_htj2k_53_frames: u64,
280    /// Frames encoded to JPEG on CPU.
281    pub jpeg_cpu_encode_frames: u64,
282    /// Frames encoded to JPEG on Metal.
283    pub jpeg_metal_encode_frames: u64,
284    /// Frames that required JPEG decode fallback.
285    pub jpeg_decode_fallback_frames: u64,
286}
287
288/// Direct JPEG-to-HTJ2K coefficient-transcode counters and timings.
289#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
290#[non_exhaustive]
291pub struct JpegDirectHtj2kMetrics {
292    /// Frames emitted by direct JPEG-to-HTJ2K 5/3 transcoding.
293    pub jpeg_direct_htj2k_53_frames: u64,
294    /// Frames emitted by direct JPEG-to-HTJ2K 9/7 transcoding.
295    pub jpeg_direct_htj2k_97_frames: u64,
296    /// Frames rejected from direct JPEG-to-HTJ2K routing.
297    pub jpeg_direct_htj2k_rejected_frames: u64,
298    /// JPEG coefficient extraction time in microseconds.
299    pub jpeg_direct_htj2k_extract_micros: u128,
300    /// JPEG coefficient repack time in microseconds.
301    pub jpeg_direct_htj2k_repack_micros: u128,
302    /// JPEG coefficient transform time in microseconds.
303    pub jpeg_direct_htj2k_transform_micros: u128,
304    /// Accelerator time for direct JPEG-to-HTJ2K work in microseconds.
305    pub jpeg_direct_htj2k_accelerator_micros: u128,
306    /// CPU fallback time for direct JPEG-to-HTJ2K work in microseconds.
307    pub jpeg_direct_htj2k_cpu_fallback_micros: u128,
308    /// Direct JPEG-to-HTJ2K DWT decomposition time in microseconds.
309    pub jpeg_direct_htj2k_dwt_decompose_micros: u128,
310    /// Direct JPEG-to-HTJ2K 9/7 pack/upload time in microseconds.
311    pub jpeg_direct_htj2k_dwt97_pack_upload_micros: u128,
312    /// Direct JPEG-to-HTJ2K 9/7 row-lift time in microseconds.
313    pub jpeg_direct_htj2k_dwt97_idct_row_lift_micros: u128,
314    /// Direct JPEG-to-HTJ2K 9/7 column-lift time in microseconds.
315    pub jpeg_direct_htj2k_dwt97_column_lift_micros: u128,
316    /// Direct JPEG-to-HTJ2K 9/7 quantization time in microseconds.
317    pub jpeg_direct_htj2k_dwt97_quantize_codeblock_micros: u128,
318    /// Direct JPEG-to-HTJ2K 9/7 HT encode time in microseconds.
319    pub jpeg_direct_htj2k_dwt97_ht_encode_micros: u128,
320    /// Direct JPEG-to-HTJ2K 9/7 HT kernel time in microseconds.
321    pub jpeg_direct_htj2k_dwt97_ht_kernel_micros: u128,
322    /// Direct JPEG-to-HTJ2K 9/7 HT status readback time in microseconds.
323    pub jpeg_direct_htj2k_dwt97_ht_status_readback_micros: u128,
324    /// Direct JPEG-to-HTJ2K 9/7 HT compaction time in microseconds.
325    pub jpeg_direct_htj2k_dwt97_ht_compact_micros: u128,
326    /// Direct JPEG-to-HTJ2K 9/7 HT output readback time in microseconds.
327    pub jpeg_direct_htj2k_dwt97_ht_output_readback_micros: u128,
328    /// Direct JPEG-to-HTJ2K 9/7 HT code-block dispatch count.
329    pub jpeg_direct_htj2k_dwt97_ht_codeblock_dispatches: u64,
330    /// Direct JPEG-to-HTJ2K 9/7 readback time in microseconds.
331    pub jpeg_direct_htj2k_dwt97_readback_micros: u128,
332    /// Direct JPEG-to-HTJ2K HTJ2K encode time in microseconds.
333    pub jpeg_direct_htj2k_htj2k_encode_micros: u128,
334    /// Direct JPEG-to-HTJ2K accelerator encode dispatch count.
335    pub jpeg_direct_htj2k_encode_accelerator_dispatches: u64,
336    /// Direct JPEG-to-HTJ2K HT block-code dispatch count.
337    pub jpeg_direct_htj2k_encode_ht_code_block_dispatches: u64,
338    /// Direct JPEG-to-HTJ2K packetization dispatch count.
339    pub jpeg_direct_htj2k_encode_packetization_dispatches: u64,
340    /// Direct JPEG-to-HTJ2K batch count.
341    pub jpeg_direct_htj2k_batch_count: u64,
342    /// Direct JPEG-to-HTJ2K jobs submitted in batches.
343    pub jpeg_direct_htj2k_batch_jobs: u64,
344    /// Direct JPEG-to-HTJ2K accelerator attempts.
345    pub jpeg_direct_htj2k_accelerator_attempts: u64,
346    /// Direct JPEG-to-HTJ2K accelerator jobs.
347    pub jpeg_direct_htj2k_accelerator_jobs: u64,
348    /// Direct JPEG-to-HTJ2K accelerator dispatches.
349    pub jpeg_direct_htj2k_accelerator_dispatches: u64,
350    /// Direct JPEG-to-HTJ2K jobs dispatched to accelerator.
351    pub jpeg_direct_htj2k_accelerator_dispatched_jobs: u64,
352    /// Direct JPEG-to-HTJ2K jobs handled by CPU fallback.
353    pub jpeg_direct_htj2k_cpu_fallback_jobs: u64,
354}
355
356/// Processing and writer timing metrics.
357#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
358#[non_exhaustive]
359pub struct WriteTimings {
360    /// Input decode time in microseconds.
361    pub input_decode_micros: u128,
362    /// Frame composition time in microseconds.
363    pub compose_micros: u128,
364    /// Frame encode time in microseconds.
365    pub encode_micros: u128,
366    /// Runtime validation decode time in microseconds.
367    pub validation_micros: u128,
368    /// CPU-observed GPU dispatch time in microseconds.
369    pub gpu_dispatch_micros: u128,
370    /// Streaming pixel-data write time in microseconds.
371    pub streaming_write_micros: u128,
372    /// Pixel-data offset patch time in microseconds.
373    pub pixel_data_patch_micros: u128,
374    /// Writer backpressure time in microseconds.
375    pub writer_backpressure_micros: u128,
376    /// Total DICOM write time in microseconds.
377    pub write_micros: u128,
378}
379
380/// GPU JPEG 2000 encode configuration, counters, and stage timings.
381#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
382#[non_exhaustive]
383pub struct GpuEncodeMetrics {
384    /// Configured GPU encode in-flight tile count.
385    pub gpu_encode_configured_inflight_tiles: u64,
386    /// Effective GPU encode in-flight tile count.
387    pub gpu_encode_effective_inflight_tiles: u64,
388    /// Maximum observed GPU encode in-flight tile count.
389    pub gpu_encode_max_observed_inflight_tiles: u64,
390    /// Configured GPU encode memory budget in MiB.
391    pub gpu_encode_configured_memory_mib: u64,
392    /// Effective GPU encode memory budget in MiB.
393    pub gpu_encode_effective_memory_mib: u64,
394    /// CPU-observed GPU encode wall time in microseconds.
395    pub gpu_encode_wall_micros: u128,
396    /// GPU-reported encode hardware time in microseconds.
397    pub gpu_encode_hardware_micros: u128,
398    /// Positive CPU dispatch overhead after subtracting hardware time.
399    pub gpu_encode_dispatch_overhead_micros: u128,
400    /// GPU encode planning time in microseconds.
401    pub gpu_encode_plan_micros: u128,
402    /// GPU encode prepare/submit time in microseconds.
403    pub gpu_encode_prepare_submit_micros: u128,
404    /// GPU HT table build time in microseconds.
405    pub gpu_encode_ht_table_build_micros: u128,
406    /// GPU HT buffer allocation time in microseconds.
407    pub gpu_encode_ht_buffer_allocation_micros: u128,
408    /// GPU command encoding time in microseconds.
409    pub gpu_encode_ht_command_encode_micros: u128,
410    /// GPU codestream wait/readback time in microseconds.
411    pub gpu_encode_codestream_wait_micros: u128,
412    /// GPU encode chunk count.
413    pub gpu_encode_chunk_count: u64,
414    /// GPU encode tile count.
415    pub gpu_encode_tile_count: u64,
416    /// GPU encode code-block count.
417    pub gpu_encode_code_block_count: u64,
418    /// Effective GPU pipeline depth.
419    pub gpu_pipeline_depth: u64,
420    /// Maximum rows per GPU row batch.
421    pub gpu_row_batch_rows_max: u64,
422    /// Target tiles per GPU row batch.
423    pub gpu_row_batch_target_tiles: u64,
424}
425
426/// Export, routing, encode, validation, and writer counters.
427#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
428#[non_exhaustive]
429pub struct ExportMetrics {
430    /// Route, pixel-profile, and non-GPU-specific frame counters.
431    #[serde(flatten)]
432    pub routes: RouteCounters,
433    /// Direct JPEG-to-HTJ2K coefficient-transcode counters and timings.
434    #[serde(flatten)]
435    pub jpeg_direct_htj2k: JpegDirectHtj2kMetrics,
436    /// GPU JPEG 2000 encode configuration, counters, and stage timings.
437    #[serde(flatten)]
438    pub gpu_encode: GpuEncodeMetrics,
439    /// Processing and writer timing metrics.
440    #[serde(flatten)]
441    pub timings: WriteTimings,
442}
443
444impl Serialize for GpuEncodeMetrics {
445    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
446    where
447        S: serde::Serializer,
448    {
449        let mut state =
450            serializer.serialize_struct("GpuEncodeMetrics", Self::SERIALIZED_FIELD_COUNT)?;
451        state.serialize_field(
452            "gpu_encode_configured_inflight_tiles",
453            &self.gpu_encode_configured_inflight_tiles,
454        )?;
455        state.serialize_field(
456            "gpu_encode_effective_inflight_tiles",
457            &self.gpu_encode_effective_inflight_tiles,
458        )?;
459        state.serialize_field(
460            "gpu_encode_max_observed_inflight_tiles",
461            &self.gpu_encode_max_observed_inflight_tiles,
462        )?;
463        state.serialize_field(
464            "gpu_encode_configured_memory_mib",
465            &self.gpu_encode_configured_memory_mib,
466        )?;
467        state.serialize_field(
468            "gpu_encode_effective_memory_mib",
469            &self.gpu_encode_effective_memory_mib,
470        )?;
471        state.serialize_field("gpu_encode_wall_micros", &self.gpu_encode_wall_micros)?;
472        state.serialize_field(
473            "gpu_encode_effective_parallelism",
474            &self.effective_parallelism(),
475        )?;
476        state.serialize_field(
477            "gpu_encode_hardware_micros",
478            &self.gpu_encode_hardware_micros,
479        )?;
480        state.serialize_field(
481            "gpu_encode_dispatch_overhead_micros",
482            &self.gpu_encode_dispatch_overhead_micros,
483        )?;
484        state.serialize_field("gpu_encode_plan_micros", &self.gpu_encode_plan_micros)?;
485        state.serialize_field(
486            "gpu_encode_prepare_submit_micros",
487            &self.gpu_encode_prepare_submit_micros,
488        )?;
489        state.serialize_field(
490            "gpu_encode_ht_table_build_micros",
491            &self.gpu_encode_ht_table_build_micros,
492        )?;
493        state.serialize_field(
494            "gpu_encode_ht_buffer_allocation_micros",
495            &self.gpu_encode_ht_buffer_allocation_micros,
496        )?;
497        state.serialize_field(
498            "gpu_encode_ht_command_encode_micros",
499            &self.gpu_encode_ht_command_encode_micros,
500        )?;
501        state.serialize_field(
502            "gpu_encode_codestream_wait_micros",
503            &self.gpu_encode_codestream_wait_micros,
504        )?;
505        state.serialize_field("gpu_encode_chunk_count", &self.gpu_encode_chunk_count)?;
506        state.serialize_field("gpu_encode_tile_count", &self.gpu_encode_tile_count)?;
507        state.serialize_field(
508            "gpu_encode_code_block_count",
509            &self.gpu_encode_code_block_count,
510        )?;
511        state.serialize_field("gpu_pipeline_depth", &self.gpu_pipeline_depth)?;
512        state.serialize_field("gpu_row_batch_rows_max", &self.gpu_row_batch_rows_max)?;
513        state.serialize_field(
514            "gpu_row_batch_target_tiles",
515            &self.gpu_row_batch_target_tiles,
516        )?;
517        state.end()
518    }
519}
520
521impl GpuEncodeMetrics {
522    const SERIALIZED_FIELD_COUNT: usize = 21;
523
524    /// Ratio of summed GPU encode hardware time to observed GPU encode wall time.
525    pub fn effective_parallelism(&self) -> f64 {
526        if self.gpu_encode_wall_micros == 0 {
527            0.0
528        } else {
529            self.gpu_encode_hardware_micros as f64 / self.gpu_encode_wall_micros as f64
530        }
531    }
532}
533
534impl ExportMetrics {
535    /// Number of fields emitted by the public serialized metrics object.
536    pub const SERIALIZED_FIELD_COUNT: usize = 99;
537
538    /// Total frames emitted by compressed passthrough routes.
539    pub fn route_passthrough_frames(&self) -> u64 {
540        self.routes
541            .jpeg_passthrough_frames
542            .saturating_add(self.routes.j2k_passthrough_frames)
543    }
544
545    /// Frames emitted by JPEG compressed-domain retiling without HTJ2K transcode.
546    pub fn jpeg_retile_baseline_frames(&self) -> u64 {
547        self.routes
548            .jpeg_retile_frames
549            .saturating_sub(self.routes.jpeg_retile_to_htj2k_53_frames)
550    }
551
552    /// Frames not accounted for by a known route counter.
553    pub fn route_unclassified_frames(&self) -> u64 {
554        self.routes
555            .total_frames
556            .saturating_sub(self.route_passthrough_frames())
557            .saturating_sub(self.routes.j2k_direct_htj2k_frames)
558            .saturating_sub(self.jpeg_direct_htj2k.jpeg_direct_htj2k_53_frames)
559            .saturating_sub(self.jpeg_direct_htj2k.jpeg_direct_htj2k_97_frames)
560            .saturating_sub(self.jpeg_retile_baseline_frames())
561            .saturating_sub(self.routes.gpu_transcode_frames)
562            .saturating_sub(self.routes.cpu_fallback_frames)
563    }
564
565    pub(crate) fn add_assign(&mut self, other: Self) {
566        saturating_add_fields!(
567            self.routes,
568            other.routes,
569            [
570                total_frames,
571                cpu_input_frames,
572                gpu_input_decode_frames,
573                gpu_encode_frames,
574                gpu_validation_frames,
575                gray_frames,
576                rgb_like_frames,
577                other_component_frames,
578                unknown_pixel_profile_frames,
579                bits8_frames,
580                bits16_frames,
581                other_bit_depth_frames,
582                gpu_transcode_frames,
583                resident_gpu_transcode_frames,
584                partial_gpu_transcode_frames,
585                gpu_input_decode_batches,
586                gpu_compose_batches,
587                gpu_encode_batches,
588                auto_route_probe_frames,
589                auto_route_probe_gpu_batches,
590                auto_route_probe_cpu_micros,
591                auto_route_probe_gpu_micros,
592                auto_route_probe_selected_gpu_input_frames,
593                cpu_fallback_frames,
594                jpeg_passthrough_frames,
595                j2k_passthrough_frames,
596                j2k_direct_htj2k_frames,
597                jpeg_retile_frames,
598                jpeg_retile_rejected_frames,
599                jpeg_retile_source_unsupported_frames,
600                jpeg_retile_geometry_mismatch_frames,
601                jpeg_retile_profile_unsupported_frames,
602                jpeg_retile_mcu_invalid_frames,
603                jpeg_retile_us,
604                jpeg_retile_to_htj2k_53_frames,
605                jpeg_cpu_encode_frames,
606                jpeg_metal_encode_frames,
607                jpeg_decode_fallback_frames,
608            ]
609        );
610        saturating_add_fields!(
611            self.jpeg_direct_htj2k,
612            other.jpeg_direct_htj2k,
613            [
614                jpeg_direct_htj2k_53_frames,
615                jpeg_direct_htj2k_97_frames,
616                jpeg_direct_htj2k_rejected_frames,
617                jpeg_direct_htj2k_extract_micros,
618                jpeg_direct_htj2k_repack_micros,
619                jpeg_direct_htj2k_transform_micros,
620                jpeg_direct_htj2k_accelerator_micros,
621                jpeg_direct_htj2k_cpu_fallback_micros,
622                jpeg_direct_htj2k_dwt_decompose_micros,
623                jpeg_direct_htj2k_dwt97_pack_upload_micros,
624                jpeg_direct_htj2k_dwt97_idct_row_lift_micros,
625                jpeg_direct_htj2k_dwt97_column_lift_micros,
626                jpeg_direct_htj2k_dwt97_quantize_codeblock_micros,
627                jpeg_direct_htj2k_dwt97_ht_encode_micros,
628                jpeg_direct_htj2k_dwt97_ht_kernel_micros,
629                jpeg_direct_htj2k_dwt97_ht_status_readback_micros,
630                jpeg_direct_htj2k_dwt97_ht_compact_micros,
631                jpeg_direct_htj2k_dwt97_ht_output_readback_micros,
632                jpeg_direct_htj2k_dwt97_ht_codeblock_dispatches,
633                jpeg_direct_htj2k_dwt97_readback_micros,
634                jpeg_direct_htj2k_htj2k_encode_micros,
635                jpeg_direct_htj2k_encode_accelerator_dispatches,
636                jpeg_direct_htj2k_encode_ht_code_block_dispatches,
637                jpeg_direct_htj2k_encode_packetization_dispatches,
638                jpeg_direct_htj2k_batch_count,
639                jpeg_direct_htj2k_batch_jobs,
640                jpeg_direct_htj2k_accelerator_attempts,
641                jpeg_direct_htj2k_accelerator_jobs,
642                jpeg_direct_htj2k_accelerator_dispatches,
643                jpeg_direct_htj2k_accelerator_dispatched_jobs,
644                jpeg_direct_htj2k_cpu_fallback_jobs,
645            ]
646        );
647        saturating_add_fields!(
648            self.timings,
649            other.timings,
650            [
651                input_decode_micros,
652                compose_micros,
653                encode_micros,
654                validation_micros,
655                gpu_dispatch_micros,
656                streaming_write_micros,
657                pixel_data_patch_micros,
658                writer_backpressure_micros,
659                write_micros,
660            ]
661        );
662        saturating_add_fields!(
663            self.gpu_encode,
664            other.gpu_encode,
665            [
666                gpu_encode_wall_micros,
667                gpu_encode_hardware_micros,
668                gpu_encode_dispatch_overhead_micros,
669                gpu_encode_plan_micros,
670                gpu_encode_prepare_submit_micros,
671                gpu_encode_ht_table_build_micros,
672                gpu_encode_ht_buffer_allocation_micros,
673                gpu_encode_ht_command_encode_micros,
674                gpu_encode_codestream_wait_micros,
675                gpu_encode_chunk_count,
676                gpu_encode_tile_count,
677                gpu_encode_code_block_count,
678            ]
679        );
680        max_fields!(
681            self.gpu_encode,
682            other.gpu_encode,
683            [
684                gpu_encode_configured_inflight_tiles,
685                gpu_encode_effective_inflight_tiles,
686                gpu_encode_max_observed_inflight_tiles,
687                gpu_encode_configured_memory_mib,
688                gpu_encode_effective_memory_mib,
689                gpu_pipeline_depth,
690                gpu_row_batch_rows_max,
691                gpu_row_batch_target_tiles,
692            ]
693        );
694    }
695
696    pub(crate) fn record_cpu_input(&mut self) {
697        increment_u64(&mut self.routes.total_frames);
698        increment_u64(&mut self.routes.cpu_input_frames);
699    }
700
701    pub(crate) fn record_gpu_input(&mut self) {
702        increment_u64(&mut self.routes.total_frames);
703        increment_u64(&mut self.routes.gpu_input_decode_frames);
704    }
705
706    pub(crate) fn record_passthrough_frame(&mut self) {
707        increment_u64(&mut self.routes.total_frames);
708        increment_u64(&mut self.routes.jpeg_passthrough_frames);
709    }
710
711    pub(crate) fn record_j2k_passthrough_frame(&mut self) {
712        increment_u64(&mut self.routes.total_frames);
713        increment_u64(&mut self.routes.j2k_passthrough_frames);
714    }
715
716    pub(crate) fn record_j2k_direct_htj2k_frame(&mut self, transcode_micros: u128) {
717        increment_u64(&mut self.routes.total_frames);
718        increment_u64(&mut self.routes.j2k_direct_htj2k_frames);
719        self.timings.encode_micros = self.timings.encode_micros.saturating_add(transcode_micros);
720    }
721
722    pub(crate) fn record_jpeg_direct_htj2k_53_frame(&mut self, transcode_micros: u128) {
723        increment_u64(&mut self.routes.total_frames);
724        increment_u64(&mut self.jpeg_direct_htj2k.jpeg_direct_htj2k_53_frames);
725        self.timings.encode_micros = self.timings.encode_micros.saturating_add(transcode_micros);
726    }
727
728    pub(crate) fn record_jpeg_direct_htj2k_97_frame(&mut self, transcode_micros: u128) {
729        increment_u64(&mut self.routes.total_frames);
730        increment_u64(&mut self.jpeg_direct_htj2k.jpeg_direct_htj2k_97_frames);
731        self.timings.encode_micros = self.timings.encode_micros.saturating_add(transcode_micros);
732    }
733
734    pub(crate) fn record_jpeg_direct_htj2k_rejected_frame(&mut self) {
735        increment_u64(&mut self.jpeg_direct_htj2k.jpeg_direct_htj2k_rejected_frames);
736    }
737
738    pub(crate) fn record_jpeg_direct_htj2k_timings(
739        &mut self,
740        timings: j2k_transcode::TranscodeTimingReport,
741    ) {
742        saturating_add_values!(
743            self.jpeg_direct_htj2k,
744            [
745                jpeg_direct_htj2k_extract_micros => timings.jpeg_dct_extract_us,
746                jpeg_direct_htj2k_repack_micros => timings.jpeg_dct_repack_us,
747                jpeg_direct_htj2k_transform_micros => timings.dct_to_wavelet_total_us,
748                jpeg_direct_htj2k_accelerator_micros => timings.dct_to_wavelet_accelerator_us,
749                jpeg_direct_htj2k_cpu_fallback_micros => timings.dct_to_wavelet_cpu_fallback_us,
750                jpeg_direct_htj2k_dwt_decompose_micros => timings.dwt_decompose_us,
751                jpeg_direct_htj2k_dwt97_pack_upload_micros => timings.dwt97_batch_pack_upload_us,
752                jpeg_direct_htj2k_dwt97_idct_row_lift_micros => timings.dwt97_batch_idct_row_lift_us,
753                jpeg_direct_htj2k_dwt97_column_lift_micros => timings.dwt97_batch_column_lift_us,
754                jpeg_direct_htj2k_dwt97_quantize_codeblock_micros => timings.dwt97_batch_quantize_codeblock_us,
755                jpeg_direct_htj2k_dwt97_ht_encode_micros => timings.dwt97_batch_ht_encode_us,
756                jpeg_direct_htj2k_dwt97_ht_kernel_micros => timings.dwt97_batch_ht_kernel_us,
757                jpeg_direct_htj2k_dwt97_ht_status_readback_micros => timings.dwt97_batch_ht_status_readback_us,
758                jpeg_direct_htj2k_dwt97_ht_compact_micros => timings.dwt97_batch_ht_compact_us,
759                jpeg_direct_htj2k_dwt97_ht_output_readback_micros => timings.dwt97_batch_ht_output_readback_us,
760                jpeg_direct_htj2k_dwt97_ht_codeblock_dispatches => timings.dwt97_batch_ht_codeblock_dispatches as u64,
761                jpeg_direct_htj2k_dwt97_readback_micros => timings.dwt97_batch_readback_us,
762                jpeg_direct_htj2k_htj2k_encode_micros => timings.htj2k_encode_us,
763                jpeg_direct_htj2k_encode_accelerator_dispatches => timings.htj2k_encode_accelerator_dispatches as u64,
764                jpeg_direct_htj2k_encode_ht_code_block_dispatches => timings.htj2k_encode_ht_code_block_dispatches as u64,
765                jpeg_direct_htj2k_encode_packetization_dispatches => timings.htj2k_encode_packetization_dispatches as u64,
766                jpeg_direct_htj2k_batch_count => timings.batch_count as u64,
767                jpeg_direct_htj2k_batch_jobs => timings.batch_jobs as u64,
768                jpeg_direct_htj2k_accelerator_attempts => timings.accelerator_attempts as u64,
769                jpeg_direct_htj2k_accelerator_jobs => timings.accelerator_jobs as u64,
770                jpeg_direct_htj2k_accelerator_dispatches => timings.accelerator_dispatches as u64,
771                jpeg_direct_htj2k_accelerator_dispatched_jobs => timings.accelerator_dispatched_jobs as u64,
772                jpeg_direct_htj2k_cpu_fallback_jobs => timings.cpu_fallback_jobs as u64,
773            ]
774        );
775    }
776
777    pub(crate) fn record_jpeg_retile_baseline_frame(&mut self, duration: Duration) {
778        increment_u64(&mut self.routes.total_frames);
779        increment_u64(&mut self.routes.jpeg_retile_frames);
780        add_duration_micros(&mut self.routes.jpeg_retile_us, duration);
781    }
782
783    pub(crate) fn record_jpeg_retile_to_htj2k_53_frame(&mut self, duration: Duration) {
784        increment_u64(&mut self.routes.jpeg_retile_frames);
785        increment_u64(&mut self.routes.jpeg_retile_to_htj2k_53_frames);
786        add_duration_micros(&mut self.routes.jpeg_retile_us, duration);
787    }
788
789    pub(crate) fn record_jpeg_retile_rejected_frame(&mut self, reason: JpegRetileRejectionReason) {
790        increment_u64(&mut self.routes.jpeg_retile_rejected_frames);
791        match reason {
792            JpegRetileRejectionReason::SourceUnsupported => {
793                increment_u64(&mut self.routes.jpeg_retile_source_unsupported_frames);
794            }
795            JpegRetileRejectionReason::GeometryMismatch => {
796                increment_u64(&mut self.routes.jpeg_retile_geometry_mismatch_frames);
797            }
798            JpegRetileRejectionReason::ProfileUnsupported => {
799                increment_u64(&mut self.routes.jpeg_retile_profile_unsupported_frames);
800            }
801            JpegRetileRejectionReason::McuInvalid => {
802                increment_u64(&mut self.routes.jpeg_retile_mcu_invalid_frames);
803            }
804        }
805    }
806
807    pub(crate) fn record_pixel_profile(&mut self, profile: PixelProfile) {
808        match profile.components {
809            1 => self.routes.gray_frames = self.routes.gray_frames.saturating_add(1),
810            3 => self.routes.rgb_like_frames = self.routes.rgb_like_frames.saturating_add(1),
811            _ => {
812                self.routes.other_component_frames =
813                    self.routes.other_component_frames.saturating_add(1);
814            }
815        }
816        match profile.bits_allocated {
817            8 => self.routes.bits8_frames = self.routes.bits8_frames.saturating_add(1),
818            16 => self.routes.bits16_frames = self.routes.bits16_frames.saturating_add(1),
819            _ => {
820                self.routes.other_bit_depth_frames =
821                    self.routes.other_bit_depth_frames.saturating_add(1)
822            }
823        }
824    }
825
826    pub(crate) fn record_unknown_pixel_profile(&mut self) {
827        increment_u64(&mut self.routes.unknown_pixel_profile_frames);
828    }
829
830    pub(crate) fn record_transcode_route(&mut self, used_gpu_input: bool, used_gpu_encode: bool) {
831        if used_gpu_input || used_gpu_encode {
832            self.routes.gpu_transcode_frames = self.routes.gpu_transcode_frames.saturating_add(1);
833            if used_gpu_input && used_gpu_encode {
834                self.routes.resident_gpu_transcode_frames =
835                    self.routes.resident_gpu_transcode_frames.saturating_add(1);
836            } else {
837                self.routes.partial_gpu_transcode_frames =
838                    self.routes.partial_gpu_transcode_frames.saturating_add(1);
839            }
840        } else {
841            self.routes.cpu_fallback_frames = self.routes.cpu_fallback_frames.saturating_add(1);
842        }
843    }
844
845    pub(crate) fn record_gpu_batches(
846        &mut self,
847        input_decode_batches: u64,
848        compose_batches: u64,
849        encode_batches: u64,
850    ) {
851        self.routes.gpu_input_decode_batches = self
852            .routes
853            .gpu_input_decode_batches
854            .saturating_add(input_decode_batches);
855        self.routes.gpu_compose_batches = self
856            .routes
857            .gpu_compose_batches
858            .saturating_add(compose_batches);
859        self.routes.gpu_encode_batches = self
860            .routes
861            .gpu_encode_batches
862            .saturating_add(encode_batches);
863    }
864
865    #[cfg(all(feature = "metal", target_os = "macos"))]
866    pub(crate) fn record_gpu_encode_batch_stats(
867        &mut self,
868        stats: encode::DicomJ2kGpuEncodeBatchStats,
869    ) {
870        self.gpu_encode.gpu_encode_configured_inflight_tiles = self
871            .gpu_encode
872            .gpu_encode_configured_inflight_tiles
873            .max(stats.configured_inflight_tiles.unwrap_or(0) as u64);
874        self.gpu_encode.gpu_encode_effective_inflight_tiles = self
875            .gpu_encode
876            .gpu_encode_effective_inflight_tiles
877            .max(stats.effective_inflight_tiles as u64);
878        self.gpu_encode.gpu_encode_max_observed_inflight_tiles = self
879            .gpu_encode
880            .gpu_encode_max_observed_inflight_tiles
881            .max(stats.max_observed_inflight_tiles as u64);
882        self.gpu_encode.gpu_encode_configured_memory_mib = self
883            .gpu_encode
884            .gpu_encode_configured_memory_mib
885            .max(stats.configured_memory_mib.unwrap_or(0));
886        self.gpu_encode.gpu_encode_effective_memory_mib = self
887            .gpu_encode
888            .gpu_encode_effective_memory_mib
889            .max(stats.effective_memory_mib);
890        self.gpu_encode.gpu_encode_wall_micros = self
891            .gpu_encode
892            .gpu_encode_wall_micros
893            .saturating_add(duration_as_reported_micros(stats.encode_wall_duration));
894        self.gpu_encode.gpu_encode_plan_micros = self
895            .gpu_encode
896            .gpu_encode_plan_micros
897            .saturating_add(duration_as_reported_micros(stats.stage_stats.plan_duration));
898        self.gpu_encode.gpu_encode_prepare_submit_micros = self
899            .gpu_encode
900            .gpu_encode_prepare_submit_micros
901            .saturating_add(duration_as_reported_micros(
902                stats.stage_stats.prepare_submit_duration,
903            ));
904        self.gpu_encode.gpu_encode_ht_table_build_micros = self
905            .gpu_encode
906            .gpu_encode_ht_table_build_micros
907            .saturating_add(duration_as_reported_micros(
908                stats.stage_stats.ht_table_build_duration,
909            ));
910        self.gpu_encode.gpu_encode_ht_buffer_allocation_micros = self
911            .gpu_encode
912            .gpu_encode_ht_buffer_allocation_micros
913            .saturating_add(duration_as_reported_micros(
914                stats.stage_stats.ht_buffer_allocation_duration,
915            ));
916        self.gpu_encode.gpu_encode_ht_command_encode_micros = self
917            .gpu_encode
918            .gpu_encode_ht_command_encode_micros
919            .saturating_add(duration_as_reported_micros(
920                stats.stage_stats.ht_command_encode_duration,
921            ));
922        self.gpu_encode.gpu_encode_codestream_wait_micros = self
923            .gpu_encode
924            .gpu_encode_codestream_wait_micros
925            .saturating_add(duration_as_reported_micros(
926                stats.stage_stats.codestream_wait_duration,
927            ));
928        self.gpu_encode.gpu_encode_chunk_count = self
929            .gpu_encode
930            .gpu_encode_chunk_count
931            .saturating_add(stats.stage_stats.chunk_count as u64);
932        self.gpu_encode.gpu_encode_tile_count = self
933            .gpu_encode
934            .gpu_encode_tile_count
935            .saturating_add(stats.stage_stats.tile_count as u64);
936        self.gpu_encode.gpu_encode_code_block_count = self
937            .gpu_encode
938            .gpu_encode_code_block_count
939            .saturating_add(stats.stage_stats.code_block_count as u64);
940    }
941
942    #[cfg(all(feature = "metal", target_os = "macos"))]
943    pub(crate) fn record_auto_route_probe(
944        &mut self,
945        frames: u64,
946        cpu_duration: Duration,
947        gpu_duration: Duration,
948        gpu_batches: u64,
949        selected_gpu_input: bool,
950    ) {
951        self.routes.auto_route_probe_frames =
952            self.routes.auto_route_probe_frames.saturating_add(frames);
953        self.routes.auto_route_probe_gpu_batches = self
954            .routes
955            .auto_route_probe_gpu_batches
956            .saturating_add(gpu_batches);
957        self.routes.auto_route_probe_cpu_micros = self
958            .routes
959            .auto_route_probe_cpu_micros
960            .saturating_add(duration_as_reported_micros(cpu_duration));
961        self.routes.auto_route_probe_gpu_micros = self
962            .routes
963            .auto_route_probe_gpu_micros
964            .saturating_add(duration_as_reported_micros(gpu_duration));
965        if selected_gpu_input {
966            self.routes.auto_route_probe_selected_gpu_input_frames = self
967                .routes
968                .auto_route_probe_selected_gpu_input_frames
969                .saturating_add(frames);
970        }
971    }
972
973    pub(crate) fn record_jpeg_decode_fallback(&mut self) {
974        increment_u64(&mut self.routes.jpeg_decode_fallback_frames);
975    }
976
977    pub(crate) fn record_jpeg_cpu_fallback_route_classification(&mut self) {
978        increment_u64(&mut self.routes.total_frames);
979        increment_u64(&mut self.routes.cpu_fallback_frames);
980        increment_u64(&mut self.routes.jpeg_decode_fallback_frames);
981        self.record_unknown_pixel_profile();
982    }
983
984    pub(crate) fn record_j2k_passthrough_only_fallback_classification(&mut self) {
985        increment_u64(&mut self.routes.total_frames);
986        increment_u64(&mut self.routes.cpu_fallback_frames);
987        self.record_unknown_pixel_profile();
988    }
989
990    pub(crate) fn record_jpeg_cpu_encode(&mut self, duration: Duration) {
991        increment_u64(&mut self.routes.jpeg_cpu_encode_frames);
992        self.record_encode_duration(duration);
993    }
994
995    pub(crate) fn record_jpeg_metal_batch_encode(&mut self, frames: u64, duration: Duration) {
996        self.routes.jpeg_metal_encode_frames =
997            self.routes.jpeg_metal_encode_frames.saturating_add(frames);
998        self.record_encode_duration(duration);
999        self.record_gpu_dispatch_duration(duration);
1000    }
1001
1002    pub(crate) fn record_encoded_frame(&mut self, encoded: &encode::EncodedDicomJ2kFrame) {
1003        if encoded.used_device_encode {
1004            increment_u64(&mut self.routes.gpu_encode_frames);
1005            if let Some(duration) = encoded.gpu_encode_wall_duration {
1006                self.record_gpu_encode_wall_duration(duration);
1007            }
1008            self.record_gpu_dispatch_duration(encoded.encode_duration);
1009            self.record_gpu_encode_hardware_duration(
1010                encoded.device_gpu_duration,
1011                encoded.encode_duration,
1012            );
1013        }
1014        if encoded.used_device_validation {
1015            increment_u64(&mut self.routes.gpu_validation_frames);
1016            self.record_gpu_dispatch_duration(encoded.validation_duration);
1017        }
1018        self.record_encode_duration(encoded.encode_duration);
1019        self.record_validation_duration(encoded.validation_duration);
1020    }
1021
1022    pub(crate) fn record_input_decode_duration(&mut self, duration: Duration) {
1023        add_duration_micros(&mut self.timings.input_decode_micros, duration);
1024    }
1025
1026    pub(crate) fn record_gpu_input_decode_duration(&mut self, duration: Duration) {
1027        self.record_input_decode_duration(duration);
1028        self.record_gpu_dispatch_duration(duration);
1029    }
1030
1031    pub(crate) fn record_compose_duration(&mut self, duration: Duration) {
1032        add_duration_micros(&mut self.timings.compose_micros, duration);
1033    }
1034
1035    #[cfg(all(feature = "metal", target_os = "macos"))]
1036    pub(crate) fn record_gpu_compose_duration(&mut self, duration: Duration) {
1037        self.record_compose_duration(duration);
1038        self.record_gpu_dispatch_duration(duration);
1039    }
1040
1041    pub(crate) fn record_encode_duration(&mut self, duration: Duration) {
1042        add_duration_micros(&mut self.timings.encode_micros, duration);
1043    }
1044
1045    pub(crate) fn record_validation_duration(&mut self, duration: Duration) {
1046        add_duration_micros(&mut self.timings.validation_micros, duration);
1047    }
1048
1049    pub(crate) fn record_gpu_dispatch_duration(&mut self, duration: Duration) {
1050        add_duration_micros(&mut self.timings.gpu_dispatch_micros, duration);
1051    }
1052
1053    pub(crate) fn record_gpu_encode_hardware_duration(
1054        &mut self,
1055        gpu_duration: Option<Duration>,
1056        dispatch_duration: Duration,
1057    ) {
1058        let Some(gpu_duration) = gpu_duration else {
1059            return;
1060        };
1061        let hardware_micros = duration_as_reported_micros(gpu_duration);
1062        self.gpu_encode.gpu_encode_hardware_micros = self
1063            .gpu_encode
1064            .gpu_encode_hardware_micros
1065            .saturating_add(hardware_micros);
1066        let overhead = dispatch_duration.saturating_sub(gpu_duration);
1067        self.gpu_encode.gpu_encode_dispatch_overhead_micros = self
1068            .gpu_encode
1069            .gpu_encode_dispatch_overhead_micros
1070            .saturating_add(duration_as_reported_micros(overhead));
1071    }
1072
1073    pub(crate) fn record_gpu_encode_wall_duration(&mut self, duration: Duration) {
1074        add_duration_micros(&mut self.gpu_encode.gpu_encode_wall_micros, duration);
1075    }
1076
1077    pub(crate) fn record_write_duration(&mut self, duration: Duration) {
1078        add_duration_micros(&mut self.timings.write_micros, duration);
1079    }
1080
1081    pub(crate) fn record_streaming_write_duration(&mut self, duration: Duration) {
1082        add_duration_micros(&mut self.timings.streaming_write_micros, duration);
1083    }
1084
1085    pub(crate) fn record_pixel_data_patch_duration(&mut self, duration: Duration) {
1086        add_duration_micros(&mut self.timings.pixel_data_patch_micros, duration);
1087    }
1088
1089    #[cfg(all(feature = "metal", target_os = "macos"))]
1090    pub(crate) fn record_gpu_pipeline_depth(&mut self, depth: usize) {
1091        self.gpu_encode.gpu_pipeline_depth = self.gpu_encode.gpu_pipeline_depth.max(depth as u64);
1092    }
1093
1094    #[cfg(all(feature = "metal", target_os = "macos"))]
1095    pub(crate) fn record_gpu_row_batch_config(&mut self, rows: usize, target_tiles: Option<usize>) {
1096        self.gpu_encode.gpu_row_batch_rows_max =
1097            self.gpu_encode.gpu_row_batch_rows_max.max(rows as u64);
1098        self.gpu_encode.gpu_row_batch_target_tiles = self
1099            .gpu_encode
1100            .gpu_row_batch_target_tiles
1101            .max(target_tiles.unwrap_or(0) as u64);
1102    }
1103
1104    /// Ratio of summed GPU encode hardware time to observed GPU encode wall time.
1105    pub fn gpu_encode_effective_parallelism(&self) -> f64 {
1106        if self.gpu_encode.gpu_encode_wall_micros == 0 {
1107            0.0
1108        } else {
1109            self.gpu_encode.gpu_encode_hardware_micros as f64
1110                / self.gpu_encode.gpu_encode_wall_micros as f64
1111        }
1112    }
1113}
1114
1115fn increment_u64(value: &mut u64) {
1116    *value = value.saturating_add(1);
1117}
1118
1119fn add_duration_micros(value: &mut u128, duration: Duration) {
1120    *value = value.saturating_add(duration_as_reported_micros(duration));
1121}
1122
1123#[cfg(test)]
1124mod tests {
1125    use super::{
1126        ExportMetrics, GpuEncodeMetrics, JpegDirectHtj2kMetrics, JpegRetileRejectionReason,
1127        RouteCounters, WriteTimings,
1128    };
1129    use crate::tile::PixelProfile;
1130    use j2k_transcode::TranscodeTimingReport;
1131    use std::time::Duration;
1132
1133    #[test]
1134    fn dicom_export_metrics_serializes_stable_public_fields() {
1135        let metrics = ExportMetrics {
1136            routes: RouteCounters {
1137                total_frames: 10,
1138                cpu_fallback_frames: 4,
1139                ..RouteCounters::default()
1140            },
1141            jpeg_direct_htj2k: JpegDirectHtj2kMetrics {
1142                jpeg_direct_htj2k_rejected_frames: 3,
1143                ..JpegDirectHtj2kMetrics::default()
1144            },
1145            gpu_encode: GpuEncodeMetrics {
1146                gpu_encode_wall_micros: 5_000,
1147                gpu_encode_hardware_micros: 2_500,
1148                ..GpuEncodeMetrics::default()
1149            },
1150            ..ExportMetrics::default()
1151        };
1152
1153        let value = serde_json::to_value(metrics).expect("serialize metrics");
1154
1155        assert_eq!(value["total_frames"], 10);
1156        assert_eq!(value["jpeg_direct_htj2k_rejected_frames"], 3);
1157        assert_eq!(value["cpu_fallback_frames"], 4);
1158        assert_eq!(value["gpu_encode_wall_micros"], 5_000);
1159        assert_eq!(value["gpu_encode_hardware_micros"], 2_500);
1160        assert_eq!(value["gpu_encode_effective_parallelism"], 0.5);
1161        assert!(value.get("jpeg_retile_to_htj2k_53_frames").is_some());
1162        assert!(value.get("jpeg_retile_source_unsupported_frames").is_some());
1163        assert!(value.get("jpeg_retile_geometry_mismatch_frames").is_some());
1164        assert!(value
1165            .get("jpeg_retile_profile_unsupported_frames")
1166            .is_some());
1167        assert!(value.get("jpeg_retile_mcu_invalid_frames").is_some());
1168        assert!(value.get("writer_backpressure_micros").is_some());
1169        assert_eq!(
1170            value
1171                .as_object()
1172                .expect("metrics serialize as object")
1173                .len(),
1174            ExportMetrics::SERIALIZED_FIELD_COUNT
1175        );
1176    }
1177
1178    #[test]
1179    fn metrics_aggregation_saturates_counters_and_preserves_configuration_maxima() {
1180        let mut aggregate = ExportMetrics {
1181            routes: RouteCounters {
1182                total_frames: u64::MAX,
1183                jpeg_retile_frames: 2,
1184                ..RouteCounters::default()
1185            },
1186            jpeg_direct_htj2k: JpegDirectHtj2kMetrics {
1187                jpeg_direct_htj2k_53_frames: 3,
1188                ..JpegDirectHtj2kMetrics::default()
1189            },
1190            gpu_encode: GpuEncodeMetrics {
1191                gpu_encode_configured_inflight_tiles: 8,
1192                gpu_encode_wall_micros: 5,
1193                ..GpuEncodeMetrics::default()
1194            },
1195            ..ExportMetrics::default()
1196        };
1197        let mut next = ExportMetrics {
1198            routes: RouteCounters {
1199                total_frames: 1,
1200                jpeg_retile_frames: 7,
1201                ..RouteCounters::default()
1202            },
1203            jpeg_direct_htj2k: JpegDirectHtj2kMetrics {
1204                jpeg_direct_htj2k_53_frames: 11,
1205                ..JpegDirectHtj2kMetrics::default()
1206            },
1207            gpu_encode: GpuEncodeMetrics {
1208                gpu_encode_configured_inflight_tiles: 4,
1209                gpu_encode_effective_memory_mib: 32,
1210                gpu_encode_wall_micros: 13,
1211                ..GpuEncodeMetrics::default()
1212            },
1213            ..ExportMetrics::default()
1214        };
1215        next.timings.write_micros = 17;
1216
1217        aggregate.add_assign(next);
1218
1219        assert_eq!(aggregate.routes.total_frames, u64::MAX);
1220        assert_eq!(aggregate.routes.jpeg_retile_frames, 9);
1221        assert_eq!(aggregate.jpeg_direct_htj2k.jpeg_direct_htj2k_53_frames, 14);
1222        assert_eq!(aggregate.timings.write_micros, 17);
1223        assert_eq!(aggregate.gpu_encode.gpu_encode_wall_micros, 18);
1224        assert_eq!(aggregate.gpu_encode.gpu_encode_configured_inflight_tiles, 8);
1225        assert_eq!(aggregate.gpu_encode.gpu_encode_effective_memory_mib, 32);
1226    }
1227
1228    fn fully_populated_metrics() -> ExportMetrics {
1229        ExportMetrics {
1230            routes: RouteCounters {
1231                total_frames: 1,
1232                cpu_input_frames: 1,
1233                gpu_input_decode_frames: 1,
1234                gpu_encode_frames: 1,
1235                gpu_validation_frames: 1,
1236                gray_frames: 1,
1237                rgb_like_frames: 1,
1238                other_component_frames: 1,
1239                unknown_pixel_profile_frames: 1,
1240                bits8_frames: 1,
1241                bits16_frames: 1,
1242                other_bit_depth_frames: 1,
1243                gpu_transcode_frames: 1,
1244                resident_gpu_transcode_frames: 1,
1245                partial_gpu_transcode_frames: 1,
1246                gpu_input_decode_batches: 1,
1247                gpu_compose_batches: 1,
1248                gpu_encode_batches: 1,
1249                auto_route_probe_frames: 1,
1250                auto_route_probe_gpu_batches: 1,
1251                auto_route_probe_cpu_micros: 1,
1252                auto_route_probe_gpu_micros: 1,
1253                auto_route_probe_selected_gpu_input_frames: 1,
1254                cpu_fallback_frames: 1,
1255                jpeg_passthrough_frames: 1,
1256                j2k_passthrough_frames: 1,
1257                j2k_direct_htj2k_frames: 1,
1258                jpeg_retile_frames: 1,
1259                jpeg_retile_rejected_frames: 1,
1260                jpeg_retile_source_unsupported_frames: 1,
1261                jpeg_retile_geometry_mismatch_frames: 1,
1262                jpeg_retile_profile_unsupported_frames: 1,
1263                jpeg_retile_mcu_invalid_frames: 1,
1264                jpeg_retile_us: 1,
1265                jpeg_retile_to_htj2k_53_frames: 1,
1266                jpeg_cpu_encode_frames: 1,
1267                jpeg_metal_encode_frames: 1,
1268                jpeg_decode_fallback_frames: 1,
1269            },
1270            jpeg_direct_htj2k: JpegDirectHtj2kMetrics {
1271                jpeg_direct_htj2k_53_frames: 1,
1272                jpeg_direct_htj2k_97_frames: 1,
1273                jpeg_direct_htj2k_rejected_frames: 1,
1274                jpeg_direct_htj2k_extract_micros: 1,
1275                jpeg_direct_htj2k_repack_micros: 1,
1276                jpeg_direct_htj2k_transform_micros: 1,
1277                jpeg_direct_htj2k_accelerator_micros: 1,
1278                jpeg_direct_htj2k_cpu_fallback_micros: 1,
1279                jpeg_direct_htj2k_dwt_decompose_micros: 1,
1280                jpeg_direct_htj2k_dwt97_pack_upload_micros: 1,
1281                jpeg_direct_htj2k_dwt97_idct_row_lift_micros: 1,
1282                jpeg_direct_htj2k_dwt97_column_lift_micros: 1,
1283                jpeg_direct_htj2k_dwt97_quantize_codeblock_micros: 1,
1284                jpeg_direct_htj2k_dwt97_ht_encode_micros: 1,
1285                jpeg_direct_htj2k_dwt97_ht_kernel_micros: 1,
1286                jpeg_direct_htj2k_dwt97_ht_status_readback_micros: 1,
1287                jpeg_direct_htj2k_dwt97_ht_compact_micros: 1,
1288                jpeg_direct_htj2k_dwt97_ht_output_readback_micros: 1,
1289                jpeg_direct_htj2k_dwt97_ht_codeblock_dispatches: 1,
1290                jpeg_direct_htj2k_dwt97_readback_micros: 1,
1291                jpeg_direct_htj2k_htj2k_encode_micros: 1,
1292                jpeg_direct_htj2k_encode_accelerator_dispatches: 1,
1293                jpeg_direct_htj2k_encode_ht_code_block_dispatches: 1,
1294                jpeg_direct_htj2k_encode_packetization_dispatches: 1,
1295                jpeg_direct_htj2k_batch_count: 1,
1296                jpeg_direct_htj2k_batch_jobs: 1,
1297                jpeg_direct_htj2k_accelerator_attempts: 1,
1298                jpeg_direct_htj2k_accelerator_jobs: 1,
1299                jpeg_direct_htj2k_accelerator_dispatches: 1,
1300                jpeg_direct_htj2k_accelerator_dispatched_jobs: 1,
1301                jpeg_direct_htj2k_cpu_fallback_jobs: 1,
1302            },
1303            gpu_encode: GpuEncodeMetrics {
1304                gpu_encode_configured_inflight_tiles: 1,
1305                gpu_encode_effective_inflight_tiles: 1,
1306                gpu_encode_max_observed_inflight_tiles: 1,
1307                gpu_encode_configured_memory_mib: 1,
1308                gpu_encode_effective_memory_mib: 1,
1309                gpu_encode_wall_micros: 1,
1310                gpu_encode_hardware_micros: 1,
1311                gpu_encode_dispatch_overhead_micros: 1,
1312                gpu_encode_plan_micros: 1,
1313                gpu_encode_prepare_submit_micros: 1,
1314                gpu_encode_ht_table_build_micros: 1,
1315                gpu_encode_ht_buffer_allocation_micros: 1,
1316                gpu_encode_ht_command_encode_micros: 1,
1317                gpu_encode_codestream_wait_micros: 1,
1318                gpu_encode_chunk_count: 1,
1319                gpu_encode_tile_count: 1,
1320                gpu_encode_code_block_count: 1,
1321                gpu_pipeline_depth: 1,
1322                gpu_row_batch_rows_max: 1,
1323                gpu_row_batch_target_tiles: 1,
1324            },
1325            timings: WriteTimings {
1326                input_decode_micros: 1,
1327                compose_micros: 1,
1328                encode_micros: 1,
1329                validation_micros: 1,
1330                gpu_dispatch_micros: 1,
1331                streaming_write_micros: 1,
1332                pixel_data_patch_micros: 1,
1333                writer_backpressure_micros: 1,
1334                write_micros: 1,
1335            },
1336        }
1337    }
1338
1339    #[test]
1340    fn every_serialized_metric_field_has_aggregation_behavior() {
1341        let mut aggregate = ExportMetrics::default();
1342
1343        aggregate.add_assign(fully_populated_metrics());
1344
1345        let aggregated = serde_json::to_value(aggregate).expect("serialize aggregate metrics");
1346        for (field, value) in aggregated
1347            .as_object()
1348            .expect("metrics serialize as a flat object")
1349        {
1350            assert_eq!(
1351                value.as_f64(),
1352                Some(1.0),
1353                "serialized metric `{field}` was not aggregated"
1354            );
1355        }
1356    }
1357
1358    #[test]
1359    fn metrics_record_route_pixel_and_timing_counters() {
1360        let mut metrics = ExportMetrics::default();
1361
1362        metrics.record_cpu_input();
1363        metrics.record_gpu_input();
1364        metrics.record_passthrough_frame();
1365        metrics.record_j2k_passthrough_frame();
1366        metrics.record_j2k_direct_htj2k_frame(11);
1367        metrics.record_jpeg_direct_htj2k_53_frame(13);
1368        metrics.record_jpeg_direct_htj2k_97_frame(17);
1369        metrics.record_jpeg_direct_htj2k_rejected_frame();
1370        metrics.record_jpeg_retile_baseline_frame(Duration::from_micros(19));
1371        metrics.record_jpeg_retile_to_htj2k_53_frame(Duration::from_micros(23));
1372        metrics.record_jpeg_retile_rejected_frame(JpegRetileRejectionReason::SourceUnsupported);
1373        metrics.record_jpeg_retile_rejected_frame(JpegRetileRejectionReason::GeometryMismatch);
1374        metrics.record_jpeg_retile_rejected_frame(JpegRetileRejectionReason::ProfileUnsupported);
1375        metrics.record_jpeg_retile_rejected_frame(JpegRetileRejectionReason::McuInvalid);
1376        metrics.record_pixel_profile(PixelProfile {
1377            components: 1,
1378            bits_allocated: 8,
1379            photometric_interpretation: "MONOCHROME2",
1380        });
1381        metrics.record_pixel_profile(PixelProfile {
1382            components: 3,
1383            bits_allocated: 16,
1384            photometric_interpretation: "RGB",
1385        });
1386        metrics.record_pixel_profile(PixelProfile {
1387            components: 4,
1388            bits_allocated: 12,
1389            photometric_interpretation: "RGBA",
1390        });
1391        metrics.record_unknown_pixel_profile();
1392        metrics.record_transcode_route(true, true);
1393        metrics.record_transcode_route(true, false);
1394        metrics.record_transcode_route(false, false);
1395        metrics.record_gpu_batches(2, 3, 4);
1396        metrics.record_jpeg_decode_fallback();
1397        metrics.record_jpeg_cpu_fallback_route_classification();
1398        metrics.record_j2k_passthrough_only_fallback_classification();
1399        metrics.record_jpeg_cpu_encode(Duration::from_micros(29));
1400        metrics.record_jpeg_metal_batch_encode(5, Duration::from_micros(31));
1401        metrics.record_input_decode_duration(Duration::from_micros(37));
1402        metrics.record_gpu_input_decode_duration(Duration::from_micros(41));
1403        metrics.record_compose_duration(Duration::from_micros(43));
1404        metrics.record_encode_duration(Duration::from_micros(47));
1405        metrics.record_validation_duration(Duration::from_micros(53));
1406        metrics.record_gpu_dispatch_duration(Duration::from_micros(59));
1407        metrics.record_gpu_encode_hardware_duration(
1408            Some(Duration::from_micros(61)),
1409            Duration::from_micros(67),
1410        );
1411        metrics.record_gpu_encode_wall_duration(Duration::from_micros(69));
1412        metrics.record_write_duration(Duration::from_micros(71));
1413        metrics.record_streaming_write_duration(Duration::from_micros(73));
1414        metrics.record_pixel_data_patch_duration(Duration::from_micros(79));
1415
1416        metrics.record_jpeg_direct_htj2k_timings(TranscodeTimingReport {
1417            source_raw_probe_us: 0,
1418            read_region_decode_us: 0,
1419            compose_pad_us: 0,
1420            generated_jpeg_encode_us: 0,
1421            jpeg_dct_extract_us: 1,
1422            jpeg_dct_repack_us: 2,
1423            dct_to_wavelet_total_us: 3,
1424            dct_to_wavelet_accelerator_us: 4,
1425            dct_to_wavelet_cpu_fallback_us: 5,
1426            dwt_decompose_us: 6,
1427            dwt97_batch_pack_upload_us: 7,
1428            dwt97_batch_pack_upload_transfers: 0,
1429            dwt97_batch_pack_upload_bytes: 0,
1430            dwt97_batch_resident_dct_handoff_count: 0,
1431            dwt97_batch_idct_row_lift_us: 8,
1432            dwt97_batch_column_lift_us: 9,
1433            dwt97_batch_resident_dwt_handoff_count: 0,
1434            dwt97_batch_quantize_codeblock_us: 10,
1435            dwt97_batch_ht_encode_us: 11,
1436            dwt97_batch_ht_kernel_us: 12,
1437            dwt97_batch_ht_status_readback_us: 13,
1438            dwt97_batch_ht_status_readback_transfers: 0,
1439            dwt97_batch_ht_status_readback_bytes: 0,
1440            dwt97_batch_ht_compact_us: 14,
1441            dwt97_batch_ht_output_readback_us: 15,
1442            dwt97_batch_ht_output_readback_transfers: 0,
1443            dwt97_batch_ht_output_readback_bytes: 0,
1444            dwt97_batch_ht_codeblock_dispatches: 16,
1445            dwt97_batch_readback_us: 17,
1446            dwt97_batch_readback_transfers: 0,
1447            dwt97_batch_readback_bytes: 0,
1448            htj2k_encode_us: 18,
1449            htj2k_encode_accelerator_dispatches: 19,
1450            htj2k_encode_ht_code_block_dispatches: 20,
1451            htj2k_encode_packetization_dispatches: 21,
1452            dicom_spool_write_us: 0,
1453            dicom_final_write_us: 0,
1454            tile_count: 0,
1455            component_count: 0,
1456            batch_count: 22,
1457            batch_jobs: 23,
1458            accelerator_attempts: 24,
1459            accelerator_jobs: 25,
1460            accelerator_dispatches: 26,
1461            accelerator_dispatched_jobs: 27,
1462            cpu_fallback_jobs: 28,
1463        });
1464
1465        assert_eq!(metrics.routes.total_frames, 10);
1466        assert_eq!(metrics.route_passthrough_frames(), 2);
1467        assert_eq!(metrics.routes.j2k_direct_htj2k_frames, 1);
1468        assert_eq!(metrics.jpeg_direct_htj2k.jpeg_direct_htj2k_53_frames, 1);
1469        assert_eq!(metrics.jpeg_direct_htj2k.jpeg_direct_htj2k_97_frames, 1);
1470        assert_eq!(
1471            metrics.jpeg_direct_htj2k.jpeg_direct_htj2k_rejected_frames,
1472            1
1473        );
1474        assert_eq!(metrics.jpeg_retile_baseline_frames(), 1);
1475        assert_eq!(metrics.routes.jpeg_retile_to_htj2k_53_frames, 1);
1476        assert_eq!(metrics.routes.jpeg_retile_rejected_frames, 4);
1477        assert_eq!(metrics.routes.jpeg_retile_source_unsupported_frames, 1);
1478        assert_eq!(metrics.routes.jpeg_retile_geometry_mismatch_frames, 1);
1479        assert_eq!(metrics.routes.jpeg_retile_profile_unsupported_frames, 1);
1480        assert_eq!(metrics.routes.jpeg_retile_mcu_invalid_frames, 1);
1481        assert_eq!(metrics.routes.gray_frames, 1);
1482        assert_eq!(metrics.routes.rgb_like_frames, 1);
1483        assert_eq!(metrics.routes.other_component_frames, 1);
1484        assert_eq!(metrics.routes.bits8_frames, 1);
1485        assert_eq!(metrics.routes.bits16_frames, 1);
1486        assert_eq!(metrics.routes.other_bit_depth_frames, 1);
1487        assert_eq!(metrics.routes.unknown_pixel_profile_frames, 3);
1488        assert_eq!(metrics.routes.gpu_transcode_frames, 2);
1489        assert_eq!(metrics.routes.resident_gpu_transcode_frames, 1);
1490        assert_eq!(metrics.routes.partial_gpu_transcode_frames, 1);
1491        assert_eq!(metrics.routes.cpu_fallback_frames, 3);
1492        assert_eq!(metrics.routes.gpu_input_decode_batches, 2);
1493        assert_eq!(metrics.routes.gpu_compose_batches, 3);
1494        assert_eq!(metrics.routes.gpu_encode_batches, 4);
1495        assert_eq!(metrics.routes.jpeg_decode_fallback_frames, 2);
1496        assert_eq!(metrics.routes.jpeg_cpu_encode_frames, 1);
1497        assert_eq!(metrics.routes.jpeg_metal_encode_frames, 5);
1498        assert_eq!(
1499            metrics.jpeg_direct_htj2k.jpeg_direct_htj2k_extract_micros,
1500            1
1501        );
1502        assert_eq!(
1503            metrics
1504                .jpeg_direct_htj2k
1505                .jpeg_direct_htj2k_encode_packetization_dispatches,
1506            21
1507        );
1508        assert_eq!(
1509            metrics
1510                .jpeg_direct_htj2k
1511                .jpeg_direct_htj2k_dwt97_ht_encode_micros,
1512            11
1513        );
1514        assert_eq!(
1515            metrics
1516                .jpeg_direct_htj2k
1517                .jpeg_direct_htj2k_dwt97_ht_kernel_micros,
1518            12
1519        );
1520        assert_eq!(
1521            metrics
1522                .jpeg_direct_htj2k
1523                .jpeg_direct_htj2k_dwt97_ht_status_readback_micros,
1524            13
1525        );
1526        assert_eq!(
1527            metrics
1528                .jpeg_direct_htj2k
1529                .jpeg_direct_htj2k_dwt97_ht_compact_micros,
1530            14
1531        );
1532        assert_eq!(
1533            metrics
1534                .jpeg_direct_htj2k
1535                .jpeg_direct_htj2k_dwt97_ht_output_readback_micros,
1536            15
1537        );
1538        assert_eq!(
1539            metrics
1540                .jpeg_direct_htj2k
1541                .jpeg_direct_htj2k_dwt97_ht_codeblock_dispatches,
1542            16
1543        );
1544        assert_eq!(
1545            metrics
1546                .jpeg_direct_htj2k
1547                .jpeg_direct_htj2k_cpu_fallback_jobs,
1548            28
1549        );
1550        assert_eq!(metrics.gpu_encode.gpu_encode_wall_micros, 69);
1551        assert_eq!(metrics.gpu_encode.gpu_encode_hardware_micros, 61);
1552        assert_eq!(metrics.gpu_encode.gpu_encode_dispatch_overhead_micros, 6);
1553        assert_eq!(metrics.timings.streaming_write_micros, 73);
1554        assert_eq!(metrics.timings.pixel_data_patch_micros, 79);
1555        assert_eq!(metrics.route_unclassified_frames(), 0);
1556    }
1557}