Bug 1536170 - Replace all usage of Async.yieldingIterator with Async.yieldingForEach r=tcsc
☠☠ backed out by fd8c9a28b4d4 ☠ ☠
authorBarret Rennie <barret@brennie.ca>
Thu, 11 Apr 2019 18:40:02 +0000
changeset 469068 57c26f8e0bf7ab4e1d23bdbf68ed91c0bd925204
parent 469067 ccea2e827d9dff136b8f6ac9b357e3fabb5e3ec7
child 469069 2ac987c37cda1baa5601f0578db03c6860824ccf
push id35856
push usercsabou@mozilla.com
push dateFri, 12 Apr 2019 03:19:48 +0000
treeherdermozilla-central@940684cd1065 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstcsc
bugs1536170
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1536170 - Replace all usage of Async.yieldingIterator with Async.yieldingForEach r=tcsc Differential Revision: https://phabricator.services.mozilla.com/D26593
services/common/async.js
services/common/tests/unit/test_async_iterator.js
services/common/tests/unit/xpcshell.ini
services/sync/modules/engines/history.js
toolkit/components/places/SyncedBookmarksMirror.jsm
--- a/services/common/async.js
+++ b/services/common/async.js
@@ -155,40 +155,16 @@ var Async = {
         await Async.promiseYield();
         Async.checkAppReady();
       }
     }
 
     return false;
   },
 
-  /**
-   * Turn a synchronous iterator/iterable into an async iterator that yields in
-   * the same manner as Async.yieldingForEach.
-   *
-   * @param iterable {Iterable}
-   *        The iterable or iterator that should be wrapped.
-   *
-   * @param yieldEvery {number|object}
-   *        Either an existing Async.yieldState to use, or a number to provide as
-   *        the argument to async.yieldState.
-   */
-  async* yieldingIterator(iterable, yieldEvery = 50) {
-    const yieldState = typeof yieldEvery === "number" ? Async.yieldState(yieldEvery) : yieldEvery;
-
-    for (const item of iterable) {
-      yield item;
-
-      if (yieldState.shouldYield()) {
-        await Async.promiseYield();
-        Async.checkAppReady();
-      }
-    }
-  },
-
   asyncQueueCaller(log) {
     return new AsyncQueueCaller(log);
   },
 
   asyncObserver(log, obj) {
     return new AsyncObserver(log, obj);
   },
 };
deleted file mode 100644
--- a/services/common/tests/unit/test_async_iterator.js
+++ /dev/null
@@ -1,51 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-const {Async} = ChromeUtils.import("resource://services-common/async.js");
-const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
-const {sinon} = ChromeUtils.import("resource://testing-common/Sinon.jsm");
-
-function makeArray(length) {
-  // Start at 1 so that we can just divide by yieldEvery to get the expected
-  // call count. (we exp)
-  return Array.from({ length }, (v, i) => i + 1);
-}
-
-// Adjust if we ever change the default
-const DEFAULT_YIELD_EVERY = 50;
-
-async function checkIterYields(iterator, yieldEvery = DEFAULT_YIELD_EVERY) {
-  let spy = sinon.spy(Async, "promiseYield");
-  try {
-    for await (let i of iterator) {
-      let expectCount = Math.floor(i / yieldEvery);
-      Assert.equal(spy.callCount, expectCount);
-    }
-  } finally {
-    spy.restore();
-  }
-}
-
-add_task(async function testWrapIterable() {
-  let iterator = Async.yieldingIterator(makeArray(DEFAULT_YIELD_EVERY * 2));
-  await checkIterYields(iterator);
-});
-
-add_task(async function testWrapIterator() {
-  let innerIter = makeArray(DEFAULT_YIELD_EVERY * 2)[Symbol.iterator]();
-  await checkIterYields(Async.yieldingIterator(innerIter));
-});
-
-add_task(async function testNumberArgument() {
-  const yieldEvery = 10;
-  let iterator = Async.yieldingIterator(makeArray(yieldEvery * 2), yieldEvery);
-  await checkIterYields(iterator, yieldEvery);
-});
-
-add_task(async function testCustomJankYielder() {
-  let fakeJankYielderSpy = sinon.spy();
-  let iter = Async.yieldingIterator(makeArray(10), fakeJankYielderSpy);
-  for await (let i of iter) {
-    Assert.equal(fakeJankYielderSpy.callCount, i);
-  }
-});
--- a/services/common/tests/unit/xpcshell.ini
+++ b/services/common/tests/unit/xpcshell.ini
@@ -38,17 +38,16 @@ tags = remote-settings blocklist
 [test_utils_makeURI.js]
 [test_utils_namedTimer.js]
 [test_utils_sets.js]
 [test_utils_utf8.js]
 [test_utils_uuid.js]
 
 [test_async_chain.js]
 [test_async_foreach.js]
