diff --git a/Dashboard/Controls/QueryPerformanceContent.xaml b/Dashboard/Controls/QueryPerformanceContent.xaml
index 4691bb1d..65507ef0 100644
--- a/Dashboard/Controls/QueryPerformanceContent.xaml
+++ b/Dashboard/Controls/QueryPerformanceContent.xaml
@@ -320,6 +320,231 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Dashboard/Controls/QueryPerformanceContent.xaml.cs b/Dashboard/Controls/QueryPerformanceContent.xaml.cs
index f4c1cf61..6d75e001 100644
--- a/Dashboard/Controls/QueryPerformanceContent.xaml.cs
+++ b/Dashboard/Controls/QueryPerformanceContent.xaml.cs
@@ -44,6 +44,10 @@ public partial class QueryPerformanceContent : UserControl
private Dictionary _activeQueriesFilters = new();
private List? _activeQueriesUnfilteredData;
+ // Current Active Queries filter state
+ private Dictionary _currentActiveFilters = new();
+ private List? _currentActiveUnfilteredData;
+
// Query Stats filter state
private Dictionary _queryStatsFilters = new();
private List? _queryStatsUnfilteredData;
@@ -126,9 +130,12 @@ private void OnUnloaded(object sender, RoutedEventArgs e)
_filterPopupContent.FilterCleared -= ProcStatsFilterPopup_FilterCleared;
_filterPopupContent.FilterApplied -= QueryStoreFilterPopup_FilterApplied;
_filterPopupContent.FilterCleared -= QueryStoreFilterPopup_FilterCleared;
+ _filterPopupContent.FilterApplied -= CurrentActiveFilterPopup_FilterApplied;
+ _filterPopupContent.FilterCleared -= CurrentActiveFilterPopup_FilterCleared;
}
/* Clear large data collections to free memory */
+ _currentActiveUnfilteredData = null;
_activeQueriesUnfilteredData = null;
_queryStatsUnfilteredData = null;
_procStatsUnfilteredData = null;
@@ -149,6 +156,7 @@ private void OnLoaded(object sender, RoutedEventArgs e)
// Apply minimum column widths based on header text to all DataGrids
TabHelpers.AutoSizeColumnMinWidths(ActiveQueriesDataGrid);
+ TabHelpers.AutoSizeColumnMinWidths(CurrentActiveQueriesDataGrid);
TabHelpers.AutoSizeColumnMinWidths(QueryStatsDataGrid);
TabHelpers.AutoSizeColumnMinWidths(ProcStatsDataGrid);
TabHelpers.AutoSizeColumnMinWidths(QueryStoreDataGrid);
@@ -157,6 +165,7 @@ private void OnLoaded(object sender, RoutedEventArgs e)
// Freeze first columns for easier horizontal scrolling
TabHelpers.FreezeColumns(ActiveQueriesDataGrid, 2);
+ TabHelpers.FreezeColumns(CurrentActiveQueriesDataGrid, 2);
TabHelpers.FreezeColumns(QueryStatsDataGrid, 2);
TabHelpers.FreezeColumns(ProcStatsDataGrid, 2);
TabHelpers.FreezeColumns(QueryStoreDataGrid, 2);
@@ -543,6 +552,164 @@ private async void DownloadActiveQueryPlan_Click(object sender, RoutedEventArgs
#endregion
+ #region Current Active Queries
+
+ private async void CurrentActiveRefresh_Click(object sender, RoutedEventArgs e)
+ {
+ await RefreshCurrentActiveQueriesAsync();
+ }
+
+ private async Task RefreshCurrentActiveQueriesAsync()
+ {
+ if (_databaseService == null) return;
+
+ try
+ {
+ CurrentActiveRefreshButton.IsEnabled = false;
+
+ if (CurrentActiveQueriesDataGrid.ItemsSource == null)
+ {
+ CurrentActiveLoading.IsLoading = true;
+ CurrentActiveNoDataMessage.Visibility = Visibility.Collapsed;
+ }
+ SetStatus("Loading current active queries...");
+
+ var data = await _databaseService.GetCurrentActiveQueriesAsync();
+
+ _currentActiveUnfilteredData = data;
+ CurrentActiveQueriesDataGrid.ItemsSource = data;
+ CurrentActiveNoDataMessage.Visibility = data.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
+ CurrentActiveTimestamp.Text = $"Last refreshed: {DateTime.Now:HH:mm:ss} — {data.Count} queries";
+
+ if (_currentActiveFilters.Count > 0)
+ ApplyCurrentActiveFilters();
+
+ SetStatus($"Loaded {data.Count} current active queries");
+ }
+ catch (Exception ex)
+ {
+ Logger.Error($"Error loading current active queries: {ex.Message}");
+ CurrentActiveTimestamp.Text = $"Error: {ex.Message}";
+ SetStatus("Error loading current active queries");
+ }
+ finally
+ {
+ CurrentActiveLoading.IsLoading = false;
+ CurrentActiveRefreshButton.IsEnabled = true;
+ }
+ }
+
+ private void CurrentActiveFilter_Click(object sender, RoutedEventArgs e)
+ {
+ if (sender is not Button button || button.Tag is not string columnName) return;
+
+ EnsureFilterPopup();
+ RewireFilterPopupEvents(
+ CurrentActiveFilterPopup_FilterApplied,
+ CurrentActiveFilterPopup_FilterCleared);
+
+ _currentActiveFilters.TryGetValue(columnName, out var existingFilter);
+ _filterPopupContent!.Initialize(columnName, existingFilter);
+
+ _filterPopup!.PlacementTarget = button;
+ _filterPopup.IsOpen = true;
+ }
+
+ private void CurrentActiveFilterPopup_FilterApplied(object? sender, FilterAppliedEventArgs e)
+ {
+ if (_filterPopup != null)
+ _filterPopup.IsOpen = false;
+
+ if (e.FilterState.IsActive)
+ {
+ _currentActiveFilters[e.FilterState.ColumnName] = e.FilterState;
+ }
+ else
+ {
+ _currentActiveFilters.Remove(e.FilterState.ColumnName);
+ }
+
+ ApplyCurrentActiveFilters();
+ UpdateDataGridFilterButtonStyles(CurrentActiveQueriesDataGrid, _currentActiveFilters);
+ }
+
+ private void CurrentActiveFilterPopup_FilterCleared(object? sender, EventArgs e)
+ {
+ if (_filterPopup != null)
+ _filterPopup.IsOpen = false;
+ }
+
+ private void ApplyCurrentActiveFilters()
+ {
+ if (_currentActiveUnfilteredData == null) return;
+
+ if (_currentActiveFilters.Count == 0)
+ {
+ CurrentActiveQueriesDataGrid.ItemsSource = _currentActiveUnfilteredData;
+ return;
+ }
+
+ var filteredData = _currentActiveUnfilteredData.Where(item =>
+ {
+ foreach (var filter in _currentActiveFilters.Values)
+ {
+ if (filter.IsActive && !DataGridFilterService.MatchesFilter(item, filter))
+ {
+ return false;
+ }
+ }
+ return true;
+ }).ToList();
+
+ CurrentActiveQueriesDataGrid.ItemsSource = filteredData;
+ }
+
+ private void DownloadCurrentActiveEstPlan_Click(object sender, RoutedEventArgs e)
+ {
+ if (sender is Button button && button.DataContext is LiveQueryItem item && !string.IsNullOrEmpty(item.QueryPlan))
+ {
+ var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss", CultureInfo.InvariantCulture);
+ var defaultFileName = $"estimated_plan_{item.SessionId}_{timestamp}.sqlplan";
+
+ var saveFileDialog = new SaveFileDialog
+ {
+ FileName = defaultFileName,
+ DefaultExt = ".sqlplan",
+ Filter = "SQL Plan (*.sqlplan)|*.sqlplan|XML Files (*.xml)|*.xml|All Files (*.*)|*.*",
+ Title = "Save Query Plan"
+ };
+
+ if (saveFileDialog.ShowDialog() == true)
+ {
+ File.WriteAllText(saveFileDialog.FileName, item.QueryPlan);
+ }
+ }
+ }
+
+ private void DownloadCurrentActiveLivePlan_Click(object sender, RoutedEventArgs e)
+ {
+ if (sender is Button button && button.DataContext is LiveQueryItem item && !string.IsNullOrEmpty(item.LiveQueryPlan))
+ {
+ var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss", CultureInfo.InvariantCulture);
+ var defaultFileName = $"live_plan_{item.SessionId}_{timestamp}.sqlplan";
+
+ var saveFileDialog = new SaveFileDialog
+ {
+ FileName = defaultFileName,
+ DefaultExt = ".sqlplan",
+ Filter = "SQL Plan (*.sqlplan)|*.sqlplan|XML Files (*.xml)|*.xml|All Files (*.*)|*.*",
+ Title = "Save Live Query Plan"
+ };
+
+ if (saveFileDialog.ShowDialog() == true)
+ {
+ File.WriteAllText(saveFileDialog.FileName, item.LiveQueryPlan);
+ }
+ }
+ }
+
+ #endregion
+
#region Query Stats
private void QueryStatsFilter_Click(object sender, RoutedEventArgs e)
diff --git a/Dashboard/Models/LiveQueryItem.cs b/Dashboard/Models/LiveQueryItem.cs
new file mode 100644
index 00000000..935a9d03
--- /dev/null
+++ b/Dashboard/Models/LiveQueryItem.cs
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2026 Erik Darling, Darling Data LLC
+ *
+ * This file is part of the SQL Server Performance Monitor.
+ *
+ * Licensed under the MIT License. See LICENSE file in the project root for full license information.
+ */
+
+using System;
+
+namespace PerformanceMonitorDashboard.Models
+{
+ public class LiveQueryItem
+ {
+ public DateTime SnapshotTime { get; set; }
+ public int SessionId { get; set; }
+ public string? DatabaseName { get; set; }
+ public string ElapsedTimeFormatted { get; set; } = string.Empty;
+ public string? QueryText { get; set; }
+ public string? QueryPlan { get; set; }
+ public string? LiveQueryPlan { get; set; }
+ public string? Status { get; set; }
+ public int BlockingSessionId { get; set; }
+ public string? WaitType { get; set; }
+ public long WaitTimeMs { get; set; }
+ public string? WaitResource { get; set; }
+ public long CpuTimeMs { get; set; }
+ public long TotalElapsedTimeMs { get; set; }
+ public long Reads { get; set; }
+ public long Writes { get; set; }
+ public long LogicalReads { get; set; }
+ public decimal GrantedQueryMemoryGb { get; set; }
+ public string? TransactionIsolationLevel { get; set; }
+ public int Dop { get; set; }
+ public int ParallelWorkerCount { get; set; }
+ public string? LoginName { get; set; }
+ public string? HostName { get; set; }
+ public string? ProgramName { get; set; }
+ public int OpenTransactionCount { get; set; }
+ public decimal PercentComplete { get; set; }
+
+ public bool HasQueryPlan => !string.IsNullOrEmpty(QueryPlan);
+ public bool HasLiveQueryPlan => !string.IsNullOrEmpty(LiveQueryPlan);
+ }
+}
diff --git a/Dashboard/Services/DatabaseService.QueryPerformance.cs b/Dashboard/Services/DatabaseService.QueryPerformance.cs
index 60d2fc02..440f36ea 100644
--- a/Dashboard/Services/DatabaseService.QueryPerformance.cs
+++ b/Dashboard/Services/DatabaseService.QueryPerformance.cs
@@ -2416,5 +2416,119 @@ ORDER BY
return items;
}
+
+ ///
+ /// Queries dm_exec_requests directly for currently running queries (live snapshot).
+ /// Returns results with query plans in memory for on-demand download.
+ ///
+ public async Task> GetCurrentActiveQueriesAsync()
+ {
+ var items = new List();
+
+ await using var tc = await OpenThrottledConnectionAsync();
+ var connection = tc.Connection;
+
+ string query = @"
+SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
+SET LOCK_TIMEOUT 1000;
+
+SELECT
+ der.session_id,
+ database_name = DB_NAME(der.database_id),
+ elapsed_time_formatted =
+ CASE
+ WHEN der.total_elapsed_time < 0
+ THEN '00 00:00:00.000'
+ ELSE RIGHT(REPLICATE('0', 2) + CONVERT(varchar(10), der.total_elapsed_time / 86400000), 2) +
+ ' ' + RIGHT(CONVERT(varchar(30), DATEADD(second, der.total_elapsed_time / 1000, 0), 120), 9) +
+ '.' + RIGHT('000' + CONVERT(varchar(3), der.total_elapsed_time % 1000), 3)
+ END,
+ query_text = SUBSTRING(dest.text, (der.statement_start_offset / 2) + 1,
+ ((CASE der.statement_end_offset WHEN -1 THEN DATALENGTH(dest.text)
+ ELSE der.statement_end_offset END - der.statement_start_offset) / 2) + 1),
+ query_plan = TRY_CAST(deqp.query_plan AS nvarchar(max)),
+ live_query_plan = deqs.query_plan,
+ der.status,
+ der.blocking_session_id,
+ der.wait_type,
+ wait_time_ms = CONVERT(bigint, der.wait_time),
+ der.wait_resource,
+ cpu_time_ms = CONVERT(bigint, der.cpu_time),
+ total_elapsed_time_ms = CONVERT(bigint, der.total_elapsed_time),
+ der.reads,
+ der.writes,
+ der.logical_reads,
+ granted_query_memory_gb = CONVERT(decimal(38, 2), (der.granted_query_memory / 128. / 1024.)),
+ transaction_isolation_level =
+ CASE der.transaction_isolation_level
+ WHEN 0 THEN 'Unspecified'
+ WHEN 1 THEN 'Read Uncommitted'
+ WHEN 2 THEN 'Read Committed'
+ WHEN 3 THEN 'Repeatable Read'
+ WHEN 4 THEN 'Serializable'
+ WHEN 5 THEN 'Snapshot'
+ ELSE '???'
+ END,
+ der.dop,
+ der.parallel_worker_count,
+ des.login_name,
+ des.host_name,
+ des.program_name,
+ des.open_transaction_count,
+ der.percent_complete
+FROM sys.dm_exec_requests AS der
+JOIN sys.dm_exec_sessions AS des
+ ON des.session_id = der.session_id
+OUTER APPLY sys.dm_exec_sql_text(der.plan_handle) AS dest
+OUTER APPLY sys.dm_exec_text_query_plan(der.plan_handle, der.statement_start_offset, der.statement_end_offset) AS deqp
+OUTER APPLY sys.dm_exec_query_statistics_xml(der.session_id) AS deqs
+WHERE der.session_id <> @@SPID
+AND der.session_id >= 50
+AND dest.text IS NOT NULL
+AND der.database_id <> ISNULL(DB_ID(N'PerformanceMonitor'), 0)
+ORDER BY der.cpu_time DESC, der.parallel_worker_count DESC
+OPTION(MAXDOP 1, RECOMPILE);";
+
+ using var command = new SqlCommand(query, connection);
+ command.CommandTimeout = 30;
+
+ var snapshotTime = DateTime.Now;
+
+ using var reader = await command.ExecuteReaderAsync();
+ while (await reader.ReadAsync())
+ {
+ items.Add(new LiveQueryItem
+ {
+ SnapshotTime = snapshotTime,
+ SessionId = Convert.ToInt32(reader.GetValue(0)),
+ DatabaseName = reader.IsDBNull(1) ? null : reader.GetString(1),
+ ElapsedTimeFormatted = reader.IsDBNull(2) ? string.Empty : reader.GetString(2),
+ QueryText = reader.IsDBNull(3) ? null : reader.GetString(3),
+ QueryPlan = reader.IsDBNull(4) ? null : reader.GetString(4),
+ LiveQueryPlan = reader.IsDBNull(5) ? null : reader.GetValue(5)?.ToString(),
+ Status = reader.IsDBNull(6) ? null : reader.GetString(6),
+ BlockingSessionId = reader.IsDBNull(7) ? 0 : Convert.ToInt32(reader.GetValue(7)),
+ WaitType = reader.IsDBNull(8) ? null : reader.GetString(8),
+ WaitTimeMs = reader.IsDBNull(9) ? 0 : Convert.ToInt64(reader.GetValue(9)),
+ WaitResource = reader.IsDBNull(10) ? null : reader.GetString(10),
+ CpuTimeMs = reader.IsDBNull(11) ? 0 : Convert.ToInt64(reader.GetValue(11)),
+ TotalElapsedTimeMs = reader.IsDBNull(12) ? 0 : Convert.ToInt64(reader.GetValue(12)),
+ Reads = reader.IsDBNull(13) ? 0 : Convert.ToInt64(reader.GetValue(13)),
+ Writes = reader.IsDBNull(14) ? 0 : Convert.ToInt64(reader.GetValue(14)),
+ LogicalReads = reader.IsDBNull(15) ? 0 : Convert.ToInt64(reader.GetValue(15)),
+ GrantedQueryMemoryGb = reader.IsDBNull(16) ? 0m : reader.GetDecimal(16),
+ TransactionIsolationLevel = reader.IsDBNull(17) ? null : reader.GetString(17),
+ Dop = reader.IsDBNull(18) ? 0 : Convert.ToInt32(reader.GetValue(18)),
+ ParallelWorkerCount = reader.IsDBNull(19) ? 0 : Convert.ToInt32(reader.GetValue(19)),
+ LoginName = reader.IsDBNull(20) ? null : reader.GetString(20),
+ HostName = reader.IsDBNull(21) ? null : reader.GetString(21),
+ ProgramName = reader.IsDBNull(22) ? null : reader.GetString(22),
+ OpenTransactionCount = reader.IsDBNull(23) ? 0 : Convert.ToInt32(reader.GetValue(23)),
+ PercentComplete = reader.IsDBNull(24) ? 0m : reader.GetDecimal(24)
+ });
+ }
+
+ return items;
+ }
}
}
diff --git a/Lite/Controls/ServerTab.xaml b/Lite/Controls/ServerTab.xaml
index 127df301..79319e1b 100644
--- a/Lite/Controls/ServerTab.xaml
+++ b/Lite/Controls/ServerTab.xaml
@@ -196,7 +196,14 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -254,6 +270,12 @@
+
+
+
+
+
+
diff --git a/Lite/Controls/ServerTab.xaml.cs b/Lite/Controls/ServerTab.xaml.cs
index 16ba442c..42e8af42 100644
--- a/Lite/Controls/ServerTab.xaml.cs
+++ b/Lite/Controls/ServerTab.xaml.cs
@@ -20,6 +20,7 @@
using System.ComponentModel;
using System.Windows.Data;
using System.Windows.Threading;
+using Microsoft.Data.SqlClient;
using Microsoft.Win32;
using PerformanceMonitorLite.Database;
using PerformanceMonitorLite.Models;
@@ -525,6 +526,7 @@ await System.Threading.Tasks.Task.WhenAll(
/* Update grids (via filter managers to preserve active filters) */
_querySnapshotsFilterMgr!.UpdateData(snapshotsTask.Result);
+ LiveSnapshotIndicator.Text = "";
_queryStatsFilterMgr!.UpdateData(queryStatsTask.Result);
SetInitialSort(QueryStatsGrid, "TotalElapsedMs", ListSortDirection.Descending);
_procStatsFilterMgr!.UpdateData(procStatsTask.Result);
@@ -2233,6 +2235,78 @@ private void DownloadSnapshotLivePlan_Click(object sender, RoutedEventArgs e)
SavePlanFile(row.LiveQueryPlan, $"ActualPlan_Session{row.SessionId}");
}
+ private async void LiveSnapshot_Click(object sender, RoutedEventArgs e)
+ {
+ LiveSnapshotButton.IsEnabled = false;
+ LiveSnapshotIndicator.Text = "Querying...";
+
+ try
+ {
+ var connectionString = _server.GetConnectionString(_credentialService);
+ var builder = new SqlConnectionStringBuilder(connectionString)
+ {
+ ConnectTimeout = 15
+ };
+
+ var query = RemoteCollectorService.BuildQuerySnapshotsQuery(supportsLiveQueryPlan: true);
+
+ await using var connection = new SqlConnection(builder.ConnectionString);
+ await connection.OpenAsync();
+
+ using var command = new SqlCommand(query, connection);
+ command.CommandTimeout = 30;
+
+ using var reader = await command.ExecuteReaderAsync();
+ var results = new List();
+ var snapshotTime = DateTime.UtcNow;
+
+ while (await reader.ReadAsync())
+ {
+ results.Add(new QuerySnapshotRow
+ {
+ SessionId = Convert.ToInt32(reader.GetValue(0)),
+ DatabaseName = reader.IsDBNull(1) ? "" : reader.GetString(1),
+ ElapsedTimeFormatted = reader.IsDBNull(2) ? "" : reader.GetString(2),
+ QueryText = reader.IsDBNull(3) ? "" : reader.GetString(3),
+ QueryPlan = reader.IsDBNull(4) ? null : reader.GetString(4),
+ LiveQueryPlan = reader.IsDBNull(5) ? null : reader.GetValue(5)?.ToString(),
+ Status = reader.IsDBNull(6) ? "" : reader.GetString(6),
+ BlockingSessionId = reader.IsDBNull(7) ? 0 : Convert.ToInt32(reader.GetValue(7)),
+ WaitType = reader.IsDBNull(8) ? "" : reader.GetString(8),
+ WaitTimeMs = reader.IsDBNull(9) ? 0 : Convert.ToInt64(reader.GetValue(9)),
+ WaitResource = reader.IsDBNull(10) ? "" : reader.GetString(10),
+ CpuTimeMs = reader.IsDBNull(11) ? 0 : Convert.ToInt64(reader.GetValue(11)),
+ TotalElapsedTimeMs = reader.IsDBNull(12) ? 0 : Convert.ToInt64(reader.GetValue(12)),
+ Reads = reader.IsDBNull(13) ? 0 : Convert.ToInt64(reader.GetValue(13)),
+ Writes = reader.IsDBNull(14) ? 0 : Convert.ToInt64(reader.GetValue(14)),
+ LogicalReads = reader.IsDBNull(15) ? 0 : Convert.ToInt64(reader.GetValue(15)),
+ GrantedQueryMemoryGb = reader.IsDBNull(16) ? 0 : Convert.ToDouble(reader.GetValue(16)),
+ TransactionIsolationLevel = reader.IsDBNull(17) ? "" : reader.GetString(17),
+ Dop = reader.IsDBNull(18) ? 0 : Convert.ToInt32(reader.GetValue(18)),
+ ParallelWorkerCount = reader.IsDBNull(19) ? 0 : Convert.ToInt32(reader.GetValue(19)),
+ LoginName = reader.IsDBNull(20) ? "" : reader.GetString(20),
+ 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),
+ CollectionTime = snapshotTime
+ });
+ }
+
+ _querySnapshotsFilterMgr!.UpdateData(results);
+ LiveSnapshotIndicator.Text = $"LIVE at {DateTime.Now:HH:mm:ss} ({results.Count} queries)";
+ }
+ catch (Exception ex)
+ {
+ LiveSnapshotIndicator.Text = $"Error: {ex.Message}";
+ AppLogger.Error("ServerTab", $"Live snapshot failed: {ex.Message}");
+ }
+ finally
+ {
+ LiveSnapshotButton.IsEnabled = true;
+ }
+ }
+
private void SavePlanFile(string planXml, string defaultName)
{
var dialog = new SaveFileDialog
diff --git a/Lite/Database/DuckDbInitializer.cs b/Lite/Database/DuckDbInitializer.cs
index 103555ea..d9628c07 100644
--- a/Lite/Database/DuckDbInitializer.cs
+++ b/Lite/Database/DuckDbInitializer.cs
@@ -68,7 +68,7 @@ public void Dispose()
///
/// Current schema version. Increment this when schema changes require table rebuilds.
///
- internal const int CurrentSchemaVersion = 11;
+ internal const int CurrentSchemaVersion = 12;
private readonly string _archivePath;
@@ -417,6 +417,25 @@ can be identified by server without needing a lookup table. */
_logger?.LogInformation("Running migration to v11: rebuilding database_config for expanded sys.databases columns");
await ExecuteNonQueryAsync(connection, "DROP TABLE IF EXISTS database_config");
}
+
+ if (fromVersion < 12)
+ {
+ /* v12: Added login_name, host_name, program_name, open_transaction_count,
+ percent_complete columns to query_snapshots for Issue #149. */
+ _logger?.LogInformation("Running migration to v12: adding session columns to query_snapshots");
+ try
+ {
+ await ExecuteNonQueryAsync(connection, "ALTER TABLE query_snapshots ADD COLUMN IF NOT EXISTS login_name VARCHAR");
+ await ExecuteNonQueryAsync(connection, "ALTER TABLE query_snapshots ADD COLUMN IF NOT EXISTS host_name VARCHAR");
+ await ExecuteNonQueryAsync(connection, "ALTER TABLE query_snapshots ADD COLUMN IF NOT EXISTS program_name VARCHAR");
+ await ExecuteNonQueryAsync(connection, "ALTER TABLE query_snapshots ADD COLUMN IF NOT EXISTS open_transaction_count INTEGER");
+ await ExecuteNonQueryAsync(connection, "ALTER TABLE query_snapshots ADD COLUMN IF NOT EXISTS percent_complete DECIMAL(5,2)");
+ }
+ catch
+ {
+ /* Table doesn't exist yet — will be created with correct schema below */
+ }
+ }
}
///
diff --git a/Lite/Database/Schema.cs b/Lite/Database/Schema.cs
index 4336e8b7..070fa7b3 100644
--- a/Lite/Database/Schema.cs
+++ b/Lite/Database/Schema.cs
@@ -255,7 +255,12 @@ CREATE TABLE IF NOT EXISTS query_snapshots (
granted_query_memory_gb DECIMAL(18,2),
transaction_isolation_level VARCHAR,
dop INTEGER,
- parallel_worker_count INTEGER
+ parallel_worker_count INTEGER,
+ login_name VARCHAR,
+ host_name VARCHAR,
+ program_name VARCHAR,
+ open_transaction_count INTEGER,
+ percent_complete DECIMAL(5,2)
)";
public const string CreateTempdbStatsTable = @"
diff --git a/Lite/Services/LocalDataService.Blocking.cs b/Lite/Services/LocalDataService.Blocking.cs
index c68f9be7..4ef859f6 100644
--- a/Lite/Services/LocalDataService.Blocking.cs
+++ b/Lite/Services/LocalDataService.Blocking.cs
@@ -138,7 +138,12 @@ public async Task> GetLatestQuerySnapshotsAsync(int serve
parallel_worker_count,
query_plan,
live_query_plan,
- collection_time
+ collection_time,
+ login_name,
+ host_name,
+ program_name,
+ open_transaction_count,
+ percent_complete
FROM v_query_snapshots
WHERE server_id = $1
AND collection_time >= $2
@@ -176,7 +181,12 @@ AND query_text NOT LIKE 'WAITFOR%'
ParallelWorkerCount = reader.IsDBNull(17) ? 0 : reader.GetInt32(17),
QueryPlan = reader.IsDBNull(18) ? null : reader.GetString(18),
LiveQueryPlan = reader.IsDBNull(19) ? null : reader.GetString(19),
- CollectionTime = reader.IsDBNull(20) ? DateTime.MinValue : reader.GetDateTime(20)
+ CollectionTime = reader.IsDBNull(20) ? DateTime.MinValue : reader.GetDateTime(20),
+ LoginName = reader.IsDBNull(21) ? "" : reader.GetString(21),
+ HostName = reader.IsDBNull(22) ? "" : reader.GetString(22),
+ ProgramName = reader.IsDBNull(23) ? "" : reader.GetString(23),
+ OpenTransactionCount = reader.IsDBNull(24) ? 0 : reader.GetInt32(24),
+ PercentComplete = reader.IsDBNull(25) ? 0m : Convert.ToDecimal(reader.GetValue(25))
});
}
@@ -745,6 +755,11 @@ public class QuerySnapshotRow
public DateTime CollectionTime { get; set; }
public string? QueryPlan { get; set; }
public string? LiveQueryPlan { get; set; }
+ public string LoginName { get; set; } = "";
+ public string HostName { get; set; } = "";
+ public string ProgramName { get; set; } = "";
+ public int OpenTransactionCount { get; set; }
+ public decimal PercentComplete { get; set; }
public bool HasQueryPlan => !string.IsNullOrEmpty(QueryPlan);
public bool HasLiveQueryPlan => !string.IsNullOrEmpty(LiveQueryPlan);
public string CollectionTimeLocal => CollectionTime == DateTime.MinValue ? "" : ServerTimeHelper.FormatServerTime(CollectionTime);
diff --git a/Lite/Services/RemoteCollectorService.QuerySnapshots.cs b/Lite/Services/RemoteCollectorService.QuerySnapshots.cs
index 894a8221..17b5d847 100644
--- a/Lite/Services/RemoteCollectorService.QuerySnapshots.cs
+++ b/Lite/Services/RemoteCollectorService.QuerySnapshots.cs
@@ -19,17 +19,7 @@ namespace PerformanceMonitorLite.Services;
public partial class RemoteCollectorService
{
- ///
- /// Collects currently running queries (point-in-time snapshot).
- ///
- private async Task CollectQuerySnapshotsAsync(ServerConnection server, CancellationToken cancellationToken)
- {
- // dm_exec_query_statistics_xml requires SQL Server 2016 SP1+ (version 13)
- var serverStatus = _serverManager.GetConnectionStatus(server.Id);
- var supportsLiveQueryPlan = serverStatus.SqlMajorVersion >= 13 || serverStatus.SqlMajorVersion == 0
- || serverStatus.SqlEngineEdition == 5 || serverStatus.SqlEngineEdition == 8;
-
- const string queryBase = @"
+ private const string QuerySnapshotsBase = @"
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SET LOCK_TIMEOUT 1000;
@@ -71,8 +61,15 @@ WHEN 5 THEN 'Snapshot'
ELSE '???'
END,
der.dop,
- der.parallel_worker_count
+ der.parallel_worker_count,
+ des.login_name,
+ des.host_name,
+ des.program_name,
+ des.open_transaction_count,
+ der.percent_complete
FROM sys.dm_exec_requests AS der
+JOIN sys.dm_exec_sessions AS des
+ ON des.session_id = der.session_id
OUTER APPLY sys.dm_exec_sql_text(der.plan_handle) AS dest
OUTER APPLY sys.dm_exec_text_query_plan(der.plan_handle, der.statement_start_offset, der.statement_end_offset) AS deqp
{1}
@@ -83,9 +80,28 @@ AND dest.text IS NOT NULL
ORDER BY der.cpu_time DESC, der.parallel_worker_count DESC
OPTION(MAXDOP 1, RECOMPILE);";
- var query = supportsLiveQueryPlan
- ? string.Format(queryBase, "live_query_plan = deqs.query_plan,", "OUTER APPLY sys.dm_exec_query_statistics_xml(der.session_id) AS deqs")
- : string.Format(queryBase, "live_query_plan = CONVERT(xml, NULL),", "");
+ ///
+ /// Builds the query snapshots SQL with or without live query plan support.
+ /// Used by both the collector and the live snapshot button.
+ ///
+ internal static string BuildQuerySnapshotsQuery(bool supportsLiveQueryPlan)
+ {
+ return supportsLiveQueryPlan
+ ? string.Format(QuerySnapshotsBase, "live_query_plan = deqs.query_plan,", "OUTER APPLY sys.dm_exec_query_statistics_xml(der.session_id) AS deqs")
+ : string.Format(QuerySnapshotsBase, "live_query_plan = CONVERT(xml, NULL),", "");
+ }
+
+ ///
+ /// Collects currently running queries (point-in-time snapshot).
+ ///
+ private async Task CollectQuerySnapshotsAsync(ServerConnection server, CancellationToken cancellationToken)
+ {
+ // dm_exec_query_statistics_xml requires SQL Server 2016 SP1+ (version 13)
+ var serverStatus = _serverManager.GetConnectionStatus(server.Id);
+ var supportsLiveQueryPlan = serverStatus.SqlMajorVersion >= 13 || serverStatus.SqlMajorVersion == 0
+ || serverStatus.SqlEngineEdition == 5 || serverStatus.SqlEngineEdition == 8;
+
+ var query = BuildQuerySnapshotsQuery(supportsLiveQueryPlan);
var serverId = GetServerId(server);
var collectionTime = DateTime.UtcNow;
@@ -137,6 +153,11 @@ AND dest.text IS NOT NULL
.AppendValue(reader.IsDBNull(17) ? (string?)null : reader.GetString(17)) /* transaction_isolation_level */
.AppendValue(reader.IsDBNull(18) ? 0 : Convert.ToInt32(reader.GetValue(18))) /* dop */
.AppendValue(reader.IsDBNull(19) ? 0 : Convert.ToInt32(reader.GetValue(19))) /* parallel_worker_count */
+ .AppendValue(reader.IsDBNull(20) ? (string?)null : reader.GetString(20)) /* login_name */
+ .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 */
.EndRow();
rowsCollected++;