Add end to end Delta support to Android devices
Reviewed By: davidaurelio Differential Revision: D6338677 fbshipit-source-id: 8fa8f618bf8d6cb2291ce4405093cad23bd47fc3
This commit is contained in:
parent
0ac5a5230c
commit
231c7a0304
|
@ -33,10 +33,12 @@ const HMRClient = {
|
|||
? `${host}:${port}`
|
||||
: host;
|
||||
|
||||
bundleEntry = bundleEntry.replace(/\.(bundle|delta)/, '.js');
|
||||
|
||||
// Build the websocket url
|
||||
const wsUrl = `ws://${wsHostPort}/hot?` +
|
||||
`platform=${platform}&` +
|
||||
`bundleEntry=${bundleEntry.replace('.bundle', '.js')}`;
|
||||
`bundleEntry=${bundleEntry}`;
|
||||
|
||||
const activeWS = new WebSocket(wsUrl);
|
||||
activeWS.onerror = (e) => {
|
||||
|
|
|
@ -9,24 +9,23 @@
|
|||
|
||||
package com.facebook.react.devsupport;
|
||||
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonToken;
|
||||
import android.util.Log;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.infer.annotation.Assertions;
|
||||
import com.facebook.react.common.DebugServerException;
|
||||
import com.facebook.react.common.ReactConstants;
|
||||
import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.infer.annotation.Assertions;
|
||||
import com.facebook.react.common.ReactConstants;
|
||||
import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener;
|
||||
import com.facebook.react.common.DebugServerException;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.Callback;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
@ -36,6 +35,8 @@ import okio.Buffer;
|
|||
import okio.BufferedSource;
|
||||
import okio.Okio;
|
||||
import okio.Sink;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class BundleDownloader {
|
||||
private static final String TAG = "BundleDownloader";
|
||||
|
@ -45,6 +46,11 @@ public class BundleDownloader {
|
|||
|
||||
private final OkHttpClient mClient;
|
||||
|
||||
private final LinkedHashMap<Number, byte[]> mPreModules = new LinkedHashMap<>();
|
||||
private final LinkedHashMap<Number, byte[]> mDeltaModules = new LinkedHashMap<>();
|
||||
private final LinkedHashMap<Number, byte[]> mPostModules = new LinkedHashMap<>();
|
||||
|
||||
private @Nullable String mDeltaId;
|
||||
private @Nullable Call mDownloadBundleFromURLCall;
|
||||
|
||||
public static class BundleInfo {
|
||||
|
@ -102,13 +108,22 @@ public class BundleDownloader {
|
|||
final File outputFile,
|
||||
final String bundleURL,
|
||||
final @Nullable BundleInfo bundleInfo) {
|
||||
final Request request = new Request.Builder()
|
||||
.url(bundleURL)
|
||||
// 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
|
||||
// it means there is no progress bar displayed in the React Native overlay anymore.
|
||||
//.addHeader("Accept", "multipart/mixed")
|
||||
.build();
|
||||
|
||||
String finalUrl = bundleURL;
|
||||
|
||||
if (isDeltaUrl(bundleURL) && mDeltaId != null) {
|
||||
finalUrl += "&deltaBundleId=" + mDeltaId;
|
||||
}
|
||||
|
||||
final Request request =
|
||||
new Request.Builder()
|
||||
.url(finalUrl)
|
||||
// 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
|
||||
// it means there is no progress bar displayed in the React Native overlay anymore.
|
||||
// .addHeader("Accept", "multipart/mixed")
|
||||
.build();
|
||||
mDownloadBundleFromURLCall = Assertions.assertNotNull(mClient.newCall(request));
|
||||
mDownloadBundleFromURLCall.enqueue(new Callback() {
|
||||
@Override
|
||||
|
@ -161,6 +176,7 @@ public class BundleDownloader {
|
|||
if (!headers.containsKey("Content-Type") || !headers.get("Content-Type").equals("application/json")) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
JSONObject progress = new JSONObject(body.readUtf8());
|
||||
String status = null;
|
||||
|
@ -202,14 +218,15 @@ public class BundleDownloader {
|
|||
}
|
||||
}
|
||||
|
||||
private static void processBundleResult(
|
||||
private void processBundleResult(
|
||||
String url,
|
||||
int statusCode,
|
||||
okhttp3.Headers headers,
|
||||
BufferedSource body,
|
||||
File outputFile,
|
||||
BundleInfo bundleInfo,
|
||||
DevBundleDownloadListener callback) throws IOException {
|
||||
DevBundleDownloadListener callback)
|
||||
throws IOException {
|
||||
// Check for server errors. If the server error has the expected form, fail with more info.
|
||||
if (statusCode != 200) {
|
||||
String bodyString = body.readUtf8();
|
||||
|
@ -232,9 +249,32 @@ public class BundleDownloader {
|
|||
}
|
||||
|
||||
File tmpFile = new File(outputFile.getPath() + ".tmp");
|
||||
|
||||
boolean bundleUpdated;
|
||||
|
||||
if (isDeltaUrl(url)) {
|
||||
// If the bundle URL has the delta extension, we need to use the delta patching logic.
|
||||
bundleUpdated = storeDeltaInFile(body, tmpFile);
|
||||
} else {
|
||||
resetDeltaCache();
|
||||
bundleUpdated = storePlainJSInFile(body, tmpFile);
|
||||
}
|
||||
|
||||
if (bundleUpdated) {
|
||||
// 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();
|
||||
}
|
||||
|
||||
private static boolean storePlainJSInFile(BufferedSource body, File outputFile)
|
||||
throws IOException {
|
||||
Sink output = null;
|
||||
try {
|
||||
output = Okio.sink(tmpFile);
|
||||
output = Okio.sink(outputFile);
|
||||
body.readAll(output);
|
||||
} finally {
|
||||
if (output != null) {
|
||||
|
@ -242,11 +282,102 @@ public class BundleDownloader {
|
|||
}
|
||||
}
|
||||
|
||||
if (tmpFile.renameTo(outputFile)) {
|
||||
callback.onSuccess();
|
||||
} else {
|
||||
throw new IOException("Couldn't rename " + tmpFile + " to " + outputFile);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean storeDeltaInFile(BufferedSource body, File outputFile) throws IOException {
|
||||
|
||||
JsonReader jsonReader = new JsonReader(new InputStreamReader(body.inputStream()));
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int patchDelta(JsonReader jsonReader, LinkedHashMap<Number, byte[]> map)
|
||||
throws IOException {
|
||||
jsonReader.beginArray();
|
||||
|
||||
int numModules = 0;
|
||||
while (jsonReader.hasNext()) {
|
||||
jsonReader.beginArray();
|
||||
|
||||
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();
|
||||
|
||||
return numModules;
|
||||
}
|
||||
|
||||
private void resetDeltaCache() {
|
||||
mDeltaId = null;
|
||||
|
||||
mDeltaModules.clear();
|
||||
mPreModules.clear();
|
||||
mPostModules.clear();
|
||||
}
|
||||
|
||||
private static boolean isDeltaUrl(String bundleUrl) {
|
||||
return bundleUrl.indexOf(".delta?") != -1;
|
||||
}
|
||||
|
||||
private static void populateBundleInfo(String url, okhttp3.Headers headers, BundleInfo bundleInfo) {
|
||||
|
|
|
@ -9,12 +9,10 @@
|
|||
|
||||
package com.facebook.react.devsupport;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import com.facebook.react.common.annotations.VisibleForTesting;
|
||||
import com.facebook.react.modules.debug.interfaces.DeveloperSettings;
|
||||
import com.facebook.react.packagerconnection.PackagerConnectionSettings;
|
||||
|
@ -32,6 +30,7 @@ public class DevInternalSettings implements
|
|||
private static final String PREFS_FPS_DEBUG_KEY = "fps_debug";
|
||||
private static final String PREFS_JS_DEV_MODE_DEBUG_KEY = "js_dev_mode_debug";
|
||||
private static final String PREFS_JS_MINIFY_DEBUG_KEY = "js_minify_debug";
|
||||
private static final String PREFS_JS_BUNDLE_DELTAS_KEY = "js_bundle_deltas";
|
||||
private static final String PREFS_ANIMATIONS_DEBUG_KEY = "animations_debug";
|
||||
private static final String PREFS_RELOAD_ON_JS_CHANGE_KEY = "reload_on_js_change";
|
||||
private static final String PREFS_INSPECTOR_DEBUG_KEY = "inspector_debug";
|
||||
|
@ -81,10 +80,11 @@ public class DevInternalSettings implements
|
|||
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
if (mListener != null) {
|
||||
if (PREFS_FPS_DEBUG_KEY.equals(key) ||
|
||||
PREFS_RELOAD_ON_JS_CHANGE_KEY.equals(key) ||
|
||||
PREFS_JS_DEV_MODE_DEBUG_KEY.equals(key) ||
|
||||
PREFS_JS_MINIFY_DEBUG_KEY.equals(key)) {
|
||||
if (PREFS_FPS_DEBUG_KEY.equals(key)
|
||||
|| PREFS_RELOAD_ON_JS_CHANGE_KEY.equals(key)
|
||||
|| PREFS_JS_DEV_MODE_DEBUG_KEY.equals(key)
|
||||
|| PREFS_JS_BUNDLE_DELTAS_KEY.equals(key)
|
||||
|| PREFS_JS_MINIFY_DEBUG_KEY.equals(key)) {
|
||||
mListener.onInternalSettingsChanged();
|
||||
}
|
||||
}
|
||||
|
@ -114,6 +114,16 @@ public class DevInternalSettings implements
|
|||
mPreferences.edit().putBoolean(PREFS_INSPECTOR_DEBUG_KEY, enabled).apply();
|
||||
}
|
||||
|
||||
@SuppressLint("SharedPreferencesUse")
|
||||
public boolean isBundleDeltasEnabled() {
|
||||
return mPreferences.getBoolean(PREFS_JS_BUNDLE_DELTAS_KEY, false);
|
||||
}
|
||||
|
||||
@SuppressLint("SharedPreferencesUse")
|
||||
public void setBundleDeltasEnabled(boolean enabled) {
|
||||
mPreferences.edit().putBoolean(PREFS_JS_BUNDLE_DELTAS_KEY, enabled).apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRemoteJSDebugEnabled() {
|
||||
return mPreferences.getBoolean(PREFS_REMOTE_JS_DEBUG_KEY, false);
|
||||
|
|
|
@ -63,10 +63,8 @@ public class DevServerHelper {
|
|||
private static final String RELOAD_APP_ACTION_SUFFIX = ".RELOAD_APP_ACTION";
|
||||
|
||||
private static final String BUNDLE_URL_FORMAT =
|
||||
"http://%s/%s.bundle?platform=android&dev=%s&minify=%s";
|
||||
"http://%s/%s.%s?platform=android&dev=%s&minify=%s";
|
||||
private static final String RESOURCE_URL_FORMAT = "http://%s/%s";
|
||||
private static final String SOURCE_MAP_URL_FORMAT =
|
||||
BUNDLE_URL_FORMAT.replaceFirst("\\.bundle", ".map");
|
||||
private static final String LAUNCH_JS_DEVTOOLS_COMMAND_URL_FORMAT =
|
||||
"http://%s/launch-js-devtools";
|
||||
private static final String ONCHANGE_ENDPOINT_URL_FORMAT =
|
||||
|
@ -357,11 +355,15 @@ public class DevServerHelper {
|
|||
}
|
||||
|
||||
private static String createBundleURL(
|
||||
String host,
|
||||
String jsModulePath,
|
||||
boolean devMode,
|
||||
boolean jsMinify) {
|
||||
return String.format(Locale.US, BUNDLE_URL_FORMAT, host, jsModulePath, devMode, jsMinify);
|
||||
String host, String jsModulePath, boolean devMode, boolean jsMinify, boolean useDeltas) {
|
||||
return String.format(
|
||||
Locale.US,
|
||||
BUNDLE_URL_FORMAT,
|
||||
host,
|
||||
jsModulePath,
|
||||
useDeltas ? "delta" : "bundle",
|
||||
devMode,
|
||||
jsMinify);
|
||||
}
|
||||
|
||||
private static String createResourceURL(String host, String resourcePath) {
|
||||
|
@ -378,10 +380,11 @@ public class DevServerHelper {
|
|||
|
||||
public String getDevServerBundleURL(final String jsModulePath) {
|
||||
return createBundleURL(
|
||||
mSettings.getPackagerConnectionSettings().getDebugServerHost(),
|
||||
jsModulePath,
|
||||
getDevMode(),
|
||||
getJSMinifyMode());
|
||||
mSettings.getPackagerConnectionSettings().getDebugServerHost(),
|
||||
jsModulePath,
|
||||
getDevMode(),
|
||||
getJSMinifyMode(),
|
||||
mSettings.isBundleDeltasEnabled());
|
||||
}
|
||||
|
||||
public void isPackagerRunning(final PackagerStatusCallback callback) {
|
||||
|
@ -540,9 +543,10 @@ public class DevServerHelper {
|
|||
public String getSourceMapUrl(String mainModuleName) {
|
||||
return String.format(
|
||||
Locale.US,
|
||||
SOURCE_MAP_URL_FORMAT,
|
||||
BUNDLE_URL_FORMAT,
|
||||
mSettings.getPackagerConnectionSettings().getDebugServerHost(),
|
||||
mainModuleName,
|
||||
"map",
|
||||
getDevMode(),
|
||||
getJSMinifyMode());
|
||||
}
|
||||
|
@ -553,6 +557,7 @@ public class DevServerHelper {
|
|||
BUNDLE_URL_FORMAT,
|
||||
mSettings.getPackagerConnectionSettings().getDebugServerHost(),
|
||||
mainModuleName,
|
||||
mSettings.isBundleDeltasEnabled() ? "delta" : "bundle",
|
||||
getDevMode(),
|
||||
getJSMinifyMode());
|
||||
}
|
||||
|
@ -562,10 +567,7 @@ public class DevServerHelper {
|
|||
// same as the one needed to connect to the same server from the JavaScript proxy running on the
|
||||
// host itself.
|
||||
return createBundleURL(
|
||||
getHostForJSProxy(),
|
||||
mainModuleName,
|
||||
getDevMode(),
|
||||
getJSMinifyMode());
|
||||
getHostForJSProxy(), mainModuleName, getDevMode(), getJSMinifyMode(), false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -19,6 +19,12 @@
|
|||
android:summary="Load JavaScript bundle with minify=true for debugging minification issues."
|
||||
android:defaultValue="false"
|
||||
/>
|
||||
<CheckBoxPreference
|
||||
android:key="js_bundle_deltas"
|
||||
android:title="Use JS Deltas"
|
||||
android:summary="Request delta bundles from metro to get faster reloads (Experimental)"
|
||||
android:defaultValue="false"
|
||||
/>
|
||||
<CheckBoxPreference
|
||||
android:key="animations_debug"
|
||||
android:title="Animations FPS Summaries"
|
||||
|
|
Loading…
Reference in New Issue