Bug 569520 part 2. Implement a mozRequestAnimationFrame/mozAnimationStartTime API. r=roc, a=joe
☠☠ backed out by dc7f9edb863b ☠ ☠
authorBoris Zbarsky <bzbarsky@mit.edu>
Wed, 11 Aug 2010 17:05:28 -0400
changeset 49574 66c25030a8d47a6a6a8726b8c5732db82db621f7
parent 49573 464f978cfca50ddf05085fd0f0e614b2b43d6c08
child 49575 e96ec455618fda4fd6386897ec8c6bd22900bf0e
child 49577 dc7f9edb863bfc98d1ebcde23662735410076f1e
push idunknown
push userunknown
push dateunknown
reviewersroc, joe
bugs569520
milestone2.0b4pre
Bug 569520 part 2. Implement a mozRequestAnimationFrame/mozAnimationStartTime API. r=roc, a=joe
content/base/public/nsIDocument.h
content/base/src/nsDocument.cpp
dom/base/nsGlobalWindow.cpp
dom/interfaces/base/nsIDOMWindowInternal.idl
layout/base/nsIPresShell.h
layout/base/nsRefreshDriver.cpp
layout/base/nsRefreshDriver.h
layout/base/tests/Makefile.in
layout/base/tests/test_bug569520.html
--- a/content/base/public/nsIDocument.h
+++ b/content/base/public/nsIDocument.h
@@ -1403,16 +1403,22 @@ public:
   /**
    * This method is similar to GetElementById() from nsIDOMDocument but it
    * returns a mozilla::dom::Element instead of a nsIDOMElement.
    * It prevents converting nsIDOMElement to mozill:dom::Element which is
    * already converted from mozilla::dom::Element.
    */
   virtual mozilla::dom::Element* GetElementById(const nsAString& aElementId) = 0;
 
+  void ScheduleBeforePaintEvent();
+  void BeforePaintEventFiring()
+  {
+    mHavePendingPaint = PR_FALSE;
+  }
+
 protected:
   ~nsIDocument()
   {
     // XXX The cleanup of mNodeInfoManager (calling DropDocumentReference and
     //     releasing it) happens in the nsDocument destructor. We'd prefer to
     //     do it here but nsNodeInfoManager is a concrete class that we don't
     //     want to expose to users of the nsIDocument API outside of Gecko.
   }
@@ -1542,16 +1548,19 @@ protected:
   PRPackedBool mIsStaticDocument;
 
   // True while this document is being cloned to a static document.
   PRPackedBool mCreatingStaticClone;
 
   // True if document has ever had script handling object.
   PRPackedBool mHasHadScriptHandlingObject;
 
+  // True if we're waiting for a before-paint event.
+  PRPackedBool mHavePendingPaint;
+
   // The document's script global object, the object from which the
   // document can get its script context and scope. This is the
   // *inner* window object.
   nsCOMPtr<nsIScriptGlobalObject> mScriptGlobalObject;
 
   // If mIsStaticDocument is true, mOriginalDocument points to the original
   // document.
   nsCOMPtr<nsIDocument> mOriginalDocument;
--- a/content/base/src/nsDocument.cpp
+++ b/content/base/src/nsDocument.cpp
@@ -3069,25 +3069,33 @@ nsDocument::doCreateShell(nsPresContext*
   rv = shell->Init(this, aContext, aViewManager, aStyleSet, aCompatMode);
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Note: we don't hold a ref to the shell (it holds a ref to us)
   mPresShell = shell;
 
   mExternalResourceMap.ShowViewers();
 
+  if (mHavePendingPaint) {
+    mPresShell->GetPresContext()->RefreshDriver()->
+      ScheduleBeforePaintEvent(this);
+  }
+
   shell.swap(*aInstancePtrResult);
 
   return NS_OK;
 }
 
 void
 nsDocument::DeleteShell()
 {
   mExternalResourceMap.HideViewers();
+  if (mHavePendingPaint) {
+    mPresShell->GetPresContext()->RefreshDriver()->RevokeBeforePaintEvent(this);
+  }
   mPresShell = nsnull;
 }
 
 static void
 SubDocClearEntry(PLDHashTable *table, PLDHashEntryHdr *entry)
 {
   SubDocMapEntry *e = static_cast<SubDocMapEntry *>(entry);
 
@@ -7852,8 +7860,22 @@ nsIDocument::CreateStaticClone(nsISuppor
           }
         }
       }
     }
   }
   mCreatingStaticClone = PR_FALSE;
   return clonedDoc.forget();
 }
