Skip to content

C# SDK allocates ~131KB per database read operation regardless of payload size #4093

@rekhoff

Description

@rekhoff

Discord user tfritzy reported the following:

I looked into this because my server was stuttering every 10 seconds for 500-1000ms, which I was not able to fix after extensively tracking down garbage created by my code. I eventually did find a solution in manually invoking garbage collection in each tick. I've included a repro project as a zip.

⚠️ All below is ai generated

Every database read operation (Find(), Filter() iteration, Iter() iteration) allocates approximately 131KB of memory regardless of the actual payload size. This fixed overhead makes high-frequency game loops impractical due to excessive GC pressure.

Test Results

Read operations (the problem):
Find() TinyRecord (8 bytes payload)     → 131,816 bytes allocated
Find() MediumRecord (~50 bytes payload) → 131,928 bytes allocated
Find() LargeRecord (1 KB payload)       → 133,872 bytes allocated
Find() LargeRecord (10 KB payload)      → 152,304 bytes allocated
Find() LargeRecord (100 KB payload)     → 336,624 bytes allocated
Iter() 10 rows (80 bytes total)         → 131,632 bytes allocated
Filter() iterate 20 rows               → 133,728 bytes allocated
Filter() iterate 100 rows              → 144,688 bytes allocated
10x consecutive Find() (80 bytes)       → 1,318,160 bytes allocated

Observations
• ~131KB fixed base overhead on every read, independent of payload
• Payload size adds on top: total ≈ 131KB + payload_size
• Filter() and Iter() calls are cheap (72-160 bytes), but iterating results triggers the ~131KB allocation
• The overhead does not amortize—10 Find() calls = 10× the overhead

Minimal repro:

[Reducer]
public static void MeasureAllocations(ReducerContext ctx)
{
    long before = GC.GetAllocatedBytesForCurrentThread();
    var tiny = ctx.Db.TinyRecord.Id.Find(1); // 8 byte payload
    long after = GC.GetAllocatedBytesForCurrentThread();
    Log.Info($"TinyRecord Find: {after - before} bytes allocated");
    // Output: TinyRecord Find: 131816 bytes allocated
}

This is a valid issue, and comes from the RawTableIterBase.Enumerator in crates/bindings-csharp/Runtime/Internal/ITable.cs
This is the enumerator used by all DB reads that go through iterators (which includes unique-index Find(), Filter() enumeration, and Iter() enumeration)
RawTableIterBase.Enumerator has:

  • A default scratch buffer: byte[] buffer = new byte[0x20_000];
    • 0x20_000 == 131,072 bytes, matching the reported fixed overhead.
  • Plus a per-result copy:
    • Current = new byte[buffer_len]; Array.Copy(...)

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions