Skip to content

Azure SQL Database CPU math uses the vCores, and the Utilization card shows its memory figures - #4879

Merged
erikdarlingdata merged 10 commits into
devfrom
fix/azure-sql-database-no-host-hardware-in-math
Oct 1, 2026
Merged

erikdarlingdata merged 10 commits into
devfrom
fix/azure-sql-database-no-host-hardware-in-math

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 30, 2026 •

Copy link
Copy Markdown
Owner

Builds on #4876, which dev holds. That PR added ServerHardwareScope and stopped showing the stored hardware on an Azure SQL Database. This PR makes the CPU math use the database's vCores instead of the stored CPU count. It also restores the memory figures that #4876 hid, because they are the database's own.

What does this PR do?

On an Azure SQL Database, sys.dm_os_sys_info does not give the database's own limits. A 1-vCore serverless General Purpose database read a cpu_count of 2, the same as its visible online schedulers. Its service objective caps it at 1 vCore. It also read 911.9 GB of physical memory, which is the host's.

#4876 stops showing those values. Two calculations still divided by cpu_count. This PR fixes both, in Lite and in Darling.

Each calculation now uses the database's own count when one is collected. That count is vcore_count, parsed from the service objective. When none is collected, the part is marked not applicable and nothing is computed from the host. A DTU-model objective and an elastic pool have no vcore_count, so their CPU count is not applicable. SQL Server (editions 1 to 4) and Managed Instance (8) behave as before.

The memory figures follow a different rule. In server_properties, only physical_memory_mb, the socket count, the cores per socket and the hyperthread ratio are the host's. Its cpu_count is the database's scheduler count, which can be larger than its vCores.

The memory_stats table is the database's own. On an Azure SQL Database the collector fills memory_stats.total_physical_memory_mb from committed_target_kb, which is the database's memory limit. A 1-vCore General Purpose database reports 1,838 MB there and a Hyperscale database reports 4,476 MB. server_properties holds 911.9 GB for the same databases.

#4876 treated the memory_stats figure as the host's. This PR removes that. The FinOps Utilization card shows its Physical Memory and Buffer Pool % on an Azure SQL Database again. The health score keeps its memory term.

Why

  • CPU attribution (get_top_queries_by_cpu and get_top_procedures_by_cpu) multiplied the average CPU percent by the stored cpu_count. On the 1-vCore database the denominator was twice too large, so the attributed share was half of the true one.
  • The FinOps utilization read used COALESCE(vcore_count, cpu_count). With no vcore_count it fell back to cpu_count, which is not the database's vCore limit.
  • Azure SQL Database servers show their service objective, not the host's hardware #4876 hid Physical Memory and Buffer Pool % on the card. Both come from memory_stats, so they describe the database. Hiding them removed real values.

I traced every input of the utilization card on a 1-vCore serverless General Purpose test database.

Figure What it reads Host hardware Result
CPU Count vcore_count, else cpu_count No, but the cpu_count fallback is not the vCore limit vcore_count, or n/a
Physical Memory memory_stats.total_physical_memory_mb No, it is the database's memory limit Shown again. The caption reads "Memory limit" on an Azure SQL Database.
Buffer Pool % buffer pool over the same column No Shown again
Health score, memory term the same ratio No Not changed
Stolen Mem % (total_server_memory_mb minus buffer_pool_mb) over total_server_memory_mb No Not changed
Verdict CPU average, maximum and p95 from sys.dm_db_resource_stats.avg_cpu_percent. Workspace grants from sys.dm_exec_query_resource_semaphores. Worker threads, where current_workers_count is NULL on this edition. No The sentences name the buffer pool share again
Server Inventory health score CPU average, plus a constant memory score of 80 and a constant storage score No Not changed

