Skip to content

Parse each Java TurboModule method's JNI signature once - #59414

Open
hryhoriiK97 wants to merge 1 commit into
react:mainfrom
hryhoriiK97:android-turbomodule-signature-cache
Open

hryhoriiK97 wants to merge 1 commit into
react:mainfrom
hryhoriiK97:android-turbomodule-signature-cache

Conversation

@hryhoriiK97

@hryhoriiK97 hryhoriiK97 commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary:

This is the Android counterpart of #59394.

On Android, every call from JS to a Java TurboModule method goes through JavaTurboModule::invokeJavaMethod (ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp). On every call it parsed the method's JNI signature into a new std::vector<std::string> of argument types (getMethodArgTypesFromSignature), and for methods that return a number or a boolean it cut the return type out of the signature again with substr. Long type names such as Lcom/facebook/react/bridge/ReadableMap; don't fit in the small-string buffer, so each of them was a heap allocation on every call.

A method's signature doesn't change, so this change parses it on the method's first call and keeps the result. Each JavaTurboModule now has one MethodInfo per method (the argument types and the return type), keyed by the method's jmethodID, which is already resolved at that point. A mutex guards the map. Entries are never removed while the module is alive, and std::unordered_map doesn't move its elements, so the returned reference stays valid for the rest of the call. The members are private, so there is no API change.

Memory: one entry per method that is actually called, about 150–300 bytes each (the type strings and the map node). The same strings were already allocated on every call and freed right after.

One side effect: JavaTurboModule now holds a std::mutex, so it can't be copied or moved. Copying a module was already deprecated (it has a user-declared destructor) and nothing in the repository copies one; generated modules are created with std::make_shared.

Changelog:

[ANDROID] [CHANGED] - Parse each TurboModule method's JNI signature once instead of on every call

Test Plan:

  • clang-format --dry-run --Werror (the repository's pinned version) on the changed files passes.
  • RNTester built with Gradle in Release (assembleRelease, arm64-v8a) and run on a physical Samsung Galaxy A14 (Android 15). In the TurboModule example, "Run function call tests" returns the same values for all 18 methods with and without this change, including callbacks, a resolved and a rejected promise, and ArrayBuffers.
  • To run SampleTurboModule in a Release build I had to change two things locally, in the sample module only and in both builds:
    • In Release, creating it fails with A launcher is already registered for key 'com.facebook.fbreact.specs.SampleTurboModule:e2'. The launcher key is built from the contract class names, and R8 seems to merge the contract classes, so the three launchers get the same key. I passed an explicit key to each registerForActivityResult call.
    • It shows a Toast on every call, so I turned that off for the measurement.
  • Measurement. RNTester Playground calling NativeSampleTurboModule methods in a loop (20,000 calls each for the sync methods, 5,000 for the callback), timed with performance.now(). Two Release APKs, before and after this change, installed in the order before, after, after, before, 4 launches each after a warm-up launch. The presses were driven by Argent on the physical phone. Mean time per call:
Call before this PR change
getString 1.93 µs 1.66 µs −14%
getValue (number, string, object) 18.96 µs 17.62 µs −7%
getValueWithCallback 11.46 µs 10.66 µs −7%
getArray (3 items) 15.84 µs 14.83 µs −6%
getObject (small) 14.02 µs 13.19 µs −6%

For these rows, the ranges of the two arms don't overlap. getNumber (1.28 → 1.14 µs), getBool, the 20-key getObject, voidFunc and promises were within noise in this run. An earlier run of the same change behind a switch in one APK gave −5% to −20% for these calls and getNumber (−20%).

🤖 Generated with Claude Code

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 23, 2026
@facebook-github-tools facebook-github-tools Bot added p: Software Mansion Partner: Software Mansion Partner Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. labels Oct 23, 2026
@github-actions

Copy link
Copy Markdown

Visual regression

No visual differences at 8d39dcad167a.

android: no capture

Comment on lines +68 to +75
// A method's JNI signature, parsed on its first call since it doesn't change.
struct MethodInfo {
std::vector<std::string> argTypes;
std::string returnType;
};
std::unordered_map<jmethodID, MethodInfo> methodInfos_;
std::mutex methodInfosMutex_;
const MethodInfo &getMethodInfo(jmethodID methodID, const std::string &methodSignature);

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.

As a follow-up PR, let's actually store jmethodID here too, and index based on method name. I previously added it as a mutable method signature to act as a similar "cache", but it's actually simpler to have a single cache in the TurboModule instance.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@javache, thank you for the comment! I'll create a follow-up PR. 👍

@meta-codesync

meta-codesync Bot commented Oct 28, 2026

Copy link
Copy Markdown

@javache has imported this pull request. If you are a Meta employee, you can view this in D128040903.

@javache

javache commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

Please rebase on 3cee68f

JavaTurboModule::invokeJavaMethod parsed the method's JNI signature into a
vector of argument type strings on every call, and cut the return type
out of it again for number and boolean methods. Keep both in a MethodInfo
per method, keyed by its jmethodID, like ObjCTurboModule does on iOS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hryhoriiK97
hryhoriiK97 force-pushed the android-turbomodule-signature-cache branch from 8d39dca to 41b5c52 Compare October 28, 2026 12:06
@hryhoriiK97

Copy link
Copy Markdown
Contributor Author

Please rebase on 3cee68f

@javache done!

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. p: Software Mansion Partner: Software Mansion Partner Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants