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

UI - Avoid opening the main window multiple times from tray

Profiling - Thread safety and use high precision counters
Profiling - Profile timed updates
Timed updates - Added argument to give timed updates a name
Plugins - Affix plugin folders with a part of the plugin GUID
Debugger - Added 95th percentile column to profiling
Debugger - Fix scrolling in performance profile tab when hovering over datagrids
This commit is contained in:
Robert 2021-05-17 17:26:49 +02:00
parent 4ab8459927
commit 1660519bee
14 changed files with 315 additions and 185 deletions

View File

@ -8,40 +8,6 @@ namespace Artemis.Core
/// </summary> /// </summary>
public abstract class DataModelPluginFeature : PluginFeature public abstract class DataModelPluginFeature : PluginFeature
{ {
/// <summary>
/// Registers a timed update that whenever the plugin is enabled calls the provided <paramref name="action" /> at the
/// provided
/// <paramref name="interval" />
/// </summary>
/// <param name="interval">The interval at which the update should occur</param>
/// <param name="action">
/// The action to call every time the interval has passed. The delta time parameter represents the
/// time passed since the last update in seconds
/// </param>
/// <returns>The resulting plugin update registration which can be used to stop the update</returns>
public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Action<double> action)
{
if (action == null)
throw new ArgumentNullException(nameof(action));
return new TimedUpdateRegistration(this, interval, action);
}
/// <summary>
/// Registers a timed update that whenever the plugin is enabled calls the provided <paramref name="asyncAction" /> at the
/// provided
/// <paramref name="interval" />
/// </summary>
/// <param name="interval">The interval at which the update should occur</param>
/// <param name="asyncAction">
/// The async action to call every time the interval has passed. The delta time parameter
/// represents the time passed since the last update in seconds
/// </param>
/// <returns>The resulting plugin update registration</returns>
public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Func<double, Task> asyncAction)
{
if (asyncAction == null)
throw new ArgumentNullException(nameof(asyncAction));
return new TimedUpdateRegistration(this, interval, asyncAction);
}
} }
} }

View File

@ -146,6 +146,15 @@ namespace Artemis.Core
return profiler; return profiler;
} }
/// <summary>
/// Removes a profiler from the plugin
/// </summary>
/// <param name="profiler">The profiler to remove</param>
public void RemoveProfiler(Profiler profiler)
{
_profilers.Remove(profiler);
}
/// <inheritdoc /> /// <inheritdoc />
public override string ToString() public override string ToString()
{ {

View File

@ -51,16 +51,6 @@ namespace Artemis.Core
/// </summary> /// </summary>
public string Id => $"{GetType().FullName}-{Plugin.Guid.ToString().Substring(0, 8)}"; // Not as unique as a GUID but good enough and stays readable public string Id => $"{GetType().FullName}-{Plugin.Guid.ToString().Substring(0, 8)}"; // Not as unique as a GUID but good enough and stays readable
/// <summary>
/// Gets the last measured update time of the feature
/// </summary>
public TimeSpan UpdateTime { get; private set; }
/// <summary>
/// Gets the last measured render time of the feature
/// </summary>
public TimeSpan RenderTime { get; private set; }
internal PluginFeatureEntity Entity { get; set; } = null!; // Will be set right after construction internal PluginFeatureEntity Entity { get; set; } = null!; // Will be set right after construction
/// <summary> /// <summary>
@ -241,5 +231,47 @@ namespace Artemis.Core
} }
#endregion #endregion
#region Timed updates
/// <summary>
/// Registers a timed update that whenever the plugin is enabled calls the provided <paramref name="action" /> at the
/// provided
/// <paramref name="interval" />
/// </summary>
/// <param name="interval">The interval at which the update should occur</param>
/// <param name="action">
/// The action to call every time the interval has passed. The delta time parameter represents the
/// time passed since the last update in seconds
/// </param>
/// <param name="name">An optional name used in exceptions and profiling</param>
/// <returns>The resulting plugin update registration which can be used to stop the update</returns>
public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Action<double> action, string? name = null)
{
if (action == null)
throw new ArgumentNullException(nameof(action));
return new TimedUpdateRegistration(this, interval, action, name);
}
/// <summary>
/// Registers a timed update that whenever the plugin is enabled calls the provided <paramref name="asyncAction" /> at the
/// provided
/// <paramref name="interval" />
/// </summary>
/// <param name="interval">The interval at which the update should occur</param>
/// <param name="asyncAction">
/// The async action to call every time the interval has passed. The delta time parameter
/// represents the time passed since the last update in seconds
/// </param>
/// <param name="name">An optional name used in exceptions and profiling</param>
/// <returns>The resulting plugin update registration</returns>
public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Func<double, Task> asyncAction, string? name = null)
{
if (asyncAction == null)
throw new ArgumentNullException(nameof(asyncAction));
return new TimedUpdateRegistration(this, interval, asyncAction, name);
}
#endregion
} }
} }

