Skip to content

Lite's plan-cache trend descriptions still rated every point over the gap since the previous one, the shared unrated_note named one of two unrated reasons, and the latch/spinlock restart row was spelled two ways across SKUs: Lite carries #3700's three-state sentences, the note names the stored-0 restart beside the first-in-window LAG byte-identically on both SKUs, and both tools on both SKUs publish the unknowable row as null rates with interval_seconds beside them (partial #3653) - #3706

Merged
erikdarlingdata merged 1 commit into
devfrom
fix/3653-trend-latch-parity
Sep 19, 2026

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

Two Lite descriptions that #3695 made false, a shared sentence that named one of two reasons, and one restart row spelled two ways

Partial #3653 — three clauses of A15/A16 plus the A11 residue, each named last night by a lane that could not touch the file (#3700 named the first two, #3702 the third). Does not close the issue. A fourth clause (#3703's truncated-carries-two-facts finding) was measured and deliberately NOT done; the count and the reason are at the end.

1. Lite's plan-cache trend descriptions said every point was a rate over the gap since the previous one

Lite/Mcp/McpQueryTools.cs get_query_duration_trend and get_procedure_duration_trend, and the get_query_duration_trend row in Lite/Mcp/McpInstructions.cs, still said "Every point is a rate over the gap since the PREVIOUS point, so the window's first collection … carries null rates". Lite's reads have been three-state since #3695 (GetQueryDurationTrendAsync) and v61 (GetProcedureDurationTrendAsync): MAX(sample_interval_seconds) per collection, NULLIF(…, 0) so a restart collection is UNRATED, the LAG only where the store never recorded an interval — verified at the source before editing (LocalDataService.QueryStats.cs:1178–1180, :1273–1275). A caller reading the old sentence would take a null on a mid-window point as a bug, when it is the restart the description never mentioned.

Both descriptions now carry #3700's Darling sentences minus the hourly-route clause, plus one clause saying Lite has one tier. get_query_store_duration_trend's description (LAG-only, no rollup on Lite) was still true and is untouched. The get_procedure_duration_trend instructions row gains six words pointing at the same rule.

2. unrated_note named one of the two reasons a point goes unrated

The trio's shared envelope sentence — DarlingMcpTrendTools.SerializeTrend and Lite McpQueryTools.SerializeTrend, byte-identical and pinned so by McpMissMessageParityPinTests — said "a per-collection rate is the work since the PREVIOUS collection … and the window's first collection has no previous one inside the window". Since #3695/#3700 a stored-0 restart collection is ALSO unrated on both plan-cache trends, on both SKUs, and the note explained a null on that point with a reason that did not apply to it.

One sentence now names both: the collection's STORED sample interval is 0 (a restart or counter reset: the collector could not difference its two snapshots, so the zeros beside it were never measured), or the point is rated against the PREVIOUS one and has none inside the window (the window's first collection where no interval was stored, or one landing in the same second as its predecessor). It stays true on the Query Store trend, which stores no interval and can only hit the second arm — every one of its points is "a collection where no interval was stored". Unknowable is not 0 is kept verbatim: McpZeroIsAMeasurementTests.TheTrendTools_CountAndExplainUnratedPoints anchors on it.

