Repository navigation
Top Cpu Queries' Max Dop is the newest plan's reading with the cross-plan maximum kept as a dated history, so a MAXDOP-1 instance stops reporting 16 from a plan compiled before the pin (#3648) - #3651
Conversation
…plan maximum kept as a dated history, so a MAXDOP-1 instance stops reporting 16 from a plan compiled before the pin (#3648)
|
Reviewed the core change ( Correctness: the
Lite/Darling parity: Non-blocking observation: No security, SQL-injection (all params bound), or perf concerns — the added CTE is one extra sort over the same filtered row set the old |
There was a problem hiding this comment.
LGTM — verified the windowed-CTE/ROW_NUMBER ranking logic and QueryDopProvenance.Note's decision table against their tests, confirmed the two SKUs' SQL is byte-identical, checked the creation_time tie-break doesn't violate the clock-frame de-skew discipline, and confirmed no T-SQL/OPTION(RECOMPILE) or missing-index concerns apply (this touches Postgres/DuckDB analysis SQL, not SQL Server collectors).
…ter that reaches both trees), the live test cleans up through LiveStoreCleanup, and the DuckDB fixture reads UtcNow once so a round-tripped timestamp compares equal (#3648)
|
Reviewed the diff, the shared Findings:
No correctness, security, or parity issues found. Nice use of a live-Postgres test specifically to pin the NULLS LAST engine-default divergence rather than just string-matching the SQL. |
The lie
The High CPU card's
Top Cpu Queries(and thebad_actor_querydetail behind a BAD_ACTOR finding) reported aMax Dopper query that was computed asmax_dopissys.dm_exec_query_stats.max_dop— a per-plan high-water mark: the highest degree of parallelism that one cached plan has ever run at since it entered the cache. Grouping byquery_hashfolds every plan the statement text had inside the window into one row and keeps the largest counter, so the field reported the highest DOP any plan for that text ever ran at — not the DOP the plan spending the CPU is running at, and with nothing saying which plan, when, or whether that plan still exists. The reader also coerced a NULL reading to0and the card rendered it as a number, although the DMV never reports 0 (a serial plan is 1).Evidence
On a live page today (#3648) the #1 row said
Max Dop 16on an instance whose MAXDOP is 1 — configured 1, in use 1,values_matchtrue, zero config changes across 59 snapshots in 14 days — and whose stored plan for that exactquery_hashis serial:NonParallelPlanReason="MaxDOPSetToOne",EstimatedAvailableDegreeOfParallelism="1", no Parallelism operators. The 16 belonged to a plan compiled before the instance was pinned to 1, still sitting in the cache with its old counter. Row #3 on the same card saidMax Dop 1for a hash with one serial plan — the same field behaving honestly, so the inconsistency the reader saw was within the field's semantics, not a bug in one row. A reader recommended a MAXDOP 1 Query Store hint off the field — the exact decision the card exists to support — and retracted it after reading the plan the card should have summarized.The fix — state the basis or don't state the number (#3574 / #3632 shape)
Both SKUs'
top_cpu_queriesandbad_actor_queryreads now rank each hash's rows newest-first inside awindowedCTE (ROW_NUMBER() OVER (PARTITION BY database_name, query_hash ORDER BY collection_time DESC, creation_time DESC NULLS LAST, delta_worker_time DESC NULLS LAST)) and carry the hash-wideMAX(max_dop) OVER (...)onto every row, so the outer aggregate can say which reading is current and when the maximum was last seen:max_dopcollection_time(ties: the later-compiled plan, then the one that spent the CPU).nullwhen the DMV row carried none. Existing consumers that readmax_dopnow get the honest number.max_dop_any_planmax_dop_any_plan_last_seennullwhen unknown)plan_countCOUNT(DISTINCT query_plan_hash)in the groupdop_notePerformanceMonitor.Common.QueryDopProvenanceso both SKUs say the byte-identical thing;nullwhen the history agrees with the headlineExact
dop_noteformat, fired only when a plan in the group ran at a higher DOP than the newest plan reports:New columns are appended after the old ones so the existing reader ordinals are untouched, and the SQL text is byte-identical between Lite's inline
cmd.CommandTextand Darling'sTopCpuQueriesSql/BadActorDetailSqlconstants — theNULLS LASTis explicit because DuckDB and Postgres default DESC null placement differently and an implicit default would make the two SKUs pick different rows on the same data. ThewindowedCTE reads the same row set the old GROUP BY scanned (same window predicates, samedelta_worker_time > 0filter on the top-CPU read), adding one sort; the bad-actor read is one hash and is trivially cheap.The comments in both collectors and in the shared helper name the DMV semantics, why hash-grouping lied, and the live consequence.
Reader census (
MAX(max_dop)grouped by hash, published without a plan dimension?)DrillDownCollector.Queries.csCollectTopCpuQueries+CollectBadActorDetail; DarlingPgDrillDownCollector.Queries.csTopCpuQueriesSql+BadActorDetailSql. These are thetop_cpu_queries/bad_actor_queryrecords on the analysis card, inget_analysis_facts/analyze_serverdrill-down payloads, and through Top CPU Queries renders as a two-column field jumble on Slack — one section block per query, not seven interleaved fields #3644's summary line.queries_at_spike(both SKUs) readsv_query_snapshots.dop— the live request's actual DOP fromdm_exec_requests, not the plan-cache high-water mark. The issue's "sibling at ~592" is in factBadActorDetailSql, fixed above. Query Store readers (ViewerDataService.QueryStore,LocalDataService.QueryStore, DarlingDataReader's QS read) group by(database, query_id, plan_id, …)— a plan dimension.ViewerDataService.ItemHistoryandDarlingTrendReader/get_query_trendpublish per-rowmax_dopwith no cross-plan folding (and null it when aggregated).get_top_queries_by_cpu(DarlingDarlingDataReader.TopQueriesSql/TopQueriesByHostObjectSql; LiteLocalDataService.QueryStatsTopQueriesSql) and the viewer query-stats grids (both SKUs) group by(database, query_hash, host_object_name)and publishmin_dop/max_dopwithquery_plan_hash = MAX(…)as a representative. The MCP tool's description already labelsmax_dopa lifetime max and points toanalyze_query_plan, and itsparallel_only/min_dopfilters are defined on that lifetime max, so re-headlining there changes a documented filter contract and aTopQueryRowpositional record consumed by two tool files and their tests — a lane of its own, not a rider on this one. The same four fields (plan_count,max_dop_any_plan,max_dop_any_plan_last_seen,dop_note) are the recommended shape.What this does NOT do
PerformanceMonitor.Analysis/(FactScorer / InferenceEngine / FactAdvice),DarlingMcpTools.cs,AnalysisNotificationService.FlattenInto, orWebhookAlertService— the Top CPU Queries renders as a two-column field jumble on Slack — one section block per query, not seven interleaved fields #3644 lane owns how record-shaped drill-downs render; the new fields flow through its summary line. Read-only note for that lane:ScalarTextrendersJsonValueKind.Nullas"", so anullmax_dop/dop_notecurrently appears as its label with an empty value rather than being omitted.server_configMAXDOP — QUERY_HIGH_DOP fires on a stale lifetime max_dop, not current parallelism #2705/Lite still raises QUERY_HIGH_DOP from a stale lifetime max_dop — #2705's guard landed on Darling only #2999 already do that for the QUERY_HIGH_DOP fact; the drill-down record now carries its own provenance instead.get_top_queries_by_cpucontract (see census).Tests
Lite.Tests/DrillDownDopProvenanceTests(real DuckDB, 6): the live page's fixture — one hash, an old parallel plan at 16 last seen 3 h before window end, a new serial plan at 1 at the newest snapshot →max_dop = 1,max_dop_any_plan = 16,plan_count = 2,max_dop_any_plan_last_seen= the parallel plan's last snapshot, exactdop_note, windowed total still spans both plans; the same throughbad_actor_query; one serial plan → no note; NULL reading → JSONnullon every DOP field, never 0; newest unknown + parallel history → "DOP unknown…"; both plans at the same newest snapshot → the later-compiled plan is the headline (theNULLS LASTtie-break).Darling.Tests/DrillDownDopProvenanceParityTests(5): both reads byte-identical across SKUs — Lite's inline text read from source, Darling's from the public constants — hosted on the Darling side becausebuild.yml'sdarlingfilter reaches every Lite.cswhile thelitefilter does not reachDarling/PerformanceMonitor.Darling.Analysis(CrossAppGuardCiGateTestsenforces this, and caught head 1).Lite.Tests/DrillDownDopProvenanceParityTests(4) pins Lite's own provenance shape, that the bareMAX(max_dop) AS max_dopis gone, that no unknown DOP is coerced to 0, and meta-pins Darling's byte-identity theory — theQueryHighDopStaleMaxDopParityTestspair shape.Lite.Tests/QueryDopProvenanceTests+Darling.Tests/QueryDopProvenanceTests(7 each, identical decision table): thedop_noteformat and every no-note arm.Darling.Tests/DrillDownDopProvenanceLiveTests(DARLING_TEST_PG-gated): the same three scenarios against real Postgres throughPgDrillDownCollector.CrossAppGuardCiGateTests) on real DuckDB via the mactest harness (net10.0-windows build,Microsoft.WindowsDesktop.Appdropped from the runtimeconfig); Darling 45/45 green including the live test (throughLiveStoreCleanup, per the The other ~124 live-test finally blocks still clean up on the body's connection, so a failing test can still report its teardown's error #1902 ratchet) against atimescale/timescaledb:2.28.1-pg18rig with a seededquery_stats, plusDarlingAnalysisPipelineTests(dialect / FROM-target / AllSql-count guards over the new CTE),CreationTimeClockFrameDisciplineTests,StoreSqlClockDisciplineTests. All touched projects build-c Release -p:EnableWindowsTargeting=truewith 0 warnings.Closes #3648