Repository navigation
Fix query_store probe re-examination and query_stats/procedure_stats drain stalls - #3394
Conversation
| fleet recurrence) and why the TTL is the same GuardHours the touch guard already treats | ||
| as an acceptable staleness bound for these rows. Cache hits are implicitly resolved and | ||
| current; only misses reach the probe below. */ | ||
| var toProbe = _planProbeCache.SelectNeedingProbe( |
There was a problem hiding this comment.
Traced through this wiring (and the text-side twin at ~3775) against QueryStoreProbeCache/QueryStoreFetchProbe — the tuple-shape mismatch between references (PlanId/PlanHash) and SelectNeedingProbe's (Id, Hash) tuple is just a positional implicit conversion, not a bug, and the missing/hashById bookkeeping looks correct for cache hit, miss, hash-mismatch, and expiry cases.
One gap though: QueryStoreProbeCacheTests only unit-tests QueryStoreProbeCache in isolation, and the IL-scanning pins this PR's description calls out (ProbeDenominatorTests, FetchStoreConnectionBorrowTests, PlanFetchCarryoverKeyTests) are structural — they check that certain calls/setters exist in the compiled IL, not that the cache is wired correctly at runtime. Nothing in the test suite actually exercises FetchAndStorePlansAsync/FetchAndStoreQueryTextAsync with a cache-warm second call to confirm a cache hit really skips TouchAndProbePlansAsync/TouchAndProbeTextsAsync and still drains missing correctly. Given this codebase's heavy investment in regression-pinning tests for exactly this kind of call-site behavior, this integration path (probably needs DARLING_TEST_PG) seems like a natural candidate for one.
| /// procedure_stats, via <c>sys.dm_exec_text_query_plan</c>) is allowed to be before the collector | ||
| /// ships NULL for that row instead of the XML. | ||
| /// | ||
| /// <para><b>Why this exists:</b> both collectors already bound row COUNT with <c>TOP (200)</c>, but |
There was a problem hiding this comment.
Minor: this doc comment's <para> tags are unbalanced (8 opens vs 5 closes) — the paragraphs starting "Why this exists:", "512 KB is a measured choice…", and "Why the cap's win is bigger…" each open a <para> that's never closed before the next <para>/</summary>. Doesn't break the build since none of the csproj files in this chain set GenerateDocumentationFile, but worth tidying for consistency with the rest of the file's well-formed doc comments.
|
Reviewed the probe-cache and plan-XML-size-cap changes against CONTRIBUTING.md's conventions. Correctness. Traced Lite/Darling parity. No drift: the deferred plan/text fetch-and-probe machinery ( T-SQL style. The Security. No injectable input — Left two inline notes, both minor: a test-coverage gap on the new cache wiring, and an unbalanced- |
|
Reviewed from the 🔴 Blocker 1 — a client server identifier is in tracked source (public repo)The doc comments cite the client host by its real short-name+ordinal — 2× in 🔴 Blocker 2 — a Darling-side query-shape pin wasn't updated
✅ Core logic — verified, not just the write-up
Minor (non-blocking)The cache is deliberately unbounded, which is defensible — but a high-plan-churn target (often exactly the ones with these symptoms) accrues stale Both blockers are mechanical — fix them and this is a merge. Happy to pair on either. |
34467c3 to
c3c9e02
Compare
…drain stalls query_store: #3216 widened the liveness-touch guard but left the verdict SELECT unconditional, so #3189's diagnosed 47-52% probe share (remeasured post-#3216) was untouched. Adds QueryStoreProbeCache, an in-process TTL memoization keyed on (server, database, id, hash) tied to the existing guard window, so a reference already confirmed resolved-and-current recently skips the store round trip entirely. Measured recurrence on the fleet's outlier server (two non-overlapping ~1h windows, two hours apart): 77.3% of plan ids and 80.1% of query ids recur. query_stats/procedure_stats: both collectors read cached-plan XML via a bare GetString with no per-row size bound, so a handful of multi-megabyte plans in a 200-row batch could turn a 26-second cycle into 87-126 seconds. Root-caused via 78 real production stalls (get_collector_stall_probes, the fleet's outlier server, 2 days): every one shows a completely idle target (single-digit runnable_tasks, zero work queue, zero pending IO) at the moment of the stall, and #2880's own forensic note already found unrelated collectors' ExecuteReaderAsync calls degrading 3-152x in the same window — a process-wide GC pause from Large-Object-Heap allocation, not target contention. Adds QueryPlanXmlCaptureLimits, a per-row DATALENGTH cap (512 KB, sized from the real distribution: heavy-tailed, top 5% of rows carry 56% of bytes) that ships NULL instead of an oversized plan. Measured end to end: ~36% wall-clock reduction, every guarded trial faster than every unguarded one. Filed as follow-ups rather than built here: #3392 (capped plans currently have no path back into view — a small backlog table plus a low-frequency, one-plan-at-a-time sweep, de-risked by measuring that the outlier server's over-cap plans are long-lived cache residents, 13.6 hours to 144.6 days observed) and #3393 (query_store's own plan fetch shares the same uncapped-GetString shape with a larger aggregate budget; live evidence is inconclusive but the code-level exposure is real and worth its own measurement pass). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WTi8131EmrYtg3f3VVYCta
c3c9e02 to
1edfe78
Compare
|
Both blockers fixed (force-pushed): Blocker 1 (leaked identifier). Scrubbed both files — the real server name is gone from Blocker 2 (cadence test). Thanks for catching both — especially the identifier, that was a real mistake on my part. |
|
Reviewed the diff (
No missing-index DMV suggestions, no secrets/credentials, no new file/network/process surface. Nothing else stood out as a bug or a Lite/Darling divergence. |
What this fixes
query_store's probe re-examination (#3189, remeasured after #3216). #3216 widened theliveness-touch guard's write interval but left the verdict
SELECTunconditional — measuredagain post-#3216 on the fleet's outlier server, the probe was still 47-52% of
query_store's wall time,essentially where #3189 left it.
QueryStoreProbeCache(new) memoizes a resolved-and-currentverdict for the same TTL the touch guard already treats as an acceptable staleness bound, keyed
on
(server, database, id, hash)so an in-place plan/text rewrite is always a cache miss.Recurrence measured on the fleet's outlier server (two non-overlapping ~1h windows, two hours apart): 77.3% of
plan ids and 80.1% of query ids recur — the basis for expecting a real hit rate, not an
assumption.
query_stats/procedure_statsdrain stalls. Both collectors read cached-plan XML via abare
GetString, no per-row size bound. Root-caused viaget_collector_stall_probes— 78 realstalls on the fleet's outlier server over 2 days, every one showing a completely idle target (single-digit
runnable_tasks, zero work-queue, zero pending IO) at the moment of the stall — plus a commentalready in
DarlingCollectorRunner.cs(#2880) noting unrelated collectors'ExecuteReaderAsynccalls degrade 3-152x in the same window. That's a process-wide GC pause from Large-Object-Heap
allocation, not target contention: three fresh 87-91s events carried the same ~27-28MB payload
as a 26-second run minutes earlier on the same server.
QueryPlanXmlCaptureLimits(new) capscaptured plan XML at 512 KB per row, sized from the real distribution (heavy-tailed: top 5% of
rows carry 56% of bytes, median ~28KB) — ships NULL instead of an oversized plan. Measured end
to end on the fleet's outlier server, 4 trials each: unguarded 1.48-2.68s vs. guarded 1.12-1.40s, every guarded
trial faster than every unguarded one, ~36% wall-clock reduction.
Follow-ups filed, not built here
backlog table plus a low-frequency, deliberately one-plan-at-a-time sweep (batching large
fetches is the exact mechanism this PR fixes, so the sweep can't reintroduce it). De-risked by
measuring that the fleet's outlier server's currently-over-cap plans have compile ages of 13.6 hours to 144.6
days — not the volatile handles a naive reading of
plan_handlewould assume.query_store's own plan fetch (FetchAndStorePlansAsync) reads plan XML the sameuncapped way, with a larger aggregate budget (up to 64 MB) than
query_stats's worst observedcase. Live evidence checked (3 samples) is inconclusive, so this is filed for measurement, not
as a confirmed incident.
Testing
Both new/changed collector-definition and cache-logic test files were executed for real via a
throwaway net10.0 console harness (
Lite.Tests/Darling.Testsare Windows-only, WPF-referencingprojects and can't run on macOS) — 39/39 passing, including three pre-existing IL-scanning
structural pins (
ProbeDenominatorTests,FetchStoreConnectionBorrowTests,PlanFetchCarryoverKeyTests) that directly exercise the call sites this PR modifies. Fullsolution builds clean (
dotnet build PerformanceMonitor.sln -p:EnableWindowsTargeting=true).🤖 Generated with Claude Code
https://claude.ai/code/session_01WTi8131EmrYtg3f3VVYCta