Skip to content
Closed
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
12 changes: 11 additions & 1 deletion src/NodeRadarPro/Data/DatabaseBackupService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using LiteDB;
Expand Down Expand Up @@ -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
Expand Down
42 changes: 42 additions & 0 deletions src/NodeRadarPro/Data/LocalDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,48 @@ public void InsertLog(LogEntry entry)
}
}

public void InsertLogs(IEnumerable<LogEntry> entries)
{
var list = entries as IList<LogEntry> ?? entries.ToList();
if (list.Count == 0) return;
lock (SyncRoot)
{
var col = _db.GetCollection<LogEntry>(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<LogEntry>(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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
32 changes: 32 additions & 0 deletions tests/NodeRadarPro.Tests/Data/LocalDatabaseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<LogEntry>
{
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);
}
}