+
+void
+nsIDocument::ScheduleBeforePaintEvent()
+{
+  if (!mHavePendingPaint) {
+    // We don't want to use GetShell() here, because we want to schedule the
+    // paint even if we're frozen.  Either we'll get unfrozen and then the
+    // event will fire, or we'll quietly go away at some point.
+    mHavePendingPaint =
+      !mPresShell ||
+      mPresShell->GetPresContext()->RefreshDriver()->
+        ScheduleBeforePaintEvent(this);
+  }
+}
--- a/dom/base/nsGlobalWindow.cpp
+++ b/dom/base/nsGlobalWindow.cpp
@@ -3505,16 +3505,48 @@ nsGlobalWindow::GetMozPaintCount(PRUint6
   if (!presShell)
     return NS_OK;
 
   *aResult = presShell->GetPaintCount();
   return NS_OK;
 }
 
 NS_IMETHODIMP
+nsGlobalWindow::MozRequestAnimationFrame()
+{
+  FORWARD_TO_INNER(MozRequestAnimationFrame, (), NS_ERROR_NOT_INITIALIZED);
+
+  if (!mDoc) {
+    return NS_OK;
+  }
+
+  mDoc->ScheduleBeforePaintEvent();
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsGlobalWindow::GetMozAnimationStartTime(PRInt64 *aTime)
+{
+  FORWARD_TO_INNER(GetMozAnimationStartTime, (aTime), NS_ERROR_NOT_INITIALIZED);
+
+  if (mDoc) {
+    nsIPresShell* presShell = mDoc->GetShell();
+    if (presShell) {
+      *aTime = presShell->GetPresContext()->RefreshDriver()->
+        MostRecentRefreshEpochTime() / PR_USEC_PER_MSEC;
+      return NS_OK;
+    }
+  }
+
+  // If all else fails, just be compatible with Date.now()
+  *aTime = JS_Now() / PR_USEC_PER_MSEC;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 nsGlobalWindow::SetScreenX(PRInt32 aScreenX)
 {
   FORWARD_TO_OUTER(SetScreenX, (aScreenX), NS_ERROR_NOT_INITIALIZED);
 
   /*
    * If caller is not chrome and the user has not explicitly exempted the site,
    * prevent setting window.screenX by exiting early
    */
--- a/dom/interfaces/base/nsIDOMWindowInternal.idl
+++ b/dom/interfaces/base/nsIDOMWindowInternal.idl
@@ -215,9 +215,19 @@ interface nsIDOMWindowInternal : nsIDOMW
   [binaryname(PostMessageMoz)] void postMessage(in DOMString message,
                                                 in DOMString targetOrigin);
 
   /**
    * Returns the number of times this document for this window has
    * been painted to the screen.
    */
   readonly attribute unsigned long long mozPaintCount;
+
+  /**
+   * Request a refresh of this browser window.
+   */
+  void mozRequestAnimationFrame();
+
+  /**
+   * The current animation start time in milliseconds since the epoch.
+   */
+  readonly attribute long long mozAnimationStartTime;
 };
--- a/layout/base/nsIPresShell.h
+++ b/layout/base/nsIPresShell.h
@@ -61,17 +61,16 @@
 #include "nsCoord.h"
 #include "nsColor.h"
 #include "nsEvent.h"
 #include "nsCompatibility.h"
 #include "nsFrameManagerBase.h"
 #include "mozFlushType.h"
 #include "nsWeakReference.h"
 #include <stdio.h> // for FILE definition
-#include "nsRefreshDriver.h"
 #include "nsChangeHint.h"
 
 class nsIContent;
 class nsIDocument;
 class nsIFrame;
 class nsPresContext;
 class nsStyleSet;
 class nsIViewManager;
@@ -97,16 +96,18 @@ class gfxContext;
 class nsIDOMEvent;
 class nsDisplayList;
 class nsDisplayListBuilder;
 class nsPIDOMWindow;
 struct nsPoint;
 struct nsIntPoint;
 struct nsRect;
 struct nsIntRect;
+class nsRefreshDriver;
+class nsARefreshObserver;
 
 typedef short SelectionType;
 typedef PRUint64 nsFrameState;
 
 namespace mozilla {
 namespace dom {
 class Element;
 } // namespace dom
--- a/layout/base/nsRefreshDriver.cpp
+++ b/layout/base/nsRefreshDriver.cpp
@@ -42,16 +42,20 @@
  */
 
 #include "nsRefreshDriver.h"
 #include "nsPresContext.h"
 #include "nsComponentManagerUtils.h"
 #include "prlog.h"
 #include "nsAutoPtr.h"
 #include "nsCSSFrameConstructor.h"
+#include "nsIDocument.h"
+#include "nsGUIEvent.h"
+#include "nsEventDispatcher.h"
+#include "jsapi.h"
 
 /*
  * TODO:
  * Once this is hooked in to suppressing updates when the presentation
  * is not visible, we need to hook it up to FlushPendingNotifications so
  * that we flush when necessary.
  */
 
@@ -75,16 +79,24 @@ nsRefreshDriver::~nsRefreshDriver()
 TimeStamp
 nsRefreshDriver::MostRecentRefresh() const
 {
   const_cast<nsRefreshDriver*>(this)->EnsureTimerStarted();
 
   return mMostRecentRefresh;
 }
 
+PRInt64
+nsRefreshDriver::MostRecentRefreshEpochTime() const
+{
+  const_cast<nsRefreshDriver*>(this)->EnsureTimerStarted();
+
+  return mMostRecentRefreshEpochTime;
+}
+
 PRBool
 nsRefreshDriver::AddRefreshObserver(nsARefreshObserver *aObserver,
                                     mozFlushType aFlushType)
 {
   ObserverArray& array = ArrayFor(aFlushType);
   PRBool success = array.AppendElement(aObserver) != nsnull;
 
   EnsureTimerStarted();
@@ -138,22 +150,25 @@ PRUint32
 nsRefreshDriver::ObserverCount() const
 {
   PRUint32 sum = 0;
   for (PRUint32 i = 0; i < NS_ARRAY_LENGTH(mObservers); ++i) {
     sum += mObservers[i].Length();
   }
   sum += mStyleFlushObservers.Length();
   sum += mLayoutFlushObservers.Length();
+  sum += mBeforePaintTargets.Length();
   return sum;
 }
 
 void
 nsRefreshDriver::UpdateMostRecentRefresh()
 {
+  // Call JS_Now first, since that can have nonzero latency in some rare cases.
+  mMostRecentRefreshEpochTime = JS_Now();
   mMostRecentRefresh = TimeStamp::Now();
 }
 
 nsRefreshDriver::ObserverArray&
 nsRefreshDriver::ArrayFor(mozFlushType aFlushType)
 {
   switch (aFlushType) {
     case Flush_Style:
@@ -212,16 +227,31 @@ nsRefreshDriver::Notify(nsITimer * /* un
       obs->WillRefresh(mMostRecentRefresh);
       
       if (!mPresContext || !mPresContext->GetPresShell()) {
         StopTimer();
         return NS_OK;
       }
     }
     if (i == 0) {
+      // Don't just loop while we have things in mBeforePaintTargets,
+      // the whole point is that event handlers should readd the
+      // target as needed.
+      nsTArray<nsIDocument*> targets;
+      targets.SwapElements(mBeforePaintTargets);
+      PRInt64 eventTime = mMostRecentRefreshEpochTime / PR_USEC_PER_MSEC;
+      for (PRUint32 i = 0; i < targets.Length(); ++i) {
+        targets[i]->BeforePaintEventFiring();
+      }
+      for (PRUint32 i = 0; i < targets.Length(); ++i) {
+        nsEvent ev(PR_TRUE, NS_BEFOREPAINT);
+        ev.time = eventTime;
+        nsEventDispatcher::Dispatch(targets[i], nsnull, &ev);
+      }
+
       // This is the Flush_Style case.
       while (!mStyleFlushObservers.IsEmpty() &&
              mPresContext && mPresContext->GetPresShell()) {
         PRUint32 idx = mStyleFlushObservers.Length() - 1;
         nsCOMPtr<nsIPresShell> shell = mStyleFlushObservers[idx];
         mStyleFlushObservers.RemoveElementAt(idx);
         shell->FrameConstructor()->mObservingRefreshDriver = PR_FALSE;
         shell->FlushPendingNotifications(Flush_Style);
@@ -275,8 +305,25 @@ nsRefreshDriver::DoRefresh()
 PRBool
 nsRefreshDriver::IsRefreshObserver(nsARefreshObserver *aObserver,
                                    mozFlushType aFlushType)
 {
   ObserverArray& array = ArrayFor(aFlushType);
   return array.Contains(aObserver);
 }
 #endif
+
+PRBool
+nsRefreshDriver::ScheduleBeforePaintEvent(nsIDocument* aDocument)
+{
+  NS_ASSERTION(mBeforePaintTargets.IndexOf(aDocument) ==
+               mBeforePaintTargets.NoIndex,
+               "Shouldn't have a paint event posted for this document");
+  PRBool appended = mBeforePaintTargets.AppendElement(aDocument) != nsnull;
+  EnsureTimerStarted();
+  return appended;
+}
+
+void
+nsRefreshDriver::RevokeBeforePaintEvent(nsIDocument* aDocument)
+{
+  mBeforePaintTargets.RemoveElement(aDocument);
+}
--- a/layout/base/nsRefreshDriver.h
+++ b/layout/base/nsRefreshDriver.h
@@ -44,19 +44,21 @@
 #define nsRefreshDriver_h_
 
 #include "mozilla/TimeStamp.h"
 #include "mozFlushType.h"
 #include "nsITimer.h"
 #include "nsCOMPtr.h"
 #include "nsTObserverArray.h"
 #include "nsTArray.h"
+#include "nsAutoPtr.h"
 
 class nsPresContext;
 class nsIPresShell;
+class nsIDocument;
 
 /**
  * An abstract base class to be implemented by callers wanting to be
  * notified at refresh times.  When nothing needs to be painted, callers
  * may not be notified.
  */
 class nsARefreshObserver {
 public:
@@ -86,16 +88,20 @@ public:
   /**
    * Return the time of the most recent refresh.  This is intended to be
    * used by callers who want to start an animation now and want to know
    * what time to consider the start of the animation.  (This helps
    * ensure that multiple animations started during the same event off
    * the main event loop have the same start time.)
    */
   mozilla::TimeStamp MostRecentRefresh() const;
+  /**
+   * Same thing, but in microseconds since the epoch.
+   */
+  PRInt64 MostRecentRefreshEpochTime() const;
 
   /**
    * Add / remove refresh observers.  Returns whether the operation
    * succeeded.
    *
    * The flush type affects:
    *   + the order in which the observers are notified (lowest flush
    *     type to highest, in order registered)
@@ -136,16 +142,26 @@ public:
   void RemoveLayoutFlushObserver(nsIPresShell* aShell) {
     mLayoutFlushObservers.RemoveElement(aShell);
   }
   PRBool IsLayoutFlushObserver(nsIPresShell* aShell) {
     return mLayoutFlushObservers.Contains(aShell);
   }
 
   /**
+   * Add a document for which we should fire a MozBeforePaint event.
+   */
+  PRBool ScheduleBeforePaintEvent(nsIDocument* aDocument);
+
+  /**
+   * Remove a document for which we should fire a MozBeforePaint event.
+   */
+  void RevokeBeforePaintEvent(nsIDocument* aDocument);
+
+  /**
    * Tell the refresh driver that it is done driving refreshes and
    * should stop its timer and forget about its pres context.  This may
    * be called from within a refresh.
    */
   void Disconnect() {
     StopTimer();
     mPresContext = nsnull;
   }
@@ -183,21 +199,25 @@ private:
   PRUint32 ObserverCount() const;
   void UpdateMostRecentRefresh();
   ObserverArray& ArrayFor(mozFlushType aFlushType);
   // Trigger a refresh immediately, if haven't been disconnected or frozen.
   void DoRefresh();
 
   nsCOMPtr<nsITimer> mTimer;
   mozilla::TimeStamp mMostRecentRefresh; // only valid when mTimer non-null
+  PRInt64 mMostRecentRefreshEpochTime;   // same thing as mMostRecentRefresh,
+                                         // but in microseconds since the epoch.
 
   nsPresContext *mPresContext; // weak; pres context passed in constructor
                                // and unset in Disconnect
 
   PRBool mFrozen;
 
   // separate arrays for each flush type we support
   ObserverArray mObservers[3];
   nsAutoTArray<nsIPresShell*, 16> mStyleFlushObservers;
   nsAutoTArray<nsIPresShell*, 16> mLayoutFlushObservers;
+  // nsTArray on purpose, because we want to be able to swap.
+  nsTArray<nsIDocument*> mBeforePaintTargets;
 };
 
 #endif /* !defined(nsRefreshDriver_h_) */
--- a/layout/base/tests/Makefile.in
+++ b/layout/base/tests/Makefile.in
@@ -191,16 +191,17 @@ ifeq (,$(filter windows,$(MOZ_WIDGET_TOO
 		test_bug467672-5c.html \
 		test_bug467672-5d.html \
 		test_bug467672-5e.html \
 		test_bug467672-5f.html \
 		test_bug467672-5g.html \
 		bug467672-5.html \
 		bug467672-5-ref.html \
 		test_bug499538-1.html \
+		test_bug569520.html \
 		test_bug570378-arabic-1a.html \
 		test_bug570378-arabic-1b.html \
 		test_bug570378-arabic-1c.html \
 		test_bug570378-arabic-1d.html \
 		test_bug570378-arabic-1e.html \
 		bug570378-arabic-1.html \
 		bug570378-arabic-1-ref.html \
 		test_bug570378-arabic-2a.html \
new file mode 100644
--- /dev/null
+++ b/layout/base/tests/test_bug569520.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=569520
+-->
+<head>
+  <title>Test for Bug 569520</title>
+  <script type="application/javascript" src="/MochiKit/packed.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=569520">Mozilla Bug 569520</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+  
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 569520 **/
+SimpleTest.waitForExplicitFinish();
+
+var startNow = Date.now();
+var start = window.mozAnimationStartTime;
+var firstListenerTime;
+var secondListenerTime;
+
+function secondListener(ev) {
+  secondListenerTime = ev.timeStamp;
+  window.removeEventListener("MozBeforePaint", secondListener, false);
+
+  // They really shouldn't be more than 100ms apart, but we can get weird
+  // effects on slow machines.  5 minutes is our test timeout, though.
+  ok(Math.abs(startNow - start) <= 5 * 60 * 1000, "Bogus animation start time");
+  ok(firstListenerTime >= start, "First listener should fire after start");
+  ok(secondListenerTime > firstListenerTime,
+     "Second listener should fire after first listener");
+  SimpleTest.finish();
+}
+                            
+function firstListener(ev) {
+  firstListenerTime = ev.timeStamp;
+  window.removeEventListener("MozBeforePaint", firstListener, false);
+  window.addEventListener("MozBeforePaint", secondListener, false);
+  mozRequestAnimationFrame();
+}
+                            
+addLoadEvent(function() {
+    setTimeout(function() {
+      window.addEventListener("MozBeforePaint", firstListener, false);
+      mozRequestAnimationFrame();
+    }, 100);
+  });
+
+
+
+
+</script>
+</pre>
+</body>
+</html>