Skip to content

Commit

Permalink
[android] reduce interop calls in MauiDrawable
Browse files Browse the repository at this point in the history
Context: dotnet#12130
Context: https://github.com/angelru/CvSlowJittering

Profiling a .NET MAUI customer sample while scrolling on a Pixel 5, I
see some interesting time being spent in:

    (0.76%) microsoft.maui!Microsoft.Maui.Graphics.MauiDrawable.OnDraw(Android.Graphics.Drawables.Shapes.Shape,Android.Graphics.Canv
    (0.54%) microsoft.maui!Microsoft.Maui.Graphics.MauiDrawable.SetDefaultBackgroundColor()

This sample has a `<Border/>` inside a `<CollectionView/>` and so you
can see this work happening while scrolling.

Specifically, I found a couple places we had code like:

    _borderPaint.StrokeWidth = _strokeThickness;
    _borderPaint.StrokeJoin = _strokeLineJoin;
    _borderPaint.StrokeCap = _strokeLineCap;
    _borderPaint.StrokeMiter = _strokeMiterLimit * 2;
    if (_borderPathEffect != null)
        _borderPaint.SetPathEffect(_borderPathEffect);

This calls from C# to Java 5 times. Creating a new method in
`PlatformInterop.java` allowed me to reduce it to 1.

I also found:

    void SetDefaultBackgroundColor()
    {
        using (var background = new TypedValue())
        {
            if (_context == null || _context.Theme == null || _context.Resources == null)
                return;

            if (_context.Theme.ResolveAttribute(global::Android.Resource.Attribute.WindowBackground, background, true))
            {
                var resource = _context.Resources.GetResourceTypeName(background.ResourceId);
                var type = resource?.ToLowerInvariant();

                if (type == "color")
                {
                    var color = new AColor(ContextCompat.GetColor(_context, background.ResourceId));
                    _backgroundColor = color;
                }
            }
        }
    }

This is doing a lot of unnecessary stuff: looking up a resource by
name, etc. I found a very simple Java example we could put in
`PlatformInterop.java`:

https://stackoverflow.com/a/14468034

After these changes, I now see:

    (0.28%) microsoft.maui!Microsoft.Maui.Graphics.MauiDrawable.OnDraw(Android.Graphics.Drawables.Shapes.Shape,Android.Graphics.Canv
    (0.04%) microsoft.maui!Microsoft.Maui.Graphics.MauiDrawable.SetDefaultBackgroundColor()

This improves the performance of any `<Border/>` (and other shapes) on
Android, and drops about ~1% of the CPU time while scrolling in this
example.
  • Loading branch information
jonathanpeppers committed May 4, 2023
1 parent 2041476 commit f0a93f4
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@

import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathEffect;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.PaintDrawable;
import android.net.Uri;
import android.os.Build;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
Expand Down Expand Up @@ -373,6 +380,81 @@ public static ColorStateList createEditTextColorStateList(ColorStateList colorSt
return null;
}

/**
* Sets many values at once on a Paint object
* @param paint
* @param strokeWidth
* @param strokeJoin
* @param strokeCap
* @param strokeMiter
* @param pathEffect
*/
public static void setPaintValues(Paint paint, float strokeWidth, Paint.Join strokeJoin, Paint.Cap strokeCap, float strokeMiter, PathEffect pathEffect)
{
paint.setStrokeWidth(strokeWidth);
paint.setStrokeJoin(strokeJoin);
paint.setStrokeCap(strokeCap);
paint.setStrokeMiter(strokeMiter);
if (pathEffect != null) {
paint.setPathEffect(pathEffect);
}
}

/**
* Calls canvas.saveLayer(), draws paths for clipPath & borderPaint, then canvas.restoreToCount()
* @param drawable
* @param canvas
* @param width
* @param height
* @param clipPath
* @param borderPaint
*/
public static void drawMauiDrawablePath(PaintDrawable drawable, Canvas canvas, int width, int height, @NonNull Path clipPath, Paint borderPaint)
{
int saveCount = canvas.saveLayer(0, 0, width, height, null);

Paint paint = drawable.getPaint();
if (paint != null) {
canvas.drawPath(clipPath, paint);
}
if (borderPaint != null) {
canvas.drawPath(clipPath, borderPaint);
}

canvas.restoreToCount(saveCount);
}

/**
* Gets the value of android.R.attr.windowBackground from the given Context
* @param context
* @return the color or -1 if not found
*/
public static int getWindowBackgroundColor(Context context)
{
TypedValue value = new TypedValue();
if (!context.getTheme().resolveAttribute(android.R.attr.windowBackground, value, true) && isColorType(value)) {
return value.data;
} else {
return -1;
}
}

/**
* Needed because TypedValue.isColorType() is only API Q+
* https://github.com/aosp-mirror/platform_frameworks_base/blob/1d896eeeb8744a1498128d62c09a3aa0a2a29a16/core/java/android/util/TypedValue.java#L266-L268
* @param value
* @return true if the TypedValue is a Color
*/
private static boolean isColorType(TypedValue value)
{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
return value.isColorType();
} else {
// Implementation from AOSP
return (value.type >= TypedValue.TYPE_FIRST_COLOR_INT && value.type <= TypedValue.TYPE_LAST_COLOR_INT);
}
}

private static class ColorStates
{
static final int[] EMPTY = new int[] { };
Expand Down
42 changes: 8 additions & 34 deletions src/Core/src/Graphics/MauiDrawable.Android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -388,18 +388,12 @@ protected override void OnDraw(Shape? shape, Canvas? canvas, APaint? paint)

if (_borderPaint != null)
{
_borderPaint.StrokeWidth = _strokeThickness;
_borderPaint.StrokeJoin = _strokeLineJoin;
_borderPaint.StrokeCap = _strokeLineCap;
_borderPaint.StrokeMiter = _strokeMiterLimit * 2;

if (_borderPathEffect != null)
_borderPaint.SetPathEffect(_borderPathEffect);
PlatformInterop.SetPaintValues(_borderPaint, _strokeThickness, _strokeLineJoin, _strokeLineCap, _strokeMiterLimit * 2, _borderPathEffect);

if (_borderColor != null)
#pragma warning disable CA1416 // https://github.com/xamarin/xamarin-android/issues/6962
{
_borderPaint.Color = _borderColor.Value;
#pragma warning restore CA1416
}
else
{
if (_stroke != null)
Expand Down Expand Up @@ -427,18 +421,10 @@ protected override void OnDraw(Shape? shape, Canvas? canvas, APaint? paint)
}
}

if (canvas == null)
if (canvas == null || _clipPath == null)
return;

var saveCount = canvas.SaveLayer(0, 0, _width, _height, null);

if (_clipPath != null && Paint != null)
canvas.DrawPath(_clipPath, Paint);

if (_clipPath != null && _borderPaint != null)
canvas.DrawPath(_clipPath, _borderPaint);

canvas.RestoreToCount(saveCount);
PlatformInterop.DrawMauiDrawablePath(this, canvas, _width, _height, _clipPath, _borderPaint);
}
else
{
Expand Down Expand Up @@ -512,22 +498,10 @@ void InitializeBorderIfNeeded()

void SetDefaultBackgroundColor()
{
using (var background = new TypedValue())
var color = PlatformInterop.GetWindowBackgroundColor(_context);
if (color != -1)
{
if (_context == null || _context.Theme == null || _context.Resources == null)
return;

if (_context.Theme.ResolveAttribute(global::Android.Resource.Attribute.WindowBackground, background, true))
{
var resource = _context.Resources.GetResourceTypeName(background.ResourceId);
var type = resource?.ToLowerInvariant();

if (type == "color")
{
var color = new AColor(ContextCompat.GetColor(_context, background.ResourceId));
_backgroundColor = color;
}
}
_backgroundColor = new AColor(color);
}
}

Expand Down
Binary file modified src/Core/src/maui.aar
Binary file not shown.

0 comments on commit f0a93f4

Please sign in to comment.