Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Java.Interop.sln
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xamarin.SourceWriter-Tests"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Java.Interop.Localization", "src\Java.Interop.Localization\Java.Interop.Localization.csproj", "{998D178B-F4C7-48B5-BDEE-44E2F869BB22}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "invocation-overhead", "tests\invocation-overhead\invocation-overhead.csproj", "{3CF58D34-693C-408A-BFE7-BC5E4BE44A26}"
EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
src\Java.Interop.NamingCustomAttributes\Java.Interop.NamingCustomAttributes.projitems*{58b564a1-570d-4da2-b02d-25bddb1a9f4f}*SharedItemsImports = 5
Expand Down Expand Up @@ -272,6 +274,10 @@ Global
{998D178B-F4C7-48B5-BDEE-44E2F869BB22}.Debug|Any CPU.Build.0 = Debug|Any CPU
{998D178B-F4C7-48B5-BDEE-44E2F869BB22}.Release|Any CPU.ActiveCfg = Release|Any CPU
{998D178B-F4C7-48B5-BDEE-44E2F869BB22}.Release|Any CPU.Build.0 = Release|Any CPU
{3CF58D34-693C-408A-BFE7-BC5E4BE44A26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3CF58D34-693C-408A-BFE7-BC5E4BE44A26}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3CF58D34-693C-408A-BFE7-BC5E4BE44A26}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3CF58D34-693C-408A-BFE7-BC5E4BE44A26}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -318,6 +324,7 @@ Global
{C5B732C8-7AF3-41D3-B903-AEDFC392E5BA} = {0998E45F-8BCE-4791-A944-962CD54E2D80}
{6CF94627-BA74-4336-88CD-7EDA20C8F292} = {271C9F30-F679-4793-942B-0D9527CB3E2F}
{998D178B-F4C7-48B5-BDEE-44E2F869BB22} = {0998E45F-8BCE-4791-A944-962CD54E2D80}
{3CF58D34-693C-408A-BFE7-BC5E4BE44A26} = {271C9F30-F679-4793-942B-0D9527CB3E2F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {29204E0C-382A-49A0-A814-AD7FBF9774A5}
Expand Down
16 changes: 14 additions & 2 deletions src/Java.Interop/Java.Interop/ManagedPeer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@ namespace Java.Interop {
[JniTypeSignature (JniTypeName)]
/* static */ sealed class ManagedPeer : JavaObject {

delegate void ConstructDelegate (IntPtr jnienv,
IntPtr klass,
IntPtr n_self,
IntPtr n_assemblyQualifiedName,
IntPtr n_constructorSignature,
IntPtr n_constructorArguments);
delegate void RegisterDelegate (IntPtr jnienv,
IntPtr klass,
IntPtr n_nativeClass,
IntPtr n_assemblyQualifiedName,
IntPtr n_methods);

internal const string JniTypeName = "com/xamarin/java_interop/ManagedPeer";


Expand All @@ -25,11 +37,11 @@ static ManagedPeer ()
new JniNativeMethodRegistration (
"construct",
ConstructSignature,
(Action<IntPtr, IntPtr, IntPtr, IntPtr, IntPtr, IntPtr>) Construct),
(ConstructDelegate) Construct),
new JniNativeMethodRegistration (
"registerNativeMembers",
RegisterNativeMembersSignature,
(Action<IntPtr, IntPtr, IntPtr, IntPtr, IntPtr>) RegisterNativeMembers)
(RegisterDelegate) RegisterNativeMembers)
);
}

Expand Down
4 changes: 0 additions & 4 deletions src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,6 @@ public override string GetCurrentManagedThreadStackTrace (int skipFrames, bool f

protected override void Dispose (bool disposing)
{
var bridge = NativeMethods.java_interop_gc_bridge_get_current ();
if (bridge != IntPtr.Zero) {
NativeMethods.java_interop_gc_bridge_remove_current_app_domain (bridge);
}
base.Dispose (disposing);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ protected override void Dispose (bool disposing)
RegisteredInstances.Clear ();
RegisteredInstances = null;
}

if (bridge != IntPtr.Zero) {
NativeMethods.java_interop_gc_bridge_remove_current_app_domain (bridge);
bridge = IntPtr.Zero;
}
}

