Perfmon rate counters show a per-second rate in the web grid, its trend chart and both MCP tools - #4883
Merged
Conversation
…eb trend chart and both MCP tools
…he payloads; the rate census sweeps a bare per_second key
erikdarlingdata
marked this pull request as ready for review
October 1, 2026 00:15
…mall rates never read as 0 The web Perfmon Counters grid dropped a rate row's stored value to show its per-second figure. For a counter that seldom fires that removed the only count there was: Number of Deadlocks/sec at 37 showed no total, and its rate read 0.00. A rate row now keeps its running total under a Total since start column, whether or not a rate is known. Only the stand-in 0 delta of an unknowable interval is blanked. Gauges and every other row keep their numbers. A real low rate also printed as 0. One deadlock in a 300 s collection is 0.0033 a second, but the trend chart formatted to two decimals, so every axis label read 0 and the tooltip read 0. A rate now prints through fmtRate (two decimals from 1 up, two significant digits below it) on the chart axis, the tooltip and the grid's Per second cell. A true 0 still reads 0, and a gauge's chart prints as it did. get_perfmon_trend and get_perfmon_stats rounded per_second to 4 decimals, so one count in a day-wide bucket (86,400 s) published as a rate of 0. Both apps build their payloads through TrendPayloads, which now rounds a rate to 4 decimals, or to two significant digits where 4 decimals would keep fewer. The web test harness now runs the shipped charts.js, so the pins read the real axis labels and tooltips.
erikdarlingdata
marked this pull request as draft
October 1, 2026 01:41
erikdarlingdata
marked this pull request as ready for review
October 1, 2026 01:43
erikdarlingdata
marked this pull request as draft
October 1, 2026 02:02
The cell is the raw counter value, which counts from the counter's own start: an instance restart, or a database's own restart for a per-database counter. "Since start" did not say whose start.
Every scripted rate had two significant digits, so changing fmtRate to keep three left the suite green. Two new rows, 0.2333 and 1234.5678, pin a rate below 1 at two significant digits and a rate of 1 or more at two decimals, in the grid cell and in the chart tooltip.
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.
Why
A rate counter, such as Batch Requests/sec, stores a running total that has grown since the counter started. The web Server > Activity > Perfmon Counters grid showed that total in its Value column. Batch Requests/sec showed 11,641 next to a delta of 66, so it read as 11,641 batches a second. The trend chart under the grid drew the same total as its Value line, and that line only ever climbs. The
get_perfmon_statsMCP tool in both apps sent the total and the delta, but no rate.The desktop charts already plot a rate counter per second through
DeltaSeriesShaping(#3653). This change uses that same division everywhere else.What changes
DeltaSeriesShaping.PerSecond(delta, seconds)is now the only per-second division. It returns the delta over the seconds when the span is positive, and null otherwise.Shaperates every chart point through it, so the desktop charts plot the same numbers as before.get_perfmon_stats, Lite and Darling. A rate row addsper_second: its delta over the seconds since the previous collection. When no delta was knowable, it is null, not 0. That happens on a first collection, a counter reset or a restart. A gauge row, or any row that is not a rate, has noper_secondkey. This follows the trend payload'speak_per_second. Both apps build each row with one shared builder,TrendPayloads.PerfmonLatestRow, so the two cannot drift apart. Both latest-snapshot reads now also selectsample_interval_seconds, last, aftercntr_type.get_perfmon_statsgoes from 521 to 579 characters. Its guide text says the same at more length.get_perfmon_trend, Lite and Darling. Each rate point addsper_second, using the same division over the bucket's delta and seconds. It is null where the interval is 0. The head is unchanged. The guide text names the field.per_secondandpeak_per_secondthroughTrendPayloads.RoundRate. It keeps four decimals. A rate under 0.001 keeps two significant digits instead.bucket_minutescan be as wide as a day, 86,400 s. One count in a bucket that wide is 0.0000116 a second. Four decimals published that as 0, so a nonzero delta read as no activity. It now publishes as 0.000012. A rate of 0.001 or more rounds as before. No tool description changes, so no budget pin moves./sunit. Its axis labels and its tooltip print through the newrateformat. One deadlock in a 300 s collection is 0.0033 a second. Two decimals printed that as 0 on every axis label and in the tooltip. Now the axis runs from 0 to 0.004 and the tooltip reads 0.0033. Every other counter draws the same lines and prints the same numbers as before, so a gauge still plots its reading.rateformat.fmtRateinutil.jsprints a number from 1 up with up to two decimals, and a number below 1 with two significant digits. Only a true 0 prints as 0. It is registered asrateinFORMATTERS, andpanels.jsright-aligns aratecolumn. The custom view editor's format list mirrors the registry, so it offersratetoo.MeasurementContractCensusTestschecks every payload key whose name says per second. Its key sweep matched only names with a prefix, such aspeak_per_second. A key named justper_secondwas outside the sweep. It is inside it now, and both new keys pass as calls to the rate helper with the interval. A line in the census's own test file proves it sees a bare key, and that a bare key over a stored delta still fails.Which component(s) does this affect?
Test plan
PerfmonCounterTypeReadTests.ARateCounter_CarriesItsPerSecondFigure_AndNoOtherKindDoes. Batch Requests/sec getsper_second0.22 (66 over 300 s) and keeps value 11,641. A rate with an interval of 0 gets null. A row with no stored type whose name ends in/secgets 0.2. A gauge, an average numerator and a row with no type and no/secname have no key. The trend's rate points carry null, then 0.22, and its gauge points carry no key.PerfmonPerSecondPayloadTests(new, 6 tests). They run the tool's own row projection. The keys come out in the order they had before, withper_secondlast. Two of them cover the rounding: a counter that seldom fires, and a day-wide trend bucket through the shared trend builder.PerfmonSql_LatestSnapshot_ValueAndDeltachecks that the latest read selectssample_interval_seconds.EveryDarlingPerfmonRead_SelectsTheType_AndTheRowsKeepNullAsNullchecks that it comes right aftercntr_typein both apps' latest reads.PerfmonCounterTypeLivePostgresTestsnow assertsper_second0.2333 on the rate row (70 over 300 s). It also asserts no key on the gauge row, andper_secondon each rate trend point. It needsDARLING_TEST_PG, so it runs in CI and was skipped here.WebPerfmonPerSecondBehaviourTests(new, 10 tests). They run the shippedserver-tabs.js,util.js,panels.jsandcharts.jsunder Node throughweb-perfmon-harness.mjs, against scripted rows. The harness runs the real chart renderer, so the axis labels and the tooltip text are the ones a browser prints. Five grid tests check the headers, the number alignment and the cells. They cover a rate, a rate with no knowable delta, a gauge and an average row. They also cover a rate that seldom fires, at 37 since start and 0.0033 a second. A fifth test pins the digit rules of therateformat: 0.2333 a second prints as 0.23, and 1234.5678 a second prints as 1,234.57. Five chart tests check the lines, the axis labels and the tooltip. They cover a rate, one deadlock in 300 s, one deadlock in a day-wide bucket (86,400 s) and a gauge. A fifth test pins the same two digit rules in the tooltip.PerfmonPerSecondRoundingTests(new, 5 tests, Lite.Tests) pinsRoundRatevalue by value. It publishes a one-count bucket at every width on the bucket ladder, including the day-wide maximum. No width reads 0, and each rate stays within the rounding of delta over seconds. The last two tests cover the peak rate and the latest-snapshot row.DeltaSeriesShapingTests.PerSecond_IsTheDeltaOverTheStoredInterval_AndNullWhereTheChartBreaksItsLinecovers the division's edge cases, and checks that it equalsShape's value at the same point.2b6578f99. These runs used the product files from that commit and the new tests:PerfmonPerSecondPayloadTests,PerfmonPerSecondRoundingTestsand thePerSecondtest call code that is new in this change, so they do not build against the base commit. I set them aside for the run above.git statuswas clean after it:per_secondfor every counter:TheTrendChart_ForAGauge_IsUnchanged [FAIL].ThePayloadKeyVerdicts_FireOnEveryShape_AndThePlantedPassthroughFails [FAIL].AGaugeRow_AndAnAverageRow_KeepTheirNumbers [FAIL].TheTrendChart_ForAGauge_IsUnchanged [FAIL].TheTrendChart_DrawsARateCounterPerSecond [FAIL].MeasurementContractCensusTests.EveryRateHelper_DividesByTheIntervalItIsHanded. The firstPerSecondcalledShapeand had no visible division.PerSecondnow does the division, andShapecalls it.RoundRatesat inDeltaSeriesShaping. The census read the class name as a stored delta and failedEveryPerSecondPayloadKey_IsAReaderRateOrACSharpQuotient_OrIsNamedonpeak_per_second. The helper lives inTrendPayloadsnow, and the census passes.Worth checking
value.Average wait time (ms)under Lock waits) still shows the 0 stored as its delta after a restart. Its kind is not a rate, so this change does not touch it.per_secondandpeak_per_secondare rounded to 4 decimal places. A rate under 0.001 keeps two significant digits instead. A rate under 0.0001 now prints in exponent form in the JSON, for example1.2E-05. That is valid JSON, and both the web page and the MCP clients read it as a number.rate, because that list mirrors the formatters.Checklist
dotnet build -c Debug)CHANGELOG
SECTION: Fixed
ENTRY:
get_perfmon_statsandget_perfmon_trendMCP tools add aper_secondfield for rate counters, in Lite and in Darling. A single count in a day-wide trend bucket no longer reads as a rate of 0.REF:
[Perfmon rate counters show a per-second rate in the web grid, its trend chart and both MCP tools #4883]: Perfmon rate counters show a per-second rate in the web grid, its trend chart and both MCP tools #4883