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.JavaJSExecutor;
import com.facebook.react.bridge.JavaScriptExecutor; import com.facebook.react.bridge.JavaScriptExecutor;
import com.facebook.react.bridge.JavaScriptExecutorFactory; import com.facebook.react.bridge.JavaScriptExecutorFactory;
import com.facebook.react.bridge.NativeDeltaClient;
import com.facebook.react.bridge.NativeModuleCallExceptionHandler; import com.facebook.react.bridge.NativeModuleCallExceptionHandler;
import com.facebook.react.bridge.NativeModuleRegistry; import com.facebook.react.bridge.NativeModuleRegistry;
import com.facebook.react.bridge.NotThreadSafeBridgeIdleDebugListener; 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.soloader.SoLoader;
import com.facebook.systrace.Systrace; import com.facebook.systrace.Systrace;
import com.facebook.systrace.SystraceMessage; import com.facebook.systrace.SystraceMessage;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
@ -270,8 +272,8 @@ public class ReactInstanceManager {
} }
@Override @Override
public void onJSBundleLoadedFromServer() { public void onJSBundleLoadedFromServer(@Nullable NativeDeltaClient nativeDeltaClient) {
ReactInstanceManager.this.onJSBundleLoadedFromServer(); ReactInstanceManager.this.onJSBundleLoadedFromServer(nativeDeltaClient);
} }
@Override @Override
@ -360,7 +362,7 @@ public class ReactInstanceManager {
!devSettings.isRemoteJSDebugEnabled()) { !devSettings.isRemoteJSDebugEnabled()) {
// If there is a up-to-date bundle downloaded from server, // If there is a up-to-date bundle downloaded from server,
// with remote JS debugging disabled, always use that. // with remote JS debugging disabled, always use that.
onJSBundleLoadedFromServer(); onJSBundleLoadedFromServer(null);
} else if (mBundleLoader == null) { } else if (mBundleLoader == null) {
mDevSupportManager.handleReloadJS(); mDevSupportManager.handleReloadJS();
} else { } else {
@ -848,12 +850,17 @@ public class ReactInstanceManager {
} }
@ThreadConfined(UI) @ThreadConfined(UI)
private void onJSBundleLoadedFromServer() { private void onJSBundleLoadedFromServer(@Nullable NativeDeltaClient nativeDeltaClient) {
Log.d(ReactConstants.TAG, "ReactInstanceManager.onJSBundleLoadedFromServer()"); Log.d(ReactConstants.TAG, "ReactInstanceManager.onJSBundleLoadedFromServer()");
recreateReactContextInBackground(
mJavaScriptExecutorFactory, JSBundleLoader bundleLoader = nativeDeltaClient == null
JSBundleLoader.createCachedBundleFromNetworkLoader( ? JSBundleLoader.createCachedBundleFromNetworkLoader(
mDevSupportManager.getSourceUrl(), mDevSupportManager.getDownloadedJSBundleFile())); mDevSupportManager.getSourceUrl(),
mDevSupportManager.getDownloadedJSBundleFile())
: JSBundleLoader.createDeltaFromNetworkLoader(
mDevSupportManager.getSourceUrl(), nativeDeltaClient);
recreateReactContextInBackground(mJavaScriptExecutorFactory, bundleLoader);
} }
@ThreadConfined(UI) @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; package com.facebook.react.devsupport;
import android.util.JsonReader;
import android.util.JsonToken;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import javax.annotation.Nullable; 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; import okio.BufferedSource;
public class BundleDeltaClient { public abstract class BundleDeltaClient {
final LinkedHashMap<Number, byte[]> mPreModules = new LinkedHashMap<Number, byte[]>(); private static final String METRO_DELTA_ID_HEADER = "X-Metro-Delta-ID";
final LinkedHashMap<Number, byte[]> mDeltaModules = new LinkedHashMap<Number, byte[]>(); @Nullable private String mDeltaId;
final LinkedHashMap<Number, byte[]> mPostModules = new LinkedHashMap<Number, byte[]>();
@Nullable String mDeltaId; public enum ClientType {
NONE,
DEV_SUPPORT,
NATIVE
}
static boolean isDeltaUrl(String bundleUrl) { static boolean isDeltaUrl(String bundleUrl) {
return bundleUrl.indexOf(".delta?") != -1; 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() { public void reset() {
mDeltaId = null; mDeltaId = null;
mDeltaModules.clear();
mPreModules.clear();
mPostModules.clear();
} }
public String toDeltaUrl(String bundleURL) { public Pair<Boolean, NativeDeltaClient> processDelta(
if (isDeltaUrl(bundleURL) && mDeltaId != null) { Headers headers,
return bundleURL + "&deltaBundleId=" + mDeltaId; BufferedSource body,
} File outputFile) throws IOException {
return bundleURL;
mDeltaId = headers.get(METRO_DELTA_ID_HEADER);
return processDelta(body, outputFile);
} }
public synchronized boolean storeDeltaInFile(BufferedSource body, File outputFile) private static class BundleDeltaJavaClient extends BundleDeltaClient {
throws IOException {
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(); @Override
public boolean canHandle(ClientType type) {
int numChangedModules = 0; return type == ClientType.DEV_SUPPORT;
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();
}
} }
jsonReader.endObject(); public void reset() {
jsonReader.close(); super.reset();
mDeltaModules.clear();
if (numChangedModules == 0) { mPreModules.clear();
// If we receive an empty delta, we don't need to save the file again (it'll have the mPostModules.clear();
// same content).
return false;
} }
FileOutputStream fileOutputStream = new FileOutputStream(outputFile); @Override
public synchronized Pair<Boolean, NativeDeltaClient> processDelta(
BufferedSource body,
File outputFile) throws IOException {
try { JsonReader jsonReader = new JsonReader(new InputStreamReader(body.inputStream()));
for (byte[] code : mPreModules.values()) { jsonReader.beginObject();
fileOutputStream.write(code); int numChangedModules = 0;
fileOutputStream.write('\n');
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()) { jsonReader.endObject();
fileOutputStream.write(code); jsonReader.close();
fileOutputStream.write('\n');
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 fileOutputStream = new FileOutputStream(outputFile);
fileOutputStream.write(code);
fileOutputStream.write('\n'); 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(); return Pair.create(Boolean.TRUE, null);
fileOutputStream.close();
} }
return true; private static int patchDelta(JsonReader jsonReader, LinkedHashMap<Number, byte[]> map)
} throws IOException {
private static int patchDelta(JsonReader jsonReader, LinkedHashMap<Number, byte[]> map)
throws IOException {
jsonReader.beginArray();
int numModules = 0;
while (jsonReader.hasNext()) {
jsonReader.beginArray(); jsonReader.beginArray();
int moduleId = jsonReader.nextInt(); int numModules = 0;
while (jsonReader.hasNext()) {
jsonReader.beginArray();
if (jsonReader.peek() == JsonToken.NULL) { int moduleId = jsonReader.nextInt();
jsonReader.skipValue();
map.remove(moduleId); if (jsonReader.peek() == JsonToken.NULL) {
} else { jsonReader.skipValue();
map.put(moduleId, jsonReader.nextString().getBytes()); map.remove(moduleId);
} else {
map.put(moduleId, jsonReader.nextString().getBytes());
}
jsonReader.endArray();
numModules++;
} }
jsonReader.endArray(); 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; package com.facebook.react.devsupport;
import android.util.Log; import android.util.Log;
import android.util.Pair;
import com.facebook.common.logging.FLog; import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions; import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.NativeDeltaClient;
import com.facebook.react.common.DebugServerException; import com.facebook.react.common.DebugServerException;
import com.facebook.react.common.ReactConstants; import com.facebook.react.common.ReactConstants;
import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener; import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener;
@ -40,7 +42,7 @@ public class BundleDownloader {
private final OkHttpClient mClient; private final OkHttpClient mClient;
private final BundleDeltaClient mBundleDeltaClient = new BundleDeltaClient(); private BundleDeltaClient mBundleDeltaClient;
private @Nullable Call mDownloadBundleFromURLCall; private @Nullable Call mDownloadBundleFromURLCall;
@ -95,14 +97,15 @@ public class BundleDownloader {
} }
public void downloadBundleFromURL( public void downloadBundleFromURL(
final DevBundleDownloadListener callback, final DevBundleDownloadListener callback,
final File outputFile, final File outputFile,
final String bundleURL, final String bundleURL,
final @Nullable BundleInfo bundleInfo) { final @Nullable BundleInfo bundleInfo,
final BundleDeltaClient.ClientType clientType) {
final Request request = final Request request =
new Request.Builder() 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 // 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, // multipart message. This temporarily disables the multipart mode to work around it,
// but // but
@ -146,7 +149,7 @@ public class BundleDownloader {
try (Response r = response) { try (Response r = response) {
if (match.find()) { if (match.find()) {
processMultipartResponse( processMultipartResponse(
url, r, match.group(1), outputFile, bundleInfo, callback); url, r, match.group(1), outputFile, bundleInfo, clientType, callback);
} else { } else {
// In case the server doesn't support multipart/mixed responses, fallback to normal // In case the server doesn't support multipart/mixed responses, fallback to normal
// download. // download.
@ -157,6 +160,7 @@ public class BundleDownloader {
Okio.buffer(r.body().source()), Okio.buffer(r.body().source()),
outputFile, outputFile,
bundleInfo, bundleInfo,
clientType,
callback); 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( private void processMultipartResponse(
final String url, final String url,
final Response response, final Response response,
String boundary, String boundary,
final File outputFile, final File outputFile,
@Nullable final BundleInfo bundleInfo, @Nullable final BundleInfo bundleInfo,
final BundleDeltaClient.ClientType clientType,
final DevBundleDownloadListener callback) final DevBundleDownloadListener callback)
throws IOException { throws IOException {
@ -193,7 +204,7 @@ public class BundleDownloader {
status = Integer.parseInt(headers.get("X-Http-Status")); status = Integer.parseInt(headers.get("X-Http-Status"));
} }
processBundleResult( processBundleResult(
url, status, Headers.of(headers), body, outputFile, bundleInfo, callback); url, status, Headers.of(headers), body, outputFile, bundleInfo, clientType, callback);
} else { } else {
if (!headers.containsKey("Content-Type") if (!headers.containsKey("Content-Type")
|| !headers.get("Content-Type").equals("application/json")) { || !headers.get("Content-Type").equals("application/json")) {
@ -249,6 +260,7 @@ public class BundleDownloader {
BufferedSource body, BufferedSource body,
File outputFile, File outputFile,
BundleInfo bundleInfo, BundleInfo bundleInfo,
BundleDeltaClient.ClientType clientType,
DevBundleDownloadListener callback) DevBundleDownloadListener callback)
throws IOException { throws IOException {
// Check for server errors. If the server error has the expected form, fail with more info. // 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"); File tmpFile = new File(outputFile.getPath() + ".tmp");
boolean bundleUpdated; boolean bundleWritten;
NativeDeltaClient nativeDeltaClient = null;
if (BundleDeltaClient.isDeltaUrl(url)) { if (BundleDeltaClient.isDeltaUrl(url)) {
// If the bundle URL has the delta extension, we need to use the delta patching logic. // 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 { } else {
mBundleDeltaClient.reset(); mBundleDeltaClient = null;
bundleUpdated = storePlainJSInFile(body, tmpFile); 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 we have received a new bundle from the server, move it to its final destination.
if (!tmpFile.renameTo(outputFile)) { if (!tmpFile.renameTo(outputFile)) {
throw new IOException("Couldn't rename " + tmpFile + " to " + 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) private static boolean storePlainJSInFile(BufferedSource body, File outputFile)

View File

@ -375,7 +375,17 @@ public class DevServerHelper {
public void downloadBundleFromURL( public void downloadBundleFromURL(
DevBundleDownloadListener callback, DevBundleDownloadListener callback,
File outputFile, String bundleURL, BundleDownloader.BundleInfo bundleInfo) { 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.DefaultNativeModuleCallExceptionHandler;
import com.facebook.react.bridge.JavaJSExecutor; import com.facebook.react.bridge.JavaJSExecutor;
import com.facebook.react.bridge.JavaScriptContextHolder; import com.facebook.react.bridge.JavaScriptContextHolder;
import com.facebook.react.bridge.NativeDeltaClient;
import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactMarker; import com.facebook.react.bridge.ReactMarker;
import com.facebook.react.bridge.ReactMarkerConstants; import com.facebook.react.bridge.ReactMarkerConstants;
@ -973,7 +974,7 @@ public class DevSupportManagerImpl implements
mDevServerHelper.downloadBundleFromURL( mDevServerHelper.downloadBundleFromURL(
new DevBundleDownloadListener() { new DevBundleDownloadListener() {
@Override @Override
public void onSuccess() { public void onSuccess(final @Nullable NativeDeltaClient nativeDeltaClient) {
mDevLoadingViewController.hide(); mDevLoadingViewController.hide();
mDevLoadingViewVisible = false; mDevLoadingViewVisible = false;
synchronized (DevSupportManagerImpl.this) { synchronized (DevSupportManagerImpl.this) {
@ -981,14 +982,14 @@ public class DevSupportManagerImpl implements
mBundleStatus.updateTimestamp = System.currentTimeMillis(); mBundleStatus.updateTimestamp = System.currentTimeMillis();
} }
if (mBundleDownloadListener != null) { if (mBundleDownloadListener != null) {
mBundleDownloadListener.onSuccess(); mBundleDownloadListener.onSuccess(nativeDeltaClient);
} }
UiThreadUtil.runOnUiThread( UiThreadUtil.runOnUiThread(
new Runnable() { new Runnable() {
@Override @Override
public void run() { public void run() {
ReactMarker.logMarker(ReactMarkerConstants.DOWNLOAD_END, bundleInfo.toJSONString()); ReactMarker.logMarker(ReactMarkerConstants.DOWNLOAD_END, bundleInfo.toJSONString());
mReactInstanceManagerHelper.onJSBundleLoadedFromServer(); mReactInstanceManagerHelper.onJSBundleLoadedFromServer(nativeDeltaClient);
} }
}); });
} }

View File

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

View File

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