View File

@ -124,6 +124,8 @@ namespace Artemis.Core
/// <inheritdoc /> /// <inheritdoc />
public bool ArePrerequisitesMet() => Prerequisites.All(p => p.IsMet()); public bool ArePrerequisitesMet() => Prerequisites.All(p => p.IsMet());
internal string PreferredPluginDirectory => $"{Main.Split(".dll")[0].Replace("/", "").Replace("\\", "")}-{Guid.ToString().Substring(0, 8)}";
/// <inheritdoc /> /// <inheritdoc />
public override string ToString() public override string ToString()
{ {

View File

@ -1,4 +1,6 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace Artemis.Core namespace Artemis.Core
{ {
@ -35,13 +37,16 @@ namespace Artemis.Core
/// <param name="identifier">A unique identifier for this measurement</param> /// <param name="identifier">A unique identifier for this measurement</param>
public void StartMeasurement(string identifier) public void StartMeasurement(string identifier)
{ {
if (!Measurements.TryGetValue(identifier, out ProfilingMeasurement? measurement)) lock (Measurements)
{ {
measurement = new ProfilingMeasurement(identifier); if (!Measurements.TryGetValue(identifier, out ProfilingMeasurement? measurement))
Measurements.Add(identifier, measurement); {
} measurement = new ProfilingMeasurement(identifier);
Measurements.Add(identifier, measurement);
}
measurement.Start(); measurement.Start();
}
} }
/// <summary> /// <summary>
@ -51,13 +56,17 @@ namespace Artemis.Core
/// <returns>The number of ticks that passed since the <see cref="StartMeasurement" /> call with the same identifier</returns> /// <returns>The number of ticks that passed since the <see cref="StartMeasurement" /> call with the same identifier</returns>
public long StopMeasurement(string identifier) public long StopMeasurement(string identifier)
{ {
if (!Measurements.TryGetValue(identifier, out ProfilingMeasurement? measurement)) long lockRequestedAt = Stopwatch.GetTimestamp();
lock (Measurements)
{ {
measurement = new ProfilingMeasurement(identifier); if (!Measurements.TryGetValue(identifier, out ProfilingMeasurement? measurement))
Measurements.Add(identifier, measurement); {
} measurement = new ProfilingMeasurement(identifier);
Measurements.Add(identifier, measurement);
}
return measurement.Stop(); return measurement.Stop(Stopwatch.GetTimestamp() - lockRequestedAt);
}
} }
/// <summary> /// <summary>
@ -66,7 +75,10 @@ namespace Artemis.Core
/// <param name="identifier"></param> /// <param name="identifier"></param>
public void ClearMeasurements(string identifier) public void ClearMeasurements(string identifier)
{ {
Measurements.Remove(identifier); lock (Measurements)
{
Measurements.Remove(identifier);
}
} }
} }
} }

View File

