Skip to content

Commit 2b61592

Browse files
Fix memory leaks in Lite: delta cache, event handlers, chart helpers (#758) (#760)
- DeltaCalculator: add ClearServer() to free cache when server tab closes - MainWindow: store event handler delegates so they can be unsubscribed on tab close (AlertCountsChanged, ApplyTimeRangeRequested, ManualRefreshRequested) - ChartHoverHelper: add Dispose() to unsubscribe MouseMove/MouseLeave events - ServerTab: add DisposeChartHelpers() to clean up all 27 hover helpers - CloseServerTab now calls all cleanup methods Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8c9e8a0 commit 2b61592

5 files changed

Lines changed: 72 additions & 8 deletions

File tree

Lite/Controls/ServerTab.xaml.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5243,6 +5243,37 @@ public void StopRefresh()
52435243
_refreshTimer.Stop();
52445244
}
52455245

5246+
public void DisposeChartHelpers()
5247+
{
5248+
_waitStatsHover?.Dispose();
5249+
_perfmonHover?.Dispose();
5250+
_overviewCpuHover?.Dispose();
5251+
_overviewMemoryHover?.Dispose();
5252+
_overviewFileIoHover?.Dispose();
5253+
_overviewWaitStatsHover?.Dispose();
5254+
_cpuHover?.Dispose();
5255+
_memoryHover?.Dispose();
5256+
_tempDbHover?.Dispose();
5257+
_tempDbFileIoHover?.Dispose();
5258+
_fileIoReadHover?.Dispose();
5259+
_fileIoWriteHover?.Dispose();
5260+
_fileIoReadThroughputHover?.Dispose();
5261+
_fileIoWriteThroughputHover?.Dispose();
5262+
_collectorDurationHover?.Dispose();
5263+
_queryDurationTrendHover?.Dispose();
5264+
_procDurationTrendHover?.Dispose();
5265+
_queryStoreDurationTrendHover?.Dispose();
5266+
_executionCountTrendHover?.Dispose();
5267+
_lockWaitTrendHover?.Dispose();
5268+
_blockingTrendHover?.Dispose();
5269+
_deadlockTrendHover?.Dispose();
5270+
_memoryClerksHover?.Dispose();
5271+
_memoryGrantSizingHover?.Dispose();
5272+
_memoryGrantActivityHover?.Dispose();
5273+
_currentWaitsDurationHover?.Dispose();
5274+
_currentWaitsBlockedHover?.Dispose();
5275+
}
5276+
52465277
/* ========== Column Filtering ========== */
52475278

52485279
private void InitializeFilterManagers()

Lite/Helpers/ChartHoverHelper.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ public ChartHoverHelper(ScottPlot.WPF.WpfPlot chart, string unit)
5656

5757
public string Unit { get => _unit; set => _unit = value; }
5858

59+
public void Dispose()
60+
{
61+
_chart.MouseMove -= OnMouseMove;
62+
_chart.MouseLeave -= OnMouseLeave;
63+
_popup.IsOpen = false;
64+
_scatters.Clear();
65+
}
66+
5967
public void Clear() => _scatters.Clear();
6068

6169
public void Add(ScottPlot.Plottables.Scatter scatter, string label) =>

Lite/MainWindow.xaml.cs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public partial class MainWindow : Window
3636
private CancellationTokenSource? _backgroundCts;
3737
private SystemTrayService? _trayService;
3838
private readonly Dictionary<string, TabItem> _openServerTabs = new();
39+
private readonly Dictionary<string, (Action<int, int, DateTime?> AlertCounts, Action<int> ApplyTimeRange, Func<Task> ManualRefresh)> _tabEventHandlers = new();
3940
private readonly Dictionary<string, bool> _previousConnectionStates = new();
4041
private readonly Dictionary<string, bool> _previousCollectorErrorStates = new();
4142
private readonly Dictionary<string, DateTime> _lastCpuAlert = new();
@@ -530,15 +531,13 @@ private async void ConnectToServer(ServerConnection server)
530531
Content = serverTab
531532
};
532533

