Поиск утечек: объекты движка и память C#
Различайте количество нативных объектов, отсоединённые Entity и управляемую память.
Счётчики отвечают на разные вопросы
| Показатель | Что измеряет |
|---|---|
| ObjectCount | Число нативных объектов, зарегистрированных в ObjectDB |
| ObjectNodeCount | Entity внутри активного EntityTree; точное имя константы SDK содержит Node |
| ObjectOrphanNodeCount | Entity вне активного дерева, включая детей отсоединённой иерархии; диагностическая сборка |
| GC.GetTotalMemory(false) | Приблизительный объём managed-памяти .NET, не память всего процесса |
Первые три показателя читайте через Performance.GetMonitor с соответствующим значением Performance.Monitor.
ObjectOrphanNodeCount не означает только отсутствие родителя: ребёнок у отсоединённого корня тоже находится вне дерева. В сборке без DEBUG_ENABLED этот монитор возвращает 0, поэтому ноль там не доказывает отсутствие таких объектов.
Не ищите решение только во вкладке ObjectDB Profiler: наличие такого snapshot-интерфейса в поставке здесь не подтверждено. Доступные счётчики не являются графом удерживающих ссылок и не показывают все нативные буферы, GPU-память или managed-массивы.
Отсоединение, удаление и C#-ссылка
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 возвращает true: RemoveChild оставляет ребёнка живым, после Free родителя он всё ещё существует. Затем Free ребёнка уничтожает нативный объект, хотя List ещё хранит C#-обёртку. Поэтому не проверяйте жизнь нативного объекта только через ссылку != null.
Если ребёнок остаётся прикреплённым, Free родителя освобождает его вместе с родителем. Отдельно удерживаемые отсоединённые объекты должен освобождать их хозяин в логике приложения. При QueueFree дождитесь выполнения отложенного удаления, прежде чем сравнивать состояние.
Подтвердить рост, а не угадать причину
Повторяйте один сценарий: открыть уровень, создать объекты, закрыть уровень и дождаться отложенных удалений. Сравните одинаковую точку нескольких циклов. Первый рост может быть загрузкой кэша или инициализацией, а стабильное количество объектов не доказывает отсутствие роста их внутренних буферов.
Обычный new byte[8192] увеличивает managed-выделения, но не добавляет объект в ObjectDB. Сохраняйте список собственных созданных объектов, если нужно установить, кто не был удалён; для цепочек удержания managed-объектов нужен отдельный .NET memory-инструмент. Одна цифра ObjectCount не даёт полного диагноза утечки.