Skip to content

Commit 5d06918

Browse files
tadeuzagalloFacebook Github Bot 9
authored andcommitted
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
1 parent e565056 commit 5d06918

File tree

11 files changed

+287
-46
lines changed

11 files changed

+287
-46
lines changed

ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,9 +412,12 @@ public void run() {
412412
}
413413

414414
private void recreateReactContextInBackgroundFromBundleFile() {
415+
boolean useLazyBundle = mJSCConfig.getConfigMap().hasKey("useLazyBundle") ?
416+
mJSCConfig.getConfigMap().getBoolean("useLazyBundle") : false;
417+
415418
recreateReactContextInBackground(
416419
new JSCJavaScriptExecutor.Factory(mJSCConfig.getConfigMap()),
417-
JSBundleLoader.createFileLoader(mApplicationContext, mJSBundleFile));
420+
JSBundleLoader.createFileLoader(mApplicationContext, mJSBundleFile, useLazyBundle));
418421
}
419422

420423
/**

ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CatalystInstanceImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ private native void initializeBridge(ReactCallback callback,
161161
MessageQueueThread moduleQueue,
162162
ModuleRegistryHolder registryHolder);
163163

164-
/* package */ native void loadScriptFromAssets(AssetManager assetManager, String assetURL);
164+
/* package */ native void loadScriptFromAssets(AssetManager assetManager, String assetURL, boolean useLazyBundle);
165165
/* package */ native void loadScriptFromFile(String fileName, String sourceURL);
166166

