refactor performance counter code

This commit performs some general refactoring, including...
- avoid manually duplicating the event list;
- rework the 'is important' event flag;
- remove the new packages (Cyotek.Collections can be replaced with built-in types, and System.ValueTuple won't work in the Mono version used on Linux/Mac);
- improve performance;
- minor cleanup.
This commit is contained in:
Jesse Plamondon-Willard 2020-01-26 19:49:17 -05:00
parent 1b905205a3
commit 22a0a32b6d
No known key found for this signature in database
GPG Key ID: CF8B1456B3E29F49
22 changed files with 704 additions and 821 deletions

View File

@ -32,7 +32,6 @@
<Copy SourceFiles="$(TargetDir)\SMAPI.metadata.json" DestinationFiles="$(GamePath)\smapi-internal\metadata.json" />
<Copy SourceFiles="$(TargetDir)\0Harmony.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\Newtonsoft.Json.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\Cyotek.Collections.Generic.CircularBuffer.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\Mono.Cecil.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="@(TranslationFiles)" DestinationFolder="$(GamePath)\smapi-internal\i18n" />
</Target>

View File

@ -41,7 +41,6 @@
<Copy SourceFiles="$(CompiledSmapiPath)\0Harmony.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy SourceFiles="$(CompiledSmapiPath)\Mono.Cecil.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy SourceFiles="$(CompiledSmapiPath)\Newtonsoft.Json.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy SourceFiles="$(CompiledSmapiPath)\Cyotek.Collections.Generic.CircularBuffer.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy SourceFiles="$(CompiledSmapiPath)\SMAPI.config.json" DestinationFiles="$(PackagePath)\bundle\smapi-internal\config.json" />
<Copy SourceFiles="$(CompiledSmapiPath)\SMAPI.metadata.json" DestinationFiles="$(PackagePath)\bundle\smapi-internal\metadata.json" />
<Copy SourceFiles="$(CompiledToolkitPath)\SMAPI.Toolkit.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
@ -32,13 +32,6 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
/// <param name="index">The zero-based index of the element to get.</param>
public string this[int index] => this.Args[index];
/// <summary>A method which parses a string argument into the given value.</summary>
/// <typeparam name="T">The expected argument type.</typeparam>
/// <param name="input">The argument to parse.</param>
/// <param name="output">The parsed value.</param>
/// <returns>Returns whether the argument was successfully parsed.</returns>
public delegate bool ParseDelegate<T>(string input, out T output);
/*********
** Public methods
@ -114,19 +107,6 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
return true;
}
public bool IsDecimal(int index)
{
if (!this.TryGet(index, "", out string raw, false))
return false;
if (!decimal.TryParse(raw, NumberStyles.Number, CultureInfo.InvariantCulture, out decimal value))
{
return false;
}
return true;
}
/// <summary>Try to read a decimal argument.</summary>
/// <param name="index">The argument index.</param>
/// <param name="name">The argument name for error messages.</param>

View File

@ -7,24 +7,13 @@ using StardewModdingAPI.Framework.PerformanceCounter;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
{
// ReSharper disable once UnusedType.Global
/// <summary>A set of commands which displays or configures performance monitoring.</summary>
internal class PerformanceCounterCommand : TrainerCommand
{
/// <summary>The command names and aliases</summary>
private readonly Dictionary<SubCommand, string[]> SubCommandNames = new Dictionary<SubCommand, string[]>()
{
{SubCommand.Summary, new[] {"summary", "sum", "s"}},
{SubCommand.Detail, new[] {"detail", "d"}},
{SubCommand.Reset, new[] {"reset", "r"}},
{SubCommand.Trigger, new[] {"trigger"}},
{SubCommand.Enable, new[] {"enable"}},
{SubCommand.Disable, new[] {"disable"}},
{SubCommand.Examples, new[] {"examples"}},
{SubCommand.Concepts, new[] {"concepts"}},
{SubCommand.Help, new[] {"help"}},
};
/// <summary>The available commands enum</summary>
/*********
** Fields
*********/
/// <summary>The available commands.</summary>
private enum SubCommand
{
Summary,
@ -33,16 +22,16 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
Trigger,
Enable,
Disable,
Examples,
Help,
Concepts,
None
Help
}
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
public PerformanceCounterCommand() : base("pc", PerformanceCounterCommand.GetDescription())
{
}
public PerformanceCounterCommand()
: base("performance", PerformanceCounterCommand.GetDescription()) { }
/// <summary>Handle the command.</summary>
/// <param name="monitor">Writes messages to the console and log file.</param>
@ -50,92 +39,94 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
/// <param name="args">The command arguments.</param>
public override void Handle(IMonitor monitor, string command, ArgumentParser args)
{
if (args.TryGet(0, "command", out string subCommandString, false))
// parse args
SubCommand subcommand = SubCommand.Summary;
{
SubCommand subSubCommand = this.ParseCommandString(subCommandString);
switch (subSubCommand)
if (args.TryGet(0, "command", out string subcommandStr, false) && !Enum.TryParse(subcommandStr, ignoreCase: true, out subcommand))
{
case SubCommand.Summary:
this.HandleSummarySubCommand(monitor, args);
break;
case SubCommand.Detail:
this.HandleDetailSubCommand(monitor, args);
break;
case SubCommand.Reset:
this.HandleResetSubCommand(monitor, args);
break;
case SubCommand.Trigger:
this.HandleTriggerSubCommand(monitor, args);
break;
case SubCommand.Examples:
break;
case SubCommand.Concepts:
this.OutputHelp(monitor, SubCommand.Concepts);
break;
case SubCommand.Enable:
SCore.PerformanceCounterManager.EnableTracking = true;
monitor.Log("Performance counter tracking is now enabled", LogLevel.Info);
break;
case SubCommand.Disable:
SCore.PerformanceCounterManager.EnableTracking = false;
monitor.Log("Performance counter tracking is now disabled", LogLevel.Info);
break;
case SubCommand.Help:
if (args.TryGet(1, "command", out string commandString))
this.OutputHelp(monitor, this.ParseCommandString(commandString));
break;
default:
this.LogUsageError(monitor, $"Unknown command {subCommandString}");
break;
this.LogUsageError(monitor, $"Unknown command {subcommandStr}");
return;
}
}
else
this.HandleSummarySubCommand(monitor, args);
// handle
switch (subcommand)
{
case SubCommand.Summary:
this.HandleSummarySubCommand(monitor, args);
break;
case SubCommand.Detail:
this.HandleDetailSubCommand(monitor, args);
break;
case SubCommand.Reset:
this.HandleResetSubCommand(monitor, args);
break;
case SubCommand.Trigger:
this.HandleTriggerSubCommand(monitor, args);
break;
case SubCommand.Enable:
SCore.PerformanceMonitor.EnableTracking = true;
monitor.Log("Performance counter tracking is now enabled", LogLevel.Info);
break;
case SubCommand.Disable:
SCore.PerformanceMonitor.EnableTracking = false;
monitor.Log("Performance counter tracking is now disabled", LogLevel.Info);
break;
case SubCommand.Help:
this.OutputHelp(monitor, args.TryGet(1, "command", out _) ? subcommand : null as SubCommand?);
break;
default:
this.LogUsageError(monitor, $"Unknown command {subcommand}");
break;
}
}
/*********
** Private methods
*********/
/// <summary>Handles the summary sub command.</summary>
/// <param name="monitor">Writes messages to the console and log file.</param>
/// <param name="args">The command arguments.</param>
private void HandleSummarySubCommand(IMonitor monitor, ArgumentParser args)
{
IEnumerable<PerformanceCounterCollection> data;
if (!args.TryGet(1, "mode", out string mode, false))
{
mode = "important";
}
IEnumerable<PerformanceCounterCollection> data = SCore.PerformanceMonitor.GetCollections();
switch (mode)
{
case null:
case "important":
data = SCore.PerformanceCounterManager.PerformanceCounterCollections.Where(p => p.IsImportant);
data = data.Where(p => p.IsPerformanceCritical);
break;
case "all":
data = SCore.PerformanceCounterManager.PerformanceCounterCollections;
break;
default:
data = SCore.PerformanceCounterManager.PerformanceCounterCollections.Where(p =>
p.Name.ToLowerInvariant().Contains(mode.ToLowerInvariant()));
data = data.Where(p => p.Name.ToLowerInvariant().Contains(mode.ToLowerInvariant()));
break;
}
double? threshold = null;
if (args.TryGetDecimal(2, "threshold", out decimal t, false))
{
threshold = (double?) t;
}
StringBuilder sb = new StringBuilder();
threshold = (double?)t;
TimeSpan interval = TimeSpan.FromSeconds(60);
sb.AppendLine($"Summary over the last {interval.TotalSeconds} seconds:");
sb.AppendLine(this.GetTableString(
StringBuilder report = new StringBuilder();
report.AppendLine($"Summary over the last {interval.TotalSeconds} seconds:");
report.AppendLine(this.GetTableString(
data: data,
header: new[] {"Collection", "Avg Calls/s", "Avg Exec Time (Game)", "Avg Exec Time (Mods)", "Avg Exec Time (Game+Mods)", "Peak Exec Time"},
header: new[] { "Collection", "Avg Calls/s", "Avg Exec Time (Game)", "Avg Exec Time (Mods)", "Avg Exec Time (Game+Mods)", "Peak Exec Time" },
getRow: item => new[]
{
item.Name,
@ -148,7 +139,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
true
));
monitor.Log(sb.ToString(), LogLevel.Info);
monitor.Log(report.ToString(), LogLevel.Info);
}
/// <summary>Handles the detail sub command.</summary>
@ -163,26 +154,16 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
if (args.TryGet(1, "collection", out string collectionName))
{
collections.AddRange(SCore.PerformanceCounterManager.PerformanceCounterCollections.Where(
collection => collection.Name.ToLowerInvariant().Contains(collectionName.ToLowerInvariant())));
collections.AddRange(SCore.PerformanceMonitor.GetCollections().Where(collection => collection.Name.ToLowerInvariant().Contains(collectionName.ToLowerInvariant())));
if (args.IsDecimal(2) && args.TryGetDecimal(2, "threshold", out decimal value, false))
{
thresholdMilliseconds = (double?) value;
}
else
{
if (args.TryGet(2, "source", out string sourceName, false))
{
sourceFilter = sourceName;
}
}
if (args.Count >= 2 && decimal.TryParse(args[2], out _) && args.TryGetDecimal(2, "threshold", out decimal value, false))
thresholdMilliseconds = (double?)value;
else if (args.TryGet(2, "source", out string sourceName, false))
sourceFilter = sourceName;
}
foreach (PerformanceCounterCollection c in collections)
{
this.OutputPerformanceCollectionDetail(monitor, c, averageInterval, thresholdMilliseconds, sourceFilter);
}
}
/// <summary>Handles the trigger sub command.</summary>
@ -197,46 +178,44 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
case "list":
this.OutputAlertTriggers(monitor);
break;
case "collection":
if (args.TryGet(2, "name", out string collectionName))
{
if (args.TryGetDecimal(3, "threshold", out decimal threshold))
{
if (args.TryGet(4, "source", out string source, false))
{
this.ConfigureAlertTrigger(monitor, collectionName, source, threshold);
}
else
{
this.ConfigureAlertTrigger(monitor, collectionName, null, threshold);
}
if (!args.TryGet(4, "source", out string source, required: false))
source = null;
this.ConfigureAlertTrigger(monitor, collectionName, source, threshold);
}
}
break;
case "pause":
SCore.PerformanceCounterManager.PauseAlerts = true;
monitor.Log($"Alerts are now paused.", LogLevel.Info);
SCore.PerformanceMonitor.PauseAlerts = true;
monitor.Log("Alerts are now paused.", LogLevel.Info);
break;
case "resume":
SCore.PerformanceCounterManager.PauseAlerts = false;
monitor.Log($"Alerts are now resumed.", LogLevel.Info);
SCore.PerformanceMonitor.PauseAlerts = false;
monitor.Log("Alerts are now resumed.", LogLevel.Info);
break;
case "dump":
this.OutputAlertTriggers(monitor, true);
break;
case "clear":
this.ClearAlertTriggers(monitor);
break;
default:
this.LogUsageError(monitor, $"Unknown mode {mode}. See 'pc help trigger' for usage.");
break;
}
}
else
{
this.OutputAlertTriggers(monitor);
}
}
/// <summary>Sets up an an alert trigger.</summary>
@ -246,7 +225,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
/// <param name="threshold">The trigger threshold, or 0 to remove.</param>
private void ConfigureAlertTrigger(IMonitor monitor, string collectionName, string sourceName, decimal threshold)
{
foreach (PerformanceCounterCollection collection in SCore.PerformanceCounterManager.PerformanceCounterCollections)
foreach (PerformanceCounterCollection collection in SCore.PerformanceMonitor.GetCollections())
{
if (collection.Name.ToLowerInvariant().Equals(collectionName.ToLowerInvariant()))
{
@ -255,8 +234,8 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
if (threshold != 0)
{
collection.EnableAlerts = true;
collection.AlertThresholdMilliseconds = (double) threshold;
monitor.Log($"Set up alert triggering for '{collectionName}' with '{this.FormatMilliseconds((double?) threshold)}'", LogLevel.Info);
collection.AlertThresholdMilliseconds = (double)threshold;
monitor.Log($"Set up alert triggering for '{collectionName}' with '{this.FormatMilliseconds((double?)threshold)}'", LogLevel.Info);
}
else
{
@ -275,14 +254,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
if (threshold != 0)
{
performanceCounter.Value.EnableAlerts = true;
performanceCounter.Value.AlertThresholdMilliseconds = (double) threshold;
monitor.Log($"Set up alert triggering for '{sourceName}' in collection '{collectionName}' with '{this.FormatMilliseconds((double?) threshold)}", LogLevel.Info);
performanceCounter.Value.AlertThresholdMilliseconds = (double)threshold;
monitor.Log($"Set up alert triggering for '{sourceName}' in collection '{collectionName}' with '{this.FormatMilliseconds((double?)threshold)}", LogLevel.Info);
}
else
{
performanceCounter.Value.EnableAlerts = false;
}
return;
}
}
@ -302,7 +278,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
private void ClearAlertTriggers(IMonitor monitor)
{
int clearedTriggers = 0;
foreach (PerformanceCounterCollection collection in SCore.PerformanceCounterManager.PerformanceCounterCollections)
foreach (PerformanceCounterCollection collection in SCore.PerformanceMonitor.GetCollections())
{
if (collection.EnableAlerts)
{
@ -329,92 +305,75 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
/// <param name="asDump">True to dump the triggers as commands.</param>
private void OutputAlertTriggers(IMonitor monitor, bool asDump = false)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("Configured triggers:");
sb.AppendLine();
var collectionTriggers = new List<(string collectionName, double threshold)>();
var sourceTriggers = new List<(string collectionName, string sourceName, double threshold)>();
StringBuilder report = new StringBuilder();
report.AppendLine("Configured triggers:");
report.AppendLine();
var collectionTriggers = new List<CollectionTrigger>();
var sourceTriggers = new List<SourceTrigger>();
foreach (PerformanceCounterCollection collection in SCore.PerformanceCounterManager.PerformanceCounterCollections)
foreach (PerformanceCounterCollection collection in SCore.PerformanceMonitor.GetCollections())
{
if (collection.EnableAlerts)
{
collectionTriggers.Add((collection.Name, collection.AlertThresholdMilliseconds));
}
collectionTriggers.Add(new CollectionTrigger(collection.Name, collection.AlertThresholdMilliseconds));
sourceTriggers.AddRange(from performanceCounter in
collection.PerformanceCounters where performanceCounter.Value.EnableAlerts
select (collection.Name, performanceCounter.Value.Source, performanceCounter.Value.AlertThresholdMilliseconds));
sourceTriggers.AddRange(
from counter in collection.PerformanceCounters
where counter.Value.EnableAlerts
select new SourceTrigger(collection.Name, counter.Value.Source, counter.Value.AlertThresholdMilliseconds)
);
}
if (collectionTriggers.Count > 0)
{
sb.AppendLine("Collection Triggers:");
sb.AppendLine();
report.AppendLine("Collection Triggers:");
report.AppendLine();
if (asDump)
{
foreach (var item in collectionTriggers)
{
sb.AppendLine($"pc trigger {item.collectionName} {item.threshold}");
}
report.AppendLine($"pc trigger {item.CollectionName} {item.Threshold}");
}
else
{
sb.AppendLine(this.GetTableString(
report.AppendLine(this.GetTableString(
data: collectionTriggers,
header: new[] {"Collection", "Threshold"},
getRow: item => new[]
{
item.collectionName,
this.FormatMilliseconds(item.threshold)
},
header: new[] { "Collection", "Threshold" },
getRow: item => new[] { item.CollectionName, this.FormatMilliseconds(item.Threshold) },
true
));
}
sb.AppendLine();
report.AppendLine();
}
else
{
sb.AppendLine("No collection triggers.");
}
report.AppendLine("No collection triggers.");
if (sourceTriggers.Count > 0)
{
sb.AppendLine("Source Triggers:");
sb.AppendLine();
report.AppendLine("Source Triggers:");
report.AppendLine();
if (asDump)
{
foreach (var item in sourceTriggers)
{
sb.AppendLine($"pc trigger {item.collectionName} {item.threshold} {item.sourceName}");
}
foreach (SourceTrigger item in sourceTriggers)
report.AppendLine($"pc trigger {item.CollectionName} {item.Threshold} {item.SourceName}");
}
else
{
sb.AppendLine(this.GetTableString(
report.AppendLine(this.GetTableString(
data: sourceTriggers,
header: new[] {"Collection", "Source", "Threshold"},
getRow: item => new[]
{
item.collectionName,
item.sourceName,
this.FormatMilliseconds(item.threshold)
},
header: new[] { "Collection", "Source", "Threshold" },
getRow: item => new[] { item.CollectionName, item.SourceName, this.FormatMilliseconds(item.Threshold) },
true
));
}
sb.AppendLine();
report.AppendLine();
}
else
{
sb.AppendLine("No source triggers.");
}
report.AppendLine("No source triggers.");
monitor.Log(sb.ToString(), LogLevel.Info);
monitor.Log(report.ToString(), LogLevel.Info);
}
/// <summary>Handles the reset sub command.</summary>
@ -422,25 +381,25 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
/// <param name="args">The command arguments.</param>
private void HandleResetSubCommand(IMonitor monitor, ArgumentParser args)
{
if (args.TryGet(1, "type", out string type, false, new []{"category", "source"}))
if (args.TryGet(1, "type", out string type, false, new[] { "category", "source" }))
{
args.TryGet(2, "name", out string name);
switch (type)
{
case "category":
SCore.PerformanceCounterManager.ResetCollection(name);
SCore.PerformanceMonitor.ResetCollection(name);
monitor.Log($"All performance counters for category {name} are now cleared.", LogLevel.Info);
break;
case "source":
SCore.PerformanceCounterManager.ResetSource(name);
SCore.PerformanceMonitor.ResetSource(name);
monitor.Log($"All performance counters for source {name} are now cleared.", LogLevel.Info);
break;
}
}
else
{
SCore.PerformanceCounterManager.Reset();
SCore.PerformanceMonitor.Reset();
monitor.Log("All performance counters are now cleared.", LogLevel.Info);
}
}
@ -455,7 +414,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
private void OutputPerformanceCollectionDetail(IMonitor monitor, PerformanceCounterCollection collection,
TimeSpan averageInterval, double? thresholdMilliseconds, string sourceFilter = null)
{
StringBuilder sb = new StringBuilder($"Performance Counter for {collection.Name}:\n\n");
StringBuilder report = new StringBuilder($"Performance Counter for {collection.Name}:\n\n");
List<KeyValuePair<string, PerformanceCounter>> data = collection.PerformanceCounters.ToList();
@ -466,15 +425,13 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
}
if (thresholdMilliseconds != null)
{
data = data.Where(p => p.Value.GetAverage(averageInterval) >= thresholdMilliseconds).ToList();
}
if (data.Any())
{
sb.AppendLine(this.GetTableString(
report.AppendLine(this.GetTableString(
data: data,
header: new[] {"Mod", $"Avg Exec Time (last {(int) averageInterval.TotalSeconds}s)", "Last Exec Time", "Peak Exec Time", $"Peak Exec Time (last {(int) averageInterval.TotalSeconds}s)"},
header: new[] { "Mod", $"Avg Exec Time (last {(int)averageInterval.TotalSeconds}s)", "Last Exec Time", "Peak Exec Time", $"Peak Exec Time (last {(int)averageInterval.TotalSeconds}s)" },
getRow: item => new[]
{
item.Key,
@ -488,28 +445,13 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
}
else
{
sb.Clear();
sb.AppendLine($"Performance Counter for {collection.Name}: none.");
report.Clear();
report.AppendLine($"Performance Counter for {collection.Name}: none.");
}
monitor.Log(sb.ToString(), LogLevel.Info);
monitor.Log(report.ToString(), LogLevel.Info);
}
/// <summary>Parses a command string and returns the associated command.</summary>
/// <param name="commandString">The command string</param>
/// <returns>The parsed command.</returns>
private SubCommand ParseCommandString(string commandString)
{
foreach (var i in this.SubCommandNames.Where(i =>
i.Value.Any(str => str.Equals(commandString, StringComparison.InvariantCultureIgnoreCase))))
{
return i.Key;
}
return SubCommand.None;
}
/// <summary>Formats the given milliseconds value into a string format. Optionally
/// allows a threshold to return "-" if the value is less than the threshold.</summary>
/// <param name="milliseconds">The milliseconds to format. Returns "-" if null</param>
@ -518,175 +460,212 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
private string FormatMilliseconds(double? milliseconds, double? thresholdMilliseconds = null)
{
if (milliseconds == null || (thresholdMilliseconds != null && milliseconds < thresholdMilliseconds))
{
return "-";
}
return ((double) milliseconds).ToString("F2");
return ((double)milliseconds).ToString("F2");
}
/// <summary>Shows detailed help for a specific sub command.</summary>
/// <param name="monitor">The output monitor</param>
/// <param name="subCommand">The sub command</param>
private void OutputHelp(IMonitor monitor, SubCommand subCommand)
/// <param name="monitor">The output monitor.</param>
/// <param name="subcommand">The subcommand.</param>
private void OutputHelp(IMonitor monitor, SubCommand? subcommand)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine();
StringBuilder report = new StringBuilder();
report.AppendLine();
switch (subCommand)
switch (subcommand)
{
case SubCommand.Concepts:
sb.AppendLine("A performance counter is a metric which measures execution time. Each performance");
sb.AppendLine("counter consists of:");
sb.AppendLine();
sb.AppendLine(" - A source, which typically is a mod or the game itself.");
sb.AppendLine(" - A ring buffer which stores the data points (execution time and time when it was executed)");
sb.AppendLine();
sb.AppendLine("A set of performance counters is organized in a collection to group various areas.");
sb.AppendLine("Per default, collections for all game events [1] are created.");
sb.AppendLine();
sb.AppendLine("Example:");
sb.AppendLine();
sb.AppendLine("The performance counter collection named 'Display.Rendered' contains one performance");
sb.AppendLine("counters when the game executes the 'Display.Rendered' event, and one additional");
sb.AppendLine("performance counter for each mod which handles the 'Display.Rendered' event.");
sb.AppendLine();
sb.AppendLine("[1] https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events");
break;
case SubCommand.Detail:
sb.AppendLine("Usage: pc detail <collection> <source>");
sb.AppendLine(" pc detail <collection> <threshold>");
sb.AppendLine();
sb.AppendLine("Displays details for a specific collection.");
sb.AppendLine();
sb.AppendLine("Arguments:");
sb.AppendLine(" <collection> Required. The full or partial name of the collection to display.");
sb.AppendLine(" <source> Optional. The full or partial name of the source.");
sb.AppendLine(" <threshold> Optional. The threshold in milliseconds. Any average execution time below that");
sb.AppendLine(" threshold is not reported.");
sb.AppendLine();
sb.AppendLine("Examples:");
sb.AppendLine("pc detail Display.Rendering Displays all performance counters for the 'Display.Rendering' collection");
sb.AppendLine("pc detail Display.Rendering Pathoschild.ChestsAnywhere Displays the 'Display.Rendering' performance counter for 'Pathoschild.ChestsAnywhere'");
sb.AppendLine("pc detail Display.Rendering 5 Displays the 'Display.Rendering' performance counters exceeding an average of 5ms");
report.AppendLine("Usage: pc detail <collection> <source>");
report.AppendLine(" pc detail <collection> <threshold>");
report.AppendLine();
report.AppendLine("Displays details for a specific collection.");
report.AppendLine();
report.AppendLine("Arguments:");
report.AppendLine(" <collection> Required. The full or partial name of the collection to display.");
report.AppendLine(" <source> Optional. The full or partial name of the source.");
report.AppendLine(" <threshold> Optional. The threshold in milliseconds. Any average execution time below that");
report.AppendLine(" threshold is not reported.");
report.AppendLine();
report.AppendLine("Examples:");
report.AppendLine("pc detail Display.Rendering Displays all performance counters for the 'Display.Rendering' collection");
report.AppendLine("pc detail Display.Rendering Pathoschild.ChestsAnywhere Displays the 'Display.Rendering' performance counter for 'Pathoschild.ChestsAnywhere'");
report.AppendLine("pc detail Display.Rendering 5 Displays the 'Display.Rendering' performance counters exceeding an average of 5ms");
break;
case SubCommand.Summary:
sb.AppendLine("Usage: pc summary <mode|name> <threshold>");
sb.AppendLine();
sb.AppendLine("Displays the performance counter summary.");
sb.AppendLine();
sb.AppendLine("Arguments:");
sb.AppendLine(" <mode> Optional. Defaults to 'important' if omitted. Specifies one of these modes:");
sb.AppendLine(" - all Displays performance counters from all collections");
sb.AppendLine(" - important Displays only important performance counter collections");
sb.AppendLine();
sb.AppendLine(" <name> Optional. Only shows performance counter collections matching the given name");
sb.AppendLine(" <threshold> Optional. Hides the actual execution time if it is below this threshold");
sb.AppendLine();
sb.AppendLine("Examples:");
sb.AppendLine("pc summary all Shows all events");
sb.AppendLine("pc summary all 5 Shows all events");
sb.AppendLine("pc summary Display.Rendering Shows only the 'Display.Rendering' collection");
report.AppendLine("Usage: pc summary <mode|name> <threshold>");
report.AppendLine();
report.AppendLine("Displays the performance counter summary.");
report.AppendLine();
report.AppendLine("Arguments:");
report.AppendLine(" <mode> Optional. Defaults to 'important' if omitted. Specifies one of these modes:");
report.AppendLine(" - all Displays performance counters from all collections");
report.AppendLine(" - important Displays only important performance counter collections");
report.AppendLine();
report.AppendLine(" <name> Optional. Only shows performance counter collections matching the given name");
report.AppendLine(" <threshold> Optional. Hides the actual execution time if it is below this threshold");
report.AppendLine();
report.AppendLine("Examples:");
report.AppendLine("pc summary all Shows all events");
report.AppendLine("pc summary all 5 Shows all events");
report.AppendLine("pc summary Display.Rendering Shows only the 'Display.Rendering' collection");
break;
case SubCommand.Trigger:
sb.AppendLine("Usage: pc trigger <mode>");
sb.AppendLine("Usage: pc trigger collection <collectionName> <threshold>");
sb.AppendLine("Usage: pc trigger collection <collectionName> <threshold> <sourceName>");
sb.AppendLine();
sb.AppendLine("Manages alert triggers.");
sb.AppendLine();
sb.AppendLine("Arguments:");
sb.AppendLine(" <mode> Optional. Specifies if a specific source or a specific collection should be triggered.");
sb.AppendLine(" - list Lists current triggers");
sb.AppendLine(" - collection Sets up a trigger for a collection");
sb.AppendLine(" - clear Clears all trigger entries");
sb.AppendLine(" - pause Pauses triggering of alerts");
sb.AppendLine(" - resume Resumes triggering of alerts");
sb.AppendLine(" - dump Dumps all triggers as commands for copy and paste");
sb.AppendLine(" Defaults to 'list' if not specified.");
sb.AppendLine();
sb.AppendLine(" <collectionName> Required if the mode 'collection' is specified.");
sb.AppendLine(" Specifies the name of the collection to be triggered. Must be an exact match.");
sb.AppendLine();
sb.AppendLine(" <sourceName> Optional. Specifies the name of a specific source. Must be an exact match.");
sb.AppendLine();
sb.AppendLine(" <threshold> Required if the mode 'collection' is specified.");
sb.AppendLine(" Specifies the threshold in milliseconds (fractions allowed).");
sb.AppendLine(" Specify '0' to remove the threshold.");
sb.AppendLine();
sb.AppendLine("Examples:");
sb.AppendLine();
sb.AppendLine("pc trigger collection Display.Rendering 10");
sb.AppendLine(" Sets up an alert trigger which writes on the console if the execution time of all performance counters in");
sb.AppendLine(" the 'Display.Rendering' collection exceed 10 milliseconds.");
sb.AppendLine();
sb.AppendLine("pc trigger collection Display.Rendering 5 Pathoschild.ChestsAnywhere");
sb.AppendLine(" Sets up an alert trigger to write on the console if the execution time of Pathoschild.ChestsAnywhere in");
sb.AppendLine(" the 'Display.Rendering' collection exceed 5 milliseconds.");
sb.AppendLine();
sb.AppendLine("pc trigger collection Display.Rendering 0");
sb.AppendLine(" Removes the threshold previously defined from the collection. Note that source-specific thresholds are left intact.");
sb.AppendLine();
sb.AppendLine("pc trigger clear");
sb.AppendLine(" Clears all previously setup alert triggers.");
report.AppendLine("Usage: pc trigger <mode>");
report.AppendLine("Usage: pc trigger collection <collectionName> <threshold>");
report.AppendLine("Usage: pc trigger collection <collectionName> <threshold> <sourceName>");
report.AppendLine();
report.AppendLine("Manages alert triggers.");
report.AppendLine();
report.AppendLine("Arguments:");
report.AppendLine(" <mode> Optional. Specifies if a specific source or a specific collection should be triggered.");
report.AppendLine(" - list Lists current triggers");
report.AppendLine(" - collection Sets up a trigger for a collection");
report.AppendLine(" - clear Clears all trigger entries");
report.AppendLine(" - pause Pauses triggering of alerts");
report.AppendLine(" - resume Resumes triggering of alerts");
report.AppendLine(" - dump Dumps all triggers as commands for copy and paste");
report.AppendLine(" Defaults to 'list' if not specified.");
report.AppendLine();
report.AppendLine(" <collectionName> Required if the mode 'collection' is specified.");
report.AppendLine(" Specifies the name of the collection to be triggered. Must be an exact match.");
report.AppendLine();
report.AppendLine(" <sourceName> Optional. Specifies the name of a specific source. Must be an exact match.");
report.AppendLine();
report.AppendLine(" <threshold> Required if the mode 'collection' is specified.");
report.AppendLine(" Specifies the threshold in milliseconds (fractions allowed).");
report.AppendLine(" Specify '0' to remove the threshold.");
report.AppendLine();
report.AppendLine("Examples:");
report.AppendLine();
report.AppendLine("pc trigger collection Display.Rendering 10");
report.AppendLine(" Sets up an alert trigger which writes on the console if the execution time of all performance counters in");
report.AppendLine(" the 'Display.Rendering' collection exceed 10 milliseconds.");
report.AppendLine();
report.AppendLine("pc trigger collection Display.Rendering 5 Pathoschild.ChestsAnywhere");
report.AppendLine(" Sets up an alert trigger to write on the console if the execution time of Pathoschild.ChestsAnywhere in");
report.AppendLine(" the 'Display.Rendering' collection exceed 5 milliseconds.");
report.AppendLine();
report.AppendLine("pc trigger collection Display.Rendering 0");
report.AppendLine(" Removes the threshold previously defined from the collection. Note that source-specific thresholds are left intact.");
report.AppendLine();
report.AppendLine("pc trigger clear");
report.AppendLine(" Clears all previously setup alert triggers.");
break;
case SubCommand.Reset:
sb.AppendLine("Usage: pc reset <type> <name>");
sb.AppendLine();
sb.AppendLine("Resets performance counters.");
sb.AppendLine();
sb.AppendLine("Arguments:");
sb.AppendLine(" <type> Optional. Specifies if a collection or source should be reset.");
sb.AppendLine(" If omitted, all performance counters are reset.");
sb.AppendLine();
sb.AppendLine(" - source Clears performance counters for a specific source");
sb.AppendLine(" - collection Clears performance counters for a specific collection");
sb.AppendLine();
sb.AppendLine(" <name> Required if a <type> is given. Specifies the name of either the collection");
sb.AppendLine(" or the source. The name must be an exact match.");
sb.AppendLine();
sb.AppendLine("Examples:");
sb.AppendLine("pc reset Resets all performance counters");
sb.AppendLine("pc reset source Pathoschild.ChestsAnywhere Resets all performance for the source named Pathoschild.ChestsAnywhere");
sb.AppendLine("pc reset collection Display.Rendering Resets all performance for the collection named Display.Rendering");
report.AppendLine("Usage: pc reset <type> <name>");
report.AppendLine();
report.AppendLine("Resets performance counters.");
report.AppendLine();
report.AppendLine("Arguments:");
report.AppendLine(" <type> Optional. Specifies if a collection or source should be reset.");
report.AppendLine(" If omitted, all performance counters are reset.");
report.AppendLine();
report.AppendLine(" - source Clears performance counters for a specific source");
report.AppendLine(" - collection Clears performance counters for a specific collection");
report.AppendLine();
report.AppendLine(" <name> Required if a <type> is given. Specifies the name of either the collection");
report.AppendLine(" or the source. The name must be an exact match.");
report.AppendLine();
report.AppendLine("Examples:");
report.AppendLine("pc reset Resets all performance counters");
report.AppendLine("pc reset source Pathoschild.ChestsAnywhere Resets all performance for the source named Pathoschild.ChestsAnywhere");
report.AppendLine("pc reset collection Display.Rendering Resets all performance for the collection named Display.Rendering");
break;
}
sb.AppendLine();
monitor.Log(sb.ToString(), LogLevel.Info);
report.AppendLine();
monitor.Log(report.ToString(), LogLevel.Info);
}
/// <summary>Get the command description.</summary>
private static string GetDescription()
{
StringBuilder sb = new StringBuilder();
StringBuilder report = new StringBuilder();
sb.AppendLine("Displays and configures performance counters.");
sb.AppendLine();
sb.AppendLine("A performance counter records the invocation time of in-game events being");
sb.AppendLine("processed by mods or the game itself. See 'concepts' for a detailed explanation.");
sb.AppendLine();
sb.AppendLine("Usage: pc <command> <action>");
sb.AppendLine();
sb.AppendLine("Commands:");
sb.AppendLine();
sb.AppendLine(" summary|sum|s Displays a summary of important or all collections");
sb.AppendLine(" detail|d Shows performance counter information for a given collection");
sb.AppendLine(" reset|r Resets the performance counters");
sb.AppendLine(" trigger Configures alert triggers");
sb.AppendLine(" enable Enables performance counter recording");
sb.AppendLine(" disable Disables performance counter recording");
sb.AppendLine(" examples Displays various examples");
sb.AppendLine(" concepts Displays an explanation of the performance counter concepts");
sb.AppendLine(" help Displays verbose help for the available commands");
sb.AppendLine();
sb.AppendLine("To get help for a specific command, use 'pc help <command>', for example:");
sb.AppendLine("pc help summary");
sb.AppendLine();
sb.AppendLine("Defaults to summary if no command is given.");
sb.AppendLine();
report.AppendLine("Displays or configures performance monitoring for diagnose issues.");
report.AppendLine();
report.AppendLine("A 'performance counter' is a metric which measures execution time across a range of time for a source (e.g. a mod).");
report.AppendLine("A set of performance counters is organized in a collection to group various areas.");
report.AppendLine("For example, the performance counter collection named 'Display.Rendered' contains one performance");
report.AppendLine("counter when the game executes the 'Display.Rendered' event, and another counter for each mod which handles it.");
report.AppendLine();
report.AppendLine("Usage: pc <command> <action>");
report.AppendLine();
report.AppendLine("Commands:");
report.AppendLine();
report.AppendLine(" summary Show a summary of collections.");
report.AppendLine(" detail Show a summary for a given collection.");
report.AppendLine(" reset Reset all performance counters.");
report.AppendLine(" trigger Configure alert triggers.");
report.AppendLine(" enable Enable performance counter recording.");
report.AppendLine(" disable Disable performance counter recording.");
report.AppendLine(" help Show verbose help for the available commands.");
report.AppendLine();
report.AppendLine("To get help for a specific command, use 'pc help <command>', for example:");
report.AppendLine("pc help summary");
report.AppendLine();
report.AppendLine("Defaults to summary if no command is given.");
report.AppendLine();
return sb.ToString();
return report.ToString();
}
/*********
** Private models
*********/
/// <summary>An alert trigger for a collection.</summary>
private class CollectionTrigger
{
/*********
** Accessors
*********/
/// <summary>The collection name.</summary>
public string CollectionName { get; }
/// <summary>The trigger threshold.</summary>
public double Threshold { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="collectionName">The collection name.</param>
/// <param name="threshold">The trigger threshold.</param>
public CollectionTrigger(string collectionName, double threshold)
{
this.CollectionName = collectionName;
this.Threshold = threshold;
}
}
/// <summary>An alert triggered for a source.</summary>
private class SourceTrigger : CollectionTrigger
{
/*********
** Accessors
*********/
/// <summary>The source name.</summary>
public string SourceName { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="collectionName">The collection name.</param>
/// <param name="sourceName">The source name.</param>
/// <param name="threshold">The trigger threshold.</param>
public SourceTrigger(string collectionName, string sourceName, double threshold)
: base(collectionName, threshold)
{
this.SourceName = sourceName;
}
}
}
}

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
@ -66,7 +66,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
/// <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>
/// <param name="rightAlign">True to right-align the data, false for left-align. Default false.</param>
/// <param name="rightAlign">Whether to right-align the data.</param>
protected string GetTableString<T>(IEnumerable<T> data, string[] header, Func<T, string[]> getRow, bool rightAlign = false)
{
// get table data
@ -93,19 +93,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
};
lines.AddRange(rows);
if (rightAlign)
{
return string.Join(
Environment.NewLine,
lines.Select(line => string.Join(" | ", line.Select((field, i) => field.PadLeft(widths[i], ' ')).ToArray())
)
);
}
return string.Join(
Environment.NewLine,
lines.Select(line => string.Join(" | ", line.Select((field, i) => field.PadRight(widths[i], ' ')).ToArray())
)
lines.Select(line => string.Join(" | ",
line.Select((field, i) => rightAlign ? field.PadRight(widths[i], ' ') : field.PadLeft(widths[i], ' '))
))
);
}
}

View File

@ -67,10 +67,6 @@
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
</ItemGroup>
<Import Project="..\SMAPI.Internal\SMAPI.Internal.projitems" Label="Shared" />
<Import Project="..\..\build\common.targets" />

View File

@ -55,7 +55,7 @@ namespace StardewModdingAPI
/// <summary>The URL of the SMAPI home page.</summary>
internal const string HomePageUrl = "https://smapi.io";
/// <summary>The URL of the SMAPI home page.</summary>
/// <summary>The default performance counter name for unknown event handlers.</summary>
internal const string GamePerformanceCounterName = "<StardewValley>";
/// <summary>The absolute path to the folder containing SMAPI's internal files.</summary>
@ -103,6 +103,7 @@ namespace StardewModdingAPI
/// <summary>The language code for non-translated mod assets.</summary>
internal static LocalizedContentManager.LanguageCode DefaultLanguage { get; } = LocalizedContentManager.LanguageCode.en;
/*********
** Internal methods
*********/

View File

@ -1,4 +1,6 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using StardewModdingAPI.Events;
using StardewModdingAPI.Framework.PerformanceCounter;
@ -174,29 +176,32 @@ 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>
/// <param name="performanceCounterManager">The performance counter manager.</param>
public EventManager(IMonitor monitor, ModRegistry modRegistry, PerformanceCounterManager performanceCounterManager)
/// <param name="performanceMonitor">Tracks performance metrics.</param>
public EventManager(IMonitor monitor, ModRegistry modRegistry, PerformanceMonitor performanceMonitor)
{
// create shortcut initializers
ManagedEvent<TEventArgs> ManageEventOf<TEventArgs>(string typeName, string eventName) => new ManagedEvent<TEventArgs>($"{typeName}.{eventName}", monitor, modRegistry, performanceCounterManager);
ManagedEvent<TEventArgs> ManageEventOf<TEventArgs>(string typeName, string eventName, bool isPerformanceCritical = false)
{
return new ManagedEvent<TEventArgs>($"{typeName}.{eventName}", monitor, modRegistry, performanceMonitor, isPerformanceCritical);
}
// init events (new)
this.MenuChanged = ManageEventOf<MenuChangedEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.MenuChanged));
this.Rendering = ManageEventOf<RenderingEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendering));
this.Rendered = ManageEventOf<RenderedEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendered));
this.RenderingWorld = ManageEventOf<RenderingWorldEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingWorld));
this.RenderedWorld = ManageEventOf<RenderedWorldEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedWorld));
this.RenderingActiveMenu = ManageEventOf<RenderingActiveMenuEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingActiveMenu));
this.RenderedActiveMenu = ManageEventOf<RenderedActiveMenuEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedActiveMenu));
this.RenderingHud = ManageEventOf<RenderingHudEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingHud));
this.RenderedHud = ManageEventOf<RenderedHudEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedHud));
this.Rendering = ManageEventOf<RenderingEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendering), isPerformanceCritical: true);
this.Rendered = ManageEventOf<RenderedEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendered), isPerformanceCritical: true);
this.RenderingWorld = ManageEventOf<RenderingWorldEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingWorld), isPerformanceCritical: true);
this.RenderedWorld = ManageEventOf<RenderedWorldEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedWorld), isPerformanceCritical: true);
this.RenderingActiveMenu = ManageEventOf<RenderingActiveMenuEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingActiveMenu), isPerformanceCritical: true);
this.RenderedActiveMenu = ManageEventOf<RenderedActiveMenuEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedActiveMenu), isPerformanceCritical: true);
this.RenderingHud = ManageEventOf<RenderingHudEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingHud), isPerformanceCritical: true);
this.RenderedHud = ManageEventOf<RenderedHudEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedHud), isPerformanceCritical: true);
this.WindowResized = ManageEventOf<WindowResizedEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.WindowResized));
this.GameLaunched = ManageEventOf<GameLaunchedEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.GameLaunched));
this.UpdateTicking = ManageEventOf<UpdateTickingEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicking));
this.UpdateTicked = ManageEventOf<UpdateTickedEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicked));
this.OneSecondUpdateTicking = ManageEventOf<OneSecondUpdateTickingEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.OneSecondUpdateTicking));
this.OneSecondUpdateTicked = ManageEventOf<OneSecondUpdateTickedEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.OneSecondUpdateTicked));
this.UpdateTicking = ManageEventOf<UpdateTickingEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicking), isPerformanceCritical: true);
this.UpdateTicked = ManageEventOf<UpdateTickedEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicked), isPerformanceCritical: true);
this.OneSecondUpdateTicking = ManageEventOf<OneSecondUpdateTickingEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.OneSecondUpdateTicking), isPerformanceCritical: true);
this.OneSecondUpdateTicked = ManageEventOf<OneSecondUpdateTickedEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.OneSecondUpdateTicked), isPerformanceCritical: true);
this.SaveCreating = ManageEventOf<SaveCreatingEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.SaveCreating));
this.SaveCreated = ManageEventOf<SaveCreatedEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.SaveCreated));
this.Saving = ManageEventOf<SavingEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Saving));
@ -209,7 +214,7 @@ namespace StardewModdingAPI.Framework.Events
this.ButtonPressed = ManageEventOf<ButtonPressedEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed));
this.ButtonReleased = ManageEventOf<ButtonReleasedEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased));
this.CursorMoved = ManageEventOf<CursorMovedEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved));
this.CursorMoved = ManageEventOf<CursorMovedEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved), isPerformanceCritical: true);
this.MouseWheelScrolled = ManageEventOf<MouseWheelScrolledEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.MouseWheelScrolled));
this.PeerContextReceived = ManageEventOf<PeerContextReceivedEventArgs>(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.PeerContextReceived));
@ -230,8 +235,15 @@ namespace StardewModdingAPI.Framework.Events
this.TerrainFeatureListChanged = ManageEventOf<TerrainFeatureListChangedEventArgs>(nameof(IModEvents.World), nameof(IWorldEvents.TerrainFeatureListChanged));
this.LoadStageChanged = ManageEventOf<LoadStageChangedEventArgs>(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.LoadStageChanged));
this.UnvalidatedUpdateTicking = ManageEventOf<UnvalidatedUpdateTickingEventArgs>(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.UnvalidatedUpdateTicking));
this.UnvalidatedUpdateTicked = ManageEventOf<UnvalidatedUpdateTickedEventArgs>(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.UnvalidatedUpdateTicked));
this.UnvalidatedUpdateTicking = ManageEventOf<UnvalidatedUpdateTickingEventArgs>(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.UnvalidatedUpdateTicking), isPerformanceCritical: true);
this.UnvalidatedUpdateTicked = ManageEventOf<UnvalidatedUpdateTickedEventArgs>(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.UnvalidatedUpdateTicked), isPerformanceCritical: true);
}
/// <summary>Get all managed events.</summary>
public IEnumerable<IManagedEvent> GetAllEvents()
{
foreach (FieldInfo field in this.GetType().GetFields())
yield return (IManagedEvent)field.GetValue(this);
}
}
}