@ -1,4 +1,6 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
namespace Artemis.Core namespace Artemis.Core
@ -11,7 +13,8 @@ namespace Artemis.Core
private bool _filledArray; private bool _filledArray;
private int _index; private int _index;
private long _last; private long _last;
private DateTime? _start; private bool _open;
private long _start;
internal ProfilingMeasurement(string identifier) internal ProfilingMeasurement(string identifier)
{ {
@ -33,22 +36,24 @@ namespace Artemis.Core
/// </summary> /// </summary>
public void Start() public void Start()
{ {
_start = DateTime.UtcNow; _start = Stopwatch.GetTimestamp();
_open = true;
} }
/// <summary> /// <summary>
/// Stops measuring time and stores the time passed in the <see cref="Measurements" /> list /// Stops measuring time and stores the time passed in the <see cref="Measurements" /> list
/// </summary> /// </summary>
/// <param name="correction">An optional correction in ticks to subtract from the measurement</param>
/// <returns>The time passed since the last <see cref="Start" /> call</returns> /// <returns>The time passed since the last <see cref="Start" /> call</returns>
public long Stop() public long Stop(long correction = 0)
{ {
if (_start == null) if (!_open)
return 0; return 0;
long difference = (DateTime.UtcNow - _start.Value).Ticks; long difference = Stopwatch.GetTimestamp() - _start - correction;
_open = false;
Measurements[_index] = difference; Measurements[_index] = difference;
_start = null;
_index++; _index++;
if (_index >= 1000) if (_index >= 1000)
{ {
@ -106,5 +111,31 @@ namespace Artemis.Core
? new TimeSpan(Measurements.Max()) ? new TimeSpan(Measurements.Max())
: new TimeSpan(Measurements.Take(_index).Max()); : new TimeSpan(Measurements.Take(_index).Max());
} }
/// <summary>
/// Gets the nth percentile of the last 1000 measurements
/// </summary>
public TimeSpan GetPercentile(double percentile)
{
if (!_filledArray && _index == 0)
return TimeSpan.Zero;
long[] collection = _filledArray
? Measurements.OrderBy(l => l).ToArray()
: Measurements.Take(_index).OrderBy(l => l).ToArray();
return new TimeSpan((long) Percentile(collection, percentile));
}
private static double Percentile(long[] elements, double percentile)
{
Array.Sort(elements);
double realIndex = percentile * (elements.Length - 1);
int index = (int) realIndex;
double frac = realIndex - index;
if (index + 1 < elements.Length)
return elements[index] * (1 - frac) + elements[index + 1] * frac;
return elements[index];
}
} }
} }

