Bug 1437988 - [1.5] Add progress tracking and expose progress via a delegate callback. r=jchen,snorp,droeh
authorEugen Sawin <esawin@mozilla.com>
Thu, 26 Jul 2018 16:50:26 +0200
changeset 428615 d05c4d2a0ef86f316eaf027356fa962217f6f103
parent 428614 d89d8b34e5bd037928b22a6dafbf7137d812a936
child 428616 e25320d4cca710fdad18ce3812a319cc904326f3
push id34337
push userncsoregi@mozilla.com
push dateThu, 26 Jul 2018 21:58:45 +0000
treeherdermozilla-central@8f2f847b2f9d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjchen, snorp, droeh
bugs1437988
milestone63.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1437988 - [1.5] Add progress tracking and expose progress via a delegate callback. r=jchen,snorp,droeh
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
@@ -669,16 +669,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
@@ -111,16 +111,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
@@ -429,16 +429,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;