1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Plugins - Added profiling API

UI - Added profiling tab to debugger
This commit is contained in:
Robert 2021-05-16 20:22:13 +02:00
parent f13ab9852f
commit 4b24834fd6
18 changed files with 518 additions and 18 deletions

View File

@ -58,6 +58,7 @@
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plugins_005Cmodules_005Cactivationrequirements/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plugins_005Cprerequisites/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plugins_005Cprerequisites_005Cprerequisiteaction/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plugins_005Cprofiling/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plugins_005Csettings/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=rgb_002Enet/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Ccolorquantizer/@EntryIndexedValue">True</s:Boolean>

View File

@ -62,8 +62,8 @@ namespace Artemis.Core
/// </summary>
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()
{

View File

@ -17,6 +17,7 @@ namespace Artemis.Core
public class Plugin : CorePropertyChanged, IDisposable
{
private readonly List<PluginFeatureInfo> _features;
private readonly List<Profiler> _profilers;
private bool _isEnabled;
@ -28,6 +29,7 @@ namespace Artemis.Core
Info.Plugin = this;
_features = new List<PluginFeatureInfo>();
_profilers = new List<Profiler>();
}
/// <summary>
@ -64,6 +66,8 @@ namespace Artemis.Core
/// </summary>
public ReadOnlyCollection<PluginFeatureInfo> Features => _features.AsReadOnly();
public ReadOnlyCollection<Profiler> Profilers => _profilers.AsReadOnly();
/// <summary>
/// The assembly the plugin code lives in
/// </summary>
@ -114,7 +118,7 @@ namespace Artemis.Core
{
return _features.FirstOrDefault(i => i.Instance is T)?.Instance as T;
}
/// <summary>
/// Looks up the feature info the feature of type <typeparamref name="T" />
/// </summary>
@ -126,6 +130,22 @@ namespace Artemis.Core
return _features.First(i => i.FeatureType == typeof(T));
}
/// <summary>
/// Gets a profiler with the provided <paramref name="name" />, if it does not yet exist it will be created.
/// </summary>
/// <param name="name">The name of the profiler</param>
/// <returns>A new or existing profiler with the provided <paramref name="name" /></returns>
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;
}
/// <inheritdoc />
public override string ToString()
{

View File

@ -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
/// </summary>
public abstract class PluginFeature : CorePropertyChanged, IDisposable
{
private readonly Stopwatch _renderStopwatch = new();
private readonly Stopwatch _updateStopwatch = new();
private bool _isEnabled;
private Exception? _loadException;
/// <summary>
/// Gets the plugin feature info related to this feature
/// </summary>
@ -27,6 +23,11 @@ namespace Artemis.Core
/// </summary>
public Plugin Plugin { get; internal set; } = null!; // Will be set right after construction
/// <summary>
/// Gets the profiler that can be used to take profiling measurements
/// </summary>
public Profiler Profiler { get; internal set; } = null!; // Will be set right after construction
/// <summary>
/// Gets whether the plugin is enabled
/// </summary>
@ -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)

View File

@ -58,8 +58,7 @@ namespace Artemis.Core
/// <summary>
/// The plugins display icon that's shown in the settings see <see href="https://materialdesignicons.com" /> for
/// available
/// icons
/// available icons
/// </summary>
[JsonProperty]
public string? Icon

View File

@ -0,0 +1,72 @@
using System.Collections.Generic;
namespace Artemis.Core
{
/// <summary>
/// Represents a profiler that can measure time between calls distinguished by identifiers
/// </summary>
public class Profiler
{
internal Profiler(Plugin plugin, string name)
{
Plugin = plugin;
Name = name;
}
/// <summary>
/// Gets the plugin this profiler belongs to
/// </summary>
public Plugin Plugin { get; }
/// <summary>
/// Gets the name of this profiler
/// </summary>
public string Name { get; }
/// <summary>
/// Gets a dictionary containing measurements by their identifiers
/// </summary>
public Dictionary<string, ProfilingMeasurement> Measurements { get; set; } = new();
/// <summary>
/// Starts measuring time for the provided <paramref name="identifier" />
/// </summary>
/// <param name="identifier">A unique identifier for this measurement</param>
public void StartMeasurement(string identifier)
{
if (!Measurements.TryGetValue(identifier, out ProfilingMeasurement? measurement))
{
measurement = new ProfilingMeasurement(identifier);
Measurements.Add(identifier, measurement);
}
measurement.Start();
}
/// <summary>
/// Stops measuring time for the provided <paramref name="identifier" />
/// </summary>
/// <param name="identifier">A unique identifier for this measurement</param>
/// <returns>The number of ticks that passed since the <see cref="StartMeasurement" /> call with the same identifier</returns>
public long StopMeasurement(string identifier)
{
if (!Measurements.TryGetValue(identifier, out ProfilingMeasurement? measurement))
{
measurement = new ProfilingMeasurement(identifier);
Measurements.Add(identifier, measurement);
}
return measurement.Stop();
}
/// <summary>
/// Clears measurements with the the provided <paramref name="identifier" />
/// </summary>
/// <param name="identifier"></param>
public void ClearMeasurements(string identifier)
{
Measurements.Remove(identifier);
}
}
}

View File

@ -0,0 +1,110 @@
using System;
using System.Linq;
namespace Artemis.Core
{
/// <summary>
/// Represents a set of profiling measurements
/// </summary>
public class ProfilingMeasurement
{
private bool _filledArray;
private int _index;
private long _last;
private DateTime? _start;
internal ProfilingMeasurement(string identifier)
{
Identifier = identifier;
}
/// <summary>
/// Gets the unique identifier of this measurement
/// </summary>
public string Identifier { get; }
/// <summary>
/// Gets the last 1000 measurements
/// </summary>
public long[] Measurements { get; } = new long[1000];
/// <summary>
/// Starts measuring time until <see cref="Stop" /> is called
/// </summary>
public void Start()
{
_start = DateTime.UtcNow;
}
/// <summary>
/// Stops measuring time and stores the time passed in the <see cref="Measurements" /> list
/// </summary>
/// <returns>The time passed since the last <see cref="Start" /> call</returns>
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;
}
/// <summary>
/// Gets the last measured time
/// </summary>
public TimeSpan GetLast()
{
return new(_last);
}
/// <summary>
/// Gets the average time of the last 1000 measurements
/// </summary>
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));
}
/// <summary>
/// Gets the min time of the last 1000 measurements
/// </summary>
public TimeSpan GetMin()
{
if (!_filledArray && _index == 0)
return TimeSpan.Zero;
return _filledArray
? new TimeSpan(Measurements.Min())
: new TimeSpan(Measurements.Take(_index).Min());
}
/// <summary>
/// Gets the max time of the last 1000 measurements
/// </summary>
public TimeSpan GetMax()
{
if (!_filledArray && _index == 0)
return TimeSpan.Zero;
return _filledArray
? new TimeSpan(Measurements.Max())
: new TimeSpan(Measurements.Take(_index).Max());
}
}
}

