diff --git a/src/PlanViewer.Core/PlanViewer.Core.csproj b/src/PlanViewer.Core/PlanViewer.Core.csproj index 6cd5589..6428af3 100644 --- a/src/PlanViewer.Core/PlanViewer.Core.csproj +++ b/src/PlanViewer.Core/PlanViewer.Core.csproj @@ -21,6 +21,7 @@ + diff --git a/src/PlanViewer.Core/Resources/WaitStats.json b/src/PlanViewer.Core/Resources/WaitStats.json index 375f7ba..da02e2a 100644 --- a/src/PlanViewer.Core/Resources/WaitStats.json +++ b/src/PlanViewer.Core/Resources/WaitStats.json @@ -1,24 +1,23 @@ { "_meta": { - "purpose": "Single source of truth for wait stat handling. Per issue #215, this file will entirely drive the wait stats functionality once Part 2 lands. Today it is a seed for review — only attributes 1-5 are populated. Attributes 6-12 are intentionally null and reserved for domain-expert (Joe Obbish) population.", + "purpose": "Single source of truth for wait stat handling. Per issue #215, this file drives the wait stats functionality. Attributes 1-8 are populated from existing tool behavior; attributes 9-12 (applicable operators, descriptions, URLs, internal comment) need domain expertise and are open for contribution.", "schema": { "name": "Wait stat name (e.g., PAGEIOLATCH_SH).", "isPreemptive": "True iff the wait runs preemptively (Windows kernel time, name prefix PREEMPTIVE_).", - "isExternal": "True iff time accrues to CPU rather than (elapsed - cpu); today scoped to PREEMPTIVE_* and MEMORY_ALLOCATION*.", - "isImplemented": "True iff the tool currently has explicit handling (categorizer match, knowledge entry, or specialized benefit formula). False = wait is collected but falls to the 'Other' bucket.", + "isExternal": "True iff time accrues to CPU rather than (elapsed - cpu); routed through the CPU-based external-wait formula.", + "isImplemented": "True iff the tool currently has explicit handling (categorizer match, knowledge entry, or specialized benefit formula).", "isEnabled": "True iff the tool should surface this wait in analysis/display.", - "showWaitCount": "(Pending) Show waits-recorded count alongside wait time.", - "showAverageWaitTime": "(Pending) Show wait_ms / wait_count alongside totals.", - "timeCalculationModel": "(Pending) One of 'direct', 'cpu time based', or 'elapsed time based'.", - "applicableOperatorNames": "(Pending) Operator names this wait can plausibly attribute to (only for 'elapsed time based').", + "showWaitCount": "Show waits-recorded count alongside wait time.", + "showAverageWaitTime": "Show wait_ms / wait_count alongside totals (effective latency).", + "timeCalculationModel": "One of 'direct' (wait time IS the delay, e.g. memory grant, ASYNC_NETWORK_IO), 'cpu time based' (external/preemptive — kernel CPU-busy), or 'elapsed time based' (default — derived from elapsed - cpu per thread).", + "applicableOperatorNames": "(Pending) Operator names this wait can plausibly attribute to (only meaningful for 'elapsed time based').", "description": "(Pending) End-user description.", "helpfulUrls": "(Pending) Reference links.", "internalComment": "(Pending) Maintainer notes; never shown to end users." }, "seedNotes": [ - "Population is bounded to wait names the tool today references explicitly OR commonly-seen waits matched by prefix patterns (LCK_M_*, PAGELATCH_*, LATCH_*, HT*, PREEMPTIVE_*). Pattern members beyond these representative entries can be added as needed.", - "A small set of unimplemented-but-common waits is included with isImplemented=false so both states are represented.", - "Today the tool also collects waits at the SQL Server wait_category level via sys.query_store_wait_stats, filtering categories 11 (Idle) and 18 (User Wait). Nothing in this seed falls into those categories." + "Population is bounded to wait names that actually appear in plan XML elements. Server-level / instance-level waits (THREADPOOL, BACKUPIO, HADR_SYNC_COMMIT, DBMIRROR_*, SQLTRACE_*) and connection-setup preemptive waits (AUTHENTICATIONOPS, LOOKUPACCOUNTSID) are intentionally excluded.", + "Pattern members beyond representative entries (LCK_M_*, PAGELATCH_*, LATCH_*, HT*, PREEMPTIVE_*) can be added freely — the loader keys by exact name." ] }, @@ -29,9 +28,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": true, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -43,9 +42,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": true, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -57,9 +56,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": true, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -71,9 +70,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": true, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -86,9 +85,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -100,9 +99,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -114,9 +113,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -128,9 +127,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -143,9 +142,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -157,9 +156,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -171,9 +170,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -185,9 +184,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -200,9 +199,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -214,9 +213,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -228,9 +227,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -242,9 +241,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -256,9 +255,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -270,9 +269,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -284,9 +283,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -298,9 +297,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -312,9 +311,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -327,9 +326,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -341,9 +340,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -355,9 +354,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -369,9 +368,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -384,9 +383,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -398,9 +397,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -412,9 +411,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -426,9 +425,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -440,9 +439,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -454,9 +453,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -469,9 +468,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "direct", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -483,9 +482,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -497,9 +496,9 @@ "isExternal": true, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "cpu time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -511,9 +510,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -526,9 +525,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -540,9 +539,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -554,9 +553,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -569,9 +568,9 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "direct", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -584,38 +583,24 @@ "isExternal": false, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "elapsed time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, "internalComment": null }, - { - "name": "PREEMPTIVE_OS_AUTHENTICATIONOPS", - "isPreemptive": true, - "isExternal": true, - "isImplemented": true, - "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, - "applicableOperatorNames": null, - "description": null, - "helpfulUrls": null, - "internalComment": null - }, { "name": "PREEMPTIVE_OS_FILEOPS", "isPreemptive": true, "isExternal": true, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "cpu time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -627,23 +612,9 @@ "isExternal": true, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, - "applicableOperatorNames": null, - "description": null, - "helpfulUrls": null, - "internalComment": null - }, - { - "name": "PREEMPTIVE_OS_LOOKUPACCOUNTSID", - "isPreemptive": true, - "isExternal": true, - "isImplemented": true, - "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "cpu time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -655,9 +626,9 @@ "isExternal": true, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "cpu time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, @@ -669,94 +640,9 @@ "isExternal": true, "isImplemented": true, "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, - "applicableOperatorNames": null, - "description": null, - "helpfulUrls": null, - "internalComment": null - }, - - { - "name": "THREADPOOL", - "isPreemptive": false, - "isExternal": false, - "isImplemented": false, - "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, - "applicableOperatorNames": null, - "description": null, - "helpfulUrls": null, - "internalComment": null - }, - { - "name": "BACKUPIO", - "isPreemptive": false, - "isExternal": false, - "isImplemented": false, - "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, - "applicableOperatorNames": null, - "description": null, - "helpfulUrls": null, - "internalComment": null - }, - { - "name": "BACKUPBUFFER", - "isPreemptive": false, - "isExternal": false, - "isImplemented": false, - "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, - "applicableOperatorNames": null, - "description": null, - "helpfulUrls": null, - "internalComment": null - }, - { - "name": "HADR_SYNC_COMMIT", - "isPreemptive": false, - "isExternal": false, - "isImplemented": false, - "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, - "applicableOperatorNames": null, - "description": null, - "helpfulUrls": null, - "internalComment": null - }, - { - "name": "DBMIRROR_DBM_EVENT", - "isPreemptive": false, - "isExternal": false, - "isImplemented": false, - "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, - "applicableOperatorNames": null, - "description": null, - "helpfulUrls": null, - "internalComment": null - }, - { - "name": "SQLTRACE_BUFFER_FLUSH", - "isPreemptive": false, - "isExternal": false, - "isImplemented": false, - "isEnabled": true, - "showWaitCount": null, - "showAverageWaitTime": null, - "timeCalculationModel": null, + "showWaitCount": true, + "showAverageWaitTime": false, + "timeCalculationModel": "cpu time based", "applicableOperatorNames": null, "description": null, "helpfulUrls": null, diff --git a/src/PlanViewer.Core/Services/BenefitScorer.cs b/src/PlanViewer.Core/Services/BenefitScorer.cs index f2c96a3..149e03f 100644 --- a/src/PlanViewer.Core/Services/BenefitScorer.cs +++ b/src/PlanViewer.Core/Services/BenefitScorer.cs @@ -47,7 +47,7 @@ public static void Score(ParsedPlan plan) /// /// Emits a PlanWarning per wait stat entry, merging the per-wait benefit % - /// from ScoreWaitStats with the descriptive content from WaitStatsKnowledge. + /// from ScoreWaitStats with display flags from WaitStatsConfig. /// The existing wait-stats chart/card stays as a complementary view. /// private static void EmitWaitStatWarnings(PlanStatement stmt) @@ -61,19 +61,16 @@ private static void EmitWaitStatWarnings(PlanStatement stmt) { if (wait.WaitTimeMs <= 0) continue; - var entry = WaitStatsKnowledge.Lookup(wait.WaitType); double? benefitPct = benefitByType.TryGetValue(wait.WaitType, out var b) ? b : null; var msg = new System.Text.StringBuilder(); msg.Append(wait.WaitType); - if (!string.IsNullOrEmpty(entry.Description)) - msg.Append(": ").Append(entry.Description); msg.Append(" Observed ").Append(wait.WaitTimeMs.ToString("N0")).Append(" ms"); if (wait.WaitCount > 0) msg.Append(" across ").Append(wait.WaitCount.ToString("N0")).Append(" wait").Append(wait.WaitCount == 1 ? "" : "s"); msg.Append('.'); - if (entry.ShowEffectiveLatency && wait.WaitCount > 0) + if (WaitStatsConfig.ShowAverageWaitTime(wait.WaitType) && wait.WaitCount > 0) { var effLatency = (double)wait.WaitTimeMs / wait.WaitCount; msg.Append(" Effective latency: ") @@ -94,7 +91,7 @@ private static void EmitWaitStatWarnings(PlanStatement stmt) Message = msg.ToString(), Severity = severity, MaxBenefitPercent = benefitPct, - ActionableFix = string.IsNullOrEmpty(entry.HowToFix) ? null : entry.HowToFix + ActionableFix = null }); } } @@ -627,14 +624,12 @@ private static double CalculateExternalWaitBenefit( /// External / preemptive waits where the worker is CPU-busy in kernel rather than /// descheduled. Their wait time counts toward the query's CPU time, so the usual /// (elapsed - cpu) per-thread wait math misses them entirely. + /// + /// Per #215, classification lives in Resources/WaitStats.json. Lookup misses + /// fall back to false, matching the prior default for unrecognized waits. /// public static bool IsExternalWait(string waitType) - { - if (string.IsNullOrEmpty(waitType)) return false; - var wt = waitType.ToUpperInvariant(); - return wt.Contains("MEMORY_ALLOCATION") - || wt.StartsWith("PREEMPTIVE_"); - } + => WaitStatsConfig.IsExternal(waitType); /// /// Determines if an operator is relevant for a given wait category. diff --git a/src/PlanViewer.Core/Services/WaitStatsConfig.cs b/src/PlanViewer.Core/Services/WaitStatsConfig.cs new file mode 100644 index 0000000..1bb1016 --- /dev/null +++ b/src/PlanViewer.Core/Services/WaitStatsConfig.cs @@ -0,0 +1,106 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace PlanViewer.Core.Services; + +/// +/// Loads and serves the wait stats configuration from Resources/WaitStats.json +/// (embedded in PlanViewer.Core). Per issue #215 this is the single source of +/// truth — no other file is allowed to duplicate per-wait classifications, +/// time-calculation routing, or display flags. +/// +public static class WaitStatsConfig +{ + public sealed class Entry + { + [JsonPropertyName("name")] + public string Name { get; init; } = ""; + + [JsonPropertyName("isPreemptive")] + public bool IsPreemptive { get; init; } + + [JsonPropertyName("isExternal")] + public bool IsExternal { get; init; } + + [JsonPropertyName("isImplemented")] + public bool IsImplemented { get; init; } + + [JsonPropertyName("isEnabled")] + public bool IsEnabled { get; init; } + + [JsonPropertyName("showWaitCount")] + public bool? ShowWaitCount { get; init; } + + [JsonPropertyName("showAverageWaitTime")] + public bool? ShowAverageWaitTime { get; init; } + + [JsonPropertyName("timeCalculationModel")] + public string? TimeCalculationModel { get; init; } + } + + private static readonly Lazy> _byName = new(Load); + + public static Entry? Get(string waitType) + { + if (string.IsNullOrEmpty(waitType)) return null; + return _byName.Value.TryGetValue(waitType, out var e) ? e : null; + } + + /// + /// True iff the wait's time calculation model is "cpu time based" (preemptive + /// or external — the worker is CPU-busy in kernel rather than descheduled). + /// Lookup misses return false, preserving the prior default behavior for + /// unknown waits. + /// + public static bool IsExternal(string waitType) + => Get(waitType)?.IsExternal ?? false; + + /// + /// True iff effective per-wait latency (wait_ms / wait_count) should be + /// surfaced alongside totals. Defaults to false when the wait isn't in the + /// config — i.e. unknown waits don't get a latency line. + /// + public static bool ShowAverageWaitTime(string waitType) + => Get(waitType)?.ShowAverageWaitTime ?? false; + + private static Dictionary Load() + { + // The JSON ships embedded in PlanViewer.Core (manifest name + // PlanViewer.Core.Resources.WaitStats.json) and is also embedded into + // PlanViewer.Web's assembly via a linked , where the + // manifest prefix is PlanViewer.Web.* — so resolve by suffix to handle both. + var asm = typeof(WaitStatsConfig).Assembly; + var resourceName = asm.GetManifestResourceNames() + .FirstOrDefault(n => n.EndsWith("Resources.WaitStats.json", StringComparison.Ordinal)) + ?? throw new InvalidOperationException( + $"Embedded resource ending in 'Resources.WaitStats.json' not found in {asm.GetName().Name}. " + + "Check that Resources/WaitStats.json is included as in the project."); + + using var stream = asm.GetManifestResourceStream(resourceName) + ?? throw new InvalidOperationException($"Failed to open embedded resource '{resourceName}'."); + + using var reader = new StreamReader(stream); + var json = reader.ReadToEnd(); + + var doc = JsonSerializer.Deserialize(json, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + ReadCommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true, + }) ?? throw new InvalidOperationException("WaitStats.json deserialized to null."); + + var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var entry in doc.WaitStats) + { + if (string.IsNullOrEmpty(entry.Name)) continue; + dict[entry.Name] = entry; + } + return dict; + } + + private sealed class Document + { + [JsonPropertyName("waitStats")] + public List WaitStats { get; init; } = new(); + } +} diff --git a/src/PlanViewer.Core/Services/WaitStatsKnowledge.cs b/src/PlanViewer.Core/Services/WaitStatsKnowledge.cs deleted file mode 100644 index eb6343d..0000000 --- a/src/PlanViewer.Core/Services/WaitStatsKnowledge.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace PlanViewer.Core.Services; - -/// -/// Per-wait-type knowledge used when surfacing wait stats as warnings. -/// -/// CONTENT STATUS: descriptions and fix text are intentionally empty. The prior -/// copy was AI-drafted without expert review and Joe Obbish flagged some of it -/// as misleading (#215 D3). Entries are kept so the rendering pipeline keeps -/// emitting warnings with names, benefit %, and effective latency, but without -/// speculative advice until Erik / Joe fill in content. -/// -/// ShowEffectiveLatency flags stay because they're structural (emit a -/// wait_ms / wait_count statistic), not creative advice. -/// -public static class WaitStatsKnowledge -{ - public sealed class Entry - { - /// Short, human-readable explanation of what the wait represents. - public string Description { get; init; } = ""; - - /// Concrete guidance on how to reduce or eliminate the wait. - public string HowToFix { get; init; } = ""; - - /// - /// If true, surface an effective per-wait latency (wait_ms / wait_count) - /// in the warning message. Useful for latch/I/O waits where tail latency is - /// the thing to focus on. - /// - public bool ShowEffectiveLatency { get; init; } - } - - private static readonly Entry Default = new(); - - // Structural flags only (effective-latency display). Description/HowToFix pending - // expert-written content — see file-level comment. - private static readonly Dictionary Exact = new(StringComparer.OrdinalIgnoreCase) - { - ["PAGEIOLATCH_SH"] = new() { ShowEffectiveLatency = true }, - ["PAGEIOLATCH_EX"] = new() { ShowEffectiveLatency = true }, - ["PAGEIOLATCH_UP"] = new() { ShowEffectiveLatency = true }, - ["PAGEIOLATCH_DT"] = new() { ShowEffectiveLatency = true }, - }; - - /// - /// Look up the knowledge entry for a wait type. Falls back through family prefixes - /// for structural flags (effective-latency display) before returning a default. - /// Never returns null. - /// - public static Entry Lookup(string waitType) - { - if (string.IsNullOrEmpty(waitType)) return Default; - if (Exact.TryGetValue(waitType, out var exact)) return exact; - - var wt = waitType.ToUpperInvariant(); - - if (wt.StartsWith("PAGEIOLATCH_")) - return new Entry { ShowEffectiveLatency = true }; - - return Default; - } -} diff --git a/src/PlanViewer.Web/PlanViewer.Web.csproj b/src/PlanViewer.Web/PlanViewer.Web.csproj index 7d83410..9e23c72 100644 --- a/src/PlanViewer.Web/PlanViewer.Web.csproj +++ b/src/PlanViewer.Web/PlanViewer.Web.csproj @@ -26,7 +26,7 @@ - + @@ -36,4 +36,8 @@ + + + +