Skip to content

WPF Availability Groups tab rebuilds every card on each 30 s refresh: 10 k realized elements (42 AGs, no virtualization) stall the UI thread ~2.5 s per tick #4238

Closed

Description

@erikdarlingdata

Problem

The WPF Viewer's Availability Groups tab rebuilds its whole visual tree on every refresh, on the UI thread.

  • AvailabilityGroupsTab.Render assigns a new list: AgCards.ItemsSource = groups.
  • AgCards is a plain ItemsControl inside a ScrollViewer. It doesn't virtualize, so every card is realized whether or not it's on screen.
  • Each card holds a replica WrapPanel and a per-database DataGrid, capped at MaxHeight="260", which shows up to about 10 rows.
  • The shell's fleet timer (NocRefreshIntervalSeconds, 30 s by default) calls RefreshAgAsync while the tab is visible. So every tick tears down and rebuilds every card and every DataGrid, even when the topology hasn't changed, which is the normal case.

The store read is #4228. This issue covers what the viewer does with the rows once it has them.

Measured

Production SQL Server store A, 2026-09-25. The Viewer ran on the store host in an interactive session and was driven through UI Automation.

  • The topology is 42 AGs, 84 replicas, 398 database rows.
  • UI-thread responsiveness was probed with SendMessageTimeout(WM_NULL) once a second.
measure AG tab visible other main tabs
UI elements realized 10,353 19–1,032
longest UI-thread stall 2,548 ms ≤ 47 ms (every main tab and all 19 per-server tabs, ≤ 85 ms)
Viewer CPU per refresh ~3.5–3.9 s (bursts at 1–3 s, 17–19 s and 44–45 s of a 45 s sample) 0 at idle
  • When the tab is left open, the Viewer stops responding for about 2.5 s every 30 s. Clicks, scrolling and typing all queue behind the rebuild.
  • It's the only surface in the Viewer that stalls the UI thread measurably.
  • A settle-detection pass that walked the full automation tree never saw it settle within 90 s. That pass cost 100 s of CPU and grew the working set from 249 MB to 771 MB. Some of that is the walk itself, since realizing automation peers for 10 k elements is expensive. Screen readers and UI test tools pay the same cost.

Where (origin/dev)

  • Darling/PerformanceMonitor.Darling.Viewer/AvailabilityGroupsTab.xaml:
    • CardScroller ScrollViewer ~:41 and the AgCards ItemsControl ~:42 (no virtualizing panel);
    • the replica ItemsControl / WrapPanel ~:70;
    • the per-database DataGrid ~:106.
  • Darling/PerformanceMonitor.Darling.Viewer/AvailabilityGroupsTab.xaml.cs: LoadAsync ~:56 and Render ~:80 (AgCards.ItemsSource = groups).
  • Darling/PerformanceMonitor.Darling.Viewer/MainWindow.xaml.cs: the timer path LoadVisibleTabAsync into RefreshAgAsync ~:1104.

Fix shape

  1. Don't rebuild when nothing changed. Keep an ObservableCollection<AgTopologyCard> keyed by AG identity and update each card's replica and database values in place. Add or remove cards only when the topology changes. At minimum, compare a digest of the new rows with the previous render and skip Render when they match.
  2. Virtualize the card list. Replace the ItemsControl + ScrollViewer with a ListBox or ItemsControl using VirtualizingStackPanel (VirtualizingPanel.IsVirtualizing="True", VirtualizationMode="Recycling", ScrollUnit="Pixel") and ScrollViewer.CanContentScroll="True", so only cards on screen are realized.
  3. Optionally, collapse healthy cards by default: show a one-line summary and expand to the DataGrid on demand. Only unhealthy AGs need the per-database grid open.

Pins:

  • a Viewer test that two refreshes with identical rows leave the realized card containers as the same instances (no rebuild);
  • a XAML pin that AgCards uses a virtualizing panel;
  • a rig timing with a 40+ AG seeded topology asserting the refresh's UI-thread stall stays under 100 ms.

Activity

  1. added
    enhancementNew feature or request
    client-siteOwned by the client-site agents (other laptop). Local sessions never pick these up.
    on Sep 25, 2026
  2. erikdarlingdata commented on Sep 25, 2026

    @erikdarlingdata
    OwnerAuthor

    Ruling for the fix lane:

    1. Fix shapes 1 and 2 both go in.
      • The tab keeps one collection of cards, keyed by AG identity. A refresh updates each card's values in place. It adds or removes a card only when the topology changes.
      • When the new rows match the last render, the refresh skips Render.
      • The card list virtualizes: a VirtualizingStackPanel with recycling and pixel scrolling, and ScrollViewer.CanContentScroll="True".
    2. Fix shape 3, collapsing healthy cards, is a change to what users see. It stays out of this fix.
    3. Tests:
      • two refreshes with the same rows keep the same card containers;
      • a XAML check that AgCards uses a virtualizing panel;
      • a timing test over a 42-AG topology (84 replicas, 398 database rows). A refresh with unchanged rows must stay under 100 ms on the UI thread. The PR gives the time before and after.
    4. The lane checks whether Lite shows availability groups the same way. If it does, Lite gets the same fix. If not, the PR says why.

    🤖 Generated with Claude Code

    https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3

  3. added
    in-progressActively being worked by a local session or its agents (PR open or in flight)
    on Sep 25, 2026
  4. erikdarlingdata commented on Sep 25, 2026

    @erikdarlingdata
    OwnerAuthor

    Fixed by #4308, merged to dev (c88fa48).

  5. removed
    in-progressActively being worked by a local session or its agents (PR open or in flight)
    on Sep 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    client-siteOwned by the client-site agents (other laptop). Local sessions never pick these up.enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions