use super::{ErrorMetrics, JpegToHtj2kCoefficientPath};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BatchTranscodeReport {
pub tile_count: usize,
pub successful_tiles: usize,
pub failed_tiles: usize,
pub transformed_components: usize,
pub reversible_dwt53_batches: usize,
pub reversible_dwt53_batch_jobs: usize,
pub extract_us: u128,
pub transform_us: u128,
pub encode_us: u128,
pub timings: TranscodeTimingReport,
pub coefficient_path: JpegToHtj2kCoefficientPath,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TranscodeBatchProfileRequest {
Cpu,
MetalAuto,
MetalExplicit,
}
impl TranscodeBatchProfileRequest {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Cpu => "cpu",
Self::MetalAuto => "metal_auto",
Self::MetalExplicit => "metal_explicit",
}
}
#[must_use]
pub fn transform_processor(self, timings: &TranscodeTimingReport) -> &'static str {
if matches!(self, Self::MetalAuto | Self::MetalExplicit)
&& timings.accelerator_work_observed()
{
"metal"
} else {
"cpu"
}
}
#[must_use]
pub fn profile_path(self, timings: &TranscodeTimingReport) -> &'static str {
if self.transform_processor(timings) != "metal" {
return "cpu";
}
match self {
Self::Cpu => "cpu",
Self::MetalAuto => "auto",
Self::MetalExplicit => "metal",
}
}
}
const TRANSCODE_PROFILE_FIELD_COUNT: usize = 65;
macro_rules! push_labels {
($builder:ident; $($key:literal => $value:expr),+ $(,)?) => {
$($builder.label($key, $value)?;)+
};
}
macro_rules! push_metrics {
($builder:ident; $($key:literal => $value:expr),+ $(,)?) => {
$($builder.metric($key, $value)?;)+
};
}
#[derive(Debug, PartialEq, Eq)]
pub struct TranscodeBatchProfileRow {
fields: Vec<(&'static str, String)>,
path: &'static str,
}
pub(super) type TranscodeBatchProfileFields = Vec<(&'static str, String)>;
impl TranscodeBatchProfileRow {
pub fn new(
report: &BatchTranscodeReport,
context: impl AsRef<str>,
request: TranscodeBatchProfileRequest,
) -> j2k_profile::ProfileResult<Self> {
let timings = report.timings;
let total_us = report
.extract_us
.checked_add(report.transform_us)
.and_then(|value| value.checked_add(report.encode_us))
.ok_or(j2k_profile::ProfileError::SizeOverflow {
what: "transcode batch total profile time",
})?;
let transform_processor = request.transform_processor(&timings);
let path = request.profile_path(&timings);
let mut builder = TranscodeProfileRowBuilder::new()?;
push_labels!(builder;
"codec" => "transcode",
"op" => "transcode_batch",
"request" => request.as_str(),
"path" => path,
"pipeline" => "jpeg_to_htj2k",
"context" => SanitizedContext(context.as_ref()),
"coefficient_path" => coefficient_path_label(report.coefficient_path),
"extract_processor" => "cpu",
"transform_processor" => transform_processor,
"encode_processor" => "cpu",
);
Self::push_batch_fields(&mut builder, report, total_us)?;
Self::push_input_timing_fields(&mut builder, &timings)?;
Self::push_dwt97_timing_fields(&mut builder, &timings)?;
Self::push_transfer_fields(&mut builder, &timings)?;
Self::push_encode_timing_fields(&mut builder, &timings)?;
Self::push_accelerator_fields(&mut builder, &timings)?;
Ok(Self {
fields: builder.finish()?,
path,
})
}
fn push_batch_fields(
builder: &mut TranscodeProfileRowBuilder,
report: &BatchTranscodeReport,
total_us: u128,
) -> j2k_profile::ProfileResult<()> {
push_metrics!(builder;
"tile_count" => report.tile_count,
"successful_tiles" => report.successful_tiles,
"failed_tiles" => report.failed_tiles,
"transformed_components" => report.transformed_components,
"reversible_dwt53_batches" => report.reversible_dwt53_batches,
"reversible_dwt53_batch_jobs" => report.reversible_dwt53_batch_jobs,
"extract_us" => report.extract_us,
"transform_us" => report.transform_us,
"encode_us" => report.encode_us,
"total_us" => total_us,
);
Ok(())
}
fn push_input_timing_fields(
builder: &mut TranscodeProfileRowBuilder,
timings: &TranscodeTimingReport,
) -> j2k_profile::ProfileResult<()> {
push_metrics!(builder;
"source_raw_probe_us" => timings.source_raw_probe_us,
"read_region_decode_us" => timings.read_region_decode_us,
"compose_pad_us" => timings.compose_pad_us,
"generated_jpeg_encode_us" => timings.generated_jpeg_encode_us,
"jpeg_dct_extract_us" => timings.jpeg_dct_extract_us,
"jpeg_dct_repack_us" => timings.jpeg_dct_repack_us,
"dct_to_wavelet_total_us" => timings.dct_to_wavelet_total_us,
"dct_to_wavelet_accelerator_us" => timings.dct_to_wavelet_accelerator_us,
"dct_to_wavelet_cpu_fallback_us" => timings.dct_to_wavelet_cpu_fallback_us,
"dwt_decompose_us" => timings.dwt_decompose_us,
);
Ok(())
}
fn push_dwt97_timing_fields(
builder: &mut TranscodeProfileRowBuilder,
timings: &TranscodeTimingReport,
) -> j2k_profile::ProfileResult<()> {
push_metrics!(builder;
"dwt97_batch_pack_upload_us" => timings.dwt97_batch_pack_upload_us,
"dwt97_batch_pack_upload_transfers" => timings.dwt97_batch_pack_upload_transfers,
"dwt97_batch_pack_upload_bytes" => timings.dwt97_batch_pack_upload_bytes,
"dwt97_batch_resident_dct_handoff_count" => timings.dwt97_batch_resident_dct_handoff_count,
"dwt97_batch_idct_row_lift_us" => timings.dwt97_batch_idct_row_lift_us,
"dwt97_batch_column_lift_us" => timings.dwt97_batch_column_lift_us,
"dwt97_batch_resident_dwt_handoff_count" => timings.dwt97_batch_resident_dwt_handoff_count,
"dwt97_batch_quantize_codeblock_us" => timings.dwt97_batch_quantize_codeblock_us,
"dwt97_batch_ht_encode_us" => timings.dwt97_batch_ht_encode_us,
"dwt97_batch_ht_codeblock_dispatches" => timings.dwt97_batch_ht_codeblock_dispatches,
);
Ok(())
}
fn push_transfer_fields(
builder: &mut TranscodeProfileRowBuilder,
timings: &TranscodeTimingReport,
) -> j2k_profile::ProfileResult<()> {
let device_to_host_transfer_count = timings
.dwt97_batch_readback_transfers
.checked_add(timings.dwt97_batch_ht_status_readback_transfers)
.and_then(|value| value.checked_add(timings.dwt97_batch_ht_output_readback_transfers))
.ok_or(j2k_profile::ProfileError::SizeOverflow {
what: "device-to-host profile transfer count",
})?;
let device_to_host_transfer_bytes = timings
.dwt97_batch_readback_bytes
.checked_add(timings.dwt97_batch_ht_status_readback_bytes)
.and_then(|value| value.checked_add(timings.dwt97_batch_ht_output_readback_bytes))
.ok_or(j2k_profile::ProfileError::SizeOverflow {
what: "device-to-host profile transfer bytes",
})?;
push_metrics!(builder;
"dwt97_batch_ht_status_readback_us" => timings.dwt97_batch_ht_status_readback_us,
"dwt97_batch_ht_status_readback_transfers" => timings.dwt97_batch_ht_status_readback_transfers,
"dwt97_batch_ht_status_readback_bytes" => timings.dwt97_batch_ht_status_readback_bytes,
"dwt97_batch_ht_output_readback_us" => timings.dwt97_batch_ht_output_readback_us,
"dwt97_batch_ht_output_readback_transfers" => timings.dwt97_batch_ht_output_readback_transfers,
"dwt97_batch_ht_output_readback_bytes" => timings.dwt97_batch_ht_output_readback_bytes,
"dwt97_batch_readback_us" => timings.dwt97_batch_readback_us,
"dwt97_batch_readback_transfers" => timings.dwt97_batch_readback_transfers,
"dwt97_batch_readback_bytes" => timings.dwt97_batch_readback_bytes,
"host_to_device_transfer_count" => timings.dwt97_batch_pack_upload_transfers,
"host_to_device_transfer_bytes" => timings.dwt97_batch_pack_upload_bytes,
"device_to_host_transfer_count" => device_to_host_transfer_count,
"device_to_host_transfer_bytes" => device_to_host_transfer_bytes,
);
Ok(())
}
fn push_encode_timing_fields(
builder: &mut TranscodeProfileRowBuilder,
timings: &TranscodeTimingReport,
) -> j2k_profile::ProfileResult<()> {
push_metrics!(builder;
"htj2k_encode_us" => timings.htj2k_encode_us,
"htj2k_encode_accelerator_dispatches" => timings.htj2k_encode_accelerator_dispatches,
"htj2k_encode_ht_code_block_dispatches" => timings.htj2k_encode_ht_code_block_dispatches,
"htj2k_encode_packetization_dispatches" => timings.htj2k_encode_packetization_dispatches,
"component_count" => timings.component_count,
"batch_count" => timings.batch_count,
"batch_jobs" => timings.batch_jobs,
);
Ok(())
}
fn push_accelerator_fields(
builder: &mut TranscodeProfileRowBuilder,
timings: &TranscodeTimingReport,
) -> j2k_profile::ProfileResult<()> {
push_metrics!(builder;
"accelerator_attempts" => timings.accelerator_attempts,
"accelerator_jobs" => timings.accelerator_jobs,
"accelerator_dispatches" => timings.accelerator_dispatches,
"accelerator_dispatched_jobs" => timings.accelerator_dispatched_jobs,
"cpu_fallback_jobs" => timings.cpu_fallback_jobs,
);
Ok(())
}
#[must_use]
pub fn fields(&self) -> &[(&'static str, String)] {
&self.fields
}
#[must_use]
pub const fn codec(&self) -> &'static str {
"transcode"
}
#[must_use]
pub const fn op(&self) -> &'static str {
"transcode_batch"
}
#[must_use]
pub const fn path(&self) -> &'static str {
self.path
}
}
struct TranscodeProfileRowBuilder {
fields: TranscodeBatchProfileFields,
limits: j2k_profile::ProfileLimits,
input_bytes: usize,
retained_bytes: usize,
}
impl TranscodeProfileRowBuilder {
fn new() -> j2k_profile::ProfileResult<Self> {
let limits = j2k_profile::ProfileLimits::default();
if TRANSCODE_PROFILE_FIELD_COUNT > limits.max_fields() {
return Err(j2k_profile::ProfileError::LimitExceeded {
what: "transcode profile field count",
requested: TRANSCODE_PROFILE_FIELD_COUNT,
limit: limits.max_fields(),
});
}
let mut fields = Vec::new();
fields
.try_reserve_exact(TRANSCODE_PROFILE_FIELD_COUNT)
.map_err(|_| j2k_profile::ProfileError::AllocationFailed {
what: "transcode profile field storage",
requested: TRANSCODE_PROFILE_FIELD_COUNT,
})?;
let retained_bytes = fields
.capacity()
.checked_mul(core::mem::size_of::<(&'static str, String)>())
.ok_or(j2k_profile::ProfileError::SizeOverflow {
what: "transcode profile field storage",
})?;
if retained_bytes > limits.max_retained_bytes() {
return Err(j2k_profile::ProfileError::LimitExceeded {
what: "transcode profile retained bytes",
requested: retained_bytes,
limit: limits.max_retained_bytes(),
});
}
Ok(Self {
fields,
limits,
input_bytes: 0,
retained_bytes,
})
}
fn label(
&mut self,
key: &'static str,
value: impl core::fmt::Display,
) -> j2k_profile::ProfileResult<()> {
let field = j2k_profile::ProfileField::label_with_limits(key, value, self.limits)?;
self.push(key, field.into_value())
}
fn metric(
&mut self,
key: &'static str,
value: impl core::fmt::Display,
) -> j2k_profile::ProfileResult<()> {
let field = j2k_profile::ProfileField::metric_with_limits(key, value, self.limits)?;
self.push(key, field.into_value())
}
fn push(&mut self, key: &'static str, value: String) -> j2k_profile::ProfileResult<()> {
let field_count =
self.fields
.len()
.checked_add(1)
.ok_or(j2k_profile::ProfileError::SizeOverflow {
what: "transcode profile field count",
})?;
if field_count > self.limits.max_fields() {
return Err(j2k_profile::ProfileError::LimitExceeded {
what: "transcode profile field count",
requested: field_count,
limit: self.limits.max_fields(),
});
}
let input_bytes = self
.input_bytes
.checked_add(key.len())
.and_then(|bytes| bytes.checked_add(value.len()))
.ok_or(j2k_profile::ProfileError::SizeOverflow {
what: "transcode profile input bytes",
})?;
if input_bytes > self.limits.max_input_bytes() {
return Err(j2k_profile::ProfileError::LimitExceeded {
what: "transcode profile input bytes",
requested: input_bytes,
limit: self.limits.max_input_bytes(),
});
}
let retained_bytes = self.retained_bytes.checked_add(value.capacity()).ok_or(
j2k_profile::ProfileError::SizeOverflow {
what: "transcode profile retained bytes",
},
)?;
if retained_bytes > self.limits.max_retained_bytes() {
return Err(j2k_profile::ProfileError::LimitExceeded {
what: "transcode profile retained bytes",
requested: retained_bytes,
limit: self.limits.max_retained_bytes(),
});
}
if self.fields.len() == self.fields.capacity() {
return Err(j2k_profile::ProfileError::InvalidInput {
what: "transcode profile field reservation exhausted",
});
}
self.fields.push((key, value));
self.input_bytes = input_bytes;
self.retained_bytes = retained_bytes;
Ok(())
}
fn finish(self) -> j2k_profile::ProfileResult<TranscodeBatchProfileFields> {
if self.fields.len() != TRANSCODE_PROFILE_FIELD_COUNT {
return Err(j2k_profile::ProfileError::InvalidInput {
what: "transcode profile field schema is incomplete",
});
}
Ok(self.fields)
}
}
struct SanitizedContext<'a>(&'a str);
impl core::fmt::Display for SanitizedContext<'_> {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let mut parts = self.0.split(' ');
if let Some(first) = parts.next() {
formatter.write_str(first)?;
}
for part in parts {
formatter.write_str("_")?;
formatter.write_str(part)?;
}
Ok(())
}
}
const fn coefficient_path_label(path: JpegToHtj2kCoefficientPath) -> &'static str {
match path {
JpegToHtj2kCoefficientPath::IntegerDirect53 => "IntegerDirect53",
JpegToHtj2kCoefficientPath::FloatDirectLinear53 => "FloatDirectLinear53",
JpegToHtj2kCoefficientPath::FloatDirectLinear97 => "FloatDirectLinear97",
}
}
impl BatchTranscodeReport {
pub fn profile_row(
&self,
context: impl AsRef<str>,
request: TranscodeBatchProfileRequest,
) -> j2k_profile::ProfileResult<TranscodeBatchProfileRow> {
TranscodeBatchProfileRow::new(self, context, request)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct TranscodeTimingReport {
pub source_raw_probe_us: u128,
pub read_region_decode_us: u128,
pub compose_pad_us: u128,
pub generated_jpeg_encode_us: u128,
pub jpeg_dct_extract_us: u128,
pub jpeg_dct_repack_us: u128,
pub dct_to_wavelet_total_us: u128,
pub dct_to_wavelet_accelerator_us: u128,
pub dct_to_wavelet_cpu_fallback_us: u128,
pub dwt_decompose_us: u128,
pub dwt97_batch_pack_upload_us: u128,
pub dwt97_batch_pack_upload_transfers: usize,
pub dwt97_batch_pack_upload_bytes: u64,
pub dwt97_batch_resident_dct_handoff_count: usize,
pub dwt97_batch_idct_row_lift_us: u128,
pub dwt97_batch_column_lift_us: u128,
pub dwt97_batch_resident_dwt_handoff_count: usize,
pub dwt97_batch_quantize_codeblock_us: u128,
pub dwt97_batch_ht_encode_us: u128,
pub dwt97_batch_ht_kernel_us: u128,
pub dwt97_batch_ht_status_readback_us: u128,
pub dwt97_batch_ht_status_readback_transfers: usize,
pub dwt97_batch_ht_status_readback_bytes: u64,
pub dwt97_batch_ht_compact_us: u128,
pub dwt97_batch_ht_output_readback_us: u128,
pub dwt97_batch_ht_output_readback_transfers: usize,
pub dwt97_batch_ht_output_readback_bytes: u64,
pub dwt97_batch_ht_codeblock_dispatches: usize,
pub dwt97_batch_readback_us: u128,
pub dwt97_batch_readback_transfers: usize,
pub dwt97_batch_readback_bytes: u64,
pub htj2k_encode_us: u128,
pub htj2k_encode_accelerator_dispatches: usize,
pub htj2k_encode_ht_code_block_dispatches: usize,
pub htj2k_encode_packetization_dispatches: usize,
pub dicom_spool_write_us: u128,
pub dicom_final_write_us: u128,
pub tile_count: usize,
pub component_count: usize,
pub batch_count: usize,
pub batch_jobs: usize,
pub accelerator_attempts: usize,
pub accelerator_jobs: usize,
pub accelerator_dispatches: usize,
pub accelerator_dispatched_jobs: usize,
pub cpu_fallback_jobs: usize,
}
impl TranscodeTimingReport {
pub fn accelerator_work_observed(&self) -> bool {
self.accelerator_dispatches > 0
|| self.dwt97_batch_pack_upload_transfers > 0
|| self.dwt97_batch_pack_upload_bytes > 0
|| self.dwt97_batch_resident_dct_handoff_count > 0
|| self.dwt97_batch_idct_row_lift_us > 0
|| self.dwt97_batch_column_lift_us > 0
|| self.dwt97_batch_resident_dwt_handoff_count > 0
|| self.dwt97_batch_quantize_codeblock_us > 0
|| self.dwt97_batch_ht_encode_us > 0
|| self.dwt97_batch_ht_kernel_us > 0
|| self.dwt97_batch_ht_compact_us > 0
|| self.dwt97_batch_ht_codeblock_dispatches > 0
|| self.dwt97_batch_readback_transfers > 0
|| self.dwt97_batch_readback_bytes > 0
|| self.dwt97_batch_ht_status_readback_transfers > 0
|| self.dwt97_batch_ht_status_readback_bytes > 0
|| self.dwt97_batch_ht_output_readback_transfers > 0
|| self.dwt97_batch_ht_output_readback_bytes > 0
}
pub(super) fn add_assign(&mut self, other: Self) {
macro_rules! saturating_add_fields {
($($field:ident),+ $(,)?) => {
$(
self.$field = self.$field.saturating_add(other.$field);
)+
};
}
saturating_add_fields!(
source_raw_probe_us,
read_region_decode_us,
compose_pad_us,
generated_jpeg_encode_us,
jpeg_dct_extract_us,
jpeg_dct_repack_us,
dct_to_wavelet_total_us,
dct_to_wavelet_accelerator_us,
dct_to_wavelet_cpu_fallback_us,
dwt_decompose_us,
dwt97_batch_pack_upload_us,
dwt97_batch_pack_upload_transfers,
dwt97_batch_pack_upload_bytes,
dwt97_batch_resident_dct_handoff_count,
dwt97_batch_idct_row_lift_us,
dwt97_batch_column_lift_us,
dwt97_batch_resident_dwt_handoff_count,
dwt97_batch_quantize_codeblock_us,
dwt97_batch_ht_encode_us,
dwt97_batch_ht_kernel_us,
dwt97_batch_ht_status_readback_us,
dwt97_batch_ht_status_readback_transfers,
dwt97_batch_ht_status_readback_bytes,
dwt97_batch_ht_compact_us,
dwt97_batch_ht_output_readback_us,
dwt97_batch_ht_output_readback_transfers,
dwt97_batch_ht_output_readback_bytes,
dwt97_batch_ht_codeblock_dispatches,
dwt97_batch_readback_us,
dwt97_batch_readback_transfers,
dwt97_batch_readback_bytes,
htj2k_encode_us,
htj2k_encode_accelerator_dispatches,
htj2k_encode_ht_code_block_dispatches,
htj2k_encode_packetization_dispatches,
dicom_spool_write_us,
dicom_final_write_us,
tile_count,
component_count,
batch_count,
batch_jobs,
accelerator_attempts,
accelerator_jobs,
accelerator_dispatches,
accelerator_dispatched_jobs,
cpu_fallback_jobs,
);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TranscodeComponentReport {
pub component_index: usize,
pub width: u32,
pub height: u32,
pub block_cols: u32,
pub block_rows: u32,
pub x_rsiz: u8,
pub y_rsiz: u8,
}
pub type TranscodeValidationMetrics = ErrorMetrics;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TranscodeValidationClassification {
Exact,
OneLsbBounded,
OutsideThreshold,
}
impl TranscodeValidationClassification {
#[must_use]
pub fn classify_metrics(metrics: &TranscodeValidationMetrics) -> Self {
if metrics.exact_matches == metrics.total && metrics.max_abs_error == 0 {
Self::Exact
} else if metrics.is_one_lsb_bounded(0.999) {
Self::OneLsbBounded
} else {
Self::OutsideThreshold
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct TranscodeReport {
pub width: u32,
pub height: u32,
pub component_count: usize,
pub components: Vec<TranscodeComponentReport>,
pub float_reference_metrics: Option<TranscodeValidationMetrics>,
pub float_reference_classification: Option<TranscodeValidationClassification>,
pub integer_reference_metrics: Option<TranscodeValidationMetrics>,
pub integer_reference_classification: Option<TranscodeValidationClassification>,
pub decomposition_levels: u8,
pub coefficient_path: JpegToHtj2kCoefficientPath,
pub path: &'static str,
pub extract_us: u128,
pub transform_us: u128,
pub encode_us: u128,
pub timings: TranscodeTimingReport,
}