From 96b967832abeb16fe9543c133be6cf93b518e94c Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Fri, 4 Sep 2026 22:30:38 -0400 Subject: [PATCH] Take the window offset as a required parameter instead of reading the tab static GetTimeRange's custom-range branch converted server-local bounds to UTC with ServerTimeHelper.UtcOffsetMinutes, process-wide state written only by the WPF tab paths, while the read around it filtered on a server_id of its own. The offset now arrives as a required parameter, as GetTimeRangeServerLocal's already does, so the branch reads no ambient state and every caller has to name whose offset it means. The alert-badge read is the one that was naming the wrong server. It runs on every tab's own timer outside ServerTab's IsVisible gate, so a background tab's badge was windowed in the selected tab's offset. GetAlertCountsAsync takes the offset of the server it counts for, and RefreshAlertCountsAsync derives its own window in the tab's own UtcOffsetMinutes rather than inheriting the one computed for the sub-tab reads. Both halves of the conversion have to name one server or they stop cancelling: GetCurrentWindow converts the pickers out of the display mode and the read converts back out to UTC. The pair cancels under the UTC and LocalTime display modes and only the read's half applies under ServerTime, the default. GetCurrentWindow therefore takes the offset to convert in, and is given the selected tab's for the sub-tab reads and the tab's own for the badge. --- Lite.Tests/AlertBadgeServerOffsetTests.cs | 380 ++++++++++++++++++ Lite/Controls/ServerTab.Charts.cs | 5 +- Lite/Controls/ServerTab.Refresh.cs | 39 +- .../Services/LocalDataService.AlertHistory.cs | 2 +- Lite/Services/LocalDataService.Blocking.cs | 37 +- .../LocalDataService.BlockingStats.cs | 4 +- .../LocalDataService.CollectionHealth.cs | 2 +- .../LocalDataService.ConfigChanges.cs | 6 +- .../Services/LocalDataService.CpuScheduler.cs | 4 +- Lite/Services/LocalDataService.FileIo.cs | 6 +- .../LocalDataService.LatchSpinlock.cs | 8 +- Lite/Services/LocalDataService.LongQueries.cs | 2 +- Lite/Services/LocalDataService.Memory.cs | 8 +- .../Services/LocalDataService.MemoryGrants.cs | 6 +- Lite/Services/LocalDataService.Perfmon.cs | 6 +- Lite/Services/LocalDataService.PlanCache.cs | 6 +- .../LocalDataService.PlanCorrection.cs | 2 +- .../Services/LocalDataService.QueryHeatmap.cs | 4 +- Lite/Services/LocalDataService.QueryStats.cs | 20 +- Lite/Services/LocalDataService.QueryStore.cs | 10 +- .../LocalDataService.QueryStoreRegressions.cs | 4 +- .../Services/LocalDataService.SessionStats.cs | 2 +- .../Services/LocalDataService.SystemEvents.cs | 18 +- Lite/Services/LocalDataService.TempDb.cs | 2 +- Lite/Services/LocalDataService.WaitStats.cs | 14 +- .../Services/LocalDataService.WaitingTasks.cs | 6 +- Lite/Services/LocalDataService.cs | 34 +- Lite/Services/ServerTimeHelper.cs | 27 +- 28 files changed, 562 insertions(+), 102 deletions(-) create mode 100644 Lite.Tests/AlertBadgeServerOffsetTests.cs diff --git a/Lite.Tests/AlertBadgeServerOffsetTests.cs b/Lite.Tests/AlertBadgeServerOffsetTests.cs new file mode 100644 index 000000000..c40e757e8 --- /dev/null +++ b/Lite.Tests/AlertBadgeServerOffsetTests.cs @@ -0,0 +1,380 @@ +/* + * 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.Threading.Tasks; +using DuckDB.NET.Data; +using PerformanceMonitor.Ui; +using PerformanceMonitorLite.Database; +using PerformanceMonitorLite.Services; +using Xunit; + +namespace PerformanceMonitorLite.Tests; + +/// +/// The alert-badge read windows in the offset of the server it NAMES, not in the offset of whichever +/// server tab the desktop happens to have selected. +/// +/// Why this read and not the others. Every other windowed read in ServerTab sits +/// behind the IsVisible gate, where the tab doing the reading IS the selected tab and the two +/// offsets are the same number. The badge is the exception: it runs on every tab's own 60-second timer +/// whether or not that tab is on screen, so the server it counts for and the server the desktop is +/// showing are routinely different ones. A count windowed against the wrong clock is silent — it +/// under- or over-reports and nothing errors — and the badge exists precisely to be trusted without +/// opening the tab. +/// +/// The offset is applied TWICE per window, and the two have to agree. +/// ServerTab.GetCurrentWindow converts the toolbar pickers out of the display mode into server +/// time, and converts back out to UTC. Under +/// TimeDisplayMode.UTC and TimeDisplayMode.LocalTime those two cancel; under +/// TimeDisplayMode.ServerTime — the default — the first is the identity and only the second +/// applies. So a change that moves one side's offset source and not the other breaks the two modes that +/// currently cancel while appearing to fix the default. Every mode is therefore exercised here, and each +/// runs the REAL pair: the production display conversion feeding the production read. +/// +/// Why the assertion is an invariance, not a pinned window. A test that pins one expected +/// timestamp passes against the unfixed code whenever the static happens to hold the right server's +/// offset — which is how this defect stayed hidden. The contract here is independence: the same reads run +/// under four values of the process-wide static, including one belonging to neither server, and every run +/// must return each server its own counts. Nothing here seeds against a value it read out of the static; +/// the offsets are the servers' own, supplied the way production supplies them. +/// +/* Writes ServerTimeHelper.UtcOffsetMinutes and CurrentDisplayMode, both process-wide mutable statics. + xUnit runs test classes in parallel, so this joins the collection the other offset-touching classes + use rather than racing them. */ +[Collection("server-time-helper")] +public sealed class AlertBadgeServerOffsetTests : IClassFixture, IDisposable +{ + /* UTC-8 and UTC+5:30 — 13.5 hours apart, so a 30-minute window built with the wrong server's offset + cannot accidentally still contain the right server's rows. One offset is a HALF hour, so an + implementation that rounds to whole hours is caught too. */ + private const int WestOffset = -480; + private const int EastOffset = 330; + + /* Synthetic ids and names. Told apart by COUNT SHAPE, not by row totals: a read that returns the + other server's rows cannot look right, and neither can one that returns both servers'. */ + private const int WestServerId = 60801; + private const int EastServerId = 60802; + private const string WestServerName = "WestZoneSrv"; + private const string EastServerName = "EastZoneSrv"; + + private const int WestBlocking = 2; + private const int WestDeadlocks = 1; + private const int EastBlocking = 1; + private const int EastDeadlocks = 3; + + /* The four values the desktop static is run under: each server's own, a UTC host with no tab ever + opened, and an offset belonging to neither server. */ + private static readonly int[] DesktopStatics = [WestOffset, EastOffset, 0, 720]; + + private readonly DuckDbInitializer _duckDb; + private long _nextId = 1; + private DuckDBConnection? _seedConn; + + public AlertBadgeServerOffsetTests(SharedDuckDbFixture fixture) + { + fixture.ResetData(); + _duckDb = fixture.DuckDb; + } + + public void Dispose() => _seedConn?.Dispose(); + + /// + /// The window the operator means, in UTC. Everything seeded is placed relative to this, and every + /// display mode's picker values are derived so that the production pair lands back on exactly it. + /// + private static readonly DateTime WindowStartUtc = ChooseBase(); + private static readonly DateTime WindowEndUtc = WindowStartUtc.AddMinutes(30); + + /// + /// A base instant ~2h old whose LOCAL rendering is neither ambiguous nor invalid, so the + /// LocalTime case's picker value converts back to the instant it came from. A DST boundary + /// would otherwise make that one mode flaky for the hour a year it lands in. + /// + private static DateTime ChooseBase() + { + var candidate = Truncate(DateTime.UtcNow.AddHours(-2)); + for (var i = 0; i < 8; i++) + { + var startLocal = ToLocalPicker(candidate); + var endLocal = ToLocalPicker(candidate.AddMinutes(30)); + if (!TimeZoneInfo.Local.IsAmbiguousTime(startLocal) && !TimeZoneInfo.Local.IsInvalidTime(startLocal) + && !TimeZoneInfo.Local.IsAmbiguousTime(endLocal) && !TimeZoneInfo.Local.IsInvalidTime(endLocal)) + return candidate; + candidate = candidate.AddHours(-6); + } + return candidate; + } + + /// + /// The value the toolbar pickers would hold for in a given display mode and a + /// given server's offset — the inverse of what the production pair does to it. Kind is Unspecified, + /// which is what GetDateTimeFromPickers yields. + /// + private static DateTime PickerValueFor(DateTime utc, TimeDisplayMode mode, int utcOffsetMinutes) => mode switch + { + /* The picker reads UTC, so the pair must be a no-op overall. */ + TimeDisplayMode.UTC => utc, + /* The picker reads the operator's own wall clock. */ + TimeDisplayMode.LocalTime => ToLocalPicker(utc), + /* The default: the picker reads the MONITORED server's wall clock, so it is offset by that + server's own offset and only the read's conversion undoes it. */ + _ => utc.AddMinutes(utcOffsetMinutes) + }; + + private static DateTime ToLocalPicker(DateTime utc) => + DateTime.SpecifyKind( + TimeZoneInfo.ConvertTimeFromUtc(DateTime.SpecifyKind(utc, DateTimeKind.Utc), TimeZoneInfo.Local), + DateTimeKind.Unspecified); + + /// + /// The contract. In every display mode, and whatever the desktop static holds, each server's badge + /// counts its own rows over the window the operator picked. + /// + [Theory] + [InlineData(TimeDisplayMode.ServerTime)] // the default; the read's conversion is the only one that applies + [InlineData(TimeDisplayMode.UTC)] // the two conversions cancel + [InlineData(TimeDisplayMode.LocalTime)] // the two conversions cancel + public async Task TheBadgeCountsInItsOwnServersOffset_InEveryDisplayMode_WhateverTheDesktopStaticHolds(TimeDisplayMode mode) + { + await SeedBothServersAsync(); + + var savedOffset = ServerTimeHelper.UtcOffsetMinutes; + var savedMode = ServerTimeHelper.CurrentDisplayMode; + try + { + ServerTimeHelper.CurrentDisplayMode = mode; + + foreach (var desktopStatic in DesktopStatics) + { + ServerTimeHelper.UtcOffsetMinutes = desktopStatic; + + var west = await ReadBadgeAsync(WestServerId, WestOffset, mode); + var east = await ReadBadgeAsync(EastServerId, EastOffset, mode); + + Assert.True( + west == (WestBlocking, WestDeadlocks), + $"{mode} / static {desktopStatic}: west badge was {west}, expected ({WestBlocking}, {WestDeadlocks})"); + Assert.True( + east == (EastBlocking, EastDeadlocks), + $"{mode} / static {desktopStatic}: east badge was {east}, expected ({EastBlocking}, {EastDeadlocks})"); + } + } + finally + { + ServerTimeHelper.UtcOffsetMinutes = savedOffset; + ServerTimeHelper.CurrentDisplayMode = savedMode; + } + } + + /// + /// The same reads over a preset window (no custom range) must also be independent of the static — the + /// branch that converts is not entered at all, so a server 13.5 hours away still gets its own rows. + /// Guards against a "fix" that moved the ambient read out of the custom-range branch into the + /// hoursBack one. + /// + [Fact] + public async Task APresetWindowIsAlsoIndependentOfTheDesktopStatic() + { + await SeedBothServersAsync(); + + var savedOffset = ServerTimeHelper.UtcOffsetMinutes; + try + { + /* The seeded rows are ~2h old, so a 24h preset contains them and the out-of-window decoys + (3h before the window) as well — the assertion is that the answer does not MOVE. */ + (int, int)? firstWest = null; + (int, int)? firstEast = null; + + foreach (var desktopStatic in DesktopStatics) + { + ServerTimeHelper.UtcOffsetMinutes = desktopStatic; + + var west = await ReadPresetBadgeAsync(WestServerId, WestOffset); + var east = await ReadPresetBadgeAsync(EastServerId, EastOffset); + + firstWest ??= west; + firstEast ??= east; + + Assert.True(west == firstWest, $"static {desktopStatic}: west preset badge moved to {west} from {firstWest}"); + Assert.True(east == firstEast, $"static {desktopStatic}: east preset badge moved to {east} from {firstEast}"); + } + + /* And the two servers are still told apart, so the invariance above is not vacuous. */ + Assert.NotEqual(firstWest, firstEast); + } + finally + { + ServerTimeHelper.UtcOffsetMinutes = savedOffset; + } + } + + /// + /// The pair, as arithmetic: converting the operator's picker value into a named server's local time + /// and then back out to UTC returns the instant the operator meant — for either server's offset, in + /// every display mode, with the desktop static set to something that is neither. This is the assertion + /// that fails if one side of the pair is moved onto a different server from the other. + /// + [Fact] + public void TheDisplayConversionAndTheReadConversionCancel_ForEitherServersOffset() + { + var savedOffset = ServerTimeHelper.UtcOffsetMinutes; + try + { + /* Deliberately neither server's offset: nothing below may consult it. */ + ServerTimeHelper.UtcOffsetMinutes = 720; + + foreach (var mode in new[] { TimeDisplayMode.ServerTime, TimeDisplayMode.UTC, TimeDisplayMode.LocalTime }) + foreach (var offset in new[] { WestOffset, EastOffset }) + { + var picker = PickerValueFor(WindowStartUtc, mode, offset); + var serverTime = ServerTimeHelper.DisplayTimeToServerTime(picker, mode, offset); + var backToUtc = serverTime.AddMinutes(-offset); + + Assert.True( + backToUtc == WindowStartUtc, + $"{mode} / offset {offset}: round trip landed on {backToUtc:O}, expected {WindowStartUtc:O}"); + } + } + finally + { + ServerTimeHelper.UtcOffsetMinutes = savedOffset; + } + } + + /// + /// The explicit-offset conversion given the static's own value is the static-reading one, so the reads + /// that legitimately take the selected tab's offset are unchanged by the overload existing. + /// + [Fact] + public void TheExplicitOffsetConversionMatchesTheAmbientOne_WhenHandedTheAmbientValue() + { + var savedOffset = ServerTimeHelper.UtcOffsetMinutes; + try + { + ServerTimeHelper.UtcOffsetMinutes = EastOffset; + var picker = ToLocalPicker(WindowStartUtc); + + foreach (var mode in new[] { TimeDisplayMode.ServerTime, TimeDisplayMode.UTC, TimeDisplayMode.LocalTime }) + { + Assert.Equal( + ServerTimeHelper.DisplayTimeToServerTime(picker, mode), + ServerTimeHelper.DisplayTimeToServerTime(picker, mode, ServerTimeHelper.UtcOffsetMinutes)); + } + } + finally + { + ServerTimeHelper.UtcOffsetMinutes = savedOffset; + } + } + + /// + /// Runs the production pair: the display conversion a ServerTab performs on its pickers with its + /// OWN offset, feeding the read with that same offset. + /// + private async Task<(int Blocking, int Deadlocks)> ReadBadgeAsync(int serverId, int utcOffsetMinutes, TimeDisplayMode mode) + { + var fromDate = ServerTimeHelper.DisplayTimeToServerTime(PickerValueFor(WindowStartUtc, mode, utcOffsetMinutes), mode, utcOffsetMinutes); + var toDate = ServerTimeHelper.DisplayTimeToServerTime(PickerValueFor(WindowEndUtc, mode, utcOffsetMinutes), mode, utcOffsetMinutes); + + var (blocking, deadlocks, _) = await new LocalDataService(_duckDb) + .GetAlertCountsAsync(serverId, hoursBack: 24, fromDate: fromDate, toDate: toDate, utcOffsetMinutes: utcOffsetMinutes); + return (blocking, deadlocks); + } + + private async Task<(int Blocking, int Deadlocks)> ReadPresetBadgeAsync(int serverId, int utcOffsetMinutes) + { + var (blocking, deadlocks, _) = await new LocalDataService(_duckDb) + .GetAlertCountsAsync(serverId, hoursBack: 24, fromDate: null, toDate: null, utcOffsetMinutes: utcOffsetMinutes); + return (blocking, deadlocks); + } + + private async Task SeedBothServersAsync() + { + if (_nextId > 1) return; + + /* In-window rows, spread so no two share a timestamp. Out-of-window decoys sit 3h before the + window start — inside a 24h preset, outside the custom range — so a window shifted by either + server's offset lands on nothing rather than on a plausible-looking count. */ + var inWindow = WindowStartUtc.AddMinutes(5); + var decoy = WindowStartUtc.AddHours(-3); + + for (var i = 0; i < WestBlocking; i++) + await SeedBlockedProcessReportAsync(WestServerId, WestServerName, inWindow.AddMinutes(i)); + for (var i = 0; i < WestDeadlocks; i++) + await SeedDeadlockAsync(WestServerId, WestServerName, inWindow.AddMinutes(i)); + await SeedBlockedProcessReportAsync(WestServerId, WestServerName, decoy); + await SeedDeadlockAsync(WestServerId, WestServerName, decoy); + + for (var i = 0; i < EastBlocking; i++) + await SeedBlockedProcessReportAsync(EastServerId, EastServerName, inWindow.AddMinutes(i)); + for (var i = 0; i < EastDeadlocks; i++) + await SeedDeadlockAsync(EastServerId, EastServerName, inWindow.AddMinutes(i)); + await SeedBlockedProcessReportAsync(EastServerId, EastServerName, decoy); + await SeedDeadlockAsync(EastServerId, EastServerName, decoy); + } + + private static DateTime Truncate(DateTime t) => + new(t.Year, t.Month, t.Day, t.Hour, t.Minute, t.Second, DateTimeKind.Unspecified); + + private async Task SeedConnectionAsync() + { + if (_seedConn is null) + { + _seedConn = _duckDb.CreateConnection(); + await _seedConn.OpenAsync(); + } + return _seedConn; + } + + private async Task SeedBlockedProcessReportAsync(int serverId, string serverName, DateTime collectionTimeUtc) + { + using var readLock = _duckDb.AcquireReadLock(); + var connection = await SeedConnectionAsync(); + using var cmd = connection.CreateCommand(); + cmd.CommandText = @" +INSERT INTO blocked_process_reports + (blocked_report_id, collection_time, server_id, server_name, event_time, database_name, + blocked_spid, blocked_last_tran_started, blocking_spid, blocking_last_tran_started, + wait_time_ms, lock_mode, blocked_status, blocking_status) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)"; + cmd.Parameters.Add(new DuckDBParameter { Value = _nextId++ }); + cmd.Parameters.Add(new DuckDBParameter { Value = collectionTimeUtc }); + cmd.Parameters.Add(new DuckDBParameter { Value = serverId }); + cmd.Parameters.Add(new DuckDBParameter { Value = serverName }); + cmd.Parameters.Add(new DuckDBParameter { Value = collectionTimeUtc }); + cmd.Parameters.Add(new DuckDBParameter { Value = "SynthDb" }); + cmd.Parameters.Add(new DuckDBParameter { Value = 55 }); + cmd.Parameters.Add(new DuckDBParameter { Value = collectionTimeUtc }); + cmd.Parameters.Add(new DuckDBParameter { Value = 66 }); + cmd.Parameters.Add(new DuckDBParameter { Value = collectionTimeUtc }); + cmd.Parameters.Add(new DuckDBParameter { Value = 1000L }); + cmd.Parameters.Add(new DuckDBParameter { Value = "X" }); + cmd.Parameters.Add(new DuckDBParameter { Value = "suspended" }); + cmd.Parameters.Add(new DuckDBParameter { Value = "running" }); + await cmd.ExecuteNonQueryAsync(); + } + + private async Task SeedDeadlockAsync(int serverId, string serverName, DateTime collectionTimeUtc) + { + using var readLock = _duckDb.AcquireReadLock(); + var connection = await SeedConnectionAsync(); + using var cmd = connection.CreateCommand(); + cmd.CommandText = @" +INSERT INTO deadlocks + (deadlock_id, collection_time, server_id, server_name, deadlock_time, deadlock_graph_xml) +VALUES ($1, $2, $3, $4, $5, $6)"; + cmd.Parameters.Add(new DuckDBParameter { Value = _nextId++ }); + cmd.Parameters.Add(new DuckDBParameter { Value = collectionTimeUtc }); + cmd.Parameters.Add(new DuckDBParameter { Value = serverId }); + cmd.Parameters.Add(new DuckDBParameter { Value = serverName }); + cmd.Parameters.Add(new DuckDBParameter { Value = collectionTimeUtc }); + cmd.Parameters.Add(new DuckDBParameter { Value = "" }); + await cmd.ExecuteNonQueryAsync(); + } +} diff --git a/Lite/Controls/ServerTab.Charts.cs b/Lite/Controls/ServerTab.Charts.cs index 167967e07..e69e4120d 100644 --- a/Lite/Controls/ServerTab.Charts.cs +++ b/Lite/Controls/ServerTab.Charts.cs @@ -1484,7 +1484,10 @@ private void RevertChartAxes(ScottPlot.WPF.WpfPlot chart) return; } - var (hoursBack, fromDate, toDate) = GetCurrentWindow(); + /* Re-pins the axes onto the window the plotted data was read over, so it takes the same offset + those reads take — the selected tab's. This runs from a chart on the visible tab, where that + is this tab. */ + var (hoursBack, fromDate, toDate) = GetCurrentWindow(ServerTimeHelper.UtcOffsetMinutes); DateTime rangeEnd = toDate ?? DateTime.UtcNow.AddMinutes(UtcOffsetMinutes); DateTime rangeStart = fromDate ?? rangeEnd.AddHours(-hoursBack); chart.Plot.Axes.SetLimitsX(rangeStart.ToOADate(), rangeEnd.ToOADate()); diff --git a/Lite/Controls/ServerTab.Refresh.cs b/Lite/Controls/ServerTab.Refresh.cs index 039e7ea99..633527d3f 100644 --- a/Lite/Controls/ServerTab.Refresh.cs +++ b/Lite/Controls/ServerTab.Refresh.cs @@ -57,7 +57,15 @@ the global database filter is applied client-side on the parsed per-process rows /// window. A preset leaves from/to null (charts fall back to now − hoursBack); a valid custom range /// converts the local picker dates/times to server time. /// - private (int hoursBack, DateTime? fromDate, DateTime? toDate) GetCurrentWindow() + /// + /// Whose server time the returned bounds are in. Every read given this window converts the bounds back + /// out to UTC using an offset of its own, and the two have to be the same server's or the pair stops + /// cancelling — so this argument is chosen to match the read being fed, not chosen once for the tab. + /// ServerTimeHelper.UtcOffsetMinutes for the sub-tab reads, which take their offset from the + /// selected tab; this tab's own UtcOffsetMinutes for the badge read, which takes its offset from + /// the server it names. + /// + private (int hoursBack, DateTime? fromDate, DateTime? toDate) GetCurrentWindow(int utcOffsetMinutes) { var hoursBack = GetHoursBack(); @@ -69,8 +77,8 @@ the global database filter is applied client-side on the parsed per-process rows var toLocal = GetDateTimeFromPickers(ToDatePicker!, ToHourCombo, ToMinuteCombo); if (fromLocal.HasValue && toLocal.HasValue) { - fromDate = ServerTimeHelper.DisplayTimeToServerTime(fromLocal.Value, ServerTimeHelper.CurrentDisplayMode); - toDate = ServerTimeHelper.DisplayTimeToServerTime(toLocal.Value, ServerTimeHelper.CurrentDisplayMode); + fromDate = ServerTimeHelper.DisplayTimeToServerTime(fromLocal.Value, ServerTimeHelper.CurrentDisplayMode, utcOffsetMinutes); + toDate = ServerTimeHelper.DisplayTimeToServerTime(toLocal.Value, ServerTimeHelper.CurrentDisplayMode, utcOffsetMinutes); } } @@ -82,7 +90,9 @@ private async System.Threading.Tasks.Task RefreshAllDataAsync() if (_isRefreshing) return; _isRefreshing = true; - var (hoursBack, fromDate, toDate) = GetCurrentWindow(); + /* The selected tab's offset, because the sub-tab reads below convert back out to UTC with that + same offset and the two applications have to name one server to cancel. */ + var (hoursBack, fromDate, toDate) = GetCurrentWindow(ServerTimeHelper.UtcOffsetMinutes); try { @@ -99,9 +109,11 @@ skip the heavy sub-tab data refresh and just keep the alert badge current. Mark { _refreshPendingWhileHidden = true; } - /* Always keep alert badge current even when Blocking tab is not visible */ + /* Always keep alert badge current even when Blocking tab is not visible. Deliberately not + given the window above: that one is in the SELECTED tab's server time, and this runs on + every tab's timer regardless of which is selected. It derives its own. */ if (MainTabControl.SelectedIndex != 8) - await RefreshAlertCountsAsync(hoursBack, fromDate, toDate); + await RefreshAlertCountsAsync(); /* #1591: same reasoning as the alert badge above — a permission-denied collector is only visible on the Collection Health tab, which is precisely why it went unnoticed. Badge it from every tab. */ @@ -152,12 +164,21 @@ private async System.Threading.Tasks.Task RefreshVisibleTabAsync(int hoursBack, /// /// Lightweight alert-only refresh — fetches blocking + deadlock counts and fires AlertCountsChanged. /// Runs on every timer tick when the Blocking tab is NOT visible so the tab badge stays current. + /// + /// Derives its own window instead of taking the caller's, and derives it in THIS tab's + /// UtcOffsetMinutes rather than in the selected tab's. Every other windowed read here sits + /// behind the IsVisible gate, where this tab is the selected tab and the two offsets are the + /// same value; the badge is the one that runs for a background tab, whose server can be in a + /// different zone from the one on screen. The same offset goes into the picker conversion and into + /// , which is what keeps the two applications + /// cancelling in the UTC and Local display modes while leaving the ServerTime default correct. /// - private async System.Threading.Tasks.Task RefreshAlertCountsAsync(int hoursBack, DateTime? fromDate, DateTime? toDate) + private async System.Threading.Tasks.Task RefreshAlertCountsAsync() { try { - var (blockingCount, deadlockCount, latestEventTime) = await Task.Run(() => _dataService.GetAlertCountsAsync(_serverId, hoursBack, fromDate, toDate)); + var (hoursBack, fromDate, toDate) = GetCurrentWindow(UtcOffsetMinutes); + var (blockingCount, deadlockCount, latestEventTime) = await Task.Run(() => _dataService.GetAlertCountsAsync(_serverId, hoursBack, fromDate, toDate, UtcOffsetMinutes)); AlertCountsChanged?.Invoke(blockingCount, deadlockCount, latestEventTime); } catch (Exception ex) @@ -521,7 +542,7 @@ private async System.Threading.Tasks.Task RefreshBlockingAsync(int hoursBack, Da break; } /* Always keep alert badge current when Blocking tab is visible */ - await RefreshAlertCountsAsync(hoursBack, fromDate, toDate); + await RefreshAlertCountsAsync(); return; } diff --git a/Lite/Services/LocalDataService.AlertHistory.cs b/Lite/Services/LocalDataService.AlertHistory.cs index a325e3369..884a97cd8 100644 --- a/Lite/Services/LocalDataService.AlertHistory.cs +++ b/Lite/Services/LocalDataService.AlertHistory.cs @@ -28,7 +28,7 @@ public async Task> GetAlertHistoryAsync(int hoursBack = 24 /* Both edges, not just the lower one: the row cap is applied by the database, so trimming after the read would spend the whole LIMIT on rows newer than an as_of anchor and hand back an empty window that looks exactly like a quiet one. */ - var (cutoff, until) = GetTimeRange(hoursBack, null, null, asOfUtc); + var (cutoff, until) = GetTimeRange(hoursBack, null, null, asOfUtc, utcOffsetMinutes: 0); if (serverId.HasValue) { diff --git a/Lite/Services/LocalDataService.Blocking.cs b/Lite/Services/LocalDataService.Blocking.cs index d4ddfe691..fde5d1dc6 100644 --- a/Lite/Services/LocalDataService.Blocking.cs +++ b/Lite/Services/LocalDataService.Blocking.cs @@ -78,7 +78,7 @@ public async Task> GetRecentDeadlocksAsync(int serverId, int h using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" SELECT @@ -125,7 +125,7 @@ public async Task> GetActiveQuerySlicerDataAsync( using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc: null, SelectedServerTabUtcOffsetMinutes); var dbClause = BuildDbInClause(databaseNames, "database_name", 4, out var dbValues); command.CommandText = @" @@ -179,7 +179,7 @@ public async Task> GetLatestQuerySnapshotsAsync(int serve using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); var dbClause = BuildDbInClause(databaseNames, "database_name", 4, out var dbValues); command.CommandText = @" @@ -267,12 +267,25 @@ AND query_text NOT LIKE 'WAITFOR%' /// Gets lightweight blocking + deadlock counts and latest event time for alert badge updates. /// Much cheaper than fetching full rows with XML — just COUNT(*) and MAX(time). /// - public async Task<(int blockingCount, int deadlockCount, DateTime? latestEventTime)> GetAlertCountsAsync(int serverId, int hoursBack = 24, DateTime? fromDate = null, DateTime? toDate = null) + /// + /// The UTC offset of itself, not of whichever server tab the desktop has + /// selected. This read is the one on the badge path, which runs on every tab's own timer whether or + /// not that tab is visible, so the server it names and the server the desktop is showing are + /// routinely different ones. + /// + /// Required, and required to be the SAME offset the caller used to convert + /// / out of the display mode. Those two + /// conversions cancel in TimeDisplayMode.UTC and LocalTime and only the one here + /// applies in ServerTime; sourcing them from different servers leaves a residue in every + /// mode. ServerTab.RefreshAlertCountsAsync derives both from the tab's own + /// UtcOffsetMinutes. + /// + public async Task<(int blockingCount, int deadlockCount, DateTime? latestEventTime)> GetAlertCountsAsync(int serverId, int hoursBack, DateTime? fromDate, DateTime? toDate, int utcOffsetMinutes) { using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc: null, utcOffsetMinutes); /* blocking_count prefers the blocked-process-report; falls back to the always-on DMV snapshot when BPR captured nothing (AWS RDS). latest_event_time includes DMV blocking recency too. */ @@ -318,7 +331,7 @@ public async Task> GetRecentBlockedProcessReportsA using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); var dbClause = BuildDbInClause(databaseNames, "database_name", 4, out var dbValues); command.CommandText = @" @@ -557,7 +570,7 @@ public async Task> GetBlockingSlicerDataAsync( { using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc: null, SelectedServerTabUtcOffsetMinutes); var dbClause = BuildDbInClause(databaseNames, "database_name", 4, out var dbValues); /* BPR buckets, falling back to the always-on DMV snapshot only when BPR has no buckets in the @@ -627,7 +640,7 @@ public async Task> GetDeadlockSlicerDataAsync( { using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc: null, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" SELECT @@ -670,7 +683,7 @@ public async Task> GetBlockingTrendAsync(int serverId, int hour using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); var dbClause = BuildDbInClause(databaseNames, "database_name", 4, out var dbValues); /* Use blocked_process_reports from XE session - more reliable than point-in-time snapshots @@ -722,7 +735,7 @@ public async Task> GetDeadlockTrendAsync(int serverId, int hour using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" SELECT @@ -823,7 +836,7 @@ private async Task> GetCaptureCountsAsync( using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" SELECT @@ -891,7 +904,7 @@ public async Task> GetLockWaitTrendAsync(int serverId, using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" WITH raw AS diff --git a/Lite/Services/LocalDataService.BlockingStats.cs b/Lite/Services/LocalDataService.BlockingStats.cs index 96c566896..6f840f093 100644 --- a/Lite/Services/LocalDataService.BlockingStats.cs +++ b/Lite/Services/LocalDataService.BlockingStats.cs @@ -78,7 +78,7 @@ public async Task> GetBlockingDurationStatsAsyn using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); var dbClause = BuildDbInClause(databaseNames, "database_name", 4, out var dbValues); /* BPR per-minute severity buckets, falling back to the always-on DMV snapshot only when BPR has none @@ -152,7 +152,7 @@ public async Task> GetDeadlockSeverityStatsAsyn using (var connection = await OpenConnectionAsync()) using (var command = connection.CreateCommand()) { - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" SELECT diff --git a/Lite/Services/LocalDataService.CollectionHealth.cs b/Lite/Services/LocalDataService.CollectionHealth.cs index 932bb473b..3bd819091 100644 --- a/Lite/Services/LocalDataService.CollectionHealth.cs +++ b/Lite/Services/LocalDataService.CollectionHealth.cs @@ -302,7 +302,7 @@ public async Task> GetRecentCollectionLogAsync(int server using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" SELECT diff --git a/Lite/Services/LocalDataService.ConfigChanges.cs b/Lite/Services/LocalDataService.ConfigChanges.cs index 13c39a169..177c04adc 100644 --- a/Lite/Services/LocalDataService.ConfigChanges.cs +++ b/Lite/Services/LocalDataService.ConfigChanges.cs @@ -107,7 +107,7 @@ public async Task> GetServerConfigChangesAsync( int serverId, int hoursBack = 24, DateTime? fromDate = null, DateTime? toDate = null, DateTime? asOfUtc = null) { using var _q = TimeQuery("GetServerConfigChangesAsync", "v_server_config snapshot diff"); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); var snapshots = await ReadServerConfigSnapshotsAsync(serverId, endTime); return ConfigChangeDiff.DiffServerConfigChanges(snapshots, startTime, endTime) .Select(c => new ServerConfigChangeRow(c)) @@ -121,7 +121,7 @@ public async Task> GetDatabaseConfigChangesAsync( int serverId, int hoursBack = 24, DateTime? fromDate = null, DateTime? toDate = null, IReadOnlyList? databaseNames = null, DateTime? asOfUtc = null) { using var _q = TimeQuery("GetDatabaseConfigChangesAsync", "v_database_config snapshot diff"); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); var snapshots = await ReadDatabaseConfigSnapshotsAsync(serverId, endTime, databaseNames); return ConfigChangeDiff.DiffDatabaseConfigChanges(snapshots, startTime, endTime) .Select(c => new DatabaseConfigChangeRow(c)) @@ -135,7 +135,7 @@ public async Task> GetTraceFlagChangesAsync( int serverId, int hoursBack = 24, DateTime? fromDate = null, DateTime? toDate = null, DateTime? asOfUtc = null) { using var _q = TimeQuery("GetTraceFlagChangesAsync", "v_trace_flags snapshot diff"); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); var snapshots = await ReadTraceFlagSnapshotsAsync(serverId, endTime); return ConfigChangeDiff.DiffTraceFlagChanges(snapshots, startTime, endTime) .Select(c => new TraceFlagChangeRow(c)) diff --git a/Lite/Services/LocalDataService.CpuScheduler.cs b/Lite/Services/LocalDataService.CpuScheduler.cs index 5dfe31e1b..31338d92a 100644 --- a/Lite/Services/LocalDataService.CpuScheduler.cs +++ b/Lite/Services/LocalDataService.CpuScheduler.cs @@ -33,7 +33,7 @@ public async Task> GetCpuSchedulerTrendAsync(int se using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc: null, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" SELECT @@ -78,7 +78,7 @@ FROM v_cpu_scheduler_stats using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" SELECT diff --git a/Lite/Services/LocalDataService.FileIo.cs b/Lite/Services/LocalDataService.FileIo.cs index 0d788e451..f4d936a1e 100644 --- a/Lite/Services/LocalDataService.FileIo.cs +++ b/Lite/Services/LocalDataService.FileIo.cs @@ -73,7 +73,7 @@ public async Task> GetFileIoLatencyTrendAsync(int serverI using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" WITH top_files AS ( @@ -165,7 +165,7 @@ public async Task> GetFileIoThroughputTrendAsync(int using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc: null, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" WITH top_files AS ( @@ -236,7 +236,7 @@ public async Task> GetTempDbFileIoTrendAsync(int serverId using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc: null, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" SELECT diff --git a/Lite/Services/LocalDataService.LatchSpinlock.cs b/Lite/Services/LocalDataService.LatchSpinlock.cs index 58ea630ce..16bb45f88 100644 --- a/Lite/Services/LocalDataService.LatchSpinlock.cs +++ b/Lite/Services/LocalDataService.LatchSpinlock.cs @@ -32,7 +32,7 @@ public async Task> GetLatchStatsTrendAsync(int server using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc: null, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" WITH top_latches AS @@ -95,7 +95,7 @@ public async Task> GetLatchStatsSnapshotAsync(int se using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" WITH latest AS @@ -152,7 +152,7 @@ public async Task> GetSpinlockStatsTrendAsync(int using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc: null, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" WITH top_spinlocks AS @@ -215,7 +215,7 @@ public async Task> GetSpinlockStatsSnapshotAsync( using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" WITH latest AS diff --git a/Lite/Services/LocalDataService.LongQueries.cs b/Lite/Services/LocalDataService.LongQueries.cs index 94ca1e4a9..4e595b52c 100644 --- a/Lite/Services/LocalDataService.LongQueries.cs +++ b/Lite/Services/LocalDataService.LongQueries.cs @@ -26,7 +26,7 @@ public async Task> GetRecentLongQueryCompletionsAsy using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); var dbClause = BuildDbInClause(databaseNames, "database_name", 4, out var dbValues); command.CommandText = @" diff --git a/Lite/Services/LocalDataService.Memory.cs b/Lite/Services/LocalDataService.Memory.cs index 07fdb1c08..b20508a07 100644 --- a/Lite/Services/LocalDataService.Memory.cs +++ b/Lite/Services/LocalDataService.Memory.cs @@ -72,7 +72,7 @@ public async Task> GetMemoryTrendAsync(int serverId, int using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" SELECT @@ -141,7 +141,7 @@ public async Task> GetDistinctMemoryClerkTypesAsync(int serverId, i using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc: null, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" SELECT @@ -179,7 +179,7 @@ public async Task>> GetMemoryCler using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc: null, SelectedServerTabUtcOffsetMinutes); var typeParams = string.Join(", ", clerkTypes.Select((_, i) => "$" + (i + 4))); command.CommandText = $@" @@ -227,7 +227,7 @@ public async Task> GetMemoryPressureEventsAsync(int using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" SELECT diff --git a/Lite/Services/LocalDataService.MemoryGrants.cs b/Lite/Services/LocalDataService.MemoryGrants.cs index 0ac0d17e6..03b1a02e8 100644 --- a/Lite/Services/LocalDataService.MemoryGrants.cs +++ b/Lite/Services/LocalDataService.MemoryGrants.cs @@ -23,7 +23,7 @@ public async Task> GetMemoryGrantTrendAsync(int serverId, using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc: null, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" SELECT @@ -65,7 +65,7 @@ public async Task> GetMemoryGrantChartDataAsync(int using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" SELECT @@ -122,7 +122,7 @@ public async Task> GetResourceSemaphoreSnapshotAsync( using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" WITH latest AS diff --git a/Lite/Services/LocalDataService.Perfmon.cs b/Lite/Services/LocalDataService.Perfmon.cs index 243b5ae43..ff5700fdd 100644 --- a/Lite/Services/LocalDataService.Perfmon.cs +++ b/Lite/Services/LocalDataService.Perfmon.cs @@ -59,7 +59,7 @@ public async Task> GetDistinctPerfmonCountersAsync(int serverId, in using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" SELECT DISTINCT counter_name @@ -90,7 +90,7 @@ public async Task> GetPerfmonTrendAsync(int serverId, st using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" SELECT @@ -140,7 +140,7 @@ public async Task>> GetPerfmonTrendsB using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc: null, SelectedServerTabUtcOffsetMinutes); var nameParams = string.Join(", ", counterNames.Select((_, i) => "$" + (i + 4))); command.CommandText = $@" diff --git a/Lite/Services/LocalDataService.PlanCache.cs b/Lite/Services/LocalDataService.PlanCache.cs index a23a2e32d..977bab943 100644 --- a/Lite/Services/LocalDataService.PlanCache.cs +++ b/Lite/Services/LocalDataService.PlanCache.cs @@ -33,7 +33,7 @@ public async Task> GetPlanCacheTrendAsync(int serverId using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc: null, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" SELECT @@ -76,7 +76,7 @@ public async Task> GetPlanCacheSnapshotAsync(int serv using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" WITH latest AS @@ -144,7 +144,7 @@ public async Task GetPlanCacheSummaryAsync(int serverId, int h using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" WITH latest AS diff --git a/Lite/Services/LocalDataService.PlanCorrection.cs b/Lite/Services/LocalDataService.PlanCorrection.cs index 0c7512597..e8e224b6f 100644 --- a/Lite/Services/LocalDataService.PlanCorrection.cs +++ b/Lite/Services/LocalDataService.PlanCorrection.cs @@ -28,7 +28,7 @@ public async Task> GetPlanCorrectionsAsync(int serverId, using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); var dbClause = BuildDbInClause(databaseNames, "database_name", 4, out var dbValues); command.CommandText = @" diff --git a/Lite/Services/LocalDataService.QueryHeatmap.cs b/Lite/Services/LocalDataService.QueryHeatmap.cs index a32437ce5..6806734c3 100644 --- a/Lite/Services/LocalDataService.QueryHeatmap.cs +++ b/Lite/Services/LocalDataService.QueryHeatmap.cs @@ -66,7 +66,7 @@ public async Task> GetQueryHeatmapCellsAsync( using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, null, null, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, null, null, asOfUtc, utcOffsetMinutes: 0); var metricExpr = GetMetricColumn(metric); var dbClause = BuildDbInClause(databaseNames, "database_name", 4, out var dbValues); var bucketIndex = 4 + dbValues.Count; @@ -155,7 +155,7 @@ FROM binned using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, null, null, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, null, null, asOfUtc, utcOffsetMinutes: 0); command.CommandText = @" SELECT diff --git a/Lite/Services/LocalDataService.QueryStats.cs b/Lite/Services/LocalDataService.QueryStats.cs index b28256dfd..964a7337f 100644 --- a/Lite/Services/LocalDataService.QueryStats.cs +++ b/Lite/Services/LocalDataService.QueryStats.cs @@ -44,7 +44,7 @@ public async Task> GetQueryStatsSlicerDataAsync( { using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc: null, SelectedServerTabUtcOffsetMinutes); var dbClause = BuildDbInClause(databaseNames, "database_name", 4, out var dbValues); command.CommandText = @" @@ -95,7 +95,7 @@ public async Task> GetTopQueriesByCpuAsync(int serverId, int using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); var dbClause = BuildDbInClause(databaseNames, "database_name", 6, out var dbValues); command.CommandText = @" @@ -407,7 +407,7 @@ public async Task> GetQueryStatsHistoryAsync(int serv { using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" SELECT collection_time, @@ -537,7 +537,7 @@ public async Task> GetProcedureStatsHistoryAsync( { using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc: null, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" SELECT collection_time, @@ -826,7 +826,7 @@ public async Task> GetProcStatsSlicerDataAsync( { using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc: null, SelectedServerTabUtcOffsetMinutes); var dbClause = BuildDbInClause(databaseNames, "database_name", 4, out var dbValues); command.CommandText = @" @@ -876,7 +876,7 @@ public async Task> GetTopProceduresByCpuAsync(int server using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); var dbClause = BuildDbInClause(databaseNames, "database_name", 6, out var dbValues); command.CommandText = @" @@ -1113,7 +1113,7 @@ public async Task> GetQueryDurationTrendAsync(int serverId using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); var dbClause = BuildDbInClause(databaseNames, "database_name", 4, out var dbValues); command.CommandText = @" @@ -1190,7 +1190,7 @@ public async Task> GetProcedureDurationTrendAsync(int serv using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); var dbClause = BuildDbInClause(databaseNames, "database_name", 4, out var dbValues); command.CommandText = @" @@ -1243,7 +1243,7 @@ public async Task> GetExecutionCountTrendAsync(int serverI using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc: null, SelectedServerTabUtcOffsetMinutes); var dbClause = BuildDbInClause(databaseNames, "database_name", 4, out var dbValues); command.CommandText = @" @@ -1337,7 +1337,7 @@ public async Task GetQueryHeatmapAsync(int serverId, HeatmapMetri using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc: null, SelectedServerTabUtcOffsetMinutes); var metricExpr = GetMetricColumn(metric); var dbClause = BuildDbInClause(databaseNames, "database_name", 4, out var dbValues); diff --git a/Lite/Services/LocalDataService.QueryStore.cs b/Lite/Services/LocalDataService.QueryStore.cs index dce642bbf..01a7c1af3 100644 --- a/Lite/Services/LocalDataService.QueryStore.cs +++ b/Lite/Services/LocalDataService.QueryStore.cs @@ -28,7 +28,7 @@ public async Task> GetQueryStoreSlicerDataAsync( { using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc: null, SelectedServerTabUtcOffsetMinutes); var dbClause = BuildDbInClause(databaseNames, "database_name", 4, out var dbValues); command.CommandText = @" @@ -161,7 +161,7 @@ public async Task> GetQueryStoreTopQueriesAsync(int serverId using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); var dbClause = BuildDbInClause(databaseNames, "database_name", 5, out var dbValues); command.CommandText = @" @@ -595,7 +595,7 @@ public async Task> GetQueryStoreItemTimelineAs { using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc: null, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" WITH deduped AS @@ -690,7 +690,7 @@ public async Task> GetQueryStoreHistoryAsync(int serv { using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc: null, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" SELECT collection_time, @@ -895,7 +895,7 @@ public async Task> GetQueryStoreDurationTrendAsync(int ser using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); var dbClause = BuildDbInClause(databaseNames, "database_name", 4, out var dbValues); command.CommandText = @" diff --git a/Lite/Services/LocalDataService.QueryStoreRegressions.cs b/Lite/Services/LocalDataService.QueryStoreRegressions.cs index f74377100..183e20d0a 100644 --- a/Lite/Services/LocalDataService.QueryStoreRegressions.cs +++ b/Lite/Services/LocalDataService.QueryStoreRegressions.cs @@ -69,7 +69,7 @@ public async Task> GetQueryStoreRegressionsAsync( using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, null, null, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, null, null, asOfUtc, utcOffsetMinutes: 0); var dbClause = BuildDbInClause(databaseNames, "database_name", 4, out var dbValues); var limitIndex = 4 + dbValues.Count; @@ -229,7 +229,7 @@ ORDER BY additional_duration_ms DESC using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, null, null, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, null, null, asOfUtc, utcOffsetMinutes: 0); command.CommandText = @" SELECT diff --git a/Lite/Services/LocalDataService.SessionStats.cs b/Lite/Services/LocalDataService.SessionStats.cs index c14e2ff00..c96be60c5 100644 --- a/Lite/Services/LocalDataService.SessionStats.cs +++ b/Lite/Services/LocalDataService.SessionStats.cs @@ -36,7 +36,7 @@ public async Task> GetSessionStatsAsync(int serverId, in using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc: null, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" SELECT diff --git a/Lite/Services/LocalDataService.SystemEvents.cs b/Lite/Services/LocalDataService.SystemEvents.cs index 0f2c75e46..1e7154bfb 100644 --- a/Lite/Services/LocalDataService.SystemEvents.cs +++ b/Lite/Services/LocalDataService.SystemEvents.cs @@ -332,7 +332,7 @@ AND event_xml IS NOT NULL public async Task> GetSchedulerIssuesAsync(int serverId, int hoursBack = 24, DateTime? fromDate = null, DateTime? toDate = null, DateTime? asOfUtc = null) { using var _q = TimeQuery("GetSchedulerIssuesAsync", "v_system_health_events scheduler_monitor shred"); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); var xmls = await ReadSystemHealthEventXmlAsync(serverId, startTime, endTime, SystemHealthParser.SchedulerMonitorEvent); var rows = new List(); @@ -356,7 +356,7 @@ public async Task> GetSchedulerIssuesAsync(int serverId, public async Task> GetSevereErrorsAsync(int serverId, int hoursBack = 24, DateTime? fromDate = null, DateTime? toDate = null, IReadOnlyList? databaseNames = null, DateTime? asOfUtc = null) { using var _q = TimeQuery("GetSevereErrorsAsync", "v_system_health_events error_reported shred"); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); var map = await GetDatabaseNameMapAsync(serverId); var xmls = await ReadSystemHealthEventXmlAsync(serverId, startTime, endTime, SystemHealthParser.ErrorReportedEvent); @@ -382,7 +382,7 @@ public async Task> GetSevereErrorsAsync(int serverId, int h public async Task> GetMemoryConditionsAsync(int serverId, int hoursBack = 24, DateTime? fromDate = null, DateTime? toDate = null, DateTime? asOfUtc = null) { using var _q = TimeQuery("GetMemoryConditionsAsync", "v_system_health_events sp_server_diagnostics RESOURCE shred"); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); var xmls = await ReadSystemHealthEventXmlAsync(serverId, startTime, endTime, SystemHealthParser.SpServerDiagnosticsEvent); var rows = new List(); @@ -402,7 +402,7 @@ public async Task> GetMemoryConditionsAsync(int server public async Task> GetMemoryBrokerAsync(int serverId, int hoursBack = 24, DateTime? fromDate = null, DateTime? toDate = null, DateTime? asOfUtc = null) { using var _q = TimeQuery("GetMemoryBrokerAsync", "v_system_health_events memory_broker shred"); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); var xmls = await ReadSystemHealthEventXmlAsync(serverId, startTime, endTime, SystemHealthParser.MemoryBrokerEvent); var rows = new List(); @@ -421,7 +421,7 @@ public async Task> GetMemoryBrokerAsync(int serverId, int public async Task> GetMemoryNodeOomAsync(int serverId, int hoursBack = 24, DateTime? fromDate = null, DateTime? toDate = null, DateTime? asOfUtc = null) { using var _q = TimeQuery("GetMemoryNodeOomAsync", "v_system_health_events memory_node_oom shred"); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); var xmls = await ReadSystemHealthEventXmlAsync(serverId, startTime, endTime, SystemHealthParser.MemoryNodeOomEvent); var rows = new List(); @@ -455,7 +455,7 @@ public async Task> GetSignificantWaitsAsync(int serverI public async Task<(List Rows, int CapturedCount)> GetSignificantWaitsWithCaptureAsync(int serverId, int hoursBack = 24, DateTime? fromDate = null, DateTime? toDate = null, DateTime? asOfUtc = null) { using var _q = TimeQuery("GetSignificantWaitsAsync", "v_system_health_events wait_info shred"); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); var xmls = await ReadSystemHealthEventXmlAsync(serverId, startTime, endTime, SystemHealthParser.WaitInfoEvent); var rows = new List(); @@ -506,7 +506,7 @@ AND event_xml IS NOT NULL public async Task> GetCpuTasksAsync(int serverId, int hoursBack = 24, DateTime? fromDate = null, DateTime? toDate = null, DateTime? asOfUtc = null) { using var _q = TimeQuery("GetCpuTasksAsync", "v_system_health_events sp_server_diagnostics QUERY_PROCESSING shred"); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); var xmls = await ReadSystemHealthEventXmlAsync(serverId, startTime, endTime, SystemHealthParser.SpServerDiagnosticsEvent); var rows = new List(); @@ -530,7 +530,7 @@ public async Task> GetCpuTasksAsync(int serverId, int hoursBac public async Task> GetIoIssuesAsync(int serverId, int hoursBack = 24, DateTime? fromDate = null, DateTime? toDate = null, DateTime? asOfUtc = null) { using var _q = TimeQuery("GetIoIssuesAsync", "v_system_health_events sp_server_diagnostics IO_SUBSYSTEM shred"); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); var xmls = await ReadSystemHealthEventXmlAsync(serverId, startTime, endTime, SystemHealthParser.SpServerDiagnosticsEvent); var rows = new List(); @@ -559,7 +559,7 @@ public async Task> GetIoIssuesAsync(int serverId, int hoursBac public async Task> GetSystemHealthAsync(int serverId, int hoursBack = 24, DateTime? fromDate = null, DateTime? toDate = null, DateTime? asOfUtc = null) { using var _q = TimeQuery("GetSystemHealthAsync", "v_system_health_events sp_server_diagnostics SYSTEM shred"); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); var xmls = await ReadSystemHealthEventXmlAsync(serverId, startTime, endTime, SystemHealthParser.SpServerDiagnosticsEvent); var records = new List(); diff --git a/Lite/Services/LocalDataService.TempDb.cs b/Lite/Services/LocalDataService.TempDb.cs index 2a83ecf0c..f8780d561 100644 --- a/Lite/Services/LocalDataService.TempDb.cs +++ b/Lite/Services/LocalDataService.TempDb.cs @@ -23,7 +23,7 @@ public async Task> GetTempDbTrendAsync(int serverId, int hoursBa using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" SELECT diff --git a/Lite/Services/LocalDataService.WaitStats.cs b/Lite/Services/LocalDataService.WaitStats.cs index 02092e102..e451128b9 100644 --- a/Lite/Services/LocalDataService.WaitStats.cs +++ b/Lite/Services/LocalDataService.WaitStats.cs @@ -31,7 +31,7 @@ public async Task> GetWaitStatsAsync(int serverId, int hoursB using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); var exclude = IgnoredWaitTypes.BuildExclusionClause(_ignoredWaitTypes.Value); command.CommandText = $@" @@ -105,7 +105,7 @@ public async Task> GetDistinctWaitTypesAsync(int serverId, int hour using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); var exclude = IgnoredWaitTypes.BuildExclusionClause(_ignoredWaitTypes.Value); command.CommandText = $@" @@ -141,7 +141,7 @@ public async Task> GetWaitStatsTrendAsync(int serverId using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" WITH raw AS @@ -201,7 +201,7 @@ public async Task>> GetWaitStatsTre using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc: null, SelectedServerTabUtcOffsetMinutes); var typeParams = string.Join(", ", waitTypes.Select((_, i) => "$" + (i + 4))); command.CommandText = $@" @@ -266,7 +266,7 @@ public async Task> GetTotalWaitTrendAsync(int serverId using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc: null, SelectedServerTabUtcOffsetMinutes); var exclude = IgnoredWaitTypes.BuildExclusionClause(_ignoredWaitTypes.Value); command.CommandText = $@" @@ -364,7 +364,7 @@ public async Task> GetQuerySnapshotsByWaitTypeAsync( using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc: null, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" WITH blocked_counts AS ( @@ -488,7 +488,7 @@ public async Task> GetAllQuerySnapshotsInRangeAsync( using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc: null, SelectedServerTabUtcOffsetMinutes); command.CommandText = @" WITH blocked_counts AS ( diff --git a/Lite/Services/LocalDataService.WaitingTasks.cs b/Lite/Services/LocalDataService.WaitingTasks.cs index 693e9b2de..14cec0cc1 100644 --- a/Lite/Services/LocalDataService.WaitingTasks.cs +++ b/Lite/Services/LocalDataService.WaitingTasks.cs @@ -30,7 +30,7 @@ BuildExclusionClause the wait-stats reads use. */ /* The window's upper edge is $3, so the optional database list starts at $4. Bounding both edges (rather than only the lower one) is what lets an as_of anchor mean anything here. */ - var (startTime, endTime) = GetTimeRange(hoursBack, null, null, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, null, null, asOfUtc, utcOffsetMinutes: 0); var dbClause = BuildDbInClause(databaseNames, "database_name", 4, out var dbValues); command.CommandText = $@" SELECT @@ -105,7 +105,7 @@ public async Task> GetWaitingTaskTrendAsync(int serv using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); /* #1240 parity: exclude the user's ignored (benign) wait types at DISPLAY time (mirrors the wait-stats reads) so the Current Waits duration chart matches the Wait Stats tab. */ @@ -154,7 +154,7 @@ public async Task> GetBlockedSessionTrendAsync(in using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); - var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc); + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate, asOfUtc, SelectedServerTabUtcOffsetMinutes); var dbClause = BuildDbInClause(databaseNames, "database_name", 4, out var dbValues); command.CommandText = @" diff --git a/Lite/Services/LocalDataService.cs b/Lite/Services/LocalDataService.cs index d6990d25d..fdcd8c81c 100644 --- a/Lite/Services/LocalDataService.cs +++ b/Lite/Services/LocalDataService.cs @@ -107,18 +107,44 @@ private static long ToInt64(object value) return Convert.ToInt64(value); } + /// + /// The UTC offset of the desktop's currently selected server tab, for a read whose server-local + /// fromDate/toDate can only have come from that tab's own toolbar pickers. + /// + /// Named rather than spelled ServerTimeHelper.UtcOffsetMinutes inline because it is an + /// answer, not a value: it says "this window belongs to whichever server the desktop has selected". + /// That is true only for a read the selected tab drives. It is the wrong answer for a read that can + /// run for a server other than the selected one, and such a read has to take the offset of the server + /// it names — see , which does. + /// + /// The offset is applied twice per window and the two applications have to name the same + /// server or they stop cancelling: ServerTab.GetCurrentWindow converts the pickers from the + /// display mode into server time, and the custom-range branch below converts back out to UTC. In + /// TimeDisplayMode.UTC and LocalTime the pair cancels; in ServerTime, the + /// default, only the branch below applies anything. A caller that changes one side's offset source + /// without the other breaks the two modes that cancel, so the two are paired per path. + /// + private static int SelectedServerTabUtcOffsetMinutes => ServerTimeHelper.UtcOffsetMinutes; + /// /// Gets the time range for queries based on hoursBack or explicit date range. /// Returns UTC time for collection_time queries (most tables store collection_time in UTC). - /// When fromDate/toDate are provided, they should already be in UTC. /// - private static (DateTime startTime, DateTime endTime) GetTimeRange(int hoursBack, DateTime? fromDate, DateTime? toDate, DateTime? asOfUtc = null) + /// + /// The UTC offset of the server whose rows this window will select — the same server as the + /// server_id in the predicate beside it. REQUIRED rather than defaulted: an offset and a + /// server_id are two halves of one question, and taking the offset from ambient state is how they came + /// to name two different servers. A caller with no server-specific offset to give has to say so at the + /// call site instead of inheriting one silently. Ignored unless and + /// are both supplied, since only that branch converts. + /// + private static (DateTime startTime, DateTime endTime) GetTimeRange(int hoursBack, DateTime? fromDate, DateTime? toDate, DateTime? asOfUtc, int utcOffsetMinutes) { if (fromDate.HasValue && toDate.HasValue) { /* Custom date range - convert from server time back to UTC for storage lookup */ - var startUtc = fromDate.Value.AddMinutes(-ServerTimeHelper.UtcOffsetMinutes); - var endUtc = toDate.Value.AddMinutes(-ServerTimeHelper.UtcOffsetMinutes); + var startUtc = fromDate.Value.AddMinutes(-utcOffsetMinutes); + var endUtc = toDate.Value.AddMinutes(-utcOffsetMinutes); return (startUtc, endUtc); } diff --git a/Lite/Services/ServerTimeHelper.cs b/Lite/Services/ServerTimeHelper.cs index a94753851..2e9039b61 100644 --- a/Lite/Services/ServerTimeHelper.cs +++ b/Lite/Services/ServerTimeHelper.cs @@ -32,10 +32,13 @@ public static int UtcOffsetMinutes /// Converts a local DateTime (from date picker) to server time. /// Use when the user picks dates in their local timezone but the database stores server time. /// - private static DateTime LocalToServerTime(DateTime localTime) + private static DateTime LocalToServerTime(DateTime localTime) => + LocalToServerTime(localTime, _utcOffsetMinutes); + + private static DateTime LocalToServerTime(DateTime localTime, int utcOffsetMinutes) { var utcTime = localTime.ToUniversalTime(); - return utcTime.AddMinutes(_utcOffsetMinutes); + return utcTime.AddMinutes(utcOffsetMinutes); } /// @@ -67,10 +70,24 @@ private static DateTime ToLocalTime(DateTime serverTime) /// /// Converts a display-mode DateTime back to server time. Reverse of ConvertForDisplay. /// - public static DateTime DisplayTimeToServerTime(DateTime displayTime, TimeDisplayMode mode) => mode switch + public static DateTime DisplayTimeToServerTime(DateTime displayTime, TimeDisplayMode mode) => + DisplayTimeToServerTime(displayTime, mode, _utcOffsetMinutes); + + /// + /// Converts a display-mode DateTime back to the local time of a NAMED server, rather than of + /// whichever server the desktop currently has selected. + /// + /// For a caller that then hands the result to a read windowing on that same server: the read + /// converts server time back out to UTC with the server's offset, so the offset given here has to be + /// the one the read will use. Under TimeDisplayMode.UTC and LocalTime this conversion + /// and the read's cancel each other and the window survives unchanged; under ServerTime, the + /// default, this conversion is the identity and only the read's applies. Two different servers' + /// offsets across the pair therefore skews the window in every mode, not just the default. + /// + public static DateTime DisplayTimeToServerTime(DateTime displayTime, TimeDisplayMode mode, int utcOffsetMinutes) => mode switch { - TimeDisplayMode.LocalTime => LocalToServerTime(displayTime), - TimeDisplayMode.UTC => displayTime.AddMinutes(_utcOffsetMinutes), + TimeDisplayMode.LocalTime => LocalToServerTime(displayTime, utcOffsetMinutes), + TimeDisplayMode.UTC => displayTime.AddMinutes(utcOffsetMinutes), _ => displayTime };