What changes

  • ServerHardwareScope gets OwnCpuCount, CpuCountText and PhysicalMemoryCaption. One rule serves both apps. Its verdict sentences name the buffer pool share of the database's memory limit on an Azure SQL Database.
  • CpuAttribution.Compute has a new overload that takes the engine edition, the stored CPU count and vcore_count. On edition 5 it divides by the vCores. With no vCores it returns no ratio and the note core count not applicable: .... Both MCP tools in both apps call it. The payload keys are unchanged.
  • Both utilization reads (Lite on DuckDB, the Darling Viewer on PostgreSQL) resolve the CPU count with CASE WHEN engine_edition = 5 THEN vcore_count ELSE COALESCE(vcore_count, cpu_count) END. The card shows n/a when there is no count.
  • The utilization card shows Physical Memory and Buffer Pool % on every edition. On an Azure SQL Database the Physical Memory caption reads "Memory limit", and the Buffer Pool % tooltip says what the share is measured against.
  • UtilizationEfficiencyRow.ComputeHealthScore() exists in both apps and scores memory the same way on every edition. FinOpsHealthCalculator.Overall takes an int memory score, as it did on dev.
  • The memory and VM right-sizing rules still skip an Azure SQL Database. The comments now give the true reason: its memory comes with its service objective and cannot be resized on its own.
  • The server_properties hardware stays hidden. The web Server Properties tiles, the Server Inventory hardware cells and the hardware fields of get_server_properties are not changed.
  • The wording "a DTU-model objective or an elastic pool" now appears wherever a vCore count can be missing.
  • I rewrote the pins that held the wrong premise. New AzureSqlDatabaseMemoryScopeTests in both test projects give memory_stats and server_properties different values, 1,838 MB and 933,836 MB. Lite seeds both tables in DuckDB. Darling uses in-memory rows and checks its PostgreSQL reads as SQL text. A read that swaps the two tables fails in both.
  • I merged dev a third time. The new commit, File I/O on Azure SQL Database Hyperscale shows the real data file size and no size for the log file #4878, touches other files and merged with no conflicts. The diff against dev is 25 files.

I did not change the CPU right-sizing rule in the recommendations code. It reads the same row's CpuCount. So its rule (CpuCount > 4) can no longer fire from the stored cpu_count on a DTU-model objective or an elastic pool.

That changes one test that #4877 added. It seeded an Azure SQL Database with a stored cpu_count of 32 and no vcore_count, and expected the CPU rule to fire from that count. It now seeds a 32-vCore objective, so the rule still fires from the database's own count. A twin pins that a DTU database gets no CPU advice. The test is now named AzureSqlDatabase_MemoryAndVmRightSizingAdviseNothing_BecauseItsMemoryComesWithItsServiceObjective.

The seeder SeedRightSizingScenarioAsync takes an optional vcoreCount for this. It also gives an Azure SQL Database its own memory_stats memory (167,117 MB) beside the host's server_properties memory (933,836 MB).

The memory rule's divisor

The Recommendations memory rule's finding reads Memory over-provisioned (P95 SQL memory uses {ratio} of {n}GB RAM). Lite (LocalDataService.FinOps.Recommendations.cs) and the Darling Viewer (ViewerDataService.FinOps.Recommendations.cs) each divide by util.PhysicalMemoryMb. That value comes from memory_stats, never from server_properties.physical_memory_mb. The rule has also skipped an Azure SQL Database since #4877, so it needed no product change.

Two tests pin it: the divisor must stay the memory_stats figure, and the rules file must read no server_properties memory column. A third seeds a large Azure SQL Database and expects no memory advice. The retired Dashboard under deprecated/ is unchanged.

Left alone, and why

  • Stolen Mem % divides two memory counters from memory_stats. On an Azure SQL Database they are the database's own, so it needs no change.
  • The verdict reads no host hardware. Its CPU comes from sys.dm_db_resource_stats, its grants come from the resource semaphores, and its worker term cannot fire because current_workers_count is NULL on this edition.
  • The Server Inventory health score uses constants for memory (80) and storage. No host figure feeds it.
  • The Worker Threads card shows the maximum from sys.dm_os_sys_info.max_workers_count. On an Azure SQL Database that is the database's own setting (479 on the 1-vCore database), not a host figure, so it needs no change.
  • The analysis engine still reads the stored hardware for its hardware fact (Lite/Analysis/DuckDbFactCollector.Config.cs and Darling/PerformanceMonitor.Darling.Analysis/PgFactCollector.Config.cs). The plan viewer's Server Context card has the same gap: its Hardware row shows the stored CPU count and memory (PerformanceMonitor.PlanAnalysis/ServerContextCard.cs). On an Azure SQL Database its memory is the host's, and its CPU count is the scheduler count, not the vCores. Neither is part of this change.
  • Lite's Memory view and the memory tools show memory_stats.total_physical_memory_mb. On an Azure SQL Database that is the database's memory limit, so they need no change.
  • A window with no CPU sample gives the health score a CPU term of 100, because the p95 reads 0. FinOps recommendations skip right-sizing advice with no CPU data or on Azure SQL Database host memory #4877 gave the verdict a no-data value for that case, and the score does not follow it. This is not a host figure, so I left it.
  • The web pages do not compute attribution or health, so wwwroot is unchanged.

