Repository navigation
fix(transport): route NativeDirectByteBufAllocator initial allocation through native allocator - #53
Conversation
… through native allocator
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #53 +/- ##
============================================
- Coverage 86.78% 86.73% -0.05%
- Complexity 616 617 +1
============================================
Files 60 60
Lines 16912 16944 +32
Branches 490 494 +4
============================================
+ Hits 14677 14697 +20
- Misses 1911 1923 +12
Partials 324 324
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. |
Merging this PR will degrade performance by 10.66%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ❌ | Simulation | retain-drop[1048576] |
300.8 ns | 354.9 ns | -15.26% |
| ❌ | Simulation | retain-drop[4096] |
300.8 ns | 354.9 ns | -15.26% |
| ❌ | Simulation | retain-drop[64] |
300.8 ns | 354.9 ns | -15.26% |
| ❌ | Simulation | retain-drop[65536] |
300.8 ns | 354.9 ns | -15.26% |
| ⚡ | Simulation | overwrite[64] |
575.8 ns | 521.6 ns | +10.38% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing detail/bug-fix/fix-transport-route-nativedirectbytebufallocator-i-6db819 (086e3e2) with main (da8dbb6)
Footnotes
-
65 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩
Detail bug report: View on Detail
Change
Closes #48
NativeDirectByteBufAllocatorbuilt its direct buffers via Netty'sUnpooledUnsafeDirectByteBuf/UnpooledDirectByteBuf(alloc, int, int)constructors and only overrode the one-argallocateDirectBuffer(int)hook. In Netty 4.2.18.Final that hook runs only on thecapacity(int)growth path; the constructor path resolves through the package-private two-argallocateDirectBuffer(int, boolean), a staticPlatformDependent.allocateDirect. So every freshly allocated direct buffer's bytes came from Netty/JDK direct memory, not Bemo's native allocator — the dominant, never-grown request/response case was allocated and freed entirely inside Netty's machinery, never appeared innativeAllocatedBytes()/bemo_alloc_live, and was not freed via sizednativeFree. Introduced in d7462d6; it went undetected because the contract test only asserted native backing after growth.Fix (JVM/Netty layer only; the Rust ABI is unchanged): construct each buffer over an already-allocated native
ByteBuffer— the pattern the siblingNativeByteBufalready uses. Pre-allocate the initialAllocationviaallocate(initialCapacity), hand itsbuffer()to theUnpooledDirectByteBuf(alloc, ByteBuffer, maxCapacity)constructor (which treats it as borrowed and does no Netty allocation), keep the one-arg override as the growth hook, and free the borrowed initialCleanableDirectBufferexactly once incapacity(int)(after growth) ordeallocate()(release), whichever happens first. Applied symmetrically toUnsafeBufferandSafeBuffer;Allocation.clean()'sAtomicLongkeeps the free-once property under cross-thread release.NativeDirectAllocatorTestgains regression assertions that a freshdirectBuffer(N)and never-grown buffers are native-backed before any growth.Validation
Linux, JDK 25 (targeting
--release 22), Netty 4.2.18.Final, native libtarget/debug/libbemo_ffi.so.make build-jvm,-Xlint:all -Werror), static analysis (tools/check_java.py: Error Prone + NullAway JSpecify, Checker Framework Regex/Format), and google-java-format all pass.make test-jvm— all 20 FFM/JVM contracts pass, includingNativeDirectAllocatorTestand the siblingNativeByteBufTest. The strengthenedNativeDirectAllocatorTestnow asserts a freshdirectBuffer(N)and 256 never-growndirectBuffer(1024)are native-backed vianativeAllocatedBytes()before any growth, alongside the existing growth/copy/composite/cross-thread-release checks.AssertionError: fresh direct buffer is not backed by the Bemo native allocator; restored, it passes.make test-native-image— all 3 native-image contracts pass (CapiContract,CapiTlsChannelTest,ReusableGzipTest), exercising the full transport suite through the static C API binding.SafeBufferand forcedUnsafeBufferpaths): allocate-then-release without growth, grow-then-release, double-grow-then-release, zero-capacity→grow→release, and a 512-buffer cross-thread release all produce exactnativeAllocatedBytes()deltas with no leak or double-free.Both FFM and Native Image paths verified; Rust ABI and version handling unchanged. (This sandbox blocks Java's
posix_spawn, soopenssl/gccchild processes use the JDK's FORK launch mechanism;make test-native-imagealso neededzlib1g-devforld -lz. Spawn-mechanism only — the binaries run fine and test outcomes are unaffected.)Automatic Fixes PRs can be configured here.