-
Notifications
You must be signed in to change notification settings - Fork 677
Description
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 generatedEvery 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 allocatedObservations
• ~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 overheadMinimal 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,072bytes, matching the reported fixed overhead.
- Plus a per-result copy:
Current = new byte[buffer_len]; Array.Copy(...)