Bug 1382899 - Reduce Promise overhead in the DownloadLegacy.js progress events. r=florian draft
authorPaolo Amadini <paolo.mozmail@amadzone.org>
Tue, 08 Aug 2017 14:44:52 +0100
changeset 642569 c0b82eab2d97829a7a11cc51fa002c1eae49a4d0
parent 642518 a921bfb8a2cf3db4d9edebe9b35799a3f9d035da
child 725039 d0fc9cf9736fce2a35c790c0d5a454926a1dc180
push id72802
push userpaolo.mozmail@amadzone.org
push dateTue, 08 Aug 2017 13:45:49 +0000
reviewersflorian
bugs1382899
milestone57.0a1
Bug 1382899 - Reduce Promise overhead in the DownloadLegacy.js progress events. r=florian MozReview-Commit-ID: KS4kujUvX3s
toolkit/components/jsdownloads/src/DownloadLegacy.js
--- a/toolkit/components/jsdownloads/src/DownloadLegacy.js
+++ b/toolkit/components/jsdownloads/src/DownloadLegacy.js
@@ -15,18 +15,16 @@
 "use strict";
 
 const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
                                   "resource://gre/modules/Downloads.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
-                                  "resource://gre/modules/PromiseUtils.jsm");
 
 /**
  * nsITransfer implementation that provides a bridge to a Download object.
  *
  * Legacy downloads work differently than the JavaScript implementation.  In the
  * latter, the caller only provides the properties for the Download object and
  * the entire process is handled by the "start" method.  In the legacy
  * implementation, the caller must create a separate object to execute the
@@ -47,17 +45,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
  * cancellation requests, but after cancellation it cannot be reused again.
  *
  * Since the components that execute the download may be different and they
  * don't always give consistent results, this bridge takes care of enforcing the
  * expectations, for example by ensuring the target file exists when the
  * download is successful, even if the source has a size of zero bytes.
  */
 function DownloadLegacyTransfer() {
-  this._deferDownload = PromiseUtils.defer();
+  this._promiseDownload = new Promise(r => this._resolveDownload = r);
 }
 
 DownloadLegacyTransfer.prototype = {
   classID: Components.ID("{1b4c85df-cbdd-4bb6-b04e-613caece083c}"),
 
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
                                          Ci.nsIWebProgressListener2,
@@ -88,17 +86,17 @@ DownloadLegacyTransfer.prototype = {
       }
 
       if (blockedByParentalControls) {
         aRequest.cancel(Cr.NS_BINDING_ABORTED);
       }
 
       // The main request has just started.  Wait for the associated Download
       // object to be available before notifying.
-      this._deferDownload.promise.then(download => {
+      this._promiseDownload.then(download => {
         // If the request was blocked, now that we have the download object we
         // should set a flag that can be retrieved later when handling the
         // cancellation so that the proper error can be thrown.
         if (blockedByParentalControls) {
           download._blockedByParentalControls = true;
         }
 
         download.saver.onTransferStarted(
@@ -121,17 +119,17 @@ DownloadLegacyTransfer.prototype = {
             }
           }
         });
       }).catch(Cu.reportError);
     } else if ((aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) &&
         (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK)) {
       // The last file has been received, or the download failed.  Wait for the
       // associated Download object to be available before notifying.
-      this._deferDownload.promise.then(download => {
+      this._promiseDownload.then(download => {
         // At this point, the hash has been set and we need to copy it to the
         // DownloadSaver.
         if (Components.isSuccessCode(aStatus)) {
           download.saver.setSha256Hash(this._sha256Hash);
           download.saver.setSignatureInfo(this._signatureInfo);
           download.saver.setRedirects(this._redirects);
         }
         download.saver.onTransferFinished(aStatus);
@@ -160,35 +158,65 @@ DownloadLegacyTransfer.prototype = {
                                               aMessage) {
     // The status change may optionally be received in addition to the state
     // change, but if no network request actually started, it is possible that
     // we only receive a status change with an error status code.
     if (!Components.isSuccessCode(aStatus)) {
       this._componentFailed = true;
 
       // Wait for the associated Download object to be available.
-      this._deferDownload.promise.then(function DLT_OSC_onDownload(aDownload) {
-        aDownload.saver.onTransferFinished(aStatus);
+      this._promiseDownload.then(download => {
+        download.saver.onTransferFinished(aStatus);
       }).catch(Cu.reportError);
     }
   },
 
   onSecurityChange() { },
 
   // nsIWebProgressListener2
   onProgressChange64: function DLT_onProgressChange64(aWebProgress, aRequest,
                                                       aCurSelfProgress,
                                                       aMaxSelfProgress,
                                                       aCurTotalProgress,
                                                       aMaxTotalProgress) {
-    // Wait for the associated Download object to be available.
-    this._deferDownload.promise.then(function DLT_OPC64_onDownload(aDownload) {
-      aDownload.saver.onProgressBytes(aCurTotalProgress, aMaxTotalProgress);
+    // Since this progress function is invoked frequently, we use a slightly
+    // more complex solution that optimizes the case where we already have an
+    // associated Download object, avoiding the Promise overhead.
+    if (this._download) {
+      this._hasDelayedProgress = false;
+      this._download.saver.onProgressBytes(aCurTotalProgress,
+                                           aMaxTotalProgress);
+      return;
+    }
+
+    // If we don't have a Download object yet, store the most recent progress
+    // notification to send later. We must do this because there is no guarantee
+    // that a future notification will be sent if the download stalls.
+    this._delayedCurTotalProgress = aCurTotalProgress;
+    this._delayedMaxTotalProgress = aMaxTotalProgress;
+
+    // Do not enqueue multiple callbacks for the progress report.
+    if (this._hasDelayedProgress) {
+      return;
+    }
+    this._hasDelayedProgress = true;
+
+    this._promiseDownload.then(download => {
+      // Check whether an immediate progress report has been already processed
+      // before we could send the delayed progress report.
+      if (!this._hasDelayedProgress) {
+        return;
+      }
+      download.saver.onProgressBytes(this._delayedCurTotalProgress,
+                                     this._delayedMaxTotalProgress);
     }).catch(Cu.reportError);
   },
+  _hasDelayedProgress: false,
+  _delayedCurTotalProgress: 0,
+  _delayedMaxTotalProgress: 0,
 
   // nsIWebProgressListener2
   onRefreshAttempted: function DLT_onRefreshAttempted(aWebProgress, aRefreshURI,
                                                       aMillis, aSameURI) {
     // Indicate that refreshes and redirects are allowed by default.  However,
     // note that download components don't usually call this method at all.
     return true;
   },
@@ -228,17 +256,18 @@ DownloadLegacyTransfer.prototype = {
       if (aTempFile) {
         aDownload.tryToKeepPartialData = true;
       }
 
       // Start the download before allowing it to be controlled.  Ignore errors.
       aDownload.start().catch(() => {});
 
       // Start processing all the other events received through nsITransfer.
-      this._deferDownload.resolve(aDownload);
+      this._download = aDownload;
+      this._resolveDownload(aDownload);
 
       // Add the download to the list, allowing it to be seen and canceled.
       return Downloads.getList(Downloads.ALL).then(list => list.add(aDownload));
     }).catch(Cu.reportError);
   },
 
   setSha256Hash(hash) {
     this._sha256Hash = hash;
@@ -248,20 +277,30 @@ DownloadLegacyTransfer.prototype = {
     this._signatureInfo = signatureInfo;
   },
 
   setRedirects(redirects) {
     this._redirects = redirects;
   },
 
   /**
-   * This deferred object contains a promise that is resolved with the Download
-   * object associated with this nsITransfer instance, when it is available.
+   * Download object associated with this nsITransfer instance. This is not
+   * available immediately when the nsITransfer instance is created.
    */
-  _deferDownload: null,
+  _download: null,
+
+  /**
+   * Promise that resolves to the Download object associated with this
+   * nsITransfer instance after the _resolveDownload method is invoked.
+   *
+   * Waiting on this promise using "then" ensures that the callbacks are invoked
+   * in the correct order even if enqueued before the object is available.
+   */
+  _promiseDownload: null,
+  _resolveDownload: null,
 
   /**
    * Reference to the component that is executing the download.  This component
    * allows cancellation through its nsICancelable interface.
    */
   _cancelable: null,
 
   /**