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) {
26         timers.stableInsert(Timer(Clock.currTime + d, action));
27     }
28 
29     void put(Timer.Action action, SysTime t) {
30         timers.stableInsert(Timer(t, action));
31     }
32 
33     /// Get how long until the next timer expire. The minimum is minSleep.
34     Duration expireAt(Duration minSleep) nothrow {
35         import std.algorithm : max;
36 
37         if (empty) {
38             return minSleep;
39         }
40         return max(minSleep, timers.front.expire - Clock.currTime);
41     }
42 
43     /// Sleep until the next action triggers.
44     void sleep(Duration minSleep) @trusted {
45         import core.thread : Thread;
46 
47         Thread.sleep(expireAt(minSleep));
48     }
49 
50     /// Sleep until the next action triggers and execute it, if there are any.
51     void tick(Duration minSleep) {
52         sleep(minSleep);
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     bool opEquals()(auto ref const typeof(this) s) const {
102         return expire == s.expire && id == s.id;
103     }
104 
105     int opCmp(ref const typeof(this) rhs) const {
106         // return -1 if "this" is less than rhs, 1 if bigger and zero equal
107         if (expire < rhs.expire)
108             return -1;
109         if (expire > rhs.expire)
110             return 1;
111         if (id < rhs.id)
112             return -1;
113         if (id > rhs.id)
114             return 1;
115         return 0;
116     }
117 }
118 
119 @("shall pop the first timer;")
120 unittest {
121     int timerPopped;
122     auto timers = makeTimers;
123 
124     timers.put((ref Timers) { timerPopped = 42; }, 1000.dur!"msecs");
125     timers.put((ref Timers) { timerPopped = 2; }, 2.dur!"msecs");
126 
127     timers.sleep(1.dur!"msecs");
128     timers.front.action(timers);
129     assert(timerPopped == 2);
130 }
131 
132 /// A negative duration mean it will be removed.
133 alias IntervalAction = Duration delegate();
134 
135 /// Timers that fire each interval. The intervals is adjusted by `action` and
136 /// removed if the interval is < 0.
137 auto makeInterval(ref Timers ts, IntervalAction action, Duration interval) {
138     void repeatFn(ref Timers ts) @safe {
139         const res = action();
140         if (res >= Duration.zero) {
141             ts.put(&repeatFn, res);
142         }
143     }
144 
145     ts.put(&repeatFn, interval);
146 }
147 
148 @("shall remove the interval timer when it return false")
149 unittest {
150     int ticks;
151     auto timers = makeTimers;
152 
153     makeInterval(timers, () {
154         ticks++;
155         if (ticks < 3)
156             return 2.dur!"msecs";
157         return -1.dur!"seconds";
158     }, 2.dur!"msecs");
159     while (!timers.empty) {
160         timers.tick(Duration.zero);
161     }
162 
163     assert(ticks == 3);
164 }