Skip to content

Commit f6f9822

Browse files
committed
Elevation modal, overlay refactor and perf tweaks
Refactor UI overlays and add an elevation warning modal; improve performance monitoring and refresh behavior. - UI: Reworked MainWindow XAML to consolidate content into UIContent, replaced AppContentLayer/backdrop animations with style-driven blur, added ElevationWarningOverlay (shell-style elevation prompt), reorganized PerformanceIntro and Loading overlays, and added popup overlay in PerformanceView. - MainWindow logic: Added ApplyUIContentBlur/ClearUIContentBlur helpers, elevation warning show/dismiss/request handlers (with persisted HasSeenElevationWarning), suspend/resume logic for system-tray and background refreshes when hidden, and concurrency guards for system-tray updates. - Services/API: IPerformanceMonitoringService now supports a lightweight GetSystemMetricsAsync(bool lightweight) call. PerformanceMonitoringService caches total physical memory, skips expensive metrics for lightweight calls, increases monitoring interval to 2s, and adds re-entrancy guards. - ViewModels: PerformanceViewModel gains popup state/commands and blur binding; PowerPlanViewModel can pause/resume auto-refresh, increased refresh interval to 10s and added concurrency protection. These changes reduce startup and background resource usage, prevent timer overlap, and introduce a clear user-facing elevation prompt while preserving existing behavior via settings.
1 parent 1ab85b2 commit f6f9822

8 files changed

Lines changed: 574 additions & 212 deletions

MainWindow.xaml

Lines changed: 181 additions & 104 deletions
Large diffs are not rendered by default.

MainWindow.xaml.cs

Lines changed: 200 additions & 72 deletions
Large diffs are not rendered by default.

Models/ApplicationSettingsModel.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,9 @@ public partial class ApplicationSettingsModel : ObservableObject, IModel
168168
[ObservableProperty]
169169
private bool hasSeenPerformanceIntro = false;
170170

171+
[ObservableProperty]
172+
private bool hasSeenElevationWarning = false;
173+
171174
[ObservableProperty]
172175
private int maxLogFileSizeMb = 10;
173176

@@ -239,6 +242,7 @@ public void CopyFrom(ApplicationSettingsModel other)
239242
this.EnableDebugLogging = other.EnableDebugLogging;
240243
this.EnablePerformanceCounters = other.EnablePerformanceCounters;
241244
this.HasSeenPerformanceIntro = other.HasSeenPerformanceIntro;
245+
this.HasSeenElevationWarning = other.HasSeenElevationWarning;
242246
this.MaxLogFileSizeMb = other.MaxLogFileSizeMb;
243247
this.LogRetentionDays = other.LogRetentionDays;
244248

Services/IPerformanceMonitoringService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public interface IPerformanceMonitoringService
2929
/// <summary>
3030
/// Get current system performance metrics.
3131
/// </summary>
32-
Task<SystemPerformanceMetrics> GetSystemMetricsAsync();
32+
Task<SystemPerformanceMetrics> GetSystemMetricsAsync(bool lightweight = false);
3333

3434
/// <summary>
3535
/// Get per-core CPU usage.

Services/PerformanceMonitoringService.cs

Lines changed: 57 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ public class PerformanceMonitoringService : IPerformanceMonitoringService, IDisp
3939
private readonly PerformanceCounter memoryCounter;
4040
private readonly List<PerformanceCounter> cpuCoreCounters;
4141
private System.Threading.Timer? monitoringTimer;
42+
private readonly object totalMemoryCacheLock = new();
43+
private readonly TimeSpan totalPhysicalMemoryCacheDuration = TimeSpan.FromMinutes(5);
44+
private long cachedTotalPhysicalMemory;
45+
private DateTime totalPhysicalMemoryCacheUtc = DateTime.MinValue;
46+
private int isMonitoringTickInProgress;
4247
private bool isMonitoring;
4348
private bool disposed;
4449

@@ -62,40 +67,44 @@ public PerformanceMonitoringService(
6267
this.InitializeCpuCoreCounters();
6368
}
6469