View File

@ -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)

View File

@ -372,5 +372,8 @@
<Page Update="Screens\ProfileEditor\Dialogs\ProfileEditView.xaml">
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
</Page>
<Page Update="Screens\Settings\Debug\Tabs\Performance\PerformanceDebugView.xaml">
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
</Page>
</ItemGroup>
</Project>

View File

@ -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));
}

View File

@ -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);

View File

@ -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";
}
}
}

View File

@ -0,0 +1,33 @@
<UserControl x:Class="Artemis.UI.Screens.Settings.Debug.Tabs.Performance.PerformanceDebugPluginView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.Settings.Debug.Tabs.Performance"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
xmlns:s="https://github.com/canton7/Stylet"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:PerformanceDebugPluginViewModel}">
<materialDesign:Card Margin="0 5" Padding="10">
<StackPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<shared:ArtemisIcon Grid.Column="0" Icon="{Binding Icon}" Width="24" Height="24" />
<TextBlock Grid.Column="1" VerticalAlignment="Center" Style="{StaticResource MaterialDesignSubtitle1TextBlock}" Text="{Binding Plugin.Info.Name}" />
</Grid>
<ItemsControl ItemsSource="{Binding Profilers}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl s:View.Model="{Binding}" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch" IsTabStop="False" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</materialDesign:Card>
</UserControl>

