summaryrefslogtreecommitdiffstats
path: root/widget/SystemTimeConverter.h
diff options
context:
space:
mode:
Diffstat (limited to 'widget/SystemTimeConverter.h')
-rw-r--r--widget/SystemTimeConverter.h235
1 files changed, 235 insertions, 0 deletions
diff --git a/widget/SystemTimeConverter.h b/widget/SystemTimeConverter.h
new file mode 100644
index 0000000000..4a074d573f
--- /dev/null
+++ b/widget/SystemTimeConverter.h
@@ -0,0 +1,235 @@
+/* -*- Mode: C++; 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/. */
+
+#ifndef SystemTimeConverter_h
+#define SystemTimeConverter_h
+
+#include <limits>
+#include <type_traits>
+#include "mozilla/TimeStamp.h"
+
+namespace mozilla {
+
+// Utility class that converts time values represented as an unsigned integral
+// number of milliseconds from one time source (e.g. a native event time) to
+// corresponding mozilla::TimeStamp objects.
+//
+// This class handles wrapping of integer values and skew between the time
+// source and mozilla::TimeStamp values.
+//
+// It does this by using an historical reference time recorded in both time
+// scales (i.e. both as a numerical time value and as a TimeStamp).
+//
+// For performance reasons, this class is careful to minimize calls to the
+// native "current time" function (e.g. gdk_x11_server_get_time) since this can
+// be slow.
+template <typename Time, typename TimeStampNowProvider = TimeStamp>
+class SystemTimeConverter {
+ public:
+ SystemTimeConverter()
+ : mReferenceTime(Time(0)),
+ mReferenceTimeStamp() // Initializes to the null timestamp
+ ,
+ mLastBackwardsSkewCheck(Time(0)),
+ kTimeRange(std::numeric_limits<Time>::max()),
+ kTimeHalfRange(kTimeRange / 2),
+ kBackwardsSkewCheckInterval(Time(2000)) {
+ static_assert(!std::is_signed_v<Time>, "Expected Time to be unsigned");
+ }
+
+ template <typename CurrentTimeGetter>
+ mozilla::TimeStamp GetTimeStampFromSystemTime(
+ Time aTime, CurrentTimeGetter& aCurrentTimeGetter) {
+ TimeStamp roughlyNow = TimeStampNowProvider::Now();
+
+ // If the reference time is not set, use the current time value to fill
+ // it in.
+ if (mReferenceTimeStamp.IsNull()) {
+ // This sometimes happens when ::GetMessageTime returns 0 for the first
+ // message on Windows.
+ if (!aTime) return roughlyNow;
+ UpdateReferenceTime(aTime, aCurrentTimeGetter);
+ }
+
+ // Check for skew between the source of Time values and TimeStamp values.
+ // We do this by comparing two durations (both in ms):
+ //
+ // i. The duration from the reference time to the passed-in time.
+ // (timeDelta in the diagram below)
+ // ii. The duration from the reference timestamp to the current time
+ // based on TimeStamp::Now.
+ // (timeStampDelta in the diagram below)
+ //
+ // Normally, we'd expect (ii) to be slightly larger than (i) to account
+ // for the time taken between generating the event and processing it.
+ //
+ // If (ii) - (i) is negative then the source of Time values is getting
+ // "ahead" of TimeStamp. We call this "forwards" skew below.
+ //
+ // For the reverse case, if (ii) - (i) is positive (and greater than some
+ // tolerance factor), then we may have "backwards" skew. This is often
+ // the case when we have a backlog of events and by the time we process
+ // them, the time given by the system is comparatively "old".
+ //
+ // The IsNewerThanTimestamp function computes the equivalent of |aTime| in
+ // the TimeStamp scale and returns that in |timeAsTimeStamp|.
+ //
+ // Graphically:
+ //
+ // mReferenceTime aTime
+ // Time scale: ........+.......................*........
+ // |--------timeDelta------|
+ //
+ // mReferenceTimeStamp roughlyNow
+ // TimeStamp scale: ........+...........................*....
+ // |------timeStampDelta-------|
+ //
+ // |---|
+ // roughlyNow-timeAsTimeStamp
+ //
+ TimeStamp timeAsTimeStamp;
+ bool newer = IsTimeNewerThanTimestamp(aTime, roughlyNow, &timeAsTimeStamp);
+
+ // Tolerance when detecting clock skew.
+ static const TimeDuration kTolerance = TimeDuration::FromMilliseconds(30.0);
+
+ // Check for forwards skew
+ if (newer) {
+ // Make aTime correspond to roughlyNow
+ UpdateReferenceTime(aTime, roughlyNow);
+
+ // We didn't have backwards skew so don't bother checking for
+ // backwards skew again for a little while.
+ mLastBackwardsSkewCheck = aTime;
+
+ return roughlyNow;
+ }
+
+ if (roughlyNow - timeAsTimeStamp <= kTolerance) {
+ // If the time between event times and TimeStamp values is within
+ // the tolerance then assume we don't have clock skew so we can
+ // avoid checking for backwards skew for a while.
+ mLastBackwardsSkewCheck = aTime;
+ } else if (aTime - mLastBackwardsSkewCheck > kBackwardsSkewCheckInterval) {
+ aCurrentTimeGetter.GetTimeAsyncForPossibleBackwardsSkew(roughlyNow);
+ mLastBackwardsSkewCheck = aTime;
+ }
+
+ // Finally, calculate the timestamp
+ return timeAsTimeStamp;
+ }
+
+ void CompensateForBackwardsSkew(Time aReferenceTime,
+ const TimeStamp& aLowerBound) {
+ // Check if we actually have backwards skew. Backwards skew looks like
+ // the following:
+ //
+ // mReferenceTime
+ // Time: ..+...a...b...c..........................
+ //
+ // mReferenceTimeStamp
+ // TimeStamp: ..+.....a.....b.....c....................
+ //
+ // Converted
+ // time: ......a'..b'..c'.........................
+ //
+ // What we need to do is bring mReferenceTime "forwards".
+ //
+ // Suppose when we get (c), we detect possible backwards skew and trigger
+ // an async request for the current time (which is passed in here as
+ // aReferenceTime).
+ //
+ // We end up with something like the following:
+ //
+ // mReferenceTime aReferenceTime
+ // Time: ..+...a...b...c...v......................
+ //
+ // mReferenceTimeStamp
+ // TimeStamp: ..+.....a.....b.....c..........x.........
+ // ^ ^
+ // aLowerBound TimeStamp::Now()
+ //
+ // If the duration (aLowerBound - mReferenceTimeStamp) is greater than
+ // (aReferenceTime - mReferenceTime) then we know we have backwards skew.
+ //
+ // If that's not the case, then we probably just got caught behind
+ // temporarily.
+ if (IsTimeNewerThanTimestamp(aReferenceTime, aLowerBound, nullptr)) {
+ return;
+ }
+
+ // We have backwards skew; the equivalent TimeStamp for aReferenceTime lies
+ // somewhere between aLowerBound (which was the TimeStamp when we triggered
+ // the async request for the current time) and TimeStamp::Now().
+ //
+ // If aReferenceTime was waiting in the event queue for a long time, the
+ // equivalent TimeStamp might be much closer to aLowerBound than
+ // TimeStamp::Now() so for now we just set it to aLowerBound. That's
+ // guaranteed to be at least somewhat of an improvement.
+ UpdateReferenceTime(aReferenceTime, aLowerBound);
+ }
+
+ private:
+ template <typename CurrentTimeGetter>
+ void UpdateReferenceTime(Time aReferenceTime,
+ const CurrentTimeGetter& aCurrentTimeGetter) {
+ Time currentTime = aCurrentTimeGetter.GetCurrentTime();
+ TimeStamp currentTimeStamp = TimeStampNowProvider::Now();
+ Time timeSinceReference = currentTime - aReferenceTime;
+ TimeStamp referenceTimeStamp =
+ currentTimeStamp - TimeDuration::FromMilliseconds(timeSinceReference);
+ UpdateReferenceTime(aReferenceTime, referenceTimeStamp);
+ }
+
+ void UpdateReferenceTime(Time aReferenceTime,
+ const TimeStamp& aReferenceTimeStamp) {
+ mReferenceTime = aReferenceTime;
+ mReferenceTimeStamp = aReferenceTimeStamp;
+ }
+
+ bool IsTimeNewerThanTimestamp(Time aTime, TimeStamp aTimeStamp,
+ TimeStamp* aTimeAsTimeStamp) {
+ Time timeDelta = aTime - mReferenceTime;
+
+ // Cast the result to signed 64-bit integer first since that should be
+ // enough to hold the range of values returned by ToMilliseconds() and
+ // the result of converting from double to an integer-type when the value
+ // is outside the integer range is undefined.
+ // Then we do an implicit cast to Time (typically an unsigned 32-bit
+ // integer) which wraps times outside that range.
+ TimeDuration timeStampDelta = (aTimeStamp - mReferenceTimeStamp);
+ int64_t wholeMillis = static_cast<int64_t>(timeStampDelta.ToMilliseconds());
+ Time wrappedTimeStampDelta = wholeMillis; // truncate to unsigned
+
+ Time timeToTimeStamp = wrappedTimeStampDelta - timeDelta;
+ bool isNewer = false;
+ if (timeToTimeStamp == 0) {
+ // wholeMillis needs no adjustment
+ } else if (timeToTimeStamp < kTimeHalfRange) {
+ wholeMillis -= timeToTimeStamp;
+ } else {
+ isNewer = true;
+ wholeMillis += (-timeToTimeStamp);
+ }
+ if (aTimeAsTimeStamp) {
+ *aTimeAsTimeStamp =
+ mReferenceTimeStamp + TimeDuration::FromMilliseconds(wholeMillis);
+ }
+
+ return isNewer;
+ }
+
+ Time mReferenceTime;
+ TimeStamp mReferenceTimeStamp;
+ Time mLastBackwardsSkewCheck;
+
+ const Time kTimeRange;
+ const Time kTimeHalfRange;
+ const Time kBackwardsSkewCheckInterval;
+};
+
+} // namespace mozilla
+
+#endif /* SystemTimeConverter_h */