using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using HarmonyLib;
using Debug = UnityEngine.Debug;
namespace NecromancerTome
{
///
/// Замер утечки сущностей (указание 09.10: «добавь замер сразу; если не будет тормозить, пригодится для
/// анализа»). Повод - лог 09.10: в строке игры «Ent: 7 (31)» живых сущностей 7, а экземпляров в памяти 31
/// (Entity.InstanceCount уменьшается только в финализаторе), и разрыв рос за сессию.
///
/// Как работает. Каждое удаление сущности из мира проходит через World.unloadEntity (смерть, отзыв,
/// выгрузка чанка, чёрный разрыв NecroBurst). Там удалённая сущность запоминается СЛАБОЙ ссылкой
/// (WeakReference сама не держит объект), с классом, причиной и источником - первым методом мода в стеке
/// вызова (или «игра»). Раз в минуту: если с удаления прошло не меньше двух сборок мусора (GC.CollectionCount),
/// а объект всё ещё жив, - его кто-то держит. В лог идёт строка «EntityLeakProbe» со сводкой по классу и
/// источнику; ничего не держится - строки нет.
///
/// Нагрузка: удаления редки (единицы в минуту), стек снимается только в этот момент; раз в минуту - проход
/// по короткому списку. GC принудительно НЕ вызывается (дал бы рывок). Вышедшие из памяти записи
/// выбрасываются, список не растёт.
///
public static class EntityLeakProbe
{
class Rec
{
public WeakReference Ref;
public string What;
public int Gc;
}
static readonly List _recs = new List();
static readonly Dictionary _sum = new Dictionary();
static float _next;
const float Period = 60f;
public static void OnUnload(Entity _e, EnumRemoveEntityReason _reason)
{
if (_e == null || _e is EntityPlayerLocal)
{
return;
}
string cls = EntityClass.list.TryGetValue(_e.entityClass, out EntityClass ec) && ec != null ? ec.entityClassName : _e.GetType().Name;
_recs.Add(new Rec
{
Ref = new WeakReference(_e),
What = cls + " / " + _reason + " / " + Origin(),
Gc = GC.CollectionCount(0)
});
}
/// Первый метод мода в стеке (кроме этого класса и патчей Harmony) - кто удалил; иначе «игра».
static string Origin()
{
StackTrace st = new StackTrace(2, false);
for (int i = 0; i < st.FrameCount && i < 24; i++)
{
System.Reflection.MethodBase m = st.GetFrame(i)?.GetMethod();
Type t = m?.DeclaringType;
if (t == null || t == typeof(EntityLeakProbe) || t.Namespace != nameof(NecromancerTome) || t.Name.StartsWith("Patch_World_unloadEntity"))
{
continue;
}
return (t.DeclaringType != null ? t.DeclaringType.Name + "." : "") + t.Name + "." + m.Name;
}
return "игра";
}
/// Раз в минуту (из ModEvents.UnityUpdate мода).
public static void Tick()
{
float now = UnityEngine.Time.time;
if (now < _next)
{
return;
}
_next = now + Period;
int gc = GC.CollectionCount(0);
_sum.Clear();
int held = 0;
for (int i = _recs.Count - 1; i >= 0; i--)
{
Rec r = _recs[i];
object o = r.Ref.Target;
if (o == null)
{
_recs.RemoveAt(i);
continue;
}
if (gc - r.Gc < 2)
{
continue; // сборок мусора ещё не было - рано судить
}
held++;
_sum.TryGetValue(r.What, out int n);
_sum[r.What] = n + 1;
}
if (held == 0)
{
return;
}
StringBuilder sb = new StringBuilder();
sb.Append("[NecromancerTome] EntityLeakProbe: удалённых, но живых в памяти ").Append(held)
.Append(" (Entity.InstanceCount ").Append(Entity.InstanceCount).Append(", сборок мусора ").Append(gc).Append("):");
foreach (KeyValuePair kv in _sum)
{
sb.Append("\n ").Append(kv.Value).Append(" x ").Append(kv.Key);
}
Debug.Log(sb.ToString());
}
/// Выход из мира - забыть всё.
public static void Reset()
{
_recs.Clear();
_next = 0f;
}
}
[HarmonyPatch(typeof(World), nameof(World.unloadEntity))]
public static class Patch_World_unloadEntity_LeakProbe
{
public static void Prefix(Entity _e, EnumRemoveEntityReason _reason)
{
try
{
EntityLeakProbe.OnUnload(_e, _reason);
}
catch (Exception)
{
// Замер не должен ломать удаление сущности ни при каких условиях.
}
}
}
}