Bug 914854 - BrowserElementPanning causes sync reflow by quering scrollLeft/Top. r=bz, r=vingtetun, a=koi+
authorFabrice Desré <fabrice@mozilla.com>
Mon, 30 Sep 2013 10:32:02 -0700
changeset 160530 eb1b2309837f84503c899be922f0a2d2ca4f3c5d
parent 160529 772abe4f65ec0bd2c6dc2e112b438df2acf03ea5
child 160531 844587e80414f1442cd3663aa3e9db9a473b38e6
push id2961
push userlsblakk@mozilla.com
push dateMon, 28 Oct 2013 21:59:28 +0000
treeherdermozilla-beta@73ef4f13486f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbz, vingtetun, koi
bugs914854
milestone26.0a2
Bug 914854 - BrowserElementPanning causes sync reflow by quering scrollLeft/Top. r=bz, r=vingtetun, a=koi+
content/base/public/Element.h
content/base/public/mozFlushType.h
content/base/src/Element.cpp
dom/browser-element/BrowserElementPanning.js
dom/webidl/Element.webidl
--- a/content/base/public/Element.h
+++ b/content/base/public/Element.h
@@ -671,16 +671,21 @@ public:
   void SetScrollLeft(int32_t aScrollLeft)
   {
     nsIScrollableFrame* sf = GetScrollFrame();
     if (sf) {
       sf->ScrollToCSSPixels(CSSIntPoint(aScrollLeft,
                                         sf->GetScrollPositionCSSPixels().y));
     }
   }
+  /* Scrolls without flushing the layout.
+   * aDx is the x offset, aDy the y offset in CSS pixels.
+   * Returns true if we actually scrolled.
+   */
+  bool ScrollByNoFlush(int32_t aDx, int32_t aDy);
   int32_t ScrollWidth();
   int32_t ScrollHeight();
   int32_t ClientTop()
   {
     return nsPresContext::AppUnitsToIntCSSPixels(GetClientAreaRect().y);
   }
   int32_t ClientLeft()
   {
@@ -1137,17 +1142,18 @@ protected:
 
 private:
   /**
    * Get this element's client area rect in app units.
    * @return the frame's client area
    */
   nsRect GetClientAreaRect();
 
-  nsIScrollableFrame* GetScrollFrame(nsIFrame **aStyledFrame = nullptr);
+  nsIScrollableFrame* GetScrollFrame(nsIFrame **aStyledFrame = nullptr,
+                                     bool aFlushLayout = true);
 
   nsresult GetMarkup(bool aIncludeSelf, nsAString& aMarkup);
 
   // Data members
   nsEventStates mState;
 };
 
 NS_DEFINE_STATIC_IID_ACCESSOR(Element, NS_ELEMENT_IID)
