Repository navigation
Plan analysis: wait stats become findings, and external and preemptive waits get a real benefit (#4516, #4517) - #4555
Merged
Conversation
Each wait recorded on a statement in an actual plan now emits a "Wait: <type>" finding, sitting alongside the existing per-operator findings so it sorts by benefit percent instead of only living in the raw wait-stats list. - BenefitScorer emits one finding per wait after it scores the wait's benefit percent. Severity comes from that percent: Critical at 50 or more, Warning at 10 or more, otherwise Info. - PAGEIOLATCH_* findings also carry the average time per wait (wait time divided by wait count). - Per-wait display flags and curated descriptions come from an embedded WaitStats.json read through WaitStatsConfig, so Darling, Lite, and the viewer share one copy through the shared PlanAnalysis project. Most entries have no description yet; that fills in over time. Refs #4511
MEMORY_ALLOCATION* and PREEMPTIVE_* waits keep the worker CPU-busy in the kernel, so elapsed is about equal to CPU for those threads and the standard elapsed-minus-cpu wait math barely scores them. BenefitScorer.IsExternalWait classifies those waits and routes them through a separate formula: the wait's share of statement CPU, scaled by the sum of each operator's max per-thread self-CPU (PlanAnalyzer.GetOperatorMaxThreadOwnCpuMs, next to GetOperatorOwnElapsedMs). Refs #4517. Part of #4511.
# Conflicts: # PerformanceMonitor.PlanAnalysis/PlanAnalyzer.cs
erikdarlingdata
marked this pull request as ready for review
September 28, 2026 04:11
This was referenced Sep 28, 2026
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.
Fixes #4516
Fixes #4517
Part of #4511
Why
BenefitScorer.ScoreWaitStatsalready computes a benefit percent for eachwait type recorded on a statement in an actual plan, and stores it on
PlanStatement.WaitBenefits. Two gaps sat next to that:elapsed time waiting on I/O or locks got no warning about it — the wait
only showed up in the raw wait-stats list, sorted apart from the
operator findings that share the same benefit scale.
preemptive waits (
MEMORY_ALLOCATION_EXT,RESERVED_MEMORY_ALLOCATION_EXT,the
PREEMPTIVE_*waits) keep the worker thread CPU-busy in the kernel,so its elapsed time is about equal to its CPU time. The existing
per-thread
elapsed - cpumath barely shows those waits at all.Takes effect once the scorer runs in the product: #4552 (Fixes #4546) wires it in. Nothing in the product calls
BenefitScorer.Scorebefore that.What changes
BenefitScorer.EmitWaitStatWarningsruns right afterScoreWaitStatsforevery statement with wait stats, and adds one
"Wait: <type>"finding perwait (skipping zero-time waits, same as before).
Warning at 10 or more, otherwise Info.
PAGEIOLATCH_*findings (and any other wait configured for it) alsocarry the average time per wait (wait time divided by wait count).
WaitStatsConfigreads an embeddedResources/WaitStats.json(44wait types, one entry per name) and is the single source of the per-wait
display flags and any curated description. Most entries have no
description yet — this ships the mechanism, not the copy; descriptions
fill in over time.
WaitStats.jsonis anEmbeddedResourceinPerformanceMonitor.PlanAnalysis.csproj, so Darling, Lite, and the planviewer all read the same copy through their shared project reference — no
per-app duplicate.
BenefitScorer.IsExternalWaitclassifiesMEMORY_ALLOCATION*andPREEMPTIVE_*waits and routes them through a separate formula insteadof the standard per-thread
elapsed - cpuwait math: the wait's share ofthe statement's total CPU, scaled by the sum of each operator's max
per-thread self-CPU.
PlanAnalyzer.GetOperatorMaxThreadOwnCpuMs, sitting next to theexisting
GetOperatorOwnElapsedMs, gives that per-operator maxper-thread self-CPU (non-cumulative, so summing across operators in a
serial plan doesn't double-count).
AnalyzerConfigrule-disable guard: this project doesn't have one.Not ported
AdviceContentBuilderinline wait label (a short"I/O — reading from disk" style tag next to the wait-stats card) is
UI-side display code with no equivalent surface in this project's
PlanAnalysislibrary. The finding message carries the curateddescription instead, when one exists in the JSON.
HTML export runtime card, DOP efficiency) by subtracting external-wait
time from CPU. This project has no equivalent runtime-card surface today,
so that half of the source commit isn't ported; only the benefit-scoring
half (
IsExternalWait, the external-wait formula, andGetOperatorMaxThreadOwnCpuMs) is.Consumers and impact
Once #4552 runs the scorer, the "Wait: " findings reach the plan viewer, the MCP plan tools and Darling's stored plan advisories. Measured on 60 real showplans, they add 77 Info, 1 Warning and 11 Critical findings. In Darling's analysis they reach only the
PLAN_WARNINGfact (scored presence-only, 0.4, below the Warning band and the notification threshold) and the advice headline's critical count, so no alert or health band moves.Test plan
Darling.Tests.PlanSync4516Tests(9 tests) — pins the severity tiers(including the 10% and 50% boundaries), the PAGEIOLATCH_* average-latency
line, the curated-description round trip through
WaitStatsConfig, anabsent-wait miss, the embedded resource loading a real wait type, and one
pin through the full
ShowPlanParser.Parse→PlanAnalyzer.Analyze→BenefitScorer.Scorepipeline.Darling.Tests.PlanSync4517Tests(11 tests) —IsExternalWait's prefixmatching, PerformanceStudio's worked example (48.3%), a
PREEMPTIVE_OS_WRITEFILEGATHERwait on a CPU-busy thread scoring wellabove the old formula's result on the same inputs, an ordinary wait
taking the unchanged old route,
GetOperatorMaxThreadOwnCpuMs's serialself-CPU subtraction, and one pin through the full parse → analyze →
score pipeline.
Darling.Tests.PlanSync4517ProbeTests(1 test) — the runtime RED thatwas previously recorded compile-only. It builds only the pre-existing
members (
PlanAnalyzer.Analyze,BenefitScorer.Score) that alreadyship on
dev, feeds them the same CPU-busy-thread shape as The benefit for external and preemptive waits is far too low (MEMORY_ALLOCATION_EXT, PREEMPTIVE_*) #4517'sworked example, and asserts the new 90.2% CPU-share answer. Run
against a detached
origin/devworktree (pre-The benefit for external and preemptive waits is far too low (MEMORY_ALLOCATION_EXT, PREEMPTIVE_*) #4517), it fails:Assert.Equal() Failure: Values differ / Expected: 90.200000000000003 / Actual: 50— the old per-thread elapsed-minus-cpu formula's answer forthat same shape. On this branch it's green.
PlanSync*class plus the plan-analysis classes passed.DarlingAnalysisPipelineTestsandDarlingMcpPlanToolsLivePostgresTestsneed a live store and run in CI.EmitWaitStatWarningscall fails 5 of the 9 Wait stats never become findings: BenefitScorer computes a benefit for each wait but emits no warning #4516 facts;IsExternalWaitreturning false fails 6 of the 11 The benefit for external and preemptive waits is far too low (MEMORY_ALLOCATION_EXT, PREEMPTIVE_*) #4517 facts.main:GetOperatorMaxThreadOwnCpuMsskips the coordinator thread and looks through batch mode zones and Compute Scalar pass-throughs, as PerformanceStudio's shared per-thread helper does.Lite.Tests(Release,-p:EnableWindowsTargeting=true): 0 errors,builds only (net10.0-windows can't run on macOS). No Lite.Tests class
references
BenefitScorer,WaitStatsConfig, orGetOperatorMaxThreadOwnCpuMstoday, so none needed updating.CHANGELOG
SECTION: Added
ENTRY:
REF:
[Plan analysis: wait stats become findings, and external and preemptive waits get a real benefit (#4516, #4517) #4555]: Plan analysis: wait stats become findings, and external and preemptive waits get a real benefit (#4516, #4517) #4555