Three pins moved deliberately: the literal in McpMissMessageParityPinTests (60/60 executed — the fragment must appear in BOTH SKUs' trees, which is the byte-identity proof), and the two substring pins in DarlingPerformanceTrendsReadTests and PerformanceTrendsToolTests, which now assert both fragments — the reason their seed exercises (first-in-window LAG) and the one it does not (stored 0) — so a rewording that drops either reds there and not only in the parity pin. The stale comment above Darling's note ("it still names only the first-collection case; widening it is a both-SKU edit") is replaced by the reason the sentence carries both arms.

3. The latch/spinlock restart row was spelled two ways across SKUs

Read at the source, the two twins were never one shape: Darling's get_latch_stats / get_spinlock_stats are WINDOW AGGREGATES (total_delta_* summed over hours_back, rates and the band from the LATEST interval), Lite's are the newest snapshot (cumulative counters plus the last interval's deltas). The class remark on Darling's tool file says so. What the brief's finding actually named is narrower and real: the row whose latest interval was unknowable — the (0, 0) restart marker beside a stored sample_interval_seconds = 0 — was published as

One vocabulary, argued rather than picked. The brief preferred Darling's shape (interval_seconds beside the delta — it says WHY the rates are null). Half of that is right and adopted: interval_seconds is the why-key, and Lite gains it, with Darling's per-second pair beside it. The other half of Darling's shape — the raw 0 published as the delta — is the #3642 lie both viewers' snapshot grids stopped rendering last night (#3702: "—" with "restart / first sample" beside it) and Lite's tool stopped publishing in the same PR; adopting it would revert that a night later and put the MCP payload at odds with the grid on the same SKU. So the row is spelled, on both tools and both SKUs: rates null, interval_seconds null beside them, the latest delta null — the delta says the numbers were never measured, the interval says why, and neither is inferred from the other. Concretely:

Why not sample_interval_seconds for the key. Six sites on the wire (get_file_io_stats, get_perfmon_trend, get_memory_grants, both SKUs) publish the stored interval under the column's name; two (Darling's latch block, the PG trends) say interval_seconds. The latch/spinlock tools already spelled it interval_seconds on Darling and this PR makes the pair agree with itself across SKUs; collapsing the two spellings across the whole surface is a vocabulary pass of its own (residue, noted for the coordinator).

What severity: null changes for a reader. server-tabs.js's LATCH_COLUMNS renders severity through statusSev → statusToSev(String(status || "")) → the default arm, and applyFormat prints — for a null — no JS edit needed and none made. DarlingMcpInstructions.cs's latch row ("severity is banded from the LATEST interval only") stays true and is untouched.

The pins

  • Darling.Tests/McpPageContractTests.TheSameToolName_SpellsTheUnknowableRowTheSameWay_OnBothSkus (new) — the TheSameToolName_EmitsTheSamePageContractKeys_OnBothSkus idiom for the two tools whose rows are not the same shape: per tool, the set of interval_seconds / *_per_second keys emitted by each SKU's projection is exactly the expected three, and each description on each SKU names interval_seconds and says the row is unknowable. Matcher witnessed both ways (avg_wait_ms_per_request, spins_per_collision are ratios, not rates, and do not match).
  • Lite.Tests/McpPageContractTests.GetLatchStats_PublishesNullDeltas_OnTheRestartMarkerRow (extended) and GetSpinlockStats_PublishesNullDeltasAndRates_OnTheRestartMarkerRow (new) — real DuckDB through the real tools, three interval states per tool: the marker null on every key that spells it; the measured row rated over its STORED 60 s (4,000 ms / 100 requests → 66.67 ms/s, 1.67 waits/s; 4,000 collisions / 1,000 spins → 66.67, 16.67 per second); the pre-v60 row keeping its deltas with the interval and rates null.
  • DarlingMcpLatchSpinlockToolsLivePostgresTests (extended) — the same three states over Postgres through the real tools: a class whose newer row stores 60 s while the pair is spaced 120 s apart (6,000 / 60 = 100 ms/s proves the stored path; 50 would have proved the LAG ran where it should not), a pre-V127 pair rated by the 120 s LAG (16.67), and the marker: interval_seconds, both rates, severity, severity_banded_from.delta_wait_time_ms and .interval_seconds all null, total_delta_wait_time_ms honestly 0, the row still on the page. Spinlocks: 60,000 / 60 = 1,000 and 300,000 / 60 = 5,000 per second on the stored row, three nulls on the marker.
  • DarlingMcpLatchSpinlockToolsSurfaceAndSqlTests — both SQL pins gain END AS latest_interval_seconds / l.latest_interval_seconds; new TheUnknowableRow_IsNullOnEveryKeyThatSpellsIt_InBothProjections pins the projection shape from source (the null predicates present, the retired spellings absent, the false class remark gone).
  • McpLatestSnapshotStampTests.LatchSeverity_NamesTheIntervalItWasBandedFrom — its anchor on delta_wait_time_ms = r.LatestDeltaWaitTimeMs moved to the guarded spelling, on purpose.

What this PR does NOT do (clause 4, measured)

truncated on the trend family (get_query_duration_trend / get_procedure_duration_trend / get_query_store_duration_trend / get_query_trend, both SKUs) and on get_query_store_top means the #2364 WINDOW floor, not #3594's page cut. The brief said rename it (window_truncated) if the blast radius stayed inside ~6 consumers, else stop and count. Counted: ~15. Emitters and descriptions: DarlingMcpTrendTools.cs (TrendDisclosure.WriteTo, get_query_trend's anonymous payload, three doc paragraphs), Lite McpQueryTools.WriteDisclosure, four McpInstructions.cs rows, four DarlingMcpInstructions.cs rows, and get_query_store_top — which lives in DarlingMcpDataTools.cs:765–848, not in the trend tools file the brief named, and outside this lane's fence. Consumers: server-tabs.js:379 (drawQueryTrend reads trend.data.truncated). Tests: PerformanceTrendsToolTests (4 asserts), DarlingPerformanceTrendsReadTests (4 asserts + the six-key order block), TrendEmptyParityToolTests (4), QueryStoreTrendRoutingLiveTests (3, live), McpZeroIsAMeasurementTests:784, QueryStoreTopWindowTests:123, DarlingMcpTrendToolsTests (prose), and McpPayloadContractCensusTests' CutNoteKeys roster + class residue paragraph. Lite's envelope DOES publish the key (the brief's hypothesis that it might not was wrong — WriteDisclosure sets it off TruncationSlack), so the rename is both-SKU. Stopped, per the brief; ships 1–3.

Also not done: the frozen deprecated/Dashboard twins (a semantics upgrade, not a bug fix, on a frozen twin); sample_interval_seconds vs interval_seconds across the six other wire sites; CHANGELOG.md; any PgTarget*, collector, perfmon, MeasureCatalog or DarlingTrendReader file.

Executed here

Darling.Tests built net10.0-windows, run in-process with the WindowsDesktop framework entry stripped; the live classes against a throwaway timescale/timescaledb:2.28.1-pg18 (Docker context desktop-linux, migrated by PgMigrations, torn down): DarlingMcpLatchSpinlockToolsSurfaceAndSqlTests 24/24, DarlingMcpLatchSpinlockToolsLivePostgresTests 1/1 (live), DarlingPerformanceTrendsReadTests 1/1 (live), McpPageContractTests 43/43, McpLatestSnapshotStampTests 34/34, McpZeroIsAMeasurementTests 20/20, McpPayloadContractCensusTests 49/49, DarlingMcpTrendToolsSurfaceAndSqlTests 43/43, DarlingMcpToolsTests 16 (1 pre-existing skip), AsOfWindowAnchorTests 29, RepoFileAdoptionTests 2, CommentFilterAdoptionTests 4, ServerPageTabsTests 17, FleetIdentifierScrubTests 2, DocCommentHygieneTests 77 — green. Lite through a throwaway net10.0-windows harness Compile-Including the classes: McpMissMessageParityPinTests 60/60, PerformanceTrendsToolTests 5/5, TrendEmptyParityToolTests 5/5, McpLatestSnapshotStampTests 12/12, McpDescriptionTruthPinTests 4/4, McpPageContractTests 19/21 and DeltaFamilyUnknowableRowReadTests 11/12 — the three reds are the known IgnoredWaitTypes.Load() → PresentationFramework platform artefact on untouched wait-stats reads, reproduced on dev by #3702/#3703.

Builds -c Release -p:EnableWindowsTargeting=true: PerformanceMonitor.Darling.Service, PerformanceMonitorLite, Darling.Tests — 0 warnings; Lite.Tests — dev's one pre-existing xUnit2000 at LogTailOverlapThresholdPinTests.cs:95, untouched. No JS edited (node --check not applicable); zero hits for the retired spellings (no previous one inside the window, per-collection rate is the work since) across the tree.

First executed in CI: the rest of Lite.Tests. No SQL const changed except the spinlock reader's one added column, executed above.

Refs #3653 (partial — A15/A16 Lite trend descriptions + unrated_note + latch/spinlock unknowable-row parity; A11's Lite description residue). Review-dark window: auto-merge NOT armed; self-review in the lane report.

Which component(s) does this affect?

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

How was this tested?

See Executed here: in-process on this Mac, the two live classes against a throwaway PostgreSQL 18 + TimescaleDB 2.28.1 store; no SQL Server involved (store-side reads and descriptions only).

Checklist

  • I have read the contributing guide
  • My code builds with zero warnings (dotnet build -c Release -p:EnableWindowsTargeting=true)
  • I have tested my changes against at least one store (PostgreSQL 18 + TimescaleDB 2.28.1 throwaway; DuckDB through the Lite harness)
  • I have not introduced any hardcoded credentials or server names

…ated over the gap since the previous one, the shared unrated_note named one of the two reasons a point goes unrated, and the latch/spinlock restart row was spelled two ways across SKUs: Lite's descriptions carry #3700's three-state sentences, the note names the stored-0 restart beside the first-in-window LAG byte-identically on both SKUs, and both tools on both SKUs publish the unknowable row as null rates with interval_seconds beside them (partial #3653)

Three clauses of #3653 A15/A16 plus the A11 residue, each named by a lane last
night that could not touch the file.

Lite/Mcp/McpQueryTools.cs get_query_duration_trend / get_procedure_duration_trend
and the McpInstructions row said "a rate over the gap since the PREVIOUS point";
Lite's reads have been three-state since #3695 (query) and v61 (procedure). The
descriptions now carry #3700's Darling sentences minus the hourly-route clause
(Lite has one tier). get_query_store_duration_trend's description was still true
(LAG-only) and is untouched.

The trio's shared unrated_note said only "no previous one inside the window"; a
restart collection (stored interval 0) has also been unrated on both plan-cache
trends since #3695/#3700. One sentence now names both, byte-identical on both
SKUs; McpMissMessageParityPinTests holds the literal, and the two substring pins
(DarlingPerformanceTrendsReadTests, PerformanceTrendsToolTests) pin both reasons.

Lite's get_latch_stats / get_spinlock_stats published a bare null delta on the
restart row (#3702); Darling's published the raw 0 as the delta the band came
from, interval_seconds: null, null rates, and a LOW severity banded from that 0.
One vocabulary now: null rates + interval_seconds beside them (null when
unknowable) + the latest delta null, on both tools and both SKUs. Lite gains
interval_seconds and Darling's per-second pair per row; Darling's latch row gains
row-level interval_seconds and nulls severity and severity_banded_from.delta_wait_time_ms
on an unknowable interval; Darling's spinlock query gains the latch query's own
latest_interval_seconds line so its null rates carry the why too.
DarlingMcpLatchSpinlockTools.cs's class remark no longer says the delta
collectors store no sample_interval_seconds (false since V127, #3595).

McpPageContractTests.TheSameToolName_SpellsTheUnknowableRowTheSameWay_OnBothSkus
holds the key sets equal from source; Lite's McpPageContractTests execute the
three interval states over DuckDB; DarlingMcpLatchSpinlockToolsLivePostgresTests
executes them over Postgres (stored 60 s rated 6000/60 not 6000/120, the LAG
pair, the (0, 0) marker null on every key).

Not done: the trend family's `truncated` (the #2364 window floor) is not renamed
- the blast radius is ~15 consumers including get_query_store_top in
DarlingMcpDataTools.cs, outside this lane. Reported.
@erikdarlingdata
erikdarlingdata merged commit 78f60bf into dev Sep 19, 2026
7 of 8 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/3653-trend-latch-parity branch September 19, 2026 09:11
erikdarlingdata added a commit that referenced this pull request Sep 20, 2026
… the page dialect's truncated: get_query_trend, the duration-trend trio and get_query_store_top publish the store's reach under its own key, every consumer and description follows, and the vocabulary census holds the two facts apart (partial #3653 — item 17) (#3793)

`truncated` carried two facts on the wire. On twenty-odd paged tools it is the caller's `limit` biting — observed off a `cap + 1` fetch, beside `*_returned`, remedied by a bigger limit. On the #2364 / #2353 trend family and on get_query_store_top it was the store's REACH: the served series begins later than the requested start because the tier that answered no longer holds the window's head, beside `effective_start` / `effective_hours_back`, and no limit changes it. #3703 classified the homonym and left it as stated residue; #3706 counted the blast radius (~15) and stopped. This is that rename.

Emitters (4): DarlingMcpTrendTools get_query_trend initializer + TrendDisclosure.WriteTo (the trio, data and empty envelopes); DarlingMcpDataTools get_query_store_top (truncation_note keeps its name — it is the prose for this flag); Lite McpQueryTools.WriteDisclosure (the trio + get_query_trend). Consumers: server-tabs.js drawQueryTrend reads window_truncated (undefined is falsy — the old key would have silently dropped the notice). Descriptions: McpHelpers.WindowTruncatedDescription, one shared clause on the nine publishing tools (5 Darling, 4 Lite), naming the key, the reach beside it, and the WIRE CHANGE; nine instructions rows and five web-catalog rows say window_truncated. C# members (Truncated, TruncationSlack, DescribeCoverage) keep their names — the wire key renamed.

Census: McpPayloadContractCensusTests classifies window_truncated as the second bound it is (SecondBoundCutKeys), reads the ordered-envelope idiom (`envelope["key"] =`) the disclosure block uses so Lite's writer is in the roster, and adds TheWindowFloor_IsSpelledWindowTruncated_BesideItsReach_AndNeverBareTruncated_OnBothSkus: every block on both SKUs that writes effective_hours_back must write window_truncated and effective_start and never bare truncated — brace-walked over strings-blanked code, rostered as an equality on (file, idiom, count), checked first on a synthetic offender and a synthetic page cut. The discriminator is the reach vocabulary, not window_start: get_query_heatmap publishes window_start beside a bare truncated that IS a page cut. EveryWindowFloorTool_CarriesTheSharedClause_AndNoOtherToolDoes is the description half. The residue sentence is gone.
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