Relanding Bug 834539 - Replace getPageTitle with an async API. r=mak. sr=gavin.
authorAsaf Romano <mano@mozilla.com>
Wed, 19 Jun 2013 10:18:38 +0300
changeset 135565 734147446def7c703e3d42d8740644bac4c3dec3
parent 135564 0414d4228e8525834e52aed3533db2d1fadcce27
child 135566 59679baba21b48fc662d731b094b544e2c5e8c03
push id24842
push useremorley@mozilla.com
push dateWed, 19 Jun 2013 13:23:18 +0000
treeherdermozilla-central@bef2efa89087 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmak, gavin
bugs834539
milestone24.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
Relanding Bug 834539 - Replace getPageTitle with an async API. r=mak. sr=gavin.
toolkit/components/places/Helpers.cpp
toolkit/components/places/Helpers.h
toolkit/components/places/History.cpp
toolkit/components/places/History.h
toolkit/components/places/PlaceInfo.cpp
toolkit/components/places/PlaceInfo.h
toolkit/components/places/PlacesUtils.jsm
toolkit/components/places/mozIAsyncHistory.idl
toolkit/components/places/nsINavHistoryService.idl
toolkit/components/places/nsNavHistory.cpp
toolkit/components/places/tests/unit/test_getPlacesInfo.js
toolkit/components/places/tests/unit/xpcshell.ini
--- a/toolkit/components/places/Helpers.cpp
+++ b/toolkit/components/places/Helpers.cpp
@@ -295,17 +295,17 @@ GenerateGUID(nsCString& _guid)
   rv = Base64urlEncode(buffer, kRequiredBytesLength, _guid);
   NS_ENSURE_SUCCESS(rv, rv);
 
   NS_ASSERTION(_guid.Length() == GUID_LENGTH, "GUID is not the right size!");
   return NS_OK;
 }
 
 bool
