CurtHagenlocher opened a new issue, #464:
URL: https://github.com/apache/arrow-dotnet/issues/464

   ### Describe the bug
   
   `CArrowArrayStreamExporter.GetNext` reads a `RecordBatch` from the 
underlying `IArrowArrayStream`, exports it with 
`CArrowArrayExporter.ExportRecordBatch`, and then never disposes it:
   
   ```csharp
   RecordBatch recordBatch = 
arrayStream.ArrowArrayStream.ReadNextRecordBatchAsync().Result;
   if (recordBatch != null)
   {
       CArrowArrayExporter.ExportRecordBatch(recordBatch, cArray);
   }
   ```
   
   For allocator-backed buffers, `ArrowBuffer.TryExport` takes its own 
reference (`_handle.Retain()`), so the exported `ArrowArray` keeps the memory 
alive independently of the batch. When the consumer calls `release`, only the 
exported reference is dropped. The batch's own reference is never released, so 
with the default `NativeMemoryAllocator` the native memory is only freed when 
the `NativeMemoryManager` finalizer eventually runs.
   
   In practice this means every batch exported through the C stream interface 
depends on finalization to free its buffers. Reclamation is driven entirely by 
GC activity in the exporting runtime, which is mostly the `AddMemoryPressure` 
calls in `NativeMemoryAllocator` and `ExportedAllocationOwner`.
   
   This is especially visible in NativeAOT-compiled libraries (for example, 
ADBC drivers). They have their own private GC heap, and nothing collects that 
heap once the library stops allocating.
   
   ### Measurements
   
   I used a NativeAOT-compiled ADBC driver loaded by a C host. The host drains 
a stream of 6 batches × 4M rows twice and releases every array, the stream and 
the statement/connection/database. All numbers are process private bytes; the 
baseline is about 3 MB.
   
   | | as-is | stream disposes each batch after export |
   |---|---|---|
   | after release, no GC | 395 MB | 335 MB |
   | after `GC.Collect(2, Aggressive, true, true)` | 138 MB | 5.8 MB |
   | after Collect + `WaitForPendingFinalizers` + Aggressive | 5.5 MB | — |
   
   The 138 MB left behind in the "as-is" column is native memory that only the 
finalizer queue frees.
   
   ### Suggested fix
   
   Have `GetNext` dispose the batch once it has been exported. Disposing is 
safe for buffers that `TryExport` retains (`_handle != null`).
   
   There is one caveat. Buffers without a shared handle are exported by pinning 
`_memory`, and some `MemoryManager` implementations (e.g. 
`NativeMemoryManager`) throw when disposed while pinned. A complete fix 
therefore needs one of these:
   - make the export path always take an owning reference, or
   - only dispose the batch when every buffer was exported by reference, or
   - move ownership of the batch into the `ExportedAllocationOwner`, so it is 
disposed when the exported array is released.
   
   The third option is probably the cleanest, since it also covers the 
non-shared case.
   
   ### Component(s)
   
   C#
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to