Skip to content

Commit d429e44

Browse files
Merge pull request #931 from erikdarlingdata/dev
Release v2.10.0 to main
2 parents 531dae3 + 2a3c10b commit d429e44

114 files changed

Lines changed: 38284 additions & 32288 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build.yml

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: Build
22

33
on:
44
push:
5-
branches: [main]
5+
branches: [main, dev]
66
pull_request:
77
branches: [main, dev]
88
release:
@@ -20,16 +20,62 @@ jobs:
2020
steps:
2121
- uses: actions/checkout@v4
2222

23+
- name: Detect changed paths
24+
id: filter
25+
if: github.event_name != 'release'
26+
uses: dorny/paths-filter@v3
27+
with:
28+
# On push events, compare against the previous commit on this branch
29+
# (github.event.before). Without this, the action defaults to comparing
30+
# against the default branch on non-default branch pushes, which would
31+
# match every accumulated change and defeat the filter.
32+
base: ${{ github.event_name == 'push' && github.event.before || '' }}
33+
filters: |
34+
lite_analysis:
35+
- 'Lite/Analysis/**'
36+
- 'Lite/Services/**'
37+
- 'Lite/DuckDb/**'
38+
- 'Lite/Models/**'
39+
- 'Lite.Tests/**'
40+
- 'Lite/PerformanceMonitorLite.csproj'
41+
installer:
42+
- 'Installer/**'
43+
- 'Installer.Core/**'
44+
- 'Installer.Tests/**'
45+
- 'install/**'
46+
- 'upgrades/**'
47+
2348
- name: Setup .NET 8.0
2449
uses: actions/setup-dotnet@v4
2550
with:
2651
dotnet-version: 8.0.x
52+
cache: true
53+
cache-dependency-path: '**/packages.lock.json'
2754

2855
- name: Restore dependencies
2956
run: |
30-
dotnet restore Dashboard/Dashboard.csproj
31-
dotnet restore Lite/PerformanceMonitorLite.csproj
32-
dotnet restore Installer/PerformanceMonitorInstaller.csproj
57+
dotnet restore Dashboard/Dashboard.csproj --locked-mode
58+
dotnet restore Lite/PerformanceMonitorLite.csproj --locked-mode
59+
dotnet restore Installer/PerformanceMonitorInstaller.csproj --locked-mode
60+
dotnet restore Lite.Tests/Lite.Tests.csproj --locked-mode
61+
dotnet restore Installer.Tests/Installer.Tests.csproj --locked-mode
62+
63+
- name: Build Lite.Tests
64+
run: dotnet build Lite.Tests/Lite.Tests.csproj -c Release --no-restore
65+
66+
- name: Build Installer.Tests
67+
run: dotnet build Installer.Tests/Installer.Tests.csproj -c Release --no-restore
68+
69+
- name: Run Lite fast tests
70+
run: dotnet test Lite.Tests/Lite.Tests.csproj -c Release --no-build --verbosity normal --filter "FullyQualifiedName!~AnomalyDetectorTests&FullyQualifiedName!~FactCollectorTests&FullyQualifiedName!~FactCollectorMiseryTests&FullyQualifiedName!~BaselineProviderTests&FullyQualifiedName!~InferenceEngineTests&FullyQualifiedName!~ScenarioTests&FullyQualifiedName!~AnalysisServiceTests"
71+
72+
- name: Run Lite analysis-heavy tests
73+
if: steps.filter.outputs.lite_analysis == 'true' || github.event_name == 'release'
74+
run: dotnet test Lite.Tests/Lite.Tests.csproj -c Release --no-build --verbosity normal --filter "FullyQualifiedName~AnomalyDetectorTests|FullyQualifiedName~FactCollectorTests|FullyQualifiedName~FactCollectorMiseryTests|FullyQualifiedName~BaselineProviderTests|FullyQualifiedName~InferenceEngineTests|FullyQualifiedName~ScenarioTests|FullyQualifiedName~AnalysisServiceTests"
75+
76+
- name: Run Installer tests
77+
if: steps.filter.outputs.installer == 'true' || github.event_name == 'release'
78+
run: dotnet test Installer.Tests/Installer.Tests.csproj -c Release --no-build --verbosity normal --filter "FullyQualifiedName!~VersionDetectionTests&FullyQualifiedName!~IdempotencyTests&FullyQualifiedName!~AdversarialTests"
3379

3480
- name: Get version
3581
id: version

.github/workflows/ci.yml

Lines changed: 0 additions & 36 deletions
This file was deleted.