Test plan

  • Both test projects build with 0 warnings and 0 errors.
  • Targeted classes pass: the three AzureSqlDatabase classes in each project, CpuAttributionTests, HealthCalculatorTests, FinOpsTests, FinOpsVerdictSourcePinTests, FinOpsRowDisplayZoneTests, ProvisioningVerdictTests, ViewerFinOpsRecommendationsTests, HourlyAttributionSpanTests and McpPayloadContractCensusTests.
  • New pins: AzureSqlDatabaseMemoryScopeTests has 28 tests in Lite.Tests and 24 in Darling.Tests. Lite runs the reads against a seeded DuckDB. Darling pins its PostgreSQL reads as SQL text, because no PostgreSQL ran.
  • Lite.Tests: the utilization read runs against a seeded DuckDB. Edition 5 with vCores gives that count. Edition 5 on a DTU-model objective gives 0, which shows as n/a. Editions 3 and 8 give the stored count.
  • Red proof on the previous head. I ran the new classes against the code this branch had before this change. Seven tests failed in each project (7 of 20 in Lite.Tests, 7 of 16 in Darling.Tests), the same seven by name. Each line below gives the failing check.
    • RightSizedSentence_OnAzureSqlDatabase_CitesTheBufferPoolShare_OfTheDatabasesMemoryLimit: the sentence ended at "p95 62.0%). No action needed." with no share.
    • OverProvisionedSentence_OnAzureSqlDatabase_CitesTheBufferPoolShare_OfTheDatabasesMemoryLimit: the sentence went from "max 11%)" straight to "This database may have".
    • FinOpsUtilizationCard_ShowsPhysicalMemoryAndTheBufferPoolShare_OnEveryEdition: not found MemoryRatioText.Text = $"{bpPct:N0}%"; (the Viewer name is FinOpsMemoryRatioText).
    • HealthScore_OnAzureSqlDatabase_CarriesTheMemoryTermScoredFromMemoryStats: Expected 98, Actual 97.
    • HealthScore_DoesNotDependOnTheEngineEdition(engineEdition: 5): Expected 98, Actual 97.
    • MemoryRecommendation_NeverDividesByServerPropertiesMemory_SoItCannotPrintTheHostsGigabytes: found physical_memory_mb in the old comment.
    • MemoryAndVmRules_OnAzureSqlDatabase_SayWhyTheyStandDown_AndDoNotCallTheMemoryTheHosts: found "reports the HOST's memory".
  • Red proof by swapping sources. I changed one source at a time, ran the new classes, and restored the file. Each test below failed.
    • Lite, the utilization read takes Physical Memory from server_properties: 7 red. UtilizationRead_TakesTheMemoryFiguresFromMemoryStats_OnEveryEdition for editions 5, 3 and 8 (Expected 1838, Actual 933836). HealthScore_OnAzureSqlDatabase_CarriesTheMemoryTermScoredFromMemoryStats (Expected 98, Actual 86). HealthScore_OnSqlServerAndManagedInstance_IsTheSameScoreFromTheSameMemoryStats for editions 3 and 8 (Expected 98, Actual 86). UtilizationRead_DoesNotReadServerPropertiesForMemory.
    • Lite, the server_properties read takes physical_memory_mb from memory_stats: 2 red, ServerPropertiesReads_OnAzureSqlDatabase_StayTheHostsAndNull_WhateverMemoryStatsHolds and ServerPropertiesReads_OnSqlServer_KeepTheirHardware (Expected 933836, Actual 1838).
    • Lite, edition 5 shows the stored hardware in the get_server_properties payload and in the inventory row: 5 red. They are the two GetServerProperties_OnAzureSqlDatabase_... pins, LatestServerProperties_ReadsTheVcoreCountTheCollectorStored, InventoryRow_OnAzureSqlDatabase_LeavesTheMemoryAndCoreCellsBlank_AndSaysWhy (933888 where null was expected) and ServerPropertiesReads_OnAzureSqlDatabase_StayTheHostsAndNull_WhateverMemoryStatsHolds.
    • Darling, the utilization SQL takes Physical Memory from server_properties: 1 red, UtilizationRead_TakesTheMemoryFiguresFromMemoryStats_NeverFromServerProperties.
    • Darling, the inventory SQL and the latest-properties SQL take physical_memory_mb from memory_stats: 1 red, ServerPropertiesReads_TakeTheirHardwareFromServerProperties_NeverFromMemoryStats (found "memory_stats").
    • Darling, edition 5 shows the stored hardware in the payload and the inventory row: 4 red, the same kinds of pins as in Lite.
    • Both apps, the caption says "Physical: " on edition 5: 1 red each, PhysicalMemoryCaption_NamesTheDatabasesLimit_OnAzureSqlDatabase_AndPhysicalMemoryEverywhereElse (Expected "Memory limit: ", Actual "Physical: ").
  • Red proof for the pins that read source text. I broke each target, ran the class without a rebuild, and restored the file. Each one failed in both apps. The tooltip sentence is pinned by FinOpsUtilizationCard_ExplainsTheBufferPoolShareForADatabase_InTheWordsBothAppsUse. The caption x:Name and the caption assignment are pinned by FinOpsUtilizationCard_AsksTheSharedRule_ForTheWordsAroundItsMemoryFigures. The true reason in each of the two rule comments is pinned by MemoryAndVmRules_OnAzureSqlDatabase_SayWhyTheyStandDown_AndDoNotCallTheMemoryTheHosts. A finding that divides by another figure, and a rules file that mentions physical_memory_mb, both fail MemoryRecommendation_NeverDividesByServerPropertiesMemory_SoItCannotPrintTheHostsGigabytes.
  • Red proof for the edition skip. I removed the Azure SQL Database condition from the memory rule. In Lite, AzureSqlDatabase_MemoryAndVmRightSizingAdviseNothing_BecauseItsMemoryComesWithItsServiceObjective failed because the rule advised. In Darling, MemoryAndVmRightSizing_StandDownOnAzureSqlDatabase did not find the gate.
  • Red proof for the CPU pins, which this PR keeps. I made OwnCpuCount, CpuCountText, CpuAttribution.Compute and the utilization read ignore the edition, which is the old behavior, and ran AzureSqlDatabaseHostMathTests and CpuAttributionTests. Lite.Tests had 7 red of 39 and Darling.Tests had 6 red of 37. These checks failed. Attribution_OnAzureSqlDatabase_WithVcores_... gave Expected 1800, Actual 3600. Attribution_OnAzureSqlDatabase_WithNoVcores_... for null and 0 gave a value of 3600 where null was expected. OwnCpuCount_... gave Expected 1, Actual 2. CpuCountText_OnAzureSqlDatabase_... gave Expected "n/a", Actual "0". In Lite, UtilizationRead_ResolvesTheCpuCountThroughTheEdition(engineEdition: 5, vcoreCount: null, expectedCpuCount: 0) gave Expected 0, Actual 2, and the card pin failed on the missing CASE text. In Darling, the read pin did not find the CASE text.
  • Full Lite.Tests on the merged tree: Total: 6490, Errors: 0, Failed: 0, Skipped: 0.
  • Full Darling.Tests on the merged tree: Total: 19107, Errors: 0, Failed: 0, Skipped: 1197, Not Run: 1. The skipped tests are live classes that need a PostgreSQL rig. I did not start one.
  • Not run: anything against a live Azure SQL Database. The memory values in this description came from live databases before this change. I did not connect to any server while making it. The Darling Viewer reads were not run against PostgreSQL.

