Bug 1437988 - [1.5] Add progress tracking and expose progress via a delegate callback. r=jchen,snorp,droeh a=ritu
authorEugen Sawin <esawin@mozilla.com>
Thu, 26 Jul 2018 16:50:26 +0200
changeset 478203 9dcf2844400a88a44cc9c39a7e88e9af867ba264
parent 478202 7f9a3b5d43164f001b31d504e7642425fa788740
child 478204 94572bfa7c3b448672ce944d1292406c193e50d9
push id9571
push usernerli@mozilla.com
push dateThu, 02 Aug 2018 13:49:02 +0000
treeherdermozilla-beta@60840467a49e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjchen, snorp, droeh, ritu
bugs1437988
milestone62.0
Bug 1437988 - [1.5] Add progress tracking and expose progress via a delegate callback. r=jchen,snorp,droeh a=ritu
mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
mobile/android/base/java/org/mozilla/gecko/webapps/WebAppActivity.java
mobile/android/chrome/geckoview/GeckoViewProgressContent.js
mobile/android/chrome/geckoview/geckoview.js
mobile/android/chrome/geckoview/jar.mn
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/GeckoSessionTestRuleTest.kt
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/Callbacks.kt
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
--- a/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
+++ b/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
@@ -659,16 +659,20 @@ public class CustomTabsActivity extends 
     @Override
     public void onPageStop(GeckoSession session, boolean success) {
         mCanStop = false;
         updateCanStop();
         updateProgress(100);
     }
 
     @Override
+    public void onProgressChange(GeckoSession session, int progress) {
+    }
+
+    @Override
     public void onSecurityChange(GeckoSession session, SecurityInformation securityInfo) {
         mSecurityInformation = securityInfo;
         updateActionBar();
     }
 
     /* GeckoSession.ContentDelegate */
     @Override
     public void onTitleChange(GeckoSession session, String title) {
--- a/mobile/android/base/java/org/mozilla/gecko/webapps/WebAppActivity.java
+++ b/mobile/android/base/java/org/mozilla/gecko/webapps/WebAppActivity.java
@@ -110,16 +110,21 @@ public class WebAppActivity extends AppC
             }
 
             @Override
             public void onPageStop(GeckoSession session, boolean success) {
 
             }
 
             @Override
