From 5d06918d0748df522ec9444d5742377095df609b Mon Sep 17 00:00:00 2001 From: Tadeu Zagallo Date: Mon, 11 Jul 2016 06:52:06 -0700 Subject: [PATCH] Add new FileSourceProvider Summary: Add a new interface to JSC that allows loading a file lazily from disk, i.e. using mmap, instead of loading the whole file upfront and copying into the VM. Reviewed By: michalgr Differential Revision: D3534042 fbshipit-source-id: 98b193cc7b7e33248073e2556ea94ce3391507c7 --- .../react/XReactInstanceManagerImpl.java | 5 +- .../react/cxxbridge/CatalystInstanceImpl.java | 2 +- .../react/cxxbridge/JSBundleLoader.java | 9 +- .../jni/xreact/jni/CatalystInstanceImpl.cpp | 125 +++++++++++++++++- .../jni/xreact/jni/CatalystInstanceImpl.h | 2 +- .../src/main/jni/xreact/jni/OnLoad.cpp | 8 +- ReactAndroid/src/main/jni/xreact/jni/OnLoad.h | 1 + ReactCommon/cxxreact/Executor.h | 64 +++++++++ ReactCommon/cxxreact/JSCExecutor.cpp | 23 ++++ ReactCommon/cxxreact/JSCHelpers.cpp | 90 +++++++------ ReactCommon/cxxreact/JSCHelpers.h | 12 ++ 11 files changed, 291 insertions(+), 50 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java index e510d129c..42911f885 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java @@ -412,9 +412,12 @@ import static com.facebook.systrace.Systrace.TRACE_TAG_REACT_JAVA_BRIDGE; } private void recreateReactContextInBackgroundFromBundleFile() { + boolean useLazyBundle = mJSCConfig.getConfigMap().hasKey("useLazyBundle") ? + mJSCConfig.getConfigMap().getBoolean("useLazyBundle") : false; + recreateReactContextInBackground( new JSCJavaScriptExecutor.Factory(mJSCConfig.getConfigMap()), - JSBundleLoader.createFileLoader(mApplicationContext, mJSBundleFile)); + JSBundleLoader.createFileLoader(mApplicationContext, mJSBundleFile, useLazyBundle)); } /** diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CatalystInstanceImpl.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CatalystInstanceImpl.java index 007495e76..569ae980e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CatalystInstanceImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CatalystInstanceImpl.java @@ -161,7 +161,7 @@ public class CatalystInstanceImpl implements CatalystInstance { MessageQueueThread moduleQueue, ModuleRegistryHolder registryHolder); - /* package */ native void loadScriptFromAssets(AssetManager assetManager, String assetURL); + /* package */ native void loadScriptFromAssets(AssetManager assetManager, String assetURL, boolean useLazyBundle); /* package */ native void loadScriptFromFile(String fileName, String sourceURL); @Override diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JSBundleLoader.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JSBundleLoader.java index c55b30ce6..7b4d6e499 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JSBundleLoader.java +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JSBundleLoader.java @@ -28,11 +28,18 @@ public abstract class JSBundleLoader { public static JSBundleLoader createFileLoader( final Context context, final String fileName) { + return createFileLoader(context, fileName, false); + } + + public static JSBundleLoader createFileLoader( + final Context context, + final String fileName, + final boolean useLazyBundle) { return new JSBundleLoader() { @Override public void loadScript(CatalystInstanceImpl instance) { if (fileName.startsWith("assets://")) { - instance.loadScriptFromAssets(context.getAssets(), fileName); + instance.loadScriptFromAssets(context.getAssets(), fileName, useLazyBundle); } else { instance.loadScriptFromFile(fileName, fileName); } diff --git a/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp b/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp index e9bf94140..612107b7c 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp @@ -13,6 +13,8 @@ #include #include +#include + #include #include #include @@ -23,6 +25,7 @@ #include "ModuleRegistryHolder.h" #include "NativeArray.h" #include "JNativeRunnable.h" +#include "OnLoad.h" using namespace facebook::jni; @@ -98,7 +101,7 @@ void CatalystInstanceImpl::registerNatives() { makeNativeMethod("initHybrid", CatalystInstanceImpl::initHybrid), makeNativeMethod("initializeBridge", CatalystInstanceImpl::initializeBridge), makeNativeMethod("loadScriptFromAssets", - "(Landroid/content/res/AssetManager;Ljava/lang/String;)V", + "(Landroid/content/res/AssetManager;Ljava/lang/String;Z)V", CatalystInstanceImpl::loadScriptFromAssets), makeNativeMethod("loadScriptFromFile", CatalystInstanceImpl::loadScriptFromFile), makeNativeMethod("callJSFunction", CatalystInstanceImpl::callJSFunction), @@ -149,20 +152,132 @@ void CatalystInstanceImpl::initializeBridge( mrh->getModuleRegistry()); } +#ifdef WITH_FBJSCEXTENSIONS +static std::unique_ptr loadScriptFromCache( + AAssetManager* manager, + std::string& sourceURL) { + + // 20-byte sha1 as hex + static const size_t HASH_STR_SIZE = 40; + + // load bundle hash from the metadata file in the APK + auto hash = react::loadScriptFromAssets(manager, sourceURL + ".meta"); + auto cacheDir = getApplicationCacheDir() + "/rn-bundle"; + auto encoding = static_cast(hash->c_str()[20]); + + if (mkdir(cacheDir.c_str(), 0755) == -1 && errno != EEXIST) { + throw std::runtime_error("Can't create cache directory"); + } + + if (encoding != JSBigMmapString::Encoding::Ascii) { + throw std::runtime_error("Can't use mmap fastpath for non-ascii bundles"); + } + + // convert hash to string + char hashStr[HASH_STR_SIZE + 1]; + for (size_t i = 0; i < HASH_STR_SIZE; i += 2) { + snprintf(hashStr + i, 3, "%02hhx", hash->c_str()[i / 2] & 0xFF); + } + + // the name of the cached bundle file should be the hash + std::string cachePath = cacheDir + "/" + hashStr; + FILE *cache = fopen(cachePath.c_str(), "r"); + SCOPE_EXIT { if (cache) fclose(cache); }; + + size_t size = 0; + if (cache == NULL) { + // delete old bundle, if there was one. + std::string metaPath = cacheDir + "/meta"; + if (auto meta = fopen(metaPath.c_str(), "r")) { + char oldBundleHash[HASH_STR_SIZE + 1]; + if (fread(oldBundleHash, HASH_STR_SIZE, 1, meta) == HASH_STR_SIZE) { + remove((cacheDir + "/" + oldBundleHash).c_str()); + remove(metaPath.c_str()); + } + fclose(meta); + } + + // load script from the APK and write to temporary file + auto script = react::loadScriptFromAssets(manager, sourceURL); + auto tmpPath = cachePath + "_"; + cache = fopen(tmpPath.c_str(), "w"); + if (!cache) { + throw std::runtime_error("Can't open cache, errno: " + errno); + } + if (fwrite(script->c_str(), 1, script->size(), cache) != size) { + remove(tmpPath.c_str()); + throw std::runtime_error("Failed to unpack bundle"); + } + + // force data to be written to disk + fsync(fileno(cache)); + fclose(cache); + + // move script to final path - atomic operation + if (rename(tmpPath.c_str(), cachePath.c_str())) { + throw std::runtime_error("Failed to update cache, errno: " + errno); + } + + // store the bundle hash in a metadata file + auto meta = fopen(metaPath.c_str(), "w"); + if (!meta) { + throw std::runtime_error("Failed to open metadata file to store bundle hash"); + } + if (fwrite(hashStr, HASH_STR_SIZE, 1, meta) != HASH_STR_SIZE) { + throw std::runtime_error("Failed to write bundle hash to metadata file"); + } + fsync(fileno(meta)); + fclose(meta); + + // return the final written cache + cache = fopen(cachePath.c_str(), "r"); + if (!cache) { + throw std::runtime_error("Cache has been cleared"); + } + } else { + struct stat fileInfo = {0}; + if (fstat(fileno(cache), &fileInfo)) { + throw std::runtime_error("Failed to get cache stats, errno: " + errno); + } + size = fileInfo.st_size; + } + + return folly::make_unique( + dup(fileno(cache)), + size, + reinterpret_cast(hash->c_str()), + encoding); +} +#endif + void CatalystInstanceImpl::loadScriptFromAssets(jobject assetManager, - const std::string& assetURL) { + const std::string& assetURL, + bool useLazyBundle) { const int kAssetsLength = 9; // strlen("assets://"); auto sourceURL = assetURL.substr(kAssetsLength); - auto manager = react::extractAssetManager(assetManager); - auto script = react::loadScriptFromAssets(manager, sourceURL); + if (JniJSModulesUnbundle::isUnbundle(manager, sourceURL)) { + auto script = react::loadScriptFromAssets(manager, sourceURL); instance_->loadUnbundle( folly::make_unique(manager, sourceURL), std::move(script), sourceURL); + return; } else { - instance_->loadScriptFromString(std::move(script), std::move(sourceURL)); +#ifdef WITH_FBJSCEXTENSIONS + if (useLazyBundle) { + try { + auto script = loadScriptFromCache(manager, sourceURL); + instance_->loadScriptFromString(std::move(script), sourceURL); + return; + } catch (...) { + LOG(WARNING) << "Failed to load bundle as Source Code"; + } + } +#endif + auto script = react::loadScriptFromAssets(manager, sourceURL); + instance_->loadScriptFromString(std::move(script), sourceURL); } } diff --git a/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.h b/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.h index 1e5f2a894..64835e6b4 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.h +++ b/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.h @@ -47,7 +47,7 @@ class CatalystInstanceImpl : public jni::HybridClass { jni::alias_ref jsQueue, jni::alias_ref moduleQueue, ModuleRegistryHolder* mrh); - void loadScriptFromAssets(jobject assetManager, const std::string& assetURL); + void loadScriptFromAssets(jobject assetManager, const std::string& assetURL, bool useLazyBundle); void loadScriptFromFile(jni::alias_ref fileName, const std::string& sourceURL); void callJSFunction(JExecutorToken* token, std::string module, std::string method, NativeArray* arguments); void callJSCallback(JExecutorToken* token, jint callbackId, NativeArray* arguments); diff --git a/ReactAndroid/src/main/jni/xreact/jni/OnLoad.cpp b/ReactAndroid/src/main/jni/xreact/jni/OnLoad.cpp index 952a6b46c..67e167b4e 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/OnLoad.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/OnLoad.cpp @@ -51,10 +51,6 @@ static std::string getApplicationDir(const char* methodName) { return getAbsolutePathMethod(dirObj)->toStdString(); } -static std::string getApplicationCacheDir() { - return getApplicationDir("getCacheDir"); -} - static std::string getApplicationPersistentDir() { return getApplicationDir("getFilesDir"); } @@ -162,6 +158,10 @@ class JReactMarker : public JavaClass { } +std::string getApplicationCacheDir() { + return getApplicationDir("getCacheDir"); +} + extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { return initialize(vm, [] { // Inject some behavior into react/ diff --git a/ReactAndroid/src/main/jni/xreact/jni/OnLoad.h b/ReactAndroid/src/main/jni/xreact/jni/OnLoad.h index 5cd3c1749..713d8b582 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/OnLoad.h +++ b/ReactAndroid/src/main/jni/xreact/jni/OnLoad.h @@ -10,5 +10,6 @@ namespace facebook { namespace react { jmethodID getLogMarkerMethod(); +std::string getApplicationCacheDir(); } // namespace react } // namespace facebook diff --git a/ReactCommon/cxxreact/Executor.h b/ReactCommon/cxxreact/Executor.h index e2c5a331b..6b99df0b2 100644 --- a/ReactCommon/cxxreact/Executor.h +++ b/ReactCommon/cxxreact/Executor.h @@ -7,6 +7,8 @@ #include #include +#include + #include #include "JSModulesUnbundle.h" @@ -134,6 +136,68 @@ private: size_t m_size; }; +class JSBigMmapString : public JSBigString { +public: + enum class Encoding { + Unknown, + Ascii, + Utf8, + Utf16, + }; + + + JSBigMmapString(int fd, size_t size, const uint8_t sha1[20], Encoding encoding) : + m_fd(fd), + m_size(size), + m_encoding(encoding), + m_str(nullptr) + { + memcpy(m_hash, sha1, 20); + } + + ~JSBigMmapString() { + if (m_str) { + CHECK(munmap((void *)m_str, m_size) != -1); + } + close(m_fd); + } + + bool isAscii() const override { + return m_encoding == Encoding::Ascii; + } + + const char* c_str() const override { + if (!m_str) { + m_str = (const char *)mmap(0, m_size, PROT_READ, MAP_SHARED, m_fd, 0); + CHECK(m_str != MAP_FAILED); + } + return m_str; + } + + size_t size() const override { + return m_size; + } + + int fd() const { + return m_fd; + } + + const uint8_t* hash() const { + return m_hash; + } + + Encoding encoding() const { + return m_encoding; + } + +private: + int m_fd; + size_t m_size; + uint8_t m_hash[20]; + Encoding m_encoding; + mutable const char *m_str; +}; + class JSExecutor { public: /** diff --git a/ReactCommon/cxxreact/JSCExecutor.cpp b/ReactCommon/cxxreact/JSCExecutor.cpp index 0b624da12..d26409c2e 100644 --- a/ReactCommon/cxxreact/JSCExecutor.cpp +++ b/ReactCommon/cxxreact/JSCExecutor.cpp @@ -253,10 +253,33 @@ void JSCExecutor::terminateOnJSVMThread() { m_context = nullptr; } +#ifdef WITH_FBJSCEXTENSIONS +static void loadApplicationSource( + const JSGlobalContextRef context, + const JSBigMmapString* script, + const std::string& sourceURL) { + String jsSourceURL(sourceURL.c_str()); + bool is8bit = script->encoding() == JSBigMmapString::Encoding::Ascii || script->encoding() == JSBigMmapString::Encoding::Utf8; + JSSourceCodeRef sourceCode = JSCreateSourceCode(script->fd(), script->size(), jsSourceURL, script->hash(), is8bit); + evaluateSourceCode(context, sourceCode, jsSourceURL); + JSReleaseSourceCode(sourceCode); +} +#endif + void JSCExecutor::loadApplicationScript(std::unique_ptr script, std::string sourceURL) throw(JSException) { SystraceSection s("JSCExecutor::loadApplicationScript", "sourceURL", sourceURL); + #ifdef WITH_FBJSCEXTENSIONS + if (auto source = dynamic_cast(script.get())) { + loadApplicationSource(m_context, source, sourceURL); + bindBridge(); + flush(); + ReactMarker::logMarker("CREATE_REACT_CONTEXT_END"); + return; + } + #endif + #ifdef WITH_FBSYSTRACE fbsystrace_begin_section( TRACE_TAG_REACT_CXX_BRIDGE, diff --git a/ReactCommon/cxxreact/JSCHelpers.cpp b/ReactCommon/cxxreact/JSCHelpers.cpp index 40554219d..194cc4737 100644 --- a/ReactCommon/cxxreact/JSCHelpers.cpp +++ b/ReactCommon/cxxreact/JSCHelpers.cpp @@ -44,47 +44,63 @@ JSValueRef evaluateScript(JSContextRef context, JSStringRef script, JSStringRef JSValueRef exn, result; result = JSEvaluateScript(context, script, NULL, source, 0, &exn); if (result == nullptr) { - Value exception = Value(context, exn); - - std::string exceptionText = exception.toString().str(); - - // The null/empty-ness of source tells us if the JS came from a - // file/resource, or was a constructed statement. The location - // info will include that source, if any. - std::string locationInfo = source != nullptr ? String::ref(source).str() : ""; - Object exObject = exception.asObject(); - auto line = exObject.getProperty("line"); - if (line != nullptr && line.isNumber()) { - if (locationInfo.empty() && line.asInteger() != 1) { - // If there is a non-trivial line number, but there was no - // location info, we include a placeholder, and the line - // number. - locationInfo = folly::to(":", line.asInteger()); - } else if (!locationInfo.empty()) { - // If there is location info, we always include the line - // number, regardless of its value. - locationInfo += folly::to(":", line.asInteger()); - } - } - - if (!locationInfo.empty()) { - exceptionText += " (" + locationInfo + ")"; - } - - LOG(ERROR) << "Got JS Exception: " << exceptionText; - - Value jsStack = exObject.getProperty("stack"); - if (jsStack.isNull() || !jsStack.isString()) { - throwJSExecutionException("%s", exceptionText.c_str()); - } else { - LOG(ERROR) << "Got JS Stack: " << jsStack.toString().str(); - throwJSExecutionExceptionWithStack( - exceptionText.c_str(), jsStack.toString().str().c_str()); - } + formatAndThrowJSException(context, exn, source); } return result; } +#if WITH_FBJSCEXTENSIONS +JSValueRef evaluateSourceCode(JSContextRef context, JSSourceCodeRef source, JSStringRef sourceURL) { + JSValueRef exn, result; + result = JSEvaluateSourceCode(context, source, NULL, &exn); + if (result == nullptr) { + formatAndThrowJSException(context, exn, sourceURL); + } + return result; +} +#endif + +void formatAndThrowJSException(JSContextRef context, JSValueRef exn, JSStringRef source) { + Value exception = Value(context, exn); + + std::string exceptionText = exception.toString().str(); + + // The null/empty-ness of source tells us if the JS came from a + // file/resource, or was a constructed statement. The location + // info will include that source, if any. + std::string locationInfo = source != nullptr ? String::ref(source).str() : ""; + Object exObject = exception.asObject(); + auto line = exObject.getProperty("line"); + if (line != nullptr && line.isNumber()) { + if (locationInfo.empty() && line.asInteger() != 1) { + // If there is a non-trivial line number, but there was no + // location info, we include a placeholder, and the line + // number. + locationInfo = folly::to(":", line.asInteger()); + } else if (!locationInfo.empty()) { + // If there is location info, we always include the line + // number, regardless of its value. + locationInfo += folly::to(":", line.asInteger()); + } + } + + if (!locationInfo.empty()) { + exceptionText += " (" + locationInfo + ")"; + } + + LOG(ERROR) << "Got JS Exception: " << exceptionText; + + Value jsStack = exObject.getProperty("stack"); + if (jsStack.isNull() || !jsStack.isString()) { + throwJSExecutionException("%s", exceptionText.c_str()); + } else { + LOG(ERROR) << "Got JS Stack: " << jsStack.toString().str(); + throwJSExecutionExceptionWithStack( + exceptionText.c_str(), jsStack.toString().str().c_str()); + } +} + + JSValueRef makeJSError(JSContextRef ctx, const char *error) { JSValueRef nestedException = nullptr; JSValueRef args[] = { Value(ctx, String(error)) }; diff --git a/ReactCommon/cxxreact/JSCHelpers.h b/ReactCommon/cxxreact/JSCHelpers.h index 3be708164..a55906d56 100644 --- a/ReactCommon/cxxreact/JSCHelpers.h +++ b/ReactCommon/cxxreact/JSCHelpers.h @@ -49,6 +49,18 @@ JSValueRef evaluateScript( JSStringRef script, JSStringRef sourceURL); +#if WITH_FBJSCEXTENSIONS +JSValueRef evaluateSourceCode( + JSContextRef ctx, + JSSourceCodeRef source, + JSStringRef sourceURL); +#endif + +void formatAndThrowJSException( + JSContextRef ctx, + JSValueRef exn, + JSStringRef sourceURL); + JSValueRef makeJSError(JSContextRef ctx, const char *error); JSValueRef translatePendingCppExceptionToJSError(JSContextRef ctx, const char *exceptionLocation);