Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
f0402ce
Tests for the shared display-zone time parts: wall-clock ticks, typed…
erikdarlingdata Sep 29, 2026
5ba8dec
Shared display-zone time parts: wall-clock ticks, typed bounds and UT…
erikdarlingdata Sep 29, 2026
0e8cece
Test: keep a zone id assertion from reading as a server name and ordi…
erikdarlingdata Sep 29, 2026
d1aaa7b
Lite: time windows stay UTC from the picker, slicer and drill to the …
erikdarlingdata Sep 29, 2026
a00e115
Darling viewer: typed time ranges keep the instants they name across …
erikdarlingdata Sep 29, 2026
f5557eb
Merge origin/dev into fix/4766-display-frame
erikdarlingdata Sep 29, 2026
e374c9a
Darling viewer: the clock for a server with no collected clock is cho…
erikdarlingdata Sep 29, 2026
392ab66
Lite tests: windows are UTC pairs from the tab to the read, so the pi…
erikdarlingdata Sep 29, 2026
3558aa4
Merge branch 'fix/4766-display-frame' of https://github.com/erikdarli…
erikdarlingdata Sep 29, 2026
882402e
Lite tests: the drill, chart-axis and clock source pins follow the UT…
erikdarlingdata Sep 29, 2026
07db701
Test: leave the alert row time test as it was; a finally in that file…
erikdarlingdata Sep 29, 2026
5d3ef30
Merge origin/fix/4766-display-frame
erikdarlingdata Sep 29, 2026
a1d8732
Lite tests: a range over the repeated hour reads that hour, the slice…
erikdarlingdata Sep 29, 2026
b8816da
Merge branch 'fix/4766-display-frame' of https://github.com/erikdarli…
erikdarlingdata Sep 29, 2026
dc754e9
Merge remote-tracking branch 'origin/dev' into frame-l2a2
erikdarlingdata Sep 29, 2026
365bf3f
Lite tests: the collection-log and CPU window reads take their custom…
erikdarlingdata Sep 29, 2026
bc3ea8e
Darling viewer: charts plot the instant, and ticks, hover, the crossh…
erikdarlingdata Sep 29, 2026
0d51a15
Merge remote-tracking branch 'origin/fix/4766-display-frame' into fra…
erikdarlingdata Sep 29, 2026
2a2e0e7
Lite: the CPU read can bucket on each sample's own instant, so two re…
erikdarlingdata Sep 29, 2026
fa91314
Merge branch 'fix/4766-display-frame' of https://github.com/erikdarli…
erikdarlingdata Sep 29, 2026
053c1aa
Lite: the UTC frame's wall-time conversion takes a local, not the col…
erikdarlingdata Sep 29, 2026
b52a8e4
Lite: server tab charts plot the instant, and their ticks and hover s…
erikdarlingdata Sep 29, 2026
85b7dcd
Merge branch 'fix/4766-display-frame' of https://github.com/erikdarli…
erikdarlingdata Sep 29, 2026
e1e5857
Merge branch 'fix/4766-display-frame' of https://github.com/erikdarli…
erikdarlingdata Sep 29, 2026
959713a
Lite: text times, the alert history list and recommendation cards sho…
erikdarlingdata Sep 29, 2026
ca01e92
Lite: history windows plot the instant, and the time slicers label wh…
erikdarlingdata Sep 29, 2026
ca572f2
Merge branch 'fix/4766-display-frame' of https://github.com/erikdarli…
erikdarlingdata Sep 29, 2026
880c12e
Merge remote-tracking branch 'origin/dev' into frame-l3d
erikdarlingdata Sep 29, 2026
89cba7f
Darling viewer: the CSV export and the chart renderers are pinned to …
erikdarlingdata Sep 29, 2026
2e7420d
Merge remote-tracking branch 'origin/fix/4766-display-frame'
erikdarlingdata Sep 29, 2026
8dd8dcf
Merge remote-tracking branch 'origin/fix/4766-display-frame' into fra…
erikdarlingdata Sep 29, 2026
d91707d
Lite: the Overview lanes and the CPU chart plot each sample's instant…
erikdarlingdata Sep 29, 2026
06ba7aa
Darling viewer: the alert history clock pin follows the cached read, …
erikdarlingdata Sep 29, 2026
e9221b0
Lite: the chart CSV export and the last refresh time show the display…
erikdarlingdata Sep 29, 2026
3558416
Merge dev into fix/4766-display-frame
erikdarlingdata Sep 29, 2026
1468bb3
Merge branch 'fix/4766-display-frame' of https://github.com/erikdarli…
erikdarlingdata Sep 29, 2026
ec27d95
Lite.Tests: the slicer wiring pin no longer reads as a user of the sh…
erikdarlingdata Sep 29, 2026
3e37ebb
Merge remote-tracking branch 'origin/fix/4766-display-frame' into fra…
erikdarlingdata Sep 29, 2026
ce71ae6
Merge origin/fix/4766-display-frame
erikdarlingdata Sep 29, 2026
4de540f
Lite: the chart time conversion tests stay in the shared-clock collec…
erikdarlingdata Sep 29, 2026
56b5d91
Lite: the version store chart and the server tab's remaining time lab…
erikdarlingdata Sep 29, 2026
e8e2c87
Merge branch 'fix/4766-display-frame' of https://github.com/erikdarli…
erikdarlingdata Sep 29, 2026
a88d45c
Merge remote-tracking branch 'origin/frame-l3a' into frame-m3a
erikdarlingdata Sep 29, 2026
93ccacd
Lite: a history window's grid shows its times in the window's own zon…
erikdarlingdata Sep 29, 2026
79cb95f
Merge branch 'fix/4766-display-frame' of https://github.com/erikdarli…
erikdarlingdata Sep 29, 2026
7455e60
Lite: the 'Showing since' banner and the last refresh label follow th…
erikdarlingdata Sep 29, 2026
b41597a
Merge branch 'fix/4766-display-frame' of https://github.com/erikdarli…
erikdarlingdata Sep 29, 2026
8104e1a
Merge branch 'fix/4766-display-frame' of https://github.com/erikdarli…
erikdarlingdata Sep 29, 2026
e32fdc1
Lite.Tests: the last refresh pin accepts the refresh instant read onc…
erikdarlingdata Sep 29, 2026
2a1b931
Merge branch 'fix/4766-display-frame' of https://github.com/erikdarli…
erikdarlingdata Sep 29, 2026
d4e3e9c
Merge remote-tracking branch 'origin/fix/4766-display-frame' into fra…
erikdarlingdata Sep 29, 2026
93f447f
Merge branch 'fix/4766-display-frame' of https://github.com/erikdarli…
erikdarlingdata Sep 29, 2026
a8f937e
Lite: the history rows' comment on the server-clock columns says what…
erikdarlingdata Sep 29, 2026
dbd94a1
Merge branch 'fix/4766-display-frame' of https://github.com/erikdarli…
erikdarlingdata Sep 29, 2026
90e8c74
Lite: the old chart time hook is gone, and a pin keeps the old axis a…
erikdarlingdata Sep 29, 2026
baad8e5
Lite.Tests: the chart time conversion tests' summary says X is the na…
erikdarlingdata Sep 29, 2026
58811de
Merge branch 'fix/4766-display-frame' of https://github.com/erikdarli…
erikdarlingdata Sep 29, 2026
121b26f
Lite: a history window's server-clock columns use the opening tab's c…
erikdarlingdata Sep 29, 2026
01597a0
Darling.Tests: the frame census and the member scan account for the h…
erikdarlingdata Sep 29, 2026
4948667
Merge remote-tracking branch 'origin/dev' into fix/4766-display-frame
erikdarlingdata Sep 29, 2026
a57b0e7
Lite.Tests: the tab pin lets the two stats windows take the tab's own…
erikdarlingdata Sep 30, 2026
c165fe7
Lite: comments and test summaries describe the instant-based chart fr…
erikdarlingdata Sep 30, 2026
e429809
Merge dev into fix/4766-display-frame
erikdarlingdata Sep 30, 2026
e38c855
Lite: the display zone and the open tab's clock are required where ev…
erikdarlingdata Sep 30, 2026
181421d
Merge dev into fix/4766-display-frame
erikdarlingdata Sep 30, 2026
61bbfb5
Darling viewer: Apply to All leaves the visible tab's clock in place,…
erikdarlingdata Sep 30, 2026
68ba22c
Merge branch 'fix/4766-display-frame' of https://github.com/erikdarli…
erikdarlingdata Sep 30, 2026
1d4ac5e
Merge the display-frame branch's latest commits
erikdarlingdata Sep 30, 2026
ca767b2
Lite: the Recommendations tab takes the open tab's clock as a require…
erikdarlingdata Sep 30, 2026
38f349e
Merge branch 'dev' into fix/4766-display-frame
erikdarlingdata Sep 30, 2026
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
34 changes: 19 additions & 15 deletions Darling/Darling.Tests/ConsumedTimestampFrameDisciplineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1158,8 +1158,11 @@ private static string Pascal(string column) =>
reached DateTime.ToLocalTime() rather than any renderer, so nothing judged it) and the four
plan_correction stamps in the Darling viewer, which name their renderer at the site instead of
reaching one through a wrapper (FormatStoredUtc since #3419, FormatServerClock before it - the
site count is the same either way, which is why this figure did not move). */
private const int JudgedRenderSites = 43;
site count is the same either way, which is why this figure did not move). The 43 became 46 for two
reasons (#4766): this branch's head judged 42, one below the 43, and the history rows' four server-clock
properties (a query's creation and last execution, a procedure's cached and last execution) now name
FormatServerClock in both arms of their Clock test, which adds four. */
private const int JudgedRenderSites = 46;

/* Call sites of a declared RenderWrapper passing a census column, and how many (file, method)
wrappers reach one at all. Measured at 16 sites across 2 of the 6 wrappers - every one of them
Expand All @@ -1169,8 +1172,10 @@ wrappers reach one at all. Measured at 16 sites across 2 of the 6 wrappers - eve
private const int WrapperCallSitesOverCensusColumns = 16;

/* Calls of ANY identifier on a census timestamp column across the render surface. Floored so the
shape-free residual check cannot satisfy its set equality with an empty left side. */
private const int CallsOnACensusColumn = 72;
shape-free residual check cannot satisfy its set equality with an empty left side. Measured at 79
(#4766): the 72 before, plus the four second arms of the history rows' Clock tests and the calls the
branch's earlier changes added. */
private const int CallsOnACensusColumn = 79;
private const int WrapperFilesReachingACensusColumn = 2;

/// <summary>
Expand Down Expand Up @@ -1575,6 +1580,9 @@ private static readonly (string Name, string Why)[] NonRenderingCallsOnACensusCo
+ "stored server-local stamp to naive UTC, which is the de-skew itself rather than a rendering of it"),
("UtcOrNull", "UtcOrNull(r.BlockedLastTranStarted) and the like in Lite get_blocking and get_pvs_stats (#4793) "
+ "- the file's local function for the same server-clock conversion of a nullable stamp to naive UTC"),
("GhostX", "TimeWindows.GhostX(d.CollectionTime, days, zone) in Lite's Overview lanes (#4766) - moves a "
+ "comparison row's naive-UTC instant onto the current axis by a day count, so it converts an instant "
+ "and does not word one"),
];

