Skip to content

Develop#7

Merged
TheMysteriousStranger90 merged 12 commits into
masterfrom
develop
Feb 5, 2026
Merged

Develop#7
TheMysteriousStranger90 merged 12 commits into
masterfrom
develop

Conversation

@TheMysteriousStranger90

Copy link
Copy Markdown
Owner

This pull request introduces several improvements across the codebase, focusing on database query efficiency, code quality configuration, and infrastructure consistency. The most significant changes include optimizing log statistics queries, adding an asynchronous command helper, and standardizing line endings and editor settings for better cross-platform support.

Database and Repository Improvements:

  • Refactored LogRepository methods to use more efficient, database-side grouping and counting for statistics and time series, reducing memory usage and improving performance. This includes changes to GetDetailedStatisticsAsync, GetLogsTimeSeriesAsync, and related methods to avoid loading all logs into memory and to use AsNoTracking for read-only queries. [1] [2] [3] [4] [5] [6]
  • Enhanced database indexing in LogAnalyzerDbContext by adding composite and named indexes to improve query performance for common filtering scenarios.

Infrastructure and Configuration:

  • Added a .gitattributes file to enforce consistent line endings for various file types and improve cross-platform development experience.
  • Updated .editorconfig to set code quality rules (such as unused parameters and code analysis warnings) to "suggestion" instead of "warning", and added file-specific settings for migrations and YAML files. [1] [2] [3]

Dependency Injection and Service Registration:

  • Adjusted service lifetimes and registrations in ConfigureServices to use AddSingleton or AddTransient as appropriate, and added new services for statistics and export functionality.

Command Infrastructure:

  • Introduced AsyncRelayCommand and AsyncRelayCommand<T> classes to simplify asynchronous command execution in the UI layer, improving code readability and user experience.

Other Notable Changes:

  • Added a new DeleteSessionAsync method to the ILogRepository interface for deleting logs by session.
  • Updated the database context to explicitly set query tracking behavior to NoTracking for improved performance in read scenarios.
  • Minor code cleanup and formatting improvements.

These changes collectively enhance the application's scalability, maintainability, and developer experience.

@TheMysteriousStranger90 TheMysteriousStranger90 added the enhancement New feature or request label Feb 5, 2026
Copilot AI review requested due to automatic review settings February 5, 2026 09:04
@TheMysteriousStranger90
TheMysteriousStranger90 merged commit 29e7bfc into master Feb 5, 2026
9 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request refactors and enhances the LogAnalyzerForWindows application with a focus on performance optimization, code quality, and maintainability. The changes introduce significant improvements to database query efficiency, a new caching layer for statistics, consolidated command handling, and standardized development environment configuration.

Changes:

  • Optimized database queries in LogRepository to use server-side grouping and aggregation instead of loading all data into memory, significantly reducing memory usage and improving performance
  • Introduced new service layer with LogStatisticsService (for caching statistics queries) and LogExportService (for centralized log export functionality)
  • Refactored ViewModels to use a shared ViewModelBase class and new AsyncRelayCommand for cleaner async/await patterns
  • Migrated auto-start mechanism from Windows Registry to Task Scheduler for better reliability and cross-platform compatibility
  • Enhanced database indexing with composite indexes for common query patterns
  • Added .gitattributes and updated .editorconfig for consistent line endings and code quality settings across the development team

Reviewed changes

