1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 
6 #ifndef SystemTimeConverter_h
7 #define SystemTimeConverter_h
8 
9 #include <limits>
10 #include "mozilla/TimeStamp.h"
11 #include "mozilla/TypeTraits.h"
12 
13 namespace mozilla {
14 
15 // Utility class that converts time values represented as an unsigned integral
16 // number of milliseconds from one time source (e.g. a native event time) to
17 // corresponding mozilla::TimeStamp objects.
18 //
19 // This class handles wrapping of integer values and skew between the time
20 // source and mozilla::TimeStamp values.
21 //
22 // It does this by using an historical reference time recorded in both time
23 // scales (i.e. both as a numerical time value and as a TimeStamp).
24 //
25 // For performance reasons, this class is careful to minimize calls to the
26 // native "current time" function (e.g. gdk_x11_server_get_time) since this can
27 // be slow.
28 template <typename Time>
29 class SystemTimeConverter {
30 public:
31   SystemTimeConverter()
32     : mReferenceTime(Time(0))
33     , mReferenceTimeStamp() // Initializes to the null timestamp
34     , mLastBackwardsSkewCheck(Time(0))
35     , kTimeRange(std::numeric_limits<Time>::max())
36     , kTimeHalfRange(kTimeRange / 2)
37     , kBackwardsSkewCheckInterval(Time(2000))
38   {
39     static_assert(!IsSigned<Time>::value, "Expected Time to be unsigned");
40   }
41 
42   template <typename CurrentTimeGetter>
43   mozilla::TimeStamp
44   GetTimeStampFromSystemTime(Time aTime,
45                              CurrentTimeGetter& aCurrentTimeGetter) {
46     // If the reference time is not set, use the current time value to fill
47     // it in.
48     if (mReferenceTimeStamp.IsNull()) {
49       UpdateReferenceTime(aTime, aCurrentTimeGetter);
50     }
51     TimeStamp roughlyNow = TimeStamp::Now();
52 
53     // Check for skew between the source of Time values and TimeStamp values.
54     // We do this by comparing two durations (both in ms):
55     //
56     // i.  The duration from the reference time to the passed-in time.
57     //     (timeDelta in the diagram below)
58     // ii. The duration from the reference timestamp to the current time
59     //     based on TimeStamp::Now.
60     //     (timeStampDelta in the diagram below)
61     //
62     // Normally, we'd expect (ii) to be slightly larger than (i) to account
63     // for the time taken between generating the event and processing it.
64     //
65     // If (ii) - (i) is negative then the source of Time values is getting
66     // "ahead" of TimeStamp. We call this "forwards" skew below.
67     //
68     // For the reverse case, if (ii) - (i) is positive (and greater than some
69     // tolerance factor), then we may have "backwards" skew. This is often
70     // the case when we have a backlog of events and by the time we process
71     // them, the time given by the system is comparatively "old".
72     //
73     // We call the absolute difference between (i) and (ii), "deltaFromNow".
74     //
75     // Graphically:
76     //
77     //                    mReferenceTime              aTime
78     // Time scale:      ........+.......................*........
79     //                          |--------timeDelta------|
80     //
81     //                  mReferenceTimeStamp             roughlyNow
82     // TimeStamp scale: ........+...........................*....
83     //                          |------timeStampDelta-------|
84     //
85     //                                                  |---|
86     //                                               deltaFromNow
87     //
88     Time deltaFromNow;
89     bool newer = IsTimeNewerThanTimestamp(aTime, roughlyNow, &deltaFromNow);
90 
91     // Tolerance when detecting clock skew.
92     static const Time kTolerance = 30;
93 
94     // Check for forwards skew
95     if (newer) {
96       // Make aTime correspond to roughlyNow
97       UpdateReferenceTime(aTime, roughlyNow);
98 
99       // We didn't have backwards skew so don't bother checking for
100       // backwards skew again for a little while.
101       mLastBackwardsSkewCheck = aTime;
102 
103       return roughlyNow;
104     }
105 
106     if (deltaFromNow <= kTolerance) {
107       // If the time between event times and TimeStamp values is within
108       // the tolerance then assume we don't have clock skew so we can
109       // avoid checking for backwards skew for a while.
110       mLastBackwardsSkewCheck = aTime;
111     } else if (aTime - mLastBackwardsSkewCheck > kBackwardsSkewCheckInterval) {
112       aCurrentTimeGetter.GetTimeAsyncForPossibleBackwardsSkew(roughlyNow);
113       mLastBackwardsSkewCheck = aTime;
114     }
115 
116     // Finally, calculate the timestamp
117     return roughlyNow - TimeDuration::FromMilliseconds(deltaFromNow);
118   }
119 
120   void
121   CompensateForBackwardsSkew(Time aReferenceTime,
122                              const TimeStamp &aLowerBound) {
123     // Check if we actually have backwards skew. Backwards skew looks like
124     // the following:
125     //
126     //        mReferenceTime
127     // Time:      ..+...a...b...c..........................
128     //
129     //     mReferenceTimeStamp
130     // TimeStamp: ..+.....a.....b.....c....................
131     //
132     // Converted
133     // time:      ......a'..b'..c'.........................
134     //
135     // What we need to do is bring mReferenceTime "forwards".
136     //
137     // Suppose when we get (c), we detect possible backwards skew and trigger
138     // an async request for the current time (which is passed in here as
139     // aReferenceTime).
140     //
141     // We end up with something like the following:
142     //
143     //        mReferenceTime     aReferenceTime
144     // Time:      ..+...a...b...c...v......................
145     //
146     //     mReferenceTimeStamp
147     // TimeStamp: ..+.....a.....b.....c..........x.........
148     //                                ^          ^
149     //                          aLowerBound  TimeStamp::Now()
150     //
151     // If the duration (aLowerBound - mReferenceTimeStamp) is greater than
152     // (aReferenceTime - mReferenceTime) then we know we have backwards skew.
153     //
154     // If that's not the case, then we probably just got caught behind
155     // temporarily.
156     Time delta;
157     if (IsTimeNewerThanTimestamp(aReferenceTime, aLowerBound, &delta)) {
158       return;
159     }
160 
161     // We have backwards skew; the equivalent TimeStamp for aReferenceTime lies
162     // somewhere between aLowerBound (which was the TimeStamp when we triggered
163     // the async request for the current time) and TimeStamp::Now().
164     //
165     // If aReferenceTime was waiting in the event queue for a long time, the
166     // equivalent TimeStamp might be much closer to aLowerBound than
167     // TimeStamp::Now() so for now we just set it to aLowerBound. That's
168     // guaranteed to be at least somewhat of an improvement.
169     UpdateReferenceTime(aReferenceTime, aLowerBound);
170   }
171 
172 private:
173   template <typename CurrentTimeGetter>
174   void
175   UpdateReferenceTime(Time aReferenceTime,
176                       const CurrentTimeGetter& aCurrentTimeGetter) {
177     Time currentTime = aCurrentTimeGetter.GetCurrentTime();
178     TimeStamp currentTimeStamp = TimeStamp::Now();
179     Time timeSinceReference = currentTime - aReferenceTime;
180     TimeStamp referenceTimeStamp =
181       currentTimeStamp - TimeDuration::FromMilliseconds(timeSinceReference);
182     UpdateReferenceTime(aReferenceTime, referenceTimeStamp);
183   }
184 
185   void
186   UpdateReferenceTime(Time aReferenceTime,
187                       const TimeStamp& aReferenceTimeStamp) {
188     mReferenceTime = aReferenceTime;
189     mReferenceTimeStamp = aReferenceTimeStamp;
190   }
191 
192   bool
193   IsTimeNewerThanTimestamp(Time aTime, TimeStamp aTimeStamp, Time* aDelta)
194   {
195     Time timeDelta = aTime - mReferenceTime;
196 
197     // Cast the result to signed 64-bit integer first since that should be
198     // enough to hold the range of values returned by ToMilliseconds() and
199     // the result of converting from double to an integer-type when the value
200     // is outside the integer range is undefined.
201     // Then we do an implicit cast to Time (typically an unsigned 32-bit
202     // integer) which wraps times outside that range.
203     Time timeStampDelta =
204       static_cast<int64_t>((aTimeStamp - mReferenceTimeStamp).ToMilliseconds());
205 
206     Time timeToTimeStamp = timeStampDelta - timeDelta;
207     bool isNewer = false;
208     if (timeToTimeStamp == 0) {
209       *aDelta = 0;
210     } else if (timeToTimeStamp < kTimeHalfRange) {
211       *aDelta = timeToTimeStamp;
212     } else {
213       isNewer = true;
214       *aDelta = timeDelta - timeStampDelta;
215     }
216 
217     return isNewer;
218   }
219 
220   Time mReferenceTime;
221   TimeStamp mReferenceTimeStamp;
222   Time mLastBackwardsSkewCheck;
223 
224   const Time kTimeRange;
225   const Time kTimeHalfRange;
226   const Time kBackwardsSkewCheckInterval;
227 };
228 
229 } // namespace mozilla
230 
231 #endif /* SystemTimeConverter_h */
232