Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
30 changes: 11 additions & 19 deletions Darling/Darling.Tests/AzureSqlDatabaseHardwareTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ namespace Darling.Tests;
/// On an Azure SQL Database (engine edition 5) the collected <c>server_properties</c> hardware columns are the
/// HOST's: a 1-vCore serverless General Purpose database read 2 logical CPUs, 0 sockets, 32 cores per socket, a
/// hyperthread ratio of 64 and 911.9 GB of physical memory. Nothing may present them as the database's; the service
/// objective and the <c>vcore_count</c> parsed from it describe the database.
/// objective and the <c>vcore_count</c> parsed from it describe the database. The <c>memory_stats</c> table is a different
/// source: its memory figures are the database's own and are pinned in <see cref="AzureSqlDatabaseMemoryScopeTests"/>.
///
/// <para>Pinned where the rule is applied: the <c>get_server_properties</c> payload (which the web Server Properties
/// list reads through <c>/api/read</c>), the web list's own tiles, the FinOps Server Inventory row the grid binds, and
/// the FinOps utilization card's sentences. Every test has an edition-3 twin that keeps today's values. Lite.Tests
/// pins the same table for the other app, in the same words.</para>
/// the words the FinOps utilization card takes from the shared rule. Every test has an edition-3 twin that keeps today's
/// values. Lite.Tests pins the same table for the other app, in the same words.</para>
/// </summary>
public sealed class AzureSqlDatabaseHardwareTests
{
Expand Down Expand Up @@ -214,30 +215,21 @@ public void InventoryRow_OnEdition3_KeepsItsHardware_AndHasNoNote()
// ── FinOps utilization card ──

[Fact]
public void OverProvisionedSentence_OnAzureSqlDatabase_CitesNoPhysicalMemoryShare()
{
var onAzure = ServerHardwareScope.OverProvisionedExplanation(3.2m, 11, 0.4, azureSqlDatabase: true);
var onBox = ServerHardwareScope.OverProvisionedExplanation(3.2m, 11, 0.4, azureSqlDatabase: false);

Assert.DoesNotContain("physical", onAzure, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("buffer pool", onAzure, StringComparison.OrdinalIgnoreCase);
Assert.Equal(
"CPU is lightly loaded (avg 3.2%, max 11%) and buffer pool uses only 0% of physical RAM. This server may have more resources than it needs.",
onBox);
}

[Fact]
public void FinOpsUtilizationCard_AsksTheSharedRule_BeforeItShowsPhysicalMemory()
public void FinOpsUtilizationCard_AsksTheSharedRule_ForTheWordsAroundItsMemoryFigures()
{
var tab = ReadRepoFile("Darling", "PerformanceMonitor.Darling.Viewer", "FinOpsTab.Loaders.cs");
var xaml = ReadRepoFile("Darling", "PerformanceMonitor.Darling.Viewer", "FinOpsTab.xaml");
var read = ReadRepoFile("Darling", "PerformanceMonitor.Darling.Viewer", "ViewerDataService.FinOps.Utilization.cs");

Assert.Contains("ServerHardwareScope.HardwareIsTheHosts(data.EngineEdition)", tab, StringComparison.Ordinal);
Assert.Contains("FinOpsPhysicalMemoryText.Text = azureSqlDb ? ServerHardwareScope.NotApplicable", tab, StringComparison.Ordinal);
Assert.Contains("FinOpsPhysicalMemoryCaption.Text = ServerHardwareScope.PhysicalMemoryCaption(data.EngineEdition);", tab, StringComparison.Ordinal);
Assert.Contains("x:Name=\"FinOpsPhysicalMemoryCaption\"", xaml, StringComparison.Ordinal);
Assert.Contains("ServerHardwareScope.OverProvisionedExplanation(", tab, StringComparison.Ordinal);
Assert.Contains("ServerHardwareScope.RightSizedExplanation(", tab, StringComparison.Ordinal);
Assert.DoesNotContain("of physical RAM", tab, StringComparison.Ordinal);
Assert.Contains("COALESCE(vcore_count, cpu_count) AS cpu_count, engine_edition", read, StringComparison.Ordinal);
/* The CPU count is resolved through the edition (AzureSqlDatabaseHostMathTests pins the CASE): off edition 5 it is still
COALESCE(vcore_count, cpu_count), and the edition still rides along for the card. */
Assert.Contains("ELSE COALESCE(vcore_count, cpu_count) END AS cpu_count, engine_edition", read, StringComparison.Ordinal);
Assert.Contains("EngineEdition = reader.IsDBNull(16)", read, StringComparison.Ordinal);
}

Expand Down
212 changes: 212 additions & 0 deletions Darling/Darling.Tests/AzureSqlDatabaseHostMathTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
/*
* 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.Globalization;
using System.Linq;
using PerformanceMonitor.Common;
using PerformanceMonitor.Darling.Viewer;
using Xunit;
using static Darling.Tests.RepoFile;

namespace Darling.Tests;

/// <summary>
/// On an Azure SQL Database (engine edition 5) <c>sys.dm_os_sys_info</c> describes the HOST: a 1-vCore serverless
/// General Purpose database read 2 logical CPUs and 911.9 GB of physical memory. <see cref="AzureSqlDatabaseHardwareTests"/>
/// pins that nothing SHOWS those <c>server_properties</c> values as the database's. These pins are the calculations that USED
/// the host's CPU count: the attributed-CPU denominator and the FinOps utilization card's CPU count. The memory figures come
/// from a different table and are pinned in <see cref="AzureSqlDatabaseMemoryScopeTests"/>.
///
/// <para>The rule: on an Azure SQL Database each of those uses the database's own figure where one is collected (the
/// <c>vcore_count</c> parsed from the service objective) and is otherwise NOT APPLICABLE. A DTU-model objective or an elastic
/// pool has no vCore count, so its CPU count is not applicable and nothing is computed from the host's. SQL Server
/// (editions 1 to 4) and Managed Instance (8) behave exactly as before, and every test has that twin. Lite.Tests pins the
/// same table for the other app, in the same words.</para>
/// </summary>
public sealed class AzureSqlDatabaseHostMathTests
{
private static readonly DateTime s_start = new(2026, 9, 30, 0, 0, 0, DateTimeKind.Utc);
private static readonly DateTime s_end = s_start.AddHours(1);

// ── CPU attribution ──

/// <summary>Half of one CPU for an hour is 1,800 CPU-seconds; half of the host's two would be 3,600.</summary>
private static CpuAttribution.Result Attribute(int? engineEdition, int storedCpuCount, int? vcoreCount) =>
CpuAttribution.Compute(
rankedCpuSeconds: 900, s_start, s_end,
sampleCount: 60, firstSampleUtc: s_start, lastSampleUtc: s_end, avgSqlCpuPercent: 50,
engineEdition, storedCpuCount, vcoreCount);

[Fact]
public void Attribution_OnAzureSqlDatabase_WithVcores_DividesByTheVcores_NotTheHostsCpus()
{
var result = Attribute(5, storedCpuCount: 2, vcoreCount: 1);

Assert.Equal(1800, result.SqlCpuSecondsInWindow);
Assert.Equal(0.5, result.AttributedCpuRatio);
Assert.Null(result.Note);
}

[Theory]
[InlineData(null)]
[InlineData(0)]
public void Attribution_OnAzureSqlDatabase_WithNoVcores_IsNotApplicable_AndComputesNothingFromTheHost(int? vcoreCount)
{
var result = Attribute(5, storedCpuCount: 2, vcoreCount);

Assert.Equal(900, result.RankedCpuSeconds);
Assert.Null(result.SqlCpuSecondsInWindow);
Assert.Null(result.AttributedCpuRatio);
Assert.Equal(CpuAttribution.CoreCountNotApplicableNote, result.Note);
Assert.Contains("not applicable", result.Note, StringComparison.Ordinal);
Assert.Contains("a DTU-model objective or an elastic pool", result.Note, StringComparison.Ordinal);
Assert.DoesNotContain("no server_properties snapshot", result.Note, StringComparison.Ordinal);
}

[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
[InlineData(8)]
public void Attribution_OnEveryOtherEdition_IsTheStoredCountMathItAlwaysWas(int engineEdition)
{
var legacy = CpuAttribution.Compute(900, s_start, s_end, 60, s_start, s_end, 50, 8);

Assert.Equal(legacy, Attribute(engineEdition, storedCpuCount: 8, vcoreCount: null));
/* A vcore_count beside a non-Azure edition is not read: only edition 5 resolves it. */
Assert.Equal(legacy, Attribute(engineEdition, storedCpuCount: 8, vcoreCount: 1));
Assert.Equal(14400, legacy.SqlCpuSecondsInWindow);
}

[Fact]
public void Attribution_WithNoServerPropertiesRow_KeepsItsUnavailableNote()
{
var result = Attribute(engineEdition: null, storedCpuCount: 0, vcoreCount: null);

Assert.Null(result.AttributedCpuRatio);
Assert.Equal(CpuAttribution.CoreCountUnavailableNote, result.Note);
Assert.Contains("no server_properties snapshot", result.Note, StringComparison.Ordinal);
}

[Fact]
public void OwnCpuCount_IsTheVcoresOnAzureSqlDatabase_AndTheStoredCountEverywhereElse()
{
Assert.Equal(1, ServerHardwareScope.OwnCpuCount(5, cpuCount: 2, vcoreCount: 1));
Assert.Null(ServerHardwareScope.OwnCpuCount(5, cpuCount: 2, vcoreCount: null));
Assert.Null(ServerHardwareScope.OwnCpuCount(5, cpuCount: 2, vcoreCount: 0));
Assert.Equal(16, ServerHardwareScope.OwnCpuCount(3, cpuCount: 16, vcoreCount: null));
Assert.Equal(4, ServerHardwareScope.OwnCpuCount(8, cpuCount: 4, vcoreCount: null));
Assert.Null(ServerHardwareScope.OwnCpuCount(3, cpuCount: null, vcoreCount: null));
}

[Fact]
public void TopQueriesAndTopProceduresTools_PassTheEditionAndTheVcores_NotOnlyTheStoredCount()
{
var tool = ReadRepoFile("Darling", "PerformanceMonitor.Darling.Service", "Mcp", "DarlingMcpDataTools.cs");

Assert.Equal(2, CountOf(tool, "properties?.EngineEdition, properties?.CpuCount ?? 0, properties?.VcoreCount);"));
Assert.DoesNotContain("properties?.CpuCount ?? 0);", tool, StringComparison.Ordinal);
}

// ── FinOps utilization card: the CPU count ──

[Fact]
public void CpuCountText_OnAzureSqlDatabase_IsNotApplicableWithNoVcores_AndTheVcoresOtherwise()
{
Assert.Equal(ServerHardwareScope.NotApplicable, ServerHardwareScope.CpuCountText(5, 0));
Assert.Equal("n/a", ServerHardwareScope.CpuCountText(5, 0));
Assert.Equal("1", ServerHardwareScope.CpuCountText(5, 1));
}

[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
[InlineData(8)]
[InlineData(null)]
public void CpuCountText_OnEveryOtherEdition_IsTheCountAsItAlwaysWas(int? engineEdition)
{
Assert.Equal(1234.ToString("N0", CultureInfo.CurrentCulture), ServerHardwareScope.CpuCountText(engineEdition, 1234));
Assert.Equal("16", ServerHardwareScope.CpuCountText(engineEdition, 16));
Assert.Equal("0", ServerHardwareScope.CpuCountText(engineEdition, 0));
}

/// <summary>The CASE the Viewer read uses, as one line. Lite's read carries the same line, so the two apps resolve the
/// count the same way.</summary>
private static readonly string s_cpuCountCase =
$"SELECT CASE WHEN engine_edition = {ServerHardwareScope.AzureSqlDatabaseEngineEdition} THEN vcore_count ELSE COALESCE(vcore_count, cpu_count) END AS cpu_count, engine_edition";

[Fact]
public void UtilizationRead_ResolvesTheCpuCountThroughTheEdition_NeverFallingBackToTheHostsCount()
{
var sql = ViewerDataService.UtilizationEfficiencySql;

Assert.Contains(s_cpuCountCase, sql, StringComparison.Ordinal);
/* Only edition 5 lacks the fall-back: the bare COALESCE that took the host's count is gone. */
Assert.DoesNotContain("SELECT COALESCE(vcore_count, cpu_count) AS cpu_count", sql, StringComparison.Ordinal);
Assert.Equal(1, CountOf(sql, "COALESCE(vcore_count, cpu_count)"));
}

[Fact]
public void UtilizationRead_ResolvesTheCpuCountTheSameWayLiteDoes()
{
var lite = ReadRepoFile("Lite", "Services", "LocalDataService.FinOps.Utilization.cs");

Assert.Contains(s_cpuCountCase, lite, StringComparison.Ordinal);
}

// ── FinOps utilization card: the health score ──

/// <summary>CPU p95 of 7% scores 95 and 50% free storage scores 100. Buffer pool 40 GB of 933,888 MB is 4%, which scores 60:
/// 95 * 0.4 + 60 * 0.3 + 100 * 0.3 = 86. The same arithmetic runs on every edition, an Azure SQL Database included, with the
/// figures <c>memory_stats</c> holds for it (see <see cref="AzureSqlDatabaseMemoryScopeTests"/>).</summary>
private static UtilizationEfficiencyRow Utilization(int engineEdition, int bufferPoolMb, int physicalMemoryMb) => new()
{
EngineEdition = engineEdition,
P95CpuPct = 7m,
BufferPoolMb = bufferPoolMb,
PhysicalMemoryMb = physicalMemoryMb,
FreeSpacePct = 50m,
};

[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
[InlineData(8)]
public void HealthScore_OnSqlServerAndManagedInstance_ScoresItsMemoryTerm(int engineEdition)
{
Assert.Equal(86, Utilization(engineEdition, 40_960, 933_888).ComputeHealthScore());
Assert.Equal(98, Utilization(engineEdition, 600_000, 933_888).ComputeHealthScore());
}

// ── the wiring, pinned at the source ──

private static int CountOf(string text, string needle)
{
var count = 0;
for (var at = text.IndexOf(needle, StringComparison.Ordinal); at >= 0; at = text.IndexOf(needle, at + needle.Length, StringComparison.Ordinal))
count++;
return count;
}

[Fact]
public void FinOpsUtilizationCard_AsksTheSharedRule_ForTheCpuCountAndTheHealthScore()
{
var tab = ReadRepoFile("Darling", "PerformanceMonitor.Darling.Viewer", "FinOpsTab.Loaders.cs");

Assert.Contains("FinOpsCpuCountText.Text = ServerHardwareScope.CpuCountText(data.EngineEdition, data.CpuCount);", tab, StringComparison.Ordinal);
Assert.DoesNotContain("data.CpuCount.ToString(", tab, StringComparison.Ordinal);
Assert.Contains("data.HealthScore = data.ComputeHealthScore();", tab, StringComparison.Ordinal);
Assert.DoesNotContain("FinOpsHealthCalculator.MemoryScore(", tab, StringComparison.Ordinal);
}
}
Loading
Loading