Skip to content
This repository was archived by the owner on May 20, 2025. It is now read-only.

Commit 533192b

Browse files
committed
Merge pull request #316 from Microsoft/download-progress-perf
Improve download progress perf
2 parents 57908f4 + 27e9f34 commit 533192b

File tree

8 files changed

+182
-98
lines changed

8 files changed

+182
-98
lines changed

CodePush.js

Lines changed: 43 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ const PackageMixins = require("./package-mixins")(NativeCodePush);
99
async function checkForUpdate(deploymentKey = null) {
1010
/*
1111
* Before we ask the server if an update exists, we
12-
* need to retrieve three pieces of information from the
12+
* need to retrieve three pieces of information from the
1313
* native side: deployment key, app version (e.g. 1.0.1)
1414
* and the hash of the currently running update (if there is one).
1515
* This allows the client to only receive updates which are targetted
1616
* for their specific deployment and version and which are actually
1717
* different from the CodePush update they have already installed.
1818
*/
1919
const nativeConfig = await getConfiguration();
20-
20+
2121
/*
2222
* If a deployment key was explicitly provided,
2323
* then let's override the one we retrieved
@@ -30,7 +30,7 @@ async function checkForUpdate(deploymentKey = null) {
3030

3131
// Use dynamically overridden getCurrentPackage() during tests.
3232
const localPackage = await module.exports.getCurrentPackage();
33-
33+
3434
/*
3535
* If the app has a previously installed update, and that update
3636
* was targetted at the same app version that is currently running,
@@ -48,9 +48,9 @@ async function checkForUpdate(deploymentKey = null) {
4848
queryPackage.packageHash = config.packageHash;
4949
}
5050
}
51-
51+
5252
const update = await sdk.queryUpdateWithCurrentPackage(queryPackage);
53-
53+
5454
/*
5555
* There are four cases where checkForUpdate will resolve to null:
5656
* ----------------------------------------------------------------
@@ -69,13 +69,13 @@ async function checkForUpdate(deploymentKey = null) {
6969
* because we want to avoid having to install diff updates against the binary's
7070
* version, which we can't do yet on Android.
7171
*/
72-
if (!update || update.updateAppVersion ||
73-
localPackage && (update.packageHash === localPackage.packageHash) ||
72+
if (!update || update.updateAppVersion ||
73+
localPackage && (update.packageHash === localPackage.packageHash) ||
7474
(!localPackage || localPackage._isDebugOnly) && config.packageHash === update.packageHash) {
7575
if (update && update.updateAppVersion) {
7676
log("An update is available but it is targeting a newer binary version than you are currently running.");
7777
}
78-
78+
7979
return null;
8080
} else {
8181
const remotePackage = { ...update, ...PackageMixins.remote(sdk.reportStatusDownload) };
@@ -93,7 +93,7 @@ const getConfiguration = (() => {
9393
} else if (testConfig) {
9494
return testConfig;
9595
} else {
96-
config = await NativeCodePush.getConfiguration();
96+
config = await NativeCodePush.getConfiguration();
9797
return config;
9898
}
9999
}
@@ -123,7 +123,7 @@ function getPromisifiedSdk(requestFetchAdapter, config) {
123123
} else {
124124
resolve(update);
125125
}
126-
});
126+
});
127127
});
128128
};
129129

@@ -135,7 +135,7 @@ function getPromisifiedSdk(requestFetchAdapter, config) {
135135
} else {
136136
resolve();
137137
}
138-
});
138+
});
139139
});
140140
};
141141

@@ -147,7 +147,7 @@ function getPromisifiedSdk(requestFetchAdapter, config) {
147147
} else {
148148
resolve();
149149
}
150-
});
150+
});
151151
});
152152
};
153153

@@ -159,21 +159,21 @@ function log(message) {
159159
console.log(`[CodePush] ${message}`)
160160
}
161161

