Skip to content

Commit f78bdd4

Browse files
authored
Follow-up to #59414: cache each Java TurboModule method's ID with its signature, by name
## Summary: This is a follow-up to #59414, suggested by @javache in [its review](#59414 (comment)): keep a method's `jmethodID` in the same per-module cache as its parsed JNI signature, and index that cache by method name, instead of having two caches. Before this change, a Java TurboModule method's ID was cached by its caller: every generated host function kept a `static jmethodID cachedMethodId` and passed it to `invokeJavaMethod` by reference, and `JavaInteropTurboModule` kept a `methodIDs_` vector for the same purpose. `JavaTurboModule` then used that ID as the key of its `MethodInfo` cache (#59414). With this change: - `JavaTurboModule::MethodInfo` also holds the method's `jmethodID`, and `methodInfos_` is keyed by the method name (a `std::string`, looked up through a `std::string_view` without allocating). On a method's first call, `getMethodInfo` looks the method up with `GetMethodID` and parses its signature. A signature that doesn't match still turns into a JS error (redbox) before anything is called, and nothing is cached for it. - `invokeJavaMethod` gets an overload that takes the method name and JNI signature as C strings and no `cachedMethodID`. Generated code passes string literals, so a call no longer builds two `std::string`s; the JNI signature is usually longer than the small-string buffer, so that was a heap allocation on every call. - The previous overload (`const std::string&` name and signature, `jmethodID&`) is kept, for code generated by earlier codegen versions and for modules that call it directly, and marked `[[deprecated]]`. It forwards to the new one; `cachedMethodID` is no longer read or written. Libraries that ship pre-generated JNI code build it with `-Wall -Werror` (the codegen CMake template applies `target_compile_reactnative_options`), so their builds will fail on the deprecation warning until they regenerate that code. - The codegen template for Java module JNI files (`GenerateModuleJniCpp`) no longer emits `static jmethodID cachedMethodId`, and `JavaInteropTurboModule` drops `methodIDs_`. Besides one cache instead of two, every generated host function gets smaller: RNTester's `libreactnative.so` (which includes `FBReactNativeSpec`) is 61 KB smaller and its `libappmodules.so` 9.4 KB smaller. ## Changelog: [ANDROID] [CHANGED] - Java TurboModules cache each method's ID with its parsed signature, by name, instead of in every generated host function [ANDROID] [DEPRECATED] - `JavaTurboModule::invokeJavaMethod` with a `cachedMethodID` argument; use the overload without it ## Test Plan: - `yarn jest packages/react-native-codegen` passes (64 suites, 3,130 tests); the `GenerateModuleJniCpp` snapshots are updated for the template change. - `node scripts/clang-format.js --check` reports no changes for the touched C++ files. - C++ API snapshots: the new `invokeJavaMethod` overload is added to the four `ReactAndroid*Cxx.api` snapshots by hand, where the generator sorts it; `scripts/cxx-api` can't run on current `main` locally (its codegen step fails with `SyntaxError: Unexpected token '{'` while processing `FBReactNativeSpec`, without this change too). - `./gradlew :packages:rn-tester:android:app:assembleRelease -PreactNativeArchitectures=arm64-v8a` succeeds with this change; RNTester's generated `FBReactNativeSpec` and app module code use the new overload. - **On a device.** Two RNTester release APKs built from the same local checkout (`main` at f7de203 with local benchmark screens), without and with this change, installed alternately on a moto g57 power (Android 16), three installs each with two launches. A Playground screen times synchronous calls to `NativeSampleTurboModule` (20,000 calls each, 5,000 for void, callback and promise calls), median of the six launches: | Call (JNI signature) | before | this PR | change | |---|--:|--:|--:| | `getBool` (`(Z)Z`) | 1.10 µs | 1.15 µs | +4% | | `getNumber` (`(D)D`) | 1.46 µs | 1.53 µs | +5%, ranges overlap | | `getString` (`(Ljava/lang/String;)Ljava/lang/String;`) | 2.05 µs | 1.92 µs | −6% | | `getArray`, 3 items | 10.34 µs | 10.06 µs | −3% | | `getObject`, 3 keys | 9.90 µs | 9.58 µs | −3% | | `getObject`, 20 keys | 35.67 µs | 35.28 µs | −1% | | `getValue(number, string, object)` | 13.94 µs | 14.21 µs | +2%, within noise | | `voidFunc` | 4.55 µs | 4.59 µs | within noise | | `getValueWithCallback` | 8.54 µs | 7.99 µs | −6.5% | | `getValueWithPromise` | 18.26 µs | 17.38 µs | −5% | Calls whose JNI signature is longer than the small-string buffer no longer allocate it, and are 5–6% cheaper. The cheapest calls, whose short signatures never allocated, now hash the method name instead of a pointer, about 50 ns more. Native library sizes are from the same two APKs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Differential Revision: D128349302 Pull Request resolved: #59619
1 parent 3aee67b commit f78bdd4

