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) { // Замер не должен ломать удаление сущности ни при каких условиях. } } } }