Apply optimizations in Avalonia Android#20968
Conversation
|
You can test this PR using the following package version. |
| if(Input != null) | ||
| { | ||
| var args = new RawTextInputEventArgs(AndroidKeyboardDevice.Instance!, (ulong)DateTime.Now.Ticks, InputRoot!, text); | ||
| var args = new RawTextInputEventArgs(AndroidKeyboardDevice.Instance!, (ulong)SystemClock.UptimeMillis(), InputRoot!, text); |
There was a problem hiding this comment.
I highly doubt going to java-interop monstroucity for SystemClock.UptimeMillis is better here.
Shouldn't we use Stopwatch instead, as .NET equvivalent?
There was a problem hiding this comment.
Also, we should use the same source everywhere so that timestamps are comparable.
(For a given platform. They don't have to be cross-platform.)
There was a problem hiding this comment.
Note: the matching benchmark does use Stopwatch.
There was a problem hiding this comment.
Good points.
C# → JNI → native clock_gettime
Android numbers (100,000 iterations on emulator):
| Approach | Time | Per-call |
|---|---|---|
| DateTime.Now.Ticks | 60 ms | 600 ns |
| SystemClock.UptimeMillis() | 57 ms | 570 ns |
| Environment.TickCount64 | 2 ms | 20 ns |
| Stopwatch.GetTimestamp() | 4 ms | 40 ns |
Key findings:
SystemClock.UptimeMillis()is essentially almost same speed asDateTime.Now.Tickson Android. The JNI overhead is real.Environment.TickCount64is 29x faster than both.Stopwatch.GetTimestamp()is 15x faster than both.
Used SystemClock.UptimeMillis() to keep timestamps comparable with KeyEvent.EventTime and MotionEvent.EventTime, which both use SystemClock.uptimeMillis() as their clock source. Using a different .NET clock (Environment.TickCount64 or Stopwatch) would produce timestamps on a different epoch that doesn't pause during deep sleep, causing drift.
The benchmark uses Stopwatch.GetTimestamp() as a desktop proxy since SystemClock is Android-only.
There was a problem hiding this comment.
On CoreCLR UtcNow is faster than Now because time zone adjustments are skipped. That might end up being faster than UptimeMillis.
There was a problem hiding this comment.
Are we discussing performance of something that takes nanoseconds to execute and is called like 3 times per second at most?
There was a problem hiding this comment.
The event should use the same timestamp source as the rest of input events, please check what matches MotionEvent.eventTime and use it there.
There was a problem hiding this comment.
Android's KeyEvent and MotionEvent timestamps use the SystemClock.uptimeMillis() time base, and the existing Android raw input paths already use that same clock base via EventTime/UptimeMillis. I changed TopLevelImpl.TextInput to use SystemClock.UptimeMillis() as well so it stays consistent with the rest of the Android input events.
|
You can test this PR using the following package version. |
Android Benchmark Results (.NET 10.0.0, ARM64 emulator)Ran the benchmarks on an actual Android device to get real numbers including JNI overhead. Each benchmark runs 10,000 iterations after 100 warmup iterations. DispatchDraw: Paint allocation
Dispatcher: lock vs Interlocked
Timestamp sources
The original code ( Accessibility: LINQ vs foreach
Keyboard: char conversion vs cached
Regarding the desktop benchmark file ( |
So we should remove it from other input events too. |
|
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?
Applies performance optimizations to the Avalonia Android backend, reducing GC pressure and lock contention on the UI thread during rendering, input dispatch, and accessibility operations.
What is the current behavior?
Some Android hot paths use heavier synchronization than needed:
DispatchDrawcreates a newPaintobject every frame (24 KB/1000 frames)AndroidDispatcherImpl.Signal()useslockfor a simple boolean flagTopLevelImpl.TextInputusesDateTime.Now.Ticks(wrong unit)OnPerformActionForVirtualViewuses LINQ.Select().Aggregate()(72 KB/1000 calls)AndroidKeyboardEventsHelpercallschar.ToString()on every key event (24 KB/1000 events)What is the updated/expected behavior with this PR?
Applied changes to optimize while preserve behavior with measurable improvements.
Measured on Android, 10,000 iterations after 100 warmup:
char.ConvertFromUtf32→ cachedTextInput timestamp change (
DateTime.Now.TickstoSystemClock.UptimeMillis()) is a fix, not a performance optimization. The original code used the wrong unit (100ns ticks instead of milliseconds) and a non-monotonic clock.How was the solution implemented (if it's not obvious)?
TopLevelImpl.DispatchDraw: Cache thePaintinstance as a field. Properties never change after init andDispatchDrawruns on the UI thread only.AndroidDispatcherImpl.Signal/OnSignaled/OnIdle: Replacelock+bool _signaledwithint _signaledusingInterlocked.CompareExchange/Interlocked.Exchange/Volatile.Read. The flag is a simple set/reset gate with no compound critical section, making lock-free operations a good eplacement.TopLevelImpl.TextInput: ReplaceDateTime.Now.TickswithSystemClock.UptimeMillis(). This matches the same epoch and unit used byKeyEvent.EventTimeandMotionEvent.EventTimethroughout the Android input pipeline, making text input timestamps directly comparable to pointer and keyboard timestamps.AvaloniaAccessHelper.OnPerformActionForVirtualView: Replace.Select().Aggregate()with aforeachloop and an explicitnullcheck. Eliminates iterator and delegate allocations.AndroidKeyboardEventsHelper: Pre-cachechar.ToString()for ASCII range 0-127 in a static array. Non-ASCII falls through tochar.ConvertFromUtf32()unchanged.Checklist
Benchmarks added in
tests/Avalonia.Benchmarks.Android/to validate each optimization.Breaking changes
None.
Obsoletions / Deprecations
None.