Which component(s) does this affect?

  • Lite
  • Darling
  • Lite Tests
  • Darling Tests
  • SQL collection scripts
  • Documentation
  • Full Dashboard (deprecated)
  • CLI Installer (deprecated)

Checklist

  • I have read the contributing guide
  • My code builds with zero warnings (dotnet build -c Debug)
  • I have tested my changes against at least one SQL Server version (no connection to a monitored server was allowed, see the test plan)
  • I have not introduced any hardcoded credentials or server names

Changelog entry

The changelog line is written from this entry at release, so CHANGELOG.md is not edited here.

SECTION: Fixed
ENTRY:

…'s hardware

On an Azure SQL Database the collected logical CPUs, sockets, cores per socket, hyperthread ratio and physical memory are the host's, not the database's allocation. get_server_properties (Lite and Darling) now returns those five as null with vcore_count and a hardware_note, the web Server Properties list hides them and shows the service objective and vCores, and the FinOps Server Inventory and utilization card stop presenting the host's memory and cores. Every other engine edition is unchanged.
…e server properties pin expects vcore_count after the clock pair
… no longer use the host's hardware

On an Azure SQL Database sys.dm_os_sys_info describes the host: a 1-vCore serverless General Purpose database read 2 CPUs and 911.9 GB of memory. Three calculations still divided by or scored those values. Each now uses the database's own figure where one is collected (vcore_count from the service objective) and is not applicable otherwise. A DTU objective names no vCores, so its CPU count is not applicable and nothing is computed from the host. SQL Server and Managed Instance are unchanged.

