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
2 changes: 1 addition & 1 deletion Darling/Darling.Tests/ViewerBlockingSourceLabelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public void TheCharts_PutTheLabelOnTheAxisOnlyWhenTheyDrawRows()
var duration = Between(Tab, "private void RenderBlockingDurationChart(", "private void RenderBlockingTotalDurationChart(");
var total = Between(Tab, "private void RenderBlockingTotalDurationChart(", "private void RenderDeadlockWaitChart(");

Assert.Single(Occurrences(trend, "BlockingSourceLabel.For(\"Blocking Incidents\", data.Select(d => d.Source))"));
Assert.Single(Occurrences(trend, "BlockingSourceLabel.For(\"Blocking events\", data.Select(d => d.Source))"));
Assert.Single(Occurrences(duration, "BlockingSourceLabel.For(\"Block Duration (ms)\", data.Select(d => d.Source))"));
Assert.Single(Occurrences(total, "BlockingSourceLabel.For(\"Total Block Duration (ms)\", data.Select(d => d.Source))"));
/* The zero-row branch returns before it: nothing answered, so nothing is named. */
Expand Down
113 changes: 113 additions & 0 deletions Darling/Darling.Tests/ViewerShortPlotTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright (c) 2026 Erik Darling, Darling Data LLC
*
* This file is part of the SQL Server Performance Monitor.
*
* Licensed under the MIT License. See LICENSE file in the project root for full license information.
*/

using System.Linq;
using PerformanceMonitor.Ui;
using ScottPlot;
using Xunit;

namespace Darling.Tests;

