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.