View File

@ -1,7 +1,15 @@
namespace StardewModdingAPI.Framework.Events
{
/// <summary>Metadata for an event raised by SMAPI.</summary>
internal interface IManagedEvent
{
string GetName();
/*********
** Accessors
*********/
/// <summary>A human-readable name for the event.</summary>
string EventName { get; }
/// <summary>Whether the event is typically called at least once per second.</summary>
bool IsPerformanceCritical { get; }
}
}

View File

@ -1,13 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using PerformanceCounterManager = StardewModdingAPI.Framework.PerformanceCounter.PerformanceCounterManager;
using StardewModdingAPI.Framework.PerformanceCounter;
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>: IManagedEvent
internal class ManagedEvent<TEventArgs> : IManagedEvent
{
/*********
** Fields
@ -15,9 +15,6 @@ namespace StardewModdingAPI.Framework.Events
/// <summary>The underlying event.</summary>
private event EventHandler<TEventArgs> Event;
/// <summary>A human-readable name for the event.</summary>
private readonly string EventName;
/// <summary>Writes messages to the log.</summary>
private readonly IMonitor Monitor;
@ -30,8 +27,19 @@ namespace StardewModdingAPI.Framework.Events
/// <summary>The cached invocation list.</summary>
private EventHandler<TEventArgs>[] CachedInvocationList;
/// <summary>The performance counter manager.</summary>
private readonly PerformanceCounterManager PerformanceCounterManager;
/// <summary>Tracks performance metrics.</summary>
private readonly PerformanceMonitor PerformanceMonitor;
/*********
** Accessors
*********/
/// <summary>A human-readable name for the event.</summary>
public string EventName { get; }
/// <summary>Whether the event is typically called at least once per second.</summary>
public bool IsPerformanceCritical { get; }
/*********
** Public methods
@ -40,19 +48,15 @@ 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>
/// <param name="performanceCounterManager">The performance counter manager</param>
public ManagedEvent(string eventName, IMonitor monitor, ModRegistry modRegistry, PerformanceCounterManager performanceCounterManager)
/// <param name="performanceMonitor">Tracks performance metrics.</param>
/// <param name="isPerformanceCritical">Whether the event is typically called at least once per second.</param>
public ManagedEvent(string eventName, IMonitor monitor, ModRegistry modRegistry, PerformanceMonitor performanceMonitor, bool isPerformanceCritical = false)
{
this.EventName = eventName;
this.Monitor = monitor;
this.ModRegistry = modRegistry;
this.PerformanceCounterManager = performanceCounterManager;
}
/// <summary>Gets the event name.</summary>
public string GetName()
{
return this.EventName;
this.PerformanceMonitor = performanceMonitor;
this.IsPerformanceCritical = isPerformanceCritical;
}
/// <summary>Get whether anything is listening to the event.</summary>
@ -93,22 +97,20 @@ namespace StardewModdingAPI.Framework.Events
return;
this.PerformanceCounterManager.BeginTrackInvocation(this.EventName);
foreach (EventHandler<TEventArgs> handler in this.CachedInvocationList)
this.PerformanceMonitor.Track(this.EventName, () =>
{
try
foreach (EventHandler<TEventArgs> handler in this.CachedInvocationList)
{
this.PerformanceCounterManager.Track(this.EventName, this.GetModNameForPerformanceCounters(handler),
() => handler.Invoke(null, args));
try
{
this.PerformanceMonitor.Track(this.EventName, this.GetModNameForPerformanceCounters(handler), () => handler.Invoke(null, args));
}
catch (Exception ex)
{
this.LogError(handler, ex);
}
}
catch (Exception ex)
{
this.LogError(handler, ex);
}
}
this.PerformanceCounterManager.EndTrackInvocation(this.EventName);
});
}
/// <summary>Raise the event and notify all handlers.</summary>
@ -139,18 +141,17 @@ namespace StardewModdingAPI.Framework.Events
/*********
** Private methods
*********/
/// <summary>Get the mod name for a given event handler to display in performance monitoring reports.</summary>
/// <param name="handler">The event handler.</param>
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;
return mod.HasManifest()
? mod.Manifest.UniqueID
: mod.DisplayName;
}
/// <summary>Track an event handler.</summary>