-[test_async_iterator.js]
 
 [test_hawkclient.js]
 skip-if = os == "android"
 [test_hawkrequest.js]
 skip-if = os == "android"
 
 [test_logmanager.js]
 [test_observers.js]
--- a/services/sync/modules/engines/history.js
+++ b/services/sync/modules/engines/history.js
@@ -175,17 +175,17 @@ HistoryStore.prototype = {
   },
 
   async applyIncomingBatch(records) {
     // Convert incoming records to mozIPlaceInfo objects which are applied as
     // either history additions or removals.
     let failed = [];
     let toAdd = [];
     let toRemove = [];
-    for await (let record of Async.yieldingIterator(records)) {
+    await Async.yieldingForEach(records, async (record) => {
       if (record.deleted) {
         toRemove.push(record);
       } else {
         try {
           let pageInfo = await this._recordToPlaceInfo(record);
           if (pageInfo) {
             toAdd.push(pageInfo);
           }
@@ -193,17 +193,17 @@ HistoryStore.prototype = {
           if (Async.isShutdownException(ex)) {
             throw ex;
           }
           this._log.error("Failed to create a place info", ex);
           this._log.trace("The record that failed", record);
           failed.push(record.id);
         }
       }
-    }
+    });
     if (toAdd.length || toRemove.length) {
       // We want to notify history observers that a batch operation is underway
       // so they don't do lots of work for each incoming record.
       let observers = PlacesUtils.history.getObservers();
       const notifyHistoryObservers = (notification) => {
         for (let observer of observers) {
           try {
             observer[notification]();
@@ -216,28 +216,28 @@ HistoryStore.prototype = {
       };
       notifyHistoryObservers("onBeginUpdateBatch");
       try {
         if (toRemove.length) {
           // PlacesUtils.history.remove takes an array of visits to remove,
           // but the error semantics are tricky - a single "bad" entry will cause
           // an exception before anything is removed. So we do remove them one at
           // a time.
-          for await (let record of Async.yieldingIterator(toRemove)) {
+          await Async.yieldingForEach(toRemove, async (record) => {
             try {
               await this.remove(record);
             } catch (ex) {
               if (Async.isShutdownException(ex)) {
                 throw ex;
               }
               this._log.error("Failed to delete a place info", ex);
               this._log.trace("The record that failed", record);
               failed.push(record.id);
             }
-          }
+          });
         }
         for (let chunk of this._generateChunks(toAdd)) {
           // Per bug 1415560, we ignore any exceptions returned by insertMany
           // as they are likely to be spurious. We do supply an onError handler
           // and log the exceptions seen there as they are likely to be
           // informative, but we still never abort the sync based on them.
           try {
             await PlacesUtils.history.insertMany(chunk, null, failedVisit => {
--- a/toolkit/components/places/SyncedBookmarksMirror.jsm
+++ b/toolkit/components/places/SyncedBookmarksMirror.jsm
@@ -123,19 +123,16 @@ const SQLITE_MAX_VARIABLE_NUMBER = 999;
 // The current mirror database schema version. Bump for migrations, then add
 // migration code to `migrateMirrorSchema`.
 const MIRROR_SCHEMA_VERSION = 4;
 
 const DEFAULT_MAX_FRECENCIES_TO_RECALCULATE = 400;
 
 // Use a shared jankYielder in these functions
 XPCOMUtils.defineLazyGetter(this, "yieldState", () => Async.yieldState());
-function yieldingIterator(collection) {
-  return Async.yieldingIterator(collection, yieldState);
-}
 
 /** Adapts a `Log.jsm` logger to a `mozISyncedBookmarksMirrorLogger`. */
 class MirrorLoggerAdapter {
   constructor(log) {
     this.log = log;
   }
 
   get maxLevel() {
@@ -399,52 +396,52 @@ class SyncedBookmarksMirror {
    *        `true` for incoming records, and `false` for successfully uploaded
    *        records. Tests can also pass `false` to set up an existing mirror.
    */
   async store(records, { needsMerge = true } = {}) {
     let options = { needsMerge };
     await this.db.executeBeforeShutdown(
       "SyncedBookmarksMirror: store",
       db => db.executeTransaction(async () => {
-        for await (let record of yieldingIterator(records)) {
+        await Async.yieldingForEach(records, async (record) => {
           let guid = PlacesSyncUtils.bookmarks.recordIdToGuid(record.id);
           if (guid == PlacesUtils.bookmarks.rootGuid) {
             // The engine should hard DELETE Places roots from the server.
             throw new TypeError("Can't store Places root");
           }
           MirrorLog.trace(`Storing in mirror: ${record.cleartextToString()}`);
           switch (record.type) {
             case "bookmark":
               await this.storeRemoteBookmark(record, options);
-              continue;
+              return;
 
             case "query":
               await this.storeRemoteQuery(record, options);
-              continue;
+              return;
 
             case "folder":
               await this.storeRemoteFolder(record, options);
-              continue;
+              return;
 
             case "livemark":
               await this.storeRemoteLivemark(record, options);
-              continue;
+              return;
 
             case "separator":
               await this.storeRemoteSeparator(record, options);
-              continue;
+              return;
 
             default:
               if (record.deleted) {
                 await this.storeRemoteTombstone(record, options);
-                continue;
+                return;
               }
           }
           MirrorLog.warn("Ignoring record with unknown type", record.type);
-        }
+        }, yieldState);
       }
     ));
   }
 
   /**
    * Builds a complete merged tree from the local and remote trees, resolves
    * value and structure conflicts, dedupes local items, applies the merged
    * tree back to Places, and notifies observers about the changes.
@@ -917,31 +914,31 @@ class SyncedBookmarksMirror {
       FROM structure s
       WHERE s.guid <> :rootGuid
       GROUP BY s.parentGuid
       HAVING (sum(DISTINCT position + 1) -
                   (count(*) * (count(*) + 1) / 2)) <> 0
       ORDER BY guid`,
       { rootGuid: PlacesUtils.bookmarks.rootGuid });
 
-    for await (let row of yieldingIterator(orphanRows)) {
+    await Async.yieldingForEach(orphanRows, row => {
       let guid = row.getResultByName("guid");
       let missingParent = row.getResultByName("missingParent");
       if (missingParent) {
         infos.missingParents.push(guid);
       }
       let missingChild = row.getResultByName("missingChild");
       if (missingChild) {
         infos.missingChildren.push(guid);
       }
       let parentWithGaps = row.getResultByName("parentWithGaps");
       if (parentWithGaps) {
         infos.parentsWithGaps.push(guid);
       }
-    }
+    }, yieldState);
 
     return infos;
   }
 
   /**
    * Checks the sync statuses of all items for consistency. All merged items in
    * the remote tree should exist as either items or tombstones in the local
    * tree, and all NORMAL items and tombstones in the local tree should exist
@@ -994,31 +991,31 @@ class SyncedBookmarksMirror {
       WHERE EXISTS(SELECT 1 FROM items
                    WHERE NOT needsMerge AND
                          guid <> :rootGuid) AND
             b.guid <> :rootGuid AND
             b.syncStatus <> :syncStatus`,
       { syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
         rootGuid: PlacesUtils.bookmarks.rootGuid });
 
-    for await (let row of yieldingIterator(problemRows)) {
+    await Async.yieldingForEach(problemRows, row => {
       let guid = row.getResultByName("guid");
       let missingLocal = row.getResultByName("missingLocal");
       if (missingLocal) {
         infos.missingLocal.push(guid);
       }
       let missingRemote = row.getResultByName("missingRemote");
       if (missingRemote) {
         infos.missingRemote.push(guid);
       }
       let wrongSyncStatus = row.getResultByName("wrongSyncStatus");
       if (wrongSyncStatus) {
         infos.wrongSyncStatus.push(guid);
       }
-    }
+    }, yieldState);
 
     return infos;
   }
 
   /*
    * Checks if Places or mirror have any unsynced/unmerged changes.
    *
    * @return {Boolean}
@@ -1086,63 +1083,63 @@ class SyncedBookmarksMirror {
     let changeRecords = {};
     let childRecordIdsByLocalParentId = new Map();
     let tagsByLocalId = new Map();
 
     let childGuidRows = await this.db.execute(`
       SELECT parentId, guid FROM structureToUpload
       ORDER BY parentId, position`);
 
-    for await (let row of yieldingIterator(childGuidRows)) {
+    await Async.yieldingForEach(childGuidRows, row => {
       let localParentId = row.getResultByName("parentId");
       let childRecordId = PlacesSyncUtils.bookmarks.guidToRecordId(
         row.getResultByName("guid"));
       let childRecordIds = childRecordIdsByLocalParentId.get(localParentId);
       if (childRecordIds) {
         childRecordIds.push(childRecordId);
       } else {
         childRecordIdsByLocalParentId.set(localParentId, [childRecordId]);
       }
-    }
+    }, yieldState);
 
     let tagRows = await this.db.execute(`
       SELECT id, tag FROM tagsToUpload`);
 
-    for await (let row of yieldingIterator(tagRows)) {
+    await Async.yieldingForEach(tagRows, row => {
       let localId = row.getResultByName("id");
       let tag = row.getResultByName("tag");
       let tags = tagsByLocalId.get(localId);
       if (tags) {
         tags.push(tag);
       } else {
         tagsByLocalId.set(localId, [tag]);
       }
-    }
+    }, yieldState);
 
     let itemRows = await this.db.execute(`
       SELECT id, syncChangeCounter, guid, isDeleted, type, isQuery,
              tagFolderName, keyword, url, IFNULL(title, "") AS title,
              position, parentGuid,
              IFNULL(parentTitle, "") AS parentTitle, dateAdded
       FROM itemsToUpload`);
 
-    for await (let row of yieldingIterator(itemRows)) {
+    await Async.yieldingForEach(itemRows, row => {
       let syncChangeCounter = row.getResultByName("syncChangeCounter");
 
       let guid = row.getResultByName("guid");
       let recordId = PlacesSyncUtils.bookmarks.guidToRecordId(guid);
 
       // Tombstones don't carry additional properties.
       let isDeleted = row.getResultByName("isDeleted");
       if (isDeleted) {
         changeRecords[recordId] = new BookmarkChangeRecord(syncChangeCounter, {
           id: recordId,
           deleted: true,
         });
-        continue;
+        return;
       }
 
       let parentGuid = row.getResultByName("parentGuid");
       let parentRecordId = PlacesSyncUtils.bookmarks.guidToRecordId(parentGuid);
 
       let type = row.getResultByName("type");
       switch (type) {
         case PlacesUtils.bookmarks.TYPE_BOOKMARK: {
@@ -1167,17 +1164,17 @@ class SyncedBookmarksMirror {
               dateAdded: row.getResultByName("dateAdded") || undefined,
               bmkUri: row.getResultByName("url"),
               title: row.getResultByName("title"),
               // folderName should never be an empty string or null
               folderName: row.getResultByName("tagFolderName") || undefined,
             };
             changeRecords[recordId] = new BookmarkChangeRecord(
               syncChangeCounter, queryCleartext);
-            continue;
+            return;
           }
 
           let bookmarkCleartext = {
             id: recordId,
             type: "bookmark",
             parentid: parentRecordId,
             hasDupe: true,
             parentName: row.getResultByName("parentTitle"),
@@ -1191,17 +1188,17 @@ class SyncedBookmarksMirror {
           }
           let localId = row.getResultByName("id");
           let tags = tagsByLocalId.get(localId);
           if (tags) {
             bookmarkCleartext.tags = tags;
           }
           changeRecords[recordId] = new BookmarkChangeRecord(
             syncChangeCounter, bookmarkCleartext);
-          continue;
+          return;
         }
 
         case PlacesUtils.bookmarks.TYPE_FOLDER: {
           let folderCleartext = {
             id: recordId,
             type: "folder",
             parentid: parentRecordId,
             hasDupe: true,
@@ -1209,39 +1206,39 @@ class SyncedBookmarksMirror {
             dateAdded: row.getResultByName("dateAdded") || undefined,
             title: row.getResultByName("title"),
           };
           let localId = row.getResultByName("id");
           let childRecordIds = childRecordIdsByLocalParentId.get(localId);
           folderCleartext.children = childRecordIds || [];
           changeRecords[recordId] = new BookmarkChangeRecord(
             syncChangeCounter, folderCleartext);
-          continue;
+          return;
         }
 
         case PlacesUtils.bookmarks.TYPE_SEPARATOR: {
           let separatorCleartext = {
             id: recordId,
             type: "separator",
             parentid: parentRecordId,
             hasDupe: true,
             parentName: row.getResultByName("parentTitle"),
             dateAdded: row.getResultByName("dateAdded") || undefined,
             // Older Desktops use `pos` for deduping.
             pos: row.getResultByName("position"),
           };
           changeRecords[recordId] = new BookmarkChangeRecord(
             syncChangeCounter, separatorCleartext);
-          continue;
+          return;
         }
 
         default:
           throw new TypeError("Can't create record for unknown Places item");
       }
-    }
+    }, yieldState);
 
     return changeRecords;
   }
 
   /**
    * Closes the mirror database connection. This is called automatically on
    * shutdown, but may also be called explicitly when the mirror is no longer
    * needed.
@@ -2128,131 +2125,131 @@ class BookmarkObserverRecorder {
     // by parent and position so that the notifications are well-ordered for
     // tests.
     let removedItemRows = await this.db.execute(`
       SELECT v.itemId AS id, v.parentId, v.parentGuid, v.position, v.type,
              h.url, v.guid, v.isUntagging
       FROM itemsRemoved v
       LEFT JOIN moz_places h ON h.id = v.placeId
       ORDER BY v.level DESC, v.parentId, v.position`);
-    for await (let row of yieldingIterator(removedItemRows)) {
+    await Async.yieldingForEach(removedItemRows, row => {
       let info = {
         id: row.getResultByName("id"),
         parentId: row.getResultByName("parentId"),
         position: row.getResultByName("position"),
         type: row.getResultByName("type"),
         urlHref: row.getResultByName("url"),
         guid: row.getResultByName("guid"),
         parentGuid: row.getResultByName("parentGuid"),
         isUntagging: row.getResultByName("isUntagging"),
       };
       this.noteItemRemoved(info);
-    }
+    }, yieldState);
 
     MirrorLog.trace("Recording observer notifications for changed GUIDs");
     let changedGuidRows = await this.db.execute(`
       SELECT b.id, b.lastModified, b.type, b.guid AS newGuid,
              c.oldGuid, p.id AS parentId, p.guid AS parentGuid
       FROM guidsChanged c
       JOIN moz_bookmarks b ON b.id = c.itemId
       JOIN moz_bookmarks p ON p.id = b.parent
       ORDER BY c.level, p.id, b.position`);
-    for await (let row of yieldingIterator(changedGuidRows)) {
+    await Async.yieldingForEach(changedGuidRows, row => {
       let info = {
         id: row.getResultByName("id"),
         lastModified: row.getResultByName("lastModified"),
         type: row.getResultByName("type"),
         newGuid: row.getResultByName("newGuid"),
         oldGuid: row.getResultByName("oldGuid"),
         parentId: row.getResultByName("parentId"),
         parentGuid: row.getResultByName("parentGuid"),
       };
       this.noteGuidChanged(info);
-    }
+    }, yieldState);
 
     MirrorLog.trace("Recording observer notifications for new items");
     let newItemRows = await this.db.execute(`
       SELECT b.id, p.id AS parentId, b.position, b.type, h.url,
              IFNULL(b.title, "") AS title, b.dateAdded, b.guid,
              p.guid AS parentGuid, n.isTagging
       FROM itemsAdded n
       JOIN moz_bookmarks b ON b.guid = n.guid
       JOIN moz_bookmarks p ON p.id = b.parent
       LEFT JOIN moz_places h ON h.id = b.fk
       ORDER BY n.level, p.id, b.position`);
-    for await (let row of yieldingIterator(newItemRows)) {
+    await Async.yieldingForEach(newItemRows, row => {
       let info = {
         id: row.getResultByName("id"),
         parentId: row.getResultByName("parentId"),
         position: row.getResultByName("position"),
         type: row.getResultByName("type"),
         urlHref: row.getResultByName("url"),
         title: row.getResultByName("title"),
         dateAdded: row.getResultByName("dateAdded"),
         guid: row.getResultByName("guid"),
         parentGuid: row.getResultByName("parentGuid"),
         isTagging: row.getResultByName("isTagging"),
       };
       this.noteItemAdded(info);
-    }
+    }, yieldState);
 
     MirrorLog.trace("Recording observer notifications for moved items");
     let movedItemRows = await this.db.execute(`
       SELECT b.id, b.guid, b.type, p.id AS newParentId, c.oldParentId,
              p.guid AS newParentGuid, c.oldParentGuid,
              b.position AS newPosition, c.oldPosition, h.url
       FROM itemsMoved c
       JOIN moz_bookmarks b ON b.id = c.itemId
       JOIN moz_bookmarks p ON p.id = b.parent
       LEFT JOIN moz_places h ON h.id = b.fk
       ORDER BY c.level, newParentId, newPosition`);
-    for await (let row of yieldingIterator(movedItemRows)) {
+    await Async.yieldingForEach(movedItemRows, row => {
       let info = {
         id: row.getResultByName("id"),
         guid: row.getResultByName("guid"),
         type: row.getResultByName("type"),
         newParentId: row.getResultByName("newParentId"),
         oldParentId: row.getResultByName("oldParentId"),
         newParentGuid: row.getResultByName("newParentGuid"),
         oldParentGuid: row.getResultByName("oldParentGuid"),
         newPosition: row.getResultByName("newPosition"),
         oldPosition: row.getResultByName("oldPosition"),
         urlHref: row.getResultByName("url"),
       };
       this.noteItemMoved(info);
-    }
+    }, yieldState);
 
     MirrorLog.trace("Recording observer notifications for changed items");
     let changedItemRows = await this.db.execute(`
       SELECT b.id, b.guid, b.lastModified, b.type,
              IFNULL(b.title, "") AS newTitle,
              IFNULL(c.oldTitle, "") AS oldTitle,
              h.url AS newURL, i.url AS oldURL,
              p.id AS parentId, p.guid AS parentGuid
       FROM itemsChanged c
       JOIN moz_bookmarks b ON b.id = c.itemId
       JOIN moz_bookmarks p ON p.id = b.parent
       LEFT JOIN moz_places h ON h.id = b.fk
       LEFT JOIN moz_places i ON i.id = c.oldPlaceId
       ORDER BY c.level, p.id, b.position`);
-    for await (let row of yieldingIterator(changedItemRows)) {
+    await Async.yieldingForEach(changedItemRows, row => {
       let info = {
         id: row.getResultByName("id"),
         guid: row.getResultByName("guid"),
         lastModified: row.getResultByName("lastModified"),
         type: row.getResultByName("type"),
         newTitle: row.getResultByName("newTitle"),
         oldTitle: row.getResultByName("oldTitle"),
         newURLHref: row.getResultByName("newURL"),
         oldURLHref: row.getResultByName("oldURL"),
         parentId: row.getResultByName("parentId"),
         parentGuid: row.getResultByName("parentGuid"),
       };
       this.noteItemChanged(info);
-    }
+    }, yieldState);
 
     MirrorLog.trace("Recording notifications for changed keywords");
     let keywordsChangedRows = await this.db.execute(`
       SELECT EXISTS(SELECT 1 FROM itemsAdded WHERE keywordChanged) OR
              EXISTS(SELECT 1 FROM itemsChanged WHERE keywordChanged)
              AS keywordsChanged`);
     this.shouldInvalidateKeywords =
       !!keywordsChangedRows[0].getResultByName("keywordsChanged");
@@ -2329,28 +2326,28 @@ class BookmarkObserverRecorder {
   }
 
   async notifyBookmarkObservers() {
     MirrorLog.trace("Notifying bookmark observers");
     let observers = PlacesUtils.bookmarks.getObservers();
     for (let observer of observers) {
       this.notifyObserver(observer, "onBeginUpdateBatch");
     }
-    for await (let info of yieldingIterator(this.bookmarkObserverNotifications)) {
+    await Async.yieldingForEach(this.bookmarkObserverNotifications, info => {
       if (info instanceof PlacesEvent) {
         PlacesObservers.notifyListeners([info]);
       } else {
         for (let observer of observers) {
           if (info.isTagging && observer.skipTags) {
-            continue;
+            return;
           }
           this.notifyObserver(observer, info.name, info.args);
         }
       }
-    }
+    }, yieldState);
     for (let observer of observers) {
       this.notifyObserver(observer, "onEndUpdateBatch");
     }
   }
 
   notifyObserver(observer, notification, args = []) {
     try {
       observer[notification](...args);