Skip to content

[Java.Interop] Avoid locks for cached JNI member lookups - #12377

Open
jonathanpeppers wants to merge 1 commit into
mainfrom
jonathanpeppers-cache-jni-method-ids
Open

[Java.Interop] Avoid locks for cached JNI member lookups#12377
jonathanpeppers wants to merge 1 commit into
mainfrom
jonathanpeppers-cache-jni-method-ids

Conversation

@jonathanpeppers

@jonathanpeppers jonathanpeppers commented Aug 13, 2026

Copy link
Copy Markdown
Member

Description

JniPeerMembers cached JNI method and field IDs in dictionaries protected by
monitor locks. Every generated binding invocation paid that lock cost even
after the JNI member had already been resolved.

Use ConcurrentDictionary for the instance/static method and field caches,
plus subclass constructor dispatch. Cached reads no longer enter a monitor,
while first field resolution remains serialized and existing method
replacement, fallback, exception, and lifetime behavior is preserved.

This also adds an instrumentation-only Android BenchmarkDotNet app for
measuring JNI invocation changes on-device.

Related to #11885.

Performance

Pixel 5, .NET 11 CoreCLR:

Path Before After Change
Cached virtual JNI invocation 221.4 ns 180.7 ns -18%

Pixel 5 MAUI/CoreCLR cold startup, 40 counterbalanced pairs built from
identical restored packages:

APK Mean startup
Base 835.62 ms
Changed 832.85 ms

Paired changed-minus-base delta: -2.77 ms, with a 95% confidence
interval of -7.61 to +2.06 ms.

apkdiff reports:

Metric Change
APK size 0 bytes (0.00%)
Shared libraries -1,952 bytes (-0.01%)

Testing

  • Java.Interop managed build

  • JniPeerMembersTests: 8 passed, 1 skipped

  • Android BenchmarkDotNet instrumentation run

  • 40-pair counterbalanced MAUI/CoreCLR cold-start comparison

  • apkdiff package comparison

  • Useful description of why the change is necessary

  • Links to related issues

  • Unit tests

Copilot AI lite review requested due to automatic review settings August 13, 2026 16:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves Java.Interop JNI member lookup performance by removing monitor-lock overhead from cached method/field ID access in JniPeerMembers, and adds an Android on-device BenchmarkDotNet instrumentation app to measure JNI invocation changes.

Changes:

  • Replace lock-protected Dictionary caches with ConcurrentDictionary caches for JNI instance/static method and field lookups.
  • Preserve serialized first-time field resolution while making cached reads lock-free.
  • Add a new instrumentation-only Android BenchmarkDotNet app and wire it into the test solution.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
Xamarin.Android-Tests.slnx Adds the new Android benchmarks project to the tests solution.
tests/Android.Benchmarks/README.md Documents how to run the on-device BenchmarkDotNet instrumentation benchmarks.
tests/Android.Benchmarks/JniMethodInfoBenchmarks.cs Adds benchmarks comparing cached JNI invocation vs generated binding calls.
tests/Android.Benchmarks/BenchmarkInstrumentation.cs Implements the Android instrumentation entry point that runs BenchmarkDotNet in-process.
tests/Android.Benchmarks/AndroidManifest.xml Provides a minimal manifest for the instrumentation-only benchmark app.
tests/Android.Benchmarks/Android.Benchmarks.csproj Defines the Android benchmark app project and BenchmarkDotNet dependency.
external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs Updates test reflection expectations to match ConcurrentDictionary caches.
external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs Switches static method cache to ConcurrentDictionary and removes monitor locking on cached reads.
external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticFields.cs Switches static field cache to ConcurrentDictionary and adjusts the synchronization strategy.
external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs Switches instance method + subclass-constructor caches to ConcurrentDictionary and removes monitor locking on cached reads.
external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceFields.cs Switches instance field cache to ConcurrentDictionary and adjusts the synchronization strategy.

Comment thread tests/Android.Benchmarks/Android.Benchmarks.csproj
Use ConcurrentDictionary with static GetOrAdd factories for the
JniPeerMembers instance and static method and field caches, plus subclass
constructor dispatch. Cached JNI member lookups no longer enter monitor
locks, while existing remapping, fallback, exception, and lifetime behavior
is preserved.

Add an instrumentation-only Android BenchmarkDotNet app for measuring JNI
member lookup and invocation changes on-device.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 45264ff0-e493-4402-a76f-576afd88ee70
@jonathanpeppers
jonathanpeppers force-pushed the jonathanpeppers-cache-jni-method-ids branch from 66f4b4e to 5d91fc5 Compare August 13, 2026 16:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants