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 @@
+
+
+
+