Browse Source

Merge 24ed9e0752 into 658afb8717

pull/20968/merge
Javier Suárez 16 hours ago
committed by GitHub
parent
commit
4c2f0ae1c2
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 21
      src/Android/Avalonia.Android/AndroidDispatcherImpl.cs
  2. 17
      src/Android/Avalonia.Android/AvaloniaAccessHelper.cs
  3. 16
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  4. 25
      src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs
  5. 194
      tests/Avalonia.Benchmarks/Rendering/AndroidRenderingBenchmarks.cs

21
src/Android/Avalonia.Android/AndroidDispatcherImpl.cs

@ -1,5 +1,6 @@
using System;
using System.Diagnostics;
using System.Threading;
using Android.OS;
using Avalonia.Controls.Documents;
using Avalonia.Threading;
@ -19,8 +20,7 @@ namespace Avalonia.Android
private readonly Runnable _timerSignaler;
private readonly Runnable _wakeupSignaler;
private readonly MessageQueue _queue;
private readonly object _lock = new();
private bool _signaled;
private int _signaled;
private bool _backgroundProcessingRequested;
@ -46,8 +46,7 @@ namespace Avalonia.Android
public event Action? Signaled;
private void OnSignaled()
{
lock (_lock)
_signaled = false;
Interlocked.Exchange(ref _signaled, 0);
Signaled?.Invoke();
}
@ -68,13 +67,11 @@ namespace Avalonia.Android
public void Signal()
{
lock (_lock)
if (Interlocked.CompareExchange(ref _signaled, 1, 0) != 0)
{
if(_signaled)
return;
_signaled = true;
_handler.Post(_signaler);
return;
}
_handler.Post(_signaler);
}
readonly Stopwatch _clock = Stopwatch.StartNew();
@ -133,11 +130,9 @@ namespace Avalonia.Android
// "background" jobs not being processed
// So we need to examine the queue state to prevent that scenario
lock (_lock)
if (Volatile.Read(ref _signaled) != 0)
{
// There are higher priority jobs enqueued, we'll be called again
if (_signaled)
return;
return;
}
if (CanQueryPendingInput)

17
src/Android/Avalonia.Android/AvaloniaAccessHelper.cs

@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Android.OS;
using AndroidX.Core.View.Accessibility;
using AndroidX.CustomView.Widget;
@ -134,9 +134,18 @@ namespace Avalonia.Android
protected override bool OnPerformActionForVirtualView(int virtualViewId, int action, Bundle? arguments)
{
return (GetNodeInfoProvidersFromVirtualViewId(virtualViewId) ?? [])
.Select(x => TryPerformNodeAction(x, action, arguments))
.Aggregate(false, (a, b) => a | b);
var providers = GetNodeInfoProvidersFromVirtualViewId(virtualViewId);
if (providers == null)
{
return false;
}
var result = false;
foreach (var provider in providers)
{
result |= TryPerformNodeAction(provider, action, arguments);
}
return result;
}
private static bool TryPerformNodeAction(INodeInfoProvider nodeInfoProvider, int action, Bundle? arguments)

