Feature/multi query stores dashboard overview#311
Feature/multi query stores dashboard overview#311erikdarlingdata merged 9 commits intoerikdarlingdata:devfrom
Conversation
… open the first line by default
…ary of what was created: New Files: 1. QueryStoreOverviewModels.cs — Models for: • QueryStoreState enum (Off, ReadOnly, ReadWrite) • DatabaseQueryStoreState — state per database • DatabaseMetrics — aggregated metrics (total + avg) per database • DatabaseTimeSlice — time slice data tagged by database • DatabaseWaitCategoryTimeSlice — wait stats tagged by database 2. QueryStoreOverviewService.cs — Parallel data fetching with: • SemaphoreSlim throttling (default DOP=8) • ConcurrentBag<T> for thread-safe result collection • Methods: FetchAllStatesAsync(string, int, CancellationToken), FetchAllMetricsAsync(string, List<string>, DateTime, DateTime, int, CancellationToken), FetchAllTimeSlicesAsync(string, List<string>, int, int, CancellationToken), FetchAllWaitStatsAsync(string, List<string>, DateTime, DateTime, int, CancellationToken) 3. QueryStoreOverviewControl.axaml — Layout with 3 rows: • Row 1: Donut chart + consolidated time slicer + consolidated wait stats ribbon • Row 2: 7 bar chart cards (Total metrics) • Row 3: 7 bar chart cards (Avg metrics) 4. QueryStoreOverviewControl.axaml.cs — Code-behind with: • Donut chart (RW=light blue, RO=dark blue, OFF=grey, center shows active/total) • Consolidated time slicer (30-day, 24h default selection) • Consolidated wait stats ribbon (sum across databases) • Top-N bar cards with consistent database colors, adaptive font color, tooltips, and right-click "Drill Down to DB Query Store" context menu Modified Files: 5. QuerySessionControl.axaml — Added "QS Overview" button 6. QuerySessionControl.axaml.cs — Added QueryStoreOverview_Click(object?, RoutedEventArgs) handler that opens the overview tab and wires drill-down to open single-DB Query Store tabs
…ownEventArgs containing Database, StartUtc, and EndUtc. The session control calls grid.SetInitialTimeRange() before the grid auto-fetches, so the drilled-down Query Store tab starts with the same time range selected in the overview. 2. Progress bar: Added an indeterminate ProgressBar at the top of the overview. It shows during LoadAsync() (all 3 phases) and during RefreshMetricsAndWaitStatsAsync(CancellationToken) (when the slicer range changes), and hides when complete via try/finally.
|
Arf... sorry for the conflicts! i may have done a mistake when branching... but hard to know where and what to do with them! |
Resolves conflicts after dev's CRLF->LF normalization (erikdarlingdata#307) and the QueryStoreGridControl GroupBy default change (erikdarlingdata#305, erikdarlingdata#306). The only real conflict was leftover blank lines in QueryStoreGridControl.axaml.cs where erikdarlingdata#306 collapsed whitespace after removing ExpandRowRecursive.
erikdarlingdata
left a comment
There was a problem hiding this comment.
Code Review
Overview
Adds a cross-database Query Store overview dashboard (donut + slicer + wait-stats ribbon + 14 metric bar cards) with right-click drill-down into the per-DB Query Store tab carrying the time range. ~1,524 lines, 7 files. Compiles clean against dev after the merge.
What's good
- Parallelism is well-bounded —
SemaphoreSlim(maxDop=8)+ConcurrentBagfor fan-out across N user databases. Cancellation propagates through toOpenAsync/ExecuteReaderAsync. - All SQL is parameterized (
SqlParameterfor@start,@end,@daysBack); database names come fromsys.databases, not user input; per-DB connection strings are built viaSqlConnectionStringBuilder. Clean on the security side. FetchAllWaitStatsWithErrorsAsyncis the right pattern — captures per-DB errors withwhen (ex is not OperationCanceledException)instead of swallowing them._supportsWaitStatsgating + descriptive fallback for SQL 2016/older.- Drill-down API on
QueryStoreGridControl.SetInitialTimeRangeis small and explicit.
Bugs / correctness
-
Dead code —
FetchDatabaseWaitStatsAsync(QueryStoreOverviewService.cs:332-373) andDatabaseWaitCategoryTimeSlice(QueryStoreOverviewModels.cs:66-73) have no callers. OnlyFetchDatabaseWaitAmountAsyncis used. Delete both. -
Errors swallowed silently —
QueryStoreOverviewService.cs:109and:143use barecatch { /* skip */ }, which will eatOperationCanceledExceptiontoo and defeat cancellation. Apply the samewhen (ex is not OperationCanceledException)pattern used inFetchAllWaitStatsWithErrorsAsync. -
Permission errors look like "Off" —
FetchAllStatesAsync:37-40maps any per-DB exception toQueryStoreState.Off, so a missingVIEW DATABASE STATEgrant looks identical to QS being disabled in the donut. At minimum log/surface; ideally add anUnknown/Errorstate. -
_ctsleak —LoadAsync:95-96andOnSlicerRangeChanged:424-425cancel the old token source but neverDispose()it. Add_cts?.Dispose()between cancel and reassign, and dispose on control detach. -
Race in
DrawWaitStatsChart— line 477var topDbs = _dbColorMap.Keys.ToList();depends onDrawBarCardshaving populated_dbColorMapfirst. The normal call order does this, butSizeChanged(constructor:84-88) callsDrawWaitStatsChartindependently — if a resize fires betweenLoadAsyncPhase 1 and Phase 3, every wait bar gets bucketed intoOthers. Defer the wait chart, or computetopDbsfrom_metricsdirectly. -
Misnamed field —
WaitRatioonDatabaseWaitAmountTimeSlice(andDatabaseWaitCategoryTimeSlice) is wait time in hours (SUM(total_query_wait_time_ms) / 3,600,000), not a ratio. Rename toWaitHours/WaitAmountHoursto match the SQL andWaitRatioFormattersemantics elsewhere.
Style / conventions
- Tab vs space inconsistency —
QueryStoreOverviewService.cs:375-411(FetchDatabaseWaitAmountAsync) is tab-indented while every other method is 4-space. Same inQueryStoreOverviewModels.cs:82(WaitRatioonDatabaseWaitAmountTimeSlice). Will look broken in any editor without tab-width=4. - Trailing blank lines in
QueryStoreOverviewModels.cs:85-87. _dbColorMapis declared mid-file (line 630) instead of with the other private fields (lines 31-39).if (db != "Others")on line 742 — extractprivate const string OthersLabel = "Others"and reuse.- Repeated
Color.Parse("#E4E6EB")is already available asDynamicResource ForegroundBrushin XAML; resolve once or useFindResource.
Performance (minor)
topDbs.Contains(item.Db)inside the hour loop (:511) and per-metric loop (:699) —List<string>.Containsis O(n). Use aHashSet<string>since the list is small but checked many times per redraw.SizeChangedredraws everything; rapid resizing will thrash. A small debounce would help — not blocking.
Tests
No tests added for ~1,500 lines of new code (PR checklist has Tests unchecked). QueryStoreOverviewService is a static class with discrete query methods — at minimum, table-driven tests against a real DB would catch column-order regressions in the SQL.
SQL notes
WHERE database_id > 4correctly excludes system DBs.READ UNCOMMITTEDon QS DMVs is fine and conventional.bucket_hour = DATEADD(HOUR, DATEDIFF(HOUR, 0, rsi.start_time), 0)truncates to hour-of-epoch in server local time (becauseDATEDIFF(HOUR, 0, ...)is naïve). The result is thenSpecifyKind(..., Utc)on the C# side, which mislabels it on a non-UTC server. Either compute UTC-truncation server-side or stop forcingUtckind when the SQL hasn't promised it. Worth verifying against a non-UTC server before merge.
Verdict
Functionally solid, security-clean, well-bounded async. Blockers are small: the dead code, the silent catches eating cancellation, and the bucket-hour timezone question. The rest is polish (indentation, naming, dispose). Recommend addressing #1–6 before merge; style/perf items can be follow-ups.
…seWaitCategoryTimeSlice (no callers). 2. Bare catch blocks fixed — All 4 parallel fetch methods now use when (ex is not OperationCanceledException) or when (!ct.IsCancellationRequested) so cancellation propagates correctly. 3. Permission errors surfaced — Added QueryStoreState.Error enum value and ErrorMessage property to DatabaseQueryStoreState. The donut now shows a red "Error" segment, and clicking it lists databases with their error messages. 4. _cts leak fixed — Added _cts?.Dispose() before every reassignment in LoadAsync() and OnSlicerRangeChanged(object?, TimeRangeChangedEventArgs), plus a DetachedFromVisualTree handler that cancels and disposes on control teardown. 5. SizeChanged race fixed — DrawWaitStatsChart() now returns early if _dbColorMap is empty, preventing all bars from being bucketed into "Others" when a resize fires before DrawBarCards() has run. 6. Misnamed field renamed — WaitRatio → WaitAmountHours on DatabaseWaitAmountTimeSlice and all references in the service and control.
|
|
@erikdarlingdata : thanks for the merge from dev :-) |
Multi-Query Store Overview Dashboard
Summary
Adds a new Query Store Overview dashboard that provides a consolidated, cross-database view of all Query Store-enabled databases on a SQL Server instance. This gives users a single pane of glass to compare workload metrics across databases before drilling down into individual database Query Store details.
Features
Overview Dashboard Layout
TimeRangeSlicerControlwith data aggregated across all databases, allowing users to select a time range that filters all dashboard panelsDrill-Down
SetInitialTimeRange()Data Pipeline
QueryStoreOverviewServicefetches states, time slices, metrics, and wait stats across all user databases in parallel usingSemaphoreSlim(DOP=8)FetchAllWaitStatsWithErrorsAsynccaptures per-database errors instead of silently swallowing them, surfacing failures to the user in a dialog windowUX
supportsWaitStatsflag (SQL 2017+ / Azure) is checked before attempting wait stats queries, with a descriptive fallback message when unsupportedISNULL,reader.IsDBNullguards) andreader.FieldCountchecks to handle databases returning varying result shapesNew Files
QueryStoreOverviewControl.axamlQueryStoreOverviewControl.axaml.csModified Files
QueryStoreOverviewService.csFetchAllStatesAsync,FetchAllMetricsAsync,FetchAllTimeSlicesAsync,FetchAllWaitStatsAsync,FetchAllWaitStatsWithErrorsAsyncQueryStoreOverviewModels.csDatabaseQueryStoreState,DatabaseMetrics,DatabaseTimeSlice,DatabaseWaitAmountTimeSlice,DatabaseWaitCategoryTimeSliceQuerySessionControl.axaml.csDrillDownRequestedwiring,OpenQueryStoreForDatabaseAsyncnow accepts optionalinitialStartUtc/initialEndUtcQueryStoreGridControl.axaml.csSetInitialTimeRange()for pre-setting the slicer range on drill-downTechnical Notes
topN=4by default) get distinct palette colors; remaining databases are aggregated as "Others" in grey#2EAEF1(blue),#F2994A(orange),#27AE60(green),#9B51E0(purple),#EB5757(red),#F2C94C(yellow),#56CCF2(light blue),#BB6BD9(violet)Which component(s) does this affect?
How was this tested?
Describe the testing you've done. Include:
Checklist
dotnet build -c Debug)dotnet test)