Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
96d7b86
Azure SQL Database servers show their service objective, not the host…
erikdarlingdata Sep 30, 2026
fb75432
Azure SQL Database hardware tests read repo files as they are, and th…
erikdarlingdata Sep 30, 2026
1013e34
Merge dev into azure-sql-database-properties-not-host-hardware
erikdarlingdata Sep 30, 2026
3b90c0e
Merge branch 'dev' into fix/azure-sql-database-properties-not-host-ha…
erikdarlingdata Sep 30, 2026
6f1e000
Azure SQL Database CPU attribution, FinOps CPU count and health score…
erikdarlingdata Sep 30, 2026
d818b08
Merge remote-tracking branch 'origin/dev' into fix/azure-sql-database…
erikdarlingdata Sep 30, 2026
ae8768b
Right-sizing test seeds an Azure SQL Database with vCores, and a DTU …
erikdarlingdata Sep 30, 2026
ab7735d
Merge remote-tracking branch 'origin/dev' into fix/azure-sql-database…
erikdarlingdata Sep 30, 2026
fc9ad86
Keep the memory figures on an Azure SQL Database: they come from memo…
erikdarlingdata Sep 30, 2026
d5ecb4c
Merge remote-tracking branch 'origin/dev' into fix/azure-sql-database…
erikdarlingdata Sep 30, 2026
4e8db86
Azure SQL Database: the hardware fact, worker threads and health scor…
erikdarlingdata Sep 30, 2026
02d8825
Merge origin/fix/azure-sql-database-no-host-hardware-in-math into fix…
erikdarlingdata Sep 30, 2026
ab140a2
Merge origin/dev into fix/azure-sql-database-host-hardware-leftovers
erikdarlingdata Oct 1, 2026
3b3f1a7
Azure SQL Database: cpu_count and the worker ceiling are the database…
erikdarlingdata Oct 1, 2026
cd500fb
Azure SQL Database: show the memory limit and Logical CPUs under thei…
erikdarlingdata Oct 1, 2026
bcbd94c
Pin the audit_config MAXDOP recommendation (vCores on an Azure SQL Da…
erikdarlingdata Oct 1, 2026
a9cce5b
Name the Azure SQL Database own-figures tests for what they pin, and …
erikdarlingdata Oct 1, 2026
3b79ba2
Clear two build warnings: a duplicate using directive and a property …
erikdarlingdata Oct 1, 2026
e3c2bee
Darling get_memory_stats and the Viewer Memory tab read the server_pr…
erikdarlingdata Oct 1, 2026
a6cdbd8
get_memory_stats on an Azure SQL Database says what its memory figure…
erikdarlingdata Oct 1, 2026
a90e06c
Merge dev into the Azure SQL Database host-hardware branch: get_memor…
erikdarlingdata Oct 1, 2026
6fae165
Point the CPU right-sizing stand-down pin at the moved guard
erikdarlingdata Oct 1, 2026
337e562
Memory > Overview names its first two figures from the edition its ot…
erikdarlingdata Oct 1, 2026
24cfd0a
The latest-memory reads no longer carry an edition: nothing reads the…
erikdarlingdata Oct 1, 2026
15e1b1f
The store-views pin scans the analysis and storage projects too, and …
erikdarlingdata Oct 1, 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
52 changes: 34 additions & 18 deletions Darling/Darling.Tests/AzureSqlDatabaseHardwareTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@
namespace Darling.Tests;

/// <summary>
/// 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. 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"/>.
/// On an Azure SQL Database (engine edition 5) four of the collected <c>server_properties</c> hardware columns are the
/// HOST's: a 1-vCore serverless General Purpose database read 0 sockets, 32 cores per socket, a hyperthread ratio of 64 and
/// 911.9 GB of physical memory. Nothing may present those as the database's. The fifth column, <c>cpu_count</c>, is the
/// database's own scheduler count (that database read 2), so it is shown as read. The service objective and the
/// <c>vcore_count</c> parsed from it describe the allocation. 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
Expand All @@ -33,8 +34,9 @@ namespace Darling.Tests;
/// </summary>
public sealed class AzureSqlDatabaseHardwareTests
{
/// <summary>The four columns that describe the host on an Azure SQL Database. <c>cpu_count</c> is not among them.</summary>
private static readonly string[] s_hostKeys =
["cpu_count", "hyperthread_ratio", "socket_count", "cores_per_socket", "physical_memory_mb"];
["hyperthread_ratio", "socket_count", "cores_per_socket", "physical_memory_mb"];

private static DarlingDataReader.ServerPropertiesReadRow HostRow(int engineEdition, string edition, string? objective, int? vcores) => new(
new DateTime(2026, 9, 30, 12, 0, 0, DateTimeKind.Utc), edition, "12.0.2000.8", "RTM", null,
Expand All @@ -46,33 +48,38 @@ private static JsonElement Payload(DarlingDataReader.ServerPropertiesReadRow row
// ── get_server_properties ──

[Fact]
public void GetServerProperties_OnAzureSqlDatabase_ReturnsTheHostsFiveAsNull_AndTheDatabasesVcores_WithANote()
public void GetServerProperties_OnAzureSqlDatabase_ReturnsTheHostsFourAsNull_PassesItsOwnCpuCountThrough_AndReturnsTheVcores_WithANote()
{
var json = Payload(HostRow(5, "SQL Azure", "GP_S_Gen5_1", 1));

foreach (var key in s_hostKeys)
Assert.Equal(JsonValueKind.Null, json.GetProperty(key).ValueKind);

/* The stored count is the database's own scheduler count (2 for this 1-vCore database), not the host's, and not the vCores. */
Assert.Equal(2, json.GetProperty("cpu_count").GetInt32());

Assert.Equal("GP_S_Gen5_1", json.GetProperty("service_objective").GetString());
Assert.Equal(1, json.GetProperty("vcore_count").GetInt32());
var note = json.GetProperty("hardware_note").GetString();
Assert.Equal(ServerHardwareScope.McpHardwareNote, note);
Assert.Contains("service_objective", note, StringComparison.Ordinal);
Assert.Contains("vcore_count", note, StringComparison.Ordinal);
Assert.Contains("not this database's allocation", note, StringComparison.Ordinal);
Assert.Contains("describe the host machine, not this database", note, StringComparison.Ordinal);
Assert.Contains("cpu_count is the database's own scheduler count", note, StringComparison.Ordinal);

var names = json.EnumerateObject().Select(p => p.Name).ToList();
Assert.Equal(names.IndexOf("service_objective") + 1, names.IndexOf("vcore_count"));
Assert.Equal(5, json.GetProperty("engine_edition").GetInt32());
}

[Fact]
public void GetServerProperties_OnAzureSqlDatabase_WithNoVcores_StillHidesTheHost_AndReturnsANullVcoreCount()
public void GetServerProperties_OnAzureSqlDatabase_WithNoVcores_StillHidesTheHost_KeepsItsCpuCount_AndReturnsANullVcoreCount()
{
var json = Payload(HostRow(5, "SQL Azure", "S0", null));

foreach (var key in s_hostKeys)
Assert.Equal(JsonValueKind.Null, json.GetProperty(key).ValueKind);
Assert.Equal(2, json.GetProperty("cpu_count").GetInt32());
Assert.Equal(JsonValueKind.Null, json.GetProperty("vcore_count").ValueKind);
Assert.True(json.TryGetProperty("hardware_note", out _));
}
Expand Down Expand Up @@ -109,7 +116,7 @@ public void ServerPropertiesRead_CarriesTheStoredVcoreCount()
// ── the web Server Properties list ──

[Fact]
public void WebServerProperties_DescriptorHidesTheHostFive_OnEdition5_AndShowsTheServiceObjectiveAndVcores()
public void WebServerProperties_DescriptorHidesTheHostFour_OnEdition5_KeepsLogicalCpus_AndShowsTheServiceObjectiveAndVcores()
{
var js = ReadRepoFile("Darling", "PerformanceMonitor.Darling.Service", "wwwroot", "js", "pages", "server-tabs.js");
var start = js.IndexOf("const PROPERTY_STATS = [", StringComparison.Ordinal);
Expand All @@ -123,6 +130,12 @@ public void WebServerProperties_DescriptorHidesTheHostFive_OnEdition5_AndShowsTh
Assert.Contains("hideWhen: AZURE_SQL_DATABASE", line, StringComparison.Ordinal);
}

/* Logical CPUs is the database's own scheduler count on edition 5, so its tile is drawn there like any other. */
var cpuCount = list.Split('\n').Single(l => l.Contains("key: \"cpu_count\"", StringComparison.Ordinal));
Assert.Contains("label: \"Logical CPUs\"", cpuCount, StringComparison.Ordinal);
Assert.DoesNotContain("hideWhen", cpuCount, StringComparison.Ordinal);
Assert.DoesNotContain("showWhen", cpuCount, StringComparison.Ordinal);

Assert.Contains("{ key: \"vcore_count\", label: \"vCores\", format: \"int\", showWhen: AZURE_SQL_DATABASE }", list, StringComparison.Ordinal);
var objective = list.Split('\n').Single(l => l.Contains("key: \"service_objective\"", StringComparison.Ordinal));
Assert.DoesNotContain("hideWhen", objective, StringComparison.Ordinal);
Expand All @@ -139,7 +152,7 @@ public void WebStatRenderer_DropsTilesByTheirCondition_OnEdition5_AndKeepsEveryT
const m = await import(process.argv[1]);
const H = { key: "engine_edition", equals: 5 };
const stats = [
{ key: "cpu_count", hideWhen: H }, { key: "physical_memory_mb", hideWhen: H },
{ key: "cpu_count" }, { key: "socket_count", hideWhen: H }, { key: "physical_memory_mb", hideWhen: H },
{ key: "service_objective" }, { key: "vcore_count", showWhen: H },
];
const names = (d) => m.visibleStats(stats, d).map((s) => s.key).join(",");
Expand All @@ -161,39 +174,42 @@ public void WebStatRenderer_DropsTilesByTheirCondition_OnEdition5_AndKeepsEveryT
var error = proc.StandardError.ReadToEnd();
Assert.True(proc.WaitForExit(20000) && proc.ExitCode == 0, "node failed: " + error);
using var doc = JsonDocument.Parse(output);
Assert.Equal("service_objective,vcore_count", doc.RootElement.GetProperty("e5").GetString());
Assert.Equal("cpu_count,physical_memory_mb,service_objective", doc.RootElement.GetProperty("e3").GetString());
Assert.Equal("cpu_count,physical_memory_mb,service_objective", doc.RootElement.GetProperty("none").GetString());
Assert.Equal("cpu_count,service_objective,vcore_count", doc.RootElement.GetProperty("e5").GetString());
Assert.Equal("cpu_count,socket_count,physical_memory_mb,service_objective", doc.RootElement.GetProperty("e3").GetString());
Assert.Equal("cpu_count,socket_count,physical_memory_mb,service_objective", doc.RootElement.GetProperty("none").GetString());
}
}

// ── FinOps Server Inventory row ──

[Fact]
public void InventoryRow_OnAzureSqlDatabase_LeavesTheMemoryAndCoreCellsBlank_AndSaysWhy()
public void InventoryRow_OnAzureSqlDatabase_LeavesTheHostCellsBlank_ShowsItsOwnCpuCount_AndSaysWhy()
{
var row = new ServerPropertyRow
{
Edition = "SQL Azure", EngineEdition = 5, CpuCount = 2, PhysicalMemoryMb = 933_888, SocketCount = 0, CoresPerSocket = 32,
};

Assert.Null(row.CpuCount);
Assert.Equal(2, row.CpuCount);
Assert.Null(row.PhysicalMemoryMb);
Assert.Null(row.SocketCount);
Assert.Null(row.CoresPerSocket);
Assert.Equal(ServerHardwareScope.InventoryHardwareNote, row.HardwareUnavailableReason);
Assert.Contains("memory, sockets, cores per socket and hyperthread ratio are the host's", row.HardwareUnavailableReason, StringComparison.Ordinal);
Assert.Contains("Logical CPUs is the database's own scheduler count", row.HardwareUnavailableReason, StringComparison.Ordinal);
}

[Fact]
public void InventoryRow_OnAzureSqlDatabase_DoesNotDependOnTheOrderTheLoaderAssignsInAndKeepsAReadsOwnReason()
{
var row = new ServerPropertyRow { CpuCount = 2, PhysicalMemoryMb = 933_888, SocketCount = 0, CoresPerSocket = 32 };
Assert.Equal(2, row.CpuCount);
Assert.Equal(933_888L, row.PhysicalMemoryMb);

row.EngineEdition = 5;
row.HardwareUnavailableReason = "Hardware read denied";

Assert.Null(row.CpuCount);
Assert.Null(row.PhysicalMemoryMb);
Assert.Equal(2, row.CpuCount);
Assert.Equal("Hardware read denied", row.HardwareUnavailableReason);
}

Expand Down
24 changes: 13 additions & 11 deletions Darling/Darling.Tests/AzureSqlDatabaseHostMathTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@
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"/>.
/// On an Azure SQL Database (engine edition 5) <c>sys.dm_os_sys_info</c> reports the HOST's memory, sockets and cores per
/// socket, while its <c>cpu_count</c> is the database's own scheduler count, which is not the CPU the database is given: a
/// 1-vCore serverless General Purpose database read 2. <see cref="AzureSqlDatabaseHardwareTests"/> pins what is SHOWN. These
/// pins are the calculations that must count the CPU the database is GIVEN (its vCores) and not the schedulers it can see:
/// 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
/// pool has no vCore count, so its CPU count is not applicable and nothing is computed from the stored scheduler count. 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>
Expand All @@ -36,15 +37,15 @@ public sealed class AzureSqlDatabaseHostMathTests

// ── 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>
/// <summary>Half of one CPU for an hour is 1,800 CPU-seconds; half of the two schedulers the database can see 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()
public void Attribution_OnAzureSqlDatabase_WithVcores_DividesByTheVcores_NotTheStoredSchedulerCount()
{
var result = Attribute(5, storedCpuCount: 2, vcoreCount: 1);

Expand All @@ -56,7 +57,7 @@ public void Attribution_OnAzureSqlDatabase_WithVcores_DividesByTheVcores_NotTheH
[Theory]
[InlineData(null)]
[InlineData(0)]
public void Attribution_OnAzureSqlDatabase_WithNoVcores_IsNotApplicable_AndComputesNothingFromTheHost(int? vcoreCount)
public void Attribution_OnAzureSqlDatabase_WithNoVcores_IsNotApplicable_AndComputesNothingFromTheStoredCount(int? vcoreCount)
{
var result = Attribute(5, storedCpuCount: 2, vcoreCount);

Expand Down Expand Up @@ -145,12 +146,12 @@ public void CpuCountText_OnEveryOtherEdition_IsTheCountAsItAlwaysWas(int? engine
$"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()
public void UtilizationRead_ResolvesTheCpuCountThroughTheEdition_NeverFallingBackToTheStoredSchedulerCount()
{
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. */
/* Only edition 5 lacks the fall-back: the bare COALESCE that took the stored scheduler 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)"));
}
Expand All @@ -171,6 +172,7 @@ public void UtilizationRead_ResolvesTheCpuCountTheSameWayLiteDoes()
private static UtilizationEfficiencyRow Utilization(int engineEdition, int bufferPoolMb, int physicalMemoryMb) => new()
{
EngineEdition = engineEdition,
ProvisioningStatus = ProvisioningVerdict.RightSized, // a measured window: a window with no CPU sample has no CPU term
P95CpuPct = 7m,
BufferPoolMb = bufferPoolMb,
PhysicalMemoryMb = physicalMemoryMb,
Expand Down
Loading
Loading