Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 50 additions & 25 deletions MainWindow.Behaviors.partial.cs
Original file line number Diff line number Diff line change
Expand Up @@ -402,33 +402,12 @@ private async Task LoadViewModelsAsync()
this.LogDebug($"ProcessViewModel fallback (LoadProcesses only) completed after exception, process count: {this.processViewModel.Processes?.Count ?? 0}, filtered count: {this.processViewModel.FilteredProcesses?.Count ?? 0}");
}

this.LogDebug("About to load PowerPlanViewModel...");
var powerPlanTask = this.powerPlanViewModel.LoadPowerPlans();
var powerPlanResult = await Task.WhenAny(powerPlanTask, Task.Delay(5000)); // 5 second timeout
if (powerPlanResult != powerPlanTask)
{
throw new TimeoutException("PowerPlanViewModel.LoadPowerPlans() timed out after 5 seconds");
}
await powerPlanTask; // Ensure we get any exceptions
this.LogDebug("PowerPlanViewModel loaded successfully");

this.LogDebug("Skipping optional diagnostics initialization until the diagnostics page is opened.");

this.LogDebug("About to load SystemTweaksViewModel...");
var systemTweaksTask = this.systemTweaksViewModel.LoadCommand.ExecuteAsync(null);
var systemTweaksResult = await Task.WhenAny(systemTweaksTask, Task.Delay(5000)); // 5 second timeout
if (systemTweaksResult != systemTweaksTask)
{
throw new TimeoutException("SystemTweaksViewModel.LoadCommand.ExecuteAsync() timed out after 5 seconds");
}
await systemTweaksTask; // Ensure we get any exceptions
this.LogDebug("SystemTweaksViewModel loaded successfully");
this.LogDebug("Skipping inactive view initialization until each page is opened.");

// Initialize keyboard shortcuts after window is loaded
this.Loaded += this.OnWindowLoaded;
this.LogDebug("Keyboard shortcuts event handler attached");

// The association view model loads its data automatically in its constructor
this.LogDebug("=== LoadViewModelsAsync completed successfully ===");
}
catch (Exception ex)
Expand Down Expand Up @@ -1385,8 +1364,13 @@ private AppActivityState GetForegroundActivityState()
return AppActivityState.ForegroundProcessView;
}

return this.PerformanceViewControl.Visibility == Visibility.Visible
? AppActivityState.ForegroundDiagnosticsView
if (this.PerformanceViewControl.Visibility == Visibility.Visible)
{
return AppActivityState.ForegroundDiagnosticsView;
}

return this.PowerPlanViewControl.Visibility == Visibility.Visible
? AppActivityState.ForegroundPowerPlanView
: AppActivityState.ForegroundOtherTab;
}

Expand Down Expand Up @@ -1419,7 +1403,7 @@ private void ApplyAppRefreshPolicy(AppActivityState state)

if (decision.PowerPlanUiRefreshEnabled)
{
this.powerPlanViewModel.ResumeAutoRefresh(refreshImmediately: state != AppActivityState.ForegroundOtherTab);
this.powerPlanViewModel.ResumeAutoRefresh();
}
else
{
Expand Down Expand Up @@ -1887,12 +1871,42 @@ private void ApplySectionVisibility(string tag)
{
"Process" => AppActivityState.ForegroundProcessView,
"Performance" => AppActivityState.ForegroundDiagnosticsView,
"Power" => AppActivityState.ForegroundPowerPlanView,
_ => AppActivityState.ForegroundOtherTab,
};

this.ApplyAppRefreshPolicy(activityState);
}

private async Task InitializeSectionAsync(string tag)
{
if (!this.initializedSections.Add(tag))
{
return;
}

try
{
switch (tag)
{
case "Rules":
await this.associationViewModel.InitializeAsync();
break;
case "Tweaks":
await this.systemTweaksViewModel.LoadAsync();
break;
case "Settings":
await this.settingsViewModel.InitializeAsync();
break;
}
}
catch
{
this.initializedSections.Remove(tag);
throw;
}
}

