Bug 662178 - Simplify timed callbacks. r=rnewman
authorPhilipp von Weitershausen <philipp@weitershausen.de>
Mon, 06 Jun 2011 21:27:36 +0200
changeset 71087 f66e634eb6eb
parent 71086 c69124dfd7b7
child 71088 e8f788d0b653
push id20491
push userpweitershausen@mozilla.com
push date2011-06-15 09:24 +0000
Treeherderresults
reviewersrnewman
bugs662178
milestone7.0a1
Bug 662178 - Simplify timed callbacks. r=rnewman
browser/base/content/syncQuota.js
services/sync/modules/async.js
services/sync/modules/engines.js
services/sync/modules/engines/forms.js
services/sync/modules/engines/history.js
services/sync/modules/jpakeclient.js
services/sync/modules/resource.js
services/sync/modules/service.js
services/sync/modules/util.js
services/sync/tests/unit/test_async_helpers.js
services/sync/tests/unit/test_async_querySpinningly.js
services/sync/tests/unit/test_bookmark_livemarks.js
services/sync/tests/unit/test_history_store.js
services/sync/tests/unit/test_history_tracker.js
services/sync/tests/unit/test_jpakeclient.js
services/sync/tests/unit/test_utils_namedTimer.js
services/sync/tests/unit/xpcshell.ini
--- a/browser/base/content/syncQuota.js
+++ b/browser/base/content/syncQuota.js
@@ -88,17 +88,17 @@ let gSyncQuota = {
   onAccept: function onAccept() {
     let engines = gUsageTreeView.getEnginesToDisable();
     for each (let engine in engines) {
       Weave.Engines.get(engine).enabled = false;
     }
     if (engines.length) {
       // The 'Weave' object will disappear once the window closes.
       let Service = Weave.Service;
-      Weave.Utils.delay(function() Service.sync(), 0);
+      Weave.Utils.nextTick(function() { Service.sync(); });
     }
     return true;
   },
 
   convertKB: function convertKB(value) {
     return DownloadUtils.convertByteUnits(value * 1024);
   }
 
--- a/services/sync/modules/async.js
+++ b/services/sync/modules/async.js
@@ -250,17 +250,17 @@ let Async = {
         } catch (ex) {
           err = ex;
         }
       }
       if ((++i == count) || err) {
         output(err);
         return;
       }
