From 5ac4a9962e66351830f5ceaa9b0a1e6c000bbf93 Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Wed, 30 Sep 2026 18:16:01 -0400 Subject: [PATCH 1/3] Database Sizes: show the Hyperscale log file as n/a (log service) and leave it out of allocated totals On Azure SQL Database Hyperscale, sys.database_files reports the LOG file at about 1 TB (1,046,528 MB) while the data file reports its real allocation. The log lives in Hyperscale's log service, so that figure is not storage the database holds or pays for (Hyperscale bills allocated data storage). Database Sizes in both apps showed the database at 1,056,768 MB allocated against 315 MB used. The Azure SQL Database branch of the database_size_stats collector now detects Hyperscale from DATABASEPROPERTYEX(DB_NAME(), 'Edition') and stores a NULL size, growth and ceiling for the LOG row only. The data file keeps its size, and used space and the VLF count stay as collected. SQL Server, Managed Instance and non-Hyperscale Azure SQL Database are unchanged. Lite's DuckDB store drops NOT NULL from database_size_stats.total_size_mb (schema v67); Darling's Postgres store already held it nullable. Every reader in both apps passes the NULL through instead of reading it as 0: the Database Sizes grid words it as n/a (log service) and sorts it last, the allocated and free totals, the cost share and the utilization chart leave it out, and get_database_sizes returns null with a short note in both apps. The web Database Sizes table shows that note. --- .../DatabaseSizeLatestPlanShapeLiveTests.cs | 2 +- .../Darling.Tests/HyperscaleLogSizeTests.cs | 189 ++++++++++ .../Mcp/DarlingMcpObjectStatsTools.cs | 81 ++-- .../Mcp/DarlingObjectStatsReader.cs | 8 +- .../wwwroot/js/pages/server-tabs.js | 5 +- .../FinOpsTab.Loaders.cs | 9 +- .../FinOpsTab.xaml | 2 +- ...iewerDataService.FinOps.Recommendations.cs | 2 +- .../ViewerDataService.FinOps.Storage.cs | 10 +- .../ViewerDataService.FinOps.cs | 22 +- Lite.Tests/AgedDatabaseMigrationTests.cs | 49 +++ Lite.Tests/DuckDbSchemaEquivalenceTests.cs | 7 + Lite.Tests/HyperscaleLogSizeTests.cs | 354 ++++++++++++++++++ Lite/Controls/FinOpsTab.xaml | 3 +- Lite/Controls/FinOpsTab.xaml.cs | 9 +- Lite/Database/DuckDbInitializer.cs | 40 +- Lite/Database/DuckDbSchemaGenerator.cs | 7 +- Lite/Mcp/McpServerInfoTools.cs | 80 ++-- .../LocalDataService.FinOps.Inventory.cs | 8 +- ...LocalDataService.FinOps.Recommendations.cs | 2 +- Lite/Services/LocalDataService.FinOps.cs | 22 +- Lite/Services/LocalDataService.ServerInfo.cs | 16 +- .../DatabaseSizeStatsCollector.cs | 67 +++- .../HyperscaleLogSize.cs | 32 ++ 24 files changed, 936 insertions(+), 90 deletions(-) create mode 100644 Darling/Darling.Tests/HyperscaleLogSizeTests.cs create mode 100644 Lite.Tests/HyperscaleLogSizeTests.cs create mode 100644 PerformanceMonitor.Common/HyperscaleLogSize.cs diff --git a/Darling/Darling.Tests/DatabaseSizeLatestPlanShapeLiveTests.cs b/Darling/Darling.Tests/DatabaseSizeLatestPlanShapeLiveTests.cs index 6928c21fc..a2b2c46fe 100644 --- a/Darling/Darling.Tests/DatabaseSizeLatestPlanShapeLiveTests.cs +++ b/Darling/Darling.Tests/DatabaseSizeLatestPlanShapeLiveTests.cs @@ -208,7 +208,7 @@ private static async Task> ReadOldLatestAsync(NpgsqlConnection conn private static List FormatViewerLatest(List rows) => rows.Select(r => string.Join("|", r.DatabaseName, r.FileTypeDesc, r.FileName, - r.TotalSizeMb.ToString(CultureInfo.InvariantCulture), + r.TotalSizeMb?.ToString(CultureInfo.InvariantCulture) ?? "", r.UsedSizeMb?.ToString(CultureInfo.InvariantCulture) ?? "", r.VolumeMountPoint ?? "", r.VolumeTotalMb?.ToString(CultureInfo.InvariantCulture) ?? "", diff --git a/Darling/Darling.Tests/HyperscaleLogSizeTests.cs b/Darling/Darling.Tests/HyperscaleLogSizeTests.cs new file mode 100644 index 000000000..6e3c24ef5 --- /dev/null +++ b/Darling/Darling.Tests/HyperscaleLogSizeTests.cs @@ -0,0 +1,189 @@ +/* + * 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.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.RegularExpressions; +using PerformanceMonitor.Collectors; +using PerformanceMonitor.Common; +using PerformanceMonitor.Darling.Service.Mcp; +using PerformanceMonitor.Darling.Storage; +using PerformanceMonitor.Darling.Viewer; +using Xunit; + +namespace Darling.Tests; + +/// +/// Azure SQL Database Hyperscale keeps its transaction log in the log service. sys.database_files still lists a +/// LOG row for it, sized at about 1 TB (1,046,528 MB), which is not storage the database holds or pays for: Hyperscale +/// bills allocated data storage. The rule these pins hold, in Darling's half: the collector (shared with Lite) stores a +/// NULL size for that one row, the Postgres store keeps it NULL, every reader shows it as "n/a (log service)", and it +/// stays out of every allocated total. The data file keeps its real size. SQL Server, Managed Instance and +/// non-Hyperscale Azure SQL Database are unchanged. Lite's HyperscaleLogSize*Tests are the twins. +/// +public sealed class HyperscaleLogSizeTests +{ + private static CollectorContext Context(bool azure) => new() + { + ServerId = 42, + ServerName = "test-server", + CollectionTime = new DateTime(2026, 9, 30, 12, 0, 0, DateTimeKind.Utc), + Deltas = new CollectorDeltaCalculator(), + Target = new CollectorTargetInfo { IsAzureSqlDb = azure }, + }; + + [Fact] + public void AzureSqlDbQuery_NullsTheSizeOfTheHyperscaleLogRowOnly_AndKeepsTheRealSizeOtherwise() + { + var text = DatabaseSizeStatsCollector.Instance.BuildQuery(Context(azure: true)).Text; + + Assert.Contains("CONVERT(nvarchar(64), DATABASEPROPERTYEX(DB_NAME(), N'Edition')) = N'Hyperscale'", text, StringComparison.Ordinal); + Assert.Matches(new Regex(@"is_log_service =\s*CASE\s*WHEN @is_hyperscale = 1\s*AND\s+df\.type = 1 /\*LOG\*/\s*THEN CONVERT\(bit, 1\)\s*ELSE CONVERT\(bit, 0\)"), text); + Assert.Matches(new Regex(@"total_size_mb =\s*CASE\s*WHEN ls\.is_log_service = 1\s*THEN CONVERT\(decimal\(19,2\), NULL\)\s*ELSE CONVERT\(decimal\(19,2\), df\.size \* 8\.0 / 1024\.0\)\s*END"), text); + Assert.Matches(new Regex(@"max_size_mb =\s*CASE\s*WHEN ls\.is_log_service = 1\s*THEN CONVERT\(decimal\(19,2\), NULL\)"), text); + Assert.Matches(new Regex(@"auto_growth_mb =\s*CASE\s*WHEN ls\.is_log_service = 1\s*THEN CONVERT\(decimal\(19,2\), NULL\)"), text); + Assert.Contains("CONVERT(decimal(19,2), FILEPROPERTY(df.name, N'SpaceUsed') * 8.0 / 1024.0)", text, StringComparison.Ordinal); + } + + [Fact] + public void OnPremQuery_IsUnchanged_NoHyperscaleBranch() + { + var text = DatabaseSizeStatsCollector.Instance.BuildQuery(Context(azure: false)).Text; + + Assert.DoesNotContain("Hyperscale", text, StringComparison.Ordinal); + Assert.DoesNotContain("is_log_service", text, StringComparison.Ordinal); + Assert.Contains("CONVERT(decimal(19,2), df.size * 8.0 / 1024.0)", text, StringComparison.Ordinal); + } + + [Fact] + public void PostgresStore_KeepsTotalSizeNullable() + { + /* Postgres emits every payload column nullable, so the NULL size needs no migration here; this holds it + that way (Lite's DuckDB store needed its v67 rung for the same row). */ + var ddl = PgSchemaGenerator.CreateTable(DatabaseSizeStatsCollector.Instance); + + var line = ddl.Split('\n').Single(l => l.TrimStart().StartsWith("total_size_mb ", StringComparison.Ordinal)); + Assert.DoesNotContain("NOT NULL", line, StringComparison.Ordinal); + } + + [Fact] + public void ViewerRow_HyperscaleLogRow_ReadsAsNotApplicable_AndOutOfTheAllocatedTotals_WhileTheDataRowCounts() + { + var rows = new List + { + new() { DatabaseName = "hsdb", FileTypeDesc = "ROWS", FileName = "hsdb_data", TotalSizeMb = 10_240m, UsedSizeMb = 315m }, + new() { DatabaseName = "hsdb", FileTypeDesc = "LOG", FileName = "hsdb_log", TotalSizeMb = null, UsedSizeMb = 40m }, + }; + + Assert.Null(rows[1].FreeSpaceMb); + Assert.Null(rows[1].UsedPct); + Assert.Equal(40m, rows[1].UsedSizeMb); + + Assert.Equal(10_240m, DatabaseSizeRow.AllocatedTotalMb(rows)); + Assert.Equal(10_240m - 315m, DatabaseSizeRow.FreeTotalMb(rows)); + + /* Unchanged: a non-Hyperscale database (or a SQL Server one) has a real size on its log row, which counts. */ + var plain = new List + { + new() { FileTypeDesc = "ROWS", TotalSizeMb = 100m, UsedSizeMb = 10m }, + new() { FileTypeDesc = "LOG", TotalSizeMb = 50m, UsedSizeMb = 5m }, + }; + Assert.Equal(150m, DatabaseSizeRow.AllocatedTotalMb(plain)); + Assert.Equal(135m, DatabaseSizeRow.FreeTotalMb(plain)); + Assert.Equal(10.0m, plain[0].UsedPct); + } + + [Fact] + public void ViewerGrid_WordsANullSize_AsTheSharedHyperscaleText_AndSortsNullSizesLast() + { + Assert.Equal("n/a (log service)", HyperscaleLogSize.Display); + + var xaml = File.ReadAllText(Path.Combine(RepoRoot(), "Darling", "PerformanceMonitor.Darling.Viewer", "FinOpsTab.xaml")); + Assert.Contains("{Binding TotalSizeMb, StringFormat=N2, TargetNullValue='" + HyperscaleLogSize.Display + "'}", xaml, StringComparison.Ordinal); + + /* Postgres sorts NULL first under DESC, which would put the log-service row at the top of a grid whose + lead is the biggest files. */ + Assert.Contains("ORDER BY total_size_mb DESC NULLS LAST,", ViewerDataService.DatabaseSizeLatestSql, StringComparison.Ordinal); + } + + [Fact] + public void ViewerUtilizationChart_UsedIsSummedOnlyOverTheFilesWhoseSizeCounts() + { + Assert.Contains("SUM(CASE WHEN total_size_mb IS NOT NULL THEN used_size_mb END) AS used_mb", ViewerDataService.DatabaseSizeSummarySql, StringComparison.Ordinal); + } + + private static DarlingObjectStatsReader.DatabaseSizeRow SizeRow(string database, string name, string fileType, double? total, double used, double? growth, double? max) => + new(new DateTime(2026, 9, 30, 12, 0, 0, DateTimeKind.Utc), database, name, fileType, total, used, growth, max, null, null, null); + + [Fact] + public void GetDatabaseSizesPayload_HyperscaleLogFile_IsNullWithTheNote_AndOutOfTheDatabaseTotal() + { + var rows = new[] + { + SizeRow("hsdb", "hsdb_data", "ROWS", 10_240, 315, 1_024, 4_194_304), + SizeRow("hsdb", "hsdb_log", "LOG", null, 40, null, null), + SizeRow("plain", "plain_data", "ROWS", 100, 10, 64, -1), + SizeRow("plain", "plain_log", "LOG", 50, 5, 64, 2_097_152), + }; + + using var doc = JsonDocument.Parse(DarlingMcpObjectStatsTools.DatabaseSizesPayload("srv", rows)); + var root = doc.RootElement; + + Assert.Equal(HyperscaleLogSize.Note, root.GetProperty("note").GetString()); + + var hs = root.GetProperty("databases").EnumerateArray().Single(d => d.GetProperty("database_name").GetString() == "hsdb"); + Assert.Equal(10_240d, hs.GetProperty("total_size_mb").GetDouble()); + var hsLog = hs.GetProperty("files").EnumerateArray().Single(f => f.GetProperty("file_type").GetString() == "LOG"); + Assert.Equal(JsonValueKind.Null, hsLog.GetProperty("total_size_mb").ValueKind); + Assert.Equal(JsonValueKind.Null, hsLog.GetProperty("auto_growth_mb").ValueKind); + Assert.Equal(JsonValueKind.Null, hsLog.GetProperty("max_size_mb").ValueKind); + Assert.Equal(40d, hsLog.GetProperty("used_size_mb").GetDouble()); + var hsData = hs.GetProperty("files").EnumerateArray().Single(f => f.GetProperty("file_type").GetString() == "ROWS"); + Assert.Equal(10_240d, hsData.GetProperty("total_size_mb").GetDouble()); + + /* Unchanged: a database whose log row carries a real size counts it. */ + var plain = root.GetProperty("databases").EnumerateArray().Single(d => d.GetProperty("database_name").GetString() == "plain"); + Assert.Equal(150d, plain.GetProperty("total_size_mb").GetDouble()); + } + + [Fact] + public void GetDatabaseSizesPayload_NoLogServiceFile_HasNoNote_AndTheSameShapeAsBefore() + { + var rows = new[] + { + SizeRow("plain", "plain_data", "ROWS", 100, 10, 64, -1), + SizeRow("plain", "plain_log", "LOG", 50, 5, 64, 2_097_152), + }; + + using var doc = JsonDocument.Parse(DarlingMcpObjectStatsTools.DatabaseSizesPayload("srv", rows)); + var root = doc.RootElement; + + Assert.False(root.TryGetProperty("note", out _)); + Assert.Equal(new[] { "server", "captured_at", "file_count", "databases" }, root.EnumerateObject().Select(p => p.Name).ToArray()); + Assert.Equal(150d, root.GetProperty("databases")[0].GetProperty("total_size_mb").GetDouble()); + } + + [Fact] + public void WebDatabaseSizesTable_RendersTheReadsNote() + { + var js = File.ReadAllText(Path.Combine(RepoRoot(), "Darling", "PerformanceMonitor.Darling.Service", "wwwroot", "js", "pages", "server-tabs.js")); + + /* The table() helper's ninth argument is the noteKey: the read's own note rides above the rows. */ + var block = js.Substring(js.IndexOf("\"Database Sizes\",", StringComparison.Ordinal)); + block = block.Substring(0, block.IndexOf("),", StringComparison.Ordinal)); + Assert.Contains("\"get_database_sizes\"", block, StringComparison.Ordinal); + Assert.Matches(new Regex("1,\\s*\"note\""), block); + } + + private static string RepoRoot([CallerFilePath] string thisFile = "") + => Path.GetFullPath(Path.Combine(Path.GetDirectoryName(thisFile)!, "..", "..")); +} diff --git a/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpObjectStatsTools.cs b/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpObjectStatsTools.cs index 419f4e06b..702a8e8dd 100644 --- a/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpObjectStatsTools.cs +++ b/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpObjectStatsTools.cs @@ -341,38 +341,71 @@ public static async Task GetDatabaseSizes( return await DarlingEngineCapability.NotCollectedStatusAsync(postgres, resolved.ServerId, resolved.ServerName, "database_size_stats", cancellationToken) ?? McpHelpers.Status("unavailable", "No database size data available. The size collector may not have run yet."); + return DatabaseSizesPayload(resolved.ServerName, rows); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + return McpHelpers.FormatError("get_database_sizes", ex); + } + } + + /// + /// The get_database_sizes payload, shaped apart from the read so it can be pinned without a store. A file with + /// no allocated size (the LOG file of an Azure SQL Database Hyperscale database, which lives in the log + /// service) keeps a null total_size_mb and adds nothing to its database's total; the payload then + /// carries . Lite's twin gives the same words and shape. + /// + internal static string DatabaseSizesPayload(string serverName, IReadOnlyList rows) + { + var databases = rows + .GroupBy(r => r.DatabaseName) + .Select(g => new + { + database_name = g.Key, + /* A file with no allocated size (the Hyperscale log file, in the log service) adds nothing, + so a Hyperscale database's total is its data file alone. */ + total_size_mb = g.Sum(r => r.TotalSizeMb ?? 0), + used_size_mb = g.Sum(r => r.UsedSizeMb ?? 0), + files = g.Select(r => new + { + file_name = r.FileName, + file_type = r.FileTypeDesc, + total_size_mb = r.TotalSizeMb, + used_size_mb = r.UsedSizeMb, + auto_growth_mb = r.AutoGrowthMb, + max_size_mb = r.MaxSizeMb, + volume_mount_point = r.VolumeMountPoint, + volume_total_mb = r.VolumeTotalMb, + volume_free_mb = r.VolumeFreeMb + }) + }) + .ToList(); + + /* A null total_size_mb is the Hyperscale log file, whose size is n/a (log service) rather than a + storage figure. The note rides only on a payload that has one, so every other server's shape is + unchanged. Lite's get_database_sizes says the same, in the same words. */ + if (rows.Any(r => r.TotalSizeMb is null)) + { return JsonSerializer.Serialize(new { - server = resolved.ServerName, + server = serverName, /* #3653: captured_at, the #3637 census's one spelling for a latest read's stamp - see DarlingMcpDataTools.GetServerProperties for why it is a cut-over and not an alias. */ captured_at = rows[0].CollectionTime.ToString("o"), file_count = rows.Count, - databases = rows - .GroupBy(r => r.DatabaseName) - .Select(g => new - { - database_name = g.Key, - total_size_mb = g.Sum(r => r.TotalSizeMb), - used_size_mb = g.Sum(r => r.UsedSizeMb ?? 0), - files = g.Select(r => new - { - file_name = r.FileName, - file_type = r.FileTypeDesc, - total_size_mb = r.TotalSizeMb, - used_size_mb = r.UsedSizeMb, - auto_growth_mb = r.AutoGrowthMb, - max_size_mb = r.MaxSizeMb, - volume_mount_point = r.VolumeMountPoint, - volume_total_mb = r.VolumeTotalMb, - volume_free_mb = r.VolumeFreeMb - }) - }) + note = HyperscaleLogSize.Note, + databases }, McpHelpers.JsonOptions); } - catch (Exception ex) when (ex is not OperationCanceledException) + + return JsonSerializer.Serialize(new { - return McpHelpers.FormatError("get_database_sizes", ex); - } + server = serverName, + /* #3653: captured_at, the #3637 census's one spelling for a latest read's stamp - see + DarlingMcpDataTools.GetServerProperties for why it is a cut-over and not an alias. */ + captured_at = rows[0].CollectionTime.ToString("o"), + file_count = rows.Count, + databases + }, McpHelpers.JsonOptions); } } diff --git a/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingObjectStatsReader.cs b/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingObjectStatsReader.cs index 9c831dee8..4220abf5f 100644 --- a/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingObjectStatsReader.cs +++ b/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingObjectStatsReader.cs @@ -110,9 +110,10 @@ public sealed record IndexLockingRow( double ReservedMb, long TotalRows, long RowLockWaitCount, long RowLockWaitInMs, long PageLockWaitCount, long PageLockWaitInMs, long IndexLockPromotionCount, long PageLatchWaitInMs, long PageIoLatchWaitInMs); - /// One database file's latest size snapshot. + /// One database file's latest size snapshot. TotalSizeMb is null for the LOG file of an Azure SQL + /// Database Hyperscale database (the log service): see . public sealed record DatabaseSizeRow( - DateTime CollectionTime, string DatabaseName, string? FileName, string? FileTypeDesc, double TotalSizeMb, + DateTime CollectionTime, string DatabaseName, string? FileName, string? FileTypeDesc, double? TotalSizeMb, double? UsedSizeMb, double? AutoGrowthMb, double? MaxSizeMb, string? VolumeMountPoint, double? VolumeTotalMb, double? VolumeFreeMb); /* ─────────────────────────── table / index sizes + growth ─────────────────────────── */ @@ -558,7 +559,8 @@ public static async Task> GetLatestDatabaseSizesAsync( reader.IsDBNull(1) ? "" : reader.GetString(1), reader.IsDBNull(2) ? null : reader.GetString(2), reader.IsDBNull(3) ? null : reader.GetString(3), - reader.IsDBNull(4) ? 0 : reader.GetDouble(4), + /* NULL is the Hyperscale log file: it stays null, never 0. */ + reader.IsDBNull(4) ? null : reader.GetDouble(4), reader.IsDBNull(5) ? null : reader.GetDouble(5), reader.IsDBNull(6) ? null : reader.GetDouble(6), reader.IsDBNull(7) ? null : reader.GetDouble(7), diff --git a/Darling/PerformanceMonitor.Darling.Service/wwwroot/js/pages/server-tabs.js b/Darling/PerformanceMonitor.Darling.Service/wwwroot/js/pages/server-tabs.js index 2e948c309..c68707051 100644 --- a/Darling/PerformanceMonitor.Darling.Service/wwwroot/js/pages/server-tabs.js +++ b/Darling/PerformanceMonitor.Darling.Service/wwwroot/js/pages/server-tabs.js @@ -1039,6 +1039,8 @@ export const SERVER_TABS = [ span: 2, emptyText: "No tempdb samples in this window.", }), + /* `noteKey` carries the read's own note when a Hyperscale log file is in the snapshot: its size is n/a + (log service) and it is left out of these totals, so the page says so instead of looking short. */ table( "Database Sizes", "get_database_sizes", @@ -1047,7 +1049,8 @@ export const SERVER_TABS = [ DB_SIZE_COLUMNS, SNAPSHOT, "No database sizes in the latest snapshot.", - 1 + 1, + "note" ), table( "Table & Index Sizes", diff --git a/Darling/PerformanceMonitor.Darling.Viewer/FinOpsTab.Loaders.cs b/Darling/PerformanceMonitor.Darling.Viewer/FinOpsTab.Loaders.cs index 1c3b5dbca..aaa22c27c 100644 --- a/Darling/PerformanceMonitor.Darling.Viewer/FinOpsTab.Loaders.cs +++ b/Darling/PerformanceMonitor.Darling.Viewer/FinOpsTab.Loaders.cs @@ -195,8 +195,8 @@ private async Task LoadFinOpsUtilizationAsync() /* Free space % for the storage health score, from the latest database sizes. */ var dbSizes = await _dataService.GetDatabaseSizeLatestAsync(_server.ServerId); - var totalStorageMb = dbSizes.Sum(d => d.TotalSizeMb); - var totalFreeMb = dbSizes.Sum(d => d.FreeSpaceMb ?? 0m); + var totalStorageMb = DatabaseSizeRow.AllocatedTotalMb(dbSizes); + var totalFreeMb = DatabaseSizeRow.FreeTotalMb(dbSizes); data.FreeSpacePct = totalStorageMb > 0 ? totalFreeMb / totalStorageMb * 100m : 100m; } @@ -375,11 +375,12 @@ private async Task LoadFinOpsDatabaseSizesAsync() /* Proportional cost share by size (mirrors Lite's LoadDatabaseSizesAsync). */ if (_server.MonthlyCostUsd > 0 && data.Count > 0) { - var totalMb = data.Sum(d => d.TotalSizeMb); + /* A row with no allocated size (the Hyperscale log file, in the log service) takes no share. */ + var totalMb = DatabaseSizeRow.AllocatedTotalMb(data); if (totalMb > 0) { foreach (var d in data) - d.MonthlyCostShare = (d.TotalSizeMb / totalMb) * _server.MonthlyCostUsd; + d.MonthlyCostShare = ((d.TotalSizeMb ?? 0m) / totalMb) * _server.MonthlyCostUsd; } } diff --git a/Darling/PerformanceMonitor.Darling.Viewer/FinOpsTab.xaml b/Darling/PerformanceMonitor.Darling.Viewer/FinOpsTab.xaml index f35536041..cdf0e491d 100644 --- a/Darling/PerformanceMonitor.Darling.Viewer/FinOpsTab.xaml +++ b/Darling/PerformanceMonitor.Darling.Viewer/FinOpsTab.xaml @@ -661,7 +661,7 @@