Bug 855015 - Send contacts to the child process less often. r=gwagner a=tef+
authorReuben Morais <reuben.morais@gmail.com>
Thu, 18 Apr 2013 10:04:43 +0200
changeset 118749 0c76ef5f86771e459ed327676496e15d172d3a2a
parent 118748 6d338a0f8c95cc724d982031fa373fe34f9f4d2e
child 118750 ba2874f6a9841307287a7d64930705770f39305b
push id109
push userreuben.morais@gmail.com
push dateThu, 18 Apr 2013 08:07:18 +0000
reviewersgwagner, tef
bugs855015
milestone18.0
Bug 855015 - Send contacts to the child process less often. r=gwagner a=tef+
dom/contacts/ContactManager.js
dom/contacts/fallback/ContactDB.jsm
dom/contacts/fallback/ContactService.jsm
dom/contacts/tests/test_contacts_getall.html
--- a/dom/contacts/ContactManager.js
+++ b/dom/contacts/ContactManager.js
@@ -18,16 +18,18 @@ Cu.import("resource://gre/modules/DOMReq
 XPCOMUtils.defineLazyGetter(Services, "DOMRequest", function() {
   return Cc["@mozilla.org/dom/dom-request-service;1"].getService(Ci.nsIDOMRequestService);
 });
 
 XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
                                    "@mozilla.org/childprocessmessagemanager;1",
                                    "nsIMessageSender");
 
+const CONTACTS_SENDMORE_MINIMUM = 5;
+
 const nsIClassInfo            = Ci.nsIClassInfo;
 const CONTACTPROPERTIES_CID   = Components.ID("{f5181640-89e8-11e1-b0c4-0800200c9a66}");
 const nsIDOMContactProperties = Ci.nsIDOMContactProperties;
 
 // ContactProperties is not directly instantiated. It is used as interface.
 
 function ContactProperties(aProp) { if (DEBUG) debug("ContactProperties Constructor"); }
 
