1 /**
2 Copyright: Copyright (c) 2018, Joakim Brännström. All rights reserved.
3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0)
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 This module contain timer utilities.
7 */
8 module my.timer;
9 
10 import logger = std.experimental.logger;
11 import core.time : Duration, dur;
12 import std.datetime : SysTime, Clock;
13 import std.container.rbtree;
14 import std.typecons : Nullable;
15 
16 @safe:
17 
18 /// A collection of timers.
19 struct Timers {
20     private {
21         RedBlackTree!Timer timers;
22         Nullable!Timer front_;
23     }
24 
25     void put(Timer.Action action, Duration d) @trusted {
26         timers.stableInsert(Timer(Clock.currTime + d, action));
27     }
28 
29     void put(Timer.Action action, SysTime t) @trusted {
30         timers.stableInsert(Timer(t, action));
31     }
32 
33     /// Get how long until the next timer expire. The default is defaultSleep.
34     Duration expireAt(Duration defaultSleep) nothrow {
35         import std.algorithm : max;
36 
37         if (empty) {
38             return defaultSleep;
39         }
40         return max(Duration.zero, timers.front.expire - Clock.currTime);
41     }
42 
43     /// Sleep until the next action triggers.
44     void sleep(Duration defaultSleep) @trusted {
45         import core.thread : Thread;
46 
47         Thread.sleep(expireAt(defaultSleep));
48     }
49 
50     /// Sleep until the next action triggers and execute it, if there are any.
51     void tick(Duration defaultSleep) {
52         sleep(defaultSleep);
53         if (!empty) {
54             front.action(this);
55             popFront;
56         }
57     }
58 
59     Timer front() pure nothrow {
60         assert(!empty, "Can't get front of an empty range");
61         if (front_.isNull && !timers.empty)
62             front_ = timers.front;
63         return front_.get;
64     }
65 
66     void popFront() {
67         assert(!empty, "Can't pop front of an empty range");
68         if (!front_.isNull) {
69             timers.removeKey(front_.get);
70             front_.nullify;
71         } else {
72             timers.removeFront;
73         }
74     }
75 
76     bool empty() pure nothrow @nogc {
77         return timers.empty && front_.isNull;
78     }
79 }
80 
81 auto makeTimers() {
82     return Timers(new RedBlackTree!Timer);
83 }
84 
85 /// An individual timer.
86 struct Timer {
87     private {
88         alias Action = void delegate(ref Timers);
89         SysTime expire;
90         size_t id;
91     }
92 
93     Action action;
94 
95     this(SysTime expire, Action action) {
96         this.expire = expire;
97         this.action = action;
98         this.id = () @trusted { return cast(size_t)&action; }();
99     }
100 
101     size_t toHash() pure nothrow const @nogc scope {
102         return expire.toHash.hashOf(id); // mixing two hash values
103     }
104 
105     bool opEquals()(auto ref const typeof(this) s) const {
106         return expire == s.expire && id == s.id;
107     }
108 
109     int opCmp(ref const typeof(this) rhs) const {
110         // return -1 if "this" is less than rhs, 1 if bigger and zero equal
111         if (expire < rhs.expire)
112             return -1;
113         if (expire > rhs.expire)
114             return 1;
115         if (id < rhs.id)
116             return -1;
117         if (id > rhs.id)
118             return 1;
119         return 0;
120     }
121 }
122 
123 @("shall pop the first timer;")
124 unittest {
125     int timerPopped;
126     auto timers = makeTimers;
127 
128     timers.put((ref Timers) { timerPopped = 42; }, 1000.dur!"msecs");
129     timers.put((ref Timers) { timerPopped = 2; }, 2.dur!"msecs");
130 
131     timers.sleep(1.dur!"msecs");
132     timers.front.action(timers);
133     assert(timerPopped == 2);
134 }
135 
136 /// A negative duration mean it will be removed.
137 alias IntervalAction = Duration delegate();
138 
139 /// Timers that fire each interval. The intervals is adjusted by `action` and
140 /// removed if the interval is < 0.
141 auto makeInterval(ref Timers ts, IntervalAction action, Duration interval) {
142     void repeatFn(ref Timers ts) @safe {
143         const res = action();
144         if (res >= Duration.zero) {
145             ts.put(&repeatFn, res);
146         }
147     }
148 
149     ts.put(&repeatFn, interval);
150 }
151 
152 @("shall remove the interval timer when it return false")
153 unittest {
154     int ticks;
155     auto timers = makeTimers;
156 
157     makeInterval(timers, () {
158         ticks++;
159         if (ticks < 3)
160             return 2.dur!"msecs";
161         return -1.dur!"seconds";
162     }, 2.dur!"msecs");
163     while (!timers.empty) {
164         timers.tick(Duration.zero);
165     }
166 
167     assert(ticks == 3);
168 }