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
treeherdermozilla-central@0a409e965d39 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrnewman
bugs662178
milestone7.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 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]