Bug 1363829 P5 Add an executor class to represent the single timer executing timers for a window. r=ehsan
authorBen Kelly <ben@wanderview.com>
Wed, 31 May 2017 17:13:19 -0700
changeset 361741 9ac282f673708b4ec618b135829da348529778f4
parent 361740 aea41a0174ae49453fe22b261e58c000655460a9
child 361742 5214ce6bed250add6892a0c7f2eeca6807f71cef
push id31940
push usercbook@mozilla.com
push dateThu, 01 Jun 2017 11:51:11 +0000
treeherdermozilla-central@0bcea6bac179 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersehsan
bugs1363829
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1363829 P5 Add an executor class to represent the single timer executing timers for a window. r=ehsan
dom/base/TimeoutExecutor.cpp
dom/base/TimeoutExecutor.h
dom/base/TimeoutManager.cpp
dom/base/TimeoutManager.h
dom/base/moz.build
new file mode 100644
--- /dev/null
+++ b/dom/base/TimeoutExecutor.cpp
@@ -0,0 +1,227 @@
+/* -*- 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 "TimeoutExecutor.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_ISUPPORTS(TimeoutExecutor, nsIRunnable, nsITimerCallback, nsINamed)
+
+TimeoutExecutor::~TimeoutExecutor()
+{
+  // The TimeoutManager should keep the Executor alive until its destroyed,
+  // and then call Shutdown() explicitly.
+  MOZ_DIAGNOSTIC_ASSERT(mMode == Mode::Shutdown);
+  MOZ_DIAGNOSTIC_ASSERT(!mOwner);
+  MOZ_DIAGNOSTIC_ASSERT(!mTimer);
+}
+
+nsresult
+TimeoutExecutor::ScheduleImmediate(const TimeStamp& aDeadline,
+                                   const TimeStamp& aNow)
+{
+  MOZ_DIAGNOSTIC_ASSERT(mDeadline.IsNull());
+  MOZ_DIAGNOSTIC_ASSERT(mMode == Mode::None);
+  MOZ_DIAGNOSTIC_ASSERT(aDeadline <= aNow);
+
+  nsresult rv =
+    mOwner->EventTarget()->Dispatch(this, nsIEventTarget::DISPATCH_NORMAL);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  mMode = Mode::Immediate;
+  mDeadline = aDeadline;
+
+  return NS_OK;
+}
+
+nsresult
+TimeoutExecutor::ScheduleDelayed(const TimeStamp& aDeadline,
+                                 const TimeStamp& aNow)
+{
+  MOZ_DIAGNOSTIC_ASSERT(mDeadline.IsNull());
+  MOZ_DIAGNOSTIC_ASSERT(mMode == Mode::None);
+  MOZ_DIAGNOSTIC_ASSERT(aDeadline > aNow);
+
+  nsresult rv = NS_OK;
+
+  if (!mTimer) {
+    mTimer = do_CreateInstance("@mozilla.org/timer;1", &rv);
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
+
+  // Always call Cancel() in case we are re-using a timer.  Otherwise
+  // the subsequent SetTarget() may fail.
+  rv = mTimer->Cancel();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = mTimer->SetTarget(mOwner->EventTarget());
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // Convert the precise delay to integral milliseconds for nsITimer.  We
+  // favor rounding down here.  If we fire early we will simply be rescheduled
+  // for an immediate runnable or a 0-ms timer.  This ends up giving us the
+  // most accurate firing time at the cost of a few more runnables.  This cost
+  // is only incurred when the browser is idle, though.  When the busy main
+  // thread is busy there will be a delay and we won't actually be early.
+  // TODO: In the future we could pass a precision argument in and round
+  //       up here for low-precision background timers.  We don't really care
+  //       if those timers fire late.
+  TimeDuration delay(aDeadline - aNow - TimeDuration::FromMilliseconds(0.1));
+  rv = mTimer->InitWithCallback(this, delay.ToMilliseconds(),
+                                nsITimer::TYPE_ONE_SHOT);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  mMode = Mode::Delayed;
+  mDeadline = aDeadline;
+
+  return NS_OK;
+}
+
+nsresult
+TimeoutExecutor::Schedule(const TimeStamp& aDeadline)
+{
+  TimeStamp now(TimeStamp::Now());
+
+  // Schedule an immediate runnable if the desired deadline has passed
+  // or is slightly in the future.  This is similar to how nsITimer will
+  // fire timers early based on the interval resolution.
+  if (aDeadline <= now) {
+    return ScheduleImmediate(aDeadline, now);
+  }
+
+  return ScheduleDelayed(aDeadline, now);
+}
+
+nsresult
+TimeoutExecutor::MaybeReschedule(const TimeStamp& aDeadline)
+{
+  MOZ_DIAGNOSTIC_ASSERT(!mDeadline.IsNull());
+  MOZ_DIAGNOSTIC_ASSERT(mMode == Mode::Immediate ||
+                        mMode == Mode::Delayed);
+
+  if (aDeadline >= mDeadline) {
+    return NS_OK;
+  }
+
+  if (mMode == Mode::Immediate) {
+    // Don't reduce the deadline here as we want to execute the
+    // timer we originally scheduled even if its a few microseconds
+    // in the future.
+    return NS_OK;
+  }
+
+  Cancel();
+  return Schedule(aDeadline);
+}
+
+void
+TimeoutExecutor::MaybeExecute()
+{
+  MOZ_DIAGNOSTIC_ASSERT(mMode != Mode::Shutdown && mMode != Mode::None);
+  MOZ_DIAGNOSTIC_ASSERT(mOwner);
+  MOZ_DIAGNOSTIC_ASSERT(!mDeadline.IsNull());
+
+  TimeStamp deadline(mDeadline);
+
+  // Sometimes nsITimer or canceled timers will fire too early.  If this
+  // happens then just cap our deadline to our maximum time in the future
+  // and proceed.  If there are no timers ready we will get rescheduled
+  // by TimeoutManager.
+  TimeStamp now(TimeStamp::Now());
+  if (deadline > now) {
+    deadline = now;
+  }
+
+  Cancel();
+
+  mOwner->RunTimeout(deadline);
+}
+
+TimeoutExecutor::TimeoutExecutor(TimeoutManager* aOwner)
+  : mOwner(aOwner)
+  , mMode(Mode::None)
+{
+  MOZ_DIAGNOSTIC_ASSERT(mOwner);
+}
+
+void
+TimeoutExecutor::Shutdown()
+{
+  mOwner = nullptr;
+
+  if (mTimer) {
+    mTimer->Cancel();
+    mTimer = nullptr;
+  }
+
+  mMode = Mode::Shutdown;
+  mDeadline = TimeStamp();
+}
+
+nsresult
+TimeoutExecutor::MaybeSchedule(const TimeStamp& aDeadline)
+{
+  MOZ_DIAGNOSTIC_ASSERT(!aDeadline.IsNull());
+
+  if (mMode == Mode::Shutdown) {
+    return NS_OK;
+  }
+
+  if (mMode == Mode::Immediate || mMode == Mode::Delayed) {
+    return MaybeReschedule(aDeadline);
+  }
+
+  return Schedule(aDeadline);
+}
+
+void
+TimeoutExecutor::Cancel()
+{
+  if (mTimer) {
+    mTimer->Cancel();
+  }
+  mMode = Mode::None;
+  mDeadline = TimeStamp();
+}
+
+NS_IMETHODIMP
+TimeoutExecutor::Run()
+{
+  // If the executor is canceled and then rescheduled its possible to get
+  // spurious executions here.  Ignore these unless our current mode matches.
+  if (mMode == Mode::Immediate) {
+    MaybeExecute();
+  }
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+TimeoutExecutor::Notify(nsITimer* aTimer)
+{
+  // If the executor is canceled and then rescheduled its possible to get
+  // spurious executions here.  Ignore these unless our current mode matches.
+  if (mMode == Mode::Delayed) {
+    MaybeExecute();
+  }
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+TimeoutExecutor::GetName(nsACString& aNameOut)
+{
+  aNameOut.AssignLiteral("TimeoutExecutor Runnable");
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+TimeoutExecutor::SetName(const char* aName)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/base/TimeoutExecutor.h
@@ -0,0 +1,85 @@
+/* -*- 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_timeoutexecutor_h
+#define mozilla_dom_timeoutexecutor_h
+
+#include "nsIRunnable.h"
+#include "nsITimer.h"
+#include "nsINamed.h"
+
+namespace mozilla {
+namespace dom {
+
+class TimeoutExecutor final : public nsIRunnable
+                            , public nsITimerCallback
+                            , public nsINamed
+{
+  TimeoutManager* mOwner;
+  nsCOMPtr<nsITimer> mTimer;
+  TimeStamp mDeadline;
+
+  // The TimeoutExecutor is repeatedly scheduled by the TimeoutManager
+  // to fire for the next soonest Timeout.  Since the executor is re-used
+  // it needs to handle switching between a few states.
+  enum class Mode
+  {
+    // None indicates the executor is idle.  It may be scheduled or shutdown.
+    None,
+    // Immediate means the executor is scheduled to run a Timeout with a
+    // deadline that has already expired.
+    Immediate,
+    // Delayed means the executor is scheduled to run a Timeout with a
+    // deadline in the future.
+    Delayed,
+    // Shutdown means the TimeoutManager has been destroyed.  Once this
+    // state is reached the executor cannot be scheduled again.  If the
+    // executor is already dispatched as a runnable or held by a timer then
+    // we may still get a Run()/Notify() call which will be ignored.
+    Shutdown
+  };
+
+  Mode mMode;
+
+  ~TimeoutExecutor();
+
+  nsresult
+  ScheduleImmediate(const TimeStamp& aDeadline, const TimeStamp& aNow);
+
+  nsresult
+  ScheduleDelayed(const TimeStamp& aDeadline, const TimeStamp& aNow);
+
+  nsresult
+  Schedule(const TimeStamp& aDeadline);
+
+  nsresult
+  MaybeReschedule(const TimeStamp& aDeadline);
+
+  void
+  MaybeExecute();
+
+public:
+  explicit TimeoutExecutor(TimeoutManager* aOwner);
+
+  void
+  Shutdown();
+
+  nsresult
+  MaybeSchedule(const TimeStamp& aDeadline);
+
+  void
+  Cancel();
+
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSIRUNNABLE
+  NS_DECL_NSITIMERCALLBACK
+  NS_DECL_NSINAMED
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_timeoutexecutor_h
--- a/dom/base/TimeoutManager.cpp
+++ b/dom/base/TimeoutManager.cpp
@@ -1522,8 +1522,14 @@ TimeoutManager::BeginSyncOperation()
 void
 TimeoutManager::EndSyncOperation()
 {
   // If we're running a timeout, restart the measurement from here.
   if (!mWindow.IsChromeWindow() && mRunningTimeout) {
     TimeoutTelemetry::Get().StartRecording(TimeStamp::Now());
   }
 }
+
+nsIEventTarget*
+TimeoutManager::EventTarget()
+{
+  return mWindow.EventTargetFor(TaskCategory::Timer);
+}
--- a/dom/base/TimeoutManager.h
+++ b/dom/base/TimeoutManager.h
@@ -113,16 +113,19 @@ public:
     if (!mNormalTimeouts.ForEachAbortable(c)) {
       mTrackingTimeouts.ForEachAbortable(c);
     }
   }
 
   void BeginSyncOperation();
   void EndSyncOperation();
 
+  nsIEventTarget*
+  EventTarget();
+
   static const uint32_t InvalidFiringId;
 
 private:
   nsresult ResetTimersForThrottleReduction(int32_t aPreviousThrottleDelayMS);
   void MaybeStartThrottleTrackingTimout();
 
   bool IsBackground() const;
 
--- a/dom/base/moz.build
+++ b/dom/base/moz.build
@@ -341,16 +341,17 @@ UNIFIED_SOURCES += [
     'StructuredCloneHolder.cpp',
     'StyleSheetList.cpp',
     'SubtleCrypto.cpp',
     'TabGroup.cpp',
     'Text.cpp',
     'TextInputProcessor.cpp',
     'ThirdPartyUtil.cpp',
     'Timeout.cpp',
+    'TimeoutExecutor.cpp',
     'TimeoutHandler.cpp',
     'TimeoutManager.cpp',
     'TreeWalker.cpp',
     'WebKitCSSMatrix.cpp',
     'WebSocket.cpp',
     'WindowNamedPropertiesHandler.cpp',
     'WindowOrientationObserver.cpp',
 ]