-      Utils.delay(function () { f(items[i], cb); });
+      Utils.nextTick(function () { f(items[i], cb); });
     }
     f(items[i], cb);
   },
 
   /*
    * Return a callback which executes `f` then `callback`, regardless of
    * whether it was invoked with an error. If an exception is thrown during the
    * evaluation of `f`, it takes precedence over an error provided to the
--- a/services/sync/modules/engines.js
+++ b/services/sync/modules/engines.js
@@ -103,17 +103,17 @@ Tracker.prototype = {
   },
 
   // Should be called by service everytime a sync has been done for an engine
   resetScore: function T_resetScore() {
     this._score = 0;
   },
 
   saveChangedIDs: function T_saveChangedIDs() {
-    Utils.delay(function() {
+    Utils.namedTimer(function() {
       Utils.jsonSave("changes/" + this.file, this, this.changedIDs);
     }, 1000, this, "_lazySave");
   },
 
   loadChangedIDs: function T_loadChangedIDs() {
     Utils.jsonLoad("changes/" + this.file, this, function(json) {
       if (json) {
         this.changedIDs = json;
@@ -197,18 +197,17 @@ function Store(name) {
   XPCOMUtils.defineLazyGetter(this, "_timer", function() {
     return Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
   });
 }
 Store.prototype = {
 
   _sleep: function _sleep(delay) {
     let cb = Async.makeSyncCallback();
-    this._timer.initWithCallback({notify: cb}, delay,
-                                 Ci.nsITimer.TYPE_ONE_SHOT);
+    this._timer.initWithCallback(cb, delay, Ci.nsITimer.TYPE_ONE_SHOT);
     Async.waitForSyncCallback(cb);
   },
 
   applyIncomingBatch: function applyIncomingBatch(records) {
     let failed = [];
     for each (let record in records) {
       try {
         this.applyIncoming(record);
@@ -485,17 +484,17 @@ SyncEngine.prototype = {
 
   get toFetch() this._toFetch,
   set toFetch(val) {
     // Coerce the array to a string for more efficient comparison.
     if (val + "" == this._toFetch) {
       return;
     }
     this._toFetch = val;
-    Utils.delay(function () {
+    Utils.namedTimer(function () {
       Utils.jsonSave("toFetch/" + this.name, this, val);
     }, 0, this, "_toFetchDelay");
   },
 
   loadToFetch: function loadToFetch() {
     // Initialize to empty if there's no file
     this._toFetch = [];
     Utils.jsonLoad("toFetch/" + this.name, this, function(toFetch) {
@@ -507,17 +506,17 @@ SyncEngine.prototype = {
 
   get previousFailed() this._previousFailed,
   set previousFailed(val) {
     // Coerce the array to a string for more efficient comparison.
     if (val + "" == this._previousFailed) {
       return;
     }
     this._previousFailed = val;
-    Utils.delay(function () {
+    Utils.namedTimer(function () {
       Utils.jsonSave("failed/" + this.name, this, val);
     }, 0, this, "_previousFailedDelay");
   },
 
   loadPreviousFailed: function loadPreviousFailed() {
     // Initialize to empty if there's no file
     this._previousFailed = [];
     Utils.jsonLoad("failed/" + this.name, this, function(previousFailed) {
--- a/services/sync/modules/engines/forms.js
+++ b/services/sync/modules/engines/forms.js
@@ -338,15 +338,15 @@ FormTracker.prototype = {
       }
 
       if (name === "") {
         this._log.trace("Text input element has no name or id");
         continue;
       }
 
       // Get the GUID on a delay so that it can be added to the DB first...
-      Utils.delay(function() {
+      Utils.nextTick(function() {
         this._log.trace("Logging form element: " + [name, el.value]);
         this.trackEntry(name, el.value);
-      }, 0, this);
+      }, this);
     }
   }
 };
--- a/services/sync/modules/engines/history.js
+++ b/services/sync/modules/engines/history.js
@@ -442,21 +442,21 @@ HistoryTracker.prototype = {
     this.score += 1;
   },
 
   onVisit: function HT_onVisit(uri, vid, time, session, referrer, trans) {
     if (this.ignoreAll)
       return;
     this._log.trace("onVisit: " + uri.spec);
     let self = this;
-    Utils.delay(function() {
+    Utils.nextTick(function() {
       if (self.addChangedID(self._GUIDForUri(uri, true))) {
         self._upScore();
       }
-    }, 0);
+    });
   },
   onDeleteVisits: function onDeleteVisits() {
   },
   onPageExpired: function HT_onPageExpired(uri, time, entry) {
   },
   onBeforeDeleteURI: function onBeforeDeleteURI(uri) {
     if (this.ignoreAll)
       return;
--- a/services/sync/modules/jpakeclient.js
+++ b/services/sync/modules/jpakeclient.js
@@ -206,18 +206,18 @@ JPAKEClient.prototype = {
 
     // Default to "user aborted".
     if (!error)
       error = JPAKE_ERROR_USERABORT;
 
     if (error == JPAKE_ERROR_CHANNEL
         || error == JPAKE_ERROR_NETWORK
         || error == JPAKE_ERROR_NODATA) {
-      Utils.delay(function() { this.observer.onAbort(error); }, 0,
-                  this, "_timer_onAbort");
+      Utils.namedTimer(function() { this.observer.onAbort(error); }, 0,
+                       this, "_timer_onAbort");
     } else {
       this._reportFailure(error, function() { self.observer.onAbort(error); });
     }
   },
 
   /*
    * Utilities
    */
@@ -273,18 +273,18 @@ JPAKEClient.prototype = {
         this.abort(JPAKE_ERROR_CHANNEL);
         return;
       }
       this._log.debug("Using channel " + this._channel);
       this._channelUrl = this._serverUrl + this._channel;
 
       // Don't block on UI code.
       let pin = this._secret + this._channel;
