diff --git a/Darling/Darling.Tests/Viewer4578Tests.cs b/Darling/Darling.Tests/Viewer4578Tests.cs new file mode 100644 index 0000000000..0f811054c3 --- /dev/null +++ b/Darling/Darling.Tests/Viewer4578Tests.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using PerformanceMonitor.Common; +using Xunit; + +namespace Darling.Tests; + +/// +/// #4578: the wait-category chart colours port PerformanceStudio dev's contrast-checked palette +/// (erikdarlingdata/PerformanceStudio@161785f) into byte for +/// byte. Golden pin on every category's hex, plus a WCAG contrast pin against the dark chart +/// background PS measured against. +/// +public class Viewer4578Tests +{ + // PS DarkTheme.axaml's WaitCategory.* brushes at 161785f (unchanged through PS dev 85492a1). + private static readonly Dictionary ExpectedDarkHex = + new(StringComparer.OrdinalIgnoreCase) + { + ["Unknown"] = "#9CA1A5", + ["CPU"] = "#3EBD50", + ["Worker Thread"] = "#72E1A9", + ["Lock"] = "#FB6640", + ["Latch"] = "#FEB5ED", + ["Buffer Latch"] = "#E8469D", + ["Buffer IO"] = "#238CFA", + ["Compilation"] = "#A1A3FB", + ["SQL CLR"] = "#63C5C6", + ["Mirroring"] = "#30907E", + ["Transaction"] = "#DB9027", + ["Preemptive"] = "#FFA8C0", + ["Service Broker"] = "#FF9289", + ["Tran Log IO"] = "#8AC6FF", + ["Network IO"] = "#BBE2FE", + ["Parallelism"] = "#9A84FE", + ["Memory"] = "#FFE6B1", + ["Tracing"] = "#BDC8CE", + ["Full Text Search"] = "#D8F2FC", + ["Other Disk IO"] = "#0FB3E2", + ["Replication"] = "#5DF9ED", + ["Log Rate Governor"] = "#FFCACA", + ["Others"] = "#676E73", + // PM-ahead: PS's ~161785f diff shows no "Batch Mode" category (PM classifies batch-mode + // hash/bitmap waits separately; PS folds them elsewhere). Not part of the port, kept as-is. + }; + + // PS's plan-viewer background brush (BackgroundBrush, DarkTheme.axaml) that PS measured + // 3:1 contrast against when laying the palette out. + private const string DarkChartBackground = "#1A1D23"; + + [Theory] + [MemberData(nameof(CategoryNames))] + public void WaitColor_MatchesPerformanceStudioDarkPalette(string category) + { + var expected = ExpectedDarkHex[category]; + var actual = ChartPalette.WaitColor(category, lightBackground: false); + + Assert.Equal(expected, actual, ignoreCase: true); + } + + [Theory] + [MemberData(nameof(CategoryNames))] + public void WaitColor_ClearsWcagContrastFloorAgainstDarkChartBackground(string category) + { + var hex = ChartPalette.WaitColor(category, lightBackground: false); + + var ratio = ContrastRatio(hex, DarkChartBackground); + + Assert.True(ratio >= 3.0, $"{category} = {hex} contrast {ratio:F2} against {DarkChartBackground}, below the 3:1 floor"); + } + + public static IEnumerable CategoryNames() + { + foreach (var name in ExpectedDarkHex.Keys) + yield return new object[] { name }; + } + + // WCAG 2.x relative-luminance contrast ratio (sRGB). + private static double ContrastRatio(string hexA, string hexB) + { + var lumA = RelativeLuminance(hexA); + var lumB = RelativeLuminance(hexB); + var lighter = Math.Max(lumA, lumB); + var darker = Math.Min(lumA, lumB); + return (lighter + 0.05) / (darker + 0.05); + } + + private static double RelativeLuminance(string hex) + { + hex = hex.TrimStart('#'); + var r = Convert.ToInt32(hex.Substring(0, 2), 16); + var g = Convert.ToInt32(hex.Substring(2, 2), 16); + var b = Convert.ToInt32(hex.Substring(4, 2), 16); + + static double Linearize(int channel) + { + var c = channel / 255.0; + return c <= 0.03928 ? c / 12.92 : Math.Pow((c + 0.055) / 1.055, 2.4); + } + + return 0.2126 * Linearize(r) + 0.7152 * Linearize(g) + 0.0722 * Linearize(b); + } +} diff --git a/PerformanceMonitor.Common/ChartPalette.cs b/PerformanceMonitor.Common/ChartPalette.cs index 42c22fcd7c..7fad9ef808 100644 --- a/PerformanceMonitor.Common/ChartPalette.cs +++ b/PerformanceMonitor.Common/ChartPalette.cs @@ -147,46 +147,53 @@ public static string AccentColor(string name) => Accents.TryGetValue(name, out var hex) ? hex : "#B0BEC5"; // ── Wait categories ──────────────────────────────────────────────────────────────── - // PerformanceStudio's ~21 semantic wait-category colors (Themes/DarkTheme.axaml). Constant - // across themes EXCEPT the handful flagged near-invisible on a light background, which get a - // darker variant when lightBackground = true (D3). + // PerformanceStudio's wait-category colors (Themes/DarkTheme.axaml, laid out rather than + // picked: erikdarlingdata/PerformanceStudio@161785f). Hue carries the family - greens for + // compute, blues for storage and network, warm for contention, grey for "no information" - + // and within a family the categories climb a lightness ladder, which is what keeps + // neighbours apart under simulated deuteranopia. Every color clears 3:1 contrast against + // the dark chart background. Constant across themes EXCEPT the handful flagged + // near-invisible on a light background, which get a darker variant when + // lightBackground = true (D3). private static readonly Dictionary WaitCategoryColors = new(StringComparer.OrdinalIgnoreCase) { - ["CPU"] = "#00BD23", - ["Worker Thread"] = "#52E3B5", - ["Lock"] = "#EE5A24", - ["Latch"] = "#F368E0", - ["Buffer Latch"] = "#F5069E", - ["Buffer IO"] = "#2D13F2", - ["Compilation"] = "#A29BFE", - ["SQL CLR"] = "#B8DCDC", - ["Mirroring"] = "#54625F", - ["Transaction"] = "#C77416", - ["Preemptive"] = "#FD79A8", - ["Service Broker"] = "#E17055", - ["Tran Log IO"] = "#0984E3", - ["Network IO"] = "#75BBF8", - ["Parallelism"] = "#7B4FFF", + ["CPU"] = "#3EBD50", + ["Worker Thread"] = "#72E1A9", + ["Lock"] = "#FB6640", + ["Latch"] = "#FEB5ED", + ["Buffer Latch"] = "#E8469D", + ["Buffer IO"] = "#238CFA", + ["Compilation"] = "#A1A3FB", + ["SQL CLR"] = "#63C5C6", + ["Mirroring"] = "#30907E", + ["Transaction"] = "#DB9027", + ["Preemptive"] = "#FFA8C0", + ["Service Broker"] = "#FF9289", + ["Tran Log IO"] = "#8AC6FF", + ["Network IO"] = "#BBE2FE", + ["Parallelism"] = "#9A84FE", ["Batch Mode"] = "#00CEC9", - ["Memory"] = "#FDCB6E", - ["Tracing"] = "#B2BEC3", - ["Full Text Search"] = "#DFE6E9", - ["Other Disk IO"] = "#636E72", - ["Replication"] = "#81ECEC", - ["Log Rate Governor"]= "#FAB1A0", - ["Unknown"] = "#8E8E93", - ["Others"] = "#555D66", + ["Memory"] = "#FFE6B1", + ["Tracing"] = "#BDC8CE", + ["Full Text Search"] = "#D8F2FC", + ["Other Disk IO"] = "#0FB3E2", + ["Replication"] = "#5DF9ED", + ["Log Rate Governor"]= "#FFCACA", + ["Unknown"] = "#9CA1A5", + ["Others"] = "#676E73", }; - // Darker variants for the three categories that are near-invisible on light/CoolBreeze - // backgrounds (D3). Only consulted when lightBackground = true. + // Darker variants for the categories that are near-invisible on light/CoolBreeze + // backgrounds (D3). Only consulted when lightBackground = true. Re-checked against the + // new hexes above (161785f): SQL CLR and Full Text Search still need a light-theme + // override; Replication's new hue is dark enough to read on light backgrounds unmodified, + // so its override is dropped (PM-ahead: kept the mechanism, retired the entry). private static readonly Dictionary WaitCategoryColorsLight = new(StringComparer.OrdinalIgnoreCase) { - ["SQL CLR"] = "#5BA3A3", // was #B8DCDC - ["Full Text Search"] = "#8FA3AB", // was #DFE6E9 - ["Replication"] = "#1FA8A8", // was #81ECEC + ["SQL CLR"] = "#3E8384", // was #5BA3A3, now darkened vs new #63C5C6 + ["Full Text Search"] = "#6E8890", // was #8FA3AB, now darkened vs new #D8F2FC }; /// Color for a wait category. Pass lightBackground=true on Light/CoolBreeze themes.