Two speed items from the #224 audit that need a workspace-level decision rather than a local
change.
1. Parallelism across filter trials
PngEncoder::compress_scanlines (src/encoder.rs:427-437) runs BruteForce's six candidates
sequentially — six full filter passes and six full DEFLATE streams, one after another. They are
embarrassingly parallel, and there is no threading anywhere in gamut-png or gamut-deflate.
oxipng parallelises exactly here (filters.par_iter().with_max_len(1)), not across deflate
attempts, and ships src/rayon.rs — a serial shim implementing join/spawn/par_iter with
identical signatures for when the feature is off. That is the cleanest pattern to copy.
The decision this needs: gamut-deflate advertises zero runtime dependencies, and rayon on
wasm32 builds but only degrades to serial for the implicit global pool (build_global() errors).
So it would have to be feature-gated off by default. Worth noting the closest precedent,
image-rs's png 0.18.1, has no rayon at all.
Determinism must survive: the reduction stays min_by_key over an ordered collection, and the
size contract's encoded_size_is_deterministic already guards it.
2. No composed effort dial
with_compression(Level), with_effort(u8) and with_filter(FilterStrategy) are three
independent knobs, plus with_auto_reduce and now with_transparent_cleanup. Nothing maps one
integer onto a sensible ladder, so a caller wanting "smallest" has to know that it means
Level::Best + BruteForce + auto-reduce. gamut-cli exposes only --png-effort and hardcodes
the rest. gamut-deflate's with_optimal_parse_limit is not re-exported on PngEncoder at all,
so PNG callers are stuck at 1 MiB spans.
Refs #224
Two speed items from the #224 audit that need a workspace-level decision rather than a local
change.
1. Parallelism across filter trials
PngEncoder::compress_scanlines(src/encoder.rs:427-437) runsBruteForce's six candidatessequentially — six full filter passes and six full DEFLATE streams, one after another. They are
embarrassingly parallel, and there is no threading anywhere in
gamut-pngorgamut-deflate.oxipng parallelises exactly here (
filters.par_iter().with_max_len(1)), not across deflateattempts, and ships
src/rayon.rs— a serial shim implementingjoin/spawn/par_iterwithidentical signatures for when the feature is off. That is the cleanest pattern to copy.
The decision this needs:
gamut-deflateadvertises zero runtime dependencies, andrayononwasm32 builds but only degrades to serial for the implicit global pool (
build_global()errors).So it would have to be feature-gated off by default. Worth noting the closest precedent,
image-rs's
png0.18.1, has no rayon at all.Determinism must survive: the reduction stays
min_by_keyover an ordered collection, and thesize contract's
encoded_size_is_deterministicalready guards it.2. No composed effort dial
with_compression(Level),with_effort(u8)andwith_filter(FilterStrategy)are threeindependent knobs, plus
with_auto_reduceand nowwith_transparent_cleanup. Nothing maps oneinteger onto a sensible ladder, so a caller wanting "smallest" has to know that it means
Level::Best+BruteForce+ auto-reduce.gamut-cliexposes only--png-effortand hardcodesthe rest.
gamut-deflate'swith_optimal_parse_limitis not re-exported onPngEncoderat all,so PNG callers are stuck at 1 MiB spans.
Refs #224