Skip to content

Commit 2a299eb

Browse files
authored
[invocation-overhead] Fix; .NET Core support (#800)
`tests/invocation-overhead` partially bitrot since it was last touched in 8602581: it insta-crashes: % make run mono64 --debug=casts test-overheads.exe 2021-02-11 17:33:49.479 mono64[90565:6542595] CheckForInstalledJavaRuntimes: Please visit http://www.java.com for information on installing java. make: *** [run] Error 97 The primary "cause" is d1cce19: it was never a good idea to P/Invoke into `jvm.dll`, as: * The name for `jvm.dll` differs between platforms! It's `jvm` some places, `jre` others, and `jli` in still others! If we want (eventual) .NET Framework/.NET Core support, this is a non-starter. * The actual on-disk path is also highly variable. The solution to this conundrum was to update the `java-interop` native library to have a new `java_interop_jvm_load()` export, and use *that* to load the JVM. (Actual determination of which file to load is left "elsewhere"; once the JVM to load is *found*, then we can sanely P/Invoke into `java_interop_jvm_load()`.) This change never made it to `tests/invocation-overhead`. Rework `tests/invocation-overhead` so that it's *less* "stand-alone": it now uses `JreRuntime` from `Java.Runtime.Environment.dll` to create an in-process JVM, using the `$JI_JVM_PATH` environment variable to determine *which* JVM to load. (This is how `tests/TestJVM` works.) This change requires "re-structuring" how certain types such as `JniObjectReference` work, as it means we must now reference `Java.Interop.dll`, which *also* defines `JniObjectReference`/etc. Square this circle by moving various types into the appropriate sub-namespaces, e.g. `Java.Interop.SafeHandles` has its own special `JniObjectReference` declaration. "While we're at it", what's .NET Core's JNI invocation performance look like? Update `invocation-overhead` to multitarget net472 and netcoreapp3.1. Update `src/java-interop` so that the `java-interop` native library can be built as a netcoreapp3.1 library. (This involves removing all mention of Mono.) Update `Java.Runtime.Environment` so that `MonoRuntimeValueManager` disposes the GC Bridge, not `JreRuntime`. This avoids an `EntryPointNotFoundException`, as the `java-interop` native lib doesn't provide `java_interop_gc_bridge_get_current()`. Add a new `msbuild /t:Run` target which runs `invocation-overhead` under both the desktop `$(Runtime)` & .NET Core make prepare make all make -C tests/invocation-overhead run # Runs tests under mono & .NET Core .NET Core is faster than Mono for this particular benchmark: * `SafeTiming` timing: Mono: 9.3850449 sec .NET Core: 5.1734443 sec * `XAIntPtrTiming` timing: Mono: 4.4930288 sec .NET Core: 3.1048897 sec * `JIIntPtrTiming` timing: Mono: 4.5563368 sec .NET Core: 3.4353958 sec * `JIPinvokeTiming` timing: Mono: 3.4710383 sec .NET Core: 2.7470934 sec
1 parent ba6b013 commit 2a299eb

File tree

11 files changed

+334
-259
lines changed

11 files changed

+334
-259
lines changed

Java.Interop.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xamarin.SourceWriter-Tests"
9797
EndProject
9898
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Java.Interop.Localization", "src\Java.Interop.Localization\Java.Interop.Localization.csproj", "{998D178B-F4C7-48B5-BDEE-44E2F869BB22}"
9999
EndProject
100+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "invocation-overhead", "tests\invocation-overhead\invocation-overhead.csproj", "{3CF58D34-693C-408A-BFE7-BC5E4BE44A26}"
101+
EndProject
100102
Global
101103
GlobalSection(SharedMSBuildProjectFiles) = preSolution
102104
src\Java.Interop.NamingCustomAttributes\Java.Interop.NamingCustomAttributes.projitems*{58b564a1-570d-4da2-b02d-25bddb1a9f4f}*SharedItemsImports = 5
@@ -272,6 +274,10 @@ Global
272274
{998D178B-F4C7-48B5-BDEE-44E2F869BB22}.Debug|Any CPU.Build.0 = Debug|Any CPU
273275
{998D178B-F4C7-48B5-BDEE-44E2F869BB22}.Release|Any CPU.ActiveCfg = Release|Any CPU
274276
{998D178B-F4C7-48B5-BDEE-44E2F869BB22}.Release|Any CPU.Build.0 = Release|Any CPU
277+
{3CF58D34-693C-408A-BFE7-BC5E4BE44A26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
278+
{3CF58D34-693C-408A-BFE7-BC5E4BE44A26}.Debug|Any CPU.Build.0 = Debug|Any CPU
279+
{3CF58D34-693C-408A-BFE7-BC5E4BE44A26}.Release|Any CPU.ActiveCfg = Release|Any CPU
280+
{3CF58D34-693C-408A-BFE7-BC5E4BE44A26}.Release|Any CPU.Build.0 = Release|Any CPU
275281
EndGlobalSection
276282
GlobalSection(SolutionProperties) = preSolution
277283
HideSolutionNode = FALSE
@@ -318,6 +324,7 @@ Global
318324
{C5B732C8-7AF3-41D3-B903-AEDFC392E5BA} = {0998E45F-8BCE-4791-A944-962CD54E2D80}
319325
{6CF94627-BA74-4336-88CD-7EDA20C8F292} = {271C9F30-F679-4793-942B-0D9527CB3E2F}
320326
{998D178B-F4C7-48B5-BDEE-44E2F869BB22} = {0998E45F-8BCE-4791-A944-962CD54E2D80}
327+
{3CF58D34-693C-408A-BFE7-BC5E4BE44A26} = {271C9F30-F679-4793-942B-0D9527CB3E2F}
321328
EndGlobalSection
322329
GlobalSection(ExtensibilityGlobals) = postSolution
323330
SolutionGuid = {29204E0C-382A-49A0-A814-AD7FBF9774A5}

src/Java.Interop/Java.Interop/ManagedPeer.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,18 @@ namespace Java.Interop {
1414
[JniTypeSignature (JniTypeName)]
1515
/* static */ sealed class ManagedPeer : JavaObject {
1616

17+
delegate void ConstructDelegate (IntPtr jnienv,
18+
IntPtr klass,
19+
IntPtr n_self,
20+
IntPtr n_assemblyQualifiedName,
21+
IntPtr n_constructorSignature,
22+
IntPtr n_constructorArguments);
23+
delegate void RegisterDelegate (IntPtr jnienv,
24+
IntPtr klass,
25+
IntPtr n_nativeClass,
26+
IntPtr n_assemblyQualifiedName,
27+
IntPtr n_methods);
28+
1729
internal const string JniTypeName = "com/xamarin/java_interop/ManagedPeer";
1830

1931

@@ -25,11 +37,11 @@ static ManagedPeer ()
2537
new JniNativeMethodRegistration (
2638
"construct",
2739
ConstructSignature,
28-
(Action<IntPtr, IntPtr, IntPtr, IntPtr, IntPtr, IntPtr>) Construct),
40+
(ConstructDelegate) Construct),
2941
new JniNativeMethodRegistration (
3042
"registerNativeMembers",
3143
RegisterNativeMembersSignature,
32-
(Action<IntPtr, IntPtr, IntPtr, IntPtr, IntPtr>) RegisterNativeMembers)
44+
(RegisterDelegate) RegisterNativeMembers)
3345
);
3446
}
3547