@@ -637,16 +639,19 @@ ContactManager.prototype = {
 
   handleContinue: function CM_handleContinue(aCursorId) {
     if (DEBUG) debug("handleContinue: " + aCursorId);
     let data = this._cursorData[aCursorId];
     if (data.cachedContacts.length > 0) {
       if (DEBUG) debug("contact in cache");
       let contact = data.cachedContacts.shift();
       this.nextTick(this._fireSuccessOrDone.bind(this, data.cursor, contact));
+      if (data.cachedContacts.length < CONTACTS_SENDMORE_MINIMUM) {
+        cpmm.sendAsyncMessage("Contacts:GetAll:SendNow", { cursorId: aCursorId });
+      }
     } else {
       if (DEBUG) debug("waiting for contact");
       data.waitingForNext = true;
     }
   },
 
   remove: function removeContact(aRecord) {
     let request;
--- a/dom/contacts/fallback/ContactDB.jsm
+++ b/dom/contacts/fallback/ContactDB.jsm
@@ -17,25 +17,113 @@ Cu.import("resource://gre/modules/Servic
 Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
 Cu.import("resource://gre/modules/PhoneNumberUtils.jsm");
 
 const DB_NAME = "contacts";
 const DB_VERSION = 8;
 const STORE_NAME = "contacts";
 const SAVED_GETALL_STORE_NAME = "getallcache";
 const CHUNK_SIZE = 20;
+const CHUNK_INTERVAL = 500;
+
+// This gives us >=2^30 unique timer IDs, enough for 1 per ms for 12.4 days.
+let gNextTimeoutId = 0;
+
+let gTimeoutTable = new Map(); // int -> nsITimer
+
+function setTimeout(aCallback, aMilliseconds) {
+  let id = gNextTimeoutId++;
+  let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+  timer.initWithCallback({
+    notify: function notify_callback() {
+        clearTimeout(id);
+        aCallback();
+      }
+    },
+    aMilliseconds,
+    timer.TYPE_ONE_SHOT);
+  gTimeoutTable.set(id, timer);
+  return id;
+}
+
+function clearTimeout(aId) {
+  let timer = gTimeoutTable.get(aId);
+  if (timer) {
+    timer.cancel();
+    gTimeoutTable.delete(aId);
+  }
+}
+
+function ContactDispatcher(aContacts, aFullContacts, aCallback, aNewTxn, aClearDispatcher) {
+  this.nextIndex = 0;
+
+  this.cancelTimeout = function() {
+    if (this.interval) {
+      clearTimeout(this.interval);
+      this.interval = null;
+    }
+  };
+
+  if (aFullContacts) {
+    this.sendChunk = function() {
+      if (aContacts.length > 0) {
+        aCallback(aContacts.splice(0, CHUNK_SIZE));
+        this.interval = setTimeout(this.sendChunk, CHUNK_INTERVAL);
+      } else {
+        aCallback(null);
+        this.cancelTimeout();
+        aClearDispatcher();
+      }
+    }.bind(this);
+  } else {
+    this.count = 0;
+    this.sendChunk = function() {
+      let chunk = [];
+      aNewTxn("readonly", STORE_NAME, function(txn, store) {
+        for (let i = this.nextIndex; i < Math.min(this.nextIndex+CHUNK_SIZE, aContacts.length); ++i) {
+          store.get(aContacts[i]).onsuccess = function(e) {
+            chunk.push(e.target.result);
+            this.count++;
+            if (this.count == aContacts.length) {
+              aCallback(chunk)
+              aCallback(null);
+              this.cancelTimeout();
+              aClearDispatcher();
+            } else if (chunk.length == CHUNK_SIZE) {
+              aCallback(chunk);
+              chunk.length = 0;
+              this.nextIndex += CHUNK_SIZE;
+              this.interval = setTimeout(this.sendChunk, CHUNK_INTERVAL);
+            }
+          }.bind(this);
+        }
+      }.bind(this));
+    }.bind(this);
+  }
+
+  this.sendChunk(0);
+}
+
+ContactDispatcher.prototype = {
+  sendNow: function() {
+    this.cancelTimeout();
+    this.interval = setTimeout(this.sendChunk, 0);
+  }
+};
 
 this.ContactDB = function ContactDB(aGlobal) {
   if (DEBUG) debug("Constructor");
   this._global = aGlobal;
 }
 
 ContactDB.prototype = {
   __proto__: IndexedDBHelper.prototype,
 
+  _dispatcher: {},
+
   upgradeSchema: function upgradeSchema(aTransaction, aDb, aOldVersion, aNewVersion) {
     if (DEBUG) debug("upgrade schema from: " + aOldVersion + " to " + aNewVersion + " called!");
     let db = aDb;
     let objectStore;
     for (let currVersion = aOldVersion; currVersion < aNewVersion; currVersion++) {
       if (currVersion == 0) {
         /**
          * Create the initial database schema.
@@ -511,63 +599,41 @@ ContactDB.prototype = {
         }
       }.bind(this);
       req.onerror = function() {
 
       };
     }.bind(this));
   },
 
-  //TODO Use Timer.jsm (bug 840360) when it's available on b2g18
-  nextTick: function nextTick(aCallback, thisObj) {
-    if (thisObj)
-      aCallback = aCallback.bind(thisObj);
-
-    Services.tm.currentThread.dispatch(aCallback, Ci.nsIThread.DISPATCH_NORMAL);
+  sendNow: function CDB_sendNow(aCursorId) {
+    if (aCursorId in this._dispatcher) {
+      this._dispatcher[aCursorId].sendNow();
+    }
   },
 
-  getAll: function CDB_getAll(aSuccessCb, aFailureCb, aOptions) {
+  _clearDispatcher: function CDB_clearDispatcher(aCursorId) {
+    if (aCursorId in this._dispatcher) {
+      delete this._dispatcher[aCursorId];
+    }
+  },
+
+  getAll: function CDB_getAll(aSuccessCb, aFailureCb, aOptions, aCursorId) {
     if (DEBUG) debug("getAll")
     let optionStr = JSON.stringify(aOptions);
     this.getCacheForQuery(optionStr, function(aCachedResults, aFullContacts) {
       // aFullContacts is true if the cache didn't exist and had to be created.
       // In that case, we receive the full contacts since we already have them
-      // in memory to create the cache anyway. This allows us to avoid accessing
-      // the main object store again.
+      // in memory to create the cache. This allows us to avoid accessing the
+      // object store again.
       if (aCachedResults && aCachedResults.length > 0) {
-        if (DEBUG) debug("query returned " + aCachedResults.length + " contacts");
-        if (aFullContacts) {
-          if (DEBUG) debug("full contacts: " + aCachedResults.length);
-          while(aCachedResults.length) {
-            aSuccessCb(aCachedResults.splice(0, CHUNK_SIZE));
-          }
-          aSuccessCb(null);
-        } else {
-          let count = 0;
-          let sendChunk = function(start) {
-            let chunk = [];
-            this.newTxn("readonly", STORE_NAME, function(txn, store) {
-              for (let i = start; i < Math.min(start+CHUNK_SIZE, aCachedResults.length); ++i) {
-                store.get(aCachedResults[i]).onsuccess = function(e) {
-                  chunk.push(e.target.result);
-                  count++;
-                  if (count == aCachedResults.length) {
-                    aSuccessCb(chunk);
-                    aSuccessCb(null);
-                  } else if (chunk.length == CHUNK_SIZE) {
-                    aSuccessCb(chunk);
-                    chunk.length = 0;
-                    this.nextTick(sendChunk.bind(this, start+CHUNK_SIZE));
-                  }
-                };
-              }
-            });
-          }.bind(this);
-          sendChunk(0);
-        }
+        let newTxnFn = this.newTxn.bind(this);
+        let clearDispatcherFn = this._clearDispatcher.bind(this, aCursorId);
+        this._dispatcher[aCursorId] = new ContactDispatcher(aCachedResults, aFullContacts,
+                                                            aSuccessCb, newTxnFn, clearDispatcherFn);
       } else { // no contacts
         if (DEBUG) debug("query returned no contacts");
         aSuccessCb(null);
       }
     }.bind(this));
   },
 
   /*
--- a/dom/contacts/fallback/ContactService.jsm
+++ b/dom/contacts/fallback/ContactService.jsm
@@ -36,17 +36,17 @@ XPCOMUtils.defineLazyGetter(this, "mRIL"
          getInterface(Ci.nsIRadioInterfaceLayer);
 });
 
 let myGlobal = this;
 
 this.DOMContactManager = {
   init: function() {
     if (DEBUG) debug("Init");
-    this._messages = ["Contacts:Find", "Contacts:GetAll",
+    this._messages = ["Contacts:Find", "Contacts:GetAll", "Contacts:GetAll:SendNow",
                       "Contacts:Clear", "Contact:Save",
                       "Contact:Remove", "Contacts:GetSimContacts",
                       "Contacts:RegisterForMessages", "child-process-shutdown"];
     this._children = [];
     this._messages.forEach(function(msgName) {
       ppmm.addMessageListener(msgName, this);
     }.bind(this));
 
@@ -113,17 +113,22 @@ this.DOMContactManager = {
         if (!this.assertPermission(aMessage, "contacts-read")) {
           return null;
         }
         this._db.getAll(
           function(aContacts) {
             mm.sendAsyncMessage("Contacts:GetAll:Next", {cursorId: msg.cursorId, contacts: aContacts});
           },
           function(aErrorMsg) { mm.sendAsyncMessage("Contacts:Find:Return:KO", { errorMsg: aErrorMsg }); },
-          msg.findOptions);
+          msg.findOptions, msg.cursorId);
+        break;
+      case "Contacts:GetAll:SendNow":
+        // sendNow is a no op if there isn't an existing cursor in the DB, so we
+        // don't need to assert the permission again.
+        this._db.sendNow(msg.cursorId);
         break;
       case "Contact:Save":
         if (msg.options.reason === "create") {
           if (!this.assertPermission(aMessage, "contacts-create")) {
             return null;
           }
         } else {
           if (!this.assertPermission(aMessage, "contacts-write")) {
--- a/dom/contacts/tests/test_contacts_getall.html
+++ b/dom/contacts/tests/test_contacts_getall.html
@@ -56,18 +56,26 @@ function onUnwantedSuccess() {
   ok(false, "onUnwantedSuccess: shouldn't get here");
 }
 
 function onFailure() {
   ok(false, "in on Failure!");
 }
 
 function checkStr(str1, str2, msg) {
-  if (str1)
-    ok(typeof str1 == "string" ? [str1] : str1, (typeof str2 == "string") ? [str2] : str2, msg);
+  // comparing /[null(,null)+]/ and undefined should pass
+  function nonNull(e) {
+    return e != null;
+  }
+  if ((Array.isArray(str1) && str1.filter(nonNull).length == 0 && str2 == undefined)
+     ||(Array.isArray(str2) && str2.filter(nonNull).length == 0 && str1 == undefined)) {
+    ok(true, msg);
+  } else if (str1) {
+    is(JSON.stringify(typeof str1 == "string" ? [str1] : str1), JSON.stringify(typeof str2 == "string" ? [str2] : str2), msg);
+  }
 }
 
 function checkAddress(adr1, adr2) {
   checkStr(adr1.type, adr2.type, "Same type");
   checkStr(adr1.streetAddress, adr2.streetAddress, "Same streetAddress");
   checkStr(adr1.locality, adr2.locality, "Same locality");
   checkStr(adr1.region, adr2.region, "Same region");
   checkStr(adr1.postalCode, adr2.postalCode, "Same postalCode");
@@ -154,36 +162,46 @@ function clearDatabase() {
   req = mozContacts.clear();
   req.onsuccess = function() {
     ok(true, "Cleared the database");
     next();
   };
   req.onerror = onFailure;
 }
 
-function add20Contacts() {
-  ok(true, "Adding 20 contacts");
-  for (let i=0; i<19; i++) {
+function addContacts() {
+  ok(true, "Adding 40 contacts");
+  function addContact(i) {
     createResult1 = new mozContact();
+    properties1.familyName[0] = "Testname" + (i < 10 ? "0" + i : i);
     createResult1.init(properties1);
     req = mozContacts.save(createResult1);
-    req.onsuccess = function() {
-      ok(createResult1.id, "The contact now has an ID.");
-    };
+    req.onsuccess = (function(name) {
+      return function() {
+        ok(createResult1.id, "The contact now has an ID.");
+        is(createResult1.familyName[0], name, "Same name");
+        if (i < 38) {
+          addContact(i+1);
+        } else {
+          createResult1 = new mozContact()
+          properties1.familyName[0] = "Testname39";;
+          createResult1.init(properties1);
+          req = mozContacts.save(createResult1);
+          req.onsuccess = function() {
+            ok(createResult1.id, "The contact now has an ID.");
+            ok(createResult1.name == properties1.name, "Same Name");
+            next();
+          };
+          req.onerror = onFailure;
+        }
+      };
+    })("Testname" + (i < 10 ? "0" + i : i));
     req.onerror = onFailure;
-  };
-  createResult1 = new mozContact();
-  createResult1.init(properties1);
-  req = mozContacts.save(createResult1);
-  req.onsuccess = function() {
-    ok(createResult1.id, "The contact now has an ID.");
-    ok(createResult1.name == properties1.name, "Same Name");
-    next();
-  };
-  req.onerror = onFailure;
+  }
+  addContact(0);
 }
 
 let createResult1;
 
 let index = 0;
 let req;
 let mozContacts = window.navigator.mozContacts;
 
@@ -213,29 +231,37 @@ let steps = [
         is(count, 1, "last contact - only one contact returned");
         next();
       }
     };
     req.onerror = onFailure;
   },
 
   clearDatabase,
-  add20Contacts,
+  addContacts,
 
   function() {
-    ok(true, "Retrieving 20 contacts with getAll");
-    req = mozContacts.getAll({});
+    ok(true, "Retrieving 40 contacts with getAll");
+    req = mozContacts.getAll({
+      sortBy: "familyName",
+      sortOrder: "ascending"
+    });
     let count = 0;
+    let result;
+    let props;
     req.onsuccess = function(event) {
       if (req.result) {
         ok(true, "result is valid");
+        result = req.result;
+        properties1.familyName[0] = "Testname" + (count < 10 ? "0" + count : count);
+        is(result.familyName[0], properties1.familyName[0], "Same familyName");
         count++;
         req.continue();
       } else {
-        is(count, 20, "last contact - 20 contacts returned");
+        is(count, 40, "last contact - 40 contacts returned");
         next();
       }
     };
     req.onerror = onFailure;
   },
   function() {
     ok(true, "Deleting one contact");
     req = mozContacts.remove(createResult1);
@@ -250,25 +276,25 @@ let steps = [
     let count = 0;
     req.onsuccess = function(event) {
       ok(true, "on success");
       if (req.result) {
         ok(true, "result is valid");
         count++;
         req.continue();
       } else {
-        is(count, 19, "last contact - 19 contacts returned");
+        is(count, 39, "last contact - 39 contacts returned");
         next();
       }
     };
     req.onerror = onFailure;
   },
 
   clearDatabase,
-  add20Contacts,
+  addContacts,
 
   function() {
     ok(true, "Test cache consistency when deleting contact during getAll");
     req = mozContacts.find({});
     req.onsuccess = function(e) {
       let lastContact = e.target.result[e.target.result.length-1];
       req = mozContacts.getAll({});
       let count = 0;
@@ -285,26 +311,26 @@ let steps = [
             req.continue();
           };
         } else {
           if (req.result) {
             ok(true, "result is valid");
             count++;
             req.continue();
           } else {
-            is(count, 20, "last contact - 20 contacts returned");
+            is(count, 40, "last contact - 40 contacts returned");
             next();
           }
         }
       };
     };
   },
 
   clearDatabase,
-  add20Contacts,
+  addContacts,
 
   function() {
     ok(true, "Delete the current contact while iterating");
     req = mozContacts.getAll({});
     let count = 0;
     let previousId = null;
     req.onsuccess = function() {
       if (req.result) {
@@ -315,72 +341,72 @@ let steps = [
         previousId = req.result.id;
         count++;
         let delReq = mozContacts.remove(req.result);
         delReq.onsuccess = function() {
           ok(true, "deleted current contact");
           req.continue();
         };
       } else {
-        is(count, 20, "returned 20 contacts");
+        is(count, 40, "returned 40 contacts");
         next();
       }
     };
   },
 
   clearDatabase,
-  add20Contacts,
+  addContacts,
 
   function() {
     ok(true, "Iterating through the contact list inside a cursor callback");
     let count1 = 0, count2 = 0;
     let req1 = mozContacts.getAll({});
     let req2;
     req1.onsuccess = function() {
       if (count1 == 0) {
         count1++;
         req2 = mozContacts.getAll({});
         req2.onsuccess = function() {
           if (req2.result) {
             count2++;
             req2.continue();
           } else {
-            is(count2, 20, "inner cursor returned 20 contacts");
+            is(count2, 40, "inner cursor returned 40 contacts");
             req1.continue();
           }
         };
       } else {
         if (req1.result) {
           count1++;
           req1.continue();
         } else {
-          is(count1, 20, "outer cursor returned 20 contacts");
+          is(count1, 40, "outer cursor returned 40 contacts");
           next();
         }
       }
     };
   },
 
   clearDatabase,
-  add20Contacts,
+  addContacts,
 
   function() {
     ok(true, "20 concurrent cursors");
     const NUM_CURSORS = 20;
     let completed = 0;
     for (let i = 0; i < NUM_CURSORS; ++i) {
       mozContacts.getAll({}).onsuccess = (function(i) {
         let count = 0;
         return function(event) {
           let req = event.target;
           if (req.result) {
             count++;
             req.continue();
           } else {
-            is(count, 20, "cursor " + i + " returned 20 contacts");
+            is(count, 40, "cursor " + i + " returned 40 contacts");
             if (++completed == NUM_CURSORS) {
               next();
             }
           }
         };
       })(i);
     }
   },