/// <summary>
/// #6652: at 1366x768 the Viewer's stacked charts get short plots. A long left-axis title was cut at both ends (the unit
/// lost) and the y axis could show only "0". The title fit and the two-tick floor are the pure rules behind the fix.
/// </summary>
public sealed class ViewerShortPlotTests
{
// The plot height a chart row at its minimum leaves for the rotated title.
private static double MinRowTitleSpace => ChartLayout.MinPlotHeight - ChartAxisTitle.ReservedHeight;

public static TheoryData<string> LongTitles => new()
{
"Lock Wait Time (ms/sec)",
"Total Block Duration (ms)",
"Total Deadlock Wait (ms)",
"Total Wait Duration (ms)",
"Avg Wait Time (ms/wait)",
"Read Throughput (MB/s)",
"Write Throughput (MB/s)",
"Pressure Events per Hour",
"Slowest run per bucket (ms)",
"Plan Cache Size (MB)",
};

[Theory]
[MemberData(nameof(LongTitles))]
public void LongTitle_FitsTheMinimumRowAndKeepsItsUnit(string title)
{
var (text, size) = ChartAxisTitle.Fit(title, MinRowTitleSpace);

Assert.InRange(size, ChartAxisTitle.MinFontSize, ChartAxisTitle.BaseFontSize);
Assert.True(text.Length * 0.6 * size <= MinRowTitleSpace + 0.5, $"'{text}' at {size}pt is longer than {MinRowTitleSpace}px");
if (title.EndsWith(')'))
{
Assert.EndsWith(title[title.LastIndexOf('(')..], text);
}
}

[Fact]
public void ShortTitle_IsLeftAlone()
{
Assert.Equal(("CPU %", ChartAxisTitle.BaseFontSize), ChartAxisTitle.Fit("CPU %", MinRowTitleSpace));
}

[Fact]
public void LongSourceNote_IsTrimmedNotTreatedAsAUnit()
{
var (text, _) = ChartAxisTitle.Fit("Blocking events (blocked-process-report)", MinRowTitleSpace);
Assert.StartsWith("Blocking", text);
}

[Theory]
[InlineData(0)]
[InlineData(-5)]
[InlineData(double.NaN)]
public void UnknownSpace_ReturnsTheTitleUnchanged(double available)
{
Assert.Equal(("Lock wait ms/s", ChartAxisTitle.BaseFontSize), ChartAxisTitle.Fit("Lock wait ms/s", available));
}

[Fact]
public void AllZeroAxis_GetsAtLeastTwoLabelledTicks()
{
var ticks = MinimumTickGenerator.EnsureMinimum(System.Array.Empty<Tick>(), 0, 1);

Assert.True(ticks.Count(t => t.IsMajor) >= 2);
Assert.Contains(ticks, t => t.Label == "0");
Assert.Contains(ticks, t => t.Label == "1");
}

[Fact]
public void OnlyZeroTick_GetsTheTopOfTheAxisAdded()
{
var ticks = MinimumTickGenerator.EnsureMinimum(new[] { Tick.Major(0, "0") }, 0, 1.15);

Assert.Equal(new[] { 0.0, 1.15 }, ticks.Select(t => t.Position).ToArray());
Assert.Equal("1.15", ticks[1].Label);
}

[Fact]
public void EnoughTicks_AreReturnedUntouched()
{
var given = new[] { Tick.Major(0, "0"), Tick.Major(50, "50"), Tick.Major(100, "100") };
Assert.Same(given, MinimumTickGenerator.EnsureMinimum(given, 0, 105));
}

[Fact]
public void ZeroWidthRange_StillGetsTwoTicks()
{
var ticks = MinimumTickGenerator.EnsureMinimum(System.Array.Empty<Tick>(), 0, 0);
Assert.Equal(new[] { 0.0, 1.0 }, ticks.Select(t => t.Position).ToArray());
}

[Fact]
public void MinimumPlotHeight_IsTallEnoughForTicks()
{
Assert.InRange(ChartLayout.MinPlotHeight, 140, 200);
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
<UserControl x:Class="PerformanceMonitor.Darling.Viewer.CorrelatedTimelineLanesControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ScottPlot="clr-namespace:ScottPlot.WPF;assembly=ScottPlot.WPF">
xmlns:ScottPlot="clr-namespace:ScottPlot.WPF;assembly=ScottPlot.WPF"
xmlns:ui="clr-namespace:PerformanceMonitor.Ui;assembly=PerformanceMonitor.Ui">

<Grid Margin="8">
<!-- #6652: five lanes keep a minimum height each; the control scrolls when they do not fit (1366x768). -->
<ScrollViewer Margin="8" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<Grid Height="{Binding ViewportHeight, RelativeSource={RelativeSource AncestorType=ScrollViewer}}" MinHeight="750">
<Grid.RowDefinitions>
<!-- 5 lanes, equal height -->
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*" MinHeight="{x:Static ui:ChartLayout.MinPlotHeight}"/>
<RowDefinition Height="*" MinHeight="{x:Static ui:ChartLayout.MinPlotHeight}"/>
<RowDefinition Height="*" MinHeight="{x:Static ui:ChartLayout.MinPlotHeight}"/>
<RowDefinition Height="*" MinHeight="{x:Static ui:ChartLayout.MinPlotHeight}"/>
<RowDefinition Height="*" MinHeight="{x:Static ui:ChartLayout.MinPlotHeight}"/>
</Grid.RowDefinitions>

<!-- Lane 1: CPU -->
Expand Down Expand Up @@ -88,4 +91,5 @@
<ScottPlot:WpfPlot Grid.Column="1" x:Name="FileIoChart"/>
</Grid>
</Grid>
</ScrollViewer>
</UserControl>
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ public partial class CorrelatedTimelineLanesControl : UserControl
public CorrelatedTimelineLanesControl()
{
InitializeComponent();
// #6652: the lanes scroll at small sizes; the wheel over a lane stays that lane's zoom.
PerformanceMonitor.Ui.ChartLayout.KeepWheelOnPlot(CpuChart, WaitStatsChart, BlockingChart, MemoryChart, FileIoChart);
/* No Unloaded → Dispose() handler: WPF fires Unloaded for transient
reasons (tab virtualization, layout rebuilds) and Dispose() clears
the crosshair manager's lane list, permanently breaking the crosshair
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ private void RenderLockWaitTrendChart(List<LockWaitTrendPoint> data)
LockWaitTrendChart.Plot.Axes.DateTimeTicksBottomUtc(ViewerTimeHelper.CurrentDisplayZone);
LockWaitTrendChart.Plot.Axes.SetLimitsX(rangeStart.ToOADate(), rangeEnd.ToOADate());
ReapplyAxisColors(LockWaitTrendChart);
LockWaitTrendChart.Plot.YLabel("Lock Wait Time (ms/sec)");
LockWaitTrendChart.Plot.YLabel("Lock wait ms/s");
SetChartYLimitsWithLegendPadding(LockWaitTrendChart, 0, 1);
ShowChartLegend(LockWaitTrendChart);
LockWaitTrendChart.Refresh();
Expand Down Expand Up @@ -534,7 +534,7 @@ private void RenderLockWaitTrendChart(List<LockWaitTrendPoint> data)
LockWaitTrendChart.Plot.Axes.DateTimeTicksBottomUtc(ViewerTimeHelper.CurrentDisplayZone);
LockWaitTrendChart.Plot.Axes.SetLimitsX(rangeStart.ToOADate(), rangeEnd.ToOADate());
ReapplyAxisColors(LockWaitTrendChart);
LockWaitTrendChart.Plot.YLabel("Lock Wait Time (ms/sec)");
LockWaitTrendChart.Plot.YLabel("Lock wait ms/s");
SetChartYLimitsWithLegendPadding(LockWaitTrendChart, 0, globalMax > 0 ? globalMax : 1);
ShowChartLegend(LockWaitTrendChart);
LockWaitTrendChart.Refresh();
Expand Down Expand Up @@ -563,7 +563,7 @@ private void RenderBlockingTrendChart(List<BlockingTrendPoint> data)
BlockingTrendChart.Plot.Axes.DateTimeTicksBottomUtc(ViewerTimeHelper.CurrentDisplayZone);
BlockingTrendChart.Plot.Axes.SetLimitsX(rangeStart.ToOADate(), rangeEnd.ToOADate());
ReapplyAxisColors(BlockingTrendChart);
BlockingTrendChart.Plot.YLabel("Blocking Incidents");
BlockingTrendChart.Plot.YLabel("Blocking events");
SetChartYLimitsWithLegendPadding(BlockingTrendChart, 0, 1);
ShowChartLegend(BlockingTrendChart);
BlockingTrendChart.Refresh();
Expand Down Expand Up @@ -605,7 +605,7 @@ private void RenderBlockingTrendChart(List<BlockingTrendPoint> data)
BlockingTrendChart.Plot.Axes.DateTimeTicksBottomUtc(ViewerTimeHelper.CurrentDisplayZone);
BlockingTrendChart.Plot.Axes.SetLimitsX(rangeStart.ToOADate(), rangeEnd.ToOADate());
ReapplyAxisColors(BlockingTrendChart);
BlockingTrendChart.Plot.YLabel(PerformanceMonitor.Ui.BlockingSourceLabel.For("Blocking Incidents", data.Select(d => d.Source)));
BlockingTrendChart.Plot.YLabel(PerformanceMonitor.Ui.BlockingSourceLabel.For("Blocking events", data.Select(d => d.Source)));
SetChartYLimitsWithLegendPadding(BlockingTrendChart, 0, data.Max(d => d.Count));
ShowChartLegend(BlockingTrendChart);
BlockingTrendChart.Refresh();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ private void RenderTempDbFileIoChart(List<TempDbFileIoSample> data, DateTime sta
TempDbFileIoChart.Plot.Axes.DateTimeTicksBottomUtc(ViewerTimeHelper.CurrentDisplayZone);
TempDbFileIoChart.Plot.Axes.SetLimitsX(rangeStart, rangeEnd);
ReapplyAxisColors(TempDbFileIoChart);
TempDbFileIoChart.Plot.YLabel("tempdb File I/O Latency (ms)");
TempDbFileIoChart.Plot.YLabel("tempdb latency ms");
SetChartYLimitsWithLegendPadding(TempDbFileIoChart, 0, maxLatency > 0 ? maxLatency : 10);
ShowChartLegend(TempDbFileIoChart);
TempDbFileIoChart.Refresh();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ private void UpdateQueryDurationTrendChart(QueryTrendSeries series, DateTime sta
ClearChart(QueryDurationTrendChart);
ApplyTheme(QueryDurationTrendChart);

if (data.Count == 0) { RefreshEmptyChart(QueryDurationTrendChart, "Query Duration", "Duration (ms/sec)"); return; }
if (data.Count == 0) { RefreshEmptyChart(QueryDurationTrendChart, "Query Duration", "Duration ms/s"); return; }
ShowTrendCoverage(QueryDurationTrendChart, series);

var rangeStart = startUtc.ToOADate();
Expand All @@ -246,7 +246,7 @@ private void UpdateQueryDurationTrendChart(QueryTrendSeries series, DateTime sta
QueryDurationTrendChart.Plot.Axes.DateTimeTicksBottomUtc(ViewerTimeHelper.CurrentDisplayZone);
QueryDurationTrendChart.Plot.Axes.SetLimitsX(rangeStart, rangeEnd);
ReapplyAxisColors(QueryDurationTrendChart);
QueryDurationTrendChart.Plot.YLabel("Duration (ms/sec)");
QueryDurationTrendChart.Plot.YLabel("Duration ms/s");
SetChartYLimitsWithLegendPadding(QueryDurationTrendChart, 0, values.Max());
ShowChartLegend(QueryDurationTrendChart);
QueryDurationTrendChart.Refresh();
Expand All @@ -258,7 +258,7 @@ private void UpdateProcDurationTrendChart(QueryTrendSeries series, DateTime star
ClearChart(ProcDurationTrendChart);
ApplyTheme(ProcDurationTrendChart);

if (data.Count == 0) { RefreshEmptyChart(ProcDurationTrendChart, "Procedure Duration", "Duration (ms/sec)"); return; }
if (data.Count == 0) { RefreshEmptyChart(ProcDurationTrendChart, "Procedure Duration", "Duration ms/s"); return; }
ShowTrendCoverage(ProcDurationTrendChart, series);

var rangeStart = startUtc.ToOADate();
Expand All @@ -277,7 +277,7 @@ private void UpdateProcDurationTrendChart(QueryTrendSeries series, DateTime star
ProcDurationTrendChart.Plot.Axes.DateTimeTicksBottomUtc(ViewerTimeHelper.CurrentDisplayZone);
ProcDurationTrendChart.Plot.Axes.SetLimitsX(rangeStart, rangeEnd);
ReapplyAxisColors(ProcDurationTrendChart);
ProcDurationTrendChart.Plot.YLabel("Duration (ms/sec)");
ProcDurationTrendChart.Plot.YLabel("Duration ms/s");
SetChartYLimitsWithLegendPadding(ProcDurationTrendChart, 0, values.Max());
ShowChartLegend(ProcDurationTrendChart);
ProcDurationTrendChart.Refresh();
Expand All @@ -289,7 +289,7 @@ private void UpdateQueryStoreDurationTrendChart(QueryStoreTrendSeries series, Da
ClearChart(QueryStoreDurationTrendChart);
ApplyTheme(QueryStoreDurationTrendChart);

if (data.Count == 0) { RefreshEmptyChart(QueryStoreDurationTrendChart, "Query Store Duration", "Duration (ms/sec)"); return; }
if (data.Count == 0) { RefreshEmptyChart(QueryStoreDurationTrendChart, "Query Store Duration", "Duration ms/s"); return; }
ShowTrendCoverage(QueryStoreDurationTrendChart, series);

var rangeStart = startUtc.ToOADate();
Expand All @@ -308,7 +308,7 @@ private void UpdateQueryStoreDurationTrendChart(QueryStoreTrendSeries series, Da
QueryStoreDurationTrendChart.Plot.Axes.DateTimeTicksBottomUtc(ViewerTimeHelper.CurrentDisplayZone);
QueryStoreDurationTrendChart.Plot.Axes.SetLimitsX(rangeStart, rangeEnd);
ReapplyAxisColors(QueryStoreDurationTrendChart);
QueryStoreDurationTrendChart.Plot.YLabel("Duration (ms/sec)");
QueryStoreDurationTrendChart.Plot.YLabel("Duration ms/s");
SetChartYLimitsWithLegendPadding(QueryStoreDurationTrendChart, 0, values.Max());
ShowChartLegend(QueryStoreDurationTrendChart);
QueryStoreDurationTrendChart.Refresh();
Expand Down
Loading
Loading