Dictionary<int, List<WeakReference<IJavaPeerable>>> RegisteredInstances = new Dictionary<int, List<WeakReference<IJavaPeerable>>>();
Expand Down
5 changes: 4 additions & 1 deletion src/java-interop/java-interop.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.Build.NoTargets">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<TargetFrameworks>net472;netcoreapp3.1</TargetFrameworks>
<OutputPath>$(ToolOutputFullPath)</OutputPath>
<JNIEnvGenPath>$(BuildToolOutputFullPath)</JNIEnvGenPath>
<OutputName>java-interop</OutputName>
Expand Down Expand Up @@ -44,6 +44,9 @@
<ClCompile Include="java-interop-dlfcn.cc" />
<ClCompile Include="java-interop-jvm.cc" />
<ClCompile Include="java-interop-logger.cc" />
</ItemGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'net472' ">
<ClCompile Include="java-interop-mono.cc" />
<ClCompile Include="java-interop-gc-bridge-mono.cc" />
</ItemGroup>
Expand Down
46 changes: 46 additions & 0 deletions tests/invocation-overhead/Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<Project>
<Target Name="BuildJniEnvironment_g_cs"
BeforeTargets="BeforeCompile"
Inputs="$(_JNIEnvGenPath)"
Outputs="jni.cs;jni.c">
<Exec
Command="$(_RunJNIEnvGen) jni.cs jni.c"
/>
</Target>

<ItemGroup>
<_NativeLibsSrc Include="$(ToolOutputFullPath)\libjava-interop.*" />
<_NativeLibsDst Include="@(_NativeLibsSrc->'$(OutputPath)%(Filename)%(Extension)')" />
</ItemGroup>

<Target Name="CopyNativeLibs"
BeforeTargets="BeforeCompile"
Inputs="@(_NativeLibsSrc)"
Outputs="@(_NativeLibsDst)">
<Copy
SourceFiles="@(_NativeLibsSrc)"
DestinationFiles="@(_NativeLibsDst)"
/>
</Target>

<Target Name="Run">
<MSBuild Projects="$(MSBuildThisFileDirectory)invocation-overhead.csproj"
Properties="TargetFramework=net472"
Targets="_Run_net472"
/>
<MSBuild Projects="$(MSBuildThisFileDirectory)invocation-overhead.csproj"
Properties="TargetFramework=netcoreapp3.1"
Targets="_Run_netcoreapp"
/>
</Target>

<Target Name="_Run_net472">
<Message Text="Mono timing:" Importance="High" />
<Exec Command="JI_JVM_PATH=&quot;$(JdkJvmPath)&quot; $(Runtime) $(TargetPath)" />
</Target>

<Target Name="_Run_netcoreapp">
<Message Text=".NET Core timing:" Importance="High" />
<Exec Command="JI_JVM_PATH=&quot;$(JdkJvmPath)&quot; dotnet $(TargetPath)" />
</Target>
</Project>
34 changes: 6 additions & 28 deletions tests/invocation-overhead/Makefile
Original file line number Diff line number Diff line change
@@ -1,34 +1,12 @@
CONFIGURATION = Debug
JNIENV_GEN = ../../bin/BuildDebug/jnienv-gen.exe

all: test-overheads.exe libjava-interop.dylib
all: bin/$(CONFIGURATION)/net472/invocation-overheads.exe

clean:
-rm test-overheads.exe test-overheads.exe.mdb
-rm -Rf libJavaInterop.dylib*

include ../../build-tools/scripts/mono.mk
include ../../build-tools/scripts/jdk.mk
include ../../bin/BuildDebug/JdkInfo.mk
include ../../build-tools/scripts/msbuild.mk

