From 4e3a659ca9dbce0ab387a6a8cb87f299ad9aacce Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 5 Jun 2026 14:48:14 -0400 Subject: [PATCH] PERF: Enlarge zlib read buffer in NIfTI reader for NAS performance The NIfTI reader opened files with a bare gzopen(), leaving zlib at its 8 KB default internal buffer. zlib refills from the underlying file in buffer-sized chunks, so a multi-GB .nii.gz triggered hundreds of thousands of small reads. On network attached storage each refill is a latency-bound round-trip, making reads disproportionately slow compared to a local disk. --- .../Filters/Algorithms/ReadNIfTIFile.cpp | 6 ++++++ .../src/SimplnxCore/utils/NiftiUtilities.cpp | 4 +++- .../src/SimplnxCore/utils/NiftiUtilities.hpp | 21 +++++++++++++++++-- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadNIfTIFile.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadNIfTIFile.cpp index 586e75f206..4f81488f0d 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadNIfTIFile.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadNIfTIFile.cpp @@ -378,6 +378,12 @@ Result<> ReadNIfTIFile::operator()() return MakeErrorResult(-34720, fmt::format("Could not open NIfTI file for reading: '{}'", m_InputValues->InputFilePath.string())); } + // Enlarge zlib's internal buffer before the first read/seek so the file is + // refilled in large chunks instead of zlib's 8 KB default. This drastically + // reduces the number of latency-bound network round-trips when the input + // lives on a NAS. See nifti::k_GzReadBufferSize for the rationale. + gzbuffer(gz, static_cast(nx::core::nifti::k_GzReadBufferSize)); + const z_off_t targetOffset = static_cast(md.voxOffset); if(gzseek(gz, targetOffset, SEEK_SET) != targetOffset) { diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/utils/NiftiUtilities.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/utils/NiftiUtilities.cpp index de430a4c33..f1f6e28db9 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/utils/NiftiUtilities.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/utils/NiftiUtilities.cpp @@ -119,8 +119,10 @@ Result ReadNiftiHeader(const std::filesystem::path& filePath, boo return MakeErrorResult(-34700, fmt::format("Could not open NIfTI file for reading: '{}'", pathStr)); } + gzbuffer(gz, k_GzReadBufferSize); + nifti_1_header hdr{}; - int bytesRead = gzread(gz, &hdr, static_cast(k_HeaderSize)); + int bytesRead = gzread(gz, &hdr, k_HeaderSize); gzclose(gz); if(bytesRead != static_cast(k_HeaderSize)) diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/utils/NiftiUtilities.hpp b/src/Plugins/SimplnxCore/src/SimplnxCore/utils/NiftiUtilities.hpp index e4a6124c24..29298c48fb 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/utils/NiftiUtilities.hpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/utils/NiftiUtilities.hpp @@ -26,7 +26,7 @@ * `byteSwapRequired` from the metadata to do its own stream reads. * * The helpers here are deliberately independent of the simplnx filter - * framework so they can be reused from unit tests, a future writer, + * framework, so they can be reused from unit tests, a future writer, * or a command-line tool. */ @@ -36,7 +36,7 @@ namespace nx::core::nifti * @brief Size of the NIfTI-1 header in bytes. Always 348 for a valid * NIfTI-1 file regardless of byte order. */ -inline constexpr usize k_HeaderSize = 348; +inline constexpr unsigned int k_HeaderSize = 348; /** * @brief Minimum legal value of the `vox_offset` field in a single-file @@ -46,6 +46,23 @@ inline constexpr usize k_HeaderSize = 348; */ inline constexpr usize k_MinVoxOffset = 352; +/** + * @brief Size (in bytes) of zlib's internal input/output buffer, set via + * `gzbuffer()` immediately after every `gzopen()`. + * + * zlib defaults to an 8 KB internal buffer, which means it refills from + * the underlying file in 8 KB chunks of compressed data — one `read()` + * syscall each. On a local disk that is negligible, but on network + * attached storage (NAS) every refill is a separate network round-trip, + * so a multi-GB `.nii.gz` triggers hundreds of thousands of small, + * latency-bound reads. Raising the buffer to 4 MiB cuts the number of + * round-trips by ~512x while costing only a few × this size in working + * memory (negligible next to the voxel volume). Larger values yield + * diminishing returns once the buffer exceeds the link's + * bandwidth-delay product and the client's own read-ahead. + */ +inline constexpr unsigned int k_GzReadBufferSize = 4194304; + /** * @brief Magic bytes that identify the single-file `.nii` / `.nii.gz` * format. Trailing null is included per the spec.