From dd036c23284c52da693302727b4d1b112c37e807 Mon Sep 17 00:00:00 2001 From: David Aurelio Date: Thu, 3 May 2018 08:38:13 -0700 Subject: [PATCH] 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 --- .../facebook/react/ReactInstanceManager.java | 23 +- .../react/devsupport/BundleDeltaClient.java | 224 ++++++++++++------ .../react/devsupport/BundleDownloader.java | 52 ++-- .../react/devsupport/DevServerHelper.java | 12 +- .../devsupport/DevSupportManagerImpl.java | 7 +- .../ReactInstanceManagerDevHelper.java | 4 +- .../interfaces/DevBundleDownloadListener.java | 3 +- 7 files changed, 222 insertions(+), 103 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java index b97f6f45b..24a57c759 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java @@ -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) diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/BundleDeltaClient.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/BundleDeltaClient.java index 68ef0ce13..93fbb4967 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/BundleDeltaClient.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/BundleDeltaClient.java @@ -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 mPreModules = new LinkedHashMap(); - final LinkedHashMap mDeltaModules = new LinkedHashMap(); - final LinkedHashMap mPostModules = new LinkedHashMap(); - @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 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 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 mPreModules = new LinkedHashMap(); + final LinkedHashMap mDeltaModules = new LinkedHashMap(); + final LinkedHashMap mPostModules = new LinkedHashMap(); - 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 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 map) - throws IOException { - jsonReader.beginArray(); - - int numModules = 0; - while (jsonReader.hasNext()) { + private static int patchDelta(JsonReader jsonReader, LinkedHashMap 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 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(); + } } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/BundleDownloader.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/BundleDownloader.java index b7c7ed7b9..69f50a832 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/BundleDownloader.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/BundleDownloader.java @@ -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 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) diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java index 946377680..f0638b0b4 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java @@ -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; + } } /** diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java index ea25bf86c..667350370 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java @@ -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); } }); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/ReactInstanceManagerDevHelper.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/ReactInstanceManagerDevHelper.java index 7694c68db..7033a21fd 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/ReactInstanceManagerDevHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/ReactInstanceManagerDevHelper.java @@ -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. diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevBundleDownloadListener.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevBundleDownloadListener.java index 661a8cba2..b08ba9706 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevBundleDownloadListener.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevBundleDownloadListener.java @@ -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); }