Files
photocraft/docs/deflate-zlib-rs-performance.md
T
David Mann 053f0b7198 Integration: level 3 instead of fast() under zlib-rs, io test into tests/it (#7689)
zlib-rs maps level 1 to deflate_quick, which writes static Huffman blocks
only, so every Compression::fast() caller (PDF export and print, brush
presets, PNG Fast) wrote 5-32% bigger streams than miniz's level 1 did.
Level 3 is the lowest level that is never bigger than miniz level 1 on the
docs/images photos, raw and Up-filtered (level 2 was +0.9% on one); it is
0-16% smaller and takes 1.2-3.4 times as long. The new test exports a
noisy-gradient RGB document as PDF and checks the image stream against
miniz level 1 of the same samples: 603007 vs 547315 bytes with fast(),
passes with level 3. The PR's doc no longer says the levels are unchanged.

Main moved the io integration tests into crates/io/tests/it/, so the new
deflate_compatibility.rs moves there and uses crate::common.

Co-authored-by: Rene Leonhardt <65483435+reneleonhardt@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 02:12:13 +01:00

9.5 KiB
Raw Blame History

Deflate backend comparison

Replace the workspace flate2 Rust backend with zlib-rs and runtime CPU detection. Tiles in .pcraft remain Zstandard; deflate changes its PNG previews.

Size, import-speed and dependency tradeoffs remain maintainer decisions. Some PSD ZIP files grow by up to 0.71%. The macOS 16-bit city PSD import is about 6% slower in confirmation. The small web-layout PNG grows by 598 bytes (+1.40%). One .pcraft preview adds 25 bytes. Real photo PNGs become smaller and encode faster. The default and best levels are unchanged; callers that used level 1 (PDF export and print, brush tips, PNG Fast) now use level 3, because zlib-rs level 1 writes static Huffman blocks only (13-32% bigger on photos). Level 3 is never bigger than miniz's level 1 on the docs/images photos (0-16% smaller) and takes 1.2-3.4 times as long. There is no native-format tile speedup from deflate.

The timings below measure only the backend switch. They exclude the later ZIP allocation, validation and slice decoder changes.

Measured on 2026-10-19 against main d95fdf31, independently of the other performance changes. Host: Apple M4 Max, 16 CPUs, 128 GiB RAM, macOS arm64, rustc 1.99.1. Release settings: optimization level 3, no LTO, 16 codegen units and no debug information. Builds finish before measurement. Measurements use default priority without concurrent builds or profiling. Five alternating pairs follow a warmup. Confirmation uses nine pairs. Time changes compare medians, and intervals resample paired ratios. Negative changes mean less elapsed time. Small differences are not automatically established improvements or regressions.

The full-resolution CC0 Pixls photographs are P1000475.RW2 (24 MP foliage centre crop, no resampling) and IMG_4059.CR2 (12 MP city), each converted to U8/U16/F32 with the ProPhoto RGB profile. F32 derives from 16-bit development, not HDR capture. MIT psd-tools masks.psd supplies a 640 x 1394 web layout with 22 imported layers and three masks; mask-index.psd supplies a 1000 x 867 illustrated card with a mask. The sources and hashes are pinned by the existing corpus. These are the available authored layered files, not a large layered-photo corpus. No generated noise, repeating ramps, enlarged charts or duplicated photo layers are used.

The shared harness times in-memory operations. Input loading, image conversion, thumbnail rendering, hashes and equivalence checks are outside timers. Every save uses a fresh PcraftWriter and a real rendered thumbnail. Common reads use main-encoded files; own-output reads are separate. PSD exports use ZIP merged images at U8/U16, Raw merged images at F32, and normal RLE layer channels at U8/U16 / ZIP prediction at F32. Results do not measure filesystem throughput or browser performance.

Document Operation Main ms Branch ms Time change Paired 95% interval Main / branch bytes
canon-16 psd-import (confirm) 168.891 178.614 +5.8% +3.5..+5.9% 140,673,004 / 140,673,004
panasonic-8 save 166.659 162.322 -2.6% -3.9..-1.0% 49,643,217 / 49,643,242
panasonic-8 load-common 410.794 409.622 -0.3% -0.5..+0.3% 49,643,217 / 49,643,217
panasonic-8 png-encode 373.880 165.381 -55.8% -56.3..-47.4% 24,569,965 / 24,238,416
panasonic-8 png-decode 168.141 167.247 -0.5% -2.0..+1.0% 24,569,965 / 24,569,965
panasonic-8 psd-export 2938.276 1115.740 -62.0% -62.3..-61.6% 98,823,859 / 98,837,585
panasonic-8 psd-import 275.713 174.046 -36.9% -38.8..-34.7% 98,823,859 / 98,823,859
panasonic-16 save 357.998 355.900 -0.6% -1.8..+0.5% 175,681,852 / 175,681,844
panasonic-16 load-common 387.986 391.411 +0.9% -1.3..+1.1% 175,681,852 / 175,681,852
panasonic-16 png-encode 354.924 288.664 -18.7% -27.8..-14.2% 111,583,441 / 109,241,690
panasonic-16 png-decode 434.083 433.645 -0.1% -1.0..+1.7% 111,583,441 / 111,583,441
panasonic-16 psd-export 2645.838 2249.281 -15.0% -15.6..-13.8% 274,409,683 / 275,654,405
panasonic-16 psd-import 371.291 374.777 +0.9% -1.1..+1.8% 274,409,683 / 274,409,683
panasonic-32 save 639.677 631.556 -1.3% -5.2..+1.8% 252,231,671 / 252,231,663
panasonic-32 load-common 1338.160 1326.936 -0.8% -1.1..-0.2% 252,231,671 / 252,231,671
panasonic-32 psd-export 2522.700 1352.144 -46.4% -49.6..-44.6% 461,042,978 / 459,545,262
panasonic-32 psd-import 356.077 310.425 -12.8% -20.2..-4.7% 461,042,978 / 461,042,978
canon-8 save 89.139 85.965 -3.6% -8.9..+2.7% 26,151,631 / 26,151,203
canon-8 load-common 219.711 219.088 -0.3% -0.3..+1.0% 26,151,631 / 26,151,631
canon-8 png-encode 179.944 80.123 -55.5% -60.8..-40.0% 13,720,268 / 13,551,023
canon-8 png-decode 69.704 70.040 +0.5% -0.4..+0.6% 13,720,268 / 13,720,268
canon-8 psd-export 1074.094 475.802 -55.7% -56.1..-55.4% 56,209,597 / 56,608,401
canon-8 psd-import 142.523 96.323 -32.4% -34.4..-26.9% 56,209,597 / 56,209,597
canon-16 save 185.898 183.472 -1.3% -2.8..-0.2% 88,750,003 / 88,749,555
canon-16 load-common 193.409 193.704 +0.2% -0.2..+0.8% 88,750,003 / 88,750,003
canon-16 png-encode 169.432 135.364 -20.1% -26.2..-15.4% 58,279,529 / 57,568,207
canon-16 png-decode 211.947 211.829 -0.1% -0.4..+0.5% 58,279,529 / 58,279,529
canon-16 psd-export 1295.076 1130.697 -12.7% -13.5..-9.9% 140,673,004 / 141,105,829
canon-32 save 340.781 334.426 -1.9% -5.3..+0.7% 120,957,984 / 120,957,536
canon-32 load-common 774.663 778.401 +0.5% -1.3..+8.5% 120,957,984 / 120,957,984
canon-32 psd-export 1028.651 647.483 -37.1% -40.5..-36.1% 228,234,238 / 227,497,106
canon-32 psd-import 177.500 146.373 -17.5% -18.5..-17.1% 228,234,238 / 228,234,238
layout-masks save 18.588 18.268 -1.7% -2.7..+0.6% 433,949 / 433,924
layout-masks load-common 16.099 16.080 -0.1% -1.5..+0.6% 433,949 / 433,949
layout-masks png-encode 3.911 2.557 -34.6% -36.0..-32.3% 42,842 / 43,440
layout-masks png-decode 0.781 0.767 -1.8% -2.5..+2.2% 42,842 / 42,842
layout-masks psd-export 56.043 46.949 -16.2% -18.5..-15.9% 493,006 / 493,145
layout-masks psd-import 12.324 12.334 +0.1% -4.8..+2.3% 493,006 / 493,006
layout-mask-index save 7.367 7.202 -2.2% -5.5..-0.7% 344,892 / 344,746
layout-mask-index load-common 6.601 6.602 +0.0% -2.8..+4.7% 344,892 / 344,892
layout-mask-index png-encode 11.520 5.971 -48.2% -49.5..-46.0% 115,229 / 114,892
layout-mask-index png-decode 1.451 1.444 -0.5% -4.2..+0.5% 115,229 / 115,229
layout-mask-index psd-export 19.135 12.721 -33.5% -34.0..-31.5% 411,435 / 411,594
layout-mask-index psd-import 3.362 2.721 -19.1% -21.3..-11.2% 411,435 / 411,435

Every PNG encode decodes to identical source pixels, each PSD operation produces the same decoded layer/mask samples as main, and complete .pcraft documents compare equal at every depth. Compressed bytes may differ. The committed compatibility test independently inflates PNG IDAT and PSD ZIP/ZIP-prediction data using old miniz_oxide and checks .pcraft tiles plus thumbnails.

Allocation and validation changes

ZIP lookup keys borrow valid UTF-8 entry paths, avoiding a second string allocation per entry. Invalid names keep their previous lossy spelling and duplicate-name lookup still selects the first entry. The bundle writer reserves the complete archive after compression, including its central directory. It rejects entry paths that exceed ZIP's 65,535-byte field before writing and checks that the central-directory size fits its ZIP field. These archive paths are independent of filesystem path limits.

Affinity zlib entries, deep EXR ZIP chunks and compressed SVG input now decode directly from their byte slices. This removes flate2's 32 KiB input-buffer allocation and copy for each decoder. Output bounds, checksums and SVG concatenated gzip members remain covered by tests. These are structural allocation reductions, not measured timing gains.

Linux and Windows x86-64 validation

GitHub Actions run passed the deflate compatibility test, the full IO/codecs/format package tests and cargo xtask wasm on Ubuntu 24.04 and Windows 2022 x86-64. The temporary ci/deflate-zlib-rs-x86 branch at 46232997 contains the workflow and benchmark setup. The run's deflate-x86-results artifact contains all raw samples, the full table, input and binary hashes, backend features and host details.

The timing host used an Intel Xeon 6973P-C with four vCPUs and rustc 1.99.1. Main at d95fdf31 and the backend change at a37198d4 used matching release settings. One warmup preceded nine alternating pairs on the same 12/24 MP photos at U8/U16. Both importers received the same baseline-encoded PSD files. Loading, conversion, hashes and checks stayed outside the elapsed-time measurements. PNG encode time fell 23.8–56.8%, PSD export time 18.1–58.2%, and PSD import time 7.8–21.6%. The 16-bit city import fell from 440.733 to 402.057 ms (-8.78%); the macOS slowdown did not recur on this runner. Photo PNGs became 1.2–2.1% smaller. Exported PSDs grew by 0.014–0.709%. All decoded pixels, dimensions, depths and profiles matched. These results cover this Linux host; Windows and F32 were not timed.