-
Notifications
You must be signed in to change notification settings - Fork 30
Add growth rate and VLF count columns (#567) #625
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7fd0acc
42db655
b44d269
f0a09ee
2209b3d
28afa2f
68aba96
b919cbc
6b35145
9c7680a
2aced6b
79a0234
08a292c
6f14b44
d3e3c5e
c71cf65
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| /* | ||
| * 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.Linq; | ||
| using System.Windows; | ||
| using System.Windows.Controls; | ||
| using System.Windows.Media; | ||
| using PerformanceMonitorDashboard.Models; | ||
|
|
||
| namespace PerformanceMonitorDashboard.Services; | ||
|
|
||
| /// <summary> | ||
| /// Non-generic interface for looking up filter state from a shared dictionary. | ||
| /// </summary> | ||
| public interface IDataGridFilterManager | ||
| { | ||
| Dictionary<string, ColumnFilterState> Filters { get; } | ||
| void SetFilter(ColumnFilterState filterState); | ||
| void UpdateFilterButtonStyles(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Manages column filter state, unfiltered data capture, and filter application | ||
| /// for a single DataGrid. Eliminates per-grid boilerplate code. | ||
| /// </summary> | ||
| public class DataGridFilterManager<T> : IDataGridFilterManager | ||
| { | ||
| private readonly DataGrid _dataGrid; | ||
| private readonly Dictionary<string, ColumnFilterState> _filters = new(); | ||
| private List<T>? _unfilteredData; | ||
|
|
||
| public DataGridFilterManager(DataGrid dataGrid) | ||
| { | ||
| _dataGrid = dataGrid; | ||
| } | ||
|
|
||
| public Dictionary<string, ColumnFilterState> Filters => _filters; | ||
|
|
||
| /// <summary> | ||
| /// Called when new data arrives (refresh cycle). Captures unfiltered data, | ||
| /// then re-applies any active filters. | ||
| /// </summary> | ||
| public void UpdateData(List<T> newData) | ||
| { | ||
| _unfilteredData = newData; | ||
|
|
||
| if (!HasActiveFilters()) | ||
| { | ||
| _dataGrid.ItemsSource = newData; | ||
| return; | ||
| } | ||
|
|
||
| ApplyFilters(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Applies or removes a filter and re-filters the data. | ||
| /// </summary> | ||
| public void SetFilter(ColumnFilterState filterState) | ||
| { | ||
| if (filterState.IsActive) | ||
| _filters[filterState.ColumnName] = filterState; | ||
| else | ||
| _filters.Remove(filterState.ColumnName); | ||
|
|
||
| ApplyFilters(); | ||
| UpdateFilterButtonStyles(); | ||
| } | ||
|
|
||
| private bool HasActiveFilters() | ||
| { | ||
| return _filters.Count > 0 && _filters.Values.Any(f => f.IsActive); | ||
| } | ||
|
|
||
| private void ApplyFilters() | ||
| { | ||
| if (_unfilteredData == null) return; | ||
|
|
||
| if (!HasActiveFilters()) | ||
| { | ||
| _dataGrid.ItemsSource = _unfilteredData; | ||
| return; | ||
| } | ||
|
|
||
| var filteredData = _unfilteredData.Where(item => | ||
| { | ||
| foreach (var filter in _filters.Values) | ||
| { | ||
| if (filter.IsActive && !DataGridFilterService.MatchesFilter(item!, filter)) | ||
| return false; | ||
| } | ||
| return true; | ||
| }).ToList(); | ||
|
|
||
| _dataGrid.ItemsSource = filteredData; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Updates filter icon colors (gold when active, dim when inactive). | ||
| /// </summary> | ||
| public void UpdateFilterButtonStyles() | ||
| { | ||
| foreach (var column in _dataGrid.Columns) | ||
| { | ||
| if (column.Header is StackPanel headerPanel) | ||
| { | ||
| var filterButton = headerPanel.Children.OfType<Button>().FirstOrDefault(); | ||
| if (filterButton != null && filterButton.Tag is string columnName) | ||
| { | ||
| bool hasActive = _filters.TryGetValue(columnName, out var filter) && filter.IsActive; | ||
|
|
||
| var textBlock = new TextBlock | ||
| { | ||
| Text = hasActive ? "\uE16E" : "\uE71C", | ||
| FontFamily = new FontFamily("Segoe MDL2 Assets"), | ||
| Foreground = hasActive | ||
| ? new SolidColorBrush(Color.FromRgb(0xFF, 0xD7, 0x00)) | ||
| : (Brush)Application.Current.FindResource("ForegroundDimBrush") | ||
| }; | ||
| filterButton.Content = textBlock; | ||
|
|
||
| filterButton.ToolTip = hasActive && filter != null | ||
| ? $"Filter: {filter.DisplayText}\n(Click to modify)" | ||
| : "Click to filter"; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -322,7 +322,10 @@ public async Task<List<FinOpsDatabaseSizeStats>> GetFinOpsDatabaseSizeStatsAsync | |
| state_desc, | ||
| volume_mount_point, | ||
| volume_total_mb, | ||
| volume_free_mb | ||
| volume_free_mb, | ||
| is_percent_growth, | ||
| growth_pct, | ||
| vlf_count | ||
| FROM collect.database_size_stats | ||
| WHERE collection_time = | ||
| ( | ||
|
|
@@ -364,7 +367,10 @@ ORDER BY | |
| StateDesc = reader.IsDBNull(15) ? "" : reader.GetString(15), | ||
| VolumeMountPoint = reader.IsDBNull(16) ? "" : reader.GetString(16), | ||
| VolumeTotalMb = reader.IsDBNull(17) ? 0m : Convert.ToDecimal(reader.GetValue(17)), | ||
| VolumeFreeMb = reader.IsDBNull(18) ? 0m : Convert.ToDecimal(reader.GetValue(18)) | ||
| VolumeFreeMb = reader.IsDBNull(18) ? 0m : Convert.ToDecimal(reader.GetValue(18)), | ||
| IsPercentGrowth = reader.IsDBNull(19) ? null : (bool?)(Convert.ToInt32(reader.GetValue(19)) == 1), | ||
| GrowthPct = reader.IsDBNull(20) ? null : Convert.ToInt32(reader.GetValue(20)), | ||
| VlfCount = reader.IsDBNull(21) ? null : Convert.ToInt32(reader.GetValue(21)) | ||
| }); | ||
| } | ||
| } | ||
|
|
@@ -2540,9 +2546,32 @@ public class FinOpsDatabaseSizeStats | |
| public string VolumeMountPoint { get; set; } = ""; | ||
| public decimal VolumeTotalMb { get; set; } | ||
| public decimal VolumeFreeMb { get; set; } | ||
| public bool? IsPercentGrowth { get; set; } | ||
| public int? GrowthPct { get; set; } | ||
| public int? VlfCount { get; set; } | ||
|
|
||
| // FinOps cost — proportional share of server monthly budget | ||
| public decimal MonthlyCostShare { get; set; } | ||
|
|
||
| public string GrowthDisplay => IsPercentGrowth switch | ||
| { | ||
| null => "-", | ||
| true => GrowthPct.HasValue ? $"{GrowthPct}%" : "-", | ||
| false => AutoGrowthMb == 0 ? "Disabled" : $"{AutoGrowthMb:N0} MB" | ||
| }; | ||
|
Comment on lines
+2556
to
+2561
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: find . -type f -name "DatabaseService.FinOps.cs" | head -5Repository: erikdarlingdata/PerformanceMonitor Length of output: 124 🏁 Script executed: # Get file size first
wc -l ./Dashboard/Services/DatabaseService.FinOps.csRepository: erikdarlingdata/PerformanceMonitor Length of output: 129 🏁 Script executed: # Read the GrowthDisplay property and surrounding context
sed -n '2540,2580p' ./Dashboard/Services/DatabaseService.FinOps.csRepository: erikdarlingdata/PerformanceMonitor Length of output: 1757 🏁 Script executed: # Search for the model class definition and property declarations
rg "class.*Growth|GrowthPct|IsPercentGrowth|AutoGrowthMb" ./Dashboard/Services/DatabaseService.FinOps.cs -B 2 -A 2Repository: erikdarlingdata/PerformanceMonitor Length of output: 2840 🏁 Script executed: # Find the SQL query that fetches growth and is_percent_growth
rg "growth|is_percent_growth" ./Dashboard/Services/DatabaseService.FinOps.cs -B 3 -A 3 | head -100Repository: erikdarlingdata/PerformanceMonitor Length of output: 1646 🏁 Script executed: # Search for the query that selects from collect.database_size_stats or the underlying source
rg "FROM collect.database_size_stats|sys\.database_files|is_percent_growth" ./Dashboard/Services/DatabaseService.FinOps.cs -B 5 -A 20 | head -150Repository: erikdarlingdata/PerformanceMonitor Length of output: 5024 🏁 Script executed: # Look for SQL files in the repo that might contain the query definition
find . -type f \( -name "*.sql" -o -name "*.SQL" \) | head -20Repository: erikdarlingdata/PerformanceMonitor Length of output: 863 🏁 Script executed: # Search for any stored procedures or SQL files in Scripts or sql directories
find . -type d \( -name Scripts -o -name sql -o -name SQL \) 2>/dev/null | head -10Repository: erikdarlingdata/PerformanceMonitor Length of output: 91 🏁 Script executed: # Find the file that collects database size stats
grep -l "database_size_stats\|is_percent_growth\|growth_pct" ./install/*.sqlRepository: erikdarlingdata/PerformanceMonitor Length of output: 315 🏁 Script executed: # Check the delta framework file which likely contains collection logic
grep -l "growth" ./install/*.sqlRepository: erikdarlingdata/PerformanceMonitor Length of output: 517 🏁 Script executed: # Read the database size stats collection script
cat ./install/52_collect_database_size_stats.sql | head -300Repository: erikdarlingdata/PerformanceMonitor Length of output: 11573 🏁 Script executed: # Check if there's more content and look for the growth logic
wc -l ./install/52_collect_database_size_stats.sqlRepository: erikdarlingdata/PerformanceMonitor Length of output: 126 🏁 Script executed: # Read more of the SQL to see the rest of the growth_pct logic
sed -n '200,250p' ./install/52_collect_database_size_stats.sqlRepository: erikdarlingdata/PerformanceMonitor Length of output: 2166 🏁 Script executed: # Verify that GrowthPct can be 0 by checking the full property definitions one more time
sed -n '2550,2570p' ./Dashboard/Services/DatabaseService.FinOps.csRepository: erikdarlingdata/PerformanceMonitor Length of output: 869 Render disabled percent-growth files as SQL Server allows 🔧 Suggested fix public string GrowthDisplay => IsPercentGrowth switch
{
null => "-",
- true => GrowthPct.HasValue ? $"{GrowthPct}%" : "-",
+ true => !GrowthPct.HasValue ? "-"
+ : GrowthPct.Value == 0 ? "Disabled"
+ : $"{GrowthPct.Value}%",
false => AutoGrowthMb == 0 ? "Disabled" : $"{AutoGrowthMb:N0} MB"
};🤖 Prompt for AI Agents |
||
|
|
||
| public decimal AutoGrowthSort => IsPercentGrowth switch | ||
| { | ||
| null => -1m, | ||
| true => (decimal)(GrowthPct ?? -1), | ||
| false => AutoGrowthMb | ||
| }; | ||
|
|
||
| public string VlfCountDisplay => string.Equals(FileTypeDesc, "LOG", StringComparison.OrdinalIgnoreCase) | ||
| ? (VlfCount?.ToString() ?? "-") : "N/A"; | ||
|
|
||
| public int VlfCountSort => string.Equals(FileTypeDesc, "LOG", StringComparison.OrdinalIgnoreCase) | ||
| ? (VlfCount ?? 0) : -1; | ||
| } | ||
|
|
||
| public class FinOpsTopResourceConsumer | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -86,7 +86,7 @@ public void Dispose() | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Current schema version. Increment this when schema changes require table rebuilds. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| internal const int CurrentSchemaVersion = 21; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| internal const int CurrentSchemaVersion = 22; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private readonly string _archivePath; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -583,6 +583,22 @@ New tables only — no existing table changes needed. Tables created by | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _logger?.LogWarning("Migration to v21 encountered an error (non-fatal): {Error}", ex.Message); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (fromVersion < 22) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _logger?.LogInformation("Running migration to v22: adding growth rate and VLF count columns to database_size_stats"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await ExecuteNonQueryAsync(connection, "ALTER TABLE database_size_stats ADD COLUMN IF NOT EXISTS is_percent_growth BOOLEAN"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await ExecuteNonQueryAsync(connection, "ALTER TABLE database_size_stats ADD COLUMN IF NOT EXISTS growth_pct INTEGER"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await ExecuteNonQueryAsync(connection, "ALTER TABLE database_size_stats ADD COLUMN IF NOT EXISTS vlf_count INTEGER"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| catch (Exception ex) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _logger?.LogError(ex, "Migration to v22 failed"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+587
to
+601
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard v22 ALTERs when On upgrades from older schemas, Line 592 can run before 💡 Suggested fix if (fromVersion < 22)
{
_logger?.LogInformation("Running migration to v22: adding growth rate and VLF count columns to database_size_stats");
try
{
- await ExecuteNonQueryAsync(connection, "ALTER TABLE database_size_stats ADD COLUMN IF NOT EXISTS is_percent_growth BOOLEAN");
- await ExecuteNonQueryAsync(connection, "ALTER TABLE database_size_stats ADD COLUMN IF NOT EXISTS growth_pct INTEGER");
- await ExecuteNonQueryAsync(connection, "ALTER TABLE database_size_stats ADD COLUMN IF NOT EXISTS vlf_count INTEGER");
+ using var existsCmd = connection.CreateCommand();
+ existsCmd.CommandText = @"
+SELECT COUNT(*)
+FROM information_schema.tables
+WHERE table_name = 'database_size_stats'";
+ var tableExists = Convert.ToInt32(await existsCmd.ExecuteScalarAsync()) > 0;
+
+ if (tableExists)
+ {
+ await ExecuteNonQueryAsync(connection, "ALTER TABLE database_size_stats ADD COLUMN IF NOT EXISTS is_percent_growth BOOLEAN");
+ await ExecuteNonQueryAsync(connection, "ALTER TABLE database_size_stats ADD COLUMN IF NOT EXISTS growth_pct INTEGER");
+ await ExecuteNonQueryAsync(connection, "ALTER TABLE database_size_stats ADD COLUMN IF NOT EXISTS vlf_count INTEGER");
+ }
+ else
+ {
+ _logger?.LogDebug("Skipping v22 ALTERs because database_size_stats does not exist yet; it will be created during table initialization.");
+ }
}
catch (Exception ex)
{
_logger?.LogError(ex, "Migration to v22 failed");
throw;
}
}As per coding guidelines, 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Initialize
_dbSizesFilterMgrbefore first refresh path can use it.ServerSelector.SelectedIndex = 0(Line 93) can trigger data loading beforeOnLoadedruns. Then Line 634 dereferences_dbSizesFilterMgr!, which can be null on first load.Proposed fix
public FinOpsContent() { InitializeComponent(); + _dbSizesFilterMgr = new DataGridFilterManager<FinOpsDatabaseSizeStats>(DatabaseSizesDataGrid); Loaded += OnLoaded; } @@ - _dbSizesFilterMgr = new DataGridFilterManager<FinOpsDatabaseSizeStats>(DatabaseSizesDataGrid); + _dbSizesFilterMgr ??= new DataGridFilterManager<FinOpsDatabaseSizeStats>(DatabaseSizesDataGrid); @@ - _dbSizesFilterMgr!.UpdateData(data); + _dbSizesFilterMgr ??= new DataGridFilterManager<FinOpsDatabaseSizeStats>(DatabaseSizesDataGrid); + _dbSizesFilterMgr.UpdateData(data);As per coding guidelines, "
Dashboard/**/*.cs: ... Watch for: null reference risks ...".Also applies to: 634-635
🤖 Prompt for AI Agents