Process files in parallel (#531)
This PR makes the oxipng binary process multiple files in parallel, finally fulfilling #275. There seemed to be some debate about whether oxipng _should_ do this or not but there's a couple of reasons I think it makes sense: 1. The concern seemed mostly around the complexity of such a feature. Not to worry, it was trivial* 🙂 2. Since then, oxipng has dropped from a max of something like 180 simultaneous compression trials down to 10, which is very much a good thing but it does mean it's not utilising any more cores than that. Some benchmarks on around 100 files on a machine with 8 cores: Level | Master time | PR time -|-|- 2 | 28.303 | 19.005 3 | 36.507 | 23.089 5 | 1:10.86 | 1:16.01 *Some additional changes were required in order to make sure sensible output is printed to the terminal, since things won't be in order anymore. Here's some example output from before: ``` Processing: tests/files/fully_optimized.png file size = 67 bytes (0 bytes = 0.00% decrease) File already optimized Processing: tests/files/corrupted_header.png Invalid PNG header detected Processing: tests/files/verbose_mode.png file size = 102480 bytes (12228 bytes = 10.66% decrease) Output: tests/files/verbose_mode.png ``` And after: ``` Processing: tests/files/verbose_mode.png Processing: tests/files/fully_optimized.png Processing: tests/files/corrupted_header.png tests/files/corrupted_header.png: Invalid PNG header detected tests/files/fully_optimized.png: Could not optimize further, no change written 102480 bytes (10.66% smaller): tests/files/verbose_mode.png ``` Closes #275, #84, #169, #196 and #419. [edit] This is the last thing I wanted to land before the next release 🥳
This commit is contained in:
parent
31e796c4e8
commit
4ae64c568b
4 changed files with 35 additions and 15 deletions
26
src/lib.rs
26
src/lib.rs
|
|
@ -453,13 +453,15 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(
|
||||||
// Run the optimizer on the decoded PNG.
|
// Run the optimizer on the decoded PNG.
|
||||||
let mut optimized_output = optimize_png(&mut png, &in_data, opts, deadline)?;
|
let mut optimized_output = optimize_png(&mut png, &in_data, opts, deadline)?;
|
||||||
|
|
||||||
|
let in_length = in_data.len();
|
||||||
|
|
||||||
if is_fully_optimized(in_data.len(), optimized_output.len(), opts) {
|
if is_fully_optimized(in_data.len(), optimized_output.len(), opts) {
|
||||||
info!("File already optimized");
|
|
||||||
match (output, input) {
|
match (output, input) {
|
||||||
// if p is None, it also means same as the input path
|
// if p is None, it also means same as the input path
|
||||||
(OutFile::Path(ref p), InFile::Path(ref input_path))
|
(OutFile::Path(ref p), InFile::Path(ref input_path))
|
||||||
if p.as_ref().map_or(true, |p| p == input_path) =>
|
if p.as_ref().map_or(true, |p| p == input_path) =>
|
||||||
{
|
{
|
||||||
|
info!("{}: Could not optimize further, no change written", input);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
|
@ -468,8 +470,22 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let savings = if in_length >= optimized_output.len() {
|
||||||
|
format!(
|
||||||
|
"{} bytes ({:.2}% smaller)",
|
||||||
|
optimized_output.len(),
|
||||||
|
(in_length - optimized_output.len()) as f64 / in_length as f64 * 100_f64
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"{} bytes ({:.2}% larger)",
|
||||||
|
optimized_output.len(),
|
||||||
|
(optimized_output.len() - in_length) as f64 / in_length as f64 * 100_f64
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
if opts.pretend {
|
if opts.pretend {
|
||||||
info!("Running in pretend mode, no output");
|
info!("{}: Running in pretend mode, no output", savings);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -516,7 +532,7 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(
|
||||||
if let Some(metadata_input) = &opt_metadata_preserved {
|
if let Some(metadata_input) = &opt_metadata_preserved {
|
||||||
copy_times(metadata_input, output_path)?;
|
copy_times(metadata_input, output_path)?;
|
||||||
}
|
}
|
||||||
info!("Output: {}", output_path.display());
|
info!("{}: {}", savings, output_path.display());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -606,14 +622,14 @@ fn optimize_png(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if file_original_size >= output.len() {
|
if file_original_size >= output.len() {
|
||||||
info!(
|
debug!(
|
||||||
" file size = {} bytes ({} bytes = {:.2}% decrease)",
|
" file size = {} bytes ({} bytes = {:.2}% decrease)",
|
||||||
output.len(),
|
output.len(),
|
||||||
file_original_size - output.len(),
|
file_original_size - output.len(),
|
||||||
(file_original_size - output.len()) as f64 / file_original_size as f64 * 100_f64
|
(file_original_size - output.len()) as f64 / file_original_size as f64 * 100_f64
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
info!(
|
debug!(
|
||||||
" file size = {} bytes ({} bytes = {:.2}% increase)",
|
" file size = {} bytes ({} bytes = {:.2}% increase)",
|
||||||
output.len(),
|
output.len(),
|
||||||
output.len() - file_original_size,
|
output.len() - file_original_size,
|
||||||
|
|
|
||||||
22
src/main.rs
22
src/main.rs
|
|
@ -13,6 +13,9 @@
|
||||||
#![warn(clippy::range_plus_one)]
|
#![warn(clippy::range_plus_one)]
|
||||||
#![allow(clippy::cognitive_complexity)]
|
#![allow(clippy::cognitive_complexity)]
|
||||||
|
|
||||||
|
#[cfg(not(feature = "parallel"))]
|
||||||
|
mod rayon;
|
||||||
|
|
||||||
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command};
|
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command};
|
||||||
use indexmap::IndexSet;
|
use indexmap::IndexSet;
|
||||||
use log::{error, warn};
|
use log::{error, warn};
|
||||||
|
|
@ -21,6 +24,7 @@ use oxipng::Options;
|
||||||
use oxipng::RowFilter;
|
use oxipng::RowFilter;
|
||||||
use oxipng::StripChunks;
|
use oxipng::StripChunks;
|
||||||
use oxipng::{InFile, OutFile};
|
use oxipng::{InFile, OutFile};
|
||||||
|
use rayon::prelude::*;
|
||||||
use std::fs::DirBuilder;
|
use std::fs::DirBuilder;
|
||||||
#[cfg(feature = "zopfli")]
|
#[cfg(feature = "zopfli")]
|
||||||
use std::num::NonZeroU8;
|
use std::num::NonZeroU8;
|
||||||
|
|
@ -313,9 +317,8 @@ Heuristic filter selection strategies:
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut success = false;
|
let success = files.into_par_iter().filter(|(input, output)| {
|
||||||
for (input, output) in files {
|
match oxipng::optimize(input, output, &opts) {
|
||||||
match oxipng::optimize(&input, &output, &opts) {
|
|
||||||
// For optimizing single files, this will return the correct exit code always.
|
// For optimizing single files, this will return the correct exit code always.
|
||||||
// For recursive optimization, the correct choice is a bit subjective.
|
// For recursive optimization, the correct choice is a bit subjective.
|
||||||
// We're choosing to return a 0 exit code if ANY file in the set
|
// We're choosing to return a 0 exit code if ANY file in the set
|
||||||
|
|
@ -323,16 +326,15 @@ Heuristic filter selection strategies:
|
||||||
// The reason for this is that recursion may pick up files that are not
|
// The reason for this is that recursion may pick up files that are not
|
||||||
// PNG files, and return an error for them.
|
// PNG files, and return an error for them.
|
||||||
// We don't really want to return an error code for those files.
|
// We don't really want to return an error code for those files.
|
||||||
Ok(_) => {
|
Ok(_) => true,
|
||||||
success = true;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("{}", e);
|
error!("{}: {}", input, e);
|
||||||
|
false
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
if !success {
|
if success.count() == 0 {
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@ pub fn join<A, B>(a: impl FnOnce() -> A, b: impl FnOnce() -> B) -> (A, B) {
|
||||||
(a(), b())
|
(a(), b())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn spawn<A>(a: impl FnOnce() -> A) -> A {
|
pub fn spawn<A>(a: impl FnOnce() -> A) -> A {
|
||||||
a()
|
a()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -188,6 +188,7 @@ fn verbose_mode() {
|
||||||
"Found better combination:",
|
"Found better combination:",
|
||||||
" zc = 11 f = None ",
|
" zc = 11 f = None ",
|
||||||
" IDAT size = ",
|
" IDAT size = ",
|
||||||
|
" file size = ",
|
||||||
];
|
];
|
||||||
assert_eq!(logs.len(), expected_prefixes.len());
|
assert_eq!(logs.len(), expected_prefixes.len());
|
||||||
for (i, log) in logs.into_iter().enumerate() {
|
for (i, log) in logs.into_iter().enumerate() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue