Bug 913160 - Back out Bug 894331 to solve browser hangs when deleting history. a=lsblakk
authorMarco Bonardo <mbonardo@mozilla.com>
Wed, 13 Nov 2013 09:45:41 -0500
changeset 166497 1e9034137b68846d46dd44248b855b17c487e6dd
parent 166496 ba2c8a25709286762bfe77dceab8d37321b3aa9c
child 166498 6e80754f7257de8f875e8f490b57c6d66276936c
push id3066
push userakeybl@mozilla.com
push dateMon, 09 Dec 2013 19:58:46 +0000
treeherdermozilla-beta@a31a0dce83aa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerslsblakk
bugs913160, 894331
milestone27.0a2
Bug 913160 - Back out Bug 894331 to solve browser hangs when deleting history. a=lsblakk
toolkit/components/places/nsNavHistoryResult.cpp
toolkit/components/places/nsNavHistoryResult.h
toolkit/components/places/tests/queries/head_queries.js
toolkit/components/places/tests/queries/test_history_queries_tags_liveUpdate.js
toolkit/components/places/tests/queries/test_history_queries_titles_liveUpdate.js
toolkit/components/places/tests/unit/test_nsINavHistoryViewer.js
--- a/toolkit/components/places/nsNavHistoryResult.cpp
+++ b/toolkit/components/places/nsNavHistoryResult.cpp
@@ -46,31 +46,19 @@
 // expand the macro here and change it so that it works. Yuck.
 #define NS_INTERFACE_MAP_STATIC_AMBIGUOUS(_class) \
   if (aIID.Equals(NS_GET_IID(_class))) { \
     NS_ADDREF(this); \
     *aInstancePtr = this; \
     return NS_OK; \
   } else
 
