VirtualizingStackPanel - content virtualization and advanced scroll position calculation supporting varying item sizes#20993
Conversation
|
@MrJul @kekekeks I am sorry for bothering you. From my point of view, this is a necessity, not an option. Now to be fair: This PR is not complete yet. If you do not find this interesting, I will just extract out a "MyVirtualizingStackpanel" and let it live in our codebase. |
|
Hi @gentledepp, First of all, thank you for your contribution! General performance improvements are always welcome. Right now, as you might have noticed, we're in the process of shipping Avalonia v12 and won't be able to review this PR before the final release. Personally, I really like the concept of reusing content across templates. It makes a lot of sense. Assuming the implementation is good enough and well tested (and it looks like it is!), this is something I'm interested in merging, so please keep it open for now :) One thing that could really help us get this PR merged faster is to split it. For example, can all the improvements in scrolling calculation be extracted to a separate PR? That way, we could review the |
VirtualizingStackPanel can handle items with (very) different heights (no scroll jumping, no layout cycles) IVirtualizingDataTemplate implementation for ItemsControls (not only ItemContainers are re-used but also the Content)
9ecc3d3 to
2a1d9bf
Compare
What does the pull request do?
This PR implements content-level virtualization for Avalonia's
ItemsControlandVirtualizingStackPanel, delivering 10-100x performance improvements for complex data templates. It introduces the newIVirtualizingDataTemplateinterface for type-aware container recycling, enabling container + child controls to be reused as a single unit rather than destroying and recreating expensive UI elements during scrolling.Additionally, this PR includes significant improvements to
VirtualizingStackPanel's scrolling behavior for heterogeneous lists (items with varying heights), reducing measure passes from 2-3 down to 1 per scroll event and eliminating scroll jumping.Key Features:
IVirtualizingDataTemplateinterface with opt-inEnableVirtualizationproperty (I would like to get rid of this, but maybe it is good for A/b testing during the pr)PersonViewModel,TaskViewModel)DataTypesetWhat is the current behavior?
Before this PR:
Limited Recycling: Only containers (
ListBoxItem,ContentPresenter) are recycled during virtualization. The content (the expensive nested UI created by data templates) is destroyed and recreated on every scroll.High Performance Cost:
Measure/Arrangecycle on every virtualization eventPoor Scrolling Performance with Heterogeneous Lists:
Memory Issues:
What is the updated/expected behavior with this PR?
After this PR:
Container-Level Virtualization:
ProductItemcontainers only reused forProductItemdata (template compatibility guaranteed)Performance Improvements:
Measure/Arrangecycles during scrollingSmooth Scrolling:
MeasureOverridepass, subsequent calls return the previousDesiredSizeand post a deferredInvalidateMeasureatLoadedpriority, preventing tight layout loops while ensuring items eventually settle at correct sizesValidateStartU: measurement fluctuations < 1px are absorbed silently, preventing cascade invalidation from non-deterministic controls. Only genuine resizes (≥ 1px) markStartUas unstableScrollViewerfrom adjusting the offset in response to oscillating values. The frozen extent converges toward reality once measurements stabilizeStartUisNaN(unstable),CaptureViewportAnchoralways re-evaluates instead of returning a stale cached anchor — preventing positive-feedback drift inCompensateForExtentChangeItemContainerIndexChanged) instead of fullPrepareItemContainer+MeasureU=0when realized, skipped during frozen extent)CompensateForExtentChangeafterStartUre-estimation: whenValidateStartUmarksStartUas unstable and positions are re-estimated from scratch,CompensateForExtentChangeis skipped to let the scroll anchor mechanism adjust naturallyEasy Adoption:
Enable/Disable globally:
Note: I am aware that the naming
ContentVirtualizationDiagnosticsis terrible. But you may want me to delete this central way of enabling/disabling anyways.How was the solution implemented (if it's not obvious)?
Phase 1: IVirtualizingDataTemplate Interface
Created new interface for explicit content virtualization with custom recycling keys:
Implemented in
DataTemplatewithEnableVirtualizationXAML property.Phase 2: Automatic Virtualization
Templates implementing both
IRecyclingDataTemplateandITypedDataTemplatewithDataTypeset automatically benefit from pool-based recycling (default pool size: 5 controls per type).Phase 3: Container-Level Virtualization (Critical Performance Fix)
Key Insight: When virtualization is active, the child should stay attached to its container. The container + child become a single reusable unit, pooled by data type.
Implementation:
Type-Aware Container Recycling (
ItemsControl.NeedsContainerOverride):IVirtualizingDataTemplate.GetKey()for explicit recycling keysDataTypefor automatic type-safe poolingitem.GetType()for type-aware recyclingConditional Content Clearing (
ItemsControl.ClearContainerForItemOverride):Content/ContentTemplatepropertiesForced Content Update (
ItemsControl.PrepareContainerForItemOverride):SetCurrentValue(ContentProperty, item)instead ofSetIfUnsetTemplate Reuse (
ContentPresenter.CreateChild):Build()receives existing attached childnewChild == oldChildPhase 4: Smooth Scrolling Improvements
Fixed multiple measure passes during scrolling with heterogeneous items:
Eliminated Temporal Mismatch:
RealizeElements()instead of beforeSkip Redundant Re-estimation:
Direct Averaging (No Smoothing) for Larger Samples:
Preserve Estimate Tracking on Reset:
Item 0 Clipping Fix:
Layout Cycle Breaker:
DesiredSizeresults across passes, causing extent oscillation and infinite re-measurementMeasureOverridepass, subsequent calls return the previousDesiredSizeand post a deferredInvalidateMeasureatLoadedpriority (before next input/scroll event)_measurePostponedflag ensures at most one deferred measure is queued; the counter resets inArrangeOverride,OnEffectiveViewportChanged, andOnItemsChangedValidateStartUTolerance, Suppression, and Partial-State Detection:StartUas unstableValidateStartUfires once,_suppressValidateStartUprevents re-firing untilArrangeOverrideresets the flagIsMeasureValid == false), markingStartUas unstable instead of incorrectly applyingpreDeltacompensationStartUis adjusted by the accumulatedpreDeltato preserve the anchor's visual positionpreDeltacompensation during frozen extent: whenlockSizes=true, size diffs for items at or before the anchor (itemIndex <= anchorIndex) are tracked and compensated via_startU -= preDelta, keeping visible items at their current positions without invalidating the estimate cacheStale Anchor Guard:
!double.IsNaN(startU)toCaptureViewportAnchor's skip-cache conditionStartUis unstable, the anchor is always re-evaluated, preventing a positive-feedback drift loop inCompensateForExtentChangeSmart Container Reuse During Disjunct Scroll:
RetainMatchingContainers: beforeRecycleAllElements, extracts containers whoseDataContextmatches items in the estimated new viewport, stored in_retainedForReuseGetOrCreateElementintegration: checks_retainedForReusefirst; matching containers get a lightweightItemContainerIndexChangedinstead of fullPrepareItemContainerNullifyElementinRealizedStackElements: removes an element from the realized list without recycling itRecycleUnusedRetainedContainers: properly recycles any retained containers that weren't reusedImproved Anchor Estimation for Heterogeneous Heights:
U=0(skipped during frozen extent to prevent false disjunct detection)Estimate Caching:
_lastEstimateFirstIndex,_lastEstimateLastIndex); if unchanged, returns the cached estimateValidateStartUdetects a genuine resizeCompensateForExtentChangeDampening:Skip
CompensateForExtentChangeAfterStartURe-estimation:ValidateStartUmarksStartUas unstable (NaN) and positions are re-estimated from scratch,CompensateForExtentChangeis skipped — the scroll anchor mechanism adjusts the viewport naturally in a subsequent layout pass. The extent tracking (_lastMeasuredExtentU) is still updated to maintain a correct baselineNeedsContainerCheck Order Fix:item is Tcheck before the virtualization logic so items that are their own containers are not incorrectly wrappedExtent Oscillation Detection and Frozen Extent:
_frozenExtentU): locks the extent reported toScrollViewerat the pre-oscillation value, converging toward reality when the actual extent stabilizes within ±5px for 2+ consecutive passesOnItemsChangedandOnDetachedFromVisualTree_frozenExtentUaccordingly_frozenExtentUatcontentEndUPhase 5: Warmup Strategy
Pre-creates and measures containers before they're needed:
Checklist
Optional
Breaking changes
This is a fully backward-compatible enhancement with one exception:
EnableVirtualization="True"DataTypeException: If your data templates use view lifecycle events (loaded/unloaded, added/removed to/from visual/logical tree) and forward these events to their viewmodels, this behavior changes now.
Should we "fake-fire" those events for recycled controls?
Or add new events that viewmodels can listen on that work with virtualization as well?
Obsoletions / Deprecations
None.
Fixed issues
Fixes #20259
Test coverage
Added 22 new tests covering:
<=anchor)Files changed
src/Avalonia.Controls/VirtualizingStackPanel.cs— All panel-level changes (cycle breaker, frozen extent, boundary clamping, item 0 correction guard, extent oscillation detection)src/Avalonia.Controls/Utils/RealizedStackElements.cs—NullifyElement,ValidateStartUtolerance/suppression/lockSizes preDelta,CompensateStartUsrc/Avalonia.Controls/ItemsControl.cs—NeedsContainercheck ordertests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs— 22 new testssamples/ControlCatalog/Pages/ComplexVirtualizationPage.xaml— PhotoItem template with image displaysamples/ControlCatalog/ViewModels/ComplexVirtualizationPageViewModel.cs— PhotoItem image/height properties, sample image loading