Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
1ab826d
Split Lite god-class files into partial classes
erikdarlingdata Apr 29, 2026
2fb8cd3
Split Dashboard god-class files into partial classes
erikdarlingdata Apr 29, 2026
95fc881
Merge pull request #912 from erikdarlingdata/feature/dashboard-file-s…
erikdarlingdata Apr 29, 2026
b146158
Split FinOps services and content code-behinds into partial classes
erikdarlingdata Apr 29, 2026
5025718
Extend issue #422 TrayToolTip suppression to AppDomain handler
erikdarlingdata Apr 29, 2026
8bde6fd
Merge pull request #913 from erikdarlingdata/feature/lite-dashboard-f…
erikdarlingdata Apr 29, 2026
9696ee8
Consolidate CI workflows: fold ci.yml's test step into build.yml
erikdarlingdata Apr 29, 2026
11b4336
Merge pull request #914 from erikdarlingdata/feature/consolidate-ci-w…
erikdarlingdata Apr 29, 2026
cf7519a
Fix #917 — base FinOps memory recommendation on 7-day P95, not a snap…
erikdarlingdata May 2, 2026
7cc2265
Merge pull request #918 from erikdarlingdata/feature/917-finops-memor…
erikdarlingdata May 2, 2026
d27bdba
Fix #916 — Memory tab tooltip stops working after returning to tab
erikdarlingdata May 2, 2026
4e58e8a
Merge pull request #919 from erikdarlingdata/feature/916-memory-tab-t…
erikdarlingdata May 2, 2026
6ce9e37
Fix #917 (Lite) — base FinOps memory recommendation on 7-day P95, not…
erikdarlingdata May 2, 2026
658a0ca
Merge pull request #920 from erikdarlingdata/feature/917-lite-finops-…
erikdarlingdata May 2, 2026
9581b80
Fix #916 (Lite) — Memory tab tooltip stops working after returning to…
erikdarlingdata May 2, 2026
b9361f3
Merge pull request #921 from erikdarlingdata/feature/916-lite-memory-…
erikdarlingdata May 2, 2026
5b833ef
Apply #916 popup-wedge fix to CorrelatedCrosshairManager (Dashboard +…
erikdarlingdata May 2, 2026
7410bc5
Merge pull request #922 from erikdarlingdata/feature/916-correlated-c…
erikdarlingdata May 2, 2026
56d8154
Fix CI: relax FinOps memory sample-count guard, seed enough rows in t…
erikdarlingdata May 2, 2026
76d825f
Merge pull request #923 from erikdarlingdata/fix/finops-memory-rec-te…
erikdarlingdata May 2, 2026
86e6258
Speed up Lite tests: enable parallelization + share DuckDB in FinOpsT…
erikdarlingdata May 2, 2026
987dc61
Merge pull request #924 from erikdarlingdata/feature/lite-tests-parallel
erikdarlingdata May 2, 2026
0312a2a
CI: enable NuGet caching via setup-dotnet to cut restore from ~60s
erikdarlingdata May 2, 2026
0f70036
Merge pull request #925 from erikdarlingdata/feature/ci-nuget-cache
erikdarlingdata May 2, 2026
e46a443
Revert "Merge pull request #924 from erikdarlingdata/feature/lite-tes…
erikdarlingdata May 2, 2026
62ef9d5
Merge pull request #926 from erikdarlingdata/revert/lite-tests-parallel
erikdarlingdata May 2, 2026
d9f24d4
CI: split tests into fast and analysis-heavy lanes
erikdarlingdata May 3, 2026
0b9c6f1
CI: skip SQL-dependent Installer tests
erikdarlingdata May 3, 2026
e74ad7a
Merge pull request #927 from erikdarlingdata/feature/ci-fast-test-lane
erikdarlingdata May 3, 2026
6a09ef5
CI: pin paths-filter base to previous commit on push events
erikdarlingdata May 3, 2026
b9b495a
Merge pull request #928 from erikdarlingdata/feature/ci-fix-paths-fil…
erikdarlingdata May 3, 2026
acefc56
Docs: per-database grants for FinOps Index Analysis (#915)
erikdarlingdata May 4, 2026
46814a3
Release: bump to 2.10.0
erikdarlingdata May 4, 2026
2a3c10b
Merge pull request #930 from erikdarlingdata/release/v2.10.0
erikdarlingdata May 4, 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
54 changes: 50 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Build

on:
push:
branches: [main]
branches: [main, dev]
pull_request:
branches: [main, dev]
release:
Expand All @@ -20,16 +20,62 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Detect changed paths
id: filter
if: github.event_name != 'release'
uses: dorny/paths-filter@v3
with:
# On push events, compare against the previous commit on this branch
# (github.event.before). Without this, the action defaults to comparing
# against the default branch on non-default branch pushes, which would
# match every accumulated change and defeat the filter.
base: ${{ github.event_name == 'push' && github.event.before || '' }}
filters: |
lite_analysis:
- 'Lite/Analysis/**'
- 'Lite/Services/**'
- 'Lite/DuckDb/**'
- 'Lite/Models/**'
- 'Lite.Tests/**'
- 'Lite/PerformanceMonitorLite.csproj'
installer:
- 'Installer/**'
- 'Installer.Core/**'
- 'Installer.Tests/**'
- 'install/**'
- 'upgrades/**'

- name: Setup .NET 8.0
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
cache: true
cache-dependency-path: '**/packages.lock.json'

- name: Restore dependencies
run: |
dotnet restore Dashboard/Dashboard.csproj
dotnet restore Lite/PerformanceMonitorLite.csproj
dotnet restore Installer/PerformanceMonitorInstaller.csproj
dotnet restore Dashboard/Dashboard.csproj --locked-mode
dotnet restore Lite/PerformanceMonitorLite.csproj --locked-mode
dotnet restore Installer/PerformanceMonitorInstaller.csproj --locked-mode
dotnet restore Lite.Tests/Lite.Tests.csproj --locked-mode
dotnet restore Installer.Tests/Installer.Tests.csproj --locked-mode

- name: Build Lite.Tests
run: dotnet build Lite.Tests/Lite.Tests.csproj -c Release --no-restore

- name: Build Installer.Tests
run: dotnet build Installer.Tests/Installer.Tests.csproj -c Release --no-restore

- name: Run Lite fast tests
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"

- name: Run Lite analysis-heavy tests
if: steps.filter.outputs.lite_analysis == 'true' || github.event_name == 'release'
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"

- name: Run Installer tests
if: steps.filter.outputs.installer == 'true' || github.event_name == 'release'
run: dotnet test Installer.Tests/Installer.Tests.csproj -c Release --no-build --verbosity normal --filter "FullyQualifiedName!~VersionDetectionTests&FullyQualifiedName!~IdempotencyTests&FullyQualifiedName!~AdversarialTests"

- name: Get version
id: version
Expand Down
36 changes: 0 additions & 36 deletions .github/workflows/ci.yml

This file was deleted.

10 changes: 6 additions & 4 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ jobs:
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
cache: true
cache-dependency-path: '**/packages.lock.json'

- name: Set nightly version
id: version
Expand All @@ -59,10 +61,10 @@ jobs:

- name: Restore dependencies
run: |
dotnet restore Dashboard/Dashboard.csproj
dotnet restore Lite/PerformanceMonitorLite.csproj
dotnet restore Installer/PerformanceMonitorInstaller.csproj
dotnet restore Lite.Tests/Lite.Tests.csproj
dotnet restore Dashboard/Dashboard.csproj --locked-mode
dotnet restore Lite/PerformanceMonitorLite.csproj --locked-mode
dotnet restore Installer/PerformanceMonitorInstaller.csproj --locked-mode
dotnet restore Lite.Tests/Lite.Tests.csproj --locked-mode

- name: Run tests
run: dotnet test Lite.Tests/Lite.Tests.csproj -c Release --verbosity normal
Expand Down
17 changes: 16 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,22 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.9.0] - TBD
## [2.10.0] - TBD