/// <summary>
Expand Down Expand Up @@ -1751,18 +1759,14 @@ outside the census with nothing failing. */

/// <summary>
/// Render sites this guard DECLINES: a frame-ambiguous property name whose table cannot be narrowed to
/// one frame, because the file holds no SQL and no row type that resolves it. All three plot
/// <c>cpu_utilization_stats.sample_time</c>, which <c>GetCpuUtilizationAsync</c> de-skews to naive UTC
/// in SQL (#1262) before the chart sees it — so <c>ForDisplay</c> is correct there — but it is the
/// READ that establishes that, not anything this scan can see. Pinned at set equality so the decline
/// cannot grow, which is the whole cost of refusing to key on the column name.
/// one frame, because the file holds no SQL and no row type that resolves it. There were three, all plotting
/// <c>cpu_utilization_stats.sample_time</c> (which <c>GetCpuUtilizationAsync</c> de-skews to naive UTC in SQL,
/// #1262, before the chart sees it) through <c>ForDisplay</c>; since #4766 a chart plots the UTC instant and
/// draws its labels in the display zone, so no chart projects a sample time through <c>ForDisplay</c> any
/// more and the decline is empty. Pinned at set equality so a new decline is a deliberate edit, which is the
/// whole cost of refusing to key on the column name.
/// </summary>
private static readonly string[] DeclinedAmbiguousRenderSites =
[
"Darling/PerformanceMonitor.Darling.Viewer/CorrelatedTimelineLanesControl.xaml.cs|sample_time|ForDisplay",
"Darling/PerformanceMonitor.Darling.Viewer/CorrelatedTimelineLanesControl.xaml.cs|sample_time|ForDisplay",
"Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.Charts.cs|sample_time|ForDisplay",
];
private static readonly string[] DeclinedAmbiguousRenderSites = [];

