Bug 1018383 - Added request-level caching in NetworkGeolocationProvider.js. r=jdm
authorGarvan Keeley <gkeeley@mozilla.com>
Mon, 07 Jul 2014 07:16:00 +0200
changeset 208918 5a30769e14ceba942bb00d51b907f83f698f4c5a
parent 208917 8e992de2080ce08f303cb8bad65b88b8728db8d6
child 208919 2ace7164be11ada5ed827a0f11ade581c19e0f2d
push id494
push userraliiev@mozilla.com
push dateMon, 25 Aug 2014 18:42:16 +0000
treeherdermozilla-release@a3cc3e46b571 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjdm
bugs1018383
milestone32.0a2
Bug 1018383 - Added request-level caching in NetworkGeolocationProvider.js. r=jdm This is a cleanup of the patch used in the Tarako bug. I made it clearer that this is request-level caching, NOT position caching. This is specifically for preventing unnecessary requests to the server. It is unrelated to the W3C PositionOptions and their effect on position caching. Hopefully this detail is clearer in this patch. Updated mochitests that were relying on old logic to succeed The failing mochitests were relying on the provider to have a specific behaviour -behaviours that are incidental to the test case. I updated these to reflect the behaviour of the current system.
dom/system/NetworkGeolocationProvider.js
dom/tests/mochitest/geolocation/geolocation_common.js
dom/tests/mochitest/geolocation/test_cachedPosition.html
dom/tests/mochitest/geolocation/test_timerRestartWatch.html
--- a/dom/system/NetworkGeolocationProvider.js
+++ b/dom/system/NetworkGeolocationProvider.js
@@ -15,17 +15,17 @@ const SETTINGS_WIFI_ENABLED = "wifi.enab
 
 let gLoggingEnabled = false;
 
 /*
    The gLocationRequestTimeout controls how long we wait on receiving an update
    from the Wifi subsystem.  If this timer fires, we believe the Wifi scan has
    had a problem and we no longer can use Wifi to position the user this time
    around (we will continue to be hopeful that Wifi will recover).
- 
+
    This timeout value is also used when Wifi scanning is disabled (see
    gWifiScanningEnabled).  In this case, we use this timer to collect cell/ip
    data and xhr it to the location server.
 */
 
 let gLocationRequestTimeout = 5000;
 
 let gWifiScanningEnabled = true;
@@ -34,16 +34,182 @@ let gCellScanningEnabled = false;
 function LOG(aMsg) {
   if (gLoggingEnabled) {
     aMsg = "*** WIFI GEO: " + aMsg + "\n";
     Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService).logStringMessage(aMsg);
     dump(aMsg);
   }
 }
 
