summaryrefslogtreecommitdiff
path: root/src/SMAPI
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI')
-rw-r--r--src/SMAPI/Constants.cs2
-rw-r--r--src/SMAPI/Framework/Events/EventManager.cs5
-rw-r--r--src/SMAPI/Framework/Events/IManagedEvent.cs7
-rw-r--r--src/SMAPI/Framework/Events/ManagedEvent.cs103
-rw-r--r--src/SMAPI/Framework/PerformanceCounter/AlertContext.cs14
-rw-r--r--src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs20
-rw-r--r--src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCategory.cs16
-rw-r--r--src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCollection.cs11
-rw-r--r--src/SMAPI/Framework/PerformanceCounter/IPerformanceCounterEvent.cs1
-rw-r--r--src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs18
-rw-r--r--src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs144
-rw-r--r--src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs233
-rw-r--r--src/SMAPI/Framework/SCore.cs193
-rw-r--r--src/SMAPI/Framework/SGame.cs10
-rw-r--r--src/SMAPI/Program.cs2
15 files changed, 429 insertions, 350 deletions
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs
index 0923494c..76cb6f89 100644
--- a/src/SMAPI/Constants.cs
+++ b/src/SMAPI/Constants.cs
@@ -56,7 +56,7 @@ namespace StardewModdingAPI
internal const string HomePageUrl = "https://smapi.io";
/// <summary>The URL of the SMAPI home page.</summary>
- internal const string GamePerformanceCounterName = "-internal-";
+ internal const string GamePerformanceCounterName = "<StardewValley>";
/// <summary>The absolute path to the folder containing SMAPI's internal files.</summary>
internal static readonly string InternalFilesPath = Program.DllSearchPath;
diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs
index 892cbc7b..9c65a6cc 100644
--- a/src/SMAPI/Framework/Events/EventManager.cs
+++ b/src/SMAPI/Framework/Events/EventManager.cs
@@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using StardewModdingAPI.Events;
+using StardewModdingAPI.Framework.PerformanceCounter;
namespace StardewModdingAPI.Framework.Events
{
@@ -173,10 +174,10 @@ namespace StardewModdingAPI.Framework.Events
/// <summary>Construct an instance.</summary>
/// <param name="monitor">Writes messages to the log.</param>
/// <param name="modRegistry">The mod registry with which to identify mods.</param>
- public EventManager(IMonitor monitor, ModRegistry modRegistry)
+ public EventManager(IMonitor monitor, ModRegistry modRegistry, PerformanceCounterManager performanceCounterManager)
{
// create shortcut initializers
- ManagedEvent<TEventArgs> ManageEventOf<TEventArgs>(string typeName, string eventName) => new ManagedEvent<TEventArgs>($"{typeName}.{eventName}", monitor, modRegistry);
+ ManagedEvent<TEventArgs> ManageEventOf<TEventArgs>(string typeName, string eventName) => new ManagedEvent<TEventArgs>($"{typeName}.{eventName}", monitor, modRegistry, performanceCounterManager);
// init events (new)
this.MenuChanged = ManageEventOf<MenuChangedEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.MenuChanged));
diff --git a/src/SMAPI/Framework/Events/IManagedEvent.cs b/src/SMAPI/Framework/Events/IManagedEvent.cs
new file mode 100644
index 00000000..04476866
--- /dev/null
+++ b/src/SMAPI/Framework/Events/IManagedEvent.cs
@@ -0,0 +1,7 @@
+namespace StardewModdingAPI.Framework.Events
+{
+ internal interface IManagedEvent
+ {
+ string GetName();
+ }
+}
diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs
index bb915738..bba94c35 100644
--- a/src/SMAPI/Framework/Events/ManagedEvent.cs
+++ b/src/SMAPI/Framework/Events/ManagedEvent.cs
@@ -1,15 +1,13 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.Linq;
-using StardewModdingAPI.Framework.Utilities;
-using PerformanceCounter = StardewModdingAPI.Framework.PerformanceCounter.PerformanceCounter;
+using PerformanceCounterManager = StardewModdingAPI.Framework.PerformanceCounter.PerformanceCounterManager;
namespace StardewModdingAPI.Framework.Events
{
/// <summary>An event wrapper which intercepts and logs errors in handler code.</summary>
/// <typeparam name="TEventArgs">The event arguments type.</typeparam>
- internal class ManagedEvent<TEventArgs>: IPerformanceCounterEvent
+ internal class ManagedEvent<TEventArgs>: IManagedEvent
{
/*********
** Fields
@@ -32,38 +30,8 @@ namespace StardewModdingAPI.Framework.Events
/// <summary>The cached invocation list.</summary>
private EventHandler<TEventArgs>[] CachedInvocationList;
- public IDictionary<string, PerformanceCounter.PerformanceCounter> PerformanceCounters { get; } = new Dictionary<string, PerformanceCounter.PerformanceCounter>();
-
- 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());
- }
+ /// <summary>The performance counter manager.</summary>
+ private readonly PerformanceCounterManager PerformanceCounterManager;
/*********
** Public methods
@@ -72,11 +40,18 @@ namespace StardewModdingAPI.Framework.Events
/// <param name="eventName">A human-readable name for the event.</param>
/// <param name="monitor">Writes messages to the log.</param>
/// <param name="modRegistry">The mod registry with which to identify mods.</param>
- public ManagedEvent(string eventName, IMonitor monitor, ModRegistry modRegistry)
+ public ManagedEvent(string eventName, IMonitor monitor, ModRegistry modRegistry, PerformanceCounterManager performanceCounterManager)
{
this.EventName = eventName;
this.Monitor = monitor;
this.ModRegistry = modRegistry;
+ this.PerformanceCounterManager = performanceCounterManager;
+ }
+
+ /// <summary>Gets the event name.</summary>
+ public string GetName()
+ {
+ return this.EventName;
}
/// <summary>Get whether anything is listening to the event.</summary>
@@ -99,8 +74,6 @@ namespace StardewModdingAPI.Framework.Events
{
this.Event += handler;
this.AddTracking(mod, handler, this.Event?.GetInvocationList().Cast<EventHandler<TEventArgs>>());
-
-
}
/// <summary>Remove an event handler.</summary>
@@ -111,18 +84,6 @@ namespace StardewModdingAPI.Framework.Events
this.RemoveTracking(handler, this.Event?.GetInvocationList().Cast<EventHandler<TEventArgs>>());
}
- public long GetAverageCallsPerSecond()
- {
- long runtimeInSeconds = (long)DateTime.Now.Subtract(this.StartDateTime).TotalSeconds;
-
- if (runtimeInSeconds == 0)
- {
- return 0;
- }
-
- return this.EventCallCount / runtimeInSeconds;
- }
-
/// <summary>Raise the event and notify all handlers.</summary>
/// <param name="args">The event arguments to pass.</param>
public void Raise(TEventArgs args)
@@ -130,37 +91,23 @@ namespace StardewModdingAPI.Framework.Events
if (this.Event == null)
return;
- this.EventCallCount++;
+
+ this.PerformanceCounterManager.BeginTrackInvocation(this.EventName);
foreach (EventHandler<TEventArgs> 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);
-
+ this.PerformanceCounterManager.Track(this.EventName, this.GetModNameForPerformanceCounters(handler),
+ () => handler.Invoke(null, args));
}
catch (Exception ex)
{
this.LogError(handler, ex);
}
}
+
+ this.PerformanceCounterManager.EndTrackInvocation(this.EventName);
}
/// <summary>Raise the event and notify all handlers.</summary>
@@ -191,6 +138,20 @@ namespace StardewModdingAPI.Framework.Events
/*********
** Private methods
*********/
+
+ private string GetModNameForPerformanceCounters(EventHandler<TEventArgs> handler)
+ {
+ IModMetadata mod = this.GetSourceMod(handler);
+
+ if (mod == null)
+ {
+ return Constants.GamePerformanceCounterName;
+ }
+
+ return mod.HasManifest() ? mod.Manifest.UniqueID : mod.DisplayName;
+
+ }
+
/// <summary>Track an event handler.</summary>
/// <param name="mod">The mod which added the handler.</param>
/// <param name="handler">The event handler.</param>
diff --git a/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs b/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs
new file mode 100644
index 00000000..c4a57a49
--- /dev/null
+++ b/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs
@@ -0,0 +1,14 @@
+namespace StardewModdingAPI.Framework.PerformanceCounter
+{
+ public struct AlertContext
+ {
+ public string Source;
+ public double Elapsed;
+
+ public AlertContext(string source, double elapsed)
+ {
+ this.Source = source;
+ this.Elapsed = elapsed;
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs b/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs
new file mode 100644
index 00000000..284af1ce
--- /dev/null
+++ b/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+
+namespace StardewModdingAPI.Framework.PerformanceCounter
+{
+ internal struct AlertEntry
+ {
+ public PerformanceCounterCollection Collection;
+ public double ExecutionTimeMilliseconds;
+ public double Threshold;
+ public List<AlertContext> Context;
+
+ public AlertEntry(PerformanceCounterCollection collection, double executionTimeMilliseconds, double threshold, List<AlertContext> context)
+ {
+ this.Collection = collection;
+ this.ExecutionTimeMilliseconds = executionTimeMilliseconds;
+ this.Threshold = threshold;
+ this.Context = context;
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCategory.cs b/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCategory.cs
deleted file mode 100644
index 14f74317..00000000
--- a/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCategory.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-namespace StardewModdingAPI.Framework.Utilities
-{
- public class EventPerformanceCounterCategory
- {
- public IPerformanceCounterEvent Event { get; }
- public double MonitorThreshold { get; }
- public bool IsImportant { get; }
- public bool Monitor { get; }
-
- public EventPerformanceCounterCategory(IPerformanceCounterEvent @event, bool isImportant)
- {
- this.Event = @event;
- this.IsImportant = isImportant;
- }
- }
-}
diff --git a/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCollection.cs b/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCollection.cs
new file mode 100644
index 00000000..1aec28f3
--- /dev/null
+++ b/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCollection.cs
@@ -0,0 +1,11 @@
+using StardewModdingAPI.Framework.Events;
+
+namespace StardewModdingAPI.Framework.PerformanceCounter
+{
+ internal class EventPerformanceCounterCollection: PerformanceCounterCollection
+ {
+ public EventPerformanceCounterCollection(PerformanceCounterManager manager, IManagedEvent @event, bool isImportant) : base(manager, @event.GetName(), isImportant)
+ {
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/PerformanceCounter/IPerformanceCounterEvent.cs b/src/SMAPI/Framework/PerformanceCounter/IPerformanceCounterEvent.cs
index 6b83586d..1bcf4fa0 100644
--- a/src/SMAPI/Framework/PerformanceCounter/IPerformanceCounterEvent.cs
+++ b/src/SMAPI/Framework/PerformanceCounter/IPerformanceCounterEvent.cs
@@ -7,7 +7,6 @@ namespace StardewModdingAPI.Framework.Utilities
{
string GetEventName();
long GetAverageCallsPerSecond();
- IDictionary<string, PerformanceCounter.PerformanceCounter> PerformanceCounters { get; }
double GetGameAverageExecutionTime();
double GetModsAverageExecutionTime();
diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs
index 0b0275b7..3dbc693a 100644
--- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs
+++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs
@@ -6,22 +6,25 @@ using StardewModdingAPI.Framework.Utilities;
namespace StardewModdingAPI.Framework.PerformanceCounter
{
- public class PerformanceCounter
+ internal class PerformanceCounter
{
private const int MAX_ENTRIES = 16384;
- public string Name { get; }
+ public string Source { get; }
public static Stopwatch Stopwatch = new Stopwatch();
public static long TotalNumEventsLogged;
-
+ public double MonitorThresholdMilliseconds { get; set; }
+ public bool Monitor { get; set; }
+ private readonly PerformanceCounterCollection ParentCollection;
private readonly CircularBuffer<PerformanceCounterEntry> _counter;
private PerformanceCounterEntry? PeakPerformanceCounterEntry;
- public PerformanceCounter(string name)
+ public PerformanceCounter(PerformanceCounterCollection parentCollection, string source)
{
- this.Name = name;
+ this.ParentCollection = parentCollection;
+ this.Source = source;
this._counter = new CircularBuffer<PerformanceCounterEntry>(PerformanceCounter.MAX_ENTRIES);
}
@@ -47,6 +50,11 @@ namespace StardewModdingAPI.Framework.PerformanceCounter
PerformanceCounter.Stopwatch.Start();
this._counter.Put(entry);
+ if (this.Monitor && entry.Elapsed.TotalMilliseconds > this.MonitorThresholdMilliseconds)
+ {
+ this.ParentCollection.AddAlert(entry.Elapsed.TotalMilliseconds, this.MonitorThresholdMilliseconds, new AlertContext(this.Source, entry.Elapsed.TotalMilliseconds));
+ }
+
if (this.PeakPerformanceCounterEntry == null)
{
this.PeakPerformanceCounterEntry = entry;
diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs
new file mode 100644
index 00000000..343fddf6
--- /dev/null
+++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs
@@ -0,0 +1,144 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using StardewModdingAPI.Framework.Utilities;
+
+namespace StardewModdingAPI.Framework.PerformanceCounter
+{
+ internal class PerformanceCounterCollection
+ {
+ public IDictionary<string, PerformanceCounter> PerformanceCounters { get; } = new Dictionary<string, PerformanceCounter>();
+ private DateTime StartDateTime = DateTime.Now;
+ private long CallCount;
+ public string Name { get; private set; }
+ public bool IsImportant { get; set; }
+ private readonly Stopwatch Stopwatch = new Stopwatch();
+ private readonly PerformanceCounterManager PerformanceCounterManager;
+ public double MonitorThresholdMilliseconds { get; set; }
+ public bool Monitor { get; set; }
+ private readonly List<AlertContext> TriggeredPerformanceCounters = new List<AlertContext>();
+
+ public PerformanceCounterCollection(PerformanceCounterManager performanceCounterManager, string name, bool isImportant)
+ {
+ this.Name = name;
+ this.PerformanceCounterManager = performanceCounterManager;
+ this.IsImportant = isImportant;
+ }
+
+ public PerformanceCounterCollection(PerformanceCounterManager performanceCounterManager, string name)
+ {
+ this.PerformanceCounterManager = performanceCounterManager;
+ this.Name = name;
+ }
+
+ public void Track(string source, PerformanceCounterEntry entry)
+ {
+ if (!this.PerformanceCounters.ContainsKey(source))
+ {
+ this.PerformanceCounters.Add(source, new PerformanceCounter(this, source));
+ }
+ this.PerformanceCounters[source].Add(entry);
+
+ if (this.Monitor)
+ {
+ this.TriggeredPerformanceCounters.Add(new AlertContext(source, entry.Elapsed.TotalMilliseconds));
+ }
+ }
+
+ 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 double GetGameAverageExecutionTime()
+ {
+ if (this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter gameExecTime))
+ {
+ return gameExecTime.GetAverage();
+ }
+
+ return 0;
+ }
+
+ public void BeginTrackInvocation()
+ {
+ if (this.Monitor)
+ {
+ this.TriggeredPerformanceCounters.Clear();
+ this.Stopwatch.Reset();
+ this.Stopwatch.Start();
+ }
+
+ this.CallCount++;
+
+ }
+
+ public void EndTrackInvocation()
+ {
+ if (!this.Monitor) return;
+
+ this.Stopwatch.Stop();
+ if (this.Stopwatch.Elapsed.TotalMilliseconds >= this.MonitorThresholdMilliseconds)
+ {
+ this.AddAlert(this.Stopwatch.Elapsed.TotalMilliseconds,
+ this.MonitorThresholdMilliseconds, this.TriggeredPerformanceCounters);
+ }
+ }
+
+ public void AddAlert(double executionTimeMilliseconds, double threshold, List<AlertContext> alerts)
+ {
+ this.PerformanceCounterManager.AddAlert(new AlertEntry(this, executionTimeMilliseconds,
+ threshold, alerts));
+ }
+
+ public void AddAlert(double executionTimeMilliseconds, double threshold, AlertContext alert)
+ {
+ this.AddAlert(executionTimeMilliseconds, threshold, new List<AlertContext>() {alert});
+ }
+
+ public void ResetCallsPerSecond()
+ {
+ this.CallCount = 0;
+ this.StartDateTime = DateTime.Now;
+ }
+
+ public void Reset()
+ {
+ foreach (var i in this.PerformanceCounters)
+ {
+ i.Value.Reset();
+ i.Value.ResetPeak();
+ }
+ }
+
+ public void ResetSource(string source)
+ {
+ foreach (var i in this.PerformanceCounters)
+ {
+ if (i.Value.Source.Equals(source, StringComparison.InvariantCultureIgnoreCase))
+ {
+ i.Value.Reset();
+ i.Value.ResetPeak();
+ }
+ }
+ }
+
+ public long GetAverageCallsPerSecond()
+ {
+ long runtimeInSeconds = (long) DateTime.Now.Subtract(this.StartDateTime).TotalSeconds;
+
+ if (runtimeInSeconds == 0)
+ {
+ return 0;
+ }
+
+ return this.CallCount / runtimeInSeconds;
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs
index ae7258e2..9e77e2fa 100644
--- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs
+++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs
@@ -1,4 +1,8 @@
+using System;
using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
using StardewModdingAPI.Framework.Events;
using StardewModdingAPI.Framework.Utilities;
@@ -6,92 +10,187 @@ namespace StardewModdingAPI.Framework.PerformanceCounter
{
internal class PerformanceCounterManager
{
- public HashSet<EventPerformanceCounterCategory> PerformanceCounterEvents = new HashSet<EventPerformanceCounterCategory>();
+ public HashSet<PerformanceCounterCollection> PerformanceCounterCollections = new HashSet<PerformanceCounterCollection>();
+ public List<AlertEntry> Alerts = new List<AlertEntry>();
+ private readonly IMonitor Monitor;
+ private readonly Stopwatch Stopwatch = new Stopwatch();
- private readonly EventManager EventManager;
-
- public PerformanceCounterManager(EventManager eventManager)
+ public PerformanceCounterManager(IMonitor monitor)
{
- this.EventManager = eventManager;
- this.InitializePerformanceCounterEvents();
+ this.Monitor = monitor;
}
public void Reset()
{
- foreach (var performanceCounter in this.PerformanceCounterEvents)
+ foreach (var performanceCounter in this.PerformanceCounterCollections)
{
- this.ResetCategory(performanceCounter);
+ foreach (var eventPerformanceCounter in performanceCounter.PerformanceCounters)
+ {
+ eventPerformanceCounter.Value.Reset();
+ }
}
}
- public void ResetCategory(EventPerformanceCounterCategory category)
+ /// <summary>Print any queued messages.</summary>
+ public void PrintQueued()
{
- foreach (var eventPerformanceCounter in category.Event.PerformanceCounters)
+ if (this.Alerts.Count == 0)
+ {
+ return;
+ }
+ StringBuilder sb = new StringBuilder();
+
+ foreach (var alert in this.Alerts)
{
- eventPerformanceCounter.Value.Reset();
+ sb.AppendLine($"{alert.Collection.Name} took {alert.ExecutionTimeMilliseconds:F2}ms (exceeded threshold of {alert.Threshold:F2}ms)");
+
+ foreach (var context in alert.Context)
+ {
+ sb.AppendLine($"{context.Source}: {context.Elapsed:F2}ms");
+ }
}
+
+ this.Alerts.Clear();
+
+ this.Monitor.Log(sb.ToString(), LogLevel.Error);
+ }
+
+ public void BeginTrackInvocation(string collectionName)
+ {
+ this.GetOrCreateCollectionByName(collectionName).BeginTrackInvocation();
+ }
+
+ public void EndTrackInvocation(string collectionName)
+ {
+ this.GetOrCreateCollectionByName(collectionName).EndTrackInvocation();
}
- private void InitializePerformanceCounterEvents()
+ public void Track(string collectionName, string modName, Action action)
{
- this.PerformanceCounterEvents = new HashSet<EventPerformanceCounterCategory>()
+ DateTime eventTime = DateTime.UtcNow;
+ this.Stopwatch.Reset();
+ this.Stopwatch.Start();
+
+ try
+ {
+ action();
+ }
+ finally
{
- new EventPerformanceCounterCategory(this.EventManager.MenuChanged, false),
+ this.Stopwatch.Stop();
- // Rendering Events
- new EventPerformanceCounterCategory(this.EventManager.Rendering, true),
- new EventPerformanceCounterCategory(this.EventManager.Rendered, true),
- new EventPerformanceCounterCategory(this.EventManager.RenderingWorld, true),
- new EventPerformanceCounterCategory(this.EventManager.RenderedWorld, true),
- new EventPerformanceCounterCategory(this.EventManager.RenderingActiveMenu, true),
- new EventPerformanceCounterCategory(this.EventManager.RenderedActiveMenu, true),
- new EventPerformanceCounterCategory(this.EventManager.RenderingHud, true),
- new EventPerformanceCounterCategory(this.EventManager.RenderedHud, true),
-
- new EventPerformanceCounterCategory(this.EventManager.WindowResized, false),
- new EventPerformanceCounterCategory(this.EventManager.GameLaunched, false),
- new EventPerformanceCounterCategory(this.EventManager.UpdateTicking, true),
- new EventPerformanceCounterCategory(this.EventManager.UpdateTicked, true),
- new EventPerformanceCounterCategory(this.EventManager.OneSecondUpdateTicking, true),
- new EventPerformanceCounterCategory(this.EventManager.OneSecondUpdateTicked, true),
-
- new EventPerformanceCounterCategory(this.EventManager.SaveCreating, false),
- new EventPerformanceCounterCategory(this.EventManager.SaveCreated, false),
- new EventPerformanceCounterCategory(this.EventManager.Saving, false),
- new EventPerformanceCounterCategory(this.EventManager.Saved, false),
-
- new EventPerformanceCounterCategory(this.EventManager.DayStarted, false),
- new EventPerformanceCounterCategory(this.EventManager.DayEnding, false),
-
- new EventPerformanceCounterCategory(this.EventManager.TimeChanged, true),
-
- new EventPerformanceCounterCategory(this.EventManager.ReturnedToTitle, false),
-
- new EventPerformanceCounterCategory(this.EventManager.ButtonPressed, true),
- new EventPerformanceCounterCategory(this.EventManager.ButtonReleased, true),
- new EventPerformanceCounterCategory(this.EventManager.CursorMoved, true),
- new EventPerformanceCounterCategory(this.EventManager.MouseWheelScrolled, true),
-
- new EventPerformanceCounterCategory(this.EventManager.PeerContextReceived, true),
- new EventPerformanceCounterCategory(this.EventManager.ModMessageReceived, true),
- new EventPerformanceCounterCategory(this.EventManager.PeerDisconnected, true),
- new EventPerformanceCounterCategory(this.EventManager.InventoryChanged, true),
- new EventPerformanceCounterCategory(this.EventManager.LevelChanged, true),
- new EventPerformanceCounterCategory(this.EventManager.Warped, true),
-
- new EventPerformanceCounterCategory(this.EventManager.LocationListChanged, true),
- new EventPerformanceCounterCategory(this.EventManager.BuildingListChanged, true),
- new EventPerformanceCounterCategory(this.EventManager.LocationListChanged, true),
- new EventPerformanceCounterCategory(this.EventManager.DebrisListChanged, true),
- new EventPerformanceCounterCategory(this.EventManager.LargeTerrainFeatureListChanged, true),
- new EventPerformanceCounterCategory(this.EventManager.NpcListChanged, true),
- new EventPerformanceCounterCategory(this.EventManager.ObjectListChanged, true),
- new EventPerformanceCounterCategory(this.EventManager.ChestInventoryChanged, true),
- new EventPerformanceCounterCategory(this.EventManager.TerrainFeatureListChanged, true),
- new EventPerformanceCounterCategory(this.EventManager.LoadStageChanged, false),
- new EventPerformanceCounterCategory(this.EventManager.UnvalidatedUpdateTicking, true),
- new EventPerformanceCounterCategory(this.EventManager.UnvalidatedUpdateTicked, true),
+ this.GetOrCreateCollectionByName(collectionName).Track(modName, new PerformanceCounterEntry
+ {
+ EventTime = eventTime,
+ Elapsed = this.Stopwatch.Elapsed
+ });
+ }
+ }
+
+ public PerformanceCounterCollection GetCollectionByName(string name)
+ {
+ return this.PerformanceCounterCollections.FirstOrDefault(collection => collection.Name == name);
+ }
+
+ public PerformanceCounterCollection GetOrCreateCollectionByName(string name)
+ {
+ PerformanceCounterCollection collection = this.GetCollectionByName(name);
+ if (collection == null)
+ {
+ collection = new PerformanceCounterCollection(this, name);
+ this.PerformanceCounterCollections.Add(collection);
+ }
+
+ return collection;
+ }
+
+ public void ResetCategory(string name)
+ {
+ foreach (var performanceCounterCollection in this.PerformanceCounterCollections)
+ {
+ if (performanceCounterCollection.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase))
+ {
+ performanceCounterCollection.ResetCallsPerSecond();
+ performanceCounterCollection.Reset();
+ }
+ }
+ }
+
+ public void ResetSource(string name)
+ {
+ foreach (var performanceCounterCollection in this.PerformanceCounterCollections)
+ {
+ performanceCounterCollection.ResetSource(name);
+ }
+ }
+
+
+ public void AddAlert(AlertEntry entry)
+ {
+ this.Alerts.Add(entry);
+ }
+
+ public void InitializePerformanceCounterEvents(EventManager eventManager)
+ {
+ this.PerformanceCounterCollections = new HashSet<PerformanceCounterCollection>()
+ {
+ new EventPerformanceCounterCollection(this, eventManager.MenuChanged, false),
+
+
+ // Rendering Events
+ new EventPerformanceCounterCollection(this, eventManager.Rendering, true),
+ new EventPerformanceCounterCollection(this, eventManager.Rendered, true),
+ new EventPerformanceCounterCollection(this, eventManager.RenderingWorld, true),
+ new EventPerformanceCounterCollection(this, eventManager.RenderedWorld, true),
+ new EventPerformanceCounterCollection(this, eventManager.RenderingActiveMenu, true),
+ new EventPerformanceCounterCollection(this, eventManager.RenderedActiveMenu, true),
+ new EventPerformanceCounterCollection(this, eventManager.RenderingHud, true),
+ new EventPerformanceCounterCollection(this, eventManager.RenderedHud, true),
+
+ new EventPerformanceCounterCollection(this, eventManager.WindowResized, false),
+ new EventPerformanceCounterCollection(this, eventManager.GameLaunched, false),
+ new EventPerformanceCounterCollection(this, eventManager.UpdateTicking, true),
+ new EventPerformanceCounterCollection(this, eventManager.UpdateTicked, true),
+ new EventPerformanceCounterCollection(this, eventManager.OneSecondUpdateTicking, true),
+ new EventPerformanceCounterCollection(this, eventManager.OneSecondUpdateTicked, true),
+
+ new EventPerformanceCounterCollection(this, eventManager.SaveCreating, false),
+ new EventPerformanceCounterCollection(this, eventManager.SaveCreated, false),
+ new EventPerformanceCounterCollection(this, eventManager.Saving, false),
+ new EventPerformanceCounterCollection(this, eventManager.Saved, false),
+
+ new EventPerformanceCounterCollection(this, eventManager.DayStarted, false),
+ new EventPerformanceCounterCollection(this, eventManager.DayEnding, false),
+
+ new EventPerformanceCounterCollection(this, eventManager.TimeChanged, true),
+
+ new EventPerformanceCounterCollection(this, eventManager.ReturnedToTitle, false),
+
+ new EventPerformanceCounterCollection(this, eventManager.ButtonPressed, true),
+ new EventPerformanceCounterCollection(this, eventManager.ButtonReleased, true),
+ new EventPerformanceCounterCollection(this, eventManager.CursorMoved, true),
+ new EventPerformanceCounterCollection(this, eventManager.MouseWheelScrolled, true),
+
+ new EventPerformanceCounterCollection(this, eventManager.PeerContextReceived, true),
+ new EventPerformanceCounterCollection(this, eventManager.ModMessageReceived, true),
+ new EventPerformanceCounterCollection(this, eventManager.PeerDisconnected, true),
+ new EventPerformanceCounterCollection(this, eventManager.InventoryChanged, true),
+ new EventPerformanceCounterCollection(this, eventManager.LevelChanged, true),
+ new EventPerformanceCounterCollection(this, eventManager.Warped, true),
+
+ new EventPerformanceCounterCollection(this, eventManager.LocationListChanged, true),
+ new EventPerformanceCounterCollection(this, eventManager.BuildingListChanged, true),
+ new EventPerformanceCounterCollection(this, eventManager.LocationListChanged, true),
+ new EventPerformanceCounterCollection(this, eventManager.DebrisListChanged, true),
+ new EventPerformanceCounterCollection(this, eventManager.LargeTerrainFeatureListChanged, true),
+ new EventPerformanceCounterCollection(this, eventManager.NpcListChanged, true),
+ new EventPerformanceCounterCollection(this, eventManager.ObjectListChanged, true),
+ new EventPerformanceCounterCollection(this, eventManager.ChestInventoryChanged, true),
+ new EventPerformanceCounterCollection(this, eventManager.TerrainFeatureListChanged, true),
+ new EventPerformanceCounterCollection(this, eventManager.LoadStageChanged, false),
+ new EventPerformanceCounterCollection(this, eventManager.UnvalidatedUpdateTicking, false),
+ new EventPerformanceCounterCollection(this, eventManager.UnvalidatedUpdateTicked, false),
};
}
}
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index d1dba9ea..5b0c6691 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -25,7 +25,6 @@ using StardewModdingAPI.Framework.ModLoading;
using StardewModdingAPI.Framework.Patching;
using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Framework.Serialization;
-using StardewModdingAPI.Framework.Utilities;
using StardewModdingAPI.Patches;
using StardewModdingAPI.Toolkit;
using StardewModdingAPI.Toolkit.Framework.Clients.WebApi;
@@ -82,8 +81,6 @@ namespace StardewModdingAPI.Framework
/// <summary>Manages SMAPI events for mods.</summary>
private readonly EventManager EventManager;
- private readonly PerformanceCounterManager PerformanceCounterManager;
-
/// <summary>Whether the game is currently running.</summary>
private bool IsGameRunning;
@@ -137,6 +134,10 @@ namespace StardewModdingAPI.Framework
/// <remarks>This is initialized after the game starts. This is accessed directly because it's not part of the normal class model.</remarks>
internal static DeprecationManager DeprecationManager { get; private set; }
+ /// <summary>Manages performance counters.</summary>
+ /// <remarks>This is initialized after the game starts. This is accessed directly because it's not part of the normal class model.</remarks>
+ internal static PerformanceCounterManager PerformanceCounterManager { get; private set; }
+
/*********
** Public methods
@@ -165,8 +166,10 @@ namespace StardewModdingAPI.Framework
ShowFullStampInConsole = this.Settings.DeveloperMode
};
this.MonitorForGame = this.GetSecondaryMonitor("game");
- this.EventManager = new EventManager(this.Monitor, this.ModRegistry);
- this.PerformanceCounterManager = new PerformanceCounterManager(this.EventManager);
+
+ SCore.PerformanceCounterManager = new PerformanceCounterManager(this.Monitor);
+ this.EventManager = new EventManager(this.Monitor, this.ModRegistry, SCore.PerformanceCounterManager);
+ SCore.PerformanceCounterManager.InitializePerformanceCounterEvents(this.EventManager);
SCore.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry);
@@ -245,6 +248,7 @@ namespace StardewModdingAPI.Framework
jsonHelper: this.Toolkit.JsonHelper,
modRegistry: this.ModRegistry,
deprecationManager: SCore.DeprecationManager,
+ performanceCounterManager: SCore.PerformanceCounterManager,
onGameInitialized: this.InitializeAfterGameStart,
onGameExiting: this.Dispose,
cancellationToken: this.CancellationToken,
@@ -488,20 +492,6 @@ namespace StardewModdingAPI.Framework
this.Monitor.Log("Type 'help' for help, or 'help <cmd>' for a command's usage", LogLevel.Info);
this.GameInstance.CommandManager.Add(null, "help", "Lists command documentation.\n\nUsage: help\nLists all available commands.\n\nUsage: help <cmd>\n- cmd: The name of a command whose documentation to display.", this.HandleCommand);
this.GameInstance.CommandManager.Add(null, "reload_i18n", "Reloads translation files for all mods.\n\nUsage: reload_i18n", this.HandleCommand);
- this.GameInstance.CommandManager.Add(null, "performance_counters",
- "Displays performance counters.\n\n"+
- "Usage: performance_counters\n" +
- "Shows the most important event invocation times\n\n"+
- "Usage: performance_counters summary|sum [all|important|name]\n"+
- "- summary or sum: Forces summary mode\n"+
- "- all, important or name: Displays all event performance counters, only important ones, or a specific event by name (defaults to important)\n\n"+
- "Usage: performance_counters [name] [threshold]\n"+
- "Shows detailed performance counters for a specific event\n"+
- "- name: The (partial) name of the event\n"+
- "- threshold: The minimum avg execution time (ms) of the event\n\n"+
- "Usage: performance_counters reset\n"+
- "Resets all performance counters\n", this.HandleCommand);
- this.GameInstance.CommandManager.Add(null, "pc", "Alias for performance_counters", this.HandleCommand);
// start handling command line input
Thread inputThread = new Thread(() =>
@@ -1317,176 +1307,11 @@ namespace StardewModdingAPI.Framework
this.ReloadTranslations(this.ModRegistry.GetAll(contentPacks: false));
this.Monitor.Log("Reloaded translation files for all mods. This only affects new translations the mods fetch; if they cached some text, it may not be updated.", LogLevel.Info);
break;
- case "performance_counters":
- case "pc":
- this.DisplayPerformanceCounters(arguments.ToList());
- break;
default:
throw new NotSupportedException($"Unrecognized core SMAPI command '{name}'.");
}
}
- /// <summary>Get an ASCII table to show tabular data in the console.</summary>
- /// <typeparam name="T">The data type.</typeparam>
- /// <param name="data">The data to display.</param>
- /// <param name="header">The table header.</param>
- /// <param name="getRow">Returns a set of fields for a data value.</param>
- protected string GetTableString<T>(IEnumerable<T> data, string[] header, Func<T, string[]> getRow)
- {
- // get table data
- int[] widths = header.Select(p => p.Length).ToArray();
- string[][] rows = data
- .Select(item =>
- {
- string[] fields = getRow(item);
- if (fields.Length != widths.Length)
- throw new InvalidOperationException($"Expected {widths.Length} columns, but found {fields.Length}: {string.Join(", ", fields)}");
-
- for (int i = 0; i < fields.Length; i++)
- widths[i] = Math.Max(widths[i], fields[i].Length);
-
- return fields;
- })
- .ToArray();
-
- // render fields
- List<string[]> lines = new List<string[]>(rows.Length + 2)
- {
- header,
- header.Select((value, i) => "".PadRight(widths[i], '-')).ToArray()
- };
- lines.AddRange(rows);
-
- return string.Join(
- Environment.NewLine,
- lines.Select(line => string.Join(" | ", line.Select((field, i) => field.PadLeft(widths[i], ' ')).ToArray())
- )
- );
- }
-
- private void DisplayPerformanceCounters(IList<string> arguments)
- {
- bool showSummary = true;
- bool showSummaryOnlyImportant = true;
- string filterByName = null;
-
- if (arguments.Any())
- {
- switch (arguments[0])
- {
- case "summary":
- case "sum":
- showSummary = true;
-
- if (arguments.Count > 1)
- {
- switch (arguments[1].ToLower())
- {
- case "all":
- showSummaryOnlyImportant = false;
- break;
- case "important":
- showSummaryOnlyImportant = true;
- break;
- default:
- filterByName = arguments[1];
- break;
- }
- }
- break;
- case "reset":
- this.PerformanceCounterManager.Reset();
- return;
- default:
- showSummary = false;
- filterByName = arguments[0];
- break;
-
- }
- }
- var lastMinute = TimeSpan.FromSeconds(60);
-
- if (showSummary)
- {
- this.DisplayPerformanceCounterSummary(showSummaryOnlyImportant, filterByName);
- }
- else
- {
- var data = this.PerformanceCounterManager.PerformanceCounterEvents.Where(p => p.Event.GetEventName().ToLowerInvariant().Contains(filterByName.ToLowerInvariant()));
-
- foreach (var i in data)
- {
- this.DisplayPerformanceCounter(i, lastMinute);
- }
- }
-
- double avgTime = PerformanceCounter.PerformanceCounter.Stopwatch.ElapsedMilliseconds / (double)PerformanceCounter.PerformanceCounter.TotalNumEventsLogged;
- this.Monitor.Log($"Logged {PerformanceCounter.PerformanceCounter.TotalNumEventsLogged} events in {PerformanceCounter.PerformanceCounter.Stopwatch.ElapsedMilliseconds}ms (avg {avgTime:F4}ms / event)");
-
- }
-
- private void DisplayPerformanceCounterSummary(bool showOnlyImportant, string eventNameFilter = null)
- {
- StringBuilder sb = new StringBuilder($"Performance Counter Summary:\n\n");
-
- IEnumerable<EventPerformanceCounterCategory> data;
-
- if (eventNameFilter != null)
- {
- data = this.PerformanceCounterManager.PerformanceCounterEvents.Where(p => p.Event.GetEventName().ToLowerInvariant().Contains(eventNameFilter.ToLowerInvariant()));
- }
- else
- {
- if (showOnlyImportant)
- {
- data = this.PerformanceCounterManager.PerformanceCounterEvents.Where(p => p.IsImportant);
- }
- else
- {
- data = this.PerformanceCounterManager.PerformanceCounterEvents;
- }
- }
-
-
- sb.AppendLine(this.GetTableString(
- data: data,
- header: new[] {"Event", "Avg Calls/s", "Avg Execution Time (Game)", "Avg Execution Time (Mods)", "Avg Execution Time (Game+Mods)"},
- getRow: item => new[]
- {
- item.Event.GetEventName(),
- item.Event.GetAverageCallsPerSecond().ToString(),
- item.Event.GetGameAverageExecutionTime().ToString("F2") + " ms",
- item.Event.GetModsAverageExecutionTime().ToString("F2") + " ms",
- item.Event.GetAverageExecutionTime().ToString("F2") + " ms"
- }
- ));
-
- this.Monitor.Log(sb.ToString(), LogLevel.Info);
- }
-
- private void DisplayPerformanceCounter (EventPerformanceCounterCategory obj, TimeSpan averageInterval)
- {
- StringBuilder sb = new StringBuilder($"Performance Counter for {obj.Event.GetEventName()}:\n\n");
-
- sb.AppendLine(this.GetTableString(
- data: obj.Event.PerformanceCounters,
- header: new[] {"Mod", $"Avg Execution Time over {(int)averageInterval.TotalSeconds}s", "Last Execution Time", "Peak Execution Time"},
- getRow: item => new[]
- {
- item.Key,
- item.Value.GetAverage(averageInterval).ToString("F2") + " ms" ?? "-",
- item.Value.GetLastEntry()?.Elapsed.TotalMilliseconds.ToString("F2") + " ms" ?? "-",
- item.Value.GetPeak()?.Elapsed.TotalMilliseconds.ToString("F2") + " ms" ?? "-",
- }
- ));
-
- sb.AppendLine($"Average execution time (Game+Mods): {obj.Event.GetAverageExecutionTime():F2} ms");
- sb.AppendLine($"Average execution time (Game only) : {obj.Event.GetGameAverageExecutionTime():F2} ms");
- sb.AppendLine($"Average execution time (Mods only) : {obj.Event.GetModsAverageExecutionTime():F2} ms");
-
- this.Monitor.Log(sb.ToString(), LogLevel.Info);
- }
-
/// <summary>Redirect messages logged directly to the console to the given monitor.</summary>
/// <param name="gameMonitor">The monitor with which to log messages as the game.</param>
/// <param name="message">The message to log.</param>
diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs
index d6c3b836..8aba9b57 100644
--- a/src/SMAPI/Framework/SGame.cs
+++ b/src/SMAPI/Framework/SGame.cs
@@ -17,6 +17,7 @@ using StardewModdingAPI.Framework.Content;
using StardewModdingAPI.Framework.Events;
using StardewModdingAPI.Framework.Input;
using StardewModdingAPI.Framework.Networking;
+using StardewModdingAPI.Framework.PerformanceCounter;
using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Framework.StateTracking.Comparers;
using StardewModdingAPI.Framework.StateTracking.Snapshots;
@@ -58,6 +59,8 @@ namespace StardewModdingAPI.Framework
/// <summary>Manages deprecation warnings.</summary>
private readonly DeprecationManager DeprecationManager;
+ private readonly PerformanceCounterManager PerformanceCounterManager;
+
/// <summary>The maximum number of consecutive attempts SMAPI should make to recover from a draw error.</summary>
private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second
@@ -152,11 +155,12 @@ namespace StardewModdingAPI.Framework
/// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</param>
/// <param name="modRegistry">Tracks the installed mods.</param>
/// <param name="deprecationManager">Manages deprecation warnings.</param>
+ /// <param name="performanceCounterManager">Manages performance monitoring.</param>
/// <param name="onGameInitialized">A callback to invoke after the game finishes initializing.</param>
/// <param name="onGameExiting">A callback to invoke when the game exits.</param>
/// <param name="cancellationToken">Propagates notification that SMAPI should exit.</param>
/// <param name="logNetworkTraffic">Whether to log network traffic.</param>
- internal SGame(Monitor monitor, IMonitor monitorForGame, Reflector reflection, Translator translator, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, DeprecationManager deprecationManager, Action onGameInitialized, Action onGameExiting, CancellationTokenSource cancellationToken, bool logNetworkTraffic)
+ internal SGame(Monitor monitor, IMonitor monitorForGame, Reflector reflection, Translator translator, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, DeprecationManager deprecationManager, PerformanceCounterManager performanceCounterManager, Action onGameInitialized, Action onGameExiting, CancellationTokenSource cancellationToken, bool logNetworkTraffic)
{
this.OnLoadingFirstAsset = SGame.ConstructorHack.OnLoadingFirstAsset;
SGame.ConstructorHack = null;
@@ -176,6 +180,7 @@ namespace StardewModdingAPI.Framework
this.Reflection = reflection;
this.Translator = translator;
this.DeprecationManager = deprecationManager;
+ this.PerformanceCounterManager = performanceCounterManager;
this.OnGameInitialized = onGameInitialized;
this.OnGameExiting = onGameExiting;
Game1.input = new SInputState();
@@ -307,6 +312,7 @@ namespace StardewModdingAPI.Framework
try
{
this.DeprecationManager.PrintQueued();
+ this.PerformanceCounterManager.PrintQueued();
/*********
** First-tick initialization
@@ -382,7 +388,7 @@ namespace StardewModdingAPI.Framework
// state while mods are running their code. This is risky, because data changes can
// conflict (e.g. collection changed during enumeration errors) and data may change
// unexpectedly from one mod instruction to the next.
- //
+ //
// Therefore we can just run Game1.Update here without raising any SMAPI events. There's
// a small chance that the task will finish after we defer but before the game checks,
// which means technically events should be raised, but the effects of missing one
diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs
index b5e6307a..933590d3 100644
--- a/src/SMAPI/Program.cs
+++ b/src/SMAPI/Program.cs
@@ -11,7 +11,7 @@ using StardewModdingAPI.Framework;
using StardewModdingAPI.Toolkit.Utilities;
[assembly: InternalsVisibleTo("SMAPI.Tests")]
-[assembly: InternalsVisibleTo("SMAPI.Mods.ConsoleCommands")]
+[assembly: InternalsVisibleTo("ConsoleCommands")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Moq for unit testing
namespace StardewModdingAPI
{