src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,6 @@ public override string GetCurrentManagedThreadStackTrace (int skipFrames, bool f
147147

148148
protected override void Dispose (bool disposing)
149149
{
150-
var bridge = NativeMethods.java_interop_gc_bridge_get_current ();
151-
if (bridge != IntPtr.Zero) {
152-
NativeMethods.java_interop_gc_bridge_remove_current_app_domain (bridge);
153-
}
154150
base.Dispose (disposing);
155151
}
156152
}

src/Java.Runtime.Environment/Java.Interop/MonoRuntimeValueManager.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ protected override void Dispose (bool disposing)
9090
RegisteredInstances.Clear ();
9191
RegisteredInstances = null;
9292
}
93+
94+
if (bridge != IntPtr.Zero) {
95+
NativeMethods.java_interop_gc_bridge_remove_current_app_domain (bridge);
96+
bridge = IntPtr.Zero;
97+
}
9398
}
9499

95100
Dictionary<int, List<WeakReference<IJavaPeerable>>> RegisteredInstances = new Dictionary<int, List<WeakReference<IJavaPeerable>>>();

src/java-interop/java-interop.csproj

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project Sdk="Microsoft.Build.NoTargets">
22
<PropertyGroup>
3-
<TargetFramework>net472</TargetFramework>
3+
<TargetFrameworks>net472;netcoreapp3.1</TargetFrameworks>
44
<OutputPath>$(ToolOutputFullPath)</OutputPath>
55
<JNIEnvGenPath>$(BuildToolOutputFullPath)</JNIEnvGenPath>
66
<OutputName>java-interop</OutputName>
@@ -44,6 +44,9 @@
4444
<ClCompile Include="java-interop-dlfcn.cc" />
4545
<ClCompile Include="java-interop-jvm.cc" />
4646
<ClCompile Include="java-interop-logger.cc" />
47+
</ItemGroup>
48+
49+
<ItemGroup Condition=" '$(TargetFramework)' == 'net472' ">
4750
<ClCompile Include="java-interop-mono.cc" />
4851
<ClCompile Include="java-interop-gc-bridge-mono.cc" />
4952
</ItemGroup>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<Project>
2+
<Target Name="BuildJniEnvironment_g_cs"
3+
BeforeTargets="BeforeCompile"
4+
Inputs="$(_JNIEnvGenPath)"
5+
Outputs="jni.cs;jni.c">
6+
<Exec
7+
Command="$(_RunJNIEnvGen) jni.cs jni.c"
8+
/>
9+
</Target>
10+
11+
<ItemGroup>
12+
<_NativeLibsSrc Include="$(ToolOutputFullPath)\libjava-interop.*" />
13+
<_NativeLibsDst Include="@(_NativeLibsSrc->'$(OutputPath)%(Filename)%(Extension)')" />
14+
</ItemGroup>
15+
16+
<Target Name="CopyNativeLibs"
17+
BeforeTargets="BeforeCompile"
18+
Inputs="@(_NativeLibsSrc)"
19+
Outputs="@(_NativeLibsDst)">
20+
<Copy
21+
SourceFiles="@(_NativeLibsSrc)"
22+
DestinationFiles="@(_NativeLibsDst)"
23+
/>
24+
</Target>
25+
26+
<Target Name="Run">
27+
<MSBuild Projects="$(MSBuildThisFileDirectory)invocation-overhead.csproj"
28+
Properties="TargetFramework=net472"
29+
Targets="_Run_net472"
30+
/>
31+
<MSBuild Projects="$(MSBuildThisFileDirectory)invocation-overhead.csproj"
32+
Properties="TargetFramework=netcoreapp3.1"
33+
Targets="_Run_netcoreapp"
34+
/>
35+
</Target>
36+
37+
<Target Name="_Run_net472">
38+
<Message Text="Mono timing:" Importance="High" />
39+
<Exec Command="JI_JVM_PATH=&quot;$(JdkJvmPath)&quot; $(Runtime) $(TargetPath)" />
40+
</Target>
41+
42+
<Target Name="_Run_netcoreapp">
43+
<Message Text=".NET Core timing:" Importance="High" />
44+
<Exec Command="JI_JVM_PATH=&quot;$(JdkJvmPath)&quot; dotnet $(TargetPath)" />
45+
</Target>
46+
</Project>

