Shadows and MaterialFrame blur: Performance Updates

https://github.com/roubachof/Sharpnado.MaterialFrame
https://github.com/roubachof/Sharpnado.Shadows

Shadows gets itself a BitmapCache

Well it was a bit of a journey (https://www.sharpnado.com/shadows-leaks/), but Shadows gets finally its BitmapCache \o/

With this cache for a given Color, BlurRadius and Size, only one instance of an Android Bitmap will be created and shared between all views.

Imagine using Shadows in a list like this one:

Neumorphism shadows require 2 shadows, and we have 3 of them (frame, round button, reset button). Maybe 8 recycled views will be created on Android.

Without the BitmapCache we would end up creating: 3 * 2 * 8 = 48 bitmaps

Using the BitmapCache will result in: 3 * 2 = 6 bitmaps

Since the bitmap creation is the performance bottleneck, scrolling will be hugely improved.

For the curious ones, here is the BitmapCache implementation:

public class BitmapCache : IDisposable  
{
    private const string LogTag = nameof(BitmapCache);

    private static BitmapCache instance;

    private readonly Dictionary<string, Bucket> _cache = new Dictionary<string, Bucket>();

    private readonly object _cacheLock = new object();

    public static BitmapCache Instance => instance ?? Initialize();

    public void Dispose()
    {
        lock (_cacheLock)
        {
            foreach (var bucket in _cache.Values)
            {
                DisposeBitmap(bucket.Value);
            }

            _cache?.Clear();
        }
    }

    public Bitmap GetOrCreate(string hash, Func<Bitmap> create)
    {
        InternalLogger.Debug(LogTag, () => $"GetOrCreate( hash: {hash} )");
        lock (_cacheLock)
        {
            if (_cache.TryGetValue(hash, out var bucket))
            {
                return bucket.Value;
            }

            return Create(hash, create);
        }
    }

    public Bitmap Add(string hash, Func<Bitmap> create)
    {
        InternalLogger.Debug(LogTag, () => $"Add( hash: {hash} )");
        lock (_cacheLock)
        {
            if (_cache.TryGetValue(hash, out var bucket))
            {
                bucket.ReferenceCount++;
                InternalLogger.Debug(LogTag, () => $"Reference count: {bucket.ReferenceCount}");
                return bucket.Value;
            }

            return Create(hash, create);
        }
    }

    public bool Remove(string hash)
    {
        InternalLogger.Debug(LogTag, () => $"Remove( hash: {hash} )");
        lock (_cacheLock)
        {
            if (_cache.TryGetValue(hash, out var bucket))
            {
                bucket.ReferenceCount--;
                InternalLogger.Debug(LogTag, () => $"Reference count: {bucket.ReferenceCount}");

                if (bucket.ReferenceCount <= 0)
                {
                    _cache.Remove(hash);
                    InternalLogger.Debug(LogTag, () => $"Removing bitmap, bitmap count is {_cache.Count}");

                    DisposeBitmap(bucket.Value);
                }

                return true;
            }

            return false;
        }
    }

    private static BitmapCache Initialize()
    {
        instance?.Dispose();
        return instance = new BitmapCache();
    }

    private static void DisposeBitmap(Bitmap bitmap)
    {
        if (bitmap.IsNullOrDisposed() || bitmap.IsRecycled)
        {
            return;
        }

        bitmap.Recycle();
        bitmap.Dispose();
    }

    private Bitmap Create(string hash, Func<Bitmap> create)
    {
        var newBitmap = create();
        _cache.Add(hash, new Bucket(1, newBitmap));
        InternalLogger.Debug(LogTag, () => $"New bitmap created, bitmap count is {_cache.Count}");
        return newBitmap;
    }

    public class Bucket
    {
        public Bucket(int referenceCount, Bitmap value)
        {
            ReferenceCount = referenceCount;
            Value = value;
        }

        public int ReferenceCount { get; set; }

        public Bitmap Value { get; }
    }
}

MaterialFrame and the Xamarin.Android jni bugs

All began with this issue opened by Mauro:

issue

The AndroidMaterialFrameRenderer.ThrowStopExceptionOnDraw is a specific Android configuration property:

If set to true, the rendering result could be better (clearer blur not mixing front elements). However due to a bug in the Xamarin framework https://github.com/xamarin/xamarin-android/issues/4548, debugging is impossible with this mode (causes SIGSEGV). My suggestion would be to set it to false for debug, and to true for releases.

The blur effect for the android implementation is produced by an adaptation of the RealtimeBlurView.

Basically, when you create this kind of view, when the view draws itself, it will:

  1. Get the root view (your page content view)
  2. get all the views within the bounds of the frame between the root view and the blur view
  3. create a bitmap of this
  4. blur the bitmap
public override void Draw(Canvas canvas)  
{
    if (mIsRendering)
    {
        InternalLogger.Debug($"BlurView@{GetHashCode()}", $"Draw() => throwing stop exception");

        // Quit here, don't draw views above me
        if (AndroidMaterialFrameRenderer.ThrowStopExceptionOnDraw)
        {
            throw STOP_EXCEPTION; // static instance of StopException
        }

        return;
    }

    if (RENDERING_COUNT > 0)
    {
        InternalLogger.Debug($"BlurView@{GetHashCode()}", $"Draw() => Doesn't support blurview overlap on another blurview");

        // Doesn't support blurview overlap on another blurview
    }
    else
    {
        InternalLogger.Debug($"BlurView@{GetHashCode()}", $"Draw() => calling base draw");
        base.Draw(canvas);
    }
}

public bool OnPredraw()  
{
    ... 

    int rc = blurView.mBlurringCanvas.Save();
    blurView.mIsRendering = true;
    RENDERING_COUNT++;
    try
    {
        blurView.mBlurringCanvas.Scale(
            1f * blurView.mBitmapToBlur.Width / blurView.Width,
            1f * blurView.mBitmapToBlur.Height / blurView.Height);
        blurView.mBlurringCanvas.Translate(-x, -y);
        if (decor.Background != null)
        {
            decor.Background.Draw(blurView.mBlurringCanvas);
        }

        decor.Draw(blurView.mBlurringCanvas);
    }
    catch (StopException)
    {
        InternalLogger.Debug($"BlurView@{blurView.GetHashCode()}", $"OnPreDraw(formsId: {blurView._formsId}) => in catch StopException");
    }
    catch (Exception)
    {
        InternalLogger.Debug($"BlurView@{blurView.GetHashCode()}", $"OnPreDraw(formsId: {blurView._formsId}) => in catch global exception");
    }
    finally
    {
        blurView.mIsRendering = false;
        RENDERING_COUNT--;
        blurView.mBlurringCanvas.RestoreToCount(rc);
    }

    ...
}

In the Android implementation, to stop the drawing at the right time, an exception is drawn.
Doing this, it prevents that the views in the same hierarchy level as the blur view are also blurred into the bitmap, achieving a cleaner effect.

Unfortunately, in debug, it created a SIGSEV crash.
I opened an issue for it:

https://github.com/xamarin/xamarin-android/issues/4548

Brendan Zagaeski first added some information and then Jonathan Pryor nailed it.
The good news is that it is fixed in d16.8 (I guess VS 2019 16.8).

But the usage of this property in release configuration, also resulted in bad performance.
After investigation, I discovered that this "handled exception" was regarded as a UNHANDLED EXCEPTION by JNI.

unhandled exception

So I opened an issue about it:

https://github.com/xamarin/xamarin-android/issues/4632

What happened?

Brendan Zagaeski first added some information and then Jonathan Pryor nailed it.
And so this issue is also fixed in VS 16.8.

But then I also discovered that for some reason, for each unhandled exception raised, the exception stack got bigger and bigger:

bigger bigger

What did I do?

I added a comment to the #4632 issue:

comment

And guess what happened?

suspense







closed




I was like:

weebae

But then, more than a month later:

brendan

And then: https://github.com/xamarin/xamarin-android/issues/4987

And so:

workaround

And now, with VS 16.8 and this workaround, mauro's issue should be fixed :)

camelot

To sum-up

OPEN ISSUES IN XAMARIN REPOS IF YOU WANT BUGS TO BE FIXED.

Well sorry I misspoke:

OPEN DETAILED AND REPRODUCIBLE ISSUES IN XAMARIN REPOS IF YOU WANT BUGS TO BE FIXED.

And also thank you so much John and Brendan for your amazing work :)