/// <summary>Each <see cref="SiteLabel.DeSkewedAtRead"/> entry's conversion is present in the reader it
/// depends on. Without this the label is a way to delete a site from the census by asserting it is
Expand Down
51 changes: 18 additions & 33 deletions Darling/Darling.Tests/CorrelatedLanesXAxisWindowTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,67 +7,52 @@
*/

using System;
using PerformanceMonitor.Analysis.Baselines;
using PerformanceMonitor.Darling.Viewer;
using PerformanceMonitor.Ui;
using Xunit;

namespace Darling.Tests;

/// <summary>
/// #4766: the Overview lanes' X-axis window on a US Eastern server. Each lane sample is plotted at its own instant in
/// the display frame; the axis window has to be the same frame and the same length. A preset range is hoursBack REAL
/// hours: subtracting hoursBack from the display end as wall clock starts it an hour off when the 2026 spring change
/// (8 March, 07:00 UTC, 02:00 EST to 03:00 EDT) falls inside the window. The conversion is the pure
/// <c>ConvertToDisplay</c> core, so the process-wide display statics are not touched.
/// #4766: the Overview lanes' X-axis window. Each lane sample is plotted at its own UTC instant, so the axis window is
/// the UTC window as is, in whatever display zone the labels are drawn: a preset range is hoursBack REAL hours, which
/// wall-clock arithmetic in a zone with a clock change inside the window (the 2026 US spring change is 8 March, 07:00
/// UTC, 02:00 EST to 03:00 EDT) would start an hour off. The window is pure, so the process-wide display statics are
/// not touched.
/// </summary>
public sealed class CorrelatedLanesXAxisWindowTests
{
private static ServerClock Eastern() => ServerClock.Resolve("Eastern Standard Time", -300);

private static DateTime At(int y, int mo, int d, int h, int mi) => new(y, mo, d, h, mi, 0, DateTimeKind.Unspecified);

[Fact]
public void PresetRange_ServerTime_AcrossSpringForward_StartsHoursBackRealHoursBeforeNow()
{
/* 09:00 UTC on 8 March is 05:00 EDT; six real hours earlier is 03:00 UTC = 22:00 EST the evening before. */
var (start, end) = CorrelatedTimelineLanesControl.GetXAxisWindow(
6, null, null, At(2026, 3, 8, 9, 0), TimeDisplayMode.ServerTime, Eastern());

Assert.Equal(At(2026, 3, 8, 5, 0), end);
Assert.Equal(At(2026, 3, 7, 22, 0), start);
}

[Fact]
public void PresetRange_UtcDisplay_IsHoursBackOfUtc()
public void PresetRange_AcrossSpringForward_StartsHoursBackRealHoursBeforeNow()
{
var (start, end) = CorrelatedTimelineLanesControl.GetXAxisWindow(
6, null, null, At(2026, 3, 8, 9, 0), TimeDisplayMode.UTC, Eastern());
/* 09:00 UTC on 8 March, six real hours back is 03:00 UTC, the same six hours on an Eastern axis that
crosses the clock change. */
var (start, end) = CorrelatedTimelineLanesControl.GetXAxisWindow(6, null, null, At(2026, 3, 8, 9, 0));

Assert.Equal(At(2026, 3, 8, 9, 0), end);
Assert.Equal(At(2026, 3, 8, 3, 0), start);
Assert.Equal(TimeSpan.FromHours(6), end - start);
}

[Fact]
public void CustomRange_ConvertsEachBoundWithItsOwnOffset()
public void CustomRange_IsTheTypedUtcInstantsAsIs()
{
/* 12:00 UTC on 1 March is 07:00 EST; 12:00 UTC on 9 March is 08:00 EDT. */
/* The bounds are UTC instants, on either side of a clock change in any zone: no offset is applied. */
var (start, end) = CorrelatedTimelineLanesControl.GetXAxisWindow(
6, At(2026, 3, 1, 12, 0), At(2026, 3, 9, 12, 0), At(2026, 9, 29, 12, 0), TimeDisplayMode.ServerTime, Eastern());
6, At(2026, 3, 1, 12, 0), At(2026, 3, 9, 12, 0), At(2026, 9, 29, 12, 0));

Assert.Equal(At(2026, 3, 1, 7, 0), start);
Assert.Equal(At(2026, 3, 9, 8, 0), end);
Assert.Equal(At(2026, 3, 1, 12, 0), start);
Assert.Equal(At(2026, 3, 9, 12, 0), end);
}

