From 1660519bee1513a42749cf43983cad8c8dd7d934 Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 17 May 2021 17:26:49 +0200 Subject: [PATCH] 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 --- .../Plugins/DataModelPluginFeature.cs | 36 +---- src/Artemis.Core/Plugins/Plugin.cs | 9 ++ src/Artemis.Core/Plugins/PluginFeature.cs | 52 ++++++-- src/Artemis.Core/Plugins/PluginInfo.cs | 2 + .../Plugins/Profiling/Profiler.cs | 36 +++-- .../Plugins/Profiling/ProfilingMeasurement.cs | 47 +++++-- .../Plugins/TimedUpdateRegistration.cs | 126 ++++++++++-------- .../Services/PluginManagementService.cs | 39 +++--- .../PerformanceDebugMeasurementViewModel.cs | 8 ++ .../PerformanceDebugProfilerView.xaml | 1 + .../Performance/PerformanceDebugView.xaml | 2 +- .../Performance/PerformanceDebugView.xaml.cs | 35 +++++ src/Artemis.UI/Screens/TrayView.xaml | 3 +- src/Artemis.UI/Screens/TrayViewModel.cs | 104 ++++++++------- 14 files changed, 315 insertions(+), 185 deletions(-) create mode 100644 src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugView.xaml.cs diff --git a/src/Artemis.Core/Plugins/DataModelPluginFeature.cs b/src/Artemis.Core/Plugins/DataModelPluginFeature.cs index 83649847a..099fba242 100644 --- a/src/Artemis.Core/Plugins/DataModelPluginFeature.cs +++ b/src/Artemis.Core/Plugins/DataModelPluginFeature.cs @@ -8,40 +8,6 @@ namespace Artemis.Core /// public abstract class DataModelPluginFeature : PluginFeature { - /// - /// Registers a timed update that whenever the plugin is enabled calls the provided at the - /// provided - /// - /// - /// The interval at which the update should occur - /// - /// The action to call every time the interval has passed. The delta time parameter represents the - /// time passed since the last update in seconds - /// - /// The resulting plugin update registration which can be used to stop the update - public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Action action) - { - if (action == null) - throw new ArgumentNullException(nameof(action)); - return new TimedUpdateRegistration(this, interval, action); - } - - /// - /// Registers a timed update that whenever the plugin is enabled calls the provided at the - /// provided - /// - /// - /// The interval at which the update should occur - /// - /// 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 - /// - /// The resulting plugin update registration - public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Func asyncAction) - { - if (asyncAction == null) - throw new ArgumentNullException(nameof(asyncAction)); - return new TimedUpdateRegistration(this, interval, asyncAction); - } + } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Plugin.cs b/src/Artemis.Core/Plugins/Plugin.cs index 4d226640a..bb6235ac0 100644 --- a/src/Artemis.Core/Plugins/Plugin.cs +++ b/src/Artemis.Core/Plugins/Plugin.cs @@ -146,6 +146,15 @@ namespace Artemis.Core return profiler; } + /// + /// Removes a profiler from the plugin + /// + /// The profiler to remove + public void RemoveProfiler(Profiler profiler) + { + _profilers.Remove(profiler); + } + /// public override string ToString() { diff --git a/src/Artemis.Core/Plugins/PluginFeature.cs b/src/Artemis.Core/Plugins/PluginFeature.cs index 2fd931ebf..983f20438 100644 --- a/src/Artemis.Core/Plugins/PluginFeature.cs +++ b/src/Artemis.Core/Plugins/PluginFeature.cs @@ -51,16 +51,6 @@ namespace Artemis.Core /// public string Id => $"{GetType().FullName}-{Plugin.Guid.ToString().Substring(0, 8)}"; // Not as unique as a GUID but good enough and stays readable - /// - /// Gets the last measured update time of the feature - /// - public TimeSpan UpdateTime { get; private set; } - - /// - /// Gets the last measured render time of the feature - /// - public TimeSpan RenderTime { get; private set; } - internal PluginFeatureEntity Entity { get; set; } = null!; // Will be set right after construction /// @@ -241,5 +231,47 @@ namespace Artemis.Core } #endregion + + #region Timed updates + + /// + /// Registers a timed update that whenever the plugin is enabled calls the provided at the + /// provided + /// + /// + /// The interval at which the update should occur + /// + /// The action to call every time the interval has passed. The delta time parameter represents the + /// time passed since the last update in seconds + /// + /// An optional name used in exceptions and profiling + /// The resulting plugin update registration which can be used to stop the update + public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Action action, string? name = null) + { + if (action == null) + throw new ArgumentNullException(nameof(action)); + return new TimedUpdateRegistration(this, interval, action, name); + } + + /// + /// Registers a timed update that whenever the plugin is enabled calls the provided at the + /// provided + /// + /// + /// The interval at which the update should occur + /// + /// 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 + /// + /// An optional name used in exceptions and profiling + /// The resulting plugin update registration + public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Func asyncAction, string? name = null) + { + if (asyncAction == null) + throw new ArgumentNullException(nameof(asyncAction)); + return new TimedUpdateRegistration(this, interval, asyncAction, name); + } + + #endregion } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/PluginInfo.cs b/src/Artemis.Core/Plugins/PluginInfo.cs index 1a6af3a88..2773a51c3 100644 --- a/src/Artemis.Core/Plugins/PluginInfo.cs +++ b/src/Artemis.Core/Plugins/PluginInfo.cs @@ -124,6 +124,8 @@ namespace Artemis.Core /// public bool ArePrerequisitesMet() => Prerequisites.All(p => p.IsMet()); + internal string PreferredPluginDirectory => $"{Main.Split(".dll")[0].Replace("/", "").Replace("\\", "")}-{Guid.ToString().Substring(0, 8)}"; + /// public override string ToString() { diff --git a/src/Artemis.Core/Plugins/Profiling/Profiler.cs b/src/Artemis.Core/Plugins/Profiling/Profiler.cs index a544bc2f0..c424c19f1 100644 --- a/src/Artemis.Core/Plugins/Profiling/Profiler.cs +++ b/src/Artemis.Core/Plugins/Profiling/Profiler.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Diagnostics; namespace Artemis.Core { @@ -35,13 +37,16 @@ namespace Artemis.Core /// A unique identifier for this measurement public void StartMeasurement(string identifier) { - if (!Measurements.TryGetValue(identifier, out ProfilingMeasurement? measurement)) + lock (Measurements) { - measurement = new ProfilingMeasurement(identifier); - Measurements.Add(identifier, measurement); - } + if (!Measurements.TryGetValue(identifier, out ProfilingMeasurement? measurement)) + { + measurement = new ProfilingMeasurement(identifier); + Measurements.Add(identifier, measurement); + } - measurement.Start(); + measurement.Start(); + } } /// @@ -51,13 +56,17 @@ namespace Artemis.Core /// 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)) + long lockRequestedAt = Stopwatch.GetTimestamp(); + lock (Measurements) { - measurement = new ProfilingMeasurement(identifier); - Measurements.Add(identifier, measurement); - } + if (!Measurements.TryGetValue(identifier, out ProfilingMeasurement? measurement)) + { + measurement = new ProfilingMeasurement(identifier); + Measurements.Add(identifier, measurement); + } - return measurement.Stop(); + return measurement.Stop(Stopwatch.GetTimestamp() - lockRequestedAt); + } } /// @@ -66,7 +75,10 @@ namespace Artemis.Core /// public void ClearMeasurements(string identifier) { - Measurements.Remove(identifier); + lock (Measurements) + { + 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 index 4c8aeaae9..eed3a391d 100644 --- a/src/Artemis.Core/Plugins/Profiling/ProfilingMeasurement.cs +++ b/src/Artemis.Core/Plugins/Profiling/ProfilingMeasurement.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Diagnostics; using System.Linq; namespace Artemis.Core @@ -11,7 +13,8 @@ namespace Artemis.Core private bool _filledArray; private int _index; private long _last; - private DateTime? _start; + private bool _open; + private long _start; internal ProfilingMeasurement(string identifier) { @@ -33,22 +36,24 @@ namespace Artemis.Core /// public void Start() { - _start = DateTime.UtcNow; + _start = Stopwatch.GetTimestamp(); + _open = true; } /// /// Stops measuring time and stores the time passed in the list /// + /// An optional correction in ticks to subtract from the measurement /// The time passed since the last call - public long Stop() + public long Stop(long correction = 0) { - if (_start == null) + if (!_open) return 0; - - long difference = (DateTime.UtcNow - _start.Value).Ticks; + + long difference = Stopwatch.GetTimestamp() - _start - correction; + _open = false; Measurements[_index] = difference; - _start = null; - + _index++; if (_index >= 1000) { @@ -106,5 +111,31 @@ namespace Artemis.Core ? new TimeSpan(Measurements.Max()) : new TimeSpan(Measurements.Take(_index).Max()); } + + /// + /// Gets the nth percentile of the last 1000 measurements + /// + 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]; + } } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/TimedUpdateRegistration.cs b/src/Artemis.Core/Plugins/TimedUpdateRegistration.cs index f93225481..65eede731 100644 --- a/src/Artemis.Core/Plugins/TimedUpdateRegistration.cs +++ b/src/Artemis.Core/Plugins/TimedUpdateRegistration.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using System.Timers; using Artemis.Core.Modules; using Artemis.Core.Services; +using Humanizer; using Ninject; using Serilog; @@ -13,19 +14,20 @@ namespace Artemis.Core /// public class TimedUpdateRegistration : IDisposable { - private DateTime _lastEvent; - private Timer? _timer; - private bool _disposed; 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 action) + internal TimedUpdateRegistration(PluginFeature feature, TimeSpan interval, Action action, string? name) { _logger = CoreService.Kernel.Get(); Feature = feature; Interval = interval; Action = action; + Name = name ?? $"TimedUpdate-{Guid.NewGuid().ToString().Substring(0, 8)}"; Feature.Enabled += FeatureOnEnabled; Feature.Disabled += FeatureOnDisabled; @@ -33,13 +35,14 @@ namespace Artemis.Core Start(); } - internal TimedUpdateRegistration(PluginFeature feature, TimeSpan interval, Func asyncAction) + internal TimedUpdateRegistration(PluginFeature feature, TimeSpan interval, Func asyncAction, string? name) { _logger = CoreService.Kernel.Get(); - + Feature = feature; Interval = interval; AsyncAction = asyncAction; + Name = name ?? $"TimedUpdate-{Guid.NewGuid().ToString().Substring(0, 8)}"; Feature.Enabled += FeatureOnEnabled; Feature.Disabled += FeatureOnDisabled; @@ -69,7 +72,12 @@ namespace Artemis.Core public Func? AsyncAction { get; } /// - /// Starts calling the or at the configured + /// Gets the name of this timed update + /// + public string Name { get; } + + /// + /// Starts calling the or at the configured /// Note: Called automatically when the plugin enables /// public void Start() @@ -93,7 +101,7 @@ namespace Artemis.Core } /// - /// Stops calling the or at the configured + /// Stops calling the or at the configured /// Note: Called automatically when the plugin disables /// 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 - /// /// Releases the unmanaged resources used by the object and optionally releases the managed resources. /// @@ -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(); + } + /// public void Dispose() { @@ -183,6 +197,12 @@ namespace Artemis.Core GC.SuppressFinalize(this); } - #endregion + /// + public sealed override string ToString() + { + if (Interval.TotalSeconds >= 1) + return $"{Name} ({Interval.TotalSeconds} sec)"; + return $"{Name} ({Interval.TotalMilliseconds} ms)"; + } } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index 0d312a262..5655b8112 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -39,9 +39,9 @@ namespace Artemis.Core.Services 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")); // Remove the old directory if it exists @@ -81,12 +81,18 @@ namespace Artemis.Core.Services using StreamReader reader = new(metaDataFileEntry.Open()); PluginInfo builtInPluginInfo = CoreJson.DeserializeObject(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 - DirectoryInfo? match = pluginDirectory.EnumerateDirectories().FirstOrDefault(d => d.Name == Path.GetFileNameWithoutExtension(zipFile.Name)); + DirectoryInfo? match = pluginDirectory.EnumerateDirectories().FirstOrDefault(d => d.Name == preferred); if (match == null) { - CopyBuiltInPlugin(zipFile, archive); + CopyBuiltInPlugin(archive, preferred); } else { @@ -94,7 +100,7 @@ namespace Artemis.Core.Services if (!File.Exists(metadataFile)) { _logger.Debug("Copying missing built-in plugin {builtInPluginInfo}", builtInPluginInfo); - CopyBuiltInPlugin(zipFile, archive); + CopyBuiltInPlugin(archive, preferred); } else { @@ -114,7 +120,7 @@ namespace Artemis.Core.Services if (builtInPluginInfo.Version > pluginInfo.Version) { _logger.Debug("Copying updated built-in plugin from {pluginInfo} to {builtInPluginInfo}", pluginInfo, builtInPluginInfo); - CopyBuiltInPlugin(zipFile, archive); + CopyBuiltInPlugin(archive, preferred); } } catch (Exception e) @@ -342,8 +348,8 @@ namespace Artemis.Core.Services foreach (Type featureType in featureTypes) { // Load the enabled state and if not found, default to true - PluginFeatureEntity featureEntity = plugin.Entity.Features.FirstOrDefault(i => i.Type == featureType.FullName) ?? - new PluginFeatureEntity { IsEnabled = plugin.Info.AutoEnableFeatures, Type = featureType.FullName! }; + PluginFeatureEntity featureEntity = plugin.Entity.Features.FirstOrDefault(i => i.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)))); } @@ -502,6 +508,7 @@ namespace Artemis.Core.Services Plugin? existing = _plugins.FirstOrDefault(p => p.Guid == pluginInfo.Guid); if (existing != null) + { try { 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); } - - 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 - DirectoryInfo directoryInfo = new(Path.Combine(pluginDirectory.FullName, uniqueTargetDirectory)); + DirectoryInfo directoryInfo = new(Path.Combine(pluginDirectory.FullName, targetDirectory)); Utilities.CreateAccessibleDirectory(directoryInfo.FullName); string metaDataDirectory = metaDataFileEntry.FullName.Replace(metaDataFileEntry.Name, ""); foreach (ZipArchiveEntry zipArchiveEntry in archive.Entries) diff --git a/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugMeasurementViewModel.cs b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugMeasurementViewModel.cs index 7bec70c87..4b47d5e0a 100644 --- a/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugMeasurementViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugMeasurementViewModel.cs @@ -9,6 +9,7 @@ namespace Artemis.UI.Screens.Settings.Debug.Tabs.Performance private string _last; private string _max; private string _min; + private string _percentile; public PerformanceDebugMeasurementViewModel(ProfilingMeasurement measurement) { @@ -41,12 +42,19 @@ namespace Artemis.UI.Screens.Settings.Debug.Tabs.Performance set => SetAndNotify(ref _max, value); } + public string Percentile + { + get => _percentile; + set => SetAndNotify(ref _percentile, value); + } + public void Update() { Last = Measurement.GetLast().TotalMilliseconds + " ms"; Average = Measurement.GetAverage().TotalMilliseconds + " ms"; Min = Measurement.GetMin().TotalMilliseconds + " ms"; Max = Measurement.GetMax().TotalMilliseconds + " ms"; + Percentile = Measurement.GetPercentile(0.95).TotalMilliseconds + " ms"; } } } \ 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 index f8e4cad6a..6fa8f1dba 100644 --- a/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugProfilerView.xaml +++ b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugProfilerView.xaml @@ -27,6 +27,7 @@ + diff --git a/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugView.xaml b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugView.xaml index cdc24de90..a63321f76 100644 --- a/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugView.xaml +++ b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugView.xaml @@ -19,7 +19,7 @@ If you are having performance issues, below you can find out which plugin might be the culprit. - + diff --git a/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugView.xaml.cs b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugView.xaml.cs new file mode 100644 index 000000000..6492ab859 --- /dev/null +++ b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugView.xaml.cs @@ -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 +{ + /// + /// Interaction logic for PerformanceDebugView.xaml + /// + 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; + } + } +} diff --git a/src/Artemis.UI/Screens/TrayView.xaml b/src/Artemis.UI/Screens/TrayView.xaml index de13735d3..746897449 100644 --- a/src/Artemis.UI/Screens/TrayView.xaml +++ b/src/Artemis.UI/Screens/TrayView.xaml @@ -10,8 +10,7 @@ + DoubleClickCommand="{s:Action TrayBringToForeground}"> diff --git a/src/Artemis.UI/Screens/TrayViewModel.cs b/src/Artemis.UI/Screens/TrayViewModel.cs index 42df43ce9..b8d96909d 100644 --- a/src/Artemis.UI/Screens/TrayViewModel.cs +++ b/src/Artemis.UI/Screens/TrayViewModel.cs @@ -26,10 +26,11 @@ namespace Artemis.UI.Screens private readonly IKernel _kernel; private readonly ThemeWatcher _themeWatcher; private readonly IWindowManager _windowManager; + private ImageSource _icon; + private bool _openingMainWindow; private RootViewModel _rootViewModel; private SplashViewModel _splashViewModel; private TaskbarIcon _taskBarIcon; - private ImageSource _icon; public TrayViewModel(IKernel kernel, IWindowManager windowManager, @@ -55,7 +56,7 @@ namespace Artemis.UI.Screens _themeWatcher.AppsThemeChanged += _themeWatcher_AppsThemeChanged; ApplyColorSchemeSetting(); - ApplyTryIconTheme(_themeWatcher.GetSystemTheme()); + ApplyTrayIconTheme(_themeWatcher.GetSystemTheme()); windowService.ConfigureMainWindowProvider(this); bool autoRunning = Bootstrapper.StartupArguments.Contains("--autorun"); @@ -63,7 +64,9 @@ namespace Artemis.UI.Screens bool showOnAutoRun = settingsService.GetSetting("UI.ShowOnStartup", true).Value; if (autoRunning && !showOnAutoRun || minimized) + { coreService.Initialized += (_, _) => updateService.AutoUpdate(); + } else { 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.Closed += RootViewModelOnClosed; - _windowManager.ShowWindow(_rootViewModel); - }); - - OnMainWindowOpened(); - } - public ImageSource Icon { get => _icon; 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.Closed += RootViewModelOnClosed; + _windowManager.ShowWindow(_rootViewModel); + + IsMainWindowOpen = true; + _openingMainWindow = false; + }); + + OnMainWindowOpened(); + } + finally + { + _openingMainWindow = false; + } + } + public void TrayActivateSidebarItem(string sidebarItem) { TrayBringToForeground(); @@ -122,14 +144,6 @@ namespace Artemis.UI.Screens _taskBarIcon = (TaskbarIcon) ((ContentControl) view).Content; } - public void OnTrayBalloonTipClicked(object sender, EventArgs e) - { - if (!IsMainWindowOpen) - TrayBringToForeground(); - else - FocusMainWindow(); - } - private void FocusMainWindow() { // Wrestle the main window to the front @@ -158,10 +172,15 @@ namespace Artemis.UI.Screens private void RootViewModelOnClosed(object sender, CloseEventArgs e) { - if (_rootViewModel != null) + lock (this) { - _rootViewModel.Closed -= RootViewModelOnClosed; - _rootViewModel = null; + if (_rootViewModel != null) + { + _rootViewModel.Closed -= RootViewModelOnClosed; + _rootViewModel = null; + } + + IsMainWindowOpen = false; } OnMainWindowClosed(); @@ -187,7 +206,7 @@ namespace Artemis.UI.Screens ChangeMaterialColors(ApplicationColorScheme.Light); } - private void ApplyTryIconTheme(ThemeWatcher.WindowsTheme theme) + private void ApplyTrayIconTheme(ThemeWatcher.WindowsTheme theme) { Execute.PostToUIThread(() => { @@ -215,7 +234,7 @@ namespace Artemis.UI.Screens private void _themeWatcher_SystemThemeChanged(object sender, WindowsThemeEventArgs e) { - ApplyTryIconTheme(e.Theme); + ApplyTrayIconTheme(e.Theme); } private void ColorSchemeOnSettingChanged(object sender, EventArgs e) @@ -231,10 +250,7 @@ namespace Artemis.UI.Screens public bool OpenMainWindow() { - if (IsMainWindowOpen) - Execute.OnUIThread(FocusMainWindow); - else - TrayBringToForeground(); + TrayBringToForeground(); return _rootViewModel.ScreenState == ScreenState.Active; } @@ -250,13 +266,11 @@ namespace Artemis.UI.Screens protected virtual void OnMainWindowOpened() { - IsMainWindowOpen = true; MainWindowOpened?.Invoke(this, EventArgs.Empty); } protected virtual void OnMainWindowClosed() { - IsMainWindowOpen = false; MainWindowClosed?.Invoke(this, EventArgs.Empty); }