Bug 1340498 - Implement new Places Observers interface r=mrbkap
☠☠ backed out by 83d286f5134f ☠ ☠
authorDoug Thayer <dothayer@mozilla.com>
Wed, 14 Feb 2018 09:06:15 -0800
changeset 478705 df9a67c58183a189b4175dbe063bed7407f12b37
parent 478704 a5977e0708ea2bd07e9029b3b80b3035c7243534
child 478706 8ed32495b46f58741c6c154de3bc5a1b5a5c5a31
push id9719
push userffxbld-merge
push dateFri, 24 Aug 2018 17:49:46 +0000
treeherdermozilla-beta@719ec98fba77 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmrbkap
bugs1340498
milestone63.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1340498 - Implement new Places Observers interface r=mrbkap See the design doc[1] for further info. We would like to redesign the places observer system to be more performant and more friendly to consume. WebIDL was recommended as it simplifies creating simple dictionary payloads while allowing dynamic typing with `any`. There were some difficulties with WebIDL though, most of which revolved around allowing consumers to be weakly referenced, from both C++ and JS. The simplest solution I could come up with was to create a simple native interface for the C++ case, and a WebIDL wrapper for a JS callback in the JS case. Suggestions for simpler alternatives are very welcome though. [1] https://docs.google.com/document/d/1G45vfd6RXFXwNz7i4FV40lDCU0ao-JX_bZdgJV4tLjk/edit?usp=sharing MozReview-Commit-ID: ACnAEfa5WxO
dom/base/PlacesEvent.cpp
dom/base/PlacesEvent.h
dom/base/PlacesObservers.cpp
dom/base/PlacesObservers.h
dom/base/PlacesVisit.h
dom/base/PlacesWeakCallbackWrapper.cpp
dom/base/PlacesWeakCallbackWrapper.h
dom/base/moz.build
dom/chrome-webidl/PlacesEvent.webidl
dom/chrome-webidl/PlacesObservers.webidl
dom/chrome-webidl/moz.build
toolkit/components/places/History.cpp
toolkit/components/places/INativePlacesEventCallback.h
toolkit/components/places/moz.build
toolkit/components/places/nsINavHistoryService.idl
new file mode 100644
--- /dev/null
+++ b/dom/base/PlacesEvent.cpp
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/PlacesEvent.h"
+
+#include "mozilla/HoldDropJSObjects.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(PlacesEvent)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(PlacesEvent)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(PlacesEvent)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(PlacesEvent)
+  NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(PlacesEvent, AddRef)
+NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(PlacesEvent, Release)
+
+already_AddRefed<PlacesEvent>
+PlacesEvent::Constructor(const GlobalObject& aGlobal,
+                         PlacesEventType aType,
+                         ErrorResult& rv)
+{
+  RefPtr<PlacesEvent> event = new PlacesEvent(aType);
+  return event.forget();
+}
+
+nsISupports*
+PlacesEvent::GetParentObject() const
+{
+  return nullptr;
+}
+
+JSObject*
+PlacesEvent::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
+{
+  return PlacesEventBinding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/base/PlacesEvent.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_PlacesEvent_h
+#define mozilla_dom_PlacesEvent_h
+
+#include "mozilla/dom/PlacesEventBinding.h"
+#include "mozilla/ErrorResult.h"
+#include "nsWrapperCache.h"
+
+namespace mozilla {
+namespace dom {
+
+class PlacesEvent : public nsWrapperCache
+{
+public:
+  explicit PlacesEvent(PlacesEventType aType)
+    : mType(aType)
+  {}
+
+  static already_AddRefed<PlacesEvent>
+  Constructor(const GlobalObject& aGlobal,
+              PlacesEventType aType,
+              ErrorResult& aRv);
+
+  NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(PlacesEvent)
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(PlacesEvent)
+
+  nsISupports* GetParentObject() const;
+
+  JSObject*
+  WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
+
+  PlacesEventType Type() const { return mType; }
+
+  virtual const PlacesVisit* AsPlacesVisit() const { return nullptr; }
+protected:
+  virtual ~PlacesEvent() = default;
+  PlacesEventType mType;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_PlacesEvent_h
new file mode 100644
--- /dev/null
+++ b/dom/base/PlacesObservers.cpp
@@ -0,0 +1,350 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "PlacesObservers.h"
+
+#include "PlacesWeakCallbackWrapper.h"
+#include "nsIWeakReferenceUtils.h"
+#include "mozilla/ClearOnShutdown.h"
+
+namespace mozilla {
+namespace dom {
+
+template <class T>
+struct Flagged
+{
+  Flagged(uint32_t aFlags, T&& aValue)
+    : flags(aFlags)
+    , value(std::forward<T>(aValue))
+  {}
+  Flagged(Flagged&& aOther)
+    : Flagged(std::move(aOther.flags), std::move(aOther.value))
+  {}
+  Flagged(const Flagged& aOther) = default;
+  ~Flagged() = default;
+
+  uint32_t flags;
+  T value;
+};
+
+template <class T>
+using FlaggedArray = nsTArray<Flagged<T>>;
+
+template <class T>
+struct ListenerCollection
+{
+  static StaticAutoPtr<FlaggedArray<T>> gListeners;
+  static StaticAutoPtr<FlaggedArray<T>> gListenersToRemove;
+
+  static FlaggedArray<T>* GetListeners() {
+    MOZ_ASSERT(NS_IsMainThread());
+    if (!gListeners) {
+      gListeners = new FlaggedArray<T>();
+      ClearOnShutdown(&gListeners);
+    }
+    return gListeners;
+  }
+
+  static FlaggedArray<T>* GetListenersToRemove() {
+    MOZ_ASSERT(NS_IsMainThread());
+    if (!gListenersToRemove) {
+      gListenersToRemove = new FlaggedArray<T>();
+      ClearOnShutdown(&gListenersToRemove);
+    }
+    return gListenersToRemove;
+  }
+};
+
+template <class T>
+StaticAutoPtr<FlaggedArray<T>> ListenerCollection<T>::gListeners;
+template <class T>
+StaticAutoPtr<FlaggedArray<T>> ListenerCollection<T>::gListenersToRemove;
+
+typedef ListenerCollection<RefPtr<PlacesEventCallback>> JSListeners;
+typedef ListenerCollection<WeakPtr<PlacesWeakCallbackWrapper>> WeakJSListeners;
+typedef ListenerCollection<WeakPtr<places::INativePlacesEventCallback>> WeakNativeListeners;
+
+static bool gCallingListeners = false;
+
+uint32_t
+GetEventTypeFlag(PlacesEventType aEventType)
+{
+  if (aEventType == PlacesEventType::None) {
+    return 0;
+  }
+  return 1 << ((uint32_t)aEventType - 1);
+}
+
+uint32_t
+GetFlagsForEventTypes(const nsTArray<PlacesEventType>& aEventTypes)
+{
+  uint32_t flags = 0;
+  for (PlacesEventType eventType : aEventTypes) {
+    flags |= GetEventTypeFlag(eventType);
+  }
+  return flags;
+}
+
+uint32_t
+GetFlagsForEvents(const nsTArray<OwningNonNull<PlacesEvent>>& aEvents)
+{
+  uint32_t flags = 0;
+  for (const PlacesEvent& event : aEvents) {
+    flags |= GetEventTypeFlag(event.Type());
+  }
+  return flags;
+}
+
+template <class TWrapped, class TUnwrapped>
+void CallListeners(uint32_t aEventFlags,
+                   FlaggedArray<TWrapped>& aListeners,
+                   const Sequence<OwningNonNull<PlacesEvent>>& aEvents,
+                   const std::function<TUnwrapped(TWrapped&)>& aUnwrapListener,
+                   const std::function<void(TUnwrapped&, const Sequence<OwningNonNull<PlacesEvent>>&)>& aCallListener)
+{
+  for (uint32_t i = 0; i < aListeners.Length(); i++) {
+    Flagged<TWrapped>& l = aListeners[i];
+    TUnwrapped unwrapped = aUnwrapListener(l.value);
+    if (!unwrapped) {
+      aListeners.RemoveElementAt(i);
+      i--;
+      continue;
+    }
+
+    if ((l.flags & aEventFlags) == aEventFlags) {
+      aCallListener(unwrapped, aEvents);
+    } else if (l.flags & aEventFlags) {
+      Sequence<OwningNonNull<PlacesEvent>> filtered;
+      for (const OwningNonNull<PlacesEvent>& event : aEvents) {
+        if (l.flags & GetEventTypeFlag(event->Type())) {
+          bool success = !!filtered.AppendElement(event, fallible);
+          MOZ_RELEASE_ASSERT(success);
+        }
+      }
+      aCallListener(unwrapped, filtered);
+    }
+  }
+}
+
+void
+PlacesObservers::AddListener(GlobalObject& aGlobal,
+                             const nsTArray<PlacesEventType>& aEventTypes,
+                             PlacesEventCallback& aCallback,
+                             ErrorResult& rv)
+{
+  uint32_t flags = GetFlagsForEventTypes(aEventTypes);
+
+  FlaggedArray<RefPtr<PlacesEventCallback>>* listeners =
+    JSListeners::GetListeners();
+  Flagged<RefPtr<PlacesEventCallback>> pair(flags, &aCallback);
+  listeners->AppendElement(pair);
+}
+
+void
+PlacesObservers::AddListener(GlobalObject& aGlobal,
+                             const nsTArray<PlacesEventType>& aEventTypes,
+                             PlacesWeakCallbackWrapper& aCallback,
+                             ErrorResult& rv)
+{
+  uint32_t flags = GetFlagsForEventTypes(aEventTypes);
+
+  FlaggedArray<WeakPtr<PlacesWeakCallbackWrapper>>* listeners =
+    WeakJSListeners::GetListeners();
+  WeakPtr<PlacesWeakCallbackWrapper> weakCb(&aCallback);
+  MOZ_ASSERT(weakCb.get());
+  Flagged<WeakPtr<PlacesWeakCallbackWrapper>> flagged(flags, std::move(weakCb));
+  listeners->AppendElement(flagged);
+}
+
+void
+PlacesObservers::AddListener(const nsTArray<PlacesEventType>& aEventTypes,
+                             places::INativePlacesEventCallback* aCallback)
+{
+  uint32_t flags = GetFlagsForEventTypes(aEventTypes);
+
+  FlaggedArray<WeakPtr<places::INativePlacesEventCallback>>* listeners =
+    WeakNativeListeners::GetListeners();
+  Flagged<WeakPtr<places::INativePlacesEventCallback>> pair(flags, aCallback);
+  listeners->AppendElement(pair);
+}
+
+void
+PlacesObservers::RemoveListener(GlobalObject& aGlobal,
+                                const nsTArray<PlacesEventType>& aEventTypes,
+                                PlacesEventCallback& aCallback,
+                                ErrorResult& rv)
+{
+  uint32_t flags = GetFlagsForEventTypes(aEventTypes);
+  if (gCallingListeners) {
+    FlaggedArray<RefPtr<PlacesEventCallback>>* listeners =
+      JSListeners::GetListenersToRemove();
+    Flagged<RefPtr<PlacesEventCallback>> pair(flags, &aCallback);
+    listeners->AppendElement(pair);
+  } else {
+    RemoveListener(flags, aCallback);
+  }
+}
+
+void
+PlacesObservers::RemoveListener(GlobalObject& aGlobal,
+                                const nsTArray<PlacesEventType>& aEventTypes,
+                                PlacesWeakCallbackWrapper& aCallback,
+                                ErrorResult& rv)
+{
+  uint32_t flags = GetFlagsForEventTypes(aEventTypes);
+  if (gCallingListeners) {
+    FlaggedArray<WeakPtr<PlacesWeakCallbackWrapper>>* listeners =
+      WeakJSListeners::GetListenersToRemove();
+    WeakPtr<PlacesWeakCallbackWrapper> weakCb(&aCallback);
+    MOZ_ASSERT(weakCb.get());
+    Flagged<WeakPtr<PlacesWeakCallbackWrapper>> flagged(flags, std::move(weakCb));
+    listeners->AppendElement(flagged);
+  } else {
+    RemoveListener(flags, aCallback);
+  }
+}
+
+void
+PlacesObservers::RemoveListener(const nsTArray<PlacesEventType>& aEventTypes,
+                                places::INativePlacesEventCallback* aCallback)
+{
+  uint32_t flags = GetFlagsForEventTypes(aEventTypes);
+  if (gCallingListeners) {
+    FlaggedArray<WeakPtr<places::INativePlacesEventCallback>>* listeners =
+      WeakNativeListeners::GetListenersToRemove();
+    Flagged<WeakPtr<places::INativePlacesEventCallback>> pair(flags, aCallback);
+    listeners->AppendElement(pair);
+  } else {
+    RemoveListener(flags, aCallback);
+  }
+}
+
+void
+PlacesObservers::RemoveListener(uint32_t aFlags,
+                                PlacesEventCallback& aCallback)
+{
+  FlaggedArray<RefPtr<PlacesEventCallback>>& listeners =
+    *JSListeners::GetListeners();
+  for (uint32_t i = 0; i < listeners.Length(); i++) {
+    Flagged<RefPtr<PlacesEventCallback>>& l = listeners[i];
+    if (!(*l.value == aCallback)) {
+      continue;
+    }
+    if (l.flags == aFlags) {
+      listeners.RemoveElementAt(i);
+      i--;
+    } else {
+      l.flags &= ~aFlags;
+    }
+  }
+}
+
+void
+PlacesObservers::RemoveListener(uint32_t aFlags,
+                                PlacesWeakCallbackWrapper& aCallback)
+{
+  FlaggedArray<WeakPtr<PlacesWeakCallbackWrapper>>& listeners =
+    *WeakJSListeners::GetListeners();
+  for (uint32_t i = 0; i < listeners.Length(); i++) {
+    Flagged<WeakPtr<PlacesWeakCallbackWrapper>>& l = listeners[i];
+    RefPtr<PlacesWeakCallbackWrapper> unwrapped = l.value.get();
+    if (unwrapped != &aCallback) {
+      continue;
+    }
+    if (l.flags == aFlags) {
+      listeners.RemoveElementAt(i);
+      i--;
+    } else {
+      l.flags &= ~aFlags;
+    }
+  }
+}
+
+void
+PlacesObservers::RemoveListener(uint32_t aFlags,
+                                places::INativePlacesEventCallback* aCallback)
+{
+  FlaggedArray<WeakPtr<places::INativePlacesEventCallback>>& listeners =
+    *WeakNativeListeners::GetListeners();
+  for (uint32_t i = 0; i < listeners.Length(); i++) {
+    Flagged<WeakPtr<places::INativePlacesEventCallback>>& l = listeners[i];
+    RefPtr<places::INativePlacesEventCallback> unwrapped = l.value.get();
+    if (unwrapped != aCallback) {
+      continue;
+    }
+    if (l.flags == aFlags) {
+      listeners.RemoveElementAt(i);
+      i--;
+    } else {
+      l.flags &= ~aFlags;
+    }
+  }
+}
+
+void
+PlacesObservers::NotifyListeners(GlobalObject& aGlobal,
+                                 const Sequence<OwningNonNull<PlacesEvent>>& aEvents,
+                                 ErrorResult& rv)
+{
+  NotifyListeners(aEvents);
+}
+
+void
+PlacesObservers::NotifyListeners(const Sequence<OwningNonNull<PlacesEvent>>& aEvents)
+{
+  MOZ_RELEASE_ASSERT(!gCallingListeners);
+  gCallingListeners = true;
+  uint32_t flags = GetFlagsForEvents(aEvents);
+
+  CallListeners<RefPtr<PlacesEventCallback>, RefPtr<PlacesEventCallback>>(
+    flags, *JSListeners::GetListeners(), aEvents,
+    [](auto& cb) { return cb; },
+    [&](auto& cb, const auto& events) {
+      cb->Call(aEvents);
+    });
+
+  CallListeners<WeakPtr<places::INativePlacesEventCallback>,
+                RefPtr<places::INativePlacesEventCallback>>(
+    flags, *WeakNativeListeners::GetListeners(), aEvents,
+    [](auto& cb) { return cb.get(); },
+    [&](auto& cb, const Sequence<OwningNonNull<PlacesEvent>>& events) {
+      cb->HandlePlacesEvent(events);
+    });
+
+  CallListeners<WeakPtr<PlacesWeakCallbackWrapper>,
+                RefPtr<PlacesWeakCallbackWrapper>>(
+    flags, *WeakJSListeners::GetListeners(), aEvents,
+    [](auto& cb) { return cb.get(); },
+    [&](auto& cb, const auto& events) {
+      cb->mCallback->Call(aEvents);
+    });
+
+  auto& listenersToRemove = *JSListeners::GetListenersToRemove();
+  if (listenersToRemove.Length() > 0) {
+    for (auto& listener : listenersToRemove) {
+      RemoveListener(listener.flags, *listener.value);
+    }
+  }
+
+  auto& weakListenersToRemove = *WeakJSListeners::GetListenersToRemove();
+  if (weakListenersToRemove.Length() > 0) {
+    for (auto& listener : weakListenersToRemove) {
+      RemoveListener(listener.flags, *listener.value.get());
+    }
+  }
+
+  auto& nativeListenersToRemove = *WeakNativeListeners::GetListenersToRemove();
+  if (nativeListenersToRemove.Length() > 0) {
+    for (auto& listener : nativeListenersToRemove) {
+      RemoveListener(listener.flags, listener.value.get());
+    }
+  }
+
+  gCallingListeners = false;
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/base/PlacesObservers.h
@@ -0,0 +1,62 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_PlacesObservers__
+#define mozilla_dom_PlacesObservers__
+
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/PlacesObserversBinding.h"
+#include "mozilla/dom/PlacesEvent.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/Pair.h"
+#include "mozilla/places/INativePlacesEventCallback.h"
+#include "nsIWeakReferenceUtils.h"
+
+namespace mozilla {
+
+namespace dom {
+
+class PlacesObservers
+{
+public:
+  static void AddListener(GlobalObject& aGlobal,
+                          const nsTArray<PlacesEventType>& aEventTypes,
+                          PlacesEventCallback& aCallback,
+                          ErrorResult& rv);
+  static void AddListener(GlobalObject& aGlobal,
+                          const nsTArray<PlacesEventType>& aEventTypes,
+                          PlacesWeakCallbackWrapper& aCallback,
+                          ErrorResult& rv);
+  static void AddListener(const nsTArray<PlacesEventType>& aEventTypes,
+                          places::INativePlacesEventCallback* aCallback);
+  static void RemoveListener(GlobalObject& aGlobal,
+                             const nsTArray<PlacesEventType>& aEventTypes,
+                             PlacesEventCallback& aCallback,
+                             ErrorResult& rv);
+  static void RemoveListener(GlobalObject& aGlobal,
+                             const nsTArray<PlacesEventType>& aEventTypes,
+                             PlacesWeakCallbackWrapper& aCallback,
+                             ErrorResult& rv);
+  static void RemoveListener(const nsTArray<PlacesEventType>& aEventTypes,
+                             places::INativePlacesEventCallback* aCallback);
+
+  static void NotifyListeners(GlobalObject& aGlobal,
+                              const Sequence<OwningNonNull<PlacesEvent>>& aEvents,
+                              ErrorResult& rv);
+
+  static void NotifyListeners(const Sequence<OwningNonNull<PlacesEvent>>& aEvents);
+
+private:
+  static void RemoveListener(uint32_t aFlags, PlacesEventCallback& aCallback);
+  static void RemoveListener(uint32_t aFlags, PlacesWeakCallbackWrapper& aCallback);
+  static void RemoveListener(uint32_t aFlags, places::INativePlacesEventCallback* aCallback);
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_PlacesObservers__
new file mode 100644
--- /dev/null
+++ b/dom/base/PlacesVisit.h
@@ -0,0 +1,67 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_PlacesVisit_h
+#define mozilla_dom_PlacesVisit_h
+
+#include "mozilla/dom/PlacesEvent.h"
+
+namespace mozilla {
+namespace dom {
+
+class PlacesVisit final : public PlacesEvent
+{
+public:
+  explicit PlacesVisit() : PlacesEvent(PlacesEventType::Page_visited) {}
+
+  static already_AddRefed<PlacesVisit>
+  Constructor(const GlobalObject& aGlobal,
+              ErrorResult& aRv) {
+    RefPtr<PlacesVisit> event = new PlacesVisit();
+    return event.forget();
+  }
+
+  JSObject*
+  WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override
+  {
+    return PlacesVisitBinding::Wrap(aCx, this, aGivenProto);
+  }
+
+  const PlacesVisit* AsPlacesVisit() const override { return this; }
+
+  void GetUrl(nsString& aUrl) { aUrl = mUrl; }
+  uint64_t VisitId() { return mVisitId; }
+  uint64_t VisitTime() { return mVisitTime; }
+  uint64_t ReferringVisitId() { return mReferringVisitId; }
+  uint64_t TransitionType() { return mTransitionType; }
+  void GetPageGuid(nsTString<char>& aPageGuid) { aPageGuid = mPageGuid; }
+  bool Hidden() { return mHidden; }
+  uint32_t VisitCount() { return mVisitCount; }
+  uint32_t TypedCount() { return mTypedCount; }
+  void GetLastKnownTitle(nsString& aLastKnownTitle) { aLastKnownTitle = mLastKnownTitle; }
+
+  // It's convenient for these to be directly available in C++, so just expose
+  // them. These are generally passed around with const qualifiers anyway, so
+  // it shouldn't be a problem.
+  nsString mUrl;
+  uint64_t mVisitId;
+  uint64_t mVisitTime;
+  uint64_t mReferringVisitId;
+  uint32_t mTransitionType;
+  nsCString mPageGuid;
+  bool mHidden;
+  uint32_t mVisitCount;
+  uint32_t mTypedCount;
+  nsString mLastKnownTitle;
+
+private:
+  ~PlacesVisit() = default;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_PlacesVisit_h
new file mode 100644
--- /dev/null
+++ b/dom/base/PlacesWeakCallbackWrapper.cpp
@@ -0,0 +1,55 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/PlacesWeakCallbackWrapper.h"
+
+#include "mozilla/HoldDropJSObjects.h"
+#include "mozilla/dom/ProcessGlobal.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PlacesWeakCallbackWrapper, mParent, mCallback)
+NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(PlacesWeakCallbackWrapper, AddRef)
+NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(PlacesWeakCallbackWrapper, Release)
+
+PlacesWeakCallbackWrapper::PlacesWeakCallbackWrapper(nsISupports* aParent,
+                                                     PlacesEventCallback& aCallback)
+  : mParent(do_GetWeakReference(aParent))
+  , mCallback(&aCallback)
+{
+}
+
+already_AddRefed<PlacesWeakCallbackWrapper>
+PlacesWeakCallbackWrapper::Constructor(const GlobalObject& aGlobal,
+                                       PlacesEventCallback& aCallback,
+                                       ErrorResult& rv)
+{
+  nsCOMPtr<nsISupports> parent = aGlobal.GetAsSupports();
+  RefPtr<PlacesWeakCallbackWrapper> wrapper =
+    new PlacesWeakCallbackWrapper(parent, aCallback);
+  return wrapper.forget();
+}
+
+PlacesWeakCallbackWrapper::~PlacesWeakCallbackWrapper()
+{
+}
+
+nsISupports*
+PlacesWeakCallbackWrapper::GetParentObject() const
+{
+  nsCOMPtr<nsISupports> parent = do_QueryReferent(mParent);
+  return parent;
+}
+
+JSObject*
+PlacesWeakCallbackWrapper::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
+{
+  return PlacesWeakCallbackWrapperBinding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/base/PlacesWeakCallbackWrapper.h
@@ -0,0 +1,49 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_PlacesWeakCallbackWrapper_h
+#define mozilla_dom_PlacesWeakCallbackWrapper_h
+
+#include "mozilla/dom/PlacesObserversBinding.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/Pair.h"
+#include "nsWrapperCache.h"
+
+namespace mozilla {
+namespace dom {
+
+class PlacesWeakCallbackWrapper final : public nsWrapperCache
+                                      , public SupportsWeakPtr<PlacesWeakCallbackWrapper>
+{
+public:
+  MOZ_DECLARE_WEAKREFERENCE_TYPENAME(PlacesWeakCallbackWrapper)
+  NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(PlacesWeakCallbackWrapper)
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(PlacesWeakCallbackWrapper)
+
+  explicit PlacesWeakCallbackWrapper(nsISupports* aParent,
+                                     PlacesEventCallback& aCallback);
+
+  static already_AddRefed<PlacesWeakCallbackWrapper>
+  Constructor(const GlobalObject& aGlobal,
+              PlacesEventCallback& aCallback,
+              ErrorResult& rv);
+
+  nsISupports* GetParentObject() const;
+
+  JSObject*
+  WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
+
+protected:
+  friend class PlacesObservers;
+  ~PlacesWeakCallbackWrapper();
+  nsWeakPtr mParent;
+  RefPtr<PlacesEventCallback> mCallback;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_PlacesWeakCallbackWrapper_h
--- a/dom/base/moz.build
+++ b/dom/base/moz.build
@@ -196,16 +196,20 @@ EXPORTS.mozilla.dom += [
     'MessageSender.h',
     'MozQueryInterface.h',
     'NameSpaceConstants.h',
     'Navigator.h',
     'NodeInfo.h',
     'NodeInfoInlines.h',
     'NodeIterator.h',
     'ParentProcessMessageManager.h',
+    'PlacesEvent.h',
+    'PlacesObservers.h',
+    'PlacesVisit.h',
+    'PlacesWeakCallbackWrapper.h',
     'Pose.h',
     'ProcessGlobal.h',
     'ProcessMessageManager.h',
     'ResponsiveImageSelector.h',
     'SameProcessMessageQueue.h',
     'ScreenOrientation.h',
     'Selection.h',
     'ShadowRoot.h',
@@ -385,16 +389,23 @@ if CONFIG['MOZ_WEBRTC']:
         'nsDOMDataChannel.cpp',
     ]
 
 if CONFIG['FUZZING']:
     UNIFIED_SOURCES += [
         'FuzzingFunctions.cpp',
     ]
 
+if CONFIG['MOZ_PLACES']:
+    UNIFIED_SOURCES += [
+        'PlacesEvent.cpp',
+        'PlacesObservers.cpp',
+        'PlacesWeakCallbackWrapper.cpp',
+    ]
+
 # these files couldn't be in UNIFIED_SOURCES for now for reasons given below:
 SOURCES += [
     # Several conflicts with other bindings.
     'DOMIntersectionObserver.cpp',
     # Because of OS X headers.
     'nsContentUtils.cpp',
     # this file doesn't like windows.h
     'nsDOMWindowUtils.cpp',
new file mode 100644
--- /dev/null
+++ b/dom/chrome-webidl/PlacesEvent.webidl
@@ -0,0 +1,69 @@
+enum PlacesEventType {
+  "none",
+
+  /**
+   * data: PlacesVisit. Fired whenever a page is visited.
+   */
+  "page-visited",
+};
+
+[ChromeOnly, Exposed=(Window,System)]
+interface PlacesEvent {
+  readonly attribute PlacesEventType type;
+};
+
+[ChromeOnly, Exposed=(Window,System)]
+interface PlacesVisit : PlacesEvent {
+  /**
+   * URL of the visit.
+   */
+  readonly attribute DOMString url;
+
+  /**
+   * Id of the visit.
+   */
+  readonly attribute unsigned long long visitId;
+
+  /**
+   * Time of the visit, in milliseconds since epoch.
+   */
+  readonly attribute unsigned long long visitTime;
+
+  /**
+   * The id of the visit the user came from, defaults to 0 for no referrer.
+   */
+  readonly attribute unsigned long long referringVisitId;
+
+  /**
+   * One of nsINavHistory.TRANSITION_*
+   */
+  readonly attribute unsigned long transitionType;
+
+  /**
+   * The unique id associated with the page.
+   */
+  readonly attribute ByteString pageGuid;
+
+  /**
+   * Whether the visited page is marked as hidden.
+   */
+  readonly attribute boolean hidden;
+
+  /**
+   * Number of visits (including this one) for this URL.
+   */
+  readonly attribute unsigned long visitCount;
+
+  /**
+   * Whether the URL 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 typedCount;
+
+  /**
+   * 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 DOMString? lastKnownTitle;
+};
new file mode 100644
--- /dev/null
+++ b/dom/chrome-webidl/PlacesObservers.webidl
@@ -0,0 +1,32 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+callback PlacesEventCallback = void (sequence<PlacesEvent> events);
+
+[ChromeOnly, Exposed=(Window,System),
+ Constructor(PlacesEventCallback callback)]
+interface PlacesWeakCallbackWrapper {
+};
+
+// Global singleton which should handle all events for places.
+[ChromeOnly, Exposed=(Window,System)]
+namespace PlacesObservers {
+  [Throws]
+  void addListener(sequence<PlacesEventType> eventTypes,
+                   PlacesEventCallback listener);
+  [Throws]
+  void addListener(sequence<PlacesEventType> eventTypes,
+                   PlacesWeakCallbackWrapper listener);
+  [Throws]
+  void removeListener(sequence<PlacesEventType> eventTypes,
+                      PlacesEventCallback listener);
+  [Throws]
+  void removeListener(sequence<PlacesEventType> eventTypes,
+                      PlacesWeakCallbackWrapper listener);
+  [Throws]
+  void notifyListeners(sequence<PlacesEvent> events);
+};
+
--- a/dom/chrome-webidl/moz.build
+++ b/dom/chrome-webidl/moz.build
@@ -41,8 +41,14 @@ WEBIDL_FILES = [
     'MozStorageStatementParams.webidl',
     'MozStorageStatementRow.webidl',
     'PrecompiledScript.webidl',
     'PromiseDebugging.webidl',
     'StructuredCloneHolder.webidl',
     'WebExtensionContentScript.webidl',
     'WebExtensionPolicy.webidl',
 ]
+
+if CONFIG['MOZ_PLACES']:
+    WEBIDL_FILES += [
+        'PlacesEvent.webidl',
+        'PlacesObservers.webidl',
+    ]
--- a/toolkit/components/places/History.cpp
+++ b/toolkit/components/places/History.cpp
@@ -165,131 +165,16 @@ struct VisitData {
   bool shouldUpdateFrecency;
 
   // Whether to override the visit type bonus with a redirect bonus when
   // calculating frecency on the most recent visit.
   bool useFrecencyRedirectBonus;
 };
 
 ////////////////////////////////////////////////////////////////////////////////
-//// 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)
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/INativePlacesEventCallback.h
@@ -0,0 +1,34 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_image_INativePlacesEventCallback_h
+#define mozilla_image_INativePlacesEventCallback_h
+
+#include "mozilla/dom/PlacesObserversBinding.h"
+#include "mozilla/WeakPtr.h"
+#include "nsISupports.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+namespace places {
+
+class INativePlacesEventCallback : public SupportsWeakPtr<INativePlacesEventCallback>
+{
+public:
+  typedef dom::Sequence<OwningNonNull<dom::PlacesEvent>> PlacesEventSequence;
+
+  MOZ_DECLARE_WEAKREFERENCE_TYPENAME(INativePlacesEventCallback)
+  NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+  virtual void HandlePlacesEvent(const PlacesEventSequence& aEvents) = 0;
+
+protected:
+  virtual ~INativePlacesEventCallback() { }
+};
+
+} // namespace places
+} // namespace mozilla
+
+#endif // mozilla_image_INativePlacesEventCallback_h
--- a/toolkit/components/places/moz.build
+++ b/toolkit/components/places/moz.build
@@ -24,16 +24,17 @@ if CONFIG['MOZ_PLACES']:
         'nsIFaviconService.idl',
         'nsINavBookmarksService.idl',
         'nsITaggingService.idl',
     ]
 
     EXPORTS.mozilla.places = [
         'Database.h',
         'History.h',
+        'INativePlacesEventCallback.h',
         'Shutdown.h',
     ]
 
     UNIFIED_SOURCES += [
         'Database.cpp',
         'FaviconHelpers.cpp',
         'Helpers.cpp',
         'History.cpp',
--- a/toolkit/components/places/nsINavHistoryService.idl
+++ b/toolkit/components/places/nsINavHistoryService.idl
@@ -24,77 +24,16 @@ interface nsIAsyncShutdownClient;
 
 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;
@@ -638,28 +577,16 @@ 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, 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.
-   */
-  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).
    *
    * Note that there is a difference between an empty title and a NULL title.
    * An empty string means that somebody specifically set the title to be