65-
public async Task<SystemPerformanceMetrics> GetSystemMetricsAsync()
70+
public async Task<SystemPerformanceMetrics> GetSystemMetricsAsync(bool lightweight = false)
6671
{
6772
try
6873
{
6974
var metrics = new SystemPerformanceMetrics
7075
{
7176
Timestamp = DateTime.UtcNow,
7277
TotalCpuUsage = await this.GetTotalCpuUsageAsync(),
73-
CpuCoreUsages = await this.GetCpuCoreUsageAsync(),
74-
TotalMemoryUsage = await this.GetTotalMemoryUsageAsync(),
7578
AvailableMemory = await this.GetAvailableMemoryAsync(),
76-
ActiveProcessCount = Process.GetProcesses().Length,
7779
};
7880

7981
// Calculate memory percentage
8082
metrics.TotalMemory = await this.GetTotalPhysicalMemoryAsync();
83+
metrics.TotalMemoryUsage = Math.Max(0, metrics.TotalMemory - metrics.AvailableMemory);
8184
metrics.MemoryUsagePercentage = metrics.TotalMemory > 0
8285
? ((double)(metrics.TotalMemory - metrics.AvailableMemory) / metrics.TotalMemory) * 100
8386
: 0;
8487

85-
// Get top processes
86-
var topCpuProcesses = await this.GetTopCpuProcessesAsync(1);
87-
metrics.TopCpuProcess = topCpuProcesses.FirstOrDefault();
88+
if (!lightweight)
89+
{
90+
metrics.CpuCoreUsages = await this.GetCpuCoreUsageAsync();
91+
metrics.ActiveProcessCount = Process.GetProcesses().Length;
8892

89-
var topMemoryProcesses = await this.GetTopMemoryProcessesAsync(1);
90-
metrics.TopMemoryProcess = topMemoryProcesses.FirstOrDefault();
93+
// Get top processes
94+
var topCpuProcesses = await this.GetTopCpuProcessesAsync(1);
95+
metrics.TopCpuProcess = topCpuProcesses.FirstOrDefault();
9196

92-
// Store in historical data
93-
this.historicalData.Add(metrics);
97+
var topMemoryProcesses = await this.GetTopMemoryProcessesAsync(1);
98+
metrics.TopMemoryProcess = topMemoryProcesses.FirstOrDefault();
9499

95-
// Keep only last 1000 entries (about 16 minutes at 1-second intervals)
96-
if (this.historicalData.Count > 1000)
97-
{
98-
this.historicalData.RemoveAt(0);
100+
// Store in historical data
101+
this.historicalData.Add(metrics);
102+
103+
// Keep only last 1000 entries (about 16 minutes at 1-second intervals)
104+
if (this.historicalData.Count > 1000)
105+
{
106+
this.historicalData.RemoveAt(0);
107+
}
99108
}
100109

101110
return metrics;
@@ -248,11 +257,17 @@ public async Task StartMonitoringAsync()
248257

249258
this.logger.LogInformation("Starting performance monitoring");
250259
this.isMonitoring = true;
260+
Interlocked.Exchange(ref this.isMonitoringTickInProgress, 0);
251261

252262
// PERFORMANCE OPTIMIZATION: Increased interval from 1s to 2s for better performance
253263
this.monitoringTimer = new System.Threading.Timer(
254264
async _ =>
255265
{
266+
if (Interlocked.Exchange(ref this.isMonitoringTickInProgress, 1) == 1)
267+
{
268+
return;
269+
}
270+
256271
try
257272
{
258273
var metrics = await this.GetSystemMetricsAsync();
@@ -262,6 +277,10 @@ public async Task StartMonitoringAsync()
262277
{
263278
this.logger.LogError(ex, "Error during performance monitoring update");
264279
}
280+
finally
281+
{
282+
Interlocked.Exchange(ref this.isMonitoringTickInProgress, 0);
283+
}
265284
}, null, TimeSpan.Zero, TimeSpan.FromSeconds(2));
266285
}
267286

@@ -274,6 +293,7 @@ public async Task StopMonitoringAsync()
274293

275294
this.logger.LogInformation("Stopping performance monitoring");
276295
this.isMonitoring = false;
296+
Interlocked.Exchange(ref this.isMonitoringTickInProgress, 0);
277297

278298
this.monitoringTimer?.Dispose();
279299
this.monitoringTimer = null;
@@ -323,21 +343,6 @@ private async Task<double> GetTotalCpuUsageAsync()
323343
}
324344
}
325345

326-
private async Task<long> GetTotalMemoryUsageAsync()
327-
{
328-
try
329-
{
330-
var totalMemory = await this.GetTotalPhysicalMemoryAsync();
331-
var availableMemory = await this.GetAvailableMemoryAsync();
332-
return totalMemory - availableMemory;
333-
}
334-
catch (Exception ex)
335-
{
336-
this.logger.LogError(ex, "Error getting total memory usage");
337-
return 0;
338-
}
339-
}
340-
341346
private async Task<long> GetAvailableMemoryAsync()
342347
{
343348
try
@@ -353,13 +358,33 @@ private async Task<long> GetAvailableMemoryAsync()
353358

354359
private async Task<long> GetTotalPhysicalMemoryAsync()
355360
{
361+
var now = DateTime.UtcNow;
362+
363+
lock (this.totalMemoryCacheLock)
364+
{
365+
if (this.cachedTotalPhysicalMemory > 0 &&
366+
(now - this.totalPhysicalMemoryCacheUtc) < this.totalPhysicalMemoryCacheDuration)
367+
{
368+
return this.cachedTotalPhysicalMemory;
369+
}
370+
}
371+
356372
try
357373
{
358374
using var searcher = new ManagementObjectSearcher("SELECT TotalPhysicalMemory FROM Win32_ComputerSystem");
359375
foreach (var obj in searcher.Get())
360376
{
361-
return Convert.ToInt64(obj["TotalPhysicalMemory"]);
377+
var totalMemory = Convert.ToInt64(obj["TotalPhysicalMemory"]);
378+
379+
lock (this.totalMemoryCacheLock)
380+
{
381+
this.cachedTotalPhysicalMemory = totalMemory;
382+
this.totalPhysicalMemoryCacheUtc = DateTime.UtcNow;
383+
}
384+
385+
return totalMemory;
362386
}
387+
363388
return 0;
364389
}
365390
catch (Exception ex)
@@ -416,6 +441,7 @@ public void Dispose()
416441
}
417442

418443
this.monitoringTimer?.Dispose();
444+
Interlocked.Exchange(ref this.isMonitoringTickInProgress, 0);
419445
this.totalCpuCounter?.Dispose();
420446
this.memoryCounter?.Dispose();
421447

ViewModels/PerformanceViewModel.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,18 @@ public partial class PerformanceViewModel : BaseViewModel
153153
[ObservableProperty]
154154
private bool isRuleCreateBusy;
155155

156+
[ObservableProperty]
157+
private bool isPopupVisible;
158+
159+
[ObservableProperty]
160+
private string popupTitle = string.Empty;
161+
162+
[ObservableProperty]
163+
private string popupContent = string.Empty;
164+
165+
[ObservableProperty]
166+
private int blurRadius;
167+
156168
private readonly Dictionary<string, DateTime> lastRuleApplyByExecutable = new(StringComparer.OrdinalIgnoreCase);
157169
private bool monitoringWasActiveBeforeSuspend;
158170
private readonly SemaphoreSlim topProcessRefreshGate = new(1, 1);
@@ -452,6 +464,20 @@ private void ToggleProcessDetails()
452464
this.ShowProcessDetails = !this.ShowProcessDetails;
453465
}
454466

467+
[RelayCommand]
468+
private void ShowPopup((string Title, string Content) parameters)
469+
{
470+
this.PopupTitle = parameters.Title;
471+
this.PopupContent = parameters.Content;
472+
this.IsPopupVisible = true;
473+
}
474+
475+
[RelayCommand]
476+
private void HidePopup()
477+
{
478+
this.IsPopupVisible = false;
479+
}
480+
455481
partial void OnSelectedHotspotProcessChanged(ProcessPerformanceInfo? value)
456482
{
457483
_ = RefreshSelectedProcessRuleImpactAsync();
@@ -478,6 +504,11 @@ partial void OnProcessSearchTextChanged(string value)
478504
_ = LoadTopProcessesAsync();
479505
}
480506

507+
partial void OnIsPopupVisibleChanged(bool value)
508+
{
509+
this.BlurRadius = value ? 15 : 0;
510+
}
511+
481512
private async Task RefreshSelectedProcessRuleImpactAsync()
482513
{
483514
try

ViewModels/PowerPlanViewModel.cs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ namespace ThreadPilot.ViewModels
1919
using System;
2020
using System.Collections.ObjectModel;
2121
using System.Linq;
22+
using System.Threading;
2223
using System.Threading.Tasks;
2324
using System.Windows.Input;
2425
using CommunityToolkit.Mvvm.ComponentModel;
@@ -33,6 +34,8 @@ public partial class PowerPlanViewModel : BaseViewModel
3334
{
3435
private readonly IPowerPlanService powerPlanService;
3536
private System.Timers.Timer? refreshTimer;
37+
private bool isAutoRefreshPaused;
38+
private int isRefreshInProgress;
3639

3740
[ObservableProperty]
3841
private ObservableCollection<PowerPlanModel> powerPlans = new();
@@ -64,22 +67,68 @@ private void SetupRefreshTimer()
6467
this.refreshTimer = new System.Timers.Timer(10000); // PERFORMANCE OPTIMIZATION: Increased to 10 second refresh - power plans change infrequently
6568
this.refreshTimer.Elapsed += async (s, e) =>
6669
{
70+
if (this.isAutoRefreshPaused)
71+
{
72+
return;
73+
}
74+
75+
if (Interlocked.Exchange(ref this.isRefreshInProgress, 1) == 1)
76+
{
77+
return;
78+
}
79+
6780
try
6881
{
6982
// Marshal timer callback to UI thread to prevent cross-thread access exceptions
7083
await System.Windows.Application.Current.Dispatcher.InvokeAsync(async () =>
7184
{
72-
await this.RefreshPowerPlansCommand.ExecuteAsync(null);
85+
if (!this.isAutoRefreshPaused)
86+
{
87+
await this.RefreshPowerPlansCommand.ExecuteAsync(null);
88+
}
7389
});
7490
}
7591
catch (Exception ex)
7692
{
7793
System.Diagnostics.Debug.WriteLine($"Power plan refresh timer error: {ex.Message}");
7894
}
95+
finally
96+
{
97+
Interlocked.Exchange(ref this.isRefreshInProgress, 0);
98+
}
7999
};
80100
this.refreshTimer.Start();
81101
}
82102

103+
public void PauseAutoRefresh()
104+
{
105+
this.isAutoRefreshPaused = true;
106+
this.refreshTimer?.Stop();
107+
}
108+
109+
public void ResumeAutoRefresh(bool refreshImmediately = true)
110+
{
111+
this.isAutoRefreshPaused = false;
112+
this.refreshTimer?.Start();
113+
114+
if (!refreshImmediately)
115+
{
116+
return;
117+
}
118+
119+
_ = System.Windows.Application.Current.Dispatcher.InvokeAsync(async () =>
120+
{
121+
try
122+
{
123+
await this.RefreshPowerPlansCommand.ExecuteAsync(null);
124+
}
125+
catch (Exception ex)
126+
{
127+
System.Diagnostics.Debug.WriteLine($"Power plan immediate refresh error: {ex.Message}");
128+
}
129+
});
130+
}
131+
83132
[RelayCommand]
84133
public async Task LoadPowerPlans()
85134
{

0 commit comments

Comments
 (0)