533-
/* Subscribe to alert counts for badge updates */
534+
/* Subscribe to events — store handlers so we can unsubscribe on tab close */
534535
var serverId = server.Id;
535-
serverTab.AlertCountsChanged += (blockingCount, deadlockCount, latestEventTime) =>
536+
Action<int, int, DateTime?> alertHandler = (blockingCount, deadlockCount, latestEventTime) =>
536537
{
537538
Dispatcher.Invoke(() => UpdateTabBadge(tabHeader, serverId, blockingCount, deadlockCount, latestEventTime));
538539
};
539-
540-
/* Subscribe to "Apply to All" time range propagation */
541-
serverTab.ApplyTimeRangeRequested += (selectedIndex) =>
540+
Action<int> timeRangeHandler = (selectedIndex) =>
542541
{
543542
Dispatcher.Invoke(() =>
544543
{
@@ -551,9 +550,7 @@ private async void ConnectToServer(ServerConnection server)
551550
}
552551
});
553552
};
554-
555-
/* Re-collect on-load data (config, trace flags) when refresh button is clicked */
556-
serverTab.ManualRefreshRequested += async () =>
553+
Func<Task> refreshHandler = async () =>
557554
{
558555
if (_collectorService != null)
559556
{
@@ -572,6 +569,11 @@ private async void ConnectToServer(ServerConnection server)
572569
}
573570
};
574571

572+
serverTab.AlertCountsChanged += alertHandler;
573+
serverTab.ApplyTimeRangeRequested += timeRangeHandler;
574+
serverTab.ManualRefreshRequested += refreshHandler;
575+
_tabEventHandlers[server.Id] = (alertHandler, timeRangeHandler, refreshHandler);
576+
575577
_openServerTabs[server.Id] = tabItem;
576578
ServerTabControl.Items.Add(tabItem);
577579
ServerTabControl.SelectedItem = tabItem;
@@ -793,7 +795,20 @@ private void CloseServerTab(string serverId)
793795
{
794796
if (tab.Content is ServerTab serverTab)
795797
{
798+
/* Unsubscribe event handlers to prevent memory leaks */
799+
if (_tabEventHandlers.TryGetValue(serverId, out var handlers))
800+
{
801+
serverTab.AlertCountsChanged -= handlers.AlertCounts;
802+
serverTab.ApplyTimeRangeRequested -= handlers.ApplyTimeRange;
803+
serverTab.ManualRefreshRequested -= handlers.ManualRefresh;
804+
_tabEventHandlers.Remove(serverId);
805+
}
806+
796807
serverTab.StopRefresh();
808+
serverTab.DisposeChartHelpers();
809+
810+
/* Clear delta cache for this server to free memory */
811+
_collectorService?.DeltaCalculator?.ClearServer(serverTab.ServerId);
797812
}
798813

799814
ServerTabControl.Items.Remove(tab);

Lite/Services/DeltaCalculator.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ public async Task SeedFromDatabaseAsync(DuckDbInitializer duckDb)
5959
}
6060
}
6161

62+
/// <summary>
63+
/// Removes all cached entries for a server (e.g., when the server tab is closed).
64+
/// Next collection will re-seed from database if needed.
65+
/// </summary>
66+
public void ClearServer(int serverId)
67+
{
68+
_cache.TryRemove(serverId, out _);
69+
}
70+
6271
/// <summary>
6372
/// Calculates the delta between the current value and the previous cached value.
6473
/// First-ever sighting (no baseline): returns currentValue so single-execution queries appear.

Lite/Services/RemoteCollectorService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ public partial class RemoteCollectorService
5959
private readonly ScheduleManager _scheduleManager;
6060
private readonly ILogger<RemoteCollectorService>? _logger;
6161
private readonly DeltaCalculator _deltaCalculator;
62+
public DeltaCalculator DeltaCalculator => _deltaCalculator;
6263
private static long s_idCounter = DateTime.UtcNow.Ticks;
6364

6465
/// <summary>

0 commit comments

Comments
 (0)