Skip to content

Commit 231c7a0

Browse files
rafecafacebook-github-bot
authored andcommitted
Add end to end Delta support to Android devices
Reviewed By: davidaurelio Differential Revision: D6338677 fbshipit-source-id: 8fa8f618bf8d6cb2291ce4405093cad23bd47fc3
1 parent 0ac5a52 commit 231c7a0

File tree

5 files changed

+202
-51
lines changed

5 files changed

+202
-51
lines changed

Libraries/Utilities/HMRClient.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@ const HMRClient = {
3333
? `${host}:${port}`
3434
: host;
3535

36+
bundleEntry = bundleEntry.replace(/\.(bundle|delta)/, '.js');
37+
3638
// Build the websocket url
3739
const wsUrl = `ws://${wsHostPort}/hot?` +
3840
`platform=${platform}&` +
39-
`bundleEntry=${bundleEntry.replace('.bundle', '.js')}`;
41+
`bundleEntry=${bundleEntry}`;
4042

4143
const activeWS = new WebSocket(wsUrl);
4244
activeWS.onerror = (e) => {

ReactAndroid/src/main/java/com/facebook/react/devsupport/BundleDownloader.java

+157-26
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,23 @@
99

1010
package com.facebook.react.devsupport;
1111

12+
import android.util.JsonReader;
13+
import android.util.JsonToken;
1214
import android.util.Log;
13-
import javax.annotation.Nullable;
14-
15+
import com.facebook.common.logging.FLog;
16+
import com.facebook.infer.annotation.Assertions;
17+
import com.facebook.react.common.DebugServerException;
18+
import com.facebook.react.common.ReactConstants;
19+
import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener;
1520
import java.io.File;
21+
import java.io.FileOutputStream;
1622
import java.io.IOException;
23+
import java.io.InputStreamReader;
24+
import java.util.LinkedHashMap;
1725
import java.util.Map;
1826
import java.util.regex.Matcher;
1927
import java.util.regex.Pattern;
20-
21-
import com.facebook.common.logging.FLog;
22-
import com.facebook.infer.annotation.Assertions;
23-
import com.facebook.react.common.ReactConstants;
24-
import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener;
25-
import com.facebook.react.common.DebugServerException;
26-
27-
import org.json.JSONException;
28-
import org.json.JSONObject;
29-
28+
import javax.annotation.Nullable;
3029
import okhttp3.Call;
3130
import okhttp3.Callback;
3231
import okhttp3.OkHttpClient;
@@ -36,6 +35,8 @@
3635
import okio.BufferedSource;
3736
import okio.Okio;
3837
import okio.Sink;
38+
import org.json.JSONException;
39+
import org.json.JSONObject;
3940

4041
public class BundleDownloader {
4142
private static final String TAG = "BundleDownloader";
@@ -45,6 +46,11 @@ public class BundleDownloader {
4546

4647
private final OkHttpClient mClient;
4748

49+
private final LinkedHashMap<Number, byte[]> mPreModules = new LinkedHashMap<>();
50+
private final LinkedHashMap<Number, byte[]> mDeltaModules = new LinkedHashMap<>();
51+
private final LinkedHashMap<Number, byte[]> mPostModules = new LinkedHashMap<>();
52+
53+
private @Nullable String mDeltaId;
4854
private @Nullable Call mDownloadBundleFromURLCall;
4955

5056
public static class BundleInfo {
@@ -102,13 +108,22 @@ public void downloadBundleFromURL(
102108
final File outputFile,
103109
final String bundleURL,
104110
final @Nullable BundleInfo bundleInfo) {
105-
final Request request = new Request.Builder()
106-
.url(bundleURL)
107-
// FIXME: there is a bug that makes MultipartStreamReader to never find the end of the
108-
// multipart message. This temporarily disables the multipart mode to work around it, but
109-
// it means there is no progress bar displayed in the React Native overlay anymore.
110-
//.addHeader("Accept", "multipart/mixed")
111-
.build();
111+
112+
String finalUrl = bundleURL;
113+
114+
if (isDeltaUrl(bundleURL) && mDeltaId != null) {
115+
finalUrl += "&deltaBundleId=" + mDeltaId;
116+
}
117+
118+
final Request request =
119+
new Request.Builder()
120+
.url(finalUrl)
121+
// FIXME: there is a bug that makes MultipartStreamReader to never find the end of the
122+
// multipart message. This temporarily disables the multipart mode to work around it,
123+
// but
124+
// it means there is no progress bar displayed in the React Native overlay anymore.
125+
// .addHeader("Accept", "multipart/mixed")
126+
.build();
112127
mDownloadBundleFromURLCall = Assertions.assertNotNull(mClient.newCall(request));
113128
mDownloadBundleFromURLCall.enqueue(new Callback() {
114129
@Override
@@ -161,6 +176,7 @@ public void execute(Map<String, String> headers, Buffer body, boolean finished)
161176
if (!headers.containsKey("Content-Type") || !headers.get("Content-Type").equals("application/json")) {
162177
return;
163178
}
179+
164180
try {
165181
JSONObject progress = new JSONObject(body.readUtf8());
166182
String status = null;
@@ -202,14 +218,15 @@ public void cancelDownloadBundleFromURL() {
202218
}
203219
}
204220

205-
private static void processBundleResult(
221+
private void processBundleResult(
206222
String url,
207223
int statusCode,
208224
okhttp3.Headers headers,
209225
BufferedSource body,
210226
File outputFile,
211227
BundleInfo bundleInfo,
212-
DevBundleDownloadListener callback) throws IOException {
228+
DevBundleDownloadListener callback)
229+
throws IOException {
213230
// Check for server errors. If the server error has the expected form, fail with more info.
214231
if (statusCode != 200) {
215232
String bodyString = body.readUtf8();
@@ -232,21 +249,135 @@ private static void processBundleResult(
232249
}
233250

234251
File tmpFile = new File(outputFile.getPath() + ".tmp");
252+
253+
boolean bundleUpdated;
254+
255+
if (isDeltaUrl(url)) {
256+
// If the bundle URL has the delta extension, we need to use the delta patching logic.
257+
bundleUpdated = storeDeltaInFile(body, tmpFile);
258+
} else {
259+
resetDeltaCache();
260+
bundleUpdated = storePlainJSInFile(body, tmpFile);
261+
}
262+
263+
if (bundleUpdated) {
264+
// If we have received a new bundle from the server, move it to its final destination.
265+
if (!tmpFile.renameTo(outputFile)) {
266+
throw new IOException("Couldn't rename " + tmpFile + " to " + outputFile);
267+
}
268+
}
269+
270+
callback.onSuccess();
271+
}
272+
273+
private static boolean storePlainJSInFile(BufferedSource body, File outputFile)
274+
throws IOException {
235275
Sink output = null;
236276
try {
237-
output = Okio.sink(tmpFile);
277+
output = Okio.sink(outputFile);
238278
body.readAll(output);
239279
} finally {
240280
if (output != null) {
241281
output.close();
242282
}
243283
}
244284

245-
if (tmpFile.renameTo(outputFile)) {
246-
callback.onSuccess();
247-
} else {
248-
throw new IOException("Couldn't rename " + tmpFile + " to " + outputFile);
285+
return true;
286+
}
287+
288+
private boolean storeDeltaInFile(BufferedSource body, File outputFile) throws IOException {
289+
290+
JsonReader jsonReader = new JsonReader(new InputStreamReader(body.inputStream()));
291+
292+
jsonReader.beginObject();
293+
294+
int numChangedModules = 0;
295+
296+
while (jsonReader.hasNext()) {
297+
String name = jsonReader.nextName();
298+
if (name.equals("id")) {
299+
mDeltaId = jsonReader.nextString();
300+
} else if (name.equals("pre")) {
301+
numChangedModules += patchDelta(jsonReader, mPreModules);
302+
} else if (name.equals("post")) {
303+
numChangedModules += patchDelta(jsonReader, mPostModules);
304+
} else if (name.equals("delta")) {
305+
numChangedModules += patchDelta(jsonReader, mDeltaModules);
306+
} else {
307+
jsonReader.skipValue();
308+
}
309+
}
310+
311+
jsonReader.endObject();
312+
jsonReader.close();
313+
314+
if (numChangedModules == 0) {
315+
// If we receive an empty delta, we don't need to save the file again (it'll have the
316+
// same content).
317+
return false;
249318
}
319+
320+
FileOutputStream fileOutputStream = new FileOutputStream(outputFile);
321+
322+
try {
323+
for (byte[] code : mPreModules.values()) {
324+
fileOutputStream.write(code);
325+
fileOutputStream.write('\n');
326+
}
327+
328+
for (byte[] code : mDeltaModules.values()) {
329+
fileOutputStream.write(code);
330+
fileOutputStream.write('\n');
331+
}
332+
333+
for (byte[] code : mPostModules.values()) {
334+
fileOutputStream.write(code);
335+
fileOutputStream.write('\n');
336+
}
337+
} finally {
338+
fileOutputStream.flush();
339+
fileOutputStream.close();
340+
}
341+
342+
return true;
343+
}
344+
345+
private static int patchDelta(JsonReader jsonReader, LinkedHashMap<Number, byte[]> map)
346+
throws IOException {
347+
jsonReader.beginArray();
348+
349+
int numModules = 0;
350+
while (jsonReader.hasNext()) {
351+
jsonReader.beginArray();
352+
353+
int moduleId = jsonReader.nextInt();
354+
355+
if (jsonReader.peek() == JsonToken.NULL) {
356+
jsonReader.skipValue();
357+
map.remove(moduleId);
358+
} else {
359+
map.put(moduleId, jsonReader.nextString().getBytes());
360+
}
361+
362+
jsonReader.endArray();
363+
numModules++;
364+
}
365+
366+
jsonReader.endArray();
367+
368+
return numModules;
369+
}
370+
371+
private void resetDeltaCache() {
372+
mDeltaId = null;
373+
374+
mDeltaModules.clear();
375+
mPreModules.clear();
376+
mPostModules.clear();
377+
}
378+
379+
private static boolean isDeltaUrl(String bundleUrl) {
380+
return bundleUrl.indexOf(".delta?") != -1;
250381
}
251382

252383
private static void populateBundleInfo(String url, okhttp3.Headers headers, BundleInfo bundleInfo) {

ReactAndroid/src/main/java/com/facebook/react/devsupport/DevInternalSettings.java

+17-7
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,10 @@
99

1010
package com.facebook.react.devsupport;
1111

12-
import javax.annotation.Nullable;
13-
12+
import android.annotation.SuppressLint;
1413
import android.content.Context;
1514
import android.content.SharedPreferences;
1615
import android.preference.PreferenceManager;
17-
1816
import com.facebook.react.common.annotations.VisibleForTesting;
1917
import com.facebook.react.modules.debug.interfaces.DeveloperSettings;
2018
import com.facebook.react.packagerconnection.PackagerConnectionSettings;
@@ -32,6 +30,7 @@ public class DevInternalSettings implements
3230
private static final String PREFS_FPS_DEBUG_KEY = "fps_debug";
3331
private static final String PREFS_JS_DEV_MODE_DEBUG_KEY = "js_dev_mode_debug";
3432
private static final String PREFS_JS_MINIFY_DEBUG_KEY = "js_minify_debug";
33+
private static final String PREFS_JS_BUNDLE_DELTAS_KEY = "js_bundle_deltas";
3534
private static final String PREFS_ANIMATIONS_DEBUG_KEY = "animations_debug";
3635
private static final String PREFS_RELOAD_ON_JS_CHANGE_KEY = "reload_on_js_change";
3736
private static final String PREFS_INSPECTOR_DEBUG_KEY = "inspector_debug";
@@ -81,10 +80,11 @@ public boolean isJSMinifyEnabled() {
8180

8281
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
8382
if (mListener != null) {
84-
if (PREFS_FPS_DEBUG_KEY.equals(key) ||
85-
PREFS_RELOAD_ON_JS_CHANGE_KEY.equals(key) ||
86-
PREFS_JS_DEV_MODE_DEBUG_KEY.equals(key) ||
87-
PREFS_JS_MINIFY_DEBUG_KEY.equals(key)) {
83+
if (PREFS_FPS_DEBUG_KEY.equals(key)
84+
|| PREFS_RELOAD_ON_JS_CHANGE_KEY.equals(key)
85+
|| PREFS_JS_DEV_MODE_DEBUG_KEY.equals(key)
86+
|| PREFS_JS_BUNDLE_DELTAS_KEY.equals(key)
87+
|| PREFS_JS_MINIFY_DEBUG_KEY.equals(key)) {
8888
mListener.onInternalSettingsChanged();
8989
}
9090
}
@@ -114,6 +114,16 @@ public void setElementInspectorEnabled(boolean enabled) {
114114
mPreferences.edit().putBoolean(PREFS_INSPECTOR_DEBUG_KEY, enabled).apply();
115115
}
116116

117+
@SuppressLint("SharedPreferencesUse")
118+
public boolean isBundleDeltasEnabled() {
119+
return mPreferences.getBoolean(PREFS_JS_BUNDLE_DELTAS_KEY, false);
120+
}
121+
122+
@SuppressLint("SharedPreferencesUse")
123+
public void setBundleDeltasEnabled(boolean enabled) {
124+
mPreferences.edit().putBoolean(PREFS_JS_BUNDLE_DELTAS_KEY, enabled).apply();
125+
}
126+
117127
@Override
118128
public boolean isRemoteJSDebugEnabled() {
119129
return mPreferences.getBoolean(PREFS_REMOTE_JS_DEBUG_KEY, false);

0 commit comments

Comments
 (0)