Copilot reviewed 29 out of 29 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
LogAnalyzerForWindows/ViewModels/ViewModelBase.cs New base class providing shared INotifyPropertyChanged implementation for all ViewModels
LogAnalyzerForWindows/ViewModels/{SettingsViewModel, PaginationViewModel, MainWindowViewModel, DashboardViewModel}.cs Refactored to inherit from ViewModelBase and use AsyncRelayCommand for async operations
LogAnalyzerForWindows/Commands/AsyncRelayCommand.cs New command implementation for handling async operations in MVVM pattern
LogAnalyzerForWindows/Services/{LogStatisticsService, LogExportService}.cs New services for caching statistics and centralizing export logic
LogAnalyzerForWindows/Services/SettingsService.cs Replaced Registry-based auto-start with Task Scheduler implementation
LogAnalyzerForWindows/Database/Repositories/LogRepository.cs Optimized queries to use database-side aggregation instead of in-memory processing
LogAnalyzerForWindows/Database/LogAnalyzerDbContext.cs Added composite indexes for improved query performance
LogAnalyzerForWindows/Models/LogMonitor.cs Refactored to use Channel-based producer-consumer pattern for better concurrency
LogAnalyzerForWindows/Models/Writer/* Removed unused LogManager and ILogWriter abstractions
LogAnalyzerForWindows/Views/MainWindow.axaml Added delete session button and formatting improvements
LogAnalyzerForWindows/App.axaml.cs Updated DI configuration with proper service lifetimes and new services
.gitattributes New file enforcing consistent line endings across platforms
.editorconfig Updated with migration-specific rules and code quality settings
Comments suppressed due to low confidence (1)

LogAnalyzerForWindows/Migrations/20260203190033_Initial.Designer.cs:15

  • The migration class is named Initial but the Migration attribute references 20260203190033_Initial. The class name should typically match the migration name. While this will work, it could cause confusion when referencing migrations by name.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1219 to 1252
if (_disposedValue) return;

try
{
if (_monitor.IsMonitoring)
{
StopMonitoring();
}

_processingCts?.Cancel();
_processingCts?.Dispose();

_monitor.MonitoringStarted -= OnMonitoringStateChanged;
_monitor.MonitoringStopped -= OnMonitoringStateChanged;

_folderWatcher.Created -= OnLogDirectoryChanged;
_folderWatcher.Deleted -= OnLogDirectoryChanged;
_folderWatcher.Renamed -= OnLogDirectoryChanged;
_folderWatcher.Changed -= OnLogDirectoryChanged;
_folderWatcher.EnableRaisingEvents = false;
_folderWatcher.Dispose();

if (_monitor is IDisposable disposableMonitor)
{
disposableMonitor.Dispose();
}
}
catch (ObjectDisposedException ex)
{
Debug.WriteLine($"Object already disposed during cleanup: {ex.Message}");
}

_disposedValue = true;
GC.SuppressFinalize(this);

Copilot AI Feb 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The MainWindowViewModel implements both IAsyncDisposable and IDisposable, but the Dispose() method doesn't call DisposeAsync() or dispose of the DashboardViewModel. This creates an inconsistent disposal pattern where calling Dispose() synchronously leaves the DashboardViewModel undisposed, potentially causing resource leaks.

According to the IAsyncDisposable pattern guidelines, when a class implements both interfaces, the synchronous Dispose() method should typically call DisposeAsync().AsTask().GetAwaiter().GetResult() or implement a shared disposal logic that both methods can call. Alternatively, you should document that consumers must call DisposeAsync() instead of Dispose().

Suggested change
if (_disposedValue) return;
try
{
if (_monitor.IsMonitoring)
{
StopMonitoring();
}
_processingCts?.Cancel();
_processingCts?.Dispose();
_monitor.MonitoringStarted -= OnMonitoringStateChanged;
_monitor.MonitoringStopped -= OnMonitoringStateChanged;
_folderWatcher.Created -= OnLogDirectoryChanged;
_folderWatcher.Deleted -= OnLogDirectoryChanged;
_folderWatcher.Renamed -= OnLogDirectoryChanged;
_folderWatcher.Changed -= OnLogDirectoryChanged;
_folderWatcher.EnableRaisingEvents = false;
_folderWatcher.Dispose();
if (_monitor is IDisposable disposableMonitor)
{
disposableMonitor.Dispose();
}
}
catch (ObjectDisposedException ex)
{
Debug.WriteLine($"Object already disposed during cleanup: {ex.Message}");
}
_disposedValue = true;
GC.SuppressFinalize(this);
DisposeAsync().AsTask().GetAwaiter().GetResult();

Copilot uses AI. Check for mistakes.
}

return logs.ToList();
return logs.OrderBy(l => l.Timestamp).ToList();

Copilot AI Feb 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorting logs by timestamp at the end of reading (line 436) is good for consistency, but it adds O(n log n) complexity to every read operation. For large log sets, this could be a performance bottleneck. Consider whether sorting is always necessary, or if it could be done on-demand when the user needs sorted results.

Suggested change
return logs.OrderBy(l => l.Timestamp).ToList();
// Materialize the concurrent collection into a list
var result = logs.ToList();
// If there are 0 or 1 elements, no sorting is needed
if (result.Count <= 1)
{
return result;
}
// Check if the list is already sorted by Timestamp
bool alreadySorted = true;
for (int i = 1; i < result.Count; i++)
{
DateTime? prev = result[i - 1].Timestamp;
DateTime? current = result[i].Timestamp;
// Use Nullable.Compare to handle nulls consistently
if (Nullable.Compare(prev, current) > 0)
{
alreadySorted = false;
break;
}
}
if (!alreadySorted)
{
result.Sort((a, b) => Nullable.Compare(a.Timestamp, b.Timestamp));
}
return result;

Copilot uses AI. Check for mistakes.
Comment on lines +77 to +88
var dateFolder = DateTime.Now.ToString("yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture);
var targetFolder = Path.Combine(basePath, dateFolder);

if (!Directory.Exists(targetFolder))
{
Directory.CreateDirectory(targetFolder);
}

string fileName;
if (string.IsNullOrEmpty(customFileName))
{
var timestamp = DateTime.Now.ToString("HHmmss", System.Globalization.CultureInfo.InvariantCulture);

Copilot AI Feb 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using DateTime.Now instead of DateTime.UtcNow for file paths could cause issues in different time zones or during daylight saving time transitions. Two exports made at the "same" local time in different time zones would end up in different folders, and DST transitions could cause confusion.

Consider using DateTime.UtcNow for consistency with the session ID naming pattern used elsewhere (e.g., in MainWindowViewModel line 580).

Copilot uses AI. Check for mistakes.
@@ -96,22 +96,27 @@ private static void ConfigureServices(IServiceCollection services)
services.AddDbContextFactory<LogAnalyzerDbContext>(options =>
{
options.UseSqlite(DbContextConfig.ConnectionString);

Copilot AI Feb 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting the default query tracking behavior to NoTracking at the DbContext level (line 99) is good for read-heavy operations, but it means write operations will need to explicitly attach entities or use tracking. Ensure that all write operations (e.g., SaveLogsAsync) are tested to verify they work correctly with this global setting.

This is generally a good optimization for this read-heavy application, but it's worth documenting this design decision.

Suggested change
options.UseSqlite(DbContextConfig.ConnectionString);
options.UseSqlite(DbContextConfig.ConnectionString);
// NOTE:
// This application is predominantly read-heavy, so we intentionally set the
// global query tracking behavior to NoTracking for performance reasons.
// Any write operations that rely on change tracking (for example, methods
// like SaveLogsAsync) must explicitly enable tracking (e.g., via AsTracking())
// or attach entities to the context before saving. Ensure such write paths
// are covered by tests to verify they behave correctly with this setting.

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +38
public async void Execute(object? parameter)
{
if (!CanExecute(parameter)) return;

_isExecuting = true;
OnCanExecuteChanged();

try
{
await _execute().ConfigureAwait(false);
}
finally
{
_isExecuting = false;
await Dispatcher.UIThread.InvokeAsync(OnCanExecuteChanged);
}
}

Copilot AI Feb 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Execute method is marked as async void, which is generally appropriate for event handlers and ICommand implementations. However, any exceptions thrown during the execution of _execute() will be swallowed and not observable by the caller. Consider adding try-catch blocks around the await _execute() call to log exceptions or show error messages to the user.

The same applies to the generic AsyncRelayCommand<T> class (line 64).

Copilot uses AI. Check for mistakes.
Comment on lines 289 to +332
var interval = groupBy ?? TimeSpan.FromHours(1);

var grouped = logs
.GroupBy(t => new DateTime(
t.Year, t.Month, t.Day,
interval.TotalHours >= 24 ? 0 : t.Hour,
interval.TotalMinutes >= 60 ? 0 : (t.Minute / (int)interval.TotalMinutes) * (int)interval.TotalMinutes,
0))
.Select(g => new TimeSeriesPoint
List<TimeSeriesPoint> result;

if (interval.TotalHours >= 24)
{
var grouped = await query
.Where(l => l.Timestamp.HasValue)
.GroupBy(l => new { l.Timestamp!.Value.Year, l.Timestamp.Value.Month, l.Timestamp.Value.Day })
.Select(g => new { Date = g.Key, Count = g.Count() })
.OrderBy(x => x.Date.Year).ThenBy(x => x.Date.Month).ThenBy(x => x.Date.Day)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);

result = grouped.Select(g => new TimeSeriesPoint
{
Time = g.Key,
Count = g.Count(),
Label = interval.TotalHours >= 24
? g.Key.ToString("dd MMM", CultureInfo.InvariantCulture)
: g.Key.ToString("HH:mm", CultureInfo.InvariantCulture)
})
.OrderBy(p => p.Time)
.ToList();
Time = new DateTime(g.Date.Year, g.Date.Month, g.Date.Day),
Count = g.Count,
Label = new DateTime(g.Date.Year, g.Date.Month, g.Date.Day).ToString("dd MMM",
CultureInfo.InvariantCulture)
}).ToList();
}
else
{
var grouped = await query
.Where(l => l.Timestamp.HasValue)
.GroupBy(l => new
{
l.Timestamp!.Value.Year, l.Timestamp.Value.Month, l.Timestamp.Value.Day, l.Timestamp.Value.Hour
})
.Select(g => new { Date = g.Key, Count = g.Count() })
.OrderBy(x => x.Date.Year).ThenBy(x => x.Date.Month).ThenBy(x => x.Date.Day)
.ThenBy(x => x.Date.Hour)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);

result = grouped.Select(g => new TimeSeriesPoint
{
Time = new DateTime(g.Date.Year, g.Date.Month, g.Date.Day, g.Date.Hour, 0, 0),
Count = g.Count,
Label = new DateTime(g.Date.Year, g.Date.Month, g.Date.Day, g.Date.Hour, 0, 0).ToString("HH:mm",
CultureInfo.InvariantCulture)
}).ToList();
}

Copilot AI Feb 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The time series grouping logic only supports day-level (>=24 hours) and hour-level (<24 hours) grouping. The groupBy parameter accepts any TimeSpan value, but values like 30 minutes, 2 hours, or 3 days are all treated the same as either 1 hour or 1 day. This could be misleading to callers.

Consider either:

  1. Documenting this limitation clearly in the method documentation
  2. Validating the groupBy parameter and throwing an exception for unsupported values
  3. Implementing more granular grouping options

Copilot uses AI. Check for mistakes.
Comment on lines +206 to +207
catch
{

Copilot AI Feb 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The empty catch block silently ignores failures when deleting a scheduled task. This can hide legitimate errors (e.g., insufficient permissions, corrupted task scheduler database). Consider logging the exception or only catching specific expected error codes.

Recommendation: At minimum, log the exception to Debug output for diagnostic purposes.

Suggested change
catch
{
catch (Exception ex)
{
Debug.WriteLine($"Error deleting scheduled task '{TaskName}': {ex}");

Copilot uses AI. Check for mistakes.
Comment on lines 783 to 795
await Dispatcher.UIThread.InvokeAsync(async () =>
{
if (UseDatabaseMode && PaginationViewModel != null)
{
await PaginationViewModel.LoadLogsAsync().ConfigureAwait(false);
}

if (DashboardViewModel != null)
{
await DashboardViewModel.LoadSessionsAsync().ConfigureAwait(false);
await DashboardViewModel.LoadDashboardDataAsync().ConfigureAwait(false);
}
}).ConfigureAwait(false);

Copilot AI Feb 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling async methods (LoadSessionsAsync and LoadDashboardDataAsync) from within a UI thread invocation could lead to deadlocks or unexpected behavior. The Dispatcher.UIThread.InvokeAsync executes on the UI thread, and calling .ConfigureAwait(false) inside it doesn't help since you're already on the UI thread.

Consider restructuring this to await the async operations outside the InvokeAsync call, or use Dispatcher.UIThread.Post if you don't need to wait for completion.

Copilot uses AI. Check for mistakes.
Comment on lines 978 to 1004
@@ -1001,6 +992,14 @@
{
TextBlock = "Monitoring stopped.";
IsLoading = false;

_statisticsService.InvalidateCache();

if (DashboardViewModel != null)
{
await DashboardViewModel.LoadSessionsAsync().ConfigureAwait(false);
await DashboardViewModel.LoadDashboardDataAsync().ConfigureAwait(false);
}
}
});

Copilot AI Feb 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as in ClearOldHistoryAsync: calling async methods inside Dispatcher.UIThread.InvokeAsync can cause problems. The nested async lambda (line 978) is executed on the UI thread, and awaiting async operations within it could potentially block the UI thread or cause unexpected behavior.

Consider restructuring to execute the async operations outside the InvokeAsync call.

Copilot uses AI. Check for mistakes.
DataLabelsPaint = new SolidColorPaint(SKColors.White),
DataLabelsFormatter = point => $"{d.Name}: {point.Coordinate.PrimaryValue}"
DataLabelsSize = 12,
DataLabelsFormatter = point => $"{d.Name} {d.Value}"

Copilot AI Feb 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The DataLabelsFormatter lambda captures the loop variable d, which creates a closure over each iteration's value. This means all pie slices will show the label from their respective data point, not the last one. However, the formatter references d.Name and d.Value directly instead of using the point parameter that's provided to the formatter. This works, but it's not using the LiveCharts API as intended.

Consider using point.Model to access the actual value or rely on the point's coordinate values for more robust charting behavior.

Suggested change
DataLabelsFormatter = point => $"{d.Name} {d.Value}"
DataLabelsFormatter = point => $"{point.Context.Series.Name} {point.PrimaryValue}"

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants