diff --git a/src/NodeRadarPro/Data/DatabaseBackupService.cs b/src/NodeRadarPro/Data/DatabaseBackupService.cs index ad3d7d8..77d257d 100644 --- a/src/NodeRadarPro/Data/DatabaseBackupService.cs +++ b/src/NodeRadarPro/Data/DatabaseBackupService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using LiteDB; @@ -153,10 +154,19 @@ private void CleanupOldBackups(string backupDir) .Skip(7) // Keep last 7 backups .ToList(); + if (files.Count == 0) return; + + var logItems = new List<(LogLevel level, string source, string message)>(files.Count); + foreach (var file in files) { file.Delete(); - _database.Log(LogLevel.Info, LogSource, $"Cleaned up old backup: {file.Name}"); + logItems.Add((LogLevel.Info, LogSource, $"Cleaned up old backup: {file.Name}")); + } + + if (logItems.Count > 0) + { + _database.LogBulk(logItems); } } catch diff --git a/src/NodeRadarPro/Data/LocalDatabase.cs b/src/NodeRadarPro/Data/LocalDatabase.cs index dd4107d..d418635 100644 --- a/src/NodeRadarPro/Data/LocalDatabase.cs +++ b/src/NodeRadarPro/Data/LocalDatabase.cs @@ -652,6 +652,48 @@ public void InsertLog(LogEntry entry) } } + public void InsertLogs(IEnumerable entries) + { + var list = entries as IList ?? entries.ToList(); + if (list.Count == 0) return; + lock (SyncRoot) + { + var col = _db.GetCollection(LogsCollection); + col.InsertBulk(list); + } + } + + public void LogBulk(IEnumerable<(LogLevel level, string source, string message, string? deviceMac)> logItems) + { + var itemsList = logItems as IList<(LogLevel level, string source, string message, string? deviceMac)> ?? logItems.ToList(); + if (itemsList.Count == 0) return; + + var now = DateTime.UtcNow; + var entries = new List(itemsList.Count); + + foreach (var item in itemsList) + { + entries.Add(new LogEntry + { + Level = item.level, + Source = item.source, + Message = item.message, + DeviceMac = item.deviceMac, + Timestamp = now + }); + + // Physical File log (for Technical Support) + Logger.Log(item.level, item.source, item.message, item.deviceMac); + } + + InsertLogs(entries); + } + + public void LogBulk(IEnumerable<(LogLevel level, string source, string message)> logItems) + { + LogBulk(logItems.Select(x => (x.level, x.source, x.message, (string?)null))); + } + public void Log(LogLevel level, string source, string message, string? deviceMac = null) { // 1. Database log (for UI viewer) diff --git a/tests/NodeRadarPro.Tests/Data/DatabaseBackupServiceBenchmarkTests.cs b/tests/NodeRadarPro.Tests/Data/DatabaseBackupServiceBenchmarkTests.cs new file mode 100644 index 0000000..f40dfc8 --- /dev/null +++ b/tests/NodeRadarPro.Tests/Data/DatabaseBackupServiceBenchmarkTests.cs @@ -0,0 +1,78 @@ +using System.Diagnostics; +using NodeRadarPro.Core; +using NodeRadarPro.Data; + +namespace NodeRadarPro.Tests.Data; + +public class DatabaseBackupServiceBenchmarkTests : IDisposable +{ + private readonly string _tempDir; + private readonly string _dbPath; + private readonly LocalDatabase _db; + + public DatabaseBackupServiceBenchmarkTests() + { + _tempDir = Path.Combine(Path.GetTempPath(), "DatabaseBackupServiceBenchmarkTests_" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(_tempDir); + + _dbPath = Path.Combine(_tempDir, "bench_noderadar.db"); + _db = new LocalDatabase(_dbPath, "testpass"); + } + + public void Dispose() + { + try + { + _db.DisposeConnection(); + } + catch { } + + try + { + if (Directory.Exists(_tempDir)) + { + Directory.Delete(_tempDir, true); + } + } + catch { } + GC.SuppressFinalize(this); + } + + [Fact] + public void Benchmark_CleanupOldBackups_NPlusOneVsBatched() + { + int fileCount = 50; + + // Warmup + _db.Log(LogLevel.Info, "Database", "Warmup"); + + // --- Baseline Simulation: Individual Log() Calls (N+1 inserts) --- + var swBaseline = Stopwatch.StartNew(); + for (int i = 0; i < fileCount; i++) + { + _db.Log(LogLevel.Info, "Database", $"Cleaned up old backup: noderadar_backup_baseline_{i}.db"); + } + swBaseline.Stop(); + long baselineMs = swBaseline.ElapsedMilliseconds; + + // --- Batched LogBulk Calls (1 InsertBulk) --- + var logItems = new List<(LogLevel level, string source, string message)>(fileCount); + for (int i = 0; i < fileCount; i++) + { + logItems.Add((LogLevel.Info, "Database", $"Cleaned up old backup: noderadar_backup_batched_{i}.db")); + } + + var swBatched = Stopwatch.StartNew(); + _db.LogBulk(logItems); + swBatched.Stop(); + long batchedMs = swBatched.ElapsedMilliseconds; + + // Verify all logs were recorded in database + var logs = _db.GetLogs(limit: 1000); + Assert.True(logs.Count >= fileCount * 2); + + // Verify speedup / output benchmark metrics + double speedup = batchedMs > 0 ? (double)baselineMs / batchedMs : baselineMs; + Assert.True(swBatched.ElapsedTicks <= swBaseline.ElapsedTicks || batchedMs <= baselineMs + 50); + } +} diff --git a/tests/NodeRadarPro.Tests/Data/LocalDatabaseTests.cs b/tests/NodeRadarPro.Tests/Data/LocalDatabaseTests.cs index 3c221b0..16bda51 100644 --- a/tests/NodeRadarPro.Tests/Data/LocalDatabaseTests.cs +++ b/tests/NodeRadarPro.Tests/Data/LocalDatabaseTests.cs @@ -753,4 +753,36 @@ public void PruneOldData_DefaultArguments_UsesSettingsValues() Assert.DoesNotContain(remaining, l => l.Message == "Expired Log"); Assert.Contains(remaining, l => l.Message == "Valid Log"); } + + [Fact] + public void InsertLogs_BulkInsertsMultipleEntries() + { + var entries = new List + { + new LogEntry { Message = "Bulk 1", Level = LogLevel.Info, Source = "Test" }, + new LogEntry { Message = "Bulk 2", Level = LogLevel.Warning, Source = "Test" } + }; + + _db.InsertLogs(entries); + + var logs = _db.GetLogs(); + Assert.Contains(logs, l => l.Message == "Bulk 1"); + Assert.Contains(logs, l => l.Message == "Bulk 2"); + } + + [Fact] + public void LogBulk_InsertsMultipleLogItemsInBatch() + { + var items = new List<(LogLevel level, string source, string message)> + { + (LogLevel.Info, "BulkSource", "Item 1"), + (LogLevel.Error, "BulkSource", "Item 2") + }; + + _db.LogBulk(items); + + var logs = _db.GetLogs(); + Assert.Contains(logs, l => l.Message == "Item 1" && l.Source == "BulkSource"); + Assert.Contains(logs, l => l.Message == "Item 2" && l.Level == LogLevel.Error); + } }