From b2f7a8729ab612d70d9dc486de3942db76acd260 Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Sun, 22 Feb 2026 22:04:21 -0500 Subject: [PATCH 1/2] Fix archive view column mismatch, wait_stats thread-safety, and percent_complete type cast (#234) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UNION ALL → UNION ALL BY NAME in archive views so schema migrations don't break parquet unions (query_snapshots grid was empty) - Lazy> for ignored wait types to prevent concurrent HashSet corruption when multiple servers collect in parallel - Convert.ToDecimal for percent_complete (dm_exec_requests returns real, not decimal) Co-Authored-By: Claude Opus 4.6 --- Lite/Controls/ServerTab.xaml.cs | 2 +- Lite/Database/DuckDbInitializer.cs | 2 +- .../RemoteCollectorService.QuerySnapshots.cs | 2 +- .../RemoteCollectorService.WaitStats.cs | 20 ++++++++----------- Lite/Services/RemoteCollectorService.cs | 1 + 5 files changed, 12 insertions(+), 15 deletions(-) diff --git a/Lite/Controls/ServerTab.xaml.cs b/Lite/Controls/ServerTab.xaml.cs index 42e8af42..f3a6ab95 100644 --- a/Lite/Controls/ServerTab.xaml.cs +++ b/Lite/Controls/ServerTab.xaml.cs @@ -2288,7 +2288,7 @@ private async void LiveSnapshot_Click(object sender, RoutedEventArgs e) HostName = reader.IsDBNull(21) ? "" : reader.GetString(21), ProgramName = reader.IsDBNull(22) ? "" : reader.GetString(22), OpenTransactionCount = reader.IsDBNull(23) ? 0 : Convert.ToInt32(reader.GetValue(23)), - PercentComplete = reader.IsDBNull(24) ? 0m : reader.GetDecimal(24), + PercentComplete = reader.IsDBNull(24) ? 0m : Convert.ToDecimal(reader.GetValue(24)), CollectionTime = snapshotTime }); } diff --git a/Lite/Database/DuckDbInitializer.cs b/Lite/Database/DuckDbInitializer.cs index d9628c07..79603449 100644 --- a/Lite/Database/DuckDbInitializer.cs +++ b/Lite/Database/DuckDbInitializer.cs @@ -522,7 +522,7 @@ public async Task CreateArchiveViewsAsync() if (hasParquetFiles) { var globPath = parquetGlob.Replace("\\", "/"); - viewSql = $"CREATE OR REPLACE VIEW v_{table} AS SELECT * FROM {table} UNION ALL SELECT * FROM read_parquet('{globPath}', union_by_name=true)"; + viewSql = $"CREATE OR REPLACE VIEW v_{table} AS SELECT * FROM {table} UNION ALL BY NAME SELECT * FROM read_parquet('{globPath}', union_by_name=true)"; } else { diff --git a/Lite/Services/RemoteCollectorService.QuerySnapshots.cs b/Lite/Services/RemoteCollectorService.QuerySnapshots.cs index 17b5d847..bb469fe7 100644 --- a/Lite/Services/RemoteCollectorService.QuerySnapshots.cs +++ b/Lite/Services/RemoteCollectorService.QuerySnapshots.cs @@ -157,7 +157,7 @@ private async Task CollectQuerySnapshotsAsync(ServerConnection server, Canc .AppendValue(reader.IsDBNull(21) ? (string?)null : reader.GetString(21)) /* host_name */ .AppendValue(reader.IsDBNull(22) ? (string?)null : reader.GetString(22)) /* program_name */ .AppendValue(reader.IsDBNull(23) ? 0 : Convert.ToInt32(reader.GetValue(23))) /* open_transaction_count */ - .AppendValue(reader.IsDBNull(24) ? 0m : reader.GetDecimal(24)) /* percent_complete */ + .AppendValue(reader.IsDBNull(24) ? 0m : Convert.ToDecimal(reader.GetValue(24))) /* percent_complete */ .EndRow(); rowsCollected++; diff --git a/Lite/Services/RemoteCollectorService.WaitStats.cs b/Lite/Services/RemoteCollectorService.WaitStats.cs index 52767e7f..2ba17854 100644 --- a/Lite/Services/RemoteCollectorService.WaitStats.cs +++ b/Lite/Services/RemoteCollectorService.WaitStats.cs @@ -22,19 +22,15 @@ namespace PerformanceMonitorLite.Services; public partial class RemoteCollectorService { - private HashSet? _ignoredWaitTypes; + private readonly Lazy> _ignoredWaitTypes; /// - /// Gets the set of wait types to ignore during collection. + /// Loads the set of wait types to ignore during collection. + /// Thread-safe via Lazy<T> (multiple server tasks call this in parallel). /// - private HashSet GetIgnoredWaitTypes() + private HashSet LoadIgnoredWaitTypes() { - if (_ignoredWaitTypes != null) - { - return _ignoredWaitTypes; - } - - _ignoredWaitTypes = new HashSet(StringComparer.OrdinalIgnoreCase); + var waits = new HashSet(StringComparer.OrdinalIgnoreCase); var configPath = Path.Combine(App.ConfigDirectory, "ignored_wait_types.json"); if (File.Exists(configPath)) @@ -51,7 +47,7 @@ private HashSet GetIgnoredWaitTypes() var waitType = wait.GetString(); if (!string.IsNullOrEmpty(waitType)) { - _ignoredWaitTypes.Add(waitType); + waits.Add(waitType); } } } @@ -62,7 +58,7 @@ private HashSet GetIgnoredWaitTypes() } } - return _ignoredWaitTypes; + return waits; } /// @@ -82,7 +78,7 @@ FROM sys.dm_os_wait_stats AS ws WHERE ws.wait_time_ms > 0 OPTION(RECOMPILE);"; - var ignoredWaits = GetIgnoredWaitTypes(); + var ignoredWaits = _ignoredWaitTypes.Value; var serverId = GetServerId(server); var collectionTime = DateTime.UtcNow; var rowsCollected = 0; diff --git a/Lite/Services/RemoteCollectorService.cs b/Lite/Services/RemoteCollectorService.cs index 565159ac..7d3bee6d 100644 --- a/Lite/Services/RemoteCollectorService.cs +++ b/Lite/Services/RemoteCollectorService.cs @@ -112,6 +112,7 @@ public RemoteCollectorService( _scheduleManager = scheduleManager; _logger = logger; _deltaCalculator = new DeltaCalculator(logger); + _ignoredWaitTypes = new Lazy>(LoadIgnoredWaitTypes); } /// From 5dd66f754c2dbdefac421e0c48f519716012dd40 Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Sun, 22 Feb 2026 22:10:56 -0500 Subject: [PATCH 2/2] Fix collector health status bar text color to match other status text Co-Authored-By: Claude Opus 4.6 --- Lite/MainWindow.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lite/MainWindow.xaml.cs b/Lite/MainWindow.xaml.cs index e7ef039f..0c8427d6 100644 --- a/Lite/MainWindow.xaml.cs +++ b/Lite/MainWindow.xaml.cs @@ -354,7 +354,7 @@ private void UpdateCollectorHealth() else { CollectorHealthText.Text = $"Collectors: {health.TotalCollectors} OK"; - CollectorHealthText.Foreground = (System.Windows.Media.Brush)FindResource("ForegroundMutedBrush"); + CollectorHealthText.Foreground = (System.Windows.Media.Brush)FindResource("ForegroundBrush"); CollectorHealthText.ToolTip = null; } }