16
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@ -4,6 +4,7 @@ using Android.App;
using Android.Content;
using Android.Graphics;
using Android.Graphics.Drawables;
using Android.OS;
using Android.Runtime;
using Android.Views;
using AndroidX.AppCompat.App;
@ -144,6 +145,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
private readonly TopLevelImpl _tl;
private Size _oldSize;
private double _oldScaling;
private Paint? _clearPaint;
public SurfaceViewImpl(Context context, TopLevelImpl tl, bool placeOnTop) : base(context)
{
@ -159,11 +161,13 @@ namespace Avalonia.Android.Platform.SkiaPlatform
// can be seen below, but it does not.
if (OperatingSystem.IsAndroidVersionAtLeast(29))
{
// Android 10+ does this (BlendMode was new)
var paint = new Paint();
paint.SetColor(0);
paint.BlendMode = BlendMode.Clear;
canvas.DrawRect(0, 0, Width, Height, paint);
if (_clearPaint == null)
{
_clearPaint = new Paint();
_clearPaint.SetColor(0);
_clearPaint.BlendMode = BlendMode.Clear;
}
canvas.DrawRect(0, 0, Width, Height, _clearPaint);
}
else
{
@ -384,7 +388,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
{
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);
Input(args);
}

25
src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs

@ -10,6 +10,18 @@ namespace Avalonia.Android.Platform.Specific.Helpers
{
internal class AndroidKeyboardEventsHelper<TView> : IDisposable where TView : TopLevelImpl
{
private static readonly string[] s_asciiStringCache = InitAsciiStringCache();
private static string[] InitAsciiStringCache()
{
var cache = new string[128];
for (int i = 0; i < 128; i++)
{
cache[i] = ((char)i).ToString();
}
return cache;
}
private readonly TView _view;
public bool HandleEvents { get; set; }
@ -79,7 +91,7 @@ namespace Avalonia.Android.Platform.Specific.Helpers
AndroidKeyboardDevice.Instance!,
Convert.ToUInt64(e.EventTime),
inputRoot,
unicodeTextInput ?? Convert.ToChar(e.UnicodeChar).ToString()
unicodeTextInput ?? CharToString(e.UnicodeChar)
);
_view.Input?.Invoke(rawTextEvent);
@ -107,6 +119,15 @@ namespace Avalonia.Android.Platform.Specific.Helpers
return rv;
}
private static string CharToString(int unicodeChar)
{
if (unicodeChar >= 0 && unicodeChar < s_asciiStringCache.Length)
{
return s_asciiStringCache[unicodeChar];
}
return char.ConvertFromUtf32(unicodeChar);
}
private static string? GetKeySymbol(int unicodeChar, PhysicalKey physicalKey)
{
// Handle a very limited set of control characters so that we're consistent with other platforms
@ -126,7 +147,7 @@ namespace Avalonia.Android.Platform.Specific.Helpers
if (unicodeChar <= 0x7F)
{
var asciiChar = (char)unicodeChar;
return KeySymbolHelper.IsAllowedAsciiKeySymbol(asciiChar) ? asciiChar.ToString() : null;
return KeySymbolHelper.IsAllowedAsciiKeySymbol(asciiChar) ? s_asciiStringCache[asciiChar] : null;
}
return char.ConvertFromUtf32(unicodeChar);
}

194
tests/Avalonia.Benchmarks/Rendering/AndroidRenderingBenchmarks.cs

@ -0,0 +1,194 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using BenchmarkDotNet.Attributes;
namespace Avalonia.Benchmarks.Rendering
{
/// <summary>
/// Benchmarks for Avalonia.Android hot path patterns.
/// Run with: dotnet run -c Release -- --filter *AndroidRenderingBenchmarks*
/// </summary>
[MemoryDiagnoser]
public class AndroidRenderingBenchmarks
{
private const int FrameCount = 1000;
// DispatchDraw: new Paint() every frame vs cached (TopLevelImpl.cs)
[Benchmark]
public object? Current_DispatchDraw_NewObjectPerFrame()
{
object? last = null;
for (int i = 0; i < FrameCount; i++)
{
var obj = new DispatchDrawState();
obj.Color = 0;
obj.Mode = 1;
last = obj;
}
return last;
}
[Benchmark]
public object? Optimized_DispatchDraw_CachedObject()
{
DispatchDrawState? cached = null;
for (int i = 0; i < FrameCount; i++)
{
if (cached == null)
{
cached = new DispatchDrawState();
cached.Color = 0;
cached.Mode = 1;
}
}
return cached;
}
private sealed class DispatchDrawState
{
public int Color;
public int Mode;
}
// Dispatcher.Signal(): lock vs Interlocked (AndroidDispatcherImpl.cs)
private readonly object _lock = new();
[Benchmark]
public int Current_DispatcherSignal_Lock()
{
int signaled = 0;
bool flag = false;
for (int i = 0; i < FrameCount; i++)
{
lock (_lock)
{
if (!flag)
{
flag = true;
signaled++;
}
}
lock (_lock)
{
flag = false;
}
}
return signaled;
}
[Benchmark]
public int Optimized_DispatcherSignal_Interlocked()
{
int signaled = 0;
int flag = 0;
for (int i = 0; i < FrameCount; i++)
{
if (Interlocked.CompareExchange(ref flag, 1, 0) == 0)
{
signaled++;
}
Interlocked.Exchange(ref flag, 0);
}
return signaled;
}
// TextInput timestamp: DateTime.Now vs monotonic clock (TopLevelImpl.cs)
[Benchmark]
public ulong Current_TextInput_DateTimeNowTicks()
{
ulong result = 0;
for (int i = 0; i < FrameCount; i++)
{
result = (ulong)DateTime.Now.Ticks;
}
return result;
}
[Benchmark]
public long Optimized_TextInput_StopwatchTimestamp()
{
long result = 0;
for (int i = 0; i < FrameCount; i++)
{
result = System.Diagnostics.Stopwatch.GetTimestamp();
}
return result;
}
// Accessibility action: LINQ vs loop (AvaloniaAccessHelper.cs)
private readonly List<int> _providers = new() { 1, 2, 3 };
[Benchmark]
public bool Current_AccessAction_Linq()
{
bool result = false;
for (int i = 0; i < FrameCount; i++)
{
result = _providers
.Select(x => x > 1)
.Aggregate(false, (a, b) => a | b);
}
return result;
}
[Benchmark]
public bool Optimized_AccessAction_Loop()
{
bool result = false;
for (int i = 0; i < FrameCount; i++)
{
foreach (var p in _providers)
{
result |= p > 1;
}
}
return result;
}
// Keyboard char.ToString() vs cached string (AndroidKeyboardEventsHelper.cs)
private static readonly string[] s_asciiStringCache = CreateAsciiCache();
private static string[] CreateAsciiCache()
{
var cache = new string[128];
for (int i = 0; i < 128; i++)
{
cache[i] = ((char)i).ToString();
}
return cache;
}
[Benchmark]
public string? Current_KeySymbol_CharToString()
{
string? last = null;
for (int i = 0; i < FrameCount; i++)
{
char c = (char)(32 + (i % 95));
last = c.ToString();
}
return last;
}
[Benchmark]
public string? Optimized_KeySymbol_CachedString()
{
string? last = null;
for (int i = 0; i < FrameCount; i++)
{
int code = 32 + (i % 95);
last = s_asciiStringCache[code];
}
return last;
}
}
}
Loading…
Cancel
Save