Skip to content

Make the Viewer match the web on dropped databases, labels, job history, wording and strip gaps (#6651) - #6667

Merged
erikdarlingdata merged 12 commits into
devfrom
fix/6651-viewer-parity
Oct 24, 2026
Merged

erikdarlingdata merged 12 commits into
devfrom
fix/6651-viewer-parity

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Claude posting for Erik Darling.

Closes #6651.

Why

#5704 and #5705 changed the web pages and the MCP tools. The Darling Viewer kept the old behaviour for six rows of the issue's table, so the Viewer and the web disagreed for the same server and window.

What changes

The Viewer changes are under Darling/PerformanceMonitor.Darling.Viewer/. Lite ships its own copies of items 1, 2, 3 and 5; round 1 part 2 fixes each of them the same way under Lite/ (see "Round 1 fixes (part 2, Lite)" below).

  1. Dropped databases (wrong data). The Top Procedures by Duration read now calls CurrentDatabaseScope.NarrowAsync, the one rule the service and web share, before it reads. With no database chosen, the dropped names are excluded in the statement ($6, raw and hourly arms), so the rank's LIMIT is not filled by them. A database the user names is read as asked; an unknown set leaves out nothing; a custom range that ended in the past is anchored at its end (the MCP as_of behaviour). The read result carries LeftOutDatabases. A new banner above the grid names the omitted databases with the shared DatabasesLeftOut.Note. On an empty page it shows the shared CollectorRunFacts.ProcedureStatsEmpty sentence ("No procedure ran in the last Nh in a database this server still has ...") built from the collector's own run history (a small viewer-side read of v_collection_log, the same three counts the service reads); the MCP pointer "(see databases_left_out)" is dropped from that sentence because the names are in the same banner. The slicer-range handler updates the banner too.
    Checked the Queries tab's other per-database reads: the Query Store clutter reads already go through DarlingQueryStoreClutterReader with the default (all databases) filter, which applies OmitDroppedAsync. The web applies the narrowing to top procedures only, so Top Queries is left alone.
  2. W21(a). Removed the Job History "Last Success" column and its filter button, and the LastSuccessfulRunLocal view-model property. The issue and brief point to a second column at ViewerServerTab.xaml (LastSuccessFormatted); that one is the Collection Health grid's per-collector "Last Success", a different fact (it is not a job's repeated success), so it stays.
  3. W19 labels. FinOps Utilization "Avg/P95/Max CPU" cards and "Avg/P95/Max CPU %" grid headers become "... SQL Server CPU" / "... SQL Server CPU %"; the Memory tab figure "Plan Cache" becomes "Plan cache (all cache stores)". The words are constants in the new ViewerLabels class, bound with {x:Static}, so a test reads the value the screen shows.
  4. W8 wording. The Settings text after the uncorroborated-route choice no longer names get_alert_settings or analysis.uncorroborated_route_source; it says the Alert Settings page shows which value is in force. Grepped the Viewer XAML and strings for other get_* and *_source names: the only others are code comments, plus the Ask-AI prompt text, which is sent to an AI and names tools on purpose.
  5. W7 gaps. The Top Queries / Top Procedures time-range strip (TimeRangeSlicerControl, shared by the other slicers) is drawn as separate runs. Runs are split by TimeSeriesGaps.BreakAtGaps (the Viewer's line-chart rule: 3x the median spacing, no break for fewer than three points), through a new static SplitAtGaps.
  6. Re-read the issue table row by row: all six rows are covered above.

Test plan

  • Build Darling.Tests: 0 Warning(s), 0 Error(s).
  • Targeted run on a throwaway PostgreSQL with TimescaleDB: new ViewerTopProceduresDroppedDatabaseTests (live raw and hourly, top 1 with the dropped database heavier, named-database case, unknown set, banner text) and ViewerWebParityWordsTests, plus ViewerHourlyWindowEdgesTests, ViewerHourlyWindowEdgesLiveTests, StoredServerClockRowTextTests, ViewerGridTimeColumnSortMemberTests, RollupWindowEndBoundTests, ViewerQueriesTests, ViewerQueryTabLoadTests, ViewerIoRollupRouteTests, ViewerQueryTagColumnLiveTests, every *Census* and *Ratchet* class, and Stage=Guard: Total: 436, Failed: 0, Skipped: 0.
  • Revert-proof (items 1, 2, 5): with narrowing disabled, the property re-added and the splitter returning one run, 4 of 11 new tests failed (both live dropped-database tests, the job row test, the strip test). Restored, all pass.
  • Items 3 and 4 are constants in ViewerLabels; the tests pin the words, but the {x:Static} binding in XAML is not exercised (no screenshot: the Viewer cannot be run against the real config).
  • Not run: the full suite and pre-push.sh (CI decides).

An existing test, HourlySql_BindsTheCeilingOnlyWhenOneIsKnown_OnBothGrids, now expects the procedures hourly arm's ceiling at $7 (the exclusion takes $6); the rule it pins (a ceiling bound only when one is known, beside the window end) still holds. Two LastSuccessfulRunLocal assertions and one sort-member table row went with the removed column.

Considered, not added

CHANGELOG entry

SECTION: Fixed
ENTRY:

Round 1 fixes (part 1)

  1. Red guard (CurrentDatabaseScopeLoggerThreadingTests). The new CurrentDatabaseScope.NarrowAsync call in ViewerDataService.ProcedureStats.cs passed no logger. It now passes logger: null, as every other Viewer call into these readers does (PgSecondaryReplicaScope.ReadAsync, the Query Store readers; the Viewer has no host logger).
  2. Red test (UncorroboratedRouteStoreKnobTests.TheSettingsWindowCombo_PrefillsSavesAndRestores_ThroughTheThreeStates). The test pinned the old XAML sentence, including the MCP key analysis.uncorroborated_route_source. This PR moved the note into ViewerLabels.UncorroboratedRouteNote on purpose (no key name in prose for a person). The test now checks the XAML binds that label and that the label keeps the precedence and sweep words and names the Alert Settings page, with no key name. Prefill, save and restore were not broken; those assertions pass unchanged.
  3. The strip gap could not fire. Redraw split _data after FillEmptyBuckets had given every hour a bucket, so no jump ever showed. The decision is now made in LoadData from the real buckets (RunsOverFilledBuckets), with the web strip's rule (chart-gaps.js gapThresholdMs: a jump over 3 times the median spacing, floor 5 minutes, no break for fewer than two spacings or a series with no steady cadence, 75th percentile over 3 times the 25th). The filled hours inside a gap are left out of the line and area, which break there; the fill still serves the bars and axis. A hole shorter than the threshold keeps its zero buckets in the line, as before. SplitAtGaps (which used TimeSeriesGaps, the line-chart rule) is removed.
    Tests (ViewerWebParityWordsTests, through LoadData and the control's DrawnRuns, on the STA thread): 18:00-21:00 plus 07:00-10:00 in an 18:00-10:30 window gives runs (0,4) and (13,4); a two-hour hole, two buckets 9 hours apart, and a steady series stay one run.

Ran (Debug build, 0 errors; throwaway PostgreSQL stopped after): CurrentDatabaseScopeLoggerThreadingTests, UncorroboratedRouteStoreKnobTests, ViewerWebParityWordsTests, ViewerTopProceduresDroppedDatabaseTests, ViewerHourlyWindowEdgesTests, *TimeRangeSlicer*, every *Census* and *Ratchet*: Total 546, Failed 0, Skipped 0. Not run: pre-push.sh and the full suite (CI decides). The wrapper scripts for CPU capping were refused by the harness, so the build ran plain with -m:1.

Considered, not added: the web's 5-minute floor never binds for hourly buckets; kept anyway so the rule matches the web word for word.

Round 1 fixes (part 2, Lite)

I confirmed each Lite twin at the lines the review named before changing it; all four were real (line numbers had moved by a few lines). Lite and the Viewer now behave the same for 1 to 4.

  1. Dropped databases in Top Procedures. The Lite grid's three reads (sub-tab switch and full refresh in ServerTab.Refresh.cs, slicer change in ServerTab.Slicers.cs) now go through one ServerTab.ReadTopProceduresAsync. With no database chosen it asks LocalDataService.GetDatabasesLeftOutOfRankingAsync (new, in LocalDataService.CurrentDatabases.cs, over the existing GetDroppedDatabasesAsync) and passes the result as excludeDatabaseNames to GetTopProceduresByCpuAsync, as the get_top_procedures_by_cpu MCP tool does. A database the user names is read as asked, an unknown set (no recent snapshot) leaves out nothing, and a window that ended in the past is anchored at its end (same rule as the Viewer). A new ProcStatsLeftOutBanner (in ServerTab.xaml, text from ServerTab.ProcedureLeftOutText) names the databases and carries the shared DatabasesLeftOut.Note; on an empty page it uses CollectorRunFacts.ProcedureStatsEmpty so it claims only the databases the server still has. To count the collector's successful runs inside the grid's exact window, GetCollectorRunFactsAsync got an optional window parameter (existing callers unchanged). Both reads run on the pool inside ReadTopProceduresAsync, which the callers now await directly (the TabLoadsOffUiThreadTests census wants every _dataService call inside a Task.Run).
  2. Job History "Last Success" column. Removed from JobHistoryTab.xaml and LastSuccessfulRunLocal removed from LocalDataService.JobHistory.cs. Nothing else read the property (grep over Lite, Lite.Tests); LastSuccessfulRun and LastSuccessfulRunUtc stay, as in Darling. The two test pins on the property (GridTimeColumnSortMemberTests roster row, StoredServerClockRowTextTests assert) were removed with it.
  3. Labels. FinOpsTab.xaml: "Avg/P95/Max CPU" and "... CPU %" now read "Avg/P95/Max SQL Server CPU" and "... SQL Server CPU %". ServerTab.xaml Memory tab: "Plan cache (all cache stores)". Plain text in the XAML; Lite has no label constants class and a test would only read source text (Tests that assert spelling, not behavior: cap them, stop the review asking for pins, delete the shape ones #5833).
  4. Strip gaps. TimeRangeSlicerControl.xaml.cs mirrors Darling's commit 223a06e: RunsOverFilledBuckets decides the gaps from the real buckets before FillEmptyBuckets, with the web rule (3x median spacing, 5-minute floor, no break for an irregular cadence or under two spacings); DrawnRuns exposes the pieces; Redraw draws one figure per run. LoadData and ShowEmpty set them. No use of TimeSeriesGaps.

Tests (new): TopProceduresGridDroppedDatabaseTests (the grid's two reads against a DuckDB store: dropped database left out and top-1 not filled by it; named dropped database read as asked; a past window anchored at its end vs the same days read to now; no snapshot leaves nothing out; banner text with rows, empty page, cap and absent), TimeRangeSlicerGapTests (through new TimeRangeSlicerControl(), LoadData, DrawnRuns on the STA thread: the same two cases as Darling's), JobHistoryLastSuccessColumnTests. Changed: SecondOccurrenceReadTests pin string for OnProcStatsSlicerChanged now names ReadTopProceduresAsync(0, e.StartUtc, e.EndUtc) (its rule, that the handler passes the event's UTC bounds straight to its read, still holds).

Ran (Debug build of Lite.Tests, 0 errors; 2 warnings, both the existing EmailAlertService.cs CA1068 in a file this PR does not touch): -trait Stage=Guard Total 1184, Failed 0; the classes *TimeRangeSlicer*, *JobHistory*, *FinOps*, *TopProcedures*, *DroppedDatabase*, *CurrentDatabases*, *StoredServerClock*, *GridTimeColumn*, *CollectorRun*, *SecondOccurrenceRead*, *TabLoadsOff*, *ServerTabEmpty*, *MultiServerTime*, *ScreenReaderRowNames*, every *Census* and *Ratchet*: Total 582, Failed 0. Also the classes ci-select.py local-classes named that were not in those: Total 326, Failed 1 (the SecondOccurrenceReadTests pin above, fixed and re-run green). Not run: pre-push.sh, the full suite and the Darling classes (CI decides). I did not revert the fix to watch the new tests fail: DrawnRuns and GetDatabasesLeftOutOfRankingAsync do not exist before the change, so the gap and left-out tests cannot compile against the old code. The wrapper scripts for CPU capping were refused for the build (the harness refused cpucap.sh), so the build ran plain with -m:1; the test slot wrapper worked.

Considered, not added: a Lite test of the banner XAML binding (source text, #5833); a revert-and-fail run of the new tests (they cannot compile on the old code, see above).

One definition each

The round-1 fixes left two pairs of identical code in the Viewer and in Lite. Each pair is now one definition (REVIEW-TRAPS #3684, alias, do not copy):

  • Left-out banner text: DatabasesLeftOut.ProcedureBanner in PerformanceMonitor.Common/DatabasesLeftOut.cs (was ViewerServerTab.ProcedureLeftOutText and ServerTab.ProcedureLeftOutText). Both tabs call it; the two existing banner test classes now call the shared method with the same assertions.
  • Strip gap rule: StripGaps in PerformanceMonitor.Ui/StripGaps.cs, beside TimeSeriesGaps (unchanged). It holds RunsOverFilledBuckets, GapThresholdTicks, GapFactor and GapFloorTicks, with the comment naming the web's chart-gaps.js gapThresholdMs. Both TimeRangeSlicerControls call StripGaps.RunsOverFilledBuckets. The two copies were diffed first: identical apart from the parameter type (IReadOnlyList<long> vs List<long>) and one comment sentence.
  • Checked: Lite.Tests -trait Stage=Guard 1184 passed; Lite.Tests TimeRangeSlicerGapTests, TopProceduresGridDroppedDatabaseTests, TimeSeriesGapsTests, *LeftOut*, *Census*, *Ratchet* 52 passed; Darling.Tests ViewerWebParityWordsTests, ViewerTopProceduresDroppedDatabaseTests (live, on a throwaway PostgreSQL, stopped), *LeftOut*, *Census*, *Ratchet* 512 passed. The full suite and pre-push.sh were not run (CI decides). The build shows 0 warnings in the changed files; the remaining warnings are in files this change does not touch.

Seat corrections

  • CI: ViewerDataStartBannerTests (the Queries tab load and drag cases) now accepts the Top Procedures left-out banner call between the truncation banner and the next call.

@erikdarlingdata
erikdarlingdata marked this pull request as ready for review October 24, 2026 03:26

/* #6651 (#5644 W7): a stretch with no samples (the service host was off overnight) is a break in the strip, not a ramp or a flat
line across it. The break rule is the one the Viewer's line charts use (TimeSeriesGaps, three times the median spacing). */
var runs = SplitAtGaps(_data.Select(d => d.BucketTime).ToList());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SplitAtGaps here runs on _data, and _data is always the output of FillEmptyBuckets (same file, lines 211-235), which synthesizes a zero-value TimeSliceBucket for every hour between floorStart and floorEnd — including hours where the service was off. Every one of the six LoadData(...) call sites (ViewerServerTab.ActiveQueries.cs:111, ViewerServerTab.Queries.cs:583,614,646, ViewerServerTab.Blocking.cs:355,365) passes non-null startUtc/endUtc, so this fill always runs.

The consequence: by the time Redraw() calls SplitAtGaps(_data.Select(d => d.BucketTime)...), consecutive BucketTime deltas are always exactly one hour — there is no longer a genuine time gap for a service-off stretch to show up as, since the empty hours were already backfilled with zero-value points before this method ever sees the data. GapThreshold (3× the median spacing) can never be exceeded when every spacing equals the median, so SplitAtGaps will always return one single run here, identical to the old unconditional-draw behavior.

In other words, the new "break at the service-off gap" drawing logic is unreachable through every real call path — the strip will never actually break. The new test (ViewerWebParityWordsTests.Strip_BreaksWhereTheServiceWasOff_InsteadOfDrawingAcrossIt) only calls SplitAtGaps directly on raw, unfilled timestamps, so it can't catch this — it never goes through LoadData/FillEmptyBuckets the way Redraw() does.

To actually fix the drawn strip, the break needs to survive past FillEmptyBuckets — e.g. by having it mark a synthesized bucket (a flag on TimeSliceBucket, or simply not filling hours with no data and instead passing the raw timestamps straight to the gap splitter) instead of relying on a uniform 1-hour-spaced series to still show a timing gap it no longer has.

@claude

claude Bot commented Oct 24, 2026

Copy link
Copy Markdown

The PR body's "Viewer only: Lite has no twin of these surfaces" is refuted for at least four of the six items. Lite ships its own, separate copies of these same UI surfaces (by this repo's established pattern of a Darling/Lite twin pair per control), and each one has the identical thing this PR fixes in Darling, left untouched:

  • Item 1 (dropped databases in Top Procedures). Lite/Services/LocalDataService.QueryStats.cs:1054's GetTopProceduresByCpuAsync already accepts excludeDatabaseNames (Lite's own get_top_procedures_by_cpu MCP tool passes it), but Lite's desktop grid never passes it: Lite/Controls/ServerTab.Refresh.cs:443,491 and Lite/Controls/ServerTab.Slicers.cs:235 all call GetTopProceduresByCpuAsync(...) without excludeDatabaseNames. Lite's own Top Procedures grid still ranks with dropped databases included, exactly the bug this PR fixes for Darling's Viewer.
  • Item 2 (W21(a), "Last Success" column). Lite/Controls/JobHistoryTab.xaml:188-192 still has the LastSuccessfulRunLocal-bound "Last Success" column with its filter button, and Lite/Services/LocalDataService.JobHistory.cs:654 still defines LastSuccessfulRunLocal the same way Darling's removed property did (including the "Never" fallback). Darling's twin column/property is gone; Lite's is not.
  • Item 3 (W19 labels). Lite/Controls/FinOpsTab.xaml:270,294,318,493,496,499 still say "Avg CPU" / "P95 CPU" / "Max CPU" / "Avg CPU %" / "P95 CPU %" / "Max CPU %", and Lite/Controls/ServerTab.xaml:1537 still says "Plan Cache" — the exact words this PR changes to "... SQL Server CPU[/ %]" and "Plan cache (all cache stores)" in Darling's FinOpsTab.xaml / ViewerServerTab.xaml.
  • Item 5 (W7 strip gaps). Lite/Controls/TimeRangeSlicerControl.xaml.cs is a separate implementation from Darling's, with the identical FillEmptyBuckets (line 205) + unconditional BeginFigure/LineTo draw (lines 313-324) that draws a continuous line across a gap. It has no SplitAtGaps counterpart, so it keeps drawing straight across a service-off stretch exactly as Darling's did before this PR (and, per the inline comment on TimeRangeSlicerControl.xaml.cs:332, arguably still does even after it, since FillEmptyBuckets already erases the gap before the new splitter runs).

Item 4 (Settings wording) and item 6 look genuinely Darling-only (no matching uncorroboratedRoute/get_alert_settings text found in Lite/Windows/SettingsWindow.xaml), so the "no twin" framing isn't wrong across the board — but it is wrong for 1, 2, 3 and 5, which is most of the PR's content. Per .github/REVIEW-TRAPS.md's repository-mechanics line: "Lite and Darling ship in parity (CONTRIBUTING's two-store rule): a fix to one SKU's reader, tool, alert or description without the counterpart's is a finding, not a nit." Worth either fixing Lite's twins alongside Darling's, or correcting the PR body/issue to say explicitly that Lite is staying out of sync here and why.

claude[bot]
claude Bot previously requested changes Oct 24, 2026

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: CHANGES REQUESTED

Claims

  • VERIFIED — "An existing test, HourlySql_BindsTheCeilingOnlyWhenOneIsKnown_OnBothGrids, now expects the procedures hourly arm's ceiling at $7" — ViewerDataService.ProcedureStats.cs:638 calls HourlyCeilingSql(ceiling, 7), matching the updated test in ViewerHourlyWindowEdgesTests.cs.
  • VERIFIED — "Checked the Queries tab's other per-database reads: the Query Store clutter reads already go through DarlingQueryStoreClutterReader ... which applies OmitDroppedAsync" — confirmed at DarlingQueryStoreClutterReader.cs:275,415,496.
  • VERIFIED — "Grepped the Viewer XAML ... for other get_* and *_source names: the only others are code comments" — spot-checked CollectionLogWindow.xaml:95, FinOpsTab.xaml:708, ViewerServerTab.xaml:1882,2384,2453,3038,4083,4157,4586, all inside <!-- --> comments.
  • REFUTED — "All under Darling/PerformanceMonitor.Darling.Viewer/. Viewer only: Lite has no twin of these surfaces." — Lite ships its own twin of four of the six items (dropped-database ranking, the Job History "Last Success" column, the FinOps/Plan-Cache labels, and TimeRangeSlicerControl's gap handling), none of them fixed; see the posted top-level comment for file:line evidence on each.
  • UNVERIFIED — "Targeted run on a throwaway PostgreSQL with TimescaleDB: ... Total: 436, Failed: 0, Skipped: 0" and the "Revert-proof" claims — these are live test-run results that need a real PostgreSQL/TimescaleDB instance and a Windows build; not checkable from a read-only tree.

Riskiest lines

  1. Darling/PerformanceMonitor.Darling.Viewer/TimeRangeSlicerControl.xaml.cs:332 — SplitAtGaps runs on _data, which FillEmptyBuckets (lines 211-235, unchanged, called from every one of the 6 LoadData sites with non-null bounds) has already backfilled to one zero-value bucket per hour across the whole window, so consecutive BucketTime deltas are always uniform and the 3×-median-spacing threshold can never trip — the "break where the service was off" feature this PR advertises cannot fire through any real call path. UNCOVERED — the only new test, ViewerWebParityWordsTests.Strip_BreaksWhereTheServiceWasOff_InsteadOfDrawingAcrossIt, calls SplitAtGaps directly on raw, unfilled timestamps and never exercises LoadData/Redraw() together, so it can't catch this.
  2. Darling/PerformanceMonitor.Darling.Viewer/ViewerDataService.ProcedureStats.cs:573 — the new $6 dropped-database exclusion clause in TopProceduresSql, added before GROUP BY/LIMIT so it narrows the ranking population rather than post-filtering the page. Covered by ViewerTopProceduresDroppedDatabaseTests.RawTier_LeavesTheDroppedDatabaseOut_RanksTheLiveOneAtTop1_AndReadsANamedDroppedDatabaseAsAsked (live, needs DARLING_TEST_PG; runs in the "Darling PostgreSQL tests" CI job).
  3. Darling/PerformanceMonitor.Darling.Viewer/ViewerDataService.ProcedureStats.cs:628 (databases.ExcludeParameter()) together with ViewerDataService.RollupAvailability.cs:99's HourlyCeilingSql(ceiling, index) — the hourly arm now binds exclusion at $6 and the optional ceiling at $7, so a parameter-count/position slip between the built SQL text and the bound NpgsqlParameters would misfire silently. Covered by HourlySql_BindsTheCeilingOnlyWhenOneIsKnown_OnBothGrids (asserts the built SQL's slot positions) and by ViewerTopProceduresDroppedDatabaseTests.HourlyTier_LeavesTheDroppedDatabaseOut_AtTop1 (live, exercises real parameter binding end to end).

Traps near this diff

  • "Every MCP filter is part of the query, not a post-filter on the page: parallel_only / min_dop / blocking_only cut before the LIMIT" — does not apply as a defect here: the new dropped-database exclusion is correctly placed in the SQL's WHERE/filter clause before GROUP BY/LIMIT in both the raw (ViewerDataService.ProcedureStats.cs:573) and hourly (ViewerDataService.ProcedureStats.cs:639) arms, not applied as a post-filter on the fetched page.
  • "A retention-tiered store answers a 7-day question from a 4-day raw tier and labels the missing days quiet unless the read routes by tier and says which tier served it" — does not apply: tier routing (RetentionTierRouter.Resolve) is unchanged by this diff, and LeftOutDatabases is attached after the tier is chosen in both the raw and hourly branches (ViewerDataService.ProcedureStats.cs:213,610), so the response still names its tier as before.
  • "Lite and Darling ship in parity (CONTRIBUTING's two-store rule): a fix to one SKU's reader, tool, alert or description without the counterpart's is a finding, not a nit." — applies: see the REFUTED claim above and the posted top-level comment; Lite has its own unfixed twin of 4 of the 6 items this PR addresses in Darling.

Divergence (blind read vs PR body)

The body frames this entire PR as Darling-Viewer-only because "Lite has no twin of these surfaces," but Lite has its own separate implementations of the Top Procedures grid, the Job History grid, the FinOps labels, and TimeRangeSlicerControl, each carrying the same issue being fixed here (see the REFUTED claim and the top-level comment for file:line evidence). Separately, the body's and CHANGELOG's claim that "the Top Queries and Top Procedures strip breaks where the service was off instead of drawing across it" does not hold for the shipped Redraw() path, because the pre-existing FillEmptyBuckets step erases the real time gap before the new SplitAtGaps call ever sees it (see the inline comment on TimeRangeSlicerControl.xaml.cs:332) — the unit-level SplitAtGaps function is correct and tested, but the integration claimed in the PR body and CHANGELOG is not reachable. Minor: the body says "Two LastSuccessfulRunLocal assertions ... went with the removed column," but the diff removes four such Assert.Equal lines across two test methods in StoredServerClockRowTextTests.cs; non-blocking.

Reviewer checklist

  • Run the Viewer against a server with a genuine multi-hour gap in procedure_stats/query_stats collection and confirm whether the Top Queries/Top Procedures strip actually breaks, or draws a continuous line through the zero-filled gap as described above.
  • Decide and record whether Lite's twins (Top Procedures dropped-database ranking, Job History "Last Success" column, FinOps/Plan-Cache labels, TimeRangeSlicerControl gap handling) are in scope for a follow-up, or intentionally left behind, and fix the PR body's "no twin" claim either way.

Comment on lines +300 to +344
internal static List<(int Start, int Count)> RunsOverFilledBuckets(IReadOnlyList<DateTime> bucketTimes, IReadOnlyList<DateTime> realTimes)
{
var real = realTimes.Select(t => FloorToHour(t).Ticks).Distinct().OrderBy(t => t).ToList();
var threshold = GapThresholdTicks(real);
var gaps = new List<(long Lo, long Hi)>();
if (threshold.HasValue)
{
for (var i = 0; i + 1 < real.Count; i++)
{
if (real[i + 1] - real[i] > threshold.Value)
{
gaps.Add((real[i], real[i + 1]));
}
}
}

var runs = new List<(int Start, int Count)>();
var start = -1;
var count = 0;
for (var i = 0; i < bucketTimes.Count; i++)
{
var t = FloorToHour(bucketTimes[i]).Ticks;
var missing = gaps.Exists(g => t > g.Lo && t < g.Hi);
if (!missing)
{
if (count == 0)
{
start = i;
}

count++;
}

var breaksAfter = i + 1 < bucketTimes.Count
&& gaps.Exists(g => t >= g.Lo && FloorToHour(bucketTimes[i + 1]).Ticks <= g.Hi);
if ((missing || breaksAfter || i + 1 == bucketTimes.Count) && count > 0)
{
runs.Add((start, count));
start = -1;
count = 0;
}
}

return runs;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking nit: when a single real sample is flanked by two gaps that each exceed the threshold (e.g. one isolated sample between two multi-hour holes), RunsOverFilledBuckets emits a run of (start, 1). In Redraw, that degenerate run draws BeginFigure → LineTo to the same point → LineTo straight down to the baseline, i.e. a zero-width spike rather than a visible point. Cosmetic only (rare in practice), not a correctness bug in the gap math itself — the run boundaries matched both manual tracing and ViewerWebParityWordsTests for the realistic cases. No action needed unless you want a visible marker for lone isolated samples.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: LGTM

Claims

  • VERIFIED — "The new CurrentDatabaseScope.NarrowAsync call ... passed no logger. It now passes logger: null, as every other Viewer call into these readers does" — confirmed at ViewerDataService.ProcedureStats.cs:198; every other Viewer logger: null call site (ViewerDataService.Findings.cs:136, QueryTrends.cs:651, RollupAvailability.cs:142, QueryStore.cs:539, FinOps.Recommendations.cs:197, QueryStoreRegressions.cs:262) does the same, and the guard regex in CurrentDatabaseScopeLoggerThreadingTests.EveryNonTestCaller_OfACurrentDatabaseScopeLookup_PassesALogger requires the literal logger token in the call expression.
  • VERIFIED — "The test now checks the XAML binds that label and that the label keeps the precedence and sweep words and names the Alert Settings page, with no key name" — UncorroboratedRouteStoreKnobTests.cs:333-337 asserts the {x:Static local:ViewerLabels.UncorroboratedRouteNote} binding plus the label's own prose, and ViewerLabels.UncorroboratedRouteNote was already wired into SettingsWindow.xaml by commit 04b10bdd0, which predates this follow-up's base.
  • VERIFIED — "Redraw split _data after FillEmptyBuckets had given every hour a bucket, so no jump ever showed. The decision is now made in LoadData from the real buckets" — confirmed: TimeRangeSlicerControl.xaml.cs:114 now computes _runs in LoadData from data.Select(d => d.BucketTime) (pre-fill) via RunsOverFilledBuckets, vs. the old code's SplitAtGaps(_data...) called in Redraw on the already hour-filled list, which is uniformly 1-hour spaced and so could never exceed a gap threshold.
  • VERIFIED — "Tests ... 18:00-21:00 plus 07:00-10:00 in an 18:00-10:30 window gives runs (0,4) and (13,4); a two-hour hole, two buckets 9 hours apart, and a steady series stay one run" — hand-traced RunsOverFilledBuckets/GapThresholdTicks against all four ViewerWebParityWordsTests cases; each produces exactly the asserted runs.
  • UNVERIFIED — "Build Darling.Tests: 0 Warning(s), 0 Error(s)" and the listed live/guard test run totals — these are build/test-run results from the author's machine; a read-only tree can't execute the suite or the live-PostgreSQL tests.

Riskiest lines

  1. Darling/PerformanceMonitor.Darling.Viewer/TimeRangeSlicerControl.xaml.cs:300-344 (RunsOverFilledBuckets) — the core gap-detection logic that decides where the strip breaks; a defect here silently draws a line across a real outage (the exact bug this commit fixes) or breaks a steady series — covered by ViewerWebParityWordsTests.Strip_BreaksWhereTheServiceWasOff_InsteadOfDrawingAcrossIt and Strip_WithAShortHole_OrFewBuckets_StaysOneRun, which fail on a regression to the old "split the filled data" behaviour or on an incorrect threshold.
  2. Darling/PerformanceMonitor.Darling.Viewer/TimeRangeSlicerControl.xaml.cs:264-291 (GapThresholdTicks) — the quantile/median threshold math; a defect (e.g. wrong quantile index, wrong floor) would misclassify a steady series as gapped or vice versa — covered by the same two tests above (the "short hole" case specifically exercises the 3x-median-vs-floor boundary).
  3. Darling/PerformanceMonitor.Darling.Viewer/ViewerDataService.ProcedureStats.cs:198 (logger: null) — low risk; functionally a no-op (the parameter already defaulted to null), its only effect is satisfying the repo's source-text guard — UNCOVERED by any behaviour test (the guard itself is a source-text census per the traps file's tests-and-censuses ruling, so it does not count as coverage), but the PR body states why no behaviour test applies (the Viewer has no host logger to assert against) and the change carries no behavioural risk.

Traps near this diff

  • Ruling (#5833): tests are load-bearing. ... never answer a trap with a test that reads source text or asserts how code is spelled — applies to CurrentDatabaseScopeLoggerThreadingTests.EveryNonTestCaller_OfACurrentDatabaseScopeLookup_PassesALogger (walks the repo tree with a regex over source text), which is what this commit's logger fix satisfies — but that test pre-dates this follow-up range (unchanged by these commits) and is not newly added here, so this commit doesn't violate the ruling; it's just answering an existing census-shaped guard that this diff didn't introduce.
  • Lite and Darling ship in parity (CONTRIBUTING's two-store rule): a fix to one SKU's reader, tool, alert or description without the counterpart's is a finding — does not apply to the TimeRangeSlicerControl gap-break logic: Lite's copy (Lite/Controls/TimeRangeSlicerControl.xaml.cs) has no SplitAtGaps/gap-break logic at all, and the Darling file's own header comment ("W1e copy-parity; the viewer owns its own copy, Lite/Dashboard are untouched") documents this as a pre-existing, already-accepted divergence from before this follow-up's base commit, not something newly introduced here.
  • ORDER BY 3 + 4 DESC / integer division / percent-of-page / etc. (SQL traps) — do not apply; this follow-up touches no SQL.
  • truncated = rows.Count >= limit / page-vs-population counts (storage/MCP traps) — do not apply; no paged reader or MCP payload changed in this range.

Divergence (blind read vs PR body)

None — the "Round 1 fixes (part 1)" section of the PR body accurately describes all three changes in this range (the logger pass-through, the ViewerLabels test fix, and moving the gap decision from Redraw to LoadData on the real buckets), and the blind read confirms each mechanism as described.

Reviewer checklist

  • Run the Darling Viewer against a live PostgreSQL store with an overnight outage in the Top Queries/Top Procedures window and visually confirm the strip breaks at the real gap instead of drawing a flat/ramped line across it (a read-only tree can't render WPF).

…ess, label and strip-gap fixes (#6651)

Lite's Top Procedures grid leaves out dropped databases and says so, the Job History Last Success column is gone, the FinOps and Memory labels use the web's words, and the time strip breaks where the collector was off.
…wer and Lite (#6667)

Both apps held identical copies of ProcedureLeftOutText and of the strip gap rule. The banner text is now DatabasesLeftOut.ProcedureBanner (Common) and the gap rule is StripGaps (Ui, beside TimeSeriesGaps).
@erikdarlingdata
erikdarlingdata dismissed claude[bot]’s stale review October 24, 2026 07:46

Claude posting for Erik Darling. Round 1 findings fixed; round 2 (5637085836) is LGTM.

@erikdarlingdata
erikdarlingdata enabled auto-merge (squash) October 24, 2026 07:46
@erikdarlingdata
erikdarlingdata merged commit 9566fff into dev Oct 24, 2026
25 of 26 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/6651-viewer-parity branch October 24, 2026 08:54
@github-actions github-actions Bot mentioned this pull request Oct 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant