Repository navigation
Full-edition perfmon_stats.cntr_value_per_second divides as a number, not an integer; existing installs converge idempotently (#3653 item 13, Q9) - #3717
Merged
Conversation
… not an integer; existing installs converge idempotently (#3653 item 13, Q9) The computed column shipped as cntr_value_delta / NULLIF(sample_interval_seconds, 0). Both operands are integer types, so T-SQL truncated: seven batch requests over a 60-second interval read as 0 per second, every counter running under one event per second was a flat zero, and report.daily_summary_v2 averaged those zeros as throughput. #3658 fixed the Dashboard's READ by recomputing the rate; this is the schema half Erik approved in Q9 (integer division is a defect, so it qualifies under the frozen twin's bug-fix-only rule). install/02 and install/06's CREATE now define the column as cntr_value_delta * 1.0 / NULLIF(...), which the T-SQL precision rules make numeric(33,12) (bigint * numeric(2,1) = numeric(22,1); / integer = 33,12). Non-persisted, so metadata-only. install/06 converges an EXISTING install in two places on the is_processed precedent: an install-time block at the end of the file (the installer runs upgrades/ then every install/*.sql in order, so 06 runs before 47 re-creates the view) and a runtime branch inside config.ensure_collection_table for a caller that asks it to ensure a table that already exists. Guard predicate: the column exists in sys.computed_columns AND TYPE_NAME(system_type_id) is an integer type - the type IS the defect, and it is stable where the stored definition text (which SQL Server rewrites as ([cntr_value_delta]/nullif([sample_interval_seconds],(0)))) is not. Drop and re-add are guarded independently so an interrupted run converges next time. No index, schema-bound view or user statistics reference the column, so the DROP cannot be blocked. install/47 gets a comment, not a filter: the Full edition's delta framework does write a (0, 0) first row (the MIN(collection_id) arm makes a series' first row its own previous), but the column's own NULLIF turns that row's rate into NULL and AVG skips it, so the FILTER the PG/DuckDB tiers needed in #3695 has nothing left to exclude here. Executed against mcr.microsoft.com/mssql/server:2022-latest: old shape 7/60 -> 0; after 06, 0.116666666666 with definition (([cntr_value_delta]*(1.0))/nullif([sample_interval_seconds],(0))) numeric(33,12) is_persisted=0; second run of 06 a no-op (column_id unchanged); proc path rewrites once and logs exactly one SCHEMA_UPGRADE row, second call a no-op; interrupted state (column dropped) recovers; fresh 02 creates the real shape; full install 01-56 (minus 45) twice with 0 failures; daily_summary_v2 batch_requests_per_sec 0 -> 0.55 on the same rows. Partial #3653 - item 13 (Q9)
Closed
39 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Partial #3653 — item 13 (Q9)
The lie
collect.perfmon_stats.cntr_value_per_secondis the Full edition's per-second rate for every perfmon counter, and it shipped ascntr_value_deltaisbigint,sample_interval_secondsisinteger, and T-SQL divides two integer types as an integer. Seven batch requests over a 60-second interval read as 0 per second. Fifty-nine over sixty read as 0. Every counter running under one event per second — compilations, re-compilations, lock waits, log flushes on a quiet store — rendered as a flat zero, andreport.daily_summary_v2'sbatch_requests_per_sec/compilations_per_secrows were the mean of those truncated integers. On the rig, the same four rows average to 0 under the old column and 0.55 under the new one.#3658 fixed the Dashboard's READ (both perfmon readers recompute
cntr_value_delta * 1.0 / NULLIF(...)and stopped selecting the column;DashboardMirrorPinTestspins that shape). The schema column kept lying to anything else that read it — the reporting view above, and any ad-hoc query against the table. Erik's Q9 ruling: integer division is a defect, the frozen twin's rule is bug-fix-only, this qualifies; the upgrade path must be idempotent.What changes
install/02_create_tables.sql— the CREATE defines the column as* 1.0is the idiom thememory_statspercentage columns in the same file already use, and it is byte-for-byte the expression the Dashboard read uses since #3658, so the two halves agree. The type is chosen deliberately and stated in the comment:bigint * numeric(2,1)isnumeric(22,1);numeric(22,1) / integerisnumeric(33,12)under the T-SQL precision/scale rules. The rig confirmsprecision 33, scale 12. The column is non-persisted (is_persisted = 0, verified), so the redefinition is metadata-only and costs nothing on disk.install/06_ensure_collection_table.sql— three things:perfmon_statscarries the same definition (the [BUG] Linux Host CPU is shown at 100% all the time (SQL Server CPU consumption probably correct) #1048 follow-up is the precedent for keeping 06's CREATE aligned with 02 — the recreate path must not re-introduce the defect).is_processedprecedent that already lives there. The installer runsupgrades/first and then everyinstall/*.sqlin order, so this block runs on every re-install/upgrade of an existing Full install, and it runs beforeinstall/47re-creates the view that averages the column.config.ensure_collection_table's "table already exists, checking for schema upgrades" path, for a caller that asks the procedure to ensure a table that already exists. Logs oneSCHEMA_UPGRADErow toconfig.collection_log, as theis_processedbranch does.The guard predicate: the column exists in
sys.computed_columnsforcollect.perfmon_statsandTYPE_NAME(cc.system_type_id)is an integer type (bigint/int/smallint/tinyint). The type IS the defect — integer division yields an integer-typed column, real division yieldsnumeric— so a converged column can never match and a second run is a no-op. I chose this over matching the stored definition text because SQL Server rewrites that text (the old shape is stored as([cntr_value_delta]/nullif([sample_interval_seconds],(0))), lower-casenullif, bracketed, parenthesised literals) and its spelling is not a contract across versions.Drop and re-add are guarded independently (the #1048 upgrade script's "partial-failure safe" shape): if a run dropped the column and died before the re-add, the next run finds the column missing and re-adds it rather than leaving the table without its rate. Rig-verified.
Dependencies checked: nothing in
install/orupgrades/creates an index on the column, no view in 47/48/54 usesSCHEMABINDING(none ininstall/at all), and noCREATE STATISTICSnames it — so theDROP COLUMNcannot be blocked. Auto-created statistics drop with the column.install/47_create_reporting_views.sql— a comment on thethroughputCTE, no filter. The brief asked whether the view should also guardsample_interval_seconds > 0the way #3695 addedFILTER (WHERE sample_interval_seconds IS DISTINCT FROM 0)on the PG/DuckDB tiers. I looked, and the answer is no, for a reason the rig then corrected me on: the Full edition's delta framework (install/05) does write a (0, 0) row for a series' first collection — theprevious_collectionCTE'sMIN(collection_id)arm makes the first row its own previous, sodelta = value − value = 0andinterval = DATEDIFF(self, self) = 0(167 of 167 first-collection rows on the rig; the/*Exclude first collection where no previous exists*/comment in 05 is misleading). But the column's ownNULLIF(sample_interval_seconds, 0)turns that row's rate into NULL, and AVG skips NULL (rate_null = 167). On PG/DuckDB the division happens in the aggregate against a raw interval column, so the exclusion had to be a FILTER; here the division and its NULLIF live in the column, so the exclusion is already made before the aggregate sees the row. Adding a WHERE would be a second copy of a rule the column already enforces.Why this shape and not another
upgrades/<from>-to-<to>/script: that path keys on a version boundary, and the Full twin's versioned ladder stops at3.0.0-to-3.1.0while the product is 3.8.0 — picking a boundary would be an invention. Theis_processedinstall-time block in 06 is the established precedent for a schema upgrade that must land on every re-run regardless of version, and its position before 47 is load-bearing here.definition: see above.CONVERT(decimal(19,4), …):* 1.0matches the memory_stats idiom and the Dashboard read exactly; a narrower CONVERT would make the schema column and the read disagree in the twelfth decimal for no consumer that needs it.What it does NOT do
DashboardMirrorPinTests:207pins the Dashboard's read SQL (unchanged, still correct). The Dashboard read's comment says "the schema column is left as it is" — after this PR that sentence is stale but harmless (the read recomputes the same expression either way); reported below, not fixed, per the lane's T-SQL-only bound.delta = cntr_value(since restart) overinterval = spacing between collections, which under-states the rate (the counter accumulated over less than the spacing). Separate defect class, separate lane — reported.daily_summary_v2(declined with reasons above).cpu_utilization.sample_timeUTC) and Q8 (utc_offsetzone id) still belong to another lane.Executed
CI runs T-SQL on the fresh-install path only:
sql-validation.ymlinstallsinstall/01–56(minus 45, 97–99) once on SQL Server 2017 / 2019 / 2022 / 2025 containers, then seeds one row per table and materialises everyreport.*view — all four green on this PR, so the new02definition compiles on every supported engine anddaily_summary_v2executes against it. What CI cannot see is the upgrade of an existing old-shape table, the second pass (idempotency —IdempotencyTestsis excluded inbuild.yml; it needs a realSQL2022host), and the procedure's runtime branch. Those ran here:mcr.microsoft.com/mssql/server:2022-latest(RTM-CU26, x64 under emulation on the arm64 Mac,desktop-linuxcontext), scratchPerformanceMonitordatabase via the realinstall/01:02, seed(7, 60),(0, 0),(NULL, NULL),(59, 60)([cntr_value_delta]/nullif([sample_interval_seconds],(0))), bigint,is_persisted 0; rates 0, NULL, NULL, 006, run 1Dropped …+Re-added …; definition(([cntr_value_delta]*(1.0))/nullif([sample_interval_seconds],(0))), numeric(33,12),is_persisted 0; rates 0.116666666666, NULL, NULL, 0.98333333333306, run 2column_idunchanged → no-opEXECUTE config.ensure_collection_table @table_name = N'perfmon_stats', @debug = 1×2SCHEMA_UPGRADErow; run 2 no-op, still one row06Re-added …only — recoversDROP TABLE, this branch's02fresh07–56(minus45Agent jobs) in order, then the whole01–56sequence a second time06silent on pass 2report.daily_summary_v2sort_order 20 on the seeded rowsnumeric)collect.perfmon_stats_collector×2 + delta frameworkLog Flushes/sec1/3 → 0.333…) that were 0 beforeBuilds:
deprecated/Installer.Core(embedsinstall/**/*.sql) anddeprecated/Installer.Tests—-c Release -p:EnableWindowsTargeting=true, 0 warnings; CI's Installer.Tests selection 194/194 green locally.Container torn down after the run.