+            public void onProgressChange(GeckoSession session, int progress) {
+
+            }
+
+            @Override
             public void onSecurityChange(GeckoSession session, SecurityInformation security) {
                 // We want to ignore the extraneous first about:blank load
                 if (mIsFirstLoad && security.origin.startsWith("moz-nullprincipal:")) {
                     mIsFirstLoad = false;
                     return;
                 }
                 mIsFirstLoad = false;
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/chrome/geckoview/GeckoViewProgressContent.js
@@ -0,0 +1,331 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+ChromeUtils.import("resource://gre/modules/GeckoViewContentModule.jsm");
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  Services: "resource://gre/modules/Services.jsm",
+});
+
+class GeckoViewProgressContent extends GeckoViewContentModule {
+  onInit() {
+    debug `onInit`;
+  }
+
+  onEnable() {
+    debug `onEnable`;
+
+    ProgressTracker.onEnable(this);
+
+    let flags = Ci.nsIWebProgress.NOTIFY_PROGRESS |
+                Ci.nsIWebProgress.NOTIFY_STATE_NETWORK |
+                Ci.nsIWebProgress.NOTIFY_LOCATION;
+    this.progressFilter =
+      Cc["@mozilla.org/appshell/component/browser-status-filter;1"]
+      .createInstance(Ci.nsIWebProgress);
+    this.progressFilter.addProgressListener(this, flags);
+    let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                      .getInterface(Ci.nsIWebProgress);
+    webProgress.addProgressListener(this.progressFilter, flags);
+  }
+
+  onDisable() {
+    debug `onDisable`;
+
+    if (this.progressFilter) {
+      this.progressFilter.removeProgressListener(this);
+      let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                        .getInterface(Ci.nsIWebProgress);
+      webProgress.removeProgressListener(this.progressFilter);
+    }
+  }
+
+  onProgressChange(aWebProgress, aRequest, aCurSelf, aMaxSelf, aCurTotal, aMaxTotal) {
+    debug `onProgressChange ${aCurSelf}/${aMaxSelf} ${aCurTotal}/${aMaxTotal}`;
+
+    ProgressTracker.handleProgress(null, aCurTotal, aMaxTotal);
+    ProgressTracker.updateProgress();
+  }
+
+  onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+    debug `onStateChange: isTopLevel=${aWebProgress.isTopLevel},
+                          flags=${aStateFlags}, status=${aStatus}`;
+
+    if (!aWebProgress || !aWebProgress.isTopLevel) {
+      return;
+    }
+
+    const uri = aRequest.QueryInterface(Ci.nsIChannel).URI.displaySpec;
+    debug `onStateChange: uri=${uri}`;
+
+    if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) {
+      ProgressTracker.start(uri);
+    } else if ((aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) &&
+               !aWebProgress.isLoadingDocument) {
+      ProgressTracker.stop();
+    } else if (aStateFlags & Ci.nsIWebProgressListener.STATE_REDIRECTING) {
+      ProgressTracker.start(uri);
+    }
+  }
+
+  onLocationChange(aWebProgress, aRequest, aLocationURI, aFlags) {
+    debug `onLocationChange: location=${aLocationURI.displaySpec},
+                             flags=${aFlags}`;
+
+    if (!aWebProgress || !aWebProgress.isTopLevel) {
+      return;
+    }
+
+    if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
+      ProgressTracker.stop();
+    } else {
+      ProgressTracker.changeLocation(aLocationURI.displaySpec);
+    }
+  }
+}
+
+const ProgressTracker = {
+  onEnable: function(aModule) {
+    this._module = aModule;
+    this.clear();
+  },
+
+  start: function(aUri) {
+    debug `ProgressTracker start ${aUri}`;
+
+    if (this._tracking) {
+      this.stop();
+    }
+
+    addEventListener("MozAfterPaint", this,
+                     { capture: false, mozSystemGroup: true });
+    addEventListener("DOMContentLoaded", this,
+                     { capture: false, mozSystemGroup: true });
+    addEventListener("pageshow", this,
+                     { capture: false, mozSystemGroup: true });
+
+    this._tracking = true;
+    this.clear();
+    let data = this._data;
+
+    if (aUri === "about:blank") {
+      data.uri = null;
+      return;
+    }
+
+    data.uri = aUri;
+    data.pageStart = true;
+    this.updateProgress();
+  },
+
+  changeLocation: function(aUri) {
+    debug `ProgressTracker changeLocation ${aUri}`;
+
+    let data = this._data;
+    data.locationChange = true;
+    data.uri = aUri;
+  },
+
+  stop: function() {
+    debug `ProgressTracker stop`;
+
+    let data = this._data;
+    data.pageStop = true;
+    this.updateProgress();
+    this._tracking = false;
+
+    if (!data.parsed) {
+      removeEventListener("DOMContentLoaded", this,
+                          { capture: false, mozSystemGroup: true });
+    }
+    if (!data.firstPaint) {
+      removeEventListener("MozAfterPaint", this,
+                          { capture: false, mozSystemGroup: true });
+    }
+    if (!data.pageShow) {
+      removeEventListener("pageshow", this,
+                          { capture: false, mozSystemGroup: true });
+    }
+  },
+
+  get messageManager() {
+    return this._module.messageManager;
+  },
+
+  get eventDispatcher() {
+    return this._module.eventDispatcher;
+  },
+
+  handleEvent: function(aEvent) {
+    let data = this._data;
+
+    const target = aEvent.originalTarget;
+    const uri = target && target.location.href;
+
+    if (!data.uri || data.uri !== uri) {
+      return;
+    }
+
+    debug `ProgressTracker handleEvent: ${aEvent.type}`;
+
+    let needsUpdate = false;
+
+    switch (aEvent.type) {
+      case "DOMContentLoaded":
+        needsUpdate = needsUpdate || !data.parsed;
+        data.parsed = true;
+        removeEventListener("DOMContentLoaded", this,
+                            { capture: false, mozSystemGroup: true });
+        break;
+      case "MozAfterPaint":
+        needsUpdate = needsUpdate || !data.firstPaint;
+        data.firstPaint = true;
+        removeEventListener("MozAfterPaint", this,
+                            { capture: false, mozSystemGroup: true });
+        break;
+      case "pageshow":
+        needsUpdate = needsUpdate || !data.pageShow;
+        data.pageShow = true;
+        removeEventListener("pageshow", this,
+                            { capture: false, mozSystemGroup: true });
+        break;
+    }
+
+    if (needsUpdate) {
+      this.updateProgress();
+    }
+  },
+
+  clear: function() {
+    this._data = {
+      prev: 0,
+      uri: null,
+      locationChange: false,
+      pageStart: false,
+      pageStop: false,
+      firstPaint: false,
+      pageShow: false,
+      parsed: false,
+      totalReceived: 1,
+      totalExpected: 1,
+      channels: {},
+    };
+  },
+
+  _debugData: function() {
+    return {
+      prev: this._data.prev,
+      uri: this._data.uri,
+      locationChange: this._data.locationChange,
+      pageStart: this._data.pageStart,
+      pageStop: this._data.pageStop,
+      firstPaint: this._data.firstPaint,
+      pageShow: this._data.pageShow,
+      parsed: this._data.parsed,
+      totalReceived: this._data.totalReceived,
+      totalExpected: this._data.totalExpected,
+    };
+  },
+
+  handleProgress: function(aChannelUri, aProgress, aMax) {
+    debug `ProgressTracker handleProgress ${aChannelUri} ${aProgress}/${aMax}`;
+
+    let data = this._data;
+
+    if (!data.uri) {
+      return;
+    }
+
+    aChannelUri = aChannelUri || data.uri;
+
+    const now = content.performance.now();
+
+    if (!data.channels[aChannelUri]) {
+      data.channels[aChannelUri] = {
+        received: aProgress,
+        max: aMax,
+        expected: (aMax > 0 ? aMax : aProgress * 2),
+        lastUpdate: now,
+      };
+    } else {
+      let channelProgress = data.channels[aChannelUri];
+      channelProgress.received = Math.max(channelProgress.received, aProgress);
+      channelProgress.expected = Math.max(channelProgress.expected, aMax);
+      channelProgress.lastUpdate = now;
+    }
+  },
+
+  updateProgress: function() {
+    debug `ProgressTracker updateProgress`;
+
+    let data = this._data;
+
+    if (!this._tracking || !data.uri) {
+      return;
+    }
+
+    let progress = 0;
+
+    if (data.pageStart) {
+      progress += 10;
+    }
+    if (data.firstPaint) {
+      progress += 15;
+    }
+    if (data.parsed) {
+      progress += 15;
+    }
+    if (data.pageShow) {
+      progress += 15;
+    }
+    if (data.locationChange) {
+      progress += 10;
+    }
+
+    data.totalReceived = 1;
+    data.totalExpected = 1;
+    const channelOverdue = content.performance.now() - 300;
+
+    for (let channel in data.channels) {
+      if (data.channels[channel].max < 1 &&
+          channelOverdue > data.channels[channel].lastUpdate) {
+        data.channels[channel].expected = data.channels[channel].received;
+      }
+      data.totalReceived += data.channels[channel].received;
+      data.totalExpected += data.channels[channel].expected;
+    }
+
+    const minExpected = 1024 * 1;
+    const maxExpected = 1024 * 1024 * 0.5;
+
+    if (data.pageStop ||
+        (data.pageStart && data.firstPaint && data.parsed && data.pageShow &&
+         data.totalReceived > minExpected &&
+         data.totalReceived >= data.totalExpected)) {
+      progress = 100;
+    } else {
+      const a = Math.min(1, (data.totalExpected / maxExpected)) * 30;
+      progress += data.totalReceived / data.totalExpected * a;
+    }
+
+    debug `ProgressTracker onProgressChangeUpdate ${this._debugData()} ${data.totalReceived}/${data.totalExpected} progress=${progress}`;
+
+    if (data.prev >= progress) {
+      return;
+    }
+
+    this.eventDispatcher.sendRequest({
+      type: "GeckoView:ProgressChanged",
+      progress,
+    });
+
+    data.prev = progress;
+  },
+};
+
+
+let {debug, warn} = GeckoViewProgressContent.initLogging("GeckoViewProgress");
+let module = GeckoViewProgressContent.create(this);
--- a/mobile/android/chrome/geckoview/geckoview.js
+++ b/mobile/android/chrome/geckoview/geckoview.js
@@ -329,16 +329,17 @@ function startup() {
     },
     onEnable: {
       frameScript: "chrome://geckoview/content/GeckoViewNavigationContent.js",
     },
   }, {
     name: "GeckoViewProgress",
     onEnable: {
       resource: "resource://gre/modules/GeckoViewProgress.jsm",
+      frameScript: "chrome://geckoview/content/GeckoViewProgressContent.js",
     },
   }, {
     name: "GeckoViewScroll",
     onEnable: {
       frameScript: "chrome://geckoview/content/GeckoViewScrollContent.js",
     },
   }, {
     name: "GeckoViewSelectionAction",
--- a/mobile/android/chrome/geckoview/jar.mn
+++ b/mobile/android/chrome/geckoview/jar.mn
@@ -6,11 +6,12 @@ geckoview.jar:
 % content geckoview %content/
 
   content/ErrorPageEventHandler.js
   content/geckoview.xul
   content/geckoview.js
   content/GeckoViewContent.js
   content/GeckoViewContentSettings.js
   content/GeckoViewNavigationContent.js
+  content/GeckoViewProgressContent.js
   content/GeckoViewPromptContent.js
   content/GeckoViewScrollContent.js
   content/GeckoViewSelectionActionContent.js
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/GeckoSessionTestRuleTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/GeckoSessionTestRuleTest.kt
@@ -169,16 +169,20 @@ class GeckoSessionTestRuleTest : BaseSes
             override fun onPageStop(session: GeckoSession, success: Boolean) {
                 counter++
             }
 
             override fun onSecurityChange(session: GeckoSession,
                                           securityInfo: GeckoSession.ProgressDelegate.SecurityInformation) {
                 counter++
             }
+
+            override fun onProgressChange(session: GeckoSession, progress: Int) {
+                counter++
+            }
         })
 
         assertThat("Callback count should be correct", counter, equalTo(1))
     }
 
     @Test fun waitUntilCalled_specificInterfaceMethod() {
         sessionRule.session.loadTestPath(HELLO_HTML_PATH)
         sessionRule.waitUntilCalled(GeckoSession.ProgressDelegate::class,
@@ -236,16 +240,20 @@ class GeckoSessionTestRuleTest : BaseSes
             override fun onPageStop(session: GeckoSession, success: Boolean) {
                 counter++
             }
 
             override fun onSecurityChange(session: GeckoSession,
                                           securityInfo: GeckoSession.ProgressDelegate.SecurityInformation) {
                 counter++
             }
+
+            override fun onProgressChange(session: GeckoSession, progress: Int) {
+                counter++
+            }
         })
 
         assertThat("Callback count should be correct", counter, equalTo(1))
     }
 
     @Test fun waitUntilCalled_specificObjectMethod() {
         sessionRule.session.loadTestPath(HELLO_HTML_PATH)
 
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/Callbacks.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/Callbacks.kt
@@ -79,16 +79,19 @@ class Callbacks private constructor() {
 
     interface ProgressDelegate : GeckoSession.ProgressDelegate {
         override fun onPageStart(session: GeckoSession, url: String) {
         }
 
         override fun onPageStop(session: GeckoSession, success: Boolean) {
         }
 
+        override fun onProgressChange(session: GeckoSession, progress: Int) {
+        }
+
         override fun onSecurityChange(session: GeckoSession, securityInfo: GeckoSession.ProgressDelegate.SecurityInformation) {
         }
     }
 
     interface PromptDelegate : GeckoSession.PromptDelegate {
         override fun onAlert(session: GeckoSession, title: String, msg: String, callback: GeckoSession.PromptDelegate.AlertCallback) {
             callback.dismiss()
         }
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
@@ -266,30 +266,34 @@ public class GeckoSession extends LayerS
         };
 
     private final GeckoSessionHandler<ProgressDelegate> mProgressHandler =
         new GeckoSessionHandler<ProgressDelegate>(
             "GeckoViewProgress", this,
             new String[]{
                 "GeckoView:PageStart",
                 "GeckoView:PageStop",
+                "GeckoView:ProgressChanged",
                 "GeckoView:SecurityChanged"
             }
         ) {
             @Override
             public void handleMessage(final ProgressDelegate delegate,
                                       final String event,
                                       final GeckoBundle message,
                                       final EventCallback callback) {
                 if ("GeckoView:PageStart".equals(event)) {
                     delegate.onPageStart(GeckoSession.this,
                                          message.getString("uri"));
                 } else if ("GeckoView:PageStop".equals(event)) {
                     delegate.onPageStop(GeckoSession.this,
                                         message.getBoolean("success"));
+                } else if ("GeckoView:ProgressChanged".equals(event)) {
+                    delegate.onProgressChange(GeckoSession.this,
+                                              message.getInt("progress"));
                 } else if ("GeckoView:SecurityChanged".equals(event)) {
                     final GeckoBundle identity = message.getBundle("identity");
                     delegate.onSecurityChange(GeckoSession.this, new ProgressDelegate.SecurityInformation(identity));
                 }
             }
         };
 
     private final GeckoSessionHandler<ScrollDelegate> mScrollHandler =
@@ -1882,16 +1886,23 @@ public class GeckoSession extends LayerS
         /**
         * A View has finished loading content from the network.
         * @param session GeckoSession that initiated the callback.
         * @param success Whether the page loaded successfully or an error occurred.
         */
         void onPageStop(GeckoSession session, boolean success);
 
         /**
+         * Page loading has progressed.
+         * @param session GeckoSession that initiated the callback.
+         * @param progress Current page load progress value [0, 100].
+         */
+        void onProgressChange(GeckoSession session, int progress);
+
+        /**
         * The security status has been updated.
         * @param session GeckoSession that initiated the callback.
         * @param securityInfo The new security information.
         */
         void onSecurityChange(GeckoSession session, SecurityInformation securityInfo);
     }
 
     private static int getContentElementType(final String name) {
--- a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
@@ -427,16 +427,21 @@ public class GeckoViewActivity extends A
         public void onPageStop(GeckoSession session, boolean success) {
             Log.i(LOGTAG, "Stopping page load " + (success ? "successfully" : "unsuccessfully"));
             Log.i(LOGTAG, "zerdatime " + SystemClock.elapsedRealtime() +
                   " - page load stop");
             mTp.logCounters();
         }
 
         @Override
+        public void onProgressChange(GeckoSession session, int progress) {
+            Log.i(LOGTAG, "onProgressChange " + progress);
+        }
+
+        @Override
         public void onSecurityChange(GeckoSession session, SecurityInformation securityInfo) {
             Log.i(LOGTAG, "Security status changed to " + securityInfo.securityMode);
         }
     }
 
     private class ExamplePermissionDelegate implements GeckoSession.PermissionDelegate {
 
         public int androidPermissionRequestCode = 1;