Skip to content

Port PerformanceStudio #608: unused memory grants, CE guess labels and exchange operators (#4686, #4687, #4690) - #4705

Merged
erikdarlingdata merged 5 commits into
devfrom
fix/4686-4687-4690-ps608-analyzer-rules
Sep 29, 2026
Merged

erikdarlingdata merged 5 commits into
devfrom
fix/4686-4687-4690-ps608-analyzer-rules

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

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 to PerformanceMonitor.PlanAnalysis with 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 under deprecated/ is touched.

Rule 9, Excessive Memory Grant (#4686)

  • MemoryGrantInfo.HasMaxUsedMemory (new, PlanModels.cs) is true when the plan XML carries a MaxUsedMemory attribute. The parser sets it with the same long.TryParse call 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.
  • The rule (PlanAnalyzer.cs, AnalyzeStatement) now fires when the grant is at least 1 GB, HasMaxUsedMemory is 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 no MaxUsedMemory attribute 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.
  • PS lines followed at 85f9acab1: PlanAnalyzer.Statement.cs (Rule09_MemoryGrant, the "Excessive grant" block), PlanModels.cs (MemoryGrantInfo), ShowPlanParser.cs (memory grant block).
  • Difference from PS: none in behaviour. PM keeps the rule inline in AnalyzeStatement; PS moved it into a Rule09_MemoryGrant method, and that move is not ported.
  • Two existing tests build MemoryGrantInfo in code (PlanSync4531Tests, PlanSync4535StatementRulesTests). They now set HasMaxUsedMemory = true, because they model an actual plan, and without the flag the rule correctly stays quiet. Their assertions are unchanged.
  • Pins: PlanSync4686ExcessiveMemoryGrantTests (10 tests) ports all of PS's ExcessiveMemoryGrantTests on memory_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.
  • RED: with PlanAnalyzer.cs put 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).
  • Limit, as in PS: a plan captured from a query that is still running might report 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)

  • The condition (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. GetOperatorOwnElapsedMs is untouched, so the spill severity code and rule 34 are unaffected.
  • PS lines followed: PlanAnalyzer.Node.cs (Rule 35 block, the comment and the added condition) and NodeTimeAttribution.cs (IsExchangeOperator).
  • Differences from PS: PM has no NodeTimeAttribution class, so PlanAnalyzer gets a private IsExchangeOperator with the same test (PhysicalOp is "Parallelism", or LogicalOp is "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 floor Rule35MinStatementElapsedMs.
  • Pins: PlanSync4690ExpensiveOperatorExchangeTests (8 tests) ports PS's ExpensiveOperatorExchangeTests: no exchange is named on serially-parallel.sqlplan, memory_grant_wait_plan.sqlplan or spill_plan.sqlplan; the Sort in serially-parallel.sqlplan keeps 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.
  • RED: with the rule put back to the previous commit, 7 of 8 fail. The one that passes is the Sort control, which must.

Rule 33, Estimated Plan CE Guess (#4687)

  • DetectCeGuess (PlanAnalyzer.cs) is replaced with PS's version. It takes the statement's CardinalityEstimationModelVersion (passed at the call site in AnalyzeNode), 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.
  • The 100,000-row minimum is now the named constant CeGuessMinTableRows with PS's comment on why the equality check depends on it, and the comment above the rule is updated to the new labels.
  • PS lines followed: PlanAnalyzer.Node.cs (Rule33_CeGuessDetection, comment and call) and PlanAnalyzer.Timing.cs (CeGuessMinTableRows, DetectCeGuess).
  • Differences from PS: PM keeps the rule inline in AnalyzeNode. The constant sits above DetectCeGuess in PlanAnalyzer.cs (PS has it in PlanAnalyzer.Timing.cs). DetectCeGuess also documents its other two parameters. In the tests, the reference to PS's AnalyzerRuleGateTests points at PM's PlanSync4535NodeRulesATests.
  • Pins: PlanSync4687CeGuessDetectionTests (45 tests) ports all of PS's CeGuessDetectionTests: 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.
  • RED: with the rule put back to the previous commit, 36 of 45 fail. The other 9 are rows the old code also left unlabelled, plus one row whose old label happens to contain the words the test checks.
  • No test named the old labels. A search of the repo outside deprecated/ found the old label text only in PlanAnalyzer.cs.

Fixtures and baselines

  • memory_grant_wait_plan.sqlplan, serially-parallel.sqlplan and spill_plan.sqlplan are copied byte for byte from PS at 85f9acab1 into Darling/Darling.Tests/Fixtures/OriginPlans/ (the csproj glob copies *.sqlplan).
  • PM keeps no findings baseline like PS's WarningBaseline.txt. The CapabilityPins/*.baseline.txt files pin what the viewer can do, not plan findings, so no baseline line moved.

Test plan

  • Darling.Tests and Lite.Tests build with 0 warnings and 0 errors.
  • New pins: PlanSync4686ExcessiveMemoryGrantTests (10), PlanSync4690ExpensiveOperatorExchangeTests (8), PlanSync4687CeGuessDetectionTests (45): 63 of 63 pass. The PlanSync4527, 4531, 4534 and 4535 classes pass alongside them.
  • RED for each rule as above (4 of 10, 7 of 8, 36 of 45 fail with that rule's change reverted).
  • Full Lite.Tests after merging origin/dev: 5,555 total, 0 failed.
  • Full Darling.Tests without DARLING_TEST_PG: 16,813 total, 0 failed, 1,051 skipped (the live PostgreSQL tests), and the runner reports 1 not run without naming it.
  • A running-query plan from sys.dm_exec_query_statistics_xml against rule 9 (see the limit above) was not run.

CHANGELOG

SECTION: Fixed
ENTRY:

…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.
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