Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 225 additions & 0 deletions Dashboard/Controls/QueryPerformanceContent.xaml

Large diffs are not rendered by default.

167 changes: 167 additions & 0 deletions Dashboard/Controls/QueryPerformanceContent.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ public partial class QueryPerformanceContent : UserControl
private Dictionary<string, Models.ColumnFilterState> _activeQueriesFilters = new();
private List<QuerySnapshotItem>? _activeQueriesUnfilteredData;

// Current Active Queries filter state
private Dictionary<string, Models.ColumnFilterState> _currentActiveFilters = new();
private List<LiveQueryItem>? _currentActiveUnfilteredData;

// Query Stats filter state
private Dictionary<string, Models.ColumnFilterState> _queryStatsFilters = new();
private List<QueryStatsItem>? _queryStatsUnfilteredData;
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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)
Expand Down
45 changes: 45 additions & 0 deletions Dashboard/Models/LiveQueryItem.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
114 changes: 114 additions & 0 deletions Dashboard/Services/DatabaseService.QueryPerformance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2416,5 +2416,119 @@ ORDER BY

return items;
}

/// <summary>
/// Queries dm_exec_requests directly for currently running queries (live snapshot).
/// Returns results with query plans in memory for on-demand download.
/// </summary>
public async Task<List<LiveQueryItem>> GetCurrentActiveQueriesAsync()
{
var items = new List<LiveQueryItem>();

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;
}
}
}
Loading