Skip to content

Commit f7de203

Browse files
authored
Parse each Java TurboModule method's JNI signature once
## 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](https://github.com/software-mansion/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](https://claude.com/claude-code) Differential Revision: D128040903 Pull Request resolved: #59414
1 parent 745b4f3 commit f7de203

2 files changed

Lines changed: 34 additions & 7 deletions

File tree

‎packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp‎

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,24 @@ void rejectWithException(
588588

589589
} // namespace
590590

591+
const JavaTurboModule::MethodInfo& JavaTurboModule::getMethodInfo(
592+
jmethodID methodID,
593+
const std::string& methodSignature) {
594+
std::lock_guard<std::mutex> lock(methodInfosMutex_);
595+
auto entry = methodInfos_.find(methodID);
596+
if (entry == methodInfos_.end()) {
597+
entry = methodInfos_
598+
.emplace(
599+
methodID,
600+
MethodInfo{
601+
getMethodArgTypesFromSignature(methodSignature),
602+
methodSignature.substr(
603+
methodSignature.find_last_of(')') + 1)})
604+
.first;
605+
}
606+
return entry->second;
607+
}
608+
591609
jsi::Value JavaTurboModule::invokeJavaMethod(
592610
jsi::Runtime& runtime,
593611
TurboModuleMethodValueKind valueKind,
@@ -689,14 +707,13 @@ jsi::Value JavaTurboModule::invokeJavaMethod(
689707
return returnValue;
690708
}
691709

692-
std::vector<std::string> methodArgTypes =
693-
getMethodArgTypesFromSignature(methodSignature);
710+
const MethodInfo& methodInfo = getMethodInfo(methodID, methodSignature);
694711

695712
JNIArgs jniArgs = convertJSIArgsToJNIArgs(
696713
env,
697714
runtime,
698715
methodNameStr,
699-
methodArgTypes,
716+
methodInfo.argTypes,
700717
args,
701718
argCount,
702719
jsInvoker_,
@@ -712,8 +729,7 @@ jsi::Value JavaTurboModule::invokeJavaMethod(
712729

713730
switch (valueKind) {
714731
case BooleanKind: {
715-
std::string returnType =
716-
methodSignature.substr(methodSignature.find_last_of(')') + 1);
732+
const std::string& returnType = methodInfo.returnType;
717733
if (returnType == "Ljava/lang/Boolean;") {
718734
auto returnObject =
719735
env->CallObjectMethodA(instance, methodID, jargs.data());
@@ -749,8 +765,7 @@ jsi::Value JavaTurboModule::invokeJavaMethod(
749765
}
750766
}
751767
case NumberKind: {
752-
std::string returnType =
753-
methodSignature.substr(methodSignature.find_last_of(')') + 1);
768+
const std::string& returnType = methodInfo.returnType;
754769
if (returnType == "Ljava/lang/Double;" ||
755770
returnType == "Ljava/lang/Float;" ||
756771
returnType == "Ljava/lang/Integer;") {

‎packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.h‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99

1010
#include <react/cxxstableapi/UmbrellaGuard.h>
1111

12+
#include <mutex>
1213
#include <string>
14+
#include <unordered_map>
15+
#include <vector>
1316

1417
#include <React/Bridging.h>
1518
#include <React/CallInvoker.h>
@@ -61,6 +64,15 @@ class JSI_EXPORT JavaTurboModule : public TurboModule {
6164
// instance_ can be of type JTurboModule, or JNativeModule
6265
jni::global_ref<jobject> instance_;
6366
std::shared_ptr<NativeMethodCallInvoker> nativeMethodCallInvoker_;
67+
68+
// A method's JNI signature, parsed on its first call since it doesn't change.
69+
struct MethodInfo {
70+
std::vector<std::string> argTypes;
71+
std::string returnType;
72+
};
73+
std::unordered_map<jmethodID, MethodInfo> methodInfos_;
74+
std::mutex methodInfosMutex_;
75+
const MethodInfo &getMethodInfo(jmethodID methodID, const std::string &methodSignature);
6476
};
6577

6678
} // namespace facebook::react

0 commit comments

Comments
 (0)