using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using StardewModdingAPI.Framework.Utilities; using PerformanceCounter = StardewModdingAPI.Framework.PerformanceCounter.PerformanceCounter; namespace StardewModdingAPI.Framework.Events { /// An event wrapper which intercepts and logs errors in handler code. /// The event arguments type. internal class ManagedEvent: IPerformanceCounterEvent { /********* ** Fields *********/ /// The underlying event. private event EventHandler Event; /// A human-readable name for the event. private readonly string EventName; /// Writes messages to the log. private readonly IMonitor Monitor; /// The mod registry with which to identify mods. protected readonly ModRegistry ModRegistry; /// The display names for the mods which added each delegate. private readonly IDictionary, IModMetadata> SourceMods = new Dictionary, IModMetadata>(); /// The cached invocation list. private EventHandler[] CachedInvocationList; public IDictionary PerformanceCounters { get; } = new Dictionary(); private readonly Stopwatch Stopwatch = new Stopwatch(); private long EventCallCount = 0; private readonly DateTime StartDateTime = DateTime.Now; public string GetEventName() { return this.EventName; } public double GetGameAverageExecutionTime() { if (this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter.PerformanceCounter gameExecTime)) { return gameExecTime.GetAverage(); } return 0; } public double GetModsAverageExecutionTime() { return this.PerformanceCounters.Where(p => p.Key != Constants.GamePerformanceCounterName).Sum(p => p.Value.GetAverage()); } public double GetAverageExecutionTime() { return this.PerformanceCounters.Sum(p => p.Value.GetAverage()); } /********* ** Public methods *********/ /// Construct an instance. /// A human-readable name for the event. /// Writes messages to the log. /// The mod registry with which to identify mods. public ManagedEvent(string eventName, IMonitor monitor, ModRegistry modRegistry) { this.EventName = eventName; this.Monitor = monitor; this.ModRegistry = modRegistry; } /// Get whether anything is listening to the event. public bool HasListeners() { return this.CachedInvocationList?.Length > 0; } /// Add an event handler. /// The event handler. public void Add(EventHandler handler) { this.Add(handler, this.ModRegistry.GetFromStack()); } /// Add an event handler. /// The event handler. /// The mod which added the event handler. public void Add(EventHandler handler, IModMetadata mod) { this.Event += handler; this.AddTracking(mod, handler, this.Event?.GetInvocationList().Cast>()); } /// Remove an event handler. /// The event handler. public void Remove(EventHandler handler) { this.Event -= handler; this.RemoveTracking(handler, this.Event?.GetInvocationList().Cast>()); } public long GetAverageCallsPerSecond() { long runtimeInSeconds = (long)DateTime.Now.Subtract(this.StartDateTime).TotalSeconds; if (runtimeInSeconds == 0) { return 0; } return this.EventCallCount / runtimeInSeconds; } /// Raise the event and notify all handlers. /// The event arguments to pass. public void Raise(TEventArgs args) { if (this.Event == null) return; this.EventCallCount++; foreach (EventHandler handler in this.CachedInvocationList) { try { var performanceCounterEntry = new PerformanceCounterEntry() { EventTime = DateTime.Now }; this.Stopwatch.Reset(); this.Stopwatch.Start(); handler.Invoke(null, args); this.Stopwatch.Stop(); performanceCounterEntry.Elapsed = this.Stopwatch.Elapsed; string modName = this.GetSourceMod(handler)?.DisplayName ?? Constants.GamePerformanceCounterName; if (!this.PerformanceCounters.ContainsKey(modName)) { this.PerformanceCounters.Add(modName, new PerformanceCounter.PerformanceCounter($"{modName}.{this.EventName}")); } this.PerformanceCounters[modName].Add(performanceCounterEntry); } catch (Exception ex) { this.LogError(handler, ex); } } } /// Raise the event and notify all handlers. /// The event arguments to pass. /// A lambda which returns true if the event should be raised for the given mod. public void RaiseForMods(TEventArgs args, Func match) { if (this.Event == null) return; foreach (EventHandler handler in this.CachedInvocationList) { if (match(this.GetSourceMod(handler))) { try { handler.Invoke(null, args); } catch (Exception ex) { this.LogError(handler, ex); } } } } /********* ** Private methods *********/ /// Track an event handler. /// The mod which added the handler. /// The event handler. /// The updated event invocation list. protected void AddTracking(IModMetadata mod, EventHandler handler, IEnumerable> invocationList) { this.SourceMods[handler] = mod; this.CachedInvocationList = invocationList?.ToArray() ?? new EventHandler[0]; } /// Remove tracking for an event handler. /// The event handler. /// The updated event invocation list. protected void RemoveTracking(EventHandler handler, IEnumerable> invocationList) { this.CachedInvocationList = invocationList?.ToArray() ?? new EventHandler[0]; if (!this.CachedInvocationList.Contains(handler)) // don't remove if there's still a reference to the removed handler (e.g. it was added twice and removed once) this.SourceMods.Remove(handler); } /// Get the mod which registered the given event handler, if available. /// The event handler. protected IModMetadata GetSourceMod(EventHandler handler) { return this.SourceMods.TryGetValue(handler, out IModMetadata mod) ? mod : null; } /// Log an exception from an event handler. /// The event handler instance. /// The exception that was raised. protected void LogError(EventHandler handler, Exception ex) { IModMetadata mod = this.GetSourceMod(handler); if (mod != null) mod.LogAsMod($"This mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error); else this.Monitor.Log($"A mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error); } } }