View File

@ -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<PerformanceDebugProfilerViewModel> 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();
}
}
}

View File

@ -0,0 +1,33 @@
<UserControl x:Class="Artemis.UI.Screens.Settings.Debug.Tabs.Performance.PerformanceDebugProfilerView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.Settings.Debug.Tabs.Performance"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:PerformanceDebugProfilerViewModel}">
<StackPanel>
<TextBlock Style="{StaticResource MaterialDesignBody2TextBlock}" Text="{Binding Profiler.Name}" Margin="10 10 0 0" />
<DataGrid ItemsSource="{Binding Measurements}"
d:DataContext="{d:DesignInstance Type={x:Type local:PerformanceDebugMeasurementViewModel}}"
CanUserSortColumns="True"
IsReadOnly="True"
CanUserAddRows="False"
AutoGenerateColumns="False"
materialDesign:DataGridAssist.CellPadding="16 5 5 5"
materialDesign:DataGridAssist.ColumnHeaderPadding="16 5 5 5"
CanUserResizeRows="False"
Margin="10 5 10 10">
<DataGrid.Columns>
<materialDesign:DataGridTextColumn Binding="{Binding Measurement.Identifier}" Header="Identifier" />
<materialDesign:DataGridTextColumn Binding="{Binding Last}" Header="Last" />
<materialDesign:DataGridTextColumn Binding="{Binding Min}" Header="Min" />
<materialDesign:DataGridTextColumn Binding="{Binding Max}" Header="Max" />
<materialDesign:DataGridTextColumn Binding="{Binding Average}" Header="Average" />
</DataGrid.Columns>
</DataGrid>
</StackPanel>
</UserControl>

View File

@ -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<PerformanceDebugMeasurementViewModel> 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();
}
}
}

View File

@ -0,0 +1,35 @@
<UserControl x:Class="Artemis.UI.Screens.Settings.Debug.Tabs.Performance.PerformanceDebugView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.Settings.Debug.Tabs.Performance"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:s="https://github.com/canton7/Stylet"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:PerformanceDebugViewModel}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" TextWrapping="Wrap">
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.
</TextBlock>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Hidden">
<ItemsControl ItemsSource="{Binding Items}" Margin="0 0 10 0">
<ItemsControl.ItemTemplate>
<DataTemplate>
<materialDesign:TransitioningContent OpeningEffect="{materialDesign:TransitionEffect SlideInFromLeft}"
OpeningEffectsOffset="{materialDesign:IndexedItemOffsetMultiplier 0:0:0.05}">
<ContentControl s:View.Model="{Binding}" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch" IsTabStop="False" />
</materialDesign:TransitioningContent>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</UserControl>

View File

@ -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<PerformanceDebugPluginViewModel>.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
/// <inheritdoc />
protected override void OnActivate()
{
PopulateItems();
_updateTimer.Start();
_pluginManagementService.PluginDisabled += PluginToggled;
_pluginManagementService.PluginDisabled += PluginToggled;
_pluginManagementService.PluginFeatureEnabled += FeatureToggled;
_pluginManagementService.PluginFeatureDisabled += FeatureToggled;
base.OnActivate();
}
/// <inheritdoc />
protected override void OnDeactivate()
{
_updateTimer.Stop();
_pluginManagementService.PluginDisabled -= PluginToggled;
_pluginManagementService.PluginDisabled -= PluginToggled;
_pluginManagementService.PluginFeatureEnabled -= FeatureToggled;
_pluginManagementService.PluginFeatureDisabled -= FeatureToggled;
Items.Clear();
base.OnDeactivate();
}
#endregion
}
}