Finding leaks: engine objects and C# memory
Distinguish native object counts, detached entities and managed memory.
Counters answer different questions
| Metric | Meaning |
|---|---|
| ObjectCount | Native objects registered in ObjectDB |
| ObjectNodeCount | Entities inside the active EntityTree; the exact SDK constant contains Node |
| ObjectOrphanNodeCount | Entities outside the active tree, including children of detached hierarchies; debug builds |
| GC.GetTotalMemory(false) | Approximate .NET managed memory rather than whole-process memory |
Read the first three metrics through Performance.GetMonitor with the corresponding Performance.Monitor value.
ObjectOrphanNodeCount does not mean only parentless objects: a detached root's child is outside the tree too. Without DEBUG_ENABLED this monitor returns 0, so zero in such a build does not establish that these objects are absent.
Do not rely on an ObjectDB Profiler tab as the only solution: a snapshot UI of that name is not certified in this distribution. Available counters are not retaining-reference graphs and do not account for every native buffer, GPU allocation or managed array.
Detachment, deletion and managed references
ObjectLifetimeProbe.cs
using System;
using System.Collections.Generic;
using Reckfell;
public static class ObjectLifetimeProbe
{
public static bool Run()
{
var retained = new List<Entity>();
var parent = new Entity();
var child = new Entity();
retained.Add(child);
parent.AddChild(child);
parent.RemoveChild(child);
parent.Free();
bool detachedSurvived = ReckfellObject.IsInstanceValid(child);
child.Free();
bool wrapperOutlivedNative = retained[0] is not null &&
!ReckfellObject.IsInstanceValid(retained[0]);
retained.Clear();
return detachedSurvived && wrapperOutlivedNative;
}
public static void PrintSnapshot()
{
double objects = Performance.GetMonitor(Performance.Monitor.ObjectCount);
double entities = Performance.GetMonitor(Performance.Monitor.ObjectNodeCount);
double orphans = Performance.GetMonitor(Performance.Monitor.ObjectOrphanNodeCount);
long managed = GC.GetTotalMemory(false);
ReckfellApi.Print($"MEMORY objects={objects}; entitiesInTree={entities}; outsideTree={orphans}; managedApproxBytes={managed}");
}
}Run returns true: RemoveChild leaves the child alive, and freeing the parent still leaves that detached child alive. Freeing the child then deletes its native object while List retains its managed wrapper. A non-null reference alone therefore does not establish native validity.
An attached child is freed with its parent. Application logic must arrange cleanup for separately retained detached objects. With QueueFree, wait for deferred deletion before comparing state.
Establish growth before assigning a cause
Repeat one scenario: open a level, create objects, close the level and wait for queued deletion. Compare the same point across several cycles. Initial growth may be caching or initialization; stable object counts do not prove stable internal buffer sizes.
An ordinary new byte[8192] increases managed allocations without adding an ObjectDB object. Track your own created objects when identifying missing deletion; managed retaining paths require a separate .NET memory tool. One ObjectCount value cannot fully diagnose a leak.