From 85921c6b936676278c6320668a6e1af243a67a45 Mon Sep 17 00:00:00 2001 From: Alexandru Macocian Date: Sat, 4 Nov 2023 19:55:47 +0100 Subject: [PATCH 1/2] Minor fixes Fix build loading from GWCA Introduce persistence toggle for logging to reduce disk usage Improve performance of memory usage and processor usage counters Add disk usage counters Closes #462 Closes #461 --- .../Configuration/Options/LauncherOptions.cs | 5 ++ .../Configuration/ProjectConfiguration.cs | 1 + Daybreak/Daybreak.csproj | 2 +- .../BuildTemplates/BuildTemplateManager.cs | 9 ++- Daybreak/Services/Logging/JsonLogsManager.cs | 45 ++++++++++-- .../Services/Monitoring/DiskUsageMonitor.cs | 68 +++++++++++++++++++ .../Services/Monitoring/MemoryUsageMonitor.cs | 29 +++++--- .../Monitoring/ProcessorUsageMonitor.cs | 29 ++++---- Daybreak/Services/Scanner/GWCAMemoryReader.cs | 5 ++ Daybreak/Views/LauncherView.xaml.cs | 3 +- 10 files changed, 165 insertions(+), 31 deletions(-) create mode 100644 Daybreak/Services/Monitoring/DiskUsageMonitor.cs diff --git a/Daybreak/Configuration/Options/LauncherOptions.cs b/Daybreak/Configuration/Options/LauncherOptions.cs index f6d196c8..229c5f6d 100644 --- a/Daybreak/Configuration/Options/LauncherOptions.cs +++ b/Daybreak/Configuration/Options/LauncherOptions.cs @@ -2,6 +2,7 @@ using Daybreak.Views; using Newtonsoft.Json; using System; +using System.ComponentModel; namespace Daybreak.Configuration.Options; @@ -45,4 +46,8 @@ public sealed class LauncherOptions [OptionName(Name = "Mod Startup Timeout", Description = "Amount of seconds that Daybreak will wait for each mod to start-up before cancelling the tasks")] [OptionRange(MinValue = 30, MaxValue = 300)] public double ModStartupTimeout { get; set; } = 30; + + [JsonProperty(nameof(PersistentLogging))] + [OptionName(Name = "Persistent Logging", Description = "If true, the launcher will save logs in the local database. Otherwise, the launcher will only keep logs in a memory cache")] + public bool PersistentLogging { get; set; } = false; } diff --git a/Daybreak/Configuration/ProjectConfiguration.cs b/Daybreak/Configuration/ProjectConfiguration.cs index 2f18b422..5a467886 100644 --- a/Daybreak/Configuration/ProjectConfiguration.cs +++ b/Daybreak/Configuration/ProjectConfiguration.cs @@ -175,6 +175,7 @@ public override void RegisterServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(sp => sp.GetRequiredService()); services.AddSingleton(sp => sp.GetRequiredService()); services.AddSingleton(); diff --git a/Daybreak/Daybreak.csproj b/Daybreak/Daybreak.csproj index 7d50971b..0c23efdb 100644 --- a/Daybreak/Daybreak.csproj +++ b/Daybreak/Daybreak.csproj @@ -13,7 +13,7 @@ preview Daybreak.ico true - 0.9.8.136 + 0.9.8.137 true cfb2a489-db80-448d-a969-80270f314c46 True diff --git a/Daybreak/Services/BuildTemplates/BuildTemplateManager.cs b/Daybreak/Services/BuildTemplates/BuildTemplateManager.cs index 42e4e4e0..23145463 100644 --- a/Daybreak/Services/BuildTemplates/BuildTemplateManager.cs +++ b/Daybreak/Services/BuildTemplates/BuildTemplateManager.cs @@ -229,10 +229,17 @@ private Result DecodeTemplateInner(string template) for(int i = 0; i < buildMetadata.AttributeCount; i++) { var attributeId = buildMetadata.AttributesIds[i]; + if (attributeId == 0) + { + continue; + } + var maybeAttribute = build.Attributes.FirstOrDefault(a => a.Attribute!.Id == attributeId); if (maybeAttribute is null) { - this.logger.LogError($"Failed to parse attribute with id {attributeId} for professions {primaryProfession.Name}/{secondaryProfession.Name}"); + var msg = $"Failed to parse attribute with id {attributeId} for professions {primaryProfession.Name}/{secondaryProfession.Name}"; + this.logger.LogError(msg); + return new InvalidOperationException(msg); } maybeAttribute!.Points = buildMetadata.AttributePoints[i]; diff --git a/Daybreak/Services/Logging/JsonLogsManager.cs b/Daybreak/Services/Logging/JsonLogsManager.cs index a8505b6a..c17a88e7 100644 --- a/Daybreak/Services/Logging/JsonLogsManager.cs +++ b/Daybreak/Services/Logging/JsonLogsManager.cs @@ -1,8 +1,11 @@ -using LiteDB; +using Daybreak.Configuration.Options; +using LiteDB; using System; using System.Collections.Generic; +using System.Configuration; using System.Core.Extensions; using System.Extensions; +using System.Linq; using System.Linq.Expressions; using System.Logging; @@ -10,22 +13,29 @@ namespace Daybreak.Services.Logging; public sealed class JsonLogsManager : ILogsManager { + private const int MemoryCacheMaxSize = 5000; + + private readonly List memoryCache = new(); private readonly ILiteCollection collection; + private readonly ILiveOptions liveOptions; public event EventHandler? ReceivedLog; - public JsonLogsManager(ILiteCollection collection) + public JsonLogsManager( + ILiteCollection collection, + ILiveOptions liveOptions) { this.collection = collection.ThrowIfNull(); + this.liveOptions = liveOptions.ThrowIfNull(); } public IEnumerable GetLogs(Expression> filter) { - return this.collection.Find(filter); + return this.collection.Find(filter).Concat(this.memoryCache.Where(filter.Compile())); } public IEnumerable GetLogs() { - return this.collection.FindAll(); + return this.collection.FindAll().Concat(this.memoryCache); } public void WriteLog(Log log) { @@ -39,7 +49,32 @@ public void WriteLog(Log log) CorrelationVector = log.CorrelationVector }; - this.collection.Insert(dbLog); + if (this.liveOptions.Value.PersistentLogging) + { + lock (this.memoryCache) + { + if (this.memoryCache.Count > 0) + { + this.collection.InsertBulk(this.memoryCache, this.memoryCache.Count); + this.memoryCache.Clear(); + } + } + + this.collection.Insert(dbLog); + } + else + { + lock(this.memoryCache) + { + if (this.memoryCache.Count >= MemoryCacheMaxSize) + { + this.memoryCache.RemoveAt(this.memoryCache.Count - 1); + } + + this.memoryCache.Add(dbLog); + } + } + this.ReceivedLog?.Invoke(this, dbLog); } public int DeleteLogs() diff --git a/Daybreak/Services/Monitoring/DiskUsageMonitor.cs b/Daybreak/Services/Monitoring/DiskUsageMonitor.cs new file mode 100644 index 00000000..87797707 --- /dev/null +++ b/Daybreak/Services/Monitoring/DiskUsageMonitor.cs @@ -0,0 +1,68 @@ +using Daybreak.Services.Metrics; +using System.Diagnostics.Metrics; +using System.Diagnostics; +using System.Threading.Tasks; +using System.Windows.Extensions.Services; +using System.Core.Extensions; +using System.Extensions; +using System.Threading; + +namespace Daybreak.Services.Monitoring; + +public sealed class DiskUsageMonitor : IApplicationLifetimeService +{ + private const string WriteDiskUsage = "Write Disk Usage"; + private const string WriteDiskUsageUnit = "MBs/s"; + private const string WriteDiskUsageDescription = "MBs/s written by Daybreak"; + private const string ReadDiskUsage = "Read Disk Usage"; + private const string ReadDiskUsageUnit = "MBs/s"; + private const string ReadDiskUsageDescription = "MBs/s read by Daybreak"; + + private readonly Histogram writeDiskUsageHistogram; + private readonly Histogram readDiskUsageHistogram; + private readonly CancellationTokenSource cancellationTokenSource = new(); + + private PerformanceCounter? readPerformanceCounter; + private PerformanceCounter? writePerformanceCounter; + + public DiskUsageMonitor( + IMetricsService metricsService) + { + this.writeDiskUsageHistogram = metricsService.ThrowIfNull().CreateHistogram(WriteDiskUsage, WriteDiskUsageUnit, WriteDiskUsageDescription, Models.Metrics.AggregationTypes.NoAggregate); + this.readDiskUsageHistogram = metricsService.ThrowIfNull().CreateHistogram(ReadDiskUsage, ReadDiskUsageUnit, ReadDiskUsageDescription, Models.Metrics.AggregationTypes.NoAggregate); + } + + public void OnClosing() + { + this.cancellationTokenSource.Cancel(); + } + + public void OnStartup() + { + _ = Task.Run(this.StartupAndPeriodicallyReadDiskUsage, this.cancellationTokenSource.Token); + } + + private async Task StartupAndPeriodicallyReadDiskUsage() + { + var processName = Process.GetCurrentProcess().ProcessName; + this.readPerformanceCounter = new PerformanceCounter("Process", "IO Read Bytes/sec", processName); + this.writePerformanceCounter = new PerformanceCounter("Process", "IO Write Bytes/sec", processName); + while (!this.cancellationTokenSource.Token.IsCancellationRequested) + { + try + { + this.PeriodicallyCheckMemoryUsagePerfCounterBased(); + await Task.Delay(1000, this.cancellationTokenSource.Token); + } + catch + { + } + } + } + + private void PeriodicallyCheckMemoryUsagePerfCounterBased() + { + this.readDiskUsageHistogram.Record(this.readPerformanceCounter?.NextValue() / 1024 / 1024 ?? 0); + this.writeDiskUsageHistogram.Record(this.writePerformanceCounter?.NextValue() / 1024 / 1024 ?? 0); + } +} diff --git a/Daybreak/Services/Monitoring/MemoryUsageMonitor.cs b/Daybreak/Services/Monitoring/MemoryUsageMonitor.cs index 3a354c93..314b08e5 100644 --- a/Daybreak/Services/Monitoring/MemoryUsageMonitor.cs +++ b/Daybreak/Services/Monitoring/MemoryUsageMonitor.cs @@ -5,6 +5,7 @@ using System.Windows.Extensions.Services; using System.Core.Extensions; using System.Extensions; +using System.Threading; namespace Daybreak.Services.Monitoring; @@ -12,10 +13,11 @@ public sealed class MemoryUsageMonitor : IApplicationLifetimeService { private const string MemoryUsage = "Memory Usage"; private const string MemoryUsageUnit = "MBs"; - private const string MemoryUsageDescription = "MBs used by the launcher"; + private const string MemoryUsageDescription = "MBs used by Daybreak"; private readonly Histogram memoryUsageHistogram; private readonly Process currentProcess; + private readonly CancellationTokenSource cancellationTokenSource = new(); private PerformanceCounter? memoryPerformanceCounter; @@ -28,11 +30,12 @@ public MemoryUsageMonitor( public void OnClosing() { + this.cancellationTokenSource.Cancel(); } public void OnStartup() { - _ = Task.Run(this.StartupAndPeriodicallyReadMemoryUsage); + _ = Task.Run(this.StartupAndPeriodicallyReadMemoryUsage, this.cancellationTokenSource.Token); } private async Task StartupAndPeriodicallyReadMemoryUsage() @@ -51,20 +54,24 @@ private async Task StartupAndPeriodicallyReadMemoryUsage() private async Task PeriodicallyCheckMemoryUsagePerfCounterBased() { - if (this.memoryPerformanceCounter is not null) + while (!this.cancellationTokenSource.IsCancellationRequested) { - this.memoryUsageHistogram.Record(this.memoryPerformanceCounter.RawValue / 1024); - } + if (this.memoryPerformanceCounter is not null) + { + this.memoryUsageHistogram.Record(this.memoryPerformanceCounter.RawValue / 1024); + } - await Task.Delay(1000); - _ = Task.Run(this.PeriodicallyCheckMemoryUsagePerfCounterBased); + await Task.Delay(1000); + } } private async Task PeriodicallyCheckMemoryUsageProcessBased() { - this.currentProcess.Refresh(); - this.memoryUsageHistogram.Record(this.currentProcess.PrivateMemorySize64 / 1000000); - await Task.Delay(1000); - _ = Task.Run(this.PeriodicallyCheckMemoryUsageProcessBased); + while (!this.cancellationTokenSource.IsCancellationRequested) + { + this.currentProcess.Refresh(); + this.memoryUsageHistogram.Record(this.currentProcess.PrivateMemorySize64 / 1000000); + await Task.Delay(1000); + } } } diff --git a/Daybreak/Services/Monitoring/ProcessorUsageMonitor.cs b/Daybreak/Services/Monitoring/ProcessorUsageMonitor.cs index 486bc192..044c3b02 100644 --- a/Daybreak/Services/Monitoring/ProcessorUsageMonitor.cs +++ b/Daybreak/Services/Monitoring/ProcessorUsageMonitor.cs @@ -3,6 +3,7 @@ using System.Core.Extensions; using System.Diagnostics; using System.Diagnostics.Metrics; +using System.Threading; using System.Threading.Tasks; using System.Windows.Extensions.Services; @@ -12,11 +13,12 @@ public sealed class ProcessorUsageMonitor : IApplicationLifetimeService { private const string ProcessorTime = "Processor Usage"; private const string ProcessorTimeUnit = "% CPU"; - private const string ProcessorTimeDescription = "Percentage of CPU used by the launcher"; + private const string ProcessorTimeDescription = "Percentage of CPU used by Daybreak"; private readonly Histogram processorTimeHistogram; private readonly Process currentProcess; private readonly int processorCount; + private readonly CancellationTokenSource cancellationTokenSource = new(); public ProcessorUsageMonitor( IMetricsService metricsService) @@ -28,24 +30,27 @@ public ProcessorUsageMonitor( public void OnClosing() { + this.cancellationTokenSource.Cancel(); } public void OnStartup() { - _ = Task.Run(this.PeriodicallyCheckCPU); + _ = Task.Run(this.PeriodicallyCheckCPU, this.cancellationTokenSource.Token); } private async Task PeriodicallyCheckCPU() { - var stopwatch = Stopwatch.StartNew(); - var startCpuUsage = this.currentProcess.TotalProcessorTime; - await Task.Delay(1000); - - var endCpuUsage = this.currentProcess.TotalProcessorTime; - var elapsedTicks = stopwatch.ElapsedTicks; - var usage = (double)(endCpuUsage - startCpuUsage).Ticks / (double)elapsedTicks / this.processorCount * 100d; - - this.processorTimeHistogram.Record(usage); - _ = Task.Run(this.PeriodicallyCheckCPU); + while (!this.cancellationTokenSource.IsCancellationRequested) + { + var stopwatch = Stopwatch.StartNew(); + var startCpuUsage = this.currentProcess.TotalProcessorTime; + await Task.Delay(1000, this.cancellationTokenSource.Token); + + var endCpuUsage = this.currentProcess.TotalProcessorTime; + var elapsedTicks = stopwatch.ElapsedTicks; + var usage = (double)(endCpuUsage - startCpuUsage).Ticks / (double)elapsedTicks / this.processorCount * 100d; + + this.processorTimeHistogram.Record(usage); + } } } diff --git a/Daybreak/Services/Scanner/GWCAMemoryReader.cs b/Daybreak/Services/Scanner/GWCAMemoryReader.cs index 7b30d173..0ea09039 100644 --- a/Daybreak/Services/Scanner/GWCAMemoryReader.cs +++ b/Daybreak/Services/Scanner/GWCAMemoryReader.cs @@ -777,6 +777,11 @@ private static PlayerInformation ParsePayload(PartyPlayerPayload partyPlayerPayl return default; } + if (a.ActualLevel == 64) + { + return default; + } + return new AttributeEntry { Attribute = attribute, diff --git a/Daybreak/Views/LauncherView.xaml.cs b/Daybreak/Views/LauncherView.xaml.cs index 1ee82fcd..d470b8ae 100644 --- a/Daybreak/Views/LauncherView.xaml.cs +++ b/Daybreak/Views/LauncherView.xaml.cs @@ -111,12 +111,13 @@ private async void SplitButton_Click(object sender, RoutedEventArgs e) if (this.applicationLauncher.GetGuildwarsProcess(this.LatestConfiguration) is GuildWarsApplicationLaunchContext context) { // Detected already running guildwars process + await this.Dispatcher.InvokeAsync(() => this.Loading = false); if (this.focusViewOptions.Value.Enabled) { + this.menuService.CloseMenu(); this.viewManager.ShowView(context); } - await this.Dispatcher.InvokeAsync(() => this.Loading = false); return; } From dbd45f3628d2bd07760e3f38bfad45d2e44c49e0 Mon Sep 17 00:00:00 2001 From: Alexandru Macocian Date: Sat, 4 Nov 2023 20:17:56 +0100 Subject: [PATCH 2/2] Fix tests --- Daybreak.Tests/Services/JsonLoggerProviderTests.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Daybreak.Tests/Services/JsonLoggerProviderTests.cs b/Daybreak.Tests/Services/JsonLoggerProviderTests.cs index 9651548d..acd122b7 100644 --- a/Daybreak.Tests/Services/JsonLoggerProviderTests.cs +++ b/Daybreak.Tests/Services/JsonLoggerProviderTests.cs @@ -1,8 +1,12 @@ -using Daybreak.Services.Logging; +using Daybreak.Configuration.Options; +using Daybreak.Services.Logging; using FluentAssertions; using LiteDB; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; +using NSubstitute; +using NSubstitute.Extensions; +using System.Configuration; using System.IO; using System.Linq; using System.Logging; @@ -21,7 +25,9 @@ public void InitializeProvider() { File.Delete("Daybreak.db"); this.liteDatabase = new LiteDatabase("Daybreak.db"); - this.logsManager = new JsonLogsManager(this.liteDatabase.GetCollection()); + var options = Substitute.For>(); + options.Value.Returns(new LauncherOptions { PersistentLogging = true }); + this.logsManager = new JsonLogsManager(this.liteDatabase.GetCollection(), options); this.loggerProvider = new CVLoggerProvider(this.logsManager); }