perf(compression): Use zlib-rs with runtime detection

Isolate the workspace backend choice and verify PNG, PSD and complete native-format decoding. Record standalone timing and compressed-size tradeoffs.
This commit is contained in:
Rene Leonhardt
2026-10-19 11:31:58 +02:00
parent d95fdf3181
commit 4776d20b7a
5 changed files with 183 additions and 1 deletions
Generated
+1
View File
@@ -4895,6 +4895,7 @@ dependencies = [
"craft-docpath",
"crc32fast",
"flate2",
"miniz_oxide 0.9.1",
"photocraft-affinity",
"photocraft-algo",
"photocraft-cms",
+1 -1
View File
@@ -46,7 +46,7 @@ serde = { version = "1", features = ["derive", "rc"] }
serde_json = "1"
arc-swap = "1"
bytemuck = { version = "1", features = ["derive"] }
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
flate2 = { version = "1", default-features = false, features = ["zlib-rs", "runtime_detection"] }
image = { version = "0.25", default-features = false }
# Baseline-JPEG decode of lossy DNG tiles (compression 34892), shared with zune-jpeg.
zune-core = "0.5"
+1
View File
@@ -49,6 +49,7 @@ resvg = { version = "0.45", default-features = false, features = ["text", "syste
resvg = { version = "0.45", default-features = false, features = ["text", "raster-images"] }
[dev-dependencies]
miniz_oxide = "0.9"
crc32fast = "1.5"
photocraft-affinity = { workspace = true, features = ["synth"] }
flate2 = { workspace = true }
+123
View File
@@ -0,0 +1,123 @@
//! Backend compatibility, including native-format tile data rather than ZIP assembly alone.
mod common;
use std::{hint::black_box, io::Read, time::Instant};
use photocraft_codecs::{ChannelLayout, Format, Image, SampleType as CodecSample};
use photocraft_color::{ColorMode, SampleType};
use photocraft_doc::{Document, Layer, LayerContent};
use photocraft_format::{SaveOptions, load_from_bytes, read_thumbnail, save_to_bytes};
use photocraft_geom::Size;
use photocraft_io::{PsdExportOptions, document_to_psd_with, import};
use photocraft_psd::Compression;
use photocraft_raster::Rgba8Image;
fn document(depth: SampleType, size: Size) -> Document {
let mut doc = Document::new("compatibility", size, ColorMode::Rgb, depth);
doc.layers.push(Layer::new("pixels", LayerContent::Raster(common::pattern(doc.pixel_format(), doc.bounds(), 3, false))));
doc
}
fn old_and_new_inflate(bytes: &[u8]) {
let old = miniz_oxide::inflate::decompress_to_vec_zlib(bytes)
.unwrap_or_else(|e| panic!("old decoder: {:?}, header {:?}", e.status, &bytes[..bytes.len().min(8)]));
let mut new = Vec::new();
flate2::read::ZlibDecoder::new(bytes).read_to_end(&mut new).unwrap();
assert_eq!(new, old);
}
fn check_png(bytes: &[u8], image: &Image) {
assert_eq!(photocraft_codecs::decode(bytes).unwrap().data(), image.data());
let mut idat = Vec::new();
let mut offset = 8;
while offset < bytes.len() {
let len = u32::from_be_bytes(bytes[offset..offset + 4].try_into().unwrap()) as usize;
if &bytes[offset + 4..offset + 8] == b"IDAT" {
idat.extend_from_slice(&bytes[offset + 8..offset + 8 + len]);
}
offset += 12 + len;
}
old_and_new_inflate(&idat);
}
#[test]
fn png_psd_and_pcraft_decode_identically() {
for depth in [SampleType::U8, SampleType::U16, SampleType::F32] {
let doc = document(depth, Size::new(257, 131));
let pixels = doc.layers[0].surface().unwrap().read_region(doc.bounds());
if depth != SampleType::F32 {
let sample = if depth == SampleType::U8 { CodecSample::U8 } else { CodecSample::U16 };
let image = Image::from_normalized(257, 131, ChannelLayout::Rgba, sample, &pixels).unwrap();
check_png(&photocraft_codecs::encode(&image, Format::Png, &Default::default()).unwrap(), &image);
}
for compression in [Compression::Zip, Compression::ZipPrediction] {
let opts = PsdExportOptions { merged_compression: compression, merged_matte: false, ..Default::default() };
let mut file = document_to_psd_with(&doc, &opts).0;
// The IO exporter normally chooses Raw for float composites; also cover ZIP here.
if depth == SampleType::F32 {
file.image_data = photocraft_psd::ImageData::encode(compression, &file.image_data.decode(&file.header).unwrap(), &file.header).unwrap();
}
let bytes = file.to_bytes().unwrap();
let parsed = photocraft_psd::PsdFile::from_bytes(&bytes).unwrap();
assert_eq!(parsed.image_data.compression, compression);
old_and_new_inflate(&parsed.image_data.data);
let back = import("compatibility.psd", &bytes).unwrap().document;
assert_eq!(back.layers[0].surface().unwrap().read_region(doc.bounds()), pixels);
}
let mut thumbnail = Rgba8Image::new(7, 3);
thumbnail.pixels = (0..84).map(|i| (i * 37) as u8).collect();
let opts = SaveOptions { thumbnail: Some(thumbnail.clone()), ..Default::default() };
let bytes = save_to_bytes(&doc, &opts).unwrap();
assert!(load_from_bytes(&bytes).unwrap() == doc, "all tile bytes, {depth:?}");
let image = Image::from_u8(7, 3, ChannelLayout::Rgba, thumbnail.pixels).unwrap();
check_png(&read_thumbnail(&bytes).unwrap().unwrap(), &image);
}
}
#[test]
#[ignore = "manual 24 MP release benchmark; run on main and the backend branch"]
fn benchmark_deflate_formats() {
for pattern in ["ramp", "noise"] {
let mut doc = document(SampleType::U8, Size::new(6000, 4000));
if pattern == "noise" {
let mut seed = 0x9e3779b9u32;
let raw: Vec<u8> = (0..96_000_000usize)
.map(|i| {
seed ^= seed << 13;
seed ^= seed >> 17;
seed ^= seed << 5;
if i % 4 == 3 { 255 } else { (seed >> 24) as u8 }
})
.collect();
doc.layers[0].content = LayerContent::Raster(photocraft_raster::Surface::from_interleaved(doc.pixel_format(), doc.bounds(), &raw));
}
let pixels = doc.layers[0].surface().unwrap().read_region(doc.bounds());
let image = Image::from_normalized(6000, 4000, ChannelLayout::Rgba, CodecSample::U8, &pixels).unwrap();
drop(pixels);
let psd_opts = PsdExportOptions { merged_compression: Compression::Zip, merged_matte: false, ..Default::default() };
let save_opts = SaveOptions { thumbnail: Some(Rgba8Image::new(256, 170)), ..Default::default() };
let png = photocraft_codecs::encode(&image, Format::Png, &Default::default()).unwrap();
let psd = document_to_psd_with(&doc, &psd_opts).0.to_bytes().unwrap();
let pcraft = save_to_bytes(&doc, &save_opts).unwrap();
for trial in 0..6 {
for op in ["png-encode", "png-decode", "psd-encode", "psd-decode", "pcraft-save", "pcraft-load"] {
let start = Instant::now();
let size = match op {
"png-encode" => black_box(photocraft_codecs::encode(black_box(&image), Format::Png, &Default::default()).unwrap()).len(),
"png-decode" => black_box(photocraft_codecs::decode(black_box(&png)).unwrap()).data().len(),
"psd-encode" => black_box(document_to_psd_with(black_box(&doc), &psd_opts).0.to_bytes().unwrap()).len(),
"psd-decode" => {
black_box(import("bench.psd", black_box(&psd)).unwrap());
psd.len()
}
"pcraft-save" => black_box(save_to_bytes(black_box(&doc), &save_opts).unwrap()).len(),
_ => {
black_box(load_from_bytes(black_box(&pcraft)).unwrap());
pcraft.len()
}
};
println!("deflate,{pattern}-{op},{trial},{:.3},{size}", start.elapsed().as_secs_f64() * 1000.0);
}
}
}
}
+57
View File
@@ -0,0 +1,57 @@
# Deflate backend comparison
Only the workspace flate2 backend changes from miniz_oxide to zlib-rs with runtime CPU detection. The 24 MP (6000×4000) RGBA8 cases are a repeating ramp and deterministic noise; inputs are prepared outside the timers.
Measured on 2026-10-19 against `d95fdf3181c01ad41e95456091a394b105a9afb2` on an Apple M4 Max
(16 CPUs, 128 GiB), macOS arm64, rustc 1.99.1. Each branch starts directly at that
base; none of the other performance splits is applied. Release uses optimization
level 3, LTO off, 16 codegen units, debug info off and incremental compilation off.
Builds ran at nice 10 with APFS-cloned warm caches; measurements ran at normal
priority with no concurrent builds. Timings are medians of five samples after one
warm-up, except the compositor's ten Criterion samples. These synthetic workloads
are reproducible checks of this change, not a representative image corpus.
| Workload | Main ms | This change ms | Time change |
|---|---:|---:|---:|
| ramp-png-encode | 42.114 | 43.989 | +4.5% |
| ramp-png-decode | 47.100 | 8.697 | -81.5% |
| ramp-psd-encode | 200.218 | 162.012 | -19.1% |
| ramp-psd-decode | 45.092 | 31.839 | -29.4% |
| ramp-pcraft-save | 82.126 | 83.149 | +1.2% |
| ramp-pcraft-load | 1.424 | 1.474 | +3.5% |
| noise-png-encode | 206.339 | 152.328 | -26.2% |
| noise-png-decode | 187.408 | 183.032 | -2.3% |
| noise-psd-encode | 1191.777 | 1002.312 | -15.9% |
| noise-psd-decode | 30.907 | 30.394 | -1.7% |
| noise-pcraft-save | 177.713 | 175.379 | -1.3% |
| noise-pcraft-load | 103.026 | 101.085 | -1.9% |
| Encoded file | Main bytes | This change bytes |
|---|---:|---:|
| ramp-png-encode | 107,506 | 478,751 |
| ramp-psd-encode | 73,268,469 | 73,503,868 |
| ramp-pcraft-save | 88,188 | 88,214 |
| noise-png-encode | 82,731,323 | 82,272,418 |
| noise-psd-encode | 144,983,563 | 144,994,193 |
| noise-pcraft-save | 96,670,862 | 96,670,888 |
Compressed bytes are not identical across backends. The ramp PNG gets larger; the noise case and decode timings must be considered alongside encode time. The native format stores Zstandard tiles and STORE ZIP entries, so this switch only changes its PNG previews; the repeating ramp deduplicates many tiles. No tile-compression speedup is attributed to deflate.
The committed compatibility test compares old miniz_oxide and flate2 inflation of PNG IDAT and PSD ZIP/ZIP-prediction streams, checks exact decoded PNG/PSD pixels at supported depths, and compares complete .pcraft documents at U8/U16/F32 (tiles included) plus their PNG thumbnail. Float PSD composites are explicitly ZIP-encoded in the test because the IO exporter normally chooses Raw. The same test passed on the base and this branch.
Reproduce with the same release profile environment:
```sh
CARGO_PROFILE_RELEASE_LTO=false CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16 CARGO_PROFILE_RELEASE_DEBUG=0 CARGO_INCREMENTAL=0 nice -n 10 cargo test --release -p photocraft-io --test deflate_compatibility --no-run
# Run the printed executable at normal priority:
<printed-executable> benchmark_deflate_formats --ignored --nocapture --test-threads=1
```
## Local validation
- `cargo test -p photocraft-codecs -p photocraft-psd -p photocraft-format -p photocraft-io --locked`: passed (1956 passed, 9 ignored).
- `cargo clippy -p photocraft-codecs -p photocraft-psd -p photocraft-format -p photocraft-io --locked --all-targets -- -D warnings`: passed.
- `cargo xtask layers`: passed.
- `cargo xtask wasm`: passed.
- `cargo xtask test-corpus -p psd -p codecs -p io`: passed (1853 passed, 9 ignored).
- The compatibility test also passed with the main backend before measurements. To reproduce the main comparison, copy only this test and its miniz_oxide dev-dependency into a clean main worktree.