Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice
  3. Join us on November 16th, 2023, between 1 pm and 9 pm CET for Ask the Experts Online on Discord and on Unity Discussions.
    Dismiss Notice

Efficient Sharing Of Large Payloads Between C# And Java Runtimes On Android

Discussion in 'Android' started by Max-Pixel, Apr 9, 2019.

  1. Max-Pixel

    Max-Pixel

    Joined:
    Sep 3, 2013
    Posts:
    8
    I have a system in place that consumes a 3rd party Java API on Android, and passes resulting data to C# using AndroidJavaProxy.

    Code (CSharp):
    1. static class FooApi
    2. {
    3.     private static readonly AndroidJavaClass Thunks =
    4.         new AndroidJavaClass("com.company.plugin.Foo");
    5.  
    6.     private class BarCallback : AndroidJavaProxy
    7.     {
    8.         private readonly TaskCompletionSource<byte[]> _tcs;
    9.         public BarCallback(TaskCompletionSource<byte[]> tcs)
    10.             : base("com.company.plugin.BarCallback")
    11.         {
    12.             _tcs = tcs;
    13.         }
    14.  
    15.         public void Resolve(byte[] value)
    16.         {
    17.             _tcs.SetResult(value);
    18.         }
    19.     }
    20.  
    21.     public static Task<byte[]> GetBarAsync()
    22.     {
    23.         var tcs = new TaskCompletionSource<byte[]>();
    24.         Thunks.CallStatic("getBar", new BarCallback(tcs));
    25.         return tcs.Task;
    26.     }
    27. }
    In a recent test, I found that 300kb of data takes about 30ms to transfer from the 3rd party API to my Android plugin (C++ to Java). Nice! But, then it takes three minutes to transfer those 300kb from Java to C#:

    Code (CSharp):
    1. // Java
    2. Log.d(FOO_TAG, "bar start") // 0ms
    3. VendorApi.ComplicatedBar(new VendorBarCallback() {
    4.     @Override
    5.     public void result(Byte[] data) {
    6.         Log.d(FOO_TAG, "bar marshal"); // 32ms
    7.         csharpCallback.Resolve(data);
    8.         Log.d(FOO_TAG, "bar finish"); // 181,327ms
    9.     }
    10. });
    One of the first considerations is, of course, skipping Java (going straight from C++ to C#). In my situation, however, that's simply not feasible for multiple reasons.

    Clearly, AndroidJavaProxy's innerworkings are unfortunately inefficient for large payloads. What are my alternatives? What mechanism that allows C# to read data that originated in Java has the least overhead? I've implemented systems for sharing data between C# and V8 in various game engines in the past, so I'm not afraid of using unsafe code, but in this situation I have very limited access to information on how Unity has C# interacting with Java.

    These ideas come to mind as avenues that are worth pursuing:
    • Fixed (or pinned via GCHandle) byte[] declared in C#. Pointer passed to Java for writing (how? reinterpret cast? marshaling library?).
    • Java's-equivalent-of-fixed/pinned Byte[] declared in Java. Pointer passed to C# for reading with Marshal.PtrToStructure.
     
    BrendanLoBuglio likes this.
  2. Aurimas-Cernius

    Aurimas-Cernius

    Unity Technologies

    Joined:
    Jul 31, 2013
    Posts:
    3,642
    I'd start by trying to avoid any data copying Java <-> C# in the first place. Given third party Java plugin I'd look for possibility to write it all in Java and only use C# code for orchestration.
    Hard to advise without knowing the use case.
     
  3. Max-Pixel

    Max-Pixel

    Joined:
    Sep 3, 2013
    Posts:
    8
    The use-case shouldn't matter, which is part of why I framed my question generically (there are also non-disclosure agreements involved). This makes the discussion more useful for others who have the same problem, but with different use cases. Keeping everything in Java is not an option, unless most of the UnityEngine API is available there.

    To be honest, I'm surprised and disappointed to get such a non-answer from Unity staff. Do you have a post quota that you need to meet? If you're not an expert in Mono/JVM marshaling, then I believe the most useful contribution you can offer would be to refer somebody who is familiar with that area of your product's source code.

    Further, three minutes to copy 300kb of data is absurd. There's clearly something wrong that's worth addressing.
     
    Last edited: Apr 11, 2019
  4. Max-Pixel

    Max-Pixel

    Joined:
    Sep 3, 2013
    Posts:
    8
    I did some experimentation today. Here's what I found:

    Code (CSharp):
    1. class TestA : AndroidJavaProxy
    2. {
    3.     public TestA(): base("com.company.plugin.TestA"){}
    4.  
    5.     void getResult(byte[] x)
    6.     {
    7.         Debug.Log(Encoding.ASCII.GetString(x));
    8.     }
    9. }
    10.  
    11. class TestB : AndroidJavaProxy
    12. {
    13.     public TestB() : base("com.company.plugin.TestB") {}
    14.  
    15.     void getResult(AndroidJavaObject x)
    16.     {
    17.         Debug.Log(Encoding.ASCII.GetString(x.Get<byte[]>("x")));
    18.         x.Dispose();
    19.     }
    20. }
    On Average, TestA took about 130ms to read 100 bytes of data. On the other hand, TestB, despite using a totally unnecessary object to wrap the result, took about 10ms to read the same amount of data.

    It appears that AndroidJavaObject.Get<T> is a far more efficient implementation of transferring data from JVM to Mono than is AndroidJavaProxy + callback method parameter. In fact, I'd go as far as to say that passing array parameters is implemented poorly, and should probably be looked into by Unity's engine programmers (1ms per byte is a head-scratcher).

    For anybody looking to truly maximize performance, it's also possible to return a direct ByteBuffer, retrieve the "address" field, and cast it to an unsafe byte*. With that approach, I ended up with a byte[100] in a mere 2ms.
     
    markmcg likes this.
  5. Aurimas-Cernius

    Aurimas-Cernius

    Unity Technologies

    Joined:
    Jul 31, 2013
    Posts:
    3,642
    I've recently improved string marshaling around 2x (will come out in 19.2 IIRC). In older releases it is also more efficient to AndroidJavaObject instead of string.
    Sounds like I should give a spin for arrays of primitives as well.
    Could you report this as bug?
     
    Yury-Habets likes this.
  6. WayneVenter

    WayneVenter

    Joined:
    May 8, 2019
    Posts:
    56
    @Max-Pixel I desperately need an example of how this works, performance is shocking and I have to pass a byte[] from Java to C# in Unity, how can I send a ByteBuffer or access the byte[] in Java from C# in Unity using byte* ?
     
  7. jschieck

    jschieck

    Joined:
    Nov 10, 2010
    Posts:
    429
    For anyone out there who comes across this and needs the example more clear. Calling the array() method on the ByteBuffer provides insanely better performance then passing the byte[] directly (3 minutes vs <5ms for a 1MB array). Similar to just wrapping the data in an object

    Code (CSharp):
    1. // cs
    2. class Test : AndroidJavaProxy
    3. {
    4.     public Test() : base("com.company.plugin.Test") {}
    5.  
    6.     void onResultFast(AndroidJavaObject byteBuffer)
    7.     {
    8.         sbyte[] data = byteBuffer.Call<sbyte[]>("array");
    9.     }
    10.  
    11.     void onResultSlow(byte[] data) { }
    12. }
    13.  
    14.  
    15. // java
    16. byte[] array = ...;
    17. testInstance.onResultSlow(array); // 3 minutes for 1MB
    18.  
    19. testInstance.onResultFast(ByteBuffer.wrap(array)); // < 5ms for 1MB
     
    tachen and amokto like this.