-IsValidGUID(const nsCString& aGUID)
+IsValidGUID(const nsACString& aGUID)
 {
   nsCString::size_type len = aGUID.Length();
   if (len != GUID_LENGTH) {
     return false;
   }
 
   for (nsCString::size_type i = 0; i < len; i++ ) {
     char c = aGUID[i];
--- a/toolkit/components/places/Helpers.h
+++ b/toolkit/components/places/Helpers.h
@@ -132,17 +132,17 @@ nsresult GenerateGUID(nsCString& _guid);
 
 /**
  * Determines if the string is a valid guid or not.
  *
  * @param aGUID
  *        The guid to test.
  * @return true if it is a valid guid, false otherwise.
  */
-bool IsValidGUID(const nsCString& aGUID);
+bool IsValidGUID(const nsACString& aGUID);
 
 /**
  * Truncates the title if it's longer than TITLE_LENGTH_MAX.
  *
  * @param aTitle
  *        The title to truncate (if necessary)
  * @param aTrimmed
  *        Output parameter to return the trimmed string
--- a/toolkit/components/places/History.cpp
+++ b/toolkit/components/places/History.cpp
@@ -208,16 +208,78 @@ class PlaceHashKey : public nsCStringHas
 };
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Anonymous Helpers
 
 namespace {
 
 /**
+ * Convert the given js value to a js array.
+ *
+ * @param [in] aValue
+ *        the JS value to convert.
+ * @param [in] aCtx
+ *        The JSContext for aValue.
+ * @param [out] _array
+ *        the JS array.
+ * @param [out] _arrayLength
+ *        _array's length.
+ */
+nsresult
+GetJSArrayFromJSValue(const JS::Value& aValue,
+                      JSContext* aCtx,
+                      JSObject** _array,
+                      uint32_t* _arrayLength) {
+  if (aValue.isObjectOrNull()) {
+    JS::Rooted<JSObject*> val(aCtx, aValue.toObjectOrNull());
+    if (JS_IsArrayObject(aCtx, val)) {
+      *_array = val;
+      (void)JS_GetArrayLength(aCtx, *_array, _arrayLength);
+      NS_ENSURE_ARG(*_arrayLength > 0);
+      return NS_OK;
+    }
+  }
+  
+  // Build a temporary array to store this one item so the code below can
+  // just loop.
+  *_arrayLength = 1;
+  *_array = JS_NewArrayObject(aCtx, 0, nullptr);
+  NS_ENSURE_TRUE(*_array, NS_ERROR_OUT_OF_MEMORY);
+
+  JSBool rc = JS_DefineElement(aCtx, *_array, 0, aValue, nullptr, nullptr, 0);
+  NS_ENSURE_TRUE(rc, NS_ERROR_UNEXPECTED);
+  return NS_OK;
+}
+
+/**
+ * Attemps to convert a given js value to a nsIURI object.
+ * @param aCtx
+ *        The JSContext for aValue.
+ * @param aValue
+ *        The JS value to convert.
+ * @return the nsIURI object, or null if aValue is not a nsIURI object.
+ */
+already_AddRefed<nsIURI>
+GetJSValueAsURI(JSContext* aCtx,
+                const JS::Value& aValue) {
+  if (!JSVAL_IS_PRIMITIVE(aValue)) {
+    nsCOMPtr<nsIXPConnect> xpc = mozilla::services::GetXPConnect();
+
+    nsCOMPtr<nsIXPConnectWrappedNative> wrappedObj;
+    nsresult rv = xpc->GetWrappedNativeOfJSObject(aCtx, JSVAL_TO_OBJECT(aValue),
+                                                  getter_AddRefs(wrappedObj));
+    NS_ENSURE_SUCCESS(rv, nullptr);
+    nsCOMPtr<nsIURI> uri = do_QueryWrappedNative(wrappedObj);
+    return uri.forget();
+  }
+  return nullptr;
+}
+
+/**
  * Obtains an nsIURI from the "uri" property of a JSObject.
  *
  * @param aCtx
  *        The JSContext for aObject.
  * @param aObject
  *        The JSObject to get the URI from.
  * @param aProperty
  *        The name of the property to get the URI from.
@@ -226,28 +288,51 @@ namespace {
 already_AddRefed<nsIURI>
 GetURIFromJSObject(JSContext* aCtx,
                    JSObject* aObject,
                    const char* aProperty)
 {
   JS::Rooted<JS::Value> uriVal(aCtx);
   JSBool rc = JS_GetProperty(aCtx, aObject, aProperty, uriVal.address());
   NS_ENSURE_TRUE(rc, nullptr);
-
-  if (!JSVAL_IS_PRIMITIVE(uriVal)) {
-    nsCOMPtr<nsIXPConnect> xpc = mozilla::services::GetXPConnect();
-
-    nsCOMPtr<nsIXPConnectWrappedNative> wrappedObj;
-    nsresult rv = xpc->GetWrappedNativeOfJSObject(aCtx, JSVAL_TO_OBJECT(uriVal),
-                                                  getter_AddRefs(wrappedObj));
-    NS_ENSURE_SUCCESS(rv, nullptr);
-    nsCOMPtr<nsIURI> uri = do_QueryWrappedNative(wrappedObj);
-    return uri.forget();
+  return GetJSValueAsURI(aCtx, uriVal);
+}
+
+/**
+ * Attemps to convert a JS value to a string.
+ * @param aCtx
+ *        The JSContext for aObject.
+ * @param aValue
+ *        The JS value to convert.
+ * @param _string
+ *        The string to populate with the value, or set it to void.
+ */
+void
+GetJSValueAsString(JSContext* aCtx,
+                   const JS::Value& aValue,
+                   nsString& _string) {
+  if (JSVAL_IS_VOID(aValue) ||
+      !(JSVAL_IS_NULL(aValue) || JSVAL_IS_STRING(aValue))) {
+    _string.SetIsVoid(true);
+    return;
   }
-  return nullptr;
+
+  // |null| in JS maps to the empty string.
+  if (JSVAL_IS_NULL(aValue)) {
+    _string.Truncate();
+    return;
+  }
+  size_t length;
+  const jschar* chars =
+    JS_GetStringCharsZAndLength(aCtx, JSVAL_TO_STRING(aValue), &length);
+  if (!chars) {
+    _string.SetIsVoid(true);
+    return;
+  }
+  _string.Assign(static_cast<const PRUnichar*>(chars), length);
 }
 
 /**
  * Obtains the specified property of a JSObject.
  *
  * @param aCtx
  *        The JSContext for aObject.
  * @param aObject
@@ -260,34 +345,23 @@ GetURIFromJSObject(JSContext* aCtx,
 void
 GetStringFromJSObject(JSContext* aCtx,
                       JSObject* aObject,
                       const char* aProperty,
                       nsString& _string)
 {
   JS::Rooted<JS::Value> val(aCtx);
   JSBool rc = JS_GetProperty(aCtx, aObject, aProperty, val.address());
-  if (!rc || JSVAL_IS_VOID(val) ||
-      !(JSVAL_IS_NULL(val) || JSVAL_IS_STRING(val))) {
+  if (!rc) {
     _string.SetIsVoid(true);
     return;
   }
-  // |null| in JS maps to the empty string.
-  if (JSVAL_IS_NULL(val)) {
-    _string.Truncate();
-    return;
+  else {
+    GetJSValueAsString(aCtx, val, _string);
   }
-  size_t length;
-  const jschar* chars =
-    JS_GetStringCharsZAndLength(aCtx, JSVAL_TO_STRING(val), &length);
-  if (!chars) {
-    _string.SetIsVoid(true);
-    return;
-  }
-  _string.Assign(static_cast<const PRUnichar*>(chars), length);
 }
 
 /**
  * Obtains the specified property of a JSObject.
  *
  * @param aCtx
  *        The JSContext for aObject.
  * @param aObject
@@ -487,18 +561,18 @@ public:
   : mPlace(aPlace)
   , mReferrer(aReferrer)
   , mHistory(History::GetService())
   {
   }
 
   NS_IMETHOD Run()
   {
-    NS_PRECONDITION(NS_IsMainThread(),
-                    "This should be called on the main thread");
+    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;
     }
 
     nsNavHistory* navHistory = nsNavHistory::GetHistoryService();
     if (!navHistory) {
@@ -558,72 +632,84 @@ public:
   : mSpec(aSpec)
   , mTitle(aTitle)
   , mGUID(aGUID)
   {
   }
 
   NS_IMETHOD Run()
   {
-    NS_PRECONDITION(NS_IsMainThread(),
-                    "This should be called on the main thread");
+    MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread");
 
     nsNavHistory* navHistory = nsNavHistory::GetHistoryService();
     NS_ENSURE_TRUE(navHistory, NS_ERROR_OUT_OF_MEMORY);
     nsCOMPtr<nsIURI> uri;
     (void)NS_NewURI(getter_AddRefs(uri), mSpec);
     navHistory->NotifyTitleChange(uri, mTitle, mGUID);
 
     return NS_OK;
   }
 private:
   const nsCString mSpec;
   const nsString mTitle;
   const nsCString mGUID;
 };
 
 /**
- * Notifies a callback object when a visit has been handled.
+ * Helper class for methods which notify their callers through the
+ * mozIVisitInfoCallback interface.
  */
-class NotifyVisitInfoCallback : public nsRunnable
+class NotifyPlaceInfoCallback : public nsRunnable
 {
 public:
-  NotifyVisitInfoCallback(mozIVisitInfoCallback* aCallback,
+  NotifyPlaceInfoCallback(mozIVisitInfoCallback* aCallback,
                           const VisitData& aPlace,
+                          bool aIsSingleVisit,
                           nsresult aResult)
   : mCallback(aCallback)
   , mPlace(aPlace)
   , mResult(aResult)
+  , mIsSingleVisit(aIsSingleVisit)
   {
-    NS_PRECONDITION(aCallback, "Must pass a non-null callback!");
+    MOZ_ASSERT(aCallback, "Must pass a non-null callback!");
   }
 
   NS_IMETHOD Run()
   {
-    NS_PRECONDITION(NS_IsMainThread(),
-                    "This should be called on the main thread");
+    MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread");
 
     nsCOMPtr<nsIURI> referrerURI;
     if (!mPlace.referrerSpec.IsEmpty()) {
       (void)NS_NewURI(getter_AddRefs(referrerURI), mPlace.referrerSpec);
     }
 
-    nsCOMPtr<mozIVisitInfo> visit =
-      new VisitInfo(mPlace.visitId, mPlace.visitTime, mPlace.transitionType,
-                    referrerURI.forget());
-    PlaceInfo::VisitsArray visits;
-    (void)visits.AppendElement(visit);
-
     nsCOMPtr<nsIURI> uri;
     (void)NS_NewURI(getter_AddRefs(uri), mPlace.spec);
 
-    // We do not notify about the frecency of the place.
-    nsCOMPtr<mozIPlaceInfo> place =
-      new PlaceInfo(mPlace.placeId, mPlace.guid, uri.forget(), mPlace.title,
-                    -1, visits);
+    nsCOMPtr<mozIPlaceInfo> place;
+    if (mIsSingleVisit) {
+      nsCOMPtr<mozIVisitInfo> visit =
+        new VisitInfo(mPlace.visitId, mPlace.visitTime, mPlace.transitionType,
+                      referrerURI.forget());
+      PlaceInfo::VisitsArray visits;
+      (void)visits.AppendElement(visit);
+
+      // The frecency isn't exposed because it may not reflect the updated value
+      // in the case of InsertVisitedURIs.
+      place =
+        new PlaceInfo(mPlace.placeId, mPlace.guid, uri.forget(), mPlace.title,
+                      -1, visits);
+    }
+    else {
+      // Same as above.
+      place =
+        new PlaceInfo(mPlace.placeId, mPlace.guid, uri.forget(), mPlace.title,
+                      -1);
+    }
+
     if (NS_SUCCEEDED(mResult)) {
       (void)mCallback->HandleResult(place);
     }
     else {
       (void)mCallback->HandleError(mResult, place);
     }
 
     return NS_OK;
@@ -633,28 +719,29 @@ private:
   /**
    * Callers MUST hold a strong reference to this that outlives us because we
    * may be created off of the main thread, and therefore cannot call AddRef on
    * this object (and therefore cannot hold a strong reference to it).
    */
   mozIVisitInfoCallback* mCallback;
   VisitData mPlace;
   const nsresult mResult;
+  bool mIsSingleVisit;
 };
 
 /**
  * Notifies a callback object when the operation is complete.
  */
 class NotifyCompletion : public nsRunnable
 {
 public:
   NotifyCompletion(mozIVisitInfoCallback* aCallback)
   : mCallback(aCallback)
   {
-    NS_PRECONDITION(aCallback, "Must pass a non-null callback!");
+    MOZ_ASSERT(aCallback, "Must pass a non-null callback!");
   }
 
   NS_IMETHOD Run()
   {
     if (NS_IsMainThread()) {
       (void)mCallback->HandleCompletion();
     }
     else {
@@ -701,29 +788,29 @@ CanAddURI(nsIURI* aURI,
   bool canAdd;
   nsresult rv = navHistory->CanAddURI(aURI, &canAdd);
   if (NS_SUCCEEDED(rv) && canAdd) {
     return true;
   };
 
   // We cannot add the URI.  Notify the callback, if we were given one.
   if (aCallback) {
-    // NotifyVisitInfoCallback does not hold a strong reference to the callback, so we
+    // NotifyPlaceInfoCallback does not hold a strong reference to the callback, so we
     // have to manage it by AddRefing now and then releasing it after the event
     // has run.
     NS_ADDREF(aCallback);
 
     VisitData place(aURI);
     place.guid = aGUID;
     nsCOMPtr<nsIRunnable> event =
-      new NotifyVisitInfoCallback(aCallback, place, NS_ERROR_INVALID_ARG);
+      new NotifyPlaceInfoCallback(aCallback, place, true, NS_ERROR_INVALID_ARG);
     (void)NS_DispatchToMainThread(event);
 
     // Also dispatch an event to release our reference to the callback after
-    // NotifyVisitInfoCallback has run.
+    // NotifyPlaceInfoCallback has run.
     nsCOMPtr<nsIThread> mainThread = do_GetMainThread();
     (void)NS_ProxyRelease(mainThread, aCallback, true);
   }
 
   return false;
 }
 
 /**
@@ -741,36 +828,34 @@ public:
    *        The locations to record visits.
    * @param [optional] aCallback
    *        The callback to notify about the visit.
    */
   static nsresult Start(mozIStorageConnection* aConnection,
                         nsTArray<VisitData>& aPlaces,
                         mozIVisitInfoCallback* aCallback = NULL)
   {
-    NS_PRECONDITION(NS_IsMainThread(),
-                    "This should be called on the main thread");
-    NS_PRECONDITION(aPlaces.Length() > 0, "Must pass a non-empty array!");
+    MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread");
+    MOZ_ASSERT(aPlaces.Length() > 0, "Must pass a non-empty array!");
 
     nsRefPtr<InsertVisitedURIs> event =
       new InsertVisitedURIs(aConnection, aPlaces, aCallback);
 
     // Get the target thread, and then start the work!
     nsCOMPtr<nsIEventTarget> target = do_GetInterface(aConnection);
     NS_ENSURE_TRUE(target, NS_ERROR_UNEXPECTED);
     nsresult rv = target->Dispatch(event, NS_DISPATCH_NORMAL);
     NS_ENSURE_SUCCESS(rv, rv);
 
     return NS_OK;
   }
 
   NS_IMETHOD Run()
   {
-    NS_PRECONDITION(!NS_IsMainThread(),
-                    "This should not be called on the main thread");
+    MOZ_ASSERT(!NS_IsMainThread(), "This should not be called on the main thread");
 
     // Prevent the main thread from shutting down while this is running.
     MutexAutoLock lockedScope(mHistory->GetShutdownMutex());
     if (mHistory->IsShuttingDown()) {
       // If we were already shutting down, we cannot insert the URIs.
       return NS_OK;
     }
 
@@ -779,25 +864,32 @@ public:
 
     VisitData* lastPlace = NULL;
     for (nsTArray<VisitData>::size_type i = 0; i < mPlaces.Length(); i++) {
       VisitData& place = mPlaces.ElementAt(i);
       VisitData& referrer = mReferrers.ElementAt(i);
 
       // We can avoid a database lookup if it's the same place as the last
       // visit we added.
-      bool known = (lastPlace && lastPlace->IsSamePlaceAs(place)) ||
-                   mHistory->FetchPageInfo(place);
+      bool known = lastPlace && lastPlace->IsSamePlaceAs(place);
+      if (!known) {
+        nsresult rv = mHistory->FetchPageInfo(place, &known);
+        if (NS_FAILED(rv)) {
+          nsCOMPtr<nsIRunnable> event =
+            new NotifyPlaceInfoCallback(mCallback, place, true, rv);
+          return NS_DispatchToMainThread(event);
+        }
+      }
 
       FetchReferrerInfo(referrer, place);
 
       nsresult rv = DoDatabaseInserts(known, place, referrer);
       if (mCallback) {
         nsCOMPtr<nsIRunnable> event =
-          new NotifyVisitInfoCallback(mCallback, place, rv);
+          new NotifyPlaceInfoCallback(mCallback, place, true, rv);
         nsresult rv2 = NS_DispatchToMainThread(event);
         NS_ENSURE_SUCCESS(rv2, rv2);
       }
       NS_ENSURE_SUCCESS(rv, rv);
 
       nsCOMPtr<nsIRunnable> event = new NotifyVisitObservers(place, referrer);
       rv = NS_DispatchToMainThread(event);
       NS_ENSURE_SUCCESS(rv, rv);
@@ -820,18 +912,17 @@ public:
 private:
   InsertVisitedURIs(mozIStorageConnection* aConnection,
                     nsTArray<VisitData>& aPlaces,
                     mozIVisitInfoCallback* aCallback)
   : mDBConn(aConnection)
   , mCallback(aCallback)
   , mHistory(History::GetService())
   {
-    NS_PRECONDITION(NS_IsMainThread(),
-                    "This should be called on the main thread");
+    MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread");
 
     (void)mPlaces.SwapElements(aPlaces);
     (void)mReferrers.SetLength(mPlaces.Length());
 
     nsNavHistory* navHistory = nsNavHistory::GetHistoryService();
     NS_ABORT_IF_FALSE(navHistory, "Could not get nsNavHistory?!");
 
     for (nsTArray<VisitData>::size_type i = 0; i < mPlaces.Length(); i++) {
@@ -839,19 +930,16 @@ private:
 
 #ifdef DEBUG
       nsCOMPtr<nsIURI> uri;
       (void)NS_NewURI(getter_AddRefs(uri), mPlaces[i].spec);
       NS_ASSERTION(CanAddURI(uri),
                    "Passed a VisitData with a URI we cannot add to history!");
 #endif
     }
-
-    // We AddRef on the main thread, and release it when we are destroyed.
-    NS_IF_ADDREF(mCallback);
   }
 
   virtual ~InsertVisitedURIs()
   {
     if (mCallback) {
       nsCOMPtr<nsIThread> mainThread = do_GetMainThread();
       (void)NS_ProxyRelease(mainThread, mCallback, true);
     }
@@ -868,35 +956,37 @@ private:
    *        The place we are adding a visit for.
    * @param aReferrer
    *        The referrer for aPlace.
    */
   nsresult DoDatabaseInserts(bool aKnown,
                              VisitData& aPlace,
                              VisitData& aReferrer)
   {
-    NS_PRECONDITION(!NS_IsMainThread(),
-                    "This should not be called on the main thread");
+    MOZ_ASSERT(!NS_IsMainThread(), "This should not be called on the main thread");
 
     // If the page was in moz_places, we need to update the entry.
     nsresult rv;
     if (aKnown) {
       rv = mHistory->UpdatePlace(aPlace);
       NS_ENSURE_SUCCESS(rv, rv);
     }
     // Otherwise, the page was not in moz_places, so now we have to add it.
     else {
       rv = mHistory->InsertPlace(aPlace);
       NS_ENSURE_SUCCESS(rv, rv);
 
       // We need the place id and guid of the page we just inserted when we
       // have a callback or when the GUID isn't known.  No point in doing the
       // disk I/O if we do not need it.
       if (mCallback || aPlace.guid.IsEmpty()) {
-        bool exists = mHistory->FetchPageInfo(aPlace);
+        bool exists;
+        rv = mHistory->FetchPageInfo(aPlace, &exists);
+        NS_ENSURE_SUCCESS(rv, rv);
+
         if (!exists) {
           NS_NOTREACHED("should have an entry in moz_places");
         }
       }
     }
 
     rv = AddVisit(aPlace, aReferrer);
     NS_ENSURE_SUCCESS(rv, rv);
@@ -1140,30 +1230,88 @@ private:
     return NS_OK;
   }
 
   mozIStorageConnection* mDBConn;
 
   nsTArray<VisitData> mPlaces;
   nsTArray<VisitData> mReferrers;
 
-  /**
-   * We own a strong reference to this, but in an indirect way.  We call AddRef
-   * in our constructor, which happens on the main thread, and proxy the relase
-   * of the object to the main thread in our destructor.
-   */
-  mozIVisitInfoCallback* mCallback;
+  nsCOMPtr<mozIVisitInfoCallback> mCallback;
 
   /**
    * Strong reference to the History object because we do not want it to
    * disappear out from under us.
    */
   nsRefPtr<History> mHistory;
 };
 
+class GetPlaceInfo MOZ_FINAL : public nsRunnable {
+public:
+  /**
+   * Get the place info for a given place (by GUID or URI)  asynchronously.
+   */
+  static nsresult Start(mozIStorageConnection* aConnection,
+                        VisitData& aPlace,
+                        mozIVisitInfoCallback* aCallback) {
+    MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread");
+
+    nsRefPtr<GetPlaceInfo> event = new GetPlaceInfo(aPlace, aCallback);
+
+    // Get the target thread, and then start the work!
+    nsCOMPtr<nsIEventTarget> target = do_GetInterface(aConnection);
+    NS_ENSURE_TRUE(target, NS_ERROR_UNEXPECTED);
+    nsresult rv = target->Dispatch(event, NS_DISPATCH_NORMAL);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    return NS_OK;
+  }
+
+  NS_IMETHOD Run()
+  {
+    MOZ_ASSERT(!NS_IsMainThread(), "This should not be called on the main thread");
+
+    bool exists;
+    nsresult rv = mHistory->FetchPageInfo(mPlace, &exists);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    if (!exists)
+      rv = NS_ERROR_NOT_AVAILABLE;
+
+    nsCOMPtr<nsIRunnable> event =
+      new NotifyPlaceInfoCallback(mCallback, mPlace, false, rv);
+
+    rv = NS_DispatchToMainThread(event);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    return NS_OK;
+  }
+private:
+  GetPlaceInfo(VisitData& aPlace,
+               mozIVisitInfoCallback* aCallback)
+  : mPlace(aPlace)
+  , mCallback(aCallback)
+  , mHistory(History::GetService())
+  {
+    MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread");
+  }
+
+  virtual ~GetPlaceInfo()
+  {
+    if (mCallback) {
+      nsCOMPtr<nsIThread> mainThread = do_GetMainThread();
+      (void)NS_ProxyRelease(mainThread, mCallback, true);
+    }
+  }
+
+  VisitData mPlace;
+  nsCOMPtr<mozIVisitInfoCallback> mCallback;
+  nsRefPtr<History> mHistory;
+};
+
 /**
  * Sets the page title for a page in moz_places (if necessary).
  */
 class SetPageTitle : public nsRunnable
 {
 public:
   /**
    * Sets a pages title in the database asynchronously.
@@ -1174,19 +1322,18 @@ public:
    *        The URI to set the page title on.
    * @param aTitle
    *        The title to set for the page, if the page exists.
    */
   static nsresult Start(mozIStorageConnection* aConnection,
                         nsIURI* aURI,
                         const nsAString& aTitle)
   {
-    NS_PRECONDITION(NS_IsMainThread(),
-                    "This should be called on the main thread");
-    NS_PRECONDITION(aURI, "Must pass a non-null URI object!");
+    MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread");
+    MOZ_ASSERT(aURI, "Must pass a non-null URI object!");
 
     nsCString spec;
     nsresult rv = aURI->GetSpec(spec);
     NS_ENSURE_SUCCESS(rv, rv);
 
     nsRefPtr<SetPageTitle> event = new SetPageTitle(spec, aTitle);
 
     // Get the target thread, and then start the work!
@@ -1195,21 +1342,23 @@ public:
     rv = target->Dispatch(event, NS_DISPATCH_NORMAL);
     NS_ENSURE_SUCCESS(rv, rv);
 
     return NS_OK;
   }
 
   NS_IMETHOD Run()
   {
-    NS_PRECONDITION(!NS_IsMainThread(),
-                    "This should not be called on the main thread");
+    MOZ_ASSERT(!NS_IsMainThread(), "This should not be called on the main thread");
 
     // First, see if the page exists in the database (we'll need its id later).
-    bool exists = mHistory->FetchPageInfo(mPlace);
+    bool exists;
+    nsresult rv = mHistory->FetchPageInfo(mPlace, &exists);
+    NS_ENSURE_SUCCESS(rv, rv);
+
     if (!exists || !mPlace.titleChanged) {
       // We have no record of this page, or we have no title change, so there
       // is no need to do any further work.
       return NS_OK;
     }
 
     NS_ASSERTION(mPlace.placeId > 0,
                  "We somehow have an invalid place id here!");
@@ -1220,18 +1369,17 @@ public:
         "UPDATE moz_places "
         "SET title = :page_title "
         "WHERE id = :page_id "
       );
     NS_ENSURE_STATE(stmt);
 
     {
       mozStorageStatementScoper scoper(stmt);
-      nsresult rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"),
-                                          mPlace.placeId);
+      rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), mPlace.placeId);
       NS_ENSURE_SUCCESS(rv, rv);
       // Empty strings should clear the title, just like
       // nsNavHistory::SetPageTitle.
       if (mPlace.title.IsEmpty()) {
         rv = stmt->BindNullByName(NS_LITERAL_CSTRING("page_title"));
       }
       else {
         rv = stmt->BindStringByName(NS_LITERAL_CSTRING("page_title"),
@@ -1239,17 +1387,17 @@ public:
       }
       NS_ENSURE_SUCCESS(rv, rv);
       rv = stmt->Execute();
       NS_ENSURE_SUCCESS(rv, rv);
     }
 
     nsCOMPtr<nsIRunnable> event =
       new NotifyTitleObservers(mPlace.spec, mPlace.title, mPlace.guid);
-    nsresult rv = NS_DispatchToMainThread(event);
+    rv = NS_DispatchToMainThread(event);
     NS_ENSURE_SUCCESS(rv, rv);
 
     return NS_OK;
   }
 
 private:
   SetPageTitle(const nsCString& aSpec,
                const nsAString& aTitle)
@@ -1711,41 +1859,41 @@ private:
  *        The VisitData of the visit to store as an embed visit.
  * @param [optional] aCallback
  *        The mozIVisitInfoCallback to notify, if provided.
  */
 void
 StoreAndNotifyEmbedVisit(VisitData& aPlace,
                          mozIVisitInfoCallback* aCallback = NULL)
 {
-  NS_PRECONDITION(aPlace.transitionType == nsINavHistoryService::TRANSITION_EMBED,
-                  "Must only pass TRANSITION_EMBED visits to this!");
-  NS_PRECONDITION(NS_IsMainThread(), "Must be called on the main thread!");
+  MOZ_ASSERT(aPlace.transitionType == nsINavHistoryService::TRANSITION_EMBED,
+             "Must only pass TRANSITION_EMBED visits to this!");
+  MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread!");
 
   nsCOMPtr<nsIURI> uri;
   (void)NS_NewURI(getter_AddRefs(uri), aPlace.spec);
 
   nsNavHistory* navHistory = nsNavHistory::GetHistoryService();
   if (!navHistory || !uri) {
     return;
   }
 
   navHistory->registerEmbedVisit(uri, aPlace.visitTime);
 
   if (aCallback) {
-    // NotifyVisitInfoCallback does not hold a strong reference to the callback,
+    // NotifyPlaceInfoCallback does not hold a strong reference to the callback,
     // so we have to manage it by AddRefing now and then releasing it after the
     // event has run.
     NS_ADDREF(aCallback);
     nsCOMPtr<nsIRunnable> event =
-      new NotifyVisitInfoCallback(aCallback, aPlace, NS_OK);
+      new NotifyPlaceInfoCallback(aCallback, aPlace, true, NS_OK);
     (void)NS_DispatchToMainThread(event);
 
     // Also dispatch an event to release our reference to the callback after
-    // NotifyVisitInfoCallback has run.
+    // NotifyPlaceInfoCallback has run.
     nsCOMPtr<nsIThread> mainThread = do_GetMainThread();
     (void)NS_ProxyRelease(mainThread, aCallback, true);
   }
 
   VisitData noReferrer;
   nsCOMPtr<nsIRunnable> event = new NotifyVisitObservers(aPlace, noReferrer);
   (void)NS_DispatchToMainThread(event);
 }
@@ -1967,47 +2115,75 @@ History::UpdatePlace(const VisitData& aP
                              aPlace.placeId);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = stmt->Execute();
   NS_ENSURE_SUCCESS(rv, rv);
 
   return NS_OK;
 }
 
-bool
-History::FetchPageInfo(VisitData& _place)
+nsresult
+History::FetchPageInfo(VisitData& _place, bool* _exists)
 {
-  NS_PRECONDITION(!_place.spec.IsEmpty(), "must have a non-empty spec!");
+  NS_PRECONDITION(!_place.spec.IsEmpty() || !_place.guid.IsEmpty(), "must have either a non-empty spec or guid!");
   NS_PRECONDITION(!NS_IsMainThread(), "must be called off of the main thread!");
 
-  nsCOMPtr<mozIStorageStatement> stmt = GetStatement(
-      "SELECT id, title, hidden, typed, guid "
+  nsresult rv;
+
+  // URI takes precedence.
+  nsCOMPtr<mozIStorageStatement> stmt;
+  bool selectByURI = !_place.spec.IsEmpty();
+  if (selectByURI) {
+    stmt = GetStatement(
+      "SELECT guid, id, title, hidden, typed, frecency "
       "FROM moz_places "
       "WHERE url = :page_url "
     );
-  NS_ENSURE_TRUE(stmt, false);
+    rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), _place.spec);
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
+  else {
+    stmt = GetStatement(
+      "SELECT url, id, title, hidden, typed, frecency "
+      "FROM moz_places "
+      "WHERE guid = :guid "
+    );
+    rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), _place.guid);
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
+
+  NS_ENSURE_TRUE(stmt, rv);
   mozStorageStatementScoper scoper(stmt);
 
-  nsresult rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"),
-                                _place.spec);
-  NS_ENSURE_SUCCESS(rv, false);
-
-  bool hasResult;
-  rv = stmt->ExecuteStep(&hasResult);
-  NS_ENSURE_SUCCESS(rv, false);
-  if (!hasResult) {
-    return false;
+  rv = stmt->ExecuteStep(_exists);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  if (!*_exists) {
+    return NS_OK;
   }
 
-  rv = stmt->GetInt64(0, &_place.placeId);
-  NS_ENSURE_SUCCESS(rv, false);
+  if (selectByURI) {
+    if (_place.guid.IsEmpty()) {
+      rv = stmt->GetUTF8String(0, _place.guid);
+      NS_ENSURE_SUCCESS(rv, rv);
+    }
+  }
+  else {
+    nsAutoCString spec;
+    rv = stmt->GetUTF8String(0, spec);
+    NS_ENSURE_SUCCESS(rv, rv);
+    _place.spec = spec;
+  }
+
+  rv = stmt->GetInt64(1, &_place.placeId);
+  NS_ENSURE_SUCCESS(rv, rv);
 
   nsAutoString title;
-  rv = stmt->GetString(1, title);
-  NS_ENSURE_SUCCESS(rv, true);
+  rv = stmt->GetString(2, title);
+  NS_ENSURE_SUCCESS(rv, rv);
 
   // If the title we were given was void, that means we did not bother to set
   // it to anything.  As a result, ignore the fact that we may have changed the
   // title (because we don't want to, that would be empty), and set the title
   // to what is currently stored in the datbase.
   if (_place.title.IsVoid()) {
     _place.title = title;
   }
@@ -2017,36 +2193,33 @@ History::FetchPageInfo(VisitData& _place
                             (_place.title.IsEmpty() && title.IsVoid()));
   }
 
   if (_place.hidden) {
     // If this transition was hidden, it is possible that others were not.
     // Any one visible transition makes this location visible. If database
     // has location as visible, reflect that in our data structure.
     int32_t hidden;
-    rv = stmt->GetInt32(2, &hidden);
+    rv = stmt->GetInt32(3, &hidden);
+    NS_ENSURE_SUCCESS(rv, rv);
     _place.hidden = !!hidden;
-    NS_ENSURE_SUCCESS(rv, true);
   }
 
   if (!_place.typed) {
     // If this transition wasn't typed, others might have been. If database
     // has location as typed, reflect that in our data structure.
     int32_t typed;
-    rv = stmt->GetInt32(3, &typed);
+    rv = stmt->GetInt32(4, &typed);
+    NS_ENSURE_SUCCESS(rv, rv);
     _place.typed = !!typed;
-    NS_ENSURE_SUCCESS(rv, true);
   }
 
-  if (_place.guid.IsVoid()) {
-    rv = stmt->GetUTF8String(4, _place.guid);
-    NS_ENSURE_SUCCESS(rv, true);
-  }
-
-  return true;
+  rv = stmt->GetInt32(5, &_place.frecency);
+  NS_ENSURE_SUCCESS(rv, rv);
+  return NS_OK;
 }
 
 /* static */ size_t
 History::SizeOfEntryExcludingThis(KeyClass* aEntry, nsMallocSizeOfFun aMallocSizeOf, void *)
 {
   return aEntry->array.SizeOfExcludingThis(aMallocSizeOf);
 }
 
@@ -2496,39 +2669,95 @@ History::RemoveAllDownloads()
 
   return NS_OK;
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 //// mozIAsyncHistory
 
 NS_IMETHODIMP
+History::GetPlacesInfo(const JS::Value& aPlaceIdentifiers,
+                       mozIVisitInfoCallback* aCallback,
+                       JSContext* aCtx) {
+  nsNavHistory* navHistory = nsNavHistory::GetHistoryService();
+  NS_ABORT_IF_FALSE(navHistory, "Could not get nsNavHistory?!");
+
+  uint32_t placesIndentifiersLength;
+  JS::Rooted<JSObject*> placesIndentifiers(aCtx);
+  nsresult rv = GetJSArrayFromJSValue(aPlaceIdentifiers, aCtx,
+                                      placesIndentifiers.address(),
+                                      &placesIndentifiersLength);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  nsTArray<VisitData> placesInfo;
+  placesInfo.SetCapacity(placesIndentifiersLength);
+  for (uint32_t i = 0; i < placesIndentifiersLength; i++) {
+    JS::Value placeIdentifier;
+    JSBool rc = JS_GetElement(aCtx, placesIndentifiers, i, &placeIdentifier);
+    NS_ENSURE_TRUE(rc, NS_ERROR_UNEXPECTED);
+
+    // GUID
+    nsAutoString fatGUID;
+    GetJSValueAsString(aCtx, placeIdentifier, fatGUID);
+    if (!fatGUID.IsVoid()) {
+      NS_ConvertUTF16toUTF8 guid(fatGUID);
+      if (!IsValidGUID(guid))
+        return NS_ERROR_INVALID_ARG;
+
+      VisitData& placeInfo = *placesInfo.AppendElement(VisitData());
+      placeInfo.guid = guid;
+    }
+    else {
+      nsCOMPtr<nsIURI> uri = GetJSValueAsURI(aCtx, placeIdentifier);
+      if (!uri)
+        return NS_ERROR_INVALID_ARG; // neither a guid, nor a uri.
+      *placesInfo.AppendElement(VisitData(uri));
+    }
+  }
+
+  mozIStorageConnection* dbConn = GetDBConn();
+  NS_ENSURE_STATE(dbConn);
+
+  for (nsTArray<VisitData>::size_type i = 0; i < placesInfo.Length(); i++) {
+    nsresult rv = GetPlaceInfo::Start(dbConn, placesInfo.ElementAt(i), aCallback);
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
+
+  // Be sure to notify that all of our operations are complete.  This
+  // is dispatched to the background thread first and redirected to the
+  // main thread from there to make sure that all database notifications
+  // and all embed or canAddURI notifications have finished.
+  if (aCallback) {
+    // NotifyCompletion does not hold a strong reference to the callback,
+    // so we have to manage it by AddRefing now. NotifyCompletion will
+    // release it for us once it has dispatched the callback to the main
+    // thread.
+    NS_ADDREF(aCallback);
+
+    nsCOMPtr<nsIEventTarget> backgroundThread = do_GetInterface(dbConn);
+    NS_ENSURE_TRUE(backgroundThread, NS_ERROR_UNEXPECTED);
+    nsCOMPtr<nsIRunnable> event = new NotifyCompletion(aCallback);
+    return backgroundThread->Dispatch(event, NS_DISPATCH_NORMAL);
+  }
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 History::UpdatePlaces(const JS::Value& aPlaceInfos,
                       mozIVisitInfoCallback* aCallback,
                       JSContext* aCtx)
 {
   NS_ENSURE_TRUE(NS_IsMainThread(), NS_ERROR_UNEXPECTED);
   NS_ENSURE_TRUE(!JSVAL_IS_PRIMITIVE(aPlaceInfos), NS_ERROR_INVALID_ARG);
 
-  uint32_t infosLength = 1;
+  uint32_t infosLength;
   JS::Rooted<JSObject*> infos(aCtx);
-  if (JS_IsArrayObject(aCtx, aPlaceInfos.toObjectOrNull())) {
-    infos = aPlaceInfos.toObjectOrNull();
-    (void)JS_GetArrayLength(aCtx, infos, &infosLength);
-    NS_ENSURE_ARG(infosLength > 0);
-  }
-  else {
-    // Build a temporary array to store this one item so the code below can
-    // just loop.
-    infos = JS_NewArrayObject(aCtx, 0, NULL);
-    NS_ENSURE_TRUE(infos, NS_ERROR_OUT_OF_MEMORY);
-
-    JSBool rc = JS_DefineElement(aCtx, infos, 0, aPlaceInfos, NULL, NULL, 0);
-    NS_ENSURE_TRUE(rc, NS_ERROR_UNEXPECTED);
-  }
+  nsresult rv = GetJSArrayFromJSValue(aPlaceInfos, aCtx, infos.address(), &infosLength);
+  NS_ENSURE_SUCCESS(rv, rv);
 
   nsTArray<VisitData> visitData;
   for (uint32_t i = 0; i < infosLength; i++) {
     JS::Rooted<JSObject*> info(aCtx);
     nsresult rv = GetJSObjectFromArray(aCtx, infos, i, info.address());
     NS_ENSURE_SUCCESS(rv, rv);
 
     nsCOMPtr<nsIURI> uri = GetURIFromJSObject(aCtx, info, "uri");
@@ -2638,17 +2867,17 @@ History::UpdatePlaces(const JS::Value& a
     // so we have to manage it by AddRefing now. NotifyCompletion will
     // release it for us once it has dispatched the callback to the main
     // thread.
     NS_ADDREF(aCallback);
 
     nsCOMPtr<nsIEventTarget> backgroundThread = do_GetInterface(dbConn);
     NS_ENSURE_TRUE(backgroundThread, NS_ERROR_UNEXPECTED);
     nsCOMPtr<nsIRunnable> event = new NotifyCompletion(aCallback);
-    (void)backgroundThread->Dispatch(event, NS_DISPATCH_NORMAL);
+    return backgroundThread->Dispatch(event, NS_DISPATCH_NORMAL);
   }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 History::IsURIVisited(nsIURI* aURI,
                       mozIVisitedStatusCallback* aCallback)
--- a/toolkit/components/places/History.h
+++ b/toolkit/components/places/History.h
@@ -68,19 +68,20 @@ public:
    */
   nsresult UpdatePlace(const VisitData& aVisitData);
 
   /**
    * Loads information about the page into _place from moz_places.
    *
    * @param _place
    *        The VisitData for the place we need to know information about.
-   * @return true if the page was recorded in moz_places, false otherwise.
+   * @param [out] _exists
+   *        Whether or the page was recorded in moz_places, false otherwise.
    */
-  bool FetchPageInfo(VisitData& _place);
+  nsresult FetchPageInfo(VisitData& _place, bool* _exists);
 
   /**
    * Get the number of bytes of memory this History object is using,
    * including sizeof(*this))
    */
   size_t SizeOfIncludingThis(nsMallocSizeOfFun aMallocSizeOf);
 
   /**
--- a/toolkit/components/places/PlaceInfo.cpp
+++ b/toolkit/components/places/PlaceInfo.cpp
@@ -14,24 +14,40 @@ namespace places {
 
 ////////////////////////////////////////////////////////////////////////////////
 //// PlaceInfo
 
 PlaceInfo::PlaceInfo(int64_t aId,
                      const nsCString& aGUID,
                      already_AddRefed<nsIURI> aURI,
                      const nsString& aTitle,
+                     int64_t aFrecency)
+: mId(aId)
+, mGUID(aGUID)
+, mURI(aURI)
+, mTitle(aTitle)
+, mFrecency(aFrecency)
+, mVisitsAvailable(false)
+{
+  NS_PRECONDITION(mURI, "Must provide a non-null uri!");
+}
+
+PlaceInfo::PlaceInfo(int64_t aId,
+                     const nsCString& aGUID,
+                     already_AddRefed<nsIURI> aURI,
+                     const nsString& aTitle,
                      int64_t aFrecency,
                      const VisitsArray& aVisits)
 : mId(aId)
 , mGUID(aGUID)
 , mURI(aURI)
 , mTitle(aTitle)
 , mFrecency(aFrecency)
 , mVisits(aVisits)
+, mVisitsAvailable(true)
 {
   NS_PRECONDITION(mURI, "Must provide a non-null uri!");
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 //// mozIPlaceInfo
 
 NS_IMETHODIMP
@@ -68,16 +84,24 @@ PlaceInfo::GetFrecency(int64_t* _frecenc
   *_frecency = mFrecency;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 PlaceInfo::GetVisits(JSContext* aContext,
                      JS::Value* _visits)
 {
+  // If the visits data was not provided, return null rather
+  // than an empty array to distinguish this case from the case
+  // of a place without any visit.
+  if (!mVisitsAvailable) {
+    *_visits = JSVAL_NULL;
+    return NS_OK;
+  }
+
   // TODO bug 625913 when we use this in situations that have more than one
   // visit here, we will likely want to make this cache the value.
   JS::Rooted<JSObject*> visits(aContext, JS_NewArrayObject(aContext, 0, NULL));
   NS_ENSURE_TRUE(visits, NS_ERROR_OUT_OF_MEMORY);
 
   JS::Rooted<JSObject*> global(aContext, JS_GetGlobalForScopeChain(aContext));
   NS_ENSURE_TRUE(global, NS_ERROR_UNEXPECTED);
 
--- a/toolkit/components/places/PlaceInfo.h
+++ b/toolkit/components/places/PlaceInfo.h
@@ -22,24 +22,27 @@ class PlaceInfo MOZ_FINAL : public mozIP
 {
 public:
   NS_DECL_ISUPPORTS
   NS_DECL_MOZIPLACEINFO
 
   typedef nsTArray< nsCOMPtr<mozIVisitInfo> > VisitsArray;
 
   PlaceInfo(int64_t aId, const nsCString& aGUID, already_AddRefed<nsIURI> aURI,
+            const nsString& aTitle, int64_t aFrecency);
+  PlaceInfo(int64_t aId, const nsCString& aGUID, already_AddRefed<nsIURI> aURI,
             const nsString& aTitle, int64_t aFrecency,
             const VisitsArray& aVisits);
 
 private:
   const int64_t mId;
   const nsCString mGUID;
   nsCOMPtr<nsIURI> mURI;
   const nsString mTitle;
   const int64_t mFrecency;
   const VisitsArray mVisits;
+  bool mVisitsAvailable;
 };
 
 } // namespace places
 } // namespace mozilla
 
 #endif // mozilla_places_PlaceInfo_h__
--- a/toolkit/components/places/PlacesUtils.jsm
+++ b/toolkit/components/places/PlacesUtils.jsm
@@ -1750,16 +1750,66 @@ this.PlacesUtils = {
         charset = PlacesUtils.annotations.getPageAnnotation(aURI,
                                                             PlacesUtils.CHARSET_ANNO);
       } catch (ex) { }
 
       deferred.resolve(charset);
     }, Ci.nsIThread.DISPATCH_NORMAL);
 
     return deferred.promise;
+  },
+
+  /**
+   * Promised wrapper for mozIAsyncHistory::updatePlaces for a single place.
+   *
+   * @param aPlaces
+   *        a single mozIPlaceInfo object
+   * @resolves {Promise}
+   */
+  promiseUpdatePlace: function PU_promiseUpdatePlaces(aPlace) {
+    let deferred = Promise.defer();
+    PlacesUtils.asyncHistory.updatePlaces(aPlace, {
+      _placeInfo: null,
+      handleResult: function handleResult(aPlaceInfo) {
+        this._placeInfo = aPlaceInfo;
+      },
+      handleError: function handleError(aResultCode, aPlaceInfo) {
+        deferred.reject(new Components.Exception("Error", aResultCode));
+      },
+      handleCompletion: function() {
+        deferred.resolve(this._placeInfo);
+      }
+    });
+
+    return deferred.promise;
+  },
+
+  /**
+   * Promised wrapper for mozIAsyncHistory::getPlacesInfo for a single place.
+   *
+   * @param aPlaceIdentifier
+   *        either an nsIURI or a GUID (@see getPlacesInfo)
+   * @resolves to the place info object handed to handleResult.
+   */
+  promisePlaceInfo: function PU_promisePlaceInfo(aPlaceIdentifier) {
+    let deferred = Promise.defer();
+    PlacesUtils.asyncHistory.getPlacesInfo(aPlaceIdentifier, {
+      _placeInfo: null,
+      handleResult: function handleResult(aPlaceInfo) {
+        this._placeInfo = aPlaceInfo;
+      },
+      handleError: function handleError(aResultCode, aPlaceInfo) {
+        deferred.reject(new Components.Exception("Error", aResultCode));
+      },
+      handleCompletion: function() {
+        deferred.resolve(this._placeInfo);
+      }
+    });
+
+    return deferred.promise;
   }
 };
 
 /**
  * Wraps the provided statement so that invoking cancel() on the pending
  * statement object will always cause a REASON_CANCELED.
  */
 function AsyncStatementCancelWrapper(aStmt) {
--- a/toolkit/components/places/mozIAsyncHistory.idl
+++ b/toolkit/components/places/mozIAsyncHistory.idl
@@ -68,44 +68,44 @@ interface mozIPlaceInfo : nsISupports
 
   /**
    * An array of mozIVisitInfo objects for the place.
    */
   [implicit_jscontext]
   readonly attribute jsval visits;
 };
 
+/**
+ * Shared Callback interface for mozIAsyncHistory methods. The semantics
+ * for each method are detailed in mozIAsyncHistory.
+ */
 [scriptable, uuid(1f266877-2859-418b-a11b-ec3ae4f4f93d)]
 interface mozIVisitInfoCallback : nsISupports
 {
   /**
-   * Called when the given mozIPlaceInfo object could not be processed.
+   * Called when the given place could not be processed.
    *
    * @param aResultCode
    *        nsresult indicating the failure reason.
    * @param aPlaceInfo
-   *        The information that was being entered into the database.
+   *        The information that was given to the caller for the place.
    */
   void handleError(in nsresult aResultCode,
                    in mozIPlaceInfo aPlaceInfo);
 
   /**
-   * Called for each visit added, title change, or guid change when passed to
-   * mozIAsyncHistory::updatePlaces.  If more than one operation is done for 
-   * a given visit, only one callback will be given (i.e. title change and 
-   * add visit).
+   * Called for each place processed successfully.
    *
    * @param aPlaceInfo
-   *        The information that was being entered into the database.
+   *        The current info stored for the place.
    */
   void handleResult(in mozIPlaceInfo aPlaceInfo);
 
   /**
-   * Called when the mozIAsyncHistory::updatePlaces has finished processing
-   * all mozIPlaceInfo records.
+   * Called when all records were processed.
    */
   void handleCompletion();
 
 };
 
 [scriptable, function, uuid(994092bf-936f-449b-8dd6-0941e024360d)]
 interface mozIVisitedStatusCallback : nsISupports
 {
@@ -116,28 +116,58 @@ interface mozIVisitedStatusCallback : ns
    *        URI being notified about.
    * @param aVisitedStatus
    *        The visited status of aURI.
    */
   void isVisited(in nsIURI aURI,
                  in boolean aVisitedStatus);
 };
 
-[scriptable, uuid(b7edc16e-9f3c-4bf5-981b-4e8000b02d89)]
+[scriptable, uuid(1643EFD2-A329-4733-A39D-17069C8D3B2D)]
 interface mozIAsyncHistory : nsISupports
 {
   /**
+   * Gets the available information for the given array of places, each
+   * identified by either nsIURI or places GUID (string).
+   *
+   * The retrieved places info objects DO NOT include the visits data (the
+   * |visits| attribute is set to null).
+   *
+   * If a given place does not exist in the database, aCallback.handleError is
+   * called for it with NS_ERROR_NOT_AVAILABLE result code.
+   *
+   * @param aPlaceIdentifiers
+   *        The place[s] for which to retrieve information, identified by either
+   *        a single place GUID, a single URI, or a JS array of URIs and/or GUIDs.
+   * @param aCallback
+   *        A mozIVisitInfoCallback object which consists of callbacks to be
+   *        notified for successful or failed retrievals.
+   *        If there's no information available for a given place, aCallback
+   *        is called with a stub place info object, containing just the provided
+   *        data (GUID or URI).
+   *
+   * @throws NS_ERROR_INVALID_ARG
+   *         - Passing in NULL for aPlaceIdentifiers or aCallback.
+   *         - Not providing at least one valid GUID or URI. 
+   */
+  [implicit_jscontext]
+  void getPlacesInfo(in jsval aPlaceIdentifiers,
+                     in mozIVisitInfoCallback aCallback);
+
+  /**
    * Adds a set of visits for one or more mozIPlaceInfo objects, and updates
    * each mozIPlaceInfo's title or guid.
    *
+   * aCallback.handleResult is called for each visit added.
+   *
    * @param aPlaceInfo
    *        The mozIPlaceInfo object[s] containing the information to store or
    *        update.  This can be a single object, or an array of objects.
    * @param [optional] aCallback
-   *        A mozIVisitInfoCallback object which consists of callbacks to be 
+   *        A mozIVisitInfoCallback object which consists of callbacks to be
    *        notified for successful and/or failed changes.
    *
    * @throws NS_ERROR_INVALID_ARG
    *         - Passing in NULL for aPlaceInfo.
    *         - Not providing at least one valid guid, or uri for all
    *           mozIPlaceInfo object[s].
    *         - Not providing an array or nothing for the visits property of
    *           mozIPlaceInfo.
--- a/toolkit/components/places/nsINavHistoryService.idl
+++ b/toolkit/components/places/nsINavHistoryService.idl
@@ -1236,16 +1236,17 @@ interface nsINavHistoryService : nsISupp
    * the "clear history" button should be enabled or not. This is much better
    * than using BrowserHistory.count since that can be very slow if there is
    * a lot of history (it must enumerate each item). This is pretty fast.
    */
   readonly attribute boolean hasHistoryEntries;
 
   /**
    * Gets the original title of the page.
+   * @deprecated use mozIAsyncHistory.getPlacesInfo instead.
    */
   AString getPageTitle(in nsIURI aURI);
 
   /**
    * This is just like markPageAsTyped (in nsIBrowserHistory, also implemented
    * by the history service), but for bookmarks. It declares that the given URI
    * is being opened as a result of following a bookmark. If this URI is loaded
    * soon after this message has been received, that transition will be marked
--- a/toolkit/components/places/nsNavHistory.cpp
+++ b/toolkit/components/places/nsNavHistory.cpp
@@ -2823,16 +2823,18 @@ nsNavHistory::GetCharsetForURI(nsIURI* a
   }
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 nsNavHistory::GetPageTitle(nsIURI* aURI, nsAString& aTitle)
 {
+  PLACES_WARN_DEPRECATED();
+
   NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
   NS_ENSURE_ARG(aURI);
 
   aTitle.Truncate(0);
 
   nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
     "SELECT id, url, title, rev_host, visit_count, guid "
     "FROM moz_places "
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_getPlacesInfo.js
@@ -0,0 +1,116 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function promiseGetPlacesInfo(aPlacesIdentifiers) {
+  let deferred = Promise.defer();
+  PlacesUtils.asyncHistory.getPlacesInfo(aPlacesIdentifiers, {
+    _results: [],
+    _errors: [],
+
+    handleResult: function handleResult(aPlaceInfo) {
+      this._results.push(aPlaceInfo);
+    },
+    handleError: function handleError(aResultCode, aPlaceInfo) {
+      this._errors.push({ resultCode: aResultCode, info: aPlaceInfo });
+    },
+    handleCompletion: function handleCompletion() {
+      deferred.resolve({ errors: this._errors, results: this._results });
+    }
+  });
+
+  return deferred.promise;
+}
+
+function ensurePlacesInfoObjectsAreEqual(a, b) {
+  do_check_true(a.uri.equals(b.uri));
+  do_check_eq(a.title, b.title);
+  do_check_eq(a.guid, b.guid);
+  do_check_eq(a.placeId, b.placeId);
+}
+
+function test_getPlacesInfoExistentPlace() {
+  let testURI = NetUtil.newURI("http://www.example.tld");
+  yield promiseAddVisits(testURI);
+
+  let getPlacesInfoResult = yield promiseGetPlacesInfo([testURI]);
+  do_check_eq(getPlacesInfoResult.results.length, 1);
+  do_check_eq(getPlacesInfoResult.errors.length, 0);
+
+  let placeInfo = getPlacesInfoResult.results[0];
+  do_check_true(placeInfo instanceof Ci.mozIPlaceInfo);
+
+  do_check_true(placeInfo.uri.equals(testURI));
+  do_check_eq(placeInfo.title, "test visit for " + testURI.spec);
+  do_check_true(placeInfo.guid.length > 0);
+  do_check_eq(placeInfo.visits, null);
+}
+add_task(test_getPlacesInfoExistentPlace);
+
+function test_getPlacesInfoNonExistentPlace() {
+  let testURI = NetUtil.newURI("http://www.example_non_existent.tld");
+  let getPlacesInfoResult = yield promiseGetPlacesInfo(testURI);
+  do_check_eq(getPlacesInfoResult.results.length, 0);
+  do_check_eq(getPlacesInfoResult.errors.length, 1);
+}
+add_task(test_getPlacesInfoNonExistentPlace);
+
+function test_promisedHelper() {
+  let (uri = NetUtil.newURI("http://www.helper_existent_example.tld")) {
+    yield promiseAddVisits(uri);
+    let placeInfo = yield PlacesUtils.promisePlaceInfo(uri);
+    do_check_true(placeInfo instanceof Ci.mozIPlaceInfo);
+  };
+
+  let (uri = NetUtil.newURI("http://www.helper_non_existent_example.tld")) {
+    try {
+      let placeInfo = yield PlacesUtils.promisePlaceInfo(uri);
+      do_throw("PlacesUtils.promisePlaceInfo should have rejected the promise");
+    }
+    catch(ex) { }
+  };
+}
+add_task(test_promisedHelper);
+
+function test_infoByGUID() {
+  let testURI = NetUtil.newURI("http://www.guid_example.tld");
+  yield promiseAddVisits(testURI);
+
+  let placeInfoByURI = yield PlacesUtils.promisePlaceInfo(testURI);
+  let placeInfoByGUID = yield PlacesUtils.promisePlaceInfo(placeInfoByURI.guid);
+  ensurePlacesInfoObjectsAreEqual(placeInfoByURI, placeInfoByGUID);
+}
+add_task(test_infoByGUID);
+
+function test_invalid_guid() {
+  try {
+    let placeInfoByGUID = yield PlacesUtils.promisePlaceInfo("###");
+    do_throw("getPlacesInfo should fail for invalid guids")
+  }
+  catch(ex) { }
+}
+add_task(test_invalid_guid);
+
+function test_mixed_selection() {
+  let placeInfo1, placeInfo2;
+  let (uri = NetUtil.newURI("http://www.mixed_selection_test_1.tld")) {
+    yield promiseAddVisits(uri);
+    placeInfo1 = yield PlacesUtils.promisePlaceInfo(uri);
+  };
+
+  let (uri = NetUtil.newURI("http://www.mixed_selection_test_2.tld")) {
+    yield promiseAddVisits(uri);
+    placeInfo2 = yield PlacesUtils.promisePlaceInfo(uri);
+  };
+
+  let getPlacesInfoResult = yield promiseGetPlacesInfo([placeInfo1.uri, placeInfo2.guid]);
+  do_check_eq(getPlacesInfoResult.results.length, 2);
+  do_check_eq(getPlacesInfoResult.errors.length, 0);
+
+  do_check_eq(getPlacesInfoResult.results[0].uri.spec, placeInfo1.uri.spec);
+  do_check_eq(getPlacesInfoResult.results[1].guid, placeInfo2.guid);
+}
+add_task(test_mixed_selection);
+
+function run_test() {
+  run_next_test();
+}
--- a/toolkit/components/places/tests/unit/xpcshell.ini
+++ b/toolkit/components/places/tests/unit/xpcshell.ini
@@ -118,8 +118,9 @@ skip-if = os == "android"
 skip-if = os == "android"
 [test_utils_backups_create.js]
 [test_utils_getURLsForContainerNode.js]
 [test_utils_setAnnotationsFor.js]
 [test_PlacesUtils_asyncGetBookmarkIds.js]
 [test_PlacesUtils_lazyobservers.js]
 [test_placesTxn.js]
 [test_telemetry.js]
+[test_getPlacesInfo.js]