tests/invocation-overhead/Makefile

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,12 @@
11
CONFIGURATION = Debug
2-
JNIENV_GEN = ../../bin/BuildDebug/jnienv-gen.exe
32

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

6-
clean:
7-
-rm test-overheads.exe test-overheads.exe.mdb
8-
-rm -Rf libJavaInterop.dylib*
9-
10-
include ../../build-tools/scripts/mono.mk
11-
include ../../build-tools/scripts/jdk.mk
12-
include ../../bin/BuildDebug/JdkInfo.mk
13-
include ../../build-tools/scripts/msbuild.mk
14-
15-
$(JNIENV_GEN):
16-
(cd ../../build-tools/jnienv-gen ; $(MSBUILD) $(MSBUILD_FLAGS) )
17-
18-
HANDLE_FEATURES = \
19-
-d:FEATURE_JNIENVIRONMENT_JI_INTPTRS \
20-
-d:FEATURE_JNIENVIRONMENT_JI_PINVOKES \
21-
-d:FEATURE_JNIENVIRONMENT_SAFEHANDLES \
22-
-d:FEATURE_JNIENVIRONMENT_XA_INTPTRS
5+
bin/$(CONFIGURATION)/net472/invocation-overheads.exe:
6+
msbuild /restore
237

24-
test-overheads.exe: test-overheads.cs jni.cs
25-
mcs -out:$@ -unsafe $(HANDLE_FEATURES) $^
26-
27-
jni.c jni.cs: $(JNIENV_GEN)
28-
$(RUNTIME) $< jni.cs jni.c
29-
30-
libjava-interop.dylib: jni.c
31-
gcc -g -shared -fPIC -o $@ $< -m64 -DJI_DLL_EXPORT -fvisibility=hidden $(JI_JDK_INCLUDE_PATHS:%=-I%)
8+
clean:
9+
msbuild /t:Clean
3210

3311
run:
34-
$(RUNTIME) test-overheads.exe
12+
msbuild /t:Run /nologo /v:m

tests/invocation-overhead/README.md

Lines changed: 83 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,85 @@
1-
Timing:
1+
# JNI Invocation Overhead
22

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

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

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

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

15-
So...how bad is it?
17+
So...how bad is that?
1618

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

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

