Bug 1198381 - Implement the requestIdleCallback feature, r?smaug,froydnj,mattwoodrow draft
authorAndreas Farre <farre@mozilla.com>
Mon, 22 Aug 2016 14:52:45 +0200
changeset 430889 bf557503f0d0c24adda9af71e9158f9fad122437
parent 430888 09da0f9939035c1488c0ac70317723063c75c28c
child 430890 e5bf2cd47f54e5b6f647a9d1cacf7389776580ab
push id33921
push userbmo:afarre@mozilla.com
push dateFri, 28 Oct 2016 07:54:08 +0000
reviewerssmaug, froydnj, mattwoodrow
bugs1198381
milestone52.0a1
Bug 1198381 - Implement the requestIdleCallback feature, r?smaug,froydnj,mattwoodrow Expose requestIdleCallback on Window and implement running callbacks in idle periods by posting rICs to the main threads idle queue. MozReview-Commit-ID: KSYQsyaZ6is
dom/base/IdleDeadline.cpp
dom/base/IdleDeadline.h
dom/base/IdleRequest.cpp
dom/base/IdleRequest.h
dom/base/moz.build
dom/base/nsGlobalWindow.cpp
dom/base/nsGlobalWindow.h
dom/bindings/Bindings.conf
dom/ipc/ContentChild.cpp
dom/webidl/IdleDeadline.webidl
dom/webidl/Window.webidl
dom/webidl/moz.build
layout/base/nsLayoutUtils.cpp
layout/base/nsLayoutUtils.h
layout/base/nsRefreshDriver.cpp
layout/base/nsRefreshDriver.h
modules/libpref/init/all.js
xpcom/glue/nsThreadUtils.cpp
xpcom/glue/nsThreadUtils.h
xpcom/threads/nsThread.h
new file mode 100644
--- /dev/null
+++ b/dom/base/IdleDeadline.cpp
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 <algorithm>
+
+#include "mozilla/dom/IdleDeadline.h"
+#include "mozilla/dom/IdleDeadlineBinding.h"
+#include "mozilla/dom/Performance.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsDOMNavigationTiming.h"
+#include "nsPIDOMWindow.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(IdleDeadline, mWindow)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(IdleDeadline)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(IdleDeadline)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(IdleDeadline)
+  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+  NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+IdleDeadline::IdleDeadline(nsPIDOMWindowInner* aWindow, bool aDidTimeout,
+                           DOMHighResTimeStamp aDeadline)
+  : mWindow(aWindow)
+  , mDidTimeout(aDidTimeout)
+  , mDeadline(aDeadline)
+{
+}
+
+IdleDeadline::~IdleDeadline()
+{
+}
+
+JSObject*
+IdleDeadline::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
+{
+  return IdleDeadlineBinding::Wrap(aCx, this, aGivenProto);
+}
+
+DOMHighResTimeStamp
+IdleDeadline::TimeRemaining()
+{
+  if (mDidTimeout) {
+    return 0.0;
+  }
+
+  RefPtr<Performance> performance = mWindow->GetPerformance();
+  if (!performance) {
+    // If there is no performance object the window is partially torn
+    // down, so we can safely say that there is no time remaining.
+    return 0.0;
+  }
+
+  return std::max(mDeadline - performance->Now(), 0.0);
+}
+
+bool
+IdleDeadline::DidTimeout() const
+{
+  return mDidTimeout;
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/base/IdleDeadline.h
@@ -0,0 +1,55 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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_IdleDeadline_h
+#define mozilla_dom_IdleDeadline_h
+
+#include "js/TypeDecls.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsDOMNavigationTiming.h"
+#include "nsWrapperCache.h"
+
+class nsPIDOMWindowInner;
+
+namespace mozilla {
+namespace dom {
+
+class IdleDeadline final
+  : public nsISupports
+  , public nsWrapperCache
+{
+public:
+  IdleDeadline(nsPIDOMWindowInner* aWindow, bool aDidTimeout,
+               DOMHighResTimeStamp aDeadline);
+
+  nsPIDOMWindowInner* GetParentObject() const { return mWindow; }
+
+  virtual JSObject* WrapObject(JSContext* aCx,
+                               JS::Handle<JSObject*> aGivenProto) override;
+
+  DOMHighResTimeStamp TimeRemaining();
+  bool DidTimeout() const;
+
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(IdleDeadline)
+
+private:
+  ~IdleDeadline();
+
+  nsCOMPtr<nsPIDOMWindowInner> mWindow;
+  const bool mDidTimeout;
+  const DOMHighResTimeStamp mDeadline;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_IdleDeadline_h
new file mode 100644
--- /dev/null
+++ b/dom/base/IdleRequest.cpp
@@ -0,0 +1,157 @@
+/* -*- 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 "IdleRequest.h"
+
+#include "mozilla/Function.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/dom/IdleDeadline.h"
+#include "mozilla/dom/Performance.h"
+#include "mozilla/dom/PerformanceTiming.h"
+#include "mozilla/dom/WindowBinding.h"
+#include "nsComponentManagerUtils.h"
+#include "nsGlobalWindow.h"
+#include "nsISupportsPrimitives.h"
+#include "nsPIDOMWindow.h"
+
+namespace mozilla {
+namespace dom {
+
+IdleRequest::IdleRequest(JSContext* aCx, nsPIDOMWindowInner* aWindow,
+                         IdleRequestCallback& aCallback, uint32_t aHandle)
+  : mWindow(aWindow)
+  , mCallback(&aCallback)
+  , mHandle(aHandle)
+  , mTimeoutHandle(Nothing())
+{
+  MOZ_ASSERT(aWindow);
+
+  // Get the calling location.
+  nsJSUtils::GetCallingLocation(aCx, mFileName, &mLineNo, &mColumn);
+}
+
+IdleRequest::~IdleRequest()
+{
+}
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(IdleRequest)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(IdleRequest)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(IdleRequest)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(IdleRequest)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mCallback)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(IdleRequest)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCallback)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(IdleRequest)
+  NS_INTERFACE_MAP_ENTRY(nsIRunnable)
+  NS_INTERFACE_MAP_ENTRY(nsICancelableRunnable)
+  NS_INTERFACE_MAP_ENTRY(nsIIncrementalRunnable)
+  NS_INTERFACE_MAP_ENTRY(nsITimeoutHandler)
+  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsITimeoutHandler)
+NS_INTERFACE_MAP_END
+
+nsresult
+IdleRequest::SetTimeout(uint32_t aTimeout)
+{
+  int32_t handle;
+  nsresult rv = nsGlobalWindow::Cast(mWindow)->SetTimeoutOrInterval(
+    this, aTimeout, false, Timeout::Reason::eIdleCallbackTimeout, &handle);
+  mTimeoutHandle = Some(handle);
+
+  return rv;
+}
+
+nsresult
+IdleRequest::Run()
+{
+  if (mCallback) {
+    RunIdleRequestCallback(false);
+  }
+
+  return NS_OK;
+}
+
+nsresult
+IdleRequest::Cancel()
+{
+  mCallback = nullptr;
+  CancelTimeout();
+  if (isInList()) {
+    remove();
+  }
+  Release();
+
+  return NS_OK;
+}
+
+void
+IdleRequest::SetDeadline(TimeStamp aDeadline)
+{
+  mozilla::dom::Performance* perf = mWindow->GetPerformance();
+  mDeadline =
+    perf ? perf->GetDOMTiming()->TimeStampToDOMHighRes(aDeadline) : 0.0;
+}
+
+nsresult
+IdleRequest::RunIdleRequestCallback(bool aDidTimeout)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  if (!aDidTimeout) {
+    CancelTimeout();
+  }
+
+  remove();
+  ErrorResult error;
+  RefPtr<IdleDeadline> deadline =
+    new IdleDeadline(mWindow, aDidTimeout, mDeadline);
+  mCallback->Call(*deadline, error, "requestIdleCallback handler");
+  mCallback = nullptr;
+  Release();
+
+  return error.StealNSResult();
+}
+
+void
+IdleRequest::CancelTimeout()
+{
+  if (mTimeoutHandle.isSome()) {
+    nsGlobalWindow::Cast(mWindow)->ClearTimeoutOrInterval(
+      mTimeoutHandle.value(), Timeout::Reason::eIdleCallbackTimeout);
+  }
+}
+
+nsresult
+IdleRequest::Call()
+{
+  SetDeadline(TimeStamp::Now());
+  return RunIdleRequestCallback(true);
+}
+
+void
+IdleRequest::GetLocation(const char** aFileName, uint32_t* aLineNo,
+                         uint32_t* aColumn)
+{
+  *aFileName = mFileName.get();
+  *aLineNo = mLineNo;
+  *aColumn = mColumn;
+}
+
+void
+IdleRequest::MarkForCC()
+{
+  mCallback->MarkForCC();
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/base/IdleRequest.h
@@ -0,0 +1,74 @@
+/* -*- 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_idlerequest_h
+#define mozilla_dom_idlerequest_h
+
+#include "mozilla/LinkedList.h"
+#include "mozilla/Maybe.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsDOMNavigationTiming.h"
+#include "nsITimeoutHandler.h"
+
+class nsPIDOMWindowInner;
+
+namespace mozilla {
+namespace dom {
+
+class IdleRequestCallback;
+
+class IdleRequest final : public nsITimeoutHandler
+                        , public nsIRunnable
+                        , public nsICancelableRunnable
+                        , public nsIIncrementalRunnable
+                        , public LinkedListElement<IdleRequest>
+{
+public:
+  IdleRequest(JSContext* aCx, nsPIDOMWindowInner* aWindow,
+              IdleRequestCallback& aCallback, uint32_t aHandle);
+
+  virtual nsresult Call() override;
+  virtual void GetLocation(const char** aFileName, uint32_t* aLineNo,
+                           uint32_t* aColumn) override;
+  virtual void MarkForCC() override;
+
+  nsresult SetTimeout(uint32_t aTimout);
+  nsresult RunIdleRequestCallback(bool aDidTimeout);
+  void CancelTimeout();
+
+  NS_DECL_NSIRUNNABLE;
+  virtual nsresult Cancel() override;
+  virtual void SetDeadline(mozilla::TimeStamp aDeadline) override;
+
+  uint32_t Handle() const
+  {
+    return mHandle;
+  }
+
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(IdleRequest, nsITimeoutHandler)
+
+private:
+  ~IdleRequest();
+
+  // filename, line number and JS language version string of the
+  // caller of setTimeout()
+  nsCString mFileName;
+  uint32_t mLineNo;
+  uint32_t mColumn;
+
+  nsCOMPtr<nsPIDOMWindowInner> mWindow;
+  RefPtr<IdleRequestCallback> mCallback;
+  uint32_t mHandle;
+  mozilla::Maybe<int32_t> mTimeoutHandle;
+  DOMHighResTimeStamp mDeadline;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_idlerequest_h
--- a/dom/base/moz.build
+++ b/dom/base/moz.build
@@ -180,16 +180,18 @@ EXPORTS.mozilla.dom += [
     'EventSource.h',
     'File.h',
     'FileList.h',
     'FileReader.h',
     'FormData.h',
     'FragmentOrElement.h',
     'FromParser.h',
     'GroupedSHistory.h',
+    'IdleDeadline.h',
+    'IdleRequest.h',
     'ImageEncoder.h',
     'ImageTracker.h',
     'ImportManager.h',
     'Link.h',
     'Location.h',
     'MutableBlobStorage.h',
     'MutableBlobStreamListener.h',
     'NameSpaceConstants.h',
@@ -247,16 +249,18 @@ UNIFIED_SOURCES += [
     'Element.cpp',
     'EventSource.cpp',
     'File.cpp',
     'FileList.cpp',
     'FileReader.cpp',
     'FormData.cpp',
     'FragmentOrElement.cpp',
     'GroupedSHistory.cpp',
+    'IdleDeadline.cpp',
+    'IdleRequest.cpp',
     'ImageEncoder.cpp',
     'ImageTracker.cpp',
     'ImportManager.cpp',
     'Link.cpp',
     'Location.cpp',
     'MultipartBlobImpl.cpp',
     'MutableBlobStorage.cpp',
     'MutableBlobStreamListener.cpp',
--- a/dom/base/nsGlobalWindow.cpp
+++ b/dom/base/nsGlobalWindow.cpp
@@ -13,16 +13,17 @@
 // Local Includes
 #include "Navigator.h"
 #include "nsContentSecurityManager.h"
 #include "nsScreen.h"
 #include "nsHistory.h"
 #include "nsDOMNavigationTiming.h"
 #include "nsIDOMStorageManager.h"
 #include "mozilla/dom/DOMStorage.h"
+#include "mozilla/dom/IdleRequest.h"
 #include "mozilla/dom/Performance.h"
 #include "mozilla/dom/StorageEvent.h"
 #include "mozilla/dom/StorageEventBinding.h"
 #include "mozilla/dom/Timeout.h"
 #include "mozilla/IntegerPrintfMacros.h"
 #if defined(MOZ_WIDGET_ANDROID) || defined(MOZ_WIDGET_GONK)
 #include "mozilla/dom/WindowOrientationObserver.h"
 #endif
@@ -322,16 +323,21 @@ nsGlobalWindow::DOMMinTimeoutValue() con
 // uses 5.
 #define DOM_CLAMP_TIMEOUT_NESTING_LEVEL 5
 
 // The longest interval (as PRIntervalTime) we permit, or that our
 // timer code can handle, really. See DELAY_INTERVAL_LIMIT in
 // nsTimerImpl.h for details.
 #define DOM_MAX_TIMEOUT_VALUE    DELAY_INTERVAL_LIMIT
 
+// The interval at which we execute idle callbacks
+static uint32_t gThrottledIdlePeriodLength;
+
+#define DEFAULT_THROTTLED_IDLE_PERIOD_LENGTH 10000
+
 #define FORWARD_TO_OUTER(method, args, err_rval)                              \
   PR_BEGIN_MACRO                                                              \
   if (IsInnerWindow()) {                                                      \
     nsGlobalWindow *outer = GetOuterWindowInternal();                         \
     if (!AsInner()->HasActiveDocument()) {                                    \
       NS_WARNING(outer ?                                                      \
                  "Inner window does not have active document." :              \
                  "No outer window available!");                               \
@@ -535,16 +541,119 @@ DialogValueHolder::Get(JSContext* aCx, J
   if (aSubject->Subsumes(mOrigin)) {
     aError = nsContentUtils::XPConnect()->VariantToJS(aCx, aScope,
                                                       mValue, aResult);
   } else {
     aResult.setUndefined();
   }
 }
 
+void
+nsGlobalWindow::PostThrottledIdleCallback()
+{
+  AssertIsOnMainThread();
+
+  if (mThrottledIdleRequestCallbacks.isEmpty())
+    return;
+
+  RefPtr<IdleRequest> request(mThrottledIdleRequestCallbacks.popFirst());
+  // ownership transferred from mThrottledIdleRequestCallbacks to
+  // mIdleRequestCallbacks
+  mIdleRequestCallbacks.insertBack(request);
+  NS_IdleDispatchToCurrentThread(request.forget());
+}
+
+/* static */ void
+nsGlobalWindow::InsertIdleCallbackIntoList(IdleRequest* aRequest,
+                                           IdleRequests& aList)
+{
+  aList.insertBack(aRequest);
+  aRequest->AddRef();
+}
+
+uint32_t
+nsGlobalWindow::RequestIdleCallback(JSContext* aCx,
+                                    IdleRequestCallback& aCallback,
+                                    const IdleRequestOptions& aOptions,
+                                    ErrorResult& aError)
+{
+  MOZ_RELEASE_ASSERT(IsInnerWindow());
+  AssertIsOnMainThread();
+
+  uint32_t handle = ++mIdleRequestCallbackCounter;
+
+  RefPtr<IdleRequest> request =
+    new IdleRequest(aCx, AsInner(), aCallback, handle);
+
+  if (aOptions.mTimeout.WasPassed()) {
+    aError = request->SetTimeout(aOptions.mTimeout.Value());
+    if (NS_WARN_IF(aError.Failed())) {
+      return 0;
+    }
+  }
+
+  nsGlobalWindow* outer = GetOuterWindowInternal();
+  if (outer && outer->AsOuter()->IsBackground()) {
+    // mThrottledIdleRequestCallbacks now owns request
+    InsertIdleCallbackIntoList(request, mThrottledIdleRequestCallbacks);
+
+    NS_DelayedDispatchToCurrentThread(
+      NewRunnableMethod(this, &nsGlobalWindow::PostThrottledIdleCallback),
+      10000);
+  } else {
+    MOZ_ASSERT(mThrottledIdleRequestCallbacks.isEmpty());
+
+    // mIdleRequestCallbacks now owns request
+    InsertIdleCallbackIntoList(request, mIdleRequestCallbacks);
+
+    NS_IdleDispatchToCurrentThread(request.forget());
+  }
+
+  return handle;
+}
+
+void
+nsGlobalWindow::CancelIdleCallback(uint32_t aHandle)
+{
+  MOZ_RELEASE_ASSERT(IsInnerWindow());
+
+  for (IdleRequest* r : mIdleRequestCallbacks) {
+    if (r->Handle() == aHandle) {
+      r->Cancel();
+      break;
+    }
+  }
+}
+
+void
+nsGlobalWindow::DisableIdleCallbackRequests()
+{
+  while (!mIdleRequestCallbacks.isEmpty()) {
+    RefPtr<IdleRequest> request = mIdleRequestCallbacks.popFirst();
+    request->Cancel();
+  }
+
+  while (!mThrottledIdleRequestCallbacks.isEmpty()) {
+    RefPtr<IdleRequest> request = mThrottledIdleRequestCallbacks.popFirst();
+    request->Cancel();
+  }
+}
+
+void nsGlobalWindow::UnthrottleIdleCallbackRequests()
+{
+  AssertIsOnMainThread();
+
+  while (!mThrottledIdleRequestCallbacks.isEmpty()) {
+    RefPtr<IdleRequest> request(mThrottledIdleRequestCallbacks.popFirst());
+    mIdleRequestCallbacks.insertBack(request);
+    NS_IdleDispatchToCurrentThread(request.forget());
+  }
+}
+
+
 namespace mozilla {
 namespace dom {
 extern uint64_t
 NextWindowID();
 } // namespace dom
 } // namespace mozilla
 
 template<class T>
@@ -1150,16 +1259,17 @@ nsGlobalWindow::nsGlobalWindow(nsGlobalW
     mTimeoutInsertionPoint(nullptr),
     mTimeoutIdCounter(1),
     mTimeoutFiringDepth(0),
     mSuspendDepth(0),
     mFreezeDepth(0),
     mFocusMethod(0),
     mSerial(0),
     mIdleCallbackTimeoutCounter(1),
+    mIdleRequestCallbackCounter(1),
 #ifdef DEBUG
     mSetOpenerWindowCalled(false),
 #endif
 #ifdef MOZ_B2G
     mNetworkUploadObserverEnabled(false),
     mNetworkDownloadObserverEnabled(false),
 #endif
     mCleanedUp(false),
@@ -1215,16 +1325,20 @@ nsGlobalWindow::nsGlobalWindow(nsGlobalW
                                 "dom.min_timeout_value",
                                 DEFAULT_MIN_TIMEOUT_VALUE);
     Preferences::AddIntVarCache(&gMinBackgroundTimeoutValue,
                                 "dom.min_background_timeout_value",
                                 DEFAULT_MIN_BACKGROUND_TIMEOUT_VALUE);
     Preferences::AddBoolVarCache(&sIdleObserversAPIFuzzTimeDisabled,
                                  "dom.idle-observers-api.fuzz_time.disabled",
                                  false);
+
+    Preferences::AddUintVarCache(&gThrottledIdlePeriodLength,
+                                 "dom.idle_period.throttled_length",
+                                 DEFAULT_THROTTLED_IDLE_PERIOD_LENGTH);
   }
 
   if (gDumpFile == nullptr) {
     const nsAdoptingCString& fname =
       Preferences::GetCString("browser.dom.window.dump.file");
     if (!fname.IsEmpty()) {
       // if this fails to open, Dump() knows to just go to stdout
       // on null.
@@ -1561,16 +1675,17 @@ nsGlobalWindow::CleanUp()
   if (IsInnerWindow()) {
     DisableGamepadUpdates();
     mHasGamepad = false;
     DisableVRUpdates();
     mHasVREvents = false;
 #ifdef MOZ_B2G
     DisableTimeChangeNotifications();
 #endif
+    DisableIdleCallbackRequests();
   } else {
     MOZ_ASSERT(!mHasGamepad);
     MOZ_ASSERT(!mHasVREvents);
   }
 
   if (mCleanMessageManager) {
     MOZ_ASSERT(mIsChrome, "only chrome should have msg manager cleaned");
     nsGlobalChromeWindow *asChrome = static_cast<nsGlobalChromeWindow*>(this);
@@ -1640,16 +1755,18 @@ nsGlobalWindow::FreeInnerObjects()
 
   if (mIdleTimer) {
     mIdleTimer->Cancel();
     mIdleTimer = nullptr;
   }
 
   mIdleObservers.Clear();
 
+  DisableIdleCallbackRequests();
+
   mChromeEventHandler = nullptr;
 
   if (mListenerManager) {
     mListenerManager->Disconnect();
     mListenerManager = nullptr;
   }
 
   mLocation = nullptr;
@@ -1849,16 +1966,25 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mApplicationCache)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSuspendedDoc)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIndexedDB)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocumentPrincipal)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDoc)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIdleService)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWakeLock)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingStorageEvents)
+
+  for (IdleRequest* request : tmp->mIdleRequestCallbacks) {
+    cb.NoteNativeChild(request, NS_CYCLE_COLLECTION_PARTICIPANT(IdleRequest));
+  }
+
+  for (IdleRequest* request : tmp->mThrottledIdleRequestCallbacks) {
+    cb.NoteNativeChild(request, NS_CYCLE_COLLECTION_PARTICIPANT(IdleRequest));
+  }
+
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIdleObservers)
 
 #ifdef MOZ_GAMEPAD
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGamepads)
 #endif
 
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCacheStorage)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVRDisplays)
@@ -1909,16 +2035,17 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ns
     nsGlobalWindow::Cast(tmp->mOuterWindow)->MaybeClearInnerWindow(tmp);
     NS_IMPL_CYCLE_COLLECTION_UNLINK(mOuterWindow)
   }
 
   if (tmp->mListenerManager) {
     tmp->mListenerManager->Disconnect();
     NS_IMPL_CYCLE_COLLECTION_UNLINK(mListenerManager)
   }
+
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mLocation)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mHistory)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mCustomElements)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mLocalStorage)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mSessionStorage)
   if (tmp->mApplicationCache) {
     static_cast<nsDOMOfflineResourceList*>(tmp->mApplicationCache.get())->Disconnect();
     NS_IMPL_CYCLE_COLLECTION_UNLINK(mApplicationCache)
@@ -1954,16 +2081,18 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ns
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mCrypto)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mU2F)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mConsole)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mExternal)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mMozSelfSupport)
 
   tmp->UnlinkHostObjectURIs();
 
+  tmp->DisableIdleCallbackRequests();
+
   NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 
 #ifdef DEBUG
 void
 nsGlobalWindow::RiskyUnlink()
 {
   NS_CYCLE_COLLECTION_INNERNAME.Unlink(this);
@@ -9850,16 +9979,23 @@ void nsGlobalWindow::SetIsBackground(boo
 {
   MOZ_ASSERT(IsOuterWindow());
 
   bool resetTimers = (!aIsBackground && AsOuter()->IsBackground());
   nsPIDOMWindow::SetIsBackground(aIsBackground);
   if (resetTimers) {
     ResetTimersForNonBackgroundWindow();
   }
+
+  if (!aIsBackground) {
+    nsGlobalWindow* inner = GetCurrentInnerWindowInternal();
+    if (inner) {
+      inner->UnthrottleIdleCallbackRequests();
+    }
+  }
 #ifdef MOZ_GAMEPAD
   if (!aIsBackground) {
     nsGlobalWindow* inner = GetCurrentInnerWindowInternal();
     if (inner) {
       inner->SyncGamepadState();
     }
   }
 #endif
@@ -12580,18 +12716,17 @@ nsGlobalWindow::RunTimeoutHandler(Timeou
   const char *reason;
   if (timeout->mIsInterval) {
     reason = "setInterval handler";
   } else {
     reason = "setTimeout handler";
   }
 
   bool abortIntervalHandler = false;
-  nsCOMPtr<nsITimeoutHandler> basicHandler(timeout->mScriptHandler);
-  nsCOMPtr<nsIScriptTimeoutHandler> handler(do_QueryInterface(basicHandler));
+  nsCOMPtr<nsIScriptTimeoutHandler> handler(do_QueryInterface(timeout->mScriptHandler));
   if (handler) {
     RefPtr<Function> callback = handler->GetCallback();
 
     if (!callback) {
       // Evaluate the timeout expression.
       const nsAString& script = handler->GetHandlerText();
 
       const char* filename = nullptr;
@@ -12618,17 +12753,19 @@ nsGlobalWindow::RunTimeoutHandler(Timeou
       callback->Call(me, handler->GetArgs(), &ignoredVal, rv, reason);
       if (rv.IsUncatchableException()) {
         abortIntervalHandler = true;
       }
 
       rv.SuppressException();
     }
   } else {
+    nsCOMPtr<nsITimeoutHandler> basicHandler(timeout->mScriptHandler);
     nsCOMPtr<nsISupports> kungFuDeathGrip(static_cast<nsIDOMWindow*>(this));
+    mozilla::Unused << kungFuDeathGrip;
     basicHandler->Call();
   }
 
   // If we received an uncatchable exception, do not schedule the timeout again.
   // This allows the slow script dialog to break easy DoS attacks like
   // setInterval(function() { while(1); }, 100);
   if (abortIntervalHandler) {
     // If it wasn't an interval timer to begin with, this does nothing.  If it
--- a/dom/base/nsGlobalWindow.h
+++ b/dom/base/nsGlobalWindow.h
@@ -109,16 +109,18 @@ struct ChannelPixelLayout;
 class Console;
 class Crypto;
 class CustomElementRegistry;
 class DocGroup;
 class External;
 class Function;
 class Gamepad;
 enum class ImageBitmapFormat : uint32_t;
+class IdleRequest;
+class IdleRequestCallback;
 class Location;
 class MediaQueryList;
 class MozSelfSupport;
 class Navigator;
 class OwningExternalOrWindowProxy;
 class Promise;
 class PostMessageEvent;
 struct RequestInit;
@@ -746,16 +748,17 @@ public:
 
   // Inner windows only.
   // Enable/disable updates for VR
   void EnableVRUpdates();
   void DisableVRUpdates();
 
   // Update the VR displays for this window
   bool UpdateVRDisplays(nsTArray<RefPtr<mozilla::dom::VRDisplay>>& aDisplays);
+
   // Inner windows only.
   // Called to inform that the set of active VR displays has changed.
   void NotifyActiveVRDisplaysChanged();
 
 #define EVENT(name_, id_, type_, struct_)                                     \
   mozilla::dom::EventHandlerNonNull* GetOn##name_()                           \
   {                                                                           \
     mozilla::EventListenerManager* elm = GetExistingListenerManager();        \
@@ -1062,16 +1065,24 @@ public:
                      mozilla::ErrorResult& aError);
   void GetOuterHeight(JSContext* aCx, JS::MutableHandle<JS::Value> aValue,
                       mozilla::ErrorResult& aError);
   void SetOuterHeight(JSContext* aCx, JS::Handle<JS::Value> aValue,
                       mozilla::ErrorResult& aError);
   int32_t RequestAnimationFrame(mozilla::dom::FrameRequestCallback& aCallback,
                                 mozilla::ErrorResult& aError);
   void CancelAnimationFrame(int32_t aHandle, mozilla::ErrorResult& aError);
+
+  uint32_t RequestIdleCallback(JSContext* aCx,
+                               mozilla::dom::IdleRequestCallback& aCallback,
+                               const mozilla::dom::IdleRequestOptions& aOptions,
+                               mozilla::ErrorResult& aError);
+  void CancelIdleCallback(uint32_t aHandle);
+
+
 #ifdef MOZ_WEBSPEECH
   mozilla::dom::SpeechSynthesis*
     GetSpeechSynthesis(mozilla::ErrorResult& aError);
   bool HasActiveSpeechSynthesis();
 #endif
   already_AddRefed<nsICSSDeclaration>
     GetDefaultComputedStyle(mozilla::dom::Element& aElt,
                             const nsAString& aPseudoElt,
@@ -1233,17 +1244,16 @@ public:
   void GetInterface(JSContext* aCx, nsIJSID* aIID,
                     JS::MutableHandle<JS::Value> aRetval,
                     mozilla::ErrorResult& aError);
 
   already_AddRefed<nsWindowRoot> GetWindowRootOuter();
   already_AddRefed<nsWindowRoot> GetWindowRoot(mozilla::ErrorResult& aError);
 
   mozilla::dom::Performance* GetPerformance();
-
 protected:
   // Web IDL helpers
 
   // Redefine the property called aPropName on this window object to be a value
   // property with the value aValue, much like we would do for a [Replaceable]
   // property in IDL.
   void RedefineProperty(JSContext* aCx, const char* aPropName,
                         JS::Handle<JS::Value> aValue,
@@ -1835,18 +1845,32 @@ protected:
   uint32_t mSuspendDepth;
   uint32_t mFreezeDepth;
 
   // the method that was used to focus mFocusedNode
   uint32_t mFocusMethod;
 
   uint32_t mSerial;
 
+  void DisableIdleCallbackRequests();
+  void UnthrottleIdleCallbackRequests();
+
+  void PostThrottledIdleCallback();
+
+  typedef mozilla::LinkedList<mozilla::dom::IdleRequest> IdleRequests;
+  static void InsertIdleCallbackIntoList(mozilla::dom::IdleRequest* aRequest,
+                                         IdleRequests& aList);
+
    // The current idle request callback timeout handle
   uint32_t mIdleCallbackTimeoutCounter;
+  // The current idle request callback handle
+  uint32_t mIdleRequestCallbackCounter;
+  IdleRequests mIdleRequestCallbacks;
+  IdleRequests mThrottledIdleRequestCallbacks;
+
 #ifdef DEBUG
   bool mSetOpenerWindowCalled;
   nsCOMPtr<nsIURI> mLastOpenedURI;
 #endif
 
 #ifdef MOZ_B2G
   bool mNetworkUploadObserverEnabled;
   bool mNetworkDownloadObserverEnabled;
--- a/dom/bindings/Bindings.conf
+++ b/dom/bindings/Bindings.conf
@@ -1409,16 +1409,19 @@ DOMInterfaces = {
     'concrete': False,
 },
 
 'Window': {
     'nativeType': 'nsGlobalWindow',
     'binaryNames': {
         'postMessage': 'postMessageMoz',
     },
+    'implicitJSContext': [
+        'requestIdleCallback'
+    ],
 },
 
 'WindowProxy': {
     'nativeType': 'nsPIDOMWindowOuter',
     'headerFile': 'nsPIDOMWindow.h',
     'concrete': False
 },
 
--- a/dom/ipc/ContentChild.cpp
+++ b/dom/ipc/ContentChild.cpp
@@ -78,16 +78,17 @@
 #endif
 
 #include "mozilla/Unused.h"
 
 #include "mozInlineSpellChecker.h"
 #include "nsDocShell.h"
 #include "nsIConsoleListener.h"
 #include "nsICycleCollectorListener.h"
+#include "nsIIdlePeriod.h"
 #include "nsIDragService.h"
 #include "nsIIPCBackgroundChildCreateCallback.h"
 #include "nsIInterfaceRequestorUtils.h"
 #include "nsIMemoryReporter.h"
 #include "nsIMemoryInfoDumper.h"
 #include "nsIMutable.h"
 #include "nsIObserverService.h"
 #include "nsIScriptSecurityManager.h"
new file mode 100644
--- /dev/null
+++ b/dom/webidl/IdleDeadline.webidl
@@ -0,0 +1,14 @@
+/* -*- 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/.
+ *
+ * The origin of this IDL file is:
+ * https://w3c.github.io/requestidlecallback/
+ */
+
+[Pref="dom.requestIdleCallback.enabled"]
+interface IdleDeadline {
+  DOMHighResTimeStamp timeRemaining();
+  readonly attribute boolean didTimeout;
+};
--- a/dom/webidl/Window.webidl
+++ b/dom/webidl/Window.webidl
@@ -9,16 +9,17 @@
  * https://dvcs.w3.org/hg/IndexedDB/raw-file/tip/Overview.html
  * http://dev.w3.org/csswg/cssom/
  * http://dev.w3.org/csswg/cssom-view/
  * https://dvcs.w3.org/hg/webperf/raw-file/tip/specs/RequestAnimationFrame/Overview.html
  * https://dvcs.w3.org/hg/webperf/raw-file/tip/specs/NavigationTiming/Overview.html
  * https://dvcs.w3.org/hg/webcrypto-api/raw-file/tip/spec/Overview.html
  * http://dvcs.w3.org/hg/speech-api/raw-file/tip/speechapi.html
  * https://w3c.github.io/webappsec-secure-contexts/#monkey-patching-global-object
+ * https://w3c.github.io/requestidlecallback/
  */
 
 interface ApplicationCache;
 interface IID;
 interface nsIBrowserDOMWindow;
 interface nsIMessageBroadcaster;
 interface nsIDOMCrypto;
 typedef any Transferable;
@@ -485,8 +486,22 @@ partial interface Window {
   [Pref="dom.vr.enabled"]
   attribute EventHandler onvrdisplaydisconnect;
   [Pref="dom.vr.enabled"]
   attribute EventHandler onvrdisplaypresentchange;
 };
 
 Window implements ChromeWindow;
 Window implements WindowOrWorkerGlobalScope;
+
+partial interface Window {
+  [Throws, Pref="dom.requestIdleCallback.enabled"]
+  unsigned long requestIdleCallback(IdleRequestCallback callback,
+                                    optional IdleRequestOptions options);
+  [Pref="dom.requestIdleCallback.enabled"]
+  void          cancelIdleCallback(unsigned long handle);
+};
+
+dictionary IdleRequestOptions {
+  unsigned long timeout;
+};
+
+callback IdleRequestCallback = void (IdleDeadline deadline);
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -264,16 +264,17 @@ WEBIDL_FILES = [
     'IDBIndex.webidl',
     'IDBKeyRange.webidl',
     'IDBMutableFile.webidl',
     'IDBObjectStore.webidl',
     'IDBOpenDBRequest.webidl',
     'IDBRequest.webidl',
     'IDBTransaction.webidl',
     'IDBVersionChangeEvent.webidl',
+    'IdleDeadline.webidl',
     'IIRFilterNode.webidl',
     'ImageBitmap.webidl',
     'ImageBitmapRenderingContext.webidl',
     'ImageCapture.webidl',
     'ImageData.webidl',
     'ImageDocument.webidl',
     'InputEvent.webidl',
     'InputMethod.webidl',
--- a/layout/base/nsLayoutUtils.cpp
+++ b/layout/base/nsLayoutUtils.cpp
@@ -146,16 +146,23 @@ using namespace mozilla::gfx;
 #define GRID_ENABLED_PREF_NAME "layout.css.grid.enabled"
 #define GRID_TEMPLATE_SUBGRID_ENABLED_PREF_NAME "layout.css.grid-template-subgrid-value.enabled"
 #define WEBKIT_PREFIXES_ENABLED_PREF_NAME "layout.css.prefixes.webkit"
 #define DISPLAY_CONTENTS_ENABLED_PREF_NAME "layout.css.display-contents.enabled"
 #define TEXT_ALIGN_UNSAFE_ENABLED_PREF_NAME "layout.css.text-align-unsafe-value.enabled"
 #define FLOAT_LOGICAL_VALUES_ENABLED_PREF_NAME "layout.css.float-logical-values.enabled"
 #define BG_CLIP_TEXT_ENABLED_PREF_NAME "layout.css.background-clip-text.enabled"
 
+// The time in number of frames that we estimate for a refresh driver
+// to be quiescent
+#define DEFAULT_QUIESCENT_FRAMES 2
+// The time (milliseconds) we estimate is needed between the end of an
+// idle time and the next Tick.
+#define DEFAULT_IDLE_PERIOD_TIME_LIMIT 3.0f
+
 #ifdef DEBUG
 // TODO: remove, see bug 598468.
 bool nsLayoutUtils::gPreventAssertInCompareTreePosition = false;
 #endif // DEBUG
 
 typedef FrameMetrics::ViewID ViewID;
 typedef nsStyleTransformMatrix::TransformReferenceBox TransformReferenceBox;
 
@@ -169,16 +176,18 @@ typedef nsStyleTransformMatrix::Transfor
 /* static */ bool nsLayoutUtils::sInvalidationDebuggingIsEnabled;
 /* static */ bool nsLayoutUtils::sCSSVariablesEnabled;
 /* static */ bool nsLayoutUtils::sInterruptibleReflowEnabled;
 /* static */ bool nsLayoutUtils::sSVGTransformBoxEnabled;
 /* static */ bool nsLayoutUtils::sTextCombineUprightDigitsEnabled;
 #ifdef MOZ_STYLO
 /* static */ bool nsLayoutUtils::sStyloEnabled;
 #endif
+/* static */ uint32_t nsLayoutUtils::sIdlePeriodDeadlineLimit;
+/* static */ uint32_t nsLayoutUtils::sQuiescentFramesBeforeIdlePeriod;
 
 static ViewID sScrollIdCounter = FrameMetrics::START_SCROLL_ID;
 
 typedef nsDataHashtable<nsUint64HashKey, nsIContent*> ContentMap;
 static ContentMap* sContentMap = nullptr;
 static ContentMap& GetContentMap() {
   if (!sContentMap) {
     sContentMap = new ContentMap();
@@ -7866,16 +7875,22 @@ nsLayoutUtils::Initialize()
   Preferences::AddBoolVarCache(&sSVGTransformBoxEnabled,
                                "svg.transform-box.enabled");
   Preferences::AddBoolVarCache(&sTextCombineUprightDigitsEnabled,
                                "layout.css.text-combine-upright-digits.enabled");
 #ifdef MOZ_STYLO
   Preferences::AddBoolVarCache(&sStyloEnabled,
                                "layout.css.servo.enabled");
 #endif
+  Preferences::AddUintVarCache(&sIdlePeriodDeadlineLimit,
+                               "layout.idle_period.time_limit",
+                               DEFAULT_IDLE_PERIOD_TIME_LIMIT);
+  Preferences::AddUintVarCache(&sQuiescentFramesBeforeIdlePeriod,
+                               "layout.idle_period.required_quiescent_frames",
+                               DEFAULT_QUIESCENT_FRAMES);
 
   for (auto& callback : kPrefCallbacks) {
     Preferences::RegisterCallbackAndCall(callback.func, callback.name);
   }
   nsComputedDOMStyle::RegisterPrefChangeCallbacks();
 }
 
 /* static */
--- a/layout/base/nsLayoutUtils.h
+++ b/layout/base/nsLayoutUtils.h
@@ -2424,16 +2424,24 @@ public:
   static bool StyloEnabled() {
 #ifdef MOZ_STYLO
     return sStyloEnabled;
 #else
     return false;
 #endif
   }
 
+  static uint32_t IdlePeriodDeadlineLimit() {
+    return sIdlePeriodDeadlineLimit;
+  }
+
+  static uint32_t QuiescentFramesBeforeIdlePeriod() {
+    return sQuiescentFramesBeforeIdlePeriod;
+  }
+
   /**
    * See comment above "font.size.inflation.mappingIntercept" in
    * modules/libpref/src/init/all.js .
    */
   static int32_t FontSizeInflationMappingIntercept() {
     return sFontSizeInflationMappingIntercept;
   }
 
@@ -2880,16 +2888,18 @@ private:
   static bool sInvalidationDebuggingIsEnabled;
   static bool sCSSVariablesEnabled;
   static bool sInterruptibleReflowEnabled;
   static bool sSVGTransformBoxEnabled;
   static bool sTextCombineUprightDigitsEnabled;
 #ifdef MOZ_STYLO
   static bool sStyloEnabled;
 #endif
+  static uint32_t sIdlePeriodDeadlineLimit;
+  static uint32_t sQuiescentFramesBeforeIdlePeriod;
 
   /**
    * Helper function for LogTestDataForPaint().
    */
   static void DoLogTestDataForPaint(mozilla::layers::LayerManager* aManager,
                                     ViewID aScrollId,
                                     const std::string& aKey,
                                     const std::string& aValue);
--- a/layout/base/nsRefreshDriver.cpp
+++ b/layout/base/nsRefreshDriver.cpp
@@ -133,16 +133,17 @@ namespace mozilla {
  * start/stop whatever timer mechanism is in use, and ScheduleNextTick
  * is called at the start of the Tick() implementation to set a time
  * for the next tick.
  */
 class RefreshDriverTimer {
 public:
   RefreshDriverTimer()
     : mLastFireEpoch(0)
+    , mLastFireSkipped(false)
   {
   }
 
   virtual ~RefreshDriverTimer()
   {
     MOZ_ASSERT(mContentRefreshDrivers.Length() == 0, "Should have removed all content refresh drivers from here by now!");
     MOZ_ASSERT(mRootRefreshDrivers.Length() == 0, "Should have removed all root refresh drivers from here by now!");
   }
@@ -216,16 +217,45 @@ public:
       driver->mActiveTimer = aNewTimer;
     }
     mRootRefreshDrivers.Clear();
 
     aNewTimer->mLastFireEpoch = mLastFireEpoch;
     aNewTimer->mLastFireTime = mLastFireTime;
   }
 
+  virtual TimeDuration GetTimerRate() = 0;
+
+  bool LastTickSkippedAnyPaints() const
+  {
+    return mLastFireSkipped;
+  }
+
+  Maybe<TimeStamp> GetIdleDeadlineHint()
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+
+    if (LastTickSkippedAnyPaints()) {
+      return Some(TimeStamp());
+    }
+
+    TimeStamp mostRecentRefresh = MostRecentRefresh();
+    TimeDuration refreshRate = GetTimerRate();
+    TimeStamp idleEnd = mostRecentRefresh + refreshRate;
+
+    if (idleEnd +
+          refreshRate * nsLayoutUtils::QuiescentFramesBeforeIdlePeriod() <
+        TimeStamp::Now()) {
+      return Nothing();
+    }
+
+    return Some(idleEnd - TimeDuration::FromMilliseconds(
+                            nsLayoutUtils::IdlePeriodDeadlineLimit()));
+  }
+
 protected:
   virtual void StartTimer() = 0;
   virtual void StopTimer() = 0;
   virtual void ScheduleNextTick(TimeStamp aNowTime) = 0;
 
   bool IsRootRefreshDriver(nsRefreshDriver* aDriver)
   {
     nsPresContext* pc = aDriver->GetPresContext();
@@ -257,28 +287,31 @@ protected:
     nsTArray<RefPtr<nsRefreshDriver> > drivers(aDrivers);
     for (nsRefreshDriver* driver : drivers) {
       // don't poke this driver if it's in test mode
       if (driver->IsTestControllingRefreshesEnabled()) {
         continue;
       }
 
       TickDriver(driver, aJsNow, aNow);
+
+      mLastFireSkipped = mLastFireSkipped || driver->mSkippedPaints;
     }
   }
 
   /*
    * Tick the refresh drivers based on the given timestamp.
    */
   void Tick(int64_t jsnow, TimeStamp now)
   {
     ScheduleNextTick(now);
 
     mLastFireEpoch = jsnow;
     mLastFireTime = now;
+    mLastFireSkipped = false;
 
     LOG("[%p] ticking drivers...", this);
     // RD is short for RefreshDriver
     profiler_tracing("Paint", "RD", TRACING_INTERVAL_START);
 
     TickRefreshDrivers(jsnow, now, mContentRefreshDrivers);
     TickRefreshDrivers(jsnow, now, mRootRefreshDrivers);
 
@@ -288,16 +321,17 @@ protected:
 
   static void TickDriver(nsRefreshDriver* driver, int64_t jsnow, TimeStamp now)
   {
     LOG(">> TickDriver: %p (jsnow: %lld)", driver, jsnow);
     driver->Tick(jsnow, now);
   }
 
   int64_t mLastFireEpoch;
+  bool mLastFireSkipped;
   TimeStamp mLastFireTime;
   TimeStamp mTargetTime;
 
   nsTArray<RefPtr<nsRefreshDriver> > mContentRefreshDrivers;
   nsTArray<RefPtr<nsRefreshDriver> > mRootRefreshDrivers;
 
   // useful callback for nsITimer-based derived classes, here
   // bacause of c++ protected shenanigans
@@ -340,31 +374,36 @@ public:
     mRateDuration = TimeDuration::FromMilliseconds(mRateMilliseconds);
   }
 
   double GetRate() const
   {
     return mRateMilliseconds;
   }
 
+  virtual TimeDuration GetTimerRate() override
+  {
+    return mRateDuration;
+  }
+
 protected:
 
-  virtual void StartTimer()
+  virtual void StartTimer() override
   {
     // pretend we just fired, and we schedule the next tick normally
     mLastFireEpoch = JS_Now();
     mLastFireTime = TimeStamp::Now();
 
     mTargetTime = mLastFireTime + mRateDuration;
 
     uint32_t delay = static_cast<uint32_t>(mRateMilliseconds);
     mTimer->InitWithFuncCallback(TimerTick, this, delay, nsITimer::TYPE_ONE_SHOT);
   }
 
-  virtual void StopTimer()
+  virtual void StopTimer() override
   {
     mTimer->Cancel();
   }
 
   double mRateMilliseconds;
   TimeDuration mRateDuration;
   RefPtr<nsITimer> mTimer;
 };
@@ -381,26 +420,44 @@ public:
     : mVsyncChild(nullptr)
   {
     MOZ_ASSERT(XRE_IsParentProcess());
     MOZ_ASSERT(NS_IsMainThread());
     mVsyncObserver = new RefreshDriverVsyncObserver(this);
     RefPtr<mozilla::gfx::VsyncSource> vsyncSource = gfxPlatform::GetPlatform()->GetHardwareVsync();
     MOZ_ALWAYS_TRUE(mVsyncDispatcher = vsyncSource->GetRefreshTimerVsyncDispatcher());
     mVsyncDispatcher->SetParentRefreshTimer(mVsyncObserver);
+    mVsyncRate = vsyncSource->GetGlobalDisplay().GetVsyncRate();
   }
 
   explicit VsyncRefreshDriverTimer(VsyncChild* aVsyncChild)
     : mVsyncChild(aVsyncChild)
   {
     MOZ_ASSERT(!XRE_IsParentProcess());
     MOZ_ASSERT(NS_IsMainThread());
     MOZ_ASSERT(mVsyncChild);
     mVsyncObserver = new RefreshDriverVsyncObserver(this);
     mVsyncChild->SetVsyncObserver(mVsyncObserver);
+    mVsyncRate = mVsyncChild->GetVsyncRate();
+  }
+
+  virtual TimeDuration GetTimerRate() override
+  {
+    if (mVsyncRate != TimeDuration::Forever()) {
+      return mVsyncRate;
+    }
+
+    if (mVsyncChild) {
+      mVsyncRate = mVsyncChild->GetVsyncRate();
+    }
+
+    // If hardware queries fail / are unsupported, we have to just guess.
+    return mVsyncRate != TimeDuration::Forever()
+             ? mVsyncRate
+             : TimeDuration::FromMilliseconds(1000.0 / 60.0);
   }
 
 private:
   // Since VsyncObservers are refCounted, but the RefreshDriverTimer are
   // explicitly shutdown. We create an inner class that has the VsyncObserver
   // and is shutdown when the RefreshDriverTimer is deleted. The alternative is
   // to (a) make all RefreshDriverTimer RefCounted or (b) use different
   // VsyncObserver types.
@@ -453,17 +510,16 @@ private:
     }
 
     void OnTimerStart()
     {
       if (!XRE_IsParentProcess()) {
         mLastChildTick = TimeStamp::Now();
       }
     }
-
   private:
     virtual ~RefreshDriverVsyncObserver() {}
 
     void RecordTelemetryProbes(TimeStamp aVsyncTimestamp)
     {
       MOZ_ASSERT(NS_IsMainThread());
     #ifndef ANDROID  /* bug 1142079 */
       if (XRE_IsParentProcess()) {
@@ -608,16 +664,17 @@ private:
 
   RefPtr<RefreshDriverVsyncObserver> mVsyncObserver;
   // Used for parent process.
   RefPtr<RefreshTimerVsyncDispatcher> mVsyncDispatcher;
   // Used for child process.
   // The mVsyncChild will be always available before VsncChild::ActorDestroy().
   // After ActorDestroy(), StartTimer() and StopTimer() calls will be non-op.
   RefPtr<VsyncChild> mVsyncChild;
+  TimeDuration mVsyncRate;
 }; // VsyncRefreshDriverTimer
 
 /**
  * Since the content process takes some time to setup
  * the vsync IPC connection, this timer is used
  * during the intial startup process.
  * During initial startup, the refresh drivers
  * are ticked off this timer, and are swapped out once content
@@ -672,17 +729,17 @@ public:
   InactiveRefreshDriverTimer(double aRate, double aDisableAfterMilliseconds)
     : SimpleTimerBasedRefreshDriverTimer(aRate),
       mNextTickDuration(aRate),
       mDisableAfterMilliseconds(aDisableAfterMilliseconds),
       mNextDriverIndex(0)
   {
   }
 
-  virtual void AddRefreshDriver(nsRefreshDriver* aDriver)
+  virtual void AddRefreshDriver(nsRefreshDriver* aDriver) override
   {
     RefreshDriverTimer::AddRefreshDriver(aDriver);
 
     LOG("[%p] inactive timer got new refresh driver %p, resetting rate",
         this, aDriver);
 
     // reset the timer, and start with the newly added one next time.
     mNextTickDuration = mRateMilliseconds;
@@ -690,39 +747,44 @@ public:
     // we don't really have to start with the newly added one, but we may as well
     // not tick the old ones at the fastest rate any more than we need to.
     mNextDriverIndex = GetRefreshDriverCount() - 1;
 
     StopTimer();
     StartTimer();
   }
 
+  virtual TimeDuration GetTimerRate() override
+  {
+    return TimeDuration::FromMilliseconds(mNextTickDuration);
+  }
+
 protected:
   uint32_t GetRefreshDriverCount()
   {
     return mContentRefreshDrivers.Length() + mRootRefreshDrivers.Length();
   }
 
-  virtual void StartTimer()
+  virtual void StartTimer() override
   {
     mLastFireEpoch = JS_Now();
     mLastFireTime = TimeStamp::Now();
 
     mTargetTime = mLastFireTime + mRateDuration;
 
     uint32_t delay = static_cast<uint32_t>(mRateMilliseconds);
     mTimer->InitWithFuncCallback(TimerTickOne, this, delay, nsITimer::TYPE_ONE_SHOT);
   }
 
-  virtual void StopTimer()
+  virtual void StopTimer() override
   {
     mTimer->Cancel();
   }
 
-  virtual void ScheduleNextTick(TimeStamp aNowTime)
+  virtual void ScheduleNextTick(TimeStamp aNowTime) override
   {
     if (mDisableAfterMilliseconds > 0.0 &&
         mNextTickDuration > mDisableAfterMilliseconds)
     {
       // We hit the time after which we should disable
       // inactive window refreshes; don't schedule anything
       // until we get kicked by an AddRefreshDriver call.
       return;
@@ -747,24 +809,27 @@ protected:
   {
     int64_t jsnow = JS_Now();
     TimeStamp now = TimeStamp::Now();
 
     ScheduleNextTick(now);
 
     mLastFireEpoch = jsnow;
     mLastFireTime = now;
+    mLastFireSkipped = false;
 
     nsTArray<RefPtr<nsRefreshDriver> > drivers(mContentRefreshDrivers);
     drivers.AppendElements(mRootRefreshDrivers);
+    size_t index = mNextDriverIndex;
 
-    if (mNextDriverIndex < drivers.Length() &&
-        !drivers[mNextDriverIndex]->IsTestControllingRefreshesEnabled())
+    if (index < drivers.Length() &&
+        !drivers[index]->IsTestControllingRefreshesEnabled())
     {
-      TickDriver(drivers[mNextDriverIndex], jsnow, now);
+      TickDriver(drivers[index], jsnow, now);
+      mLastFireSkipped = mLastFireSkipped || drivers[index]->SkippedPaints();
     }
 
     mNextDriverIndex++;
   }
 
   static void TimerTickOne(nsITimer* aTimer, void* aClosure)
   {
     InactiveRefreshDriverTimer *timer = static_cast<InactiveRefreshDriverTimer*>(aClosure);
@@ -2223,16 +2288,34 @@ nsRefreshDriver::CancelPendingEvents(nsI
 {
   for (auto i : Reversed(MakeRange(mPendingEvents.Length()))) {
     if (mPendingEvents[i].mTarget->OwnerDoc() == aDocument) {
       mPendingEvents.RemoveElementAt(i);
     }
   }
 }
 
+/* static */ Maybe<TimeStamp>
+nsRefreshDriver::GetIdleDeadlineHint()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  if (!sRegularRateTimer) {
+    return Nothing();
+  }
+
+  // For computing idleness of refresh drivers we only care about
+  // sRegularRateTimer, since we consider refresh drivers attached to
+  // sThrottledRateTimer to be inactive. This implies that tasks
+  // resulting from a tick on the sRegularRateTimer counts as being
+  // busy but tasks resulting from a tick on sThrottledRateTimer
+  // counts as being idle.
+  return sRegularRateTimer->GetIdleDeadlineHint();
+}
+
 void
 nsRefreshDriver::Disconnect()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   StopTimer();
 
   if (mPresContext) {
--- a/layout/base/nsRefreshDriver.h
+++ b/layout/base/nsRefreshDriver.h
@@ -324,33 +324,73 @@ public:
 
   bool IsWaitingForPaint(mozilla::TimeStamp aTime);
 
   // nsARefreshObserver
   NS_IMETHOD_(MozExternalRefCountType) AddRef(void) override { return TransactionIdAllocator::AddRef(); }
   NS_IMETHOD_(MozExternalRefCountType) Release(void) override { return TransactionIdAllocator::Release(); }
   virtual void WillRefresh(mozilla::TimeStamp aTime) override;
 
+  /**
+   * Compute the time when the currently active refresh driver timer
+   * will start its next tick.
+   *
+   * Returns 'Nothing' if the refresh driver timer hasn't been
+   * initialized or if we can't tell when the next tick will happen.
+   *
+   * Returns Some(TimeStamp()), i.e. the null time, if the next tick is late.
+   *
+   * Otherwise returns Some(TimeStamp(t)), where t is the time of the next tick.
+   *
+   * Using these three types of return values it is possible to
+   * estimate three different things about the idleness of the
+   * currently active group of refresh drivers. This information is
+   * used by nsThread to schedule lower priority "idle tasks".
+   *
+   * The 'Nothing' return value indicates to nsThread that the
+   * currently active refresh drivers will be idle for a time
+   * significantly longer than the current refresh rate and that it is
+   * free to schedule longer periods for executing idle tasks. This is the
+   * expected result when we aren't animating.
+
+   * Returning the null time indicates to nsThread that we are very
+   * busy and that it should definitely not schedule idle tasks at
+   * all. This is the expected result when we are animating, but
+   * aren't able to keep up with the animation and hence need to skip
+   * paints. Since catching up to missed paints will happen as soon as
+   * possible, this is the expected result if any of the refresh
+   * drivers attached to the current refresh driver misses a paint.
+   *
+   * Returning Some(TimeStamp(t)) indicates to nsThread that we will
+   * be idle until. This is usually the case when we're animating
+   * without skipping paints.
+   */
+  static mozilla::Maybe<mozilla::TimeStamp> GetIdleDeadlineHint();
+
+  bool SkippedPaints() const
+  {
+    return mSkippedPaints;
+  }
+
 private:
   typedef nsTObserverArray<nsARefreshObserver*> ObserverArray;
   typedef nsTHashtable<nsISupportsHashKey> RequestTable;
   struct ImageStartData {
     ImageStartData()
     {
     }
 
     mozilla::Maybe<mozilla::TimeStamp> mStartTime;
     RequestTable mEntries;
   };
   typedef nsClassHashtable<nsUint32HashKey, ImageStartData> ImageStartTable;
 
   void DispatchPendingEvents();
   void DispatchAnimationEvents();
   void RunFrameRequestCallbacks(mozilla::TimeStamp aNowTime);
-
   void Tick(int64_t aNowEpoch, mozilla::TimeStamp aNowTime);
 
   enum EnsureTimerStartedFlags {
     eNone = 0,
     eForceAdjustTimer = 1 << 0,
     eAllowTimeToGoBackwards = 1 << 1,
     eNeverAdjustTimer = 1 << 2,
   };
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -183,16 +183,23 @@ pref("dom.permissions.revoke.enable", fa
 
 // Enable Performance Observer API
 #ifdef NIGHTLY_BUILD
 pref("dom.enable_performance_observer", true);
 #else
 pref("dom.enable_performance_observer", false);
 #endif
 
+// Enable requestIdleCallback API
+#ifdef NIGHTLY_BUILD
+pref("dom.requestIdleCallback.enabled", true);
+#else
+pref("dom.requestIdleCallback.enabled", false);
+#endif
+
 // Whether the Gamepad API is enabled
 pref("dom.gamepad.enabled", true);
 pref("dom.gamepad.test.enabled", false);
 #ifdef RELEASE_OR_BETA
 pref("dom.gamepad.non_standard_events.enabled", false);
 #else
 pref("dom.gamepad.non_standard_events.enabled", true);
 #endif
@@ -2689,16 +2696,24 @@ pref("layout.display-list.dump-content",
 pref("layout.frame_rate.precise", false);
 
 // pref to control whether layout warnings that are hit quite often are enabled
 pref("layout.spammy_warnings.enabled", false);
 
 // Should we fragment floats inside CSS column layout?
 pref("layout.float-fragments-inside-column.enabled", true);
 
+// The number of frames times the frame rate is the time required to
+// pass without painting used to guess that we'll not paint again soon
+pref("layout.idle_period.required_quiescent_frames", 2);
+
+// The amount of time (milliseconds) needed between an idle period's
+// end and the start of the next tick to avoid jank.
+pref("layout.idle_period.time_limit", 3);
+
 // Is support for the Web Animations API enabled?
 // Before enabling this by default, make sure also CSSPseudoElement interface
 // has been spec'ed properly, or we should add a separate pref for
 // CSSPseudoElement interface. See Bug 1174575 for further details.
 #ifdef RELEASE_OR_BETA
 pref("dom.animations-api.core.enabled", false);
 #else
 pref("dom.animations-api.core.enabled", true);
@@ -2738,16 +2753,22 @@ pref("dom.max_chrome_script_run_time", 2
 pref("dom.max_script_run_time", 10);
 
 // Stop all scripts in a compartment when the "stop script" dialog is used.
 pref("dom.global_stop_script", true);
 
 // If true, ArchiveReader will be enabled
 pref("dom.archivereader.enabled", false);
 
+// Time (milliseconds) between throttled idle callbacks.
+pref("dom.idle_period.throttled_length", 10000);
+
+// The amount of idle time (milliseconds) reserved for a long idle period
+pref("idle_queue.long_period", 50);
+
 // Hang monitor timeout after which we kill the browser, in seconds
 // (0 is disabled)
 // Disabled on all platforms per bug 705748 until the found issues are
 // resolved.
 pref("hangmonitor.timeout", 0);
 
 pref("plugins.load_appdir_plugins", false);
 // If true, plugins will be click to play
--- a/xpcom/glue/nsThreadUtils.cpp
+++ b/xpcom/glue/nsThreadUtils.cpp
@@ -2,16 +2,17 @@
 /* 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 "nsThreadUtils.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/Likely.h"
+#include "mozilla/TimeStamp.h"
 #include "LeakRefPtr.h"
 
 #ifdef MOZILLA_INTERNAL_API
 # include "nsThreadManager.h"
 #else
 # include "nsXPCOMCIDInternal.h"
 # include "nsIThreadManager.h"
 # include "nsServiceManagerUtils.h"
@@ -215,17 +216,37 @@ NS_DispatchToMainThread(already_AddRefed
 // release them here.
 nsresult
 NS_DispatchToMainThread(nsIRunnable* aEvent, uint32_t aDispatchFlags)
 {
   nsCOMPtr<nsIRunnable> event(aEvent);
   return NS_DispatchToMainThread(event.forget(), aDispatchFlags);
 }
 
-extern NS_METHOD
+nsresult
+NS_DelayedDispatchToCurrentThread(already_AddRefed<nsIRunnable>&& aEvent, uint32_t aDelayMs)
+{
+  nsCOMPtr<nsIRunnable> event(aEvent);
+#ifdef MOZILLA_INTERNAL_API
+  nsIThread* thread = NS_GetCurrentThread();
+  if (!thread) {
+    return NS_ERROR_UNEXPECTED;
+  }
+#else
+  nsresult rv;
+  nsCOMPtr<nsIThread> thread;
+  rv = NS_GetCurrentThread(getter_AddRefs(thread));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+#endif
+
+  return thread->DelayedDispatch(event.forget(), aDelayMs);
+}
+
 nsresult
 NS_IdleDispatchToCurrentThread(already_AddRefed<nsIRunnable>&& aEvent)
 {
   nsresult rv;
   nsCOMPtr<nsIRunnable> event(aEvent);
 #ifdef MOZILLA_INTERNAL_API
   nsIThread* thread = NS_GetCurrentThread();
   if (!thread) {
--- a/xpcom/glue/nsThreadUtils.h
+++ b/xpcom/glue/nsThreadUtils.h
@@ -18,16 +18,17 @@
 #include "nsIIncrementalRunnable.h"
 #include "nsStringGlue.h"
 #include "nsCOMPtr.h"
 #include "nsAutoPtr.h"
 #include "mozilla/Atomics.h"
 #include "mozilla/IndexSequence.h"
 #include "mozilla/Likely.h"
 #include "mozilla/Move.h"
+#include "mozilla/TimeStamp.h"
 #include "mozilla/Tuple.h"
 #include "mozilla/TypeTraits.h"
 
 //-----------------------------------------------------------------------------
 // These methods are alternatives to the methods on nsIThreadManager, provided
 // for convenience.
 
 /**
@@ -128,16 +129,20 @@ NS_DispatchToCurrentThread(already_AddRe
 extern nsresult
 NS_DispatchToMainThread(nsIRunnable* aEvent,
                         uint32_t aDispatchFlags = NS_DISPATCH_NORMAL);
 extern nsresult
 NS_DispatchToMainThread(already_AddRefed<nsIRunnable>&& aEvent,
                         uint32_t aDispatchFlags = NS_DISPATCH_NORMAL);
 
 extern nsresult
+NS_DelayedDispatchToCurrentThread(
+  already_AddRefed<nsIRunnable>&& aEvent, uint32_t aDelayMs);
+
+extern nsresult
 NS_IdleDispatchToCurrentThread(already_AddRefed<nsIRunnable>&& aEvent);
 
 #ifndef XPCOM_GLUE_AVOID_NSPR
 /**
  * Process all pending events for the given thread before returning.  This
  * method simply calls ProcessNextEvent on the thread while HasPendingEvents
  * continues to return true and the time spent in NS_ProcessPendingEvents
  * does not exceed the given timeout value.
--- a/xpcom/threads/nsThread.h
+++ b/xpcom/threads/nsThread.h
@@ -196,20 +196,22 @@ protected:
 
   private:
     ~nsNestedEventTarget()
     {
     }
   };
 
   // This lock protects access to mObserver, mEvents, mIdleEvents,
-  // All of those fields are only modified on the thread itself (never from
-  // another thread).  This means that we can avoid holding the lock while
-  // using mObserver and mEvents on the thread itself.  When calling PutEvent
-  // on mEvents, we have to hold the lock to synchronize with PopEventQueue.
+  // mIdlePeriod and mEventsAreDoomed.  All of those fields are only
+  // modified on the thread itself (never from another thread).  This
+  // means that we can avoid holding the lock while using mObserver
+  // and mEvents on the thread itself.  When calling PutEvent on
+  // mEvents, we have to hold the lock to synchronize with
+  // PopEventQueue.
   mozilla::Mutex mLock;
 
   nsCOMPtr<nsIThreadObserver> mObserver;
   mozilla::CycleCollectedJSContext* mScriptObserver;
 
   // Only accessed on the target thread.
   nsAutoTObserverArray<NotNull<nsCOMPtr<nsIThreadObserver>>, 2> mEventObservers;