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
This commit is contained in:
Tadeu Zagallo 2016-07-11 06:52:06 -07:00 committed by Facebook Github Bot 9
parent e5650560c0
commit 5d06918d07
11 changed files with 291 additions and 50 deletions

View File

@ -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));
}
/**

View File

@ -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

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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/

View File

@ -10,5 +10,6 @@ namespace facebook {
namespace react {
jmethodID getLogMarkerMethod();
std::string getApplicationCacheDir();
} // namespace react
} // namespace facebook

View File

@ -7,6 +7,8 @@
#include <string>
#include <vector>
#include <sys/mman.h>
#include <folly/dynamic.h>
#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:
/**

View File

@ -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,

View File

@ -44,6 +44,23 @@ JSValueRef evaluateScript(JSContextRef context, JSStringRef script, JSStringRef
JSValueRef exn, result;
result = JSEvaluateScript(context, script, NULL, source, 0, &exn);
if (result == nullptr) {
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();
@ -81,10 +98,9 @@ JSValueRef evaluateScript(JSContextRef context, JSStringRef script, JSStringRef
throwJSExecutionExceptionWithStack(
exceptionText.c_str(), jsStack.toString().str().c_str());
}
}
return result;
}
JSValueRef makeJSError(JSContextRef ctx, const char *error) {
JSValueRef nestedException = nullptr;
JSValueRef args[] = { Value(ctx, String(error)) };

View File

@ -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);