Bug 633062 p8 - Add Async observers. r?markh, kitcambridge, tcsc draft
authorEdouard Oger <eoger@fastmail.com>
Thu, 04 Jan 2018 18:07:10 -0500
changeset 749611 efb4ea0b7e86e96bebdfb1a183ca2fb0be225fc2
parent 749592 50c4253bac2d31455407d66ca9da65d05b0b3296
push id97460
push userbmo:eoger@fastmail.com
push dateWed, 31 Jan 2018 20:23:24 +0000
reviewersmarkh, kitcambridge, tcsc
bugs633062
milestone60.0a1
Bug 633062 p8 - Add Async observers. r?markh, kitcambridge, tcsc MozReview-Commit-ID: IvwyleXBcNH
browser/extensions/formautofill/FormAutofillSync.jsm
browser/extensions/formautofill/test/unit/test_sync.js
services/sync/modules/addonsreconciler.js
services/sync/modules/bookmark_repair.js
services/sync/modules/engines.js
services/sync/modules/engines/addons.js
services/sync/modules/engines/bookmarks.js
services/sync/modules/engines/clients.js
services/sync/modules/engines/extension-storage.js
services/sync/modules/engines/forms.js
services/sync/modules/engines/history.js
services/sync/modules/engines/passwords.js
services/sync/modules/engines/prefs.js
services/sync/modules/engines/tabs.js
services/sync/modules/service.js
services/sync/tests/unit/head_helpers.js
services/sync/tests/unit/test_412.js
services/sync/tests/unit/test_addons_engine.js
services/sync/tests/unit/test_addons_reconciler.js
services/sync/tests/unit/test_addons_store.js
services/sync/tests/unit/test_addons_tracker.js
services/sync/tests/unit/test_bookmark_duping.js
services/sync/tests/unit/test_bookmark_engine.js
services/sync/tests/unit/test_bookmark_store.js
services/sync/tests/unit/test_bookmark_tracker.js
services/sync/tests/unit/test_clients_engine.js
services/sync/tests/unit/test_engine.js
services/sync/tests/unit/test_engine_abort.js
services/sync/tests/unit/test_engine_changes_during_sync.js
services/sync/tests/unit/test_extension_storage_tracker.js
services/sync/tests/unit/test_forms_tracker.js
services/sync/tests/unit/test_fxa_node_reassignment.js
services/sync/tests/unit/test_history_engine.js
services/sync/tests/unit/test_history_tracker.js
services/sync/tests/unit/test_hmac_error.js
services/sync/tests/unit/test_node_reassignment.js
services/sync/tests/unit/test_password_engine.js
services/sync/tests/unit/test_password_tracker.js
services/sync/tests/unit/test_prefs_tracker.js
services/sync/tests/unit/test_score_triggers.js
services/sync/tests/unit/test_syncengine_sync.js
services/sync/tests/unit/test_tab_tracker.js
services/sync/tests/unit/test_telemetry.js
services/sync/tests/unit/test_tracker_addChanged.js
services/sync/tps/extensions/tps/resource/tps.jsm
toolkit/components/places/tests/unifiedcomplete/test_remote_tab_matches.js
--- a/browser/extensions/formautofill/FormAutofillSync.jsm
+++ b/browser/extensions/formautofill/FormAutofillSync.jsm
@@ -179,18 +179,17 @@ FormAutofillStore.prototype = {
 };
 
 function FormAutofillTracker(name, engine) {
   Tracker.call(this, name, engine);
 }
 
 FormAutofillTracker.prototype = {
   __proto__: Tracker.prototype,
-  observe: function observe(subject, topic, data) {
-    Tracker.prototype.observe.call(this, subject, topic, data);
+  async observe(subject, topic, data) {
     if (topic != "formautofill-storage-changed") {
       return;
     }
     if (subject && subject.wrappedJSObject && subject.wrappedJSObject.sourceSync) {
       return;
     }
     switch (data) {
       case "add":
@@ -209,21 +208,21 @@ FormAutofillTracker.prototype = {
   get ignoreAll() {
     return false;
   },
 
   // Define an empty setter so that the engine doesn't throw a `TypeError`
   // setting a read-only property.
   set ignoreAll(value) {},
 
-  startTracking() {
+  onStart() {
     Services.obs.addObserver(this, "formautofill-storage-changed");
   },
 
-  stopTracking() {
+  onStop() {
     Services.obs.removeObserver(this, "formautofill-storage-changed");
   },
 
   // We never want to persist changed IDs, as the changes are already stored
   // in FormAutofillStorage
   persistChangedIDs: false,
 
   // Ensure we aren't accidentally using the base persistence.
--- a/browser/extensions/formautofill/test/unit/test_sync.js
+++ b/browser/extensions/formautofill/test/unit/test_sync.js
@@ -137,16 +137,17 @@ add_task(async function test_outgoing() 
         guid: existingGUID,
       },
       {
         guid: deletedGUID,
         deleted: true,
       },
     ]);
 
+    await engine._tracker.asyncObserver.promiseObserversComplete();
     // The tracker should have a score recorded for the 2 additions we had.
     equal(engine._tracker.score, SCORE_INCREMENT_XLARGE * 2);
 
     engine.lastSync = 0;
     await engine.sync();
 
     Assert.equal(collection.count(), 2);
     Assert.ok(collection.wbo(existingGUID));
--- a/services/sync/modules/addonsreconciler.js
+++ b/services/sync/modules/addonsreconciler.js
@@ -52,17 +52,17 @@ this.EXPORTED_SYMBOLS = ["AddonsReconcil
  * The internal state is persisted to a JSON file in the profile directory.
  *
  * An instance of this is bound to an AddonsEngine instance. In reality, it
  * likely exists as a singleton. To AddonsEngine, it functions as a store and
  * an entity which emits events for tracking.
  *
  * The usage pattern for instances of this class is:
  *
- *   let reconciler = new AddonsReconciler();
+ *   let reconciler = new AddonsReconciler(...);
  *   await reconciler.ensureStateLoaded();
  *
  *   // At this point, your instance should be ready to use.
  *
  * When you are finished with the instance, please call:
  *
  *   reconciler.stopListening();
  *   await reconciler.saveState(...);
@@ -108,19 +108,20 @@ this.EXPORTED_SYMBOLS = ["AddonsReconcil
  * Restartless add-ons have interesting behavior during uninstall. These
  * add-ons are first disabled then they are actually uninstalled. So, we will
  * see AL.onDisabling and AL.onDisabled. The onUninstalling and onUninstalled
  * events only come after the Addon Manager is closed or another view is
  * switched to. In the case of Sync performing the uninstall, the uninstall
  * events will occur immediately. However, we still see disabling events and
  * heed them like they were normal. In the end, the state is proper.
  */
-this.AddonsReconciler = function AddonsReconciler() {
+this.AddonsReconciler = function AddonsReconciler(queueCaller) {
   this._log = Log.repository.getLogger("Sync.AddonsReconciler");
   this._log.manageLevelFromPref("services.sync.log.logger.addonsreconciler");
+  this.queueCaller = queueCaller;
 
   Svc.Obs.add("xpcom-shutdown", this.stopListening, this);
 };
 AddonsReconciler.prototype = {
   /** Flag indicating whether we are listening to AddonManager events. */
   _listening: false,
 
   /**
@@ -580,40 +581,40 @@ AddonsReconciler.prototype = {
       }
     } catch (ex) {
       this._log.warn("Exception", ex);
     }
   },
 
   // AddonListeners
   onEnabling: function onEnabling(addon, requiresRestart) {
-    Async.promiseSpinningly(this._handleListener("onEnabling", addon, requiresRestart));
+    this.queueCaller.enqueueCall(() => this._handleListener("onEnabling", addon, requiresRestart));
   },
   onEnabled: function onEnabled(addon) {
-    Async.promiseSpinningly(this._handleListener("onEnabled", addon));
+    this.queueCaller.enqueueCall(() => this._handleListener("onEnabled", addon));
   },
   onDisabling: function onDisabling(addon, requiresRestart) {
-    Async.promiseSpinningly(this._handleListener("onDisabling", addon, requiresRestart));
+    this.queueCaller.enqueueCall(() => this._handleListener("onDisabling", addon, requiresRestart));
   },
   onDisabled: function onDisabled(addon) {
-    Async.promiseSpinningly(this._handleListener("onDisabled", addon));
+    this.queueCaller.enqueueCall(() => this._handleListener("onDisabled", addon));
   },
   onInstalling: function onInstalling(addon, requiresRestart) {
-    Async.promiseSpinningly(this._handleListener("onInstalling", addon, requiresRestart));
+    this.queueCaller.enqueueCall(() => this._handleListener("onInstalling", addon, requiresRestart));
   },
   onInstalled: function onInstalled(addon) {
-    Async.promiseSpinningly(this._handleListener("onInstalled", addon));
+    this.queueCaller.enqueueCall(() => this._handleListener("onInstalled", addon));
   },
   onUninstalling: function onUninstalling(addon, requiresRestart) {
-    Async.promiseSpinningly(this._handleListener("onUninstalling", addon, requiresRestart));
+    this.queueCaller.enqueueCall(() => this._handleListener("onUninstalling", addon, requiresRestart));
   },
   onUninstalled: function onUninstalled(addon) {
-    Async.promiseSpinningly(this._handleListener("onUninstalled", addon));
+    this.queueCaller.enqueueCall(() => this._handleListener("onUninstalled", addon));
   },
   onOperationCancelled: function onOperationCancelled(addon) {
-    Async.promiseSpinningly(this._handleListener("onOperationCancelled", addon));
+    this.queueCaller.enqueueCall(() => this._handleListener("onOperationCancelled", addon));
   },
 
   // InstallListeners
   onInstallEnded: function onInstallEnded(install, addon) {
-    Async.promiseSpinningly(this._handleListener("onInstallEnded", addon));
+    this.queueCaller.enqueueCall(() => this._handleListener("onInstallEnded", addon));
   }
 };
--- a/services/sync/modules/bookmark_repair.js
+++ b/services/sync/modules/bookmark_repair.js
@@ -698,17 +698,17 @@ class BookmarkRepairResponder extends Co
     if (data != "bookmarks") {
       return;
     }
     Svc.Obs.remove("weave:engine:sync:uploaded", this.onUploaded, this);
     if (subject.failed) {
       return;
     }
     log.debug(`bookmarks engine has uploaded stuff - creating a repair response`, subject);
-    Async.promiseSpinningly(this._finishRepair());
+    this.service.clientsEngine._tracker.asyncObserver.enqueueCall(() => this._finishRepair());
   }
 
   async _finishRepair() {
     let clientsEngine = this.service.clientsEngine;
     let flowID = this._currentState.request.flowID;
     let response = {
       request: this._currentState.request.request,
       collection: "bookmarks",
--- a/services/sync/modules/engines.js
+++ b/services/sync/modules/engines.js
@@ -61,19 +61,17 @@ this.Tracker = function Tracker(name, en
   this._ignored = [];
   this._storage = new JSONFile({
     path: Utils.jsonFilePath("changes/" + this.file),
     dataPostProcessor: json => this._dataPostProcessor(json),
     beforeSave: () => this._beforeSave(),
   });
   this.ignoreAll = false;
 
-  Svc.Obs.add("weave:engine:start-tracking", this);
-  Svc.Obs.add("weave:engine:stop-tracking", this);
-
+  this.asyncObserver = Async.asyncObserver(this, this._log);
 };
 
 Tracker.prototype = {
   /*
    * Score can be called as often as desired to decide which engines to sync
    *
    * Valid values for score:
    * -1: Do not sync unless the user specifically requests it (almost disabled)
@@ -91,33 +89,33 @@ Tracker.prototype = {
     return typeof json == "object" && json || {};
   },
 
   // Ensure the Weave storage directory exists before writing the file.
   _beforeSave() {
     return ensureDirectory(this._storage.path);
   },
 
-  get changedIDs() {
-    Async.promiseSpinningly(this._storage.load());
-    return this._storage.data;
-  },
-
   set score(value) {
     this._score = value;
     Observers.notify("weave:engine:score:updated", this.name);
   },
 
   // Should be called by service everytime a sync has been done for an engine
   resetScore() {
     this._score = 0;
   },
 
   persistChangedIDs: true,
 
+  async getChangedIDs() {
+    await this._storage.load();
+    return this._storage.data;
+  },
+
   _saveChangedIDs() {
     if (!this.persistChangedIDs) {
       this._log.debug("Not saving changedIDs.");
       return;
     }
     this._storage.saveSoon();
   },
 
@@ -131,136 +129,129 @@ Tracker.prototype = {
   },
 
   unignoreID(id) {
     let index = this._ignored.indexOf(id);
     if (index != -1)
       this._ignored.splice(index, 1);
   },
 
-  _saveChangedID(id, when) {
+  async _saveChangedID(id, when) {
     this._log.trace(`Adding changed ID: ${id}, ${JSON.stringify(when)}`);
-    this.changedIDs[id] = when;
+    const changedIDs = await this.getChangedIDs();
+    changedIDs[id] = when;
     this._saveChangedIDs();
   },
 
-  addChangedID(id, when) {
+  async addChangedID(id, when) {
     if (!id) {
       this._log.warn("Attempted to add undefined ID to tracker");
       return false;
     }
 
     if (this.ignoreAll || this._ignored.includes(id)) {
       return false;
     }
 
     // Default to the current time in seconds if no time is provided.
     if (when == null) {
       when = this._now();
     }
 
+    const changedIDs = await this.getChangedIDs();
     // Add/update the entry if we have a newer time.
-    if ((this.changedIDs[id] || -Infinity) < when) {
-      this._saveChangedID(id, when);
+    if ((changedIDs[id] || -Infinity) < when) {
+      await this._saveChangedID(id, when);
     }
 
     return true;
   },
 
-  removeChangedID(...ids) {
+  async removeChangedID(...ids) {
     if (!ids.length || this.ignoreAll) {
       return false;
     }
     for (let id of ids) {
       if (!id) {
         this._log.warn("Attempted to remove undefined ID from tracker");
         continue;
       }
       if (this._ignored.includes(id)) {
         this._log.debug(`Not removing ignored ID ${id} from tracker`);
         continue;
       }
-      if (this.changedIDs[id] != null) {
+      const changedIDs = await this.getChangedIDs();
+      if (changedIDs[id] != null) {
         this._log.trace("Removing changed ID " + id);
-        delete this.changedIDs[id];
+        delete changedIDs[id];
       }
     }
-    this._saveChangedIDs();
+    await this._saveChangedIDs();
     return true;
   },
 
-  clearChangedIDs() {
+  async clearChangedIDs() {
     this._log.trace("Clearing changed ID list");
     this._storage.data = {};
-    this._saveChangedIDs();
+    await this._saveChangedIDs();
   },
 
   _now() {
     return Date.now() / 1000;
   },
 
   _isTracking: false,
 
-  // Override these in your subclasses.
-  startTracking() {
+  start() {
+    if (!this.engineIsEnabled()) {
+      return;
+    }
+    this._log.trace("start().");
+    if (!this._isTracking) {
+      this.onStart();
+      this._isTracking = true;
+    }
   },
 
-  stopTracking() {
+  async stop() {
+    this._log.trace("stop().");
+    if (this._isTracking) {
+      await this.asyncObserver.promiseObserversComplete();
+      this.onStop();
+      this._isTracking = false;
+    }
   },
 
+  // Override these in your subclasses.
+  onStart() {},
+  onStop() {},
+  async observe(subject, topic, data) {},
+
   engineIsEnabled() {
     if (!this.engine) {
       // Can't tell -- we must be running in a test!
       return true;
     }
     return this.engine.enabled;
   },
 
-  onEngineEnabledChanged(engineEnabled) {
+  async onEngineEnabledChanged(engineEnabled) {
     if (engineEnabled == this._isTracking) {
       return;
     }
 
     if (engineEnabled) {
-      this.startTracking();
-      this._isTracking = true;
+      this.start();
     } else {
-      this.stopTracking();
-      this._isTracking = false;
-      this.clearChangedIDs();
-    }
-  },
-
-  observe(subject, topic, data) {
-    switch (topic) {
-      case "weave:engine:start-tracking":
-        if (!this.engineIsEnabled()) {
-          return;
-        }
-        this._log.trace("Got start-tracking.");
-        if (!this._isTracking) {
-          this.startTracking();
-          this._isTracking = true;
-        }
-        return;
-      case "weave:engine:stop-tracking":
-        this._log.trace("Got stop-tracking.");
-        if (this._isTracking) {
-          this.stopTracking();
-          this._isTracking = false;
-        }
+      await this.stop();
+      await this.clearChangedIDs();
     }
   },
 
   async finalize() {
-    // Stop listening for tracking and engine enabled change notifications.
-    // Important for tests where we unregister the engine during cleanup.
-    Svc.Obs.remove("weave:engine:start-tracking", this);
-    Svc.Obs.remove("weave:engine:stop-tracking", this);
-
     // Persist all pending tracked changes to disk, and wait for the final write
     // to finish.
     this._saveChangedIDs();
     await this._storage.finalize();
   },
 };
 
 
@@ -655,16 +646,17 @@ this.Engine = function Engine(name, serv
 
   this._modified = this.emptyChangeset();
   this._tracker; // initialize tracker to load previously changed IDs
   this._log.debug("Engine constructed");
 
   XPCOMUtils.defineLazyPreferenceGetter(this, "_enabled",
     `services.sync.engine.${this.prefName}`, false,
     (data, previous, latest) =>
+      // We do not await on the promise onEngineEnabledChanged returns.
       this._tracker.onEngineEnabledChanged(latest));
 };
 Engine.prototype = {
   // _storeObj, and _trackerObj should to be overridden in subclasses
   _storeObj: Store,
   _trackerObj: Tracker,
 
   // Override this method to return a new changeset type.
@@ -707,16 +699,25 @@ Engine.prototype = {
   },
 
   get _tracker() {
     let tracker = new this._trackerObj(this.Name, this);
     this.__defineGetter__("_tracker", () => tracker);
     return tracker;
   },
 
+  startTracking() {
+    this._tracker.start();
+  },
+
+  // Returns a promise
+  stopTracking() {
+    return this._tracker.stop();
+  },
+
   async sync() {
     if (!this.enabled) {
       return false;
     }
 
     if (!this._sync) {
       throw new Error("engine does not implement _sync method");
     }
@@ -736,17 +737,17 @@ Engine.prototype = {
   },
 
   async _wipeClient() {
     await this.resetClient();
     this._log.debug("Deleting all local data");
     this._tracker.ignoreAll = true;
     await this._store.wipe();
     this._tracker.ignoreAll = false;
-    this._tracker.clearChangedIDs();
+    await this._tracker.clearChangedIDs();
   },
 
   async wipeClient() {
     return this._notify("wipe-client", this.name, this._wipeClient)();
   },
 
   /**
    * If one exists, initialize and return a validator for this engine (which
@@ -952,17 +953,17 @@ SyncEngine.prototype = {
     Svc.Prefs.set(this.name + ".lastSyncLocal", value.toString());
   },
 
   /*
    * Returns a changeset for this sync. Engine implementations can override this
    * method to bypass the tracker for certain or all changed items.
    */
   async getChangedIDs() {
-    return this._tracker.changedIDs;
+    return this._tracker.getChangedIDs();
   },
 
   // Create a new record using the store and add in metadata.
   async _createRecord(id) {
     let record = await this._store.createRecord(id, this.name);
     record.id = id;
     record.collection = this.name;
     return record;
@@ -978,17 +979,16 @@ SyncEngine.prototype = {
   },
 
   addForWeakUpload(id, { forceTombstone = false } = {}) {
     this._needWeakUpload.set(id, { forceTombstone });
   },
 
   // Any setup that needs to happen at the beginning of each sync.
   async _syncStartup() {
-
     // Determine if we need to wipe on outdated versions
     let metaGlobal = await this.service.recordManager.get(this.metaURL);
     let engines = metaGlobal.payload.engines || {};
     let engineData = engines[this.name] || {};
 
     let needsWipe = false;
 
     // Assume missing versions are 0 and wipe the server
@@ -1034,17 +1034,17 @@ SyncEngine.prototype = {
     // or any objects fail to upload, they will remain in this._modified. At
     // the end of a sync, or after an error, we add all objects remaining in
     // this._modified to the tracker.
     this.lastSyncLocal = Date.now();
     let initialChanges = await this.pullChanges();
     this._modified.replace(initialChanges);
     // Clear the tracker now. If the sync fails we'll add the ones we failed
     // to upload back.
-    this._tracker.clearChangedIDs();
+    await this._tracker.clearChangedIDs();
     this._tracker.resetScore();
 
     this._log.info(this._modified.count() +
                    " outgoing items pre-reconciliation");
 
     // Keep track of what to delete at the end of sync
     this._delete = {};
   },
@@ -1332,17 +1332,17 @@ SyncEngine.prototype = {
         throw ex;
       }
       this._log.warn("Error decrypting record", ex);
       return { shouldApply: false, error: ex };
     }
 
     if (this._shouldDeleteRemotely(item)) {
       this._log.trace("Deleting item from server without applying", item);
-      this._deleteId(item.id);
+      await this._deleteId(item.id);
       return { shouldApply: false, error: null };
     }
 
     let shouldApply;
     try {
       shouldApply = await this._reconcile(item);
     } catch (ex) {
       if (ex.code == Engine.prototype.eEngineAbortApplyIncoming) {
@@ -1407,32 +1407,32 @@ SyncEngine.prototype = {
   // record will be deleted locally. If we return true, we'll reupload the
   // record to the server -- any extra work that's needed as part of this
   // process should be done at this point (such as mark the record's parent
   // for reuploading in the case of bookmarks).
   async _shouldReviveRemotelyDeletedRecord(remoteItem) {
     return true;
   },
 
-  _deleteId(id) {
-    this._tracker.removeChangedID(id);
+  async _deleteId(id) {
+    await this._tracker.removeChangedID(id);
     this._noteDeletedId(id);
   },
 
   // Marks an ID for deletion at the end of the sync.
   _noteDeletedId(id) {
     if (this._delete.ids == null)
       this._delete.ids = [id];
     else
       this._delete.ids.push(id);
   },
 
   async _switchItemToDupe(localDupeGUID, incomingItem) {
     // The local, duplicate ID is always deleted on the server.
-    this._deleteId(localDupeGUID);
+    await this._deleteId(localDupeGUID);
 
     // We unconditionally change the item's ID in case the engine knows of
     // an item but doesn't expose it through itemExists. If the API
     // contract were stronger, this could be changed.
     this._log.debug("Switching local ID to incoming: " + localDupeGUID + " -> " +
                     incomingItem.id);
     return this._store.changeItemID(localDupeGUID, incomingItem.id);
   },
@@ -1760,16 +1760,17 @@ SyncEngine.prototype = {
       else {
         // For many ids, split into chunks of at most 100
         while (val.length > 0) {
           await doDelete(key, val.slice(0, 100));
           val = val.slice(100);
         }
       }
     }
+    await this._tracker.asyncObserver.promiseObserversComplete();
   },
 
   async _syncCleanup() {
     this._needWeakUpload.clear();
     if (!this._modified) {
       return;
     }
 
@@ -1903,27 +1904,28 @@ SyncEngine.prototype = {
   /*
    * Returns a changeset containing entries for all currently tracked items.
    * The default implementation returns a changeset with timestamps indicating
    * when the item was added to the tracker.
    *
    * @return A `Changeset` object.
    */
   async pullNewChanges() {
+    await this._tracker.asyncObserver.promiseObserversComplete();
     return this.getChangedIDs();
   },
 
   /**
    * Adds all remaining changeset entries back to the tracker, typically for
    * items that failed to upload. This method is called at the end of each sync.
    *
    */
   async trackRemainingChanges() {
     for (let [id, change] of this._modified.entries()) {
-      this._tracker.addChangedID(id, change);
+      await this._tracker.addChangedID(id, change);
     }
   },
 
   async finalize() {
     await super.finalize();
     await this._toFetchStorage.finalize();
     await this._previousFailedStorage.finalize();
   },
--- a/services/sync/modules/engines/addons.js
+++ b/services/sync/modules/engines/addons.js
@@ -109,17 +109,17 @@ Utils.deferGetSet(AddonRecord, "cleartex
  * that AddonManager doesn't.
  *
  * The engine instance overrides a handful of functions on the base class. The
  * rationale for each is documented by that function.
  */
 this.AddonsEngine = function AddonsEngine(service) {
   SyncEngine.call(this, "Addons", service);
 
-  this._reconciler = new AddonsReconciler();
+  this._reconciler = new AddonsReconciler(this._tracker.asyncObserver);
 };
 AddonsEngine.prototype = {
   __proto__:              SyncEngine.prototype,
   _storeObj:              AddonsStore,
   _trackerObj:            AddonsTracker,
   _recordObj:             AddonRecord,
   version:                1,
 
@@ -154,17 +154,18 @@ AddonsEngine.prototype = {
   },
 
   /**
    * Override getChangedIDs to pull in tracker changes plus changes from the
    * reconciler log.
    */
   async getChangedIDs() {
     let changes = {};
-    for (let [id, modified] of Object.entries(this._tracker.changedIDs)) {
+    const changedIDs = await this._tracker.getChangedIDs();
+    for (let [id, modified] of Object.entries(changedIDs)) {
       changes[id] = modified;
     }
 
     let lastSyncDate = new Date(this.lastSync * 1000);
 
     // The reconciler should have been refreshed at the beginning of a sync and
     // we assume this function is only called from within a sync.
     let reconcilerChanges = this._reconciler.getChangesSinceDate(lastSyncDate);
@@ -703,30 +704,28 @@ AddonsTracker.prototype = {
     }
 
     if (!(await this.store.isAddonSyncable(addon))) {
       this._log.debug("Ignoring change because add-on isn't syncable: " +
                       addon.id);
       return;
     }
 
-    if (this.addChangedID(addon.guid, date.getTime() / 1000)) {
+    const added = await this.addChangedID(addon.guid, date.getTime() / 1000);
+    if (added) {
       this.score += SCORE_INCREMENT_XLARGE;
     }
   },
 
-  startTracking() {
-    if (this.engine.enabled) {
-      this.reconciler.startListening();
-    }
-
+  onStart() {
+    this.reconciler.startListening();
     this.reconciler.addChangeListener(this);
   },
 
-  stopTracking() {
+  onStop() {
     this.reconciler.removeChangeListener(this);
     this.reconciler.stopListening();
   },
 };
 
 class AddonValidator extends CollectionValidator {
   constructor(engine = null) {
     super("addons", "id", [
--- a/services/sync/modules/engines/bookmarks.js
+++ b/services/sync/modules/engines/bookmarks.js
@@ -655,20 +655,16 @@ BookmarksEngine.prototype = {
     }
     let mapped = await this._mapDupe(item);
     this._log.debug(item.id + " mapped to " + mapped);
     // We must return a string, not an object, and the entries in the GUIDMap
     // are created via "new String()" making them an object.
     return mapped ? mapped.toString() : mapped;
   },
 
-  async pullNewChanges() {
-    return this._tracker.promiseChangedIDs();
-  },
-
   // Called when _findDupe returns a dupe item and the engine has decided to
   // switch the existing item to the new incoming item.
   async _switchItemToDupe(localDupeGUID, incomingItem) {
     let newChanges = await PlacesSyncUtils.bookmarks.dedupe(localDupeGUID,
                                                             incomingItem.id,
                                                             incomingItem.parentid);
     this._modified.insert(newChanges);
   },
@@ -1138,19 +1134,19 @@ BufferedBookmarksStore.prototype = {
 };
 
 // The bookmarks tracker is a special flower. Instead of listening for changes
 // via observer notifications, it queries Places for the set of items that have
 // changed since the last sync. Because it's a "pull-based" tracker, it ignores
 // all concepts of "add a changed ID." However, it still registers an observer
 // to bump the score, so that changed bookmarks are synced immediately.
 function BookmarksTracker(name, engine) {
+  Tracker.call(this, name, engine);
   this._batchDepth = 0;
   this._batchSawScoreIncrement = false;
-  Tracker.call(this, name, engine);
 }
 BookmarksTracker.prototype = {
   __proto__: Tracker.prototype,
 
   // `_ignore` checks the change source for each observer notification, so we
   // don't want to let the engine ignore all changes during a sync.
   get ignoreAll() {
     return false;
@@ -1159,75 +1155,65 @@ BookmarksTracker.prototype = {
   // Define an empty setter so that the engine doesn't throw a `TypeError`
   // setting a read-only property.
   set ignoreAll(value) {},
 
   // We never want to persist changed IDs, as the changes are already stored
   // in Places.
   persistChangedIDs: false,
 
-  startTracking() {
+  onStart() {
     PlacesUtils.bookmarks.addObserver(this, true);
-    Svc.Obs.add("bookmarks-restore-begin", this);
-    Svc.Obs.add("bookmarks-restore-success", this);
-    Svc.Obs.add("bookmarks-restore-failed", this);
+    Svc.Obs.add("bookmarks-restore-begin", this.asyncObserver);
+    Svc.Obs.add("bookmarks-restore-success", this.asyncObserver);
+    Svc.Obs.add("bookmarks-restore-failed", this.asyncObserver);
   },
 
-  stopTracking() {
+  onStop() {
     PlacesUtils.bookmarks.removeObserver(this);
-    Svc.Obs.remove("bookmarks-restore-begin", this);
-    Svc.Obs.remove("bookmarks-restore-success", this);
-    Svc.Obs.remove("bookmarks-restore-failed", this);
+    Svc.Obs.remove("bookmarks-restore-begin", this.asyncObserver);
+    Svc.Obs.remove("bookmarks-restore-success", this.asyncObserver);
+    Svc.Obs.remove("bookmarks-restore-failed", this.asyncObserver);
   },
 
   // Ensure we aren't accidentally using the base persistence.
   addChangedID(id, when) {
     throw new Error("Don't add IDs to the bookmarks tracker");
   },
 
   removeChangedID(id) {
     throw new Error("Don't remove IDs from the bookmarks tracker");
   },
 
   // This method is called at various times, so we override with a no-op
   // instead of throwing.
   clearChangedIDs() {},
 
-  promiseChangedIDs() {
+  async getChangedIDs() {
     return PlacesSyncUtils.bookmarks.pullChanges();
   },
 
-  get changedIDs() {
-    throw new Error("Use promiseChangedIDs");
-  },
-
   set changedIDs(obj) {
     throw new Error("Don't set initial changed bookmark IDs");
   },
 
-  observe: function observe(subject, topic, data) {
-    Tracker.prototype.observe.call(this, subject, topic, data);
-
+  async observe(subject, topic, data) {
     switch (topic) {
-      case "weave:engine:start-tracking":
-        break;
       case "bookmarks-restore-begin":
         this._log.debug("Ignoring changes from importing bookmarks.");
         break;
       case "bookmarks-restore-success":
         this._log.debug("Tracking all items on successful import.");
 
         if (data == "json") {
           this._log.debug("Restore succeeded: wiping server and other clients.");
-          Async.promiseSpinningly((async () => {
-            await this.engine.service.resetClient([this.name]);
-            await this.engine.service.wipeServer([this.name]);
-            await this.engine.service.clientsEngine.sendCommand("wipeEngine", [this.name],
-                                                                null, { reason: "bookmark-restore" });
-          })());
+          await this.engine.service.resetClient([this.name]);
+          await this.engine.service.wipeServer([this.name]);
+          await this.engine.service.clientsEngine.sendCommand("wipeEngine", [this.name],
+                                                              null, { reason: "bookmark-restore" });
         } else {
           // "html", "html-initial", or "json-append"
           this._log.debug("Import succeeded.");
         }
         break;
       case "bookmarks-restore-failed":
         this._log.debug("Tracking all items on failed import.");
         break;
--- a/services/sync/modules/engines/clients.js
+++ b/services/sync/modules/engines/clients.js
@@ -360,17 +360,17 @@ ClientEngine.prototype = {
     }
     this._knownStaleFxADeviceIds = Utils.arraySub(localClients, fxaClients);
   },
 
   async _syncStartup() {
     this.isFirstSync = !this.lastRecordUpload;
     // Reupload new client record periodically.
     if (Date.now() / 1000 - this.lastRecordUpload > CLIENTS_TTL_REFRESH) {
-      this._tracker.addChangedID(this.localID);
+      await this._tracker.addChangedID(this.localID);
       this.lastRecordUpload = Date.now() / 1000;
     }
     return SyncEngine.prototype._syncStartup.call(this);
   },
 
   async _processIncoming() {
     // Fetch all records from the server.
     this.lastSync = 0;
@@ -628,17 +628,17 @@ ClientEngine.prototype = {
     this._log.debug("Handling HMAC mismatch for " + item.id);
 
     let base = await SyncEngine.prototype.handleHMACMismatch.call(this, item, mayRetry);
     if (base != SyncEngine.kRecoveryStrategy.error)
       return base;
 
     // It's a bad client record. Save it to be deleted at the end of the sync.
     this._log.debug("Bad client record detected. Scheduling for deletion.");
-    this._deleteId(item.id);
+    await this._deleteId(item.id);
 
     // Neither try again nor error; we're going to delete it.
     return SyncEngine.kRecoveryStrategy.ignore;
   },
 
   /**
    * A hash of valid commands that the client knows about. The key is a command
    * and the value is a hash containing information about the command such as
@@ -679,17 +679,17 @@ ClientEngine.prototype = {
       args,
       // We send the flowID to the other client so *it* can report it in its
       // telemetry - we record it in ours below.
       flowID: telemetryExtra.flowID,
     };
 
     if ((await this._addClientCommand(clientId, action))) {
       this._log.trace(`Client ${clientId} got a new action`, [command, args]);
-      this._tracker.addChangedID(clientId);
+      await this._tracker.addChangedID(clientId);
       try {
         telemetryExtra.deviceID = this.service.identity.hashedDeviceID(clientId);
       } catch (_) {}
 
       this.service.recordTelemetryEvent("sendcommand", command, undefined, telemetryExtra);
     } else {
       this._log.trace(`Client ${clientId} got a duplicate action`, [command, args]);
     }
@@ -795,17 +795,17 @@ ClientEngine.prototype = {
         }
         // Add the command to the "cleared" commands list
         if (shouldRemoveCommand) {
           await this.removeLocalCommand(rawCommand);
           didRemoveCommand = true;
         }
       }
       if (didRemoveCommand) {
-        this._tracker.addChangedID(this.localID);
+        await this._tracker.addChangedID(this.localID);
       }
 
       if (URIsToDisplay.length) {
         this._handleDisplayURIs(URIsToDisplay);
       }
 
       return true;
     })();
@@ -909,17 +909,17 @@ ClientEngine.prototype = {
    *        send this.
    */
   _handleDisplayURIs: function _handleDisplayURIs(uris) {
     Svc.Obs.notify("weave:engine:clients:display-uris", uris);
   },
 
   async _removeRemoteClient(id) {
     delete this._store._remoteClients[id];
-    this._tracker.removeChangedID(id);
+    await this._tracker.removeChangedID(id);
     await this._removeClientCommands(id);
     this._modified.delete(id);
   },
 };
 
 function ClientStore(name, engine) {
   Store.call(this, name, engine);
 }
@@ -1042,38 +1042,31 @@ ClientStore.prototype = {
 
   async wipe() {
     this._remoteClients = {};
   },
 };
 
 function ClientsTracker(name, engine) {
   Tracker.call(this, name, engine);
-  Svc.Obs.add("weave:engine:start-tracking", this);
-  Svc.Obs.add("weave:engine:stop-tracking", this);
 }
 ClientsTracker.prototype = {
   __proto__: Tracker.prototype,
 
   _enabled: false,
 
-  observe: function observe(subject, topic, data) {
+  onStart() {
+    Svc.Prefs.observe("client.name", this.asyncObserver);
+  },
+  onStop() {
+    Svc.Prefs.ignore("client.name", this.asyncObserver);
+  },
+
+  async observe(subject, topic, data) {
     switch (topic) {
-      case "weave:engine:start-tracking":
-        if (!this._enabled) {
-          Svc.Prefs.observe("client.name", this);
-          this._enabled = true;
-        }
-        break;
-      case "weave:engine:stop-tracking":
-        if (this._enabled) {
-          Svc.Prefs.ignore("client.name", this);
-          this._enabled = false;
-        }
-        break;
       case "nsPref:changed":
         this._log.debug("client.name preference changed");
-        this.addChangedID(this.engine.localID);
+        await this.addChangedID(this.engine.localID);
         this.score += SCORE_INCREMENT_XLARGE;
         break;
     }
   }
 };
--- a/services/sync/modules/engines/extension-storage.js
+++ b/services/sync/modules/engines/extension-storage.js
@@ -56,27 +56,25 @@ ExtensionStorageEngine.prototype = {
 };
 
 function ExtensionStorageTracker(name, engine) {
   Tracker.call(this, name, engine);
 }
 ExtensionStorageTracker.prototype = {
   __proto__: Tracker.prototype,
 
-  startTracking() {
-    Svc.Obs.add("ext.storage.sync-changed", this);
+  onStart() {
+    Svc.Obs.add("ext.storage.sync-changed", this.asyncObserver);
   },
 
-  stopTracking() {
-    Svc.Obs.remove("ext.storage.sync-changed", this);
+  onStop() {
+    Svc.Obs.remove("ext.storage.sync-changed", this.asyncObserver);
   },
 
-  observe(subject, topic, data) {
-    Tracker.prototype.observe.call(this, subject, topic, data);
-
+  async observe(subject, topic, data) {
     if (this.ignoreAll) {
       return;
     }
 
     if (topic !== "ext.storage.sync-changed") {
       return;
     }
 
--- a/services/sync/modules/engines/forms.js
+++ b/services/sync/modules/engines/forms.js
@@ -216,41 +216,41 @@ function FormTracker(name, engine) {
 }
 FormTracker.prototype = {
   __proto__: Tracker.prototype,
 
   QueryInterface: XPCOMUtils.generateQI([
     Ci.nsIObserver,
     Ci.nsISupportsWeakReference]),
 
-  startTracking() {
-    Svc.Obs.add("satchel-storage-changed", this);
+  onStart() {
+    Svc.Obs.add("satchel-storage-changed", this.asyncObserver);
   },
 
-  stopTracking() {
-    Svc.Obs.remove("satchel-storage-changed", this);
+  onStop() {
+    Svc.Obs.remove("satchel-storage-changed", this.asyncObserver);
   },
 
-  observe(subject, topic, data) {
-    Tracker.prototype.observe.call(this, subject, topic, data);
+  async observe(subject, topic, data) {
     if (this.ignoreAll) {
       return;
     }
     switch (topic) {
       case "satchel-storage-changed":
         if (data == "formhistory-add" || data == "formhistory-remove") {
           let guid = subject.QueryInterface(Ci.nsISupportsString).toString();
-          this.trackEntry(guid);
+          await this.trackEntry(guid);
         }
         break;
     }
   },
 
-  trackEntry(guid) {
-    if (this.addChangedID(guid)) {
+  async trackEntry(guid) {
+    const added = await this.addChangedID(guid);
+    if (added) {
       this.score += SCORE_INCREMENT_MEDIUM;
     }
   },
 };
 
 
 class FormsProblemData extends CollectionProblemData {
   getSummary() {
--- a/services/sync/modules/engines/history.js
+++ b/services/sync/modules/engines/history.js
@@ -69,24 +69,25 @@ HistoryEngine.prototype = {
     }
   },
 
   shouldSyncURL(url) {
     return !url.startsWith("file:");
   },
 
   async pullNewChanges() {
-    let modifiedGUIDs = Object.keys(this._tracker.changedIDs);
+    const changedIDs = await this._tracker.getChangedIDs();
+    let modifiedGUIDs = Object.keys(changedIDs);
     if (!modifiedGUIDs.length) {
       return {};
     }
 
     let guidsToRemove = await PlacesSyncUtils.history.determineNonSyncableGuids(modifiedGUIDs);
-    this._tracker.removeChangedID(...guidsToRemove);
-    return this._tracker.changedIDs;
+    await this._tracker.removeChangedID(...guidsToRemove);
+    return changedIDs;
   },
 };
 
 function HistoryStore(name, engine) {
   Store.call(this, name, engine);
 
   // Explicitly nullify our references to our cached services so we don't leak
   Svc.Obs.add("places-shutdown", function() {
@@ -432,58 +433,67 @@ HistoryStore.prototype = {
 };
 
 function HistoryTracker(name, engine) {
   Tracker.call(this, name, engine);
 }
 HistoryTracker.prototype = {
   __proto__: Tracker.prototype,
 
-  startTracking() {
+  onStart() {
     this._log.info("Adding Places observer.");
     PlacesUtils.history.addObserver(this, true);
   },
 
-  stopTracking() {
+  onStop() {
     this._log.info("Removing Places observer.");
     PlacesUtils.history.removeObserver(this);
   },
 
   QueryInterface: XPCOMUtils.generateQI([
     Ci.nsINavHistoryObserver,
     Ci.nsISupportsWeakReference
   ]),
 
-  onDeleteAffectsGUID(uri, guid, reason, source, increment) {
+  async onDeleteAffectsGUID(uri, guid, reason, source, increment) {
     if (this.ignoreAll || reason == Ci.nsINavHistoryObserver.REASON_EXPIRED) {
       return;
     }
     this._log.trace(source + ": " + uri.spec + ", reason " + reason);
-    if (this.addChangedID(guid)) {
+    const added = await this.addChangedID(guid);
+    if (added) {
       this.score += increment;
     }
   },
 
   onDeleteVisits(uri, visitTime, guid, reason) {
-    this.onDeleteAffectsGUID(uri, guid, reason, "onDeleteVisits", SCORE_INCREMENT_SMALL);
+    this.asyncObserver.enqueueCall(() =>
+      this.onDeleteAffectsGUID(uri, guid, reason, "onDeleteVisits", SCORE_INCREMENT_SMALL)
+    );
   },
 
   onDeleteURI(uri, guid, reason) {
-    this.onDeleteAffectsGUID(uri, guid, reason, "onDeleteURI", SCORE_INCREMENT_XLARGE);
+    this.asyncObserver.enqueueCall(() =>
+      this.onDeleteAffectsGUID(uri, guid, reason, "onDeleteURI", SCORE_INCREMENT_XLARGE)
+    );
   },
 
   onVisits(aVisits) {
+    this.asyncObserver.enqueueCall(() => this._onVisits(aVisits));
+  },
+
+  async _onVisits(aVisits) {
     if (this.ignoreAll) {
       this._log.trace("ignoreAll: ignoring visits [" +
                       aVisits.map(v => v.guid).join(",") + "]");
       return;
     }
     for (let {uri, guid} of aVisits) {
       this._log.trace("onVisits: " + uri.spec);
-      if (this.engine.shouldSyncURL(uri.spec) && this.addChangedID(guid)) {
+      if (this.engine.shouldSyncURL(uri.spec) && (await this.addChangedID(guid))) {
         this.score += SCORE_INCREMENT_SMALL;
       }
     }
   },
 
   onClearHistory() {
     this._log.trace("onClearHistory");
     // Note that we're going to trigger a sync, but none of the cleared
--- a/services/sync/modules/engines/passwords.js
+++ b/services/sync/modules/engines/passwords.js
@@ -324,75 +324,74 @@ PasswordStore.prototype = {
 
   async wipe() {
     Services.logins.removeAllLogins();
   },
 };
 
 function PasswordTracker(name, engine) {
   Tracker.call(this, name, engine);
-  Svc.Obs.add("weave:engine:start-tracking", this);
-  Svc.Obs.add("weave:engine:stop-tracking", this);
 }
 PasswordTracker.prototype = {
   __proto__: Tracker.prototype,
 
-  startTracking() {
-    Svc.Obs.add("passwordmgr-storage-changed", this);
+  onStart() {
+    Svc.Obs.add("passwordmgr-storage-changed", this.asyncObserver);
   },
 
-  stopTracking() {
-    Svc.Obs.remove("passwordmgr-storage-changed", this);
+  onStop() {
+    Svc.Obs.remove("passwordmgr-storage-changed", this.asyncObserver);
   },
 
-  observe(subject, topic, data) {
-    Tracker.prototype.observe.call(this, subject, topic, data);
-
+  async observe(subject, topic, data) {
     if (this.ignoreAll) {
       return;
     }
 
     // A single add, remove or change or removing all items
     // will trigger a sync for MULTI_DEVICE.
     switch (data) {
       case "modifyLogin": {
         subject.QueryInterface(Ci.nsIArrayExtensions);
         let oldLogin = subject.GetElementAt(0);
         let newLogin = subject.GetElementAt(1);
         if (!isSyncableChange(oldLogin, newLogin)) {
           this._log.trace(`${data}: Ignoring change for ${newLogin.guid}`);
           break;
         }
-        if (this._trackLogin(newLogin)) {
+        const tracked = await this._trackLogin(newLogin);
+        if (tracked) {
           this._log.trace(`${data}: Tracking change for ${newLogin.guid}`);
         }
         break;
       }
 
       case "addLogin":
       case "removeLogin":
         subject.QueryInterface(Ci.nsILoginMetaInfo).QueryInterface(Ci.nsILoginInfo);
-        if (this._trackLogin(subject)) {
+        const tracked = await this._trackLogin(subject);
+        if (tracked) {
           this._log.trace(data + ": " + subject.guid);
         }
         break;
 
       case "removeAllLogins":
         this._log.trace(data);
         this.score += SCORE_INCREMENT_XLARGE;
         break;
     }
   },
 
-  _trackLogin(login) {
+  async _trackLogin(login) {
     if (Utils.getSyncCredentialsHosts().has(login.hostname)) {
       // Skip over Weave password/passphrase changes.
       return false;
     }
-    if (!this.addChangedID(login.guid)) {
+    const added = await this.addChangedID(login.guid);
+    if (!added) {
       return false;
     }
     this.score += SCORE_INCREMENT_XLARGE;
     return true;
   },
 };
 
 class PasswordValidator extends CollectionValidator {
--- a/services/sync/modules/engines/prefs.js
+++ b/services/sync/modules/engines/prefs.js
@@ -230,19 +230,17 @@ PrefStore.prototype = {
 
   async wipe() {
     this._log.trace("Ignoring wipe request");
   }
 };
 
 function PrefTracker(name, engine) {
   Tracker.call(this, name, engine);
-  Svc.Obs.add("profile-before-change", this);
-  Svc.Obs.add("weave:engine:start-tracking", this);
-  Svc.Obs.add("weave:engine:stop-tracking", this);
+  Svc.Obs.add("profile-before-change", this.asyncObserver);
 }
 PrefTracker.prototype = {
   __proto__: Tracker.prototype,
 
   get modified() {
     return Svc.Prefs.get("engine.prefs.modified", false);
   },
   set modified(value) {
@@ -256,31 +254,29 @@ PrefTracker.prototype = {
  __prefs: null,
   get _prefs() {
     if (!this.__prefs) {
       this.__prefs = new Preferences();
     }
     return this.__prefs;
   },
 
-  startTracking() {
-    Services.prefs.addObserver("", this);
+  onStart() {
+    Services.prefs.addObserver("", this.asyncObserver);
   },
 
-  stopTracking() {
+  onStop() {
     this.__prefs = null;
-    Services.prefs.removeObserver("", this);
+    Services.prefs.removeObserver("", this.asyncObserver);
   },
 
-  observe(subject, topic, data) {
-    Tracker.prototype.observe.call(this, subject, topic, data);
-
+  async observe(subject, topic, data) {
     switch (topic) {
       case "profile-before-change":
-        this.stopTracking();
+        await this.stop();
         break;
       case "nsPref:changed":
         if (this.ignoreAll) {
           break;
         }
         // Trigger a sync for MULTI-DEVICE for a change that determines
         // which prefs are synced or a regular pref change.
         if (data.indexOf(PREF_SYNC_PREFS_PREFIX) == 0 ||
--- a/services/sync/modules/engines/tabs.js
+++ b/services/sync/modules/engines/tabs.js
@@ -272,18 +272,16 @@ TabStore.prototype = {
   async update(record) {
     this._log.trace("Ignoring tab updates as local ones win");
   },
 };
 
 
 function TabTracker(name, engine) {
   Tracker.call(this, name, engine);
-  Svc.Obs.add("weave:engine:start-tracking", this);
-  Svc.Obs.add("weave:engine:stop-tracking", this);
 
   // Make sure "this" pointer is always set correctly for event listeners.
   this.onTab = Utils.bind2(this, this.onTab);
   this._unregisterListeners = Utils.bind2(this, this._unregisterListeners);
 }
 TabTracker.prototype = {
   __proto__: Tracker.prototype,
 
@@ -317,35 +315,33 @@ TabTracker.prototype = {
     for (let topic of this._topics) {
       window.removeEventListener(topic, this.onTab);
     }
     if (window.gBrowser) {
       window.gBrowser.removeProgressListener(this);
     }
   },
 
-  startTracking() {
-    Svc.Obs.add("domwindowopened", this);
+  onStart() {
+    Svc.Obs.add("domwindowopened", this.asyncObserver);
     let wins = Services.wm.getEnumerator("navigator:browser");
     while (wins.hasMoreElements()) {
       this._registerListenersForWindow(wins.getNext());
     }
   },
 
-  stopTracking() {
-    Svc.Obs.remove("domwindowopened", this);
+  onStop() {
+    Svc.Obs.remove("domwindowopened", this.asyncObserver);
     let wins = Services.wm.getEnumerator("navigator:browser");
     while (wins.hasMoreElements()) {
       this._unregisterListenersForWindow(wins.getNext());
     }
   },
 
-  observe(subject, topic, data) {
-    Tracker.prototype.observe.call(this, subject, topic, data);
-
+  async observe(subject, topic, data) {
     switch (topic) {
       case "domwindowopened":
         let onLoad = () => {
           subject.removeEventListener("load", onLoad);
           // Only register after the window is done loading to avoid unloads.
           this._registerListenersForWindow(subject);
         };
 
--- a/services/sync/modules/service.js
+++ b/services/sync/modules/service.js
@@ -297,16 +297,17 @@ Sync11Service.prototype = {
     this.scheduler = new SyncScheduler(this);
     this.errorHandler = new ErrorHandler(this);
 
     this._log = Log.repository.getLogger("Sync.Service");
     this._log.manageLevelFromPref("services.sync.log.logger.service.main");
 
     this._log.info("Loading Weave " + WEAVE_VERSION);
 
+    this.asyncObserver = Async.asyncObserver(this, this._log);
     this._clusterManager = this.identity.createClusterManager(this);
     this.recordManager = new RecordManager(this);
 
     this.enabled = true;
 
     await this._registerEngines();
 
     let ua = Cc["@mozilla.org/network/protocol;1?name=http"].
@@ -314,30 +315,31 @@ Sync11Service.prototype = {
     this._log.info(ua);
 
     if (!this._checkCrypto()) {
       this.enabled = false;
       this._log.info("Could not load the Weave crypto component. Disabling " +
                       "Weave, since it will not work correctly.");
     }
 
-    Svc.Obs.add("weave:service:setup-complete", this);
-    Svc.Obs.add("sync:collection_changed", this); // Pulled from FxAccountsCommon
-    Svc.Obs.add("fxaccounts:device_disconnected", this);
-    Services.prefs.addObserver(PREFS_BRANCH + "engine.", this);
+    Svc.Obs.add("weave:service:setup-complete", this.asyncObserver);
+    Svc.Obs.add("sync:collection_changed", this.asyncObserver); // Pulled from FxAccountsCommon
+    Svc.Obs.add("fxaccounts:device_disconnected", this.asyncObserver);
+    // We use a different synchronous observer to make testing easier.
+    Services.prefs.addObserver(PREFS_BRANCH + "engine.", this.prefObserver.bind(this));
 
     if (!this.enabled) {
       this._log.info("Firefox Sync disabled.");
     }
 
     this._updateCachedURLs();
 
     let status = this._checkSetup();
     if (status != STATUS_DISABLED && status != CLIENT_NOT_CONFIGURED) {
-      Svc.Obs.notify("weave:engine:start-tracking");
+      this._startTracking();
     }
 
     // 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.
     CommonUtils.nextTick(() => {
       this.status.ready = true;
 
@@ -411,72 +413,80 @@ Sync11Service.prototype = {
       } catch (ex) {
         this._log.warn("Could not register engine " + name, ex);
       }
     }
 
     this.engineManager.setDeclined(declined);
   },
 
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
-                                         Ci.nsISupportsWeakReference]),
-
-  // nsIObserver
-
-  observe: function observe(subject, topic, data) {
-    switch (topic) {
-      // Ideally this observer should be in the SyncScheduler, but it would require
-      // some work to know about the sync specific engines. We should move this there once it does.
-      case "sync:collection_changed":
-        // We check if we're running TPS here to avoid TPS failing because it
-        // couldn't get to get the sync lock, due to us currently syncing the
-        // clients engine.
-        if (data.includes("clients") && !Svc.Prefs.get("testing.tps", false)) {
-          // Sync in the background (it's fine not to wait on the returned promise
-          // because sync() has a lock).
-          // [] = clients collection only
-          this.sync({why: "collection_changed", engines: []}).catch(e => {
-            this._log.error(e);
-          });
-        }
-        break;
-      case "fxaccounts:device_disconnected":
-        data = JSON.parse(data);
-        if (!data.isLocalDevice) {
-          // Refresh the known stale clients list in the background.
-          this.clientsEngine.updateKnownStaleClients().catch(e => {
-            this._log.error(e);
-          });
-        }
-        break;
-      case "weave:service:setup-complete":
-        let status = this._checkSetup();
-        if (status != STATUS_DISABLED && status != CLIENT_NOT_CONFIGURED)
-            Svc.Obs.notify("weave:engine:start-tracking");
-        break;
-      case "nsPref:changed":
-        if (this._ignorePrefObserver)
-          return;
-        let engine = data.slice((PREFS_BRANCH + "engine.").length);
-        this._handleEngineStatusChanged(engine);
-        break;
+  async observe(subject, topic, data) {
+    try {
+      switch (topic) {
+        // Ideally this observer should be in the SyncScheduler, but it would require
+        // some work to know about the sync specific engines. We should move this there once it does.
+        case "sync:collection_changed":
+          // We check if we're running TPS here to avoid TPS failing because it
+          // couldn't get to get the sync lock, due to us currently syncing the
+          // clients engine.
+          if (data.includes("clients") && !Svc.Prefs.get("testing.tps", false)) {
+            // [] = clients collection only
+            await this.sync({why: "collection_changed", engines: []});
+          }
+          break;
+        case "fxaccounts:device_disconnected":
+          data = JSON.parse(data);
+          if (!data.isLocalDevice) {
+            await this.clientsEngine.updateKnownStaleClients();
+          }
+          break;
+        case "weave:service:setup-complete":
+          let status = this._checkSetup();
+          if (status != STATUS_DISABLED && status != CLIENT_NOT_CONFIGURED) {
+            this._startTracking();
+          }
+          break;
+      }
+    } catch (e) {
+      this._log.error(e);
     }
   },
 
-  _handleEngineStatusChanged: function handleEngineDisabled(engine) {
+  prefObserver(subject, topic, data) {
+    if (this._ignorePrefObserver) {
+      return;
+    }
+    const engine = data.slice((PREFS_BRANCH + "engine.").length);
     this._log.trace("Status for " + engine + " engine changed.");
     if (Svc.Prefs.get("engineStatusChanged." + engine, false)) {
       // The enabled status being changed back to what it was before.
       Svc.Prefs.reset("engineStatusChanged." + engine);
     } else {
       // Remember that the engine status changed locally until the next sync.
       Svc.Prefs.set("engineStatusChanged." + engine, true);
     }
   },
 
+  async _startTracking() {
+    const engines = this.engineManager.getAll();
+    for (let engine of engines) {
+      engine.startTracking();
+    }
+    // This is for TPS. We should try to do better.
+    Svc.Obs.notify("weave:service:tracking-started");
+  },
+
+  async _stopTracking() {
+    const engines = this.engineManager.getAll();
+    for (let engine of engines) {
+      await engine.stopTracking();
+    }
+    Svc.Obs.notify("weave:service:tracking-stopped");
+  },
+
   /**
    * Obtain a Resource instance with authentication credentials.
    */
   resource: function resource(url) {
     let res = new Resource(url);
     res.authenticator = this.identity.getResourceAuthenticator();
 
     return res;
@@ -773,17 +783,17 @@ Sync11Service.prototype = {
                                                    cryptoKeys, true);
     if (keysChanged) {
       this._log.info("Downloaded keys differed, as expected.");
     }
   },
 
   async startOver() {
     this._log.trace("Invoking Service.startOver.");
-    Svc.Obs.notify("weave:engine:stop-tracking");
+    await this._stopTracking();
     this.status.resetSync();
 
     // Deletion doesn't make sense if we aren't set up yet!
     if (this.clusterURL != "") {
       // Clear client-specific data from the server, including disabled engines.
       for (let engine of [this.clientsEngine].concat(this.engineManager.getAll())) {
         try {
           await engine.removeClientData();
@@ -1084,16 +1094,17 @@ Sync11Service.prototype = {
 
     return reason;
   },
 
   async sync({engines, why} = {}) {
     let dateStr = Utils.formatTimestamp(new Date());
     this._log.debug("User-Agent: " + Utils.userAgent);
     await this.promiseInitialized;
+    await this.asyncObserver.promiseObserversComplete();
     this._log.info(`Starting sync at ${dateStr} in browser session ${browserSessionID}`);
     return this._catch(async function() {
       // Make sure we're logged in.
       if (this._shouldLogin()) {
         this._log.debug("In sync: should login.");
         if (!(await this.login())) {
           this._log.debug("Not syncing: login returned false.");
           return;
--- a/services/sync/tests/unit/head_helpers.js
+++ b/services/sync/tests/unit/head_helpers.js
@@ -143,42 +143,57 @@ async function installAddonFromInstall(i
   return install.addon;
 }
 
 /**
  * Convenience function to install an add-on from the extensions unit tests.
  *
  * @param  name
  *         String name of add-on to install. e.g. test_install1
+ * @param  reconciler
+ *         addons reconciler, if passed we will wait on the events to be
+ *         processed before resolving
  * @return addon object that was installed
  */
-async function installAddon(name) {
+async function installAddon(name, reconciler = null) {
   let install = await getAddonInstall(name);
   Assert.notEqual(null, install);
-  return installAddonFromInstall(install);
+  const addon = await installAddonFromInstall(install);
+  if (reconciler) {
+    await reconciler.queueCaller.promiseCallsComplete();
+  }
+  return addon;
 }
 
 /**
  * Convenience function to uninstall an add-on.
  *
  * @param addon
  *        Addon instance to uninstall
+ * @param reconciler
+ *        addons reconciler, if passed we will wait on the events to be
+ *        processed before resolving
  */
-function uninstallAddon(addon) {
-  return new Promise(res => {
-    let listener = {onUninstalled(uninstalled) {
-      if (uninstalled.id == addon.id) {
-        AddonManager.removeAddonListener(listener);
-        res(uninstalled);
+async function uninstallAddon(addon, reconciler = null) {
+  const uninstallPromise = new Promise(res => {
+    let listener = {
+      onUninstalled(uninstalled) {
+        if (uninstalled.id == addon.id) {
+          AddonManager.removeAddonListener(listener);
+          res(uninstalled);
+        }
       }
-    }};
-
+    };
     AddonManager.addAddonListener(listener);
-    addon.uninstall();
   });
+  addon.uninstall();
+  await uninstallPromise;
+  if (reconciler) {
+    await reconciler.queueCaller.promiseCallsComplete();
+  }
 }
 
 async function generateNewKeys(collectionKeys, collections = null) {
   let wbo = await collectionKeys.generateNewKeysWBO(collections);
   let modified = new_timestamp();
   collectionKeys.setContents(wbo.cleartext, modified);
 }
 
--- a/services/sync/tests/unit/test_412.js
+++ b/services/sync/tests/unit/test_412.js
@@ -22,17 +22,17 @@ add_task(async function test_412_not_tre
   try {
     // Do sync.
     _("initial sync to initialize the world");
     await Service.sync();
 
     // create a new record that should be uploaded and arrange for our lastSync
     // timestamp to be wrong so we get a 412.
     engine._store.items = {new: "new record"};
-    engine._tracker.addChangedID("new", 0);
+    await engine._tracker.addChangedID("new", 0);
 
     let saw412 = false;
     let _uploadOutgoing = engine._uploadOutgoing;
     engine._uploadOutgoing = async () => {
       engine.lastSync -= 2;
       try {
         await _uploadOutgoing.call(engine);
       } catch (ex) {
--- a/services/sync/tests/unit/test_addons_engine.js
+++ b/services/sync/tests/unit/test_addons_engine.js
@@ -22,17 +22,17 @@ let reconciler;
 let tracker;
 
 async function resetReconciler() {
   reconciler._addons = {};
   reconciler._changes = [];
 
   await reconciler.saveState();
 
-  tracker.clearChangedIDs();
+  await tracker.clearChangedIDs();
 }
 
 add_task(async function setup() {
   loadAddonTestFunctions();
   startupManager();
 
   await Service.engineManager.register(AddonsEngine);
   engine = Service.engineManager.get("addons");
@@ -63,72 +63,72 @@ add_task(async function test_addon_insta
 
 add_task(async function test_find_dupe() {
   _("Ensure the _findDupe() implementation is sane.");
 
   // This gets invoked at the top of sync, which is bypassed by this
   // test, so we do it manually.
   await engine._refreshReconcilerState();
 
-  let addon = await installAddon("test_bootstrap1_1");
+  let addon = await installAddon("test_bootstrap1_1", reconciler);
 
   let record = {
     id:            Utils.makeGUID(),
     addonID:       addon.id,
     enabled:       true,
     applicationID: Services.appinfo.ID,
     source:        "amo"
   };
 
   let dupe = await engine._findDupe(record);
   Assert.equal(addon.syncGUID, dupe);
 
   record.id = addon.syncGUID;
   dupe = await engine._findDupe(record);
   Assert.equal(null, dupe);
 
-  await uninstallAddon(addon);
+  await uninstallAddon(addon, reconciler);
   await resetReconciler();
 });
 
 add_task(async function test_get_changed_ids() {
   _("Ensure getChangedIDs() has the appropriate behavior.");
 
   _("Ensure getChangedIDs() returns an empty object by default.");
   let changes = await engine.getChangedIDs();
   Assert.equal("object", typeof(changes));
   Assert.equal(0, Object.keys(changes).length);
 
   _("Ensure tracker changes are populated.");
   let now = new Date();
   let changeTime = now.getTime() / 1000;
   let guid1 = Utils.makeGUID();
-  tracker.addChangedID(guid1, changeTime);
+  await tracker.addChangedID(guid1, changeTime);
 
   changes = await engine.getChangedIDs();
   Assert.equal("object", typeof(changes));
   Assert.equal(1, Object.keys(changes).length);
   Assert.ok(guid1 in changes);
   Assert.equal(changeTime, changes[guid1]);
 
-  tracker.clearChangedIDs();
+  await tracker.clearChangedIDs();
 
   _("Ensure reconciler changes are populated.");
-  let addon = await installAddon("test_bootstrap1_1");
-  tracker.clearChangedIDs(); // Just in case.
+  let addon = await installAddon("test_bootstrap1_1", reconciler);
+  await tracker.clearChangedIDs(); // Just in case.
   changes = await engine.getChangedIDs();
   Assert.equal("object", typeof(changes));
   Assert.equal(1, Object.keys(changes).length);
   Assert.ok(addon.syncGUID in changes);
   _("Change time: " + changeTime + ", addon change: " + changes[addon.syncGUID]);
   Assert.ok(changes[addon.syncGUID] >= changeTime);
 
   let oldTime = changes[addon.syncGUID];
   let guid2 = addon.syncGUID;
-  await uninstallAddon(addon);
+  await uninstallAddon(addon, reconciler);
   changes = await engine.getChangedIDs();
   Assert.equal(1, Object.keys(changes).length);
   Assert.ok(guid2 in changes);
   Assert.ok(changes[guid2] > oldTime);
 
   _("Ensure non-syncable add-ons aren't picked up by reconciler changes.");
   reconciler._addons  = {};
   reconciler._changes = [];
--- a/services/sync/tests/unit/test_addons_reconciler.js
+++ b/services/sync/tests/unit/test_addons_reconciler.js
@@ -1,58 +1,65 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
+ChromeUtils.import("resource://services-common/async.js");
 ChromeUtils.import("resource://services-sync/addonsreconciler.js");
 ChromeUtils.import("resource://services-sync/engines/addons.js");
 ChromeUtils.import("resource://services-sync/service.js");
 ChromeUtils.import("resource://services-sync/util.js");
 
 loadAddonTestFunctions();
 startupManager();
 
-add_task(async function run_test() {
+function makeAddonsReconciler() {
+  const log = Service.engineManager.get("addons")._log;
+  const queueCaller = Async.asyncQueueCaller(log);
+  return new AddonsReconciler(queueCaller);
+}
+
+add_task(async function setup() {
   Svc.Prefs.set("engine.addons", true);
   await Service.engineManager.register(AddonsEngine);
 });
 
 add_task(async function test_defaults() {
   _("Ensure new objects have reasonable defaults.");
 
-  let reconciler = new AddonsReconciler();
+  let reconciler = makeAddonsReconciler();
   await reconciler.ensureStateLoaded();
 
   Assert.ok(!reconciler._listening);
   Assert.equal("object", typeof(reconciler.addons));
   Assert.equal(0, Object.keys(reconciler.addons).length);
   Assert.equal(0, reconciler._changes.length);
   Assert.equal(0, reconciler._listeners.length);
 });
 
 add_task(async function test_load_state_empty_file() {
   _("Ensure loading from a missing file results in defaults being set.");
 
-  let reconciler = new AddonsReconciler();
+  let reconciler = makeAddonsReconciler();
   await reconciler.ensureStateLoaded();
 
   let loaded = await reconciler.loadState();
   Assert.ok(!loaded);
 
   Assert.equal("object", typeof(reconciler.addons));
   Assert.equal(0, Object.keys(reconciler.addons).length);
   Assert.equal(0, reconciler._changes.length);
 });
 
 add_task(async function test_install_detection() {
   _("Ensure that add-on installation results in appropriate side-effects.");
 
-  let reconciler = new AddonsReconciler();
+  let reconciler = makeAddonsReconciler();
   await reconciler.ensureStateLoaded();
   reconciler.startListening();
 
   let before = new Date();
   let addon = await installAddon("test_bootstrap1_1");
   let after = new Date();
 
   Assert.equal(1, Object.keys(reconciler.addons).length);
@@ -81,28 +88,28 @@ add_task(async function test_install_det
   Assert.equal(addon.id, change[2]);
 
   await uninstallAddon(addon);
 });
 
 add_task(async function test_uninstall_detection() {
   _("Ensure that add-on uninstallation results in appropriate side-effects.");
 
-  let reconciler = new AddonsReconciler();
+  let reconciler = makeAddonsReconciler();
   await reconciler.ensureStateLoaded();
   reconciler.startListening();
 
   reconciler._addons = {};
   reconciler._changes = [];
 
   let addon = await installAddon("test_bootstrap1_1");
   let id = addon.id;
 
   reconciler._changes = [];
-  await uninstallAddon(addon);
+  await uninstallAddon(addon, reconciler);
 
   Assert.equal(1, Object.keys(reconciler.addons).length);
   Assert.ok(id in reconciler.addons);
 
   let record = reconciler.addons[id];
   Assert.ok(!record.installed);
 
   Assert.equal(1, reconciler._changes.length);
@@ -111,17 +118,17 @@ add_task(async function test_uninstall_d
   Assert.equal(id, change[2]);
 });
 
 add_task(async function test_load_state_future_version() {
   _("Ensure loading a file from a future version results in no data loaded.");
 
   const FILENAME = "TEST_LOAD_STATE_FUTURE_VERSION";
 
-  let reconciler = new AddonsReconciler();
+  let reconciler = makeAddonsReconciler();
   await reconciler.ensureStateLoaded();
 
   // First we populate our new file.
   let state = {version: 100, addons: {foo: {}}, changes: [[1, 1, "foo"]]};
 
   // jsonSave() expects an object with ._log, so we give it a reconciler
   // instance.
   await Utils.jsonSave(FILENAME, reconciler, state);
@@ -132,17 +139,17 @@ add_task(async function test_load_state_
   Assert.equal("object", typeof(reconciler.addons));
   Assert.equal(0, Object.keys(reconciler.addons).length);
   Assert.equal(0, reconciler._changes.length);
 });
 
 add_task(async function test_prune_changes_before_date() {
   _("Ensure that old changes are pruned properly.");
 
-  let reconciler = new AddonsReconciler();
+  let reconciler = makeAddonsReconciler();
   await reconciler.ensureStateLoaded();
   reconciler._changes = [];
 
   let now = new Date();
   const HOUR_MS = 1000 * 60 * 60;
 
   _("Ensure pruning an empty changes array works.");
   reconciler.pruneChangesBeforeDate(now);
--- a/services/sync/tests/unit/test_addons_store.js
+++ b/services/sync/tests/unit/test_addons_store.js
@@ -114,30 +114,30 @@ add_task(async function setup() {
   // Don't flush to disk in the middle of an event listener!
   // This causes test hangs on WinXP.
   reconciler._shouldPersist = false;
 });
 
 add_task(async function test_remove() {
   _("Ensure removing add-ons from deleted records works.");
 
-  let addon = await installAddon("test_bootstrap1_1");
+  let addon = await installAddon("test_bootstrap1_1", reconciler);
   let record = createRecordForThisApp(addon.syncGUID, addon.id, true, true);
 
   let failed = await store.applyIncomingBatch([record]);
   Assert.equal(0, failed.length);
 
   let newAddon = await AddonManager.getAddonByID(addon.id);
   Assert.equal(null, newAddon);
 });
 
 add_task(async function test_apply_enabled() {
   _("Ensures that changes to the userEnabled flag apply.");
 
-  let addon = await installAddon("test_bootstrap1_1");
+  let addon = await installAddon("test_bootstrap1_1", reconciler);
   Assert.ok(addon.isActive);
   Assert.ok(!addon.userDisabled);
 
   _("Ensure application of a disable record works as expected.");
   let records = [];
   records.push(createRecordForThisApp(addon.syncGUID, addon.id, false, false));
   let failed = await store.applyIncomingBatch(records);
   Assert.equal(0, failed.length);
@@ -159,17 +159,17 @@ add_task(async function test_apply_enabl
   records.push(createRecordForThisApp(addon.syncGUID, addon.id, false, false));
   Svc.Prefs.set("addons.ignoreUserEnabledChanges", true);
   failed = await store.applyIncomingBatch(records);
   Assert.equal(0, failed.length);
   addon = await AddonManager.getAddonByID(addon.id);
   Assert.ok(!addon.userDisabled);
   records = [];
 
-  await uninstallAddon(addon);
+  await uninstallAddon(addon, reconciler);
   Svc.Prefs.reset("addons.ignoreUserEnabledChanges");
 });
 
 add_task(async function test_apply_enabled_appDisabled() {
   _("Ensures that changes to the userEnabled flag apply when the addon is appDisabled.");
 
   let addon = await installAddon("test_install3"); // this addon is appDisabled by default.
   Assert.ok(addon.appDisabled);
@@ -192,59 +192,59 @@ add_task(async function test_apply_enabl
   records.push(createRecordForThisApp(addon.syncGUID, addon.id, true, false));
   failed = await store.applyIncomingBatch(records);
   Assert.equal(0, failed.length);
   addon = await AddonManager.getAddonByID(addon.id);
   Assert.ok(!addon.userDisabled);
   await checkReconcilerUpToDate(addon);
   records = [];
 
-  await uninstallAddon(addon);
+  await uninstallAddon(addon, reconciler);
 });
 
 add_task(async function test_ignore_different_appid() {
   _("Ensure that incoming records with a different application ID are ignored.");
 
   // We test by creating a record that should result in an update.
-  let addon = await installAddon("test_bootstrap1_1");
+  let addon = await installAddon("test_bootstrap1_1", reconciler);
   Assert.ok(!addon.userDisabled);
 
   let record = createRecordForThisApp(addon.syncGUID, addon.id, false, false);
   record.applicationID = "FAKE_ID";
 
   let failed = await store.applyIncomingBatch([record]);
   Assert.equal(0, failed.length);
 
   let newAddon = await AddonManager.getAddonByID(addon.id);
   Assert.ok(!newAddon.userDisabled);
 
-  await uninstallAddon(addon);
+  await uninstallAddon(addon, reconciler);
 });
 
 add_task(async function test_ignore_unknown_source() {
   _("Ensure incoming records with unknown source are ignored.");
 
-  let addon = await installAddon("test_bootstrap1_1");
+  let addon = await installAddon("test_bootstrap1_1", reconciler);
 
   let record = createRecordForThisApp(addon.syncGUID, addon.id, false, false);
   record.source = "DUMMY_SOURCE";
 
   let failed = await store.applyIncomingBatch([record]);
   Assert.equal(0, failed.length);
 
   let newAddon = await AddonManager.getAddonByID(addon.id);
   Assert.ok(!newAddon.userDisabled);
 
-  await uninstallAddon(addon);
+  await uninstallAddon(addon, reconciler);
 });
 
 add_task(async function test_apply_uninstall() {
   _("Ensures that uninstalling an add-on from a record works.");
 
-  let addon = await installAddon("test_bootstrap1_1");
+  let addon = await installAddon("test_bootstrap1_1", reconciler);
 
   let records = [];
   records.push(createRecordForThisApp(addon.syncGUID, addon.id, true, true));
   let failed = await store.applyIncomingBatch(records);
   Assert.equal(0, failed.length);
 
   addon = await AddonManager.getAddonByID(addon.id);
   Assert.equal(null, addon);
@@ -253,17 +253,17 @@ add_task(async function test_apply_unins
 add_task(async function test_addon_syncability() {
   _("Ensure isAddonSyncable functions properly.");
 
   Svc.Prefs.set("addons.trustedSourceHostnames",
                 "addons.mozilla.org,other.example.com");
 
   Assert.ok(!(await store.isAddonSyncable(null)));
 
-  let addon = await installAddon("test_bootstrap1_1");
+  let addon = await installAddon("test_bootstrap1_1", reconciler);
   Assert.ok((await store.isAddonSyncable(addon)));
 
   let dummy = {};
   const KEYS = ["id", "syncGUID", "type", "scope", "foreignInstall", "isSyncable"];
   for (let k of KEYS) {
     dummy[k] = addon[k];
   }
 
@@ -280,17 +280,17 @@ add_task(async function test_addon_synca
   dummy.isSyncable = false;
   Assert.ok(!(await store.isAddonSyncable(dummy)));
   dummy.isSyncable = addon.isSyncable;
 
   dummy.foreignInstall = true;
   Assert.ok(!(await store.isAddonSyncable(dummy)));
   dummy.foreignInstall = false;
 
-  await uninstallAddon(addon);
+  await uninstallAddon(addon, reconciler);
 
   Assert.ok(!store.isSourceURITrusted(null));
 
   let trusted = [
     "https://addons.mozilla.org/foo",
     "https://other.example.com/foo"
   ];
 
@@ -324,76 +324,76 @@ add_task(async function test_get_all_ids
 
   _("Installing two addons.");
   // XXX - this test seems broken - at this point, before we've installed the
   // addons below, store.getAllIDs() returns all addons installed by previous
   // tests, even though those tests uninstalled the addon.
   // So if any tests above ever add a new addon ID, they are going to need to
   // be added here too.
   // Assert.equal(0, Object.keys(store.getAllIDs()).length);
-  let addon1 = await installAddon("test_install1");
-  let addon2 = await installAddon("test_bootstrap1_1");
-  let addon3 = await installAddon("test_install3");
+  let addon1 = await installAddon("test_install1", reconciler);
+  let addon2 = await installAddon("test_bootstrap1_1", reconciler);
+  let addon3 = await installAddon("test_install3", reconciler);
 
   _("Ensure they're syncable.");
   Assert.ok((await store.isAddonSyncable(addon1)));
   Assert.ok((await store.isAddonSyncable(addon2)));
   Assert.ok((await store.isAddonSyncable(addon3)));
 
   let ids = await store.getAllIDs();
 
   Assert.equal("object", typeof(ids));
   Assert.equal(3, Object.keys(ids).length);
   Assert.ok(addon1.syncGUID in ids);
   Assert.ok(addon2.syncGUID in ids);
   Assert.ok(addon3.syncGUID in ids);
 
   addon1.install.cancel();
-  await uninstallAddon(addon2);
-  await uninstallAddon(addon3);
+  await uninstallAddon(addon2, reconciler);
+  await uninstallAddon(addon3, reconciler);
 });
 
 add_task(async function test_change_item_id() {
   _("Ensures that changeItemID() works properly.");
 
-  let addon = await installAddon("test_bootstrap1_1");
+  let addon = await installAddon("test_bootstrap1_1", reconciler);
 
   let oldID = addon.syncGUID;
   let newID = Utils.makeGUID();
 
   await store.changeItemID(oldID, newID);
 
   let newAddon = await AddonManager.getAddonByID(addon.id);
   Assert.notEqual(null, newAddon);
   Assert.equal(newID, newAddon.syncGUID);
 
-  await uninstallAddon(newAddon);
+  await uninstallAddon(newAddon, reconciler);
 });
 
 add_task(async function test_create() {
   _("Ensure creating/installing an add-on from a record works.");
 
   let server = createAndStartHTTPServer(HTTP_PORT);
 
-  let addon = await installAddon("test_bootstrap1_1");
+  let addon = await installAddon("test_bootstrap1_1", reconciler);
   let id = addon.id;
-  await uninstallAddon(addon);
+  await uninstallAddon(addon, reconciler);
 
   let guid = Utils.makeGUID();
   let record = createRecordForThisApp(guid, id, true, false);
 
   let failed = await store.applyIncomingBatch([record]);
   Assert.equal(0, failed.length);
 
   let newAddon = await AddonManager.getAddonByID(id);
   Assert.notEqual(null, newAddon);
   Assert.equal(guid, newAddon.syncGUID);
   Assert.ok(!newAddon.userDisabled);
 
-  await uninstallAddon(newAddon);
+  await uninstallAddon(newAddon, reconciler);
 
   await promiseStopServer(server);
 });
 
 add_task(async function test_create_missing_search() {
   _("Ensures that failed add-on searches are handled gracefully.");
 
   let server = createAndStartHTTPServer(HTTP_PORT);
@@ -481,31 +481,31 @@ add_task(async function test_incoming_sy
   Assert.ok(!(await AddonManager.getAddonByID(SYSTEM_ADDON_ID).userDisabled));
 
   await promiseStopServer(server);
 });
 
 add_task(async function test_wipe() {
   _("Ensures that wiping causes add-ons to be uninstalled.");
 
-  let addon1 = await installAddon("test_bootstrap1_1");
+  let addon1 = await installAddon("test_bootstrap1_1", reconciler);
 
   await store.wipe();
 
   let addon = await AddonManager.getAddonByID(addon1.id);
   Assert.equal(null, addon);
 });
 
 add_task(async function test_wipe_and_install() {
   _("Ensure wipe followed by install works.");
 
   // This tests the reset sync flow where remote data is replaced by local. The
   // receiving client will see a wipe followed by a record which should undo
   // the wipe.
-  let installed = await installAddon("test_bootstrap1_1");
+  let installed = await installAddon("test_bootstrap1_1", reconciler);
 
   let record = createRecordForThisApp(installed.syncGUID, installed.id, true,
                                       false);
 
   await store.wipe();
 
   let deleted = await AddonManager.getAddonByID(installed.id);
   Assert.equal(null, deleted);
--- a/services/sync/tests/unit/test_addons_tracker.js
+++ b/services/sync/tests/unit/test_addons_tracker.js
@@ -16,21 +16,20 @@ Svc.Prefs.set("engine.addons", true);
 let engine;
 let reconciler;
 let store;
 let tracker;
 
 const addon1ID = "addon1@tests.mozilla.org";
 
 async function cleanup() {
-  Svc.Obs.notify("weave:engine:stop-tracking");
-  tracker.stopTracking();
+  tracker.stop();
 
   tracker.resetScore();
-  tracker.clearChangedIDs();
+  await tracker.clearChangedIDs();
 
   reconciler._addons = {};
   reconciler._changes = [];
   await reconciler.saveState();
 }
 
 add_task(async function setup() {
   await Service.engineManager.register(AddonsEngine);
@@ -43,84 +42,84 @@ add_task(async function setup() {
   tracker.persistChangedIDs = false;
 
   await cleanup();
 });
 
 add_task(async function test_empty() {
   _("Verify the tracker is empty to start with.");
 
-  Assert.equal(0, Object.keys(tracker.changedIDs).length);
+  Assert.equal(0, Object.keys((await tracker.getChangedIDs())).length);
   Assert.equal(0, tracker.score);
 
   await cleanup();
 });
 
 add_task(async function test_not_tracking() {
   _("Ensures the tracker doesn't do anything when it isn't tracking.");
 
-  let addon = await installAddon("test_bootstrap1_1");
-  await uninstallAddon(addon);
+  let addon = await installAddon("test_bootstrap1_1", reconciler);
+  await uninstallAddon(addon, reconciler);
 
-  Assert.equal(0, Object.keys(tracker.changedIDs).length);
+  Assert.equal(0, Object.keys((await tracker.getChangedIDs())).length);
   Assert.equal(0, tracker.score);
 
   await cleanup();
 });
 
 add_task(async function test_track_install() {
   _("Ensure that installing an add-on notifies tracker.");
 
   reconciler.startListening();
 
-  Svc.Obs.notify("weave:engine:start-tracking");
+  tracker.start();
 
   Assert.equal(0, tracker.score);
-  let addon = await installAddon("test_bootstrap1_1");
-  let changed = tracker.changedIDs;
+  let addon = await installAddon("test_bootstrap1_1", reconciler);
+  let changed = await tracker.getChangedIDs();
 
   Assert.equal(1, Object.keys(changed).length);
   Assert.ok(addon.syncGUID in changed);
   Assert.equal(SCORE_INCREMENT_XLARGE, tracker.score);
 
-  await uninstallAddon(addon);
+  await uninstallAddon(addon, reconciler);
   await cleanup();
 });
 
 add_task(async function test_track_uninstall() {
   _("Ensure that uninstalling an add-on notifies tracker.");
 
   reconciler.startListening();
 
-  let addon = await installAddon("test_bootstrap1_1");
+  let addon = await installAddon("test_bootstrap1_1", reconciler);
   let guid = addon.syncGUID;
   Assert.equal(0, tracker.score);
 
-  Svc.Obs.notify("weave:engine:start-tracking");
+  tracker.start();
 
-  await uninstallAddon(addon);
-  let changed = tracker.changedIDs;
+  await uninstallAddon(addon, reconciler);
+  let changed = await tracker.getChangedIDs();
   Assert.equal(1, Object.keys(changed).length);
   Assert.ok(guid in changed);
   Assert.equal(SCORE_INCREMENT_XLARGE, tracker.score);
 
   await cleanup();
 });
 
 add_task(async function test_track_user_disable() {
   _("Ensure that tracker sees disabling of add-on");
 
   reconciler.startListening();
 
-  let addon = await installAddon("test_bootstrap1_1");
+  let addon = await installAddon("test_bootstrap1_1", reconciler);
   Assert.ok(!addon.userDisabled);
   Assert.ok(!addon.appDisabled);
   Assert.ok(addon.isActive);
 
-  Svc.Obs.notify("weave:engine:start-tracking");
+  tracker.start();
   Assert.equal(0, tracker.score);
 
   let disabledPromise = new Promise(res => {
     let listener = {
       onDisabled(disabled) {
         _("onDisabled");
         if (disabled.id == addon.id) {
           AddonManager.removeAddonListener(listener);
@@ -133,41 +132,43 @@ add_task(async function test_track_user_
     };
     AddonManager.addAddonListener(listener);
   });
 
   _("Disabling add-on");
   addon.userDisabled = true;
   _("Disabling started...");
   await disabledPromise;
+  await reconciler.queueCaller.promiseCallsComplete();
 
-  let changed = tracker.changedIDs;
+  let changed = await tracker.getChangedIDs();
   Assert.equal(1, Object.keys(changed).length);
   Assert.ok(addon.syncGUID in changed);
   Assert.equal(SCORE_INCREMENT_XLARGE, tracker.score);
 
-  await uninstallAddon(addon);
+  await uninstallAddon(addon, reconciler);
   await cleanup();
 });
 
 add_task(async function test_track_enable() {
   _("Ensure that enabling a disabled add-on notifies tracker.");
 
   reconciler.startListening();
 
-  let addon = await installAddon("test_bootstrap1_1");
+  let addon = await installAddon("test_bootstrap1_1", reconciler);
   addon.userDisabled = true;
   await Async.promiseYield();
 
   Assert.equal(0, tracker.score);
 
-  Svc.Obs.notify("weave:engine:start-tracking");
+  tracker.start();
   addon.userDisabled = false;
   await Async.promiseYield();
+  await reconciler.queueCaller.promiseCallsComplete();
 
-  let changed = tracker.changedIDs;
+  let changed = await tracker.getChangedIDs();
   Assert.equal(1, Object.keys(changed).length);
   Assert.ok(addon.syncGUID in changed);
   Assert.equal(SCORE_INCREMENT_XLARGE, tracker.score);
 
-  await uninstallAddon(addon);
+  await uninstallAddon(addon, reconciler);
   await cleanup();
 });
--- a/services/sync/tests/unit/test_bookmark_duping.js
+++ b/services/sync/tests/unit/test_bookmark_duping.js
@@ -18,23 +18,23 @@ async function sharedSetup() {
   let engine = new BookmarksEngine(Service);
   await engine.initialize();
   let store = engine._store;
   let server = await serverForFoo(engine);
   await SyncTestingInfrastructure(server);
 
   let collection = server.user("foo").collection("bookmarks");
 
-  Svc.Obs.notify("weave:engine:start-tracking"); // We skip usual startup...
+  engine._tracker.start(); // We skip usual startup...
 
   return { engine, store, server, collection };
 }
 
 async function cleanup(engine, server) {
-  Svc.Obs.notify("weave:engine:stop-tracking");
+  await engine._tracker.stop();
   let promiseStartOver = promiseOneObserver("weave:service:start-over:finish");
   await Service.startOver();
   await promiseStartOver;
   await promiseStopServer(server);
   await bms.eraseEverything();
   await engine.resetClient();
   await engine.finalize();
 }
--- a/services/sync/tests/unit/test_bookmark_engine.js
+++ b/services/sync/tests/unit/test_bookmark_engine.js
@@ -64,17 +64,17 @@ add_bookmark_test(async function test_de
   _("Ensure that we delete the Places and Reading List roots from the server.");
 
   let store   = engine._store;
   let server = await serverForFoo(engine);
   await SyncTestingInfrastructure(server);
 
   let collection = server.user("foo").collection("bookmarks");
 
-  Svc.Obs.notify("weave:engine:start-tracking");
+  engine._tracker.start();
 
   try {
     let placesRecord = await store.createRecord("places");
     collection.insert("places", encryptPayload(placesRecord.cleartext));
 
     let listBmk = new Bookmark("bookmarks", Utils.makeGUID());
     listBmk.bmkUri = "https://example.com";
     listBmk.title = "Example reading list entry";
@@ -111,17 +111,17 @@ add_bookmark_test(async function test_de
 
     deepEqual(collection.keys().sort(), ["menu", "mobile", "toolbar", "unfiled", newBmk.id].sort(),
       "Should remove Places root and reading list items from server; upload local roots");
   } finally {
     await store.wipe();
     Svc.Prefs.resetBranch("");
     Service.recordManager.clearCache();
     await promiseStopServer(server);
-    Svc.Obs.notify("weave:engine:stop-tracking");
+    await engine._tracker.stop();
   }
 });
 
 add_task(async function bad_record_allIDs() {
   let server = new SyncServer();
   server.start();
   await SyncTestingInfrastructure(server);
 
@@ -249,17 +249,17 @@ async function test_restoreOrImport(engi
   _(`Ensure that ${verbing} from a backup will reupload all records.`);
 
   let store  = engine._store;
   let server = await serverForFoo(engine);
   await SyncTestingInfrastructure(server);
 
   let collection = server.user("foo").collection("bookmarks");
 
-  Svc.Obs.notify("weave:engine:start-tracking"); // We skip usual startup...
+  engine._tracker.start(); // We skip usual startup...
 
   try {
 
     let folder1 = await PlacesUtils.bookmarks.insert({
       parentGuid: PlacesUtils.bookmarks.toolbarGuid,
       type: PlacesUtils.bookmarks.TYPE_FOLDER,
       title: "Folder 1",
     });
@@ -303,16 +303,17 @@ async function test_restoreOrImport(engi
     let wbos = collection.keys(function(id) {
       return ["menu", "toolbar", "mobile", "unfiled", folder1.guid].indexOf(id) == -1;
     });
     Assert.equal(wbos.length, 1);
     Assert.equal(wbos[0], bmk2.guid);
 
     _(`Now ${verb} from a backup.`);
     await bookmarkUtils.importFromFile(backupFilePath, replace);
+    await engine._tracker.asyncObserver.promiseObserversComplete();
 
     let bookmarksCollection = server.user("foo").collection("bookmarks");
     if (replace) {
       _("Verify that we wiped the server.");
       Assert.ok(!bookmarksCollection);
     } else {
       _("Verify that we didn't wipe the server.");
       Assert.ok(!!bookmarksCollection);
@@ -695,17 +696,17 @@ add_bookmark_test(async function test_sy
   await SyncTestingInfrastructure(server);
 
   let collection = server.user("foo").collection("bookmarks");
 
   // TODO: Avoid random orange (bug 1374599), this is only necessary
   // intermittently - reset the last sync date so that we'll get all bookmarks.
   await engine.setLastSync(1);
 
-  Svc.Obs.notify("weave:engine:start-tracking"); // We skip usual startup...
+  engine._tracker.start(); // We skip usual startup...
 
   // Just matters that it's in the past, not how far.
   let now = Date.now();
   let oneYearMS = 365 * 24 * 60 * 60 * 1000;
 
   try {
     let item1GUID = "abcdefabcdef";
     let item1 = new Bookmark("bookmarks", item1GUID);
@@ -844,17 +845,17 @@ add_task(async function test_sync_imap_U
   let engine = new BookmarksEngine(Service);
   await engine.initialize();
   let store  = engine._store;
   let server = await serverForFoo(engine);
   await SyncTestingInfrastructure(server);
 
   let collection = server.user("foo").collection("bookmarks");
 
-  Svc.Obs.notify("weave:engine:start-tracking"); // We skip usual startup...
+  engine._tracker.start(); // We skip usual startup...
 
   try {
     collection.insert("menu", encryptPayload({
       id: "menu",
       type: "folder",
       parentid: "places",
       title: "Bookmarks Menu",
       children: ["bookmarkAAAA"],
--- a/services/sync/tests/unit/test_bookmark_store.js
+++ b/services/sync/tests/unit/test_bookmark_store.js
@@ -332,17 +332,17 @@ add_task(async function test_move_folder
 });
 
 add_task(async function test_move_order() {
   let engine = new BookmarksEngine(Service);
   let store = engine._store;
   let tracker = engine._tracker;
 
   // Make sure the tracker is turned on.
-  Svc.Obs.notify("weave:engine:start-tracking");
+  tracker.start();
   try {
     _("Create two bookmarks");
     let bmk1 = await PlacesUtils.bookmarks.insert({
       parentGuid: PlacesUtils.bookmarks.toolbarGuid,
       url: "http://getfirefox.com/",
       title: "Get Firefox!",
     });
     let bmk2 = await PlacesUtils.bookmarks.insert({
@@ -369,17 +369,17 @@ add_task(async function test_move_order(
     delete store._childrenToOrder;
 
     _("Verify new order.");
     let newChildIds = await PlacesSyncUtils.bookmarks.fetchChildRecordIds(
       "toolbar");
     Assert.deepEqual(newChildIds, [bmk2.guid, bmk1.guid]);
 
   } finally {
-    Svc.Obs.notify("weave:engine:stop-tracking");
+    await tracker.stop();
     _("Clean up.");
     await store.wipe();
     await engine.finalize();
   }
 });
 
 add_task(async function test_orphan() {
   let engine = new BookmarksEngine(Service);
--- a/services/sync/tests/unit/test_bookmark_tracker.js
+++ b/services/sync/tests/unit/test_bookmark_tracker.js
@@ -22,62 +22,58 @@ add_task(async function setup() {
   store  = engine._store;
   tracker = engine._tracker;
   tracker.persistChangedIDs = false;
 });
 
 // Test helpers.
 async function verifyTrackerEmpty() {
   await PlacesTestUtils.promiseAsyncUpdates();
-  let changes = await tracker.promiseChangedIDs();
+  let changes = await tracker.getChangedIDs();
   deepEqual(changes, {});
   equal(tracker.score, 0);
 }
 
 async function resetTracker() {
   await PlacesTestUtils.markBookmarksAsSynced();
   tracker.resetScore();
 }
 
 async function cleanup() {
   engine.lastSync = 0;
   engine._needWeakUpload.clear();
   await store.wipe();
   await resetTracker();
-  await stopTracking();
+  await tracker.stop();
 }
 
 // startTracking is a signal that the test wants to notice things that happen
 // after this is called (ie, things already tracked should be discarded.)
 async function startTracking() {
-  Svc.Obs.notify("weave:engine:start-tracking");
+  engine._tracker.start();
   await PlacesTestUtils.markBookmarksAsSynced();
 }
 
-async function stopTracking() {
-  Svc.Obs.notify("weave:engine:stop-tracking");
-}
-
 async function verifyTrackedItems(tracked) {
   await PlacesTestUtils.promiseAsyncUpdates();
-  let changedIDs = await tracker.promiseChangedIDs();
+  let changedIDs = await tracker.getChangedIDs();
   let trackedIDs = new Set(Object.keys(changedIDs));
   for (let guid of tracked) {
     ok(guid in changedIDs, `${guid} should be tracked`);
     ok(changedIDs[guid].modified > 0, `${guid} should have a modified time`);
     ok(changedIDs[guid].counter >= -1, `${guid} should have a change counter`);
     trackedIDs.delete(guid);
   }
   equal(trackedIDs.size, 0, `Unhandled tracked IDs: ${
     JSON.stringify(Array.from(trackedIDs))}`);
 }
 
 async function verifyTrackedCount(expected) {
   await PlacesTestUtils.promiseAsyncUpdates();
-  let changedIDs = await tracker.promiseChangedIDs();
+  let changedIDs = await tracker.getChangedIDs();
   do_check_attribute_count(changedIDs, expected);
 }
 
 // A debugging helper that dumps the full bookmarks tree.
 async function dumpBookmarks() {
   let columns = ["id", "title", "guid", "syncStatus", "syncChangeCounter", "position"];
   return PlacesUtils.promiseDBConnection().then(connection => {
     let all = [];
@@ -185,27 +181,27 @@ add_task(async function test_leftPaneFol
     await startTracking();
 
     // Creates the organizer queries as a side effect.
     let leftPaneId = PlacesUIUtils.maybeRebuildLeftPane();
     _(`Left pane root ID: ${leftPaneId}`);
 
     {
       await PlacesTestUtils.promiseAsyncUpdates();
-      let changes = await tracker.promiseChangedIDs();
+      let changes = await tracker.getChangedIDs();
       deepEqual(changes, {}, "New left pane queries should not be tracked");
       Assert.equal(tracker.score, SCORE_INCREMENT_XLARGE);
     }
 
     _("Reset synced bookmarks to simulate a disconnect");
     await PlacesSyncUtils.bookmarks.reset();
 
     {
       await PlacesTestUtils.promiseAsyncUpdates();
-      let changes = await tracker.promiseChangedIDs();
+      let changes = await tracker.getChangedIDs();
       deepEqual(Object.keys(changes).sort(), ["menu", "mobile", "toolbar", "unfiled"],
         "Left pane queries should not be tracked after reset");
       Assert.equal(tracker.score, SCORE_INCREMENT_XLARGE);
       await PlacesTestUtils.markBookmarksAsSynced();
     }
 
     // The following tests corrupt the left pane queries in different ways.
     // `PlacesUIUtils.maybeRebuildLeftPane` will rebuild the entire root, but
@@ -221,45 +217,45 @@ add_task(async function test_leftPaneFol
       let folderId = await PlacesUtils.promiseItemId(folder.guid);
       await setAnnoUnchecked(folderId, PlacesUIUtils.ORGANIZER_FOLDER_ANNO, 0,
                              PlacesUtils.annotations.TYPE_INT32);
 
       leftPaneId = PlacesUIUtils.maybeRebuildLeftPane();
       _(`Left pane root ID after deleting unrelated folder: ${leftPaneId}`);
 
       await PlacesTestUtils.promiseAsyncUpdates();
-      let changes = await tracker.promiseChangedIDs();
+      let changes = await tracker.getChangedIDs();
       deepEqual(changes, {},
         "Should not track left pane items after deleting unrelated folder");
     }
 
     _("Corrupt organizer left pane version");
     {
       await setAnnoUnchecked(leftPaneId, PlacesUIUtils.ORGANIZER_FOLDER_ANNO,
                              -1, PlacesUtils.annotations.TYPE_INT32);
 
       leftPaneId = PlacesUIUtils.maybeRebuildLeftPane();
       _(`Left pane root ID after restoring version: ${leftPaneId}`);
 
       await PlacesTestUtils.promiseAsyncUpdates();
-      let changes = await tracker.promiseChangedIDs();
+      let changes = await tracker.getChangedIDs();
       deepEqual(changes, {},
         "Should not track left pane items after restoring version");
     }
 
     _("Set left pane anno on nonexistent item");
     {
       await setAnnoUnchecked(999, PlacesUIUtils.ORGANIZER_QUERY_ANNO,
                              "Tags", PlacesUtils.annotations.TYPE_STRING);
 
       leftPaneId = PlacesUIUtils.maybeRebuildLeftPane();
       _(`Left pane root ID after detecting nonexistent item: ${leftPaneId}`);
 
       await PlacesTestUtils.promiseAsyncUpdates();
-      let changes = await tracker.promiseChangedIDs();
+      let changes = await tracker.getChangedIDs();
       deepEqual(changes, {},
         "Should not track left pane items after detecting nonexistent item");
     }
 
     _("Move query out of left pane root");
     {
       let queryId = await PlacesUIUtils.leftPaneQueries.Downloads;
       let queryGuid = await PlacesUtils.promiseItemGuid(queryId);
@@ -268,17 +264,17 @@ add_task(async function test_leftPaneFol
         parentGuid: PlacesUtils.bookmarks.rootGuid,
         index: PlacesUtils.bookmarks.DEFAULT_INDEX,
       });
 
       leftPaneId = PlacesUIUtils.maybeRebuildLeftPane();
       _(`Left pane root ID after restoring moved query: ${leftPaneId}`);
 
       await PlacesTestUtils.promiseAsyncUpdates();
-      let changes = await tracker.promiseChangedIDs();
+      let changes = await tracker.getChangedIDs();
       deepEqual(changes, {},
         "Should not track left pane items after restoring moved query");
     }
 
     _("Add duplicate query");
     {
       let leftPaneGuid = await PlacesUtils.promiseItemGuid(leftPaneId);
       let query = await PlacesUtils.bookmarks.insert({
@@ -288,17 +284,17 @@ add_task(async function test_leftPaneFol
       let queryId = await PlacesUtils.promiseItemId(query.guid);
       await setAnnoUnchecked(queryId, PlacesUIUtils.ORGANIZER_QUERY_ANNO,
                              "Tags", PlacesUtils.annotations.TYPE_STRING);
 
       leftPaneId = PlacesUIUtils.maybeRebuildLeftPane();
       _(`Left pane root ID after removing dupe query: ${leftPaneId}`);
 
       await PlacesTestUtils.promiseAsyncUpdates();
-      let changes = await tracker.promiseChangedIDs();
+      let changes = await tracker.getChangedIDs();
       deepEqual(changes, {},
         "Should not track left pane items after removing dupe query");
     }
   } finally {
     _("Clean up.");
     await cleanup();
   }
 });
@@ -520,17 +516,17 @@ add_task(async function test_async_onIte
     await cleanup();
   }
 });
 
 add_task(async function test_async_onItemChanged() {
   _("Items updated using the asynchronous bookmarks API should be tracked");
 
   try {
-    await stopTracking();
+    await tracker.stop();
 
     _("Insert a bookmark");
     let fxBmk = await PlacesUtils.bookmarks.insert({
       type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
       parentGuid: PlacesUtils.bookmarks.menuGuid,
       url: "http://getfirefox.com",
       title: "Get Firefox!",
     });
@@ -555,17 +551,17 @@ add_task(async function test_async_onIte
     await cleanup();
   }
 });
 
 add_task(async function test_onItemChanged_itemDates() {
   _("Changes to item dates should be tracked");
 
   try {
-    await stopTracking();
+    await tracker.stop();
 
     _("Insert a bookmark");
     let fx_id = PlacesUtils.bookmarks.insertBookmark(
       PlacesUtils.bookmarks.bookmarksMenuFolder,
       CommonUtils.makeURI("http://getfirefox.com"),
       PlacesUtils.bookmarks.DEFAULT_INDEX,
       "Get Firefox!");
     let fx_guid = await PlacesUtils.promiseItemGuid(fx_id);
@@ -591,17 +587,17 @@ add_task(async function test_onItemChang
     await cleanup();
   }
 });
 
 add_task(async function test_onItemTagged() {
   _("Items tagged using the synchronous API should be tracked");
 
   try {
-    await stopTracking();
+    await tracker.stop();
 
     _("Create a folder");
     let folder = PlacesUtils.bookmarks.createFolder(
       PlacesUtils.bookmarks.bookmarksMenuFolder, "Parent",
       PlacesUtils.bookmarks.DEFAULT_INDEX);
     let folderGUID = await PlacesUtils.promiseItemGuid(folder);
     _("Folder ID: " + folder);
     _("Folder GUID: " + folderGUID);
@@ -628,17 +624,17 @@ add_task(async function test_onItemTagge
     await cleanup();
   }
 });
 
 add_task(async function test_onItemUntagged() {
   _("Items untagged using the synchronous API should be tracked");
 
   try {
-    await stopTracking();
+    await tracker.stop();
 
     _("Insert tagged bookmarks");
     let uri = CommonUtils.makeURI("http://getfirefox.com");
     let fx1ID = PlacesUtils.bookmarks.insertBookmark(
       PlacesUtils.bookmarks.bookmarksMenuFolder, uri,
       PlacesUtils.bookmarks.DEFAULT_INDEX, "Get Firefox!");
     let fx1GUID = await PlacesUtils.promiseItemGuid(fx1ID);
     // Different parent and title; same URL.
@@ -660,17 +656,17 @@ add_task(async function test_onItemUntag
     await cleanup();
   }
 });
 
 add_task(async function test_async_onItemUntagged() {
   _("Items untagged using the asynchronous API should be tracked");
 
   try {
-    await stopTracking();
+    await tracker.stop();
 
     _("Insert tagged bookmarks");
     let fxBmk1 = await PlacesUtils.bookmarks.insert({
       type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
       parentGuid: PlacesUtils.bookmarks.menuGuid,
       url: "http://getfirefox.com",
       title: "Get Firefox!",
     });
@@ -703,17 +699,17 @@ add_task(async function test_async_onIte
     await cleanup();
   }
 });
 
 add_task(async function test_async_onItemTagged() {
   _("Items tagged using the asynchronous API should be tracked");
 
   try {
-    await stopTracking();
+    await tracker.stop();
 
     _("Insert untagged bookmarks");
     let folder1 = await PlacesUtils.bookmarks.insert({
       type: PlacesUtils.bookmarks.TYPE_FOLDER,
       parentGuid: PlacesUtils.bookmarks.menuGuid,
       title: "Folder 1",
     });
     let fxBmk1 = await PlacesUtils.bookmarks.insert({
@@ -761,17 +757,17 @@ add_task(async function test_async_onIte
     await cleanup();
   }
 });
 
 add_task(async function test_onItemKeywordChanged() {
   _("Keyword changes via the synchronous API should be tracked");
 
   try {
-    await stopTracking();
+    await tracker.stop();
     let folder = PlacesUtils.bookmarks.createFolder(
       PlacesUtils.bookmarks.bookmarksMenuFolder, "Parent",
       PlacesUtils.bookmarks.DEFAULT_INDEX);
     _("Track changes to keywords");
     let uri = CommonUtils.makeURI("http://getfirefox.com");
     let b = PlacesUtils.bookmarks.insertBookmark(
       folder, uri,
       PlacesUtils.bookmarks.DEFAULT_INDEX, "Get Firefox!");
@@ -793,17 +789,17 @@ add_task(async function test_onItemKeywo
     await cleanup();
   }
 });
 
 add_task(async function test_async_onItemKeywordChanged() {
   _("Keyword changes via the asynchronous API should be tracked");
 
   try {
-    await stopTracking();
+    await tracker.stop();
 
     _("Insert two bookmarks with the same URL");
     let fxBmk1 = await PlacesUtils.bookmarks.insert({
       type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
       parentGuid: PlacesUtils.bookmarks.menuGuid,
       url: "http://getfirefox.com",
       title: "Get Firefox!",
     });
@@ -830,17 +826,17 @@ add_task(async function test_async_onIte
     await cleanup();
   }
 });
 
 add_task(async function test_async_onItemKeywordDeleted() {
   _("Keyword deletions via the asynchronous API should be tracked");
 
   try {
-    await stopTracking();
+    await tracker.stop();
 
     _("Insert two bookmarks with the same URL and keywords");
     let fxBmk1 = await PlacesUtils.bookmarks.insert({
       type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
       parentGuid: PlacesUtils.bookmarks.menuGuid,
       url: "http://getfirefox.com",
       title: "Get Firefox!",
     });
@@ -863,50 +859,21 @@ add_task(async function test_async_onIte
     await verifyTrackedItems([fxBmk1.guid, fxBmk2.guid]);
     Assert.equal(tracker.score, SCORE_INCREMENT_XLARGE * 2);
   } finally {
     _("Clean up.");
     await cleanup();
   }
 });
 
-add_task(async function test_onItemPostDataChanged() {
-  _("Post data changes should be tracked");
-
-  try {
-    await stopTracking();
-
-    _("Insert a bookmark");
-    let fx_id = PlacesUtils.bookmarks.insertBookmark(
-      PlacesUtils.bookmarks.bookmarksMenuFolder,
-      CommonUtils.makeURI("http://getfirefox.com"),
-      PlacesUtils.bookmarks.DEFAULT_INDEX,
-      "Get Firefox!");
-    let fx_guid = await PlacesUtils.promiseItemGuid(fx_id);
-    _(`Firefox GUID: ${fx_guid}`);
-
-    await startTracking();
-
-    // PlacesUtils.setPostDataForBookmark is deprecated, but still used by
-    // PlacesTransactions.NewBookmark.
-    _("Post data for the bookmark should be ignored");
-    await PlacesUtils.setPostDataForBookmark(fx_id, "postData");
-    await verifyTrackedItems([]);
-    Assert.equal(tracker.score, 0);
-  } finally {
-    _("Clean up.");
-    await cleanup();
-  }
-});
-
 add_task(async function test_onItemAnnoChanged() {
   _("Item annotations should be tracked");
 
   try {
-    await stopTracking();
+    await tracker.stop();
     let folder = PlacesUtils.bookmarks.createFolder(
       PlacesUtils.bookmarks.bookmarksMenuFolder, "Parent",
       PlacesUtils.bookmarks.DEFAULT_INDEX);
     _("Track changes to annos.");
     let b = PlacesUtils.bookmarks.insertBookmark(
       folder, CommonUtils.makeURI("http://getfirefox.com"),
       PlacesUtils.bookmarks.DEFAULT_INDEX, "Get Firefox!");
     let bGUID = await PlacesUtils.promiseItemGuid(b);
@@ -971,17 +938,17 @@ add_task(async function test_onItemAdded
     await cleanup();
   }
 });
 
 add_task(async function test_onItemDeleted_filtered_root() {
   _("Deleted items outside the change roots should not be tracked");
 
   try {
-    await stopTracking();
+    await tracker.stop();
 
     _("Insert a bookmark underneath the Places root");
     let rootBmkID = PlacesUtils.bookmarks.insertBookmark(
       PlacesUtils.bookmarks.placesRoot,
       CommonUtils.makeURI("http://getfirefox.com"),
       PlacesUtils.bookmarks.DEFAULT_INDEX, "Get Firefox!");
     let rootBmkGUID = await PlacesUtils.promiseItemGuid(rootBmkID);
     _(`New Places root bookmark GUID: ${rootBmkGUID}`);
@@ -998,17 +965,17 @@ add_task(async function test_onItemDelet
     await cleanup();
   }
 });
 
 add_task(async function test_onPageAnnoChanged() {
   _("Page annotations should not be tracked");
 
   try {
-    await stopTracking();
+    await tracker.stop();
 
     _("Insert a bookmark without an annotation");
     let pageURI = CommonUtils.makeURI("http://getfirefox.com");
     PlacesUtils.bookmarks.insertBookmark(
       PlacesUtils.bookmarks.bookmarksMenuFolder,
       pageURI,
       PlacesUtils.bookmarks.DEFAULT_INDEX,
       "Get Firefox!");
@@ -1032,17 +999,17 @@ add_task(async function test_onPageAnnoC
     await cleanup();
   }
 });
 
 add_task(async function test_onFaviconChanged() {
   _("Favicon changes should not be tracked");
 
   try {
-    await stopTracking();
+    await tracker.stop();
 
     let pageURI = CommonUtils.makeURI("http://getfirefox.com");
     let iconURI = CommonUtils.makeURI("http://getfirefox.com/icon");
     PlacesUtils.bookmarks.insertBookmark(
       PlacesUtils.bookmarks.bookmarksMenuFolder,
       pageURI,
       PlacesUtils.bookmarks.DEFAULT_INDEX,
       "Get Firefox!");
@@ -1098,17 +1065,17 @@ add_task(async function test_onLivemarkA
     await cleanup();
   }
 });
 
 add_task(async function test_onLivemarkDeleted() {
   _("Deleted livemarks should be tracked");
 
   try {
-    await stopTracking();
+    await tracker.stop();
 
     _("Insert a livemark");
     let livemark = await PlacesUtils.livemarks.addLivemark({
       parentGuid: PlacesUtils.bookmarks.menuGuid,
       feedURI: CommonUtils.makeURI("http://localhost:0"),
     });
     livemark.terminate();
 
@@ -1168,17 +1135,17 @@ add_task(async function test_onItemMoved
     await cleanup();
   }
 });
 
 add_task(async function test_async_onItemMoved_update() {
   _("Items moved via the asynchronous API should be tracked");
 
   try {
-    await stopTracking();
+    await tracker.stop();
 
     await PlacesUtils.bookmarks.insert({
       type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
       parentGuid: PlacesUtils.bookmarks.menuGuid,
       url: "http://getfirefox.com",
       title: "Get Firefox!",
     });
     let tbBmk = await PlacesUtils.bookmarks.insert({
@@ -1213,17 +1180,17 @@ add_task(async function test_async_onIte
     await cleanup();
   }
 });
 
 add_task(async function test_async_onItemMoved_reorder() {
   _("Items reordered via the asynchronous API should be tracked");
 
   try {
-    await stopTracking();
+    await tracker.stop();
 
     _("Insert out-of-order bookmarks");
     let fxBmk = await PlacesUtils.bookmarks.insert({
       type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
       parentGuid: PlacesUtils.bookmarks.menuGuid,
       url: "http://getfirefox.com",
       title: "Get Firefox!",
     });
@@ -1260,17 +1227,17 @@ add_task(async function test_async_onIte
     await cleanup();
   }
 });
 
 add_task(async function test_onItemDeleted_removeFolderTransaction() {
   _("Folders removed in a transaction should be tracked");
 
   try {
-    await stopTracking();
+    await tracker.stop();
 
     _("Create a folder with two children");
     let folder_id = PlacesUtils.bookmarks.createFolder(
       PlacesUtils.bookmarks.bookmarksMenuFolder,
       "Test folder",
       PlacesUtils.bookmarks.DEFAULT_INDEX);
     let folder_guid = await PlacesUtils.promiseItemGuid(folder_id);
     _(`Folder GUID: ${folder_guid}`);
@@ -1393,17 +1360,17 @@ add_task(async function test_onItemDelet
     await cleanup();
   }
 });
 
 add_task(async function test_async_onItemDeleted() {
   _("Bookmarks deleted via the asynchronous API should be tracked");
 
   try {
-    await stopTracking();
+    await tracker.stop();
 
     let fxBmk = await PlacesUtils.bookmarks.insert({
       type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
       parentGuid: PlacesUtils.bookmarks.menuGuid,
       url: "http://getfirefox.com",
       title: "Get Firefox!",
     });
     await PlacesUtils.bookmarks.insert({
@@ -1425,17 +1392,17 @@ add_task(async function test_async_onIte
     await cleanup();
   }
 });
 
 add_task(async function test_async_onItemDeleted_eraseEverything() {
   _("Erasing everything should track all deleted items");
 
   try {
-    await stopTracking();
+    await tracker.stop();
 
     let fxBmk = await PlacesUtils.bookmarks.insert({
       type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
       parentGuid: PlacesUtils.bookmarks.mobileGuid,
       url: "http://getfirefox.com",
       title: "Get Firefox!",
     });
     _(`Firefox GUID: ${fxBmk.guid}`);
--- a/services/sync/tests/unit/test_clients_engine.js
+++ b/services/sync/tests/unit/test_clients_engine.js
@@ -53,17 +53,17 @@ async function syncClientsEngine(server)
 }
 
 add_task(async function setup() {
   engine = Service.clientsEngine;
 });
 
 async function cleanup() {
   Svc.Prefs.resetBranch("");
-  engine._tracker.clearChangedIDs();
+  await engine._tracker.clearChangedIDs();
   await engine._resetClient();
   // un-cleanup the logs (the resetBranch will have reset their levels), since
   // not all the tests use SyncTestingInfrastructure, and it's cheap.
   syncTestLogging();
   // We don't finalize storage at cleanup, since we use the same clients engine
   // instance across all tests.
 }
 
@@ -330,36 +330,39 @@ add_task(async function test_sync() {
 add_task(async function test_client_name_change() {
   _("Ensure client name change incurs a client record update.");
 
   let tracker = engine._tracker;
 
   engine.localID; // Needed to increase the tracker changedIDs count.
   let initialName = engine.localName;
 
-  Svc.Obs.notify("weave:engine:start-tracking");
+  tracker.start();
   _("initial name: " + initialName);
 
   // Tracker already has data, so clear it.
-  tracker.clearChangedIDs();
+  await tracker.clearChangedIDs();
 
   let initialScore = tracker.score;
 
-  equal(Object.keys(tracker.changedIDs).length, 0);
+  let changedIDs = await tracker.getChangedIDs();
+  equal(Object.keys(changedIDs).length, 0);
 
   Svc.Prefs.set("client.name", "new name");
+  await tracker.asyncObserver.promiseObserversComplete();
 
   _("new name: " + engine.localName);
   notEqual(initialName, engine.localName);
-  equal(Object.keys(tracker.changedIDs).length, 1);
-  ok(engine.localID in tracker.changedIDs);
+  changedIDs = await tracker.getChangedIDs();
+  equal(Object.keys(changedIDs).length, 1);
+  ok(engine.localID in changedIDs);
   ok(tracker.score > initialScore);
   ok(tracker.score >= SCORE_INCREMENT_XLARGE);
 
-  Svc.Obs.notify("weave:engine:stop-tracking");
+  await tracker.stop();
 
   await cleanup();
 });
 
 add_task(async function test_last_modified() {
   _("Ensure that remote records have a sane serverLastModified attribute.");
 
   let now = Date.now() / 1000;
@@ -434,17 +437,19 @@ add_task(async function test_send_comman
   equal(clientCommands.length, 1);
 
   let command = clientCommands[0];
   equal(command.command, action);
   equal(command.args.length, 2);
   deepEqual(command.args, args);
   ok(command.flowID);
 
-  notEqual(tracker.changedIDs[remoteId], undefined);
+
+  const changes = await tracker.getChangedIDs();
+  notEqual(changes[remoteId], undefined);
 
   await cleanup();
 });
 
 // The browser UI might call _addClientCommand indirectly without awaiting on the returned promise.
 // We need to make sure this doesn't result on commands not being saved.
 add_task(async function test_add_client_command_race() {
   let promises = [];
@@ -496,17 +501,18 @@ add_task(async function test_command_val
       _("Ensuring command is sent: " + action);
       equal(clientCommands.length, 1);
 
       let command = clientCommands[0];
       equal(command.command, action);
       deepEqual(command.args, args);
 
       notEqual(engine._tracker, undefined);
-      notEqual(engine._tracker.changedIDs[remoteId], undefined);
+      const changes = await engine._tracker.getChangedIDs();
+      notEqual(changes[remoteId], undefined);
     } else {
       _("Ensuring command is scrubbed: " + action);
       equal(clientCommands, undefined);
 
       if (store._tracker) {
         equal(engine._tracker[remoteId], undefined);
       }
     }
@@ -941,17 +947,17 @@ add_task(async function test_send_uri_to
   let store = engine._store;
 
   let remoteId = Utils.makeGUID();
   let rec = new ClientsRec("clients", remoteId);
   rec.name = "remote";
   await store.create(rec);
   await store.createRecord(remoteId, "clients");
 
-  tracker.clearChangedIDs();
+  await tracker.clearChangedIDs();
   let initialScore = tracker.score;
 
   let uri = "http://www.mozilla.org/";
   let title = "Title of the Page";
   await engine.sendURIToClientForDisplay(uri, remoteId, title);
 
   let newRecord = store._remoteClients[remoteId];
 
@@ -1532,30 +1538,30 @@ add_task(async function test_command_syn
   }), Date.now() / 1000));
 
   try {
     equal(collection.count(), 2, "2 remote records written");
     await syncClientsEngine(server);
     equal(collection.count(), 3, "3 remote records written (+1 for the synced local record)");
 
     await engine.sendCommand("wipeAll", []);
-    engine._tracker.addChangedID(engine.localID);
+    await engine._tracker.addChangedID(engine.localID);
     const getClientFxaDeviceId = sinon.stub(engine, "getClientFxaDeviceId", (id) => "fxa-" + id);
     const engineMock = sinon.mock(engine);
     let _notifyCollectionChanged = engineMock.expects("_notifyCollectionChanged")
                                              .withArgs(["fxa-" + remoteId, "fxa-" + remoteId2]);
     _("Syncing.");
     await syncClientsEngine(server);
     _notifyCollectionChanged.verify();
 
     engineMock.restore();
     getClientFxaDeviceId.restore();
   } finally {
     await cleanup();
-    engine._tracker.clearChangedIDs();
+    await engine._tracker.clearChangedIDs();
 
     try {
       server.deleteCollections("foo");
     } finally {
       await promiseStopServer(server);
     }
   }
 });
@@ -1743,21 +1749,23 @@ add_task(async function test_other_clien
   }
 });
 
 add_task(async function device_disconnected_notification_updates_known_stale_clients() {
   const spyUpdate = sinon.spy(engine, "updateKnownStaleClients");
 
   Services.obs.notifyObservers(null, "fxaccounts:device_disconnected",
                                JSON.stringify({ isLocalDevice: false }));
+  await Service.asyncObserver.promiseObserversComplete();
   ok(spyUpdate.calledOnce, "updateKnownStaleClients should be called");
   spyUpdate.reset();
 
   Services.obs.notifyObservers(null, "fxaccounts:device_disconnected",
                                JSON.stringify({ isLocalDevice: true }));
+  await Service.asyncObserver.promiseObserversComplete();
   ok(spyUpdate.notCalled, "updateKnownStaleClients should not be called");
 
   spyUpdate.restore();
 });
 
 add_task(async function update_known_stale_clients() {
   const makeFakeClient = (id) => ({ id, fxaDeviceId: `fxa-${id}` });
   const clients = [makeFakeClient("one"), makeFakeClient("two"), makeFakeClient("three")];
--- a/services/sync/tests/unit/test_engine.js
+++ b/services/sync/tests/unit/test_engine.js
@@ -1,12 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 ChromeUtils.import("resource://gre/modules/osfile.jsm");
+ChromeUtils.import("resource://gre/modules/PromiseUtils.jsm");
 ChromeUtils.import("resource://services-common/observers.js");
 ChromeUtils.import("resource://services-sync/engines.js");
 ChromeUtils.import("resource://services-sync/service.js");
 ChromeUtils.import("resource://services-sync/util.js");
 
 function SteamStore(engine) {
   Store.call(this, "Steam", engine);
   this.wasWiped = false;
@@ -65,17 +66,17 @@ Observers.add("weave:engine:wipe-client:
 Observers.add("weave:engine:sync:start", engineObserver);
 Observers.add("weave:engine:sync:finish", engineObserver);
 
 async function cleanup(engine) {
   Svc.Prefs.resetBranch("");
   engine.wasReset = false;
   engine.wasSynced = false;
   engineObserver.reset();
-  engine._tracker.clearChangedIDs();
+  await engine._tracker.clearChangedIDs();
   await engine.finalize();
 }
 
 add_task(async function test_members() {
   _("Engine object members");
   let engine = new SteamEngine("Steam", Service);
   Assert.equal(engine.Name, "Steam");
   Assert.equal(engine.prefName, "steam");
@@ -118,37 +119,40 @@ add_task(async function test_invalidChan
   let engine = new SteamEngine("Steam", Service);
   let tracker = engine._tracker;
 
   await tracker._beforeSave();
   await OS.File.writeAtomic(tracker._storage.path, new TextEncoder().encode("5"),
                             { tmpPath: tracker._storage.path + ".tmp" });
 
   ok(!tracker._storage.dataReady);
-  tracker.changedIDs.placeholder = true;
-  deepEqual(tracker.changedIDs, { placeholder: true },
+  const changes = await tracker.getChangedIDs();
+  changes.placeholder = true;
+  deepEqual(changes, { placeholder: true },
     "Accessing changed IDs should load changes from disk as a side effect");
   ok(tracker._storage.dataReady);
 
-  Assert.ok(tracker.changedIDs.placeholder);
+  Assert.ok(changes.placeholder);
   await cleanup(engine);
 });
 
 add_task(async function test_wipeClient() {
   _("Engine.wipeClient calls resetClient, wipes store, clears changed IDs");
   let engine = new SteamEngine("Steam", Service);
   Assert.ok(!engine.wasReset);
   Assert.ok(!engine._store.wasWiped);
-  Assert.ok(engine._tracker.addChangedID("a-changed-id"));
-  Assert.ok("a-changed-id" in engine._tracker.changedIDs);
+  Assert.ok((await engine._tracker.addChangedID("a-changed-id")));
+  let changes = await engine._tracker.getChangedIDs();
+  Assert.ok("a-changed-id" in changes);
 
   await engine.wipeClient();
   Assert.ok(engine.wasReset);
   Assert.ok(engine._store.wasWiped);
-  Assert.equal(JSON.stringify(engine._tracker.changedIDs), "{}");
+  changes = await engine._tracker.getChangedIDs();
+  Assert.equal(JSON.stringify(changes), "{}");
   Assert.equal(engineObserver.topics[0], "weave:engine:wipe-client:start");
   Assert.equal(engineObserver.topics[1], "weave:engine:reset-client:start");
   Assert.equal(engineObserver.topics[2], "weave:engine:reset-client:finish");
   Assert.equal(engineObserver.topics[3], "weave:engine:wipe-client:finish");
 
   await cleanup(engine);
 });
 
@@ -192,28 +196,41 @@ add_task(async function test_sync() {
 add_task(async function test_disabled_no_track() {
   _("When an engine is disabled, its tracker is not tracking.");
   let engine = new SteamEngine("Steam", Service);
   let tracker = engine._tracker;
   Assert.equal(engine, tracker.engine);
 
   Assert.ok(!engine.enabled);
   Assert.ok(!tracker._isTracking);
-  do_check_empty(tracker.changedIDs);
+  let changes = await tracker.getChangedIDs();
+  do_check_empty(changes);
 
   Assert.ok(!tracker.engineIsEnabled());
-  tracker.observe(null, "weave:engine:start-tracking", null);
   Assert.ok(!tracker._isTracking);
-  do_check_empty(tracker.changedIDs);
+  changes = await tracker.getChangedIDs();
+  do_check_empty(changes);
 
-  engine.enabled = true;
-  tracker.observe(null, "weave:engine:start-tracking", null);
+  let promisePrefChangeHandled = PromiseUtils.defer();
+  const origMethod = tracker.onEngineEnabledChanged;
+  tracker.onEngineEnabledChanged = async (...args) => {
+    await origMethod.apply(tracker, args);
+    promisePrefChangeHandled.resolve();
+  };
+
+  engine.enabled = true; // Also enables the tracker automatically.
+  await promisePrefChangeHandled.promise;
   Assert.ok(tracker._isTracking);
-  do_check_empty(tracker.changedIDs);
+  changes = await tracker.getChangedIDs();
+  do_check_empty(changes);
 
-  tracker.addChangedID("abcdefghijkl");
-  Assert.ok(0 < tracker.changedIDs.abcdefghijkl);
+  await tracker.addChangedID("abcdefghijkl");
+  changes = await tracker.getChangedIDs();
+  Assert.ok(0 < changes.abcdefghijkl);
+  promisePrefChangeHandled = PromiseUtils.defer();
   Svc.Prefs.set("engine." + engine.prefName, false);
+  await promisePrefChangeHandled.promise;
   Assert.ok(!tracker._isTracking);
-  do_check_empty(tracker.changedIDs);
+  changes = await tracker.getChangedIDs();
+  do_check_empty(changes);
 
   await cleanup(engine);
 });
--- a/services/sync/tests/unit/test_engine_abort.js
+++ b/services/sync/tests/unit/test_engine_abort.js
@@ -57,11 +57,11 @@ add_task(async function test_processInco
   }
 
   Assert.equal(err, undefined);
 
   await promiseStopServer(server);
   Svc.Prefs.resetBranch("");
   Service.recordManager.clearCache();
 
-  engine._tracker.clearChangedIDs();
+  await engine._tracker.clearChangedIDs();
   await engine.finalize();
 });
--- a/services/sync/tests/unit/test_engine_changes_during_sync.js
+++ b/services/sync/tests/unit/test_engine_changes_during_sync.js
@@ -20,17 +20,17 @@ const LoginInfo = Components.Constructor
 
 async function assertChildGuids(folderGuid, expectedChildGuids, message) {
   let tree = await PlacesUtils.promiseBookmarksTree(folderGuid);
   let childGuids = tree.children.map(child => child.guid);
   deepEqual(childGuids, expectedChildGuids, message);
 }
 
 async function cleanup(engine, server) {
-  Svc.Obs.notify("weave:engine:stop-tracking");
+  await engine._tracker.stop();
   await engine._store.wipe();
   Svc.Prefs.resetBranch("");
   Service.recordManager.clearCache();
   await promiseStopServer(server);
 }
 
 add_task(async function test_history_change_during_sync() {
   _("Ensure that we don't bump the score when applying history records.");
@@ -47,20 +47,21 @@ add_task(async function test_history_cha
   let uploadOutgoing = engine._uploadOutgoing;
   engine._uploadOutgoing = async function() {
     engine._uploadOutgoing = uploadOutgoing;
     try {
       await uploadOutgoing.call(this);
     } finally {
       _("Inserting local history visit");
       await addVisit("during_sync");
+      await engine._tracker.asyncObserver.promiseObserversComplete();
     }
   };
 
-  Svc.Obs.notify("weave:engine:start-tracking");
+  engine._tracker.start();
 
   try {
     let remoteRec = new HistoryRec("history", "UrOOuzE5QM-e");
     remoteRec.histUri = "http://getfirefox.com/";
     remoteRec.title = "Get Firefox!";
     remoteRec.visits = [{
       date: PlacesUtils.toPRTime(Date.now()),
       type: PlacesUtils.history.TRANSITION_TYPED,
@@ -101,20 +102,21 @@ add_task(async function test_passwords_c
     engine._uploadOutgoing = uploadOutgoing;
     try {
       await uploadOutgoing.call(this);
     } finally {
       _("Inserting local password");
       let login = new LoginInfo("https://example.com", "", null, "username",
         "password", "", "");
       Services.logins.addLogin(login);
+      await engine._tracker.asyncObserver.promiseObserversComplete();
     }
   };
 
-  Svc.Obs.notify("weave:engine:start-tracking");
+  engine._tracker.start();
 
   try {
     let remoteRec = new LoginRec("passwords", "{765e3d6e-071d-d640-a83d-81a7eb62d3ed}");
     remoteRec.formSubmitURL = "";
     remoteRec.httpRealm = "";
     remoteRec.hostname = "https://mozilla.org";
     remoteRec.username = "username";
     remoteRec.password = "sekrit";
@@ -157,20 +159,21 @@ add_task(async function test_prefs_chang
   engine._uploadOutgoing = async function() {
     engine._uploadOutgoing = uploadOutgoing;
     try {
       await uploadOutgoing.call(this);
     } finally {
       _("Updating local pref value");
       // Change the value of a synced pref.
       Services.prefs.setCharPref(TEST_PREF, "hello");
+      await engine._tracker.asyncObserver.promiseObserversComplete();
     }
   };
 
-  Svc.Obs.notify("weave:engine:start-tracking");
+  engine._tracker.start();
 
   try {
     // All synced prefs are stored in a single record, so we'll only ever
     // have one record on the server. This test just checks that we don't
     // track or upload prefs changed during the sync.
     let guid = CommonUtils.encodeBase64URL(Services.appinfo.ID);
     let remoteRec = new PrefRec("prefs", guid);
     remoteRec.value = {
@@ -223,20 +226,21 @@ add_task(async function test_forms_chang
         FormHistory.update([{
           op: "add",
           fieldname: "favoriteDrink",
           value: "cocoa",
         }], {
           handleCompletion: resolve,
         });
       });
+      await engine._tracker.asyncObserver.promiseObserversComplete();
     }
   };
 
-  Svc.Obs.notify("weave:engine:start-tracking");
+  engine._tracker.start();
 
   try {
     // Add an existing remote form history entry. We shouldn't bump the score when
     // we apply this record.
     let remoteRec = new FormRec("forms", "Tl9dHgmJSR6FkyxS");
     remoteRec.name = "name";
     remoteRec.value = "alice";
     collection.insert(remoteRec.id, encryptPayload(remoteRec.cleartext));
@@ -293,16 +297,17 @@ add_task(async function test_bookmark_ch
       await uploadOutgoing.call(this);
     } finally {
       _("Inserting bookmark into local store");
       bmk3 = await PlacesUtils.bookmarks.insert({
         parentGuid: folder1.guid,
         url: "https://mozilla.org/",
         title: "Mozilla",
       });
+      await engine._tracker.asyncObserver.promiseObserversComplete();
     }
   };
 
   // New bookmarks that should be uploaded during the first sync.
   let folder1 = await PlacesUtils.bookmarks.insert({
     type: PlacesUtils.bookmarks.TYPE_FOLDER,
     parentGuid: PlacesUtils.bookmarks.toolbarGuid,
     title: "Folder 1",
@@ -311,17 +316,17 @@ add_task(async function test_bookmark_ch
 
   let tbBmk = await PlacesUtils.bookmarks.insert({
     parentGuid: folder1.guid,
     url: "http://getthunderbird.com/",
     title: "Get Thunderbird!",
   });
   _(`Thunderbird GUID: ${tbBmk.guid}`);
 
-  Svc.Obs.notify("weave:engine:start-tracking");
+  engine._tracker.start();
 
   try {
     let bmk2_guid = "get-firefox1"; // New child of Folder 1, created remotely.
     let folder2_guid = "folder2-1111"; // New folder, created remotely.
     let tagQuery_guid = "tag-query111"; // New tag query child of Folder 2, created remotely.
     let bmk4_guid = "example-org1"; // New tagged child of Folder 2, created remotely.
     {
       // An existing record changed on the server that should not trigger
--- a/services/sync/tests/unit/test_extension_storage_tracker.js
+++ b/services/sync/tests/unit/test_extension_storage_tracker.js
@@ -18,22 +18,22 @@ add_task(async function setup() {
   engine = Service.engineManager.get("extension-storage");
   do_get_profile(); // so we can use FxAccounts
   loadWebExtensionTestFunctions();
 });
 
 add_task(async function test_changing_extension_storage_changes_score() {
   const tracker = engine._tracker;
   const extension = {id: "my-extension-id"};
-  Svc.Obs.notify("weave:engine:start-tracking");
+  tracker.start();
   await withSyncContext(async function(context) {
     await extensionStorageSync.set(extension, {"a": "b"}, context);
   });
   Assert.equal(tracker.score, SCORE_INCREMENT_MEDIUM);
 
   tracker.resetScore();
   await withSyncContext(async function(context) {
     await extensionStorageSync.remove(extension, "a", context);
   });
   Assert.equal(tracker.score, SCORE_INCREMENT_MEDIUM);
 
-  Svc.Obs.notify("weave:engine:stop-tracking");
+  await tracker.stop();
 });
--- a/services/sync/tests/unit/test_forms_tracker.js
+++ b/services/sync/tests/unit/test_forms_tracker.js
@@ -9,65 +9,74 @@ ChromeUtils.import("resource://services-
 add_task(async function run_test() {
   _("Verify we've got an empty tracker to work with.");
   let engine = new FormEngine(Service);
   await engine.initialize();
   let tracker = engine._tracker;
   // Don't do asynchronous writes.
   tracker.persistChangedIDs = false;
 
-  do_check_empty(tracker.changedIDs);
+  let changes = await tracker.getChangedIDs();
+  do_check_empty(changes);
   Log.repository.rootLogger.addAppender(new Log.DumpAppender());
 
   async function addEntry(name, value) {
     await engine._store.create({name, value});
+    await engine._tracker.asyncObserver.promiseObserversComplete();
   }
   async function removeEntry(name, value) {
     let guid = await engine._findDupe({name, value});
     await engine._store.remove({id: guid});
+    await engine._tracker.asyncObserver.promiseObserversComplete();
   }
 
   try {
     _("Create an entry. Won't show because we haven't started tracking yet");
     await addEntry("name", "John Doe");
-    do_check_empty(tracker.changedIDs);
+    changes = await tracker.getChangedIDs();
+    do_check_empty(changes);
 
     _("Tell the tracker to start tracking changes.");
-    Svc.Obs.notify("weave:engine:start-tracking");
+    tracker.start();
     await removeEntry("name", "John Doe");
     await addEntry("email", "john@doe.com");
-    do_check_attribute_count(tracker.changedIDs, 2);
+    changes = await tracker.getChangedIDs();
+    do_check_attribute_count(changes, 2);
 
     _("Notifying twice won't do any harm.");
-    Svc.Obs.notify("weave:engine:start-tracking");
+    tracker.start();
     await addEntry("address", "Memory Lane");
-    do_check_attribute_count(tracker.changedIDs, 3);
+    changes = await tracker.getChangedIDs();
+    do_check_attribute_count(changes, 3);
 
 
     _("Check that ignoreAll is respected");
-    tracker.clearChangedIDs();
+    await tracker.clearChangedIDs();
     tracker.score = 0;
     tracker.ignoreAll = true;
     await addEntry("username", "johndoe123");
     await addEntry("favoritecolor", "green");
     await removeEntry("name", "John Doe");
     tracker.ignoreAll = false;
-    do_check_empty(tracker.changedIDs);
+    changes = await tracker.getChangedIDs();
+    do_check_empty(changes);
     equal(tracker.score, 0);
 
     _("Let's stop tracking again.");
-    tracker.clearChangedIDs();
-    Svc.Obs.notify("weave:engine:stop-tracking");
+    await tracker.clearChangedIDs();
+    await tracker.stop();
     await removeEntry("address", "Memory Lane");
-    do_check_empty(tracker.changedIDs);
+    changes = await tracker.getChangedIDs();
+    do_check_empty(changes);
 
     _("Notifying twice won't do any harm.");
-    Svc.Obs.notify("weave:engine:stop-tracking");
+    await tracker.stop();
     await removeEntry("email", "john@doe.com");
-    do_check_empty(tracker.changedIDs);
+    changes = await tracker.getChangedIDs();
+    do_check_empty(changes);
 
 
 
   } finally {
     _("Clean up.");
     await engine._store.wipe();
   }
 });
--- a/services/sync/tests/unit/test_fxa_node_reassignment.js
+++ b/services/sync/tests/unit/test_fxa_node_reassignment.js
@@ -247,17 +247,17 @@ add_task(async function test_momentary_4
   }
 
   await syncAndExpectNodeReassignment(server,
                                       "weave:service:sync:finish",
                                       between,
                                       "weave:service:sync:finish",
                                       Service.storageURL + "rotary");
 
-  tracker.clearChangedIDs();
+  await tracker.clearChangedIDs();
   await Service.engineManager.unregister(engine);
 });
 
 // This test ends up being a failing info fetch *after we're already logged in*.
 add_task(async function test_momentary_401_info_collections_loggedin() {
   enableValidationPrefs();
 
   _("Test a failure for info/collections after login that's resolved by reassignment.");
--- a/services/sync/tests/unit/test_history_engine.js
+++ b/services/sync/tests/unit/test_history_engine.js
@@ -132,17 +132,17 @@ add_task(async function test_history_dow
 });
 
 add_task(async function test_history_visit_roundtrip() {
   let engine = new HistoryEngine(Service);
   await engine.initialize();
   let server = await serverForFoo(engine);
   await SyncTestingInfrastructure(server);
 
-  Svc.Obs.notify("weave:engine:start-tracking");
+  engine._tracker.start();
 
   let id = "aaaaaaaaaaaa";
   let oneHourMS = 60 * 60 * 1000;
   // Insert a visit with a non-round microsecond timestamp (e.g. it's not evenly
   // divisible by 1000). This will typically be the case for visits that occur
   // during normal navigation.
   let time = (Date.now() - oneHourMS) * 1000 + 555;
   // We use the low level updatePlaces api since it lets us provide microseconds
@@ -187,17 +187,17 @@ add_task(async function test_history_vis
 });
 
 add_task(async function test_history_visit_dedupe_old() {
   let engine = new HistoryEngine(Service);
   await engine.initialize();
   let server = await serverForFoo(engine);
   await SyncTestingInfrastructure(server);
 
-  Svc.Obs.notify("weave:engine:start-tracking");
+  engine._tracker.start();
 
   await PlacesUtils.history.insert({
     url: "https://www.example.com",
     visits: Array.from({ length: 25 }, (_, index) => ({
       transition: PlacesUtils.history.TRANSITION_LINK,
       date: new Date(Date.UTC(2017, 10, 1 + index)),
     }))
   });
--- a/services/sync/tests/unit/test_history_tracker.js
+++ b/services/sync/tests/unit/test_history_tracker.js
@@ -42,33 +42,25 @@ async function verifyTrackedItems(tracke
     ok(guid in changes, `${guid} should be tracked`);
     ok(changes[guid] > 0, `${guid} should have a modified time`);
     trackedIDs.delete(guid);
   }
   equal(trackedIDs.size, 0, `Unhandled tracked IDs: ${
     JSON.stringify(Array.from(trackedIDs))}`);
 }
 
-async function startTracking() {
-  Svc.Obs.notify("weave:engine:start-tracking");
-}
-
-async function stopTracking() {
-  Svc.Obs.notify("weave:engine:stop-tracking");
-}
-
 async function resetTracker() {
-  tracker.clearChangedIDs();
+  await tracker.clearChangedIDs();
   tracker.resetScore();
 }
 
 async function cleanup() {
   await PlacesTestUtils.clearHistory();
   await resetTracker();
-  await stopTracking();
+  await tracker.stop();
 }
 
 add_task(async function test_empty() {
   _("Verify we've got an empty, disabled tracker to work with.");
   await verifyTrackerEmpty();
   Assert.ok(!tracker._isTracking);
 
   await cleanup();
@@ -97,40 +89,40 @@ add_task(async function test_start_track
         // Turn this back off.
         tracker.persistChangedIDs = false;
         tracker._storage._save = save;
       }
     };
   });
 
   _("Tell the tracker to start tracking changes.");
-  await startTracking();
+  tracker.start();
   let scorePromise = promiseOneObserver("weave:engine:score:updated");
   await addVisit("start_tracking");
   await scorePromise;
 
   _("Score updated in test_start_tracking.");
   await verifyTrackedCount(1);
   Assert.equal(tracker.score, SCORE_INCREMENT_SMALL);
 
   await savePromise;
 
   _("changedIDs written to disk. Proceeding.");
   await cleanup();
 });
 
 add_task(async function test_start_tracking_twice() {
   _("Verifying preconditions.");
-  await startTracking();
+  tracker.start();
   await addVisit("start_tracking_twice1");
   await verifyTrackedCount(1);
   Assert.equal(tracker.score, SCORE_INCREMENT_SMALL);
 
   _("Notifying twice won't do any harm.");
-  await startTracking();
+  tracker.start();
   let scorePromise = promiseOneObserver("weave:engine:score:updated");
   await addVisit("start_tracking_twice2");
   await scorePromise;
 
   _("Score updated in test_start_tracking_twice.");
   await verifyTrackedCount(2);
   Assert.equal(tracker.score, 2 * SCORE_INCREMENT_SMALL);
 
@@ -141,17 +133,17 @@ add_task(async function test_track_delet
   _("Deletions are tracked.");
 
   // This isn't present because we weren't tracking when it was visited.
   await addVisit("track_delete");
   let uri = CommonUtils.makeURI("http://getfirefox.com/track_delete");
   let guid = await engine._store.GUIDForUri(uri.spec);
   await verifyTrackerEmpty();
 
-  await startTracking();
+  tracker.start();
   let visitRemovedPromise = promiseVisit("removed", uri);
   let scorePromise = promiseOneObserver("weave:engine:score:updated");
   await PlacesUtils.history.remove(uri);
   await Promise.all([scorePromise, visitRemovedPromise]);
 
   await verifyTrackedItems([guid]);
   Assert.equal(tracker.score, SCORE_INCREMENT_XLARGE);
 
@@ -161,17 +153,17 @@ add_task(async function test_track_delet
 add_task(async function test_dont_track_expiration() {
   _("Expirations are not tracked.");
   let uriToRemove = await addVisit("to_remove");
   let guidToRemove = await engine._store.GUIDForUri(uriToRemove.spec);
 
   await resetTracker();
   await verifyTrackerEmpty();
 
-  await startTracking();
+  tracker.start();
   let visitRemovedPromise = promiseVisit("removed", uriToRemove);
   let scorePromise = promiseOneObserver("weave:engine:score:updated");
 
   // Observe expiration.
   Services.obs.addObserver(function onExpiration(aSubject, aTopic, aData) {
     Services.obs.removeObserver(onExpiration, aTopic);
     // Remove the remaining page to update its score.
     PlacesUtils.history.remove(uriToRemove);
@@ -186,54 +178,54 @@ add_task(async function test_dont_track_
   await Promise.all([scorePromise, visitRemovedPromise]);
   await verifyTrackedItems([guidToRemove]);
 
   await cleanup();
 });
 
 add_task(async function test_stop_tracking() {
   _("Let's stop tracking again.");
-  await stopTracking();
+  await tracker.stop();
   await addVisit("stop_tracking");
   await verifyTrackerEmpty();
 
   await cleanup();
 });
 
 add_task(async function test_stop_tracking_twice() {
-  await stopTracking();
+  await tracker.stop();
   await addVisit("stop_tracking_twice1");
 
   _("Notifying twice won't do any harm.");
-  await stopTracking();
+  await tracker.stop();
   await addVisit("stop_tracking_twice2");
   await verifyTrackerEmpty();
 
   await cleanup();
 });
 
 add_task(async function test_filter_file_uris() {
-  await startTracking();
+  tracker.start();
 
   let uri = CommonUtils.makeURI("file:///Users/eoger/tps/config.json");
   let visitAddedPromise = promiseVisit("added", uri);
   await PlacesTestUtils.addVisits({
     uri,
     visitDate: Date.now() * 1000,
     transition: PlacesUtils.history.TRANSITION_LINK
   });
   await visitAddedPromise;
 
   await verifyTrackerEmpty();
-  await stopTracking();
+  await tracker.stop();
   await cleanup();
 });
 
 add_task(async function test_filter_hidden() {
-  await startTracking();
+  tracker.start();
 
   _("Add visit; should be hidden by the redirect");
   let hiddenURI = await addVisit("hidden");
   let hiddenGUID = await engine._store.GUIDForUri(hiddenURI.spec);
   _(`Hidden visit GUID: ${hiddenGUID}`);
 
   _("Add redirect visit; should be tracked");
   let trackedURI = await addVisit("redirect", hiddenURI.spec,
--- a/services/sync/tests/unit/test_hmac_error.js
+++ b/services/sync/tests/unit/test_hmac_error.js
@@ -22,17 +22,17 @@ async function shared_setup() {
 
   hmacErrorCount = 0;
 
   // Make sure RotaryEngine is the only one we sync.
   let { engine, tracker } = await registerRotaryEngine();
   engine.lastSync = 123; // Needs to be non-zero so that tracker is queried.
   engine._store.items = {flying: "LNER Class A3 4472",
                          scotsman: "Flying Scotsman"};
-  tracker.addChangedID("scotsman", 0);
+  await tracker.addChangedID("scotsman", 0);
   Assert.equal(1, Service.engineManager.getEnabled().length);
 
   let engines = {rotary:  {version: engine.version,
                            syncID:  engine.syncID},
                  clients: {version: Service.clientsEngine.version,
                            syncID:  Service.clientsEngine.syncID}};
 
   // Common server objects.
@@ -87,17 +87,17 @@ add_task(async function hmac_error_durin
     key404Counter = 1;
     _("---------------------------");
     await sync_and_validate_telem();
     _("---------------------------");
 
     // Two rotary items, one client record... no errors.
     Assert.equal(hmacErrorCount, 0);
   } finally {
-    tracker.clearChangedIDs();
+    await tracker.clearChangedIDs();
     await Service.engineManager.unregister(engine);
     Svc.Prefs.resetBranch("");
     Service.recordManager.clearCache();
     await promiseStopServer(server);
   }
 });
 
 add_task(async function hmac_error_during_node_reassignment() {
@@ -216,17 +216,17 @@ add_task(async function hmac_error_durin
           onSyncFinished = async function() {
             // Two rotary items, one client record... no errors.
             Assert.equal(hmacErrorCount, 0);
 
             Svc.Obs.remove("weave:service:sync:finish", obs);
             Svc.Obs.remove("weave:service:sync:error", obs);
 
             (async () => {
-              tracker.clearChangedIDs();
+              await tracker.clearChangedIDs();
               await Service.engineManager.unregister(engine);
               Svc.Prefs.resetBranch("");
               Service.recordManager.clearCache();
               server.stop(resolve);
             })();
           };
 
           Service.sync();
--- a/services/sync/tests/unit/test_node_reassignment.js
+++ b/services/sync/tests/unit/test_node_reassignment.js
@@ -172,17 +172,17 @@ add_task(async function test_momentary_4
   }
 
   await syncAndExpectNodeReassignment(server,
                                       "weave:service:sync:finish",
                                       between,
                                       "weave:service:sync:finish",
                                       Service.storageURL + "rotary");
 
-  tracker.clearChangedIDs();
+  await tracker.clearChangedIDs();
   await Service.engineManager.unregister(engine);
 });
 
 // This test ends up being a failing fetch *after we're already logged in*.
 add_task(async function test_momentary_401_info_collections() {
   enableValidationPrefs();
 
   _("Test a failure for info/collections that's resolved by reassignment.");
@@ -485,11 +485,11 @@ add_task(async function test_loop_avoida
   }
 
   Svc.Obs.add(firstNotification, onFirstSync);
 
   now = Date.now();
   await Service.sync();
   await deferred.promise;
 
-  tracker.clearChangedIDs();
+  await tracker.clearChangedIDs();
   await Service.engineManager.unregister(engine);
 });
--- a/services/sync/tests/unit/test_password_engine.js
+++ b/services/sync/tests/unit/test_password_engine.js
@@ -4,17 +4,17 @@ ChromeUtils.import("resource://services-
 
 const LoginInfo = Components.Constructor(
   "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init");
 
 const PropertyBag = Components.Constructor(
   "@mozilla.org/hash-property-bag;1", Ci.nsIWritablePropertyBag);
 
 async function cleanup(engine, server) {
-  Svc.Obs.notify("weave:engine:stop-tracking");
+  await engine._tracker.stop();
   await engine.wipeClient();
   Svc.Prefs.resetBranch("");
   Service.recordManager.clearCache();
   await promiseStopServer(server);
 }
 
 add_task(async function setup() {
   await Service.engineManager.unregister("addons"); // To silence errors.
@@ -29,17 +29,17 @@ add_task(async function test_ignored_fie
   await SyncTestingInfrastructure(server);
 
   enableValidationPrefs();
 
   let login = Services.logins.addLogin(new LoginInfo("https://example.com", "",
     null, "username", "password", "", ""));
   login.QueryInterface(Ci.nsILoginMetaInfo); // For `guid`.
 
-  Svc.Obs.notify("weave:engine:start-tracking");
+  engine._tracker.start();
 
   try {
     let nonSyncableProps = new PropertyBag();
     nonSyncableProps.setProperty("timeLastUsed", Date.now());
     nonSyncableProps.setProperty("timesUsed", 3);
     Services.logins.modifyLogin(login, nonSyncableProps);
 
     let noChanges = await engine.pullNewChanges();
@@ -62,17 +62,17 @@ add_task(async function test_ignored_syn
 
   let engine = Service.engineManager.get("passwords");
 
   let server = await serverForFoo(engine);
   await SyncTestingInfrastructure(server);
 
   enableValidationPrefs();
 
-  Svc.Obs.notify("weave:engine:start-tracking");
+  engine._tracker.start();
 
   try {
     let login = Services.logins.addLogin(new LoginInfo(FXA_PWDMGR_HOST, null,
       FXA_PWDMGR_REALM, "fxa-uid", "creds", "", ""));
 
     let noChanges = await engine.pullNewChanges();
     deepEqual(noChanges, {}, "Should not track new FxA credentials");
 
@@ -150,17 +150,17 @@ add_task(async function test_password_en
     rec.password = "n3wpa55";
     rec.usernameField = oldLogin.usernameField;
     rec.passwordField = oldLogin.usernameField;
     rec.timeCreated = oldLogin.timeCreated;
     rec.timePasswordChanged = Date.now();
     collection.insert(oldLogin.guid, encryptPayload(rec.cleartext));
   }
 
-  Svc.Obs.notify("weave:engine:start-tracking");
+  await engine._tracker.stop();
 
   try {
     await sync_engine_and_validate_telem(engine, false);
 
     let newRec = JSON.parse(JSON.parse(
       collection.payload(newLogin.guid)).ciphertext);
     equal(newRec.password, "password",
       "Should update remote password for newer login");
--- a/services/sync/tests/unit/test_password_tracker.js
+++ b/services/sync/tests/unit/test_password_tracker.js
@@ -20,81 +20,90 @@ add_task(async function setup() {
   // Don't do asynchronous writes.
   tracker.persistChangedIDs = false;
 });
 
 add_task(async function test_tracking() {
   let recordNum = 0;
 
   _("Verify we've got an empty tracker to work with.");
-  do_check_empty(tracker.changedIDs);
+  let changes = await tracker.getChangedIDs();
+  do_check_empty(changes);
 
-  function createPassword() {
+  async function createPassword() {
     _("RECORD NUM: " + recordNum);
     let record = {id: "GUID" + recordNum,
                   hostname: "http://foo.bar.com",
                   formSubmitURL: "http://foo.bar.com/baz",
                   username: "john" + recordNum,
                   password: "smith",
                   usernameField: "username",
                   passwordField: "password"};
     recordNum++;
     let login = store._nsLoginInfoFromRecord(record);
     Services.logins.addLogin(login);
+    await tracker.asyncObserver.promiseObserversComplete();
   }
 
   try {
     _("Create a password record. Won't show because we haven't started tracking yet");
-    createPassword();
-    do_check_empty(tracker.changedIDs);
+    await createPassword();
+    changes = await tracker.getChangedIDs();
+    do_check_empty(changes);
     Assert.equal(tracker.score, 0);
 
     _("Tell the tracker to start tracking changes.");
-    Svc.Obs.notify("weave:engine:start-tracking");
-    createPassword();
-    do_check_attribute_count(tracker.changedIDs, 1);
+    tracker.start();
+    await createPassword();
+    changes = await tracker.getChangedIDs();
+    do_check_attribute_count(changes, 1);
     Assert.equal(tracker.score, SCORE_INCREMENT_XLARGE);
 
-    _("Notifying twice won't do any harm.");
-    Svc.Obs.notify("weave:engine:start-tracking");
-    createPassword();
-    do_check_attribute_count(tracker.changedIDs, 2);
+    _("Starting twice won't do any harm.");
+    tracker.start();
+    await createPassword();
+    changes = await tracker.getChangedIDs();
+    do_check_attribute_count(changes, 2);
     Assert.equal(tracker.score, SCORE_INCREMENT_XLARGE * 2);
 
     _("Let's stop tracking again.");
-    tracker.clearChangedIDs();
+    await tracker.clearChangedIDs();
     tracker.resetScore();
-    Svc.Obs.notify("weave:engine:stop-tracking");
-    createPassword();
-    do_check_empty(tracker.changedIDs);
+    await tracker.stop();
+    await createPassword();
+    changes = await tracker.getChangedIDs();
+    do_check_empty(changes);
     Assert.equal(tracker.score, 0);
 
-    _("Notifying twice won't do any harm.");
-    Svc.Obs.notify("weave:engine:stop-tracking");
-    createPassword();
-    do_check_empty(tracker.changedIDs);
+    _("Stopping twice won't do any harm.");
+    await tracker.stop();
+    await createPassword();
+    changes = await tracker.getChangedIDs();
+    do_check_empty(changes);
     Assert.equal(tracker.score, 0);
 
   } finally {
     _("Clean up.");
     await store.wipe();
-    tracker.clearChangedIDs();
+    await tracker.clearChangedIDs();
     tracker.resetScore();
-    Svc.Obs.notify("weave:engine:stop-tracking");
+    await tracker.stop();
   }
 });
 
 add_task(async function test_onWipe() {
   _("Verify we've got an empty tracker to work with.");
-  do_check_empty(tracker.changedIDs);
+  const changes = await tracker.getChangedIDs();
+  do_check_empty(changes);
   Assert.equal(tracker.score, 0);
 
   try {
     _("A store wipe should increment the score");
-    Svc.Obs.notify("weave:engine:start-tracking");
+    tracker.start();
     await store.wipe();
+    await tracker.asyncObserver.promiseObserversComplete();
 
     Assert.equal(tracker.score, SCORE_INCREMENT_XLARGE);
   } finally {
     tracker.resetScore();
-    Svc.Obs.notify("weave:engine:stop-tracking");
+    await tracker.stop();
   }
 });
--- a/services/sync/tests/unit/test_prefs_tracker.js
+++ b/services/sync/tests/unit/test_prefs_tracker.js
@@ -44,45 +44,50 @@ add_task(async function run_test() {
 
     _("Test fixtures.");
     Svc.Prefs.set("prefs.sync.testing.int", true);
 
     _("Test fixtures haven't upped the tracker score yet because it hasn't started tracking yet.");
     Assert.equal(tracker.score, 0);
 
     _("Tell the tracker to start tracking changes.");
-    Svc.Obs.notify("weave:engine:start-tracking");
+    tracker.start();
     prefs.set("testing.int", 23);
+    await tracker.asyncObserver.promiseObserversComplete();
     Assert.equal(tracker.score, SCORE_INCREMENT_XLARGE);
     Assert.equal(tracker.modified, true);
 
     _("Clearing changed IDs reset modified status.");
-    tracker.clearChangedIDs();
+    await tracker.clearChangedIDs();
     Assert.equal(tracker.modified, false);
 
     _("Resetting a pref ups the score, too.");
     prefs.reset("testing.int");
+    await tracker.asyncObserver.promiseObserversComplete();
     Assert.equal(tracker.score, SCORE_INCREMENT_XLARGE * 2);
     Assert.equal(tracker.modified, true);
-    tracker.clearChangedIDs();
+    await tracker.clearChangedIDs();
 
     _("So does changing a pref sync pref.");
     Svc.Prefs.set("prefs.sync.testing.int", false);
+    await tracker.asyncObserver.promiseObserversComplete();
     Assert.equal(tracker.score, SCORE_INCREMENT_XLARGE * 3);
     Assert.equal(tracker.modified, true);
-    tracker.clearChangedIDs();
+    await tracker.clearChangedIDs();
 
     _("Now that the pref sync pref has been flipped, changes to it won't be picked up.");
     prefs.set("testing.int", 42);
+    await tracker.asyncObserver.promiseObserversComplete();
     Assert.equal(tracker.score, SCORE_INCREMENT_XLARGE * 3);
     Assert.equal(tracker.modified, false);
-    tracker.clearChangedIDs();
+    await tracker.clearChangedIDs();
 
     _("Changing some other random pref won't do anything.");
     prefs.set("testing.other", "blergh");
+    await tracker.asyncObserver.promiseObserversComplete();
     Assert.equal(tracker.score, SCORE_INCREMENT_XLARGE * 3);
     Assert.equal(tracker.modified, false);
 
   } finally {
-    Svc.Obs.notify("weave:engine:stop-tracking");
+    await tracker.stop();
     prefs.resetBranch("");
   }
 });
--- a/services/sync/tests/unit/test_score_triggers.js
+++ b/services/sync/tests/unit/test_score_triggers.js
@@ -59,17 +59,17 @@ add_task(async function test_tracker_sco
 
     tracker.score += SCORE_INCREMENT_SMALL;
     Assert.equal(engine.score, SCORE_INCREMENT_SMALL);
 
     Assert.equal(scoreUpdated, 1);
   } finally {
     Svc.Obs.remove("weave:engine:score:updated", onScoreUpdated);
     tracker.resetScore();
-    tracker.clearChangedIDs();
+    await tracker.clearChangedIDs();
     await Service.engineManager.unregister(engine);
   }
 });
 
 add_task(async function test_sync_triggered() {
   let server = sync_httpd_setup();
   let { engine, tracker } = await setUp(server);
 
@@ -81,17 +81,17 @@ add_task(async function test_sync_trigge
   Assert.equal(Status.login, LOGIN_SUCCEEDED);
   tracker.score += SCORE_INCREMENT_XLARGE;
 
   await promiseOneObserver("weave:service:sync:finish");
 
   await Service.startOver();
   await promiseStopServer(server);
 
-  tracker.clearChangedIDs();
+  await tracker.clearChangedIDs();
   await Service.engineManager.unregister(engine);
 });
 
 add_task(async function test_clients_engine_sync_triggered() {
   enableValidationPrefs();
 
   _("Ensure that client engine score changes trigger a sync.");
 
@@ -108,17 +108,17 @@ add_task(async function test_clients_eng
   Service.clientsEngine._tracker.score += SCORE_INCREMENT_XLARGE;
 
   await promiseOneObserver("weave:service:sync:finish");
   _("Sync due to clients engine change completed.");
 
   await Service.startOver();
   await promiseStopServer(server);
 
-  tracker.clearChangedIDs();
+  await tracker.clearChangedIDs();
   await Service.engineManager.unregister(engine);
 });
 
 add_task(async function test_incorrect_credentials_sync_not_triggered() {
   enableValidationPrefs();
 
   _("Ensure that score changes don't trigger a sync if Status.login != LOGIN_SUCCEEDED.");
   let server = sync_httpd_setup();
@@ -142,11 +142,11 @@ add_task(async function test_incorrect_c
 
   Svc.Obs.remove("weave:service:sync:start", onSyncStart);
 
   Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED);
 
   await Service.startOver();
   await promiseStopServer(server);
 
-  tracker.clearChangedIDs();
+  await tracker.clearChangedIDs();
   await Service.engineManager.unregister(engine);
 });
--- a/services/sync/tests/unit/test_syncengine_sync.js
+++ b/services/sync/tests/unit/test_syncengine_sync.js
@@ -14,17 +14,17 @@ ChromeUtils.import("resource://testing-c
 function makeRotaryEngine() {
   return new RotaryEngine(Service);
 }
 
 async function clean(engine) {
   Svc.Prefs.resetBranch("");
   Svc.Prefs.set("log.logger.engine.rotary", "Trace");
   Service.recordManager.clearCache();
-  engine._tracker.clearChangedIDs();
+  await engine._tracker.clearChangedIDs();
   await engine.finalize();
 }
 
 async function cleanAndGo(engine, server) {
   await clean(engine);
   await promiseStopServer(server);
 }
 
@@ -93,17 +93,18 @@ add_task(async function test_syncStartup
 
   await SyncTestingInfrastructure(server);
 
   let engine = makeRotaryEngine();
   engine._store.items = {rekolok: "Rekonstruktionslokomotive"};
   try {
 
     // Confirm initial environment
-    Assert.equal(engine._tracker.changedIDs.rekolok, undefined);
+    const changes = await engine._tracker.getChangedIDs();
+    Assert.equal(changes.rekolok, undefined);
     let metaGlobal = await Service.recordManager.get(engine.metaURL);
     Assert.equal(metaGlobal.payload.engines, undefined);
     Assert.ok(!!collection.payload("flying"));
     Assert.ok(!!collection.payload("scotsman"));
 
     engine.lastSync = Date.now() / 1000;
     engine.lastSyncLocal = Date.now();
 
@@ -168,17 +169,18 @@ add_task(async function test_syncStartup
                              {engines: {rotary: {version: engine.version,
                                                 syncID: "foobar"}}});
   server.registerPathHandler("/1.1/foo/storage/meta/global", global.handler());
 
   try {
 
     // Confirm initial environment
     Assert.equal(engine.syncID, "fake-guid-00");
-    Assert.equal(engine._tracker.changedIDs.rekolok, undefined);
+    const changes = await engine._tracker.getChangedIDs();
+    Assert.equal(changes.rekolok, undefined);
 
     engine.lastSync = Date.now() / 1000;
     engine.lastSyncLocal = Date.now();
     await engine._syncStartup();
 
     // The engine has assumed the server's syncID
     Assert.equal(engine.syncID, "foobar");
 
@@ -326,34 +328,35 @@ add_task(async function test_processInco
   let engine = makeRotaryEngine();
   engine._store.items = {newerserver: "New data, but not as new as server!",
                          olderidentical: "Older but identical",
                          updateclient: "Got data?",
                          original: "Original Entry",
                          long_original: "Long Original Entry",
                          nukeme: "Nuke me!"};
   // Make this record 1 min old, thus older than the one on the server
-  engine._tracker.addChangedID("newerserver", Date.now() / 1000 - 60);
+  await engine._tracker.addChangedID("newerserver", Date.now() / 1000 - 60);
   // This record has been changed 2 mins later than the one on the server
-  engine._tracker.addChangedID("olderidentical", Date.now() / 1000);
+  await engine._tracker.addChangedID("olderidentical", Date.now() / 1000);
 
   let meta_global = Service.recordManager.set(engine.metaURL,
                                               new WBORecord(engine.metaURL));
   meta_global.payload.engines = {rotary: {version: engine.version,
                                          syncID: engine.syncID}};
 
   try {
 
     // Confirm initial environment
     Assert.equal(engine._store.items.newrecord, undefined);
     Assert.equal(engine._store.items.newerserver, "New data, but not as new as server!");
     Assert.equal(engine._store.items.olderidentical, "Older but identical");
     Assert.equal(engine._store.items.updateclient, "Got data?");
     Assert.equal(engine._store.items.nukeme, "Nuke me!");
-    Assert.ok(engine._tracker.changedIDs.olderidentical > 0);
+    let changes = await engine._tracker.getChangedIDs();
+    Assert.ok(changes.olderidentical > 0);
 
     await engine._syncStartup();
     await engine._processIncoming();
 
     // Timestamps of last sync and last server modification are set.
     Assert.ok(engine.lastSync > 0);
     Assert.ok(engine.lastModified > 0);
 
@@ -361,17 +364,18 @@ add_task(async function test_processInco
     Assert.equal(engine._store.items.newrecord, "New stuff...");
 
     // The 'newerserver' record is updated since the server data is newer.
     Assert.equal(engine._store.items.newerserver, "New data!");
 
     // The data for 'olderidentical' is identical on the server, so
     // it's no longer marked as changed anymore.
     Assert.equal(engine._store.items.olderidentical, "Older but identical");
-    Assert.equal(engine._tracker.changedIDs.olderidentical, undefined);
+    changes = await engine._tracker.getChangedIDs();
+    Assert.equal(changes.olderidentical, undefined);
 
     // Updated with server data.
     Assert.equal(engine._store.items.updateclient, "Get this!");
 
     // The incoming ID is preferred.
     Assert.equal(engine._store.items.original, undefined);
     Assert.equal(engine._store.items.duplication, "Original Entry");
     Assert.notEqual(engine._delete.ids.indexOf("original"), -1);
@@ -455,17 +459,17 @@ add_task(async function test_processInco
   engine.lastModified = now + 1;
 
   let record = encryptPayload({id: "DUPE_INCOMING", denomination: "incoming"});
   let wbo = new ServerWBO("DUPE_INCOMING", record, now + 2);
   server.insertWBO(user, "rotary", wbo);
 
   // Simulate a locally-deleted item.
   engine._store.items = {};
-  engine._tracker.addChangedID("DUPE_LOCAL", now + 3);
+  await engine._tracker.addChangedID("DUPE_LOCAL", now + 3);
   Assert.equal(false, (await engine._store.itemExists("DUPE_LOCAL")));
   Assert.equal(false, (await engine._store.itemExists("DUPE_INCOMING")));
   Assert.equal("DUPE_LOCAL", (await engine._findDupe({id: "DUPE_INCOMING"})));
 
   engine.lastModified = server.getCollection(user, engine.name).timestamp;
   await engine._sync();
 
   // After the sync, the server's payload for the original ID should be marked
@@ -494,17 +498,17 @@ add_task(async function test_processInco
   engine.lastModified = now + 1;
 
   let record = encryptPayload({id: "DUPE_INCOMING", denomination: "incoming"});
   let wbo = new ServerWBO("DUPE_INCOMING", record, now + 2);
   server.insertWBO(user, "rotary", wbo);
 
   // Simulate a locally-deleted item.
   engine._store.items = {};
-  engine._tracker.addChangedID("DUPE_LOCAL", now + 1);
+  await engine._tracker.addChangedID("DUPE_LOCAL", now + 1);
   Assert.equal(false, (await engine._store.itemExists("DUPE_LOCAL")));
   Assert.equal(false, (await engine._store.itemExists("DUPE_INCOMING")));
   Assert.equal("DUPE_LOCAL", (await engine._findDupe({id: "DUPE_INCOMING"})));
 
   await engine._sync();
 
   // Since the remote change is newer, the incoming item should exist locally.
   do_check_attribute_count(engine._store.items, 1);
@@ -530,17 +534,17 @@ add_task(async function test_processInco
   engine.lastModified = now + 1;
 
   // The local record is newer than the incoming one, so it should be retained.
   let record = encryptPayload({id: "DUPE_INCOMING", denomination: "incoming"});
   let wbo = new ServerWBO("DUPE_INCOMING", record, now + 2);
   server.insertWBO(user, "rotary", wbo);
 
   await engine._store.create({id: "DUPE_LOCAL", denomination: "local"});
-  engine._tracker.addChangedID("DUPE_LOCAL", now + 3);
+  await engine._tracker.addChangedID("DUPE_LOCAL", now + 3);
   Assert.ok((await engine._store.itemExists("DUPE_LOCAL")));
   Assert.equal("DUPE_LOCAL", (await engine._findDupe({id: "DUPE_INCOMING"})));
 
   engine.lastModified = server.getCollection(user, engine.name).timestamp;
   await engine._sync();
 
   // The ID should have been changed to incoming.
   do_check_attribute_count(engine._store.items, 1);
@@ -569,17 +573,17 @@ add_task(async function test_processInco
   engine.lastSync = now;
   engine.lastModified = now + 1;
 
   let record = encryptPayload({id: "DUPE_INCOMING", denomination: "incoming"});
   let wbo = new ServerWBO("DUPE_INCOMING", record, now + 2);
   server.insertWBO(user, "rotary", wbo);
 
   await engine._store.create({id: "DUPE_LOCAL", denomination: "local"});
-  engine._tracker.addChangedID("DUPE_LOCAL", now + 1);
+  await engine._tracker.addChangedID("DUPE_LOCAL", now + 1);
   Assert.ok((await engine._store.itemExists("DUPE_LOCAL")));
   Assert.equal("DUPE_LOCAL", (await engine._findDupe({id: "DUPE_INCOMING"})));
 
   engine.lastModified = server.getCollection(user, engine.name).timestamp;
   await engine._sync();
 
   // The ID should have been changed to incoming.
   do_check_attribute_count(engine._store.items, 1);
@@ -1051,17 +1055,17 @@ add_task(async function test_uploadOutgo
   await SyncTestingInfrastructure(server);
   await generateNewKeys(Service.collectionKeys);
 
   let engine = makeRotaryEngine();
   engine.lastSync = 123; // needs to be non-zero so that tracker is queried
   engine._store.items = {flying: "LNER Class A3 4472",
                          scotsman: "Flying Scotsman"};
   // Mark one of these records as changed
-  engine._tracker.addChangedID("scotsman", 0);
+  await engine._tracker.addChangedID("scotsman", 0);
 
   let meta_global = Service.recordManager.set(engine.metaURL,
                                               new WBORecord(engine.metaURL));
   meta_global.payload.engines = {rotary: {version: engine.version,
                                          syncID: engine.syncID}};
 
   try {
 
@@ -1077,17 +1081,18 @@ add_task(async function test_uploadOutgo
     Assert.ok(engine.lastSyncLocal > 0);
 
     // Ensure the marked record ('scotsman') has been uploaded and is
     // no longer marked.
     Assert.equal(collection.payload("flying"), undefined);
     Assert.ok(!!collection.payload("scotsman"));
     Assert.equal(JSON.parse(collection.wbo("scotsman").data.ciphertext).id,
                  "scotsman");
-    Assert.equal(engine._tracker.changedIDs.scotsman, undefined);
+    const changes = await engine._tracker.getChangedIDs();
+    Assert.equal(changes.scotsman, undefined);
 
     // The 'flying' record wasn't marked so it wasn't uploaded
     Assert.equal(collection.payload("flying"), undefined);
 
   } finally {
     await cleanAndGo(engine, server);
   }
 });
@@ -1107,18 +1112,18 @@ async function test_uploadOutgoing_max_r
   await SyncTestingInfrastructure(server);
   await generateNewKeys(Service.collectionKeys);
 
   let engine = makeRotaryEngine();
   engine.allowSkippedRecord = allowSkippedRecord;
   engine.lastSync = 1;
   engine._store.items = { flying: "a".repeat(1024 * 1024), scotsman: "abcd" };
 
-  engine._tracker.addChangedID("flying", 1000);
-  engine._tracker.addChangedID("scotsman", 1000);
+  await engine._tracker.addChangedID("flying", 1000);
+  await engine._tracker.addChangedID("scotsman", 1000);
 
   let meta_global = Service.recordManager.set(engine.metaURL,
                                               new WBORecord(engine.metaURL));
   meta_global.payload.engines = {rotary: {version: engine.version,
                                          syncID: engine.syncID}};
 
   try {
     // Confirm initial environment
@@ -1133,27 +1138,29 @@ async function test_uploadOutgoing_max_r
       do_throw("should not get here");
     }
 
     await engine.trackRemainingChanges();
 
     // Check we uploaded the other record to the server
     Assert.ok(collection.payload("scotsman"));
     // And that we won't try to upload the huge record next time.
-    Assert.equal(engine._tracker.changedIDs.flying, undefined);
+    const changes = await engine._tracker.getChangedIDs();
+    Assert.equal(changes.flying, undefined);
 
   } catch (e) {
     if (allowSkippedRecord) {
       do_throw("should not get here");
     }
 
     await engine.trackRemainingChanges();
 
     // Check that we will try to upload the huge record next time
-    Assert.equal(engine._tracker.changedIDs.flying, 1000);
+    const changes = await engine._tracker.getChangedIDs();
+    Assert.equal(changes.flying, 1000);
   } finally {
     // Check we didn't upload the oversized record to the server
     Assert.equal(collection.payload("flying"), undefined);
     await cleanAndGo(engine, server);
   }
 }
 
 
@@ -1185,48 +1192,50 @@ add_task(async function test_uploadOutgo
   engine.lastSync = 123; // needs to be non-zero so that tracker is queried
   engine._store.items = {flying: "LNER Class A3 4472",
                          scotsman: "Flying Scotsman",
                          peppercorn: "Peppercorn Class"};
   // Mark these records as changed
   const FLYING_CHANGED = 12345;
   const SCOTSMAN_CHANGED = 23456;
   const PEPPERCORN_CHANGED = 34567;
-  engine._tracker.addChangedID("flying", FLYING_CHANGED);
-  engine._tracker.addChangedID("scotsman", SCOTSMAN_CHANGED);
-  engine._tracker.addChangedID("peppercorn", PEPPERCORN_CHANGED);
+  await engine._tracker.addChangedID("flying", FLYING_CHANGED);
+  await engine._tracker.addChangedID("scotsman", SCOTSMAN_CHANGED);
+  await engine._tracker.addChangedID("peppercorn", PEPPERCORN_CHANGED);
 
   let meta_global = Service.recordManager.set(engine.metaURL,
                                               new WBORecord(engine.metaURL));
   meta_global.payload.engines = {rotary: {version: engine.version,
                                          syncID: engine.syncID}};
 
   try {
 
     // Confirm initial environment
     Assert.equal(engine.lastSyncLocal, 0);
     Assert.equal(collection.payload("flying"), undefined);
-    Assert.equal(engine._tracker.changedIDs.flying, FLYING_CHANGED);
-    Assert.equal(engine._tracker.changedIDs.scotsman, SCOTSMAN_CHANGED);
-    Assert.equal(engine._tracker.changedIDs.peppercorn, PEPPERCORN_CHANGED);
+    let changes = await engine._tracker.getChangedIDs();
+    Assert.equal(changes.flying, FLYING_CHANGED);
+    Assert.equal(changes.scotsman, SCOTSMAN_CHANGED);
+    Assert.equal(changes.peppercorn, PEPPERCORN_CHANGED);
 
     engine.enabled = true;
     await sync_engine_and_validate_telem(engine, true);
 
     // Local timestamp has been set.
     Assert.ok(engine.lastSyncLocal > 0);
 
     // Ensure the 'flying' record has been uploaded and is no longer marked.
     Assert.ok(!!collection.payload("flying"));
-    Assert.equal(engine._tracker.changedIDs.flying, undefined);
+    changes = await engine._tracker.getChangedIDs();
+    Assert.equal(changes.flying, undefined);
 
     // The 'scotsman' and 'peppercorn' records couldn't be uploaded so
     // they weren't cleared from the tracker.
-    Assert.equal(engine._tracker.changedIDs.scotsman, SCOTSMAN_CHANGED);
-    Assert.equal(engine._tracker.changedIDs.peppercorn, PEPPERCORN_CHANGED);
+    Assert.equal(changes.scotsman, SCOTSMAN_CHANGED);
+    Assert.equal(changes.peppercorn, PEPPERCORN_CHANGED);
 
   } finally {
     await promiseClean(engine, server);
   }
 });
 
 async function createRecordFailTelemetry(allowSkippedRecord) {
   Service.identity.username = "foo";
@@ -1250,63 +1259,67 @@ async function createRecordFailTelemetry
     return oldCreateRecord.call(engine._store, id, col);
   };
   engine.lastSync = 123; // needs to be non-zero so that tracker is queried
   engine._store.items = {flying: "LNER Class A3 4472",
                          scotsman: "Flying Scotsman"};
   // Mark these records as changed
   const FLYING_CHANGED = 12345;
   const SCOTSMAN_CHANGED = 23456;
-  engine._tracker.addChangedID("flying", FLYING_CHANGED);
-  engine._tracker.addChangedID("scotsman", SCOTSMAN_CHANGED);
+  await engine._tracker.addChangedID("flying", FLYING_CHANGED);
+  await engine._tracker.addChangedID("scotsman", SCOTSMAN_CHANGED);
 
   let meta_global = Service.recordManager.set(engine.metaURL,
                                               new WBORecord(engine.metaURL));
   meta_global.payload.engines = {rotary: {version: engine.version,
                                          syncID: engine.syncID}};
 
   let ping;
   try {
     // Confirm initial environment
     Assert.equal(engine.lastSyncLocal, 0);
     Assert.equal(collection.payload("flying"), undefined);
-    Assert.equal(engine._tracker.changedIDs.flying, FLYING_CHANGED);
-    Assert.equal(engine._tracker.changedIDs.scotsman, SCOTSMAN_CHANGED);
+    let changes = await engine._tracker.getChangedIDs();
+    Assert.equal(changes.flying, FLYING_CHANGED);
+    Assert.equal(changes.scotsman, SCOTSMAN_CHANGED);
 
     engine.enabled = true;
     ping = await sync_engine_and_validate_telem(engine, true, onErrorPing => {
       ping = onErrorPing;
     });
 
     if (!allowSkippedRecord) {
       do_throw("should not get here");
     }
 
     // Ensure the 'flying' record has been uploaded and is no longer marked.
     Assert.ok(!!collection.payload("flying"));
-    Assert.equal(engine._tracker.changedIDs.flying, undefined);
+    changes = await engine._tracker.getChangedIDs();
+    Assert.equal(changes.flying, undefined);
   } catch (err) {
     if (allowSkippedRecord) {
       do_throw("should not get here");
     }
 
     // Ensure the 'flying' record has not been uploaded and is still marked
     Assert.ok(!collection.payload("flying"));
-    Assert.ok(engine._tracker.changedIDs.flying);
+    const changes = await engine._tracker.getChangedIDs();
+    Assert.ok(changes.flying);
   } finally {
     // Local timestamp has been set.
     Assert.ok(engine.lastSyncLocal > 0);
 
     // We reported in telemetry that we failed a record
     Assert.equal(ping.engines[0].outgoing[0].failed, 1);
 
     // In any case, the 'scotsman' record couldn't be created so it wasn't
     // uploaded nor it was not cleared from the tracker.
     Assert.ok(!collection.payload("scotsman"));
-    Assert.equal(engine._tracker.changedIDs.scotsman, SCOTSMAN_CHANGED);
+    const changes = await engine._tracker.getChangedIDs();
+    Assert.equal(changes.scotsman, SCOTSMAN_CHANGED);
 
     engine._store.createRecord = oldCreateRecord;
     await promiseClean(engine, server);
   }
 }
 
 add_task(async function test_uploadOutgoing_createRecord_throws_reported_telemetry() {
   _("SyncEngine._uploadOutgoing reports a failed record to telemetry if createRecord throws");
@@ -1321,17 +1334,17 @@ add_task(async function test_uploadOutgo
 add_task(async function test_uploadOutgoing_largeRecords() {
   _("SyncEngine._uploadOutgoing throws on records larger than the max record payload size");
 
   let collection = new ServerCollection();
 
   let engine = makeRotaryEngine();
   engine.allowSkippedRecord = false;
   engine._store.items["large-item"] = "Y".repeat(Service.getMaxRecordPayloadSize() * 2);
-  engine._tracker.addChangedID("large-item", 0);
+  await engine._tracker.addChangedID("large-item", 0);
   collection.insert("large-item");
 
 
   let meta_global = Service.recordManager.set(engine.metaURL,
                                               new WBORecord(engine.metaURL));
   meta_global.payload.engines = {rotary: {version: engine.version,
                                          syncID: engine.syncID}};
 
@@ -1493,17 +1506,17 @@ add_task(async function test_sync_partia
       return orig.apply(this, arguments);
     };
   }(collection.post));
 
   // Create a bunch of records (and server side handlers)
   for (let i = 0; i < 234; i++) {
     let id = "record-no-" + i;
     engine._store.items[id] = "Record No. " + i;
-    engine._tracker.addChangedID(id, i);
+    await engine._tracker.addChangedID(id, i);
     // Let two items in the first upload batch fail.
     if ((i != 23) && (i != 42)) {
       collection.insert(id);
     }
   }
 
   let meta_global = Service.recordManager.set(engine.metaURL,
                                               new WBORecord(engine.metaURL));
@@ -1520,26 +1533,27 @@ add_task(async function test_sync_partia
       error = ex;
     }
 
     ok(!!error);
 
     // The timestamp has been updated.
     Assert.ok(engine.lastSyncLocal > 456);
 
+    const changes = await engine._tracker.getChangedIDs();
     for (let i = 0; i < 234; i++) {
       let id = "record-no-" + i;
       // Ensure failed records are back in the tracker:
       // * records no. 23 and 42 were rejected by the server,
       // * records after the third batch and higher couldn't be uploaded because
       //   we failed hard on the 3rd upload.
       if ((i == 23) || (i == 42) || (i >= 200))
-        Assert.equal(engine._tracker.changedIDs[id], i);
+        Assert.equal(changes[id], i);
       else
-        Assert.equal(false, id in engine._tracker.changedIDs);
+        Assert.equal(false, id in changes);
     }
 
   } finally {
     Service.serverConfiguration = oldServerConfiguration;
     await promiseClean(engine, server);
   }
 });
 
--- a/services/sync/tests/unit/test_tab_tracker.js
+++ b/services/sync/tests/unit/test_tab_tracker.js
@@ -59,69 +59,69 @@ add_task(async function run_test() {
   Assert.ok(tracker.modified);
   Assert.ok(Utils.deepEquals(Object.keys((await engine.getChangedIDs())),
                              [clientsEngine.localID]));
 
   let logs;
 
   _("Test listeners are registered on windows");
   logs = fakeSvcWinMediator();
-  Svc.Obs.notify("weave:engine:start-tracking");
+  tracker.start();
   Assert.equal(logs.length, 2);
   for (let log of logs) {
     Assert.equal(log.addTopics.length, 5);
     Assert.ok(log.addTopics.indexOf("pageshow") >= 0);
     Assert.ok(log.addTopics.indexOf("TabOpen") >= 0);
     Assert.ok(log.addTopics.indexOf("TabClose") >= 0);
     Assert.ok(log.addTopics.indexOf("TabSelect") >= 0);
     Assert.ok(log.addTopics.indexOf("unload") >= 0);
     Assert.equal(log.remTopics.length, 0);
     Assert.equal(log.numAPL, 1, "Added 1 progress listener");
     Assert.equal(log.numRPL, 0, "Didn't remove a progress listener");
   }
 
   _("Test listeners are unregistered on windows");
   logs = fakeSvcWinMediator();
-  Svc.Obs.notify("weave:engine:stop-tracking");
+  await tracker.stop();
   Assert.equal(logs.length, 2);
   for (let log of logs) {
     Assert.equal(log.addTopics.length, 0);
     Assert.equal(log.remTopics.length, 5);
     Assert.ok(log.remTopics.indexOf("pageshow") >= 0);
     Assert.ok(log.remTopics.indexOf("TabOpen") >= 0);
     Assert.ok(log.remTopics.indexOf("TabClose") >= 0);
     Assert.ok(log.remTopics.indexOf("TabSelect") >= 0);
     Assert.ok(log.remTopics.indexOf("unload") >= 0);
     Assert.equal(log.numAPL, 0, "Didn't add a progress listener");
     Assert.equal(log.numRPL, 1, "Removed 1 progress listener");
   }
 
   _("Test tab listener");
   for (let evttype of ["TabOpen", "TabClose", "TabSelect"]) {
     // Pretend we just synced.
-    tracker.clearChangedIDs();
+    await tracker.clearChangedIDs();
     Assert.ok(!tracker.modified);
 
     // Send a fake tab event
     tracker.onTab({type: evttype, originalTarget: evttype});
     Assert.ok(tracker.modified);
     Assert.ok(Utils.deepEquals(Object.keys((await engine.getChangedIDs())),
                                [clientsEngine.localID]));
   }
 
   // Pretend we just synced.
-  tracker.clearChangedIDs();
+  await tracker.clearChangedIDs();
   Assert.ok(!tracker.modified);
 
   tracker.onTab({type: "pageshow", originalTarget: "pageshow"});
   Assert.ok(Utils.deepEquals(Object.keys((await engine.getChangedIDs())),
                              [clientsEngine.localID]));
 
   // Pretend we just synced and saw some progress listeners.
-  tracker.clearChangedIDs();
+  await tracker.clearChangedIDs();
   Assert.ok(!tracker.modified);
   tracker.onLocationChange({ isTopLevel: false }, undefined, undefined, 0);
   Assert.ok(!tracker.modified, "non-toplevel request didn't flag as modified");
 
   tracker.onLocationChange({ isTopLevel: true }, undefined, undefined,
                            Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT);
   Assert.ok(!tracker.modified, "location change within the same document request didn't flag as modified");
 
--- a/services/sync/tests/unit/test_telemetry.js
+++ b/services/sync/tests/unit/test_telemetry.js
@@ -53,17 +53,17 @@ SteamEngine.prototype = {
 
 function BogusEngine(service) {
   Engine.call(this, "bogus", service);
 }
 
 BogusEngine.prototype = Object.create(SteamEngine.prototype);
 
 async function cleanAndGo(engine, server) {
-  engine._tracker.clearChangedIDs();
+  await engine._tracker.clearChangedIDs();
   Svc.Prefs.resetBranch("");
   syncTestLogging();
   Service.recordManager.clearCache();
   await promiseStopServer(server);
 }
 
 add_task(async function setup() {
   // Avoid addon manager complaining about not being initialized
@@ -215,37 +215,39 @@ add_task(async function test_upload_fail
   engine._store.items = {
     flying: "LNER Class A3 4472",
     scotsman: "Flying Scotsman",
     peppercorn: "Peppercorn Class"
   };
   const FLYING_CHANGED = 12345;
   const SCOTSMAN_CHANGED = 23456;
   const PEPPERCORN_CHANGED = 34567;
-  engine._tracker.addChangedID("flying", FLYING_CHANGED);
-  engine._tracker.addChangedID("scotsman", SCOTSMAN_CHANGED);
-  engine._tracker.addChangedID("peppercorn", PEPPERCORN_CHANGED);
+  await engine._tracker.addChangedID("flying", FLYING_CHANGED);
+  await engine._tracker.addChangedID("scotsman", SCOTSMAN_CHANGED);
+  await engine._tracker.addChangedID("peppercorn", PEPPERCORN_CHANGED);
 
   let meta_global = Service.recordManager.set(engine.metaURL, new WBORecord(engine.metaURL));
   meta_global.payload.engines = { rotary: { version: engine.version, syncID: engine.syncID } };
 
   try {
+    let changes = await engine._tracker.getChangedIDs();
     _(`test_upload_failed: Rotary tracker contents at first sync: ${
-      JSON.stringify(engine._tracker.changedIDs)}`);
+      JSON.stringify(changes)}`);
     engine.enabled = true;
     let ping = await sync_engine_and_validate_telem(engine, true);
     ok(!!ping);
     equal(ping.engines.length, 1);
     equal(ping.engines[0].incoming, null);
     deepEqual(ping.engines[0].outgoing, [{ sent: 3, failed: 2 }]);
     engine.lastSync = 123;
     engine.lastSyncLocal = 456;
 
+    changes = await engine._tracker.getChangedIDs();
     _(`test_upload_failed: Rotary tracker contents at second sync: ${
-      JSON.stringify(engine._tracker.changedIDs)}`);
+      JSON.stringify(changes)}`);
     ping = await sync_engine_and_validate_telem(engine, true);
     ok(!!ping);
     equal(ping.engines.length, 1);
     equal(ping.engines[0].incoming.reconciled, 1);
     deepEqual(ping.engines[0].outgoing, [{ sent: 2, failed: 2 }]);
 
   } finally {
     await cleanAndGo(engine, server);
@@ -265,54 +267,56 @@ add_task(async function test_sync_partia
   engine.lastSync = 123;
   engine.lastSyncLocal = 456;
 
 
   // Create a bunch of records (and server side handlers)
   for (let i = 0; i < 234; i++) {
     let id = "record-no-" + i;
     engine._store.items[id] = "Record No. " + i;
-    engine._tracker.addChangedID(id, i);
+    await engine._tracker.addChangedID(id, i);
     // Let two items in the first upload batch fail.
     if (i != 23 && i != 42) {
       collection.insert(id);
     }
   }
 
   let meta_global = Service.recordManager.set(engine.metaURL,
                                               new WBORecord(engine.metaURL));
   meta_global.payload.engines = {rotary: {version: engine.version,
                                           syncID: engine.syncID}};
 
   try {
+    let changes = await engine._tracker.getChangedIDs();
     _(`test_sync_partialUpload: Rotary tracker contents at first sync: ${
-      JSON.stringify(engine._tracker.changedIDs)}`);
+      JSON.stringify(changes)}`);
     engine.enabled = true;
     let ping = await sync_engine_and_validate_telem(engine, true);
 
     ok(!!ping);
     ok(!ping.failureReason);
     equal(ping.engines.length, 1);
     equal(ping.engines[0].name, "rotary");
     ok(!ping.engines[0].incoming);
     ok(!ping.engines[0].failureReason);
     deepEqual(ping.engines[0].outgoing, [{ sent: 234, failed: 2 }]);
 
     collection.post = function() { throw new Error("Failure"); };
 
     engine._store.items["record-no-1000"] = "Record No. 1000";
-    engine._tracker.addChangedID("record-no-1000", 1000);
+    await engine._tracker.addChangedID("record-no-1000", 1000);
     collection.insert("record-no-1000", 1000);
 
     engine.lastSync = 123;
     engine.lastSyncLocal = 456;
     ping = null;
 
+    changes = await engine._tracker.getChangedIDs();
     _(`test_sync_partialUpload: Rotary tracker contents at second sync: ${
-      JSON.stringify(engine._tracker.changedIDs)}`);
+      JSON.stringify(changes)}`);
     try {
       // should throw
       await sync_engine_and_validate_telem(engine, true, errPing => ping = errPing);
     } catch (e) {}
     // It would be nice if we had a more descriptive error for this...
     let uploadFailureError = {
       name: "othererror",
       error: "error.engine.reason.record_upload_fail"
@@ -343,18 +347,19 @@ add_task(async function test_generic_eng
   let engine = Service.engineManager.get("steam");
   engine.enabled = true;
   let server = await serverForFoo(engine);
   await SyncTestingInfrastructure(server);
   let e = new Error("generic failure message");
   engine._errToThrow = e;
 
   try {
+    const changes = await engine._tracker.getChangedIDs();
     _(`test_generic_engine_fail: Steam tracker contents: ${
-      JSON.stringify(engine._tracker.changedIDs)}`);
+      JSON.stringify(changes)}`);
     let ping = await sync_and_validate_telem(true);
     equal(ping.status.service, SYNC_FAILED_PARTIAL);
     deepEqual(ping.engines.find(err => err.name === "steam").failureReason, {
       name: "unexpectederror",
       error: String(e)
     });
   } finally {
     await cleanAndGo(engine, server);
@@ -407,18 +412,19 @@ add_task(async function test_engine_fail
     // filesystem.)
     await Utils._real_jsonMove("file-does-not-exist", "anything", {});
   } catch (ex) {
     engine._errToThrow = ex;
   }
   ok(engine._errToThrow, "expecting exception");
 
   try {
+    const changes = await engine._tracker.getChangedIDs();
     _(`test_engine_fail_ioerror: Steam tracker contents: ${
-      JSON.stringify(engine._tracker.changedIDs)}`);
+      JSON.stringify(changes)}`);
     let ping = await sync_and_validate_telem(true);
     equal(ping.status.service, SYNC_FAILED_PARTIAL);
     let failureReason = ping.engines.find(e => e.name === "steam").failureReason;
     equal(failureReason.name, "unexpectederror");
     // ensure the profile dir in the exception message has been stripped.
     ok(!failureReason.error.includes(OS.Constants.Path.profileDir), failureReason.error);
     ok(failureReason.error.includes("[profileDir]"), failureReason.error);
   } finally {
@@ -433,18 +439,19 @@ add_task(async function test_clean_urls(
   await Service.engineManager.register(SteamEngine);
   let engine = Service.engineManager.get("steam");
   engine.enabled = true;
   let server = await serverForFoo(engine);
   await SyncTestingInfrastructure(server);
   engine._errToThrow = new TypeError("http://www.google .com is not a valid URL.");
 
   try {
+    const changes = await engine._tracker.getChangedIDs();
     _(`test_clean_urls: Steam tracker contents: ${
-      JSON.stringify(engine._tracker.changedIDs)}`);
+      JSON.stringify(changes)}`);
     let ping = await sync_and_validate_telem(true);
     equal(ping.status.service, SYNC_FAILED_PARTIAL);
     let failureReason = ping.engines.find(e => e.name === "steam").failureReason;
     equal(failureReason.name, "unexpectederror");
     equal(failureReason.error, "<URL> is not a valid URL.");
     // Handle other errors that include urls.
     engine._errToThrow = "Other error message that includes some:url/foo/bar/ in it.";
     ping = await sync_and_validate_telem(true);
@@ -467,18 +474,19 @@ add_task(async function test_initial_syn
   engine.enabled = true;
   // These are the only ones who actually have things to sync at startup.
   let engineNames = ["clients", "bookmarks", "prefs", "tabs"];
   let server = await serverForEnginesWithKeys({"foo": "password"}, ["bookmarks", "prefs", "tabs"].map(name =>
     Service.engineManager.get(name)
   ));
   await SyncTestingInfrastructure(server);
   try {
+    const changes = await engine._tracker.getChangedIDs();
     _(`test_initial_sync_engines: Steam tracker contents: ${
-      JSON.stringify(engine._tracker.changedIDs)}`);
+      JSON.stringify(changes)}`);
     let ping = await wait_for_ping(() => Service.sync(), true);
 
     equal(ping.engines.find(e => e.name === "clients").outgoing[0].sent, 1);
     equal(ping.engines.find(e => e.name === "tabs").outgoing[0].sent, 1);
 
     // for the rest we don't care about specifics
     for (let e of ping.engines) {
       if (!engineNames.includes(engine.name)) {
@@ -501,18 +509,19 @@ add_task(async function test_nserror() {
 
   await Service.engineManager.register(SteamEngine);
   let engine = Service.engineManager.get("steam");
   engine.enabled = true;
   let server = await serverForFoo(engine);
   await SyncTestingInfrastructure(server);
   engine._errToThrow = Components.Exception("NS_ERROR_UNKNOWN_HOST", Cr.NS_ERROR_UNKNOWN_HOST);
   try {
+    const changes = await engine._tracker.getChangedIDs();
     _(`test_nserror: Steam tracker contents: ${
-      JSON.stringify(engine._tracker.changedIDs)}`);
+      JSON.stringify(changes)}`);
     let ping = await sync_and_validate_telem(true);
     deepEqual(ping.status, {
       service: SYNC_FAILED_PARTIAL,
       sync: LOGIN_FAILED_NETWORK_ERROR
     });
     let enginePing = ping.engines.find(e => e.name === "steam");
     deepEqual(enginePing.failureReason, {
       name: "nserror",
@@ -531,18 +540,19 @@ add_task(async function test_sync_why() 
   let engine = Service.engineManager.get("steam");
   engine.enabled = true;
   let server = await serverForFoo(engine);
   await SyncTestingInfrastructure(server);
   let e = new Error("generic failure message");
   engine._errToThrow = e;
 
   try {
+    const changes = await engine._tracker.getChangedIDs();
     _(`test_generic_engine_fail: Steam tracker contents: ${
-      JSON.stringify(engine._tracker.changedIDs)}`);
+      JSON.stringify(changes)}`);
     let ping = await wait_for_ping(() => Service.sync({why: "user"}), true, false);
     _(JSON.stringify(ping));
     equal(ping.why, "user");
   } finally {
     await cleanAndGo(engine, server);
     await Service.engineManager.unregister(engine);
   }
 });
--- a/services/sync/tests/unit/test_tracker_addChanged.js
+++ b/services/sync/tests/unit/test_tracker_addChanged.js
@@ -7,51 +7,57 @@ ChromeUtils.import("resource://services-
 
 add_task(async function test_tracker_basics() {
   let tracker = new Tracker("Tracker", Service);
   tracker.persistChangedIDs = false;
 
   let id = "the_id!";
 
   _("Make sure nothing exists yet..");
-  Assert.equal(tracker.changedIDs[id], null);
+  let changes = await tracker.getChangedIDs();
+  Assert.equal(changes[id], null);
 
   _("Make sure adding of time 0 works");
-  tracker.addChangedID(id, 0);
-  Assert.equal(tracker.changedIDs[id], 0);
+  await tracker.addChangedID(id, 0);
+  changes = await tracker.getChangedIDs();
+  Assert.equal(changes[id], 0);
 
   _("A newer time will replace the old 0");
-  tracker.addChangedID(id, 10);
-  Assert.equal(tracker.changedIDs[id], 10);
+  await tracker.addChangedID(id, 10);
+  changes = await tracker.getChangedIDs();
+  Assert.equal(changes[id], 10);
 
   _("An older time will not replace the newer 10");
-  tracker.addChangedID(id, 5);
-  Assert.equal(tracker.changedIDs[id], 10);
+  await tracker.addChangedID(id, 5);
+  changes = await tracker.getChangedIDs();
+  Assert.equal(changes[id], 10);
 
   _("Adding without time defaults to current time");
-  tracker.addChangedID(id);
-  Assert.ok(tracker.changedIDs[id] > 10);
+  await tracker.addChangedID(id);
+  changes = await tracker.getChangedIDs();
+  Assert.ok(changes[id] > 10);
 });
 
 add_task(async function test_tracker_persistence() {
   let tracker = new Tracker("Tracker", Service);
   let id = "abcdef";
 
   tracker.persistChangedIDs = true;
 
   let promiseSave = new Promise((resolve, reject) => {
     let save = tracker._storage._save;
     tracker._storage._save = function() {
       save.call(tracker._storage).then(resolve, reject);
     };
   });
 
-  tracker.addChangedID(id, 5);
+  await tracker.addChangedID(id, 5);
 
   await promiseSave;
 
   _("IDs saved.");
-  Assert.equal(5, tracker.changedIDs[id]);
+  const changes = await tracker.getChangedIDs();
+  Assert.equal(5, changes[id]);
 
   let json = await Utils.jsonLoad("changes/tracker", tracker);
   Assert.equal(5, json[id]);
   tracker.persistChangedIDs = false;
 });
--- a/services/sync/tps/extensions/tps/resource/tps.jsm
+++ b/services/sync/tps/extensions/tps/resource/tps.jsm
@@ -93,18 +93,18 @@ const ACTIONS = [
   ACTION_VERIFY_NOT,
 ];
 
 const OBSERVER_TOPICS = ["fxaccounts:onlogin",
                          "fxaccounts:onlogout",
                          "private-browsing",
                          "profile-before-change",
                          "sessionstore-windows-restored",
-                         "weave:engine:start-tracking",
-                         "weave:engine:stop-tracking",
+                         "weave:service:tracking-started",
+                         "weave:service:tracking-stopped",
                          "weave:service:login:error",
                          "weave:service:setup-complete",
                          "weave:service:sync:finish",
                          "weave:service:sync:delayed",
                          "weave:service:sync:error",
                          "weave:service:sync:start",
                          "weave:service:resyncs-finished",
                          "places-browser-init-complete",
@@ -242,21 +242,21 @@ var TPS = {
           // Ensure that the sync operation has been started by TPS
           if (!this._triggeredSync) {
             this.DumpError("Automatic sync got triggered, which is not allowed.");
           }
 
           this._syncActive = true;
           break;
 
-        case "weave:engine:start-tracking":
+        case "weave:service:tracking-started":
           this._isTracking = true;
           break;
 
-        case "weave:engine:stop-tracking":
+        case "weave:service:tracking-stopped":
           this._isTracking = false;
           break;
       }
     } catch (e) {
       this.DumpError("Observer failed", e);
 
     }
   },
@@ -1094,17 +1094,17 @@ var TPS = {
     }
   },
 
   /**
    * Waits for Sync to start tracking before returning.
    */
   async waitForTracking() {
     if (!this._isTracking) {
-      await this.waitForEvent("weave:engine:start-tracking");
+      await this.waitForEvent("weave:service:tracking-started");
     }
   },
 
   /**
    * Login on the server
    */
   async Login(force) {
     if ((await Authentication.isLoggedIn()) && !force) {
--- a/toolkit/components/places/tests/unifiedcomplete/test_remote_tab_matches.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_remote_tab_matches.js
@@ -12,16 +12,17 @@ Services.prefs.setCharPref("services.syn
 // engine. We pass a constructor that Sync creates.
 function MockTabsEngine() {
   this.clients = null; // We'll set this dynamically
 }
 
 MockTabsEngine.prototype = {
   name: "tabs",
 
+  startTracking() {},
   getAllClients() {
     return this.clients;
   },
 };
 
 // A clients engine that doesn't need to be a constructor.
 let MockClientsEngine = {
   isMobile(guid) {