Expose chunk-level HDF5 APIs (H5Dchunk_iter, direct chunk I/O) to Java (JNI + FFM) - #6547
Expose chunk-level HDF5 APIs (H5Dchunk_iter, direct chunk I/O) to Java (JNI + FFM)#6547mkitti wants to merge 18 commits into
Conversation
…a bindings Adds native declarations and JNI glue for H5Dchunk_iter (with a new H5D_chunk_iter_cb/H5D_chunk_iter_t callback pair) plus the by-index chunk inspection functions H5Dget_num_chunks and H5Dget_chunk_info, enabling Java callers to enumerate chunks without a per-tool JNI helper. Includes JUnit coverage and a quick benchmark comparing H5Dchunk_iter against a per-index H5Dget_chunk_info loop, mirroring the scaling analysis from JuliaIO/HDF5.jl#1031 (comment).
…a bindings Adds the FFM H5.java wrappers (upcall stub via the jextract-generated H5D_chunk_iter_op_t typedef class) for H5Dchunk_iter, plus H5Dget_num_chunks and H5Dget_chunk_info for by-index chunk inspection, with matching H5D_chunk_iter_cb/H5D_chunk_iter_t callback interfaces. Includes JUnit coverage and the same quick benchmark added to the JNI tree, comparing H5Dchunk_iter against a per-index H5Dget_chunk_info loop per JuliaIO/HDF5.jl#1031 (comment).
Adds H5Dget_chunk_info_by_coord, H5Dget_chunk_storage_size, H5Dget_chunk_index_type, H5Dwrite_chunk, and H5Dread_chunk (calling the current H5Dread_chunk2 symbol) to the JNI tree, rounding out low-level raw chunk access alongside the H5Dchunk_iter/H5Dget_num_chunks/H5Dget_chunk_info added earlier this session. Also adds the five missing H5D_CHUNK_IDX_* HDF5Constants values needed to interpret H5Dget_chunk_index_type's result. H5Dread_chunk guards against a real silent-failure hazard in the underlying H5D__chunk_direct_read(): if the caller's buffer doesn't match the true on-disk chunk size, the C function still returns success but leaves the buffer untouched. Since the Java API doesn't expose the in/out buf_size parameter, the JNI glue checks it internally and throws IllegalArgumentException on a size mismatch instead of silently returning stale data. Includes JUnit coverage for each new function, including the buffer-size mismatch case.
Adds H5Dget_chunk_info_by_coord, H5Dget_chunk_storage_size, H5Dget_chunk_index_type, H5Dwrite_chunk, and H5Dread_chunk (calling the current H5Dread_chunk2 symbol) to the FFM tree, matching the JNI tree added in the same session and rounding out low-level raw chunk access alongside H5Dchunk_iter/H5Dget_num_chunks/H5Dget_chunk_info added earlier. Also adds the five missing H5D_CHUNK_IDX_* HDF5Constants values needed to interpret H5Dget_chunk_index_type's result. H5Dread_chunk guards against a real silent-failure hazard in the underlying H5D__chunk_direct_read(): if the caller's buffer doesn't match the true on-disk chunk size, the C function still returns success but leaves the buffer untouched. Since the Java API doesn't expose the in/out buf_size parameter, the wrapper checks it internally and throws IllegalArgumentException on a size mismatch instead of silently returning stale data. Includes JUnit coverage for each new function, including the buffer-size mismatch case.
…arm-up H5D_chunk_iter_cb was doing three wasteful things on every single chunk callback instead of once per H5Dchunk_iter() call: - AttachCurrentThread/DetachCurrentThread, even though the callback runs synchronously on the same (already-attached) thread that entered Java_hdf_hdf5lib_H5_H5Dchunk_1iter - GetObjectClass + GetMethodID, a name/signature lookup repeated per chunk instead of resolved once - NewLongArray, allocating a fresh JVM heap array per chunk instead of reusing one All three are now resolved/allocated once in Java_hdf_hdf5lib_H5_H5Dchunk_1iter and threaded through the callback wrapper struct. Measured ~2.8x faster at 4096 chunks (3.7ms -> 1.3ms), cutting JNI's overhead relative to an equivalent pure-C H5Dchunk_iter call from ~18x to ~6.5x. Full TestH5D suite (38 tests) still passes. Reusing the same offset array across chunks means it's only valid for the duration of a single callback invocation now (documented in the callback's javadoc) -- this actually brings JNI's contract in line with the FFM callback's MemorySegment, which was always a transient view over native memory. Also adds a warm-up pass to TestH5DChunkIterPerf before timing, so the smallest (most overhead-sensitive) sweep entry isn't dominated by one-time JIT/class-loading cost unrelated to what's being compared.
Adds a warm-up pass to TestH5DChunkIterPerf before timing, so the smallest (most overhead-sensitive) sweep entry isn't dominated by one-time JIT/ class-loading/MethodHandle-linkage cost unrelated to what's being compared. Also adds countByIndexSharedArena(), a diagnostic variant of the by-index loop that calls the raw jextract binding directly with a single reused Arena/MemorySegment set instead of H5.H5Dget_chunk_info()'s one-Arena-per-call cost. This isolated how much of H5Dget_chunk_info's FFM overhead is Arena allocation (real at small chunk counts, ~2-5x) versus the downcall itself (dominant at large chunk counts, where shared-arena and per-call-arena times converge to within ~1-2% of each other and of the equivalent JNI/pure-C numbers) -- confirming the by-index approach's blowup is a C-library property, not a binding-layer one. Documents that H5D_chunk_iter_cb's MemorySegment offset parameter is a transient view over native memory owned by the H5Dchunk_iter call and must not be retained past the callback returning -- this was always true, just not previously stated explicitly.
Adds H5Dchunk_iter_all(dataset_id, dxpl_id), a bulk convenience form of H5Dchunk_iter() that returns every chunk's offset, filter mask, address, and size as a single H5D_chunk_info_t instead of requiring the caller to author a callback. The JNI implementation accumulates chunk info in a plain C callback with zero JVM crossings per chunk (just memcpy into pre-sized native buffers, pre-sized exactly via H5Dget_num_chunks over the dataset's own dataspace), converting to Java arrays only once at the end. Measured ~2x faster than the already-optimized streaming H5Dchunk_iter at ~4000 chunks, and close to the pure-C baseline established earlier this session. H5D_chunk_info_t holds per-chunk fields as parallel primitive arrays rather than an array of per-chunk objects, avoiding one Java allocation per chunk on the way out. Includes JUnit coverage cross-checking the bulk result against the per-chunk H5Dget_chunk_info() results (matched by offset, since the two APIs aren't guaranteed to visit chunks in the same order), and an added column in TestH5DChunkIterPerf comparing all three chunk-enumeration strategies.
Adds H5Dchunk_iter_all(dataset_id, dxpl_id), matching the JNI tree added in the same session: a bulk convenience form of H5Dchunk_iter() returning every chunk's offset, filter mask, address, and size as a single H5D_chunk_info_t instead of requiring the caller to author a callback. Buffers are pre-sized exactly via H5Dget_num_chunks over the dataset's own dataspace, matching the JNI implementation's approach. Unlike the JNI implementation, this is NOT a performance optimization on FFM -- measured slower than the already-optimized streaming H5Dchunk_iter at large chunk counts, and documented as such in the method's javadoc. The C library still invokes a callback once per chunk, and in FFM that callback must be an upcall stub crossing back into the JVM on every invocation -- there is no way to give the native library a callback that runs without JVM involvement using java.lang.foreign alone, unlike JNI where the callback can be plain C. This method pays that same per-chunk upcall cost plus the extra work of copying each chunk's data into the accumulating buffers, so it's offered here purely for a simpler call site (no callback to write), not as a speedup. Includes JUnit coverage cross-checking the bulk result against the per-chunk H5Dget_chunk_info() results (matched by offset), and an added column in TestH5DChunkIterPerf comparing all chunk-enumeration strategies.
Review ChecklistThis PR touches the following areas. Each needs a sign-off
|
CI's Formatting Check (clang-format 17) flagged alignment/wrapping differences in the chunk-related additions from this branch. Ran clang-format 17 (matching CI exactly, via pixi) over the changed files and committed the result -- purely whitespace, no semantic changes (verified by reviewing every diff and rebuilding + rerunning the full TestH5D suite, which still passes 39/39).
CI's Formatting Check (clang-format 17) flagged alignment/wrapping differences in the chunk-related additions from this branch. Ran clang-format 17 (matching CI exactly, via pixi) over the changed files and committed the result -- purely whitespace, no semantic changes (verified by reviewing every diff; JNI-tree equivalent rebuilt and retested to confirm the formatter didn't alter behavior).
java/src-jni/test/CMakeLists.txt's HDFTEST_COPY_FILE unconditionally requires testfiles/JUnit-<test>.txt to exist as a build dependency for every entry in HDF5_JAVA_TEST_SOURCES, including TestH5DChunkIterPerf added earlier this session. The file was never committed, so any build with BUILD_TESTING=ON failed outright at the ninja/make generation step with "missing and no known rule to make it" -- this is what was breaking CI broadly (not just the Formatting Check fixed in the previous commits). Committed as empty rather than a captured run's output: this benchmark's printed timings are inherently non-deterministic across machines, and COMPARE_TEST (config/cmake/runExecute.cmake) explicitly skips content comparison when the reference file is empty, checking only exit code -- exactly the right behavior here. Verified locally: clean rebuild no longer fails, and ctest reports "COMPARE Result: 0" / test passed.
Same issue as the JNI-tree commit: java/test/CMakeLists.txt's HDFTEST_COPY_FILE requires testfiles/JUnit-TestH5DChunkIterPerf.txt to exist as a build dependency, and it was never committed, breaking any BUILD_TESTING=ON build. Committed empty for the same reason: this benchmark's output is inherently non-deterministic across machines, and an empty reference file makes COMPARE_TEST check only exit code.
The tracked reference predated this session's new TestH5D test methods (testH5Dget_chunk_info_by_coord, testH5Dget_chunk_storage_size, testH5Dget_chunk_index_type, testH5Dwrite_chunk_and_read_chunk, testH5Dchunk_iter, testH5Dchunk_iter_all, testH5Dread_chunk_buffer_size_mismatch), so JUnit4's hash-based MethodSorters.DEFAULT ordering reshuffled the whole class's dot-progress sequence and the final "OK (32 tests)" count no longer matched, breaking JUnit-TestH5D across every CI job that builds Java tests. Regenerated from an actual local run (44/44 JUnit tests, 178/178 broader ctest passing). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Resolves conflict in java/src-jni/test/testfiles/JUnit-TestH5D.txt: develop added testH5DArray_datatype_ids_stable while this branch added several chunk-related tests. TestH5D.java itself auto-merged cleanly (both sets of new test methods are independent). Regenerated the reference file from an actual local run (40 tests total: 32 pre-existing + 1 from develop + 7 new chunk tests from this branch), verified via ctest (44/44 JUnit tests pass). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same root cause as the JNI-side fix: the tracked reference predated this session's new TestH5D test methods (testH5Dget_chunk_info_by_coord, testH5Dget_chunk_index_type, testH5Dchunk_iter_all, testH5Dchunk_iter, testH5Dwrite_chunk_and_read_chunk, testH5Dget_chunk_storage_size, testH5Dread_chunk_buffer_size_mismatch), so JUnit4's hash-based MethodSorters.DEFAULT ordering reshuffled the whole class's dot-progress sequence and the "OK (32 tests)" count no longer matched. Regenerated from an actual scoped local FFM build/run (JDK 25 + jextract; 44/44 JUnit tests passing), matching the CI failure output exactly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
| chunks | JNI iter_ms(streaming) |
JNI all_ms(bulk) |
FFM iter_ms(streaming) |
FFM all_ms(bulk) |
indx_ms(by-index loop, either tree)† |
|---|---|---|---|---|---|
| 4 | 0.012 | 0.011 | 0.377 | 0.543 | ~0.02 |
| 16 | 0.022 | 0.016 | 0.488 | 0.728 | ~0.05 |
| 64 | 0.036 | 0.019 | 0.531 | 1.470 | ~0.28 |
| 256 | 0.130 | 0.057 | 1.829 | 4.426 | ~7 |
| 1024 | 0.308 | 0.155 | 2.276 | 8.166 | ~50 |
| 4096 | 1.168 | 0.535 | 4.089 | 11.819 | ~700 |
† indx_ms (looping H5Dget_chunk_info by index) is shown once since it's
dominated by the C-library algorithm, not the binding layer — both trees
match to within normal run-to-run noise. It grows super-linearly and is
already ~130-600× slower than either chunk-iterate approach by 4096 chunks,
confirming this is a library/index-structure effect, not a JNI/FFM artifact.
Interpretation
- JNI:
H5Dchunk_iter_allis a genuine speedup, ~1.9-2.3× faster than
streamingH5Dchunk_iterfrom a few hundred chunks up. This is real and
architectural: JNI's callback trampoline is plain C, so the bulk variant's
callback (H5D_chunk_iter_all_cb) does zero JNI/JVM crossings per chunk —
just a native accumulate — with allnchunks converted to Java arrays
once, at the end, instead of once per chunk. - FFM:
H5Dchunk_iter_allis slower than streamingH5Dchunk_iter,
by about 1.4-3.6× depending on size — the opposite direction from JNI.
This is also architectural, not a missed optimization: in FFM, the
callback handed to the C library must be a Java upcall stub (there is
no way to giveH5Dchunk_itera pure-native callback via
java.lang.foreignalone), so it pays the same per-chunk JVM-crossing
cost as streamingH5Dchunk_iter, plus extra per-chunk work: the
mandatory.reinterpret()on the zero-lengthoffsetMemorySegment
jextract hands the upcall, plus aMemorySegment.copyand three
setAtIndexwrites to stash each chunk's data before the final bulk
conversion. There's no callback-side win available to claim back. - FFM is slower than JNI in absolute terms for both variants, as
expected for a downcall/upcall-per-chunk cost model vs. plain JNI, with
the gap most pronounced for the bulk variant (~22× at 4096 chunks) since
that's exactly where FFM's structural overhead compounds and JNI's does
not. - Despite FFM's
H5Dchunk_iter_allnot being a perf win there, it's kept in
both trees for API symmetry/ergonomics — it's documented in the FFM
javadoc andjava/CLAUDE.mdas ergonomics-only on that side, not a
performance claim. - The actually load-bearing result for the original HDF5.jl-style
concern is chunk-iterate (either variant, either tree) vs. by-index
looping, not JNI vs. FFM — that gap (700ms vs ~1ms at 4096 chunks) is the
one that matters for any caller enumerating a large number of chunks.
Implementation: 24e9353 (JNI), 96d25ed (FFM). Benchmark source:
java/src-jni/test/TestH5DChunkIterPerf.java /
java/test/TestH5DChunkIterPerf.java.
There was a problem hiding this comment.
Pull request overview
This PR expands the HDF5 Java bindings (both java/src-jni JNI and java/hdf FFM/Panama trees) to expose chunk-level dataset APIs, including chunk iteration, chunk metadata queries, and direct raw chunk I/O, with accompanying tests and a JNI callback-overhead optimization.
Changes:
- Adds Java bindings for chunk enumeration (
H5Dchunk_iter,H5Dchunk_iter_all) and chunk metadata queries (H5Dget_num_chunks,H5Dget_chunk_info*,H5Dget_chunk_storage_size,H5Dget_chunk_index_type). - Adds direct raw chunk I/O APIs (
H5Dwrite_chunk,H5Dread_chunk) plus missingH5D_CHUNK_IDX_*constants. - Adds new/updated JUnit tests (including a perf/diagnostic benchmark test) and integrates them into the Java test CMake targets.
Reviewed changes
Copilot reviewed 23 out of 25 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| java/test/TestH5DChunkIterPerf.java | Adds an FFM-side JUnit benchmark/diagnostic test comparing chunk enumeration strategies. |
| java/test/TestH5D.java | Adds FFM-side functional tests for chunk iteration, bulk iteration, metadata queries, and direct chunk I/O. |
| java/test/testfiles/JUnit-TestH5DChunkIterPerf.txt | Adds reference file (empty to avoid stdout comparison) for the new perf test. |
| java/test/testfiles/JUnit-TestH5D.txt | Updates expected JUnit output to include the newly added TestH5D tests. |
| java/test/CMakeLists.txt | Registers TestH5DChunkIterPerf in the FFM Java test build/run list. |
| java/src-jni/test/TestH5DChunkIterPerf.java | Adds a JNI-side JUnit benchmark test comparing chunk enumeration strategies. |
| java/src-jni/test/TestH5D.java | Adds JNI-side functional tests for chunk iteration, bulk iteration, metadata queries, and direct chunk I/O. |
| java/src-jni/test/testfiles/JUnit-TestH5DChunkIterPerf.txt | Adds reference file (empty to avoid stdout comparison) for the new perf test. |
| java/src-jni/test/testfiles/JUnit-TestH5D.txt | Updates expected JUnit output to include the newly added TestH5D tests. |
| java/src-jni/test/CMakeLists.txt | Registers TestH5DChunkIterPerf in the JNI Java test build/run list. |
| java/src-jni/jni/h5dImp.h | Declares new JNI entry points for chunk iteration, chunk metadata queries, and direct chunk I/O. |
| java/src-jni/jni/h5dImp.c | Implements new JNI bindings, adds optimized chunk-iter callback handling, and implements bulk chunk iteration accumulation. |
| java/src-jni/jni/h5Constants.c | Exposes additional H5D_CHUNK_IDX_* constants to the JNI constants layer. |
| java/src-jni/hdf/hdf5lib/structs/H5D_chunk_info_t.java | Adds JNI-side H5D_chunk_info_t struct wrapper for bulk chunk-iteration results. |
| java/src-jni/hdf/hdf5lib/HDF5Constants.java | Adds new public H5D_CHUNK_IDX_* constants (JNI tree). |
| java/src-jni/hdf/hdf5lib/H5.java | Adds JNI method declarations and Javadoc for new chunk-level APIs. |
| java/src-jni/hdf/hdf5lib/CMakeLists.txt | Adds new callback interfaces and struct wrapper to the JNI Java build sources. |
| java/src-jni/hdf/hdf5lib/callbacks/H5D_chunk_iter_t.java | Adds JNI callback operator-data marker interface for chunk iteration. |
| java/src-jni/hdf/hdf5lib/callbacks/H5D_chunk_iter_cb.java | Adds JNI callback interface for chunk iteration. |
| java/hdf/hdf5lib/structs/H5D_chunk_info_t.java | Adds FFM-side H5D_chunk_info_t struct wrapper for bulk chunk-iteration results. |
| java/hdf/hdf5lib/HDF5Constants.java | Adds new public H5D_CHUNK_IDX_* constants (FFM tree). |
| java/hdf/hdf5lib/H5.java | Adds FFM wrappers for chunk iteration, bulk iteration, chunk metadata queries, and direct chunk I/O. |
| java/hdf/hdf5lib/CMakeLists.txt | Adds new callback interfaces and struct wrapper to the FFM Java build sources. |
| java/hdf/hdf5lib/callbacks/H5D_chunk_iter_t.java | Adds FFM callback operator-data marker interface for chunk iteration. |
| java/hdf/hdf5lib/callbacks/H5D_chunk_iter_cb.java | Adds FFM callback interface for chunk iteration (upcall stub). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /** | ||
| * Data class for link callback for H5Dchunk_iter. | ||
| * | ||
| */ |
| /** | ||
| * Data class for link callback for H5Dchunk_iter. | ||
| * | ||
| */ |
| /** | ||
| * Information class for link callback for H5Dchunk_iter. | ||
| * | ||
| */ |
| /** | ||
| * Information class for link callback for H5Dchunk_iter. | ||
| * | ||
| */ |
| assertEquals("testH5Dchunk_iter_all: H5Dget_num_chunks agrees", info.getNumChunks(), | ||
| nchunks_by_index); | ||
|
|
||
| for (int i = 0; i < nchunks_by_index; i++) { |
| assertEquals("testH5Dchunk_iter_all: H5Dget_num_chunks agrees", info.getNumChunks(), | ||
| nchunks_by_index); | ||
|
|
||
| for (int i = 0; i < nchunks_by_index; i++) { |
| Time: XXXX | ||
|
|
||
| OK (33 tests) | ||
| OK (40 tests) |
| * invoked from a library-created worker thread). */ | ||
| JNIEnv *cbenv = wrapper->env; | ||
| jobject visit_callback = wrapper->visit_callback; | ||
| void *op_data = (void *)wrapper->op_data; |
- Fix copy/paste Javadoc ("link callback" -> chunk iterator callback)
in both trees' H5D_chunk_iter_t/H5D_chunk_iter_cb interfaces
- Use a long loop counter in TestH5D's by-index chunk-info cross-check
to avoid truncating a chunk count above Integer.MAX_VALUE
- Keep op_data as jobject in H5D_chunk_iter_cb (JNI) instead of
casting it to void* before passing it to CallIntMethod
|
I do not think the one CI failure is related to my changes. |
|
After thinking about |
Summary
Exposes HDF5's chunk-level APIs to both Java binding trees (
java/src-jniJNI andjava/hdfFFM/Panama), working toward full C API parity in the Java bindings:H5Dchunk_iter,H5Dget_num_chunks,H5Dget_chunk_info(by index)H5Dget_chunk_info_by_coord,H5Dget_chunk_storage_size,H5Dget_chunk_index_type,H5Dwrite_chunk,H5Dread_chunk(direct raw chunk I/O, bypassing the filter pipeline/hyperslab), plus the five missingH5D_CHUNK_IDX_*constantsH5Dchunk_iter_all, a bulk convenience form ofH5Dchunk_iterreturning every chunk's offset/filter mask/address/size as a singleH5D_chunk_info_tinstead of requiring a callbackAlso includes a performance fix:
H5D_chunk_iter_cb(JNI) was redundantly doingAttachCurrentThread/GetMethodID/NewLongArrayon every chunk instead of once perH5Dchunk_itercall; resolving these once cut per-chunk callback overhead by ~2.8x at large chunk counts.Notable design notes
H5Dread_chunkguards against a real silent-failure hazard in the underlyingH5D__chunk_direct_read(): if the caller's buffer doesn't match the true on-disk chunk size, the C function still returns success but leaves the buffer untouched. Both implementations check this internally and throwIllegalArgumentExceptionon mismatch.H5Dchunk_iter_all's JNI implementation accumulates chunk info in a callback that is itself pure C (zero JVM crossings per chunk), converting to Java arrays only once at the end — measured ~2x faster than streamingH5Dchunk_iterat ~4000 chunks. The FFM implementation cannot replicate this: FFM's callback is necessarily an upcall stub that crosses back into the JVM on every invocation, soH5Dchunk_iter_allis offered on FFM purely for a simpler call site (no callback to author), not as a performance win — documented in the method's javadoc.TestH5DChunkIterPerf, both trees) comparingH5Dchunk_iter/H5Dchunk_iter_allagainst a by-indexH5Dget_chunk_infoloop confirms the same scaling blowup reported in HDF5.jl#1031: the by-index loop is ~150-1400x slower at ~4000 chunks depending on which chunk-enumeration strategy it's compared against.Test plan
TestH5Dsuite passes in both JNI (40 tests) and FFM (39 tests) builds — the count differs becausetestH5Dvlen_string_buffercarries@Test+@Ignorein the FFM tree (so JUnit reports it as skipped) but only@Ignore(no@Test) in the JNI tree (so it isn't discovered as a test at all); both trees exercise the same 39 active tests plus this one long-pre-existing disabled caseRequest.method), not just as part of the full suiteH5Dchunk_iter_all's bulk result cross-checked against per-chunkH5Dget_chunk_info()results, matched by offset (the two APIs aren't guaranteed to visit chunks in the same order)H5Dread_chunk's buffer-size-mismatch guard exercised directly (expectsIllegalArgumentException)🤖 Generated with Claude Code