Bug 569520 part 2. Implement a mozRequestAnimationFrame/mozAnimationStartTime API. r=roc, a=joe
authorBoris Zbarsky <bzbarsky@mit.edu>
Wed, 11 Aug 2010 17:05:28 -0400
changeset 50368 96de52e4a44203a33165281dbbdef1063f50a2d8
parent 50367 bf2386d0cc1b9de8a779e94a938ebb11a896d909
child 50369 edbc3ebfd63c5ffaafbd5f39099d8ce54451ec7f
push id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersroc, joe
bugs569520
milestone2.0b4pre
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 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>