Hook up native delta client

Summary:
Adds support for native clients to `ReactAndroid`:
- `.devsupport.BundleDeltaClient` is now abstract with two implementations: the existing Java client, and a native client
- `BundleDeltaClient#processDelta(...)` can now return a native delta client object
- if that client object is non-null, the bridge is started up with that client rather than a script written to disk

Reviewed By: fromcelticpark

Differential Revision: D7845135

fbshipit-source-id: 379a9c6f9319c62eec3c370cda9ffa0969266a29
This commit is contained in:
David Aurelio 2018-05-03 08:38:13 -07:00 committed by Facebook Github Bot
parent 8f85abdb14
commit dd036c2328
7 changed files with 222 additions and 103 deletions

View File

@ -50,6 +50,7 @@ import com.facebook.react.bridge.JSIModulesProvider;
import com.facebook.react.bridge.JavaJSExecutor;
import com.facebook.react.bridge.JavaScriptExecutor;
import com.facebook.react.bridge.JavaScriptExecutorFactory;
import com.facebook.react.bridge.NativeDeltaClient;
import com.facebook.react.bridge.NativeModuleCallExceptionHandler;
import com.facebook.react.bridge.NativeModuleRegistry;
import com.facebook.react.bridge.NotThreadSafeBridgeIdleDebugListener;
@ -84,6 +85,7 @@ import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper;
import com.facebook.soloader.SoLoader;
import com.facebook.systrace.Systrace;
import com.facebook.systrace.SystraceMessage;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@ -270,8 +272,8 @@ public class ReactInstanceManager {
}
@Override
public void onJSBundleLoadedFromServer() {
ReactInstanceManager.this.onJSBundleLoadedFromServer();
public void onJSBundleLoadedFromServer(@Nullable NativeDeltaClient nativeDeltaClient) {
ReactInstanceManager.this.onJSBundleLoadedFromServer(nativeDeltaClient);
}
@Override
@ -360,7 +362,7 @@ public class ReactInstanceManager {
!devSettings.isRemoteJSDebugEnabled()) {
// If there is a up-to-date bundle downloaded from server,
// with remote JS debugging disabled, always use that.
onJSBundleLoadedFromServer();
onJSBundleLoadedFromServer(null);
} else if (mBundleLoader == null) {
mDevSupportManager.handleReloadJS();
} else {
@ -848,12 +850,17 @@ public class ReactInstanceManager {
}
@ThreadConfined(UI)
private void onJSBundleLoadedFromServer() {
private void onJSBundleLoadedFromServer(@Nullable NativeDeltaClient nativeDeltaClient) {
Log.d(ReactConstants.TAG, "ReactInstanceManager.onJSBundleLoadedFromServer()");
recreateReactContextInBackground(
mJavaScriptExecutorFactory,
JSBundleLoader.createCachedBundleFromNetworkLoader(
mDevSupportManager.getSourceUrl(), mDevSupportManager.getDownloadedJSBundleFile()));
JSBundleLoader bundleLoader = nativeDeltaClient == null
? JSBundleLoader.createCachedBundleFromNetworkLoader(
mDevSupportManager.getSourceUrl(),
mDevSupportManager.getDownloadedJSBundleFile())
: JSBundleLoader.createDeltaFromNetworkLoader(
mDevSupportManager.getSourceUrl(), nativeDeltaClient);
recreateReactContextInBackground(mJavaScriptExecutorFactory, bundleLoader);
}
@ThreadConfined(UI)

View File

@ -1,121 +1,197 @@
/**
* Copyright (c) 2018-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react.devsupport;
import android.util.JsonReader;
import android.util.JsonToken;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.LinkedHashMap;
import javax.annotation.Nullable;
import android.util.JsonReader;
import android.util.JsonToken;
import android.util.Pair;
import com.facebook.react.bridge.NativeDeltaClient;
import okhttp3.Headers;
import okio.Buffer;
import okio.BufferedSource;
public class BundleDeltaClient {
public abstract class BundleDeltaClient {
final LinkedHashMap<Number, byte[]> mPreModules = new LinkedHashMap<Number, byte[]>();
final LinkedHashMap<Number, byte[]> mDeltaModules = new LinkedHashMap<Number, byte[]>();
final LinkedHashMap<Number, byte[]> mPostModules = new LinkedHashMap<Number, byte[]>();
@Nullable String mDeltaId;
private static final String METRO_DELTA_ID_HEADER = "X-Metro-Delta-ID";
@Nullable private String mDeltaId;
public enum ClientType {
NONE,
DEV_SUPPORT,
NATIVE
}
static boolean isDeltaUrl(String bundleUrl) {
return bundleUrl.indexOf(".delta?") != -1;
}
@Nullable
static BundleDeltaClient create(ClientType type) {
switch (type) {
case DEV_SUPPORT:
return new BundleDeltaJavaClient();
case NATIVE:
return new BundleDeltaNativeClient();
}
return null;
}
abstract public boolean canHandle(ClientType type);
abstract protected Pair<Boolean, NativeDeltaClient> processDelta(
BufferedSource body,
File outputFile) throws IOException;
final public String extendUrlForDelta(String bundleURL) {
return mDeltaId != null ? bundleURL + "&deltaBundleId=" + mDeltaId : bundleURL;
}
public void reset() {
mDeltaId = null;
mDeltaModules.clear();
mPreModules.clear();
mPostModules.clear();
}
public String toDeltaUrl(String bundleURL) {
if (isDeltaUrl(bundleURL) && mDeltaId != null) {
return bundleURL + "&deltaBundleId=" + mDeltaId;
}
return bundleURL;
public Pair<Boolean, NativeDeltaClient> processDelta(
Headers headers,
BufferedSource body,
File outputFile) throws IOException {
mDeltaId = headers.get(METRO_DELTA_ID_HEADER);
return processDelta(body, outputFile);
}
public synchronized boolean storeDeltaInFile(BufferedSource body, File outputFile)
throws IOException {
private static class BundleDeltaJavaClient extends BundleDeltaClient {
JsonReader jsonReader = new JsonReader(new InputStreamReader(body.inputStream()));
final LinkedHashMap<Number, byte[]> mPreModules = new LinkedHashMap<Number, byte[]>();
final LinkedHashMap<Number, byte[]> mDeltaModules = new LinkedHashMap<Number, byte[]>();
final LinkedHashMap<Number, byte[]> mPostModules = new LinkedHashMap<Number, byte[]>();
jsonReader.beginObject();
int numChangedModules = 0;
while (jsonReader.hasNext()) {
String name = jsonReader.nextName();
if (name.equals("id")) {
mDeltaId = jsonReader.nextString();
} else if (name.equals("pre")) {
numChangedModules += patchDelta(jsonReader, mPreModules);
} else if (name.equals("post")) {
numChangedModules += patchDelta(jsonReader, mPostModules);
} else if (name.equals("delta")) {
numChangedModules += patchDelta(jsonReader, mDeltaModules);
} else {
jsonReader.skipValue();
}
@Override
public boolean canHandle(ClientType type) {
return type == ClientType.DEV_SUPPORT;
}
jsonReader.endObject();
jsonReader.close();
if (numChangedModules == 0) {
// If we receive an empty delta, we don't need to save the file again (it'll have the
// same content).
return false;
public void reset() {
super.reset();
mDeltaModules.clear();
mPreModules.clear();
mPostModules.clear();
}
FileOutputStream fileOutputStream = new FileOutputStream(outputFile);
@Override
public synchronized Pair<Boolean, NativeDeltaClient> processDelta(
BufferedSource body,
File outputFile) throws IOException {
try {
for (byte[] code : mPreModules.values()) {
fileOutputStream.write(code);
fileOutputStream.write('\n');
JsonReader jsonReader = new JsonReader(new InputStreamReader(body.inputStream()));
jsonReader.beginObject();
int numChangedModules = 0;
while (jsonReader.hasNext()) {
String name = jsonReader.nextName();
if (name.equals("pre")) {
numChangedModules += patchDelta(jsonReader, mPreModules);
} else if (name.equals("post")) {
numChangedModules += patchDelta(jsonReader, mPostModules);
} else if (name.equals("delta")) {
numChangedModules += patchDelta(jsonReader, mDeltaModules);
} else {
jsonReader.skipValue();
}
}
for (byte[] code : mDeltaModules.values()) {
fileOutputStream.write(code);
fileOutputStream.write('\n');
jsonReader.endObject();
jsonReader.close();
if (numChangedModules == 0) {
// If we receive an empty delta, we don't need to save the file again (it'll have the
// same content).
return Pair.create(Boolean.FALSE, null);
}
for (byte[] code : mPostModules.values()) {
fileOutputStream.write(code);
fileOutputStream.write('\n');
FileOutputStream fileOutputStream = new FileOutputStream(outputFile);
try {
for (byte[] code : mPreModules.values()) {
fileOutputStream.write(code);
fileOutputStream.write('\n');
}
for (byte[] code : mDeltaModules.values()) {
fileOutputStream.write(code);
fileOutputStream.write('\n');
}
for (byte[] code : mPostModules.values()) {
fileOutputStream.write(code);
fileOutputStream.write('\n');
}
} finally {
fileOutputStream.flush();
fileOutputStream.close();
}
} finally {
fileOutputStream.flush();
fileOutputStream.close();
return Pair.create(Boolean.TRUE, null);
}
return true;
}
private static int patchDelta(JsonReader jsonReader, LinkedHashMap<Number, byte[]> map)
throws IOException {
jsonReader.beginArray();
int numModules = 0;
while (jsonReader.hasNext()) {
private static int patchDelta(JsonReader jsonReader, LinkedHashMap<Number, byte[]> map)
throws IOException {
jsonReader.beginArray();
int moduleId = jsonReader.nextInt();
int numModules = 0;
while (jsonReader.hasNext()) {
jsonReader.beginArray();
if (jsonReader.peek() == JsonToken.NULL) {
jsonReader.skipValue();
map.remove(moduleId);
} else {
map.put(moduleId, jsonReader.nextString().getBytes());
int moduleId = jsonReader.nextInt();
if (jsonReader.peek() == JsonToken.NULL) {
jsonReader.skipValue();
map.remove(moduleId);
} else {
map.put(moduleId, jsonReader.nextString().getBytes());
}
jsonReader.endArray();
numModules++;
}
jsonReader.endArray();
numModules++;
return numModules;
}
}
private static class BundleDeltaNativeClient extends BundleDeltaClient {
private final NativeDeltaClient nativeClient = new NativeDeltaClient();
@Override
public boolean canHandle(ClientType type) {
return type == ClientType.NATIVE;
}
jsonReader.endArray();
@Override
protected Pair<Boolean, NativeDeltaClient> processDelta(
BufferedSource body,
File outputFile) throws IOException {
nativeClient.processDelta(body);
return Pair.create(Boolean.FALSE, nativeClient);
}
return numModules;
@Override
public void reset() {
super.reset();
nativeClient.reset();
}
}
}

View File

@ -8,8 +8,10 @@
package com.facebook.react.devsupport;
import android.util.Log;
import android.util.Pair;
import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.NativeDeltaClient;
import com.facebook.react.common.DebugServerException;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener;
@ -40,7 +42,7 @@ public class BundleDownloader {
private final OkHttpClient mClient;
private final BundleDeltaClient mBundleDeltaClient = new BundleDeltaClient();
private BundleDeltaClient mBundleDeltaClient;
private @Nullable Call mDownloadBundleFromURLCall;
@ -95,14 +97,15 @@ public class BundleDownloader {
}
public void downloadBundleFromURL(
final DevBundleDownloadListener callback,
final File outputFile,
final String bundleURL,
final @Nullable BundleInfo bundleInfo) {
final DevBundleDownloadListener callback,
final File outputFile,
final String bundleURL,
final @Nullable BundleInfo bundleInfo,
final BundleDeltaClient.ClientType clientType) {
final Request request =
new Request.Builder()
.url(mBundleDeltaClient.toDeltaUrl(bundleURL))
.url(formatBundleUrl(bundleURL, clientType))
// FIXME: there is a bug that makes MultipartStreamReader to never find the end of the
// multipart message. This temporarily disables the multipart mode to work around it,
// but
@ -146,7 +149,7 @@ public class BundleDownloader {
try (Response r = response) {
if (match.find()) {
processMultipartResponse(
url, r, match.group(1), outputFile, bundleInfo, callback);
url, r, match.group(1), outputFile, bundleInfo, clientType, callback);
} else {
// In case the server doesn't support multipart/mixed responses, fallback to normal
// download.
@ -157,6 +160,7 @@ public class BundleDownloader {
Okio.buffer(r.body().source()),
outputFile,
bundleInfo,
clientType,
callback);
}
}
@ -164,12 +168,19 @@ public class BundleDownloader {
});
}
private String formatBundleUrl(String bundleURL, BundleDeltaClient.ClientType clientType) {
return BundleDeltaClient.isDeltaUrl(bundleURL) && mBundleDeltaClient != null && mBundleDeltaClient.canHandle(clientType)
? mBundleDeltaClient.extendUrlForDelta(bundleURL)
: bundleURL;
}
private void processMultipartResponse(
final String url,
final Response response,
String boundary,
final File outputFile,
@Nullable final BundleInfo bundleInfo,
final BundleDeltaClient.ClientType clientType,
final DevBundleDownloadListener callback)
throws IOException {
@ -193,7 +204,7 @@ public class BundleDownloader {
status = Integer.parseInt(headers.get("X-Http-Status"));
}
processBundleResult(
url, status, Headers.of(headers), body, outputFile, bundleInfo, callback);
url, status, Headers.of(headers), body, outputFile, bundleInfo, clientType, callback);
} else {
if (!headers.containsKey("Content-Type")
|| !headers.get("Content-Type").equals("application/json")) {
@ -249,6 +260,7 @@ public class BundleDownloader {
BufferedSource body,
File outputFile,
BundleInfo bundleInfo,
BundleDeltaClient.ClientType clientType,
DevBundleDownloadListener callback)
throws IOException {
// Check for server errors. If the server error has the expected form, fail with more info.
@ -274,24 +286,36 @@ public class BundleDownloader {
File tmpFile = new File(outputFile.getPath() + ".tmp");
boolean bundleUpdated;
boolean bundleWritten;
NativeDeltaClient nativeDeltaClient = null;
if (BundleDeltaClient.isDeltaUrl(url)) {
// If the bundle URL has the delta extension, we need to use the delta patching logic.
bundleUpdated = mBundleDeltaClient.storeDeltaInFile(body, tmpFile);
BundleDeltaClient deltaClient = getBundleDeltaClient(clientType);
Assertions.assertNotNull(deltaClient);
Pair<Boolean, NativeDeltaClient> result = deltaClient.processDelta(headers, body, tmpFile);
bundleWritten = result.first;
nativeDeltaClient = result.second;
} else {
mBundleDeltaClient.reset();
bundleUpdated = storePlainJSInFile(body, tmpFile);
mBundleDeltaClient = null;
bundleWritten = storePlainJSInFile(body, tmpFile);
}
if (bundleUpdated) {
if (bundleWritten) {
// If we have received a new bundle from the server, move it to its final destination.
if (!tmpFile.renameTo(outputFile)) {
throw new IOException("Couldn't rename " + tmpFile + " to " + outputFile);
}
}
callback.onSuccess();
callback.onSuccess(nativeDeltaClient);
}
private BundleDeltaClient getBundleDeltaClient(BundleDeltaClient.ClientType clientType) {
if (mBundleDeltaClient == null || !mBundleDeltaClient.canHandle(clientType)) {
mBundleDeltaClient = BundleDeltaClient.create(clientType);
}
return mBundleDeltaClient;
}
private static boolean storePlainJSInFile(BufferedSource body, File outputFile)

View File

@ -375,7 +375,17 @@ public class DevServerHelper {
public void downloadBundleFromURL(
DevBundleDownloadListener callback,
File outputFile, String bundleURL, BundleDownloader.BundleInfo bundleInfo) {
mBundleDownloader.downloadBundleFromURL(callback, outputFile, bundleURL, bundleInfo);
mBundleDownloader.downloadBundleFromURL(callback, outputFile, bundleURL, bundleInfo, getDeltaClientType());
}
private BundleDeltaClient.ClientType getDeltaClientType() {
if (mSettings.isBundleDeltasCppEnabled()) {
return BundleDeltaClient.ClientType.NATIVE;
} else if (mSettings.isBundleDeltasEnabled()) {
return BundleDeltaClient.ClientType.DEV_SUPPORT;
} else {
return BundleDeltaClient.ClientType.NONE;
}
}
/**

View File

@ -32,6 +32,7 @@ import com.facebook.react.bridge.CatalystInstance;
import com.facebook.react.bridge.DefaultNativeModuleCallExceptionHandler;
import com.facebook.react.bridge.JavaJSExecutor;
import com.facebook.react.bridge.JavaScriptContextHolder;
import com.facebook.react.bridge.NativeDeltaClient;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactMarker;
import com.facebook.react.bridge.ReactMarkerConstants;
@ -973,7 +974,7 @@ public class DevSupportManagerImpl implements
mDevServerHelper.downloadBundleFromURL(
new DevBundleDownloadListener() {
@Override
public void onSuccess() {
public void onSuccess(final @Nullable NativeDeltaClient nativeDeltaClient) {
mDevLoadingViewController.hide();
mDevLoadingViewVisible = false;
synchronized (DevSupportManagerImpl.this) {
@ -981,14 +982,14 @@ public class DevSupportManagerImpl implements
mBundleStatus.updateTimestamp = System.currentTimeMillis();
}
if (mBundleDownloadListener != null) {
mBundleDownloadListener.onSuccess();
mBundleDownloadListener.onSuccess(nativeDeltaClient);
}
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
ReactMarker.logMarker(ReactMarkerConstants.DOWNLOAD_END, bundleInfo.toJSONString());
mReactInstanceManagerHelper.onJSBundleLoadedFromServer();
mReactInstanceManagerHelper.onJSBundleLoadedFromServer(nativeDeltaClient);
}
});
}

View File

@ -8,9 +8,9 @@
package com.facebook.react.devsupport;
import android.app.Activity;
import com.facebook.react.bridge.JavaJSExecutor;
import com.facebook.react.bridge.NativeDeltaClient;
import javax.annotation.Nullable;
/**
@ -27,7 +27,7 @@ public interface ReactInstanceManagerDevHelper {
/**
* Notify react instance manager about new JS bundle version downloaded from the server.
*/
void onJSBundleLoadedFromServer();
void onJSBundleLoadedFromServer(@Nullable NativeDeltaClient nativeDeltaClient);
/**
* Request to toggle the react element inspector.

View File

@ -7,10 +7,11 @@
package com.facebook.react.devsupport.interfaces;
import com.facebook.react.bridge.NativeDeltaClient;
import javax.annotation.Nullable;
public interface DevBundleDownloadListener {
void onSuccess();
void onSuccess(@Nullable NativeDeltaClient nativeDeltaClient);
void onProgress(@Nullable String status, @Nullable Integer done, @Nullable Integer total);
void onFailure(Exception cause);
}