diff --git a/Lite.Tests/OverviewBlockingLaneDataStartLiveTests.cs b/Lite.Tests/OverviewBlockingLaneDataStartLiveTests.cs
new file mode 100644
index 0000000000..ce7ec2ac3f
--- /dev/null
+++ b/Lite.Tests/OverviewBlockingLaneDataStartLiveTests.cs
@@ -0,0 +1,205 @@
+/*
+ * Copyright (c) 2026 Erik Darling, Darling Data LLC
+ *
+ * This file is part of the SQL Server Performance Monitor Lite.
+ *
+ * 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.Threading;
+using System.Threading.Tasks;
+using DuckDB.NET.Data;
+using PerformanceMonitorLite.Controls;
+using PerformanceMonitorLite.Database;
+using PerformanceMonitorLite.Services;
+using Xunit;
+
+namespace PerformanceMonitorLite.Tests;
+
+///
+/// The Overview blocking chart's "Showing since" note (#4966) against a real store, through the same step the control runs
+/// (): the notice, the quiet start, one series answering, a failed probe and a short window.
+/// Anchors are fixed so the tests never straddle a day boundary.
+///
+[Collection("server-time-helper")]
+public sealed class OverviewBlockingLaneDataStartLiveTests : IDisposable
+{
+ private const int ServerId = 4966;
+ private const string ServerName = "OverviewBlockingServer";
+ private static readonly DateTime End = new(2026, 9, 20, 12, 0, 0, DateTimeKind.Unspecified);
+ private readonly string _tempDir;
+ private readonly DuckDbInitializer _duckDb;
+ private long _nextId = 1;
+
+ public OverviewBlockingLaneDataStartLiveTests()
+ {
+ _tempDir = Path.Combine(Path.GetTempPath(), "OverviewBlockingLane_" + Guid.NewGuid().ToString("N")[..8]);
+ Directory.CreateDirectory(_tempDir);
+ _duckDb = new DuckDbInitializer(Path.Combine(_tempDir, "test.duckdb"));
+ }
+
+ public void Dispose()
+ {
+ try { if (Directory.Exists(_tempDir)) Directory.Delete(_tempDir, recursive: true); }
+ catch { /* best-effort cleanup */ }
+ }
+
+ private static string CollectorOf(QueryWindowRelation relation) =>
+ relation == QueryWindowRelation.BlockedProcessReports ? "blocked_process_report" : "deadlocks";
+
+ private async Task SeedLogRunsAsync(QueryWindowRelation relation, DateTime firstUtc, DateTime lastUtc)
+ {
+ using var connection = _duckDb.CreateConnection();
+ await connection.OpenAsync();
+ using var readLock = _duckDb.AcquireReadLock();
+ using var cmd = connection.CreateCommand();
+ cmd.CommandText = @"
+INSERT INTO collection_log (log_id, server_id, server_name, collector_name, collection_time, duration_ms, status, rows_collected)
+SELECT $1 + row_number() OVER (), $2, $3, $4, g.t, 12, 'SUCCESS', 0
+FROM generate_series($5::TIMESTAMP, $6::TIMESTAMP, INTERVAL 30 MINUTE) AS g(t)";
+ cmd.Parameters.Add(new DuckDBParameter { Value = _nextId });
+ cmd.Parameters.Add(new DuckDBParameter { Value = ServerId });
+ cmd.Parameters.Add(new DuckDBParameter { Value = ServerName });
+ cmd.Parameters.Add(new DuckDBParameter { Value = CollectorOf(relation) });
+ cmd.Parameters.Add(new DuckDBParameter { Value = firstUtc });
+ cmd.Parameters.Add(new DuckDBParameter { Value = lastUtc });
+ _nextId += await cmd.ExecuteNonQueryAsync() + 1;
+ }
+
+ private static string SinceText(DateTime instant) =>
+ "Showing since " + PerformanceMonitor.Ui.DisplayZone.Format(instant, TimeZoneInfo.Utc, "yyyy-MM-dd HH:mm:ss");
+
+ private static TrendPoint Bar(DateTime at, int count = 1) => new() { Time = at, Count = count };
+
+ private async Task<(bool Visible, string Text)> NoteAsync(
+ DateTime startUtc, DateTime endUtc, IEnumerable blockingBars, IEnumerable deadlockBars,
+ Func>? floorOf = null)
+ {
+ var service = new LocalDataService(_duckDb);
+ floorOf ??= relation => service.GetQueryWindowFloorAsync(relation, ServerId, startUtc, endUtc);
+ /* The real note step, end to end, on an STA thread: the banner is a WPF object. No sync context there, so the blocking wait is safe. */
+ return OnStaThread(() =>
+ {
+ var banner = new System.Windows.Controls.TextBlock();
+ LiteBlockingLaneDataStart.ShowAsync(banner, floorOf, startUtc, endUtc, blockingBars, deadlockBars, TimeZoneInfo.Utc).GetAwaiter().GetResult();
+ return (banner.Visibility == System.Windows.Visibility.Visible, banner.Text);
+ });
+ }
+
+ /// Blocking was collected from 2 days back, deadlocks from 3: the one note names the later start, in the chart's zone.
+ [Fact]
+ public async Task Notice_NamesTheLaterOfTheTwoSeriesStarts()
+ {
+ await _duckDb.InitializeAsync();
+ var blockingFrom = End.AddDays(-2);
+ await SeedLogRunsAsync(QueryWindowRelation.BlockedProcessReports, blockingFrom, End);
+ await SeedLogRunsAsync(QueryWindowRelation.Deadlocks, End.AddDays(-3), End);
+
+ var (visible, text) = await NoteAsync(End.AddDays(-7), End, [], []);
+
+ Assert.True(visible);
+ Assert.Equal(SinceText(blockingFrom), text);
+ }
+
+ /// Both collectors ran across the whole range and the first bars come late: no note, and a zero bucket changes nothing.
+ [Fact]
+ public async Task QuietStart_BothCollectorsCoveredTheRange_ShowsNoNote()
+ {
+ await _duckDb.InitializeAsync();
+ await SeedLogRunsAsync(QueryWindowRelation.BlockedProcessReports, End.AddDays(-9), End);
+ await SeedLogRunsAsync(QueryWindowRelation.Deadlocks, End.AddDays(-9), End);
+
+ var (visible, text) = await NoteAsync(
+ End.AddDays(-7), End, [Bar(End.AddDays(-7).AddHours(5)), Bar(End.AddHours(-1))], [Bar(End.AddDays(-7), 0), Bar(End.AddHours(-2))]);
+
+ Assert.False(visible);
+ Assert.Equal(string.Empty, text);
+ }
+
+ /// One collector covers the range and the other began 2 days ago: the later start is named, not the covered one.
+ [Fact]
+ public async Task OneSeriesCovered_TheOtherStartedLate_NamesTheLateOne()
+ {
+ await _duckDb.InitializeAsync();
+ var lateFrom = End.AddDays(-2);
+ await SeedLogRunsAsync(QueryWindowRelation.BlockedProcessReports, End.AddDays(-9), End);
+ await SeedLogRunsAsync(QueryWindowRelation.Deadlocks, lateFrom, End);
+
+ var (visible, text) = await NoteAsync(End.AddDays(-7), End, [], []);
+
+ Assert.True(visible);
+ Assert.Equal(SinceText(lateFrom), text);
+ }
+
+ /// A probe that throws costs only its own series' start; the other still names the note.
+ [Fact]
+ public async Task AFailedProbe_CostsOnlyItsSeries()
+ {
+ await _duckDb.InitializeAsync();
+ var from = End.AddDays(-2);
+ await SeedLogRunsAsync(QueryWindowRelation.Deadlocks, from, End);
+ var service = new LocalDataService(_duckDb);
+
+ var (visible, text) = await NoteAsync(End.AddDays(-7), End, [], [],
+ relation => relation == QueryWindowRelation.BlockedProcessReports
+ ? Task.FromException(new InvalidOperationException("probe failed"))
+ : service.GetQueryWindowFloorAsync(relation, ServerId, End.AddDays(-7), End));
+
+ Assert.True(visible);
+ Assert.Equal(SinceText(from), text);
+ }
+
+ /// A failed blocking probe with a late bar names no start; the covered deadlock series starts at the window start, so no note.
+ [Fact]
+ public async Task AFailedProbe_WithALateBar_AndACoveredOtherSeries_ShowsNoNote()
+ {
+ await _duckDb.InitializeAsync();
+ await SeedLogRunsAsync(QueryWindowRelation.Deadlocks, End.AddDays(-9), End);
+ var service = new LocalDataService(_duckDb);
+
+ var (visible, text) = await NoteAsync(End.AddDays(-7), End, [Bar(End.AddHours(-1))], [],
+ relation => relation == QueryWindowRelation.BlockedProcessReports
+ ? Task.FromException(new InvalidOperationException("probe failed"))
+ : service.GetQueryWindowFloorAsync(relation, ServerId, End.AddDays(-7), End));
+
+ Assert.False(visible);
+ Assert.Equal(string.Empty, text);
+ }
+
+ /// A window of 90 minutes or less starts no probe, even on a store that would earn a note.
+ [Fact]
+ public async Task AShortWindow_StartsNoProbe_AndShowsNoNote()
+ {
+ await _duckDb.InitializeAsync();
+ var probes = 0;
+
+ var (visible, _) = await NoteAsync(End.AddMinutes(-90), End, [], [], relation => { probes++; return Task.FromResult(End); });
+
+ Assert.False(visible);
+ Assert.Equal(0, probes);
+ }
+
+ private static T OnStaThread(Func body)
+ {
+ T result = default!;
+ Exception? error = null;
+ var thread = new Thread(() =>
+ {
+ try { result = body(); }
+ catch (Exception ex) { error = ex; }
+ });
+ thread.SetApartmentState(ApartmentState.STA);
+ thread.Start();
+ thread.Join();
+
+ if (error is not null)
+ {
+ throw error;
+ }
+
+ return result;
+ }
+}
diff --git a/Lite.Tests/OverviewBlockingLaneDataStartPinTests.cs b/Lite.Tests/OverviewBlockingLaneDataStartPinTests.cs
new file mode 100644
index 0000000000..52896feaff
--- /dev/null
+++ b/Lite.Tests/OverviewBlockingLaneDataStartPinTests.cs
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2026 Erik Darling, Darling Data LLC
+ *
+ * This file is part of the SQL Server Performance Monitor Lite.
+ *
+ * Licensed under the MIT License. See LICENSE file in the project root for full license information.
+ */
+
+using System;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Text.RegularExpressions;
+using Xunit;
+
+namespace PerformanceMonitorLite.Tests;
+
+///
+/// How the Overview's blocking chart is wired to say where its data starts (#4966), read from the source because the control needs a
+/// window to run. The choice is in OverviewBlockingLaneDataStartTests, the notice in OverviewBlockingLaneDataStartLiveTests.
+///
+public sealed class OverviewBlockingLaneDataStartPinTests
+{
+ private static string Code() => Read("CorrelatedTimelineLanesControl.xaml.cs");
+
+ private static string Xaml() => Read("CorrelatedTimelineLanesControl.xaml");
+
+ private static string Step() => Read("LiteBlockingLaneDataStart.cs");
+
+ private static string Between(string source, string from, string to)
+ {
+ var start = source.IndexOf(from, StringComparison.Ordinal);
+ Assert.True(start >= 0, from + " was not found");
+ var end = source.IndexOf(to, start, StringComparison.Ordinal);
+ Assert.True(end > start, to + " was not found after " + from);
+ return source[start..end];
+ }
+
+ [Fact]
+ public void RefreshAsync_RunsTheNoteAfterTheLanesAreDrawn_OutsideTheReadsWhenAll()
+ {
+ var refresh = Between(Code(), "public async Task RefreshAsync(", "private async Task ShowBlockingLaneDataStartAsync(");
+
+ var whenAll = Between(refresh, "await Task.WhenAll(", ");");
+ Assert.DoesNotContain("DataStart", whenAll, StringComparison.Ordinal);
+
+ var drawn = refresh.IndexOf("UpdateBlockingLane(blockingData", StringComparison.Ordinal);
+ var ghost = refresh.IndexOf("AddGhostLine(BlockingChart", StringComparison.Ordinal);
+ var axes = refresh.IndexOf("SyncXAxes(hoursBack", StringComparison.Ordinal);
+ var note = refresh.IndexOf("await ShowBlockingLaneDataStartAsync(", StringComparison.Ordinal);
+ Assert.True(drawn > 0 && ghost > drawn && axes > ghost && note > axes, "the lanes, ghost lines and axes come before the note");
+ Assert.Single(Regex.Matches(refresh, "ShowBlockingLaneDataStartAsync"));
+ }
+
+ [Fact]
+ public void TheWindow_IsTheOneTheReadsTake_AndTheNoteIsFedFromBothSeriesBars()
+ {
+ var refresh = Between(Code(), "public async Task RefreshAsync(", "private async Task ShowBlockingLaneDataStartAsync(");
+ Assert.Contains("var (windowStartUtc, windowEndUtc) = LocalDataService.GetTimeRange(hoursBack, fromDate, toDate, asOfUtc: null);", refresh, StringComparison.Ordinal);
+ Assert.Contains("GetBlockingTrendAsync(_serverId, hoursBack, fromDate, toDate)", refresh, StringComparison.Ordinal);
+ Assert.Contains("GetDeadlockTrendAsync(_serverId, hoursBack, fromDate, toDate)", refresh, StringComparison.Ordinal);
+
+ var call = Between(refresh, "await ShowBlockingLaneDataStartAsync(", ");");
+ Assert.Contains("windowStartUtc, windowEndUtc", call, StringComparison.Ordinal);
+ Assert.Contains("blockingTask.Result", call, StringComparison.Ordinal);
+ Assert.Contains("deadlockTask.Result", call, StringComparison.Ordinal);
+
+ var wrapper = Between(Code(), "private async Task ShowBlockingLaneDataStartAsync(", "private void UpdateBlockingLane(");
+ Assert.Contains("LiteBlockingLaneDataStart.ShowAsync(", wrapper, StringComparison.Ordinal);
+ Assert.Contains("BlockingLaneDataStartBanner", wrapper, StringComparison.Ordinal);
+ Assert.Contains("GetQueryWindowFloorAsync(relation, _serverId, startUtc, endUtc)", wrapper, StringComparison.Ordinal);
+ Assert.Contains("_displayZone()", wrapper, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void TheStep_ProbesBothRelations_ThroughTheShortWindowGuard_AndWordsTheBanner()
+ {
+ var step = Between(Step(), "internal static async Task ShowAsync(", "/// One series' start:");
+ Assert.DoesNotContain("ProbeWindowFloorOrNullAsync", step, StringComparison.Ordinal);
+ Assert.True(step.IndexOf("McpQueryTools.CanWindowBeTruncated(startUtc, endUtc)", StringComparison.Ordinal)
+ < step.IndexOf("Probe(floorOf, QueryWindowRelation.BlockedProcessReports)", StringComparison.Ordinal));
+ Assert.Contains("Probe(floorOf, QueryWindowRelation.Deadlocks)", step, StringComparison.Ordinal);
+ Assert.Contains("StartAsync(floorOf, startUtc, endUtc, blockingBars, deadlockBars)", step, StringComparison.Ordinal);
+ Assert.Contains("ChooseAsync(blockingProbe, deadlockProbe, blockingBars, deadlockBars)", step, StringComparison.Ordinal);
+ Assert.Contains("ServerTab.ApplyWindowFloorToBanner(banner, start, startUtc, zone)", step, StringComparison.Ordinal);
+ Assert.Contains("bar.Count > 0", Step(), StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void TheBanner_IsDeclaredInAnAutoRowAboveTheChart_AndOnlyOnTheBlockingLane()
+ {
+ var chart = Between(Xaml(), "", "
+
+
+
+
+
-
+
diff --git a/Lite/Controls/CorrelatedTimelineLanesControl.xaml.cs b/Lite/Controls/CorrelatedTimelineLanesControl.xaml.cs
index a65d08e2fd..2d1b797c67 100644
--- a/Lite/Controls/CorrelatedTimelineLanesControl.xaml.cs
+++ b/Lite/Controls/CorrelatedTimelineLanesControl.xaml.cs
@@ -196,6 +196,9 @@ public async Task RefreshAsync(int hoursBack, DateTime? fromDate, DateTime? toDa
{
_crosshairManager?.PrepareForRefresh();
+ /* #4966: the UTC window the blocking and deadlock reads below take (their own GetTimeRange call), for the data-start note. */
+ var (windowStartUtc, windowEndUtc) = LocalDataService.GetTimeRange(hoursBack, fromDate, toDate, asOfUtc: null);
+
var cpuTask = Task.Run(() => _dataService.GetCpuUtilizationAsync(_serverId, hoursBack, fromDate, toDate, frame: CpuTimeFrame.Utc));
var waitTask = Task.Run(() => _dataService.GetTotalWaitTrendAsync(_serverId, hoursBack, fromDate, toDate));
var blockingTask = Task.Run(() => _dataService.GetBlockingTrendAsync(_serverId, hoursBack, fromDate, toDate));
@@ -346,6 +349,14 @@ Each ghost row goes back onto the current axis by the same rule in reverse (Time
the render set when the chart refreshes. */
_crosshairManager?.ReattachVLines();
SyncXAxes(hoursBack, fromDate, toDate);
+
+ /* #4966: the blocking chart draws event counts, so an empty stretch reads as "nothing happened" and the chart says
+ where its data starts. The note comes last, after every lane and the ghost lines are drawn and synced, and on its
+ own: the probes are not part of the reads' WhenAll above, and a probe that fails costs the note, never the bars. */
+ await ShowBlockingLaneDataStartAsync(
+ windowStartUtc, windowEndUtc,
+ blockingTask.IsCompletedSuccessfully ? blockingTask.Result : [],
+ deadlockTask.IsCompletedSuccessfully ? deadlockTask.Result : []);
}
finally
{
@@ -357,6 +368,21 @@ the render set when the chart refreshes. */
}
}
+ ///
+ /// Raises or hides the blocking chart's "Showing since" note (#4966). The chart's two series, blocking and deadlocks, are
+ /// event counts: an empty stretch before a series starts reads as "nothing happened". A window of 90 minutes or less
+ /// starts no probe (). The text goes through the shared banner step in
+ /// the lanes' own clock (_displayZone, which is the tab's GetPickerZone), to the second.
+ ///
+ private async Task ShowBlockingLaneDataStartAsync(
+ DateTime startUtc, DateTime endUtc, IReadOnlyList blockingBars, IReadOnlyList deadlockBars)
+ {
+ await LiteBlockingLaneDataStart.ShowAsync(
+ BlockingLaneDataStartBanner,
+ relation => Task.Run(() => _dataService!.GetQueryWindowFloorAsync(relation, _serverId, startUtc, endUtc)),
+ startUtc, endUtc, blockingBars, deadlockBars, _displayZone());
+ }
+
private void UpdateBlockingLane(List<(double Time, double Value)> blockingData,
List<(double Time, double Value)> deadlockData, BaselineBucket? baseline = null)
{
diff --git a/Lite/Controls/LiteBlockingLaneDataStart.cs b/Lite/Controls/LiteBlockingLaneDataStart.cs
new file mode 100644
index 0000000000..e03974dc82
--- /dev/null
+++ b/Lite/Controls/LiteBlockingLaneDataStart.cs
@@ -0,0 +1,129 @@
+/*
+ * Copyright (c) 2026 Erik Darling, Darling Data LLC
+ *
+ * This file is part of the SQL Server Performance Monitor Lite.
+ *
+ * Licensed under the MIT License. See LICENSE file in the project root for full license information.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows.Controls;
+using PerformanceMonitorLite.Mcp;
+using PerformanceMonitorLite.Services;
+
+namespace PerformanceMonitorLite.Controls;
+
+///
+/// Where the Overview's blocking chart says its data starts (#4966). The chart draws two event-count series, blocking and
+/// deadlocks, so an empty stretch before a series starts reads as "nothing happened". Each series starts at the EARLIER of its
+/// coverage floor and its earliest bar drawn (a zero bucket is not a bar); the chart has one note, and it names the LATER of
+/// the two starts so it is true of both series. A series with no start (no floor, no bar, or a probe that failed with no bar)
+/// is left out; when neither has one the chart shows no note. The Darling viewer's twin is ViewerBlockingLaneDataStart.
+/// Two rules: a probe that FAILED gives its series no start, whatever its bars (a failure is not evidence of a late start, so
+/// the other series answers alone); a probe that ANSWERED null (no collector run in the window) with bars drawn names the first
+/// bar, as the viewer does (ViewerEventDataStart.Of). Lite's own Blocking tab differs: it keeps the null there
+/// (EarlierOfFloorAndRowShown).
+///
+internal static class LiteBlockingLaneDataStart
+{
+ ///
+ /// The instant the chart's note names, or null when it names nothing. A probe that throws costs its series' start and
+ /// nothing else: it is logged and the other series answers alone.
+ ///
+ internal static async Task ChooseAsync(
+ Task blockingProbe, Task deadlockProbe,
+ IEnumerable blockingBars, IEnumerable deadlockBars)
+ {
+ ArgumentNullException.ThrowIfNull(blockingProbe);
+ ArgumentNullException.ThrowIfNull(deadlockProbe);
+ ArgumentNullException.ThrowIfNull(blockingBars);
+ ArgumentNullException.ThrowIfNull(deadlockBars);
+
+ var blocking = await AnswerAsync(blockingProbe, "blocking");
+ var deadlock = await AnswerAsync(deadlockProbe, "deadlocks");
+
+ return Later(
+ blocking.Failed ? null : SeriesStart(blocking.Floor, blockingBars),
+ deadlock.Failed ? null : SeriesStart(deadlock.Floor, deadlockBars));
+ }
+
+ ///
+ /// The note step: chooses the start the note names, then words the banner in
+ /// (). The banner is touched on the caller's thread only, after the start is chosen.
+ ///
+ internal static async Task ShowAsync(
+ TextBlock banner, Func> floorOf, DateTime startUtc, DateTime endUtc,
+ IEnumerable blockingBars, IEnumerable deadlockBars, TimeZoneInfo zone)
+ {
+ var start = await StartAsync(floorOf, startUtc, endUtc, blockingBars, deadlockBars);
+ ServerTab.ApplyWindowFloorToBanner(banner, start, startUtc, zone);
+ }
+
+ ///
+ /// Probes BOTH relations over the window the chart's reads took (a window of 90 minutes or less starts no probe, and a probe
+ /// that throws costs only its series' start) and chooses the start. is
+ /// LocalDataService.GetQueryWindowFloorAsync for the relation. Touches no WPF object, so the live-store tests drive it.
+ ///
+ internal static async Task StartAsync(
+ Func> floorOf, DateTime startUtc, DateTime endUtc,
+ IEnumerable blockingBars, IEnumerable deadlockBars)
+ {
+ if (!McpQueryTools.CanWindowBeTruncated(startUtc, endUtc))
+ {
+ return null;
+ }
+
+ /* The shared floor-or-null wrapper is not used here: it turns a throw into null, and null here would read as "no collector run". */
+ var blockingProbe = Probe(floorOf, QueryWindowRelation.BlockedProcessReports);
+ var deadlockProbe = Probe(floorOf, QueryWindowRelation.Deadlocks);
+
+ return await ChooseAsync(blockingProbe, deadlockProbe, blockingBars, deadlockBars);
+ }
+
+ /// One series' start: the earlier of its floor and its earliest bar with a count above zero; whichever exists; null when neither.
+ internal static DateTime? SeriesStart(DateTime? floor, IEnumerable bars)
+ {
+ DateTime? firstBar = null;
+ foreach (var bar in bars)
+ {
+ if (bar.Count > 0 && (firstBar is null || bar.Time < firstBar.Value))
+ {
+ firstBar = bar.Time;
+ }
+ }
+
+ return floor is DateTime f && firstBar is DateTime b ? (b < f ? b : f) : floor ?? firstBar;
+ }
+
+ /// The later of two series' starts; the one that answered when only one did; null when neither did.
+ internal static DateTime? Later(DateTime? blocking, DateTime? deadlock) =>
+ blocking.HasValue && deadlock.HasValue ? (blocking.Value >= deadlock.Value ? blocking : deadlock) : blocking ?? deadlock;
+
+ private static Task Probe(Func> floorOf, QueryWindowRelation relation)
+ {
+ try
+ {
+ return floorOf(relation);
+ }
+ catch (Exception ex)
+ {
+ return Task.FromException(ex);
+ }
+ }
+
+ private static async Task<(bool Failed, DateTime? Floor)> AnswerAsync(Task probe, string series)
+ {
+ try
+ {
+ return (false, await probe);
+ }
+ catch (Exception ex)
+ {
+ AppLogger.Warn("CorrelatedLanes", $"Overview blocking chart ({series}): the data-start probe failed, so that series names no start: {ex.Message}");
+ return (true, null);
+ }
+ }
+}