From 4e5813c59c055b2fcf8346f85c59c09c31f9b215 Mon Sep 17 00:00:00 2001 From: ASLant <77436463+Y-ASLant@users.noreply.github.com> Date: Thu, 11 Jun 2026 09:58:53 +0800 Subject: [PATCH] perf: optimize resource allocation and improve thread safety - Cache Languages list in Localizer to avoid repeated initialization - Replace GCHandle.Alloc with Marshal.AllocHGlobal in ComputerService to reduce GC pressure and heap fragmentation - Cache Font and StringFormat in NotificationService for icon rendering - Cache Brushes collection in MainViewModel and fix process disposal leak - Replace BitArray with Brian Kernighan bit counting algorithm - Add Interlocked guard for concurrent optimization in WinService - Fix lock granularity in MainViewModel.Optimize for better throughput - Simplify ServiceController usage in WinService - Remove redundant Settings.Save() call in Settings finalizer Closes #168 Closes #181 Closes #183 Closes #190 Closes #192 --- src/Core/Localizer.cs | 10 ++- src/Core/Settings.cs | 1 - src/Service/ComputerService.cs | 22 ++---- src/Service/NotificationService.cs | 32 ++++++-- src/ViewModel/MainViewModel.cs | 123 +++++++++++++++++++++-------- src/WindowsService/WinService.cs | 72 +++++++++++------ 6 files changed, 179 insertions(+), 81 deletions(-) diff --git a/src/Core/Localizer.cs b/src/Core/Localizer.cs index 9058a83e..e49bd82c 100644 --- a/src/Core/Localizer.cs +++ b/src/Core/Localizer.cs @@ -27,6 +27,7 @@ public static class Localizer private static CultureInfo _culture; private static Language _language; + private static List _languages; #endregion @@ -118,6 +119,9 @@ public static List Languages { get { + if (_languages != null) + return _languages; + try { var resourceNames = Assembly.GetExecutingAssembly().GetManifestResourceNames() @@ -140,7 +144,7 @@ public static List Languages // ignored } - return CultureInfo.GetCultures(CultureTypes.AllCultures) + _languages = CultureInfo.GetCultures(CultureTypes.AllCultures) .Where(culture => resourceNames.Contains(culture.EnglishName, StringComparer.OrdinalIgnoreCase)) .OrderBy(culture => culture.EnglishName, StringComparer.InvariantCultureIgnoreCase) .Select(culture => new Language(culture)) @@ -150,8 +154,10 @@ public static List Languages { Logger.Error(e); - return new List { new Language(new CultureInfo(Constants.Windows.Locale.Name.English)) }; + _languages = new List { new Language(new CultureInfo(Constants.Windows.Locale.Name.English)) }; } + + return _languages; } } diff --git a/src/Core/Settings.cs b/src/Core/Settings.cs index 1155ee04..71c844ab 100644 --- a/src/Core/Settings.cs +++ b/src/Core/Settings.cs @@ -19,7 +19,6 @@ public static class Settings static Settings() { Load(); - Save(); } #endregion diff --git a/src/Service/ComputerService.cs b/src/Service/ComputerService.cs index a4ca5319..ab763e1e 100644 --- a/src/Service/ComputerService.cs +++ b/src/Service/ComputerService.cs @@ -466,7 +466,7 @@ private void OptimizeCombinedPageList() if (!SetIncreasePrivilege(Constants.Windows.Privilege.SeProfSingleProcessName)) throw new Exception(string.Format(Localizer.Culture, Localizer.String.ErrorAdminPrivilegeRequired, Constants.Windows.Privilege.SeProfSingleProcessName)); - var handle = GCHandle.Alloc(0); + var handle = default(GCHandle); try { @@ -634,25 +634,19 @@ private void OptimizeStandbyList(bool lowPriority = false) if (!SetIncreasePrivilege(Constants.Windows.Privilege.SeProfSingleProcessName)) throw new Exception(string.Format(Localizer.Culture, Localizer.String.ErrorAdminPrivilegeRequired, Constants.Windows.Privilege.SeProfSingleProcessName)); - object memoryPurgeStandbyList = lowPriority ? Constants.Windows.SystemMemoryListCommand.MemoryPurgeLowPriorityStandbyList : Constants.Windows.SystemMemoryListCommand.MemoryPurgeStandbyList; - var handle = GCHandle.Alloc(memoryPurgeStandbyList, GCHandleType.Pinned); + var memoryPurgeStandbyList = lowPriority ? Constants.Windows.SystemMemoryListCommand.MemoryPurgeLowPriorityStandbyList : Constants.Windows.SystemMemoryListCommand.MemoryPurgeStandbyList; + var buffer = Marshal.AllocHGlobal(sizeof(int)); try { - if (NativeMethods.NtSetSystemInformation(Constants.Windows.SystemInformationClass.SystemMemoryListInformation, handle.AddrOfPinnedObject(), (uint)Marshal.SizeOf(memoryPurgeStandbyList)) != Constants.Windows.SystemErrorCode.ErrorSuccess) + Marshal.WriteInt32(buffer, (int)memoryPurgeStandbyList); + + if (NativeMethods.NtSetSystemInformation(Constants.Windows.SystemInformationClass.SystemMemoryListInformation, buffer, (uint)sizeof(int)) != Constants.Windows.SystemErrorCode.ErrorSuccess) throw new Win32Exception(Marshal.GetLastWin32Error()); } finally { - try - { - if (handle.IsAllocated) - handle.Free(); - } - catch (InvalidOperationException) - { - // ignored - } + Marshal.FreeHGlobal(buffer); } } @@ -669,7 +663,7 @@ private void OptimizeSystemFileCache() if (!SetIncreasePrivilege(Constants.Windows.Privilege.SeIncreaseQuotaName)) throw new Exception(string.Format(Localizer.Culture, Localizer.String.ErrorAdminPrivilegeRequired, Constants.Windows.Privilege.SeIncreaseQuotaName)); - var handle = GCHandle.Alloc(0); + var handle = default(GCHandle); try { diff --git a/src/Service/NotificationService.cs b/src/Service/NotificationService.cs index 78290cb8..38bae557 100644 --- a/src/Service/NotificationService.cs +++ b/src/Service/NotificationService.cs @@ -22,10 +22,12 @@ public class NotificationService : INotificationService private int _currentRotationAngle; private Icon _currentIcon; private bool _disposed; + private readonly Font _cachedFont; private readonly Icon _imageIcon; private readonly NotifyIcon _notifyIcon; private readonly object _disposeLock = new object(); private DispatcherTimer _rotationTimer; + private readonly StringFormat _cachedStringFormat; #endregion @@ -38,6 +40,8 @@ public class NotificationService : INotificationService public NotificationService(NotifyIcon notifyIcon) { _currentRotationAngle = 0; + _cachedFont = new Font("Consolas", 14F, FontStyle.Regular, GraphicsUnit.Pixel); + _cachedStringFormat = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }; _imageIcon = Icon.ExtractAssociatedIcon(Assembly.GetExecutingAssembly().Location); _notifyIcon = notifyIcon; @@ -156,6 +160,26 @@ protected virtual void Dispose(bool disposing) Logger.Debug(ex); } + try + { + if (_cachedFont != null) + _cachedFont.Dispose(); + } + catch (Exception ex) + { + Logger.Debug(ex); + } + + try + { + if (_cachedStringFormat != null) + _cachedStringFormat.Dispose(); + } + catch (Exception ex) + { + Logger.Debug(ex); + } + try { if (_notifyIcon != null) @@ -293,15 +317,9 @@ private Icon GetMemoryUsageIcon(Memory memory, bool isOptimizing) { using (var image = new Bitmap(16, 16)) using (var graphics = Graphics.FromImage(image)) - using (var font = new Font("Consolas", 14F, FontStyle.Regular, GraphicsUnit.Pixel)) - using (var format = new StringFormat()) using (var backgroundBrush = GetBackgroundBrush(memory, isOptimizing)) using (var textBrush = GetTextBrush(memory, isOptimizing)) { - // Configure format - format.Alignment = StringAlignment.Center; - format.LineAlignment = StringAlignment.Center; - // Configure graphics quality graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; @@ -324,7 +342,7 @@ private Icon GetMemoryUsageIcon(Memory memory, bool isOptimizing) } // Draw text - graphics.DrawString(string.Format(CultureInfo.InvariantCulture, "{0:00}", memory.Physical.Used.Percentage == 100 ? 99 : memory.Physical.Used.Percentage), font, textBrush, 8F, 9F, format); + graphics.DrawString(string.Format(CultureInfo.InvariantCulture, "{0:00}", memory.Physical.Used.Percentage == 100 ? 99 : memory.Physical.Used.Percentage), _cachedFont, textBrush, 8F, 9F, _cachedStringFormat); var handle = image.GetHicon(); diff --git a/src/ViewModel/MainViewModel.cs b/src/ViewModel/MainViewModel.cs index 84feb147..cce45a63 100644 --- a/src/ViewModel/MainViewModel.cs +++ b/src/ViewModel/MainViewModel.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; @@ -20,6 +19,7 @@ public class MainViewModel : ViewModel, IDisposable #region Fields private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); + private ObservableCollection _cachedBrushes; private Computer _computer; private readonly IComputerService _computerService; private readonly IHotkeyService _hotKeyService; @@ -250,7 +250,10 @@ public ObservableCollection Brushes { get { - return new ObservableCollection(App.IsInDesignMode ? new List { System.Windows.Media.Brushes.White } : ThemeManager.Brushes); + if (_cachedBrushes == null) + _cachedBrushes = new ObservableCollection(App.IsInDesignMode ? new List { System.Windows.Media.Brushes.White } : ThemeManager.Brushes); + + return _cachedBrushes; } } @@ -543,6 +546,7 @@ public Language Language RaisePropertyChanged(() => Computer); _trayIconItems = null; + _cachedBrushes = null; NotificationService.Initialize(); NotificationService.Update(Computer.Memory, IsOptimizationRunning); @@ -818,16 +822,32 @@ public ObservableCollection Processes { get { - var processes = new ObservableCollection(Process.GetProcesses() - .Where(process => process != null && !process.ProcessName.Equals(Constants.App.Name) && !Settings.ProcessExclusionList.Contains(process.ProcessName, StringComparer.OrdinalIgnoreCase)) - .Select(process => process.ProcessName.ToLower(Localizer.Culture).Replace(".exe", string.Empty)) - .Distinct() - .OrderBy(name => name)); + var allProcesses = Process.GetProcesses(); + + try + { + var names = allProcesses + .Where(process => process != null && !process.ProcessName.Equals(Constants.App.Name) && !Settings.ProcessExclusionList.Contains(process.ProcessName)) + .Select(process => process.ProcessName.ToLower(Localizer.Culture).Replace(".exe", string.Empty)) + .Distinct() + .OrderBy(name => name) + .ToList(); + + var processes = new ObservableCollection(names); - if (!processes.Contains(SelectedProcess, StringComparer.OrdinalIgnoreCase)) - SelectedProcess = processes.FirstOrDefault(); + if (!processes.Contains(SelectedProcess, StringComparer.OrdinalIgnoreCase)) + SelectedProcess = processes.FirstOrDefault(); - return processes; + return processes; + } + finally + { + foreach (var process in allProcesses) + { + if (process != null) + process.Dispose(); + } + } } } @@ -1619,7 +1639,12 @@ private void MonitorApp() { // Check if it's busy if (IsBusy) + { + if (_cancellationTokenSource.Token.WaitHandle.WaitOne(1000)) + break; + continue; + } // Delay if (_cancellationTokenSource.Token.WaitHandle.WaitOne(60000)) @@ -1706,7 +1731,12 @@ private void MonitorComputer() { // Check if it's busy if (IsBusy) + { + if (_cancellationTokenSource.Token.WaitHandle.WaitOne(1000)) + break; + continue; + } lock (_lockObject) { @@ -1748,9 +1778,12 @@ private void OnOptimizeProgressUpdate(byte value, string step) /// Optimization reason private void Optimize(Enums.Memory.Optimization.Reason reason) { - lock (_lockObject) + try { - try + long tempPhysicalAvailable; + long tempVirtualAvailable; + + lock (_lockObject) { IsBusy = true; IsOptimizationRunning = true; @@ -1761,11 +1794,14 @@ private void Optimize(Enums.Memory.Optimization.Reason reason) App.SetPriority(Settings.RunOnPriority); // Memory optimize - var tempPhysicalAvailable = Computer.Memory.Physical.Free.Bytes; - var tempVirtualAvailable = Computer.Memory.Virtual.Free.Bytes; + tempPhysicalAvailable = Computer.Memory.Physical.Free.Bytes; + tempVirtualAvailable = Computer.Memory.Virtual.Free.Bytes; + } - _computerService.Optimize(reason, Settings.MemoryAreas); + _computerService.Optimize(reason, Settings.MemoryAreas); + lock (_lockObject) + { // Update memory info Computer.Memory = _computerService.Memory; RaisePropertyChanged(() => Computer); @@ -1783,29 +1819,32 @@ private void Optimize(Enums.Memory.Optimization.Reason reason) Notify(message); } } - finally + } + finally + { + lock (_lockObject) { IsOptimizationRunning = false; IsBusy = false; NotificationService.Update(Computer.Memory, IsOptimizationRunning); + } - // Raise the event after IsOptimizationRunning is set to false - // Use BeginInvoke to ensure it runs after all property changes propagate - WpfApplication.Current.Dispatcher.BeginInvoke((Action)(() => - { - // Force command manager to re-evaluate CanExecute on all commands - CommandManager.InvalidateRequerySuggested(); - }), System.Windows.Threading.DispatcherPriority.Normal); + // Raise the event after IsOptimizationRunning is set to false + // Use BeginInvoke to ensure it runs after all property changes propagate + WpfApplication.Current.Dispatcher.BeginInvoke((Action)(() => + { + // Force command manager to re-evaluate CanExecute on all commands + CommandManager.InvalidateRequerySuggested(); + }), System.Windows.Threading.DispatcherPriority.Normal); - // Raise completion event with lower priority to ensure commands are refreshed first - if (OnOptimizeCommandCompleted != null) - { - WpfApplication.Current.Dispatcher.BeginInvoke((Action)(() => - { - OnOptimizeCommandCompleted(); - }), System.Windows.Threading.DispatcherPriority.ApplicationIdle); - } + // Raise completion event with lower priority to ensure commands are refreshed first + if (OnOptimizeCommandCompleted != null) + { + WpfApplication.Current.Dispatcher.BeginInvoke((Action)(() => + { + OnOptimizeCommandCompleted(); + }), System.Windows.Threading.DispatcherPriority.ApplicationIdle); } } } @@ -1818,12 +1857,12 @@ private void OptimizeAsync(Enums.Memory.Optimization.Reason reason) { try { - if (IsOptimizationRunning) + if (_isOptimizationRunning) return; OptimizationProgressStep = Localizer.String.Optimize; OptimizationProgressValue = 0; - OptimizationProgressTotal = (byte)(new BitArray(new[] { (int)Settings.MemoryAreas }).OfType().Count(x => x) + 1); + OptimizationProgressTotal = (byte)(CountSetBits((int)Settings.MemoryAreas) + 1); ThreadPool.QueueUserWorkItem(_ => Optimize(reason)); } @@ -1833,6 +1872,24 @@ private void OptimizeAsync(Enums.Memory.Optimization.Reason reason) } } + /// + /// Counts the number of set bits (1s) in the binary representation of the value. + /// + /// The value to count bits in. + /// The number of set bits. + private static int CountSetBits(int value) + { + int count = 0; + + while (value != 0) + { + count++; + value &= value - 1; + } + + return count; + } + /// /// Registers the optimization hotkey. /// diff --git a/src/WindowsService/WinService.cs b/src/WindowsService/WinService.cs index a77aee8a..5d3999a4 100644 --- a/src/WindowsService/WinService.cs +++ b/src/WindowsService/WinService.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.ServiceProcess; +using System.Threading; using System.Timers; namespace WinMemoryCleaner @@ -14,7 +15,8 @@ public class WinService : ServiceBase private readonly IComputerService _computerService; private DateTimeOffset _lastAutoOptimizationByInterval = DateTimeOffset.Now; private DateTimeOffset _lastAutoOptimizationByMemoryUsage = DateTimeOffset.Now; - private readonly Timer _timer = new Timer(60000); + private int _optimizing; + private readonly System.Timers.Timer _timer = new System.Timers.Timer(60000); /// /// Initializes a new instance of the class. @@ -44,7 +46,19 @@ public static bool IsInstalled { get { - return ServiceController.GetServices().Any(sc => string.Equals(sc.ServiceName, Constants.App.Name, StringComparison.OrdinalIgnoreCase)); + try + { + using (var service = new ServiceController(Constants.App.Name)) + { + var status = service.Status; + + return true; + } + } + catch + { + return false; + } } } @@ -60,13 +74,13 @@ public static Enums.ServiceStatus Status { try { - using (var service = ServiceController.GetServices().First(sc => string.Equals(sc.ServiceName, Constants.App.Name, StringComparison.OrdinalIgnoreCase))) + using (var service = new ServiceController(Constants.App.Name)) { service.Refresh(); return (Enums.ServiceStatus)service.Status; } - } + } catch { return Enums.ServiceStatus.NotInstalled; @@ -110,31 +124,41 @@ protected override void OnStop() /// private void OnTimer(object sender, ElapsedEventArgs e) { - // App priority - App.SetPriority(Settings.RunOnPriority); - - // Update memory info - _computer.Memory = _computerService.Memory; + if (Interlocked.CompareExchange(ref _optimizing, 1, 0) != 0) + return; - // Interval - if (Settings.AutoOptimizationInterval > 0 && - DateTimeOffset.Now.Subtract(_lastAutoOptimizationByInterval).TotalHours >= Settings.AutoOptimizationInterval) + try { - DependencyInjection.Container.Resolve().Optimize(Enums.Memory.Optimization.Reason.Schedule, Settings.MemoryAreas); + // App priority + App.SetPriority(Settings.RunOnPriority); - _lastAutoOptimizationByInterval = DateTimeOffset.Now; - return; - } + // Update memory info + _computer.Memory = _computerService.Memory; - // Memory usage - if (Settings.AutoOptimizationMemoryUsage > 0 && - _computer.Memory.Physical.Free.Percentage < Settings.AutoOptimizationMemoryUsage && - DateTimeOffset.Now.Subtract(_lastAutoOptimizationByMemoryUsage).TotalMinutes >= Constants.App.AutoOptimizationMemoryUsageInterval) - { - DependencyInjection.Container.Resolve().Optimize(Enums.Memory.Optimization.Reason.LowMemory, Settings.MemoryAreas); + // Interval + if (Settings.AutoOptimizationInterval > 0 && + DateTimeOffset.Now.Subtract(_lastAutoOptimizationByInterval).TotalHours >= Settings.AutoOptimizationInterval) + { + DependencyInjection.Container.Resolve().Optimize(Enums.Memory.Optimization.Reason.Schedule, Settings.MemoryAreas); - _lastAutoOptimizationByMemoryUsage = DateTimeOffset.Now; - return; + _lastAutoOptimizationByInterval = DateTimeOffset.Now; + return; + } + + // Memory usage + if (Settings.AutoOptimizationMemoryUsage > 0 && + _computer.Memory.Physical.Free.Percentage < Settings.AutoOptimizationMemoryUsage && + DateTimeOffset.Now.Subtract(_lastAutoOptimizationByMemoryUsage).TotalMinutes >= Constants.App.AutoOptimizationMemoryUsageInterval) + { + DependencyInjection.Container.Resolve().Optimize(Enums.Memory.Optimization.Reason.LowMemory, Settings.MemoryAreas); + + _lastAutoOptimizationByMemoryUsage = DateTimeOffset.Now; + return; + } + } + finally + { + Interlocked.Exchange(ref _optimizing, 0); } } }