Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions Dashboard.Tests/InferenceEngineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,37 @@ public void Graph_CxPacketEdgeDoesNotFire_WhenSosIsLow()
var edges = graph.GetActiveEdges("CXPACKET", facts);
Assert.DoesNotContain(edges, e => e.Destination == "SOS_SCHEDULER_YIELD");
}

// WS3: a config-advisory fact (DB_CONFIG/SERVER_CONFIG) roots a standalone recommendation
// at its base severity (e.g. RCSI-off = 0.3), below the 0.5 incident threshold — so a
// standing misconfig surfaces on a quiet, healthy server. An incident fact at the same
// severity does NOT root.
[Fact]
public void ConfigFact_RootsStandalone_BelowMinimumSeverity()
{
var engine = new InferenceEngine(new RelationshipGraph());
var facts = new List<Fact>
{
new() { Key = "DB_CONFIG", Source = "config", Value = 1, Severity = 0.3,
Metadata = new Dictionary<string, double> { ["rcsi_off_count"] = 9 } }
};

var stories = engine.BuildStories(facts);

Assert.Contains(stories, s => s.RootFactKey == "DB_CONFIG");
}

[Fact]
public void IncidentFact_BelowMinimumSeverity_DoesNotRoot()
{
var engine = new InferenceEngine(new RelationshipGraph());
var facts = new List<Fact>
{
new() { Key = "CPU_SQL_PERCENT", Source = "cpu", Value = 60, Severity = 0.3 }
};

var stories = engine.BuildStories(facts);

Assert.DoesNotContain(stories, s => s.RootFactKey == "CPU_SQL_PERCENT");
}
}
24 changes: 22 additions & 2 deletions PerformanceMonitor.Analysis/InferenceEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@ public class InferenceEngine
private const double MinimumSeverityThreshold = 0.5;
private const int MaxPathDepth = 10; // Safety limit

/// <summary>
/// Config-advisory fact keys that root a finding at ANY positive severity, bypassing the
/// MinimumSeverityThreshold. A standing misconfiguration (RCSI off, auto-shrink on, MAXDOP at
/// a silly default) is an advisory the operator should see regardless of current load — unlike
/// an incident fact, which must clear 0.5 to be worth surfacing. The existing severity-ordered
/// <c>consumed</c> traversal still suppresses duplicates: a higher-severity incident story that
/// consumes the config fact (e.g. CXPACKET → CONFIG_MAXDOP, or LCK_M_S → DB_CONFIG) wins, and
/// only an UN-consumed config fact roots a standalone recommendation.
/// </summary>
private static readonly HashSet<string> ConfigAdvisoryRootKeys = new(StringComparer.Ordinal)
{
"DB_CONFIG",
"SERVER_CONFIG",
};

private readonly RelationshipGraph _graph;

public InferenceEngine(RelationshipGraph graph)
Expand All @@ -43,9 +58,14 @@ public List<AnalysisStory> BuildStories(List<Fact> facts)
.ToDictionary(f => f.Key, f => f);
var consumed = new HashSet<string>();

// Process facts in severity order
// Process facts in severity order. Incident facts must clear the 0.5 threshold to root;
// config-advisory facts (DB_CONFIG/SERVER_CONFIG) root at any positive severity so a
// standing misconfig surfaces on a quiet, healthy server (it would otherwise never reach
// 0.5 without contention — e.g. RCSI-off is base 0.3). Severity ordering + `consumed`
// (below) keep an incident story from being shadowed by, or duplicating, its config leaf.
var entryPoints = facts
.Where(f => f.Severity >= MinimumSeverityThreshold)
.Where(f => f.Severity >= MinimumSeverityThreshold
|| (ConfigAdvisoryRootKeys.Contains(f.Key) && f.Severity > 0))
.OrderByDescending(f => f.Severity)
.ToList();

Expand Down
Loading