Pool render data nodes to reduce per-frame GC pressure#20885
Pool render data nodes to reduce per-frame GC pressure#20885ZehMatt wants to merge 3 commits intoAvaloniaUI:masterfrom
Conversation
|
You can test this PR using the following package version. |
|
|
@cla-avalonia agree |
|
Note that this would mean that the memory used by nodes is never reclaimed. We probably need some opt-in flag for that or employ a strategy similar to one used in batch stream pools where we collect item usage statistics and release unused items on timer tick. This will probably require a specialized pool implementation. ThreadSafeObjectPool is rather simple and is designed for types that won't really have lots of instances. |
|
(not a suggestion to go and rewrite everything) We could also use WPF's approach where it doesn't use proper nodes and instead serializes operations into binary representation: https://github.com/dotnet/wpf/blob/5599cc923d6a464ea0afc7864e722a7b57ab4281/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Media/Generated/RenderDataDrawingContext.cs#L55-L69. So instead of lots of individual drawing operation nodes our render data would consists of a |
Would you like me to use BatchStreamPoolBase instead of ThreadSafeObjectPool? |
I wouldn't mind doing this also, since I use Avalonia now in one of my projects I have personal interest in making this as smooth as possible, the project simulates thousands of things and renders them which is how I ran into this issue also. |
|
I've updated the implementation. After experimenting with |
|
You can test this PR using the following package version. |
|
You can test this PR using the following package version. |
What does the pull request do?
Adds object pooling for all
IRenderDataItemnode types created byRenderDataDrawingContext. Instead of allocating new node objects every frame, nodes are returned to aRenderDataNodePool<T>after the server-side render data is consumed and reused on subsequent frames. Pools automatically reclaim unused items during idle periods via a single shared cleanup timer.Related to #19363
What is the current behavior?
Every draw call (
DrawRectangle,DrawLine,DrawGlyphRun,PushClip,PushTransform, etc.) allocates a newIRenderDataItemnode vianew. These nodes are created on the UI thread, serialized to the render thread, consumed once, then abandoned to the GC. In high-frequency rendering scenarios (e.g. 26K+ rectangles per frame), this creates significant GC pressure and frame stutter.What is the updated/expected behavior with this PR?
After the first frame, all render data nodes are served from per-type object pools. Nodes are returned to pools when
ServerCompositionRenderData.Reset()orCompositionRenderData.Dispose()runs. Per-frame allocations of render data nodes drop to near zero. When rendering activity stops, pooled items are gradually released (~1/3 per second) so memory is reclaimed during idle periods.How was the solution implemented (if it's not obvious)?
IPoolableRenderDataIteminterface with aReturnToPool()methodRenderDataItemPoolHelper.DisposeAndReturnToPool()which recurses into push node children, then either returns poolable items to their pool or disposes non-poolable itemsRenderDataNodePool<T>— a lightweight array-backed object pool with idle-based reclamation. All pool instances register withRenderDataNodePoolCleanupwhich runs a single sharedSystem.Threading.Timer. During active use the pool retains all items for reuse; once idle,Reduce()gradually releases a third of excess items per cycle. Pools are tracked via weak references so they can be garbage collected if no longer referenced.RenderDataNodePool<T>, aGet()factory method, and aReturnToPool()that resets state and returns to pool. For nodes with disposable resources (GlyphRun,Bitmap,CustomOperation),ReturnToPool()callsDispose()before returningRenderDataDrawingContextusesNodeType.Get()instead ofnew NodeTypePooled node types:
RenderDataRectangleNode,RenderDataEllipseNode,RenderDataLineNode,RenderDataGeometryNode,RenderDataGlyphRunNode,RenderDataBitmapNode,RenderDataCustomNode,RenderDataClipNode,RenderDataPushMatrixNode,RenderDataOpacityNode,RenderDataOpacityMaskNode,RenderDataGeometryClipNode,RenderDataRenderOptionsNode,RenderDataTextOptionsNodeBreaking changes
None. Internal classes only, no public API changes.
Obsoletions / Deprecations
None.
Fixed issues
Addresses some points of #19363 in regards to rendering.