1

I am writing a Xamarin.Android app that offers syntax highlighting for code. It's choking on large files. I debugged my app and randomly stopped it while it was processing one. To my surprise, I stopped on this line nearly all of the time:

var span = new ForegroundColorSpan(color);

This is the only piece of Java code that I call very frequently while I'm syntax highlighting. This is the implementation of the method, which is a one-liner that assigns to an int field. Obviously, Xamarin's Java interop must be the culprit.

I'm not sure how I can get around this. I tried caching the ForegroundColorSpan for a given Color using Dictionary, but due to the way the Spannable APIs are designed you can't call SpannableString.setSpan with the same span instance for different ranges. Do I have any other options here?

edit: Sorry, interop, not marshalling. I changed the title.

edit 2: OK, I have been taking a much deeper look into this issue. First, I came up with a minimal repro:

var list = new List<ForegroundColorSpan>();
for (int i = 0; i < 100000; i++)
{
    list.Add(new ForegroundColorSpan(Color.Blue));
}

Try running this in a blank Xamarin app and it will take forever. The really, really strange thing about this, though, is that the time it takes isn't proportional to the number of iterations. Try setting the number of iterations to 40000, and it'll complete in a second. Try setting it to 50000, and it'll take forever. If you randomly pause the debugger and inspect the i, you'll see that it's always around 45/46K. There appears to be a magic cutoff point at around 45K elements where the loop suddenly becomes unusably slow-- and by that, I mean 5 new objects get created per second.

Second, I looked into what the constructor does under the hood by reading the Working with JNI document, specifically this part, and I manually wrote bindings for ForegroundColorSpan so I could debug them:

[Register("android/text/style/ForegroundColorSpan", DoNotGenerateAcw = true)]
internal class FastForegroundColorSpan : JavaObject
{
    private static readonly IntPtr class_ref = JNIEnv.FindClass("android/text/style/ForegroundColorSpan");

    private static IntPtr id_ctor_I;

    [Register(".ctor", "(I)V", "")]
    public unsafe FastForegroundColorSpan(int color)
        : base(IntPtr.Zero, JniHandleOwnership.DoNotTransfer)
    {
        if (Handle != IntPtr.Zero)
        {
            return;
        }

        if (id_ctor_I == IntPtr.Zero)
        {
            id_ctor_I = JNIEnv.GetMethodID(class_ref, "<init>", "(I)V");
        }

        var args = stackalloc JValue[1];
        args[0] = new JValue(color);
        var handle = JNIEnv.NewObject(class_ref, id_ctor_I, args);
        SetHandle(handle, JniHandleOwnership.TransferLocalRef);
    }
}

I switched my code to use new FastForegroundColorSpan instead of new ForegroundColorSpan. When I randomly stop the code in the debugger now, it always breaks at SetHandle. I looked into the source code of SetHandle, and indeed there is a lot of stuff going on there, including many interop calls to JNIEnv and locking/weak references. What I still can't explain, though, is why the app always slows (and so dramatically) at some magic cutoff point after ~45K spans.

James Ko
  • 32,215
  • 30
  • 128
  • 239
  • Would be nice if you could share a bit of the code that replicated the problem. – jzeferino Jun 17 '17 at 10:36
  • @jzeferino I did, it's the one-liner snippet. If you are looking for how I use it in my app you can find that [here](https://github.com/jamesqo/Repository/blob/wip-spannabletext/Repository/EditorServices/Highlighting/SyntaxColorer.cs#L33), but really all you need to do is write a for-loop that runs a lot of times and calls `new ForegroundColorSpan()`. – James Ko Jun 17 '17 at 10:40

1 Answers1

1

The really, really strange thing about this, though, is that the time it takes isn't proportional to the number of iterations. Try setting the number of iterations to 40000, and it'll complete in a second. Try setting it to 50000, and it'll take forever.

The problem is you are having too many Java.lang.object instances alive at once. If you refer to Global Reference Message, you can find the following statement:

Unfortunately, Android emulators only allow 2000 global references to exist at a time. Hardware has a much higher limit of 52000 global references. The lower limit can be problematic when running applications on the emulator, so knowing where the instance came from can be very useful.

You can enable the Global Reference loggig (GREF) logging by:

adb shell setprop debug.mono.log gref

Also there is a similar case that you can refer to.

So, for solution, you will need to delete this loop, and try to work around this according to your requirement.

Elvis Xia - MSFT
  • 10,801
  • 1
  • 13
  • 24
  • Elvis, thank you for the clear and thorough answer. I am pleasantly surprised to have gotten a response from someone who has an in-depth knowledge of this subject. I ended up working around it yesterday by creating a Java library where I instantiated all of the objects, and calling into that once from C# via a bindings library. – James Ko Jun 19 '17 at 03:18