private void NavMenuItem_Click(object sender, RoutedEventArgs e)
{
TaskSafety.FireAndForget(this.NavMenuItem_ClickAsync(sender, e), ex =>
Expand Down Expand Up @@ -1942,8 +1956,19 @@ private async Task NavMenuItem_ClickAsync(object sender, RoutedEventArgs e)
this.GetPerformanceViewModel();
}

if (!string.Equals(tag, "Logs", StringComparison.Ordinal))
{
await this.logViewerViewModel.SetActiveAsync(false);
}

await this.InitializeSectionAsync(tag);
this.ApplySectionVisibility(tag);

if (string.Equals(tag, "Logs", StringComparison.Ordinal))
{
await this.logViewerViewModel.SetActiveAsync(true);
}

if (string.Equals(tag, "Performance", StringComparison.Ordinal))
{
this.TryShowPerformanceIntro();
Expand Down
1 change: 1 addition & 0 deletions MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
private readonly bool skipProcessMonitoringDuringStartup = false;
private bool isPerformingShutdown = false;
private readonly NavigationBehavior navigationBehavior = new();
private readonly HashSet<string> initializedSections = new(StringComparer.Ordinal);
private bool isPerformanceIntroVisible = false;
private double previousAppContentOpacity = 1;
private TaskCompletionSource<MessageBoxResult>? unsavedSettingsDialogCompletionSource;
Expand Down
12 changes: 10 additions & 2 deletions Services/AppRefreshPolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ public enum AppActivityState
{
ForegroundProcessView,
ForegroundDiagnosticsView,
ForegroundPowerPlanView,
ForegroundOtherTab,
Minimized,
TrayHidden,
Expand Down Expand Up @@ -33,21 +34,28 @@ public static AppRefreshDecision Evaluate(AppActivityState state)
ImmediateProcessRefresh: true,
VirtualizedPreloadEnabled: true,
PerformanceUiMonitoringEnabled: false,
PowerPlanUiRefreshEnabled: true,
PowerPlanUiRefreshEnabled: false,
BackgroundAutomationEnabled: true),
AppActivityState.ForegroundDiagnosticsView => new AppRefreshDecision(
ProcessUiRefreshEnabled: false,
ImmediateProcessRefresh: false,
VirtualizedPreloadEnabled: false,
PerformanceUiMonitoringEnabled: true,
PowerPlanUiRefreshEnabled: false,
BackgroundAutomationEnabled: true),
AppActivityState.ForegroundPowerPlanView => new AppRefreshDecision(
ProcessUiRefreshEnabled: false,
ImmediateProcessRefresh: false,
VirtualizedPreloadEnabled: false,
PerformanceUiMonitoringEnabled: false,
PowerPlanUiRefreshEnabled: true,
BackgroundAutomationEnabled: true),
AppActivityState.ForegroundOtherTab => new AppRefreshDecision(
ProcessUiRefreshEnabled: false,
ImmediateProcessRefresh: false,
VirtualizedPreloadEnabled: false,
PerformanceUiMonitoringEnabled: false,
PowerPlanUiRefreshEnabled: true,
PowerPlanUiRefreshEnabled: false,
BackgroundAutomationEnabled: true),
AppActivityState.Minimized or AppActivityState.TrayHidden => new AppRefreshDecision(
ProcessUiRefreshEnabled: false,
Expand Down
13 changes: 10 additions & 3 deletions Services/EnhancedLoggingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class EnhancedLoggingService : IEnhancedLoggingService, IDisposable
private readonly System.Threading.Timer flushTimer;
private readonly string logDirectory;
private string currentLogFilePath;
private int flushScheduled;
private bool isInitialized;
private bool disposed;

Expand All @@ -43,8 +44,7 @@ public EnhancedLoggingService(ILogger<EnhancedLoggingService> logger, IApplicati
this.logDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "ThreadPilot", "Logs");
this.currentLogFilePath = this.GetCurrentLogFilePath();

// Create flush timer (flush every 5 seconds)
this.flushTimer = new System.Threading.Timer(this.FlushLogs, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));
this.flushTimer = new System.Threading.Timer(this.FlushLogs, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
}

public async Task InitializeAsync()
Expand Down Expand Up @@ -228,15 +228,22 @@ private async Task LogStructuredEventAsync(string category, string message, LogL

this.logQueue.Enqueue(logEntry);

// Force immediate flush for errors and critical events
// Force immediate flush for errors and critical events.
if (level >= LogLevel.Error)
{
this.flushTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
Interlocked.Exchange(ref this.flushScheduled, 0);
await this.FlushLogsAsync();
}
else if (Interlocked.Exchange(ref this.flushScheduled, 1) == 0)
{
this.flushTimer.Change(TimeSpan.FromSeconds(5), Timeout.InfiniteTimeSpan);
}
}

private void FlushLogs(object? state)
{
Interlocked.Exchange(ref this.flushScheduled, 0);
TaskSafety.FireAndForget(this.FlushLogsAsync(), ex =>
{
this.logger.LogWarning(ex, "Periodic log flush failed");
Expand Down
6 changes: 2 additions & 4 deletions Services/PowerPlanService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class PowerPlanService : IPowerPlanService
private static readonly Lazy<string> powerPlansPath = new(GetPowerPlansPath);
private static readonly string powerCfgExecutablePath = Path.Combine(Environment.SystemDirectory, "powercfg.exe");
private static readonly TimeSpan powerCfgTimeout = TimeSpan.FromSeconds(20);
private static readonly Regex powerSchemeRegex = new(@"^Power Scheme GUID: (.*?) \((.*)\)(?:\s+\*)?$", RegexOptions.Multiline | RegexOptions.Compiled);
private static readonly Regex powerSchemeRegex = new(@"^Power Scheme GUID:\s*([0-9a-fA-F-]+)[ \t]+\((.*)\)[ \t]*(\*)?[ \t]*\r?$", RegexOptions.Multiline | RegexOptions.Compiled);
private static readonly Regex pathTraversalRegex = new(@"(^|[\\/])\.\.([\\/]|$)", RegexOptions.Compiled);

private static string PowerPlansPath => powerPlansPath.Value;
Expand Down Expand Up @@ -102,8 +102,6 @@ private static string GetPowerPlansPath()
public async Task<ObservableCollection<PowerPlanModel>> GetPowerPlansAsync()
{
var powerPlans = new ObservableCollection<PowerPlanModel>();
var activePlan = await this.GetActivePowerPlan().ConfigureAwait(false);

var result = await this.RunPowerCfgAsync("/list").ConfigureAwait(false);
var matches = powerSchemeRegex.Matches(result.StandardOutput);

Expand All @@ -116,7 +114,7 @@ public async Task<ObservableCollection<PowerPlanModel>> GetPowerPlansAsync()
{
Guid = guid,
Name = name,
IsActive = guid == activePlan?.Guid,
IsActive = match.Groups[3].Success,
IsCustomPlan = false,
};

Expand Down
38 changes: 32 additions & 6 deletions Services/ProcessMonitorService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ public class ProcessMonitorService : IProcessMonitorService
private int fallbackPollingIntervalMs = 5000; // Default 5 seconds
private int currentFallbackPollingIntervalMs = 5000;
private int idlePollingMultiplier = 1;
private readonly int wmiRetryDelayMs = 10000; // 10 seconds
private const int MaxIdlePollingMultiplier = 6;
private bool enableWmiMonitoring = true;
private bool enableFallbackPolling = true;
private int isFallbackPollingInProgress;
private int isWmiRecoveryInProgress;
private int wmiRecoveryFailureCount;
private DateTime lastWmiRetryAttemptUtc = DateTime.MinValue;

public event EventHandler<ProcessEventArgs>? ProcessStarted;
Expand Down Expand Up @@ -106,6 +106,8 @@ public async Task StartMonitoringAsync()

if (!wmiStarted && this.enableFallbackPolling)
{
this.lastWmiRetryAttemptUtc = DateTime.UtcNow;
this.wmiRecoveryFailureCount = 0;
// Fall back to polling if WMI is not available
this.StartFallbackPolling();
}
Expand Down Expand Up @@ -172,10 +174,14 @@ public async Task StopMonitoringAsync()

public async Task<IEnumerable<ProcessModel>> GetRunningProcessesAsync()
{
if (this.isMonitoring)
{
return this.runningProcesses.Values.ToArray();
}

try
{
var processes = await this.processService.GetProcessesAsync().ConfigureAwait(false);
return processes;
return await this.processService.GetProcessesAsync().ConfigureAwait(false);
}
catch (Exception)
{
Expand All @@ -200,7 +206,7 @@ private async Task InitializeProcessListAsync()
{
try
{
var processes = await this.GetRunningProcessesAsync().ConfigureAwait(false);
var processes = await this.processService.GetProcessesAsync().ConfigureAwait(false);
this.runningProcesses.Clear();

foreach (var process in processes)
Expand Down Expand Up @@ -256,6 +262,8 @@ await Task.Run(() =>
}).ConfigureAwait(false);

this.isWmiAvailable = true;
this.wmiRecoveryFailureCount = 0;
this.lastWmiRetryAttemptUtc = DateTime.MinValue;

// Prefer WMI when available to reduce polling overhead
if (this.isFallbackPollingActive)
Expand Down Expand Up @@ -314,6 +322,8 @@ private void OnWmiWatcherStopped(object sender, StoppedEventArgs e)
}

this.isWmiAvailable = false;
this.wmiRecoveryFailureCount = 0;
this.lastWmiRetryAttemptUtc = DateTime.UtcNow;
this.OnMonitoringStatusChanged($"WMI watcher stopped ({e.Status})");

if (this.enableFallbackPolling && !this.isFallbackPollingActive)
Expand Down Expand Up @@ -461,7 +471,7 @@ private async Task RunFallbackPollingAsync(CancellationToken cancellationToken)
return;
}

var currentProcesses = await this.GetRunningProcessesAsync().ConfigureAwait(false);
var currentProcesses = await this.processService.GetProcessesAsync().ConfigureAwait(false);
var detectedChanges = 0;

this.pollBuffer.Clear();
Expand Down Expand Up @@ -559,7 +569,7 @@ private async Task TryRecoverWmiMonitoringAsync()
}

var now = DateTime.UtcNow;
if ((now - this.lastWmiRetryAttemptUtc).TotalMilliseconds < this.wmiRetryDelayMs)
if (now - this.lastWmiRetryAttemptUtc < GetWmiRetryDelay(this.wmiRecoveryFailureCount))
{
return;
}
Expand All @@ -576,15 +586,29 @@ private async Task TryRecoverWmiMonitoringAsync()
var recovered = await this.TryStartWmiMonitoringAsync().ConfigureAwait(false);
if (recovered)
{
this.wmiRecoveryFailureCount = 0;
this.OnMonitoringStatusChanged("WMI monitoring recovered successfully");
}
else
{
this.wmiRecoveryFailureCount++;
}
}
finally
{
Interlocked.Exchange(ref this.isWmiRecoveryInProgress, 0);
}
}

internal static TimeSpan GetWmiRetryDelay(int failureCount) =>
failureCount switch
{
<= 0 => TimeSpan.FromSeconds(10),
1 => TimeSpan.FromSeconds(30),
2 => TimeSpan.FromMinutes(1),
_ => TimeSpan.FromMinutes(5),
};

private void OnMonitoringStatusChanged(string? message = null, Exception? error = null)
{
this.MonitoringStatusChanged?.Invoke(this, new MonitoringStatusEventArgs(
Expand All @@ -606,6 +630,8 @@ public void UpdateSettings()
var previousFallbackEnabled = this.enableFallbackPolling;

this.UpdateMonitoringSettings();
this.wmiRecoveryFailureCount = 0;
this.lastWmiRetryAttemptUtc = DateTime.MinValue;

if (!this.isMonitoring)
{
Expand Down
Loading
Loading