Repository navigation
skip the blocked-process/deadlock XE ring-buffer shred when nothing new arrived (#4200) - #4212
Merged
Merged
Conversation
…n_count (#4200) Both collectors cast+shred their whole ring buffer on every run even though a healthy estate finds nothing new almost every time (0.02% / 1.35% of runs stored a row, measured on a production fleet). Read execution_count from sys.dm_xe_session_targets before the cast and skip the cast+shred when it matches the prior cycle's own reading; any other case (grew, fell, unreadable, no prior) shreds exactly as before, so stored rows are unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
erikdarlingdata
marked this pull request as ready for review
September 25, 2026 03:31
This was referenced Sep 25, 2026
erikdarlingdata
added a commit
that referenced
this pull request
Sep 26, 2026
…nt (#4213) (#4371) * Gate the deprecated Dashboard's XE ring-buffer shred on execution_count (#4213) install/22_collect_blocked_processes.sql and install/24_collect_deadlock_xml.sql cast the whole ring_buffer target to XML and shred it on every scheduled run, even when nothing new has arrived (#4200's defect, fixed for Darling and Lite in #4212). Both collectors now read the target's own execution_count before the cast and compare it against a value carried in a new config.xe_shred_state table; a match skips the conversion and shred entirely. A missing count, no stored count, or a count that has gone down (session restart) still does a full read. Adds config.xe_shred_state (install/03_create_config_tables.sql) and a source-guard test class (deprecated/Installer.Tests/XeShredGateSqlTests.cs) that pins the gate's presence in both collector scripts and fails against the pre-fix file. Also brings deprecated/Installer/README.md:3 in line with the root README's v3.3.0 release-assets note. Closes #4213. * Add two upgrade-path pins and fix an undeclared @shred_needed in the XE gate (#4213) XeShredGateSqlTests gains two source-text assertions: config.xe_shred_state is created with the repo's existing IF OBJECT_ID(...) IS NULL idiom, and a missing state row means both collectors fail open to a full read rather than skipping real data. Measuring the gate against a throwaway container turned up a real defect the pins do not cover: @shred_needed was referenced inside the dynamic SQL string in both collectors' server-scoped and Azure branches but never declared there, so every live run threw 'Must declare the scalar variable' and fell through to the CATCH block. Declared it locally in each dynamic batch. * Add source-text pins for the dedupe predicate and dynamic-SQL DECLARE coverage (#4213) XeShredGateSqlTests gains two source guards: CollectorsDedupeReShreddedEventsOnInsert pins the NOT EXISTS predicate both collectors' payload INSERT already carries (unchanged since before this PR, matching origin/dev) so a changed execution_count's full re-shred does not re-insert events already stored. Confirmed present at head for both platform branches of both collectors, so no dedupe fix was needed in this PR -- the predicate already exists. DynamicSqlBatchesDeclareOrParameterizeEveryVariableTheyReference parses each sp_executesql call's SQL text and params string and asserts every @variable referenced inside the SQL text is either DECLAREd there or listed as a parameter. Confirmed RED against f8ded1e (this PR's own first commit, before the DECLARE fix) with the exact failure the live run hit: @shred_needed referenced without a local DECLARE in all 4 dynamic batches. GREEN at head.
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.
Closes #4200.
Why
blocked_process_reportanddeadlockseach read a dedicated, single-event XE ring-buffersession. Every run cast the target's
target_datato XML and shredded it with.nodes(...),then filtered by timestamp. This ran whether or not anything new had arrived. The issue
measured a production fleet (43 servers, 6 hours). Only 0.02% of
blocked_process_reportrunsand 1.35% of
deadlocksruns stored a row. The rest paid the full cast and shred to storenothing. That cost about 4 hours a day of SQL time on the monitored servers of one estate.
What changes
Both collectors now read the session target's own
execution_countbefore the cast, in thesame batch. The read comes from
sys.dm_xe_session_targetsjoined tosys.dm_xe_sessionsbysession address (the Azure SQL DB database-scoped twins on the per-database path). Each of
these two sessions carries exactly one event type. That is confirmed against
DarlingXeSessions.cs: oneADD EVENTper session. So the counter is an exact signal forwhether anything arrived since the last run, and reading it costs one integer with no XML
materialized.
The cast and shred now run only when the counter does not exactly match the prior cycle's own
reading of it. Every other case shreds exactly as before. A first run has no stored prior. A
restarted host or a store that lost the row also has no stored prior. The counter can be
unreadable this cycle when the session or target is missing or mid-provisioning. The counter
can grow, the ordinary case with new data. The counter can fall when the session restarted or
was recreated, so the buffer no longer matches what the prior was measured against.
Only an exact match skips the cast and shred. When it does skip, the rest of the batch still
runs: the shred step, the object-resolution updates, the final projection. It runs against an
empty source and produces the same zero rows a full shred of an unchanged buffer produces. The rows a run stores stay identical to today's. The gate only skips runs that stored nothing.
Each collector's own
ReadAsyncalways reads a new trailing result set(
execution_count,gated) throughNextResultAsync, before it returns. It writes theobserved count to
CollectorContext.PendingState. That carries this cycle's count forward asnext cycle's
@last_execution_count, whether or not this cycle shredded. On Azure SQL DB thestate is keyed per database through
XeShredGate.KeyFor. Each database's session dispatchesindependently. This mirrors
PerDatabaseWatermarkColumn, which both collectors already usefor the same reason.
The
deadlockscollector's Azure path also reads a durable, file-backed telemetry blob(
sys.fn_xe_telemetry_blob_target_read_file) in aUNION ALLalongside the ring-buffer read.That branch has no
execution_countof its own. It is not gated and keeps running on everycycle exactly as before. A pin (
Deadlocks_Azure_TelemetryBlobBranch_IsNotGated) checks thatthe telemetry read sits outside the gate's
IFblock.Both collectors are shared definitions in
PerformanceMonitor.Collectors. Darling and Liteboth run them through the same
BuildQuery/ReadAsync. There is one implementation, so Liteparity holds by construction here rather than needing a second copy kept in sync. Lite's own
files (
RemoteCollectorService.*.cs) own only XE session lifecycle, not the read.Deprecated Dashboard.
install/22_collect_blocked_processes.sqlandinstall/24_collect_deadlock_xml.sqlhave the identical pattern: an unconditionalTRY_CAST(xet.target_data AS xml)thenring_buffer.nodes(...), confirmed by grep. Per thebrief, this PR does not touch install scripts. Flagging it here for the coordinator to decide
whether the deprecated Dashboard still gets the same fix.
Files
PerformanceMonitor.Collectors/XeShredGate.cs(new): the shared, pure gate. It hasKeyFor,ReadLast,ShouldShred, andToStateValue.PerformanceMonitor.Collectors/BlockedProcessReportCollector.csandPerformanceMonitor.Collectors/DeadlocksCollector.cs: the gate SQL,StateKeys, thetrailing result set, and
ReadAsync's consumption of it.@last_execution_countparameter and the new trailingresult set's position ahead of the existing probe-failure set. See Test plan.
Test plan
Lite.Tests/XeShredGateTests.cs(
ShouldShred_MatchesTheFiveGateShapes, a[Theory]) cover all five shapes from the issue.A first run with no current and no prior count shreds. No stored prior shreds. An unreadable
current count shreds. An unchanged count skips. A count that grew shreds. A count that fell
shreds. The same file also pins
ReadLast(an absent, blank, or malformed stored value allread as no prior) and
KeyFor(the server-scoped key versus the per-database compound key,with no collision between them).
BlockedProcessReport_BuildQuery_ReadsExecutionCountBeforeTheCastandDeadlocks_BuildQuery_ReadsExecutionCountBeforeTheCastcover both the server-scoped andAzure query text. Each asserts the
execution_countread and theIF @shred_needed = 1gate appear in the generated SQL text before
TRY_CAST(xet.target_data AS xml). A furtherpin checks the telemetry-blob branch stays outside that
IFblock. A parameter-wiring pin(
BuildQuery_PassesTheStoredPrior_PerServer_AndPerDatabase) checks that a value stored incontext.Statereaches the bound@last_execution_countparameter.ReadAsyncround-tripsPendingState. Unit tests usingFakeCollectorDataReader.WithResultSetsproveReadAsyncreads the trailing(execution_count, gated)result set and writesPendingState, for both the server-scopedand the per-database key. A NULL current count writes nothing, so a bad read this cycle
cannot fabricate a false "unchanged" reading next cycle.
Measure(ShredGatedMeasurement, ...)reflects the gate's owngatedbit.XeShredGateand every SQL fragment the pinscheck for (
@last_execution_count,@shred_needed,@execution_count) do not exist onorigin/dev. Every new test fails to even compile there. That is the strongest form of red
for a purely additive change.
the full suite. They sit outside the original brief, but are mechanical and land in files
this PR already touches.
CollectorMeasurementSeamTests.EveryMeasurementLabelInTheCollectorsIsLegalandCollectorStateContractTests.TheCollectorsDeclaringStateArePinnedin Darling.Tests, plusthe matching Lite.Tests pin in
DefaultTraceEventsCollectorDefinitionTests.cs, are censustests. Each lists every collector-declared measurement label or state key by hand. Both
got the new
shred_gatedlabel and the two collectors' newStateKeysentry added.DarlingBlockedProcessReportUnresolvedReasonTests.ReadAsync_Stops_At_Its_Payload_So_The_Trailing_Set_Survives_For_The_Hostand its Lite.Tests mirror in
BlockedProcessReportUnresolvedReasonTestsbuild a realmulti-result-set reader. They prove
ReadAsyncstops at the payload and leaves theprobe-failure set for the host's
ReadPayloadProbeFailuresAsync. The new gate result setnow sits between those two sets. Both fakes needed a third table added at the right
position. Without it,
ReadAsync's own newNextResultAsynccall would consume theprobe-failure set by mistake instead. Both tests failed right after the production change
and before the test-fake fix, and both pass after it.
BuildQuery_PinsWatermarkCutoff_AndTenMinuteFallbacktests, inBlockedProcessReportCollectorDefinitionTestsandDeadlocksCollectorDefinitionTests,asserted exactly one bound parameter. Both now select
@cutoff_timeby name and assert acount of two parameters.
Darling.TestsandLite.Tests. Both report0 Warning(s),0 Error(s).Darling.Tests: Total 13741, Errors 0, Failed0, Skipped 689, Not Run 1, 93.2s.
Lite.Tests: Total 5332, Errors 0, Failed 0, 255.6s.The local sql20xx instances count as monitored, and no other instance was available. So the
actual server-side behavior (unchanged skips the cast, changed does not) is unverified
against a live engine, as opposed to the SQL-text-order pin and the pure decision-table
tests above.
sys.dm_xe_session_targets.execution_count's semantics come from the issue'sown research, not independently confirmed here.
BuildQueryandReadAsyncare a single shared implementation inPerformanceMonitor.Collectors. Lite's own files own only XE session lifecycle. So theLite.Tests coverage above exercises the exact code both products run. Darling.Tests was
still touched, and is green, for the two census pins the new code affects.
CHANGELOG entry
SECTION: Fixed
ENTRY:
REF:
[skip the blocked-process/deadlock XE ring-buffer shred when nothing new arrived (#4200) #4212]: skip the blocked-process/deadlock XE ring-buffer shred when nothing new arrived (#4200) #4212
🤖 Generated with Claude Code
https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3