Bug 973543 - Part 1: add reference counting to added routes. r=echen
authorJessica Jong <jjong@mozilla.com>
Wed, 11 Feb 2015 16:01:08 +0800
changeset 255614 a75fca8e0b9df54bf671b6c9c096fb39fe33eb68
parent 255613 e70f1860736dd89f0484c14581b2c86b90db8ee5
child 255615 fe2b1047b5c803ae2f5513f275a755b70c121774
push id4610
push userjlund@mozilla.com
push dateMon, 30 Mar 2015 18:32:55 +0000
treeherdermozilla-beta@4df54044d9ef [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersechen
bugs973543
milestone38.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 973543 - Part 1: add reference counting to added routes. r=echen
dom/system/gonk/NetworkManager.js
dom/system/gonk/NetworkService.js
dom/system/gonk/NetworkUtils.cpp
--- a/dom/system/gonk/NetworkManager.js
+++ b/dom/system/gonk/NetworkManager.js
@@ -129,22 +129,61 @@ try {
 
 function defineLazyRegExp(obj, name, pattern) {
   obj.__defineGetter__(name, function() {
     delete obj[name];
     return obj[name] = new RegExp(pattern);
   });
 }
 
+function NetworkInterfaceLinks()
+{
+  this.resetLinks();
+}
+NetworkInterfaceLinks.prototype = {
+  linkRoutes: null,
+  gateways: null,
+  interfaceName: null,
+  extraRoutes: null,
+
+  setLinks: function(linkRoutes, gateways, interfaceName) {
+    this.linkRoutes = linkRoutes;
+    this.gateways = gateways;
+    this.interfaceName = interfaceName;
+  },
+
+  resetLinks: function() {
+    this.linkRoutes = [];
+    this.gateways = [];
+    this.interfaceName = "";
+    this.extraRoutes = [];
+  },
+
+  compareGateways: function(gateways) {
+    if (this.gateways.length != gateways.length) {
+      return false;
+    }
+
+    for (let i = 0; i < this.gateways.length; i++) {
+      if (this.gateways[i] != gateways[i]) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+};
+
 /**
  * This component watches for network interfaces changing state and then
  * adjusts routes etc. accordingly.
  */
 function NetworkManager() {
   this.networkInterfaces = {};
+  this.networkInterfaceLinks = {};
   Services.obs.addObserver(this, TOPIC_XPCOM_SHUTDOWN, false);
   Services.obs.addObserver(this, TOPIC_MOZSETTINGS_CHANGED, false);
 
   try {
     this._manageOfflineStatus =
       Services.prefs.getBoolPref(PREF_MANAGE_OFFLINE_STATUS);
   } catch(ex) {
     // Ignore.
@@ -298,16 +337,17 @@ NetworkManager.prototype = {
                                  Cr.NS_ERROR_INVALID_ARG);
     }
     let networkId = this.getNetworkId(network);
     if (networkId in this.networkInterfaces) {
       throw Components.Exception("Network with that type already registered!",
                                  Cr.NS_ERROR_INVALID_ARG);
     }
     this.networkInterfaces[networkId] = network;
+    this.networkInterfaceLinks[networkId] = new NetworkInterfaceLinks();
 
     if (network.type == Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE_DUN) {
       debug("Force setting " + SETTINGS_DUN_REQUIRED + " to true.");
       this.tetheringSettings[SETTINGS_DUN_REQUIRED] = true;
     }
 
     Services.obs.notifyObservers(network, TOPIC_INTERFACE_REGISTERED, null);
     debug("Network '" + networkId + "' registered.");
@@ -315,17 +355,20 @@ NetworkManager.prototype = {
 
   _addSubnetRoutes: function(network) {
     let ips = {};
     let prefixLengths = {};
     let length = network.getAddresses(ips, prefixLengths);
     for (let i = 0; i < length; i++) {
       debug('Adding subnet routes: ' + ips.value[i] + '/' + prefixLengths.value[i]);
       gNetworkService.modifyRoute(Ci.nsINetworkService.MODIFY_ROUTE_ADD,
-                                  network.name, ips.value[i], prefixLengths.value[i]);
+                                  network.name, ips.value[i], prefixLengths.value[i])
+        .catch((aError) => {
+          debug("_addSubnetRoutes error: " + aError);
+        });
     }
   },
 
   updateNetworkInterface: function(network) {
     if (!(network instanceof Ci.nsINetworkInterface)) {
       throw Components.Exception("Argument must be nsINetworkInterface.",
                                  Cr.NS_ERROR_INVALID_ARG);
     }
@@ -341,18 +384,26 @@ NetworkManager.prototype = {
     // something through netd, so we add createNetwork/destroyNetwork
     // to deal with that explicitly.
 
     switch (network.state) {
       case Ci.nsINetworkInterface.NETWORK_STATE_CONNECTED:
         gNetworkService.createNetwork(network.name, () => {
           // Add host route for data calls
           if (this.isNetworkTypeMobile(network.type)) {
-            gNetworkService.removeHostRoutes(network.name);
-            this.setHostRoutes(network);
+            let currentInterfaceLinks = this.networkInterfaceLinks[networkId];
+            let newLinkRoutes = network.getDnses().concat(network.httpProxyHost);
+            // If gateways have changed, remove all old routes first.
+            this._handleGateways(networkId, network.getGateways())
+              .then(() => this._updateRoutes(currentInterfaceLinks.linkRoutes,
+                                             newLinkRoutes,
+                                             network.getGateways(), network.name))
+              .then(() => currentInterfaceLinks.setLinks(newLinkRoutes,
+                                                         network.getGateways(),
+                                                         network.name));
           }
 
           // Remove pre-created default route and let setAndConfigureActive()
           // to set default route only on preferred network
           gNetworkService.removeDefaultRoute(network);
 
           // Dun type is a special case where we add the default route to a
           // secondary table.
@@ -381,17 +432,17 @@ NetworkManager.prototype = {
           Services.obs.notifyObservers(network, TOPIC_CONNECTION_STATE_CHANGED,
                                        this.convertConnectionType(network));
         });
 
         break;
       case Ci.nsINetworkInterface.NETWORK_STATE_DISCONNECTED:
         // Remove host route for data calls
         if (this.isNetworkTypeMobile(network.type)) {
-          this.removeHostRoutes(network);
+          this._cleanupAllHostRoutes(networkId);
         }
         // Remove secondary default route for dun.
         if (network.type == Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE_DUN) {
           this.removeSecondaryDefaultRoute(network);
         }
         // Remove routing table in /proc/net/route
         if (network.type == Ci.nsINetworkInterface.NETWORK_TYPE_WIFI) {
           gNetworkService.resetRoutingTable(network);
@@ -430,31 +481,40 @@ NetworkManager.prototype = {
       throw Components.Exception("Argument must be nsINetworkInterface.",
                                  Cr.NS_ERROR_INVALID_ARG);
     }
     let networkId = this.getNetworkId(network);
     if (!(networkId in this.networkInterfaces)) {
       throw Components.Exception("No network with that type registered.",
                                  Cr.NS_ERROR_INVALID_ARG);
     }
+
+    // This is for in case a network gets unregistered without being
+    // DISCONNECTED.
+    if (this.isNetworkTypeMobile(network.type)) {
+      this._cleanupAllHostRoutes(networkId);
+    }
+
     delete this.networkInterfaces[networkId];
 
     if (network.type == Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE_DUN) {
       this.tetheringSettings[SETTINGS_DUN_REQUIRED] =
         libcutils.property_get("ro.tethering.dun_required") === "1";
     }
 
     Services.obs.notifyObservers(network, TOPIC_INTERFACE_UNREGISTERED, null);
     debug("Network '" + networkId + "' unregistered.");
   },
 
   _manageOfflineStatus: true,
 
   networkInterfaces: null,
 
+  networkInterfaceLinks: null,
+
   _dataDefaultServiceId: null,
 
   _preferredNetworkType: DEFAULT_PREFERRED_NETWORK_TYPE,
   get preferredNetworkType() {
     return this._preferredNetworkType;
   },
   set preferredNetworkType(val) {
     if ([Ci.nsINetworkInterface.NETWORK_TYPE_WIFI,
@@ -472,17 +532,34 @@ NetworkManager.prototype = {
          Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE].indexOf(val) == -1) {
       throw "Invalid network type";
     }
 
     this._overriddenActive = network;
     this.setAndConfigureActive();
   },
 
-  _updateRoutes: function(doAdd, ipAddresses, networkName, gateways) {
+  _updateRoutes: function(oldLinks, newLinks, gateways, interfaceName) {
+    // Returns items that are in base but not in target.
+    function getDifference(base, target) {
+      return base.filter(function(i) { return target.indexOf(i) < 0; });
+    }
+
+    let addedLinks = getDifference(newLinks, oldLinks);
+    let removedLinks = getDifference(oldLinks, newLinks);
+
+    if (addedLinks.length === 0 && removedLinks.length === 0) {
+      return Promise.resolve();
+    }
+
+    return this._setHostRoutes(false, removedLinks, interfaceName, gateways)
+      .then(this._setHostRoutes(true, addedLinks, interfaceName, gateways));
+  },
+
+  _setHostRoutes: function(doAdd, ipAddresses, networkName, gateways) {
     let getMaxPrefixLength = (aIp) => {
       return aIp.match(this.REGEXP_IPV4) ? IPV4_MAX_PREFIX_LENGTH : IPV6_MAX_PREFIX_LENGTH;
     }
 
     let promises = [];
 
     ipAddresses.forEach((aIpAddress) => {
       let gateway = this.selectGateway(gateways, aIpAddress);
@@ -512,56 +589,115 @@ NetworkManager.prototype = {
   },
 
   addHostRoute: function(network, host) {
     if (!this.isValidatedNetwork(network)) {
       return Promise.reject("Invalid network interface.");
     }
 
     return this.resolveHostname(network, host)
-      .then((ipAddresses) => this._updateRoutes(true,
-                                                ipAddresses,
-                                                network.name,
-                                                network.getGateways()));
+      .then((ipAddresses) => {
+        let promises = [];
+        let networkId = this.getNetworkId(network);
+
+        ipAddresses.forEach((aIpAddress) => {
+          let promise =
+            this._setHostRoutes(true, [aIpAddress], network.name, network.getGateways())
+              .then(() => this.networkInterfaceLinks[networkId].extraRoutes.push(aIpAddress));
+
+          promises.push(promise);
+        });
+
+        return Promise.all(promises);
+      });
   },
 
   removeHostRoute: function(network, host) {
     if (!this.isValidatedNetwork(network)) {
       return Promise.reject("Invalid network interface.");
     }
 
     return this.resolveHostname(network, host)
-      .then((ipAddresses) => this._updateRoutes(false,
-                                                ipAddresses,
-                                                network.name,
-                                                network.getGateways()));
+      .then((ipAddresses) => {
+        let promises = [];
+        let networkId = this.getNetworkId(network);
+
+        ipAddresses.forEach((aIpAddress) => {
+          let found = this.networkInterfaceLinks[networkId].extraRoutes.indexOf(aIpAddress);
+          if (found < 0) {
+            return; // continue
+          }
+
+          let promise =
+            this._setHostRoutes(false, [aIpAddress], network.name, network.getGateways())
+              .then(() => {
+                this.networkInterfaceLinks[networkId].extraRoutes.splice(found, 1);
+              }, () => {
+                // We should remove it even if the operation failed.
+                this.networkInterfaceLinks[networkId].extraRoutes.splice(found, 1);
+              });
+          promises.push(promise);
+        });
+
+        return Promise.all(promises);
+      });
   },
 
   isNetworkTypeSecondaryMobile: function(type) {
     return (type == Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE_MMS ||
             type == Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE_SUPL ||
             type == Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE_IMS ||
             type == Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE_DUN);
   },
 
   isNetworkTypeMobile: function(type) {
     return (type == Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE ||
             this.isNetworkTypeSecondaryMobile(type));
   },
 
-  setHostRoutes: function(network) {
-    let hosts = network.getDnses().concat(network.httpProxyHost);
+  _handleGateways: function(networkId, gateways) {
+    let currentNetworkLinks = this.networkInterfaceLinks[networkId];
+    if (currentNetworkLinks.gateways.length == 0 ||
+        currentNetworkLinks.compareGateways(gateways)) {
+      return Promise.resolve();
+    }
 
-    return this._updateRoutes(true, hosts, network.name, network.getGateways());
+    let currentExtraRoutes = currentNetworkLinks.extraRoutes;
+    return this._cleanupAllHostRoutes(networkId)
+      .then(() => {
+        // If gateways have changed, re-add extra host routes with new gateways.
+        if (currentExtraRoutes.length > 0) {
+          this._setHostRoutes(true,
+                              currentExtraRoutes,
+                              currentNetworkLinks.interfaceName,
+                              gateways)
+          .then(() => {
+            currentNetworkLinks.extraRoutes = currentExtraRoutes;
+          });
+        }
+      });
   },
 
-  removeHostRoutes: function(network) {
-    let hosts = network.getDnses().concat(network.httpProxyHost);
+  _cleanupAllHostRoutes: function(networkId) {
+    let currentNetworkLinks = this.networkInterfaceLinks[networkId];
+    let hostRoutes = currentNetworkLinks.linkRoutes.concat(
+      currentNetworkLinks.extraRoutes);
+
+    if (hostRoutes.length === 0) {
+      return Promise.resolve();
+    }
 
-    return this._updateRoutes(false, hosts, network.name, network.getGateways());
+    return this._setHostRoutes(false,
+                               hostRoutes,
+                               currentNetworkLinks.interfaceName,
+                               currentNetworkLinks.gateways)
+      .catch((aError) => {
+        debug("Error (" + aError + ") on _cleanupAllHostRoutes, keep proceeding.");
+      })
+      .then(() => currentNetworkLinks.resetLinks());
   },
 
   selectGateway: function(gateways, host) {
     for (let i = 0; i < gateways.length; i++) {
       let gateway = gateways[i];
       if (gateway.match(this.REGEXP_IPV4) && host.match(this.REGEXP_IPV4) ||
           gateway.indexOf(":") != -1 && host.indexOf(":") != -1) {
         return gateway;
--- a/dom/system/gonk/NetworkService.js
+++ b/dom/system/gonk/NetworkService.js
@@ -51,16 +51,76 @@ function isError(code) {
   let type = netdResponseType(code);
   return (type !== NETD_COMMAND_PROCEEDING && type !== NETD_COMMAND_OKAY);
 }
 
 function debug(msg) {
   dump("-*- NetworkService: " + msg + "\n");
 }
 
+function Task(id, params, setupFunction) {
+  this.id = id;
+  this.params = params;
+  this.setupFunction = setupFunction;
+}
+
+function NetworkWorkerRequestQueue(networkService) {
+  this.networkService = networkService;
+  this.tasks = [];
+}
+NetworkWorkerRequestQueue.prototype = {
+  runQueue: function() {
+    if (this.tasks.length === 0) {
+      return;
+    }
+
+    let task = this.tasks[0];
+    if (DEBUG) debug("run task id: " + task.id);
+
+    if (typeof task.setupFunction === 'function') {
+      // If setupFunction returns false, skip sending to Network Worker but call
+      // handleWorkerMessage() directly with task id, as if the response was
+      // returned from Network Worker.
+      if (!task.setupFunction()) {
+        this.networkService.handleWorkerMessage({id: task.id});
+        return;
+      }
+    }
+
+    gNetworkWorker.postMessage(task.params);
+  },
+
+  enqueue: function(id, params, setupFunction) {
+    if (DEBUG) debug("enqueue id: " + id);
+    this.tasks.push(new Task(id, params, setupFunction));
+
+    if (this.tasks.length === 1) {
+      this.runQueue();
+    }
+  },
+
+  dequeue: function(id) {
+    if (DEBUG) debug("dequeue id: " + id);
+
+    if (!this.tasks.length || this.tasks[0].id != id) {
+      if (DEBUG) debug("Id " + id + " is not on top of the queue");
+      return;
+    }
+
+    this.tasks.shift();
+    if (this.tasks.length > 0) {
+      // Run queue on the next tick.
+      Services.tm.currentThread.dispatch(() => {
+        this.runQueue();
+      }, Ci.nsIThread.DISPATCH_NORMAL);
+    }
+  }
+};
+
+
 /**
  * This component watches for network interfaces changing state and then
  * adjusts routes etc. accordingly.
  */
 function NetworkService() {
   if(DEBUG) debug("Starting net_worker.");
 
   let self = this;
@@ -71,41 +131,52 @@ function NetworkService() {
         self.handleWorkerMessage(event);
       }
     };
     gNetworkWorker.start(networkListener);
   }
   // Callbacks to invoke when a reply arrives from the net_worker.
   this.controlCallbacks = Object.create(null);
 
+  this.addedRoutes = new Map();
+  this.netWorkerRequestQueue = new NetworkWorkerRequestQueue(this);
   this.shutdown = false;
   Services.obs.addObserver(this, "xpcom-shutdown", false);
 }
 
 NetworkService.prototype = {
   classID:   NETWORKSERVICE_CID,
   classInfo: XPCOMUtils.generateCI({classID: NETWORKSERVICE_CID,
                                     contractID: NETWORKSERVICE_CONTRACTID,
                                     classDescription: "Network Service",
                                     interfaces: [Ci.nsINetworkService]}),
   QueryInterface: XPCOMUtils.generateQI([Ci.nsINetworkService]),
 
   // Helpers
 
+  addedRoutes: null,
   idgen: 0,
-  controlMessage: function(params, callback) {
+  controlMessage: function(params, callback, setupFunction) {
     if (this.shutdown) {
       return;
     }
 
+    let id = this.idgen++;
+    params.id = id;
     if (callback) {
-      let id = this.idgen++;
-      params.id = id;
       this.controlCallbacks[id] = callback;
     }
+
+    // For now, we use setupFunction to determine if this command needs to be
+    // queued or not.
+    if (setupFunction) {
+      this.netWorkerRequestQueue.enqueue(id, params, setupFunction);
+      return;
+    }
+
     if (gNetworkWorker) {
       gNetworkWorker.postMessage(params);
     }
   },
 
   handleWorkerMessage: function(response) {
     if(DEBUG) debug("NetworkManager received message from worker: " + JSON.stringify(response));
     let id = response.id;
@@ -113,16 +184,18 @@ NetworkService.prototype = {
       Services.obs.notifyObservers(null, response.topic, response.reason);
       return;
     }
     let callback = this.controlCallbacks[id];
     if (callback) {
       callback.call(this, response);
       delete this.controlCallbacks[id];
     }
+
+    this.netWorkerRequestQueue.dequeue(id);
   },
 
   // nsINetworkService
 
   getNetworkInterfaceStats: function(networkName, callback) {
     if(DEBUG) debug("getNetworkInterfaceStats for " + networkName);
 
     let file = new FileUtils.File("/proc/net/dev");
@@ -302,48 +375,84 @@ NetworkService.prototype = {
     let options = {
       cmd: "removeDefaultRoute",
       ifname: network.name,
       gateways: gateways
     };
     this.controlMessage(options);
   },
 
+  _routeToString: function(interfaceName, host, prefixLength, gateway) {
+    return host + "-" + prefixLength + "-" + gateway + "-" + interfaceName;
+  },
+
   modifyRoute: function(action, interfaceName, host, prefixLength, gateway) {
     let command;
 
     switch (action) {
       case Ci.nsINetworkService.MODIFY_ROUTE_ADD:
         command = 'addHostRoute';
         break;
       case Ci.nsINetworkService.MODIFY_ROUTE_REMOVE:
         command = 'removeHostRoute';
         break;
       default:
         if (DEBUG) debug('Unknown action: ' + action);
         return Promise.reject();
     }
 
+    let route = this._routeToString(interfaceName, host, prefixLength, gateway);
+    let setupFunc = () => {
+      let count = this.addedRoutes.get(route);
+      if (DEBUG) debug(command + ": " + route + " -> " + count);
+
+      // Return false if there is no need to send the command to network worker.
+      if ((action == Ci.nsINetworkService.MODIFY_ROUTE_ADD && count) ||
+          (action == Ci.nsINetworkService.MODIFY_ROUTE_REMOVE &&
+           (!count || count > 1))) {
+        return false;
+      }
+
+      return true;
+    };
+
     if (DEBUG) debug(command + " " + host + " on " + interfaceName);
-    let deferred = Promise.defer();
     let options = {
       cmd: command,
       ifname: interfaceName,
       gateway: gateway,
       prefixLength: prefixLength,
       ip: host
     };
-    this.controlMessage(options, function(data) {
-      if (data.error) {
-        deferred.reject(data.reason);
-        return;
-      }
-      deferred.resolve();
+
+    return new Promise((aResolve, aReject) => {
+      this.controlMessage(options, (data) => {
+        let count = this.addedRoutes.get(route);
+
+        // Remove route from addedRoutes on success or failure.
+        if (action == Ci.nsINetworkService.MODIFY_ROUTE_REMOVE) {
+          if (count > 1) {
+            this.addedRoutes.set(route, count - 1);
+          } else {
+            this.addedRoutes.delete(route);
+          }
+        }
+
+        if (data.error) {
+          aReject(data.reason);
+          return;
+        }
+
+        if (action == Ci.nsINetworkService.MODIFY_ROUTE_ADD) {
+          this.addedRoutes.set(route, count ? count + 1 : 1);
+        }
+
+        aResolve();
+      }, setupFunc);
     });
-    return deferred.promise;
   },
 
   removeHostRoutes: function(ifname) {
     if(DEBUG) debug("Going to remove all host routes on " + ifname);
     let options = {
       cmd: "removeHostRoutes",
       ifname: ifname,
     };
--- a/dom/system/gonk/NetworkUtils.cpp
+++ b/dom/system/gonk/NetworkUtils.cpp
@@ -1950,16 +1950,21 @@ CommandResult NetworkUtils::addHostRoute
   return CommandResult::Pending();
 }
 
 /**
  * Add host route for given network interface.
  */
 CommandResult NetworkUtils::addHostRouteLegacy(NetworkParams& aOptions)
 {
+  if (aOptions.mGateway.IsEmpty()) {
+    ERROR("addHostRouteLegacy does not support empty gateway.");
+    return EINVAL;
+  }
+
   NS_ConvertUTF16toUTF8 autoIfname(aOptions.mIfname);
   NS_ConvertUTF16toUTF8 autoHostname(aOptions.mIp);
   NS_ConvertUTF16toUTF8 autoGateway(aOptions.mGateway);
   int type, prefix;
 
   type = getIpType(autoHostname.get());
   if (type != AF_INET && type != AF_INET6) {
     return EAFNOSUPPORT;