View File

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using System.Timers; using System.Timers;
using Artemis.Core.Modules; using Artemis.Core.Modules;
using Artemis.Core.Services; using Artemis.Core.Services;
using Humanizer;
using Ninject; using Ninject;
using Serilog; using Serilog;
@ -13,19 +14,20 @@ namespace Artemis.Core
/// </summary> /// </summary>
public class TimedUpdateRegistration : IDisposable public class TimedUpdateRegistration : IDisposable
{ {
private DateTime _lastEvent;
private Timer? _timer;
private bool _disposed;
private readonly object _lock = new(); private readonly object _lock = new();
private ILogger _logger; private bool _disposed;
private DateTime _lastEvent;
private readonly ILogger _logger;
private Timer? _timer;
internal TimedUpdateRegistration(PluginFeature feature, TimeSpan interval, Action<double> action) internal TimedUpdateRegistration(PluginFeature feature, TimeSpan interval, Action<double> action, string? name)
{ {
_logger = CoreService.Kernel.Get<ILogger>(); _logger = CoreService.Kernel.Get<ILogger>();
Feature = feature; Feature = feature;
Interval = interval; Interval = interval;
Action = action; Action = action;
Name = name ?? $"TimedUpdate-{Guid.NewGuid().ToString().Substring(0, 8)}";
Feature.Enabled += FeatureOnEnabled; Feature.Enabled += FeatureOnEnabled;
Feature.Disabled += FeatureOnDisabled; Feature.Disabled += FeatureOnDisabled;
@ -33,13 +35,14 @@ namespace Artemis.Core
Start(); Start();
} }
internal TimedUpdateRegistration(PluginFeature feature, TimeSpan interval, Func<double, Task> asyncAction) internal TimedUpdateRegistration(PluginFeature feature, TimeSpan interval, Func<double, Task> asyncAction, string? name)
{ {
_logger = CoreService.Kernel.Get<ILogger>(); _logger = CoreService.Kernel.Get<ILogger>();
Feature = feature; Feature = feature;
Interval = interval; Interval = interval;
AsyncAction = asyncAction; AsyncAction = asyncAction;
Name = name ?? $"TimedUpdate-{Guid.NewGuid().ToString().Substring(0, 8)}";
Feature.Enabled += FeatureOnEnabled; Feature.Enabled += FeatureOnEnabled;
Feature.Disabled += FeatureOnDisabled; Feature.Disabled += FeatureOnDisabled;
@ -69,7 +72,12 @@ namespace Artemis.Core
public Func<double, Task>? AsyncAction { get; } public Func<double, Task>? AsyncAction { get; }
/// <summary> /// <summary>
/// Starts calling the <see cref="Action" /> or <see cref="AsyncAction"/> at the configured <see cref="Interval" /> /// Gets the name of this timed update
/// </summary>
public string Name { get; }
/// <summary>
/// Starts calling the <see cref="Action" /> or <see cref="AsyncAction" /> at the configured <see cref="Interval" />
/// <para>Note: Called automatically when the plugin enables</para> /// <para>Note: Called automatically when the plugin enables</para>
/// </summary> /// </summary>
public void Start() public void Start()
@ -93,7 +101,7 @@ namespace Artemis.Core
} }
/// <summary> /// <summary>
/// Stops calling the <see cref="Action" /> or <see cref="AsyncAction"/> at the configured <see cref="Interval" /> /// Stops calling the <see cref="Action" /> or <see cref="AsyncAction" /> at the configured <see cref="Interval" />
/// <para>Note: Called automatically when the plugin disables</para> /// <para>Note: Called automatically when the plugin disables</para>
/// </summary> /// </summary>
public void Stop() public void Stop()
@ -113,49 +121,6 @@ namespace Artemis.Core
} }
} }
private void TimerOnElapsed(object? sender, ElapsedEventArgs e)
{
if (!Feature.IsEnabled)
return;
lock (_lock)
{
TimeSpan interval = DateTime.Now - _lastEvent;
_lastEvent = DateTime.Now;
// Modules don't always want to update, honor that
if (Feature is Module module && !module.IsUpdateAllowed)
return;
try
{
if (Action != null)
Action(interval.TotalSeconds);
else if (AsyncAction != null)
{
Task task = AsyncAction(interval.TotalSeconds);
task.Wait();
}
}
catch (Exception exception)
{
_logger.Error(exception, "Timed update uncaught exception in plugin {plugin}", Feature.Plugin);
}
}
}
private void FeatureOnEnabled(object? sender, EventArgs e)
{
Start();
}
private void FeatureOnDisabled(object? sender, EventArgs e)
{
Stop();
}
#region IDisposable
/// <summary> /// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources. /// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary> /// </summary>
@ -176,6 +141,55 @@ namespace Artemis.Core
} }
} }
private void TimerOnElapsed(object? sender, ElapsedEventArgs e)
{
if (!Feature.IsEnabled)
return;
lock (_lock)
{
Feature.Profiler.StartMeasurement(ToString());
TimeSpan interval = DateTime.Now - _lastEvent;
_lastEvent = DateTime.Now;
// Modules don't always want to update, honor that
if (Feature is Module module && !module.IsUpdateAllowed)
return;
try
{
if (Action != null)
{
Action(interval.TotalSeconds);
}
else if (AsyncAction != null)
{
Task task = AsyncAction(interval.TotalSeconds);
task.Wait();
}
}
catch (Exception exception)
{
_logger.Error(exception, "{timedUpdate} uncaught exception in plugin {plugin}", this, Feature.Plugin);
}
finally
{
Feature.Profiler.StopMeasurement(ToString());
}
}
}
private void FeatureOnEnabled(object? sender, EventArgs e)
{
Start();
}
private void FeatureOnDisabled(object? sender, EventArgs e)
{
Stop();
}
/// <inheritdoc /> /// <inheritdoc />
public void Dispose() public void Dispose()
{ {
@ -183,6 +197,12 @@ namespace Artemis.Core
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
#endregion /// <inheritdoc />
public sealed override string ToString()
{
if (Interval.TotalSeconds >= 1)
return $"{Name} ({Interval.TotalSeconds} sec)";
return $"{Name} ({Interval.TotalMilliseconds} ms)";
}
} }
} }

View File

