Skip to content

Commit

Permalink
Add end to end Delta support to Android devices
Browse files Browse the repository at this point in the history
Reviewed By: davidaurelio

Differential Revision: D6338677

fbshipit-source-id: 8fa8f618bf8d6cb2291ce4405093cad23bd47fc3
  • Loading branch information
rafeca authored and facebook-github-bot committed Nov 17, 2017
1 parent 0ac5a52 commit 231c7a0
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 51 deletions.
4 changes: 3 additions & 1 deletion Libraries/Utilities/HMRClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -36,6 +35,8 @@
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";
Expand All @@ -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 {
Expand Down Expand Up @@ -102,13 +108,22 @@ public void downloadBundleFromURL(
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
Expand Down Expand Up @@ -161,6 +176,7 @@ public void execute(Map<String, String> headers, Buffer body, boolean finished)
if (!headers.containsKey("Content-Type") || !headers.get("Content-Type").equals("application/json")) {
return;
}

try {
JSONObject progress = new JSONObject(body.readUtf8());
String status = null;
Expand Down Expand Up @@ -202,14 +218,15 @@ public void cancelDownloadBundleFromURL() {
}
}

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();
Expand All @@ -232,21 +249,135 @@ private static void processBundleResult(
}

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) {
output.close();
}
}

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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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";
Expand Down Expand Up @@ -81,10 +80,11 @@ public boolean isJSMinifyEnabled() {

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();
}
}
Expand Down Expand Up @@ -114,6 +114,16 @@ public void setElementInspectorEnabled(boolean enabled) {
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);
Expand Down
Loading

0 comments on commit 231c7a0

Please sign in to comment.