diff --git a/Dashboard/ServerTab.xaml b/Dashboard/ServerTab.xaml
index 370993fe..18061d80 100644
--- a/Dashboard/ServerTab.xaml
+++ b/Dashboard/ServerTab.xaml
@@ -241,92 +241,98 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Dashboard/ServerTab.xaml.cs b/Dashboard/ServerTab.xaml.cs
index c04f0d8b..f99a6b3d 100644
--- a/Dashboard/ServerTab.xaml.cs
+++ b/Dashboard/ServerTab.xaml.cs
@@ -78,6 +78,7 @@ public partial class ServerTab : UserControl
private Helpers.ChartHoverHelper? _blockingDurationHover;
private Helpers.ChartHoverHelper? _deadlocksHover;
private Helpers.ChartHoverHelper? _deadlockWaitTimeHover;
+ private Helpers.ChartHoverHelper? _collectorDurationHover;
public ServerTab(ServerConnection serverConnection, int utcOffsetMinutes = 0)
{
@@ -92,6 +93,7 @@ public ServerTab(ServerConnection serverConnection, int utcOffsetMinutes = 0)
_blockingDurationHover = new Helpers.ChartHoverHelper(BlockingStatsDurationChart, "ms");
_deadlocksHover = new Helpers.ChartHoverHelper(BlockingStatsDeadlocksChart, "events");
_deadlockWaitTimeHover = new Helpers.ChartHoverHelper(BlockingStatsDeadlockWaitTimeChart, "ms");
+ _collectorDurationHover = new Helpers.ChartHoverHelper(CollectorDurationChart, "ms");
_serverConnection = serverConnection;
UtcOffsetMinutes = utcOffsetMinutes;
@@ -1247,6 +1249,7 @@ private async Task LoadDataAsync()
// Fetch all data in parallel — overview queries + all tab refreshes
var healthTask = _databaseService.GetCollectionHealthAsync();
+ var durationLogsTask = _databaseService.GetCollectionDurationLogsAsync();
var blockingEventsTask = _databaseService.GetBlockingEventsAsync();
var deadlocksTask = _databaseService.GetDeadlocksAsync();
var blockingStatsTask = _databaseService.GetBlockingDeadlockStatsAsync(_blockingStatsHoursBack, _blockingStatsFromDate, _blockingStatsToDate);
@@ -1266,7 +1269,7 @@ private async Task LoadDataAsync()
// Wait for everything to complete before _isRefreshing resets
await Task.WhenAll(
- healthTask, blockingEventsTask, deadlocksTask, blockingStatsTask, lockWaitStatsTask,
+ healthTask, durationLogsTask, blockingEventsTask, deadlocksTask, blockingStatsTask, lockWaitStatsTask,
performanceTask, memoryTask, resourceOverviewTask, runningJobsTask,
resourceMetricsTask, dailySummaryTask, criticalIssuesTask, defaultTraceTask, currentConfigTask, configChangesTask, systemEventsTask);
@@ -1276,6 +1279,9 @@ await Task.WhenAll(
UpdateDataGridFilterButtonStyles(HealthDataGrid, _collectionHealthFilters);
HealthNoDataMessage.Visibility = healthData.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
+ var durationLogs = await durationLogsTask;
+ UpdateCollectorDurationChart(durationLogs);
+
try
{
var blockingEvents = await blockingEventsTask;
@@ -1740,6 +1746,55 @@ private void LoadBlockingStatsCharts(List data, int h
BlockingStatsDeadlockWaitTimeChart.Refresh();
}
+ private void UpdateCollectorDurationChart(List data)
+ {
+ if (_legendPanels.TryGetValue(CollectorDurationChart, out var existingPanel) && existingPanel != null)
+ {
+ CollectorDurationChart.Plot.Axes.Remove(existingPanel);
+ _legendPanels[CollectorDurationChart] = null;
+ }
+ CollectorDurationChart.Plot.Clear();
+ _collectorDurationHover?.Clear();
+ ApplyDarkModeToChart(CollectorDurationChart);
+
+ if (data.Count == 0) { CollectorDurationChart.Refresh(); return; }
+
+ var groups = data
+ .Where(d => d.CollectorName != "scheduled_master_collector")
+ .GroupBy(d => d.CollectorName)
+ .OrderBy(g => g.Key)
+ .ToList();
+
+ var colors = TabHelpers.ChartColors;
+ int colorIndex = 0;
+ foreach (var group in groups)
+ {
+ var points = group.OrderBy(d => d.CollectionTime).ToList();
+ if (points.Count < 2) continue;
+
+ var (xs, ys) = TabHelpers.FillTimeSeriesGaps(
+ points.Select(d => d.CollectionTime),
+ points.Select(d => (double)d.DurationMs));
+
+ var scatter = CollectorDurationChart.Plot.Add.Scatter(xs, ys);
+ scatter.LegendText = group.Key;
+ scatter.Color = colors[colorIndex % colors.Length];
+ scatter.LineWidth = 2;
+ scatter.MarkerSize = 0;
+ _collectorDurationHover?.Add(scatter, group.Key);
+ colorIndex++;
+ }
+
+ CollectorDurationChart.Plot.Axes.DateTimeTicksBottom();
+ TabHelpers.ReapplyAxisColors(CollectorDurationChart);
+ CollectorDurationChart.Plot.YLabel("Duration (ms)");
+ CollectorDurationChart.Plot.Axes.AutoScale();
+ _legendPanels[CollectorDurationChart] = CollectorDurationChart.Plot.ShowLegend(ScottPlot.Edge.Bottom);
+ CollectorDurationChart.Plot.Legend.FontSize = 12;
+ LockChartVerticalAxis(CollectorDurationChart);
+ CollectorDurationChart.Refresh();
+ }
+
private void LoadLockWaitStatsChart(List data, int hoursBack, DateTime? fromDate, DateTime? toDate)
{
// Calculate the time range for X-axis limits (use server time, not local time)
diff --git a/Dashboard/Services/DatabaseService.cs b/Dashboard/Services/DatabaseService.cs
index f255a10b..40fb2d38 100644
--- a/Dashboard/Services/DatabaseService.cs
+++ b/Dashboard/Services/DatabaseService.cs
@@ -180,6 +180,48 @@ CASE health_status
return items;
}
+ public async Task> GetCollectionDurationLogsAsync(int hoursBack = 24)
+ {
+ var items = new List();
+
+ await using var tc = await OpenThrottledConnectionAsync();
+ var connection = tc.Connection;
+
+ string query = @"
+ SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
+
+ SELECT
+ collector_name,
+ collection_time,
+ duration_ms
+ FROM config.collection_log
+ WHERE collection_status = 'SUCCESS'
+ AND duration_ms IS NOT NULL
+ AND collection_time >= DATEADD(HOUR, -@hours_back, GETUTCDATE())
+ ORDER BY
+ collection_time;";
+
+ using var command = new SqlCommand(query, connection);
+ command.CommandTimeout = 120;
+ command.Parameters.Add(new SqlParameter("@hours_back", SqlDbType.Int) { Value = hoursBack });
+
+ using (StartQueryTiming("Collection Duration Logs", query, connection))
+ {
+ using var reader = await command.ExecuteReaderAsync();
+ while (await reader.ReadAsync())
+ {
+ items.Add(new CollectionLogEntry
+ {
+ CollectorName = reader.GetString(0),
+ CollectionTime = reader.GetDateTime(1),
+ DurationMs = reader.GetInt32(2)
+ });
+ }
+ }
+
+ return items;
+ }
+
// ============================================
// Helper Methods for Safe Type Conversion
// ============================================