Port PerformanceStudio #608: unused memory grants, CE guess labels and exchange operators (#4686, #4687, #4690) - #4705
Merged
Conversation
…e of it (#4686) A grant of 1 GB or more with MaxUsedMemory=0 is the largest possible waste, but the check required MaxUsedMemory above 0 and skipped it. MemoryGrantInfo.HasMaxUsedMemory now records whether the plan XML carried the attribute, so a plan with no MaxUsedMemory (estimated, or no runtime grant info) still does not read as used-nothing. Follows PerformanceStudio#608.
An exchange spends most of its elapsed time waiting on the operators that feed it and drain it, so Rule 35 could name a Gather, Distribute or Repartition Streams while the operator next to it did the work. The rule now skips exchanges; the rule number, warning type, 20% share and 1,000 ms floor are unchanged. Follows PerformanceStudio#608.
… it, and follows the plan's CE version (#4687) DetectCeGuess called 30% an equality guess and 10% and 1% inequality guesses, none of which those predicates produce, never checked for the equality guess, and ignored the CE model version. It now takes the statement's CardinalityEstimationModelVersion, labels each measured guess for what produces it, detects the equality guess (rows^0.5 from CE 120, rows^0.75 under CE 70, 1% tolerance), and reports a band only when the plan's estimator has it. The 100,000-row minimum is the named constant CeGuessMinTableRows. Follows PerformanceStudio#608.
…08-analyzer-rules
…08-analyzer-rules
erikdarlingdata
marked this pull request as ready for review
September 29, 2026 02:13
This was referenced Sep 29, 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.
Closes #4686, closes #4687, closes #4690.
Why
erikdarlingdata/PerformanceStudio#608 (merge commit
85f9acab1) fixed three analyzer findings after the PS-to-PM sync in #4511 was finished. This ports them toPerformanceMonitor.PlanAnalysiswith the same conditions, thresholds and message text. Lite, Darling and the deprecated Dashboard all read this one project, and none of them has a separate copy of these rules, so no other product code changes.What changes
The change is in three commits, one per rule, in this order: rule 9, rule 35, rule 33. Everything is in
PerformanceMonitor.PlanAnalysis(PlanAnalyzer.cs,PlanModels.cs,ShowPlanParser.cs) plus tests. Nothing underdeprecated/is touched.Rule 9, Excessive Memory Grant (#4686)
MemoryGrantInfo.HasMaxUsedMemory(new,PlanModels.cs) is true when the plan XML carries aMaxUsedMemoryattribute. The parser sets it with the samelong.TryParsecall PS uses (ShowPlanParser.cs, memory grant block). The model and parser change is part of rule 9, because that is the only rule that reads the flag, so it is in the first commit.PlanAnalyzer.cs,AnalyzeStatement) now fires when the grant is at least 1 GB,HasMaxUsedMemoryis true, and the query used 0 KB or the grant is at least 10 times the use (the old test). For 0 KB used the message is "Granted N MB but the query used none of it. The unused memory is reserved and unavailable to other queries." with no ratio and no division. A plan with noMaxUsedMemoryattribute does not fire. The adaptive join note from Excessive Memory Grant does not say when an adaptive join ran as Nested Loops #4531 is appended to both messages, as before.85f9acab1:PlanAnalyzer.Statement.cs(Rule09_MemoryGrant, the "Excessive grant" block),PlanModels.cs(MemoryGrantInfo),ShowPlanParser.cs(memory grant block).AnalyzeStatement; PS moved it into aRule09_MemoryGrantmethod, and that move is not ported.MemoryGrantInfoin code (PlanSync4531Tests,PlanSync4535StatementRulesTests). They now setHasMaxUsedMemory = true, because they model an actual plan, and without the flag the rule correctly stays quiet. Their assertions are unchanged.PlanSync4686ExcessiveMemoryGrantTests(10 tests) ports all of PS'sExcessiveMemoryGrantTestsonmemory_grant_wait_plan.sqlplan(edited in the XML text, so parser and analyzer are both in the path), plus one test that the adaptive join note follows the used-none message.PlanAnalyzer.csput back to dev's version (model and parser kept so the tests compile), 4 of 10 fail:GrantThatUsedNothing_Fires,GrantThatUsedNothing_MessageHasNoRatio,GrantThatUsedNothing_AtOneGbExactly_Fires, and the adaptive join note test. The other 6 pin what must not change (the parser flag, under 1 GB, the ratio message, under 10x, most of the grant used).MaxUsedMemory="0"before the query uses its grant, and the rule would call that grant unused. That case is not tested here.Rule 35, Expensive Operator (#4690)
PlanAnalyzer.cs,AnalyzeNode) gains&& !IsExchangeOperator(node), and the comment above the rule gains PS's paragraph on why exchanges are skipped. The rule number, warning type, message, 20% share and 1,000 ms floor are unchanged.GetOperatorOwnElapsedMsis untouched, so the spill severity code and rule 34 are unaffected.PlanAnalyzer.Node.cs(Rule 35 block, the comment and the added condition) andNodeTimeAttribution.cs(IsExchangeOperator).NodeTimeAttributionclass, soPlanAnalyzergets a privateIsExchangeOperatorwith the same test (PhysicalOpis "Parallelism", orLogicalOpis "Gather Streams", "Distribute Streams" or "Repartition Streams"). The last sentence of PS's comment, about the text report's "Expensive operators" list, is dropped because PM has no such list. PM keeps its literal 1,000 ms floor and its own longer comment about it; PS names the floorRule35MinStatementElapsedMs.PlanSync4690ExpensiveOperatorExchangeTests(8 tests) ports PS'sExpensiveOperatorExchangeTests: no exchange is named onserially-parallel.sqlplan,memory_grant_wait_plan.sqlplanorspill_plan.sqlplan; the Sort inserially-parallel.sqlplankeeps its warning; and a code-built node with 6,000 ms in a 10,000 ms statement is flagged as a Sort and not flagged as Repartition, Gather or Distribute Streams.Rule 33, Estimated Plan CE Guess (#4687)
DetectCeGuess(PlanAnalyzer.cs) is replaced with PS's version. It takes the statement'sCardinalityEstimationModelVersion(passed at the call site inAnalyzeNode), labels each measured guess for what produces it (30% is the inequality guess; 9%, 10%, 16.4% and 1% follow the PS table), detects the equality guess (the square root of the row count from CE 120, the row count to the power 0.75 under CE 70, 1% tolerance), and reports a band only when the plan's estimator has it. A plan with no version can match any band. Rule number, warning type and the text around the label are unchanged; only the label text changes.CeGuessMinTableRowswith PS's comment on why the equality check depends on it, and the comment above the rule is updated to the new labels.PlanAnalyzer.Node.cs(Rule33_CeGuessDetection, comment and call) andPlanAnalyzer.Timing.cs(CeGuessMinTableRows,DetectCeGuess).AnalyzeNode. The constant sits aboveDetectCeGuessinPlanAnalyzer.cs(PS has it inPlanAnalyzer.Timing.cs).DetectCeGuessalso documents its other two parameters. In the tests, the reference to PS'sAnalyzerRuleGateTestspoints at PM'sPlanSync4535NodeRulesATests.PlanSync4687CeGuessDetectionTests(45 tests) ports all of PS'sCeGuessDetectionTests: every measured row (label per estimator version), the not-a-guess rows, the 1% tolerance, the equality guess scaling with the table, the table size floor, and four tests that run real plan XML with the estimator version and predicate filled in.deprecated/found the old label text only inPlanAnalyzer.cs.Fixtures and baselines
memory_grant_wait_plan.sqlplan,serially-parallel.sqlplanandspill_plan.sqlplanare copied byte for byte from PS at85f9acab1intoDarling/Darling.Tests/Fixtures/OriginPlans/(the csproj glob copies*.sqlplan).WarningBaseline.txt. TheCapabilityPins/*.baseline.txtfiles pin what the viewer can do, not plan findings, so no baseline line moved.Test plan
Darling.TestsandLite.Testsbuild with 0 warnings and 0 errors.PlanSync4686ExcessiveMemoryGrantTests(10),PlanSync4690ExpensiveOperatorExchangeTests(8),PlanSync4687CeGuessDetectionTests(45): 63 of 63 pass. ThePlanSync4527,4531,4534and4535classes pass alongside them.Lite.Testsafter mergingorigin/dev: 5,555 total, 0 failed.Darling.TestswithoutDARLING_TEST_PG: 16,813 total, 0 failed, 1,051 skipped (the live PostgreSQL tests), and the runner reports 1 not run without naming it.sys.dm_exec_query_statistics_xmlagainst rule 9 (see the limit above) was not run.CHANGELOG
SECTION: Fixed
ENTRY:
MaxUsedMemorywas 0, so a grant of 1 GB or more that the query never used, the largest possible waste, got no finding. The parser now records whether the plan carried aMaxUsedMemoryattribute, and the rule fires for a grant of at least 1 GB that used 0 KB with the message "Granted N MB but the query used none of it." A plan with noMaxUsedMemoryattribute (an estimated plan, or one with no runtime grant info) still does not fire, and the ratio message and 10x threshold for a grant that used some of its memory are unchanged.REF:
[Port PerformanceStudio #608: unused memory grants, CE guess labels and exchange operators (#4686, #4687, #4690) #4705]: Port PerformanceStudio #608: unused memory grants, CE guess labels and exchange operators (#4686, #4687, #4690) #4705