### Fixed

- **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])
- **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])

### Changed

- **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])

[#915]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/915
[#916]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/916
[#917]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/917

## [2.9.0] - 2026-04-29

### Important

Expand Down
10 changes: 10 additions & 0 deletions Dashboard/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,16 @@ protected override void OnExit(ExitEventArgs e)
private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
{
var exception = e.ExceptionObject as Exception;

/* Silently swallow Hardcodet TrayToolTip race condition (issue #422) when it
escapes the Dispatcher path — happens during tray-Exit shutdown when the
Dispatcher's exception hooks are torn down before the tray library finishes. */
if (exception != null && IsTrayToolTipCrash(exception))
{
Logger.Warning("Suppressed Hardcodet TrayToolTip crash (issue #422) in AppDomain handler");
return;
}

Logger.Fatal("Unhandled AppDomain Exception", exception ?? new Exception("Unknown exception"));

if (e.IsTerminating)
Expand Down
149 changes: 149 additions & 0 deletions Dashboard/Controls/MemoryContent.CopyExport.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
* 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;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using Microsoft.Win32;
using PerformanceMonitorDashboard.Helpers;
using PerformanceMonitorDashboard.Models;
using PerformanceMonitorDashboard.Services;

namespace PerformanceMonitorDashboard.Controls
{
public partial class MemoryContent : UserControl
{
#region Context Menu Handlers

private void CopyCell_Click(object sender, RoutedEventArgs e)
{
if (sender is MenuItem menuItem && menuItem.Parent is ContextMenu contextMenu)
{
var dataGrid = TabHelpers.FindDataGridFromContextMenu(contextMenu);
if (dataGrid != null && dataGrid.CurrentCell.Item != null)
{
var cellContent = TabHelpers.GetCellContent(dataGrid, dataGrid.CurrentCell);
if (!string.IsNullOrEmpty(cellContent))
{
/* Use SetDataObject with copy=false to avoid WPF's problematic Clipboard.Flush() */
Clipboard.SetDataObject(cellContent, false);
}
}
}
}

private void CopyRow_Click(object sender, RoutedEventArgs e)
{
if (sender is MenuItem menuItem && menuItem.Parent is ContextMenu contextMenu)
{
var dataGrid = TabHelpers.FindDataGridFromContextMenu(contextMenu);
if (dataGrid != null && dataGrid.SelectedItem != null)
{
var rowText = TabHelpers.GetRowAsText(dataGrid, dataGrid.SelectedItem);
/* Use SetDataObject with copy=false to avoid WPF's problematic Clipboard.Flush() */
Clipboard.SetDataObject(rowText, false);
}
}
}

private void CopyAllRows_Click(object sender, RoutedEventArgs e)
{
if (sender is MenuItem menuItem && menuItem.Parent is ContextMenu contextMenu)
{
var dataGrid = TabHelpers.FindDataGridFromContextMenu(contextMenu);
if (dataGrid != null && dataGrid.Items.Count > 0)
{
var sb = new StringBuilder();

// Add headers
var headers = new List<string>();
foreach (var column in dataGrid.Columns)
{
if (column is DataGridBoundColumn)
{
headers.Add(Helpers.DataGridClipboardBehavior.GetHeaderText(column));
}
}
sb.AppendLine(string.Join("\t", headers));

// Add all rows
foreach (var item in dataGrid.Items)
{
sb.AppendLine(TabHelpers.GetRowAsText(dataGrid, item));
}

/* Use SetDataObject with copy=false to avoid WPF's problematic Clipboard.Flush() */
Clipboard.SetDataObject(sb.ToString(), false);
}
}
}

private void ExportToCsv_Click(object sender, RoutedEventArgs e)
{
if (sender is MenuItem menuItem && menuItem.Parent is ContextMenu contextMenu)
{
var dataGrid = TabHelpers.FindDataGridFromContextMenu(contextMenu);
if (dataGrid != null && dataGrid.Items.Count > 0)
{
string prefix = "memory";


var saveFileDialog = new SaveFileDialog
{
FileName = $"{prefix}_{DateTime.Now:yyyyMMdd_HHmmss}.csv",
DefaultExt = ".csv",
Filter = "CSV Files (*.csv)|*.csv|All Files (*.*)|*.*"
};

if (saveFileDialog.ShowDialog() == true)
{
try
{
var sb = new StringBuilder();

// Add headers
var headers = new List<string>();
foreach (var column in dataGrid.Columns)
{
if (column is DataGridBoundColumn)
{
headers.Add(TabHelpers.EscapeCsvField(Helpers.DataGridClipboardBehavior.GetHeaderText(column), TabHelpers.CsvSeparator));
}
}
sb.AppendLine(string.Join(TabHelpers.CsvSeparator, headers));

// Add all rows
foreach (var item in dataGrid.Items)
{
var values = TabHelpers.GetRowValues(dataGrid, item);
sb.AppendLine(string.Join(TabHelpers.CsvSeparator, values.Select(v => TabHelpers.EscapeCsvField(v, TabHelpers.CsvSeparator))));
}

File.WriteAllText(saveFileDialog.FileName, sb.ToString());
MessageBox.Show($"Data exported successfully to:\n{saveFileDialog.FileName}", "Export Complete", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
MessageBox.Show($"Error exporting data:\n\n{ex.Message}", "Export Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
}
}

#endregion
}
}
Loading
Loading