1 /// It's about time
2 module mecca.lib.time;
3 
4 // Licensed under the Boost license. Full copyright information in the AUTHORS file
5 
6 import core.sys.posix.sys.time : timespec;
7 
8 public import std.datetime;
9 import mecca.lib.division: S64Divisor;
10 public import mecca.platform.x86: readTSC;
11 import mecca.log;
12 
13 /**
14  * A time point maintained through the TSC timer
15  *
16  * TSC is a time counter maintained directly by the CPU. It counts how many "cycles" (loosly corresponding to actual CPU cycles)
17  * since an arbitrary start point. It is read by a single assembly instruciton, and is more efficient to read than kernel
18  * operatons.
19  */
20 struct TscTimePoint {
21     private enum HECTONANO = 10_000_000;
22     /// Minimal, maximal and zero constants for reference.
23     enum min = TscTimePoint(long.min);
24     enum zero = TscTimePoint(0); /// ditto
25     enum max = TscTimePoint(long.max); /// ditto
26 
27     /// Provide the cycles/time ratio for the current machine.
28     static shared immutable long cyclesPerSecond;
29     static shared immutable long cyclesPerMsec;         /// ditto
30     static shared immutable long cyclesPerUsec;         /// ditto
31     alias frequency = cyclesPerSecond;                  /// ditto
32 
33     /** Prepared dividor for the cycles/time value
34      *
35      * An S64Divisor for the cycles/time value, for quickly dividing by it at runtime.
36      */
37     static shared immutable S64Divisor cyclesPerSecondDivisor;
38     static shared immutable S64Divisor cyclesPerMsecDivisor;    /// ditto
39     static shared immutable S64Divisor cyclesPerUsecDivisor;    /// ditto
40 
41 private:
42     /* thread local */ static ubyte refetchInterval; // How many soft calls to do before doing hard fetch of timer
43     /* thread local */ static ubyte fetchCounter;
44     /* thread local */ static TscTimePoint lastTsc;
45 
46 public:
47     /// Time represented by TscTimePoint in units of cycles.
48     long cycles;
49 
50     /**
51      * Get a TscTimePoint representing now.
52      *
53      * There are two variants of this method. "now" and "hardNow". By default, they do exactly the same thing:
54      * report the time right now, as reported by the cycles counter in the CPU.
55      *
56      * As fetching the cycles counter may be relatively expensive, threads that do a lot of time keeping may find that getting
57      * the hardware counter each and every time is too costly. In that case, you can call "setHardNowThreshold" with a threshold.
58      * Calling, e.g. setHardNow(3) will mean that now will call hardNow every third invocation.
59      *
60      * Even when now doesn't call hardNow, it is still guaranteed to montoniously advance, but it not guaranteed to be accurate.
61      *
62      * A good rule of thumb is this: If you use requires getting the time on a semi-regular basis, call now whenever precise
63      * time is not required. If your use requires time only sporadically, only use hardNow.
64      *
65      * Warning:
66      * now does not guarantee monotonity across different threads. If you need different threads to have comparable times, use
67      * hardNow.
68      */
69     static TscTimePoint now() nothrow @nogc @safe {
70         if (fetchCounter < refetchInterval || refetchInterval==ubyte.max) {
71             fetchCounter++;
72             lastTsc.cycles++;
73             return lastTsc;
74         }
75         else {
76             return hardNow();
77         }
78     }
79 
80     /// ditto
81     static TscTimePoint hardNow() nothrow @nogc @safe {
82         pragma(inline, true);
83         lastTsc.cycles = readTSC();
84         fetchCounter = 0;
85         return lastTsc;
86     }
87 
88     /**
89      * Set the frequency with which we take a hard TSC reading
90      *
91      * See complete documentation in the now method.
92      *
93      * Params:
94      * interval = take a hardNow every so many calls to now. A value of 1 mean that now and hardNow are identical. A value of 0
95      *   means that hardNow is $(B never) implicitly taken.
96      */
97     static void setHardNowThreshold(ubyte interval) nothrow @nogc @safe {
98         refetchInterval = cast(ubyte)(interval-1);
99     }
100 
101     shared static this() {
102         import mecca.platform.os: calculateCycles;
103 
104         const cycles = calculateCycles();
105         cyclesPerSecond = cycles.perSecond;
106         cyclesPerMsec = cycles.perMsec;
107         cyclesPerUsec = cycles.perUsec;
108 
109         cyclesPerSecondDivisor = S64Divisor(cyclesPerSecond);
110         cyclesPerMsecDivisor = S64Divisor(cyclesPerMsec);
111         cyclesPerUsecDivisor = S64Divisor(cyclesPerUsec);
112 
113         hardNow();
114     }
115 
116     /// Calculate a TscTimePoint for a set duration from now
117     static auto fromNow(Duration dur) @nogc {
118         return hardNow + toCycles(dur);
119     }
120 
121     /// Calculate a TscTimePoint for a time given in systime from now
122     ///
123     /// Not @nogc and nothrow, because Clock.currTime does GC and may throw
124     @("notrace") static auto fromSysTime(SysTime time) @safe {
125         Duration diff = time - Clock.currTime();
126         return fromNow(diff);
127     }
128 
129     /// Various conversion functions
130     static long toCycles(Duration dur) @nogc @safe nothrow {
131         long hns = dur.total!"hnsecs";
132         return (hns / HECTONANO) * cyclesPerSecond + ((hns % HECTONANO) * cyclesPerSecond) / HECTONANO;
133     }
134     /// ditto
135     static long toCycles(string unit)(long n) @nogc @safe nothrow {
136         static if (unit == "usecs") {
137             return n * cyclesPerUsec;
138         } else static if (unit == "msecs") {
139             return n * cyclesPerMsec;
140         } else static if (unit == "seconds") {
141             return n * cyclesPerSecond;
142         }
143     }
144     /// ditto
145     static Duration toDuration(long cycles) pure @safe @nogc nothrow {
146         return hnsecs((cycles / cyclesPerSecond) * HECTONANO + ((cycles % cyclesPerSecond) * HECTONANO) / cyclesPerSecond);
147     }
148     /// ditto
149     Duration toDuration() const @safe @nogc nothrow {
150         return hnsecs((cycles / cyclesPerSecond) * HECTONANO + ((cycles % cyclesPerSecond) * HECTONANO) / cyclesPerSecond);
151     }
152     /// ditto
153     static long toUsecs(long cycles) pure @nogc @safe nothrow {
154         return cycles / cyclesPerUsecDivisor;
155     }
156     /// ditto
157     long toUsecs() const pure @nogc @safe nothrow {
158         return cycles / cyclesPerUsecDivisor;
159     }
160     /// ditto
161     static long toMsecs(long cycles) pure @nogc @safe nothrow {
162         return cycles / cyclesPerMsecDivisor;
163     }
164     /// ditto
165     long toMsecs() pure const @nogc @safe nothrow {
166         return cycles / cyclesPerMsecDivisor;
167     }
168 
169     int opCmp(TscTimePoint rhs) pure const @nogc @safe nothrow {
170         return (cycles > rhs.cycles) ? 1 : ((cycles < rhs.cycles) ? -1 : 0);
171     }
172     bool opEquals()(TscTimePoint rhs) pure const @nogc @safe nothrow {
173         return cycles == rhs.cycles;
174     }
175 
176     TscTimePoint opBinary(string op: "+")(long cycles) const @nogc @safe nothrow pure {
177         return TscTimePoint(this.cycles + cycles);
178     }
179     TscTimePoint opBinary(string op: "+")(Duration dur) const @nogc @safe nothrow {
180         return TscTimePoint(cycles + toCycles(dur));
181     }
182 
183     TscTimePoint opBinary(string op: "-")(long cycles) const @nogc @safe nothrow pure {
184         return TscTimePoint(this.cycles - cycles);
185     }
186     Duration opBinary(string op: "-")(TscTimePoint rhs) const @nogc @safe nothrow pure {
187         return TscTimePoint.toDuration(cycles - rhs.cycles);
188     }
189     TscTimePoint opBinary(string op: "-")(Duration dur) const @nogc @safe nothrow {
190         return TscTimePoint(cycles - toCycles(dur));
191     }
192 
193     ref auto opOpAssign(string op)(Duration dur) @nogc if (op == "+" || op == "-") {
194         mixin("cycles " ~ op ~ "= toCycles(dur);");
195         return this;
196     }
197     ref auto opOpAssign(string op)(long cycles) @nogc if (op == "+" || op == "-") {
198         mixin("this.cycles " ~ op ~ "= cycles;");
199         return this;
200     }
201 
202     /// Calculate difference between two TscTimePoint in the given units
203     @("notrace") long diff(string units)(TscTimePoint rhs) nothrow @safe @nogc
204             if (units == "usecs" || units == "msecs" || units == "seconds" || units == "cycles")
205     {
206         static if (units == "usecs") {
207             return (cycles - rhs.cycles) / cyclesPerUsecDivisor;
208         }
209         else static if (units == "msecs") {
210             return (cycles - rhs.cycles) / cyclesPerMsecDivisor;
211         }
212         else static if (units == "seconds") {
213             return (cycles - rhs.cycles) / cyclesPerSecondDivisor;
214         }
215         else static if (units == "cycles") {
216             return (cycles - rhs.cycles);
217         }
218         else {
219             static assert (false, units);
220         }
221     }
222 
223     /// Convert to any of the units accepted by toDuration
224     long to(string unit)() const @nogc @safe nothrow {
225         return toDuration.total!unit();
226     }
227 }
228 
229 unittest {
230     auto t0 = TscTimePoint.hardNow;
231     assert (t0.cycles > 0);
232     assert (TscTimePoint.cyclesPerSecond > 1_000_000);
233 }
234 
235 /// A type for specifying absolute timeouts
236 struct Timeout {
237     /// Constant specifying an already elapsed timeout
238     enum Timeout elapsed = Timeout(TscTimePoint.min);
239     /// Constant specifying a timeout that will never elapse
240     enum Timeout infinite = Timeout(TscTimePoint.max);
241 
242     /// The expected expiry time
243     TscTimePoint expiry;
244 
245     /// Construct a timeout from TscTimePoint
246     @safe @nogc pure nothrow
247     this(TscTimePoint expiry) {
248         this.expiry = expiry;
249     }
250 
251     /**
252      * Construct a timeout from Duration
253      *
254      * Params:
255      * dur = Duration until timeout expires
256      * now = If provided, the base time to compute the timeout relative to
257      */
258     this(Duration dur, TscTimePoint now = TscTimePoint.now) nothrow @safe @nogc {
259         if (dur == Duration.max) {
260             this.expiry = TscTimePoint.max;
261         }
262         else {
263             this.expiry = now + dur;
264         }
265     }
266 
267     /**
268      * Report how much time until the timeout expires
269      */
270     @notrace @property Duration remaining(TscTimePoint now = TscTimePoint.now) const @safe @nogc nothrow {
271         if (expiry == TscTimePoint.max) {
272             return Duration.max;
273         }
274         if (expiry.cycles < now.cycles) {
275             return Duration.zero;
276         }
277         return expiry - now;
278     }
279 
280     /**
281      * Checks whether a Timeout has expired.
282      *
283      * Params:
284      * now = time point relative to which to check.
285      */
286     @notrace @property bool expired(TscTimePoint now = TscTimePoint.now) pure const nothrow @safe @nogc {
287         if( this == infinite )
288             return false;
289 
290         return expiry <= now;
291     }
292 
293     @notrace int opCmp(in Timeout rhs) const nothrow @safe @nogc {
294         return expiry.opCmp(rhs.expiry);
295     }
296 
297     @notrace bool opEquals()(in Timeout rhs) const nothrow @safe @nogc {
298         return expiry == rhs.expiry;
299     }
300 }
301 
302 package(mecca) timespec toTimespec(Duration duration) nothrow pure @safe @nogc
303 {
304     timespec spec;
305     duration.split!("seconds", "nsecs")(spec.tv_sec, spec.tv_nsec);
306 
307     return spec;
308 }