Skip to content

Plan analysis: PerformanceStudio's per-rule configuration, rule numbers, rule 38 and the legacy marker (#4535, #4566) - #4585

Merged
erikdarlingdata merged 16 commits into
devfrom
plan-sync/4535-analyzer-config
Sep 28, 2026
Merged

erikdarlingdata merged 16 commits into
devfrom
plan-sync/4535-analyzer-config

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

Refs #4535 (the override mechanism is here; the configuration source that lets a user set overrides comes in the follow-up, so the issue stays open until then)
Fixes #4566
Refs #4530 (the entry points that pass the server metadata come in a follow-up)
Part of #4511

Why

PerformanceMonitor's and PerformanceStudio's plan analyzers are kept equivalent. This change ports PerformanceStudio's per-rule configuration plumbing, the rule numbers it tags each finding with, its Standard Edition DOP-2 rule, and its legacy-finding marker.

What changes

  • AnalyzerConfig (with RulesConfig, disabled rules and severity overrides) and ServerMetadata are new types the analyzer accepts. PlanAnalyzer.Analyze and PlanAnalysisPipeline.Run gain overloads that take them; the existing no-argument overloads are unchanged and behave exactly as before (both pinned byte-for-byte identical).
  • Every PlanWarning the analyzer constructs now carries a RuleNumber. A rule number census over the analyzer's source confirms this for every construction site, with two documented exceptions: rules 7 (spill severity) and 29 (implicit-conversion severity escalation) mutate an existing finding's severity/message rather than constructing a new one, so they carry a disable guard with no stamp.
  • Each rule's !cfg.IsRuleDisabled(N) guard is wired at its own construction point, so a disabled rule produces no finding. A cross-step test disables every stamped rule number and confirms the fixtures then produce zero analyzer findings (SQL Server's own findings, which aren't numbered, are unaffected).
  • A severity-override pass (ApplySeverityOverrides) applies configured severity overrides to any stamped, analyzer-sourced finding, walking into stored-procedure and UDF bodies the same way the analyzer's own statement walk does. SQL Server-sourced findings are never overridden.
  • Rule 38 (Standard Edition DOP-2 limitation with batch mode) is new, as in PerformanceStudio dev: it looks at a statement that ran at DOP 2 with at least one batch-mode operator, unless the statement carries a MAXDOP 2 hint (masked for comments and literals, like rule 39's hint check), which makes the cap intentional. With the edition known to be Standard and the server's MAXDOP above 2 it is a Warning; with the edition unknown it is reported as a possible limitation (Info). Until the entry points pass server metadata (the follow-up), the edition is always unknown, so every rule-38 finding is Info.
  • Findings of a known-legacy type (an internal list of 21 warning types, matching PerformanceStudio's) are marked IsLegacy = true on the finding, except when the finding came from SQL Server itself rather than the analyzer. This reaches the MCP plan tools' JSON (is_legacy per warning) and the plan viewer's warning header (an appended " [legacy]" tag, after the source tag).

Not yet

Loading a per-rule configuration from a file is the next PR. Until then, the default configuration applies everywhere: no rule is disabled, and no severity is overridden.

Test plan

Each step's own pins and REDs (recorded in the pre-merge step reports) plus, at the combine:

  • The stale mid-rollout pin removed: PlanSync4535ConfigPlumbingTests.RuleNumber_DefaultsToNull_UntilLaterStepsStampIt pinned the state partway through the rollout (only rule 3 stamped) and is wrong now that every rule-stamping step has landed.
  • New PlanSync4535RuleNumberCensusTests (replaces it): (a) a code census walking every new PlanWarning construction in PlanAnalyzer.cs, asserting each sets RuleNumber unless it's SQL-Server-sourced; (b) a runtime census over plan fixtures, asserting every analyzer-sourced finding (excluding the benefit scorer's Wait: findings, which aren't a numbered rule) has a non-null RuleNumber; (c) disabling every rule number the code census found, plus rules 7 and 29 (guard-only, no construction site), and confirming the fixtures then produce zero analyzer findings.
    • RED confirmed on the pre-merge sha (b20e89003): all 4 pins failed there (2 runtime failures from unstamped/undisabled rules, 1 empty-collection failure from having no rules to disable yet, and a missing-substring failure since the rule 7/29 guards didn't exist at that point).
    • Mutation: removed the RuleNumber = 20 stamp from the Local Variables construction site, rebuilt, confirmed the code census caught it (EveryAnalyzerConstructedWarning_StampsRuleNumber_UnlessSqlServerSourced [FAIL]), then reverted and rebuilt clean.
  • Build: Darling.Tests, PerformanceMonitor.Ui, Lite/PerformanceMonitorLite.csproj, Lite.Tests, Darling/PerformanceMonitor.Darling.Service and deprecated/Dashboard.Tests all build Release with EnableWindowsTargeting=true: 0 warnings, 0 errors on every one of them.
  • Run: in-process on macOS (Microsoft.WindowsDesktop.App stripped from the runtime config), every PlanSync* class plus BenefitScorerWiringTests, McpToolsListBudgetTests, McpPayloadContractCensusTests and the required non-PlanSync classes (ShowPlanParserCondAndMultiplePlanTests, the ActualPlan* classes, QueryModificationDetectorTests, ReproScriptBuilderHardeningTests, DarlingAnalysisPipelineTests, DarlingMcpPlanToolsSurfaceAndSqlTests, DarlingMcpPlanToolsLivePostgresTests, McpPlanAnalysisEnvelopeTests, SerialLoopStoreSizeSourceTests, TsqlConventionGuardTests, DocCommentHygieneTests):
    Total: 563, Errors: 0, Failed: 0, Skipped: 2, Not Run: 0. The 2 skips are the two live-Postgres tests that need DARLING_TEST_PG, unaffected by this change.
  • Lite.Tests was built (0 warnings/0 errors) but not run — it targets net10.0-windows and can't run on macOS; CI decides it.

CHANGELOG

SECTION: Added
ENTRY: - Plan analysis flags a possible Standard Edition batch-mode DOP limit, and marks legacy findings ([#4585]) - A plan that ran at DOP 2 with batch-mode operators gets a Standard Edition DOP Limitation finding (Info until the server's edition is known). Findings from rules that predate the benefit-scoring framework carry is_legacy in the MCP plan output and a [legacy] tag in the plan viewer.
REF: [#4585]: #4585

erikdarlingdata and others added 16 commits September 28, 2026 09:15
Ports PerformanceStudio's AnalyzerConfig/RulesConfig (disabled rules,
severity overrides) and the top-level ServerMetadata fields into
PerformanceMonitor.PlanAnalysis, adds PlanWarning.RuleNumber, and adds
Analyze/Run overloads that take (config, serverMetadata, cancellationToken)
and thread both down to the statement and node walks. The existing
overloads still forward to AnalyzerConfig.Default and a null metadata, and
no rule reads either new parameter yet, so analyzer output is unchanged.
…ing (#4535)

Ports erikdarlingdata/PerformanceStudio's severity-override pass: overrides now find a warning's rule via PlanWarning.RuleNumber instead of matching WarningType against a rule-to-name table, and the engine's own warnings (Source == SqlServer) are always skipped. Also ports PlanStatements.EnumerateAllWithContainer, with EnumerateAll delegating to it so the plain and container-carrying walks stay one walk.
#4535, #4566)

The plumbing step's RuleNumber_DefaultsToNull_UntilLaterStepsStampIt pinned the
state partway through the per-rule configuration rollout (only rule 3 stamped).
Every step that stamps a rule has landed on this branch, so that pin is wrong
by design now.

Replaces it with PlanSync4535RuleNumberCensusTests: a code census over every
new PlanWarning construction in PlanAnalyzer.cs (each must set RuleNumber,
except a SqlServer-sourced initializer), a runtime census over the existing
plan fixtures (every analyzer finding carries a non-null RuleNumber), and a
cross-step check that disabling every stamped rule number leaves zero analyzer
findings.
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