From 827dc82cd4be31f68d4e211cac8224b4fd1699df Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 9 Nov 2026 11:04:49 +0500 Subject: [PATCH] perf(android): cache the thread's JNIEnv instead of calling GetEnv per JEnv --- NativeScript/ffi/jni/jsi/jni/JEnv.cpp | 24 +++++------- NativeScript/ffi/jni/jsi/jni/JEnv.h | 38 +++++++++++++++++-- NativeScript/ffi/jni/napi/jni/JEnv.cpp | 24 +++++------- NativeScript/ffi/jni/napi/jni/JEnv.h | 38 +++++++++++++++++-- NativeScript/runtime/android/jsi/Runtime.cpp | 2 + .../runtime/android/jsi/com_tns_Runtime.cpp | 5 +++ .../android/jsi/workers/WorkerWrapper.cpp | 1 + NativeScript/runtime/android/napi/Runtime.cpp | 2 + .../runtime/android/napi/com_tns_Runtime.cpp | 5 +++ .../android/napi/workers/WorkerWrapper.cpp | 1 + 10 files changed, 106 insertions(+), 34 deletions(-) diff --git a/NativeScript/ffi/jni/jsi/jni/JEnv.cpp b/NativeScript/ffi/jni/jsi/jni/JEnv.cpp index b37415507..1c27d1cbf 100644 --- a/NativeScript/ffi/jni/jsi/jni/JEnv.cpp +++ b/NativeScript/ffi/jni/jsi/jni/JEnv.cpp @@ -7,8 +7,7 @@ using namespace tns; using namespace std; -JEnv::JEnv() - : m_env(nullptr) { +JNIEnv *JEnv::GetEnvSlow() { JNIEnv *env = nullptr; jint ret = s_jvm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6); @@ -18,22 +17,17 @@ JEnv::JEnv() assert(env != nullptr); } - m_env = env; + return env; } -JEnv::JEnv(JNIEnv *jniEnv) { - jint ret = s_jvm->GetEnv(reinterpret_cast(&jniEnv), JNI_VERSION_1_6); - - if ((ret != JNI_OK) || (jniEnv == nullptr)) { - ret = s_jvm->AttachCurrentThread(&jniEnv, nullptr); - assert(ret == JNI_OK); - assert(jniEnv != nullptr); - } - - m_env = jniEnv; +void JEnv::PinThreadEnv(JNIEnv *env) { + s_threadEnv = env; + s_threadEnvPinned = true; } -JEnv::~JEnv() { +void JEnv::UnpinThreadEnv() { + s_threadEnv = nullptr; + s_threadEnvPinned = false; } JEnv::operator JNIEnv* () const { @@ -861,6 +855,8 @@ void JEnv::CheckForJavaException() { } JavaVM *JEnv::s_jvm = nullptr; +thread_local JNIEnv *JEnv::s_threadEnv = nullptr; +thread_local bool JEnv::s_threadEnvPinned = false; robin_hood::unordered_map JEnv::s_classCache; robin_hood::unordered_map JEnv::s_missingClasses; jclass JEnv::RUNTIME_CLASS = nullptr; diff --git a/NativeScript/ffi/jni/jsi/jni/JEnv.h b/NativeScript/ffi/jni/jsi/jni/JEnv.h index b1a544e4c..597cc6379 100644 --- a/NativeScript/ffi/jni/jsi/jni/JEnv.h +++ b/NativeScript/ffi/jni/jsi/jni/JEnv.h @@ -8,9 +8,9 @@ namespace tns { class JEnv { public: - JEnv(); + JEnv() : m_env(s_threadEnv != nullptr ? s_threadEnv : GetEnvSlow()) {} - JEnv(JNIEnv *jniEnv); + JEnv(JNIEnv *jniEnv) : m_env(jniEnv != nullptr ? jniEnv : JEnv().m_env) {} // Wrap an already-obtained JNIEnv* WITHOUT re-querying the JavaVM // (no GetEnv). Use only when the pointer is known to belong to the @@ -18,7 +18,7 @@ namespace tns { enum class Adopt { Trusted }; JEnv(JNIEnv *jniEnv, Adopt) : m_env(jniEnv) {} - ~JEnv(); + ~JEnv() = default; operator JNIEnv *() const; @@ -447,11 +447,43 @@ namespace tns { static void Init(JavaVM *jvm); + static void PinThreadEnv(JNIEnv *env); + + static void UnpinThreadEnv(); + + class EntryScope { + public: + explicit EntryScope(JNIEnv *env) : m_active(!s_threadEnvPinned), m_previous(s_threadEnv) { + if (m_active) { + s_threadEnv = env; + } + } + + ~EntryScope() { + if (m_active && !s_threadEnvPinned) { + s_threadEnv = m_previous; + } + } + + EntryScope(const EntryScope &) = delete; + + EntryScope &operator=(const EntryScope &) = delete; + + private: + bool m_active; + JNIEnv *m_previous; + }; + private: + static JNIEnv *GetEnvSlow(); + void CheckForJavaException(); JNIEnv *m_env; + static thread_local JNIEnv *s_threadEnv; + static thread_local bool s_threadEnvPinned; + static JavaVM *s_jvm; static jclass RUNTIME_CLASS; diff --git a/NativeScript/ffi/jni/napi/jni/JEnv.cpp b/NativeScript/ffi/jni/napi/jni/JEnv.cpp index 190b2817a..9ec0c0933 100644 --- a/NativeScript/ffi/jni/napi/jni/JEnv.cpp +++ b/NativeScript/ffi/jni/napi/jni/JEnv.cpp @@ -7,8 +7,7 @@ using namespace tns; using namespace std; -JEnv::JEnv() - : m_env(nullptr) { +JNIEnv *JEnv::GetEnvSlow() { JNIEnv *env = nullptr; jint ret = s_jvm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6); @@ -18,22 +17,17 @@ JEnv::JEnv() assert(env != nullptr); } - m_env = env; + return env; } -JEnv::JEnv(JNIEnv *jniEnv) { - jint ret = s_jvm->GetEnv(reinterpret_cast(&jniEnv), JNI_VERSION_1_6); - - if ((ret != JNI_OK) || (jniEnv == nullptr)) { - ret = s_jvm->AttachCurrentThread(&jniEnv, nullptr); - assert(ret == JNI_OK); - assert(jniEnv != nullptr); - } - - m_env = jniEnv; +void JEnv::PinThreadEnv(JNIEnv *env) { + s_threadEnv = env; + s_threadEnvPinned = true; } -JEnv::~JEnv() { +void JEnv::UnpinThreadEnv() { + s_threadEnv = nullptr; + s_threadEnvPinned = false; } JEnv::operator JNIEnv* () const { @@ -861,6 +855,8 @@ void JEnv::CheckForJavaException() { } JavaVM *JEnv::s_jvm = nullptr; +thread_local JNIEnv *JEnv::s_threadEnv = nullptr; +thread_local bool JEnv::s_threadEnvPinned = false; robin_hood::unordered_map JEnv::s_classCache; robin_hood::unordered_map JEnv::s_missingClasses; jclass JEnv::RUNTIME_CLASS = nullptr; diff --git a/NativeScript/ffi/jni/napi/jni/JEnv.h b/NativeScript/ffi/jni/napi/jni/JEnv.h index b1a544e4c..597cc6379 100644 --- a/NativeScript/ffi/jni/napi/jni/JEnv.h +++ b/NativeScript/ffi/jni/napi/jni/JEnv.h @@ -8,9 +8,9 @@ namespace tns { class JEnv { public: - JEnv(); + JEnv() : m_env(s_threadEnv != nullptr ? s_threadEnv : GetEnvSlow()) {} - JEnv(JNIEnv *jniEnv); + JEnv(JNIEnv *jniEnv) : m_env(jniEnv != nullptr ? jniEnv : JEnv().m_env) {} // Wrap an already-obtained JNIEnv* WITHOUT re-querying the JavaVM // (no GetEnv). Use only when the pointer is known to belong to the @@ -18,7 +18,7 @@ namespace tns { enum class Adopt { Trusted }; JEnv(JNIEnv *jniEnv, Adopt) : m_env(jniEnv) {} - ~JEnv(); + ~JEnv() = default; operator JNIEnv *() const; @@ -447,11 +447,43 @@ namespace tns { static void Init(JavaVM *jvm); + static void PinThreadEnv(JNIEnv *env); + + static void UnpinThreadEnv(); + + class EntryScope { + public: + explicit EntryScope(JNIEnv *env) : m_active(!s_threadEnvPinned), m_previous(s_threadEnv) { + if (m_active) { + s_threadEnv = env; + } + } + + ~EntryScope() { + if (m_active && !s_threadEnvPinned) { + s_threadEnv = m_previous; + } + } + + EntryScope(const EntryScope &) = delete; + + EntryScope &operator=(const EntryScope &) = delete; + + private: + bool m_active; + JNIEnv *m_previous; + }; + private: + static JNIEnv *GetEnvSlow(); + void CheckForJavaException(); JNIEnv *m_env; + static thread_local JNIEnv *s_threadEnv; + static thread_local bool s_threadEnvPinned; + static JavaVM *s_jvm; static jclass RUNTIME_CLASS; diff --git a/NativeScript/runtime/android/jsi/Runtime.cpp b/NativeScript/runtime/android/jsi/Runtime.cpp index b5c49f7df..1e69bbe3d 100644 --- a/NativeScript/runtime/android/jsi/Runtime.cpp +++ b/NativeScript/runtime/android/jsi/Runtime.cpp @@ -203,6 +203,7 @@ Runtime::Init(JNIEnv *_env, jobject obj, int runtimeId, jstring filesPath, jstri void Runtime::Init(JNIEnv *_env, jstring filesPath, jstring nativeLibsDir, bool verboseLoggingEnabled, bool isDebuggable, jstring packageName, jobjectArray args, jstring callingDir, int maxLogcatObjectSize, bool forceLog) { + JEnv::PinThreadEnv(_env); LogEnabled = verboseLoggingEnabled; auto filesRoot = ArgConverter::jstringToString(filesPath); @@ -576,6 +577,7 @@ void Runtime::DestroyRuntime() { m_finalizerQueue = nullptr; } is_destroying = true; + JEnv::UnpinThreadEnv(); engine::Runtime &rt = engineHost->GetRuntime(); if (m_looperTasks != nullptr) { m_looperTasks->Terminate(); diff --git a/NativeScript/runtime/android/jsi/com_tns_Runtime.cpp b/NativeScript/runtime/android/jsi/com_tns_Runtime.cpp index 20ab03d8f..74313fa8d 100644 --- a/NativeScript/runtime/android/jsi/com_tns_Runtime.cpp +++ b/NativeScript/runtime/android/jsi/com_tns_Runtime.cpp @@ -104,6 +104,7 @@ extern "C" JNIEXPORT void Java_com_tns_Runtime_setManualInstrumentationModeLegac // is interop that both flavours need, so only this one is compiled out. #if !defined(NS_JSI_HOST_RUNTIME) extern "C" JNIEXPORT void Java_com_tns_Runtime_initNativeScript(JNIEnv* _env, jobject obj, jint runtimeId, jstring filesPath, jstring nativeLibDir, jboolean verboseLoggingEnabled, jboolean isDebuggable, jstring packageName, jobjectArray args, jstring callingDir, jint maxLogcatObjectSize, jboolean forceLog) { + JEnv::EntryScope envScope(_env); try { DEBUG_WRITE("NativeScript Initializing!"); Runtime::Init(_env, obj, runtimeId, filesPath, nativeLibDir, verboseLoggingEnabled, isDebuggable, packageName, args, callingDir, maxLogcatObjectSize, forceLog); @@ -145,6 +146,7 @@ Runtime* TryGetRuntime(int runtimeId) { // embedder's bundler owns module loading. #if !defined(NS_JSI_HOST_RUNTIME) extern "C" JNIEXPORT void Java_com_tns_Runtime_runModule(JNIEnv* _env, jobject obj, jint runtimeId, jstring scriptFile) { + JEnv::EntryScope envScope(_env); auto runtime = TryGetRuntime(runtimeId); if (runtime == nullptr) { return; @@ -171,6 +173,7 @@ extern "C" JNIEXPORT void Java_com_tns_Runtime_runModule(JNIEnv* _env, jobject o #if !defined(NS_JSI_HOST_RUNTIME) extern "C" JNIEXPORT jobject Java_com_tns_Runtime_runScript(JNIEnv* _env, jobject obj, jint runtimeId, jstring scriptFile) { + JEnv::EntryScope envScope(_env); jobject result = nullptr; auto runtime = TryGetRuntime(runtimeId); @@ -198,6 +201,7 @@ extern "C" JNIEXPORT jobject Java_com_tns_Runtime_runScript(JNIEnv* _env, jobjec extern "C" JNIEXPORT jobject Java_com_tns_Runtime_callJSMethodNative(JNIEnv* _env, jobject obj, jint runtimeId, jint javaObjectID, jclass claz, jstring methodName,jint retType, jboolean isConstructor, jobjectArray packagedArgs) { + JEnv::EntryScope envScope(_env); jobject result = nullptr; auto runtime = TryGetRuntime(runtimeId); if (runtime == nullptr) return result; @@ -222,6 +226,7 @@ extern "C" JNIEXPORT jobject Java_com_tns_Runtime_callJSMethodNative(JNIEnv* _en extern "C" JNIEXPORT void Java_com_tns_Runtime_createJSInstanceNative(JNIEnv* _env, jobject obj, jint runtimeId, jobject javaObject, jint javaObjectID, jstring className) { + JEnv::EntryScope envScope(_env); auto runtime = TryGetRuntime(runtimeId); if (runtime == nullptr) return; diff --git a/NativeScript/runtime/android/jsi/workers/WorkerWrapper.cpp b/NativeScript/runtime/android/jsi/workers/WorkerWrapper.cpp index 86c26c576..719633dcb 100644 --- a/NativeScript/runtime/android/jsi/workers/WorkerWrapper.cpp +++ b/NativeScript/runtime/android/jsi/workers/WorkerWrapper.cpp @@ -451,6 +451,7 @@ void WorkerWrapper::BackgroundLooper(std::shared_ptr self) { // ART aborts if a native thread exits while still attached. This must be // the very last JNI-touching action on this thread. + JEnv::UnpinThreadEnv(); jvm->DetachCurrentThread(); } diff --git a/NativeScript/runtime/android/napi/Runtime.cpp b/NativeScript/runtime/android/napi/Runtime.cpp index 382b5624e..786e3327b 100644 --- a/NativeScript/runtime/android/napi/Runtime.cpp +++ b/NativeScript/runtime/android/napi/Runtime.cpp @@ -214,6 +214,7 @@ napi_value Runtime::GlobalAccessorCallback(napi_env env, napi_callback_info inf void Runtime::Init(JNIEnv *_env, jstring filesPath, jstring nativeLibsDir, bool verboseLoggingEnabled, bool isDebuggable, jstring packageName, jobjectArray args, jstring callingDir, int maxLogcatObjectSize, bool forceLog) { + JEnv::PinThreadEnv(_env); LogEnabled = verboseLoggingEnabled; auto filesRoot = ArgConverter::jstringToString(filesPath); @@ -508,6 +509,7 @@ void Runtime::DestroyRuntime() { return; } is_destroying = true; + JEnv::UnpinThreadEnv(); delete m_loopTimer; m_loopTimer = nullptr; napi_status status; diff --git a/NativeScript/runtime/android/napi/com_tns_Runtime.cpp b/NativeScript/runtime/android/napi/com_tns_Runtime.cpp index 452104588..cbb4b594b 100644 --- a/NativeScript/runtime/android/napi/com_tns_Runtime.cpp +++ b/NativeScript/runtime/android/napi/com_tns_Runtime.cpp @@ -94,6 +94,7 @@ extern "C" JNIEXPORT void Java_com_tns_Runtime_setManualInstrumentationModeLegac } extern "C" JNIEXPORT void Java_com_tns_Runtime_initNativeScript(JNIEnv* _env, jobject obj, jint runtimeId, jstring filesPath, jstring nativeLibDir, jboolean verboseLoggingEnabled, jboolean isDebuggable, jstring packageName, jobjectArray args, jstring callingDir, jint maxLogcatObjectSize, jboolean forceLog) { + JEnv::EntryScope envScope(_env); try { DEBUG_WRITE("NativeScript Initializing!"); Runtime::Init(_env, obj, runtimeId, filesPath, nativeLibDir, verboseLoggingEnabled, isDebuggable, packageName, args, callingDir, maxLogcatObjectSize, forceLog); @@ -130,6 +131,7 @@ Runtime* TryGetRuntime(int runtimeId) { } extern "C" JNIEXPORT void Java_com_tns_Runtime_runModule(JNIEnv* _env, jobject obj, jint runtimeId, jstring scriptFile) { + JEnv::EntryScope envScope(_env); auto runtime = TryGetRuntime(runtimeId); if (runtime == nullptr) { return; @@ -153,6 +155,7 @@ extern "C" JNIEXPORT void Java_com_tns_Runtime_runModule(JNIEnv* _env, jobject o } extern "C" JNIEXPORT jobject Java_com_tns_Runtime_runScript(JNIEnv* _env, jobject obj, jint runtimeId, jstring scriptFile) { + JEnv::EntryScope envScope(_env); jobject result = nullptr; auto runtime = TryGetRuntime(runtimeId); @@ -178,6 +181,7 @@ extern "C" JNIEXPORT jobject Java_com_tns_Runtime_runScript(JNIEnv* _env, jobjec } extern "C" JNIEXPORT jobject Java_com_tns_Runtime_callJSMethodNative(JNIEnv* _env, jobject obj, jint runtimeId, jint javaObjectID, jclass claz, jstring methodName,jint retType, jboolean isConstructor, jobjectArray packagedArgs) { + JEnv::EntryScope envScope(_env); jobject result = nullptr; auto runtime = TryGetRuntime(runtimeId); if (runtime == nullptr) return result; @@ -202,6 +206,7 @@ extern "C" JNIEXPORT jobject Java_com_tns_Runtime_callJSMethodNative(JNIEnv* _en extern "C" JNIEXPORT void Java_com_tns_Runtime_createJSInstanceNative(JNIEnv* _env, jobject obj, jint runtimeId, jobject javaObject, jint javaObjectID, jstring className) { + JEnv::EntryScope envScope(_env); auto runtime = TryGetRuntime(runtimeId); if (runtime == nullptr) return; diff --git a/NativeScript/runtime/android/napi/workers/WorkerWrapper.cpp b/NativeScript/runtime/android/napi/workers/WorkerWrapper.cpp index c16efaa1e..a7da02ad6 100644 --- a/NativeScript/runtime/android/napi/workers/WorkerWrapper.cpp +++ b/NativeScript/runtime/android/napi/workers/WorkerWrapper.cpp @@ -528,6 +528,7 @@ void WorkerWrapper::BackgroundLooper(std::shared_ptr self) { // ART aborts if a native thread exits while still attached. This must be // the very last JNI-touching action on this thread. + JEnv::UnpinThreadEnv(); jvm->DetachCurrentThread(); }