$(JNIENV_GEN):
(cd ../../build-tools/jnienv-gen ; $(MSBUILD) $(MSBUILD_FLAGS) )

HANDLE_FEATURES = \
-d:FEATURE_JNIENVIRONMENT_JI_INTPTRS \
-d:FEATURE_JNIENVIRONMENT_JI_PINVOKES \
-d:FEATURE_JNIENVIRONMENT_SAFEHANDLES \
-d:FEATURE_JNIENVIRONMENT_XA_INTPTRS
bin/$(CONFIGURATION)/net472/invocation-overheads.exe:
msbuild /restore

test-overheads.exe: test-overheads.cs jni.cs
mcs -out:$@ -unsafe $(HANDLE_FEATURES) $^

jni.c jni.cs: $(JNIENV_GEN)
$(RUNTIME) $< jni.cs jni.c

libjava-interop.dylib: jni.c
gcc -g -shared -fPIC -o $@ $< -m64 -DJI_DLL_EXPORT -fvisibility=hidden $(JI_JDK_INCLUDE_PATHS:%=-I%)
clean:
msbuild /t:Clean

run:
$(RUNTIME) test-overheads.exe
msbuild /t:Run /nologo /v:m
103 changes: 83 additions & 20 deletions tests/invocation-overhead/README.md
Original file line number Diff line number Diff line change
@@ -1,54 +1,85 @@
Timing:
# JNI Invocation Overhead

The original Java.Interop effort weanted a type-safe and simple binding. As such, it usedd SafeHandles.
The original Java.Interop effort wanted a *type-safe* and *simple*
binding around JNI. As such, it used `SafeHandle`s.

As the Xamarin.Forms team has turned their attention to profiling
Xamarin.Forms apps, and finding major Xamarin.Android-related
performance issues, performance needs to be considered.
performance issues, performance needed to be considered.

For example, GC object allocation is a MAJOR concern for them;
ideally, you could have ZERO GC ALLOCATIONS performed when
invoking a Java method.

SafeHandles don't fit "nicely" in that world; every method that returns a SafeHandle ALLOCATES A NEW GC OBJECT.
`SafeHandle`s don't fit "nicely" in that world; every method that
returns a `SafeHandle` ALLOCATES A NEW GC OBJECT.

So...how bad is it?
So...how bad is that?

What's in this directory is a VERY TRIMMED DOWN Java.Interop layer.
Really, it's NOT Java.Interop; it's the core generated JniEnvironment.g.cs (as `jni.cs`)
with code for both SafeHandles and IntPtr-oriented invocation strategies.
What's in this directory is insanity: there are four different "strategies"
for dealing with JNI:

The test? Invoke java.util.Arrays.binarySearch(int[], int) for 10,000,000 times.
1. `SafeHandle` All The Things! (`SafeTiming`)

Result:
2. Xamarin.Android JNI handling from 2011 until Xamarin.Android 6.1 (2016)
(`XAIntPtrTiming`)

This uses `IntPtr`s *everywhere*, e.g. `JNIEnv::CallObjectMethod()` returns
an `IntPtr`.

3. "Happier Medium?" (`JIIntPtrTiming`)

`IntPtr`s everywhere means it's trivial to forget that
a JNI handle is a GREF vs. an LREF vs… What if we used the same `JNIEnv`
invocation logic as `XAIntPtrTiming`, but instead of `IntPtr`s everywhere
we instead had a `JniObjectReference` structure?

4. "Optimize (3)" (`JIPinvokeTiming`)

(3) was slower than (2). What if we rethought the `JNIEnv`
invocation logic and removed all the `Marshal.GetDelegateForFunctionPointer()`
invocations with normal P/Invokes?

To compare these four strategies, `jnienv-gen.exe` was updated so that *all*
of them could be emitted into the same `.cs` file, into separate namespaces.
These "core" JNI bindings could then be used with to invoke
`java.util.Arrays.binarySearch(int[], int)`, 10,000,000 times, and compare
the results.

