Skip to content

Commit 64a6247

Browse files
Fix Dashboard auto-refresh stalling by using stop/start timer pattern (#834)
The _isRefreshing guard in LoadDataAsync could get permanently stuck if any SQL query hung (connection drop, slow timeout), silently killing all subsequent auto-refresh ticks. Replaced with a stop/start pattern: the timer stops before each tick and restarts in finally, bypassing LoadDataAsync entirely for auto-refresh. This guarantees the timer always restarts regardless of what happens during the refresh. Also removed the redundant _isRefreshing guard from CorrelatedTimelineLanesControl.RefreshAsync which was independently causing Server Trends charts to silently skip updates. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ef8435a commit 64a6247

2 files changed

Lines changed: 204 additions & 159 deletions

File tree

Dashboard/Controls/CorrelatedTimelineLanesControl.xaml.cs

Lines changed: 145 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ public partial class CorrelatedTimelineLanesControl : UserControl
2727
private DatabaseService? _dataService;
2828
private SqlServerBaselineProvider? _baselineProvider;
2929
private CorrelatedCrosshairManager? _crosshairManager;
30-
private bool _isRefreshing;
3130

3231
public CorrelatedTimelineLanesControl()
3332
{
@@ -66,176 +65,168 @@ public void Initialize(DatabaseService dataService, SqlServerBaselineProvider? b
6665
public async Task RefreshAsync(int hoursBack, DateTime? fromDate, DateTime? toDate,
6766
(DateTime From, DateTime To)? comparisonRange = null)
6867
{
69-
if (_dataService == null || _isRefreshing) return;
70-
_isRefreshing = true;
68+
if (_dataService == null) return;
69+
70+
_crosshairManager?.PrepareForRefresh();
71+
72+
var cpuTask = _dataService.GetCpuUtilizationAsync(hoursBack, fromDate, toDate);
73+
var waitTask = _dataService.GetTotalWaitStatsTrendAsync(hoursBack, fromDate, toDate);
74+
var blockingTask = _dataService.GetBlockedSessionTrendAsync(hoursBack, fromDate, toDate);
75+
var deadlockTask = _dataService.GetDeadlockTrendAsync(hoursBack, fromDate, toDate);
76+
var memoryTask = _dataService.GetMemoryStatsAsync(hoursBack, fromDate, toDate);
77+
var fileIoTask = _dataService.GetFileIoLatencyTimeSeriesAsync(false, hoursBack, fromDate, toDate);
78+
79+
// Fetch baselines for band rendering if provider is available
80+
var referenceTime = fromDate ?? DateTime.UtcNow.AddHours(-hoursBack);
81+
Task<BaselineBucket?>? cpuBaselineTask = null;
82+
Task<BaselineBucket?>? waitBaselineTask = null;
83+
Task<BaselineBucket?>? ioBaselineTask = null;
84+
Task<BaselineBucket?>? blockingBaselineTask = null;
85+
Task<BaselineBucket?>? deadlockBaselineTask = null;
86+
87+
if (_baselineProvider != null)
88+
{
89+
cpuBaselineTask = GetBaselineAsync(SqlServerMetricNames.Cpu, referenceTime);
90+
waitBaselineTask = GetBaselineAsync(SqlServerMetricNames.WaitStats, referenceTime);
91+
ioBaselineTask = GetBaselineAsync(SqlServerMetricNames.IoLatency, referenceTime);
92+
blockingBaselineTask = GetBaselineAsync(SqlServerMetricNames.Blocking, referenceTime);
93+
deadlockBaselineTask = GetBaselineAsync(SqlServerMetricNames.Deadlock, referenceTime);
94+
}
7195

7296
try
7397
{
74-
_crosshairManager?.PrepareForRefresh();
75-
76-
var cpuTask = _dataService.GetCpuUtilizationAsync(hoursBack, fromDate, toDate);
77-
var waitTask = _dataService.GetTotalWaitStatsTrendAsync(hoursBack, fromDate, toDate);
78-
var blockingTask = _dataService.GetBlockedSessionTrendAsync(hoursBack, fromDate, toDate);
79-
var deadlockTask = _dataService.GetDeadlockTrendAsync(hoursBack, fromDate, toDate);
80-
var memoryTask = _dataService.GetMemoryStatsAsync(hoursBack, fromDate, toDate);
81-
var fileIoTask = _dataService.GetFileIoLatencyTimeSeriesAsync(false, hoursBack, fromDate, toDate);
82-
83-
// Fetch baselines for band rendering if provider is available
84-
var referenceTime = fromDate ?? DateTime.UtcNow.AddHours(-hoursBack);
85-
Task<BaselineBucket?>? cpuBaselineTask = null;
86-
Task<BaselineBucket?>? waitBaselineTask = null;
87-
Task<BaselineBucket?>? ioBaselineTask = null;
88-
Task<BaselineBucket?>? blockingBaselineTask = null;
89-
Task<BaselineBucket?>? deadlockBaselineTask = null;
90-
91-
if (_baselineProvider != null)
92-
{
93-
cpuBaselineTask = GetBaselineAsync(SqlServerMetricNames.Cpu, referenceTime);
94-
waitBaselineTask = GetBaselineAsync(SqlServerMetricNames.WaitStats, referenceTime);
95-
ioBaselineTask = GetBaselineAsync(SqlServerMetricNames.IoLatency, referenceTime);
96-
blockingBaselineTask = GetBaselineAsync(SqlServerMetricNames.Blocking, referenceTime);
97-
deadlockBaselineTask = GetBaselineAsync(SqlServerMetricNames.Deadlock, referenceTime);
98-
}
98+
var tasks = new List<Task> { cpuTask, waitTask, blockingTask, deadlockTask, memoryTask, fileIoTask };
99+
if (cpuBaselineTask != null) tasks.Add(cpuBaselineTask);
100+
if (waitBaselineTask != null) tasks.Add(waitBaselineTask);
101+
if (ioBaselineTask != null) tasks.Add(ioBaselineTask);
102+
if (blockingBaselineTask != null) tasks.Add(blockingBaselineTask);
103+
if (deadlockBaselineTask != null) tasks.Add(deadlockBaselineTask);
104+
await Task.WhenAll(tasks);
105+
}
106+
catch (Exception ex)
107+
{
108+
Debug.WriteLine($"CorrelatedLanes: Data fetch failed: {ex.Message}");
109+
}
99110

100-
try
101-
{
102-
var tasks = new List<Task> { cpuTask, waitTask, blockingTask, deadlockTask, memoryTask, fileIoTask };
103-
if (cpuBaselineTask != null) tasks.Add(cpuBaselineTask);
104-
if (waitBaselineTask != null) tasks.Add(waitBaselineTask);
105-
if (ioBaselineTask != null) tasks.Add(ioBaselineTask);
106-
if (blockingBaselineTask != null) tasks.Add(blockingBaselineTask);
107-
if (deadlockBaselineTask != null) tasks.Add(deadlockBaselineTask);
108-
await Task.WhenAll(tasks);
109-
}
110-
catch (Exception ex)
111-
{
112-
Debug.WriteLine($"CorrelatedLanes: Data fetch failed: {ex.Message}");
113-
}
111+
var cpuBaseline = cpuBaselineTask is { IsCompletedSuccessfully: true } ? cpuBaselineTask.Result : null;
112+
var waitBaseline = waitBaselineTask is { IsCompletedSuccessfully: true } ? waitBaselineTask.Result : null;
113+
var ioBaseline = ioBaselineTask is { IsCompletedSuccessfully: true } ? ioBaselineTask.Result : null;
114+
var blockingBaseline = blockingBaselineTask is { IsCompletedSuccessfully: true } ? blockingBaselineTask.Result : null;
115+
var deadlockBaseline = deadlockBaselineTask is { IsCompletedSuccessfully: true } ? deadlockBaselineTask.Result : null;
116+
var blockingLaneBaseline = blockingBaseline ?? deadlockBaseline;
117+
118+
// minAnomalyValue: absolute floor below which dots/arrows are suppressed even if outside band.
119+
// Prevents "1% CPU above 0.5% baseline" false alarms on idle servers.
120+
if (cpuTask.IsCompletedSuccessfully)
121+
UpdateLane(CpuChart, "CPU %",
122+
cpuTask.Result.OrderBy(d => d.SampleTime)
123+
.Select(d => (d.SampleTime.ToOADate(), (double)d.SqlServerCpuUtilization)).ToList(),
124+
"#4FC3F7", 0, 105, cpuBaseline, minAnomalyValue: 10);
125+
else
126+
ShowEmpty(CpuChart, "CPU %");
114127

115-
var cpuBaseline = cpuBaselineTask is { IsCompletedSuccessfully: true } ? cpuBaselineTask.Result : null;
116-
var waitBaseline = waitBaselineTask is { IsCompletedSuccessfully: true } ? waitBaselineTask.Result : null;
117-
var ioBaseline = ioBaselineTask is { IsCompletedSuccessfully: true } ? ioBaselineTask.Result : null;
118-
var blockingBaseline = blockingBaselineTask is { IsCompletedSuccessfully: true } ? blockingBaselineTask.Result : null;
119-
var deadlockBaseline = deadlockBaselineTask is { IsCompletedSuccessfully: true } ? deadlockBaselineTask.Result : null;
120-
var blockingLaneBaseline = blockingBaseline ?? deadlockBaseline;
121-
122-
// minAnomalyValue: absolute floor below which dots/arrows are suppressed even if outside band.
123-
// Prevents "1% CPU above 0.5% baseline" false alarms on idle servers.
124-
if (cpuTask.IsCompletedSuccessfully)
125-
UpdateLane(CpuChart, "CPU %",
126-
cpuTask.Result.OrderBy(d => d.SampleTime)
127-
.Select(d => (d.SampleTime.ToOADate(), (double)d.SqlServerCpuUtilization)).ToList(),
128-
"#4FC3F7", 0, 105, cpuBaseline, minAnomalyValue: 10);
129-
else
130-
ShowEmpty(CpuChart, "CPU %");
131-
132-
if (waitTask.IsCompletedSuccessfully)
133-
UpdateLane(WaitStatsChart, "Wait ms/sec",
134-
waitTask.Result.Select(d => (d.CollectionTime.ToOADate(), (double)d.WaitTimeMsPerSecond)).ToList(),
135-
"#FFB74D", baseline: waitBaseline, minAnomalyValue: 100);
136-
else
137-
ShowEmpty(WaitStatsChart, "Wait ms/sec");
138-
139-
try
140-
{
141-
var blockingData = blockingTask.IsCompletedSuccessfully
142-
? blockingTask.Result
143-
.GroupBy(d => d.CollectionTime)
144-
.OrderBy(g => g.Key)
145-
.Select(g => (g.Key.ToOADate(), (double)g.Sum(x => x.BlockedCount)))
146-
.ToList()
147-
: new List<(double, double)>();
148-
var deadlockData = deadlockTask.IsCompletedSuccessfully
149-
? deadlockTask.Result
150-
.Select(d => (d.CollectionTime.ToOADate(), (double)d.BlockedCount))
151-
.ToList()
152-
: new List<(double, double)>();
153-
UpdateBlockingLane(blockingData, deadlockData, blockingLaneBaseline);
154-
}
155-
catch (Exception ex)
156-
{
157-
Debug.WriteLine($"CorrelatedLanes: Blocking lane failed: {ex}");
158-
ShowEmpty(BlockingChart, "Blocking & Deadlocking");
159-
}
128+
if (waitTask.IsCompletedSuccessfully)
129+
UpdateLane(WaitStatsChart, "Wait ms/sec",
130+
waitTask.Result.Select(d => (d.CollectionTime.ToOADate(), (double)d.WaitTimeMsPerSecond)).ToList(),
131+
"#FFB74D", baseline: waitBaseline, minAnomalyValue: 100);
132+
else
133+
ShowEmpty(WaitStatsChart, "Wait ms/sec");
160134

161-
if (memoryTask.IsCompletedSuccessfully)
162-
UpdateLane(MemoryChart, "Buffer Pool MB",
163-
memoryTask.Result.Select(d => (d.CollectionTime.ToOADate(), (double)d.TotalMemoryMb)).ToList(),
164-
"#CE93D8");
165-
else
166-
ShowEmpty(MemoryChart, "Buffer Pool MB");
135+
try
136+
{
137+
var blockingData = blockingTask.IsCompletedSuccessfully
138+
? blockingTask.Result
139+
.GroupBy(d => d.CollectionTime)
140+
.OrderBy(g => g.Key)
141+
.Select(g => (g.Key.ToOADate(), (double)g.Sum(x => x.BlockedCount)))
142+
.ToList()
143+
: new List<(double, double)>();
144+
var deadlockData = deadlockTask.IsCompletedSuccessfully
145+
? deadlockTask.Result
146+
.Select(d => (d.CollectionTime.ToOADate(), (double)d.BlockedCount))
147+
.ToList()
148+
: new List<(double, double)>();
149+
UpdateBlockingLane(blockingData, deadlockData, blockingLaneBaseline);
150+
}
151+
catch (Exception ex)
152+
{
153+
Debug.WriteLine($"CorrelatedLanes: Blocking lane failed: {ex}");
154+
ShowEmpty(BlockingChart, "Blocking & Deadlocking");
155+
}
156+
157+
if (memoryTask.IsCompletedSuccessfully)
158+
UpdateLane(MemoryChart, "Buffer Pool MB",
159+
memoryTask.Result.Select(d => (d.CollectionTime.ToOADate(), (double)d.TotalMemoryMb)).ToList(),
160+
"#CE93D8");
161+
else
162+
ShowEmpty(MemoryChart, "Buffer Pool MB");
163+
164+
if (fileIoTask.IsCompletedSuccessfully)
165+
{
166+
var ioGrouped = fileIoTask.Result
167+
.GroupBy(d => d.CollectionTime)
168+
.OrderBy(g => g.Key)
169+
.Select(g => (g.Key.ToOADate(), (double)g.Average(x => x.ReadLatencyMs)))
170+
.ToList();
171+
UpdateLane(FileIoChart, "I/O ms", ioGrouped, "#81C784", baseline: ioBaseline, minAnomalyValue: 2);
172+
}
173+
else
174+
ShowEmpty(FileIoChart, "I/O ms");
175+
176+
// Comparison overlay — fetch reference period data and render as ghost lines
177+
if (comparisonRange.HasValue)
178+
{
179+
var refFrom = comparisonRange.Value.From;
180+
var refTo = comparisonRange.Value.To;
181+
var timeShift = (fromDate ?? DateTime.UtcNow.AddHours(-hoursBack)) - refFrom;
182+
183+
var refCpuTask = _dataService.GetCpuUtilizationAsync(0, refFrom, refTo);
184+
var refWaitTask = _dataService.GetTotalWaitStatsTrendAsync(0, refFrom, refTo);
185+
var refBlockingTask = _dataService.GetBlockedSessionTrendAsync(0, refFrom, refTo);
186+
var refMemoryTask = _dataService.GetMemoryStatsAsync(0, refFrom, refTo);
187+
var refIoTask = _dataService.GetFileIoLatencyTimeSeriesAsync(false, 0, refFrom, refTo);
167188

168-
if (fileIoTask.IsCompletedSuccessfully)
189+
try { await Task.WhenAll(refCpuTask, refWaitTask, refBlockingTask, refMemoryTask, refIoTask); }
190+
catch (Exception ex) { Debug.WriteLine($"CorrelatedLanes: Comparison fetch failed: {ex.Message}"); }
191+
192+
if (refCpuTask.IsCompletedSuccessfully)
193+
AddGhostLine(CpuChart, refCpuTask.Result
194+
.Select(d => (d.SampleTime.Add(timeShift).ToOADate(), (double)d.SqlServerCpuUtilization)).ToList(), "#4FC3F7");
195+
196+
if (refWaitTask.IsCompletedSuccessfully)
197+
AddGhostLine(WaitStatsChart, refWaitTask.Result
198+
.Select(d => (d.CollectionTime.Add(timeShift).ToOADate(), (double)d.WaitTimeMsPerSecond)).ToList(), "#FFB74D");
199+
200+
if (refBlockingTask.IsCompletedSuccessfully)
169201
{
170-
var ioGrouped = fileIoTask.Result
202+
var refBlocking = refBlockingTask.Result
171203
.GroupBy(d => d.CollectionTime)
172204
.OrderBy(g => g.Key)
173-
.Select(g => (g.Key.ToOADate(), (double)g.Average(x => x.ReadLatencyMs)))
205+
.Select(g => (g.Key.Add(timeShift).ToOADate(), (double)g.Sum(x => x.BlockedCount)))
174206
.ToList();
175-
UpdateLane(FileIoChart, "I/O ms", ioGrouped, "#81C784", baseline: ioBaseline, minAnomalyValue: 2);
207+
if (refBlocking.Count > 0)
208+
AddGhostLine(BlockingChart, refBlocking, "#E57373");
176209
}
177-
else
178-
ShowEmpty(FileIoChart, "I/O ms");
179210

180-
// Comparison overlay — fetch reference period data and render as ghost lines
181-
if (comparisonRange.HasValue)
211+
if (refMemoryTask.IsCompletedSuccessfully)
212+
AddGhostLine(MemoryChart, refMemoryTask.Result
213+
.Select(d => (d.CollectionTime.Add(timeShift).ToOADate(), (double)d.TotalMemoryMb)).ToList(), "#CE93D8");
214+
215+
if (refIoTask.IsCompletedSuccessfully)
182216
{
183-
var refFrom = comparisonRange.Value.From;
184-
var refTo = comparisonRange.Value.To;
185-
var timeShift = (fromDate ?? DateTime.UtcNow.AddHours(-hoursBack)) - refFrom;
186-
187-
var refCpuTask = _dataService.GetCpuUtilizationAsync(0, refFrom, refTo);
188-
var refWaitTask = _dataService.GetTotalWaitStatsTrendAsync(0, refFrom, refTo);
189-
var refBlockingTask = _dataService.GetBlockedSessionTrendAsync(0, refFrom, refTo);
190-
var refMemoryTask = _dataService.GetMemoryStatsAsync(0, refFrom, refTo);
191-
var refIoTask = _dataService.GetFileIoLatencyTimeSeriesAsync(false, 0, refFrom, refTo);
192-
193-
try { await Task.WhenAll(refCpuTask, refWaitTask, refBlockingTask, refMemoryTask, refIoTask); }
194-
catch (Exception ex) { Debug.WriteLine($"CorrelatedLanes: Comparison fetch failed: {ex.Message}"); }
195-
196-
if (refCpuTask.IsCompletedSuccessfully)
197-
AddGhostLine(CpuChart, refCpuTask.Result
198-
.Select(d => (d.SampleTime.Add(timeShift).ToOADate(), (double)d.SqlServerCpuUtilization)).ToList(), "#4FC3F7");
199-
200-
if (refWaitTask.IsCompletedSuccessfully)
201-
AddGhostLine(WaitStatsChart, refWaitTask.Result
202-
.Select(d => (d.CollectionTime.Add(timeShift).ToOADate(), (double)d.WaitTimeMsPerSecond)).ToList(), "#FFB74D");
203-
204-
if (refBlockingTask.IsCompletedSuccessfully)
205-
{
206-
var refBlocking = refBlockingTask.Result
207-
.GroupBy(d => d.CollectionTime)
208-
.OrderBy(g => g.Key)
209-
.Select(g => (g.Key.Add(timeShift).ToOADate(), (double)g.Sum(x => x.BlockedCount)))
210-
.ToList();
211-
if (refBlocking.Count > 0)
212-
AddGhostLine(BlockingChart, refBlocking, "#E57373");
213-
}
214-
215-
if (refMemoryTask.IsCompletedSuccessfully)
216-
AddGhostLine(MemoryChart, refMemoryTask.Result
217-
.Select(d => (d.CollectionTime.Add(timeShift).ToOADate(), (double)d.TotalMemoryMb)).ToList(), "#CE93D8");
218-
219-
if (refIoTask.IsCompletedSuccessfully)
220-
{
221-
var refIo = refIoTask.Result
222-
.GroupBy(d => d.CollectionTime)
223-
.OrderBy(g => g.Key)
224-
.Select(g => (g.Key.Add(timeShift).ToOADate(), (double)g.Average(x => x.ReadLatencyMs)))
225-
.ToList();
226-
AddGhostLine(FileIoChart, refIo, "#81C784");
227-
}
228-
229-
_crosshairManager?.SetComparisonLabel(ComparisonLabel(comparisonRange.Value, fromDate, hoursBack));
217+
var refIo = refIoTask.Result
218+
.GroupBy(d => d.CollectionTime)
219+
.OrderBy(g => g.Key)
220+
.Select(g => (g.Key.Add(timeShift).ToOADate(), (double)g.Average(x => x.ReadLatencyMs)))
221+
.ToList();
222+
AddGhostLine(FileIoChart, refIo, "#81C784");
230223
}
231224

232-
_crosshairManager?.ReattachVLines();
233-
SyncXAxes(hoursBack, fromDate, toDate);
234-
}
235-
finally
236-
{
237-
_isRefreshing = false;
225+
_crosshairManager?.SetComparisonLabel(ComparisonLabel(comparisonRange.Value, fromDate, hoursBack));
238226
}
227+
228+
_crosshairManager?.ReattachVLines();
229+
SyncXAxes(hoursBack, fromDate, toDate);
239230
}
240231

241232
/// <summary>

0 commit comments

Comments
 (0)