-      Utils.delay(function() { this.observer.displayPIN(pin); }, 0,
-                  this, "_timer_displayPIN");
+      Utils.namedTimer(function() { this.observer.displayPIN(pin); }, 0,
+                       this, "_timer_displayPIN");
       callback();
     }));
   },
 
   // Generic handler for uploading data.
   _putStep: function _putStep(callback) {
     this._log.trace("Uploading message " + this._outgoing.type);
     let resource = new AsyncResource(this._channelUrl);
@@ -303,18 +303,18 @@ JPAKEClient.prototype = {
         this._log.error("Could not upload data. Server responded with HTTP "
                         + response.status);
         this.abort(JPAKE_ERROR_SERVER);
         return;
       }
       // There's no point in returning early here since the next step will
       // always be a GET so let's pause for twice the poll interval.
       this._etag = response.headers["etag"];
-      Utils.delay(function () { callback(); }, this._pollInterval * 2, this,
-                  "_pollTimer");
+      Utils.namedTimer(function () { callback(); }, this._pollInterval * 2,
+                       this, "_pollTimer");
     }));
   },
 
   // Generic handler for polling for and retrieving data.
   _pollTries: 0,
   _getStep: function _getStep(callback) {
     this._log.trace("Retrieving next message.");
     let resource = new AsyncResource(this._channelUrl);
@@ -336,18 +336,18 @@ JPAKEClient.prototype = {
       if (response.status == 304) {
         this._log.trace("Channel hasn't been updated yet. Will try again later.");
         if (this._pollTries >= this._maxTries) {
           this._log.error("Tried for " + this._pollTries + " times, aborting.");
           this.abort(JPAKE_ERROR_TIMEOUT);
           return;
         }
         this._pollTries += 1;
-        Utils.delay(function() { this._getStep(callback); },
-                    this._pollInterval, this, "_pollTimer");
+        Utils.namedTimer(function() { this._getStep(callback); },
+                         this._pollInterval, this, "_pollTimer");
         return;
       }
       this._pollTries = 0;
 
       if (response.status == 404) {
         this._log.error("No data found in the channel.");
         this.abort(JPAKE_ERROR_NODATA);
         return;
@@ -582,13 +582,13 @@ JPAKEClient.prototype = {
 
     this._log.trace("Decrypted data.");
     callback();
   },
 
   _complete: function _complete() {
     this._log.debug("Exchange completed.");
     this._finished = true;
-    Utils.delay(function () { this.observer.onComplete(this._newData); },
-                0, this, "_timer_onComplete");
+    Utils.namedTimer(function () { this.observer.onComplete(this._newData); },
+                     0, this, "_timer_onComplete");
   }
 
 };
--- a/services/sync/modules/resource.js
+++ b/services/sync/modules/resource.js
@@ -594,17 +594,17 @@ ChannelListener.prototype = {
     
     this.delayAbort();
   },
 
   /**
    * Create or push back the abort timer that kills this request
    */
   delayAbort: function delayAbort() {
-    Utils.delay(this.abortRequest, this._timeout, this, "abortTimer");
+    Utils.namedTimer(this.abortRequest, this._timeout, this, "abortTimer");
   },
 
   abortRequest: function abortRequest() {
     // Ignore any callbacks if we happen to get any now
     this.onStopRequest = function() {};
     let error = Components.Exception("Aborting due to channel inactivity.",
                                      Cr.NS_ERROR_NET_TIMEOUT);
     this._onComplete(error);
--- a/services/sync/modules/service.js
+++ b/services/sync/modules/service.js
@@ -433,20 +433,20 @@ WeaveSvc.prototype = {
     let delay = Svc.Prefs.get("autoconnectDelay");
     if (delay) {
       this.delayedAutoConnect(delay);
     }
 
     // Send an event now that Weave service is ready.  We don't do this
     // synchronously so that observers can import this module before
     // registering an observer.
-    Utils.delay(function() {
+    Utils.nextTick(function() {
       Status.ready = true;
       Svc.Obs.notify("weave:service:ready");
-    }, 0);
+    });
   },
 
   _checkSetup: function WeaveSvc__checkSetup() {
     if (!this.enabled)
       return Status.service = STATUS_DISABLED;
     return Status.checkSetup();
   },
 
@@ -586,30 +586,31 @@ WeaveSvc.prototype = {
         break;
       case "weave:resource:status:401":
         this._handleResource401(subject);
         break;
       case "idle":
         this._log.trace("Idle time hit, trying to sync");
         Svc.Idle.removeIdleObserver(this, this._idleTime);
         this._idleTime = 0;
-        Utils.delay(function() this.sync(), 0, this);
+        Utils.nextTick(this.sync, this);
         break;
       case "nsPref:changed":
         if (this._ignorePrefObserver)
           return;
         let engine = data.slice((PREFS_BRANCH + "engine.").length);
         this._handleEngineStatusChanged(engine);
         break;
     }
   },
 
   _handleScoreUpdate: function WeaveSvc__handleScoreUpdate() {
     const SCORE_UPDATE_DELAY = 3000;
-    Utils.delay(this._calculateScore, SCORE_UPDATE_DELAY, this, "_scoreTimer");
+    Utils.namedTimer(this._calculateScore, SCORE_UPDATE_DELAY, this,
+                     "_scoreTimer");
   },
 
   _calculateScore: function WeaveSvc_calculateScoreAndDoStuff() {
     var engines = Engines.getEnabled();
     for (let i = 0;i < engines.length;i++) {
       this._log.trace(engines[i].name + ": score: " + engines[i].score);
       this.globalScore += engines[i].score;
       engines[i]._tracker.resetScore();
@@ -1093,17 +1094,17 @@ WeaveSvc.prototype = {
     });
   },
 
   delayedAutoConnect: function delayedAutoConnect(delay) {
     if (this._loggedIn)
       return;
 
     if (this._checkSetup() == STATUS_OK && Svc.Prefs.get("autoconnect")) {
-      Utils.delay(this._autoConnect, delay * 1000, this, "_autoTimer");
+      Utils.namedTimer(this._autoConnect, delay * 1000, this, "_autoTimer");
     }
   },
 
   _autoConnect: let (attempts = 0) function _autoConnect() {
     let isLocked = Utils.mpLocked();
     if (isLocked) {
       // There's no reason to back off if we're locked: we'll just try to login
       // during sync. Clear our timer, see if we should go ahead and sync, then
@@ -1130,17 +1131,17 @@ WeaveSvc.prototype = {
       if (this.login())
         return;
     }
 
     // Something failed, so try again some time later.
     let interval = this._calculateBackoff(++attempts, 60 * 1000);
     this._log.debug("Autoconnect failed: " + (reason || Status.login) +
       "; retry in " + Math.ceil(interval / 1000) + " sec.");
-    Utils.delay(function() this._autoConnect(), interval, this, "_autoTimer");
+    Utils.namedTimer(this._autoConnect, interval, this, "_autoTimer");
   },
 
   persistLogin: function persistLogin() {
     // Canceled master password prompt can prevent these from succeeding.
     try {
       ID.get("WeaveID").persist();
       ID.get("WeaveCryptoID").persist();
     }
@@ -1570,17 +1571,17 @@ WeaveSvc.prototype = {
     // Start the sync right away if we're already late
     if (interval <= 0) {
       if (this.syncOnIdle())
         this._log.debug("Syncing as soon as we're idle.");
       return;
     }
 
     this._log.trace("Next sync in " + Math.ceil(interval / 1000) + " sec.");
-    Utils.delay(function() this.syncOnIdle(), interval, this, "_syncTimer");
+    Utils.namedTimer(this.syncOnIdle, interval, this, "_syncTimer");
 
     // Save the next sync time in-case sync is disabled (logout/offline/etc.)
     this.nextSync = Date.now() + interval;
 
     // if we're a single client, set up a heartbeat to detect new clients sooner
     if (this.numClients == 1)
       this._scheduleHeartbeat();
   },
@@ -1646,17 +1647,17 @@ WeaveSvc.prototype = {
 
     if (this.nextHeartbeat)
       interval = this.nextHeartbeat - now;
     else
       this.nextHeartbeat = now + interval;
 
     this._log.trace("Setting up heartbeat, next ping in " +
                     Math.ceil(interval / 1000) + " sec.");
-    Utils.delay(function() this._doHeartbeat(), interval, this, "_heartbeatTimer");
+    Utils.namedTimer(this._doHeartbeat, interval, this, "_heartbeatTimer");
   },
 
   /**
    * Incorporates the backoff/retry logic used in error handling and elective
    * non-syncing.
    */
   _scheduleAtInterval: function _scheduleAtInterval(minimumInterval) {
     const MINIMUM_BACKOFF_INTERVAL = 15 * 60 * 1000;     // 15 minutes
--- a/services/sync/modules/util.js
+++ b/services/sync/modules/util.js
@@ -929,26 +929,35 @@ let Utils = {
     NetUtil.asyncCopy(is, fos, function (result) {
       if (typeof callback == "function") {
         callback.call(that);        
       }
     });
   },
 
   /**
+   * Execute a function on the next event loop tick.
+   */
+  nextTick: function nextTick(callback, thisObj) {
+    if (thisObj) {
+      callback = callback.bind(thisObj);
+    }
+    let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+    timer.initWithCallback(callback, 0, timer.TYPE_ONE_SHOT);
+  },
+
+  /**
    * Return a timer that is scheduled to call the callback after waiting the
    * provided time or as soon as possible. The timer will be set as a property
    * of the provided object with the given timer name.
    */
-  delay: function delay(callback, wait, thisObj, name) {
-    // Default to running right away
-    wait = wait || 0;
-
-    // Use a dummy object if one wasn't provided
-    thisObj = thisObj || {};
+  namedTimer: function delay(callback, wait, thisObj, name) {
+    if (!thisObj || !name) {
+      throw "You must provide both an object and a property name for the timer!";
+    }
 
     // Delay an existing timer if it exists
     if (name in thisObj && thisObj[name] instanceof Ci.nsITimer) {
       thisObj[name].delay = wait;
       return;
     }
 
     // Create a special timer that we can add extra properties
--- a/services/sync/tests/unit/test_async_helpers.js
+++ b/services/sync/tests/unit/test_async_helpers.js
@@ -187,25 +187,25 @@ add_test(function test_countedCallback()
     do_check_eq(2, counter);
     do_check_eq("error!", error);
     do_check_eq(2, output);
     do_check_eq("b", context);
     do_check_eq(err, "Oh dear.");
 
     // If we call the counted callback again (once this output function is
     // done, that is), then the component callback is not invoked.
-    Utils.delay(function () {
+    Utils.nextTick(function () {
       _("Don't expect component callback.");
       c1("not", "running", "now");
       do_check_eq(2, counter);
       do_check_eq("error!", error);
       do_check_eq(2, output);
       do_check_eq("b", context);
       run_next_test();
-    }, 1, this);
+    });
   });
 
   c1(1, "foo", "a");
   do_check_eq(1, counter);
   do_check_eq(1, error);
   do_check_eq("foo", output);
   do_check_eq("a", context);
 
--- a/services/sync/tests/unit/test_async_querySpinningly.js
+++ b/services/sync/tests/unit/test_async_querySpinningly.js
@@ -6,17 +6,17 @@ const SQLITE_CONSTRAINT_VIOLATION = 19; 
 function run_test() {
   initTestLogging("Trace");
 
   _("Using the form service to test queries");
   function c(query) Svc.Form.DBConnection.createStatement(query);
 
   _("Make sure the call is async and allows other events to process");
   let isAsync = false;
-  Utils.delay(function() isAsync = true, 0);
+  Utils.nextTick(function() { isAsync = true; });
   do_check_false(isAsync);
 
   _("Empty out the formhistory table");
   let r0 = Async.querySpinningly(c("DELETE FROM moz_formhistory"));
   do_check_eq(r0, null);
 
   _("Make sure there's nothing there");
   let r1 = Async.querySpinningly(c("SELECT 1 FROM moz_formhistory"));
--- a/services/sync/tests/unit/test_bookmark_livemarks.js
+++ b/services/sync/tests/unit/test_bookmark_livemarks.js
@@ -131,10 +131,10 @@ add_test(function test_livemark_invalid(
   _("Parent is a Livemark. Will be skipped.");
   let lmParentRec = makeLivemark(record631361.payload, true);
   lmParentRec._parent = recID;
   store.create(lmParentRec);
   // No exception, but no creation occurs.
   do_check_eq(-1, store.idForGUID(lmParentRec.id, true));
   
   // Clear event loop.
-  Utils.delay(run_next_test, 0);
+  Utils.nextTick(run_next_test);
 });
--- a/services/sync/tests/unit/test_history_store.js
+++ b/services/sync/tests/unit/test_history_store.js
@@ -30,17 +30,17 @@ function queryHistoryVisits(uri) {
 
 function onNextTitleChanged(callback) {
   PlacesUtils.history.addObserver({
     onBeginUpdateBatch: function onBeginUpdateBatch() {},
     onEndUpdateBatch: function onEndUpdateBatch() {},
     onPageChanged: function onPageChanged() {},
     onTitleChanged: function onTitleChanged() {
       PlacesUtils.history.removeObserver(this);
-      Utils.delay(callback, 0, this);
+      Utils.nextTick(callback);
     },
     onVisit: function onVisit() {},
     onDeleteVisits: function onDeleteVisits() {},
     onPageExpired: function onPageExpired() {},
     onBeforeDeleteURI: function onBeforeDeleteURI() {},
     onDeleteURI: function onDeleteURI() {},
     onClearHistory: function onClearHistory() {},
     QueryInterface: XPCOMUtils.generateQI([
--- a/services/sync/tests/unit/test_history_tracker.js
+++ b/services/sync/tests/unit/test_history_tracker.js
@@ -35,20 +35,20 @@ add_test(function test_empty() {
   _("Verify we've got an empty tracker to work with.");
   do_check_eq([id for (id in tracker.changedIDs)].length, 0);
   run_next_test();
 });
 
 add_test(function test_not_tracking(next) {
   _("Create history item. Won't show because we haven't started tracking yet");
   addVisit();
-  Utils.delay(function() {
+  Utils.nextTick(function() {
     do_check_eq([id for (id in tracker.changedIDs)].length, 0);
     run_next_test();
-  }, 0);
+  });
 });
 
 add_test(function test_start_tracking() {
   _("Tell the tracker to start tracking changes.");
   onScoreUpdated(function() {
     do_check_eq([id for (id in tracker.changedIDs)].length, 1);
     run_next_test();
   });
@@ -80,29 +80,29 @@ add_test(function test_track_delete() {
   PlacesUtils.history.removePage(uri);
 });
 
 add_test(function test_stop_tracking() {
   _("Let's stop tracking again.");
   tracker.clearChangedIDs();
   Svc.Obs.notify("weave:engine:stop-tracking");
   addVisit();
-  Utils.delay(function() {
+  Utils.nextTick(function() {
     do_check_eq([id for (id in tracker.changedIDs)].length, 0);
     run_next_test();
-  }, 0);
+  });
 });
 
 add_test(function test_stop_tracking_twice() {
   _("Notifying twice won't do any harm.");
   Svc.Obs.notify("weave:engine:stop-tracking");
   addVisit();
-  Utils.delay(function() {
+  Utils.nextTick(function() {
     do_check_eq([id for (id in tracker.changedIDs)].length, 0);
     run_next_test();
-  }, 0);
+  });
 });
 
 add_test(function cleanup() {
    _("Clean up.");
   PlacesUtils.history.removeAllPages();
   run_next_test();
 });
--- a/services/sync/tests/unit/test_jpakeclient.js
+++ b/services/sync/tests/unit/test_jpakeclient.js
@@ -152,18 +152,17 @@ add_test(function test_success_receiveNo
     },
     onComplete: function onComplete() {}
   });
 
   let rec = new JPAKEClient({
     displayPIN: function displayPIN(pin) {
       _("Received PIN " + pin + ". Entering it in the other computer...");
       this.cid = pin.slice(JPAKE_LENGTH_SECRET);
-      Utils.delay(function() { snd.sendWithPIN(pin, DATA); }, 0,
-                  this, "_timer");
+      Utils.nextTick(function() { snd.sendWithPIN(pin, DATA); });
     },
     onAbort: function onAbort(error) {
       do_throw("Shouldn't have aborted! " + error);
     },
     onComplete: function onComplete(a) {
       // Ensure channel was cleared, no error report.
       do_check_eq(channels[this.cid].data, undefined);
       do_check_eq(error_report, undefined);
@@ -218,18 +217,17 @@ add_test(function test_wrongPIN() {
   let rec = new JPAKEClient({
     displayPIN: function displayPIN(pin) {
       this.cid = pin.slice(JPAKE_LENGTH_SECRET);
       let secret = pin.slice(0, JPAKE_LENGTH_SECRET);
       secret = [char for each (char in secret)].reverse().join("");
       let new_pin = secret + this.cid;
       _("Received PIN " + pin + ", but I'm entering " + new_pin);
 
-      Utils.delay(function() { snd.sendWithPIN(new_pin, DATA); }, 0,
-                  this, "_timer");
+      Utils.nextTick(function() { snd.sendWithPIN(new_pin, DATA); });
     },
     onAbort: function onAbort(error) {
       do_check_eq(error, JPAKE_ERROR_NODATA);
       // Ensure channel was cleared.
       do_check_eq(channels[this.cid].data, undefined);
       run_next_test();
     },
     onComplete: function onComplete() {
@@ -253,18 +251,17 @@ add_test(function test_abort_receiver() 
       // Ensure channel was cleared.
       do_check_eq(channels[this.cid].data, undefined);
       do_check_eq(error_report, JPAKE_ERROR_USERABORT);
       error_report = undefined;
       run_next_test();
     },
     displayPIN: function displayPIN(pin) {
       this.cid = pin.slice(JPAKE_LENGTH_SECRET);
-      Utils.delay(function() { rec.abort(); },
-                  0, this, "_timer");
+      Utils.nextTick(function() { rec.abort(); });
     }
   });
   rec.receiveNoPIN();
 });
 
 
 add_test(function test_abort_sender() {
   _("Test user abort on sending side.");
@@ -293,20 +290,19 @@ add_test(function test_abort_sender() {
       // Ensure channel was cleared, no error report.
       do_check_eq(channels[this.cid].data, undefined);
       do_check_eq(error_report, undefined);
       run_next_test();
     },
     displayPIN: function displayPIN(pin) {
       _("Received PIN " + pin + ". Entering it in the other computer...");
       this.cid = pin.slice(JPAKE_LENGTH_SECRET);
-      Utils.delay(function() { snd.sendWithPIN(pin, DATA); }, 0,
-                  this, "_timer");
-      Utils.delay(function() { snd.abort(); },
-                  POLLINTERVAL, this, "_abortTimer");
+      Utils.nextTick(function() { snd.sendWithPIN(pin, DATA); });
+      Utils.namedTimer(function() { snd.abort(); },
+                       POLLINTERVAL, this, "_abortTimer");
     }
   });
   rec.receiveNoPIN();
 });
 
 
 add_test(function test_wrongmessage() {
   let cid = new_channel();
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/unit/test_utils_namedTimer.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-sync/util.js");
+
+function run_test() {
+  run_next_test();
+}
+
+add_test(function test_required_args() {
+  try {
+    Utils.namedTimer(function callback() {
+      do_throw("Shouldn't fire.");
+    }, 0);
+    do_throw("Should have thrown!");
+  } catch(ex) {
+    run_next_test();
+  }
+});
+
+add_test(function test_simple() {
+  _("Test basic properties of Utils.namedTimer.");
+
+  const delay = 200;
+  let that = {};
+  let t0 = Date.now();
+  Utils.namedTimer(function callback(timer) {
+    do_check_eq(this, that);
+    do_check_eq(this._zetimer, null);
+    do_check_true(timer instanceof Ci.nsITimer);
+    do_check_true((Date.now() - t0) >= delay);
+    run_next_test();
+  }, delay, that, "_zetimer");
+});
+
+add_test(function test_delay() {
+  _("Test delaying a timer that hasn't fired yet.");
+  
+  const delay = 100;
+  let that = {};
+  let t0 = Date.now();
+  function callback(timer) {
+    // The 2nd delay counts.
+    do_check_true((Date.now() - t0) >= 2 * delay);
+    run_next_test();
+  }
+  Utils.namedTimer(callback, delay, that, "_zetimer");
+  Utils.namedTimer(callback, 2 * delay, that, "_zetimer");
+  run_next_test();
+});
+
+add_test(function test_clear() {
+  _("Test clearing a timer that hasn't fired yet.");
+
+  const delay = 0;
+  let that = {};
+  Utils.namedTimer(function callback(timer) {
+    do_throw("Shouldn't fire!");
+  }, delay, that, "_zetimer");
+
+  that._zetimer.clear();
+  do_check_eq(that._zetimer, null);
+  Utils.nextTick(run_next_test);
+
+  run_next_test();
+});
--- a/services/sync/tests/unit/xpcshell.ini
+++ b/services/sync/tests/unit/xpcshell.ini
@@ -87,16 +87,17 @@ tail =
 [test_utils_getIcon.js]
 [test_utils_hkdfExpand.js]
 [test_utils_isArray.js]
 [test_utils_json.js]
 [test_utils_lazyStrings.js]
 [test_utils_lock.js]
 [test_utils_makeGUID.js]
 [test_utils_makeURI.js]
+[test_utils_namedTimer.js]
 [test_utils_notify.js]
 [test_utils_passphrase.js]
 [test_utils_pbkdf2.js]
 [test_utils_sha1.js]
 [test_utils_sha1hmac.js]
 [test_utils_sha256HMAC.js]
 [test_utils_slices.js]
 [test_utils_stackTrace.js]