Result in 2015 (commit [25de1f38][25de]):

[25de]: https://github.com/xamarin/Java.Interop/commit/25de1f38bb6b3ef2d4c98d2d95923a4bd50d2ea0

# SafeHandle timing: 00:00:02.7913432
# Average Invocation: 0.00027913432ms
# JniObjectReference timing: 00:00:01.9809859
# JIIntPtrTiming timing: 00:00:01.9809859
# Average Invocation: 0.00019809859ms

Basically, with a `JniObjectReference` struct-oriented approach, SafeHandles take ~1.4x as long to run.
Rephrased: the JniObjectReference struct takes 70% of the time of SafeHandles.
Basically, with a `JniObjectReference` struct-oriented approach, SafeHandles
take ~1.4x longer to run. Rephrased: the `JniObjectReference` struct takes
70% of the time of SafeHandles.

Ouch.

What about the current Xamarin.Android "all IntPtrs all the time!" approach?

# SafeHandle timing: 00:00:02.8118485
# Average Invocation: 0.00028118485ms
# JniObjectReference timing: 00:00:02.0061727
# XAIntPtrTiming timing: 00:00:02.0061727
# Average Invocation: 0.00020061727ms

The performance difference is comparable -- SafeHandles take ~1.4x as long to run, or
IntPtrs take ~70% as long as using SafeHandles.
The performance difference is comparable -- SafeHandles take ~1.4x as long to
run, or IntPtrs take ~70% as long as using SafeHandles.

Interesting -- but probably not *that* interesting -- is that in an absolute sense, the `JniObjectReference`
struct was *faster* than the `IntPtr` approach, even though `JniObjectReference` contains *both* an `IntPtr`
*and* an enum -- and is thus bigger!
Interesting -- but probably not *that* interesting -- is that in an absolute
sense, the `JniObjectReference` struct was *faster* than the `IntPtr` approach,
even though `JniObjectReference` contains *both* an `IntPtr` *and* an enum --
and is thus bigger!

That doesn't make any sense.

Regardless, `JniObjectReference` doesn't appear to be *slower*, and thus should be a viable option here.
Regardless, `JniObjectReference` doesn't appear to be *slower*, and thus should
be a viable option here.

---

Expand Down Expand Up @@ -90,3 +121,35 @@ when passed as an argument to native code they'll be automagically pinned and ke
The current (above) timing comparison uses `IntPtr` for arguments.

We should standardize on `JniObjectReference` (again).

## 2021 Timing Update

How do these timings compare in 2021 on Desktop Mono (macOS)?

# SafeTiming timing: 00:00:09.3850449
# Average Invocation: 0.00093850449ms
# XAIntPtrTiming timing: 00:00:04.4930288
# Average Invocation: 0.00044930288ms
# JIIntPtrTiming timing: 00:00:04.5563368
# Average Invocation: 0.00045563368ms
# JIPinvokeTiming timing: 00:00:03.4710383
# Average Invocation: 0.00034710383ms

In an absolute sense, things are worse: 10e6 invocations in 2015 took 2-3sec.
Now, they're taking at least 3.5sec.

In a relative sense, `SafeHandles` got *worse*, and takes 2.09x longer than
`XAIntPtrTiming`, and 2.7x longer than `JIPinvokeTiming`!

What about .NET Core 3.1? After some finagling, *that* can work too!

# SafeTiming timing: 00:00:05.1734443
# Average Invocation: 0.00051734443ms
# XAIntPtrTiming timing: 00:00:03.1048897
# Average Invocation: 0.00031048897ms
# JIIntPtrTiming timing: 00:00:03.4353958
# Average Invocation: 0.00034353958ms
# JIPinvokeTiming timing: 00:00:02.7470934
# Average Invocation: 0.00027470934000000004ms

Relative performance is a similar story: `SafeHandle`s are slowest.
Loading