--- a/content/base/public/mozFlushType.h
+++ b/content/base/public/mozFlushType.h
@@ -8,16 +8,17 @@
 /**
  * This is the enum used by nsIDocument::FlushPendingNotifications to
  * decide what to flush.
  *
  * Please note that if you change these values, you should sync it with the
  * flushTypeNames array inside PresShell::FlushPendingNotifications.
  */
 enum mozFlushType {
+  Flush_None             = 0, /* Actually don't flush anything */
   Flush_Content          = 1, /* flush the content model construction */
   Flush_ContentAndNotify = 2, /* As above, plus flush the frame model
                                  construction and other nsIMutationObserver
                                  notifications. */
   Flush_Style            = 3, /* As above, plus flush style reresolution */
   Flush_Frames           = Flush_Style,
   Flush_InterruptibleLayout = 4, /* As above, plus flush reflow,
                                     but allow it to be interrupted (so
--- a/content/base/src/Element.cpp
+++ b/content/base/src/Element.cpp
@@ -467,27 +467,31 @@ Element::GetElementsByTagName(const nsAS
 nsIFrame*
 Element::GetStyledFrame()
 {
   nsIFrame *frame = GetPrimaryFrame(Flush_Layout);
   return frame ? nsLayoutUtils::GetStyleFrame(frame) : nullptr;
 }
 
 nsIScrollableFrame*
-Element::GetScrollFrame(nsIFrame **aStyledFrame)
+Element::GetScrollFrame(nsIFrame **aStyledFrame, bool aFlushLayout)
 {
   // it isn't clear what to return for SVG nodes, so just return nothing
   if (IsSVG()) {
     if (aStyledFrame) {
       *aStyledFrame = nullptr;
     }
     return nullptr;
   }
 
-  nsIFrame* frame = GetStyledFrame();
+  // Inline version of GetStyledFrame to use Flush_None if needed.
+  nsIFrame* frame = GetPrimaryFrame(aFlushLayout ? Flush_Layout : Flush_None);
+  if (frame) {
+    frame = nsLayoutUtils::GetStyleFrame(frame);
+  }
 
   if (aStyledFrame) {
     *aStyledFrame = frame;
   }
   if (!frame) {
     return nullptr;
   }
 
@@ -535,16 +539,38 @@ Element::ScrollIntoView(bool aTop)
   presShell->ScrollContentIntoView(this,
                                    nsIPresShell::ScrollAxis(
                                      vpercent,
                                      nsIPresShell::SCROLL_ALWAYS),
                                    nsIPresShell::ScrollAxis(),
                                    nsIPresShell::SCROLL_OVERFLOW_HIDDEN);
 }
 
+bool
+Element::ScrollByNoFlush(int32_t aDx, int32_t aDy)
+{
+  nsIScrollableFrame* sf = GetScrollFrame(nullptr, false);
+  if (!sf) {
+    return false;
+  }
+
+  nsWeakFrame weakRef(sf->GetScrolledFrame());
+
+  CSSIntPoint before = sf->GetScrollPositionCSSPixels();
+  sf->ScrollToCSSPixelsApproximate(CSSIntPoint(before.x + aDx, before.y + aDy));
+
+  // The frame was destroyed, can't keep on scrolling.
+  if (!weakRef.IsAlive()) {
+    return false;
+  }
+
+  CSSIntPoint after = sf->GetScrollPositionCSSPixels();
+  return (before != after);
+}
+
 static nsSize GetScrollRectSizeForOverflowVisibleFrame(nsIFrame* aFrame)
 {
   if (!aFrame) {
     return nsSize(0,0);
   }
 
   nsRect paddingRect = aFrame->GetPaddingRectRelativeToSelf();
   nsOverflowAreas overflowAreas(paddingRect, paddingRect);
@@ -1442,17 +1468,19 @@ Element::GetPrimaryFrame(mozFlushType aT
 {
   nsIDocument* doc = GetCurrentDoc();
   if (!doc) {
     return nullptr;
   }
 
   // Cause a flush, so we get up-to-date frame
   // information
-  doc->FlushPendingNotifications(aType);
+  if (aType != Flush_None) {
+    doc->FlushPendingNotifications(aType);
+  }
 
   return GetPrimaryFrame();
 }
 
 //----------------------------------------------------------------------
 nsresult
 Element::LeaveLink(nsPresContext* aPresContext)
 {
--- a/dom/browser-element/BrowserElementPanning.js
+++ b/dom/browser-element/BrowserElementPanning.js
@@ -380,40 +380,25 @@ const ContentPanning = {
 
     if (nodeContent.frameElement) {
       return this._findPannable(nodeContent.frameElement);
     }
 
     return null;
   },
 
-  _generateCallback: function cp_generateCallback(content) {
+  _generateCallback: function cp_generateCallback(root) {
     let firstScroll = true;
     let target;
-    let isScrolling = false;
-    let oldX, oldY, newX, newY;
+    let current;
     let win, doc, htmlNode, bodyNode;
-    let xScrollable;
-    let yScrollable;
 
     function doScroll(node, delta) {
-      // recalculate scrolling direction
-      xScrollable = node.scrollWidth > node.clientWidth;
-      yScrollable = node.scrollHeight > node.clientHeight;
       if (node instanceof Ci.nsIDOMHTMLElement) {
-        newX = oldX = node.scrollLeft, newY = oldY = node.scrollTop;
-        if (xScrollable) {
-           node.scrollLeft += delta.x;
-           newX = node.scrollLeft;
-        }
-        if (yScrollable) {
-           node.scrollTop += delta.y;
-           newY = node.scrollTop;
-        }
-        return (newX != oldX || newY != oldY);
+        return node.scrollByNoFlush(delta.x, delta.y);
       } else if (node instanceof Ci.nsIDOMWindow) {
         win = node;
         doc = win.document;
 
         // "overflow:hidden" on either the <html> or the <body> node should
         // prevent the user from scrolling the root viewport.
         if (doc instanceof Ci.nsIDOMHTMLDocument) {
           htmlNode = doc.documentElement;
@@ -422,51 +407,49 @@ const ContentPanning = {
               win.getComputedStyle(bodyNode, null).overflowX == "hidden") {
             delta.x = 0;
           }
           if (win.getComputedStyle(htmlNode, null).overflowY == "hidden" ||
               win.getComputedStyle(bodyNode, null).overflowY == "hidden") {
             delta.y = 0;
           }
         }
-        oldX = node.scrollX, oldY = node.scrollY;
+        let oldX = node.scrollX;
+        let oldY = node.scrollY;
         node.scrollBy(delta.x, delta.y);
-        newX = node.scrollX, newY = node.scrollY;
-        return (newX != oldX || newY != oldY);
+        return (node.scrollX != oldX || node.scrollY != oldY);
       }
       // If we get here, |node| isn't an HTML element and it's not a window,
       // but findPannable apparently thought it was scrollable... What is it?
       return false;
-    };
+    }
 
     function targetParent(node) {
-      if (node.parentNode) {
-        return node.parentNode;
-      }
-      if (node.frameElement) {
-        return node.frameElement;
-      }
-      return null;
+      return node.parentNode || node.frameElement || null;
     }
 
     function scroll(delta) {
-      for (target = content; target;
-           target = ContentPanning._findPannable(targetParent(target))) {
-        isScrolling = doScroll(target, delta);
-        if (isScrolling || !firstScroll) {
-          break;
+      current = root;
+      while (current) {
+        if (doScroll(current, delta)) {
+          firstScroll = false;
+          return true;
         }
+
+        // TODO The current code looks for possible scrolling regions only if
+        // this is the first scroll action but this should be more dynamic.
+        if (!firstScroll) {
+          return false;
+        }
+
+        current = ContentPanning._findPannable(targetParent(current));
       }
-      if (isScrolling) {
-        if (firstScroll) {
-          content = target; // set scrolling target to the first scrolling region
-        }
-        firstScroll = false; // lockdown the scrolling target after a success scrolling
-      }
-      return isScrolling;
+
+      // There is nothing scrollable here.
+      return false;
     }
     return scroll;
   },
 
   get _domUtils() {
     delete this._domUtils;
     return this._domUtils = Cc['@mozilla.org/inspector/dom-utils;1']
                               .getService(Ci.inIDOMUtils);
--- a/dom/webidl/Element.webidl
+++ b/dom/webidl/Element.webidl
@@ -126,16 +126,23 @@ interface Element : Node {
   Attr? getAttributeNode(DOMString name);
   [Throws]
   Attr? setAttributeNode(Attr newAttr);
   [Throws]
   Attr? removeAttributeNode(Attr oldAttr);
   Attr? getAttributeNodeNS(DOMString? namespaceURI, DOMString localName);
   [Throws]
   Attr? setAttributeNodeNS(Attr newAttr);
+
+  [ChromeOnly]
+  /**
+   * Scrolls the element by (dx, dy) CSS pixels without doing any
+   * layout flushing.
+   */
+  boolean scrollByNoFlush(long dx, long dy);
 };
 
 // http://dev.w3.org/csswg/cssom-view/#extensions-to-the-element-interface
 partial interface Element {
   ClientRectList getClientRects();
   ClientRect getBoundingClientRect();
 
   // scrolling