diff --git a/Dashboard/MainWindow.xaml.cs b/Dashboard/MainWindow.xaml.cs
index 00ffaf9..44a66a6 100644
--- a/Dashboard/MainWindow.xaml.cs
+++ b/Dashboard/MainWindow.xaml.cs
@@ -502,7 +502,26 @@ private async Task OpenServerTabAsync(ServerConnection server)
var utcOffset = connStatus.UtcOffsetMinutes ?? (int)TimeZoneInfo.Local.GetUtcOffset(DateTime.UtcNow).TotalMinutes;
Helpers.ServerTimeHelper.UtcOffsetMinutes = utcOffset;
- var serverTab = new ServerTab(server, utcOffset);
+ ServerTab serverTab;
+ try
+ {
+ serverTab = new ServerTab(server, utcOffset);
+ }
+ catch (Exception ex)
+ {
+ var inner = ex.InnerException?.Message ?? ex.Message;
+ System.Windows.MessageBox.Show(
+ $"Failed to open server tab for '{server.DisplayName}'.\n\n" +
+ $"This is usually caused by a missing Visual C++ Redistributable (x64) " +
+ $"or an OS compatibility issue with the SkiaSharp rendering library.\n\n" +
+ $"Download the latest VC++ Redistributable from:\n" +
+ $"https://aka.ms/vs/17/release/vc_redist.x64.exe\n\n" +
+ $"Error: {inner}",
+ "Chart Initialization Error",
+ System.Windows.MessageBoxButton.OK,
+ System.Windows.MessageBoxImage.Error);
+ return;
+ }
serverTab.AlertAcknowledged += (_, _) =>
{
_emailAlertService.HideAllAlerts(8760, server.DisplayName);
diff --git a/Lite/Services/DeltaCalculator.cs b/Lite/Services/DeltaCalculator.cs
index 377130d..383f054 100644
--- a/Lite/Services/DeltaCalculator.cs
+++ b/Lite/Services/DeltaCalculator.cs
@@ -24,9 +24,9 @@ namespace PerformanceMonitorLite.Services;
public class DeltaCalculator
{
///
- /// Cache structure: serverId -> collectorName -> key -> previousValue
+ /// Cache structure: serverId -> collectorName -> key -> (previousValue, timestamp)
///
- private readonly ConcurrentDictionary>> _cache = new();
+ private readonly ConcurrentDictionary>> _cache = new();
private readonly ILogger? _logger;
@@ -63,12 +63,15 @@ public async Task SeedFromDatabaseAsync(DuckDbInitializer duckDb)
/// Calculates the delta between the current value and the previous cached value.
/// First-ever sighting (no baseline): returns currentValue so single-execution queries appear.
/// Counter reset (value decreased): returns 0 to avoid inflated deltas from plan cache churn.
+ /// Gap detection: if collectionTime and maxGapSeconds are provided and the gap since the
+ /// last cached value exceeds maxGapSeconds, returns 0 to avoid inflated deltas after restarts.
/// Thread-safe via atomic AddOrUpdate.
///
- public long CalculateDelta(int serverId, string collectorName, string key, long currentValue, bool baselineOnly = false)
+ public long CalculateDelta(int serverId, string collectorName, string key, long currentValue,
+ bool baselineOnly = false, DateTime? collectionTime = null, int maxGapSeconds = 0)
{
- var serverCache = _cache.GetOrAdd(serverId, _ => new ConcurrentDictionary>());
- var collectorCache = serverCache.GetOrAdd(collectorName, _ => new ConcurrentDictionary());
+ var serverCache = _cache.GetOrAdd(serverId, _ => new ConcurrentDictionary>());
+ var collectorCache = serverCache.GetOrAdd(collectorName, _ => new ConcurrentDictionary());
long delta = 0;
@@ -80,15 +83,24 @@ public long CalculateDelta(int serverId, string collectorName, string key, long
_ =>
{
delta = baselineOnly ? 0 : currentValue;
- return currentValue;
+ return (currentValue, collectionTime);
},
/* Update: compute delta atomically */
- (_, previousValue) =>
+ (_, previous) =>
{
- delta = currentValue < previousValue
+ /* Gap detection: if too much time has passed since the last cached value,
+ treat this as a new baseline to avoid inflated deltas after app restarts */
+ if (maxGapSeconds > 0 && collectionTime.HasValue && previous.Timestamp.HasValue
+ && (collectionTime.Value - previous.Timestamp.Value).TotalSeconds > maxGapSeconds)
+ {
+ delta = 0;
+ return (currentValue, collectionTime);
+ }
+
+ delta = currentValue < previous.Value
? 0 /* counter reset (plan cache eviction/re-entry) — not real new work */
- : currentValue - previousValue;
- return currentValue;
+ : currentValue - previous.Value;
+ return (currentValue, collectionTime);
});
return delta;
@@ -97,18 +109,18 @@ public long CalculateDelta(int serverId, string collectorName, string key, long
///
/// Seeds a single value into the cache without computing a delta.
///
- private void Seed(int serverId, string collectorName, string key, long value)
+ private void Seed(int serverId, string collectorName, string key, long value, DateTime? timestamp = null)
{
- var serverCache = _cache.GetOrAdd(serverId, _ => new ConcurrentDictionary>());
- var collectorCache = serverCache.GetOrAdd(collectorName, _ => new ConcurrentDictionary());
- collectorCache[key] = value;
+ var serverCache = _cache.GetOrAdd(serverId, _ => new ConcurrentDictionary>());
+ var collectorCache = serverCache.GetOrAdd(collectorName, _ => new ConcurrentDictionary());
+ collectorCache[key] = (value, timestamp);
}
private async Task SeedWaitStatsAsync(DuckDBConnection connection)
{
using var cmd = connection.CreateCommand();
cmd.CommandText = @"
-SELECT server_id, wait_type, waiting_tasks_count, wait_time_ms, signal_wait_time_ms
+SELECT server_id, wait_type, waiting_tasks_count, wait_time_ms, signal_wait_time_ms, collection_time
FROM wait_stats
WHERE (server_id, collection_time) IN (
SELECT server_id, MAX(collection_time) FROM wait_stats GROUP BY server_id
@@ -119,9 +131,10 @@ FROM wait_stats
{
var serverId = reader.GetInt32(0);
var waitType = reader.GetString(1);
- Seed(serverId, "wait_stats_tasks", waitType, reader.GetInt64(2));
- Seed(serverId, "wait_stats_time", waitType, reader.GetInt64(3));
- Seed(serverId, "wait_stats_signal", waitType, reader.GetInt64(4));
+ var ts = reader.IsDBNull(5) ? (DateTime?)null : reader.GetDateTime(5);
+ Seed(serverId, "wait_stats_tasks", waitType, reader.GetInt64(2), ts);
+ Seed(serverId, "wait_stats_time", waitType, reader.GetInt64(3), ts);
+ Seed(serverId, "wait_stats_signal", waitType, reader.GetInt64(4), ts);
count++;
}
if (count > 0) _logger?.LogDebug("Seeded {Count} wait_stats baseline rows", count);
@@ -134,7 +147,8 @@ private async Task SeedFileIoStatsAsync(DuckDBConnection connection)
SELECT server_id, database_name, file_name,
num_of_reads, num_of_writes, read_bytes, write_bytes,
io_stall_read_ms, io_stall_write_ms,
- io_stall_queued_read_ms, io_stall_queued_write_ms
+ io_stall_queued_read_ms, io_stall_queued_write_ms,
+ collection_time
FROM file_io_stats
WHERE (server_id, collection_time) IN (
SELECT server_id, MAX(collection_time) FROM file_io_stats GROUP BY server_id
@@ -147,14 +161,15 @@ FROM file_io_stats
var dbName = reader.IsDBNull(1) ? "" : reader.GetString(1);
var fileName = reader.IsDBNull(2) ? "" : reader.GetString(2);
var deltaKey = $"{dbName}|{fileName}";
- Seed(serverId, "file_io_reads", deltaKey, reader.IsDBNull(3) ? 0 : reader.GetInt64(3));
- Seed(serverId, "file_io_writes", deltaKey, reader.IsDBNull(4) ? 0 : reader.GetInt64(4));
- Seed(serverId, "file_io_read_bytes", deltaKey, reader.IsDBNull(5) ? 0 : reader.GetInt64(5));
- Seed(serverId, "file_io_write_bytes", deltaKey, reader.IsDBNull(6) ? 0 : reader.GetInt64(6));
- Seed(serverId, "file_io_stall_read", deltaKey, reader.IsDBNull(7) ? 0 : reader.GetInt64(7));
- Seed(serverId, "file_io_stall_write", deltaKey, reader.IsDBNull(8) ? 0 : reader.GetInt64(8));
- Seed(serverId, "file_io_stall_queued_read", deltaKey, reader.IsDBNull(9) ? 0 : reader.GetInt64(9));
- Seed(serverId, "file_io_stall_queued_write", deltaKey, reader.IsDBNull(10) ? 0 : reader.GetInt64(10));
+ var ts = reader.IsDBNull(11) ? (DateTime?)null : reader.GetDateTime(11);
+ Seed(serverId, "file_io_reads", deltaKey, reader.IsDBNull(3) ? 0 : reader.GetInt64(3), ts);
+ Seed(serverId, "file_io_writes", deltaKey, reader.IsDBNull(4) ? 0 : reader.GetInt64(4), ts);
+ Seed(serverId, "file_io_read_bytes", deltaKey, reader.IsDBNull(5) ? 0 : reader.GetInt64(5), ts);
+ Seed(serverId, "file_io_write_bytes", deltaKey, reader.IsDBNull(6) ? 0 : reader.GetInt64(6), ts);
+ Seed(serverId, "file_io_stall_read", deltaKey, reader.IsDBNull(7) ? 0 : reader.GetInt64(7), ts);
+ Seed(serverId, "file_io_stall_write", deltaKey, reader.IsDBNull(8) ? 0 : reader.GetInt64(8), ts);
+ Seed(serverId, "file_io_stall_queued_read", deltaKey, reader.IsDBNull(9) ? 0 : reader.GetInt64(9), ts);
+ Seed(serverId, "file_io_stall_queued_write", deltaKey, reader.IsDBNull(10) ? 0 : reader.GetInt64(10), ts);
count++;
}
if (count > 0) _logger?.LogDebug("Seeded {Count} file_io_stats baseline rows", count);
@@ -164,7 +179,7 @@ private async Task SeedPerfmonStatsAsync(DuckDBConnection connection)
{
using var cmd = connection.CreateCommand();
cmd.CommandText = @"
-SELECT server_id, object_name, counter_name, instance_name, cntr_value
+SELECT server_id, object_name, counter_name, instance_name, cntr_value, collection_time
FROM perfmon_stats
WHERE (server_id, collection_time) IN (
SELECT server_id, MAX(collection_time) FROM perfmon_stats GROUP BY server_id
@@ -177,7 +192,8 @@ FROM perfmon_stats
var objectName = reader.IsDBNull(1) ? "" : reader.GetString(1);
var counter = reader.IsDBNull(2) ? "" : reader.GetString(2);
var instance = reader.IsDBNull(3) ? "" : reader.GetString(3);
- Seed(serverId, "perfmon", $"{objectName}|{counter}|{instance}", reader.GetInt64(4));
+ var ts = reader.IsDBNull(5) ? (DateTime?)null : reader.GetDateTime(5);
+ Seed(serverId, "perfmon", $"{objectName}|{counter}|{instance}", reader.GetInt64(4), ts);
count++;
}
if (count > 0) _logger?.LogDebug("Seeded {Count} perfmon_stats baseline rows", count);
@@ -202,8 +218,8 @@ FROM memory_grant_stats
var poolId = reader.IsDBNull(1) ? 0 : reader.GetInt32(1);
var semaphoreId = reader.IsDBNull(2) ? (short)0 : reader.GetInt16(2);
var deltaKey = $"{poolId}_{semaphoreId}";
- Seed(serverId, "memory_grants_timeouts", deltaKey, reader.IsDBNull(3) ? 0 : reader.GetInt64(3));
- Seed(serverId, "memory_grants_forced", deltaKey, reader.IsDBNull(4) ? 0 : reader.GetInt64(4));
+ Seed(serverId, "memory_grants_timeouts", deltaKey, reader.IsDBNull(3) ? 0 : reader.GetInt64(3), null);
+ Seed(serverId, "memory_grants_forced", deltaKey, reader.IsDBNull(4) ? 0 : reader.GetInt64(4), null);
count++;
}
if (count > 0) _logger?.LogDebug("Seeded {Count} memory_grant_stats baseline rows", count);
diff --git a/Lite/Services/LocalDataService.Perfmon.cs b/Lite/Services/LocalDataService.Perfmon.cs
index f748846..7d7748d 100644
--- a/Lite/Services/LocalDataService.Perfmon.cs
+++ b/Lite/Services/LocalDataService.Perfmon.cs
@@ -95,13 +95,14 @@ public async Task> GetPerfmonTrendAsync(int serverId, st
command.CommandText = @"
SELECT
collection_time,
- cntr_value,
- delta_cntr_value
+ SUM(cntr_value) AS cntr_value,
+ SUM(delta_cntr_value) AS delta_cntr_value
FROM v_perfmon_stats
WHERE server_id = $1
AND counter_name = $2
AND collection_time >= $3
AND collection_time <= $4
+GROUP BY collection_time
ORDER BY collection_time";
command.Parameters.Add(new DuckDBParameter { Value = serverId });
diff --git a/Lite/Services/RemoteCollectorService.Perfmon.cs b/Lite/Services/RemoteCollectorService.Perfmon.cs
index 8e0b1ec..145b77b 100644
--- a/Lite/Services/RemoteCollectorService.Perfmon.cs
+++ b/Lite/Services/RemoteCollectorService.Perfmon.cs
@@ -178,9 +178,11 @@ WHERE pc.counter_name IN (
var instanceName = reader.IsDBNull(2) ? "" : reader.GetString(2);
var cntrValue = reader.GetInt64(3);
- /* Delta for per-second counters */
+ /* Delta for per-second counters — gap detection at 5min (5x the 1-min collection interval)
+ prevents inflated deltas after app restarts */
var deltaKey = $"{objectName}|{counterName}|{instanceName}";
- var deltaCntrValue = _deltaCalculator.CalculateDelta(serverId, "perfmon", deltaKey, cntrValue, baselineOnly: true);
+ var deltaCntrValue = _deltaCalculator.CalculateDelta(serverId, "perfmon", deltaKey, cntrValue,
+ baselineOnly: true, collectionTime: collectionTime, maxGapSeconds: 300);
var row = appender.CreateRow();
row.AppendValue(GenerateCollectionId())
@@ -192,7 +194,7 @@ WHERE pc.counter_name IN (
.AppendValue(instanceName)
.AppendValue(cntrValue)
.AppendValue(deltaCntrValue)
- .AppendValue(600) /* 10-minute interval */
+ .AppendValue(60) /* 1-minute collection interval */
.EndRow();
rowsCollected++;