From 6c628befe2229056734e0c39c10e6b91fd6d4cf0 Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Sun, 15 Feb 2026 12:41:13 -0500 Subject: [PATCH 01/34] Fix XE ring buffer query timeouts on large buffers (#37) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Materialize the ring buffer XML into a table variable before shredding with CROSS APPLY .nodes(). The inline subquery pattern forced SQL Server to repeatedly parse the full 4MB+ XML blob, causing 2+ minute execution times and 30-second CommandTimeout failures during sustained load. Applied to all 4 XE ring buffer queries: - Deadlock collection (on-prem + Azure SQL DB) - Blocked process report collection (on-prem + Azure SQL DB) Performance: ~2:44 → ~0.46s (~350x improvement). Co-Authored-By: Claude Opus 4.6 --- ...teCollectorService.BlockedProcessReport.cs | 64 +++++++++++++------ .../RemoteCollectorService.Deadlocks.cs | 64 +++++++++++++------ 2 files changed, 92 insertions(+), 36 deletions(-) diff --git a/Lite/Services/RemoteCollectorService.BlockedProcessReport.cs b/Lite/Services/RemoteCollectorService.BlockedProcessReport.cs index f08a7ecf..62f241c5 100644 --- a/Lite/Services/RemoteCollectorService.BlockedProcessReport.cs +++ b/Lite/Services/RemoteCollectorService.BlockedProcessReport.cs @@ -273,23 +273,37 @@ Use .query() to get XML with structure intact, then CONVERT to nvarchar(max) */ query = $@" SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; +DECLARE + @PerformanceMonitor_BlockedProcess TABLE +( + ring_buffer xml NOT NULL +); + +INSERT + @PerformanceMonitor_BlockedProcess +( + ring_buffer +) +SELECT + ring_xml = TRY_CAST(xet.target_data AS xml) +FROM sys.dm_xe_database_session_targets AS xet +JOIN sys.dm_xe_database_sessions AS xes + ON xes.address = xet.event_session_address +WHERE xes.name = N'{BlockedProcessXeSessionName}' +AND xet.target_name = N'ring_buffer' +OPTION(RECOMPILE); + SELECT event_time = evt.value('(@timestamp)[1]', 'datetime2'), blocked_process_report_xml = CONVERT(nvarchar(max), evt.query('data[@name=""blocked_process""]/value/blocked-process-report')) FROM ( SELECT - ring_xml = TRY_CAST(xet.target_data AS xml) - FROM sys.dm_xe_database_session_targets AS xet - JOIN sys.dm_xe_database_sessions AS xes - ON xes.address = xet.event_session_address - WHERE xes.name = N'{BlockedProcessXeSessionName}' - AND xet.target_name = N'ring_buffer' + pmd.ring_buffer + FROM @PerformanceMonitor_BlockedProcess AS pmd ) AS rb -CROSS APPLY rb.ring_xml.nodes('RingBufferTarget/event[@name=""blocked_process_report""]') AS q(evt) +CROSS APPLY rb.ring_buffer.nodes('RingBufferTarget/event[@name=""blocked_process_report""]') AS q(evt) WHERE evt.value('(@timestamp)[1]', 'datetime2') > DATEADD(MINUTE, -10, SYSUTCDATETIME()) -ORDER BY - evt.value('(@timestamp)[1]', 'datetime2') DESC OPTION(RECOMPILE);"; } else @@ -299,23 +313,37 @@ Use .query() to get XML with structure intact, then CONVERT to nvarchar(max) */ query = $@" SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; +DECLARE + @PerformanceMonitor_BlockedProcess TABLE +( + ring_buffer xml NOT NULL +); + +INSERT + @PerformanceMonitor_BlockedProcess +( + ring_buffer +) +SELECT + ring_xml = TRY_CAST(xet.target_data AS xml) +FROM sys.dm_xe_session_targets AS xet +JOIN sys.dm_xe_sessions AS xes + ON xes.address = xet.event_session_address +WHERE xes.name = N'{BlockedProcessXeSessionName}' +AND xet.target_name = N'ring_buffer' +OPTION(RECOMPILE); + SELECT event_time = evt.value('(@timestamp)[1]', 'datetime2'), blocked_process_report_xml = CONVERT(nvarchar(max), evt.query('data[@name=""blocked_process""]/value/blocked-process-report')) FROM ( SELECT - ring_xml = TRY_CAST(xet.target_data AS xml) - FROM sys.dm_xe_session_targets AS xet - JOIN sys.dm_xe_sessions AS xes - ON xes.address = xet.event_session_address - WHERE xes.name = N'{BlockedProcessXeSessionName}' - AND xet.target_name = N'ring_buffer' + pmd.ring_buffer + FROM @PerformanceMonitor_BlockedProcess AS pmd ) AS rb -CROSS APPLY rb.ring_xml.nodes('RingBufferTarget/event[@name=""blocked_process_report""]') AS q(evt) +CROSS APPLY rb.ring_buffer.nodes('RingBufferTarget/event[@name=""blocked_process_report""]') AS q(evt) WHERE evt.value('(@timestamp)[1]', 'datetime2') > DATEADD(MINUTE, -10, SYSUTCDATETIME()) -ORDER BY - evt.value('(@timestamp)[1]', 'datetime2') DESC OPTION(RECOMPILE);"; } diff --git a/Lite/Services/RemoteCollectorService.Deadlocks.cs b/Lite/Services/RemoteCollectorService.Deadlocks.cs index 67c4502e..af40e42d 100644 --- a/Lite/Services/RemoteCollectorService.Deadlocks.cs +++ b/Lite/Services/RemoteCollectorService.Deadlocks.cs @@ -271,6 +271,26 @@ Use .query() to get XML with structure intact, then CONVERT to nvarchar(max) */ query = $@" SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; +DECLARE + @PerformanceMonitor_Deadlock TABLE +( + ring_buffer xml NOT NULL +); + +INSERT + @PerformanceMonitor_Deadlock +( + ring_buffer +) +SELECT + ring_xml = TRY_CAST(xet.target_data AS xml) +FROM sys.dm_xe_database_session_targets AS xet +JOIN sys.dm_xe_database_sessions AS xes + ON xes.address = xet.event_session_address +WHERE xes.name = N'{DeadlockXeSessionName}' +AND xet.target_name = N'ring_buffer' +OPTION(RECOMPILE); + SELECT deadlock_time = evt.value('(@timestamp)[1]', 'datetime2'), victim_process_id = evt.value('(data[@name=""xml_report""]/value/deadlock/victim-list/victimProcess/@id)[1]', 'varchar(50)'), @@ -278,17 +298,11 @@ Use .query() to get XML with structure intact, then CONVERT to nvarchar(max) */ FROM ( SELECT - ring_xml = TRY_CAST(xet.target_data AS xml) - FROM sys.dm_xe_database_session_targets AS xet - JOIN sys.dm_xe_database_sessions AS xes - ON xes.address = xet.event_session_address - WHERE xes.name = N'{DeadlockXeSessionName}' - AND xet.target_name = N'ring_buffer' + pmd.ring_buffer + FROM @PerformanceMonitor_Deadlock AS pmd ) AS rb -CROSS APPLY rb.ring_xml.nodes('RingBufferTarget/event[@name=""database_xml_deadlock_report""]') AS q(evt) +CROSS APPLY rb.ring_buffer.nodes('RingBufferTarget/event[@name=""database_xml_deadlock_report""]') AS q(evt) WHERE evt.value('(@timestamp)[1]', 'datetime2') > DATEADD(MINUTE, -10, SYSUTCDATETIME()) -ORDER BY - evt.value('(@timestamp)[1]', 'datetime2') DESC OPTION(RECOMPILE);"; } else @@ -298,6 +312,26 @@ Use .query() to get XML with structure intact, then CONVERT to nvarchar(max) */ query = $@" SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; +DECLARE + @PerformanceMonitor_Deadlock TABLE +( + ring_buffer xml NOT NULL +); + +INSERT + @PerformanceMonitor_Deadlock +( + ring_buffer +) +SELECT + ring_xml = TRY_CAST(xet.target_data AS xml) +FROM sys.dm_xe_session_targets AS xet +JOIN sys.dm_xe_sessions AS xes + ON xes.address = xet.event_session_address +WHERE xes.name = N'{DeadlockXeSessionName}' +AND xet.target_name = N'ring_buffer' +OPTION(RECOMPILE); + SELECT deadlock_time = evt.value('(@timestamp)[1]', 'datetime2'), victim_process_id = evt.value('(data[@name=""xml_report""]/value/deadlock/victim-list/victimProcess/@id)[1]', 'varchar(50)'), @@ -305,17 +339,11 @@ Use .query() to get XML with structure intact, then CONVERT to nvarchar(max) */ FROM ( SELECT - ring_xml = TRY_CAST(xet.target_data AS xml) - FROM sys.dm_xe_session_targets AS xet - JOIN sys.dm_xe_sessions AS xes - ON xes.address = xet.event_session_address - WHERE xes.name = N'{DeadlockXeSessionName}' - AND xet.target_name = N'ring_buffer' + pmd.ring_buffer + FROM @PerformanceMonitor_Deadlock AS pmd ) AS rb -CROSS APPLY rb.ring_xml.nodes('RingBufferTarget/event[@name=""xml_deadlock_report""]') AS q(evt) +CROSS APPLY rb.ring_buffer.nodes('RingBufferTarget/event[@name=""xml_deadlock_report""]') AS q(evt) WHERE evt.value('(@timestamp)[1]', 'datetime2') > DATEADD(MINUTE, -10, SYSUTCDATETIME()) -ORDER BY - evt.value('(@timestamp)[1]', 'datetime2') DESC OPTION(RECOMPILE);"; } From e21f642cb932466a2bbbafd19634f6050b11f3b6 Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Sun, 15 Feb 2026 14:32:13 -0500 Subject: [PATCH 02/34] Add Collection Health tab to Lite UI (#39) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Surface the existing GetCollectionHealthAsync() data in a new tab showing per-collector status, run counts, failure rates, avg duration, timestamps, and last error message. Includes column-level filtering. The backend data (collection_log table, health computation) already existed but was never displayed in the UI — only accessible via status bar tooltip or MCP tool. Co-Authored-By: Claude Opus 4.6 --- Lite/Controls/ServerTab.xaml | 43 +++++++++++++++++++ Lite/Controls/ServerTab.xaml.cs | 7 ++- .../LocalDataService.CollectionHealth.cs | 8 ++++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/Lite/Controls/ServerTab.xaml b/Lite/Controls/ServerTab.xaml index 144f662a..54c51ed7 100644 --- a/Lite/Controls/ServerTab.xaml +++ b/Lite/Controls/ServerTab.xaml @@ -971,6 +971,49 @@ + + + + + + +