167167
@Override

ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JSBundleLoader.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,18 @@ public abstract class JSBundleLoader {
2828
public static JSBundleLoader createFileLoader(
2929
final Context context,
3030
final String fileName) {
31+
return createFileLoader(context, fileName, false);
32+
}
33+
34+
public static JSBundleLoader createFileLoader(
35+
final Context context,
36+
final String fileName,
37+
final boolean useLazyBundle) {
3138
return new JSBundleLoader() {
3239
@Override
3340
public void loadScript(CatalystInstanceImpl instance) {
3441
if (fileName.startsWith("assets://")) {
35-
instance.loadScriptFromAssets(context.getAssets(), fileName);
42+
instance.loadScriptFromAssets(context.getAssets(), fileName, useLazyBundle);
3643
} else {
3744
instance.loadScriptFromFile(fileName, fileName);
3845
}

ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp

Lines changed: 120 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
#include <jni/Countable.h>
1414
#include <jni/LocalReference.h>
1515

16+
#include <sys/stat.h>
17+
1618
#include <cxxreact/Instance.h>
1719
#include <cxxreact/MethodCall.h>
1820
#include <cxxreact/ModuleRegistry.h>
@@ -23,6 +25,7 @@
2325
#include "ModuleRegistryHolder.h"
2426
#include "NativeArray.h"
2527
#include "JNativeRunnable.h"
28+
#include "OnLoad.h"
2629

2730
using namespace facebook::jni;
2831

@@ -98,7 +101,7 @@ void CatalystInstanceImpl::registerNatives() {
98101
makeNativeMethod("initHybrid", CatalystInstanceImpl::initHybrid),
99102
makeNativeMethod("initializeBridge", CatalystInstanceImpl::initializeBridge),
100103
makeNativeMethod("loadScriptFromAssets",
101-
"(Landroid/content/res/AssetManager;Ljava/lang/String;)V",
104+
"(Landroid/content/res/AssetManager;Ljava/lang/String;Z)V",
102105
CatalystInstanceImpl::loadScriptFromAssets),
103106
makeNativeMethod("loadScriptFromFile", CatalystInstanceImpl::loadScriptFromFile),
104107
makeNativeMethod("callJSFunction", CatalystInstanceImpl::callJSFunction),
@@ -149,20 +152,132 @@ void CatalystInstanceImpl::initializeBridge(
149152
mrh->getModuleRegistry());
150153
}
151154

155+
#ifdef WITH_FBJSCEXTENSIONS
156+
static std::unique_ptr<const JSBigString> loadScriptFromCache(
157+
AAssetManager* manager,
158+
std::string& sourceURL) {
159+
160+
// 20-byte sha1 as hex
161+
static const size_t HASH_STR_SIZE = 40;
162+
163+
// load bundle hash from the metadata file in the APK
164+
auto hash = react::loadScriptFromAssets(manager, sourceURL + ".meta");
165+
auto cacheDir = getApplicationCacheDir() + "/rn-bundle";
166+
auto encoding = static_cast<JSBigMmapString::Encoding>(hash->c_str()[20]);
167+
168+
if (mkdir(cacheDir.c_str(), 0755) == -1 && errno != EEXIST) {
169+
throw std::runtime_error("Can't create cache directory");
170+
}
171+
172+
if (encoding != JSBigMmapString::Encoding::Ascii) {
173+
throw std::runtime_error("Can't use mmap fastpath for non-ascii bundles");
174+
}
175+
176+
// convert hash to string
177+
char hashStr[HASH_STR_SIZE + 1];
178+
for (size_t i = 0; i < HASH_STR_SIZE; i += 2) {
179+
snprintf(hashStr + i, 3, "%02hhx", hash->c_str()[i / 2] & 0xFF);
180+
}
181+
182+
// the name of the cached bundle file should be the hash
183+
std::string cachePath = cacheDir + "/" + hashStr;
184+
FILE *cache = fopen(cachePath.c_str(), "r");
185+
SCOPE_EXIT { if (cache) fclose(cache); };
186+
187+
size_t size = 0;
188+
if (cache == NULL) {
189+
// delete old bundle, if there was one.
190+
std::string metaPath = cacheDir + "/meta";
191+
if (auto meta = fopen(metaPath.c_str(), "r")) {
192+
char oldBundleHash[HASH_STR_SIZE + 1];
193+
if (fread(oldBundleHash, HASH_STR_SIZE, 1, meta) == HASH_STR_SIZE) {
194+
remove((cacheDir + "/" + oldBundleHash).c_str());
195+
remove(metaPath.c_str());
196+
}
197+
fclose(meta);
198+
}
199+
200+
// load script from the APK and write to temporary file
201+
auto script = react::loadScriptFromAssets(manager, sourceURL);
202+
auto tmpPath = cachePath + "_";
203+
cache = fopen(tmpPath.c_str(), "w");
204+
if (!cache) {
205+
throw std::runtime_error("Can't open cache, errno: " + errno);
206+
}
207+
if (fwrite(script->c_str(), 1, script->size(), cache) != size) {
208+
remove(tmpPath.c_str());
209+
throw std::runtime_error("Failed to unpack bundle");
210+
}
211+
212+
// force data to be written to disk
213+
fsync(fileno(cache));
214+
fclose(cache);
215+
216+
// move script to final path - atomic operation
217+
if (rename(tmpPath.c_str(), cachePath.c_str())) {
218+
throw std::runtime_error("Failed to update cache, errno: " + errno);
219+
}
220+
221+
// store the bundle hash in a metadata file
222+
auto meta = fopen(metaPath.c_str(), "w");
223+
if (!meta) {
224+
throw std::runtime_error("Failed to open metadata file to store bundle hash");
225+
}
226+
if (fwrite(hashStr, HASH_STR_SIZE, 1, meta) != HASH_STR_SIZE) {
227+
throw std::runtime_error("Failed to write bundle hash to metadata file");
228+
}
229+
fsync(fileno(meta));
230+
fclose(meta);
231+
232+
// return the final written cache
233+
cache = fopen(cachePath.c_str(), "r");
234+
if (!cache) {
235+
throw std::runtime_error("Cache has been cleared");
236+
}
237+
} else {
238+
struct stat fileInfo = {0};
239+
if (fstat(fileno(cache), &fileInfo)) {
240+
throw std::runtime_error("Failed to get cache stats, errno: " + errno);
241+
}
242+
size = fileInfo.st_size;
243+
}
244+
245+
return folly::make_unique<const JSBigMmapString>(
246+
dup(fileno(cache)),
247+
size,
248+
reinterpret_cast<const uint8_t*>(hash->c_str()),
249+
encoding);
250+
}
251+
#endif
252+
152253
void CatalystInstanceImpl::loadScriptFromAssets(jobject assetManager,
153-
const std::string& assetURL) {
254+
const std::string& assetURL,
255+
bool useLazyBundle) {
154256
const int kAssetsLength = 9; // strlen("assets://");
155257
auto sourceURL = assetURL.substr(kAssetsLength);
156-
157258
auto manager = react::extractAssetManager(assetManager);
158-
auto script = react::loadScriptFromAssets(manager, sourceURL);
259+
159260
if (JniJSModulesUnbundle::isUnbundle(manager, sourceURL)) {
261+
auto script = react::loadScriptFromAssets(manager, sourceURL);
160262
instance_->loadUnbundle(
161263
folly::make_unique<JniJSModulesUnbundle>(manager, sourceURL),
162264
std::move(script),
163265
sourceURL);
266+
return;
164267
} else {
165-
instance_->loadScriptFromString(std::move(script), std::move(sourceURL));
268+
#ifdef WITH_FBJSCEXTENSIONS
269+
if (useLazyBundle) {
270+
try {
271+
auto script = loadScriptFromCache(manager, sourceURL);
272+
instance_->loadScriptFromString(std::move(script), sourceURL);
273+
return;
274+
} catch (...) {
275+
LOG(WARNING) << "Failed to load bundle as Source Code";
276+
}
277+
}
278+
#endif
279+
auto script = react::loadScriptFromAssets(manager, sourceURL);
280+
instance_->loadScriptFromString(std::move(script), sourceURL);
166281
}
167282
}
168283

ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class CatalystInstanceImpl : public jni::HybridClass<CatalystInstanceImpl> {
4747
jni::alias_ref<JavaMessageQueueThread::javaobject> jsQueue,
4848
jni::alias_ref<JavaMessageQueueThread::javaobject> moduleQueue,
4949
ModuleRegistryHolder* mrh);
50-
void loadScriptFromAssets(jobject assetManager, const std::string& assetURL);
50+
void loadScriptFromAssets(jobject assetManager, const std::string& assetURL, bool useLazyBundle);
5151
void loadScriptFromFile(jni::alias_ref<jstring> fileName, const std::string& sourceURL);
5252
void callJSFunction(JExecutorToken* token, std::string module, std::string method, NativeArray* arguments);
5353
void callJSCallback(JExecutorToken* token, jint callbackId, NativeArray* arguments);

ReactAndroid/src/main/jni/xreact/jni/OnLoad.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,6 @@ static std::string getApplicationDir(const char* methodName) {
5151
return getAbsolutePathMethod(dirObj)->toStdString();
5252
}
5353

54-
static std::string getApplicationCacheDir() {
55-
return getApplicationDir("getCacheDir");
56-
}
57-
5854
static std::string getApplicationPersistentDir() {
5955
return getApplicationDir("getFilesDir");
6056
}
@@ -162,6 +158,10 @@ class JReactMarker : public JavaClass<JReactMarker> {
162158

163159
}
164160

161+
std::string getApplicationCacheDir() {
162+
return getApplicationDir("getCacheDir");
163+
}
164+
165165
extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
166166
return initialize(vm, [] {
167167
// Inject some behavior into react/

ReactAndroid/src/main/jni/xreact/jni/OnLoad.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ namespace facebook {
1010
namespace react {
1111

1212
jmethodID getLogMarkerMethod();
13+
std::string getApplicationCacheDir();
1314
} // namespace react
1415
} // namespace facebook

ReactCommon/cxxreact/Executor.h

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
#include <string>
88
#include <vector>
99

10+
#include <sys/mman.h>
11+
1012
#include <folly/dynamic.h>
1113

1214
#include "JSModulesUnbundle.h"
@@ -134,6 +136,68 @@ class JSBigBufferString : public facebook::react::JSBigString {
134136
size_t m_size;
135137
};
136138

139+
class JSBigMmapString : public JSBigString {
140+
public:
141+
enum class Encoding {
142+
Unknown,
143+
Ascii,
144+
Utf8,
145+
Utf16,
146+
};
147+
148+
149+
JSBigMmapString(int fd, size_t size, const uint8_t sha1[20], Encoding encoding) :
150+
m_fd(fd),
151+
m_size(size),
152+
m_encoding(encoding),
153+
m_str(nullptr)
154+
{
155+
memcpy(m_hash, sha1, 20);
156+
}
157+
158+
~JSBigMmapString() {
159+
if (m_str) {
160+
CHECK(munmap((void *)m_str, m_size) != -1);
161+
}
162+
close(m_fd);
163+
}
164+
165+
bool isAscii() const override {
166+
return m_encoding == Encoding::Ascii;
167+
}
168+
169+
const char* c_str() const override {
170+
if (!m_str) {
171+
m_str = (const char *)mmap(0, m_size, PROT_READ, MAP_SHARED, m_fd, 0);
172+
CHECK(m_str != MAP_FAILED);
173+
}
174+
return m_str;
175+
}
176+
177+
size_t size() const override {
178+
return m_size;
179+
}
180+
181+
int fd() const {
182+
return m_fd;
183+
}
184+
185+
const uint8_t* hash() const {
186+
return m_hash;
187+
}
188+
189+
Encoding encoding() const {
190+
return m_encoding;
191+
}
192+
193+
private:
194+
int m_fd;
195+
size_t m_size;
196+
uint8_t m_hash[20];
197+
Encoding m_encoding;
198+
mutable const char *m_str;
199+
};
200+
137201
class JSExecutor {
138202
public:
139203
/**

ReactCommon/cxxreact/JSCExecutor.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,10 +253,33 @@ void JSCExecutor::terminateOnJSVMThread() {
253253
m_context = nullptr;
254254
}
255255

256+
#ifdef WITH_FBJSCEXTENSIONS
257+
static void loadApplicationSource(
258+
const JSGlobalContextRef context,
259+
const JSBigMmapString* script,
260+
const std::string& sourceURL) {
261+
String jsSourceURL(sourceURL.c_str());
262+
bool is8bit = script->encoding() == JSBigMmapString::Encoding::Ascii || script->encoding() == JSBigMmapString::Encoding::Utf8;
263+
JSSourceCodeRef sourceCode = JSCreateSourceCode(script->fd(), script->size(), jsSourceURL, script->hash(), is8bit);
264+
evaluateSourceCode(context, sourceCode, jsSourceURL);
265+
JSReleaseSourceCode(sourceCode);
266+
}
267+
#endif
268+
256269
void JSCExecutor::loadApplicationScript(std::unique_ptr<const JSBigString> script, std::string sourceURL) throw(JSException) {
257270
SystraceSection s("JSCExecutor::loadApplicationScript",
258271
"sourceURL", sourceURL);
259272

273+
#ifdef WITH_FBJSCEXTENSIONS
274+
if (auto source = dynamic_cast<const JSBigMmapString *>(script.get())) {
275+
loadApplicationSource(m_context, source, sourceURL);
276+
bindBridge();
277+
flush();
278+
ReactMarker::logMarker("CREATE_REACT_CONTEXT_END");
279+
return;
280+
}
281+
#endif
282+
260283
#ifdef WITH_FBSYSTRACE
261284
fbsystrace_begin_section(
262285
TRACE_TAG_REACT_CXX_BRIDGE,

0 commit comments

Comments
 (0)