Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve memory usage of evaluator #688

Merged
merged 2 commits into from
Mar 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 15 additions & 14 deletions src/evaluate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,18 @@ use crate::{atomicmin::AtomicMin, deflate, filters::RowFilter, png::PngImage, De

pub(crate) struct Candidate {
pub image: Arc<PngImage>,
pub idat_data: Vec<u8>,
pub filtered: Vec<u8>,
pub data: Vec<u8>,
pub data_is_compressed: bool,
pub estimated_output_size: usize,
pub filter: RowFilter,
// For determining tie-breaker
nth: usize,
}

impl Candidate {
/// Return an estimate of the output size which can help with evaluation of very small data
#[must_use]
pub fn estimated_output_size(&self) -> usize {
self.idat_data.len() + self.image.key_chunks_size()
}

fn cmp_key(&self) -> impl Ord {
(
self.estimated_output_size(),
self.estimated_output_size,
self.image.data.len(),
self.filter,
// Prefer the later image added (e.g. baseline, which is always added last)
Expand All @@ -52,6 +47,7 @@ pub(crate) struct Evaluator {
filters: IndexSet<RowFilter>,
deflater: Deflaters,
optimize_alpha: bool,
final_round: bool,
nth: AtomicUsize,
executed: Arc<AtomicUsize>,
best_candidate_size: Arc<AtomicMin>,
Expand All @@ -69,6 +65,7 @@ impl Evaluator {
filters: IndexSet<RowFilter>,
deflater: Deflaters,
optimize_alpha: bool,
final_round: bool,
) -> Self {
#[cfg(feature = "parallel")]
let eval_channel = unbounded();
Expand All @@ -77,6 +74,7 @@ impl Evaluator {
filters,
deflater,
optimize_alpha,
final_round,
nth: AtomicUsize::new(0),
executed: Arc::new(AtomicUsize::new(0)),
best_candidate_size: Arc::new(AtomicMin::new(None)),
Expand Down Expand Up @@ -127,6 +125,7 @@ impl Evaluator {
let filters = self.filters.clone();
let deflater = self.deflater;
let optimize_alpha = self.optimize_alpha;
let final_round = self.final_round;
let executed = self.executed.clone();
let best_candidate_size = self.best_candidate_size.clone();
let description = description.to_string();
Expand All @@ -149,21 +148,23 @@ impl Evaluator {
let filtered = image.filter_image(filter, optimize_alpha);
let idat_data = deflater.deflate(&filtered, best_candidate_size.get());
if let Ok(idat_data) = idat_data {
let estimated_output_size = image.estimated_output_size(&idat_data);
// For the final round we need the IDAT data, otherwise the filtered data
let new = Candidate {
image: image.clone(),
idat_data,
filtered,
data: if final_round { idat_data } else { filtered },
data_is_compressed: final_round,
estimated_output_size,
filter,
nth,
};
let size = new.estimated_output_size();
best_candidate_size.set_min(size);
best_candidate_size.set_min(estimated_output_size);
trace!(
"Eval: {}-bit {:23} {:8} {} bytes",
image.ihdr.bit_depth,
description,
filter,
size
estimated_output_size
);

#[cfg(feature = "parallel")]
Expand Down
55 changes: 32 additions & 23 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ impl RawImage {

let mut png = PngData {
raw: result.image,
idat_data: result.idat_data,
idat_data: result.data,
aux_chunks,
frames: Vec::new(),
};
Expand Down Expand Up @@ -355,11 +355,11 @@ fn optimize_png(
let max_size = if opts.force {
None
} else {
Some(png.estimated_output_size())
Some(png.raw.estimated_output_size(&png.idat_data))
};
if let Some(result) = optimize_raw(raw.clone(), &opts, deadline.clone(), max_size) {
png.raw = result.image;
png.idat_data = result.idat_data;
png.idat_data = result.data;
recompress_frames(png, &opts, deadline, result.filter)?;
postprocess_chunks(&mut png.aux_chunks, &png.raw.ihdr, &raw.ihdr);
}
Expand Down Expand Up @@ -433,7 +433,13 @@ fn optimize_raw(
indexset! {RowFilter::None, RowFilter::Bigrams}
};
// This will collect all versions of images and pick one that compresses best
let eval = Evaluator::new(deadline.clone(), eval_filters.clone(), eval_deflater, false);
let eval = Evaluator::new(
deadline.clone(),
eval_filters.clone(),
eval_deflater,
false,
opts.deflate == eval_deflater,
);
let mut new_image = perform_reductions(image.clone(), opts, &deadline, &eval);
let eval_result = eval.get_best_candidate();
if let Some(ref result) = eval_result {
Expand Down Expand Up @@ -464,7 +470,9 @@ fn optimize_raw(
(eval_result?, eval_deflater)
};

if max_size.map_or(true, |max_size| result.estimated_output_size() < max_size) {
if result.data_is_compressed
&& max_size.map_or(true, |max_size| result.estimated_output_size < max_size)
{
debug!("Found better result:");
debug!(" {}, f = {}", deflater, result.filter);
return Some(result);
Expand Down Expand Up @@ -499,35 +507,36 @@ fn perform_trials(
filters,
eval_deflater,
opts.optimize_alpha,
opts.deflate == eval_deflater,
);
if let Some(result) = &eval_result {
eval.set_best_size(result.estimated_output_size());
eval.set_best_size(result.estimated_output_size);
}
eval.try_image(image.clone());
if let Some(result) = eval.get_best_candidate() {
eval_result = Some(result);
}
}
if opts.deflate == eval_deflater {
// No further compression required
return eval_result;
}

// We should have a result here - fail if not (e.g. deadline passed)
let mut result = eval_result?;

// Recompress with the main deflater
debug!("Trying filter {} with {}", result.filter, opts.deflate);
match opts.deflate.deflate(&result.filtered, max_size) {
Ok(idat_data) => {
result.idat_data = idat_data;
trace!("{} bytes", result.estimated_output_size());
}
Err(PngError::DeflatedDataTooLong(bytes)) => {
trace!(">{bytes} bytes");
}
Err(_) => (),
};
if !result.data_is_compressed {
// Compress with the main deflater
debug!("Trying filter {} with {}", result.filter, opts.deflate);
match opts.deflate.deflate(&result.data, max_size) {
Ok(idat_data) => {
result.estimated_output_size = result.image.estimated_output_size(&idat_data);
result.data = idat_data;
result.data_is_compressed = true;
trace!("{} bytes", result.estimated_output_size);
}
Err(PngError::DeflatedDataTooLong(bytes)) => {
trace!(">{bytes} bytes");
}
Err(_) => (),
};
}
return Some(result);
}

Expand All @@ -545,7 +554,7 @@ fn perform_trials(
}

debug!("Trying {} filters with {}", filters.len(), opts.deflate);
let eval = Evaluator::new(deadline, filters, opts.deflate, opts.optimize_alpha);
let eval = Evaluator::new(deadline, filters, opts.deflate, opts.optimize_alpha, true);
if let Some(max_size) = max_size {
eval.set_best_size(max_size);
}
Expand Down
12 changes: 6 additions & 6 deletions src/png/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,12 +185,6 @@ impl PngData {
})
}

/// Return an estimate of the output size which can help with evaluation of very small data
#[must_use]
pub fn estimated_output_size(&self) -> usize {
self.idat_data.len() + self.raw.key_chunks_size()
}

/// Format the `PngData` struct into a valid PNG bytestream
#[must_use]
pub fn output(&self) -> Vec<u8> {
Expand Down Expand Up @@ -355,6 +349,12 @@ impl PngImage {
}
}

/// Return an estimate of the output size which can help with evaluation of very small data
#[must_use]
pub fn estimated_output_size(&self, idat_data: &[u8]) -> usize {
idat_data.len() + self.key_chunks_size()
}

/// Return an iterator over the scanlines of the image
#[inline]
#[must_use]
Expand Down