Add end to end Delta support to Android devices

Reviewed By: davidaurelio

Differential Revision: D6338677

fbshipit-source-id: 8fa8f618bf8d6cb2291ce4405093cad23bd47fc3
This commit is contained in:
Rafael Oleza 2017-11-17 07:30:13 -08:00 committed by Facebook Github Bot
parent 0ac5a5230c
commit 231c7a0304
5 changed files with 202 additions and 51 deletions

View File

@ -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) => {

View File

@ -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) {

View File

@ -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);

View File

@ -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);
}
/**

View File

@ -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"