@ -39,9 +39,9 @@ namespace Artemis.Core.Services
ProcessQueuedActions(); ProcessQueuedActions();
} }
private void CopyBuiltInPlugin(FileInfo zipFileInfo, ZipArchive zipArchive) private void CopyBuiltInPlugin(ZipArchive zipArchive, string targetDirectory)
{ {
DirectoryInfo pluginDirectory = new(Path.Combine(Constants.DataFolder, "plugins", Path.GetFileNameWithoutExtension(zipFileInfo.Name))); DirectoryInfo pluginDirectory = new(Path.Combine(Constants.DataFolder, "plugins", targetDirectory));
bool createLockFile = File.Exists(Path.Combine(pluginDirectory.FullName, "artemis.lock")); bool createLockFile = File.Exists(Path.Combine(pluginDirectory.FullName, "artemis.lock"));
// Remove the old directory if it exists // Remove the old directory if it exists
@ -81,12 +81,18 @@ namespace Artemis.Core.Services
using StreamReader reader = new(metaDataFileEntry.Open()); using StreamReader reader = new(metaDataFileEntry.Open());
PluginInfo builtInPluginInfo = CoreJson.DeserializeObject<PluginInfo>(reader.ReadToEnd())!; PluginInfo builtInPluginInfo = CoreJson.DeserializeObject<PluginInfo>(reader.ReadToEnd())!;
string preferred = builtInPluginInfo.PreferredPluginDirectory;
string oldPreferred = Path.GetFileNameWithoutExtension(zipFile.Name);
// Rename folders to the new format
// TODO: Get rid of this eventually, it's nice to keep around but it's extra IO that's best avoided
if (pluginDirectory.EnumerateDirectories().FirstOrDefault(d => d.Name == oldPreferred) != null)
Directory.Move(Path.Combine(pluginDirectory.FullName, oldPreferred), Path.Combine(pluginDirectory.FullName, preferred));
// Find the matching plugin in the plugin folder // Find the matching plugin in the plugin folder
DirectoryInfo? match = pluginDirectory.EnumerateDirectories().FirstOrDefault(d => d.Name == Path.GetFileNameWithoutExtension(zipFile.Name)); DirectoryInfo? match = pluginDirectory.EnumerateDirectories().FirstOrDefault(d => d.Name == preferred);
if (match == null) if (match == null)
{ {
CopyBuiltInPlugin(zipFile, archive); CopyBuiltInPlugin(archive, preferred);
} }
else else
{ {
@ -94,7 +100,7 @@ namespace Artemis.Core.Services
if (!File.Exists(metadataFile)) if (!File.Exists(metadataFile))
{ {
_logger.Debug("Copying missing built-in plugin {builtInPluginInfo}", builtInPluginInfo); _logger.Debug("Copying missing built-in plugin {builtInPluginInfo}", builtInPluginInfo);
CopyBuiltInPlugin(zipFile, archive); CopyBuiltInPlugin(archive, preferred);
} }
else else
{ {
@ -114,7 +120,7 @@ namespace Artemis.Core.Services
if (builtInPluginInfo.Version > pluginInfo.Version) if (builtInPluginInfo.Version > pluginInfo.Version)
{ {
_logger.Debug("Copying updated built-in plugin from {pluginInfo} to {builtInPluginInfo}", pluginInfo, builtInPluginInfo); _logger.Debug("Copying updated built-in plugin from {pluginInfo} to {builtInPluginInfo}", pluginInfo, builtInPluginInfo);
CopyBuiltInPlugin(zipFile, archive); CopyBuiltInPlugin(archive, preferred);
} }
} }
catch (Exception e) catch (Exception e)
@ -342,8 +348,8 @@ namespace Artemis.Core.Services
foreach (Type featureType in featureTypes) foreach (Type featureType in featureTypes)
{ {
// Load the enabled state and if not found, default to true // Load the enabled state and if not found, default to true
PluginFeatureEntity featureEntity = plugin.Entity.Features.FirstOrDefault(i => i.Type == featureType.FullName) ?? PluginFeatureEntity featureEntity = plugin.Entity.Features.FirstOrDefault(i => i.Type == featureType.FullName) ??
new PluginFeatureEntity { IsEnabled = plugin.Info.AutoEnableFeatures, Type = featureType.FullName! }; new PluginFeatureEntity {IsEnabled = plugin.Info.AutoEnableFeatures, Type = featureType.FullName!};
plugin.AddFeature(new PluginFeatureInfo(plugin, featureType, featureEntity, (PluginFeatureAttribute?) Attribute.GetCustomAttribute(featureType, typeof(PluginFeatureAttribute)))); plugin.AddFeature(new PluginFeatureInfo(plugin, featureType, featureEntity, (PluginFeatureAttribute?) Attribute.GetCustomAttribute(featureType, typeof(PluginFeatureAttribute))));
} }
@ -502,6 +508,7 @@ namespace Artemis.Core.Services
Plugin? existing = _plugins.FirstOrDefault(p => p.Guid == pluginInfo.Guid); Plugin? existing = _plugins.FirstOrDefault(p => p.Guid == pluginInfo.Guid);
if (existing != null) if (existing != null)
{
try try
{ {
RemovePlugin(existing, false); RemovePlugin(existing, false);
@ -510,20 +517,14 @@ namespace Artemis.Core.Services
{ {
throw new ArtemisPluginException("A plugin with the same GUID is already loaded, failed to remove old version", e); throw new ArtemisPluginException("A plugin with the same GUID is already loaded, failed to remove old version", e);
} }
string targetDirectory = pluginInfo.Main.Split(".dll")[0].Replace("/", "").Replace("\\", "");
string uniqueTargetDirectory = targetDirectory;
int attempt = 2;
// Find a unique folder
while (pluginDirectory.EnumerateDirectories().Any(d => d.Name == uniqueTargetDirectory))
{
uniqueTargetDirectory = targetDirectory + "-" + attempt;
attempt++;
} }
string targetDirectory = pluginInfo.PreferredPluginDirectory;
if (Directory.Exists(Path.Combine(pluginDirectory.FullName, targetDirectory)))
throw new ArtemisPluginException($"A directory for this plugin already exists {Path.Combine(pluginDirectory.FullName, targetDirectory)}");
// Extract everything in the same archive directory to the unique plugin directory // Extract everything in the same archive directory to the unique plugin directory
DirectoryInfo directoryInfo = new(Path.Combine(pluginDirectory.FullName, uniqueTargetDirectory)); DirectoryInfo directoryInfo = new(Path.Combine(pluginDirectory.FullName, targetDirectory));
Utilities.CreateAccessibleDirectory(directoryInfo.FullName); Utilities.CreateAccessibleDirectory(directoryInfo.FullName);
string metaDataDirectory = metaDataFileEntry.FullName.Replace(metaDataFileEntry.Name, ""); string metaDataDirectory = metaDataFileEntry.FullName.Replace(metaDataFileEntry.Name, "");
foreach (ZipArchiveEntry zipArchiveEntry in archive.Entries) foreach (ZipArchiveEntry zipArchiveEntry in archive.Entries)

View File

@ -9,6 +9,7 @@ namespace Artemis.UI.Screens.Settings.Debug.Tabs.Performance
private string _last; private string _last;
private string _max; private string _max;
private string _min; private string _min;
private string _percentile;
public PerformanceDebugMeasurementViewModel(ProfilingMeasurement measurement) public PerformanceDebugMeasurementViewModel(ProfilingMeasurement measurement)
{ {
@ -41,12 +42,19 @@ namespace Artemis.UI.Screens.Settings.Debug.Tabs.Performance
set => SetAndNotify(ref _max, value); set => SetAndNotify(ref _max, value);
} }
public string Percentile
{
get => _percentile;
set => SetAndNotify(ref _percentile, value);
}
public void Update() public void Update()
{ {
Last = Measurement.GetLast().TotalMilliseconds + " ms"; Last = Measurement.GetLast().TotalMilliseconds + " ms";
Average = Measurement.GetAverage().TotalMilliseconds + " ms"; Average = Measurement.GetAverage().TotalMilliseconds + " ms";
Min = Measurement.GetMin().TotalMilliseconds + " ms"; Min = Measurement.GetMin().TotalMilliseconds + " ms";
Max = Measurement.GetMax().TotalMilliseconds + " ms"; Max = Measurement.GetMax().TotalMilliseconds + " ms";
Percentile = Measurement.GetPercentile(0.95).TotalMilliseconds + " ms";
} }
} }
} }

View File

@ -27,6 +27,7 @@
<materialDesign:DataGridTextColumn Binding="{Binding Min}" Header="Min" /> <materialDesign:DataGridTextColumn Binding="{Binding Min}" Header="Min" />
<materialDesign:DataGridTextColumn Binding="{Binding Max}" Header="Max" /> <materialDesign:DataGridTextColumn Binding="{Binding Max}" Header="Max" />
<materialDesign:DataGridTextColumn Binding="{Binding Average}" Header="Average" /> <materialDesign:DataGridTextColumn Binding="{Binding Average}" Header="Average" />
<materialDesign:DataGridTextColumn Binding="{Binding Percentile}" Header="95th percentile" />
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
</StackPanel> </StackPanel>

View File

@ -19,7 +19,7 @@
If you are having performance issues, below you can find out which plugin might be the culprit. If you are having performance issues, below you can find out which plugin might be the culprit.
</TextBlock> </TextBlock>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Hidden"> <ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Hidden" PreviewMouseWheel="ScrollViewer_PreviewMouseWheel">
<ItemsControl ItemsSource="{Binding Items}" Margin="0 0 10 0"> <ItemsControl ItemsSource="{Binding Items}" Margin="0 0 10 0">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate> <DataTemplate>

View File

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Artemis.UI.Screens.Settings.Debug.Tabs.Performance
{
/// <summary>
/// Interaction logic for PerformanceDebugView.xaml
/// </summary>
public partial class PerformanceDebugView : UserControl
{
public PerformanceDebugView()
{
InitializeComponent();
}
private void ScrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
ScrollViewer scv = (ScrollViewer)sender;
scv.ScrollToVerticalOffset(scv.VerticalOffset - e.Delta);
e.Handled = true;
}
}
}

View File

@ -10,8 +10,7 @@
<tb:TaskbarIcon IconSource="{Binding Icon}" <tb:TaskbarIcon IconSource="{Binding Icon}"
MenuActivation="LeftOrRightClick" MenuActivation="LeftOrRightClick"
PopupActivation="DoubleClick" PopupActivation="DoubleClick"
DoubleClickCommand="{s:Action TrayBringToForeground}" DoubleClickCommand="{s:Action TrayBringToForeground}">
TrayBalloonTipClicked="{s:Action OnTrayBalloonTipClicked}">
<tb:TaskbarIcon.TrayToolTip> <tb:TaskbarIcon.TrayToolTip>
<Border Background="{DynamicResource MaterialDesignToolTipBackground}" CornerRadius="2" Padding="5"> <Border Background="{DynamicResource MaterialDesignToolTipBackground}" CornerRadius="2" Padding="5">

View File

@ -26,10 +26,11 @@ namespace Artemis.UI.Screens
private readonly IKernel _kernel; private readonly IKernel _kernel;
private readonly ThemeWatcher _themeWatcher; private readonly ThemeWatcher _themeWatcher;
private readonly IWindowManager _windowManager; private readonly IWindowManager _windowManager;
private ImageSource _icon;
private bool _openingMainWindow;
private RootViewModel _rootViewModel; private RootViewModel _rootViewModel;
private SplashViewModel _splashViewModel; private SplashViewModel _splashViewModel;
private TaskbarIcon _taskBarIcon; private TaskbarIcon _taskBarIcon;
private ImageSource _icon;
public TrayViewModel(IKernel kernel, public TrayViewModel(IKernel kernel,
IWindowManager windowManager, IWindowManager windowManager,
@ -55,7 +56,7 @@ namespace Artemis.UI.Screens
_themeWatcher.AppsThemeChanged += _themeWatcher_AppsThemeChanged; _themeWatcher.AppsThemeChanged += _themeWatcher_AppsThemeChanged;
ApplyColorSchemeSetting(); ApplyColorSchemeSetting();
ApplyTryIconTheme(_themeWatcher.GetSystemTheme()); ApplyTrayIconTheme(_themeWatcher.GetSystemTheme());
windowService.ConfigureMainWindowProvider(this); windowService.ConfigureMainWindowProvider(this);
bool autoRunning = Bootstrapper.StartupArguments.Contains("--autorun"); bool autoRunning = Bootstrapper.StartupArguments.Contains("--autorun");
@ -63,7 +64,9 @@ namespace Artemis.UI.Screens
bool showOnAutoRun = settingsService.GetSetting("UI.ShowOnStartup", true).Value; bool showOnAutoRun = settingsService.GetSetting("UI.ShowOnStartup", true).Value;
if (autoRunning && !showOnAutoRun || minimized) if (autoRunning && !showOnAutoRun || minimized)
{
coreService.Initialized += (_, _) => updateService.AutoUpdate(); coreService.Initialized += (_, _) => updateService.AutoUpdate();
}
else else
{ {
ShowSplashScreen(); ShowSplashScreen();
@ -71,36 +74,55 @@ namespace Artemis.UI.Screens
} }
} }
public void TrayBringToForeground()
{
if (IsMainWindowOpen)
{
Execute.PostToUIThread(FocusMainWindow);
return;
}
// Initialize the shared UI when first showing the window
if (!UI.Shared.Bootstrapper.Initialized)
UI.Shared.Bootstrapper.Initialize(_kernel);
Execute.OnUIThreadSync(() =>
{
_splashViewModel?.RequestClose();
_splashViewModel = null;
_rootViewModel = _kernel.Get<RootViewModel>();
_rootViewModel.Closed += RootViewModelOnClosed;
_windowManager.ShowWindow(_rootViewModel);
});
OnMainWindowOpened();
}
public ImageSource Icon public ImageSource Icon
{ {
get => _icon; get => _icon;
set => SetAndNotify(ref _icon, value); set => SetAndNotify(ref _icon, value);
} }
public void TrayBringToForeground()
{
if (_openingMainWindow)
return;
try
{
_openingMainWindow = true;
if (IsMainWindowOpen)
{
Execute.OnUIThreadSync(() =>
{
FocusMainWindow();
_openingMainWindow = false;
});
return;
}
// Initialize the shared UI when first showing the window
if (!UI.Shared.Bootstrapper.Initialized)
UI.Shared.Bootstrapper.Initialize(_kernel);
Execute.OnUIThreadSync(() =>
{
_splashViewModel?.RequestClose();
_splashViewModel = null;
_rootViewModel = _kernel.Get<RootViewModel>();
_rootViewModel.Closed += RootViewModelOnClosed;
_windowManager.ShowWindow(_rootViewModel);
IsMainWindowOpen = true;
_openingMainWindow = false;
});
OnMainWindowOpened();
}
finally
{
_openingMainWindow = false;
}
}
public void TrayActivateSidebarItem(string sidebarItem) public void TrayActivateSidebarItem(string sidebarItem)
{ {
TrayBringToForeground(); TrayBringToForeground();
@ -122,14 +144,6 @@ namespace Artemis.UI.Screens
_taskBarIcon = (TaskbarIcon) ((ContentControl) view).Content; _taskBarIcon = (TaskbarIcon) ((ContentControl) view).Content;
} }
public void OnTrayBalloonTipClicked(object sender, EventArgs e)
{
if (!IsMainWindowOpen)
TrayBringToForeground();
else
FocusMainWindow();
}
private void FocusMainWindow() private void FocusMainWindow()
{ {
// Wrestle the main window to the front // Wrestle the main window to the front
@ -158,10 +172,15 @@ namespace Artemis.UI.Screens
private void RootViewModelOnClosed(object sender, CloseEventArgs e) private void RootViewModelOnClosed(object sender, CloseEventArgs e)
{ {
if (_rootViewModel != null) lock (this)
{ {
_rootViewModel.Closed -= RootViewModelOnClosed; if (_rootViewModel != null)
_rootViewModel = null; {
_rootViewModel.Closed -= RootViewModelOnClosed;
_rootViewModel = null;
}
IsMainWindowOpen = false;
} }
OnMainWindowClosed(); OnMainWindowClosed();
@ -187,7 +206,7 @@ namespace Artemis.UI.Screens
ChangeMaterialColors(ApplicationColorScheme.Light); ChangeMaterialColors(ApplicationColorScheme.Light);
} }
private void ApplyTryIconTheme(ThemeWatcher.WindowsTheme theme) private void ApplyTrayIconTheme(ThemeWatcher.WindowsTheme theme)
{ {
Execute.PostToUIThread(() => Execute.PostToUIThread(() =>
{ {
@ -215,7 +234,7 @@ namespace Artemis.UI.Screens
private void _themeWatcher_SystemThemeChanged(object sender, WindowsThemeEventArgs e) private void _themeWatcher_SystemThemeChanged(object sender, WindowsThemeEventArgs e)
{ {
ApplyTryIconTheme(e.Theme); ApplyTrayIconTheme(e.Theme);
} }
private void ColorSchemeOnSettingChanged(object sender, EventArgs e) private void ColorSchemeOnSettingChanged(object sender, EventArgs e)
@ -231,10 +250,7 @@ namespace Artemis.UI.Screens
public bool OpenMainWindow() public bool OpenMainWindow()
{ {
if (IsMainWindowOpen) TrayBringToForeground();
Execute.OnUIThread(FocusMainWindow);
else
TrayBringToForeground();
return _rootViewModel.ScreenState == ScreenState.Active; return _rootViewModel.ScreenState == ScreenState.Active;
} }
@ -250,13 +266,11 @@ namespace Artemis.UI.Screens
protected virtual void OnMainWindowOpened() protected virtual void OnMainWindowOpened()
{ {
IsMainWindowOpen = true;
MainWindowOpened?.Invoke(this, EventArgs.Empty); MainWindowOpened?.Invoke(this, EventArgs.Empty);
} }
protected virtual void OnMainWindowClosed() protected virtual void OnMainWindowClosed()
{ {
IsMainWindowOpen = false;
MainWindowClosed?.Invoke(this, EventArgs.Empty); MainWindowClosed?.Invoke(this, EventArgs.Empty);
} }