162-
// This ensures that notifyApplicationReadyInternal is only called once
162+
// This ensures that notifyApplicationReadyInternal is only called once
163163
// in the lifetime of this module instance.
164164
const notifyApplicationReady = (() => {
165165
let notifyApplicationReadyPromise;
166166
return () => {
167167
if (!notifyApplicationReadyPromise) {
168168
notifyApplicationReadyPromise = notifyApplicationReadyInternal();
169169
}
170-
170+
171171
return notifyApplicationReadyPromise;
172172
};
173173
})();
174174

175175
async function notifyApplicationReadyInternal() {
176-
await NativeCodePush.notifyApplicationReady();
176+
await NativeCodePush.notifyApplicationReady();
177177
const statusReport = await NativeCodePush.getNewStatusReport();
178178
if (statusReport) {
179179
const config = await getConfiguration();
@@ -208,15 +208,15 @@ function setUpTestDependencies(testSdk, providedTestConfig, testNativeBridge) {
208208
const sync = (() => {
209209
let syncInProgress = false;
210210
const setSyncCompleted = () => { syncInProgress = false; };
211-
211+
212212
return (options = {}, syncStatusChangeCallback, downloadProgressCallback) => {
213213
if (syncInProgress) {
214214
typeof syncStatusChangeCallback === "function"
215215
? syncStatusChangeCallback(CodePush.SyncStatus.SYNC_IN_PROGRESS)
216216
: log("Sync already in progress.");
217217
return Promise.resolve(CodePush.SyncStatus.SYNC_IN_PROGRESS);
218-
}
219-
218+
}
219+
220220
syncInProgress = true;
221221
const syncPromise = syncInternal(options, syncStatusChangeCallback, downloadProgressCallback);
222222
syncPromise
@@ -230,7 +230,7 @@ const sync = (() => {
230230
/*
231231
* The syncInternal method provides a simple, one-line experience for
232232
* incorporating the check, download and installation of an update.
233-
*
233+
*
234234
* It simply composes the existing API methods together and adds additional
235235
* support for respecting mandatory updates, ignoring previously failed
236236
* releases, and displaying a standard confirmation UI to the end-user
@@ -245,9 +245,9 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
245245
mandatoryInstallMode: CodePush.InstallMode.IMMEDIATE,
246246
minimumBackgroundDuration: 0,
247247
updateDialog: null,
248-
...options
248+
...options
249249
};
250-
250+
251251
syncStatusChangeCallback = typeof syncStatusChangeCallback === "function"
252252
? syncStatusChangeCallback
253253
: (syncStatus) => {
@@ -271,7 +271,7 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
271271
log("User cancelled the update.");
272272
break;
273273
case CodePush.SyncStatus.UPDATE_INSTALLED:
274-
/*
274+
/*
275275
* If the install mode is IMMEDIATE, this will not get returned as the
276276
* app will be restarted to a new Javascript context.
277277
*/
@@ -290,40 +290,34 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
290290
break;
291291
}
292292
};
293-
294-
downloadProgressCallback = typeof downloadProgressCallback === "function"
295-
? downloadProgressCallback
296-
: (downloadProgress) => {
297-
log(`Expecting ${downloadProgress.totalBytes} bytes, received ${downloadProgress.receivedBytes} bytes.`);
298-
};
299-
293+
300294
try {
301295
await CodePush.notifyApplicationReady();
302-
296+
303297
syncStatusChangeCallback(CodePush.SyncStatus.CHECKING_FOR_UPDATE);
304298
const remotePackage = await checkForUpdate(syncOptions.deploymentKey);
305-
299+
306300
const doDownloadAndInstall = async () => {
307301
syncStatusChangeCallback(CodePush.SyncStatus.DOWNLOADING_PACKAGE);
308302
const localPackage = await remotePackage.download(downloadProgressCallback);
309-
303+
310304
// Determine the correct install mode based on whether the update is mandatory or not.
311305
resolvedInstallMode = localPackage.isMandatory ? syncOptions.mandatoryInstallMode : syncOptions.installMode;
312-
306+
313307
syncStatusChangeCallback(CodePush.SyncStatus.INSTALLING_UPDATE);
314308
await localPackage.install(resolvedInstallMode, syncOptions.minimumBackgroundDuration, () => {
315309
syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_INSTALLED);
316310
});
317-
311+
318312
return CodePush.SyncStatus.UPDATE_INSTALLED;
319313
};
320-
314+
321315
const updateShouldBeIgnored = remotePackage && (remotePackage.failedInstall && syncOptions.ignoreFailedUpdates);
322316
if (!remotePackage || updateShouldBeIgnored) {
323317
if (updateShouldBeIgnored) {
324318
log("An update is available, but it is being ignored due to having been previously rolled back.");
325319
}
326-
320+
327321
syncStatusChangeCallback(CodePush.SyncStatus.UP_TO_DATE);
328322
return CodePush.SyncStatus.UP_TO_DATE;
329323
} else if (syncOptions.updateDialog) {
@@ -334,24 +328,24 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
334328
} else {
335329
syncOptions.updateDialog = { ...CodePush.DEFAULT_UPDATE_DIALOG, ...syncOptions.updateDialog };
336330
}
337-
338-
return await new Promise((resolve, reject) => {
331+
332+
return await new Promise((resolve, reject) => {
339333
let message = null;
340334
const dialogButtons = [{
341335
text: null,
342-
onPress: async () => {
336+
onPress: async () => {
343337
resolve(await doDownloadAndInstall());
344338
}
345339
}];
346-
340+
347341
if (remotePackage.isMandatory) {
348342
message = syncOptions.updateDialog.mandatoryUpdateMessage;
349343
dialogButtons[0].text = syncOptions.updateDialog.mandatoryContinueButtonLabel;
350344
} else {
351345
message = syncOptions.updateDialog.optionalUpdateMessage;
352-
dialogButtons[0].text = syncOptions.updateDialog.optionalInstallButtonLabel;
346+
dialogButtons[0].text = syncOptions.updateDialog.optionalInstallButtonLabel;
353347
// Since this is an optional update, add another button
354-
// to allow the end-user to ignore it
348+
// to allow the end-user to ignore it
355349
dialogButtons.push({
356350
text: syncOptions.updateDialog.optionalIgnoreButtonLabel,
357351
onPress: () => {
@@ -360,13 +354,13 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
360354
}
361355
});
362356
}
363-
357+
364358
// If the update has a description, and the developer
365359
// explicitly chose to display it, then set that as the message
366360
if (syncOptions.updateDialog.appendReleaseDescription && remotePackage.description) {
367-
message += `${syncOptions.updateDialog.descriptionPrefix} ${remotePackage.description}`;
361+
message += `${syncOptions.updateDialog.descriptionPrefix} ${remotePackage.description}`;
368362
}
369-
363+
370364
syncStatusChangeCallback(CodePush.SyncStatus.AWAITING_USER_ACTION);
371365
Alert.alert(syncOptions.updateDialog.title, message, dialogButtons);
372366
});
@@ -375,16 +369,16 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
375369
}
376370
} catch (error) {
377371
syncStatusChangeCallback(CodePush.SyncStatus.UNKNOWN_ERROR);
378-
log(error.message);
372+
log(error.message);
379373
throw error;
380-
}
374+
}
381375
};
382376

383377
let CodePush;
384378

385-
// If the "NativeCodePush" variable isn't defined, then
379+
// If the "NativeCodePush" variable isn't defined, then
386380
// the app didn't properly install the native module,
387-
// and therefore, it doesn't make sense initializing
381+
// and therefore, it doesn't make sense initializing
388382
// the JS interface when it wouldn't work anyways.
389383
if (NativeCodePush) {
390384
CodePush = {

android/app/src/main/java/com/microsoft/codepush/react/CodePush.java

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import com.facebook.react.bridge.WritableMap;
1414
import com.facebook.react.bridge.WritableNativeMap;
1515
import com.facebook.react.modules.core.DeviceEventManagerModule;
16+
import com.facebook.react.uimanager.ReactChoreographer;
1617
import com.facebook.react.uimanager.ViewManager;
1718
import com.facebook.soloader.SoLoader;
1819

@@ -25,6 +26,7 @@
2526
import android.content.pm.PackageManager;
2627
import android.os.AsyncTask;
2728
import android.provider.Settings;
29+
import android.view.Choreographer;
2830

2931
import org.json.JSONArray;
3032
import org.json.JSONException;
@@ -236,7 +238,7 @@ private void initializeUpdateAfterRestart() {
236238
// Reset the state which indicates that
237239
// the app was just freshly updated.
238240
didUpdate = false;
239-
241+
240242
JSONObject pendingUpdate = getPendingUpdate();
241243
if (pendingUpdate != null) {
242244
try {
@@ -251,7 +253,7 @@ private void initializeUpdateAfterRestart() {
251253
// There is in fact a new update running for the first
252254
// time, so update the local state to ensure the client knows.
253255
didUpdate = true;
254-
256+
255257
// Mark that we tried to initialize the new update, so that if it crashes,
256258
// we will know that we need to rollback when the app next starts.
257259
savePendingUpdate(pendingUpdate.getString(PENDING_UPDATE_HASH_KEY),
@@ -439,19 +441,56 @@ public void run() {
439441
}
440442

441443
@ReactMethod
442-
public void downloadUpdate(final ReadableMap updatePackage, final Promise promise) {
444+
public void downloadUpdate(final ReadableMap updatePackage, final boolean notifyProgress, final Promise promise) {
443445
AsyncTask<Void, Void, Void> asyncTask = new AsyncTask<Void, Void, Void>() {
444446
@Override
445447
protected Void doInBackground(Void... params) {
446448
try {
447449
WritableMap mutableUpdatePackage = CodePushUtils.convertReadableMapToWritableMap(updatePackage);
448450
mutableUpdatePackage.putString(BINARY_MODIFIED_TIME_KEY, "" + getBinaryResourcesModifiedTime());
449451
codePushPackage.downloadPackage(mutableUpdatePackage, CodePush.this.assetsBundleFileName, new DownloadProgressCallback() {
452+
private boolean hasScheduledNextFrame = false;
453+
private DownloadProgress latestDownloadProgress = null;
454+
450455
@Override
451456
public void call(DownloadProgress downloadProgress) {
457+
if (!notifyProgress) {
458+
return;
459+
}
460+
461+
latestDownloadProgress = downloadProgress;
462+
// If the download is completed, synchronously send the last event.
463+
if (latestDownloadProgress.isCompleted()) {
464+
dispatchDownloadProgressEvent();
465+
return;
466+
}
467+
468+
if (hasScheduledNextFrame) {
469+
return;
470+
}
471+
472+
hasScheduledNextFrame = true;
473+
getReactApplicationContext().runOnUiQueueThread(new Runnable() {
474+
@Override
475+
public void run() {
476+
ReactChoreographer.getInstance().postFrameCallback(ReactChoreographer.CallbackType.TIMERS_EVENTS, new Choreographer.FrameCallback() {
477+
@Override
478+
public void doFrame(long frameTimeNanos) {
479+
if (!latestDownloadProgress.isCompleted()) {
480+
dispatchDownloadProgressEvent();
481+
}
482+
483+
hasScheduledNextFrame = false;
484+
}
485+
});
486+
}
487+
});
488+
}
489+
490+
public void dispatchDownloadProgressEvent() {
452491
getReactApplicationContext()
453492
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
454-
.emit(DOWNLOAD_PROGRESS_EVENT_NAME, downloadProgress.createWritableMap());
493+
.emit(DOWNLOAD_PROGRESS_EVENT_NAME, latestDownloadProgress.createWritableMap());
455494
}
456495
});
457496

android/app/src/main/java/com/microsoft/codepush/react/DownloadProgress.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,8 @@ public WritableMap createWritableMap() {
2323
}
2424
return map;
2525
}
26+
27+
public boolean isCompleted() {
28+
return this.totalBytes == this.receivedBytes;
29+
}
2630
}

0 commit comments

Comments
 (0)