Skip to content

Commit 6fea15f

Browse files
authored
[C#] Update RawTableIterBase.Enumerator to use ArrayPool for buffer (#4385)
This is the implementation of a fix for #4093 # Description of Changes * Updated `RawTableIterBase.Enumerator` to rent its scratch buffer from `ArrayPool<byte>.Shared` (with dynamic re-rent on `BUFFER_TOO_SMALL`) and return it on dispose, so iterating rows no longer allocates a fresh `byte[]` per step. * The enumerator still exposes the row bytes via `ArraySegment<byte> Current`, but now the underlying storage is reused across iterations rather than recreated each time. Testing results from `master`: ``` allocated_bytes=14000 elapsed_ticks=1521829 sum=1249975000 row_count=50000 Find() TinyRecord (8 bytes payload) -> 132208 bytes allocated Find() MediumRecord (~50 bytes payload) -> 132440 bytes allocated Find() LargeRecord (1 KB payload) -> 134528 bytes allocated Find() LargeRecord (10 KB payload) -> 152408 bytes allocated Find() LargeRecord (100 KB payload) -> 336728 bytes allocated Iter() 10 rows -> 131896 bytes allocated Filter() iterate 20 rows -> 133288 bytes allocated Filter() iterate 100 rows -> 135976 bytes allocated 10x consecutive Find() (TinyRecord) -> 1319120 bytes allocated ``` Testing results with this fix: ``` allocated_bytes=14000 elapsed_ticks=1504949 sum=1249975000 row_count=50000 Find() TinyRecord (8 bytes payload) -> 1096 bytes allocated Find() MediumRecord (~50 bytes payload) -> 1280 bytes allocated Find() LargeRecord (1 KB payload) -> 4464 bytes allocated Find() LargeRecord (10 KB payload) -> 27464 bytes allocated Find() LargeRecord (100 KB payload) -> 234312 bytes allocated Iter() 10 rows -> 680 bytes allocated Filter() iterate 20 rows -> 1872 bytes allocated Filter() iterate 100 rows -> 3280 bytes allocated 10x consecutive Find() (TinyRecord) -> 8000 bytes allocated ``` # API and ABI breaking changes No API or ABI changes # Expected complexity level and risk 2 - low/moderate: Touches the hot-path iterator that every `Iter`/`Find`/`Filter` call uses # Testing - [X] Compiled CLI and ran regression tests locally - [X] Verified iterator-based harness runs (client + module reducers) on both `master` and this branch, confirming allocations drop from ~131 KB per read to payload-scaled values. - [X] Ensured no regressions in standard harness sanity checks (`row_count=50000`, `sum=1249975000`).
1 parent 5fcd934 commit 6fea15f

1 file changed

Lines changed: 26 additions & 7 deletions

File tree

  • crates/bindings-csharp/Runtime/Internal

crates/bindings-csharp/Runtime/Internal/ITable.cs

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
namespace SpacetimeDB.Internal;
22

3+
using System.Buffers;
34
using SpacetimeDB.BSATN;
45

56
internal abstract class RawTableIterBase<T>
67
where T : IStructuralReadWrite, new()
78
{
89
public sealed class Enumerator(FFI.RowIter handle) : IDisposable
910
{
10-
byte[] buffer = new byte[0x20_000];
11-
public byte[] Current { get; private set; } = [];
11+
private const int InitialBufferSize = 1024;
12+
private byte[]? buffer = ArrayPool<byte>.Shared.Rent(InitialBufferSize);
13+
public ArraySegment<byte> Current { get; private set; } = ArraySegment<byte>.Empty;
1214

1315
public bool MoveNext()
1416
{
@@ -17,6 +19,11 @@ public bool MoveNext()
1719
return false;
1820
}
1921

22+
if (buffer is null)
23+
{
24+
return false;
25+
}
26+
2027
uint buffer_len;
2128
while (true)
2229
{
@@ -38,11 +45,10 @@ public bool MoveNext()
3845
{
3946
// Iterator advanced and may also be `EXHAUSTED`.
4047
// When `OK`, we'll need to advance the iterator in the next call to `MoveNext`.
41-
// In both cases, copy over the row data to `Current` from the scratch `buffer`.
48+
// In both cases, update `Current` to point at the valid range in the scratch `buffer`.
4249
case Errno.EXHAUSTED
4350
or Errno.OK:
44-
Current = new byte[buffer_len];
45-
Array.Copy(buffer, 0, Current, 0, buffer_len);
51+
Current = new ArraySegment<byte>(buffer, 0, (int)buffer_len);
4652
return buffer_len != 0;
4753
// Couldn't find the iterator, error!
4854
case Errno.NO_SUCH_ITER:
@@ -51,7 +57,8 @@ public bool MoveNext()
5157
// Grow `buffer` and try again.
5258
// The `buffer_len` will have been updated with the necessary size.
5359
case Errno.BUFFER_TOO_SMALL:
54-
buffer = new byte[buffer_len];
60+
ArrayPool<byte>.Shared.Return(buffer);
61+
buffer = ArrayPool<byte>.Shared.Rent((int)buffer_len);
5562
continue;
5663
default:
5764
throw new UnknownException(ret);
@@ -66,6 +73,12 @@ public void Dispose()
6673
FFI.row_iter_bsatn_close(handle);
6774
handle = FFI.RowIter.INVALID;
6875
}
76+
77+
if (buffer is not null)
78+
{
79+
ArrayPool<byte>.Shared.Return(buffer);
80+
buffer = null;
81+
}
6982
}
7083

7184
public void Reset()
@@ -87,7 +100,13 @@ public IEnumerable<T> Parse()
87100
{
88101
foreach (var chunk in this)
89102
{
90-
using var stream = new MemoryStream(chunk);
103+
using var stream = new MemoryStream(
104+
chunk.Array!,
105+
chunk.Offset,
106+
chunk.Count,
107+
writable: false,
108+
publiclyVisible: true
109+
);
91110
using var reader = new BinaryReader(stream);
92111
while (stream.Position < stream.Length)
93112
{

0 commit comments

Comments
 (0)