View File

@ -3,13 +3,20 @@ namespace StardewModdingAPI.Framework.PerformanceCounter
/// <summary>The context for an alert.</summary>
internal struct AlertContext
{
/*********
** Accessors
*********/
/// <summary>The source which triggered the alert.</summary>
public readonly string Source;
public string Source { get; }
/// <summary>The elapsed milliseconds.</summary>
public readonly double Elapsed;
public double Elapsed { get; }
/// <summary>Creates a new alert context.</summary>
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="source">The source which triggered the alert.</param>
/// <param name="elapsed">The elapsed milliseconds.</param>
public AlertContext(string source, double elapsed)
@ -18,6 +25,7 @@ namespace StardewModdingAPI.Framework.PerformanceCounter
this.Elapsed = elapsed;
}
/// <summary>Get a human-readable text form of this instance.</summary>
public override string ToString()
{
return $"{this.Source}: {this.Elapsed:F2}ms";

View File

@ -1,28 +1,33 @@
using System.Collections.Generic;
namespace StardewModdingAPI.Framework.PerformanceCounter
{
/// <summary>A single alert entry.</summary>
internal struct AlertEntry
{
/*********
** Accessors
*********/
/// <summary>The collection in which the alert occurred.</summary>
public readonly PerformanceCounterCollection Collection;
public PerformanceCounterCollection Collection { get; }
/// <summary>The actual execution time in milliseconds.</summary>
public readonly double ExecutionTimeMilliseconds;
public double ExecutionTimeMilliseconds { get; }
/// <summary>The configured alert threshold. </summary>
public readonly double ThresholdMilliseconds;
/// <summary>The configured alert threshold in milliseconds.</summary>
public double ThresholdMilliseconds { get; }
/// <summary>The context list, which records all sources involved in exceeding the threshold.</summary>
public readonly List<AlertContext> Context;
/// <summary>The sources involved in exceeding the threshold.</summary>
public AlertContext[] Context { get; }
/// <summary>Creates a new alert entry.</summary>
/// <param name="collection">The source collection in which the alert occurred.</param>
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="collection">The collection in which the alert occurred.</param>
/// <param name="executionTimeMilliseconds">The actual execution time in milliseconds.</param>
/// <param name="thresholdMilliseconds">The configured threshold in milliseconds.</param>
/// <param name="context">A list of AlertContext to record which sources were involved</param>
public AlertEntry(PerformanceCounterCollection collection, double executionTimeMilliseconds, double thresholdMilliseconds, List<AlertContext> context)
/// <param name="thresholdMilliseconds">The configured alert threshold in milliseconds.</param>
/// <param name="context">The sources involved in exceeding the threshold.</param>
public AlertEntry(PerformanceCounterCollection collection, double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext[] context)
{
this.Collection = collection;
this.ExecutionTimeMilliseconds = executionTimeMilliseconds;

View File

@ -1,16 +0,0 @@
using StardewModdingAPI.Framework.Events;
namespace StardewModdingAPI.Framework.PerformanceCounter
{
/// <summary>Represents a performance counter collection specific to game events.</summary>
internal class EventPerformanceCounterCollection: PerformanceCounterCollection
{
/// <summary>Creates a new event performance counter collection.</summary>
/// <param name="manager">The performance counter manager.</param>
/// <param name="event">The ManagedEvent.</param>
/// <param name="isImportant">If the event is flagged as important.</param>
public EventPerformanceCounterCollection(PerformanceCounterManager manager, IManagedEvent @event, bool isImportant) : base(manager, @event.GetName(), isImportant)
{
}
}
}

View File

@ -1,20 +1,31 @@
using System;
using System.Collections.Generic;
namespace StardewModdingAPI.Framework.PerformanceCounter
{
/// <summary>A peak invocation time.</summary>
internal struct PeakEntry
{
/*********
** Accessors
*********/
/// <summary>The actual execution time in milliseconds.</summary>
public readonly double ExecutionTimeMilliseconds;
public double ExecutionTimeMilliseconds { get; }
/// <summary>The DateTime when the entry occured.</summary>
public DateTime EventTime;
/// <summary>When the entry occurred.</summary>
public DateTime EventTime { get; }
/// <summary>The context list, which records all sources involved in exceeding the threshold.</summary>
public readonly List<AlertContext> Context;
/// <summary>The sources involved in exceeding the threshold.</summary>
public AlertContext[] Context { get; }
public PeakEntry(double executionTimeMilliseconds, DateTime eventTime, List<AlertContext> context)
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="executionTimeMilliseconds">The actual execution time in milliseconds.</param>
/// <param name="eventTime">When the entry occurred.</param>
/// <param name="context">The sources involved in exceeding the threshold.</param>
public PeakEntry(double executionTimeMilliseconds, DateTime eventTime, AlertContext[] context)
{
this.ExecutionTimeMilliseconds = executionTimeMilliseconds;
this.EventTime = eventTime;

View File

@ -1,23 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Cyotek.Collections.Generic;
using Harmony;
namespace StardewModdingAPI.Framework.PerformanceCounter
{
/// <summary>Tracks metadata about a particular code event.</summary>
internal class PerformanceCounter
{
/*********
** Fields
*********/
/// <summary>The size of the ring buffer.</summary>
private const int MAX_ENTRIES = 16384;
private readonly int MaxEntries = 16384;
/// <summary>The collection to which this performance counter belongs.</summary>
private readonly PerformanceCounterCollection ParentCollection;
/// <summary>The circular buffer which stores all performance counter entries</summary>
private readonly CircularBuffer<PerformanceCounterEntry> _counter;
/// <summary>The performance counter entries.</summary>
private readonly Stack<PerformanceCounterEntry> Entries;
/// <summary>The peak execution time</summary>
/// <summary>The entry with the highest execution time.</summary>
private PerformanceCounterEntry? PeakPerformanceCounterEntry;
/*********
** Accessors
*********/
/// <summary>The name of the source.</summary>
public string Source { get; }
@ -27,118 +36,90 @@ namespace StardewModdingAPI.Framework.PerformanceCounter
/// <summary>If alerting is enabled or not</summary>
public bool EnableAlerts { get; set; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="parentCollection">The collection to which this performance counter belongs.</param>
/// <param name="source">The name of the source.</param>
public PerformanceCounter(PerformanceCounterCollection parentCollection, string source)
{
this.ParentCollection = parentCollection;
this.Source = source;
this._counter = new CircularBuffer<PerformanceCounterEntry>(PerformanceCounter.MAX_ENTRIES);
this.Entries = new Stack<PerformanceCounterEntry>(this.MaxEntries);
}
/// <summary>Adds a new performance counter entry to the list. Updates the peak entry and adds an alert if
/// monitoring is enabled and the execution time exceeds the threshold.</summary>
/// <summary>Add a performance counter entry to the list, update monitoring, and raise alerts if needed.</summary>
/// <param name="entry">The entry to add.</param>
public void Add(PerformanceCounterEntry entry)
{
this._counter.Put(entry);
// add entry
if (this.Entries.Count > this.MaxEntries)
this.Entries.Pop();
this.Entries.Add(entry);
if (this.EnableAlerts && entry.ElapsedMilliseconds > this.AlertThresholdMilliseconds)
this.ParentCollection.AddAlert(entry.ElapsedMilliseconds, this.AlertThresholdMilliseconds,
new AlertContext(this.Source, entry.ElapsedMilliseconds));
if (this.PeakPerformanceCounterEntry == null)
// update metrics
if (this.PeakPerformanceCounterEntry == null || entry.ElapsedMilliseconds > this.PeakPerformanceCounterEntry.Value.ElapsedMilliseconds)
this.PeakPerformanceCounterEntry = entry;
else
{
if (entry.ElapsedMilliseconds > this.PeakPerformanceCounterEntry.Value.ElapsedMilliseconds)
this.PeakPerformanceCounterEntry = entry;
}
// raise alert
if (this.EnableAlerts && entry.ElapsedMilliseconds > this.AlertThresholdMilliseconds)
this.ParentCollection.AddAlert(entry.ElapsedMilliseconds, this.AlertThresholdMilliseconds, new AlertContext(this.Source, entry.ElapsedMilliseconds));
}
/// <summary>Clears all performance counter entries and resets the peak entry.</summary>
/// <summary>Clear all performance counter entries and monitoring.</summary>
public void Reset()
{
this._counter.Clear();
this.Entries.Clear();
this.PeakPerformanceCounterEntry = null;
}
/// <summary>Returns the peak entry.</summary>
/// <returns>The peak entry.</returns>
/// <summary>Get the peak entry.</summary>
public PerformanceCounterEntry? GetPeak()
{
return this.PeakPerformanceCounterEntry;
}
/// <summary>Returns the peak entry.</summary>
/// <returns>The peak entry.</returns>
public PerformanceCounterEntry? GetPeak(TimeSpan range, DateTime? relativeTo = null)
/// <summary>Get the entry with the highest execution time.</summary>
/// <param name="range">The time range to search.</param>
/// <param name="endTime">The end time for the <paramref name="range"/>, or null for the current time.</param>
public PerformanceCounterEntry? GetPeak(TimeSpan range, DateTime? endTime = null)
{
if (this._counter.IsEmpty)
return null;
endTime ??= DateTime.UtcNow;
DateTime startTime = endTime.Value.Subtract(range);
if (relativeTo == null)
relativeTo = DateTime.UtcNow;
DateTime start = relativeTo.Value.Subtract(range);
var entries = this._counter.Where(x => (x.EventTime >= start) && (x.EventTime <= relativeTo)).ToList();
if (!entries.Any())
return null;
return entries.OrderByDescending(x => x.ElapsedMilliseconds).First();
return this.Entries
.Where(entry => entry.EventTime >= startTime && entry.EventTime <= endTime)
.OrderByDescending(x => x.ElapsedMilliseconds)
.FirstOrDefault();
}
/// <summary>Resets the peak entry.</summary>
public void ResetPeak()
{
this.PeakPerformanceCounterEntry = null;
}
/// <summary>Returns the last entry added to the list.</summary>
/// <returns>The last entry</returns>
/// <summary>Get the last entry added to the list.</summary>
public PerformanceCounterEntry? GetLastEntry()
{
if (this._counter.IsEmpty)
if (this.Entries.Count == 0)
return null;
return this._counter.PeekLast();
return this.Entries.Peek();
}
/// <summary>Returns the average execution time of all entries.</summary>
/// <returns>The average execution time in milliseconds.</returns>
public double GetAverage()
/// <summary>Get the average over a given time span.</summary>
/// <param name="range">The time range to search.</param>
/// <param name="endTime">The end time for the <paramref name="range"/>, or null for the current time.</param>
public double GetAverage(TimeSpan range, DateTime? endTime = null)
{
if (this._counter.IsEmpty)
return 0;
endTime ??= DateTime.UtcNow;
DateTime startTime = endTime.Value.Subtract(range);
return this._counter.Average(p => p.ElapsedMilliseconds);
}
double[] entries = this.Entries
.Where(entry => entry.EventTime >= startTime && entry.EventTime <= endTime)
.Select(p => p.ElapsedMilliseconds)
.ToArray();
/// <summary>Returns the average over a given time span.</summary>
/// <param name="range">The time range to retrieve.</param>
/// <param name="relativeTo">The DateTime from which to start the average. Defaults to DateTime.UtcNow if null</param>
/// <returns>The average execution time in milliseconds.</returns>
/// <remarks>
/// The relativeTo parameter specifies from which point in time the range is subtracted. Example:
/// If DateTime is set to 60 seconds ago, and the range is set to 60 seconds, the method would return
/// the average between all entries between 120s ago and 60s ago.
/// </remarks>
public double GetAverage(TimeSpan range, DateTime? relativeTo = null)
{
if (this._counter.IsEmpty)
return 0;
if (relativeTo == null)
relativeTo = DateTime.UtcNow;
DateTime start = relativeTo.Value.Subtract(range);
var entries = this._counter.Where(x => (x.EventTime >= start) && (x.EventTime <= relativeTo)).ToList();
if (!entries.Any())
return 0;
return entries.Average(x => x.ElapsedMilliseconds);
return entries.Length > 0
? entries.Average()
: 0;
}
}
}

View File

@ -2,153 +2,129 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Cyotek.Collections.Generic;
namespace StardewModdingAPI.Framework.PerformanceCounter
{
internal class PerformanceCounterCollection
{
/// <summary>The size of the ring buffer.</summary>
private const int MAX_ENTRIES = 16384;
/*********
** Fields
*********/
/// <summary>The number of peak invocations to keep.</summary>
private readonly int MaxEntries = 16384;
/// <summary>The list of triggered performance counters.</summary>
/// <summary>The sources involved in exceeding alert thresholds.</summary>
private readonly List<AlertContext> TriggeredPerformanceCounters = new List<AlertContext>();
/// <summary>The stopwatch used to track the invocation time.</summary>
private readonly Stopwatch InvocationStopwatch = new Stopwatch();
/// <summary>The performance counter manager.</summary>
private readonly PerformanceCounterManager PerformanceCounterManager;
private readonly PerformanceMonitor PerformanceMonitor;
/// <summary>Holds the time to calculate the average calls per second.</summary>
/// <summary>The time to calculate average calls per second.</summary>
private DateTime CallsPerSecondStart = DateTime.UtcNow;
/// <summary>The number of invocations of this collection.</summary>
/// <summary>The number of invocations.</summary>
private long CallCount;
/// <summary>The circular buffer which stores all peak invocations</summary>
private readonly CircularBuffer<PeakEntry> PeakInvocations;
/// <summary>The peak invocations.</summary>
private readonly Stack<PeakEntry> PeakInvocations;
/*********
** Accessors
*********/
/// <summary>The associated performance counters.</summary>
public IDictionary<string, PerformanceCounter> PerformanceCounters { get; } = new Dictionary<string, PerformanceCounter>();
/// <summary>The name of this collection.</summary>
public string Name { get; }
/// <summary>Flag if this collection is important (used for the console summary command).</summary>
public bool IsImportant { get; }
/// <summary>Whether the source is typically invoked at least once per second.</summary>
public bool IsPerformanceCritical { get; }
/// <summary>The alert threshold in milliseconds.</summary>
public double AlertThresholdMilliseconds { get; set; }
/// <summary>If alerting is enabled or not</summary>
/// <summary>Whether alerts are enabled.</summary>
public bool EnableAlerts { get; set; }
public PerformanceCounterCollection(PerformanceCounterManager performanceCounterManager, string name, bool isImportant)
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="performanceMonitor">The performance counter manager.</param>
/// <param name="name">The name of this collection.</param>
/// <param name="isPerformanceCritical">Whether the source is typically invoked at least once per second.</param>
public PerformanceCounterCollection(PerformanceMonitor performanceMonitor, string name, bool isPerformanceCritical = false)
{
this.PeakInvocations = new CircularBuffer<PeakEntry>(PerformanceCounterCollection.MAX_ENTRIES);
this.PeakInvocations = new Stack<PeakEntry>(this.MaxEntries);
this.Name = name;
this.PerformanceCounterManager = performanceCounterManager;
this.IsImportant = isImportant;
this.PerformanceMonitor = performanceMonitor;
this.IsPerformanceCritical = isPerformanceCritical;
}
public PerformanceCounterCollection(PerformanceCounterManager performanceCounterManager, string name)
{
this.PeakInvocations = new CircularBuffer<PeakEntry>(PerformanceCounterCollection.MAX_ENTRIES);
this.PerformanceCounterManager = performanceCounterManager;
this.Name = name;
}
/// <summary>Tracks a single invocation for a named source.</summary>
/// <summary>Track a single invocation for a named source.</summary>
/// <param name="source">The name of the source.</param>
/// <param name="entry">The entry.</param>
public void Track(string source, PerformanceCounterEntry entry)
{
// add entry
if (!this.PerformanceCounters.ContainsKey(source))
this.PerformanceCounters.Add(source, new PerformanceCounter(this, source));
this.PerformanceCounters[source].Add(entry);
// raise alert
if (this.EnableAlerts)
this.TriggeredPerformanceCounters.Add(new AlertContext(source, entry.ElapsedMilliseconds));
}
/// <summary>Returns the average execution time for all non-game internal sources.</summary>
/// <returns>The average execution time in milliseconds</returns>
public double GetModsAverageExecutionTime()
{
return this.PerformanceCounters.Where(p =>
p.Key != Constants.GamePerformanceCounterName).Sum(p => p.Value.GetAverage());
}
/// <summary>Returns the average execution time for all non-game internal sources.</summary>
/// <summary>Get the average execution time for all non-game internal sources in milliseconds.</summary>
/// <param name="interval">The interval for which to get the average, relative to now</param>
/// <returns>The average execution time in milliseconds</returns>
public double GetModsAverageExecutionTime(TimeSpan interval)
{
return this.PerformanceCounters.Where(p =>
p.Key != Constants.GamePerformanceCounterName).Sum(p => p.Value.GetAverage(interval));
return this.PerformanceCounters
.Where(entry => entry.Key != Constants.GamePerformanceCounterName)
.Sum(entry => entry.Value.GetAverage(interval));
}
/// <summary>Returns the overall average execution time.</summary>
/// <returns>The average execution time in milliseconds</returns>
public double GetAverageExecutionTime()
{
return this.PerformanceCounters.Sum(p => p.Value.GetAverage());
}
/// <summary>Returns the overall average execution time.</summary>
/// <summary>Get the overall average execution time in milliseconds.</summary>
/// <param name="interval">The interval for which to get the average, relative to now</param>
/// <returns>The average execution time in milliseconds</returns>
public double GetAverageExecutionTime(TimeSpan interval)
{
return this.PerformanceCounters.Sum(p => p.Value.GetAverage(interval));
return this.PerformanceCounters
.Sum(entry => entry.Value.GetAverage(interval));
}
/// <summary>Returns the average execution time for game-internal sources.</summary>
/// <returns>The average execution time in milliseconds</returns>
public double GetGameAverageExecutionTime()
{
if (this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter gameExecTime))
return gameExecTime.GetAverage();
return 0;
}
/// <summary>Returns the average execution time for game-internal sources.</summary>
/// <returns>The average execution time in milliseconds</returns>
/// <summary>Get the average execution time for game-internal sources in milliseconds.</summary>
public double GetGameAverageExecutionTime(TimeSpan interval)
{
if (this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter gameExecTime))
return gameExecTime.GetAverage(interval);
return 0;
return this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter gameExecTime)
? gameExecTime.GetAverage(interval)
: 0;
}
/// <summary>Returns the peak execution time</summary>
/// <param name="interval">The interval for which to get the peak, relative to <paramref name="relativeTo"/></param>
/// <param name="relativeTo">The DateTime which the <paramref name="interval"/> is relative to, or DateTime.Now if not given</param>
/// <returns>The peak execution time</returns>
public double GetPeakExecutionTime(TimeSpan range, DateTime? relativeTo = null)
/// <summary>Get the peak execution time in milliseconds.</summary>
/// <param name="range">The time range to search.</param>
/// <param name="endTime">The end time for the <paramref name="range"/>, or null for the current time.</param>
public double GetPeakExecutionTime(TimeSpan range, DateTime? endTime = null)
{
if (this.PeakInvocations.IsEmpty)
if (this.PeakInvocations.Count == 0)
return 0;
if (relativeTo == null)
relativeTo = DateTime.UtcNow;
endTime ??= DateTime.UtcNow;
DateTime startTime = endTime.Value.Subtract(range);
DateTime start = relativeTo.Value.Subtract(range);
var entries = this.PeakInvocations.Where(x => (x.EventTime >= start) && (x.EventTime <= relativeTo)).ToList();
if (!entries.Any())
return 0;
return entries.OrderByDescending(x => x.ExecutionTimeMilliseconds).First().ExecutionTimeMilliseconds;
return this.PeakInvocations
.Where(entry => entry.EventTime >= startTime && entry.EventTime <= endTime)
.OrderByDescending(x => x.ExecutionTimeMilliseconds)
.Select(p => p.ExecutionTimeMilliseconds)
.FirstOrDefault();
}
/// <summary>Begins tracking the invocation of this collection.</summary>
/// <summary>Start tracking the invocation of this collection.</summary>
public void BeginTrackInvocation()
{
this.TriggeredPerformanceCounters.Clear();
@ -158,60 +134,58 @@ namespace StardewModdingAPI.Framework.PerformanceCounter
this.CallCount++;
}
/// <summary>Ends tracking the invocation of this collection. Also records an alert if alerting is enabled
/// and the invocation time exceeds the threshold.</summary>
/// <summary>End tracking the invocation of this collection, and raise an alert if needed.</summary>
public void EndTrackInvocation()
{
this.InvocationStopwatch.Stop();
this.PeakInvocations.Put(
new PeakEntry(this.InvocationStopwatch.Elapsed.TotalMilliseconds,
DateTime.UtcNow,
this.TriggeredPerformanceCounters));
// add invocation
if (this.PeakInvocations.Count >= this.MaxEntries)
this.PeakInvocations.Pop();
this.PeakInvocations.Push(new PeakEntry(this.InvocationStopwatch.Elapsed.TotalMilliseconds, DateTime.UtcNow, this.TriggeredPerformanceCounters.ToArray()));
if (!this.EnableAlerts) return;
if (this.InvocationStopwatch.Elapsed.TotalMilliseconds >= this.AlertThresholdMilliseconds)
this.AddAlert(this.InvocationStopwatch.Elapsed.TotalMilliseconds,
this.AlertThresholdMilliseconds, this.TriggeredPerformanceCounters);
// raise alert
if (this.EnableAlerts && this.InvocationStopwatch.Elapsed.TotalMilliseconds >= this.AlertThresholdMilliseconds)
this.AddAlert(this.InvocationStopwatch.Elapsed.TotalMilliseconds, this.AlertThresholdMilliseconds, this.TriggeredPerformanceCounters.ToArray());
}
/// <summary>Adds an alert.</summary>
/// <summary>Add an alert.</summary>
/// <param name="executionTimeMilliseconds">The execution time in milliseconds.</param>
/// <param name="thresholdMilliseconds">The configured threshold.</param>
/// <param name="alerts">The list of alert contexts.</param>
public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, List<AlertContext> alerts)
/// <param name="alerts">The sources involved in exceeding the threshold.</param>
public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext[] alerts)
{
this.PerformanceCounterManager.AddAlert(new AlertEntry(this, executionTimeMilliseconds,
thresholdMilliseconds, alerts));
this.PerformanceMonitor.AddAlert(
new AlertEntry(this, executionTimeMilliseconds, thresholdMilliseconds, alerts)
);
}
/// <summary>Adds an alert for a single AlertContext</summary>
/// <summary>Add an alert.</summary>
/// <param name="executionTimeMilliseconds">The execution time in milliseconds.</param>
/// <param name="thresholdMilliseconds">The configured threshold.</param>
/// <param name="alert">The context</param>
/// <param name="alert">The source involved in exceeding the threshold.</param>
public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext alert)
{
this.AddAlert(executionTimeMilliseconds, thresholdMilliseconds, new List<AlertContext>() {alert});
this.AddAlert(executionTimeMilliseconds, thresholdMilliseconds, new[] { alert });
}
/// <summary>Resets the calls per second counter.</summary>
/// <summary>Reset the calls per second counter.</summary>
public void ResetCallsPerSecond()
{
this.CallCount = 0;
this.CallsPerSecondStart = DateTime.UtcNow;
}
/// <summary>Resets all performance counters in this collection.</summary>
/// <summary>Reset all performance counters in this collection.</summary>
public void Reset()
{
this.PeakInvocations.Clear();
foreach (var i in this.PerformanceCounters)
i.Value.Reset();
foreach (var counter in this.PerformanceCounters)
counter.Value.Reset();
}
/// <summary>Resets the performance counter for a specific source.</summary>
/// <param name="source">The source name</param>
/// <summary>Reset the performance counter for a specific source.</summary>
/// <param name="source">The source name.</param>
public void ResetSource(string source)
{
foreach (var i in this.PerformanceCounters)
@ -219,15 +193,13 @@ namespace StardewModdingAPI.Framework.PerformanceCounter
i.Value.Reset();
}
/// <summary>Returns the average calls per second.</summary>
/// <returns>The average calls per second.</returns>
/// <summary>Get the average calls per second.</summary>
public long GetAverageCallsPerSecond()
{
long runtimeInSeconds = (long) DateTime.UtcNow.Subtract(this.CallsPerSecondStart).TotalSeconds;
if (runtimeInSeconds == 0) return 0;
return this.CallCount / runtimeInSeconds;
long runtimeInSeconds = (long)DateTime.UtcNow.Subtract(this.CallsPerSecondStart).TotalSeconds;
return runtimeInSeconds > 0
? this.CallCount / runtimeInSeconds
: 0;
}
}
}

View File

@ -2,13 +2,29 @@ using System;
namespace StardewModdingAPI.Framework.PerformanceCounter
{
/// <summary>A single performance counter entry. Records the DateTime of the event and the elapsed millisecond.</summary>
/// <summary>A single performance counter entry.</summary>
internal struct PerformanceCounterEntry
{
/// <summary>The DateTime when the entry occured.</summary>
public DateTime EventTime;
/*********
** Accessors
*********/
/// <summary>When the entry occurred.</summary>
public DateTime EventTime { get; }
/// <summary>The elapsed milliseconds</summary>
public double ElapsedMilliseconds;
/// <summary>The elapsed milliseconds.</summary>
public double ElapsedMilliseconds { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="eventTime">When the entry occurred.</param>
/// <param name="elapsedMilliseconds">The elapsed milliseconds.</param>
public PerformanceCounterEntry(DateTime eventTime, double elapsedMilliseconds)
{
this.EventTime = eventTime;
this.ElapsedMilliseconds = elapsedMilliseconds;
}
}
}

View File

@ -7,12 +7,14 @@ using StardewModdingAPI.Framework.Events;
namespace StardewModdingAPI.Framework.PerformanceCounter
{
internal class PerformanceCounterManager
/// <summary>Tracks performance metrics.</summary>
internal class PerformanceMonitor
{
public HashSet<PerformanceCounterCollection> PerformanceCounterCollections = new HashSet<PerformanceCounterCollection>();
/*********
** Fields
*********/
/// <summary>The recorded alerts.</summary>
private readonly List<AlertEntry> Alerts = new List<AlertEntry>();
private readonly IList<AlertEntry> Alerts = new List<AlertEntry>();
/// <summary>The monitor for output logging.</summary>
private readonly IMonitor Monitor;
@ -20,62 +22,64 @@ namespace StardewModdingAPI.Framework.PerformanceCounter
/// <summary>The invocation stopwatch.</summary>
private readonly Stopwatch InvocationStopwatch = new Stopwatch();
/// <summary>Specifies if alerts should be paused.</summary>
/// <summary>The underlying performance counter collections.</summary>
private readonly IDictionary<string, PerformanceCounterCollection> Collections = new Dictionary<string, PerformanceCounterCollection>(StringComparer.InvariantCultureIgnoreCase);
/*********
** Accessors
*********/
/// <summary>Whether alerts are paused.</summary>
public bool PauseAlerts { get; set; }
/// <summary>Specifies if performance counter tracking should be enabled.</summary>
/// <summary>Whether performance counter tracking is enabled.</summary>
public bool EnableTracking { get; set; }
/// <summary>Constructs a performance counter manager.</summary>
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="monitor">The monitor for output logging.</param>
public PerformanceCounterManager(IMonitor monitor)
public PerformanceMonitor(IMonitor monitor)
{
this.Monitor = monitor;
}
/// <summary>Resets all performance counters in all collections.</summary>
/// <summary>Reset all performance counters in all collections.</summary>
public void Reset()
{
foreach (PerformanceCounterCollection collection in this.PerformanceCounterCollections)
{
foreach (PerformanceCounterCollection collection in this.Collections.Values)
collection.Reset();
}
foreach (var eventPerformanceCounter in
this.PerformanceCounterCollections.SelectMany(performanceCounter => performanceCounter.PerformanceCounters))
{
eventPerformanceCounter.Value.Reset();
}
}
/// <summary>Begins tracking the invocation for a collection.</summary>
/// <param name="collectionName">The collection name</param>
public void BeginTrackInvocation(string collectionName)
/// <summary>Track the invocation time for a collection.</summary>
/// <param name="collectionName">The name of the collection.</param>
/// <param name="action">The action to execute and track.</param>
public void Track(string collectionName, Action action)
{
if (!this.EnableTracking)
{
action();
return;
}
this.GetOrCreateCollectionByName(collectionName).BeginTrackInvocation();
}
/// <summary>Ends tracking the invocation for a collection.</summary>
/// <param name="collectionName"></param>
public void EndTrackInvocation(string collectionName)
{
if (!this.EnableTracking)
PerformanceCounterCollection collection = this.GetOrCreateCollectionByName(collectionName);
collection.BeginTrackInvocation();
try
{
return;
action();
}
finally
{
collection.EndTrackInvocation();
}
this.GetOrCreateCollectionByName(collectionName).EndTrackInvocation();
}
/// <summary>Tracks a single performance counter invocation in a specific collection.</summary>
/// <summary>Track a single performance counter invocation in a specific collection.</summary>
/// <param name="collectionName">The name of the collection.</param>
/// <param name="sourceName">The name of the source.</param>
/// <param name="action">The action to execute and track invocation time for.</param>
/// <param name="action">The action to execute and track.</param>
public void Track(string collectionName, string sourceName, Action action)
{
if (!this.EnableTracking)
@ -84,6 +88,7 @@ namespace StardewModdingAPI.Framework.PerformanceCounter
return;
}
PerformanceCounterCollection collection = this.GetOrCreateCollectionByName(collectionName);
DateTime eventTime = DateTime.UtcNow;
this.InvocationStopwatch.Reset();
this.InvocationStopwatch.Start();
@ -95,79 +100,50 @@ namespace StardewModdingAPI.Framework.PerformanceCounter
finally
{
this.InvocationStopwatch.Stop();
this.GetOrCreateCollectionByName(collectionName).Track(sourceName, new PerformanceCounterEntry
{
EventTime = eventTime,
ElapsedMilliseconds = this.InvocationStopwatch.Elapsed.TotalMilliseconds
});
collection.Track(sourceName, new PerformanceCounterEntry(eventTime, this.InvocationStopwatch.Elapsed.TotalMilliseconds));
}
}
/// <summary>Gets a collection by name.</summary>
/// <param name="name">The name of the collection.</param>
/// <returns>The collection or null if none was found.</returns>
private PerformanceCounterCollection GetCollectionByName(string name)
{
return this.PerformanceCounterCollections.FirstOrDefault(collection => collection.Name == name);
}
/// <summary>Gets a collection by name and creates it if it doesn't exist.</summary>
/// <param name="name">The name of the collection.</param>
/// <returns>The collection.</returns>
private PerformanceCounterCollection GetOrCreateCollectionByName(string name)
{
PerformanceCounterCollection collection = this.GetCollectionByName(name);
if (collection != null) return collection;
collection = new PerformanceCounterCollection(this, name);
this.PerformanceCounterCollections.Add(collection);
return collection;
}
/// <summary>Resets the performance counters for a specific collection.</summary>
/// <summary>Reset the performance counters for a specific collection.</summary>
/// <param name="name">The collection name.</param>
public void ResetCollection(string name)
{
foreach (PerformanceCounterCollection performanceCounterCollection in
this.PerformanceCounterCollections.Where(performanceCounterCollection =>
performanceCounterCollection.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)))
if (this.Collections.TryGetValue(name, out PerformanceCounterCollection collection))
{
performanceCounterCollection.ResetCallsPerSecond();
performanceCounterCollection.Reset();
collection.ResetCallsPerSecond();
collection.Reset();
}
}
/// <summary>Resets performance counters for a specific source.</summary>
/// <summary>Reset performance counters for a specific source.</summary>
/// <param name="name">The name of the source.</param>
public void ResetSource(string name)
{
foreach (PerformanceCounterCollection performanceCounterCollection in this.PerformanceCounterCollections)
foreach (PerformanceCounterCollection performanceCounterCollection in this.Collections.Values)
performanceCounterCollection.ResetSource(name);
}
/// <summary>Print any queued alerts.</summary>
public void PrintQueuedAlerts()
{
if (this.Alerts.Count == 0) return;
if (this.Alerts.Count == 0)
return;
StringBuilder sb = new StringBuilder();
StringBuilder report = new StringBuilder();
foreach (AlertEntry alert in this.Alerts)
{
sb.AppendLine($"{alert.Collection.Name} took {alert.ExecutionTimeMilliseconds:F2}ms (exceeded threshold of {alert.ThresholdMilliseconds:F2}ms)");
report.AppendLine($"{alert.Collection.Name} took {alert.ExecutionTimeMilliseconds:F2}ms (exceeded threshold of {alert.ThresholdMilliseconds:F2}ms)");
foreach (AlertContext context in alert.Context.OrderByDescending(p => p.Elapsed))
sb.AppendLine(context.ToString());
report.AppendLine(context.ToString());
}
this.Alerts.Clear();
this.Monitor.Log(sb.ToString(), LogLevel.Error);
this.Monitor.Log(report.ToString(), LogLevel.Error);
}
/// <summary>Adds an alert to the queue.</summary>
/// <summary>Add an alert to the queue.</summary>
/// <param name="entry">The alert to add.</param>
public void AddAlert(AlertEntry entry)
{
@ -175,68 +151,34 @@ namespace StardewModdingAPI.Framework.PerformanceCounter
this.Alerts.Add(entry);
}
/// <summary>Initialized the default performance counter collections.</summary>
/// <summary>Initialize the default performance counter collections.</summary>
/// <param name="eventManager">The event manager.</param>
public void InitializePerformanceCounterCollections(EventManager eventManager)
{
this.PerformanceCounterCollections = new HashSet<PerformanceCounterCollection>()
foreach (IManagedEvent @event in eventManager.GetAllEvents())
this.Collections[@event.EventName] = new PerformanceCounterCollection(this, @event.EventName, @event.IsPerformanceCritical);
}
/// <summary>Get the underlying performance counters.</summary>
public IEnumerable<PerformanceCounterCollection> GetCollections()
{
return this.Collections.Values;
}
/*********
** Public methods
*********/
/// <summary>Get a collection by name and creates it if it doesn't exist.</summary>
/// <param name="name">The name of the collection.</param>
private PerformanceCounterCollection GetOrCreateCollectionByName(string name)
{
if (!this.Collections.TryGetValue(name, out PerformanceCounterCollection collection))
{
new EventPerformanceCounterCollection(this, eventManager.MenuChanged, false),
// Rendering Events
new EventPerformanceCounterCollection(this, eventManager.Rendering, false),
new EventPerformanceCounterCollection(this, eventManager.Rendered, true),
new EventPerformanceCounterCollection(this, eventManager.RenderingWorld, false),
new EventPerformanceCounterCollection(this, eventManager.RenderedWorld, true),
new EventPerformanceCounterCollection(this, eventManager.RenderingActiveMenu, false),
new EventPerformanceCounterCollection(this, eventManager.RenderedActiveMenu, true),
new EventPerformanceCounterCollection(this, eventManager.RenderingHud, false),
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, false),
new EventPerformanceCounterCollection(this, eventManager.ModMessageReceived, false),
new EventPerformanceCounterCollection(this, eventManager.PeerDisconnected, false),
new EventPerformanceCounterCollection(this, eventManager.InventoryChanged, true),
new EventPerformanceCounterCollection(this, eventManager.LevelChanged, false),
new EventPerformanceCounterCollection(this, eventManager.Warped, false),
new EventPerformanceCounterCollection(this, eventManager.LocationListChanged, false),
new EventPerformanceCounterCollection(this, eventManager.BuildingListChanged, false),
new EventPerformanceCounterCollection(this, eventManager.LocationListChanged, false),
new EventPerformanceCounterCollection(this, eventManager.DebrisListChanged, true),
new EventPerformanceCounterCollection(this, eventManager.LargeTerrainFeatureListChanged, true),
new EventPerformanceCounterCollection(this, eventManager.NpcListChanged, false),
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),
};
collection = new PerformanceCounterCollection(this, name);
this.Collections[name] = collection;
}
return collection;
}
}
}

