Problem Overview
I'm building a real-time UI (a timeline-based audio editor) in Avalonia 11.3.2 and targeting 60–144 fps animations. However, I'm running into noticeable frame stutter, which profiling shows is due to GC activity caused by small per-frame allocations in Avalonia APIs.
Even with GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency, the GC still unpredictably collects, leading to visible jitter in UI animations.
Application Context
- Per-frame rendering is done using
DrawingContext.DrawText, DrawRect, and DrawLine.
- I've tried both rendering loops:
DispatcherTimer.Tick += OnTick
- A custom
Stopwatch loop using Dispatcher.UIThread.Post(...)
- UI controls like
Slider, ScrollBar, and TextBlock participate in two-way bindings for timeline state.
- All of the above create per-frame allocations (from formatting, layout, bindings, boxed events, etc).
GC Allocation Hotspots
-
Text rendering via DrawText:
- Creates
SKTextBlob and other objects on every frame.
-
Binding system:
- Two-way bindings allocate
AvaloniaPropertyChangedEventArgs.
-
Dispatcher use:
Dispatcher.UIThread.Post or DispatcherTimer allocates per invocation.
- This is more minimal and can be acceptable.
Request
This issue is intended to highlight a common pattern of allocations for apps requiring per-frame updates. While some of these allocations are minor, they still lead to unpredictable GC behavior and (even Gen0 collections) create noticeable stutters. I understand my use case can be niche but it would be helpful to optimize and add more support for high-framerate app.
Solution
- DrawText/DrawRect/DrawLine
- Expose methods that takes in required object for rendering like
SKTextBlob, so they can be cached in ViewModels.
- Dispatcher
- Not sure if this is changeable, allocation on this one is minimal though.
- Two-Way Binding
- Cache an
AvaloniaPropertyChangedEventArgs in control and reuse.
Notes
I have also tried using an ICustomDrawOperation with
public void Render(ImmediateDrawingContext context)
{
var leaseFeature = context.TryGetFeature<ISkiaSharpApiLeaseFeature>();
if (leaseFeature is not null)
{
using var lease = leaseFeature.Lease();
lease.SkCanvas.DrawText(blob, 0, (float)Position.Y, paint);
}
}
to replace DrawText, while this does eliminate allocation, I cannot get good result with it. The draw methods in Avalonia.Media give much smoother animation.
Problem Overview
I'm building a real-time UI (a timeline-based audio editor) in Avalonia 11.3.2 and targeting 60–144 fps animations. However, I'm running into noticeable frame stutter, which profiling shows is due to GC activity caused by small per-frame allocations in Avalonia APIs.
Even with
GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency, the GC still unpredictably collects, leading to visible jitter in UI animations.Application Context
DrawingContext.DrawText,DrawRect, andDrawLine.DispatcherTimer.Tick += OnTickStopwatchloop usingDispatcher.UIThread.Post(...)Slider,ScrollBar, andTextBlockparticipate in two-way bindings for timeline state.GC Allocation Hotspots
Text rendering via
DrawText:SKTextBloband other objects on every frame.Binding system:
AvaloniaPropertyChangedEventArgs.Dispatcher use:
Dispatcher.UIThread.PostorDispatcherTimerallocates per invocation.Request
This issue is intended to highlight a common pattern of allocations for apps requiring per-frame updates. While some of these allocations are minor, they still lead to unpredictable GC behavior and (even Gen0 collections) create noticeable stutters. I understand my use case can be niche but it would be helpful to optimize and add more support for high-framerate app.
Solution
SKTextBlob, so they can be cached in ViewModels.AvaloniaPropertyChangedEventArgsin control and reuse.Notes
I have also tried using an
ICustomDrawOperationwithto replace DrawText, while this does eliminate allocation, I cannot get good result with it. The draw methods in Avalonia.Media give much smoother animation.