+function CachedRequest(loc, cellInfo, wifiList) {
+  this.location = loc;
+
+  let wifis = new Set();
+  if (wifiList) {
+    for (let i = 0; i < wifiList.length; i++) {
+      wifis.add(wifiList[i].macAddress);
+    }
+  }
+
+  // Use only these values for equality
+  // (the JSON will contain additional values in future)
+  function makeCellKey(cell) {
+    return "" + cell.radio + ":" + cell.mobileCountryCode + ":" +
+    cell.mobileNetworkCode + ":" + cell.locationAreaCode + ":" +
+    cell.cellId;
+  }
+
+  let cells = new Set();
+  if (cellInfo) {
+    for (let i = 0; i < cellInfo.length; i++) {
+      cells.add(makeCellKey(cellInfo[i]));
+    }
+  }
+
+  this.hasCells = () => cells.size > 0;
+
+  this.hasWifis = () => wifis.size > 0;
+
+  // if fields match
+  this.isCellEqual = function(cellInfo) {
+    if (!this.hasCells()) {
+      return false;
+    }
+
+    let len1 = cells.size;
+    let len2 = cellInfo.length;
+
+    if (len1 != len2) {
+      LOG("cells not equal len");
+      return false;
+    }
+
+    for (let i = 0; i < len2; i++) {
+      if (!cells.has(makeCellKey(cellInfo[i]))) {
+        return false;
+      }
+    }
+    return true;
+  };
+
+  // if 50% of the SSIDS match
+  this.isWifiApproxEqual = function(wifiList) {
+    if (!this.hasWifis()) {
+      return false;
+    }
+
+    // if either list is a 50% subset of the other, they are equal
+    let common = 0;
+    for (let i = 0; i < wifiList.length; i++) {
+      if (wifis.has(wifiList[i].macAddress)) {
+        common++;
+      }
+    }
+    let kPercentMatch = 0.5;
+    return common >= (Math.max(wifis.size, wifiList.length) * kPercentMatch);
+  };
+
+  this.isGeoip = function() {
+    return !this.hasCells() && !this.hasWifis();
+  };
+
+  this.isCellAndWifi = function() {
+    return this.hasCells() && this.hasWifis();
+  };
+
+  this.isCellOnly = function() {
+    return this.hasCells() && !this.hasWifis();
+  };
+
+  this.isWifiOnly = function() {
+    return this.hasWifis() && !this.hasCells();
+  };
+ }
+
+let gCachedRequest = null;
+let gDebugCacheReasoning = ""; // for logging the caching logic
+
+// This function serves two purposes:
+// 1) do we have a cached request
+// 2) is the cached request better than what newCell and newWifiList will obtain
+// If the cached request exists, and we know it to have greater accuracy
+// by the nature of its origin (wifi/cell/geoip), use its cached location.
+//
+// If there is more source info than the cached request had, return false
+// In other cases, MLS is known to produce better/worse accuracy based on the
+// inputs, so base the decision on that.
+function isCachedRequestMoreAccurateThanServerRequest(newCell, newWifiList)
+{
+  gDebugCacheReasoning = "";
+  let isNetworkRequestCacheEnabled = true;
+  try {
+    // Mochitest needs this pref to simulate request failure
+    isNetworkRequestCacheEnabled = Services.prefs.getBoolPref("geo.wifi.debug.requestCache.enabled");
+    if (!isNetworkRequestCacheEnabled) {
+      gCachedRequest = null;
+    }
+  } catch (e) {}
+
+  if (!gCachedRequest || !isNetworkRequestCacheEnabled) {
+    gDebugCacheReasoning = "No cached data";
+    return false;
+  }
+
+  if (!newCell && !newWifiList) {
+    gDebugCacheReasoning = "New req. is GeoIP.";
+    return true;
+  }
+
+  if (newCell && newWifiList && (gCachedRequest.isCellOnly() || gCachedRequest.isWifiOnly())) {
+    gDebugCacheReasoning = "New req. is cell+wifi, cache only cell or wifi.";
+    return false;
+  }
+
+  if (newCell && gCachedRequest.isWifiOnly()) {
+    // In order to know if a cell-only request should trump a wifi-only request
+    // need to know if wifi is low accuracy. >5km would be VERY low accuracy,
+    // it is worth trying the cell
+    var isHighAccuracyWifi = gCachedRequest.location.coords.accuracy < 5000;
+    gDebugCacheReasoning = "Req. is cell, cache is wifi, isHigh:" + isHighAccuracyWifi;
+    return isHighAccuracyWifi;
+  }
+
+  let hasEqualCells = false;
+  if (newCell) {
+    hasEqualCells = gCachedRequest.isCellEqual(newCell);
+  }
+
+  let hasEqualWifis = false;
+  if (newWifiList) {
+    hasEqualWifis = gCachedRequest.isWifiApproxEqual(newWifiList);
+  }
+
+  gDebugCacheReasoning = "EqualCells:" + hasEqualCells + " EqualWifis:" + hasEqualWifis;
+
+  if (gCachedRequest.isCellOnly()) {
+    gDebugCacheReasoning += ", Cell only.";
+    if (hasEqualCells) {
+      return true;
+    }
+  } else if (gCachedRequest.isWifiOnly() && hasEqualWifis) {
+    gDebugCacheReasoning +=", Wifi only."
+    return true;
+  } else if (gCachedRequest.isCellAndWifi()) {
+     gDebugCacheReasoning += ", Cache has Cell+Wifi.";
+    if ((hasEqualCells && hasEqualWifis) ||
+        (!newWifiList && hasEqualCells) ||
+        (!newCell && hasEqualWifis))
+    {
+     return true;
+    }
+  }
+
+  return false;
+}
+
 function WifiGeoCoordsObject(lat, lon, acc, alt, altacc) {
   this.latitude = lat;
   this.longitude = lon;
   this.accuracy = acc;
   this.altitude = alt;
   this.altitudeAccuracy = altacc;
 }
 
