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
21 changes: 20 additions & 1 deletion Dashboard/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
78 changes: 47 additions & 31 deletions Lite/Services/DeltaCalculator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ namespace PerformanceMonitorLite.Services;
public class DeltaCalculator
{
/// <summary>
/// Cache structure: serverId -> collectorName -> key -> previousValue
/// Cache structure: serverId -> collectorName -> key -> (previousValue, timestamp)
/// </summary>
private readonly ConcurrentDictionary<int, ConcurrentDictionary<string, ConcurrentDictionary<string, long>>> _cache = new();
private readonly ConcurrentDictionary<int, ConcurrentDictionary<string, ConcurrentDictionary<string, (long Value, DateTime? Timestamp)>>> _cache = new();

private readonly ILogger? _logger;

Expand Down Expand Up @@ -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.
/// </summary>
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<string, ConcurrentDictionary<string, long>>());
var collectorCache = serverCache.GetOrAdd(collectorName, _ => new ConcurrentDictionary<string, long>());
var serverCache = _cache.GetOrAdd(serverId, _ => new ConcurrentDictionary<string, ConcurrentDictionary<string, (long Value, DateTime? Timestamp)>>());
var collectorCache = serverCache.GetOrAdd(collectorName, _ => new ConcurrentDictionary<string, (long Value, DateTime? Timestamp)>());

long delta = 0;

Expand All @@ -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;
Expand All @@ -97,18 +109,18 @@ public long CalculateDelta(int serverId, string collectorName, string key, long
/// <summary>
/// Seeds a single value into the cache without computing a delta.
/// </summary>
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<string, ConcurrentDictionary<string, long>>());
var collectorCache = serverCache.GetOrAdd(collectorName, _ => new ConcurrentDictionary<string, long>());
collectorCache[key] = value;
var serverCache = _cache.GetOrAdd(serverId, _ => new ConcurrentDictionary<string, ConcurrentDictionary<string, (long Value, DateTime? Timestamp)>>());
var collectorCache = serverCache.GetOrAdd(collectorName, _ => new ConcurrentDictionary<string, (long Value, DateTime? Timestamp)>());
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
Expand All @@ -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);
Expand All @@ -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
Expand All @@ -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);
Expand All @@ -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
Expand All @@ -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);
Expand All @@ -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);
Expand Down
5 changes: 3 additions & 2 deletions Lite/Services/LocalDataService.Perfmon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,14 @@ public async Task<List<PerfmonTrendPoint>> 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 });
Expand Down
8 changes: 5 additions & 3 deletions Lite/Services/RemoteCollectorService.Perfmon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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++;
Expand Down
Loading