-// This should be used whenever the result has to hand out up-to-date contents
-// to the caller. During batches the contents are not updated until a Refresh()
-// is executed. This ensures a Refresh() is executed before proceeding, if a
-// batch is ongoing.
-// Note that there's no point in using this in node getters, since after a
-// batch the node would be replaced by a new one, so it would hand out outdated
-// information regardless.
-#define END_RESULT_BATCH_AND_REFRESH_CONTENTS() \
-  PR_BEGIN_MACRO \
-  nsNavHistoryResult* result = GetResult(); \
-  NS_WARN_IF_FALSE(result, "Working with a non-live-updating Places container"); \
-  if (result && result->mBatchInProgress) { \
-    result->EndBatch(); \
-  } \
-  PR_END_MACRO
+// Number of changes to handle separately in a batch.  If more changes are
+// requested the node will switch to full refresh mode.
+#define MAX_BATCH_CHANGES_BEFORE_REFRESH 5
 
 // Emulate string comparison (used for sorting) for PRTime and int.
 inline int32_t ComparePRTime(PRTime a, PRTime b)
 {
   if (a < b)
     return -1;
   else if (a > b)
     return 1;
@@ -1654,64 +1642,54 @@ nsNavHistoryContainerResultNode::ChangeT
 /**
  * Complex containers (folders and queries) will override this.  Here, we
  * handle the case of simple containers (like host groups) where the children
  * are always stored.
  */
 NS_IMETHODIMP
 nsNavHistoryContainerResultNode::GetHasChildren(bool *aHasChildren)
 {
-  END_RESULT_BATCH_AND_REFRESH_CONTENTS();
-
   *aHasChildren = (mChildren.Count() > 0);
   return NS_OK;
 }
 
 
 /**
  * @throws if this node is closed.
  */
 NS_IMETHODIMP
 nsNavHistoryContainerResultNode::GetChildCount(uint32_t* aChildCount)
 {
   if (!mExpanded)
     return NS_ERROR_NOT_AVAILABLE;
-
-  END_RESULT_BATCH_AND_REFRESH_CONTENTS();
-
   *aChildCount = mChildren.Count();
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 nsNavHistoryContainerResultNode::GetChild(uint32_t aIndex,
                                           nsINavHistoryResultNode** _retval)
 {
   if (!mExpanded)
     return NS_ERROR_NOT_AVAILABLE;
-
-  END_RESULT_BATCH_AND_REFRESH_CONTENTS();
-
   if (aIndex >= uint32_t(mChildren.Count()))
     return NS_ERROR_INVALID_ARG;
   NS_ADDREF(*_retval = mChildren[aIndex]);
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 nsNavHistoryContainerResultNode::GetChildIndex(nsINavHistoryResultNode* aNode,
                                                uint32_t* _retval)
 {
   if (!mExpanded)
     return NS_ERROR_NOT_AVAILABLE;
 
-  END_RESULT_BATCH_AND_REFRESH_CONTENTS();
-
   int32_t nodeIndex = FindChild(static_cast<nsNavHistoryResultNode*>(aNode));
   if (nodeIndex == -1)
     return NS_ERROR_INVALID_ARG;
 
   *_retval = nodeIndex;
   return NS_OK;
 }
 
@@ -1720,18 +1698,16 @@ NS_IMETHODIMP
 nsNavHistoryContainerResultNode::FindNodeByDetails(const nsACString& aURIString,
                                                    PRTime aTime,
                                                    int64_t aItemId,
                                                    bool aRecursive,
                                                    nsINavHistoryResultNode** _retval) {
   if (!mExpanded)
     return NS_ERROR_NOT_AVAILABLE;
 
-  END_RESULT_BATCH_AND_REFRESH_CONTENTS();
-
   *_retval = nullptr;
   for (int32_t i = 0; i < mChildren.Count(); ++i) {
     if (mChildren[i]->mURI.Equals(aURIString) &&
         mChildren[i]->mTime == aTime &&
         mChildren[i]->mItemId == aItemId) {
       *_retval = mChildren[i];
       break;
     }
@@ -1789,29 +1765,31 @@ NS_IMPL_ISUPPORTS_INHERITED1(nsNavHistor
 nsNavHistoryQueryResultNode::nsNavHistoryQueryResultNode(
     const nsACString& aTitle, const nsACString& aIconURI,
     const nsACString& aQueryURI) :
   nsNavHistoryContainerResultNode(aQueryURI, aTitle, aIconURI,
                                   nsNavHistoryResultNode::RESULT_TYPE_QUERY,
                                   true, nullptr),
   mLiveUpdate(QUERYUPDATE_COMPLEX_WITH_BOOKMARKS),
   mHasSearchTerms(false),
-  mContentsValid(false)
+  mContentsValid(false),
+  mBatchChanges(0)
 {
 }
 
 nsNavHistoryQueryResultNode::nsNavHistoryQueryResultNode(
     const nsACString& aTitle, const nsACString& aIconURI,
     const nsCOMArray<nsNavHistoryQuery>& aQueries,
     nsNavHistoryQueryOptions* aOptions) :
   nsNavHistoryContainerResultNode(EmptyCString(), aTitle, aIconURI,
                                   nsNavHistoryResultNode::RESULT_TYPE_QUERY,
                                   true, aOptions),
   mQueries(aQueries),
   mContentsValid(false),
+  mBatchChanges(0),
   mTransitions(mQueries[0]->Transitions())
 {
   NS_ASSERTION(aQueries.Count() > 0, "Must have at least one query");
 
   nsNavHistory* history = nsNavHistory::GetHistoryService();
   NS_ASSERTION(history, "History service missing");
   if (history) {
     mLiveUpdate = history->GetUpdateRequirements(mQueries, mOptions,
@@ -1834,16 +1812,17 @@ nsNavHistoryQueryResultNode::nsNavHistor
     PRTime aTime,
     const nsCOMArray<nsNavHistoryQuery>& aQueries,
     nsNavHistoryQueryOptions* aOptions) :
   nsNavHistoryContainerResultNode(EmptyCString(), aTitle, aTime, aIconURI,
                                   nsNavHistoryResultNode::RESULT_TYPE_QUERY,
                                   true, aOptions),
   mQueries(aQueries),
   mContentsValid(false),
+  mBatchChanges(0),
   mTransitions(mQueries[0]->Transitions())
 {
   NS_ASSERTION(aQueries.Count() > 0, "Must have at least one query");
 
   nsNavHistory* history = nsNavHistory::GetHistoryService();
   NS_ASSERTION(history, "History service missing");
   if (history) {
     mLiveUpdate = history->GetUpdateRequirements(mQueries, mOptions,
@@ -1979,18 +1958,16 @@ NS_IMETHODIMP
 nsNavHistoryQueryResultNode::GetHasChildren(bool* aHasChildren)
 {
   *aHasChildren = false;
 
   if (!CanExpand()) {
     return NS_OK;
   }
 
-  END_RESULT_BATCH_AND_REFRESH_CONTENTS();
-
   uint16_t resultType = mOptions->ResultType();
 
   // Tags are always populated, otherwise they are removed.
   if (resultType == nsINavHistoryQueryOptions::RESULTS_AS_TAG_CONTENTS) {
     *aHasChildren = true;
     return NS_OK;
   }
 
@@ -2384,16 +2361,17 @@ nsNavHistoryQueryResultNode::OnEndUpdate
   // If the query has no children it's possible it's not yet listening to
   // bookmarks changes, in such a case it's safer to force a refresh to gather
   // eventual new nodes matching query options.
   if (mChildren.Count() == 0) {
     nsresult rv = Refresh();
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
+  mBatchChanges = 0;
   return NS_OK;
 }
 
 static nsresult setHistoryDetailsCallback(nsNavHistoryResultNode* aNode,
                                           const void* aClosure,
                                           const nsNavHistoryResult* aResult)
 {
   const nsNavHistoryResultNode* updatedNode =
@@ -2422,17 +2400,18 @@ nsNavHistoryQueryResultNode::OnVisit(nsI
                                      bool aHidden,
                                      uint32_t* aAdded)
 {
   if (aHidden && !mOptions->IncludeHidden())
     return NS_OK;
 
   nsNavHistoryResult* result = GetResult();
   NS_ENSURE_STATE(result);
-  if (result->mBatchInProgress) {
+  if (result->mBatchInProgress &&
+      ++mBatchChanges > MAX_BATCH_CHANGES_BEFORE_REFRESH) {
     nsresult rv = Refresh();
     NS_ENSURE_SUCCESS(rv, rv);
     return NS_OK;
   }
 
   nsNavHistory* history = nsNavHistory::GetHistoryService();
   NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
 
@@ -2567,17 +2546,18 @@ nsNavHistoryQueryResultNode::OnTitleChan
     // titles, but when a title changes, its unlikely that it will be the only
     // thing.  Therefore, we just give up.
     ClearChildren(true);
     return NS_OK; // no updates in tree state
   }
 
   nsNavHistoryResult* result = GetResult();
   NS_ENSURE_STATE(result);
-  if (result->mBatchInProgress) {
+  if (result->mBatchInProgress &&
+      ++mBatchChanges > MAX_BATCH_CHANGES_BEFORE_REFRESH) {
     nsresult rv = Refresh();
     NS_ENSURE_SUCCESS(rv, rv);
     return NS_OK;
   }
 
   // compute what the new title should be
   NS_ConvertUTF16toUTF8 newTitle(aPageTitle);
 
@@ -2637,17 +2617,18 @@ nsNavHistoryQueryResultNode::OnTitleChan
  */
 NS_IMETHODIMP
 nsNavHistoryQueryResultNode::OnDeleteURI(nsIURI* aURI,
                                          const nsACString& aGUID,
                                          uint16_t aReason)
 {
   nsNavHistoryResult* result = GetResult();
   NS_ENSURE_STATE(result);
-  if (result->mBatchInProgress) {
+  if (result->mBatchInProgress &&
+      ++mBatchChanges > MAX_BATCH_CHANGES_BEFORE_REFRESH) {
     nsresult rv = Refresh();
     NS_ENSURE_SUCCESS(rv, rv);
     return NS_OK;
   }
 
   if (IsContainersQuery()) {
     // Incremental updates of query returning queries are pretty much
     // complicated.  In this case it's possible one of the child queries has
@@ -2785,21 +2766,17 @@ nsNavHistoryQueryResultNode::NotifyIfTag
 
   // Find matching URI nodes.
   nsRefPtr<nsNavHistoryResultNode> node;
   nsNavHistory* history = nsNavHistory::GetHistoryService();
 
   nsCOMArray<nsNavHistoryResultNode> matches;
   RecursiveFindURIs(onlyOneEntry, this, spec, &matches);
 
-  bool skipRemovedURI = false;
-  if (mRemovingURI)
-    (void)mRemovingURI->Equals(aURI, &skipRemovedURI);
-
-  if (matches.Count() == 0 && mHasSearchTerms && !skipRemovedURI) {
+  if (matches.Count() == 0 && mHasSearchTerms && !mRemovingURI) {
     // A new tag has been added, it's possible it matches our query.
     NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
     rv = history->URIToResultNode(aURI, mOptions, getter_AddRefs(node));
     NS_ENSURE_SUCCESS(rv, rv);
     if (history->EvaluateQueryForNode(mQueries, mOptions, node)) {
       rv = InsertSortedChild(node, true);
       NS_ENSURE_SUCCESS(rv, rv);
     }
@@ -2842,22 +2819,16 @@ nsNavHistoryQueryResultNode::OnItemAdded
                                          int32_t aIndex,
                                          uint16_t aItemType,
                                          nsIURI* aURI,
                                          const nsACString& aTitle,
                                          PRTime aDateAdded,
                                          const nsACString& aGUID,
                                          const nsACString& aParentGUID)
 {
-  bool sameURI = false;
-  if (mRemovingURI && NS_SUCCEEDED(mRemovingURI->Equals(aURI, &sameURI)) &&
-      sameURI) {
-    mRemovingURI = nullptr;
-  }
-
   if (aItemType == nsINavBookmarksService::TYPE_BOOKMARK &&
       mLiveUpdate != QUERYUPDATE_SIMPLE &&  mLiveUpdate != QUERYUPDATE_TIME) {
     nsresult rv = Refresh();
     NS_ENSURE_SUCCESS(rv, rv);
   }
   return NS_OK;
 }
 
@@ -2867,17 +2838,16 @@ nsNavHistoryQueryResultNode::OnItemRemov
                                            int64_t aParentId,
                                            int32_t aIndex,
                                            uint16_t aItemType,
                                            nsIURI* aURI,
                                            const nsACString& aGUID,
                                            const nsACString& aParentGUID)
 {
   mRemovingURI = aURI;
-
   if (aItemType == nsINavBookmarksService::TYPE_BOOKMARK &&
       mLiveUpdate != QUERYUPDATE_SIMPLE && mLiveUpdate != QUERYUPDATE_TIME) {
     nsresult rv = Refresh();
     NS_ENSURE_SUCCESS(rv, rv);
   }
   return NS_OK;
 }
 
@@ -3093,18 +3063,16 @@ nsNavHistoryFolderResultNode::OpenContai
  * @see nsNavHistoryQueryResultNode::HasChildren.  The semantics here are a
  * little different.  Querying the contents of a bookmark folder is relatively
  * fast and it is common to have empty folders.  Therefore, we always want to
  * return the correct result so that twisties are drawn properly.
  */
 NS_IMETHODIMP
 nsNavHistoryFolderResultNode::GetHasChildren(bool* aHasChildren)
 {
-  END_RESULT_BATCH_AND_REFRESH_CONTENTS();
-
   if (!mContentsValid) {
     nsresult rv = FillChildren();
     NS_ENSURE_SUCCESS(rv, rv);
   }
   *aHasChildren = (mChildren.Count() > 0);
   return NS_OK;
 }
 
@@ -4046,17 +4014,16 @@ NS_INTERFACE_MAP_END
 nsNavHistoryResult::nsNavHistoryResult(nsNavHistoryContainerResultNode* aRoot)
   : mRootNode(aRoot)
   , mNeedsToApplySortingMode(false)
   , mIsHistoryObserver(false)
   , mIsBookmarkFolderObserver(false)
   , mIsAllBookmarksObserver(false)
   , mBookmarkFolderObservers(128)
   , mBatchInProgress(false)
-  , mRelatedNotificationsCount(0)
   , mSuppressNotifications(false)
 {
   mRootNode->mResult = this;
 }
 
 nsNavHistoryResult::~nsNavHistoryResult()
 {
   // delete all bookmark folder observer arrays which are allocated on the heap
@@ -4273,19 +4240,16 @@ nsNavHistoryResult::SetSortingMode(uint1
   mSortingMode = aSortingMode;
 
   if (!mRootNode->mExpanded) {
     // Need to do this later when node will be expanded.
     mNeedsToApplySortingMode = true;
     return NS_OK;
   }
 
-  if (mBatchInProgress)
-    EndBatch();
-
   // Actually do sorting.
   nsNavHistoryContainerResultNode::SortComparator comparator =
       nsNavHistoryContainerResultNode::GetSortingComparator(aSortingMode);
   if (comparator) {
     nsNavHistory* history = nsNavHistory::GetHistoryService();
     NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
     mRootNode->RecursiveSort(mSortingAnnotation.get(), comparator);
   }
@@ -4371,157 +4335,90 @@ nsNavHistoryResult::GetRoot(nsINavHistor
 void
 nsNavHistoryResult::requestRefresh(nsNavHistoryContainerResultNode* aContainer)
 {
   // Don't add twice the same container.
   if (mRefreshParticipants.IndexOf(aContainer) == mRefreshParticipants.NoIndex)
     mRefreshParticipants.AppendElement(aContainer);
 }
 
-// This interval is used for smart batches handling.
-// Count the number of related notification, by checking if the interval between
-// the end of the previous notification and the beginning of the next one is
-// smaller than RELATED_NOTIFICATIONS_INTERVAL_MS.
-// If there are more than RELATED_NOTIFICATIONS_THRESHOLD notifications, start
-// an automatic batch.
-// Similarly, if there are no more related notifications for
-// RELATED_NOTIFICATIONS_INTERVAL_MS, automatically close the batch.
-// Note we use LoRes TimeStamps for performance reasons.
-#define RELATED_NOTIFICATIONS_INTERVAL_MS 150
-#define RELATED_NOTIFICATIONS_THRESHOLD 10
-#define MS_FROM_NOW(_stamp) (TimeStamp::NowLoRes() - _stamp).ToMilliseconds()
-
-void
-nsNavHistoryResult::MaybeBeginBatch()
-{
-  if (!mBatchInProgress && !mLastNotificationTimeStamp.IsNull() &&
-      MS_FROM_NOW(mLastNotificationTimeStamp) < (double)RELATED_NOTIFICATIONS_INTERVAL_MS) {
-    if (++mRelatedNotificationsCount > RELATED_NOTIFICATIONS_THRESHOLD) {
-      mRelatedNotificationsCount = 0;
-      DebugOnly<nsresult> rv = BeginBatch();
-      MOZ_ASSERT(NS_SUCCEEDED(rv));
-    }
-  } else {
-    mRelatedNotificationsCount = 0;
-  }
-}
-
-// static
-void
-nsNavHistoryResult::MaybeEndBatchCallback(nsITimer* aTimer, void* aClosure)
-{
-  nsNavHistoryResult* result = static_cast<nsNavHistoryResult *>(aClosure);
-  MOZ_ASSERT(result);
-  if (result &&
-      MS_FROM_NOW(result->mLastNotificationTimeStamp) > (double)RELATED_NOTIFICATIONS_INTERVAL_MS) {
-    DebugOnly<nsresult> rv = result->EndBatch();
-    MOZ_ASSERT(NS_SUCCEEDED(rv));
-  }
-}
-
-#undef MS_FROM_NOW
-
 // nsINavBookmarkObserver implementation
 
 // Here, it is important that we create a COPY of the observer array. Some
 // observers will requery themselves, which may cause the observer array to
 // be modified or added to.
 #define ENUMERATE_BOOKMARK_FOLDER_OBSERVERS(_folderId, _functionCall) \
   PR_BEGIN_MACRO \
     FolderObserverList* _fol = BookmarkFolderObserversForId(_folderId, false); \
     if (_fol) { \
       FolderObserverList _listCopy(*_fol); \
-      if (_listCopy.Length() > 0) { \
-        MaybeBeginBatch(); \
-        for (uint32_t _fol_i = 0; _fol_i < _listCopy.Length(); ++_fol_i) { \
-          if (_listCopy[_fol_i]) \
-            _listCopy[_fol_i]->_functionCall; \
-        } \
-        mLastNotificationTimeStamp = TimeStamp::NowLoRes(); \
+      for (uint32_t _fol_i = 0; _fol_i < _listCopy.Length(); ++_fol_i) { \
+        if (_listCopy[_fol_i]) \
+          _listCopy[_fol_i]->_functionCall; \
       } \
     } \
   PR_END_MACRO
 #define ENUMERATE_LIST_OBSERVERS(_listType, _functionCall, _observersList, _conditionCall) \
   PR_BEGIN_MACRO \
     _listType _listCopy(_observersList); \
-    if (_listCopy.Length() > 0) { \
-      MaybeBeginBatch(); \
-      for (uint32_t _obs_i = 0; _obs_i < _listCopy.Length(); ++_obs_i) { \
-        if (_listCopy[_obs_i] && _listCopy[_obs_i]->_conditionCall) \
-          _listCopy[_obs_i]->_functionCall; \
-      } \
-      mLastNotificationTimeStamp = TimeStamp::NowLoRes(); \
+    for (uint32_t _obs_i = 0; _obs_i < _listCopy.Length(); ++_obs_i) { \
+      if (_listCopy[_obs_i] && _listCopy[_obs_i]->_conditionCall) \
+        _listCopy[_obs_i]->_functionCall; \
     } \
   PR_END_MACRO
 #define ENUMERATE_QUERY_OBSERVERS(_functionCall, _observersList, _conditionCall) \
   ENUMERATE_LIST_OBSERVERS(QueryObserverList, _functionCall, _observersList, _conditionCall)
 #define ENUMERATE_ALL_BOOKMARKS_OBSERVERS(_functionCall) \
   ENUMERATE_QUERY_OBSERVERS(_functionCall, mAllBookmarksObservers, IsQuery())
 #define ENUMERATE_HISTORY_OBSERVERS(_functionCall) \
   ENUMERATE_QUERY_OBSERVERS(_functionCall, mHistoryObservers, IsQuery())
-#define NOTIFY_REFRESH(_listType, _observersList, _conditionCall, _clear) \
+
+#define NOTIFY_REFRESH_PARTICIPANTS() \
   PR_BEGIN_MACRO \
-  _listType _listCopy(_observersList); \
-  for (uint32_t _obs_i = 0; _obs_i < _listCopy.Length(); ++_obs_i) { \
-    if (_listCopy[_obs_i] && _listCopy[_obs_i]->_conditionCall) \
-      _listCopy[_obs_i]->Refresh(); \
-  } \
-  if (_clear) \
-    _observersList.Clear(); \
+  ENUMERATE_LIST_OBSERVERS(ContainerObserverList, Refresh(), mRefreshParticipants, IsContainer()); \
+  mRefreshParticipants.Clear(); \
   PR_END_MACRO
 
-nsresult
-nsNavHistoryResult::BeginBatch() {
-  mBatchInProgress = true;
-  ENUMERATE_HISTORY_OBSERVERS(OnBeginUpdateBatch());
-  ENUMERATE_ALL_BOOKMARKS_OBSERVERS(OnBeginUpdateBatch());
-  NOTIFY_RESULT_OBSERVERS(this, Batching(true));
-
-  if (!mEndBatchTimer)
-    mEndBatchTimer = do_CreateInstance("@mozilla.org/timer;1");
-  MOZ_ASSERT(mEndBatchTimer);
-  if (mEndBatchTimer) {
-    mEndBatchTimer->InitWithFuncCallback(MaybeEndBatchCallback, this,
-                                         RELATED_NOTIFICATIONS_INTERVAL_MS,
-                                         nsITimer::TYPE_REPEATING_SLACK);
-  } else {
-    DebugOnly<nsresult> rv = EndBatch();
-    MOZ_ASSERT(NS_SUCCEEDED(rv));
+NS_IMETHODIMP
+nsNavHistoryResult::OnBeginUpdateBatch()
+{
+  // Since we could be observing both history and bookmarks, it's possible both
+  // notify the batch.  We can safely ignore nested calls.
+  if (!mBatchInProgress) {
+    mBatchInProgress = true;
+    ENUMERATE_HISTORY_OBSERVERS(OnBeginUpdateBatch());
+    ENUMERATE_ALL_BOOKMARKS_OBSERVERS(OnBeginUpdateBatch());
+
+    NOTIFY_RESULT_OBSERVERS(this, Batching(true));
   }
 
   return NS_OK;
 }
 
-nsresult
-nsNavHistoryResult::EndBatch() {
-  MOZ_ASSERT(mBatchInProgress);
-  if (mEndBatchTimer)
-    mEndBatchTimer->Cancel();
-
-  ENUMERATE_HISTORY_OBSERVERS(OnEndUpdateBatch());
-  ENUMERATE_ALL_BOOKMARKS_OBSERVERS(OnEndUpdateBatch());
-
-  // Setting mBatchInProgress before notifying the end of the batch to
-  // observers would make evantual calls to Refresh() directly handled rather
-  // than enqueued.  Thus set it just before handling refreshes.
-  mBatchInProgress = false;
-  NOTIFY_REFRESH(ContainerObserverList, mRefreshParticipants, IsContainer(), true);
-  NOTIFY_RESULT_OBSERVERS(this, Batching(false));
-  return NS_OK;
-}
-
-NS_IMETHODIMP
-nsNavHistoryResult::OnBeginUpdateBatch()
-{
-  return NS_OK;
-}
 
 NS_IMETHODIMP
 nsNavHistoryResult::OnEndUpdateBatch()
 {
+  // Since we could be observing both history and bookmarks, it's possible both
+  // notify the batch.  We can safely ignore nested calls.
+  // Notice it's possible we are notified OnEndUpdateBatch more times than
+  // onBeginUpdateBatch, since the result could be created in the middle of
+  // nested batches.
+  if (mBatchInProgress) {
+    ENUMERATE_HISTORY_OBSERVERS(OnEndUpdateBatch());
+    ENUMERATE_ALL_BOOKMARKS_OBSERVERS(OnEndUpdateBatch());
+
+    // Setting mBatchInProgress before notifying the end of the batch to
+    // observers would make evantual calls to Refresh() directly handled rather
+    // than enqueued.  Thus set it just before handling refreshes.
+    mBatchInProgress = false;
+    NOTIFY_REFRESH_PARTICIPANTS();
+    NOTIFY_RESULT_OBSERVERS(this, Batching(false));
+  }
+
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 nsNavHistoryResult::OnItemAdded(int64_t aItemId,
                                 int64_t aParentId,
                                 int32_t aIndex,
@@ -4740,17 +4637,17 @@ nsNavHistoryResult::OnVisit(nsIURI* aURI
       (void)mRootNode->GetAsQuery()->Refresh();
       return NS_OK;
     }
 
     // We are result of a folder node, then we should run through history
     // observers that are containers queries and refresh them.
     // We use a copy of the observers array since requerying could potentially
     // cause changes to the array.
-    NOTIFY_REFRESH(QueryObserverList, mHistoryObservers, IsContainersQuery(), false);
+    ENUMERATE_QUERY_OBSERVERS(Refresh(), mHistoryObservers, IsContainersQuery());
   }
 
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 nsNavHistoryResult::OnTitleChanged(nsIURI* aURI,
--- a/toolkit/components/places/nsNavHistoryResult.h
+++ b/toolkit/components/places/nsNavHistoryResult.h
@@ -13,17 +13,16 @@
 #define nsNavHistoryResult_h_
 
 #include "nsTArray.h"
 #include "nsInterfaceHashtable.h"
 #include "nsDataHashtable.h"
 #include "nsCycleCollectionParticipant.h"
 #include "mozilla/storage.h"
 #include "Helpers.h"
-#include "mozilla/TimeStamp.h"
 
 class nsNavHistory;
 class nsNavHistoryQuery;
 class nsNavHistoryQueryOptions;
 
 class nsNavHistoryContainerResultNode;
 class nsNavHistoryFolderResultNode;
 class nsNavHistoryQueryResultNode;
@@ -168,26 +167,18 @@ public:
   FolderObserverList* BookmarkFolderObserversForId(int64_t aFolderId, bool aCreate);
 
   typedef nsTArray< nsRefPtr<nsNavHistoryContainerResultNode> > ContainerObserverList;
 
   void RecursiveExpandCollapse(nsNavHistoryContainerResultNode* aContainer,
                                bool aExpand);
 
   void InvalidateTree();
-
+  
   bool mBatchInProgress;
-  int32_t mRelatedNotificationsCount;
-  mozilla::TimeStamp mLastNotificationTimeStamp;
-  nsCOMPtr<nsITimer> mEndBatchTimer;
-
-  void MaybeBeginBatch();
-  static void MaybeEndBatchCallback(nsITimer* aTimer, void* aClosure);
-  nsresult BeginBatch();
-  nsresult EndBatch();
 
   nsMaybeWeakPtrArray<nsINavHistoryResultObserver> mObservers;
   bool mSuppressNotifications;
 
   ContainerObserverList mRefreshParticipants;
   void requestRefresh(nsNavHistoryContainerResultNode* aContainer);
 };
 
@@ -688,16 +679,18 @@ public:
   virtual uint16_t GetSortType();
   virtual void GetSortingAnnotation(nsACString& aSortingAnnotation);
   virtual void RecursiveSort(const char* aData,
                              SortComparator aComparator);
 
   nsCOMPtr<nsIURI> mRemovingURI;
   nsresult NotifyIfTagsChanged(nsIURI* aURI);
 
+  uint32_t mBatchChanges;
+
   // Tracks transition type filters shared by all mQueries.
   nsTArray<uint32_t> mTransitions;
 };
 
 
 // nsNavHistoryFolderResultNode
 //
 //    Overridden container type for bookmark folders. It will keep the contents
--- a/toolkit/components/places/tests/queries/head_queries.js
+++ b/toolkit/components/places/tests/queries/head_queries.js
@@ -272,17 +272,16 @@ function compareArrayToResult(aArray, aR
     aRoot.containerOpen = true;
 
   // check expected number of results against actual
   var expectedResultCount = aArray.filter(function(aEl) { return aEl.isInQuery; }).length;
   if (expectedResultCount != aRoot.childCount) {
     // Debugging code for failures.
     dump_table("moz_places");
     dump_table("moz_historyvisits");
-    dump_table("moz_bookmarks");
     LOG("Found children:");
     for (let i = 0; i < aRoot.childCount; i++) {
       LOG(aRoot.getChild(i).uri);
     }
     LOG("Expected:");
     for (let i = 0; i < aArray.length; i++) {
       if (aArray[i].isInQuery)
         LOG(aArray[i].uri);
--- a/toolkit/components/places/tests/queries/test_history_queries_tags_liveUpdate.js
+++ b/toolkit/components/places/tests/queries/test_history_queries_tags_liveUpdate.js
@@ -63,91 +63,96 @@ add_task(function test_initialize()
 });
 
 add_task(function pages_query()
 {
   let [query, options] = newQueryWithOptions();
   testQueryContents(query, options, function (root) {
     compareArrayToResult([gTestData[0], gTestData[1], gTestData[2]], root);
     for (let i = 0; i < root.childCount; i++) {
-      let uri = NetUtil.newURI(root.getChild(i).uri);
-      do_check_eq(root.getChild(i).tags, null);
+      let node = root.getChild(i);
+      let uri = NetUtil.newURI(node.uri);
+      do_check_eq(node.tags, null);
       PlacesUtils.tagging.tagURI(uri, ["test-tag"]);
-      do_check_eq(root.getChild(i).tags, "test-tag");
+      do_check_eq(node.tags, "test-tag");
       PlacesUtils.tagging.untagURI(uri, ["test-tag"]);
-      do_check_eq(root.getChild(i).tags, null);
+      do_check_eq(node.tags, null);
     }
   });
 });
 
 add_task(function visits_query()
 {
   let [query, options] = newQueryWithOptions();
   options.resultType = Ci.nsINavHistoryQueryOptions.RESULTS_AS_VISIT;
   testQueryContents(query, options, function (root) {
     compareArrayToResult([gTestData[0], gTestData[1], gTestData[2]], root);
     for (let i = 0; i < root.childCount; i++) {
-      let uri = NetUtil.newURI(root.getChild(i).uri);
-      do_check_eq(root.getChild(i).tags, null);
+      let node = root.getChild(i);
+      let uri = NetUtil.newURI(node.uri);
+      do_check_eq(node.tags, null);
       PlacesUtils.tagging.tagURI(uri, ["test-tag"]);
-      do_check_eq(root.getChild(i).tags, "test-tag");
+      do_check_eq(node.tags, "test-tag");
       PlacesUtils.tagging.untagURI(uri, ["test-tag"]);
-      do_check_eq(root.getChild(i).tags, null);
+      do_check_eq(node.tags, null);
     }
   });
 });
 
 add_task(function bookmarks_query()
 {
   let [query, options] = newQueryWithOptions();
   query.setFolders([PlacesUtils.unfiledBookmarksFolderId], 1);
   testQueryContents(query, options, function (root) {
     compareArrayToResult([gTestData[0], gTestData[1], gTestData[2]], root);
     for (let i = 0; i < root.childCount; i++) {
-      let uri = NetUtil.newURI(root.getChild(i).uri);
-      do_check_eq(root.getChild(i).tags, null);
+      let node = root.getChild(i);
+      let uri = NetUtil.newURI(node.uri);
+      do_check_eq(node.tags, null);
       PlacesUtils.tagging.tagURI(uri, ["test-tag"]);
-      do_check_eq(root.getChild(i).tags, "test-tag");
+      do_check_eq(node.tags, "test-tag");
       PlacesUtils.tagging.untagURI(uri, ["test-tag"]);
-      do_check_eq(root.getChild(i).tags, null);
+      do_check_eq(node.tags, null);
     }
   });
 });
 
 add_task(function pages_searchterm_query()
 {
   let [query, options] = newQueryWithOptions();
   query.searchTerms = "example";
   testQueryContents(query, options, function (root) {
     compareArrayToResult([gTestData[0], gTestData[1], gTestData[2]], root);
     for (let i = 0; i < root.childCount; i++) {
-      let uri = NetUtil.newURI(root.getChild(i).uri);
-      do_check_eq(root.getChild(i).tags, null);
+      let node = root.getChild(i);
+      let uri = NetUtil.newURI(node.uri);
+      do_check_eq(node.tags, null);
       PlacesUtils.tagging.tagURI(uri, ["test-tag"]);
-      do_check_eq(root.getChild(i).tags, "test-tag");
+      do_check_eq(node.tags, "test-tag");
       PlacesUtils.tagging.untagURI(uri, ["test-tag"]);
-      do_check_eq(root.getChild(i).tags, null);
+      do_check_eq(node.tags, null);
     }
   });
 });
 
 add_task(function visits_searchterm_query()
 {
   let [query, options] = newQueryWithOptions();
   query.searchTerms = "example";
   options.resultType = Ci.nsINavHistoryQueryOptions.RESULTS_AS_VISIT;
   testQueryContents(query, options, function (root) {
     compareArrayToResult([gTestData[0], gTestData[1], gTestData[2]], root);
     for (let i = 0; i < root.childCount; i++) {
-      let uri = NetUtil.newURI(root.getChild(i).uri);
-      do_check_eq(root.getChild(i).tags, null);
+      let node = root.getChild(i);
+      let uri = NetUtil.newURI(node.uri);
+      do_check_eq(node.tags, null);
       PlacesUtils.tagging.tagURI(uri, ["test-tag"]);
-      do_check_eq(root.getChild(i).tags, "test-tag");
+      do_check_eq(node.tags, "test-tag");
       PlacesUtils.tagging.untagURI(uri, ["test-tag"]);
-      do_check_eq(root.getChild(i).tags, null);
+      do_check_eq(node.tags, null);
     }
   });
 });
 
 add_task(function pages_searchterm_is_tag_query()
 {
   let [query, options] = newQueryWithOptions();
   query.searchTerms = "test-tag";
--- a/toolkit/components/places/tests/queries/test_history_queries_titles_liveUpdate.js
+++ b/toolkit/components/places/tests/queries/test_history_queries_titles_liveUpdate.js
@@ -30,17 +30,16 @@ let gTestData = [
 ];
 
 function searchNodeHavingUrl(aRoot, aUrl) {
   for (let i = 0; i < aRoot.childCount; i++) {
     if (aRoot.getChild(i).uri == aUrl) {
       return aRoot.getChild(i);
     }
   }
-  return null;
 }
 
 function newQueryWithOptions()
 {
   return [ PlacesUtils.history.getNewQuery(),
            PlacesUtils.history.getNewQueryOptions() ];
 }
 
@@ -54,22 +53,23 @@ add_task(function pages_query()
   yield task_populateDB(gTestData);
 
   let [query, options] = newQueryWithOptions();
   let root = PlacesUtils.history.executeQuery(query, options).root;
   root.containerOpen = true;
 
   compareArrayToResult([gTestData[0], gTestData[1], gTestData[2]], root);
   for (let i = 0; i < root.childCount; i++) {
-    do_check_eq(root.getChild(i).title, gTestData[i].title);
-    let uri = NetUtil.newURI(root.getChild(i).uri);
+    let node = root.getChild(i);
+    do_check_eq(node.title, gTestData[i].title);
+    let uri = NetUtil.newURI(node.uri);
     yield promiseAddVisits({uri: uri, title: "changedTitle"});
-    do_check_eq(root.getChild(i).title, "changedTitle");
+    do_check_eq(node.title, "changedTitle");
     yield promiseAddVisits({uri: uri, title: gTestData[i].title});
-    do_check_eq(root.getChild(i).title, gTestData[i].title);
+    do_check_eq(node.title, gTestData[i].title);
   }
 
   root.containerOpen = false;
   yield promiseClearHistory();
 });
 
 add_task(function visits_query()
 {
@@ -104,22 +104,23 @@ add_task(function pages_searchterm_query
 
   let [query, options] = newQueryWithOptions();
   query.searchTerms = "example";
   let root = PlacesUtils.history.executeQuery(query, options).root;
   root.containerOpen = true;
 
   compareArrayToResult([gTestData[0], gTestData[1], gTestData[2]], root);
   for (let i = 0; i < root.childCount; i++) {
-    let uri = NetUtil.newURI(root.getChild(i).uri);
-    do_check_eq(root.getChild(i).title, gTestData[i].title);
+    let node = root.getChild(i);
+    let uri = NetUtil.newURI(node.uri);
+    do_check_eq(node.title, gTestData[i].title);
     yield promiseAddVisits({uri: uri, title: "changedTitle"});
-    do_check_eq(root.getChild(i).title, "changedTitle");
+    do_check_eq(node.title, "changedTitle");
     yield promiseAddVisits({uri: uri, title: gTestData[i].title});
-    do_check_eq(root.getChild(i).title, gTestData[i].title);
+    do_check_eq(node.title, gTestData[i].title);
   }
 
   root.containerOpen = false;
   yield promiseClearHistory();
 });
 
 add_task(function visits_searchterm_query()
 {
--- a/toolkit/components/places/tests/unit/test_nsINavHistoryViewer.js
+++ b/toolkit/components/places/tests/unit/test_nsINavHistoryViewer.js
@@ -58,16 +58,17 @@ var resultObserver = {
     this.invalidatedContainer = node;
   },
   sortingMode: null,
   sortingChanged: function(sortingMode) {
     this.sortingMode = sortingMode;
   },
   inBatchMode: false,
   batching: function(aToggleMode) {
+    do_check_neq(this.inBatchMode, aToggleMode);
     this.inBatchMode = aToggleMode;
   },
   result: null,
   reset: function() {
     this.insertedNode = null;
     this.removedNode = null;
     this.nodeChangedByTitle = null;
     this.nodeChangedByHistoryDetails = null;
@@ -75,112 +76,104 @@ var resultObserver = {
     this.movedNode = null;
     this.openedContainer = null;
     this.closedContainer = null;
     this.invalidatedContainer = null;
     this.sortingMode = null;
   }
 };
 
-function promiseBatch(aResult) {
-  let query = PlacesUtils.asQuery(aResult.root).getQueries()[0];
-  return Task.spawn(function() {
-    let deferred = Promise.defer();
-    aResult.addObserver({
-      batching: function (aStatus) {
-        if (!aStatus) {
-          deferred.resolve();
-          aResult.removeObserver(this);
-        }
-      }
-    }, false);
-    for (let i = 0; i < 10; i++) {
-      if (query.onlyBookmarked)
-        bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, testURI, bmsvc.DEFAULT_INDEX, "foo");
-      else
-        yield promiseAddVisits(testURI);
-    }
-    yield deferred.promise;
-  });
-}
-
 var testURI = uri("http://mozilla.com");
 
 function run_test() {
   run_next_test();
 }
 
-add_task(function check_history_query() {
+add_test(function check_history_query() {
   var options = histsvc.getNewQueryOptions();
   options.sortingMode = options.SORT_BY_DATE_DESCENDING;
   options.resultType = options.RESULTS_AS_VISIT;
   var query = histsvc.getNewQuery();
   var result = histsvc.executeQuery(query, options);
   result.addObserver(resultObserver, false);
   var root = result.root;
   root.containerOpen = true;
 
-  // nsINavHistoryResultObserver.containerStateChanged
+  // nsINavHistoryResultObserver.containerOpened
   do_check_neq(resultObserver.openedContainer, null);
 
   // nsINavHistoryResultObserver.nodeInserted
-  yield promiseAddVisits(testURI);
-  do_check_eq(testURI.spec, resultObserver.insertedNode.uri);
+  // add a visit
+  promiseAddVisits(testURI).then(function() {
+    do_check_eq(testURI.spec, resultObserver.insertedNode.uri);
 
-  // nsINavHistoryResultObserver.nodeHistoryDetailsChanged
-  // adding a visit causes nodeHistoryDetailsChanged for the folder
-  do_check_eq(root.uri, resultObserver.nodeChangedByHistoryDetails.uri);
+    // nsINavHistoryResultObserver.nodeHistoryDetailsChanged
+    // adding a visit causes nodeHistoryDetailsChanged for the folder
+    do_check_eq(root.uri, resultObserver.nodeChangedByHistoryDetails.uri);
+
+    // nsINavHistoryResultObserver.itemTitleChanged for a leaf node
+    promiseAddVisits({ uri: testURI, title: "baz" }).then(function () {
+      do_check_eq(resultObserver.nodeChangedByTitle.title, "baz");
 
-  // nsINavHistoryResultObserver.itemTitleChanged for a leaf node
-  yield promiseAddVisits({ uri: testURI, title: "baz" });
-
-  do_check_eq(resultObserver.nodeChangedByTitle.title, "baz");
+      // nsINavHistoryResultObserver.nodeRemoved
+      var removedURI = uri("http://google.com");
+      promiseAddVisits(removedURI).then(function() {
+        bhist.removePage(removedURI);
+        do_check_eq(removedURI.spec, resultObserver.removedNode.uri);
 
-  // nsINavHistoryResultObserver.nodeRemoved
-  var removedURI = uri("http://google.com");
-  yield promiseAddVisits(removedURI);
-  bhist.removePage(removedURI);
-  do_check_eq(removedURI.spec, resultObserver.removedNode.uri);
+        // nsINavHistoryResultObserver.invalidateContainer
+        bhist.removePagesFromHost("mozilla.com", false);
+        do_check_eq(root.uri, resultObserver.invalidatedContainer.uri);
+
+        // nsINavHistoryResultObserver.sortingChanged
+        resultObserver.invalidatedContainer = null;
+        result.sortingMode = options.SORT_BY_TITLE_ASCENDING;
+        do_check_eq(resultObserver.sortingMode, options.SORT_BY_TITLE_ASCENDING);
+        do_check_eq(resultObserver.invalidatedContainer, result.root);
 
-  // nsINavHistoryResultObserver.sortingChanged
-  resultObserver.invalidatedContainer = null;
-  result.sortingMode = options.SORT_BY_TITLE_ASCENDING;
-  do_check_eq(resultObserver.sortingMode, options.SORT_BY_TITLE_ASCENDING);
-  do_check_eq(resultObserver.invalidatedContainer, result.root);
-
-  // nsINavHistoryResultObserver.invalidateContainer
-  bhist.removeAllPages();
-  do_check_eq(root.uri, resultObserver.invalidatedContainer.uri);
+        // nsINavHistoryResultObserver.invalidateContainer
+        bhist.removeAllPages();
+        do_check_eq(root.uri, resultObserver.invalidatedContainer.uri);
 
-  // nsINavHistoryResultObserver.batching
-  do_check_false(resultObserver.inBatchMode);
-  yield promiseBatch(result);
-
-  // nsINavHistoryResultObserver.invalidateContainer
-  do_check_eq(root.uri, resultObserver.invalidatedContainer.uri);
+        // nsINavHistoryResultObserver.batching
+        do_check_false(resultObserver.inBatchMode);
+        histsvc.runInBatchMode({
+          runBatched: function (aUserData) {
+            do_check_true(resultObserver.inBatchMode);
+          }
+        }, null);
+        do_check_false(resultObserver.inBatchMode);
+        bmsvc.runInBatchMode({
+          runBatched: function (aUserData) {
+            do_check_true(resultObserver.inBatchMode);
+          }
+        }, null);
+        do_check_false(resultObserver.inBatchMode);
 
-  // nsINavHistoryResultObserver.containerStateChanged
-  root.containerOpen = false;
-  do_check_eq(resultObserver.closedContainer, resultObserver.openedContainer);
-  result.removeObserver(resultObserver);
-  resultObserver.reset();
-
-  yield promiseAsyncUpdates();
+        // nsINavHistoryResultObserver.containerClosed
+        root.containerOpen = false;
+        do_check_eq(resultObserver.closedContainer, resultObserver.openedContainer);
+        result.removeObserver(resultObserver);
+        resultObserver.reset();
+        promiseAsyncUpdates().then(run_next_test);
+      });
+    });
+  });
 });
 
-add_task(function check_bookmarks_query() {
+add_test(function check_bookmarks_query() {
   var options = histsvc.getNewQueryOptions();
   var query = histsvc.getNewQuery();
   query.setFolders([bmsvc.bookmarksMenuFolder], 1);
   var result = histsvc.executeQuery(query, options);
   result.addObserver(resultObserver, false);
   var root = result.root;
   root.containerOpen = true;
 
-  // nsINavHistoryResultObserver.containerStateChanged
+  // nsINavHistoryResultObserver.containerOpened
   do_check_neq(resultObserver.openedContainer, null);
 
   // nsINavHistoryResultObserver.nodeInserted
   // add a bookmark
   var testBookmark = bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, testURI, bmsvc.DEFAULT_INDEX, "foo");
   do_check_eq("foo", resultObserver.insertedNode.title);
   do_check_eq(testURI.spec, resultObserver.insertedNode.uri);
 
@@ -196,57 +189,73 @@ add_task(function check_bookmarks_query(
   var testBookmark2 = bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, uri("http://google.com"), bmsvc.DEFAULT_INDEX, "foo");
   bmsvc.moveItem(testBookmark2, bmsvc.bookmarksMenuFolder, 0);
   do_check_eq(resultObserver.movedNode.itemId, testBookmark2);
 
   // nsINavHistoryResultObserver.nodeRemoved
   bmsvc.removeItem(testBookmark2);
   do_check_eq(testBookmark2, resultObserver.removedNode.itemId);
 
+  // XXX nsINavHistoryResultObserver.invalidateContainer
+
   // nsINavHistoryResultObserver.sortingChanged
   resultObserver.invalidatedContainer = null;
   result.sortingMode = options.SORT_BY_TITLE_ASCENDING;
   do_check_eq(resultObserver.sortingMode, options.SORT_BY_TITLE_ASCENDING);
   do_check_eq(resultObserver.invalidatedContainer, result.root);
 
   // nsINavHistoryResultObserver.batching
   do_check_false(resultObserver.inBatchMode);
-  yield promiseBatch(result);
+  histsvc.runInBatchMode({
+    runBatched: function (aUserData) {
+      do_check_true(resultObserver.inBatchMode);
+    }
+  }, null);
+  do_check_false(resultObserver.inBatchMode);
+  bmsvc.runInBatchMode({
+    runBatched: function (aUserData) {
+      do_check_true(resultObserver.inBatchMode);
+    }
+  }, null);
+  do_check_false(resultObserver.inBatchMode);
 
-  // nsINavHistoryResultObserver.invalidateContainer
-  do_check_eq(root.uri, resultObserver.invalidatedContainer.uri);
-
-  // nsINavHistoryResultObserver.containerStateChanged
+  // nsINavHistoryResultObserver.containerClosed
   root.containerOpen = false;
   do_check_eq(resultObserver.closedContainer, resultObserver.openedContainer);
   result.removeObserver(resultObserver);
   resultObserver.reset();
-
-  yield promiseAsyncUpdates();
+  promiseAsyncUpdates().then(run_next_test);
 });
 
-add_task(function check_mixed_query() {
+add_test(function check_mixed_query() {
   var options = histsvc.getNewQueryOptions();
   var query = histsvc.getNewQuery();
   query.onlyBookmarked = true;
   var result = histsvc.executeQuery(query, options);
   result.addObserver(resultObserver, false);
   var root = result.root;
   root.containerOpen = true;
 
-  // nsINavHistoryResultObserver.containerStateChanged
+  // nsINavHistoryResultObserver.containerOpened
   do_check_neq(resultObserver.openedContainer, null);
 
   // nsINavHistoryResultObserver.batching
   do_check_false(resultObserver.inBatchMode);
-  yield promiseBatch(result);
+  histsvc.runInBatchMode({
+    runBatched: function (aUserData) {
+      do_check_true(resultObserver.inBatchMode);
+    }
+  }, null);
+  do_check_false(resultObserver.inBatchMode);
+  bmsvc.runInBatchMode({
+    runBatched: function (aUserData) {
+      do_check_true(resultObserver.inBatchMode);
+    }
+  }, null);
+  do_check_false(resultObserver.inBatchMode);
 
-  // nsINavHistoryResultObserver.invalidateContainer
-  do_check_eq(root.uri, resultObserver.invalidatedContainer.uri);
-
-  // nsINavHistoryResultObserver.containerStateChanged
+  // nsINavHistoryResultObserver.containerClosed
   root.containerOpen = false;
   do_check_eq(resultObserver.closedContainer, resultObserver.openedContainer);
   result.removeObserver(resultObserver);
   resultObserver.reset();
-
-  yield promiseAsyncUpdates();
+  promiseAsyncUpdates().then(run_next_test);
 });