View File

@ -23,6 +23,7 @@ using StardewModdingAPI.Framework.Models;
using StardewModdingAPI.Framework.ModHelpers;
using StardewModdingAPI.Framework.ModLoading;
using StardewModdingAPI.Framework.Patching;
using StardewModdingAPI.Framework.PerformanceCounter;
using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Framework.Serialization;
using StardewModdingAPI.Patches;
@ -33,7 +34,6 @@ using StardewModdingAPI.Toolkit.Serialization;
using StardewModdingAPI.Toolkit.Utilities;
using StardewValley;
using Object = StardewValley.Object;
using PerformanceCounterManager = StardewModdingAPI.Framework.PerformanceCounter.PerformanceCounterManager;
using ThreadState = System.Threading.ThreadState;
namespace StardewModdingAPI.Framework
@ -135,8 +135,8 @@ namespace StardewModdingAPI.Framework
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; }
/// <remarks>This is initialized after the game starts. This is non-private for use by Console Commands.</remarks>
internal static PerformanceMonitor PerformanceMonitor { get; private set; }
/*********
@ -167,9 +167,9 @@ namespace StardewModdingAPI.Framework
};
this.MonitorForGame = this.GetSecondaryMonitor("game");
SCore.PerformanceCounterManager = new PerformanceCounterManager(this.Monitor);
this.EventManager = new EventManager(this.Monitor, this.ModRegistry, SCore.PerformanceCounterManager);
SCore.PerformanceCounterManager.InitializePerformanceCounterCollections(this.EventManager);
SCore.PerformanceMonitor = new PerformanceMonitor(this.Monitor);
this.EventManager = new EventManager(this.Monitor, this.ModRegistry, SCore.PerformanceMonitor);
SCore.PerformanceMonitor.InitializePerformanceCounterCollections(this.EventManager);
SCore.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry);
@ -248,7 +248,7 @@ namespace StardewModdingAPI.Framework
jsonHelper: this.Toolkit.JsonHelper,
modRegistry: this.ModRegistry,
deprecationManager: SCore.DeprecationManager,
performanceCounterManager: SCore.PerformanceCounterManager,
performanceMonitor: SCore.PerformanceMonitor,
onGameInitialized: this.InitializeAfterGameStart,
onGameExiting: this.Dispose,
cancellationToken: this.CancellationToken,
@ -1307,6 +1307,7 @@ 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;
default:
throw new NotSupportedException($"Unrecognized core SMAPI command '{name}'.");
}

View File

@ -59,7 +59,8 @@ namespace StardewModdingAPI.Framework
/// <summary>Manages deprecation warnings.</summary>
private readonly DeprecationManager DeprecationManager;
private readonly PerformanceCounterManager PerformanceCounterManager;
/// <summary>Tracks performance metrics.</summary>
private readonly PerformanceMonitor PerformanceMonitor;
/// <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
@ -155,12 +156,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="performanceMonitor">Tracks performance metrics.</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, PerformanceCounterManager performanceCounterManager, 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, PerformanceMonitor performanceMonitor, Action onGameInitialized, Action onGameExiting, CancellationTokenSource cancellationToken, bool logNetworkTraffic)
{
this.OnLoadingFirstAsset = SGame.ConstructorHack.OnLoadingFirstAsset;
SGame.ConstructorHack = null;
@ -180,7 +181,7 @@ namespace StardewModdingAPI.Framework
this.Reflection = reflection;
this.Translator = translator;
this.DeprecationManager = deprecationManager;
this.PerformanceCounterManager = performanceCounterManager;
this.PerformanceMonitor = performanceMonitor;
this.OnGameInitialized = onGameInitialized;
this.OnGameExiting = onGameExiting;
Game1.input = new SInputState();
@ -312,7 +313,7 @@ namespace StardewModdingAPI.Framework
try
{
this.DeprecationManager.PrintQueued();
this.PerformanceCounterManager.PrintQueuedAlerts();
this.PerformanceMonitor.PrintQueuedAlerts();
/*********
** First-tick initialization

View File

@ -11,7 +11,7 @@ using StardewModdingAPI.Framework;
using StardewModdingAPI.Toolkit.Utilities;
[assembly: InternalsVisibleTo("SMAPI.Tests")]
[assembly: InternalsVisibleTo("ConsoleCommands")]
[assembly: InternalsVisibleTo("ConsoleCommands")] // for performance monitoring commands
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Moq for unit testing
namespace StardewModdingAPI
{

View File

@ -15,12 +15,7 @@
<ApplicationIcon>icon.ico</ApplicationIcon>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DefineConstants>SMAPI_FOR_WINDOWS</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Cyotek.CircularBuffer" Version="1.0.2" />
<PackageReference Include="LargeAddressAware" Version="1.0.3" />
<PackageReference Include="Lib.Harmony" Version="1.2.0.1" />
<PackageReference Include="Mono.Cecil" Version="0.11.1" />