.github/workflows/nightly.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ jobs:
4646
uses: actions/setup-dotnet@v4
4747
with:
4848
dotnet-version: 8.0.x
49+
cache: true
50+
cache-dependency-path: '**/packages.lock.json'
4951

5052
- name: Set nightly version
5153
id: version
@@ -59,10 +61,10 @@ jobs:
5961
6062
- name: Restore dependencies
6163
run: |
62-
dotnet restore Dashboard/Dashboard.csproj
63-
dotnet restore Lite/PerformanceMonitorLite.csproj
64-
dotnet restore Installer/PerformanceMonitorInstaller.csproj
65-
dotnet restore Lite.Tests/Lite.Tests.csproj
64+
dotnet restore Dashboard/Dashboard.csproj --locked-mode
65+
dotnet restore Lite/PerformanceMonitorLite.csproj --locked-mode
66+
dotnet restore Installer/PerformanceMonitorInstaller.csproj --locked-mode
67+
dotnet restore Lite.Tests/Lite.Tests.csproj --locked-mode
6668
6769
- name: Run tests
6870
run: dotnet test Lite.Tests/Lite.Tests.csproj -c Release --verbosity normal

CHANGELOG.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,22 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [2.9.0] - TBD
8+
## [2.10.0] - TBD
9+
10+
### Fixed
11+
12+
- **Memory tab tooltip** stops working after switching away and returning to the tab. Both Dashboard and Lite Memory tab crosshair tooltip handlers now reattach correctly on tab re-entry; the same popup-wedge fix is also applied to `CorrelatedCrosshairManager` ([#916])
13+
- **FinOps memory recommendation** now bases sizing on a 7-day P95 of memory samples instead of a single snapshot, so recommendations no longer swing based on instantaneous workload state. Applied in both Dashboard and Lite ([#917])
14+
15+
### Changed
16+
17+
- **Per-database grants for FinOps Index Analysis** documented in the README — sp_IndexCleanup-backed Index Analysis requires per-database `EXECUTE` grants on each user database you want to analyze ([#915])
18+
19+
[#915]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/915
20+
[#916]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/916
21+
[#917]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/917
22+
23+
## [2.9.0] - 2026-04-29
924

1025
### Important
1126

Dashboard/App.xaml.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,16 @@ protected override void OnExit(ExitEventArgs e)
8989
private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
9090
{
9191
var exception = e.ExceptionObject as Exception;
92+
93+
/* Silently swallow Hardcodet TrayToolTip race condition (issue #422) when it
94+
escapes the Dispatcher path — happens during tray-Exit shutdown when the
95+
Dispatcher's exception hooks are torn down before the tray library finishes. */
96+
if (exception != null && IsTrayToolTipCrash(exception))
97+
{
98+
Logger.Warning("Suppressed Hardcodet TrayToolTip crash (issue #422) in AppDomain handler");
99+
return;
100+
}
101+
92102
Logger.Fatal("Unhandled AppDomain Exception", exception ?? new Exception("Unknown exception"));
93103

94104
if (e.IsTerminating)
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
* Copyright (c) 2026 Erik Darling, Darling Data LLC
3+
*
4+
* This file is part of the SQL Server Performance Monitor.
5+
*
6+
* Licensed under the MIT License. See LICENSE file in the project root for full license information.
7+
*/
8+
9+
using System;
10+
using System.Collections.Generic;
11+
using System.Globalization;
12+
using System.IO;
13+
using System.Linq;
14+
using System.Text;
15+
using System.Threading.Tasks;
16+
using System.Windows;
17+
using System.Windows.Controls;
18+
using System.Windows.Controls.Primitives;
19+
using System.Windows.Data;
20+
using Microsoft.Win32;
21+
using PerformanceMonitorDashboard.Helpers;
22+
using PerformanceMonitorDashboard.Models;
23+
using PerformanceMonitorDashboard.Services;
24+
25+
namespace PerformanceMonitorDashboard.Controls
26+
{
27+
public partial class MemoryContent : UserControl
28+
{
29+
#region Context Menu Handlers
30+
31+
private void CopyCell_Click(object sender, RoutedEventArgs e)
32+
{
33+
if (sender is MenuItem menuItem && menuItem.Parent is ContextMenu contextMenu)
34+
{
35+
var dataGrid = TabHelpers.FindDataGridFromContextMenu(contextMenu);
36+
if (dataGrid != null && dataGrid.CurrentCell.Item != null)
37+
{
38+
var cellContent = TabHelpers.GetCellContent(dataGrid, dataGrid.CurrentCell);
39+
if (!string.IsNullOrEmpty(cellContent))
40+
{
41+
/* Use SetDataObject with copy=false to avoid WPF's problematic Clipboard.Flush() */
42+
Clipboard.SetDataObject(cellContent, false);
43+
}
44+
}
45+
}
46+
}
47+
48+
private void CopyRow_Click(object sender, RoutedEventArgs e)
49+
{
50+
if (sender is MenuItem menuItem && menuItem.Parent is ContextMenu contextMenu)
51+
{
52+
var dataGrid = TabHelpers.FindDataGridFromContextMenu(contextMenu);
53+
if (dataGrid != null && dataGrid.SelectedItem != null)
54+
{
55+
var rowText = TabHelpers.GetRowAsText(dataGrid, dataGrid.SelectedItem);
56+
/* Use SetDataObject with copy=false to avoid WPF's problematic Clipboard.Flush() */
57+
Clipboard.SetDataObject(rowText, false);
58+
}
59+
}
60+
}
61+
62+
private void CopyAllRows_Click(object sender, RoutedEventArgs e)
63+
{
64+
if (sender is MenuItem menuItem && menuItem.Parent is ContextMenu contextMenu)
65+
{
66+
var dataGrid = TabHelpers.FindDataGridFromContextMenu(contextMenu);
67+
if (dataGrid != null && dataGrid.Items.Count > 0)
68+
{
69+
var sb = new StringBuilder();
70+
71+
// Add headers
72+
var headers = new List<string>();
73+
foreach (var column in dataGrid.Columns)
74+
{
75+
if (column is DataGridBoundColumn)
76+
{
77+
headers.Add(Helpers.DataGridClipboardBehavior.GetHeaderText(column));
78+
}
79+
}
80+
sb.AppendLine(string.Join("\t", headers));
81+
82+
// Add all rows
83+
foreach (var item in dataGrid.Items)
84+
{
85+
sb.AppendLine(TabHelpers.GetRowAsText(dataGrid, item));
86+
}
87+
88+
/* Use SetDataObject with copy=false to avoid WPF's problematic Clipboard.Flush() */
89+
Clipboard.SetDataObject(sb.ToString(), false);
90+
}
91+
}
92+
}
93+
94+
private void ExportToCsv_Click(object sender, RoutedEventArgs e)
95+
{
96+
if (sender is MenuItem menuItem && menuItem.Parent is ContextMenu contextMenu)
97+
{
98+
var dataGrid = TabHelpers.FindDataGridFromContextMenu(contextMenu);
99+
if (dataGrid != null && dataGrid.Items.Count > 0)
100+
{
101+
string prefix = "memory";
102+
103+
104+
var saveFileDialog = new SaveFileDialog
105+
{
106+
FileName = $"{prefix}_{DateTime.Now:yyyyMMdd_HHmmss}.csv",
107+
DefaultExt = ".csv",
108+
Filter = "CSV Files (*.csv)|*.csv|All Files (*.*)|*.*"
109+
};
110+
111+
if (saveFileDialog.ShowDialog() == true)
112+
{
113+
try
114+
{
115+
var sb = new StringBuilder();
116+
117+
// Add headers
118+
var headers = new List<string>();
119+
foreach (var column in dataGrid.Columns)
120+
{
121+
if (column is DataGridBoundColumn)
122+
{
123+
headers.Add(TabHelpers.EscapeCsvField(Helpers.DataGridClipboardBehavior.GetHeaderText(column), TabHelpers.CsvSeparator));
124+
}
125+
}
126+
sb.AppendLine(string.Join(TabHelpers.CsvSeparator, headers));
127+
128+
// Add all rows
129+
foreach (var item in dataGrid.Items)
130+
{
131+
var values = TabHelpers.GetRowValues(dataGrid, item);
132+
sb.AppendLine(string.Join(TabHelpers.CsvSeparator, values.Select(v => TabHelpers.EscapeCsvField(v, TabHelpers.CsvSeparator))));
133+
}
134+
135+
File.WriteAllText(saveFileDialog.FileName, sb.ToString());
136+
MessageBox.Show($"Data exported successfully to:\n{saveFileDialog.FileName}", "Export Complete", MessageBoxButton.OK, MessageBoxImage.Information);
137+
}
138+
catch (Exception ex)
139+
{
140+
MessageBox.Show($"Error exporting data:\n\n{ex.Message}", "Export Error", MessageBoxButton.OK, MessageBoxImage.Error);
141+
}
142+
}
143+
}
144+
}
145+
}
146+
147+
#endregion
148+
}
149+
}

0 commit comments

Comments
 (0)