Skip to content

Commit

Permalink
add API to CatalystInstanceImpl for executing optimized bundle
Browse files Browse the repository at this point in the history
Reviewed By: tadeuzagallo

Differential Revision: D3545345

fbshipit-source-id: 538fec77b816c3fd767e8c2eda81c78971996b17
  • Loading branch information
Michał Gregorczyk authored and Facebook Github Bot 2 committed Jul 12, 2016
1 parent a665914 commit 1331e20
Show file tree
Hide file tree
Showing 18 changed files with 199 additions and 158 deletions.
Expand Up @@ -161,8 +161,9 @@ private native void initializeBridge(ReactCallback callback,
MessageQueueThread moduleQueue,
ModuleRegistryHolder registryHolder);

/* package */ native void loadScriptFromAssets(AssetManager assetManager, String assetURL, boolean useLazyBundle);
/* package */ native void loadScriptFromAssets(AssetManager assetManager, String assetURL);
/* package */ native void loadScriptFromFile(String fileName, String sourceURL);
/* package */ native void loadScriptFromOptimizedBundle(String path, String sourceURL, int flags);

@Override
public void runJSBundle() {
Expand Down
Expand Up @@ -30,18 +30,11 @@ 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, useLazyBundle);
instance.loadScriptFromAssets(context.getAssets(), fileName);
} else {
instance.loadScriptFromFile(fileName, fileName);
}
Expand Down
Expand Up @@ -38,6 +38,12 @@
*/
public class UnpackingJSBundleLoader extends JSBundleLoader {

/**
* Flag passed to loadScriptFromOptimizedBundle to let the bridge know that
* the unpacked unpacked js source file.
*/
static final int UNPACKED_JS_SOURCE = (1 << 0);

/**
* Name of the lock files. Multiple processes can be spawned off the same app
* and we need to guarantee that at most one unpacks files at any time. To
Expand Down Expand Up @@ -141,10 +147,10 @@ private void prepareLocked() throws IOException {
@Override
public void loadScript(CatalystInstanceImpl instance) {
prepare();
// TODO(12128379): add instance method that would take bundle directory
instance.loadScriptFromFile(
new File(mDirectoryPath, "bundle.js").getPath(),
mSourceURL);
instance.loadScriptFromOptimizedBundle(
mDirectoryPath.getPath(),
mSourceURL,
UNPACKED_JS_SOURCE);
}

@Override
Expand Down
132 changes: 14 additions & 118 deletions ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp
Expand Up @@ -13,8 +13,6 @@
#include <jni/Countable.h>
#include <jni/LocalReference.h>

#include <sys/stat.h>

#include <cxxreact/Instance.h>
#include <cxxreact/MethodCall.h>
#include <cxxreact/ModuleRegistry.h>
Expand All @@ -25,7 +23,6 @@
#include "ModuleRegistryHolder.h"
#include "NativeArray.h"
#include "JNativeRunnable.h"
#include "OnLoad.h"

using namespace facebook::jni;

Expand Down Expand Up @@ -101,9 +98,11 @@ void CatalystInstanceImpl::registerNatives() {
makeNativeMethod("initHybrid", CatalystInstanceImpl::initHybrid),
makeNativeMethod("initializeBridge", CatalystInstanceImpl::initializeBridge),
makeNativeMethod("loadScriptFromAssets",
"(Landroid/content/res/AssetManager;Ljava/lang/String;Z)V",
"(Landroid/content/res/AssetManager;Ljava/lang/String;)V",
CatalystInstanceImpl::loadScriptFromAssets),
makeNativeMethod("loadScriptFromFile", CatalystInstanceImpl::loadScriptFromFile),
makeNativeMethod("loadScriptFromOptimizedBundle",
CatalystInstanceImpl::loadScriptFromOptimizedBundle),
makeNativeMethod("callJSFunction", CatalystInstanceImpl::callJSFunction),
makeNativeMethod("callJSCallback", CatalystInstanceImpl::callJSCallback),
makeNativeMethod("getMainExecutorToken", CatalystInstanceImpl::getMainExecutorToken),
Expand Down Expand Up @@ -152,131 +151,20 @@ 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,
bool useLazyBundle) {
const std::string& assetURL) {
const int kAssetsLength = 9; // strlen("assets://");
auto sourceURL = assetURL.substr(kAssetsLength);
auto manager = react::extractAssetManager(assetManager);

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 {
#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);
}
}
Expand All @@ -287,6 +175,14 @@ void CatalystInstanceImpl::loadScriptFromFile(jni::alias_ref<jstring> fileName,
sourceURL);
}

void CatalystInstanceImpl::loadScriptFromOptimizedBundle(const std::string& bundlePath,
const std::string& sourceURL,
jint flags) {
return instance_->loadScriptFromOptimizedBundle(std::move(bundlePath),
std::move(sourceURL),
flags);
}

void CatalystInstanceImpl::callJSFunction(
JExecutorToken* token, std::string module, std::string method, NativeArray* arguments) {
// We want to share the C++ code, and on iOS, modules pass module/method
Expand Down
3 changes: 2 additions & 1 deletion ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.h
Expand Up @@ -47,8 +47,9 @@ 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, bool useLazyBundle);
void loadScriptFromAssets(jobject assetManager, const std::string& assetURL);
void loadScriptFromFile(jni::alias_ref<jstring> fileName, const std::string& sourceURL);
void loadScriptFromOptimizedBundle(const std::string& bundlePath, const std::string& sourceURL, jint flags);
void callJSFunction(JExecutorToken* token, std::string module, std::string method, NativeArray* arguments);
void callJSCallback(JExecutorToken* token, jint callbackId, NativeArray* arguments);
local_ref<JExecutorToken::JavaPart> getMainExecutorToken();
Expand Down
8 changes: 4 additions & 4 deletions ReactAndroid/src/main/jni/xreact/jni/OnLoad.cpp
Expand Up @@ -51,6 +51,10 @@ 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");
}
Expand Down Expand Up @@ -158,10 +162,6 @@ 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/
Expand Down
1 change: 0 additions & 1 deletion ReactAndroid/src/main/jni/xreact/jni/OnLoad.h
Expand Up @@ -10,6 +10,5 @@ namespace facebook {
namespace react {

jmethodID getLogMarkerMethod();
std::string getApplicationCacheDir();
} // namespace react
} // namespace facebook
Expand Up @@ -102,9 +102,10 @@ public void testCreatesLockFile() throws IOException {
@Test
public void testCallsAppropriateInstanceMethod() throws IOException {
mBuilder.build().loadScript(mCatalystInstanceImpl);
verify(mCatalystInstanceImpl).loadScriptFromFile(
eq(new File(mDestinationPath, "bundle.js").getPath()),
eq(URL));
verify(mCatalystInstanceImpl).loadScriptFromOptimizedBundle(
eq(mDestinationPath.getPath()),
eq(URL),
eq(UnpackingJSBundleLoader.UNPACKED_JS_SOURCE));
verifyNoMoreInteractions(mCatalystInstanceImpl);
}

Expand Down
1 change: 1 addition & 0 deletions ReactCommon/cxxreact/Android.mk
Expand Up @@ -5,6 +5,7 @@ include $(CLEAR_VARS)
LOCAL_MODULE := libreactnativefb

LOCAL_SRC_FILES := \
Executor.cpp \
Instance.cpp \
JSCExecutor.cpp \
JSCHelpers.cpp \
Expand Down
1 change: 1 addition & 0 deletions ReactCommon/cxxreact/BUCK
Expand Up @@ -109,6 +109,7 @@ react_library(
force_static = True,
srcs = [
'CxxMessageQueue.cpp',
'Executor.cpp',
'Instance.cpp',
'JSCExecutor.cpp',
'JSCHelpers.cpp',
Expand Down
77 changes: 77 additions & 0 deletions ReactCommon/cxxreact/Executor.cpp
@@ -0,0 +1,77 @@
// Copyright 2004-present Facebook. All Rights Reserved.

#include "Executor.h"

#include <errno.h>
#include <fcntl.h>
#include <fstream>
#include <stdio.h>
#include <sys/stat.h>

#include <folly/Memory.h>

namespace facebook {
namespace react {

void JSExecutor::loadApplicationScript(std::string bundlePath, std::string sourceURL, int flags) {
if ((flags & UNPACKED_JS_SOURCE) == 0) {
throw std::runtime_error("No unpacked js source file");
}
return loadApplicationScript(
JSBigMmapString::fromOptimizedBundle(bundlePath),
std::move(sourceURL));
}

static JSBigMmapString::Encoding encodingFromByte(uint8_t byte) {
switch (byte) {
case 0:
return JSBigMmapString::Encoding::Unknown;
case 1:
return JSBigMmapString::Encoding::Ascii;
case 2:
return JSBigMmapString::Encoding::Utf8;
case 3:
return JSBigMmapString::Encoding::Utf16;
default:
throw std::invalid_argument("Unknown bundle encoding");
}
}

std::unique_ptr<const JSBigMmapString> JSBigMmapString::fromOptimizedBundle(
const std::string& bundlePath) {
uint8_t sha1[20];
uint8_t encoding;
struct stat fileInfo;
int fd = -1;
SCOPE_FAIL { CHECK(fd == -1 || ::close(fd) == 0); };

{
auto metaPath = bundlePath + UNPACKED_META_PATH_SUFFIX;
std::ifstream metaFile;
metaFile.exceptions(std::ifstream::eofbit | std::ifstream::failbit | std::ifstream::badbit);
metaFile.open(metaPath, std::ifstream::in | std::ifstream::binary);
metaFile.read(reinterpret_cast<char*>(sha1), sizeof(sha1));
metaFile.read(reinterpret_cast<char*>(&encoding), sizeof(encoding));
}

{
auto sourcePath = bundlePath + UNPACKED_JS_SOURCE_PATH_SUFFIX;
fd = ::open(sourcePath.c_str(), O_RDONLY);
if (fd == -1) {
throw std::runtime_error(std::string("could not open js bundle file: ") + ::strerror(errno));
}
}

if (::fstat(fd, &fileInfo)) {
throw std::runtime_error(std::string("fstat on js bundle failed: ") + strerror(errno));
}

return folly::make_unique<const JSBigMmapString>(
fd,
fileInfo.st_size,
sha1,
encodingFromByte(encoding));
}

} // namespace react
} // namespace facebook

0 comments on commit 1331e20

Please sign in to comment.