@@ -135,17 +301,17 @@ WifiGeoPositionProvider.prototype = {
             self.wifiService.stopWatching(self);
           }
           if (gWifiScanningEnabled) {
             self.wifiService = Cc["@mozilla.org/wifi/monitor;1"].getService(Ci.nsIWifiMonitor);
             self.wifiService.startWatching(self);
           }
         }
       },
-      
+
       handleError: function(message) {
         gLoggingEnabled = false;
         LOG("settings callback threw an exception, dropping");
       }
     };
 
     try {
       Services.obs.addObserver(this, SETTINGS_CHANGED_TOPIC, false);
@@ -173,16 +339,20 @@ WifiGeoPositionProvider.prototype = {
   },
 
   shutdown: function() {
     LOG("shutdown called");
     if (this.started == false) {
       return;
     }
 
+    // Without clearing this, we could end up using the cache almost indefinitely
+    // TODO: add logic for cache lifespan, for now just be safe and clear it
+    gCachedRequest = null;
+
     if (this.timer) {
       this.timer.cancel();
       this.timer = null;
     }
 
     if(this.wifiService) {
       this.wifiService.stopWatching(this);
       this.wifiService = null;
@@ -227,17 +397,17 @@ WifiGeoPositionProvider.prototype = {
   },
 
   onError: function (code) {
     LOG("wifi error: " + code);
     this.sendLocationRequest(null);
   },
 
   getMobileInfo: function() {
-    LOG("updateMobileInfo called");
+    LOG("getMobileInfo called");
     try {
       let radioService = Cc["@mozilla.org/ril;1"]
                     .getService(Ci.nsIRadioInterfaceLayer);
       let numInterfaces = radioService.numRadioInterfaces;
       let result = [];
       for (let i = 0; i < numInterfaces; i++) {
         LOG("Looking for SIM in slot:" + i + " of " + numInterfaces);
         let radio = radioService.getRadioInterface(i);
@@ -264,16 +434,40 @@ WifiGeoPositionProvider.prototype = {
     }
   },
 
   notify: function (timer) {
     this.sendLocationRequest(null);
   },
 
   sendLocationRequest: function (wifiData) {
+    let data = {};
+    if (wifiData) {
+      data.wifiAccessPoints = wifiData;
+    }
+
+    if (gCellScanningEnabled) {
+      let cellData = this.getMobileInfo();
+      if (cellData && cellData.length > 0) {
+        data.cellTowers = cellData;
+      }
+    }
+
+    let useCached = isCachedRequestMoreAccurateThanServerRequest(data.cellTowers,
+                                                                 data.wifiAccessPoints);
+
+    LOG("Use request cache:" + useCached + " reason:" + gDebugCacheReasoning);
+
+    if (useCached) {
+      gCachedRequest.location.timestamp = Date.now();
+      this.listener.update(gCachedRequest.location);
+      return;
+    }
+
+    // From here on, do a network geolocation request //
     let url = Services.urlFormatter.formatURLPref("geo.wifi.uri");
     let listener = this.listener;
     LOG("Sending request: " + url + "\n");
 
     let xhr = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
                         .createInstance(Ci.nsIXMLHttpRequest);
 
     listener.locationUpdatePending();
@@ -299,29 +493,18 @@ WifiGeoPositionProvider.prototype = {
         return;
       }
 
       let newLocation = new WifiGeoPositionObject(xhr.response.location.lat,
                                                   xhr.response.location.lng,
                                                   xhr.response.accuracy);
 
       listener.update(newLocation);
+      gCachedRequest = new CachedRequest(newLocation, data.cellTowers, data.wifiAccessPoints);
     };
 
-    let data = {};
-    if (wifiData) {
-      data.wifiAccessPoints = wifiData;
-    }
-
-    if (gCellScanningEnabled) {
-      let cellData = this.getMobileInfo();
-      if (cellData) {
-        data.cellTowers = cellData;
-      }
-    }
-
-    data = JSON.stringify(data);
-    LOG("sending " + data);
-    xhr.send(data);
+    var requestData = JSON.stringify(data);
+    LOG("sending " + requestData);
+    xhr.send(requestData);
   },
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([WifiGeoPositionProvider]);
--- a/dom/tests/mochitest/geolocation/geolocation_common.js
+++ b/dom/tests/mochitest/geolocation/geolocation_common.js
@@ -1,64 +1,71 @@
+var BASE_URL = "http://mochi.test:8888/tests/dom/tests/mochitest/geolocation/network_geolocation.sjs";
 
 function sleep(delay)
 {
     var start = Date.now();
     while (Date.now() < start + delay);
 }
 
 function force_prompt(allow, callback) {
   SpecialPowers.pushPrefEnv({"set": [["geo.prompt.testing", true], ["geo.prompt.testing.allow", allow]]}, callback);
 }
 
 function start_sending_garbage(callback)
 {
-  SpecialPowers.pushPrefEnv({"set": [["geo.wifi.uri", "http://mochi.test:8888/tests/dom/tests/mochitest/geolocation/network_geolocation.sjs?action=respond-garbage"]]}, function() {
+  SpecialPowers.pushPrefEnv({"set": [["geo.wifi.uri", BASE_URL + "?action=respond-garbage"]]}, function() {
     // we need to be sure that all location data has been purged/set.
     sleep(1000);
     callback.call();
   });
 }
 
 function stop_sending_garbage(callback)
 {
-  SpecialPowers.pushPrefEnv({"set": [["geo.wifi.uri", "http://mochi.test:8888/tests/dom/tests/mochitest/geolocation/network_geolocation.sjs"]]}, function() {
+  SpecialPowers.pushPrefEnv({"set": [["geo.wifi.uri", BASE_URL + ""]]}, function() {
     // we need to be sure that all location data has been purged/set.
     sleep(1000);
     callback.call();
   });
 }
 
 function stop_geolocationProvider(callback)
 {
-  SpecialPowers.pushPrefEnv({"set": [["geo.wifi.uri", "http://mochi.test:8888/tests/dom/tests/mochitest/geolocation/network_geolocation.sjs?action=stop-responding"]]}, function() {
+  SpecialPowers.pushPrefEnv({"set": [["geo.wifi.uri", BASE_URL + "?action=stop-responding"]]}, function() {
     // we need to be sure that all location data has been purged/set.
     sleep(1000);
     callback.call();
   });
 }
 
+function set_network_request_cache_enabled(enabled, callback)
+{
+  SpecialPowers.pushPrefEnv({"set": [["geo.wifi.debug.requestCache.enabled", enabled]]}, callback);
+}
+
 function worse_geolocationProvider(callback)
 {
-  SpecialPowers.pushPrefEnv({"set": [["geo.wifi.uri", "http://mochi.test:8888/tests/dom/tests/mochitest/geolocation/network_geolocation.sjs?action=worse-accuracy"]]}, callback);
+  SpecialPowers.pushPrefEnv({"set": [["geo.wifi.uri", BASE_URL + "?action=worse-accuracy"]]}, callback);
 }
 
 function resume_geolocationProvider(callback)
 {
-  SpecialPowers.pushPrefEnv({"set": [["geo.wifi.uri", "http://mochi.test:8888/tests/dom/tests/mochitest/geolocation/network_geolocation.sjs"]]}, callback);
+  SpecialPowers.pushPrefEnv({"set": [["geo.wifi.uri", BASE_URL + ""]]}, callback);
 }
 
 function delay_geolocationProvider(delay, callback)
 {
-  SpecialPowers.pushPrefEnv({"set": [["geo.wifi.uri", "http://mochi.test:8888/tests/dom/tests/mochitest/geolocation/network_geolocation.sjs?delay=" + delay]]}, callback);
+  SpecialPowers.pushPrefEnv({"set": [["geo.wifi.uri", BASE_URL + "?delay=" + delay]]}, callback);
 }
 
 function send404_geolocationProvider(callback)
 {
-  SpecialPowers.pushPrefEnv({"set": [["geo.wifi.uri", "http://mochi.test:8888/tests/dom/tests/mochitest/geolocation/network_geolocation.sjs?action=send404"]]}, callback);
+  set_network_request_cache_enabled(false, function() {
+    SpecialPowers.pushPrefEnv({"set": [["geo.wifi.uri", BASE_URL + "?action=send404"]]}, callback);});
 }
 
 function check_geolocation(location) {
 
   ok(location, "Check to see if this location is non-null");
 
   ok("timestamp" in location, "Check to see if there is a timestamp");
 
@@ -71,18 +78,18 @@ function check_geolocation(location) {
   ok("longitude" in coords, "Check to see if there is a longitude");
   ok("accuracy" in coords, "Check to see if there is a accuracy");
   
   // optional ok("altitude" in coords, "Check to see if there is a altitude");
   // optional ok("altitudeAccuracy" in coords, "Check to see if there is a alt accuracy");
   // optional ok("heading" in coords, "Check to see if there is a heading");
   // optional ok("speed" in coords, "Check to see if there is a speed");
 
-  ok (location.coords.latitude  == 37.41857, "lat matches known value");
-  ok (location.coords.longitude == -122.08769, "lon matches known value");
+  ok (Math.abs(location.coords.latitude - 37.41857) < 0.001, "lat matches known value");
+  ok (Math.abs(location.coords.longitude + 122.08769) < 0.001, "lon matches known value");
   // optional  ok(location.coords.altitude == 42, "alt matches known value");
   // optional  ok(location.coords.altitudeAccuracy == 42, "alt acc matches known value");
 }
 
 function toggleGeolocationSetting(value, callback) {
   var mozSettings = window.navigator.mozSettings;
   var lock = mozSettings.createLock();
 
--- a/dom/tests/mochitest/geolocation/test_cachedPosition.html
+++ b/dom/tests/mochitest/geolocation/test_cachedPosition.html
@@ -17,22 +17,28 @@ https://bugzilla.mozilla.org/show_bug.cg
 
 </div>
 <pre id="test">
 <script class="testbody" type="text/javascript">
 
 SimpleTest.waitForExplicitFinish();
 
 resume_geolocationProvider(function() {
-  force_prompt(true, test1);
+    force_prompt(true, function () {
+      set_network_request_cache_enabled(false, function() {
+        test1();
+      });
+    });
 });
 
 function done() {
-  resume_geolocationProvider(function() {
-    SimpleTest.finish();
+  set_network_request_cache_enabled(true, function() {
+    resume_geolocationProvider(function() {
+      SimpleTest.finish();
+    });
   });
 }
 
 function errorCallback(err) {
   ok(false, "error callback should not have been called");
   done();
 }
 
--- a/dom/tests/mochitest/geolocation/test_timerRestartWatch.html
+++ b/dom/tests/mochitest/geolocation/test_timerRestartWatch.html
@@ -32,32 +32,38 @@ var times = 0;
 function errorCallback(err) {
   ok(err.code == err.TIMEOUT, "ensure error is a timeout.");
   times++;
 
   // make sure we got at least 3 times errorCallback
   if (times >= 3) {
     navigator.geolocation.clearWatch(watchID);
     resume_geolocationProvider(function() {
-      SimpleTest.finish();
+      set_network_request_cache_enabled(true,
+        function() { SimpleTest.finish(); } );
     });
   }
 }
 
 function successCallback(position) {
+  ok(1, "on success");
   // Now that we got a success callback, lets try to ensure
   // that we get a timeout error.
-  stop_geolocationProvider(function() {});
+  // The network cache is already off, now stop the sjs from reponding to requests
+  stop_geolocationProvider(function(){});
 }
 
 var options = {
     maximumAge: 0,
     timeout: 1000
 };
 
 function test1() {
-  watchID = navigator.geolocation.watchPosition(successCallback, errorCallback, options);
+ set_network_request_cache_enabled(false,
+      function() {
+        watchID = navigator.geolocation.watchPosition(successCallback, errorCallback, options);
+      });
 }
 </script>
 </pre>
 </body>
 </html>