Repository navigation
AG tab refreshes cards in place and virtualizes the list (#4238) - #4308
Merged
Merged
Conversation
The Darling viewer's Availability Groups tab tore down and rebuilt every card and every per-database DataGrid on each 30 s refresh, on the UI thread, with no virtualization (10,353 realized elements and a 2,548 ms stall on a 42-AG production topology). AgTopologyCard/Replica/Database (PerformanceMonitor.Common) now raise PropertyChanged and expose UpdateFrom, so a refresh can update an existing card's values in place instead of replacing it. AgTopology gains a shared Reconcile helper (keyed by (reporting server, AG) for cards, and reused for a card's nested replica/database rows) and a ComputeDigest fingerprint so a refresh whose rows hash the same as the last render skips reconciling and re-binding entirely. The tab now binds AgCards to one long-lived ObservableCollection<AgTopologyCard> instead of assigning a fresh List every refresh. AgCards.xaml moves its ScrollViewer inside the ItemsControl's own template (CanContentScroll="True" around an ItemsPresenter) and sets VirtualizingStackPanel with Recycling/Pixel virtualization, so only on-screen cards realize their Border/replica-chip/DataGrid subtree. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
Same-instance pin across two identical-row refreshes (fails on the old ItemsSource-replace code, proven once before this fix landed), a changed-value-updates-in-place pin, an added/removed-AG pin, a XAML pin that AgCards uses a virtualizing panel, and a timing pin over the production shape (42 AGs, 84 replicas, 398 database rows) asserting an unchanged-rows refresh stays under 100 ms on the UI thread. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
Lite's Availability Groups tab had the identical defect: AgCards.ItemsSource = cards, a brand new list of brand new AgTopologyCard instances on every refresh, no virtualization. It uses the same shared mechanism the Darling viewer's tab now uses (AgTopology.Reconcile / ComputeDigest / UpdateFrom), plus the same virtualizing-panel XAML change. Pinned with Lite's own same-instance, XAML and timing tests; the shared reconcile/digest behavior itself is pinned once, in Darling.Tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #4238.
Why
The Darling viewer's Availability Groups tab tore down and rebuilt every card and every per-database
DataGridon each 30 s refresh, on the UI thread. Nothing was virtualized.Renderassigned a brand newList<AgTopologyCard>toAgCards.ItemsSourceevery time, made of brand new card instances, whether or not anything had changed. The card list was a plainItemsControlinside aScrollViewer, so every card realized whether or not it was on screen. #4238 measured a production topology of 42 AGs, 84 replicas and 398 database rows. There, each refresh realized 10,353 elements and stalled the UI thread for 2,548 ms.This PR's pin tests were written first and run against the code before the fix, to confirm the cause. 4 of the 5 new tests failed. One of them checks that two refreshes with the same rows keep the same card instances. So the old code replaced every card object on every refresh, whether or not its data changed.
What changes
PerformanceMonitor.Common/AgTopology.csis shared by the Darling viewer and Lite. A search of the whole repo found that the web dashboard does not use these types.AgTopologyCard,AgTopologyReplica, andAgTopologyDatabasenow implementINotifyPropertyChanged, through a small sharedAgTopologyObservablebase. It stays plain BCL, no WPF dependency. Each type now has anUpdateFrommethod. It copies every field from a newly read instance with the same identity, then raises one change notification for all properties.AgTopology.Reconcile<TItem, TKey>is new. It syncs anObservableCollection<TItem>to a freshly read list, in the fresh list's order, and reuses existing instances (throughUpdateFrom) for every identity that survives. It adds new instances and removes stale ones. It is reused at three levels: a tab's top-level card list, and, insideAgTopologyCard.UpdateFrom, a card's own nestedReplicasandDatabasescollections. That gives one reconciliation rule instead of three near-identical ones.AgTopology.CardKeyis new: a card's identity is its reporting server plus its normalized AG name, the same pairBuildCardsalready groups by.AgTopology.ComputeDigestis new: a fingerprint of every field a card renders, including both grains' collection times. A refresh whose digest matches the last render skips the update.BuildCardsbehavior is unchanged. Only its internal construction of a card changed, becauseReplicas/Databasesare now get-onlyObservableCollections populated withAdd, not an object-initializer assignment.Darling viewer,
AvailabilityGroupsTab.xamland.xaml.cs:AgCardsnow virtualizes. ItsScrollViewermoved inside theItemsControl's ownControlTemplate, wrappingCanContentScroll="True"around anItemsPresenter, with aVirtualizingStackPanel(IsVirtualizing="True",VirtualizationMode="Recycling",ScrollUnit="Pixel"). AScrollViewerwrapped around a plainItemsControlfrom outside gives the panel unbounded height and never actually virtualizes. The fix had to move theScrollViewerinside the template, not just add virtualization properties to the old structure. The namedCardScrollerelement is gone. Its visibility toggle now targetsAgCardsdirectly.ObservableCollection<AgTopologyCard>, bound toAgCards.ItemsSourceexactly once, in the constructor.LoadAsynccomputes a digest of the freshly read rows and returns early when it matches the last render.Renderreconciles that collection in place throughAgTopology.Reconcile, instead of replacingItemsSource.Renderis nowinternal, mirroring the existingBuildSummaryprecedent, so tests can drive it directly without a live store.Lite,
Lite/Controls/AvailabilityGroupsTab.xamland.xaml.cs: Lite shows Availability Groups the same way, with the same defect:AgCards.ItemsSource = cardswholesale, a plainItemsControlin aScrollViewer. It gets the same fix, the same sharedAgTopology.Reconcile/ComputeDigest/UpdateFrommechanism, and the same XAML virtualization change.Tests:
Darling/Darling.Tests/AvailabilityGroupsTabRefreshTests.cs(new) andLite.Tests/AvailabilityGroupsTabRefreshTests.cs(new). Details below.Test plan
Darling.Tests -class "*AvailabilityGroupsTabRefreshTests*": 4 of 5 failed. The same-rows-same-instance, changed-value-updates-in-place, add/remove-one-card, and XAML virtualizing-panel pins all failed against the oldItemsSource =code. Only the timing pin passed. The note below explains why.dotnet build Darling/Darling.Tests/Darling.Tests.csproj: 0 Warning(s), 0 Error(s).Darling.Tests -class "*AvailabilityGroupsTabRefreshTests*" -class "*AgTopologyCardsTests*" -class "*RawWindowFloorViewerPortTests*". 34 total, 0 failed.dotnet build Lite.Tests/Lite.Tests.csproj: 0 Warning(s), 0 Error(s).Lite.Tests -class "*AvailabilityGroupsTabRefreshTests*" -class "*AgTopologyTests*" -class "*AgAvailabilityGroupNewestSnapshotEqualityTests*". 30 total, 0 failed.Lite.Tests.exe, no filter. 5,392 total, 0 failed, 0 errors, 314.2 s.PerformanceMonitor.Commonmodel file that no live or store test reads. Only the pure banding/projection tests read it, and those run in the classes above. CI runs the rest.f06b9b2d: build, Darling PostgreSQL tests and Lite tests all pass.A note on the timing number: the pin runs headless, with no
Measure/Arrange/UpdateLayout. It never makes WPF realize the visuals, so it passed even on the code before the fix. Without a layout pass, neither version pays the cost of creating containers, which caused the 2,548 ms that #4238 measured in the field. The 100 ms budget is still a real, permanent regression guard on the reconcile/digest code path. The object-identity pins are what actually prove the rebuild-on-every-refresh defect is gone: they fail before this fix and pass after it.CHANGELOG entry
SECTION: Fixed
ENTRY:
REF:
[AG tab refreshes cards in place and virtualizes the list (#4238) #4308]: AG tab refreshes cards in place and virtualizes the list (#4238) #4308
🤖 Generated with Claude Code
https://claude.ai/code/session_01FVjn4PBJN71NQXdFo6ZxNQ