From 22285ba85158becc275fccb0d8715accb90bf036 Mon Sep 17 00:00:00 2001 From: MS-crew <100300664+MS-crew@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:11:52 +0300 Subject: [PATCH 1/7] Update Events.cs Update --- EXILED/Exiled.Events/Config.cs | 18 +++++ EXILED/Exiled.Events/Events.cs | 24 +++++++ EXILED/Exiled.Events/Features/Event{T}.cs | 82 +++++++++++++++++++++++ 3 files changed, 124 insertions(+) diff --git a/EXILED/Exiled.Events/Config.cs b/EXILED/Exiled.Events/Config.cs index fd244f699b..46ef02f141 100644 --- a/EXILED/Exiled.Events/Config.cs +++ b/EXILED/Exiled.Events/Config.cs @@ -110,5 +110,23 @@ public sealed class Config : IConfig /// [Description("Whether to log RA commands.")] public bool LogRaCommands { get; set; } = true; + + /// + /// Gets or sets a value indicating whether the Event Profiler is enabled. + /// + [Description("Indicates whether to enable the event profiler. This detects and logs plugins that cause lag by taking too long to handle events.")] + public bool EnableEventProfiler { get; set; } = false; + + /// + /// Gets or sets the threshold in milliseconds for the Event Profiler. + /// + [Description("The threshold in milliseconds. If a plugin takes longer than this to handle an event, a warning will be logged.")] + public double EventProfilerThreshold { get; set; } = 16.6; + + /// + /// Gets or sets the allocation threshold in bytes. + /// + [Description("If a plugin allocates more memory than this (bytes) in a single event, it will be logged. Default: 16KB")] + public long EventProfilerAllocationThreshold { get; set; } = 16384; } } diff --git a/EXILED/Exiled.Events/Events.cs b/EXILED/Exiled.Events/Events.cs index c941abd7ca..15e7732fa5 100644 --- a/EXILED/Exiled.Events/Events.cs +++ b/EXILED/Exiled.Events/Events.cs @@ -30,6 +30,23 @@ namespace Exiled.Events /// public sealed class Events : Plugin { +#pragma warning disable SA1401 + /// + /// Indicates whether the event profiler is enabled. + /// + internal static bool IsProfilerEnabled; + + /// + /// Execution time threshold (ms) for profiler warnings. + /// + internal static long AllocationThreshold; + + /// + /// Allocation threshold (bytes) for profiler warnings. + /// + internal static double ProfilerThreshold; +#pragma warning restore SA1401 + private static Events instance; /// @@ -49,6 +66,11 @@ public sealed class Events : Plugin public override void OnEnabled() { instance = this; + + IsProfilerEnabled = Config.EnableEventProfiler; + ProfilerThreshold = Config.EventProfilerThreshold; + AllocationThreshold = Config.EventProfilerAllocationThreshold; + base.OnEnabled(); Stopwatch watch = Stopwatch.StartNew(); @@ -104,6 +126,8 @@ public override void OnEnabled() /// public override void OnDisabled() { + IsProfilerEnabled = false; + base.OnDisabled(); Unpatch(); diff --git a/EXILED/Exiled.Events/Features/Event{T}.cs b/EXILED/Exiled.Events/Features/Event{T}.cs index 2d6252b91e..2ba0c6c718 100644 --- a/EXILED/Exiled.Events/Features/Event{T}.cs +++ b/EXILED/Exiled.Events/Features/Event{T}.cs @@ -9,10 +9,13 @@ namespace Exiled.Events.Features { using System; using System.Collections.Generic; + using System.Diagnostics; using System.Linq; + using System.Reflection; using Exiled.API.Features; using Exiled.Events.EventArgs.Interfaces; + using MEC; /// @@ -233,6 +236,15 @@ internal void BlendedInvoke(T arg) for (int i = 0; i < count; i++) { + long startTick = 0; + long startBytes = 0; + + if (Events.IsProfilerEnabled) + { + startTick = Stopwatch.GetTimestamp(); + startBytes = GC.GetTotalMemory(false); + } + if (eventIndex < innerEvent.Length && (asyncEventIndex >= innerAsyncEvent.Length || innerEvent[eventIndex].priority >= innerAsyncEvent[asyncEventIndex].priority)) { try @@ -244,6 +256,17 @@ internal void BlendedInvoke(T arg) Log.Error($"Method \"{innerEvent[eventIndex].handler.Method.Name}\" of the class \"{innerEvent[eventIndex].handler.Method.ReflectedType.FullName}\" caused an exception when handling the event \"{GetType().FullName}\"\n{ex}"); } + if (Events.IsProfilerEnabled) + { + double elapsedMs = (Stopwatch.GetTimestamp() - startTick) * 1000.0 / Stopwatch.Frequency; + long allocatedBytes = GC.GetTotalMemory(false) - startBytes; + + if (elapsedMs > Events.ProfilerThreshold || allocatedBytes > Events.AllocationThreshold) + { + LogWarning(innerEvent[eventIndex].handler, elapsedMs, allocatedBytes); + } + } + eventIndex++; } else @@ -268,6 +291,15 @@ internal void InvokeNormal(T arg) Registration[] innerEvent = this.innerEvent.ToArray(); foreach (Registration registration in innerEvent) { + long startTick = 0; + long startBytes = 0; + + if (Events.IsProfilerEnabled) + { + startTick = Stopwatch.GetTimestamp(); + startBytes = GC.GetTotalMemory(false); + } + try { registration.handler(arg); @@ -276,6 +308,17 @@ internal void InvokeNormal(T arg) { Log.Error($"Method \"{registration.handler.Method.Name}\" of the class \"{registration.handler.Method.ReflectedType.FullName}\" caused an exception when handling the event \"{GetType().FullName}\"\n{ex}"); } + + if (Events.IsProfilerEnabled) + { + double elapsedMs = (Stopwatch.GetTimestamp() - startTick) * 1000.0 / Stopwatch.Frequency; + long allocatedBytes = GC.GetTotalMemory(false) - startBytes; + + if (elapsedMs > Events.ProfilerThreshold || allocatedBytes > Events.AllocationThreshold) + { + LogWarning(registration.handler, elapsedMs, allocatedBytes); + } + } } } @@ -295,5 +338,44 @@ internal void InvokeAsync(T arg) } } } + + private static void LogWarning(Delegate handler, double ms, long bytes) + { + MethodInfo method = handler.Method; + Type targetType = handler.Target?.GetType() ?? method.DeclaringType; + + string pluginName = targetType?.Assembly.GetName().Name; + string className = targetType?.Name; + string eventName = typeof(T).Name.Replace("EventArgs", string.Empty); + + string[] sizes = { "B", "KB", "MB", "GB" }; + int order = 0; + double len = bytes; + while (len >= 1024 && order < sizes.Length - 1) + { + order++; + len /= 1024; + } + + string ramResult = $"{len:0.##} {sizes[order]}"; + + string triggerPrefix = string.Empty; + switch (ms > Events.ProfilerThreshold, bytes > Events.AllocationThreshold) + { + case (true, false): + triggerPrefix = "[CPU]"; + break; + + case (false, true): + triggerPrefix = "[MEMORY]"; + break; + + case (true, true): + triggerPrefix = "[CPU]/[MEMORY]"; + break; + } + + Log.Warn($"[Event Profiler] {triggerPrefix} '{eventName}' | Time: {ms:F2}ms | RAM: {ramResult} | Plugin: {pluginName} | Class: {className} | Method: {method.Name}"); + } } } From 1141b25de4513856e822e82ab603560bcd937ef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?= Date: Wed, 28 Jan 2026 00:11:52 +0300 Subject: [PATCH 2/7] In some cases, the GC is the one who is guilty --- EXILED/Exiled.Events/Features/Event{T}.cs | 46 ++++++++++++++++++----- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/EXILED/Exiled.Events/Features/Event{T}.cs b/EXILED/Exiled.Events/Features/Event{T}.cs index 2ba0c6c718..cbb2a72bc1 100644 --- a/EXILED/Exiled.Events/Features/Event{T}.cs +++ b/EXILED/Exiled.Events/Features/Event{T}.cs @@ -238,11 +238,13 @@ internal void BlendedInvoke(T arg) { long startTick = 0; long startBytes = 0; + int startGcCount = 0; if (Events.IsProfilerEnabled) { startTick = Stopwatch.GetTimestamp(); startBytes = GC.GetTotalMemory(false); + startGcCount = GC.CollectionCount(0); } if (eventIndex < innerEvent.Length && (asyncEventIndex >= innerAsyncEvent.Length || innerEvent[eventIndex].priority >= innerAsyncEvent[asyncEventIndex].priority)) @@ -260,10 +262,11 @@ internal void BlendedInvoke(T arg) { double elapsedMs = (Stopwatch.GetTimestamp() - startTick) * 1000.0 / Stopwatch.Frequency; long allocatedBytes = GC.GetTotalMemory(false) - startBytes; + bool gcRan = GC.CollectionCount(0) > startGcCount; if (elapsedMs > Events.ProfilerThreshold || allocatedBytes > Events.AllocationThreshold) { - LogWarning(innerEvent[eventIndex].handler, elapsedMs, allocatedBytes); + LogWarning(innerEvent[eventIndex].handler, elapsedMs, allocatedBytes, gcRan); } } @@ -293,11 +296,13 @@ internal void InvokeNormal(T arg) { long startTick = 0; long startBytes = 0; + int startGcCount = 0; if (Events.IsProfilerEnabled) { startTick = Stopwatch.GetTimestamp(); startBytes = GC.GetTotalMemory(false); + startGcCount = GC.CollectionCount(0); } try @@ -313,10 +318,11 @@ internal void InvokeNormal(T arg) { double elapsedMs = (Stopwatch.GetTimestamp() - startTick) * 1000.0 / Stopwatch.Frequency; long allocatedBytes = GC.GetTotalMemory(false) - startBytes; + bool gcRan = GC.CollectionCount(0) > startGcCount; if (elapsedMs > Events.ProfilerThreshold || allocatedBytes > Events.AllocationThreshold) { - LogWarning(registration.handler, elapsedMs, allocatedBytes); + LogWarning(registration.handler, elapsedMs, allocatedBytes, gcRan); } } } @@ -339,7 +345,7 @@ internal void InvokeAsync(T arg) } } - private static void LogWarning(Delegate handler, double ms, long bytes) + private static void LogWarning(Delegate handler, double ms, long bytes, bool gcRan) { MethodInfo method = handler.Method; Type targetType = handler.Target?.GetType() ?? method.DeclaringType; @@ -348,6 +354,9 @@ private static void LogWarning(Delegate handler, double ms, long bytes) string className = targetType?.Name; string eventName = typeof(T).Name.Replace("EventArgs", string.Empty); + if (bytes < 0) + bytes = 0; + string[] sizes = { "B", "KB", "MB", "GB" }; int order = 0; double len = bytes; @@ -360,22 +369,39 @@ private static void LogWarning(Delegate handler, double ms, long bytes) string ramResult = $"{len:0.##} {sizes[order]}"; string triggerPrefix = string.Empty; - switch (ms > Events.ProfilerThreshold, bytes > Events.AllocationThreshold) + + switch (gcRan, ms > Events.ProfilerThreshold, bytes > Events.AllocationThreshold) { - case (true, false): - triggerPrefix = "[CPU]"; + case (true, true, true): + triggerPrefix = "[GC] [CPU]/[MEMORY]"; break; - case (false, true): - triggerPrefix = "[MEMORY]"; + case (true, true, false): + triggerPrefix = "[GC] [CPU]"; + break; + + case (true, false, true): + triggerPrefix = "[GC] [MEMORY]"; + break; + + case (true, false, false): + triggerPrefix = "[GC]"; break; - case (true, true): + case (false, true, true): triggerPrefix = "[CPU]/[MEMORY]"; break; + + case (false, true, false): + triggerPrefix = "[CPU]"; + break; + + case (false, false, true): + triggerPrefix = "[MEMORY]"; + break; } Log.Warn($"[Event Profiler] {triggerPrefix} '{eventName}' | Time: {ms:F2}ms | RAM: {ramResult} | Plugin: {pluginName} | Class: {className} | Method: {method.Name}"); } } -} +} \ No newline at end of file From 4bf5cbce6d9eb66ca08805c20f40f13ce33d0512 Mon Sep 17 00:00:00 2001 From: MS-crew <100300664+MS-crew@users.noreply.github.com> Date: Wed, 28 Jan 2026 01:56:52 +0300 Subject: [PATCH 3/7] Update Event{T}.cs --- EXILED/Exiled.Events/Features/Event{T}.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/EXILED/Exiled.Events/Features/Event{T}.cs b/EXILED/Exiled.Events/Features/Event{T}.cs index cbb2a72bc1..f68f352058 100644 --- a/EXILED/Exiled.Events/Features/Event{T}.cs +++ b/EXILED/Exiled.Events/Features/Event{T}.cs @@ -384,10 +384,6 @@ private static void LogWarning(Delegate handler, double ms, long bytes, bool gcR triggerPrefix = "[GC] [MEMORY]"; break; - case (true, false, false): - triggerPrefix = "[GC]"; - break; - case (false, true, true): triggerPrefix = "[CPU]/[MEMORY]"; break; From ebf8a1f28f1bdc96fc9b331a57de4e8d02cbbdb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?= Date: Thu, 29 Jan 2026 19:34:30 +0300 Subject: [PATCH 4/7] Perf update --- EXILED/Exiled.Events/Config.cs | 4 +- EXILED/Exiled.Events/Events.cs | 24 -- EXILED/Exiled.Events/Features/Event{T}.cs | 101 ------- .../Patches/Generic/EventProfiler.cs | 256 ++++++++++++++++++ 4 files changed, 258 insertions(+), 127 deletions(-) create mode 100644 EXILED/Exiled.Events/Patches/Generic/EventProfiler.cs diff --git a/EXILED/Exiled.Events/Config.cs b/EXILED/Exiled.Events/Config.cs index 46ef02f141..79b78e81a7 100644 --- a/EXILED/Exiled.Events/Config.cs +++ b/EXILED/Exiled.Events/Config.cs @@ -115,12 +115,12 @@ public sealed class Config : IConfig /// Gets or sets a value indicating whether the Event Profiler is enabled. /// [Description("Indicates whether to enable the event profiler. This detects and logs plugins that cause lag by taking too long to handle events.")] - public bool EnableEventProfiler { get; set; } = false; + public bool EventProfiler { get; set; } = false; /// /// Gets or sets the threshold in milliseconds for the Event Profiler. /// - [Description("The threshold in milliseconds. If a plugin takes longer than this to handle an event, a warning will be logged.")] + [Description("The threshold in milliseconds. If a plugin takes longer than this to handle an event, a warning will be logged.(For 60 fps 1 frame time is 16.6 ms)")] public double EventProfilerThreshold { get; set; } = 16.6; /// diff --git a/EXILED/Exiled.Events/Events.cs b/EXILED/Exiled.Events/Events.cs index 15e7732fa5..57bb97c6a3 100644 --- a/EXILED/Exiled.Events/Events.cs +++ b/EXILED/Exiled.Events/Events.cs @@ -15,7 +15,6 @@ namespace Exiled.Events using CentralAuth; using Exiled.API.Features.Core.UserSettings; using Exiled.Events.Features; - using HarmonyLib; using InventorySystem.Items.Pickups; using InventorySystem.Items.Usables; using PlayerRoles.Ragdolls; @@ -30,23 +29,6 @@ namespace Exiled.Events /// public sealed class Events : Plugin { -#pragma warning disable SA1401 - /// - /// Indicates whether the event profiler is enabled. - /// - internal static bool IsProfilerEnabled; - - /// - /// Execution time threshold (ms) for profiler warnings. - /// - internal static long AllocationThreshold; - - /// - /// Allocation threshold (bytes) for profiler warnings. - /// - internal static double ProfilerThreshold; -#pragma warning restore SA1401 - private static Events instance; /// @@ -67,10 +49,6 @@ public override void OnEnabled() { instance = this; - IsProfilerEnabled = Config.EnableEventProfiler; - ProfilerThreshold = Config.EventProfilerThreshold; - AllocationThreshold = Config.EventProfilerAllocationThreshold; - base.OnEnabled(); Stopwatch watch = Stopwatch.StartNew(); @@ -126,8 +104,6 @@ public override void OnEnabled() /// public override void OnDisabled() { - IsProfilerEnabled = false; - base.OnDisabled(); Unpatch(); diff --git a/EXILED/Exiled.Events/Features/Event{T}.cs b/EXILED/Exiled.Events/Features/Event{T}.cs index f68f352058..0f489ee4f0 100644 --- a/EXILED/Exiled.Events/Features/Event{T}.cs +++ b/EXILED/Exiled.Events/Features/Event{T}.cs @@ -236,17 +236,6 @@ internal void BlendedInvoke(T arg) for (int i = 0; i < count; i++) { - long startTick = 0; - long startBytes = 0; - int startGcCount = 0; - - if (Events.IsProfilerEnabled) - { - startTick = Stopwatch.GetTimestamp(); - startBytes = GC.GetTotalMemory(false); - startGcCount = GC.CollectionCount(0); - } - if (eventIndex < innerEvent.Length && (asyncEventIndex >= innerAsyncEvent.Length || innerEvent[eventIndex].priority >= innerAsyncEvent[asyncEventIndex].priority)) { try @@ -258,18 +247,6 @@ internal void BlendedInvoke(T arg) Log.Error($"Method \"{innerEvent[eventIndex].handler.Method.Name}\" of the class \"{innerEvent[eventIndex].handler.Method.ReflectedType.FullName}\" caused an exception when handling the event \"{GetType().FullName}\"\n{ex}"); } - if (Events.IsProfilerEnabled) - { - double elapsedMs = (Stopwatch.GetTimestamp() - startTick) * 1000.0 / Stopwatch.Frequency; - long allocatedBytes = GC.GetTotalMemory(false) - startBytes; - bool gcRan = GC.CollectionCount(0) > startGcCount; - - if (elapsedMs > Events.ProfilerThreshold || allocatedBytes > Events.AllocationThreshold) - { - LogWarning(innerEvent[eventIndex].handler, elapsedMs, allocatedBytes, gcRan); - } - } - eventIndex++; } else @@ -294,17 +271,6 @@ internal void InvokeNormal(T arg) Registration[] innerEvent = this.innerEvent.ToArray(); foreach (Registration registration in innerEvent) { - long startTick = 0; - long startBytes = 0; - int startGcCount = 0; - - if (Events.IsProfilerEnabled) - { - startTick = Stopwatch.GetTimestamp(); - startBytes = GC.GetTotalMemory(false); - startGcCount = GC.CollectionCount(0); - } - try { registration.handler(arg); @@ -313,18 +279,6 @@ internal void InvokeNormal(T arg) { Log.Error($"Method \"{registration.handler.Method.Name}\" of the class \"{registration.handler.Method.ReflectedType.FullName}\" caused an exception when handling the event \"{GetType().FullName}\"\n{ex}"); } - - if (Events.IsProfilerEnabled) - { - double elapsedMs = (Stopwatch.GetTimestamp() - startTick) * 1000.0 / Stopwatch.Frequency; - long allocatedBytes = GC.GetTotalMemory(false) - startBytes; - bool gcRan = GC.CollectionCount(0) > startGcCount; - - if (elapsedMs > Events.ProfilerThreshold || allocatedBytes > Events.AllocationThreshold) - { - LogWarning(registration.handler, elapsedMs, allocatedBytes, gcRan); - } - } } } @@ -344,60 +298,5 @@ internal void InvokeAsync(T arg) } } } - - private static void LogWarning(Delegate handler, double ms, long bytes, bool gcRan) - { - MethodInfo method = handler.Method; - Type targetType = handler.Target?.GetType() ?? method.DeclaringType; - - string pluginName = targetType?.Assembly.GetName().Name; - string className = targetType?.Name; - string eventName = typeof(T).Name.Replace("EventArgs", string.Empty); - - if (bytes < 0) - bytes = 0; - - string[] sizes = { "B", "KB", "MB", "GB" }; - int order = 0; - double len = bytes; - while (len >= 1024 && order < sizes.Length - 1) - { - order++; - len /= 1024; - } - - string ramResult = $"{len:0.##} {sizes[order]}"; - - string triggerPrefix = string.Empty; - - switch (gcRan, ms > Events.ProfilerThreshold, bytes > Events.AllocationThreshold) - { - case (true, true, true): - triggerPrefix = "[GC] [CPU]/[MEMORY]"; - break; - - case (true, true, false): - triggerPrefix = "[GC] [CPU]"; - break; - - case (true, false, true): - triggerPrefix = "[GC] [MEMORY]"; - break; - - case (false, true, true): - triggerPrefix = "[CPU]/[MEMORY]"; - break; - - case (false, true, false): - triggerPrefix = "[CPU]"; - break; - - case (false, false, true): - triggerPrefix = "[MEMORY]"; - break; - } - - Log.Warn($"[Event Profiler] {triggerPrefix} '{eventName}' | Time: {ms:F2}ms | RAM: {ramResult} | Plugin: {pluginName} | Class: {className} | Method: {method.Name}"); - } } } \ No newline at end of file diff --git a/EXILED/Exiled.Events/Patches/Generic/EventProfiler.cs b/EXILED/Exiled.Events/Patches/Generic/EventProfiler.cs new file mode 100644 index 0000000000..1325406cc7 --- /dev/null +++ b/EXILED/Exiled.Events/Patches/Generic/EventProfiler.cs @@ -0,0 +1,256 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.Events.Patches.Generic +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Reflection; + using System.Reflection.Emit; + using System.Runtime.InteropServices; + + using Exiled.API.Features; + using Exiled.API.Features.Pools; + using Exiled.Events.Features; + + using HarmonyLib; + + using static HarmonyLib.AccessTools; + + /// + /// Patch for adding profiler to . + /// + [HarmonyPatch] + internal static class EventProfiler + { + private static float profilerThreshold; + + private static long allocationThreshold; + + private static Dictionary handlerPropCache; + + private static bool Prepare() + { + Config config = Exiled.Events.Events.Instance?.Config; + + if (config == null || !config.EventProfiler) + return false; + + handlerPropCache = new Dictionary(); + profilerThreshold = (float)config.EventProfilerThreshold; + allocationThreshold = config.EventProfilerAllocationThreshold; + + return true; + } + + private static IEnumerable TargetMethods() + { + Assembly exiledAssembly = typeof(Exiled.Events.Events).Assembly; + + foreach (Type type in exiledAssembly.GetExportedTypes()) + { + foreach (PropertyInfo property in type.GetProperties(BindingFlags.Static | BindingFlags.Public)) + { + Type currentType = property.PropertyType; + + while (currentType != null && currentType != typeof(object)) + { + // if (currentType == typeof(Event) || (currentType.IsGenericType && currentType.GetGenericTypeDefinition() == typeof(Event<>))) + if (currentType.IsGenericType && currentType.GetGenericTypeDefinition() == typeof(Event<>)) + { + MethodInfo method = property.PropertyType.GetMethod("BlendedInvoke", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy); + + if (method != null) + yield return method; + + break; + } + + currentType = currentType.BaseType; + } + } + } + } + + private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator, MethodBase originalMethod) + { + List newInstructions = ListPool.Pool.Get(instructions); + + bool isGenericEvent = originalMethod.DeclaringType.IsGenericType; + + List