From 4b24834fd6018b5906fbe96b956cb353aa96cfb7 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 16 May 2021 20:22:13 +0200 Subject: [PATCH] Plugins - Added profiling API UI - Added profiling tab to debugger --- .../Artemis.Core.csproj.DotSettings | 1 + src/Artemis.Core/Constants.cs | 4 +- src/Artemis.Core/Plugins/Plugin.cs | 22 +++- src/Artemis.Core/Plugins/PluginFeature.cs | 21 ++-- src/Artemis.Core/Plugins/PluginInfo.cs | 3 +- .../Plugins/Profiling/Profiler.cs | 72 ++++++++++++ .../Plugins/Profiling/ProfilingMeasurement.cs | 110 ++++++++++++++++++ .../Services/PluginManagementService.cs | 1 + src/Artemis.UI/Artemis.UI.csproj | 3 + .../Timeline/TimelineSegmentViewModel.cs | 1 + .../Screens/Settings/Debug/DebugViewModel.cs | 6 +- .../PerformanceDebugMeasurementViewModel.cs | 52 +++++++++ .../PerformanceDebugPluginView.xaml | 33 ++++++ .../PerformanceDebugPluginViewModel.cs | 32 +++++ .../PerformanceDebugProfilerView.xaml | 33 ++++++ .../PerformanceDebugProfilerViewModel.cs | 30 +++++ .../Performance/PerformanceDebugView.xaml | 35 ++++++ .../Performance/PerformanceDebugViewModel.cs | 77 ++++++++++++ 18 files changed, 518 insertions(+), 18 deletions(-) create mode 100644 src/Artemis.Core/Plugins/Profiling/Profiler.cs create mode 100644 src/Artemis.Core/Plugins/Profiling/ProfilingMeasurement.cs create mode 100644 src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugMeasurementViewModel.cs create mode 100644 src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugPluginView.xaml create mode 100644 src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugPluginViewModel.cs create mode 100644 src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugProfilerView.xaml create mode 100644 src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugProfilerViewModel.cs create mode 100644 src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugView.xaml create mode 100644 src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugViewModel.cs diff --git a/src/Artemis.Core/Artemis.Core.csproj.DotSettings b/src/Artemis.Core/Artemis.Core.csproj.DotSettings index abd028c5b..daa70a5ef 100644 --- a/src/Artemis.Core/Artemis.Core.csproj.DotSettings +++ b/src/Artemis.Core/Artemis.Core.csproj.DotSettings @@ -58,6 +58,7 @@ True True True + True True True True diff --git a/src/Artemis.Core/Constants.cs b/src/Artemis.Core/Constants.cs index 4c22e8728..6f8f1c27d 100644 --- a/src/Artemis.Core/Constants.cs +++ b/src/Artemis.Core/Constants.cs @@ -62,8 +62,8 @@ namespace Artemis.Core /// public static readonly Plugin CorePlugin = new(CorePluginInfo, new DirectoryInfo(ApplicationFolder), null); - internal static readonly CorePluginFeature CorePluginFeature = new() {Plugin = CorePlugin}; - internal static readonly EffectPlaceholderPlugin EffectPlaceholderPlugin = new() {Plugin = CorePlugin}; + internal static readonly CorePluginFeature CorePluginFeature = new() {Plugin = CorePlugin, Profiler = CorePlugin.GetProfiler("Feature - Core")}; + internal static readonly EffectPlaceholderPlugin EffectPlaceholderPlugin = new() {Plugin = CorePlugin, Profiler = CorePlugin.GetProfiler("Feature - Effect Placeholder")}; internal static JsonSerializerSettings JsonConvertSettings = new() { diff --git a/src/Artemis.Core/Plugins/Plugin.cs b/src/Artemis.Core/Plugins/Plugin.cs index ddd090bfd..4d226640a 100644 --- a/src/Artemis.Core/Plugins/Plugin.cs +++ b/src/Artemis.Core/Plugins/Plugin.cs @@ -17,6 +17,7 @@ namespace Artemis.Core public class Plugin : CorePropertyChanged, IDisposable { private readonly List _features; + private readonly List _profilers; private bool _isEnabled; @@ -28,6 +29,7 @@ namespace Artemis.Core Info.Plugin = this; _features = new List(); + _profilers = new List(); } /// @@ -64,6 +66,8 @@ namespace Artemis.Core /// public ReadOnlyCollection Features => _features.AsReadOnly(); + public ReadOnlyCollection Profilers => _profilers.AsReadOnly(); + /// /// The assembly the plugin code lives in /// @@ -114,7 +118,7 @@ namespace Artemis.Core { return _features.FirstOrDefault(i => i.Instance is T)?.Instance as T; } - + /// /// Looks up the feature info the feature of type /// @@ -126,6 +130,22 @@ namespace Artemis.Core return _features.First(i => i.FeatureType == typeof(T)); } + /// + /// Gets a profiler with the provided , if it does not yet exist it will be created. + /// + /// The name of the profiler + /// A new or existing profiler with the provided + public Profiler GetProfiler(string name) + { + Profiler? profiler = _profilers.FirstOrDefault(p => p.Name == name); + if (profiler != null) + return profiler; + + profiler = new Profiler(this, name); + _profilers.Add(profiler); + return profiler; + } + /// public override string ToString() { diff --git a/src/Artemis.Core/Plugins/PluginFeature.cs b/src/Artemis.Core/Plugins/PluginFeature.cs index aa661c951..2fd931ebf 100644 --- a/src/Artemis.Core/Plugins/PluginFeature.cs +++ b/src/Artemis.Core/Plugins/PluginFeature.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Threading.Tasks; using Artemis.Storage.Entities.Plugins; @@ -12,11 +10,9 @@ namespace Artemis.Core /// public abstract class PluginFeature : CorePropertyChanged, IDisposable { - private readonly Stopwatch _renderStopwatch = new(); - private readonly Stopwatch _updateStopwatch = new(); private bool _isEnabled; private Exception? _loadException; - + /// /// Gets the plugin feature info related to this feature /// @@ -27,6 +23,11 @@ namespace Artemis.Core /// public Plugin Plugin { get; internal set; } = null!; // Will be set right after construction + /// + /// Gets the profiler that can be used to take profiling measurements + /// + public Profiler Profiler { get; internal set; } = null!; // Will be set right after construction + /// /// Gets whether the plugin is enabled /// @@ -112,24 +113,22 @@ namespace Artemis.Core internal void StartUpdateMeasure() { - _updateStopwatch.Start(); + Profiler.StartMeasurement("Update"); } internal void StopUpdateMeasure() { - UpdateTime = _updateStopwatch.Elapsed; - _updateStopwatch.Reset(); + Profiler.StopMeasurement("Update"); } internal void StartRenderMeasure() { - _renderStopwatch.Start(); + Profiler.StartMeasurement("Render"); } internal void StopRenderMeasure() { - RenderTime = _renderStopwatch.Elapsed; - _renderStopwatch.Reset(); + Profiler.StopMeasurement("Render"); } internal void SetEnabled(bool enable, bool isAutoEnable = false) diff --git a/src/Artemis.Core/Plugins/PluginInfo.cs b/src/Artemis.Core/Plugins/PluginInfo.cs index de8a77e7f..1a6af3a88 100644 --- a/src/Artemis.Core/Plugins/PluginInfo.cs +++ b/src/Artemis.Core/Plugins/PluginInfo.cs @@ -58,8 +58,7 @@ namespace Artemis.Core /// /// The plugins display icon that's shown in the settings see for - /// available - /// icons + /// available icons /// [JsonProperty] public string? Icon diff --git a/src/Artemis.Core/Plugins/Profiling/Profiler.cs b/src/Artemis.Core/Plugins/Profiling/Profiler.cs new file mode 100644 index 000000000..a544bc2f0 --- /dev/null +++ b/src/Artemis.Core/Plugins/Profiling/Profiler.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; + +namespace Artemis.Core +{ + /// + /// Represents a profiler that can measure time between calls distinguished by identifiers + /// + public class Profiler + { + internal Profiler(Plugin plugin, string name) + { + Plugin = plugin; + Name = name; + } + + /// + /// Gets the plugin this profiler belongs to + /// + public Plugin Plugin { get; } + + /// + /// Gets the name of this profiler + /// + public string Name { get; } + + + /// + /// Gets a dictionary containing measurements by their identifiers + /// + public Dictionary Measurements { get; set; } = new(); + + /// + /// Starts measuring time for the provided + /// + /// A unique identifier for this measurement + public void StartMeasurement(string identifier) + { + if (!Measurements.TryGetValue(identifier, out ProfilingMeasurement? measurement)) + { + measurement = new ProfilingMeasurement(identifier); + Measurements.Add(identifier, measurement); + } + + measurement.Start(); + } + + /// + /// Stops measuring time for the provided + /// + /// A unique identifier for this measurement + /// The number of ticks that passed since the call with the same identifier + public long StopMeasurement(string identifier) + { + if (!Measurements.TryGetValue(identifier, out ProfilingMeasurement? measurement)) + { + measurement = new ProfilingMeasurement(identifier); + Measurements.Add(identifier, measurement); + } + + return measurement.Stop(); + } + + /// + /// Clears measurements with the the provided + /// + /// + public void ClearMeasurements(string identifier) + { + Measurements.Remove(identifier); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Profiling/ProfilingMeasurement.cs b/src/Artemis.Core/Plugins/Profiling/ProfilingMeasurement.cs new file mode 100644 index 000000000..4c8aeaae9 --- /dev/null +++ b/src/Artemis.Core/Plugins/Profiling/ProfilingMeasurement.cs @@ -0,0 +1,110 @@ +using System; +using System.Linq; + +namespace Artemis.Core +{ + /// + /// Represents a set of profiling measurements + /// + public class ProfilingMeasurement + { + private bool _filledArray; + private int _index; + private long _last; + private DateTime? _start; + + internal ProfilingMeasurement(string identifier) + { + Identifier = identifier; + } + + /// + /// Gets the unique identifier of this measurement + /// + public string Identifier { get; } + + /// + /// Gets the last 1000 measurements + /// + public long[] Measurements { get; } = new long[1000]; + + /// + /// Starts measuring time until is called + /// + public void Start() + { + _start = DateTime.UtcNow; + } + + /// + /// Stops measuring time and stores the time passed in the list + /// + /// The time passed since the last call + public long Stop() + { + if (_start == null) + return 0; + + long difference = (DateTime.UtcNow - _start.Value).Ticks; + Measurements[_index] = difference; + _start = null; + + _index++; + if (_index >= 1000) + { + _filledArray = true; + _index = 0; + } + + _last = difference; + return difference; + } + + /// + /// Gets the last measured time + /// + public TimeSpan GetLast() + { + return new(_last); + } + + /// + /// Gets the average time of the last 1000 measurements + /// + public TimeSpan GetAverage() + { + if (!_filledArray && _index == 0) + return TimeSpan.Zero; + + return _filledArray + ? new TimeSpan((long) Measurements.Average(m => m)) + : new TimeSpan((long) Measurements.Take(_index).Average(m => m)); + } + + /// + /// Gets the min time of the last 1000 measurements + /// + public TimeSpan GetMin() + { + if (!_filledArray && _index == 0) + return TimeSpan.Zero; + + return _filledArray + ? new TimeSpan(Measurements.Min()) + : new TimeSpan(Measurements.Take(_index).Min()); + } + + /// + /// Gets the max time of the last 1000 measurements + /// + public TimeSpan GetMax() + { + if (!_filledArray && _index == 0) + return TimeSpan.Zero; + + return _filledArray + ? new TimeSpan(Measurements.Max()) + : new TimeSpan(Measurements.Take(_index).Max()); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index 7b3e6dea1..0d312a262 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -411,6 +411,7 @@ namespace Artemis.Core.Services featureInfo.Instance = instance; instance.Info = featureInfo; instance.Plugin = plugin; + instance.Profiler = plugin.GetProfiler("Feature - " + featureInfo.Name); instance.Entity = featureInfo.Entity; } catch (Exception e) diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj index ce31292e3..5ae4e77bb 100644 --- a/src/Artemis.UI/Artemis.UI.csproj +++ b/src/Artemis.UI/Artemis.UI.csproj @@ -372,5 +372,8 @@ $(DefaultXamlRuntime) + + $(DefaultXamlRuntime) + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineSegmentViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineSegmentViewModel.cs index 8d8c52ee9..804e39dee 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineSegmentViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineSegmentViewModel.cs @@ -190,6 +190,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline ShowRepeatButton = SegmentWidth > 45 && IsMainSegment; ShowDisableButton = SegmentWidth > 25; + if (Segment == SegmentViewModelType.Main) NotifyOfPropertyChange(nameof(RepeatSegment)); } diff --git a/src/Artemis.UI/Screens/Settings/Debug/DebugViewModel.cs b/src/Artemis.UI/Screens/Settings/Debug/DebugViewModel.cs index e661158ea..55939628d 100644 --- a/src/Artemis.UI/Screens/Settings/Debug/DebugViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Debug/DebugViewModel.cs @@ -1,8 +1,8 @@ using System; -using System.Collections.Generic; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Screens.Settings.Debug.Tabs; +using Artemis.UI.Screens.Settings.Debug.Tabs.Performance; using Stylet; namespace Artemis.UI.Screens.Settings.Debug @@ -16,12 +16,14 @@ namespace Artemis.UI.Screens.Settings.Debug ICoreService coreService, RenderDebugViewModel renderDebugViewModel, DataModelDebugViewModel dataModelDebugViewModel, - LogsDebugViewModel logsDebugViewModel) + LogsDebugViewModel logsDebugViewModel, + PerformanceDebugViewModel performanceDebugViewModel) { _coreService = coreService; Items.Add(renderDebugViewModel); Items.Add(dataModelDebugViewModel); Items.Add(logsDebugViewModel); + Items.Add(performanceDebugViewModel); ActiveItem = renderDebugViewModel; StayOnTopSetting = settingsService.GetSetting("Debugger.StayOnTop", false); diff --git a/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugMeasurementViewModel.cs b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugMeasurementViewModel.cs new file mode 100644 index 000000000..7bec70c87 --- /dev/null +++ b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugMeasurementViewModel.cs @@ -0,0 +1,52 @@ +using Artemis.Core; +using Stylet; + +namespace Artemis.UI.Screens.Settings.Debug.Tabs.Performance +{ + public class PerformanceDebugMeasurementViewModel : PropertyChangedBase + { + private string _average; + private string _last; + private string _max; + private string _min; + + public PerformanceDebugMeasurementViewModel(ProfilingMeasurement measurement) + { + Measurement = measurement; + } + + public ProfilingMeasurement Measurement { get; } + + public string Last + { + get => _last; + set => SetAndNotify(ref _last, value); + } + + public string Average + { + get => _average; + set => SetAndNotify(ref _average, value); + } + + public string Min + { + get => _min; + set => SetAndNotify(ref _min, value); + } + + public string Max + { + get => _max; + set => SetAndNotify(ref _max, value); + } + + public void Update() + { + Last = Measurement.GetLast().TotalMilliseconds + " ms"; + Average = Measurement.GetAverage().TotalMilliseconds + " ms"; + Min = Measurement.GetMin().TotalMilliseconds + " ms"; + Max = Measurement.GetMax().TotalMilliseconds + " ms"; + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugPluginView.xaml b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugPluginView.xaml new file mode 100644 index 000000000..4a3d11955 --- /dev/null +++ b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugPluginView.xaml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugPluginViewModel.cs b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugPluginViewModel.cs new file mode 100644 index 000000000..719eff26b --- /dev/null +++ b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugPluginViewModel.cs @@ -0,0 +1,32 @@ +using System.Linq; +using Artemis.Core; +using Artemis.UI.Shared; +using Stylet; + +namespace Artemis.UI.Screens.Settings.Debug.Tabs.Performance +{ + public class PerformanceDebugPluginViewModel : Screen + { + public Plugin Plugin { get; } + public object Icon { get; } + + public PerformanceDebugPluginViewModel(Plugin plugin) + { + Plugin = plugin; + Icon = PluginUtilities.GetPluginIcon(Plugin, Plugin.Info.Icon); + } + + public BindableCollection Profilers { get; } = new(); + public void Update() + { + foreach (Profiler pluginProfiler in Plugin.Profilers.Where(p => p.Measurements.Any())) + { + if (Profilers.All(p => p.Profiler != pluginProfiler)) + Profilers.Add(new PerformanceDebugProfilerViewModel(pluginProfiler)); + } + + foreach (PerformanceDebugProfilerViewModel profilerViewModel in Profilers) + profilerViewModel.Update(); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugProfilerView.xaml b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugProfilerView.xaml new file mode 100644 index 000000000..f8e4cad6a --- /dev/null +++ b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugProfilerView.xaml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + diff --git a/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugProfilerViewModel.cs b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugProfilerViewModel.cs new file mode 100644 index 000000000..c0921b87a --- /dev/null +++ b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugProfilerViewModel.cs @@ -0,0 +1,30 @@ +using System.Linq; +using Artemis.Core; +using Stylet; + +namespace Artemis.UI.Screens.Settings.Debug.Tabs.Performance +{ + public class PerformanceDebugProfilerViewModel : Screen + { + public Profiler Profiler { get; } + + public PerformanceDebugProfilerViewModel(Profiler profiler) + { + Profiler = profiler; + } + + public BindableCollection Measurements { get; } = new(); + + public void Update() + { + foreach ((string _, ProfilingMeasurement measurement) in Profiler.Measurements) + { + if (Measurements.All(m => m.Measurement != measurement)) + Measurements.Add(new PerformanceDebugMeasurementViewModel(measurement)); + } + + foreach (PerformanceDebugMeasurementViewModel profilingMeasurementViewModel in Measurements) + profilingMeasurementViewModel.Update(); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugView.xaml b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugView.xaml new file mode 100644 index 000000000..cdc24de90 --- /dev/null +++ b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugView.xaml @@ -0,0 +1,35 @@ + + + + + + + + In this window you can see how much CPU time different plugin features are taking. + If you are having performance issues, below you can find out which plugin might be the culprit. + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugViewModel.cs b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugViewModel.cs new file mode 100644 index 000000000..b30ebe72e --- /dev/null +++ b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugViewModel.cs @@ -0,0 +1,77 @@ +using System.Linq; +using System.Timers; +using Artemis.Core; +using Artemis.Core.Services; +using Stylet; + +namespace Artemis.UI.Screens.Settings.Debug.Tabs.Performance +{ + public class PerformanceDebugViewModel : Conductor.Collection.AllActive + { + private readonly IPluginManagementService _pluginManagementService; + private readonly Timer _updateTimer; + + public PerformanceDebugViewModel(IPluginManagementService pluginManagementService) + { + _pluginManagementService = pluginManagementService; + _updateTimer = new Timer(500); + + DisplayName = "PERFORMANCE"; + _updateTimer.Elapsed += UpdateTimerOnElapsed; + } + + private void UpdateTimerOnElapsed(object sender, ElapsedEventArgs e) + { + foreach (PerformanceDebugPluginViewModel viewModel in Items) + viewModel.Update(); + } + + private void FeatureToggled(object? sender, PluginFeatureEventArgs e) + { + Items.Clear(); + PopulateItems(); + } + + private void PluginToggled(object? sender, PluginEventArgs e) + { + Items.Clear(); + PopulateItems(); + } + + private void PopulateItems() + { + Items.AddRange(_pluginManagementService.GetAllPlugins() + .Where(p => p.IsEnabled && p.Profilers.Any(pr => pr.Measurements.Any())) + .OrderBy(p => p.Info.Name) + .Select(p => new PerformanceDebugPluginViewModel(p))); + } + + #region Overrides of Screen + + /// + protected override void OnActivate() + { + PopulateItems(); + _updateTimer.Start(); + _pluginManagementService.PluginDisabled += PluginToggled; + _pluginManagementService.PluginDisabled += PluginToggled; + _pluginManagementService.PluginFeatureEnabled += FeatureToggled; + _pluginManagementService.PluginFeatureDisabled += FeatureToggled; + base.OnActivate(); + } + + /// + protected override void OnDeactivate() + { + _updateTimer.Stop(); + _pluginManagementService.PluginDisabled -= PluginToggled; + _pluginManagementService.PluginDisabled -= PluginToggled; + _pluginManagementService.PluginFeatureEnabled -= FeatureToggled; + _pluginManagementService.PluginFeatureDisabled -= FeatureToggled; + Items.Clear(); + base.OnDeactivate(); + } + + #endregion + } +} \ No newline at end of file