Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8fffe7c
Fix #410 (#411)
ClaudioESSilva Mar 4, 2026
4110769
Fix #412 (#413)
ClaudioESSilva Mar 4, 2026
5c2b769
GUI installer: log installation history to config.installation_histor…
erikdarlingdata Mar 4, 2026
7bb9dd7
Feature/long running queries config settings (#415)
HannahVernon Mar 4, 2026
d3b7952
Sync plan viewer fixes from plan-b: spool labels, unmatched index det…
erikdarlingdata Mar 4, 2026
e3a646c
fixes many warnings, and pre-calculates the RegEx patterns at compile…
MisterZeus Mar 4, 2026
5fb6d47
Complete GeneratedRegex conversion and remove Compiled flags (#420)
erikdarlingdata Mar 4, 2026
6d4478a
Add permissions section to README with least-privilege setup (#421)
erikdarlingdata Mar 4, 2026
3daf1b0
Replace custom TrayToolTip with plain ToolTipText to fix crash
erikdarlingdata Mar 4, 2026
ebe6226
Add resilience to DuckDB read lock acquisition
erikdarlingdata Mar 4, 2026
562269f
Restore custom TrayToolTip and silently handle Hardcodet crash (issue…
erikdarlingdata Mar 4, 2026
b31b0f0
Merge pull request #424 from erikdarlingdata/fix/tray-tooltip-crash
erikdarlingdata Mar 4, 2026
7b22d82
Merge pull request #425 from erikdarlingdata/fix/lock-resilience
erikdarlingdata Mar 4, 2026
7544ebc
Fix incorrect table name in Data Retention section
erikdarlingdata Mar 4, 2026
d84496a
Fix RID Lookup analyzer rule to match new PhysicalOp label (#429)
erikdarlingdata Mar 4, 2026
0751e51
Merge pull request #428 from erikdarlingdata/fix/readme-retention-table
erikdarlingdata Mar 5, 2026
6e3fce3
Add uninstall option to CLI and GUI installers (#431)
erikdarlingdata Mar 5, 2026
9c04bb2
LOB compression + deduplication for query stats tables (#419)
erikdarlingdata Mar 5, 2026
d2500c2
Merge pull request #433 from erikdarlingdata/feature/lob-compression-…
erikdarlingdata Mar 5, 2026
aeecadc
Merge pull request #432 from erikdarlingdata/feature/uninstall
erikdarlingdata Mar 5, 2026
0d8868f
Add RESTORING database filter to waiting_tasks collector (#430)
erikdarlingdata Mar 5, 2026
44e5961
Merge pull request #434 from erikdarlingdata/fix/waiting-tasks-mirror…
erikdarlingdata Mar 5, 2026
30d7247
Add CI check to require version bump on PRs to main
erikdarlingdata Mar 5, 2026
f7e1b7e
Restore commercial support tiers to README
erikdarlingdata Mar 5, 2026
088351e
Merge pull request #437 from erikdarlingdata/fix/readme-commercial-su…
erikdarlingdata Mar 5, 2026
c26e2cd
Add wait stats query drill-down (#372)
erikdarlingdata Mar 5, 2026
18141ba
Merge pull request #442 from erikdarlingdata/feature/wait-drill-down
erikdarlingdata Mar 5, 2026
b85efe7
Merge pull request #435 from erikdarlingdata/feature/version-bump-check
erikdarlingdata Mar 5, 2026
7315cfa
Fix poison wait false positives and alert log parsing (#445)
erikdarlingdata Mar 6, 2026
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
48 changes: 48 additions & 0 deletions .github/workflows/check-version-bump.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Check version bump
on:
pull_request:
branches: [main]

jobs:
check-version:
if: github.head_ref == 'dev'
runs-on: ubuntu-latest

steps:
- name: Checkout PR branch
uses: actions/checkout@v4

- name: Get PR version
id: pr
shell: pwsh
run: |
$version = ([xml](Get-Content Dashboard/Dashboard.csproj)).Project.PropertyGroup.Version | Where-Object { $_ }
echo "VERSION=$version" >> $env:GITHUB_OUTPUT
Write-Host "PR version: $version"

- name: Checkout main
uses: actions/checkout@v4
with:
ref: main
path: main-branch

- name: Get main version
id: main
shell: pwsh
run: |
$version = ([xml](Get-Content main-branch/Dashboard/Dashboard.csproj)).Project.PropertyGroup.Version | Where-Object { $_ }
echo "VERSION=$version" >> $env:GITHUB_OUTPUT
Write-Host "Main version: $version"

- name: Compare versions
env:
PR_VERSION: ${{ steps.pr.outputs.VERSION }}
MAIN_VERSION: ${{ steps.main.outputs.VERSION }}
run: |
echo "Main version: $MAIN_VERSION"
echo "PR version: $PR_VERSION"
if [ "$PR_VERSION" == "$MAIN_VERSION" ]; then
echo "::error::Version in Dashboard.csproj ($PR_VERSION) has not changed from main. Bump the version before merging to main."
exit 1
fi
echo "✅ Version bumped: $MAIN_VERSION → $PR_VERSION"
20 changes: 20 additions & 0 deletions Dashboard/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)

private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{
/* Silently swallow Hardcodet TrayToolTip race condition (issue #422).
The crash occurs in Popup.CreateWindow when showing the custom visual tooltip
and is harmless — the tooltip simply doesn't show that one time. */
if (IsTrayToolTipCrash(e.Exception))
{
Logger.Warning("Suppressed Hardcodet TrayToolTip crash (issue #422)");
e.Handled = true;
return;
}

Logger.Error("Unhandled Dispatcher Exception", e.Exception);

MessageBox.Show(
Expand All @@ -114,6 +124,16 @@ private void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEv
e.SetObserved(); // Prevent process termination
}

/// <summary>
/// Detects the Hardcodet TrayToolTip race condition crash (issue #422).
/// </summary>
private static bool IsTrayToolTipCrash(Exception ex)
{
return ex is ArgumentException
&& ex.Message.Contains("VisualTarget")
&& ex.StackTrace?.Contains("TaskbarIcon") == true;
}

private void CreateCrashDump(Exception? exception)
{
try
Expand Down
6 changes: 4 additions & 2 deletions Dashboard/Controls/PlanViewerControl.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,8 @@ private void ShowPropertiesPanel(PlanNode node)

// Header
var headerText = node.PhysicalOp;
if (node.LogicalOp != node.PhysicalOp && !string.IsNullOrEmpty(node.LogicalOp))
if (node.LogicalOp != node.PhysicalOp && !string.IsNullOrEmpty(node.LogicalOp)
&& !node.PhysicalOp.Contains(node.LogicalOp, StringComparison.OrdinalIgnoreCase))
headerText += $" ({node.LogicalOp})";
PropertiesHeader.Text = headerText;
PropertiesSubHeader.Text = $"Node ID: {node.NodeId}";
Expand Down Expand Up @@ -1481,7 +1482,8 @@ private ToolTip BuildNodeTooltip(PlanNode node)

// Header
var headerText = node.PhysicalOp;
if (node.LogicalOp != node.PhysicalOp && !string.IsNullOrEmpty(node.LogicalOp))
if (node.LogicalOp != node.PhysicalOp && !string.IsNullOrEmpty(node.LogicalOp)
&& !node.PhysicalOp.Contains(node.LogicalOp, StringComparison.OrdinalIgnoreCase))
headerText += $" ({node.LogicalOp})";
stack.Children.Add(new TextBlock
{
Expand Down
45 changes: 44 additions & 1 deletion Dashboard/Controls/ResourceMetricsContent.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,8 @@ private void SetupChartContextMenus()
TabHelpers.SetupChartContextMenu(PerfmonCountersChart, "Perfmon_Counters", "collect.perfmon_stats");

// Wait Stats Detail chart
TabHelpers.SetupChartContextMenu(WaitStatsDetailChart, "Wait_Stats_Detail", "collect.wait_stats");
var waitStatsMenu = TabHelpers.SetupChartContextMenu(WaitStatsDetailChart, "Wait_Stats_Detail", "collect.wait_stats");
AddWaitDrillDownMenuItem(WaitStatsDetailChart, waitStatsMenu);
}

/// <summary>
Expand Down Expand Up @@ -1813,6 +1814,48 @@ private async void WaitType_CheckChanged(object sender, RoutedEventArgs e)
await UpdateWaitStatsDetailChartAsync();
}

private void AddWaitDrillDownMenuItem(ScottPlot.WPF.WpfPlot chart, ContextMenu contextMenu)
{
contextMenu.Items.Insert(0, new Separator());
var drillDownItem = new MenuItem { Header = "Show Queries With This Wait" };
drillDownItem.Click += ShowQueriesForWaitType_Click;
contextMenu.Items.Insert(0, drillDownItem);

contextMenu.Opened += (s, _) =>
{
var pos = System.Windows.Input.Mouse.GetPosition(chart);
var nearest = _waitStatsHover?.GetNearestSeries(pos);
if (nearest.HasValue)
{
drillDownItem.Tag = (nearest.Value.Label, nearest.Value.Time);
drillDownItem.Header = $"Show Queries With {nearest.Value.Label.Replace("_", "__")}";
drillDownItem.IsEnabled = true;
}
else
{
drillDownItem.Tag = null;
drillDownItem.Header = "Show Queries With This Wait";
drillDownItem.IsEnabled = false;
}
};
}

private void ShowQueriesForWaitType_Click(object sender, RoutedEventArgs e)
{
if (sender is not MenuItem menuItem) return;
if (menuItem.Tag is not ValueTuple<string, DateTime> tag) return;
if (_databaseService == null) return;

// ±15 minute window around the clicked point
var fromDate = tag.Item2.AddMinutes(-15);
var toDate = tag.Item2.AddMinutes(15);

var window = new WaitDrillDownWindow(
_databaseService, tag.Item1, 1, fromDate, toDate);
window.Owner = Window.GetWindow(this);
window.ShowDialog();
}

private void WaitStatsMetric_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_allWaitStatsDetailData != null)
Expand Down
7 changes: 5 additions & 2 deletions Dashboard/Converters/QueryTextCleanupConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

namespace PerformanceMonitorDashboard.Converters
{
public class QueryTextCleanupConverter : IValueConverter
public partial class QueryTextCleanupConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
Expand All @@ -28,7 +28,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn
text = text.Replace("\t", " ", StringComparison.Ordinal);

// Replace multiple spaces with single space
text = Regex.Replace(text, @"\s+", " ");
text = MultipleSpacesRegExp().Replace(text, " ");

// Trim leading/trailing whitespace
text = text.Trim();
Expand All @@ -40,5 +40,8 @@ public object ConvertBack(object value, Type targetType, object parameter, Cultu
{
throw new NotImplementedException();
}

[GeneratedRegex(@"\s+")]
private static partial Regex MultipleSpacesRegExp();
}
}
8 changes: 4 additions & 4 deletions Dashboard/Dashboard.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
<UseWPF>true</UseWPF>
<AssemblyName>PerformanceMonitorDashboard</AssemblyName>
<Product>SQL Server Performance Monitor Dashboard</Product>
<Version>2.1.0</Version>
<AssemblyVersion>2.1.0.0</AssemblyVersion>
<FileVersion>2.1.0.0</FileVersion>
<InformationalVersion>2.1.0</InformationalVersion>
<Version>2.2.0</Version>
<AssemblyVersion>2.2.0.0</AssemblyVersion>
<FileVersion>2.2.0.0</FileVersion>
<InformationalVersion>2.2.0</InformationalVersion>
<Company>Darling Data, LLC</Company>
<Copyright>Copyright © 2026 Darling Data, LLC</Copyright>
<ApplicationIcon>EDD.ico</ApplicationIcon>
Expand Down
49 changes: 47 additions & 2 deletions Dashboard/Helpers/ChartHoverHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,50 @@ public ChartHoverHelper(ScottPlot.WPF.WpfPlot chart, string unit)
public void Add(ScottPlot.Plottables.Scatter scatter, string label) =>
_scatters.Add((scatter, label));

/// <summary>
/// Returns the nearest series label and data-point time for the given mouse position,
/// or null if no series is close enough.
/// </summary>
public (string Label, DateTime Time)? GetNearestSeries(Point mousePos)
{
if (_scatters.Count == 0) return null;
try
{
var dpi = VisualTreeHelper.GetDpi(_chart);
var pixel = new ScottPlot.Pixel(
(float)(mousePos.X * dpi.DpiScaleX),
(float)(mousePos.Y * dpi.DpiScaleY));
var mouseCoords = _chart.Plot.GetCoordinates(pixel);

double bestYDistance = double.MaxValue;
ScottPlot.DataPoint bestPoint = default;
string bestLabel = "";
bool found = false;

foreach (var (scatter, label) in _scatters)
{
var nearest = scatter.Data.GetNearest(mouseCoords, _chart.Plot.LastRender);
if (!nearest.IsReal) continue;
var nearestPixel = _chart.Plot.GetPixel(
new ScottPlot.Coordinates(nearest.X, nearest.Y));
double dx = Math.Abs(nearestPixel.X - pixel.X);
double dy = Math.Abs(nearestPixel.Y - pixel.Y);
if (dx < 80 && dy < bestYDistance)
{
bestYDistance = dy;
bestPoint = nearest;
bestLabel = label;
found = true;
}
}

if (found)
return (bestLabel, DateTime.FromOADate(bestPoint.X));
}
catch { }
return null;
}

private void OnMouseMove(object sender, MouseEventArgs e)
{
if (_scatters.Count == 0) return;
Expand All @@ -71,9 +115,10 @@ private void OnMouseMove(object sender, MouseEventArgs e)
try
{
var pos = e.GetPosition(_chart);
var dpi = VisualTreeHelper.GetDpi(_chart);
var pixel = new ScottPlot.Pixel(
(float)(pos.X * _chart.DisplayScale),
(float)(pos.Y * _chart.DisplayScale));
(float)(pos.X * dpi.DpiScaleX),
(float)(pos.Y * dpi.DpiScaleY));
var mouseCoords = _chart.Plot.GetCoordinates(pixel);

/* Use X-axis (time) proximity as the primary filter, Y-axis distance
Expand Down
7 changes: 5 additions & 2 deletions Dashboard/Helpers/DateFilterHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

namespace PerformanceMonitorDashboard.Helpers
{
public static class DateFilterHelper
public static partial class DateFilterHelper
{
public static bool MatchesFilter(object? value, string? filterText)
{
Expand Down Expand Up @@ -148,7 +148,7 @@ private static bool TryConvertToDateTime(object value, out DateTime result)
}

// "last N hours/days/weeks" expressions
var lastMatch = Regex.Match(expressionLower, @"last\s+(\d+)\s+(hour|hours|day|days|week|weeks|month|months)");
var lastMatch = LastNHoursDaysWeeksMonthsRegExp().Match(expressionLower);
if (lastMatch.Success)
{
int count = int.Parse(lastMatch.Groups[1].Value, CultureInfo.InvariantCulture);
Expand Down Expand Up @@ -231,5 +231,8 @@ private static bool IsRelativeExpression(string expression)
expression == "tomorrow" ||
Regex.IsMatch(expression, @"last\s+\d+\s+(hour|hours|day|days|week|weeks|month|months)");
}

[GeneratedRegex(@"last\s+(\d+)\s+(hour|hours|day|days|week|weeks|month|months)")]
private static partial Regex LastNHoursDaysWeeksMonthsRegExp();
}
}
4 changes: 3 additions & 1 deletion Dashboard/Helpers/TabHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -603,7 +603,7 @@ public static string FormatForExport(object? value)
/// <param name="chart">The WpfPlot chart control</param>
/// <param name="chartName">A descriptive name for the chart (used in filenames)</param>
/// <param name="dataSource">Optional SQL view/table name that populates this chart</param>
public static void SetupChartContextMenu(WpfPlot chart, string chartName, string? dataSource = null)
public static ContextMenu SetupChartContextMenu(WpfPlot chart, string chartName, string? dataSource = null)
{
var contextMenu = new ContextMenu();

Expand Down Expand Up @@ -786,6 +786,8 @@ public static void SetupChartContextMenu(WpfPlot chart, string chartName, string
chart.Plot.Axes.AutoScale();
chart.Refresh();
};

return contextMenu;
}

/// <summary>
Expand Down
Loading
Loading