Repository navigation
Parse each Java TurboModule method's JNI signature once - #59414
Open
hryhoriiK97 wants to merge 1 commit into
Open
hryhoriiK97 wants to merge 1 commit into
hryhoriiK97 wants to merge 1 commit into
Conversation
Visual regressionNo visual differences at android: no capture |
javache
reviewed
Oct 28, 2026
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); |
Contributor
There was a problem hiding this comment.
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.
Contributor
Author
There was a problem hiding this comment.
@javache, thank you for the comment! I'll create a follow-up PR. 👍
javache
approved these changes
Oct 28, 2026
|
@javache has imported this pull request. If you are a Meta employee, you can view this in D128040903. |
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
force-pushed
the
android-turbomodule-signature-cache
branch
from
October 28, 2026 12:06
8d39dca to
41b5c52
Compare
Contributor
Author
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 newstd::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 withsubstr. Long type names such asLcom/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
JavaTurboModulenow has oneMethodInfoper method (the argument types and the return type), keyed by the method'sjmethodID, which is already resolved at that point. A mutex guards the map. Entries are never removed while the module is alive, andstd::unordered_mapdoesn'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:
JavaTurboModulenow holds astd::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 withstd::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.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.SampleTurboModulein a Release build I had to change two things locally, in the sample module only and in both builds: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 eachregisterForActivityResultcall.Toaston every call, so I turned that off for the measurement.NativeSampleTurboModulemethods in a loop (20,000 calls each for the sync methods, 5,000 for the callback), timed withperformance.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:getStringgetValue(number, string, object)getValueWithCallbackgetArray(3 items)getObject(small)For these rows, the ranges of the two arms don't overlap.
getNumber(1.28 → 1.14 µs),getBool, the 20-keygetObject,voidFuncand 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 andgetNumber(−20%).🤖 Generated with Claude Code