[Fact]
public void OneBoundOnly_FallsBackToThePresetWindow()
{
var utcNow = At(2026, 3, 8, 9, 0);
var preset = CorrelatedTimelineLanesControl.GetXAxisWindow(6, null, null, utcNow, TimeDisplayMode.ServerTime, Eastern());
var preset = CorrelatedTimelineLanesControl.GetXAxisWindow(6, null, null, utcNow);

Assert.Equal(preset, CorrelatedTimelineLanesControl.GetXAxisWindow(
6, At(2026, 3, 1, 12, 0), null, utcNow, TimeDisplayMode.ServerTime, Eastern()));
Assert.Equal(preset, CorrelatedTimelineLanesControl.GetXAxisWindow(
6, null, At(2026, 3, 9, 12, 0), utcNow, TimeDisplayMode.ServerTime, Eastern()));
Assert.Equal(preset, CorrelatedTimelineLanesControl.GetXAxisWindow(6, At(2026, 3, 1, 12, 0), null, utcNow));
Assert.Equal(preset, CorrelatedTimelineLanesControl.GetXAxisWindow(6, null, At(2026, 3, 9, 12, 0), utcNow));
}
}
5 changes: 5 additions & 0 deletions Darling/Darling.Tests/TsqlConventionGuardTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1514,6 +1514,11 @@ Lite twin (LocalDataService.FinOps.IndexObjects.cs, below) derives the same four
"Lite/Services/LocalDataService.FinOps.IndexObjects.cs Growth30dMb",
"Lite/Services/LocalDataService.FinOps.IndexObjects.cs Growth7dMb",
"Lite/Services/LocalDataService.FinOps.IndexObjects.cs GrowthOverAvailableHistoryMb",
/* #4766: the history rows' expression-bodied formatter, `naiveUtc is not { } instant ? "" : ...`. The
property pattern's braces are where the walk stops. What it strands is the "yyyy-MM-dd HH:mm:ss" grid
format and nothing else: not T-SQL and not a tempdb label, so no census reads a site of that kind here. */
"Lite/Services/LocalDataService.QueryStats.cs Worded",
"Lite/Services/LocalDataService.QueryStore.cs Worded",
/* #3691 lane 27 (#3798): the plan-flip record's expression-bodied `Ratio` property (`MeanAfter / MeanBefore`,
null unless both sides exist) — the same expression-shaped member as the formatter lines above. What its
range strands is the pattern-matched quotient and nothing else: no T-SQL, no tempdb label, no SQL literal a
Expand Down
167 changes: 167 additions & 0 deletions Darling/Darling.Tests/ViewerAlertClockCacheTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/*
* 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;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using PerformanceMonitor.Analysis.Baselines;
using PerformanceMonitor.Darling.Viewer;
using Xunit;

namespace Darling.Tests;

/// <summary>
/// The alert-history read stamps each row with its server's clock, and the shell polls that read on every refresh
/// tick (30 seconds by default, 10 at the fastest). The clock read sorts every retained <c>server_properties</c>
/// row, a table that changes only when a server connects and once a day after that, so the read now goes through
/// <see cref="ServerClockCache"/> and asks the store again only once its answer is <see cref="ViewerDataService.AlertClockLifetime"/>
/// old (#4766). These pin the cache without a store: what it serves, when it asks again, and that a failed read is
/// never held.
/// </summary>
public sealed class ViewerAlertClockCacheTests
{
private static readonly DateTime Start = new(2026, 11, 1, 12, 0, 0, DateTimeKind.Utc);
private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(5);

/// <summary>A cache over a counting read and a clock the test moves.</summary>
private sealed class Rig
{
public int Reads;
public DateTime Now = Start;
public Func<Dictionary<int, ServerClock>> Answer = () => new() { [1] = ServerClock.FixedOffset(-300) };
public ServerClockCache Cache { get; }

public Rig()
{
Cache = new ServerClockCache(
_ =>
{
Reads++;
return Task.FromResult(Answer());
},
Lifetime,
() => Now);
}
}

[Fact]
public async Task ThePollsInsideTheLifetimeShareOneStoreRead()
{
var rig = new Rig();

var first = await rig.Cache.GetAsync(CancellationToken.None);
rig.Now = Start.AddSeconds(30);
var second = await rig.Cache.GetAsync(CancellationToken.None);
rig.Now = Start.AddMinutes(4).AddSeconds(59);
var third = await rig.Cache.GetAsync(CancellationToken.None);

Assert.Equal(1, rig.Reads);
Assert.Same(first, second);
Assert.Same(first, third);
Assert.Equal(-300, first[1].OffsetMinutesAt(Start));
}

[Fact]
public async Task TheStoreIsAskedAgainOnceTheLifetimeHasPassed()
{
var rig = new Rig();
await rig.Cache.GetAsync(CancellationToken.None);

rig.Answer = () => new() { [1] = ServerClock.FixedOffset(-240), [2] = ServerClock.FixedOffset(60) };
rig.Now = Start + Lifetime;
var later = await rig.Cache.GetAsync(CancellationToken.None);

Assert.Equal(2, rig.Reads);
Assert.Equal(2, later.Count);
Assert.Equal(-240, later[1].OffsetMinutesAt(rig.Now));
}

[Fact]
public async Task AFailedReadIsNotHeld_AndTheNextCallAsksTheStoreAgain()
{
var rig = new Rig();
rig.Answer = () => throw new InvalidOperationException("store unreachable");

await Assert.ThrowsAsync<InvalidOperationException>(() => rig.Cache.GetAsync(CancellationToken.None));

rig.Answer = () => new() { [1] = ServerClock.FixedOffset(-300) };
var recovered = await rig.Cache.GetAsync(CancellationToken.None);
var held = await rig.Cache.GetAsync(CancellationToken.None);

Assert.Equal(2, rig.Reads);
Assert.Same(recovered, held);
}

[Fact]
public async Task ASnapshotDatedAfterTheCurrentTimeIsStale()
{
/* The machine's clock moved back: serving the snapshot until real time caught up would hold it for hours. */
var rig = new Rig();
await rig.Cache.GetAsync(CancellationToken.None);

rig.Now = Start.AddHours(-1);
await rig.Cache.GetAsync(CancellationToken.None);

Assert.Equal(2, rig.Reads);
}

[Theory]
[InlineData(0, true)]
[InlineData(299, true)]
[InlineData(300, false)]
[InlineData(301, false)]
[InlineData(-1, false)]
public void IsFresh_IsYoungerThanTheLifetimeAndNotFromTheFuture(int ageSeconds, bool expected)
{
Assert.Equal(expected, ServerClockCache.IsFresh(Start, Start.AddSeconds(ageSeconds), Lifetime));
}

[Fact]
public void TheLifetimeOutlastsTheDefaultPollAndStaysShortEnoughForANewServer()
{
/* Shorter than the poll and the cache would never hit; long enough and a server that connected since the last
read shows the viewer machine's offset in Server mode for that long. */
var poll = TimeSpan.FromSeconds(new ViewerAppSettings().NocRefreshIntervalSeconds);

Assert.True(ViewerDataService.AlertClockLifetime > poll);
Assert.True(ViewerDataService.AlertClockLifetime <= TimeSpan.FromMinutes(15));
}

[Fact]
public void TheAlertHistoryReadTakesItsClocksFromTheCache_NotFromTheStoreEveryCall()
{
var alertHistory = ViewerCode("ViewerDataService.AlertHistory.cs");
var method = Regex.Match(
alertHistory,
@"public async Task<List<ViewerAlertRow>> GetAlertHistoryAsync\(.*?\n \}\n",
RegexOptions.Singleline);

Assert.True(method.Success);
Assert.Contains("_alertClocks.GetAsync(cancellationToken)", method.Value, StringComparison.Ordinal);
Assert.DoesNotContain("GetServerClocksAsync", method.Value, StringComparison.Ordinal);

/* The cache reads the whole fleet (every row's server, whichever filter the tab has), for AlertClockLifetime. */
var service = ViewerCode("ViewerDataService.cs");
Assert.Matches(
@"_alertClocks\s*=\s*new ServerClockCache\(\s*ct\s*=>\s*GetServerClocksAsync\(serverId:\s*null,\s*ct\),\s*AlertClockLifetime\)",
service);
}

private static string ViewerCode(string fileName, [CallerFilePath] string thisFile = "")
{
var viewer = Path.Combine(
Path.GetFullPath(Path.Combine(Path.GetDirectoryName(thisFile)!, "..", "..")),
"Darling", "PerformanceMonitor.Darling.Viewer", fileName);
return Regex.Replace(File.ReadAllText(viewer), @"/\*.*?\*/|//[^\r\n]*", "", RegexOptions.Singleline)
.Replace("\r\n", "\n", StringComparison.Ordinal);
}
}
Loading
Loading