Bug 1421703 - replace onVisit with onVisits r?mak draft
authorDoug Thayer <dothayer@mozilla.com>
Wed, 20 Dec 2017 14:27:24 -0800
changeset 718599 5ea9b38ff006f4adfbcf8e1832646c826a1b934a
parent 718598 2438a090b77a56b38547f7250bb499cb2e1ffcce
child 718600 2fa49be7ce9c997129f286be86bdd4587f5b6958
push id94999
push userbmo:dothayer@mozilla.com
push dateWed, 10 Jan 2018 17:15:18 +0000
reviewersmak
bugs1421703
milestone59.0a1
Bug 1421703 - replace onVisit with onVisits r?mak There's a heavy enough overhead to going through XPConnect for every observer for every visit on the nsINavHistoryObserver interface, so this patch reduces that by replacing the single- visit notification with one which accepts an array of visits. Some notes: To avoid problems with the orderings of the various ways in which we notify about visits, we have to send our bulk onVisits notification before doing any of the others. This does mean it technically behaves slightly different than the prior approach of interleaving the notifications, but I can't find any way in which this has any consequences to the end result, and it doesn't break any tests. MozReview-Commit-ID: GdeooH8mCkg
browser/components/extensions/ext-history.js
browser/extensions/activity-stream/lib/PlacesFeed.jsm
browser/modules/WindowsPreviewPerTab.jsm
services/sync/modules/engines/history.js
toolkit/components/jsdownloads/src/DownloadIntegration.jsm
toolkit/components/places/History.cpp
toolkit/components/places/nsINavHistoryService.idl
toolkit/components/places/nsLivemarkService.js
toolkit/components/places/nsNavBookmarks.cpp
toolkit/components/places/nsNavBookmarks.h
toolkit/components/places/nsNavHistory.cpp
toolkit/components/places/nsNavHistory.h
toolkit/components/places/nsNavHistoryResult.cpp
toolkit/components/places/nsNavHistoryResult.h
toolkit/components/places/nsPlacesExpiration.js
toolkit/components/thumbnails/PageThumbs.jsm
toolkit/modules/NewTabUtils.jsm
--- a/browser/components/extensions/ext-history.js
+++ b/browser/components/extensions/ext-history.js
@@ -92,26 +92,28 @@ const convertNavHistoryContainerResultNo
 var _observer;
 
 const getHistoryObserver = () => {
   if (!_observer) {
     _observer = new class extends EventEmitter {
       onDeleteURI(uri, guid, reason) {
         this.emit("visitRemoved", {allHistory: false, urls: [uri.spec]});
       }
-      onVisit(uri, visitId, time, sessionId, referringId, transitionType, guid, hidden, visitCount, typed, lastKnownTitle) {
-        let data = {
-          id: guid,
-          url: uri.spec,
-          title: lastKnownTitle || "",
-          lastVisitTime: time / 1000,  // time from Places is microseconds,
-          visitCount,
-          typedCount: typed,
-        };
-        this.emit("visited", data);
+      onVisits(visits) {
+        for (let visit of visits) {
+          let data = {
+            id: visit.guid,
+            url: visit.uri.spec,
+            title: visit.lastKnownTitle || "",
+            lastVisitTime: visit.time / 1000,  // time from Places is microseconds,
+            visitCount: visit.visitCount,
+            typedCount: visit.typed,
+          };
+          this.emit("visited", data);
+        }
       }
       onBeginUpdateBatch() {}
       onEndUpdateBatch() {}
       onTitleChanged(uri, title) {
         this.emit("titleChanged", {url: uri.spec, title: title});
       }
       onClearHistory() {
         this.emit("visitRemoved", {allHistory: true, urls: []});
--- a/browser/extensions/activity-stream/lib/PlacesFeed.jsm
+++ b/browser/extensions/activity-stream/lib/PlacesFeed.jsm
@@ -67,17 +67,17 @@ class HistoryObserver extends Observer {
    */
   onClearHistory() {
     this.dispatch({type: at.PLACES_HISTORY_CLEARED});
   }
 
   // Empty functions to make xpconnect happy
   onBeginUpdateBatch() {}
   onEndUpdateBatch() {}
-  onVisit() {}
+  onVisits() {}
   onTitleChanged() {}
   onFrecencyChanged() {}
   onManyFrecenciesChanged() {}
   onPageChanged() {}
   onDeleteVisits() {}
 }
 
 /**
--- a/browser/modules/WindowsPreviewPerTab.jsm
+++ b/browser/modules/WindowsPreviewPerTab.jsm
@@ -838,17 +838,17 @@ this.AeroPeek = {
         });
         break;
     }
   },
 
   /* nsINavHistoryObserver implementation */
   onBeginUpdateBatch() {},
   onEndUpdateBatch() {},
-  onVisit() {},
+  onVisits() {},
   onTitleChanged() {},
   onFrecencyChanged() {},
   onManyFrecenciesChanged() {},
   onDeleteURI() {},
   onClearHistory() {},
   onDeleteVisits() {},
   onPageChanged(uri, changedConst, newValue) {
     if (this.enabled && changedConst == Ci.nsINavHistoryObserver.ATTRIBUTE_FAVICON) {
--- a/services/sync/modules/engines/history.js
+++ b/services/sync/modules/engines/history.js
@@ -465,25 +465,27 @@ HistoryTracker.prototype = {
   onDeleteVisits(uri, visitTime, guid, reason) {
     this.onDeleteAffectsGUID(uri, guid, reason, "onDeleteVisits", SCORE_INCREMENT_SMALL);
   },
 
   onDeleteURI(uri, guid, reason) {
     this.onDeleteAffectsGUID(uri, guid, reason, "onDeleteURI", SCORE_INCREMENT_XLARGE);
   },
 
-  onVisit(uri, vid, time, session, referrer, trans, guid) {
+  onVisits(aVisits) {
     if (this.ignoreAll) {
-      this._log.trace("ignoreAll: ignoring visit for " + guid);
+      this._log.trace("ignoreAll: ignoring visits [" +
+                      aVisits.map(v => v.guid).join(",") + "]");
       return;
     }
-
-    this._log.trace("onVisit: " + uri.spec);
-    if (this.engine.shouldSyncURL(uri.spec) && this.addChangedID(guid)) {
-      this.score += SCORE_INCREMENT_SMALL;
+    for (let {uri, guid} of aVisits) {
+      this._log.trace("onVisits: " + uri.spec);
+      if (this.engine.shouldSyncURL(uri.spec) && 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
     // pages are tracked, so the deletions will not be propagated.
     // See Bug 578694.
--- a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
@@ -1023,17 +1023,17 @@ this.DownloadHistoryObserver.prototype =
   // nsINavHistoryObserver
   onClearHistory: function DL_onClearHistory() {
     this._list.removeFinished();
   },
 
   onTitleChanged() {},
   onBeginUpdateBatch() {},
   onEndUpdateBatch() {},
-  onVisit() {},
+  onVisits() {},
   onPageChanged() {},
   onDeleteVisits() {},
 };
 
 /**
  * This view can be added to a DownloadList object to trigger a save operation
  * in the given DownloadStore object when a relevant change occurs.  You should
  * call the "initialize" method in order to register the view and load the
--- a/toolkit/components/places/History.cpp
+++ b/toolkit/components/places/History.cpp
@@ -164,16 +164,131 @@ struct VisitData {
   // Indicates whether frecency should be updated for this visit.
   bool shouldUpdateFrecency;
 
   // Whether this is a redirect source.
   bool redirect;
 };
 
 ////////////////////////////////////////////////////////////////////////////////
+//// nsVisitData
+
+class nsVisitData : public nsIVisitData
+{
+public:
+  explicit nsVisitData(nsIURI* aURI,
+                       int64_t aVisitId,
+                       PRTime aTime,
+                       int64_t aReferrerVisitId,
+                       int32_t aTransitionType,
+                       const nsACString& aGuid,
+                       bool aHidden,
+                       uint32_t aVisitCount,
+                       uint32_t aTyped,
+                       const nsAString& aLastKnownTitle)
+    : mURI(aURI)
+    , mVisitId(aVisitId)
+    , mTime(aTime)
+    , mReferrerVisitId(aReferrerVisitId)
+    , mTransitionType(aTransitionType)
+    , mGuid(aGuid)
+    , mHidden(aHidden)
+    , mVisitCount(aVisitCount)
+    , mTyped(aTyped)
+    , mLastKnownTitle(aLastKnownTitle)
+  {
+    MOZ_ASSERT(NS_IsMainThread(),
+               "nsVisitData should only be constructed on the main thread.");
+  }
+
+  NS_DECL_ISUPPORTS
+
+  NS_IMETHOD GetUri(nsIURI** aUri) override
+  {
+    NS_ENSURE_ARG_POINTER(aUri);
+    *aUri = mURI;
+    NS_IF_ADDREF(*aUri);
+    return NS_OK;
+  }
+
+  NS_IMETHOD GetVisitId(int64_t* aVisitId) override
+  {
+    *aVisitId = mVisitId;
+    return NS_OK;
+  }
+
+  NS_IMETHOD GetTime(PRTime* aTime) override
+  {
+    *aTime = mTime;
+    return NS_OK;
+  }
+
+  NS_IMETHOD GetReferrerId(int64_t* aReferrerVisitId) override
+  {
+    *aReferrerVisitId = mReferrerVisitId;
+    return NS_OK;
+  }
+
+  NS_IMETHOD GetTransitionType(uint32_t* aTransitionType) override
+  {
+    *aTransitionType = mTransitionType;
+    return NS_OK;
+  }
+
+  NS_IMETHOD GetGuid(nsACString& aGuid) override
+  {
+    aGuid.Assign(mGuid);
+    return NS_OK;
+  }
+
+  NS_IMETHOD GetHidden(bool* aHidden) override
+  {
+    *aHidden = mHidden;
+    return NS_OK;
+  }
+
+  NS_IMETHOD GetVisitCount(uint32_t* aVisitCount) override
+  {
+    *aVisitCount = mVisitCount;
+    return NS_OK;
+  }
+
+  NS_IMETHOD GetTyped(uint32_t* aTyped) override
+  {
+    *aTyped = mTyped;
+    return NS_OK;
+  }
+
+  NS_IMETHOD GetLastKnownTitle(nsAString& aLastKnownTitle) override
+  {
+    aLastKnownTitle.Assign(mLastKnownTitle);
+    return NS_OK;
+  }
+
+private:
+  virtual ~nsVisitData() {
+    MOZ_ASSERT(NS_IsMainThread(),
+               "nsVisitData should only be destructed on the main thread.");
+  };
+
+  nsCOMPtr<nsIURI> mURI;
+  int64_t mVisitId;
+  PRTime mTime;
+  int64_t mReferrerVisitId;
+  uint32_t mTransitionType;
+  nsCString mGuid;
+  bool mHidden;
+  uint32_t mVisitCount;
+  uint32_t mTyped;
+  nsString mLastKnownTitle;
+};
+
+NS_IMPL_ISUPPORTS(nsVisitData, nsIVisitData)
+
+////////////////////////////////////////////////////////////////////////////////
 //// RemoveVisitsFilter
 
 /**
  * Used to store visit filters for RemoveVisits.
  */
 struct RemoveVisitsFilter {
   RemoveVisitsFilter()
   : transitionType(UINT32_MAX)
@@ -643,56 +758,51 @@ public:
     , mHistory(History::GetService())
   {
     aPlaces.SwapElements(mPlaces);
   }
 
   nsresult NotifyVisit(nsNavHistory* aNavHistory,
                        nsCOMPtr<nsIObserverService>& aObsService,
                        PRTime aNow,
-                       nsTArray<URIParams>& aNotifyVisitedURIs,
+                       nsIURI* aURI,
                        const VisitData& aPlace) {
-    nsCOMPtr<nsIURI> uri;
-    MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), aPlace.spec));
-    if (!uri) {
-      return NS_ERROR_UNEXPECTED;
-    }
-    // Notify the visit.  Note that TRANSITION_EMBED visits are never added
-    // to the database, thus cannot be queried and we don't notify them.
-    if (aPlace.transitionType != nsINavHistoryService::TRANSITION_EMBED) {
-      aNavHistory->NotifyOnVisit(uri, aPlace.visitId, aPlace.visitTime,
-                                 aPlace.referrerVisitId, aPlace.transitionType,
-                                 aPlace.guid, aPlace.hidden,
-                                 aPlace.visitCount + 1, // Add current visit.
-                                 static_cast<uint32_t>(aPlace.typed),
-                                 aPlace.title);
-    }
-
     if (aObsService) {
       DebugOnly<nsresult> rv =
-        aObsService->NotifyObservers(uri, URI_VISIT_SAVED, nullptr);
+        aObsService->NotifyObservers(aURI, URI_VISIT_SAVED, nullptr);
       NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Could not notify observers");
     }
 
     if (aNow - aPlace.visitTime < RECENTLY_VISITED_URIS_MAX_AGE) {
-      mHistory->AppendToRecentlyVisitedURIs(uri);
+      mHistory->AppendToRecentlyVisitedURIs(aURI);
     }
-    mHistory->NotifyVisited(uri);
+    mHistory->NotifyVisited(aURI);
 
     if (aPlace.titleChanged) {
-      aNavHistory->NotifyTitleChange(uri, aPlace.title, aPlace.guid);
+      aNavHistory->NotifyTitleChange(aURI, aPlace.title, aPlace.guid);
     }
 
-    URIParams serialized;
-    SerializeURI(uri, serialized);
-    aNotifyVisitedURIs.AppendElement(Move(serialized));
-
     return NS_OK;
   }
 
+  void AddPlaceForNotify(const VisitData& aPlace,
+                         nsIURI* aURI,
+                         nsCOMArray<nsIVisitData>& aPlaces) {
+    if (aPlace.transitionType != nsINavHistoryService::TRANSITION_EMBED) {
+      nsCOMPtr<nsIVisitData> notifyPlace = new nsVisitData(
+        aURI, aPlace.visitId, aPlace.visitTime,
+        aPlace.referrerVisitId, aPlace.transitionType,
+        aPlace.guid, aPlace.hidden,
+        aPlace.visitCount + 1, // Add current visit.
+        static_cast<uint32_t>(aPlace.typed),
+        aPlace.title);
+      aPlaces.AppendElement(notifyPlace.forget());
+    }
+  }
+
   NS_IMETHOD Run() override
   {
     MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread");
 
     // We are in the main thread, no need to lock.
     if (mHistory->IsShuttingDown()) {
       // If we are shutting down, we cannot notify the observers.
       return NS_OK;
@@ -702,29 +812,62 @@ public:
     if (!navHistory) {
       NS_WARNING("Trying to notify visits observers but cannot get the history service!");
       return NS_OK;
     }
 
     nsCOMPtr<nsIObserverService> obsService =
       mozilla::services::GetObserverService();
 
+    nsCOMArray<nsIVisitData> places;
+    nsCOMArray<nsIURI> uris;
+    if (mPlaces.Length() > 0) {
+      for (uint32_t i = 0; i < mPlaces.Length(); ++i) {
+        nsCOMPtr<nsIURI> uri;
+        MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), mPlaces[i].spec));
+        if (!uri) {
+          return NS_ERROR_UNEXPECTED;
+        }
+        AddPlaceForNotify(mPlaces[i], uri, places);
+        uris.AppendElement(uri.forget());
+      }
+    } else {
+      nsCOMPtr<nsIURI> uri;
+      MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), mPlace.spec));
+      if (!uri) {
+        return NS_ERROR_UNEXPECTED;
+      }
+      AddPlaceForNotify(mPlace, uri, places);
+      uris.AppendElement(uri.forget());
+    }
+    if (places.Length() > 0) {
+      navHistory->NotifyOnVisits(places.Elements(), places.Length());
+    }
+
     PRTime now = PR_Now();
     if (mPlaces.Length() > 0) {
-      InfallibleTArray<URIParams> uris(mPlaces.Length());
+      InfallibleTArray<URIParams> serializableUris(mPlaces.Length());
       for (uint32_t i = 0; i < mPlaces.Length(); ++i) {
-        nsresult rv = NotifyVisit(navHistory, obsService, now, uris, mPlaces[i]);
+        nsresult rv = NotifyVisit(navHistory, obsService, now, uris[i], mPlaces[i]);
         NS_ENSURE_SUCCESS(rv, rv);
+
+        URIParams serializedUri;
+        SerializeURI(uris[i], serializedUri);
+        serializableUris.AppendElement(Move(serializedUri));
       }
-      mHistory->NotifyVisitedParent(uris);
+      mHistory->NotifyVisitedParent(serializableUris);
     } else {
-      AutoTArray<URIParams, 1> uris;
-      nsresult rv = NotifyVisit(navHistory, obsService, now, uris, mPlace);
+      AutoTArray<URIParams, 1> serializableUris;
+      nsresult rv = NotifyVisit(navHistory, obsService, now, uris[0], mPlace);
       NS_ENSURE_SUCCESS(rv, rv);
-      mHistory->NotifyVisitedParent(uris);
+
+      URIParams serializedUri;
+      SerializeURI(uris[0], serializedUri);
+      serializableUris.AppendElement(Move(serializedUri));
+      mHistory->NotifyVisitedParent(serializableUris);
     }
 
     return NS_OK;
   }
 private:
   nsTArray<VisitData> mPlaces;
   VisitData mPlace;
   RefPtr<History> mHistory;
--- a/toolkit/components/places/nsINavHistoryService.idl
+++ b/toolkit/components/places/nsINavHistoryService.idl
@@ -18,16 +18,77 @@ interface nsIFile;
 
 interface nsINavHistoryContainerResultNode;
 interface nsINavHistoryQueryResultNode;
 interface nsINavHistoryQuery;
 interface nsINavHistoryQueryOptions;
 interface nsINavHistoryResult;
 interface nsINavHistoryBatchCallback;
 
+/**
+ * This interface exists specifically for passing visit information
+ * in bulk to onVisits below.
+ */
+[scriptable, uuid(9d8df1ff-142f-4ca7-9f45-3c62a508c7e2)]
+interface nsIVisitData : nsISupports
+{
+  /**
+   * URI of the visit that was just created.
+   */
+  readonly attribute nsIURI uri;
+
+  /**
+   * Id of the visit that was just created.
+   */
+  readonly attribute long long visitId;
+
+  /**
+   * Time of the visit.
+   */
+  readonly attribute PRTime time;
+
+  /**
+   * The id of the visit the user came from, defaults to 0 for no referrer.
+   */
+  readonly attribute long long referrerId;
+
+  /**
+   * One of nsINavHistory.TRANSITION_*
+   */
+  readonly attribute unsigned long transitionType;
+
+  /**
+   * The unique id associated with the page.
+   */
+  readonly attribute ACString guid;
+
+  /**
+   * Whether the visited page is marked as hidden.
+   */
+  readonly attribute boolean hidden;
+
+  /**
+   * Number of visits (included this one) for this URI.
+   */
+  readonly attribute unsigned long visitCount;
+
+  /**
+   * Whether the URI has been typed or not.
+   * TODO (Bug 1271801): This will become a count, rather than a boolean.
+   * For future compatibility, always compare it with "> 0".
+   */
+  readonly attribute unsigned long typed;
+
+  /**
+   * The last known title of the page. Might not be from the current visit,
+   * and might be null if it is not known.
+   */
+  readonly attribute AString lastKnownTitle;
+};
+
 [scriptable, uuid(91d104bb-17ef-404b-9f9a-d9ed8de6824c)]
 interface nsINavHistoryResultNode : nsISupports
 {
   /**
    * Indentifies the parent result node in the result set. This is null for
    * top level nodes.
    */
   readonly attribute nsINavHistoryContainerResultNode parent;
@@ -618,58 +679,26 @@ interface nsINavHistoryObserver : nsISup
 
   /**
    * Notifies you that we are done doing a bunch of things and you should go
    * ahead and update UI, etc.
    */
   void onEndUpdateBatch();
 
   /**
-   * Called everytime a URI is visited.
+   * Called everytime a URI is visited, or once for a batch of visits if visits were
+   * added in bulk.
    *
    * @note TRANSITION_EMBED visits (corresponding to images in a page, for
    *       example) are not displayed in history results. Most observers can
    *       ignore TRANSITION_EMBED visit notifications (which will comprise the
    *       majority of visit notifications) to save work.
-   *
-   * @param aVisitId
-   *        Id of the visit that was just created.
-   * @param aTime
-   *        Time of the visit.
-   * @param aSessionId
-   *        No longer supported and always set to 0.
-   * @param aReferrerVisitId
-   *        The id of the visit the user came from, defaults to 0 for no referrer.
-   * @param aTransitionType
-   *        One of nsINavHistory.TRANSITION_*
-   * @param aGuid
-   *        The unique id associated with the page.
-   * @param aHidden
-   *        Whether the visited page is marked as hidden.
-   * @param aVisitCount
-   *        Number of visits (included this one) for this URI.
-   * @param aTyped
-   *        Whether the URI has been typed or not.
-   *        TODO (Bug 1271801): This will become a count, rather than a boolean.
-   *        For future compatibility, always compare it with "> 0".
-   * @param aLastKnownTitle
-   *        The last known title of the page. Might not be from the current visit,
-   *        and might be null if it is not known.
    */
-  void onVisit(in nsIURI aURI,
-               in long long aVisitId,
-               in PRTime aTime,
-               in long long aSessionId,
-               in long long aReferrerVisitId,
-               in unsigned long aTransitionType,
-               in ACString aGuid,
-               in boolean aHidden,
-               in unsigned long aVisitCount,
-               in unsigned long aTyped,
-               in AString aLastKnownTitle);
+  void onVisits([array, size_is(aVisitsCount)] in nsIVisitData aVisits,
+                in unsigned long aVisitsCount);
 
   /**
    * Called whenever either the "real" title or the custom title of the page
    * changed. BOTH TITLES ARE ALWAYS INCLUDED in this notification, even though
    * only one will change at a time. Often, consumers will want to display the
    * user title if it is available, and fall back to the page title (the one
    * specified in the <title> tag of the page).
    *
--- a/toolkit/components/places/nsLivemarkService.js
+++ b/toolkit/components/places/nsLivemarkService.js
@@ -409,20 +409,22 @@ LivemarkService.prototype = {
   onDeleteURI(aURI) {
     this._withLivemarksMap(livemarksMap => {
       for (let livemark of livemarksMap.values()) {
         livemark.updateURIVisitedStatus(aURI, false);
       }
     });
   },
 
-  onVisit(aURI) {
+  onVisits(aVisits) {
     this._withLivemarksMap(livemarksMap => {
-      for (let livemark of livemarksMap.values()) {
-        livemark.updateURIVisitedStatus(aURI, true);
+      for (let {uri} of aVisits) {
+        for (let livemark of livemarksMap.values()) {
+          livemark.updateURIVisitedStatus(uri, true);
+        }
       }
     });
   },
 
   // nsISupports
 
   classID: Components.ID("{dca61eb5-c7cd-4df1-b0fb-d0722baba251}"),
 
--- a/toolkit/components/places/nsNavBookmarks.cpp
+++ b/toolkit/components/places/nsNavBookmarks.cpp
@@ -3151,35 +3151,38 @@ nsNavBookmarks::OnEndUpdateBatch()
 
   NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
                    nsINavBookmarkObserver, OnEndUpdateBatch());
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
-nsNavBookmarks::OnVisit(nsIURI* aURI, int64_t aVisitId, PRTime aTime,
-                        int64_t aSessionID, int64_t aReferringID,
-                        uint32_t aTransitionType, const nsACString& aGUID,
-                        bool aHidden, uint32_t aVisitCount, uint32_t aTyped,
-                        const nsAString& aLastKnownTitle)
+nsNavBookmarks::OnVisits(nsIVisitData** aVisits, uint32_t aVisitsCount)
 {
-  NS_ENSURE_ARG(aURI);
-
-  // If the page is bookmarked, notify observers for each associated bookmark.
-  ItemVisitData visitData;
-  nsresult rv = aURI->GetSpec(visitData.bookmark.url);
-  NS_ENSURE_SUCCESS(rv, rv);
-  visitData.visitId = aVisitId;
-  visitData.time = aTime;
-  visitData.transitionType = aTransitionType;
-
-  RefPtr< AsyncGetBookmarksForURI<ItemVisitMethod, ItemVisitData> > notifier =
-    new AsyncGetBookmarksForURI<ItemVisitMethod, ItemVisitData>(this, &nsNavBookmarks::NotifyItemVisited, visitData);
-  notifier->Init();
+  NS_ENSURE_ARG(aVisits);
+  NS_ENSURE_ARG(aVisitsCount);
+
+  for (uint32_t i = 0; i < aVisitsCount; ++i) {
+    nsIVisitData* place = aVisits[i];
+    nsCOMPtr<nsIURI> uri;
+    MOZ_ALWAYS_SUCCEEDS(place->GetUri(getter_AddRefs(uri)));
+
+    // If the page is bookmarked, notify observers for each associated bookmark.
+    ItemVisitData visitData;
+    nsresult rv = uri->GetSpec(visitData.bookmark.url);
+    NS_ENSURE_SUCCESS(rv, rv);
+    MOZ_ALWAYS_SUCCEEDS(place->GetVisitId(&visitData.visitId));
+    MOZ_ALWAYS_SUCCEEDS(place->GetTime(&visitData.time));
+    MOZ_ALWAYS_SUCCEEDS(place->GetTransitionType(&visitData.transitionType));
+
+    RefPtr< AsyncGetBookmarksForURI<ItemVisitMethod, ItemVisitData> > notifier =
+      new AsyncGetBookmarksForURI<ItemVisitMethod, ItemVisitData>(this, &nsNavBookmarks::NotifyItemVisited, visitData);
+    notifier->Init();
+  }
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 nsNavBookmarks::OnDeleteURI(nsIURI* aURI,
                             const nsACString& aGUID,
                             uint16_t aReason)
--- a/toolkit/components/places/nsNavBookmarks.h
+++ b/toolkit/components/places/nsNavBookmarks.h
@@ -110,16 +110,22 @@ public:
     return gBookmarksService;
   }
 
   typedef mozilla::places::BookmarkData BookmarkData;
   typedef mozilla::places::ItemVisitData ItemVisitData;
   typedef mozilla::places::ItemChangeData ItemChangeData;
   typedef mozilla::places::BookmarkStatementId BookmarkStatementId;
 
+  nsresult OnVisit(nsIURI* aURI, int64_t aVisitId, PRTime aTime,
+                   int64_t aSessionId, int64_t aReferringId,
+                   uint32_t aTransitionType, const nsACString& aGUID,
+                   bool aHidden, uint32_t aVisitCount,
+                   uint32_t aTyped, const nsAString& aLastKnownTitle);
+
   nsresult ResultNodeForContainer(int64_t aID,
                                   nsNavHistoryQueryOptions* aOptions,
                                   nsNavHistoryResultNode** aNode);
 
   // Find all the children of a folder, using the given query and options.
   // For each child, a ResultNode is created and added to |children|.
   // The results are ordered by folder position.
   nsresult QueryFolderChildren(int64_t aFolderId,
--- a/toolkit/components/places/nsNavHistory.cpp
+++ b/toolkit/components/places/nsNavHistory.cpp
@@ -513,45 +513,37 @@ nsNavHistory::LoadPrefs()
   FRECENCY_PREF(mSecondBucketWeight,       PREF_FREC_SECOND_BUCKET_WEIGHT);
   FRECENCY_PREF(mThirdBucketWeight,        PREF_FREC_THIRD_BUCKET_WEIGHT);
   FRECENCY_PREF(mFourthBucketWeight,       PREF_FREC_FOURTH_BUCKET_WEIGHT);
   FRECENCY_PREF(mDefaultWeight,            PREF_FREC_DEFAULT_BUCKET_WEIGHT);
 
 #undef FRECENCY_PREF
 }
 
-
 void
-nsNavHistory::NotifyOnVisit(nsIURI* aURI,
-                            int64_t aVisitId,
-                            PRTime aTime,
-                            int64_t aReferrerVisitId,
-                            int32_t aTransitionType,
-                            const nsACString& aGuid,
-                            bool aHidden,
-                            uint32_t aVisitCount,
-                            uint32_t aTyped,
-                            const nsAString& aLastKnownTitle)
+nsNavHistory::NotifyOnVisits(nsIVisitData** aVisits, uint32_t aVisitsCount)
 {
-  MOZ_ASSERT(!aGuid.IsEmpty());
-  MOZ_ASSERT(aVisitCount, "Should have at least 1 visit when notifying");
-  // If there's no history, this visit will surely add a day.  If the visit is
-  // added before or after the last cached day, the day count may have changed.
-  // Otherwise adding multiple visits in the same day should not invalidate
-  // the cache.
+  MOZ_ASSERT(aVisits, "Can't call NotifyOnVisits with a NULL aVisits");
+  MOZ_ASSERT(aVisitsCount, "Should have at least 1 visit when notifying");
+
   if (mDaysOfHistory == 0) {
     mDaysOfHistory = 1;
-  } else if (aTime > mLastCachedEndOfDay || aTime < mLastCachedStartOfDay) {
-    mDaysOfHistory = -1;
+  }
+
+  for (uint32_t i = 0; i < aVisitsCount; ++i) {
+    PRTime time;
+    MOZ_ALWAYS_SUCCEEDS(aVisits[i]->GetTime(&time));
+    if (time > mLastCachedEndOfDay || time < mLastCachedStartOfDay) {
+      mDaysOfHistory = -1;
+    }
   }
 
   NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
                    nsINavHistoryObserver,
-                   OnVisit(aURI, aVisitId, aTime, 0, aReferrerVisitId,
-                           aTransitionType, aGuid, aHidden, aVisitCount, aTyped, aLastKnownTitle));
+                   OnVisits(aVisits, aVisitsCount));
 }
 
 void
 nsNavHistory::NotifyTitleChange(nsIURI* aURI,
                                 const nsString& aTitle,
                                 const nsACString& aGUID)
 {
   MOZ_ASSERT(!aGUID.IsEmpty());
--- a/toolkit/components/places/nsNavHistory.h
+++ b/toolkit/components/places/nsNavHistory.h
@@ -432,28 +432,19 @@ public:
   }
 
   int32_t GetNumVisitsForFrecency() const
   {
     return mNumVisitsForFrecency;
   }
 
   /**
-   * Fires onVisit event to nsINavHistoryService observers
+   * Fires onVisits event to nsINavHistoryService observers
    */
-  void NotifyOnVisit(nsIURI* aURI,
-                     int64_t aVisitId,
-                     PRTime aTime,
-                     int64_t aReferrerVisitId,
-                     int32_t aTransitionType,
-                     const nsACString& aGuid,
-                     bool aHidden,
-                     uint32_t aVisitCount,
-                     uint32_t aTyped,
-                     const nsAString& aLastKnownTitle);
+  void NotifyOnVisits(nsIVisitData** aVisits, uint32_t aVisitsCount);
 
   /**
    * Fires onTitleChanged event to nsINavHistoryService observers
    */
   void NotifyTitleChange(nsIURI* aURI,
                          const nsString& title,
                          const nsACString& aGUID);
 
--- a/toolkit/components/places/nsNavHistoryResult.cpp
+++ b/toolkit/components/places/nsNavHistoryResult.cpp
@@ -2367,24 +2367,20 @@ static nsresult setHistoryDetailsCallbac
 }
 
 /**
  * Here we need to update all copies of the URI we have with the new visit
  * count, and potentially add a new entry in our query.  This is the most
  * common update operation and it is important that it be as efficient as
  * possible.
  */
-NS_IMETHODIMP
+nsresult
 nsNavHistoryQueryResultNode::OnVisit(nsIURI* aURI, int64_t aVisitId,
-                                     PRTime aTime, int64_t aSessionId,
-                                     int64_t aReferringId,
-                                     uint32_t aTransitionType,
-                                     const nsACString& aGUID,
-                                     bool aHidden,
-                                     uint32_t* aAdded)
+                                     PRTime aTime, uint32_t aTransitionType,
+                                     bool aHidden, uint32_t* aAdded)
 {
   if (aHidden && !mOptions->IncludeHidden())
     return NS_OK;
 
   nsNavHistoryResult* result = GetResult();
   NS_ENSURE_STATE(result);
   if (result->mBatchInProgress &&
       ++mBatchChanges > MAX_BATCH_CHANGES_BEFORE_REFRESH) {
@@ -4617,34 +4613,31 @@ nsNavHistoryResult::OnItemMoved(int64_t 
   ENUMERATE_HISTORY_OBSERVERS(OnItemMoved(aItemId, aOldParent, aOldIndex,
                                           aNewParent, aNewIndex, aItemType,
                                           aGUID, aOldParentGUID,
                                           aNewParentGUID, aSource));
   return NS_OK;
 }
 
 
-NS_IMETHODIMP
+nsresult
 nsNavHistoryResult::OnVisit(nsIURI* aURI, int64_t aVisitId, PRTime aTime,
-                            int64_t aSessionId, int64_t aReferringId,
-                            uint32_t aTransitionType, const nsACString& aGUID,
-                            bool aHidden, uint32_t aVisitCount, uint32_t aTyped,
-                            const nsAString& aLastKnownTitle)
+                            uint32_t aTransitionType, const nsACString& aGUID, bool aHidden,
+                            uint32_t aVisitCount, const nsAString& aLastKnownTitle)
 {
   NS_ENSURE_ARG(aURI);
 
   // Embed visits are never shown in our views.
   if (aTransitionType == nsINavHistoryService::TRANSITION_EMBED) {
     return NS_OK;
   }
 
   uint32_t added = 0;
 
-  ENUMERATE_HISTORY_OBSERVERS(OnVisit(aURI, aVisitId, aTime, aSessionId,
-                                      aReferringId, aTransitionType, aGUID,
+  ENUMERATE_HISTORY_OBSERVERS(OnVisit(aURI, aVisitId, aTime, aTransitionType,
                                       aHidden, &added));
 
   // When we add visits through UpdatePlaces, we don't bother telling
   // the world that the title 'changed' from nothing to the first title
   // we ever see for a history entry. Our consumers here might still
   // care, though, so we have to tell them - but only for the first
   // visit we add. For subsequent changes, updateplaces will dispatch
   // ontitlechanged notifications as normal.
@@ -4709,16 +4702,45 @@ nsNavHistoryResult::OnVisit(nsIURI* aURI
     ENUMERATE_QUERY_OBSERVERS(Refresh(), mHistoryObservers, IsContainersQuery());
   }
 
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
+nsNavHistoryResult::OnVisits(nsIVisitData** aVisits,
+                             uint32_t aVisitsCount) {
+  for (uint32_t i = 0; i < aVisitsCount; ++i) {
+    nsIVisitData* place = aVisits[i];
+    nsCOMPtr<nsIURI> uri;
+    MOZ_ALWAYS_SUCCEEDS(place->GetUri(getter_AddRefs(uri)));
+    int64_t visitId;
+    MOZ_ALWAYS_SUCCEEDS(place->GetVisitId(&visitId));
+    PRTime time;
+    MOZ_ALWAYS_SUCCEEDS(place->GetTime(&time));
+    uint32_t transitionType;
+    MOZ_ALWAYS_SUCCEEDS(place->GetTransitionType(&transitionType));
+    nsCString guid;
+    MOZ_ALWAYS_SUCCEEDS(place->GetGuid(guid));
+    bool hidden;
+    MOZ_ALWAYS_SUCCEEDS(place->GetHidden(&hidden));
+    uint32_t visitCount;
+    MOZ_ALWAYS_SUCCEEDS(place->GetVisitCount(&visitCount));
+    nsString lastKnownTitle;
+    MOZ_ALWAYS_SUCCEEDS(place->GetLastKnownTitle(lastKnownTitle));
+    nsresult rv = OnVisit(uri, visitId, time, transitionType, guid, hidden,
+                          visitCount, lastKnownTitle);
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
+  return NS_OK;
+}
+
+
+NS_IMETHODIMP
 nsNavHistoryResult::OnTitleChanged(nsIURI* aURI,
                                    const nsAString& aPageTitle,
                                    const nsACString& aGUID)
 {
   NS_ENSURE_ARG(aURI);
 
   ENUMERATE_HISTORY_OBSERVERS(OnTitleChanged(aURI, aPageTitle, aGUID));
   return NS_OK;
--- a/toolkit/components/places/nsNavHistoryResult.h
+++ b/toolkit/components/places/nsNavHistoryResult.h
@@ -73,35 +73,23 @@ private:
   NS_IMETHOD OnClearHistory() __VA_ARGS__;                              \
   NS_IMETHOD OnPageChanged(nsIURI *aURI, uint32_t aChangedAttribute,    \
                            const nsAString &aNewValue,                  \
                            const nsACString &aGUID) __VA_ARGS__;        \
   NS_IMETHOD OnDeleteVisits(nsIURI* aURI, PRTime aVisitTime,            \
                             const nsACString& aGUID, uint16_t aReason,  \
                             uint32_t aTransitionType) __VA_ARGS__;
 
-// The internal version has an output aAdded parameter, it is incremented by
-// query nodes when the visited uri belongs to them. If no such query exists,
-// the history result creates a new query node dynamically.
+// The internal version is used by query nodes.
 #define NS_DECL_BOOKMARK_HISTORY_OBSERVER_INTERNAL                      \
-  NS_DECL_BOOKMARK_HISTORY_OBSERVER_BASE()                              \
-  NS_IMETHOD OnVisit(nsIURI* aURI, int64_t aVisitId, PRTime aTime,      \
-                     int64_t aSessionId, int64_t aReferringId,          \
-                     uint32_t aTransitionType, const nsACString& aGUID, \
-                     bool aHidden, uint32_t* aAdded);
+  NS_DECL_BOOKMARK_HISTORY_OBSERVER_BASE()
 
 // The external version is used by results.
 #define NS_DECL_BOOKMARK_HISTORY_OBSERVER_EXTERNAL(...)                 \
-  NS_DECL_BOOKMARK_HISTORY_OBSERVER_BASE(__VA_ARGS__)                   \
-  NS_IMETHOD OnVisit(nsIURI* aURI, int64_t aVisitId, PRTime aTime,      \
-                     int64_t aSessionId, int64_t aReferringId,          \
-                     uint32_t aTransitionType, const nsACString& aGUID, \
-                     bool aHidden, uint32_t aVisitCount,                \
-                     uint32_t aTyped, const nsAString& aLastKnownTitle) \
-                     __VA_ARGS__;
+  NS_DECL_BOOKMARK_HISTORY_OBSERVER_BASE(__VA_ARGS__)
 
 // nsNavHistoryResult
 //
 //    nsNavHistory creates this object and fills in mChildren (by getting
 //    it through GetTopLevel()). Then FilledAllResults() is called to finish
 //    object initialization.
 
 #define NS_NAVHISTORYRESULT_IID \
@@ -119,27 +107,34 @@ public:
                                    nsNavHistoryContainerResultNode* aRoot,
                                    bool aBatchInProgress,
                                    nsNavHistoryResult** result);
 
   NS_DECLARE_STATIC_IID_ACCESSOR(NS_NAVHISTORYRESULT_IID)
 
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_NSINAVHISTORYRESULT
+  NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(nsNavHistoryResult, nsINavHistoryResult)
   NS_DECL_BOOKMARK_HISTORY_OBSERVER_EXTERNAL(override)
-  NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(nsNavHistoryResult, nsINavHistoryResult)
+  NS_IMETHOD OnVisits(nsIVisitData** aVisits,
+                      uint32_t aVisitsCount) override;
 
   void AddHistoryObserver(nsNavHistoryQueryResultNode* aNode);
   void AddBookmarkFolderObserver(nsNavHistoryFolderResultNode* aNode, int64_t aFolder);
   void AddAllBookmarksObserver(nsNavHistoryQueryResultNode* aNode);
   void RemoveHistoryObserver(nsNavHistoryQueryResultNode* aNode);
   void RemoveBookmarkFolderObserver(nsNavHistoryFolderResultNode* aNode, int64_t aFolder);
   void RemoveAllBookmarksObserver(nsNavHistoryQueryResultNode* aNode);
   void StopObserving();
 
+  nsresult OnVisit(nsIURI* aURI, int64_t aVisitId, PRTime aTime,
+                   uint32_t aTransitionType, const nsACString& aGUID,
+                   bool aHidden, uint32_t aVisitCount,
+                   const nsAString& aLastKnownTitle);
+
 public:
   // two-stage init, use NewHistoryResult to construct
   explicit nsNavHistoryResult(nsNavHistoryContainerResultNode* mRoot);
   nsresult Init(nsINavHistoryQuery** aQueries,
                 uint32_t aQueryCount,
                 nsNavHistoryQueryOptions *aOptions);
 
   RefPtr<nsNavHistoryContainerResultNode> mRootNode;
@@ -634,16 +629,23 @@ public:
   NS_DECL_NSINAVHISTORYQUERYRESULTNODE
 
   bool CanExpand();
   bool IsContainersQuery();
 
   virtual nsresult OpenContainer() override;
 
   NS_DECL_BOOKMARK_HISTORY_OBSERVER_INTERNAL
+
+  // The internal version has an output aAdded parameter, it is incremented by
+  // query nodes when the visited uri belongs to them. If no such query exists,
+  // the history result creates a new query node dynamically.
+  nsresult OnVisit(nsIURI* aURI, int64_t aVisitId, PRTime aTime,
+                   uint32_t aTransitionType, bool aHidden,
+                   uint32_t* aAdded);
   virtual void OnRemoving() override;
 
 public:
   // this constructs lazily mURI from mQueries and mOptions, call
   // VerifyQueriesSerialized either this or mQueries/mOptions should be valid
   nsresult VerifyQueriesSerialized();
 
   // these may be constructed lazily from mURI, call VerifyQueriesParsed
--- a/toolkit/components/places/nsPlacesExpiration.js
+++ b/toolkit/components/places/nsPlacesExpiration.js
@@ -531,17 +531,17 @@ nsPlacesExpiration.prototype = {
       this._newTimer();
   },
 
   onClearHistory: function PEX_onClearHistory() {
     // History status is clean after a clear history.
     this.status = STATUS.CLEAN;
   },
 
-  onVisit() {},
+  onVisits() {},
   onTitleChanged() {},
   onDeleteURI() {},
   onPageChanged() {},
   onDeleteVisits() {},
 
   // nsITimerCallback
 
   notify: function PEX_timerCallback() {
--- a/toolkit/components/thumbnails/PageThumbs.jsm
+++ b/toolkit/components/thumbnails/PageThumbs.jsm
@@ -827,15 +827,15 @@ var PageThumbsHistoryObserver = {
 
   onClearHistory() {
     PageThumbsStorage.wipe();
   },
 
   onTitleChanged() {},
   onBeginUpdateBatch() {},
   onEndUpdateBatch() {},
-  onVisit() {},
+  onVisits() {},
   onPageChanged() {},
   onDeleteVisits() {},
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver,
                                          Ci.nsISupportsWeakReference])
 };
--- a/toolkit/modules/NewTabUtils.jsm
+++ b/toolkit/modules/NewTabUtils.jsm
@@ -742,21 +742,23 @@ var PlacesProvider = {
   onEndUpdateBatch() {
     this._batchProcessingDepth -= 1;
     if (this._batchProcessingDepth == 0 && this._batchCalledFrecencyChanged) {
       this.onManyFrecenciesChanged();
       this._batchCalledFrecencyChanged = false;
     }
   },
 
-  onVisit(aURI, aVisitId, aTime, aSessionId, aReferrerVisitId, aTransitionType,
-          aGuid, aHidden, aVisitCount, aTyped, aLastKnownTitle) {
-    // For new visits, if we're not batch processing, notify for a title // update
-    if (!this._batchProcessingDepth && aVisitCount == 1 && aLastKnownTitle) {
-      this.onTitleChanged(aURI, aLastKnownTitle, aGuid);
+  onVisits(aVisits) {
+    if (!this._batchProcessingDepth) {
+      for (let visit of aVisits) {
+        if (visit.visitCount == 1 && visit.lastKnownTitle) {
+          this.onTitleChanged(visit.uri, visit.lastKnownTitle, visit.guid);
+        }
+      }
     }
   },
 
   onDeleteURI: function PlacesProvider_onDeleteURI(aURI, aGUID, aReason) {
     // let observers remove sensetive data associated with deleted visit
     this._callObservers("onDeleteURI", {
       url: aURI.spec,
     });