Permalink
Browse files

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
  • Loading branch information...
1 parent e565056 commit 5d06918d0748df522ec9444d5742377095df609b @tadeuzagallo tadeuzagallo committed with Facebook Github Bot 9 Jul 11, 2016
@@ -412,9 +412,12 @@ public void run() {
}
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));
}
/**
@@ -161,7 +161,7 @@ private native void initializeBridge(ReactCallback callback,
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
@@ -28,11 +28,18 @@
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);
}
@@ -13,6 +13,8 @@
#include <jni/Countable.h>
#include <jni/LocalReference.h>
+#include <sys/stat.h>
+
#include <cxxreact/Instance.h>
#include <cxxreact/MethodCall.h>
#include <cxxreact/ModuleRegistry.h>
@@ -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<const JSBigString> 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<JSBigMmapString::Encoding>(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<const JSBigMmapString>(
+ dup(fileno(cache)),
+ size,
+ reinterpret_cast<const uint8_t*>(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<JniJSModulesUnbundle>(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);
}
}
@@ -47,7 +47,7 @@ class CatalystInstanceImpl : public jni::HybridClass<CatalystInstanceImpl> {
jni::alias_ref<JavaMessageQueueThread::javaobject> jsQueue,
jni::alias_ref<JavaMessageQueueThread::javaobject> 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<jstring> 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);
@@ -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<JReactMarker> {
}
+std::string getApplicationCacheDir() {
+ return getApplicationDir("getCacheDir");
+}
+
extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
return initialize(vm, [] {
// Inject some behavior into react/
@@ -10,5 +10,6 @@ namespace facebook {
namespace react {
jmethodID getLogMarkerMethod();
+std::string getApplicationCacheDir();
} // namespace react
} // namespace facebook
@@ -7,6 +7,8 @@
#include <string>
#include <vector>
+#include <sys/mman.h>
+
#include <folly/dynamic.h>
#include "JSModulesUnbundle.h"
@@ -134,6 +136,68 @@ class JSBigBufferString : public facebook::react::JSBigString {
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:
/**
@@ -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<const JSBigString> script, std::string sourceURL) throw(JSException) {
SystraceSection s("JSCExecutor::loadApplicationScript",
"sourceURL", sourceURL);
+ #ifdef WITH_FBJSCEXTENSIONS
+ if (auto source = dynamic_cast<const JSBigMmapString *>(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,
Oops, something went wrong.

0 comments on commit 5d06918

Please sign in to comment.