// Copyright (c) 2023 bradson // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. #if notWorking using System.Linq; using System.Reflection.Emit; using JetBrains.Annotations; namespace PerformanceFish; [UsedImplicitly] public sealed class AllocationProfiling : ClassWithFishPatches { public override bool RequiresLoadedGameForPatching => false; public static bool Active { get => _active; set { return; _active = value; FishPatch.Get().Enabled = value; } } public static FishTable Data = new(); private static bool _active; /// /// The idea here was to patch object..ctor, fetch the type, count calls and get the allocation count for every /// type by doing so, while ignoring structs which are expected to not construct as full object with type. /// This however doesn't work. The IL method bodies of class constructors do contain calls to object..ctor, but /// they appear to only go through for static constructor invocations and literal object construction. Everything /// else somehow skips the call. /// public sealed class ObjectPatch : FishPatch { public override bool DefaultState => true; public override MethodBase? TargetMethodInfo { get; } = MainTabWindow._objectConstructor; // = AccessTools.Constructor(typeof(NotAClass)); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void P0stf1x(object __instance) { CallCount++; ref var typeData = ref Data.GetOrAddReference(__instance.GetType()); typeData.Count++; // if ((typeData.Count & 1023) != 0) // return; typeData.StackTraces.GetOrAddReference(StackTraceUtility.ExtractStackTrace())++; } public static CodeInstructions Transpiler(CodeInstructions instructions) { var codes = instructions.AsOrToList(); Log.Message(string.Join("\n", codes)); var retIndex = codes.FindIndex(static code => code.opcode == OpCodes.Ret); if (retIndex == -1) retIndex = codes.Count - 1; codes.Insert(retIndex, new(OpCodes.Ldarg_0)); codes.Insert(retIndex + 1, new(OpCodes.Call, methodof(P0stf1x))); return codes; } // ReSharper disable once VirtualMemberCallInConstructor //private ObjectPatch() => TryPatch(); public static class NotAClass { } } public static ulong CallCount; public record struct TypeData { public int Count; public FishTable StackTraces = new(); [UsedImplicitly] public TypeData() { } } [UsedImplicitly] public sealed class MainTabWindow : RimWorld.MainTabWindow { public MainTabWindow() { draggable = true; resizeable = true; absorbInputAroundWindow = false; closeOnClickedOutside = false; } private GUIScope.ScrollViewStatus _scrollViewStatus = new(); private Listing_Standard _listing = new(); public override void PreOpen() { Active = true; base.PreOpen(); } public override void PostClose() { base.PostClose(); Active = false; } internal static MethodBase _objectConstructor = (MethodBase)MethodBodyReader.GetInstructions(null, AccessTools.DeclaredConstructor(typeof(PreceptComp))) .First(static code => code.opcode == OpCodes.Call && code.operand is MethodBase method && method.Name.Contains("ctor")) .operand; // = typeof(object).GetMembers(AccessTools.all).OfType() // .First(static member => member.Name.Contains("ctor")); // = AccessTools.Constructor(typeof(ObjectPatch.NotAClass)); public override void DoWindowContents(Rect inRect) { absorbInputAroundWindow = false; closeOnClickedOutside = false; if (GUIHelper.CanSkip) return; Widgets.Label(inRect with { height = Text.LineHeight }, $"Data: {Data}, count: { Data.Count.ToStringCached()}, callCount: {CallCount.ToString()}"); inRect.yMin += Text.LineHeight; var objectPatches = Harmony.GetPatchInfo(_objectConstructor); var patchWidgetLabel = $"Patches on {_objectConstructor.FullDescription()}: pre { objectPatches?.Prefixes?.Select(static patch => patch.PatchMethod.FullDescription()).ToStringSafeEnumerable()}, post: { objectPatches?.Postfixes?.Select(static patch => patch.PatchMethod.FullDescription()).ToStringSafeEnumerable()}, trans: { objectPatches?.Transpilers?.Select(static patch => patch.PatchMethod.FullDescription()).ToStringSafeEnumerable()}, final: { objectPatches?.Finalizers?.Select(static patch => patch.PatchMethod.FullDescription()).ToStringSafeEnumerable()}, Active? { Active.ToString()}, Enabled? {FishPatch.Get().Enabled.ToString()}"; var patchWidgetHeight = Text.CalcHeight(patchWidgetLabel, inRect.width); Widgets.Label(inRect with { height = patchWidgetHeight }, patchWidgetLabel); inRect.yMin += patchWidgetHeight; var objectMembers = typeof(object).GetMembers(AccessTools.all).OfType() .Where(static member => member.Name.Contains("ctor")); var objectMembersLabel = $"object members:\n{string.Join("\n --- ", objectMembers.Select(static member => member.FullDescription()))}"; var objectMembersHeight = Text.CalcHeight(objectMembersLabel, inRect.width); Widgets.Label(inRect with { height = objectMembersHeight }, objectMembersLabel); inRect.yMin += objectMembersHeight; using var scrollView = new GUIScope.ScrollableListingStandard(inRect, _scrollViewStatus, _listing); var listing = _listing; using var keyValuePairsPooled = new PooledArray>(Data); keyValuePairsPooled.Sort(_typeDataComparer); var keyValuePairs = keyValuePairsPooled.Array; var typeCount = keyValuePairsPooled.Length; for (var i = 0; i < typeCount; i++) { if (scrollView.TryCull(Text.LineHeight)) continue; var kvp = keyValuePairs[i]; var labelRect = listing.GetRect(Text.LineHeight); Widgets.Label(labelRect, string.Concat(kvp.Key, " ", kvp.Value.Count.ToStringCached())); if (Mouse.IsOver(labelRect)) { var traces = kvp.Value.StackTraces; using var traceArray = new PooledArray>(traces); traceArray.Sort(_traceComparer); // listing.Label(traceArray.Array[0].Key); listing.Label(traceArray.FirstOrDefault(static kvp => !kvp.Key.Contains("cctor")).Key ?? "NULL"); // TooltipHandler.TipRegion(labelRect, // string.Join("\n", traceArray.Select(static kvp => kvp.Key))); } } } private static IComparer> _typeDataComparer = Comparer>.Create(static (x, y) => x.Value.Count.CompareTo(y.Value.Count)); private static IComparer> _traceComparer = Comparer>.Create(static (x, y) => x.Value.CompareTo(y.Value)); } } #endif