23-
Result:
24+
2. Xamarin.Android JNI handling from 2011 until Xamarin.Android 6.1 (2016)
25+
(`XAIntPtrTiming`)
26+
27+
This uses `IntPtr`s *everywhere*, e.g. `JNIEnv::CallObjectMethod()` returns
28+
an `IntPtr`.
29+
30+
3. "Happier Medium?" (`JIIntPtrTiming`)
31+
32+
`IntPtr`s everywhere means it's trivial to forget that
33+
a JNI handle is a GREF vs. an LREF vs… What if we used the same `JNIEnv`
34+
invocation logic as `XAIntPtrTiming`, but instead of `IntPtr`s everywhere
35+
we instead had a `JniObjectReference` structure?
36+
37+
4. "Optimize (3)" (`JIPinvokeTiming`)
38+
39+
(3) was slower than (2). What if we rethought the `JNIEnv`
40+
invocation logic and removed all the `Marshal.GetDelegateForFunctionPointer()`
41+
invocations with normal P/Invokes?
42+
43+
To compare these four strategies, `jnienv-gen.exe` was updated so that *all*
44+
of them could be emitted into the same `.cs` file, into separate namespaces.
45+
These "core" JNI bindings could then be used with to invoke
46+
`java.util.Arrays.binarySearch(int[], int)`, 10,000,000 times, and compare
47+
the results.
48+
49+
Result in 2015 (commit [25de1f38][25de]):
50+
51+
[25de]: https://github.com/xamarin/Java.Interop/commit/25de1f38bb6b3ef2d4c98d2d95923a4bd50d2ea0
2452

2553
# SafeHandle timing: 00:00:02.7913432
2654
# Average Invocation: 0.00027913432ms
27-
# JniObjectReference timing: 00:00:01.9809859
55+
# JIIntPtrTiming timing: 00:00:01.9809859
2856
# Average Invocation: 0.00019809859ms
2957

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

3362
Ouch.
3463

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

3766
# SafeHandle timing: 00:00:02.8118485
3867
# Average Invocation: 0.00028118485ms
39-
# JniObjectReference timing: 00:00:02.0061727
68+
# XAIntPtrTiming timing: 00:00:02.0061727
4069
# Average Invocation: 0.00020061727ms
4170

42-
The performance difference is comparable -- SafeHandles take ~1.4x as long to run, or
43-
IntPtrs take ~70% as long as using SafeHandles.
71+
The performance difference is comparable -- SafeHandles take ~1.4x as long to
72+
run, or IntPtrs take ~70% as long as using SafeHandles.
4473

45-
Interesting -- but probably not *that* interesting -- is that in an absolute sense, the `JniObjectReference`
46-
struct was *faster* than the `IntPtr` approach, even though `JniObjectReference` contains *both* an `IntPtr`
47-
*and* an enum -- and is thus bigger!
74+
Interesting -- but probably not *that* interesting -- is that in an absolute
75+
sense, the `JniObjectReference` struct was *faster* than the `IntPtr` approach,
76+
even though `JniObjectReference` contains *both* an `IntPtr` *and* an enum --
77+
and is thus bigger!
4878

4979
That doesn't make any sense.
5080

51-
Regardless, `JniObjectReference` doesn't appear to be *slower*, and thus should be a viable option here.
81+
Regardless, `JniObjectReference` doesn't appear to be *slower*, and thus should
82+
be a viable option here.
5283

5384
---
5485

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

92123
We should standardize on `JniObjectReference` (again).
124+
125+
## 2021 Timing Update
126+
127+
How do these timings compare in 2021 on Desktop Mono (macOS)?
128+
129+
# SafeTiming timing: 00:00:09.3850449
130+
# Average Invocation: 0.00093850449ms
131+
# XAIntPtrTiming timing: 00:00:04.4930288
132+
# Average Invocation: 0.00044930288ms
133+
# JIIntPtrTiming timing: 00:00:04.5563368
134+
# Average Invocation: 0.00045563368ms
135+
# JIPinvokeTiming timing: 00:00:03.4710383
136+
# Average Invocation: 0.00034710383ms
137+
138+
In an absolute sense, things are worse: 10e6 invocations in 2015 took 2-3sec.
139+
Now, they're taking at least 3.5sec.
140+
141+
In a relative sense, `SafeHandles` got *worse*, and takes 2.09x longer than
142+
`XAIntPtrTiming`, and 2.7x longer than `JIPinvokeTiming`!
143+
144+
What about .NET Core 3.1? After some finagling, *that* can work too!
145+
146+
# SafeTiming timing: 00:00:05.1734443
147+
# Average Invocation: 0.00051734443ms
148+
# XAIntPtrTiming timing: 00:00:03.1048897
149+
# Average Invocation: 0.00031048897ms
150+
# JIIntPtrTiming timing: 00:00:03.4353958
151+
# Average Invocation: 0.00034353958ms
152+
# JIPinvokeTiming timing: 00:00:02.7470934
153+
# Average Invocation: 0.00027470934000000004ms
154+
155+
Relative performance is a similar story: `SafeHandle`s are slowest.

0 commit comments

Comments
 (0)