10 files changed

Lines changed: 137 additions & 126 deletions

File tree

‎packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,7 @@ const HostFunctionTemplate = ({
5454
jsReturnType: JSReturnType,
5555
}>) => {
5656
return `static facebook::jsi::Value __hostFunction_${hasteModuleName}SpecJSI_${propertyName}(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
57-
static jmethodID cachedMethodId = nullptr;
58-
return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, ${jsReturnType}, "${propertyName}", "${jniSignature}", args, count, cachedMethodId);
57+
return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, ${jsReturnType}, "${propertyName}", "${jniSignature}", args, count);
5958
}`;
6059
};
6160

‎packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJniCpp-test.js.snap‎

Lines changed: 38 additions & 76 deletions
Large diffs are not rendered by default.

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

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ JavaInteropTurboModule::JavaInteropTurboModule(
4242
const std::vector<MethodDescriptor>& methodDescriptors)
4343
: JavaTurboModule(params),
4444
methodDescriptors_(methodDescriptors),
45-
methodIDs_(methodDescriptors.size()),
4645
constantsCache_(jsi::Value::undefined()) {
4746
for (const auto& methodDescriptor : methodDescriptors) {
4847
methodMap_[methodDescriptor.methodName] = MethodMetadata{
@@ -73,11 +72,10 @@ jsi::Value JavaInteropTurboModule::create(
7372
jsi::Value ret = this->invokeJavaMethod(
7473
rt,
7574
this->methodDescriptors_[i].jsiReturnKind,
76-
this->methodDescriptors_[i].methodName,
77-
this->methodDescriptors_[i].jniSignature,
75+
this->methodDescriptors_[i].methodName.c_str(),
76+
this->methodDescriptors_[i].jniSignature.c_str(),
7877
args,
79-
count,
80-
this->methodIDs_[i]);
78+
count);
8179

8280
bool isRetValid = ret.isUndefined() || ret.isNull() ||
8381
(ret.isObject() && !ret.asObject(rt).isFunction(rt));
@@ -112,11 +110,10 @@ jsi::Value JavaInteropTurboModule::create(
112110
return this->invokeJavaMethod(
113111
rt,
114112
this->methodDescriptors_[i].jsiReturnKind,
115-
this->methodDescriptors_[i].methodName,
116-
this->methodDescriptors_[i].jniSignature,
113+
this->methodDescriptors_[i].methodName.c_str(),
114+
this->methodDescriptors_[i].jniSignature.c_str(),
117115
args,
118-
count,
119-
this->methodIDs_[i]);
116+
count);
120117
});
121118
}
122119
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ class JSI_EXPORT JavaInteropTurboModule : public JavaTurboModule {
4040

4141
private:
4242
std::vector<MethodDescriptor> methodDescriptors_;
43-
std::vector<jmethodID> methodIDs_;
4443
jsi::Value constantsCache_;
4544

4645
const jsi::Value &getConstants(jsi::Runtime &runtime);

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

Lines changed: 59 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ int32_t getUniqueId() {
311311
JNIArgs convertJSIArgsToJNIArgs(
312312
JNIEnv* env,
313313
jsi::Runtime& rt,
314-
const std::string& methodName,
314+
const char* methodName,
315315
const std::vector<std::string>& methodArgTypes,
316316
const jsi::Value* args,
317317
size_t count,
@@ -588,34 +588,57 @@ void rejectWithException(
588588

589589
} // namespace
590590

591-
const JavaTurboModule::MethodInfo& JavaTurboModule::getMethodInfo(
592-
jmethodID methodID,
593-
const std::string& methodSignature) {
591+
const JavaTurboModule::MethodInfo* JavaTurboModule::getMethodInfo(
592+
JNIEnv* env,
593+
const char* methodName,
594+
const char* methodSignature) {
594595
std::lock_guard<std::mutex> lock(methodInfosMutex_);
595-
auto entry = methodInfos_.find(methodID);
596+
auto entry = methodInfos_.find(std::string_view(methodName));
596597
if (entry == methodInfos_.end()) {
597-
entry =
598-
methodInfos_
599-
.emplace(
600-
methodID,
601-
MethodInfo{
602-
.argTypes = getMethodArgTypesFromSignature(methodSignature),
603-
.returnType = methodSignature.substr(
604-
methodSignature.find_last_of(')') + 1)})
605-
.first;
598+
jclass cls = env->GetObjectClass(instance_.get());
599+
jmethodID methodID = env->GetMethodID(cls, methodName, methodSignature);
600+
env->DeleteLocalRef(cls);
601+
if (methodID == nullptr) {
602+
return nullptr;
603+
}
604+
std::string signature(methodSignature);
605+
entry = methodInfos_
606+
.emplace(
607+
methodName,
608+
MethodInfo{
609+
.methodID = methodID,
610+
.argTypes = getMethodArgTypesFromSignature(signature),
611+
.returnType =
612+
signature.substr(signature.find_last_of(')') + 1)})
613+
.first;
606614
}
607-
return entry->second;
615+
return &entry->second;
608616
}
609617

610618
jsi::Value JavaTurboModule::invokeJavaMethod(
611619
jsi::Runtime& runtime,
612620
TurboModuleMethodValueKind valueKind,
613-
const std::string& methodNameStr,
621+
const std::string& methodName,
614622
const std::string& methodSignature,
615623
const jsi::Value* args,
616624
size_t argCount,
617-
jmethodID& methodID) {
618-
const char* methodName = methodNameStr.c_str();
625+
jmethodID& /*cachedMethodID*/) {
626+
return invokeJavaMethod(
627+
runtime,
628+
valueKind,
629+
methodName.c_str(),
630+
methodSignature.c_str(),
631+
args,
632+
argCount);
633+
}
634+
635+
jsi::Value JavaTurboModule::invokeJavaMethod(
636+
jsi::Runtime& runtime,
637+
TurboModuleMethodValueKind valueKind,
638+
const char* methodName,
639+
const char* methodSignature,
640+
const jsi::Value* args,
641+
size_t argCount) {
619642
const char* moduleName = name_.c_str();
620643

621644
bool isMethodSync = isSyncMethod(valueKind);
@@ -679,17 +702,21 @@ jsi::Value JavaTurboModule::invokeJavaMethod(
679702
}
680703
};
681704

682-
if (methodID == nullptr) {
683-
jclass cls = env->GetObjectClass(instance);
684-
methodID = env->GetMethodID(cls, methodName, methodSignature.c_str());
685-
705+
const MethodInfo* methodInfo =
706+
getMethodInfo(env, methodName, methodSignature);
707+
if (methodInfo == nullptr) {
686708
// If the method signature doesn't match, show a redbox here instead of
687709
// crashing later.
688710
checkJNIErrorForMethodCall();
711+
throw jsi::JSError(
712+
runtime,
713+
"Unable to find method " + std::string(methodName) + "(" +
714+
methodSignature + ") on " + name_);
689715
}
716+
jmethodID methodID = methodInfo->methodID;
690717

691718
// TODO(T43933641): Refactor to remove this special-casing
692-
if (methodNameStr == "getConstants") {
719+
if (std::strcmp(methodName, "getConstants") == 0) {
693720
TMPL::syncMethodCallArgConversionEnd(moduleName, methodName);
694721
TMPL::syncMethodCallExecutionStart(moduleName, methodName);
695722

@@ -708,13 +735,11 @@ jsi::Value JavaTurboModule::invokeJavaMethod(
708735
return returnValue;
709736
}
710737

711-
const MethodInfo& methodInfo = getMethodInfo(methodID, methodSignature);
712-
713738
JNIArgs jniArgs = convertJSIArgsToJNIArgs(
714739
env,
715740
runtime,
716-
methodNameStr,
717-
methodInfo.argTypes,
741+
methodName,
742+
methodInfo->argTypes,
718743
args,
719744
argCount,
720745
jsInvoker_,
@@ -730,7 +755,7 @@ jsi::Value JavaTurboModule::invokeJavaMethod(
730755

731756
switch (valueKind) {
732757
case BooleanKind: {
733-
const std::string& returnType = methodInfo.returnType;
758+
const std::string& returnType = methodInfo->returnType;
734759
if (returnType == "Ljava/lang/Boolean;") {
735760
auto returnObject =
736761
env->CallObjectMethodA(instance, methodID, jargs.data());
@@ -766,7 +791,7 @@ jsi::Value JavaTurboModule::invokeJavaMethod(
766791
}
767792
}
768793
case NumberKind: {
769-
const std::string& returnType = methodInfo.returnType;
794+
const std::string& returnType = methodInfo->returnType;
770795
if (returnType == "Ljava/lang/Double;" ||
771796
returnType == "Ljava/lang/Float;" ||
772797
returnType == "Ljava/lang/Integer;") {
@@ -907,7 +932,7 @@ jsi::Value JavaTurboModule::invokeJavaMethod(
907932
methodID,
908933
instance_ = jni::make_weak(instance_),
909934
moduleNameStr = name_,
910-
methodNameStr,
935+
methodNameStr = std::string(methodName),
911936
id = getUniqueId()]() mutable {
912937
TraceSection s(
913938
"JavaTurboModuleAsyncMethodInvocation",
@@ -1005,8 +1030,6 @@ jsi::Value JavaTurboModule::invokeJavaMethod(
10051030
.utf8(runtime);
10061031
}
10071032

1008-
const char* moduleName = name_.c_str();
1009-
const char* methodName = methodNameStr.c_str();
10101033
TMPL::asyncMethodCallArgConversionEnd(moduleName, methodName);
10111034
TMPL::asyncMethodCallDispatch(moduleName, methodName);
10121035
nativeMethodCallInvoker_->invokeAsync(
@@ -1017,7 +1040,7 @@ jsi::Value JavaTurboModule::invokeJavaMethod(
10171040
methodID,
10181041
instance_ = jni::make_weak(instance_),
10191042
moduleNameStr = name_,
1020-
methodNameStr,
1043+
methodNameStr = std::string(methodName),
10211044
id = getUniqueId()]() mutable {
10221045
TraceSection s(
10231046
"JavaTurboModuleAsyncMethodInvocation",
@@ -1073,7 +1096,8 @@ jsi::Value JavaTurboModule::invokeJavaMethod(
10731096
if (!returnRef->isInstanceOf(JArrayBuffer::javaClassStatic())) {
10741097
throw jsi::JSError(
10751098
runtime,
1076-
"JavaTurboModule::invokeJavaMethod: expected " + methodNameStr +
1099+
"JavaTurboModule::invokeJavaMethod: expected " +
1100+
std::string(methodName) +
10771101
" to return a com.facebook.react.bridge.ArrayBuffer.");
10781102
}
10791103

@@ -1091,7 +1115,7 @@ jsi::Value JavaTurboModule::invokeJavaMethod(
10911115
}
10921116
default:
10931117
throw std::runtime_error(
1094-
"Unable to find method module: " + methodNameStr + "(" +
1118+
"Unable to find method module: " + std::string(methodName) + "(" +
10951119
methodSignature + ")");
10961120
}
10971121
}

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

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99

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

12+
#include <functional>
1213
#include <mutex>
1314
#include <string>
15+
#include <string_view>
1416
#include <unordered_map>
1517
#include <vector>
1618

@@ -43,6 +45,19 @@ class JSI_EXPORT JavaTurboModule : public TurboModule {
4345
JavaTurboModule(const InitParams &params);
4446
virtual ~JavaTurboModule();
4547

48+
// Looks the method up on its first call and caches it by name.
49+
jsi::Value invokeJavaMethod(
50+
jsi::Runtime &runtime,
51+
TurboModuleMethodValueKind valueKind,
52+
const char *methodName,
53+
const char *methodSignature,
54+
const jsi::Value *args,
55+
size_t argCount);
56+
57+
// Kept for code generated by earlier versions of codegen and for modules
58+
// that call it directly. cachedMethodID is neither read nor written: the
59+
// method is cached by name.
60+
[[deprecated("Use the overload without cachedMethodID")]]
4661
jsi::Value invokeJavaMethod(
4762
jsi::Runtime &runtime,
4863
TurboModuleMethodValueKind valueKind,
@@ -65,14 +80,25 @@ class JSI_EXPORT JavaTurboModule : public TurboModule {
6580
jni::global_ref<jobject> instance_;
6681
std::shared_ptr<NativeMethodCallInvoker> nativeMethodCallInvoker_;
6782

68-
// A method's JNI signature, parsed on its first call since it doesn't change.
83+
// A method's ID and parsed JNI signature, looked up on its first call since they don't change.
6984
struct MethodInfo {
85+
jmethodID methodID;
7086
std::vector<std::string> argTypes;
7187
std::string returnType;
7288
};
73-
std::unordered_map<jmethodID, MethodInfo> methodInfos_;
89+
struct MethodNameHash {
90+
using is_transparent = void;
91+
size_t operator()(std::string_view methodName) const
92+
{
93+
return std::hash<std::string_view>{}(methodName);
94+
}
95+
};
96+
// Keyed by the method's name, which is unique in a TurboModule; looked up
97+
// without allocating a std::string.
98+
std::unordered_map<std::string, MethodInfo, MethodNameHash, std::equal_to<>> methodInfos_;
7499
std::mutex methodInfosMutex_;
75-
const MethodInfo &getMethodInfo(jmethodID methodID, const std::string &methodSignature);
100+
// Returns nullptr, with a pending Java exception, if the method doesn't exist.
101+
const MethodInfo *getMethodInfo(JNIEnv *env, const char *methodName, const char *methodSignature);
76102
};
77103

78104
} // namespace facebook::react

‎scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1694,6 +1694,7 @@ class facebook::react::JavaTurboModule : public facebook::react::TurboModule {
16941694
protected void configureEventEmitterCallback();
16951695
protected void setEventEmitterCallback(jni::alias_ref<jobject>);
16961696
public JavaTurboModule(const facebook::react::JavaTurboModule::InitParams& params);
1697+
public facebook::jsi::Value invokeJavaMethod(facebook::jsi::Runtime& runtime, facebook::react::TurboModuleMethodValueKind valueKind, const char* methodName, const char* methodSignature, const facebook::jsi::Value* args, size_t argCount);
16971698
public facebook::jsi::Value invokeJavaMethod(facebook::jsi::Runtime& runtime, facebook::react::TurboModuleMethodValueKind valueKind, const std::string& methodName, const std::string& methodSignature, const facebook::jsi::Value* args, size_t argCount, jmethodID& cachedMethodID);
16981699
public virtual ~JavaTurboModule();
16991700
}

‎scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2499,6 +2499,7 @@ class facebook::react::JavaTurboModule : public facebook::react::TurboModule {
24992499
protected void configureEventEmitterCallback();
25002500
protected void setEventEmitterCallback(jni::alias_ref<jobject>);
25012501
public JavaTurboModule(const facebook::react::JavaTurboModule::InitParams& params);
2502+
public facebook::jsi::Value invokeJavaMethod(facebook::jsi::Runtime& runtime, facebook::react::TurboModuleMethodValueKind valueKind, const char* methodName, const char* methodSignature, const facebook::jsi::Value* args, size_t argCount);
25022503
public facebook::jsi::Value invokeJavaMethod(facebook::jsi::Runtime& runtime, facebook::react::TurboModuleMethodValueKind valueKind, const std::string& methodName, const std::string& methodSignature, const facebook::jsi::Value* args, size_t argCount, jmethodID& cachedMethodID);
25032504
public virtual ~JavaTurboModule();
25042505
}

‎scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1693,6 +1693,7 @@ class facebook::react::JavaTurboModule : public facebook::react::TurboModule {
16931693
protected void configureEventEmitterCallback();
16941694
protected void setEventEmitterCallback(jni::alias_ref<jobject>);
16951695
public JavaTurboModule(const facebook::react::JavaTurboModule::InitParams& params);
1696+
public facebook::jsi::Value invokeJavaMethod(facebook::jsi::Runtime& runtime, facebook::react::TurboModuleMethodValueKind valueKind, const char* methodName, const char* methodSignature, const facebook::jsi::Value* args, size_t argCount);
16961697
public facebook::jsi::Value invokeJavaMethod(facebook::jsi::Runtime& runtime, facebook::react::TurboModuleMethodValueKind valueKind, const std::string& methodName, const std::string& methodSignature, const facebook::jsi::Value* args, size_t argCount, jmethodID& cachedMethodID);
16971698
public virtual ~JavaTurboModule();
16981699
}

‎scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1693,6 +1693,7 @@ class facebook::react::JavaTurboModule : public facebook::react::TurboModule {
16931693
protected void configureEventEmitterCallback();
16941694
protected void setEventEmitterCallback(jni::alias_ref<jobject>);
16951695
public JavaTurboModule(const facebook::react::JavaTurboModule::InitParams& params);
1696+
public facebook::jsi::Value invokeJavaMethod(facebook::jsi::Runtime& runtime, facebook::react::TurboModuleMethodValueKind valueKind, const char* methodName, const char* methodSignature, const facebook::jsi::Value* args, size_t argCount);
16961697
public facebook::jsi::Value invokeJavaMethod(facebook::jsi::Runtime& runtime, facebook::react::TurboModuleMethodValueKind valueKind, const std::string& methodName, const std::string& methodSignature, const facebook::jsi::Value* args, size_t argCount, jmethodID& cachedMethodID);
16971698
public virtual ~JavaTurboModule();
16981699
}

0 commit comments

Comments
 (0)