- CpuAttribution has an overload that takes the engine edition and vcore_count. On edition 5 it divides by the vCores, or omits the ratio with a not-applicable note. Both top-queries and top-procedures tools, in Lite and Darling, call it.
- The FinOps utilization read resolves its CPU count through the edition in SQL, in both apps, so edition 5 never falls back to the host's count. The card shows n/a for it.
- The FinOps health score leaves its memory term out on edition 5, because that term is the buffer pool's share of the host's physical memory. CPU and storage carry the score, and a tooltip says so.
…database gets no CPU advice

The CPU count on an Azure SQL Database is now its vCore count, or none for a DTU objective, never the host's count. The test that expects the CPU rule to fire on edition 5 seeded the host's 32 CPUs with no vcore_count, which is the DTU case. It now seeds a 32-vCore objective. A new test pins that a DTU database gets no CPU advice.
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review September 30, 2026 22:15
@erikdarlingdata
erikdarlingdata marked this pull request as draft September 30, 2026 22:22
…ry_stats, not the host

On an Azure SQL Database the FinOps memory figures read memory_stats, where
total_physical_memory_mb is filled from committed_target_kb. That is the database's own
memory limit (1,838 MB on a 1-vCore General Purpose database), not the host's 911.9 GB.
Only server_properties holds the host's memory.

- The Utilization card shows Physical Memory and Buffer Pool % on every edition. On an
  Azure SQL Database the caption reads "Memory limit" and the verdict sentences name the
  database's memory limit.
- The health score keeps its memory term on every edition. FinOpsHealthCalculator.Overall
  takes an int memory score again.
- The memory and VM right-sizing rules still skip an Azure SQL Database, now because its
  memory comes with its service objective and cannot be resized on its own.
- Comments and tests that called the memory_stats figure the host's are corrected. New
  tests seed memory_stats and server_properties with different values, so a read that
  swaps the two tables fails.
- A DTU-model objective and an elastic pool both have no vCore count. The wording now
  names both.
@erikdarlingdata erikdarlingdata changed the title Azure SQL Database CPU attribution, FinOps CPU count and health score stop using the host's hardware Azure SQL Database CPU math uses the vCores, and the Utilization card shows its memory figures Sep 30, 2026
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review September 30, 2026 23:44
@erikdarlingdata
erikdarlingdata merged commit 2b6578f into dev Oct 1, 2026
24 of 26 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/azure-sql-database-no-host-hardware-in-math branch October 1, 2026 00:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant