Bug 1272409 - Part 3: Add ResizeObserverController. r=dholbert,smaug
authorFariskhi Vidyan <farislab@gmail.com>
Fri, 26 Apr 2019 20:29:16 +0000
changeset 530409 0b7728e6fcda89443d3d580fe108f74a193c3b1c
parent 530408 b634a26d0c2ed0efe2e7739b24aafcbf5820c058
child 530410 87c7a189cbe0d917f6579f796e43a8e5c29e7681
push id11265
push userffxbld-merge
push dateMon, 13 May 2019 10:53:39 +0000
treeherdermozilla-beta@77e0fe8dbdd3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdholbert, smaug
bugs1272409
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1272409 - Part 3: Add ResizeObserverController. r=dholbert,smaug Use ResizeObserverController to schedule the observers and manage them. Document will hold this controller in the later patch. Depends on D27616 Differential Revision: https://phabricator.services.mozilla.com/D27617
dom/base/ResizeObserverController.cpp
dom/base/ResizeObserverController.h
dom/base/moz.build
new file mode 100644
--- /dev/null
+++ b/dom/base/ResizeObserverController.cpp
@@ -0,0 +1,226 @@
+/* -*- 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/ResizeObserverController.h"
+
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/ErrorEvent.h"
+#include "mozilla/PresShell.h"
+#include "nsPresContext.h"
+#include "nsRefreshDriver.h"
+#include <limits>
+
+namespace mozilla {
+namespace dom {
+
+void ResizeObserverNotificationHelper::WillRefresh(TimeStamp aTime) {
+  MOZ_ASSERT(mOwner,
+             "Why mOwner already dead when this RefreshObserver still "
+             "registered?");
+  mOwner->Notify();
+}
+
+nsRefreshDriver* ResizeObserverNotificationHelper::GetRefreshDriver() const {
+  PresShell* presShell = mOwner->GetPresShell();
+  if (MOZ_UNLIKELY(!presShell)) {
+    return nullptr;
+  }
+
+  nsPresContext* presContext = presShell->GetPresContext();
+  if (MOZ_UNLIKELY(!presContext)) {
+    return nullptr;
+  }
+
+  return presContext->RefreshDriver();
+}
+
+void ResizeObserverNotificationHelper::Register() {
+  if (mRegistered) {
+    return;
+  }
+
+  nsRefreshDriver* refreshDriver = GetRefreshDriver();
+  if (!refreshDriver) {
+    // We maybe navigating away from this page or currently in an iframe with
+    // display: none. Just abort the Register(), no need to do notification.
+    return;
+  }
+
+  refreshDriver->AddRefreshObserver(this, FlushType::Display);
+  mRegistered = true;
+}
+
+void ResizeObserverNotificationHelper::Unregister() {
+  if (!mRegistered) {
+    return;
+  }
+
+  nsRefreshDriver* refreshDriver = GetRefreshDriver();
+  if (!refreshDriver) {
+    // We can't access RefreshDriver now. Just abort the Unregister().
+    return;
+  }
+
+  DebugOnly<bool> rv =
+      refreshDriver->RemoveRefreshObserver(this, FlushType::Display);
+  MOZ_ASSERT(rv, "Should remove the observer successfully");
+
+  mRegistered = false;
+}
+
+ResizeObserverNotificationHelper::~ResizeObserverNotificationHelper() {
+  Unregister();
+}
+
+void ResizeObserverController::Traverse(
+    nsCycleCollectionTraversalCallback& aCb) {
+  ImplCycleCollectionTraverse(aCb, mResizeObservers, "mResizeObservers");
+}
+
+void ResizeObserverController::Unlink() { mResizeObservers.Clear(); }
+
+void ResizeObserverController::AddResizeObserver(ResizeObserver* aObserver) {
+  MOZ_ASSERT(aObserver,
+             "AddResizeObserver() should never be called with a null "
+             "parameter");
+  mResizeObservers.AppendElement(aObserver);
+}
+
+void ResizeObserverController::Notify() {
+  if (mResizeObservers.IsEmpty()) {
+    return;
+  }
+
+  // We may call BroadcastAllActiveObservations(), which might cause mDocument
+  // to be destroyed (and the ResizeObserverController with it).
+  // e.g. if mDocument is in an iframe, and the observer JS removes it from the
+  // parent document and we trigger an unlucky GC/CC (or maybe if the observer
+  // JS navigates to a different URL). Or the author uses elem.offsetTop API,
+  // which could flush style + layout and make the document destroyed if we're
+  // inside an iframe that has suddenly become |display:none| via the author
+  // doing something in their ResizeObserver callback.
+  //
+  // Therefore, we ref-count mDocument here to make sure it and its members
+  // (e.g. mResizeObserverController, which is `this` pointer) are still alive
+  // in the rest of this function because after it goes up, `this` is possible
+  // deleted.
+  RefPtr<Document> doc(mDocument);
+
+  uint32_t shallowestTargetDepth = 0;
+
+  GatherAllActiveObservations(shallowestTargetDepth);
+
+  while (HasAnyActiveObservations()) {
+    DebugOnly<uint32_t> oldShallowestTargetDepth = shallowestTargetDepth;
+    shallowestTargetDepth = BroadcastAllActiveObservations();
+    NS_ASSERTION(oldShallowestTargetDepth < shallowestTargetDepth,
+                 "shallowestTargetDepth should be getting strictly deeper");
+
+    // Flush layout, so that any callback functions' style changes / resizes
+    // get a chance to take effect.
+    doc->FlushPendingNotifications(FlushType::Layout);
+
+    // To avoid infinite resize loop, we only gather all active observations
+    // that have the depth of observed target element more than current
+    // shallowestTargetDepth.
+    GatherAllActiveObservations(shallowestTargetDepth);
+  }
+
+  mResizeObserverNotificationHelper->Unregister();
+
+  if (HasAnySkippedObservations()) {
+    // Per spec, we deliver an error if the document has any skipped
+    // observations. Also, we re-register via ScheduleNotification().
+    RootedDictionary<ErrorEventInit> init(RootingCx());
+
+    init.mMessage.AssignLiteral(
+        "ResizeObserver loop completed with undelivered notifications.");
+    init.mBubbles = false;
+    init.mCancelable = false;
+
+    nsEventStatus status = nsEventStatus_eIgnore;
+
+    nsCOMPtr<nsPIDOMWindowInner> window =
+        doc->GetWindow()->GetCurrentInnerWindow();
+
+    if (window) {
+      nsCOMPtr<nsIScriptGlobalObject> sgo = do_QueryInterface(window);
+      MOZ_ASSERT(sgo);
+
+      if (NS_WARN_IF(sgo->HandleScriptError(init, &status))) {
+        status = nsEventStatus_eIgnore;
+      }
+    } else {
+      // We don't fire error events at any global for non-window JS on the main
+      // thread.
+    }
+
+    // We need to deliver pending notifications in next cycle.
+    ScheduleNotification();
+  }
+}
+
+void ResizeObserverController::GatherAllActiveObservations(uint32_t aDepth) {
+  nsTObserverArray<RefPtr<ResizeObserver>>::ForwardIterator iter(
+      mResizeObservers);
+  while (iter.HasMore()) {
+    iter.GetNext()->GatherActiveObservations(aDepth);
+  }
+}
+
+uint32_t ResizeObserverController::BroadcastAllActiveObservations() {
+  uint32_t shallowestTargetDepth = std::numeric_limits<uint32_t>::max();
+
+  // Use EndLimitedIterator, so we handle the new-added observers (from the JS
+  // callback) in the next iteration of the while loop.
+  // Note: the while loop is in ResizeObserverController::Notify()).
+  nsTObserverArray<RefPtr<ResizeObserver>>::EndLimitedIterator iter(
+      mResizeObservers);
+  while (iter.HasMore()) {
+    RefPtr<ResizeObserver>& observer = iter.GetNext();
+
+    uint32_t targetDepth = observer->BroadcastActiveObservations();
+
+    if (targetDepth < shallowestTargetDepth) {
+      shallowestTargetDepth = targetDepth;
+    }
+  }
+
+  return shallowestTargetDepth;
+}
+
+bool ResizeObserverController::HasAnyActiveObservations() const {
+  nsTObserverArray<RefPtr<ResizeObserver>>::ForwardIterator iter(
+      mResizeObservers);
+  while (iter.HasMore()) {
+    if (iter.GetNext()->HasActiveObservations()) {
+      return true;
+    }
+  }
+  return false;
+}
+
+bool ResizeObserverController::HasAnySkippedObservations() const {
+  nsTObserverArray<RefPtr<ResizeObserver>>::ForwardIterator iter(
+      mResizeObservers);
+  while (iter.HasMore()) {
+    if (iter.GetNext()->HasSkippedObservations()) {
+      return true;
+    }
+  }
+  return false;
+}
+
+void ResizeObserverController::ScheduleNotification() {
+  mResizeObserverNotificationHelper->Register();
+}
+
+ResizeObserverController::~ResizeObserverController() {
+  mResizeObserverNotificationHelper->Unregister();
+}
+
+}  // namespace dom
+}  // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/base/ResizeObserverController.h
@@ -0,0 +1,125 @@
+/* -*- 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_ResizeObserverController_h
+#define mozilla_dom_ResizeObserverController_h
+
+#include "mozilla/dom/ResizeObserver.h"
+#include "mozilla/TimeStamp.h"
+#include "nsRefreshDriver.h"
+#include "nsTObserverArray.h"
+
+namespace mozilla {
+
+class PresShell;
+
+namespace dom {
+
+class Document;
+class ResizeObserverController;
+
+/**
+ * ResizeObserverNotificationHelper will trigger ResizeObserver notifications
+ * by registering with the Refresh Driver.
+ */
+class ResizeObserverNotificationHelper final : public nsARefreshObserver {
+ public:
+  NS_INLINE_DECL_REFCOUNTING(ResizeObserverNotificationHelper, override)
+
+  explicit ResizeObserverNotificationHelper(ResizeObserverController* aOwner)
+      : mOwner(aOwner), mRegistered(false) {
+    MOZ_ASSERT(mOwner, "Need a non-null owner");
+  }
+
+  MOZ_CAN_RUN_SCRIPT void WillRefresh(TimeStamp aTime) override;
+
+  nsRefreshDriver* GetRefreshDriver() const;
+
+  void Register();
+
+  void Unregister();
+
+ private:
+  virtual ~ResizeObserverNotificationHelper();
+
+  ResizeObserverController* const mOwner;
+  bool mRegistered;
+};
+
+/**
+ * ResizeObserverController contains the list of ResizeObservers and controls
+ * the flow of notification.
+ */
+class ResizeObserverController final {
+ public:
+  explicit ResizeObserverController(Document* aDocument)
+      : mDocument(aDocument),
+        mResizeObserverNotificationHelper(
+            new ResizeObserverNotificationHelper(this)) {
+    MOZ_ASSERT(mDocument, "Need a non-null document");
+  }
+
+  // Methods for supporting cycle-collection
+  void Traverse(nsCycleCollectionTraversalCallback& aCb);
+  void Unlink();
+
+  void AddResizeObserver(ResizeObserver* aObserver);
+
+  /**
+   * Schedule the notification via ResizeObserverNotificationHelper refresh
+   * observer.
+   */
+  void ScheduleNotification();
+
+  /**
+   * Notify all ResizeObservers by gathering and broadcasting all active
+   * observations.
+   */
+  MOZ_CAN_RUN_SCRIPT void Notify();
+
+  PresShell* GetPresShell() const { return mDocument->GetPresShell(); }
+
+  ~ResizeObserverController();
+
+ private:
+  /**
+   * Calls GatherActiveObservations(aDepth) for all ResizeObservers in this
+   * controller. All observations in each ResizeObserver with element's depth
+   * more than aDepth will be gathered.
+   */
+  void GatherAllActiveObservations(uint32_t aDepth);
+
+  /**
+   * Calls BroadcastActiveObservations() for all ResizeObservers in this
+   * controller. It also returns the shallowest depth of observed target
+   * elements with active observations from all ResizeObservers or
+   * numeric_limits<uint32_t>::max() if there aren't any active observations
+   * at all.
+   */
+  MOZ_CAN_RUN_SCRIPT uint32_t BroadcastAllActiveObservations();
+
+  /**
+   * Returns whether there is any ResizeObserver that has active observations.
+   */
+  bool HasAnyActiveObservations() const;
+
+  /**
+   * Returns whether there is any ResizeObserver that has skipped observations.
+   */
+  bool HasAnySkippedObservations() const;
+
+  // Raw pointer is OK because mDocument strongly owns us & hence must outlive
+  // us.
+  Document* const mDocument;
+
+  RefPtr<ResizeObserverNotificationHelper> mResizeObserverNotificationHelper;
+  nsTObserverArray<RefPtr<ResizeObserver>> mResizeObservers;
+};
+
+}  // namespace dom
+}  // namespace mozilla
+
+#endif  // mozilla_dom_ResizeObserverController_h
--- a/dom/base/moz.build
+++ b/dom/base/moz.build
@@ -212,16 +212,17 @@ EXPORTS.mozilla.dom += [
     'PlacesObservers.h',
     'PlacesVisit.h',
     'PlacesWeakCallbackWrapper.h',
     'PopupBlocker.h',
     'Pose.h',
     'PostMessageEvent.h',
     'ProcessMessageManager.h',
     'ResizeObserver.h',
+    'ResizeObserverController.h',
     'ResponsiveImageSelector.h',
     'SameProcessMessageQueue.h',
     'ScreenLuminance.h',
     'ScreenOrientation.h',
     'Selection.h',
     'ShadowIncludingTreeIterator.h',
     'ShadowRoot.h',
     'StructuredCloneBlob.h',
@@ -378,16 +379,17 @@ UNIFIED_SOURCES += [
     'nsXMLContentSerializer.cpp',
     'ParentProcessMessageManager.cpp',
     'PopupBlocker.cpp',
     'Pose.cpp',
     'PostMessageEvent.cpp',
     'ProcessMessageManager.cpp',
     'RemoteOuterWindowProxy.cpp',
     'ResizeObserver.cpp',
+    'ResizeObserverController.cpp',
     'ResponsiveImageSelector.cpp',
     'SameProcessMessageQueue.cpp',
     'ScreenLuminance.cpp',
     'ScreenOrientation.cpp',
     'ScriptableContentIterator.cpp',
     'Selection.cpp',
     'SelectionChangeEventDispatcher.cpp',
     'ShadowRoot.cpp',